引言:本文介绍一种用于二值图像或者占空图的聚类算法,并给出c++实现。
介绍
Two-Pass算法是一种基于行程的二值图像连通域标记算法,matlab中连通区域标记函数bwlabel中使用的就是该算法,其算法流程如下:
1,逐行扫描图像,我们把每一行中连续的白色像素组成一个序列称为一个团(run),并记下它的起点start、它的终点end以及它所在的行号。
2,对于除了第一行外的所有行里的团,如果它与前一行中的所有团都没有重合区域,则给它一个新的标号;如果它仅与上一行中一个团有重合区域,则将上一行的那个团的标号赋给它;如果它与上一行的2个以上的团有重叠区域,则给当前团赋一个相连团的最小标号,并将上一行的这几个团的标记写入等价对,说明它们属于一类。
3,将等价对转换为等价序列,每一个序列需要给一相同的标号,因为它们都是等价的。从1开始,给每个等价序列一个标号。
4,遍历开始团的标记,查找等价序列,给予它们新的标记。
5,将每个团的标号填入标记图像中。
6,结束。
实现
下面给出其c++实现,主要分四步进行:
第一步:查找所有团并记录,需要记录团所在的行号、团开始的位置、结束的位置,当然还有一个表征团总数的变量。需要注意的就是团开始位置和结束位置在行首和行末的情况要单独拿出来考虑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void fillRunVectors(const std::vector<std::vector<int>>& bimage, int& NumberOfRuns, std::vector<int>& stRun, std::vector<int>& enRun, std::vector<int>& rowRun, const int& rows, const int& cols, std::vector<int>& label_map) { for (int i = 0; i < rows; ++i) { auto& rowData = bimage[i]; if (rowData[0] == 1) { NumberOfRuns++; stRun.emplace_back(0); rowRun.emplace_back(i); label_map[i * cols] = NumberOfRuns; } for (int j = 1; j < cols; ++j) { int cur_idx = i * cols + j; if (rowData[j-1] == 0 && rowData[j] == 1) { NumberOfRuns++; stRun.emplace_back(j); rowRun.emplace_back(i); label_map[cur_idx] = NumberOfRuns; } else if (rowData[j-1] == 1 && rowData[j] == 0) { enRun.emplace_back(j-1); } else if (rowData[j] == 1) { label_map[cur_idx] = NumberOfRuns; } } if (rowData[cols - 1] == 1) { enRun.emplace_back(cols - 1); } } }
|
第二步:遍历所有的团,完成团的标记与等价对列表的生成。这里判断团是否相邻的关键条件是:一个团的开始位置小于另一个团的结束位置,且结束位置大于另一个团的开始位置。 这里的equivalences 用于存储等价对,offset:0对应四连通,1对应八连通。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| void markArea(vector<int>& stRun, vector<int>& enRun, vector<int>& rowRun, int NumberOfRuns, vector<int>& runLabels, vector<pair<int, int>>& equivalences, int offset) { int idxLabel = 1; int curRowIdx = 0; int firstRunOnCur = 0; int firstRunOnPre = 0; int lastRunOnPre = -1; runLabels.assign(NumberOfRuns, 0); for(int i = 0; i < NumberOfRuns; ++i) { if (rowRun[i] != curRowIdx) { curRowIdx = rowRun[i]; firstRunOnPre = firstRunOnCur; lastRunOnPre = i - 1; firstRunOnCur = i; } if(curRowIdx != rowRun[lastRunOnPre] + 1) { runLabels[i] = idxLabel++; continue; } for (int j = firstRunOnPre; j <= lastRunOnPre; ++j) { if (stRun[i] <= enRun[j] + offset && enRun[i] >= stRun[j] - offset) { if (runLabels[i] == 0) { runLabels[i] = runLabels[j]; } else if (runLabels[i] != runLabels[j]) { equalLabels.emplace_back(std::make_pair(runLabels[i], runLabels[j])); } } } if (runLabels[i] == 0) { runLabels[i] = idxLabel++; } } }
|
第三步:将等价对处理成等价序列,比如有如下等价对:(1,2),(1,6),(3,7),(9-3),(8,1),(8,10),(11,5),(11,8),(11,12),(11,13),(11,14),(15,11),得到的最终序列是:
list1:1-2-5-6-8-10-11-12-13-14-15
list2:3-7-9
list3:4
这里还是采用DFS思想,c++实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| void replaceSameLabel(std::vector<int> runLabels, std::vector<pair<int, int>>& equivalence) { int maxLabel = *std::max_element(runLabels.begin(), runLabels.end()); std::vector<std::vector<bool>> eqTab(maxLabel, std::vector<bool>(maxLabel, false)); std::vector<pair<int, int>>::iterator vecPairIt = equivalence.begin(); while (vecPairIt != equivalence.end()) { eqTab[vecPairIt->first - 1][vecPairIt->second - 1] = true; eqTab[vecPairIt->second - 1][vecPairIt->first - 1] = true; vecPairIt++; } std::vector<int> labelFlag(maxLabel, 0); std::vector<int> tempList; int curLabel = 1; for (int i = 1; i <= maxLabel; i++) { if (labelFlag[i - 1] != 0) { continue; } labelFlag[i - 1] = curLabel; tempList.emplace_back(i); for (int j = 0; j < tempList.size(); ++j) { for (int k = 1; k <= maxLabel; ++k) { if (eqTab[tempList[j] - 1][k - 1] && labelFlag[k] == 0) { labelFlag[k - 1] = curLabel; tempList.emplace_back(k); } } } curLabel++; tempList.clear(); } for (auto itr = runLabels.begin(); itr != runLabels.end(); ++iter) { *itr = labelFlag[*itr - 1]; } }
|
第四步:填充二值图中每个栅格的标签。
1 2 3 4 5 6 7 8
| void fillLabel(const std::vector<int>& runLabels, std::vector<int>& label_map) { for (size_t i = 0; i < label_map.size(); ++i) { if (label_map[i] == 0) { continue; } label_map[i] = runLabels[label_map[i]]; } }
|
总结
该算法本质上和floodFill算法是一样的,区别在于floodFill是对每个栅格进行深度优先搜索,而这个是对团进行深度优先搜索。另外,在算法的第三步在对等价对进行合并时,可以用并查集进行优化,也可以用稀疏矩阵与Dulmage-Mendelsohn分解算法用来消除等价对(matlab中的做法),比较复杂。