【OpenCV 4开发详解】图像连通域分析
重磅干货,第一时间送达
经过几个月的努力,小白终于完成了市面上第一本OpenCV 4入门书籍《OpenCV 4开发详解》。为了更让小伙伴更早的了解最新版的OpenCV 4,小白与出版社沟通,提前在公众号上连载部分内容,请持续关注小白。 |
图像的连通域是指图像中具有相同像素值并且位置相邻的像素组成的区域,连通域分析是指在图像中寻找出彼此互相独立的连通域并将其标记出来。提取图像中不同的连通域是图像处理中较为常用的方法,例如在车牌识别、文字识别、目标检测等领域对感兴趣区域分割与识别。一般情况下,一个连通域内只包含一个像素值,因此为了防止像素值波动对提取不同连通域的影响,连通域分析常处理的是二值化后的图像。
在了解图像连通域分析方法之前,首先需要了解图像邻域的概念。图像中两个像素相邻有两种定义方式,分别是4-邻域和8-邻域,这两种领域的定义方式在图6-7给出。4-邻域的定义方式如图6-7中的左侧所示,在这种定义下,两个像素相邻必须在水平和垂直方向上相邻,相邻的两个像素坐标必须只有一位不同而且只能相差1个像素,例如点的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()函数中标记连通域算法类型可选择标志
标志参数 | 简记 | 作用 |
---|---|---|
CCL_WU | 0 | 8-邻域使用SAUF算法,4-邻域用SAUF算法 |
CCL_DEFAULT | -1 | 8-邻域使用BBDT算法,4-邻域用SAUF算法 |
CCL_GRANA | 1 | 8-邻域使用BBDT算法,4-邻域用SAUF算法 |
该函数用于计算二值图像中连通域的个数,并在图像中将不同的连通域用不同的数字标签标记出,其中标签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 ()函数中统计的连通域信息种类
标志参数 | 简记 | 作用 |
---|---|---|
CC_STAT_LEFT | 0 | 连通域内最左侧像素的x坐标,它是水平方向上的包含连通域边界框的开始。 |
CC_STAT_TOP | 1 | 连通域内最上方像素的y坐标,它是垂直方向上的包含连通域边界框的开始。 |
CC_STAT_WIDTH | 2 | 包含连通域边界框的水平长度 |
CC_STAT_HEIGHT | 3 | 包含连通域边界框的垂直长度 |
CC_STAT_AREA | 4 | 连通域的面积(以像素为单位) |
CC_STAT_MAX | 5 | 统计信息种类数目,无实际含义 |
上述函数原型的所有参数都没有默认值,在调用时需要设置全部参数,增加了使用的复杂程度,因此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程序中每个连通的面积