之前寫過一個C++版本的二值圖像連通區域標記函數,當時的直觀結果沒有問題,我也使用了很久,後來才發現其結果是錯的,I'm so sorry!
這裏貼出的是一個經過改進的二值圖像連通區域標記函數,目前只支持4連通區域標記,要想做到8連通標記的話,最簡單的方法是先用[1 1 1]的核對輸入圖像(的每一行)進行dilate。
較前一個版本的改進:(1)函數經過嚴格的測試,通過與Matlab連通區域標記結果的比較,確信可以輸出正確的結果;(2)中間內存不再使用std::vector來動態申請,而是使用預申請的數組空間,效率更高(但是所需要的內存大小往往更大);(3)輸出的標籤圖像中的數值是連續的(前一版本的標籤數據是不連續的),可以很方便地進行後續操作,比如統計每個連通區域的面積。
#include <memory>
//Labling connected components in an image, where non-zero pixels are
// deemed as foreground, and will be labeled with an positive integer
// while background pixels will be labled with zeros.
//Input and output are 2D matrices of size h-by-w.
//Return maxLabel. Output labels are continuously ranged between [0,maxLabel).
//Assume each pixel has 4 neighbors.
//yuxianguo, 2018/3/27
int bwLabel(const unsigned char *bw, int *label, int h, int w)
{
memset(label, 0, h * w << 2);
//link[i]:
//(1) link label value "i" to its connected component (another label value);
//(2) if link[i] == i, then it is a root.
int maxComponents = (h * w >> 1) + 1; //max possible connected components
int * const link = new int[maxComponents];
int lb = 1, x, y, a, b, t, *p = label;
link[0] = 0;
//first row
if(bw[0]) {
p[0] = lb;
link[lb] = lb;
lb++;
}
for(x = 1; x < w; x++) if(bw[x]) {
if(p[x - 1])
p[x] = p[x - 1];
else {
p[x] = lb;
link[lb] = lb;
lb++;
}
}
bw += w, p += w;
//rest rows
for(y = 1; y < h; y++, bw += w, p += w) {
if(bw[0]) {
if(p[-w])
p[0] = p[-w];
else {
p[0] = lb;
link[lb] = lb;
lb++;
}
}
for(x = 1; x < w; x++) if(bw[x]) {
a = p[x - 1], b = p[x - w]; //left & top
if(a) {
if(a == b)
p[x] = a;
else {
//find root of a
t = a;
while(a != link[a])
a = link[a];
p[x] = link[t] = a;
if(b) {
//find root of b
t = b;
while(b != link[b])
b = link[b];
link[t] = b;
//link b to a or link a to b, both fine
if(a < b) link[b] = a; else link[a] = b;
}
}
}
else if(b) {
//find root of b
t = b;
while(b != link[b])
b = link[b];
p[x] = link[t] = b;
}
else {
//generate a new component
p[x] = lb;
link[lb] = lb;
lb++;
}
}
}
//Rearrange the labels with continuous numbers
t = 1;
for(x = 1; x < lb; x++)
if(x == link[x]) {
link[x] = -t; //using negative values to denote roots
t++;
}
for(x = 1; x < lb; x++) {
//find the root of x
y = x;
while(link[y] >= 0)
y = link[y];
//set the value of label x
link[x] = link[y];
}
//Negative to positive
for(x = 1; x < lb; x++)
link[x] = -link[x];
//Replace existing label values by the corresponding root label values
p = label;
for(y = 0; y < h; y++, p += w)
for(x = 0; x < w; x++)
p[x] = link[p[x]];
delete[] link;
return t; //num components (maxLabel + 1)
}
可以藉助Matlab和下面這段代碼來測試以上函數實現的正確性:
for n = 1:15
h = randi(400) + 50;
w = randi(400) + 50;
I = uint8(rand(h, w) * 255);
I(rand(h, w) < 0.5) = 0;
% extreme case #1:
%I(:) = 0; I(1:2:end,2:2:end) = 1; I(2:2:end,1:2:end) = 1;
% extreme case #2:
%I(:) = 0; I(1:2:end,1:2:end) = 1; I(2:2:end,2:2:end) = 1;
yusave(I,'I'); % save data to local file
% call c++ program
A = ld('A'); % load result from local file
L1 = bwlabel(I,4);
L2 = bwlabel(A,4);
S1 = regionprops(L1,'area');
S1 = sort([S1.Area]);
m = max(L2(:));
S2 = zeros(1,m);
for k = 1:m
S2(k) = nnz(A==k);
end
S2 = sort(S2);
assert(isequal(S1,S2));
end
其中yusave和ld是我自己寫的文件讀寫函數,你們可以用自己的方法去做數據交流。我驗證了多次,結果沒有問題。