重磅干货,第一时间送达
图像的连通域是指图像中具有相同像素值并且位置相邻的像素组成的区域,连通域分析是指在图像中寻找出彼此互相独立的连通域并将其标记出来。提取图像中不同的连通域是图像处理中较为常用的方法,例如在车牌识别、文字识别、目标检测等领域对感兴趣区域分割与识别。一般情况下,一个连通域内只包含一个像素值,因此为了防止像素值波动对提取不同连通域的影响,连通域分析常处理的是二值化后的图像。 在了解图像连通域分析方法之前,首先需要了解图像邻域的概念。图像中两个像素相邻有两种定义方式,分别是4-邻域和8-邻域,这两种领域的定义方式在图6-7给出。4-邻域的定义方式如图6-7中的左侧所示,在这种定义下,两个像素相邻必须在水平和垂直方向上相邻,相邻的两个像素坐标必须只有一位不同而且只能相差1个像素,例如点的4-邻域的4个像素点分别为、 和。8-邻域的定义方式如图6-7中的右侧所示,这种定义下两个像素相邻允许在对角线方向相邻,相邻的两个像素坐标在X方向和Y方向上的最大差值为1,例如点的8-邻域的8个像素点分别为、、 、 、 、、以及。根据两个像素相邻的定义方式不同,得到的连通域也不相同,因此在分析连通域的同时,一定要声明是在哪种种邻域条件下分析得到的结果。 常用的图像邻域分析法有两遍扫描法和种子填充法。两遍扫描法会遍历两次图像,第一次遍历图像时会给每一个非0像素赋予一个数字标签,当某个像素的上方和左侧邻域内的像素已经有数字标签时,取两者中的最小值作为当前像素的标签,否则赋予当前像素一个新的数字标签。第一次遍历图像的时候同一个连通域可能会被赋予一个或者多个不同的标签,如图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. )
该函数用于计算二值图像中连通域的个数,并在图像中将不同的连通域用不同的数字标签标记出,其中标签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. )
该函数原型只有四个参数,前两个参数分别表示输入图像和输出图像,第三个参数表示统计连通域时选择的邻域种类,分别用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.} 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. )
该函数能够在图像中不同连通域标记标签的同时统计每个连通域的中心位置、矩形区域大小、区域面积等信息。函数的前两个参数含义与connectedComponents()函数的前两个参数含义一致,都是输入图像和输出图像。函数的第三个参数为每个连通域统计信息矩阵,如果图像中有N个连通域,那么该参数输出的矩阵尺寸为N×5,矩阵中每一行分别保存每个连通域的统计特性,详细的统计特性在表6-4中给出,如果想读取包含第i个连通域的边界框的水平长度,可以通过stats.at
上述函数原型的所有参数都没有默认值,在调用时需要设置全部参数,增加了使用的复杂程度,因此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. )
该函数原型只有六个参数,前两个参数分别表示输入图像和输出图像,第三个参数表示每个连通域的统计信息,第四个参数表示每个连通域的质心位置。后两个参数分别表示统计连通域时选择的邻域种类,分别用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. } |
|