本文首發於“小白學視覺”微信公眾號,歡迎關注公眾號
本文作者為小白,版權歸人民郵電出版社發行所有,禁止轉載,侵權必究!
經過幾個月的努力,小白終於完成了市面上第一本OpenCV 4入門書籍《OpenCV 4開發詳解》。為了更讓小夥伴更早的瞭解最新版的OpenCV 4,小白與出版社溝通,提前在公眾號上連載部分內容,請持續關注小白。
影象的連通域是指影象中具有相同畫素值並且位置相鄰的畫素組成的區域,連通域分析是指在影象中尋找出彼此互相獨立的連通域並將其標記出來。提取影象中不同的連通域是影象處理中較為常用的方法,例如在車牌識別、文字識別、目標檢測等領域對感興趣區域分割與識別。一般情況下,一個連通域內只包含一個畫素值,因此為了防止畫素值波動對提取不同連通域的影響,連通域分析常處理的是二值化後的影象。
在瞭解影象連通域分析方法之前,首先需要了解影象鄰域的概念。影象中兩個畫素相鄰有兩種定義方式,分別是4-鄰域和8-鄰域,這兩種領域的定義方式在圖6-7給出。4-鄰域的定義方式如圖6-7中的左側所示,在這種定義下,兩個畫素相鄰必須在水平和垂直方向上相鄰,相鄰的兩個畫素座標必須只有一位不同而且只能相差1個畫素,例如點${P_0}(x,y)$的4-鄰域的4個畫素點分別為
、
、
和
。8-鄰域的定義方式如圖6-7中的右側所示,這種定義下兩個畫素相鄰允許在對角線方向相鄰,相鄰的兩個畫素座標在X方向和Y方向上的最大差值為1,例如點
的8-鄰域的8個畫素點分別為
、
、
、
、
、
、
、以及
。根據兩個畫素相鄰的定義方式不同,得到的連通域也不相同,因此在分析連通域的同時,一定要宣告是在哪種種鄰域條件下分析得到的結果。
圖6-7 4-鄰域和8-鄰域的定義方式示意圖
常用的影象鄰域分析法有兩遍掃描法和種子填充法。兩遍掃描法會遍歷兩次影象,第一次遍歷影象時會給每一個非0畫素賦予一個數字標籤,當某個畫素的上方和左側鄰域內的畫素已經有數字標籤時,取兩者中的最小值作為當前畫素的標籤,否則賦予當前畫素一個新的數字標籤。第一次遍歷影象的時候同一個連通域可能會被賦予一個或者多個不同的標籤,如圖6-8所示,因此第二次遍歷需要將這些屬於同一個連通域的不同標籤合併,最後實現同一個鄰域內的所有畫素具有相同的標籤。
圖6-8 兩遍掃描法中第一遍掃描的結果
種子填充法源於計算機影象學,常用於對某些圖形進行填充。該方法首先將所有非0畫素放到一個集合中,之後在集合中隨機選出一個畫素作為種子畫素,根據鄰域關係不斷擴充種子畫素所在的連通域,並在集合中刪除掉擴充出的畫素,直到種子畫素所在的連通域無法擴充,之後再從集合中隨機選取一個畫素作為新的種子畫素,重複上述過程直到集合中沒有畫素。
OpenCV 4提供了用於提取影象中不同連通域的connectedComponents()函式,該函式有兩個函式原型,第一種函式原型在程式碼清單6-4中給出。
程式碼清單
6
-
4
connectedComponents
()
函式原型
1
1。
int
cv
::
connectedComponents
(
InputArray
image
,
2。
OutputArray
labels
,
3。
int
connectivity
,
4。
int
ltype
,
5。
int
ccltype
6。
)
image:待標記不同連通域的單通道影象,資料型別必須為CV_8U。
labels:標記不同連通域後的輸出影象,與輸入影象具有相同的尺寸。
connectivity:標記連通域時使用的鄰域種類,4表示4-鄰域,8表示8-鄰域。
ltype:輸出影象的資料型別,目前支援CV_32S和CV_16U兩種資料型別。
ccltype:標記連通域時使用的演算法型別標誌,可以選擇的引數及含義在表6-3中給出。
表6-3 connectedComponents()函式中標記連通域演算法型別可選擇標誌
該函式用於計算二值影象中連通域的個數,並在影象中將不同的連通域用不同的數字標籤標記出,其中標籤0表示影象中的背景區域,同時函式具有一個int型別的返回資料,用於表示影象中連通域的數目。函式的第一個引數是待標記連通域的輸入影象,函式要求輸入影象必須是資料型別為CV_8U的單通道灰度影象,而且最好是經過二值化的二值影象。函式第二個引數是標記連通域後的輸出影象,影象尺寸與第一個引數的輸入影象尺寸相同,影象的資料型別與函式的第四個引數相關。函式第三個引數是統計連通域時選擇的鄰域種類,函式支援兩種鄰域,分別用4表示4-鄰域,8表示8-鄰域。函式第四個引數為輸出影象的資料型別,可以選擇的引數為CV_32S和CV_16U兩種。函式的最後一個引數是標記連通域時使用演算法的標誌,可以選擇的引數及含義在表6-3給出,目前只支援Grana(BBDT)和Wu(SAUF)兩種演算法。
上述函式原型的所有引數都沒有預設值,在呼叫時需要設定全部引數,增加了使用的複雜程度,因此OpenCV 4提供了connectedComponents()函式的簡易原型,減少了引數數量以及為部分引數增加了預設值,簡易原型在程式碼清單6-5中給出。
程式碼清單
6
-
5
connectedComponents
()
函式原型
2
1。
int
cv
::
connectedComponents
(
InputArray
image
,
2。
OutputArray
labels
,
3。
int
connectivity
=
8
,
4。
int
ltype
=
CV_32S
5。
)
image:待標記不同連通域的影象單通道,資料型別必須為CV_8U。
labels:標記不同連通域後的輸出影象,與輸入影象具有相同的尺寸。
connectivity:標記連通域時使用的鄰域種類,4表示4-鄰域,8表示8-鄰域,預設引數為8。
ltype:輸出影象的資料型別,目前支援CV_32S和CV_16U兩種資料型別,預設引數為CV_32S。
該函式原型只有四個引數,前兩個引數分別表示輸入影象和輸出影象,第三個引數表示統計連通域時選擇的鄰域種類,分別用4表示4-鄰域,8表示8-鄰域,引數的預設值為8。最後一個引數表示輸出影象的資料型別,可以選擇的引數為CV_32S和CV_16U兩種,引數的預設值為CV_32S。該函式原型有兩個引數具有預設值,在使用時最少只需要兩個引數,極大的方便了函式的呼叫。
為了瞭解connectedComponents()函式使用方式,在程式碼清單6-6中給出利用connectedComponents()函式統計影象中連通域數目的示例程式。程式中首先將影象轉換成灰度影象,然後將灰度影象二值化為二值影象,之後利用connectedComponents()函式對影象進行連通域的統計。根據統計結果,將數字不同的標籤設定成不同的顏色,以區分不同的連通域,程式執行的結果如圖6-9所示。
程式碼清單
6
-
6
myConnectedComponents
。
cpp影象連通域計算
1。
#
include
<
opencv2
\
opencv
。
hpp
>
2。
#
include
<
iostream
>
3。
#
include
<
vector
>
4。
5。
using
namespace
cv
;
6。
using
namespace
std
;
7。
8。
int
main
()
9。
{
10。
//對影象進行距離變換
11。
Mat
img
=
imread
(
“rice。png”
);
12。
if
(
img
。
empty
())
13。
{
14。
cout
<<
“請確認影象檔名稱是否正確”
<<
endl
;
15。
return
-
1
;
16。
}
17。
Mat
rice
,
riceBW
;
18。
19。
//將影象轉成二值影象,用於統計連通域
20。
cvtColor
(
img
,
rice
,
COLOR_BGR2GRAY
);
21。
threshold
(
rice
,
riceBW
,
50
,
255
,
THRESH_BINARY
);
22。
23。
//生成隨機顏色,用於區分不同連通域
24。
RNG
rng
(
10086
);
25。
Mat
out
;
26。
int
number
=
connectedComponents
(
riceBW
,
out
,
8
,
CV_16U
);
//統計影象中連通域的個數
27。
vector
<
Vec3b
>
colors
;
28。
for
(
int
i
=
0
;
i
<
number
;
i
++
)
29。
{
30。
//使用均勻分佈的隨機數確定顏色
31。
Vec3b
vec3
=
Vec3b
(
rng
。
uniform
(
0
,
256
),
rng
。
uniform
(
0
,
256
),
rng
。
uniform
(
0
,
256
));
32。
colors
。
push_back
(
vec3
);
33。
}
34。
35。
//以不同顏色標記出不同的連通域
36。
Mat
result
=
Mat
::
zeros
(
rice
。
size
(),
img
。
type
());
37。
int
w
=
result
。
cols
;
38。
int
h
=
result
。
rows
;
39。
for
(
int
row
=
0
;
row
<
h
;
row
++
)
40。
{
41。
for
(
int
col
=
0
;
col
<
w
;
col
++
)
42。
{
43。
int
label
=
out
。
at
<
uint16_t
>
(
row
,
col
);
44。
if
(
label
==
0
)
//背景的黑色不改變
45。
{
46。
continue
;
47。
}
48。
result
。
at
<
Vec3b
>
(
row
,
col
)
=
colors
[
label
];
49。
}
50。
}
51。
52。
//顯示結果
53。
imshow
(
“原圖”
,
img
);
54。
imshow
(
“標記後的影象”
,
result
);
55。
56。
waitKey
(
0
);
57。
return
0
;
58。
}
圖6-9 myConnectedComponents。cpp程式中影象連通域的計算結果
connectedComponents()函式雖然可以實現影象中多個連通域的統計,但是該函式只能透過標籤將影象中的不同連通域區分開,無法得到更多的統計資訊。有時我們希望得到每個連通域中心位置或者在影象中標記出連通域所在的矩形區域,connectedComponents()函式便無法勝任這項任務,因為該函式無法得到更多的資訊。為了能夠獲得更多有關連通域的資訊,OpenCV 4提供了connectedComponentsWithStats ()函式用於標記出影象中不同連通域的同時統計連通域的位置、面積的資訊,該函式的函式原型在程式碼清單6-7中給出。
程式碼清單
6
-
7
connectedComponentsWithStats
()
函式原型
1
1。
int
cv
::
connectedComponentsWithStats
(
InputArray
image
,
2。
OutputArray
labels
,
3。
OutputArray
stats
,
4。
OutputArray
centroids
,
5。
int
connectivity
,
6。
int
ltype
,
7。
int
ccltype
8。
)
image:待標記不同連通域的單通道影象,資料型別必須為CV_8U。
labels:標記不同連通域後的輸出影象,與輸入影象具有相同的尺寸。
stats:含有不同連通域統計資訊的矩陣,矩陣的資料型別為CV_32S。矩陣中第i行是標籤為i的連通域的統計特性,儲存的統計資訊種類在表6-4中給出。
centroids:每個連通域的質心座標,資料型別為CV_64F。
connectivity:標記連通域時使用的鄰域種類,4表示4-鄰域,8表示8-鄰域。
ltype:輸出影象的資料型別,目前支援CV_32S和CV_16U兩種資料型別。
ccltype:標記連通域使用的演算法型別標誌,可以選擇的引數及含義在表6-3中給出。
該函式能夠在影象中不同連通域標記標籤的同時統計每個連通域的中心位置、矩形區域大小、區域面積等資訊。函式的前兩個引數含義與connectedComponents()函式的前兩個引數含義一致,都是輸入影象和輸出影象。函式的第三個引數為每個連通域統計資訊矩陣,如果影象中有N個連通域,那麼該引數輸出的矩陣尺寸為N×5,矩陣中每一行分別儲存每個連通域的統計特性,詳細的統計特性在表6-4中給出,如果想讀取包含第i個連通域的邊界框的水平長度,可以透過stats。at(i, CC_STAT_WIDTH)或者stats。at(i, 0)進行讀取。函式的第四個引數為每個連通域質心的座標,如果影象中有N個連通域,那麼該引數輸出的矩陣尺寸為N×2,矩陣中每一行分別儲存每個連通域質心的x座標和y座標,可以透過centroids。at(i, 0)和 centroids。at(i, 1) 分別讀取第i個連通域質心的x座標和y座標。函式第五個引數是統計連通域時選擇的鄰域種類,函式支援兩種鄰域,分別用4表示4-鄰域,8表示8-鄰域。函式第六個引數為輸出影象的資料型別,可以選擇的引數為CV_32S和CV_16U兩種。函式的最後一個引數是標記連通域使用的演算法,可以選擇的引數在表6-3給出,目前只支援Grana(BBDT)和Wu(SAUF)兩種演算法。
表6-4 connectedComponentsWithStats ()函式中統計的連通域資訊種類
上述函式原型的所有引數都沒有預設值,在呼叫時需要設定全部引數,增加了使用的複雜程度,因此OpenCV 4提供了ConnectedComponentsWithStats()函式的簡易原型,減少了引數數量以及為部分引數增加了預設值,簡易原型在程式碼清單6-8中給出。
程式碼清單
6
-
8
connectedComponentsWithStats
()
函式原型
2
1。
int
cv
::
connectedComponentsWithStats
(
InputArray
image
,
2。
OutputArray
labels
,
3。
OutputArray
stats
,
4。
OutputArray
centroids
,
5。
int
connectivity
=
8
,
6。
int
ltype
=
CV_32S
7。
)
image:待標記不同連通域的單通道影象,資料型別必須為CV_8U。
labels:標記不同連通域後的輸出影象,與輸入影象具有相同的尺寸。
stats:不同連通域的統計資訊矩陣,矩陣的資料型別為CV_32S。矩陣中第i行是標籤為i的連通域的統計特性,儲存的統計資訊種類在表6-4中給出。
centroids:每個連通域的質心座標,資料型別為CV_64F。
connectivity:標記連通域時使用的鄰域種類,4表示4-鄰域,8表示8-鄰域,預設引數值為8。
ltype:輸出影象的資料型別,目前只支援CV_32S和CV_16U這兩種資料型別,預設引數值為CV_32S。
該函式原型只有六個引數,前兩個引數分別表示輸入影象和輸出影象,第三個引數表示每個連通域的統計資訊,第四個引數表示每個連通域的質心位置。後兩個引數分別表示統計連通域時選擇的鄰域種類,分別用4表示4-鄰域,8表示8-鄰域,引數的預設值為8。最後一個引數表示輸出影象的資料型別,可以選擇的引數為CV_32S和CV_16U兩種,引數的預設值為CV_32S。該函式原型有兩個引數具有預設值,在使用時最少只需要四個引數,極大的方便了函式的呼叫。
為了瞭解connectedComponentsWithStats ()函式使用方式,在程式碼清單6-9中給出利用該函式統計影象中連通域數目並將每個連通域資訊在影象中進行標註的示例程式。程式中首先將影象轉換成灰度影象,然後將灰度影象二值化為二值影象,之後利用connectedComponentsWithStats ()函式對影象進行連通域的統計。根據統計結果,用不同顏色的矩形框將連通域圍起來,並標記出每個連通域的質心,標出連通域的標籤數字,以區分不同的連通域,程式執行的結果如圖6-10所示。最後輸出每個連通域的面積,輸入結果在圖6-11給出。
程式碼清單
6
-
9
myConnectedComponentsWithStats
。
cpp連通域資訊統計
1。
#
include
<
opencv2
\
opencv
。
hpp
>
2。
#
include
<
iostream
>
3。
#
include
<
vector
>
4。
5。
using
namespace
cv
;
6。
using
namespace
std
;
7。
8。
int
main
()
9。
{
10。
system
(
“color F0”
);
//更改輸出介面顏色
11。
//對影象進行距離變換
12。
Mat
img
=
imread
(
“rice。png”
);
13。
if
(
img
。
empty
())
14。
{
15。
cout
<<
“請確認影象檔名稱是否正確”
<<
endl
;
16。
return
-
1
;
17。
}
18。
imshow
(
“原圖”
,
img
);
19。
Mat
rice
,
riceBW
;
20。
21。
//將影象轉成二值影象,用於統計連通域
22。
cvtColor
(
img
,
rice
,
COLOR_BGR2GRAY
);
23。
threshold
(
rice
,
riceBW
,
50
,
255
,
THRESH_BINARY
);
24。
25。
//生成隨機顏色,用於區分不同連通域
26。
RNG
rng
(
10086
);
27。
Mat
out
,
stats
,
centroids
;
28。
//統計影象中連通域的個數
29。
int
number
=
connectedComponentsWithStats
(
riceBW
,
out
,
stats
,
centroids
,
8
,
CV_16U
);
30。
vector
<
Vec3b
>
colors
;
31。
for
(
int
i
=
0
;
i
<
number
;
i
++
)
32。
{
33。
//使用均勻分佈的隨機數確定顏色
34。
Vec3b
vec3
=
Vec3b
(
rng
。
uniform
(
0
,
256
),
rng
。
uniform
(
0
,
256
),
rng
。
uniform
(
0
,
256
));
35。
colors
。
push_back
(
vec3
);
36。
}
37。
38。
//以不同顏色標記出不同的連通域
39。
Mat
result
=
Mat
::
zeros
(
rice
。
size
(),
img
。
type
());
40。
int
w
=
result
。
cols
;
41。
int
h
=
result
。
rows
;
42。
for
(
int
i
=
1
;
i
<
number
;
i
++
)
43。
{
44。
// 中心位置
45。
int
center_x
=
centroids
。
at
<
double
>
(
i
,
0
);
46。
int
center_y
=
centroids
。
at
<
double
>
(
i
,
1
);
47。
//矩形邊框
48。
int
x
=
stats
。
at
<
int
>
(
i
,
CC_STAT_LEFT
);
49。
int
y
=
stats
。
at
<
int
>
(
i
,
CC_STAT_TOP
);
50。
int
w
=
stats
。
at
<
int
>
(
i
,
CC_STAT_WIDTH
);
51。
int
h
=
stats
。
at
<
int
>
(
i
,
CC_STAT_HEIGHT
);
52。
int
area
=
stats
。
at
<
int
>
(
i
,
CC_STAT_AREA
);
53。
54。
// 中心位置繪製
55。
circle
(
img
,
Point
(
center_x
,
center_y
),
2
,
Scalar
(
0
,
255
,
0
),
2
,
8
,
0
);
56。
// 外接矩形
57。
Rect
rect
(
x
,
y
,
w
,
h
);
58。
rectangle
(
img
,
rect
,
colors
[
i
],
1
,
8
,
0
);
59。
putText
(
img
,
format
(
“%d”
,
i
),
Point
(
center_x
,
center_y
),
60。
FONT_HERSHEY_SIMPLEX
,
0。5
,
Scalar
(
0
,
0
,
255
),
1
);
61。
cout
<<
“number: ”
<<
i
<<
“,area: ”
<<
area
<<
endl
;
62。
}
63。
//顯示結果
64。
imshow
(
“標記後的影象”
,
img
);
65。
66。
waitKey
(
0
);
67。
return
0
;
68。
}
圖6-10 myConnectedComponentsWithStats。cpp程式中影象連通域的統計結果
圖6-11 myConnectedComponentsWithStats。cpp程式中每個連通的面積
經過幾個月的努力,市面上第一本OpenCV 4入門書籍《OpenCV 4開發詳解》將春節後由人民郵電出版社發行。如果小夥伴覺得內容有幫助,希望到時候多多支援!
關注小白的小夥伴可以提前看到書中的內容,我們建立了學習交流群,歡迎各位小夥伴新增小白微信加入交流群,新增小白時請備註“學習OpenCV 4”。