图像处理邻域连通性c++

2019-07-29 15:19:59 SoaringLee_fighting 阅读数 131

DATE: 2019-7-29


转载自:http://blog.sina.com.cn/s/blog_a98e39a201010six.html

1、图像的质量:

1.1、层次

? 灰度级:表示像素明暗程度的整数量
例如:像素的取值范围为0-255,就称该图像为256个灰度级的图像
? 层次:表示图像实际拥有的灰度级的数量
例如:具有32种不同取值的图像,可称该图像具有32个层次
图像数据的实际层次越多,视觉效果就越好

1.2、对比度

? 对比度:是指一幅图像中灰度反差的大小
对比度= 最大亮度/ 最小亮度

1.3、清晰度

? 与清晰度相关的主要因素
? 亮度
? 对比度
? 尺寸大小
? 细微层次
? 颜色饱和度

2、像素间的一些基本关系

? 相邻像素:
? 4邻域
? D邻域
? 8邻域
? 连通性
? 4连通
? 8连通
? m连通
? 距离

2.1、相邻像素——4邻域

? 4邻域:像素p(x,y)的4邻域是:
(x+1,y);(x-1,y);(x,y+1);(x,y-1)
? 用N4§表示像素p的4邻域

在这里插入图片描述

2.2、相邻像素——D邻域

?D邻域定义:像素p(x,y)的D邻域是:
对角上的点(x+1,y+1);(x+1,y-1);(x-1,y+1);(x-1,y-1)
?用ND§表示像素p的D邻域
数字图像处理入门(二)-邻域、连通性

2.3、相邻像素——8邻域

? 8邻域定义:像素p(x,y)的8邻域是:
4邻域的点+ D邻域的点
? 用N8§表示像素p的8邻域。
N8§ = N4§ + ND§

在这里插入图片描述

3、像素间的连通性

? 连通性是描述区域和边界的重要概念
? 两个像素连通的两个必要条件是:
? 两个像素的位置是否相邻
? 两个像素的灰度值是否满足特定的相似性准则(或者是否相等)
? 4连通、8连通、m连通的定义

3.1、像素的连通性——4连通

? 对于具有值V的像素p和q,如果q在集合N4§中,则称这两个像素是4连通的。
在这里插入图片描述

3.2、像素的连通性——8连通

? 对于具有值V的像素p和q,如果q在集合N8§中,则称这两个像素是8连通的

在这里插入图片描述

3.3、像素的连通性——m连通

对于具有值V的像素p和q,如果:
I. q在集合N4§中,或
II. q在集合ND§中,并且N4§与N4(q)的交集为空(没有值V的像素),则称两个像素是m连通的,即4连通和D连通的混合连通。
在这里插入图片描述


THE END!

2020-02-14 19:05:51 weixin_43793960 阅读数 106


OpenCV_连通区域分析(Connected Component Analysis/Labeling)


【摘要】

本文主要介绍在CVPR和图像处理领域中较为常用的一种图像区域(Blob)提取的方法——连通性分析法(连通区域标记法)。文中介绍了两种常见的连通性分析的算法:1)Two-pass;2)Seed-Filling种子填充,并给出了两个算法的基于OpenCV的C++实现代码。



一、连通区域分析

连通区域(Connected Component)一般是指图像中具有相同像素值且位置相邻的前景像素点组成的图像区域(Region,Blob)。连通区域分析(Connected Component Analysis,Connected Component Labeling)是指将图像中的各个连通区域找出并标记。

连通区域分析是一种在CVPR和图像分析处理的众多应用领域中较为常用和基本的方法。例如:OCR识别中字符分割提取(车牌识别、文本识别、字幕识别等)、视觉跟踪中的运动前景目标分割与提取(行人入侵检测、遗留物体检测、基于视觉的车辆检测与跟踪等)、医学图像处理(感兴趣目标区域提取)、等等。也就是说,在需要将前景目标提取出来以便后续进行处理的应用场景中都能够用到连通区域分析方法,通常连通区域分析处理的对象是一张二值化后的图像。


二、连通区域分析的算法

从连通区域的定义可以知道,一个连通区域是由具有相同像素值的相邻像素组成像素集合,因此,我们就可以通过这两个条件在图像中寻找连通区域,对于找到的每个连通区域,我们赋予其一个唯一的标识(Label),以区别其他连通区域。

连通区域分析有基本的算法,也有其改进算法,本文介绍其中的两种常见算法:

1)Two-Pass法;2)Seed-Filling种子填充法;


Note:

a、这里的扫描指的是按行或按列访问以便图像的所有像素,本文算法采用的是按行扫描方式;

b、图像记为B,为二值图像:前景像素(pixel value = 1),背景像素(pixel value = 0)

c、label从2开始计数;

d、像素相邻关系:4-领域、8-领域,本文算法采用4-邻域;


                                     

4—领域图例                                                     8—领域图例



1)Two-Pass(两遍扫描法)

两遍扫描法,正如其名,指的就是通过扫描两遍图像,就可以将图像中存在的所有连通区域找出并标记。思路:第一遍扫描时赋予每个像素位置一个label,扫描过程中同一个连通区域内的像素集合中可能会被赋予一个或多个不同label,因此需要将这些属于同一个连通区域但具有不同值的label合并,也就是记录它们之间的相等关系;第二遍扫描就是将具有相等关系的equal_labels所标记的像素归为一个连通区域并赋予一个相同的label(通常这个label是equal_labels中的最小值)。


下面给出Two-Pass算法的简单步骤:

(1)第一次扫描:

访问当前像素B(x,y),如果B(x,y) == 1:

a、如果B(x,y)的领域中像素值都为0,则赋予B(x,y)一个新的label:

label += 1, B(x,y) = label;

b、如果B(x,y)的领域中有像素值 > 1的像素Neighbors:

1)Neighbors中的最小值赋予给B(x,y):

B(x,y) = min{Neighbors}

2)记录Neighbors中各个值(label)之间的相等关系,即这些值(label)同属同一个连通区域;

 labelSet[i] = { label_m, .., label_n },labelSet[i]中的所有label都属于同一个连通区域(注:这里可以有多种实现方式,只要能够记录这些具有相等关系的label之间的关系即可)

(2)第二次扫描:

访问当前像素B(x,y),如果B(x,y) > 1:

a、找到与label = B(x,y)同属相等关系的一个最小label值,赋予给B(x,y)

完成扫描后,图像中具有相同label值的像素就组成了同一个连通区域


下面这张图动态地演示了Two-pass算法:





2)Seed Filling(种子填充法)

种子填充方法来源于计算机图形学,常用于对某个图形进行填充。思路:选取一个前景像素点作为种子,然后根据连通区域的两个基本条件(像素值相同、位置相邻)将与种子相邻的前景像素合并到同一个像素集合中,最后得到的该像素集合则为一个连通区域。


下面给出基于种子填充法的连通区域分析方法:

(1)扫描图像,直到当前像素点B(x,y) == 1:

a、将B(x,y)作为种子(像素位置),并赋予其一个label,然后将该种子相邻的所有前景像素都压入栈中;

b、弹出栈顶像素,赋予其相同的label,然后再将与该栈顶像素相邻的所有前景像素都压入栈中;

c、重复b步骤,直到栈为空;

此时,便找到了图像B中的一个连通区域,该区域内的像素值被标记为label;

(2)重复第(1)步,直到扫描结束;

扫描结束后,就可以得到图像B中所有的连通区域;


下面这张图动态地演示了Seed-Filling算法:




三、实验演示


1)前景二值图像


2)连通区域分析方法标记后得到的label图像


Two-pass算法:



Seed-filling算法:


注:为了显示方便,将像素值乘以了一个整数进行放大。


3)color后的label图像

Two-pass算法:


Seed-filling算法:



注:颜色是随机生成的





四、代码


1)Two-pass算法的一种实现

说明:

基于OpenCV和C++实现,领域:4-领域。实现与算法描述稍有差别(具体为记录具有相等关系的label方法实现上)。

  1. // Connected Component Analysis/Labeling By Two-Pass Algorithm
  2. // Author: www.icvpr.com
  3. // Blog : http://blog.csdn.net/icvpr
  4. #include <iostream>
  5. #include <string>
  6. #include <list>
  7. #include <vector>
  8. #include <map>
  9. #include <opencv2/imgproc/imgproc.hpp>
  10. #include <opencv2/highgui/highgui.hpp>
  11. void icvprCcaByTwoPass(const cv::Mat& _binImg, cv::Mat& _lableImg)
  12. {
  13. // connected component analysis (4-component)
  14. // use two-pass algorithm
  15. // 1. first pass: label each foreground pixel with a label
  16. // 2. second pass: visit each labeled pixel and merge neighbor labels
  17. //
  18. // foreground pixel: _binImg(x,y) = 1
  19. // background pixel: _binImg(x,y) = 0
  20. if (_binImg.empty() ||
  21. _binImg.type() != CV_8UC1)
  22. {
  23. return ;
  24. }
  25. // 1. first pass
  26. _lableImg.release() ;
  27. _binImg.convertTo(_lableImg, CV_32SC1) ;
  28. int label = 1 ; // start by 2
  29. std::vector<int> labelSet ;
  30. labelSet.push_back(0) ; // background: 0
  31. labelSet.push_back(1) ; // foreground: 1
  32. int rows = _binImg.rows - 1 ;
  33. int cols = _binImg.cols - 1 ;
  34. for (int i = 1; i < rows; i++)
  35. {
  36. int* data_preRow = _lableImg.ptr<int>(i-1) ;
  37. int* data_curRow = _lableImg.ptr<int>(i) ;
  38. for (int j = 1; j < cols; j++)
  39. {
  40. if (data_curRow[j] == 1)
  41. {
  42. std::vector<int> neighborLabels ;
  43. neighborLabels.reserve(2) ;
  44. int leftPixel = data_curRow[j-1] ;
  45. int upPixel = data_preRow[j] ;
  46. if ( leftPixel > 1)
  47. {
  48. neighborLabels.push_back(leftPixel) ;
  49. }
  50. if (upPixel > 1)
  51. {
  52. neighborLabels.push_back(upPixel) ;
  53. }
  54. if (neighborLabels.empty())
  55. {
  56. labelSet.push_back(++label) ; // assign to a new label
  57. data_curRow[j] = label ;
  58. labelSet[label] = label ;
  59. }
  60. else
  61. {
  62. std::sort(neighborLabels.begin(), neighborLabels.end()) ;
  63. int smallestLabel = neighborLabels[0] ;
  64. data_curRow[j] = smallestLabel ;
  65. // save equivalence
  66. for (size_t k = 1; k < neighborLabels.size(); k++)
  67. {
  68. int tempLabel = neighborLabels[k] ;
  69. int& oldSmallestLabel = labelSet[tempLabel] ;
  70. if (oldSmallestLabel > smallestLabel)
  71. {
  72. labelSet[oldSmallestLabel] = smallestLabel ;
  73. oldSmallestLabel = smallestLabel ;
  74. }
  75. else if (oldSmallestLabel < smallestLabel)
  76. {
  77. labelSet[smallestLabel] = oldSmallestLabel ;
  78. }
  79. }
  80. }
  81. }
  82. }
  83. }
  84. // update equivalent labels
  85. // assigned with the smallest label in each equivalent label set
  86. for (size_t i = 2; i < labelSet.size(); i++)
  87. {
  88. int curLabel = labelSet[i] ;
  89. int preLabel = labelSet[curLabel] ;
  90. while (preLabel != curLabel)
  91. {
  92. curLabel = preLabel ;
  93. preLabel = labelSet[preLabel] ;
  94. }
  95. labelSet[i] = curLabel ;
  96. }
  97. // 2. second pass
  98. for (int i = 0; i < rows; i++)
  99. {
  100. int* data = _lableImg.ptr<int>(i) ;
  101. for (int j = 0; j < cols; j++)
  102. {
  103. int& pixelLabel = data[j] ;
  104. pixelLabel = labelSet[pixelLabel] ;
  105. }
  106. }
  107. }




2)Seed-Filling种子填充方法

说明:

基于OpenCV和C++实现;领域:4-领域。


  1. // Connected Component Analysis/Labeling By Seed-Filling Algorithm
  2. // Author: www.icvpr.com
  3. // Blog : http://blog.csdn.net/icvpr
  4. #include <iostream>
  5. #include <string>
  6. #include <list>
  7. #include <vector>
  8. #include <map>
  9. #include <stack>
  10. #include <opencv2/imgproc/imgproc.hpp>
  11. #include <opencv2/highgui/highgui.hpp>
  12. void icvprCcaBySeedFill(const cv::Mat& _binImg, cv::Mat& _lableImg)
  13. {
  14. // connected component analysis (4-component)
  15. // use seed filling algorithm
  16. // 1. begin with a foreground pixel and push its foreground neighbors into a stack;
  17. // 2. pop the top pixel on the stack and label it with the same label until the stack is empty
  18. //
  19. // foreground pixel: _binImg(x,y) = 1
  20. // background pixel: _binImg(x,y) = 0
  21. if (_binImg.empty() ||
  22. _binImg.type() != CV_8UC1)
  23. {
  24. return ;
  25. }
  26. _lableImg.release() ;
  27. _binImg.convertTo(_lableImg, CV_32SC1) ;
  28. int label = 1 ; // start by 2
  29. int rows = _binImg.rows - 1 ;
  30. int cols = _binImg.cols - 1 ;
  31. for (int i = 1; i < rows-1; i++)
  32. {
  33. int* data= _lableImg.ptr<int>(i) ;
  34. for (int j = 1; j < cols-1; j++)
  35. {
  36. if (data[j] == 1)
  37. {
  38. std::stack<std::pair<int,int>> neighborPixels ;
  39. neighborPixels.push(std::pair<int,int>(i,j)) ; // pixel position: <i,j>
  40. ++label ; // begin with a new label
  41. while (!neighborPixels.empty())
  42. {
  43. // get the top pixel on the stack and label it with the same label
  44. std::pair<int,int> curPixel = neighborPixels.top() ;
  45. int curX = curPixel.first ;
  46. int curY = curPixel.second ;
  47. _lableImg.at<int>(curX, curY) = label ;
  48. // pop the top pixel
  49. neighborPixels.pop() ;
  50. // push the 4-neighbors (foreground pixels)
  51. if (_lableImg.at<int>(curX, curY-1) == 1)
  52. {// left pixel
  53. neighborPixels.push(std::pair<int,int>(curX, curY-1)) ;
  54. }
  55. if (_lableImg.at<int>(curX, curY+1) == 1)
  56. {// right pixel
  57. neighborPixels.push(std::pair<int,int>(curX, curY+1)) ;
  58. }
  59. if (_lableImg.at<int>(curX-1, curY) == 1)
  60. {// up pixel
  61. neighborPixels.push(std::pair<int,int>(curX-1, curY)) ;
  62. }
  63. if (_lableImg.at<int>(curX+1, curY) == 1)
  64. {// down pixel
  65. neighborPixels.push(std::pair<int,int>(curX+1, curY)) ;
  66. }
  67. }
  68. }
  69. }
  70. }
  71. }



3)颜色标记(用于显示)


  1. // Connected Component Analysis/Labeling -- Color Labeling
  2. // Author: www.icvpr.com
  3. // Blog : http://blog.csdn.net/icvpr
  4. #include <iostream>
  5. #include <string>
  6. #include <list>
  7. #include <vector>
  8. #include <map>
  9. #include <stack>
  10. #include <opencv2/imgproc/imgproc.hpp>
  11. #include <opencv2/highgui/highgui.hpp>
  12. cv::Scalar icvprGetRandomColor()
  13. {
  14. uchar r = 255 * (rand()/(1.0 + RAND_MAX));
  15. uchar g = 255 * (rand()/(1.0 + RAND_MAX));
  16. uchar b = 255 * (rand()/(1.0 + RAND_MAX));
  17. return cv::Scalar(b,g,r) ;
  18. }
  19. void icvprLabelColor(const cv::Mat& _labelImg, cv::Mat& _colorLabelImg)
  20. {
  21. if (_labelImg.empty() ||
  22. _labelImg.type() != CV_32SC1)
  23. {
  24. return ;
  25. }
  26. std::map<int, cv::Scalar> colors ;
  27. int rows = _labelImg.rows ;
  28. int cols = _labelImg.cols ;
  29. _colorLabelImg.release() ;
  30. _colorLabelImg.create(rows, cols, CV_8UC3) ;
  31. _colorLabelImg = cv::Scalar::all(0) ;
  32. for (int i = 0; i < rows; i++)
  33. {
  34. const int* data_src = (int*)_labelImg.ptr<int>(i) ;
  35. uchar* data_dst = _colorLabelImg.ptr<uchar>(i) ;
  36. for (int j = 0; j < cols; j++)
  37. {
  38. int pixelValue = data_src[j] ;
  39. if (pixelValue > 1)
  40. {
  41. if (colors.count(pixelValue) <= 0)
  42. {
  43. colors[pixelValue] = icvprGetRandomColor() ;
  44. }
  45. cv::Scalar color = colors[pixelValue] ;
  46. *data_dst++ = color[0] ;
  47. *data_dst++ = color[1] ;
  48. *data_dst++ = color[2] ;
  49. }
  50. else
  51. {
  52. data_dst++ ;
  53. data_dst++ ;
  54. data_dst++ ;
  55. }
  56. }
  57. }
  58. }




4)测试程序


  1. // Connected Component Analysis/Labeling -- Test code
  2. // Author: www.icvpr.com
  3. // Blog : http://blog.csdn.net/icvpr
  4. #include <iostream>
  5. #include <string>
  6. #include <list>
  7. #include <vector>
  8. #include <map>
  9. #include <stack>
  10. #include <opencv2/imgproc/imgproc.hpp>
  11. #include <opencv2/highgui/highgui.hpp>
  12. int main(int argc, char** argv)
  13. {
  14. cv::Mat binImage = cv::imread("../icvpr.com.jpg", 0) ;
  15. cv::threshold(binImage, binImage, 50, 1, CV_THRESH_BINARY_INV) ;
  16. // connected component labeling
  17. cv::Mat labelImg ;
  18. icvprCcaByTwoPass(binImage, labelImg) ;
  19. //icvprCcaBySeedFill(binImage, labelImg) ;
  20. // show result
  21. cv::Mat grayImg ;
  22. labelImg *= 10 ;
  23. labelImg.convertTo(grayImg, CV_8UC1) ;
  24. cv::imshow("labelImg", grayImg) ;
  25. cv::Mat colorLabelImg ;
  26. icvprLabelColor(labelImg, colorLabelImg) ;
  27. cv::imshow("colorImg", colorLabelImg) ;
  28. cv::waitKey(0) ;
  29. return 0 ;
  30. }




Reference

[1] http://en.wikipedia.org/wiki/Connected-component_labeling

[2] http://homepages.inf.ed.ac.uk/rbf/HIPR2/label.htm

[3] http://www.codeproject.com/Articles/336915/Connected-Component-Labeling-Algorithm



声明:

作者:icvpr  blog.csdn.net/icvpr

2018-06-21 11:33:58 sy95122 阅读数 1868

OpenCV提供了专门的函数标记连通区域connectedComponents,它有四种形式:

1. int connectedComponents(InputArray image, OutputArray labels, int connectivity, int ltype, int ccltype);

参数说明:

image : 输入原始图像,8bit单通道,二值图像

labels : 输出标记图像

connectivity : 邻域类型,4连通或8连通,所以这个值通常为两个值(4或8)

ltype : 输出标记的类型,只支持 CV_32S和CV_16U

ccltype : 连通标记的算法,这个值有三个选择CCL_WU(8连通和4连通均用SAUF算法),CCL_DEFAULT(8连通用BBDT算法,4连通用SAUF算法)和CCL_GRANA(8连通用BBDT算法,4连通用SAUF算法)。(后两种方法是一样的,这里有疑问)


2. int connectedComponents(InputArray image, OutputArray labels, int connectivity = 8, int ltype = CV_32S);

这个函数实际上就是第一个的简化版

这两个函数的返回值是标记的个数[0,N-1],0表示背景区域


3. int connectedComponentsWithStats(InputArray image, OutputArray labels, OutputArray stats, OutputArray centroids,  int connectivity, int ltype, int ccltype);

参数说明:

image : 输入原始图像,8bit单通道,二值图像

labels : 输出标记图像

stats : 每个标记的统计输出,类型为CV_32S,使用格式为stats(label, COLUMN),其中COLUMN为连通类型,包括CC_STAT_LEFT、CC_STAT_TOP、CC_STAT_WIDTH、CC_STAT_HEIGHT、CC_STAT_AREA

centroids : 每个标记区域的质心,类型为CV_64F,使用方法为x = centroids(label,0), y = centroids(label, 1)

connectivity : 邻域类型,4连通或8连通,所以这个值通常为两个值(4或8)

ltype : 输出标记的类型,只支持 CV_32S和CV_16U


4. int connectedComponentsWithStats(InputArray image, OutputArray labels, OutputArray stats, OutputArray centroids,  int connectivity = 8, int ltype = CV_32S);

第三个函数的简化版


----------------------华丽的分界线------------------------


以下转自:https://blog.csdn.net/icvpr/article/details/10259577

OpenCV_连通区域分析(Connected Component Analysis/Labeling)


【摘要】

本文主要介绍在CVPR和图像处理领域中较为常用的一种图像区域(Blob)提取的方法——连通性分析法(连通区域标记法)。文中介绍了两种常见的连通性分析的算法:1)Two-pass;2)Seed-Filling种子填充,并给出了两个算法的基于OpenCV的C++实现代码。


一、连通区域分析

连通区域(Connected Component)一般是指图像中具有相同像素值且位置相邻的前景像素点组成的图像区域(Region,Blob)。连通区域分析(Connected Component Analysis,Connected Component Labeling)是指将图像中的各个连通区域找出并标记。

连通区域分析是一种在CVPR和图像分析处理的众多应用领域中较为常用和基本的方法。例如:OCR识别中字符分割提取(车牌识别、文本识别、字幕识别等)、视觉跟踪中的运动前景目标分割与提取(行人入侵检测、遗留物体检测、基于视觉的车辆检测与跟踪等)、医学图像处理(感兴趣目标区域提取)、等等。也就是说,在需要将前景目标提取出来以便后续进行处理的应用场景中都能够用到连通区域分析方法,通常连通区域分析处理的对象是一张二值化后的图像。

二、连通区域分析的算法

从连通区域的定义可以知道,一个连通区域是由具有相同像素值的相邻像素组成像素集合,因此,我们就可以通过这两个条件在图像中寻找连通区域,对于找到的每个连通区域,我们赋予其一个唯一的标识(Label),以区别其他连通区域。

连通区域分析有基本的算法,也有其改进算法,本文介绍其中的两种常见算法:

1)Two-Pass法;2)Seed-Filling种子填充法;

Note:

a、这里的扫描指的是按行或按列访问以便图像的所有像素,本文算法采用的是按行扫描方式;

b、图像记为B,为二值图像:前景像素(pixel value = 1),背景像素(pixel value = 0)

c、label从2开始计数;

d、像素相邻关系:4-领域、8-领域,本文算法采用4-邻域;


                                     

4—领域图例                                                     8—领域图例



1)Two-Pass(两遍扫描法)

两遍扫描法,正如其名,指的就是通过扫描两遍图像,就可以将图像中存在的所有连通区域找出并标记。思路:第一遍扫描时赋予每个像素位置一个label,扫描过程中同一个连通区域内的像素集合中可能会被赋予一个或多个不同label,因此需要将这些属于同一个连通区域但具有不同值的label合并,也就是记录它们之间的相等关系;第二遍扫描就是将具有相等关系的equal_labels所标记的像素归为一个连通区域并赋予一个相同的label(通常这个label是equal_labels中的最小值)。


下面给出Two-Pass算法的简单步骤:

(1)第一次扫描:

访问当前像素B(x,y),如果B(x,y) == 1:

a、如果B(x,y)的领域中像素值都为0,则赋予B(x,y)一个新的label:

label += 1, B(x,y) = label;

b、如果B(x,y)的领域中有像素值 > 1的像素Neighbors:

1)Neighbors中的最小值赋予给B(x,y):

B(x,y) = min{Neighbors} 

2)记录Neighbors中各个值(label)之间的相等关系,即这些值(label)同属同一个连通区域;

 labelSet[i] = { label_m, .., label_n },labelSet[i]中的所有label都属于同一个连通区域(注:这里可以有多种实现方式,只要能够记录这些具有相等关系的label之间的关系即可)

(2)第二次扫描:

访问当前像素B(x,y),如果B(x,y) > 1:

a、找到与label = B(x,y)同属相等关系的一个最小label值,赋予给B(x,y)

完成扫描后,图像中具有相同label值的像素就组成了同一个连通区域


下面这张图动态地演示了Two-pass算法:





2)Seed Filling(种子填充法)

种子填充方法来源于计算机图形学,常用于对某个图形进行填充。思路:选取一个前景像素点作为种子,然后根据连通区域的两个基本条件(像素值相同、位置相邻)将与种子相邻的前景像素合并到同一个像素集合中,最后得到的该像素集合则为一个连通区域。

下面给出基于种子填充法的连通区域分析方法:

(1)扫描图像,直到当前像素点B(x,y) == 1:

a、将B(x,y)作为种子(像素位置),并赋予其一个label,然后将该种子相邻的所有前景像素都压入栈中;

b、弹出栈顶像素,赋予其相同的label,然后再将与该栈顶像素相邻的所有前景像素都压入栈中;

c、重复b步骤,直到栈为空;

此时,便找到了图像B中的一个连通区域,该区域内的像素值被标记为label;

(2)重复第(1)步,直到扫描结束;

扫描结束后,就可以得到图像B中所有的连通区域;

下面这张图动态地演示了Seed-Filling算法:



三、实验演示


1)前景二值图像


2)连通区域分析方法标记后得到的label图像


Two-pass算法:



Seed-filling算法:


注:为了显示方便,将像素值乘以了一个整数进行放大。


3)color后的label图像

Two-pass算法:


Seed-filling算法:



注:颜色是随机生成的


四、代码

1)Two-pass算法的一种实现

说明:

基于OpenCV和C++实现,领域:4-领域。实现与算法描述稍有差别(具体为记录具有相等关系的label方法实现上)。

  1. //  Connected Component Analysis/Labeling By Two-Pass Algorithm   
  2. //  Author:  www.icvpr.com    
  3. //  Blog  :  http://blog.csdn.net/icvpr   
  4. #include <iostream>  
  5. #include <string>  
  6. #include <list>  
  7. #include <vector>  
  8. #include <map>  
  9.   
  10. #include <opencv2/imgproc/imgproc.hpp>  
  11. #include <opencv2/highgui/highgui.hpp>  
  12.   
  13.   
  14. void icvprCcaByTwoPass(const cv::Mat& _binImg, cv::Mat& _lableImg)  
  15. {  
  16.     // connected component analysis (4-component)  
  17.     // use two-pass algorithm  
  18.     // 1. first pass: label each foreground pixel with a label  
  19.     // 2. second pass: visit each labeled pixel and merge neighbor labels  
  20.     //   
  21.     // foreground pixel: _binImg(x,y) = 1  
  22.     // background pixel: _binImg(x,y) = 0  
  23.   
  24.   
  25.     if (_binImg.empty() ||  
  26.         _binImg.type() != CV_8UC1)  
  27.     {  
  28.         return ;  
  29.     }  
  30.   
  31.     // 1. first pass  
  32.   
  33.     _lableImg.release() ;  
  34.     _binImg.convertTo(_lableImg, CV_32SC1) ;  
  35.   
  36.     int label = 1 ;  // start by 2  
  37.     std::vector<int> labelSet ;  
  38.     labelSet.push_back(0) ;   // background: 0  
  39.     labelSet.push_back(1) ;   // foreground: 1  
  40.   
  41.     int rows = _binImg.rows - 1 ;  
  42.     int cols = _binImg.cols - 1 ;  
  43.     for (int i = 1; i < rows; i++)  
  44.     {  
  45.         int* data_preRow = _lableImg.ptr<int>(i-1) ;  
  46.         int* data_curRow = _lableImg.ptr<int>(i) ;  
  47.         for (int j = 1; j < cols; j++)  
  48.         {  
  49.             if (data_curRow[j] == 1)  
  50.             {  
  51.                 std::vector<int> neighborLabels ;  
  52.                 neighborLabels.reserve(2) ;  
  53.                 int leftPixel = data_curRow[j-1] ;  
  54.                 int upPixel = data_preRow[j] ;  
  55.                 if ( leftPixel > 1)  
  56.                 {  
  57.                     neighborLabels.push_back(leftPixel) ;  
  58.                 }  
  59.                 if (upPixel > 1)  
  60.                 {  
  61.                     neighborLabels.push_back(upPixel) ;  
  62.                 }  
  63.   
  64.                 if (neighborLabels.empty())  
  65.                 {  
  66.                     labelSet.push_back(++label) ;  // assign to a new label  
  67.                     data_curRow[j] = label ;  
  68.                     labelSet[label] = label ;  
  69.                 }  
  70.                 else  
  71.                 {  
  72.                     std::sort(neighborLabels.begin(), neighborLabels.end()) ;  
  73.                     int smallestLabel = neighborLabels[0] ;    
  74.                     data_curRow[j] = smallestLabel ;  
  75.   
  76.                     // save equivalence  
  77.                     for (size_t k = 1; k < neighborLabels.size(); k++)  
  78.                     {  
  79.                         int tempLabel = neighborLabels[k] ;  
  80.                         int& oldSmallestLabel = labelSet[tempLabel] ;  
  81.                         if (oldSmallestLabel > smallestLabel)  
  82.                         {                             
  83.                             labelSet[oldSmallestLabel] = smallestLabel ;  
  84.                             oldSmallestLabel = smallestLabel ;  
  85.                         }                         
  86.                         else if (oldSmallestLabel < smallestLabel)  
  87.                         {  
  88.                             labelSet[smallestLabel] = oldSmallestLabel ;  
  89.                         }  
  90.                     }  
  91.                 }                 
  92.             }  
  93.         }  
  94.     }  
  95.   
  96.     // update equivalent labels  
  97.     // assigned with the smallest label in each equivalent label set  
  98.     for (size_t i = 2; i < labelSet.size(); i++)  
  99.     {  
  100.         int curLabel = labelSet[i] ;  
  101.         int preLabel = labelSet[curLabel] ;  
  102.         while (preLabel != curLabel)  
  103.         {  
  104.             curLabel = preLabel ;  
  105.             preLabel = labelSet[preLabel] ;  
  106.         }  
  107.         labelSet[i] = curLabel ;  
  108.     }  
  109.   
  110.   
  111.     // 2. second pass  
  112.     for (int i = 0; i < rows; i++)  
  113.     {  
  114.         int* data = _lableImg.ptr<int>(i) ;  
  115.         for (int j = 0; j < cols; j++)  
  116.         {  
  117.             int& pixelLabel = data[j] ;  
  118.             pixelLabel = labelSet[pixelLabel] ;   
  119.         }  
  120.     }  
  121. }


2)Seed-Filling种子填充方法

说明:

基于OpenCV和C++实现;领域:4-领域。

  1. //  Connected Component Analysis/Labeling By Seed-Filling Algorithm   
  2. //  Author:  www.icvpr.com    
  3. //  Blog  :  http://blog.csdn.net/icvpr   
  4. #include <iostream>  
  5. #include <string>  
  6. #include <list>  
  7. #include <vector>  
  8. #include <map>  
  9. #include <stack>  
  10.   
  11. #include <opencv2/imgproc/imgproc.hpp>  
  12. #include <opencv2/highgui/highgui.hpp>  
  13.   
  14.   
  15. void icvprCcaBySeedFill(const cv::Mat& _binImg, cv::Mat& _lableImg)  
  16. {  
  17.     // connected component analysis (4-component)  
  18.     // use seed filling algorithm  
  19.     // 1. begin with a foreground pixel and push its foreground neighbors into a stack;  
  20.     // 2. pop the top pixel on the stack and label it with the same label until the stack is empty  
  21.     //   
  22.     // foreground pixel: _binImg(x,y) = 1  
  23.     // background pixel: _binImg(x,y) = 0  
  24.   
  25.   
  26.     if (_binImg.empty() ||  
  27.         _binImg.type() != CV_8UC1)  
  28.     {  
  29.         return ;  
  30.     }  
  31.   
  32.     _lableImg.release() ;  
  33.     _binImg.convertTo(_lableImg, CV_32SC1) ;  
  34.   
  35.     int label = 1 ;  // start by 2  
  36.   
  37.     int rows = _binImg.rows - 1 ;  
  38.     int cols = _binImg.cols - 1 ;  
  39.     for (int i = 1; i < rows-1; i++)  
  40.     {  
  41.         int* data= _lableImg.ptr<int>(i) ;  
  42.         for (int j = 1; j < cols-1; j++)  
  43.         {  
  44.             if (data[j] == 1)  
  45.             {  
  46.                 std::stack<std::pair<int,int>> neighborPixels ;     
  47.                 neighborPixels.push(std::pair<int,int>(i,j)) ;     // pixel position: <i,j>  
  48.                 ++label ;  // begin with a new label  
  49.                 while (!neighborPixels.empty())  
  50.                 {  
  51.                     // get the top pixel on the stack and label it with the same label  
  52.                     std::pair<int,int> curPixel = neighborPixels.top() ;  
  53.                     int curX = curPixel.first ;  
  54.                     int curY = curPixel.second ;  
  55.                     _lableImg.at<int>(curX, curY) = label ;  
  56.   
  57.                     // pop the top pixel  
  58.                     neighborPixels.pop() ;  
  59.   
  60.                     // push the 4-neighbors (foreground pixels)  
  61.                     if (_lableImg.at<int>(curX, curY-1) == 1)  
  62.                     {// left pixel  
  63.                         neighborPixels.push(std::pair<int,int>(curX, curY-1)) ;  
  64.                     }  
  65.                     if (_lableImg.at<int>(curX, curY+1) == 1)  
  66.                     {// right pixel  
  67.                         neighborPixels.push(std::pair<int,int>(curX, curY+1)) ;  
  68.                     }  
  69.                     if (_lableImg.at<int>(curX-1, curY) == 1)  
  70.                     {// up pixel  
  71.                         neighborPixels.push(std::pair<int,int>(curX-1, curY)) ;  
  72.                     }  
  73.                     if (_lableImg.at<int>(curX+1, curY) == 1)  
  74.                     {// down pixel  
  75.                         neighborPixels.push(std::pair<int,int>(curX+1, curY)) ;  
  76.                     }  
  77.                 }         
  78.             }  
  79.         }  
  80.     }  
  81. }


3)颜色标记(用于显示)

  1. //  Connected Component Analysis/Labeling -- Color Labeling   
  2. //  Author:  www.icvpr.com    
  3. //  Blog  :  http://blog.csdn.net/icvpr   
  4. #include <iostream>  
  5. #include <string>  
  6. #include <list>  
  7. #include <vector>  
  8. #include <map>  
  9. #include <stack>  
  10.   
  11. #include <opencv2/imgproc/imgproc.hpp>  
  12. #include <opencv2/highgui/highgui.hpp>  
  13.   
  14. cv::Scalar icvprGetRandomColor()  
  15. {  
  16.     uchar r = 255 * (rand()/(1.0 + RAND_MAX));  
  17.     uchar g = 255 * (rand()/(1.0 + RAND_MAX));  
  18.     uchar b = 255 * (rand()/(1.0 + RAND_MAX));  
  19.     return cv::Scalar(b,g,r) ;  
  20. }  
  21.   
  22.   
  23. void icvprLabelColor(const cv::Mat& _labelImg, cv::Mat& _colorLabelImg)   
  24. {  
  25.     if (_labelImg.empty() ||  
  26.         _labelImg.type() != CV_32SC1)  
  27.     {  
  28.         return ;  
  29.     }  
  30.   
  31.     std::map<int, cv::Scalar> colors ;  
  32.   
  33.     int rows = _labelImg.rows ;  
  34.     int cols = _labelImg.cols ;  
  35.   
  36.     _colorLabelImg.release() ;  
  37.     _colorLabelImg.create(rows, cols, CV_8UC3) ;  
  38.     _colorLabelImg = cv::Scalar::all(0) ;  
  39.   
  40.     for (int i = 0; i < rows; i++)  
  41.     {  
  42.         const int* data_src = (int*)_labelImg.ptr<int>(i) ;  
  43.         uchar* data_dst = _colorLabelImg.ptr<uchar>(i) ;  
  44.         for (int j = 0; j < cols; j++)  
  45.         {  
  46.             int pixelValue = data_src[j] ;  
  47.             if (pixelValue > 1)  
  48.             {  
  49.                 if (colors.count(pixelValue) <= 0)  
  50.                 {  
  51.                     colors[pixelValue] = icvprGetRandomColor() ;  
  52.                 }  
  53.                 cv::Scalar color = colors[pixelValue] ;  
  54.                 *data_dst++   = color[0] ;  
  55.                 *data_dst++ = color[1] ;  
  56.                 *data_dst++ = color[2] ;  
  57.             }  
  58.             else  
  59.             {  
  60.                 data_dst++ ;  
  61.                 data_dst++ ;  
  62.                 data_dst++ ;  
  63.             }  
  64.         }  
  65.     }  
  66. }  


4)测试程序


  1. //  Connected Component Analysis/Labeling -- Test code  
  2. //  Author:  www.icvpr.com    
  3. //  Blog  :  http://blog.csdn.net/icvpr   
  4. #include <iostream>  
  5. #include <string>  
  6. #include <list>  
  7. #include <vector>  
  8. #include <map>  
  9. #include <stack>  
  10.   
  11. #include <opencv2/imgproc/imgproc.hpp>  
  12. #include <opencv2/highgui/highgui.hpp>  
  13.   
  14. int main(int argc, char** argv)  
  15. {  
  16.     cv::Mat binImage = cv::imread("../icvpr.com.jpg", 0) ;  
  17.     cv::threshold(binImage, binImage, 50, 1, CV_THRESH_BINARY_INV) ;  
  18.   
  19.     // connected component labeling  
  20.     cv::Mat labelImg ;  
  21.     icvprCcaByTwoPass(binImage, labelImg) ;  
  22.     //icvprCcaBySeedFill(binImage, labelImg) ;  
  23.   
  24.     // show result  
  25.     cv::Mat grayImg ;  
  26.     labelImg *= 10 ;  
  27.     labelImg.convertTo(grayImg, CV_8UC1) ;  
  28.     cv::imshow("labelImg", grayImg) ;  
  29.   
  30.     cv::Mat colorLabelImg ;  
  31.     icvprLabelColor(labelImg, colorLabelImg) ;  
  32.     cv::imshow("colorImg", colorLabelImg) ;  
  33.     cv::waitKey(0) ;  
  34.   
  35.     return 0 ;  
  36. }
2009-06-17 10:27:00 zhoubl668 阅读数 7239

二值图像是一种简单的图像格式,它只有两个灰度级,即"0"表示黑色的像素点,"255"表示白色的像素点,至于如何从一幅普通的图像获得二值图像,请参考我近期在天极网上发表的《Visual C++编程实现图像的分割》一文。二值图像处理在图像处理领域占据很重要的位置,在具体的图像处理应用系统中,往往需要对于获得的二值图像再进一步进行处理,以有利于后期的识别工作。二值图像处理运算是从数学形态学下的集合论方法发展起来的,尽管它的基本运算很简单,但是却可以产生复杂的效果。常用的二值图像处理操作有许多方法,如腐蚀、膨胀、细化、开运算和闭运算等等。本文对这些内容作些研究探讨, 希望对爱好图像处理的朋友有所帮助。

  一、腐蚀和膨胀

  形态学是一门新兴科学,它的用途主要是获取物体拓扑和结果信息,它通过物体和结构元素相互作用的某些运算,得到物体更本质的形态。它在图像处理中的应用主要是:

  1.利用形态学的基本运算,对图像进行观察和处理,从而达到改善图像质量的目的;

  2.描述和定义图像的各种几何参数和特征,如面积,周长,连通度,颗粒度,骨架和方向性。

  限于篇幅,我们只介绍简单二值图像的形态学运算,对于灰度图像的形态学运算,有兴趣的读者可以看有关的参考书。二值图像基本的形态学运算是腐蚀和膨胀,简单的腐蚀是消除物体的所有边界点的一种过程,其结果是使剩下的物体沿其周边比原物体小一个像素的面积。如果物体是圆的,它的直径在每次腐蚀后将减少两个像素,如果物体在某一点处任意方向上连通的像素小于三个,那么该物体经过一次腐蚀后将在该点处分裂为二个物体。简单的膨胀运算是将与某物体接触的所有背景点合并到该物体中的过程。过程的结果是使物体的面积增大了相应数量的点,如果物体是圆的,它的直径在每次膨胀后将增大两个像素。如果两个物体在某一点的任意方向相隔少于三个像素,它们将在该点连通起来。

  下面给出具体的实现腐蚀和膨胀的函数代码:

////////////////////////////////二值图像腐蚀操作函数
BOOL ImageErosion(BYTE *pData,int Width,int Height)
{//pData为图像数据的指针,Width和Height为图像的宽和高;
BYTE* pData1;
int m,n,i,j,sum,k,sum1;
BOOL bErosion;
if(pData==NULL)
{
AfxMessageBox("图像数据为空,请读取图像数据");
return FALSE;
}
//申请空间,pData1存放处理后的数据;
pData1=(BYTE*)new char[WIDTHBYTES(Width*8)*Height];
if(pData1==NULL)
{
AfxMessageBox("图像缓冲数据区申请失败,请重新申请图像数据缓冲区");
return FALSE ;
}
memcpy(pData1,pData,WIDTHBYTES(Width*8)*Height);
for(i=10;i<Height-10;i++)
for(j=32;j<Width-32;j++)

}
if(bErosion)

}
}
}
memcpy(pData,pData1,WIDTHBYTES(Width*8)*Height);
return TRUE;
}
////////////////////////////////////二值图像的膨胀操作
BOOL ImageDilation(BYTE *pData,int Width,int Height)
{
BYTE* pData1;
int m,n,i,j,sum,k,sum1;
BOOL bDilation;
if(pData==NULL)
{
AfxMessageBox("图像数据为空,请读取图像数据");
return FALSE;
}
//申请空间,pData1存放处理后的数据;
pData1=(BYTE*)new char[WIDTHBYTES(Width*8)*Height];
if(pData1==NULL)
{
AfxMessageBox("图像缓冲数据区申请失败,请重新申请图像数据缓冲区");
return FALSE ;
}
memcpy(pData1,pData,WIDTHBYTES(Width*8)*Height);
for(i=10;i<Height-10;i++)
for(j=32;j<Width-32;j++)

}
if(bDilation)

}
}
}
memcpy(pData,pData1,WIDTHBYTES(Width*8)*Height);
return TRUE;
}


  从上面的说明可以看出,腐蚀可以消除图像中小的噪声区域,膨胀可以填补物体中的空洞。对一个图像先进行腐蚀运算然后再膨胀的操作过程称为开运算,它可以消除细小的物体、在纤细点处分离物体、平滑较大物体的边界时不明显的改变其面积。如果对一个图像先膨胀然后再收缩,我们称之为闭运算,它具有填充物体内细小的空洞、连接邻近物体、在不明显改变物体面积的情况下平滑其边界的作用。通常情况下,当有噪声的图像用阈值二值化后,所得到的边界是很不平滑的,物体区域具有一些错判的孔洞,背景区域散布着一些小的噪声物体,连续的开和闭运算可以显著的改善这种情况,这时候需要在连接几次腐蚀迭代之后,再加上相同次数的膨胀,才可以产生所期望的效果。为了更好的显示出二值图像的处理效果,我们仍旧以图像采集卡获取的汽车图像为处理源图像,下图为处理后的效果:


(a)噪声图

(b)开运算处理

    图一 开运算效果图

  上图中,a图为包含噪声的图像,b图为经过腐蚀膨胀处理后的图像,可以看出,经过上述处理,成功的消除了图像中的噪声点,同时又起到了平滑边缘的

作用。
二、细化

  图像处理中物体的形状信息是十分重要的,为了便于描述和抽取图像特定区域的特征,对那些表示物体的区域通常需要采用细化算法处理,得到与原来物体区域形状近似的由简单的弧或曲线组成的图形,这些细线处于物体的中轴附近,这就是所谓的图像的细化。通俗的说图像细化就是从原来的图像中去掉一些点,但仍要保持目标区域的原来形状,通过细化操作可以将一个物体细化为一条单像素宽的线,从而图形化的显示出其拓补性质。实际上,图像细化就是保持原图的骨架。所谓骨架,可以理解为图象的中轴,例如一个长方形的骨架是它的长方向上的中轴线;正方形的骨架是它的中心点;圆的骨架是它的圆心,直线的骨架是它自身,孤立点的骨架也是自身。对于任意形状的区域,细化实质上是腐蚀操作的变体,细化过程中要根据每个像素点的八个相邻点的情况来判断该点是否可以剔除或保留。下面我们给几个例子来说明如何判断当前像素点是否该保留或剔除。

 
图二 根据某点的八个相邻点的情况来判断该点是否能删除

  上图给出了当前需要处理的像素点在不同的八邻域条件下的情况,可以看出:(1)不能删,因为它是个内部点,我们要求的是骨架,如果连内部点也删了,骨架也会被掏空的;(2)不能删,和(1)是同样的道理;(3)可以删,这样的点不是骨架;(4)不能删,因为删掉后,原来相连的部分断开了;(5)可以删,这样的点不是骨架;(6)不能删,因为它是直线的端点,如果这样的点删了,那么最后整个直线也被删了,剩不下什么;(7)不能删,因为孤立点的骨架就是它自身。 总结一下,有如下的判据:1.内部点不能删除;2.孤立点不能删除;3.直线端点不能删除;4.如果P是边界点,去掉P后,如果连通分量不增加,则P可以删除。我们可以根据上述的判据,事先做出一张表,从0到255共有256个元素,每个元素要么是0,要么是1。我们根据某点(当然是要处理的黑色点了)的八个相邻点的情况查表,若表中的元素是1,则表示该点可删,否则保留。查表的方法是,设白点为1,黑点为0;左上方点对应一个8位数的第一位(最低位),正上方点对应第二位,右上方点对应的第三位,左邻点对应第四位,右邻点对应第五位,左下方点对应第六位,正下方点对应第七位,右下方点对应的第八位,按这样组成的8位数去查表即可。例如上面的例子中(1)对应表中的第0项,该项应该为0;(2)对应37,该项应该为0;(3)对应173,该项应该为1;(4)对应231,该项应该为0;(5)对应237,该项应该为1;(6)对应254,该项应该为0;(7)对应255,该项应该为0。仔细考虑当前像素点的各种八邻域的情况,我们可以得到一个细化操作查找表,该表在下面的细化算法中详细介绍。

  为了避免分裂物体,细化的过程分为两个步骤,第一步是正常的腐蚀操作,但是它是有条件的,也就是说那些被标记的可除去的像素点并不立即消去;在第二步中,只将那些消除后并不破坏连通性的点消除,否则的话保留这些边界点。以上的步骤是在一个3x3邻域内运算,可以通过查表实现细化的操作。算法的实现步骤如下:

  1) 定义一个3x3模板和一个查找表,模板和查找表分别如图二和表一所示:

1 2 4
128 256 8
64 32 16

图二 细化模板

erasetable[256]={
0,0,1,1,0,0,1,1,1,1,0,1,1,1,0,1,1,1,0,0,1,1,1,1,
0,0,0,0,0,0,0,1,0,0,1,1,0,0,1,1,1,1,0,1,1,1,0,1,
1,1,0,0,1,1,1,1,0,0,0,0,0,0,0,1,1,1,0,0,1,1,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
1,1,0,0,1,1,0,0,1,1,0,1,1,1,0,1,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,1,1,0,0,1,1,1,1,0,1,1,1,0,1,
1,1,0,0,1,1,1,1,0,0,0,0,0,0,0,1,0,0,1,1,0,0,1,1,
1,1,0,1,1,1,0,1,1,1,0,0,1,1,1,1,0,0,0,0,0,0,0,0,
1,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,1,1,0,0,1,1,1,1,
0,0,0,0,0,0,0,0,1,1,0,0,1,1,0,0,1,1,0,1,1,1,0,0,
1,1,0,0,1,1,1,0,1,1,0,0,1,0,0,0
};
  
表一 细化查找表

  2)对二值图像从上到下、从左到右进行扫描;该过程结束后再对图像进行从左到右,从上到下的扫描;如果图像中当前像素点的灰度值为"0",且其左右(第一次扫描过程考虑左右像素点)或上下(第二次扫描过程考虑上下两个像素点)两个像素点中有任意一个为"255"则转至步骤三,否则回转到步骤二;

  3) 该像素点为中心的3x3区域内的各个像素值和定义的模板中的权值进行卷积求和,得到表4.1中的查找索引值k;

  4) 根据这个索引值k得到表里相应的数据,如果为"1",那么该像素点的灰度值设为"255",如果为"0",则该像素点的灰度值为"0"。

  5) 图像从头至尾扫描二遍后,如果该次扫描修改了图像中的点,则跳转至步骤二,开始新的一轮扫描。否则图像细化结束。
为了实现图像的细化算法,笔者定义了一个细化函数,具体实现代码如下:

BOOL SeneBorderThinning(BYTE *pData,int Width,int Height)
{ //pData为指向图像数据的指针,Width和Height为图像的宽度和高度;
int i,j;
int num;
//细化结束标志;
BOOL Finished;
//各个变量用来存储(i,j)位置的八邻域像素点的灰度;
int nw,n,ne,w,e,sw,s,se;
//细化表;
static int erasetable[256]=;
if(pData==NULL)
{
AfxMessageBox("图像数据为空,请读取图像数据");
return FALSE;
}
Finished=FALSE;
//开始细化;
while(!Finished)
{
Finished=TRUE;
//水平扫描;
for (i=10;i<Height-10;i++)
{
for(j=30;j<Width-30;j++)
{
if(*(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j)==0)
{
w=*(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j-1);
e=*(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j+1);
//判断(i,j)是否是边界点,如是,求该点的八邻域灰度值(0/255),根据各点的权重,计算对应查找表的索引;
if( (w==255)|| (e==255))
{
nw=*(pData+WIDTHBYTES(Width*8)*(Height-i)+j-1);
n=*(pData+WIDTHBYTES(Width*8)*(Height-i)+j);
ne=*(pData+WIDTHBYTES(Width*8)*(Height-i)+j+1);
sw=*(pData+WIDTHBYTES(Width*8)*(Height-i-2)+j-1);
s=*(pData+WIDTHBYTES(Width*8)*(Height-i-2)+j);
se=*(pData+WIDTHBYTES(Width*8)*(Height-i-2)+j+1); num=nw/255+n/255*2+ne/255*4+w/255*8+e/255*16+sw/255*32+s/255*64+se/255*128;
if(erasetable[num]==1)
{
//查表,如果符合条件,将边界点修改为图像的背景;
*(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j)=(BYTE)255;
Finished=FALSE;//再次进行扫描;
j++;
}
}
}
}
}
//垂直扫描;
for (j=30;j<Width-30;j++)
{
for(i=10;i<Height-10;i++)
{
if(*(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j)==0)
{
n=*(pData+WIDTHBYTES(Width*8)*(Height-i)+j);
s=*(pData+WIDTHBYTES(Width*8)*(Height-i-2)+j);
if( (n==255)|| (s==255))
{
nw=*(pData+WIDTHBYTES(Width*8)*(Height-i)+j-1);
ne=*(pData+WIDTHBYTES(Width*8)*(Height-i)+j+1);
w=*(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j-1);
e=*(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j+1);
sw=*(pData+WIDTHBYTES(Width*8)*(Height-i-2)+j-1);
se=*(pData+WIDTHBYTES(Width*8)*(Height-i-2)+j+1); num=nw/255+n/255*2+ne/255*4+w/255*8+e/255*16+sw/255*32+s/255*64+se/255*128;
if(erasetable[num]==1)
{
//查表,如果符合条件,将边界点修改为图像的背景;
*(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j)=(BYTE)255;
Finished=FALSE;//再次进行扫描;
i++;
}
}
}
}
}
}
return TRUE;
}

图三给出了细化处理后的效果图,其中a为原始图像,b为细化处理后的图像。


(a)

(b)
图三 细化效果图
三、图像的几何校正

  CCD获取的图像有时几何变形严重,细心的读者可以发现有时作为直线在获取的图像中已经类似为弧线,所以在图像处理系统中,经常要要对图像消除畸变。

  设原图像(未畸变图像)用( )坐标系,畸变图像坐标系为( ),两个坐标系之间的关系为:

   =h ( ) ; =h ( ) ;

  在消除摄像头几何畸变的已有工作中,可以将校正方法分为两类:一类是坐标转换函数h 、h 已知的情况下对图像进行校正,另一类是对这两个函数未知的情况下对图像进行校正。

  设 为待校正的畸形图像,g(x,y)为校正后所得到的图像,两个图像的坐标关系h ,h 是已知的情况下,这种校正方法就是根据这些函数确定与g(x,y)图像中点(x,y)相对应的( ),由对应点的灰度级设置g(x,y)。若点( )正好落在f中的数字化网格上,则用该网格的灰度值来确定g(x,y)的值;但是一般情况下,( )不一定是整数,既不一定落在数字化网格上,通常有两种方法解决这种问题:1)找到最接近( )的网格,由该网格的灰度值来确定g(x,y)的灰度;2)用( )周围四个相邻的网格点的灰度的加权内插来作为g(x,y)的值,特殊情况可以选取均值。这种方法对于图像的几何尺寸的缩小、放大和图像旋转的应用都能取得较好的效果。

  在不知道两个坐标关系的情况,对于大面积的图像来讲不一定是线性畸变,但是取一小块却可以近似认为是线性畸变。那么就可以将畸变系统和校正系统坐标用下列线性方程来联系:



  将畸变图像按具体的情况和经验分成若干小区,每个小区找三个点,利用已知图像和经验数据找到对应的点,根据这种关系确定两个坐标系之间的坐标关系,既上述式中的各个系数,然后在按已知坐标关系的情况校正图像。

  在实际图像处理系统中,我们得到的图像和校正图像的坐标转换关系是不可预知的,采用上述第二种情况的处理方法,牵涉到畸变图像的控制点的选取和确定校正后的控制点的位置,这样所需工作量就很大,为此,可以采用一种计算机自动识别控制点和确定控制点的坐标的方法。

  一般情况下,我们可以近似认为以CCD光轴为中心的一个小的圆形区域形成的图像没有畸变或畸变很小可以忽略不记,正是基于上述考虑,我们以CCD光轴在水平面上的投影点为中心确定一个正方形,在该正方形的四个顶点的位置作四个"十"字型,"十"字型的交点对应着四边形的端点,以这四个点来作为控制点,在图像中确定控制点后,根据考试场地的各边端点与控制点的坐标关系,确定校正后的边界位置。

  在上述方法实现校正几何畸变的过程中,最重要的是如何自动判别控制点,这里介绍采用模板匹配的方法识别"十"字型,针对"十"字交叉线会出现多种不规则的现象,共设计了四个模板,各个模板如图四所示:模板一用于识别图像内垂直的"十"字所包含的控制点。模板二用于识别横线上的控制点,其中a,b,c中有且只有一个"0";d,e,f中亦然。模版三用于识别竖线上的控制点,其中A,B,C中有且只有一个"0",D,E,F亦然。模板四用来搜索中心断裂的"十"字。

1 0 1
0 0 0
1 0 1
模板1

a b c
0 0 0
d e f
模板2


A 0 D
B 0 E
C 0 F
模板3

1 0 1
0 1 0
1 0 1
模板4

图四 搜索控制点用的模板

  限于篇幅的原因,关于搜索校正的实现代码就不再赘述了,笔者曾在近期的文章中介绍了图像平滑的处理,如果读者掌握了平滑的处理,那末对于采用模板匹配的实现应该不会再犯难,两者其实在编程实现上是大同小异的。