第七节 提取直线、轮廓和区域
- [1.用Canny 算子检测图像轮廓](#1.用Canny 算子检测图像轮廓)
- 2.用霍夫变换检测直线;
- 3.点集的直线拟合
- 4.提取连续区域
- 5.计算区域的形状描述子
图像的边缘区域勾画出了图像含有重要的视觉信息。正因如此,边缘可应用于目标识别等领域。但是简单的二值边缘分布图有两个主要缺点:
- 检测到的边缘过厚,这加大了识别物体边界的难度
- 也是更重要的,通常不可能找到既低到足以检测到图像中所有重要边缘,又高到足以避免产生太多无关紧要边缘的阈值
1.用Canny 算子检测图像轮廓
canny算子在低阈值边缘分布图上只保留具有连续路径的边缘点,同时把那些边缘点连接到属于高阈值边缘分布图的边缘上。高阈值分布图上的所有边缘点都被保留下来,而低阈值分布图上边缘点的孤立链全部被移除,只要指定适当的阈值,就能获得高质量的轮廓。这种基于两个阈值获得二值分布图的策略被称为滞后阈值化,可用于任何需要用阈值化获得二值分布图的场景,计算复杂度比较高。
另外,Canny 算法用了一个额外的策略来优化边缘分布图的质量。在进行滞后阈值化之前,如果梯度幅值不是梯度方向上的最大值,那么对应的边缘点都会被移除(前面讲过,梯度的方向总是与边缘垂直的)。因此,这个方向上梯度的局部最大值对应着轮廓最大强度的位置。这是一个细化轮廓的运算,它创建的轮廓宽度只有一个像素,这也解释了为什么Canny 轮廓分布图的边缘比较薄。
cpp
// 应用Canny 算法
cv::Mat contours;
cv::Canny(image, // 灰度图像
contours, // 输出轮廓
125, // 低阈值
350); // 高阈值

2.用霍夫变换检测直线;
霍夫变换(Hough transform)是一种常用于检测此类具体特征的经典算法。该算法起初用于检测图像中的直线,后来经过扩展,也能检测其他简单的图像结构。
参数ρ 是直线与图像原点(左上角)的距离,θ 是直线与垂直线间的角度。在这种表示法中,图像中的直线有一个0~π(弧度)的角θ,而半径ρ 的最大值是图像对角线的长度。例如下面的一组线:
cpp
// 应用Canny 算法
cv::Mat contours;
cv::Canny(image,contours,125,350);
// 用霍夫变换检测直线
std::vector<cv::Vec2f> lines;
cv::HoughLines(contours,lines, 1,
PI/180, // 步长
60); // 最小投票数
半径步长为1,表示函数将搜索所有可能的半径;角度步长为π/180,表示函数将搜索所有可能的角度对于垂直方向的直线,计算它与图像水平边界(即第一行和最后一行)的交叉点,然后在这两个交叉点之间画线。水平方向的直线也类似,只不过用第一列和最后一列。画线的函数是cv::line。需要注意的是,即使点的坐标超出了图像范围,这个函数也能正确运行,因此没必要检查交叉点是否在图像内部。通过遍历直线向量画出所有直线:
霍夫变换只是寻找图像中边缘像素的对齐区域。因为有些像素只是碰巧排成了直线,所以霍夫变换可能产生错误的检测结果。也可能因为多条参数相近的直线穿过了同一个像素对齐区域,而导致检测出重复的结果。为解决上述问题并检测到线段(即包含端点的直线),人们提出了霍夫变换的改进版。这就是概率霍夫变换,在OpenCV 中通过cv::HoughLinesP 函数实现。
首先,概率霍夫变换在二值分布图上随机选择像素点,而不是系统性地逐行扫描图像。一旦累加器的某个入口达到了预设的最小值,就沿着对应的直线扫描图像,并移除在这条直线上的所有像素点(包括还没投票的像素点)。这个扫描过程还检测可以接受的线段长度。为此,算法定义了两个额外的参数:一个是允许的线段最小长度,另一个是组成连续线段时允许的最大像素间距
cpp
cv::HoughLinesP(binary,lines,
deltaRho, deltaTheta, minVote,
minLength, maxGap);

霍夫变换也能用来检测其他几何物体。事实上,任何可以用一个参数方程来表示的物体,都很适合用霍夫变换来检测。还有一种泛化霍夫变换,可以检测任何形状的物体。
OpenCV 采用的策略是在用霍夫变换检测圆的实现中使用两轮筛选。第一轮筛选使用一个二维累加器,找出可能是圆的位置。因为圆周上像素点的梯度方向与半径的方向是一致的,所以对每个像素点来说,累加器只对沿着梯度方向的入口增加计数(根据预先定义的最小和最大半径值)。一旦检测到可能的圆心(即收到了预定数量的投票),就在第二轮筛选中建立半径值范围的一维直方图。这个直方图的尖峰值就是被检测圆的半径。
cpp
cv::GaussianBlur(image,image,cv::Size(5,5),1.5);
std::vector<cv::Vec3f> circles;
cv::HoughCircles(image, circles, cv::HOUGH_GRADIENT,
2, // 累加器分辨率(图像尺寸/2)
50, // 两个圆之间的最小距离
200, // Canny 算子的高阈值
100, // 最少投票数
25,
100); // 最小和最大半径

3.点集的直线拟合
在某些应用程序中,光是检测出图像中的直线还不够,还需要精确地估计直线的位置和方向。
点集的直线拟合是一个经典数学问题。OpenCV 的实现方法是使每个点到直线的距离之和最小化。在众多用于计算距离的函数中,欧几里得距离的计算速度最快,所用参数为cv::DIST_L2。这一选项对应了标准的最小二乘法直线拟合。如果点集中包含了孤立点(即不属于直线的点),可以选用其他距离函数,以减少远距离的点带来的影响。最小化计算的基础是M 估算法技术,它采用迭代方式解决加权最小二乘法问题,其中权重与点到直线的距离成反比。
cpp
cv::Vec4f line;
cv::fitLine(points,line,
cv::DIST_L2, // 距离类型
0, // L2 距离不用这个参数
0.01,0.01); // 精度

4.提取连续区域
图像通常包含各种物体,图像分析的目的之一就是识别和提取这些物体。在物体检测和识别程序中,第一步通常就是生成二值图像,找到感兴趣物体所处的位置。不管用什么方式获得二值图像(例如用第4 章的直方图反向投影,或者用第12 章的运动分析),下一个步骤都是从由1 和0 组成的像素集合中提取出物体。
cpp
// 用于存储轮廓的向量
std::vector<std::vector<cv::Point>> contours;
cv::findContours(image,
contours, // 存储轮廓的向量
cv::RETR_EXTERNAL, // 检索外部轮廓
cv::CHAIN_APPROX_NONE); // 每个轮廓的全部像素
// 在白色图像上画黑色轮廓
cv::Mat result(image.size(),CV_8U,cv::Scalar(255));
cv::drawContours(result,contours,
-1, // 画全部轮廓
0, // 用黑色画
2); // 宽度为2
5.计算区域的形状描述子
连续区域通常代表着场景中的某个物体。为了识别该物体,或将它与其他图像元素做比较,需要对此区域进行测量,以提取出部分特征
cpp
第一个是边界框,用于右下角的区域:
// 测试边界框
cv::Rect r0= cv::boundingRect(contours[0]);
// 画矩形
cv::rectangle(result,r0, 0, 2)
最小覆盖圆的情况也类似,将它用于右上角的区域:
// 测试覆盖圆
float radius;
cv::Point2f center;
cv::minEnclosingCircle(contours[1],center,radius);
// 画圆形
cv::circle(result,center, static_cast<int>(radius),
cv::Scalar(0),2);
计算区域轮廓的多边形逼近的代码如下(位于左侧区域):
// 测试多边形逼近
std::vector<cv::Point> poly;
cv::approxPolyDP(contours[2],poly,5,true);
// 画多边形
cv::polylines(result, poly, true, 0, 2);
注意,多边形绘制函数cv::polylines 与其他画图函数很相似。第三个布尔型参数表示该
轮廓是否闭合(如果闭合,最后一个点将与第一个点相连)。
凸包是另一种形式的多边形逼近(位于左侧第二个区域):
// 测试凸包
std::vector<cv::Point> hull;
cv::convexHull(contours[3],hull);
// 画多边形
cv::polylines(result, hull, true, 0, 2);
最后,计算轮廓矩是另一种功能强大的描述子(在所有区域内部画出重心):
// 测试轮廓矩
// 迭代遍历所有轮廓
itc= contours.begin();
while (itc!=contours.end()) {
// 计算所有轮廓矩
cv::Moments mom= cv::moments(cv::Mat(*itc++));
// 画重心
cv::circle(result,
// 将重心位置转换成整数
cv::Point(mom.m10/mom.m00,mom.m01/mom.m00),
2, cv::Scalar(0),2); // 画黑点
}
