在现实世界中,角点对应于物体的拐角,道路的十字路口、丁字路口等。从图像分析的角度来定义角点可以有以下两种定义:
- 角点可以是两个边缘的角点;
- 角点是邻域内具有两个主方向的特征点;
一提到角点检测,最常用的方法莫过于Harris角点检测,opencv中也提供了Harris角点检测的接口,即cornerHarris(),但是Harris角点检测存在很多缺陷(如角点是像素级别的,速度较慢等),opencv中有另一个功能更为强大的函数—goodFeaturesToTrack(),它不仅支持Harris角点检测,也支持Shi Tomasi算法的角点检测。但是,该函数检测到的角点依然是像素级别的,若想获取更为精细的角点坐标,则需要调用cv::cornerSubPix()函数进一步细化处理,即亚像素。
Harris角点
基本原理
人眼对角点的识别通常是在一个局部的小区域或小窗口完成的。如果在各个方向上移动这个特征的小窗口,窗口内区域的灰度发生了较大的变化,那么就认为在窗口内遇到了角点。如果这个特定的窗口在图像各个方向上移动时,窗口内图像的灰度没有发生变化,那么窗口内就不存在角点;如果窗口在某一个方向移动时,窗口内图像的灰度发生了较大的变化,而在另一些方向上没有发生变化,那么,窗口内的图像可能就是一条直线的线段。
Harris角点算法实现
1增大阈值,检测到的角点会减少;
2增大α的值,将减小角点响应值R,降低角点检测的灵性,减少被检测角点的数量;减小α值,将增大角点响应值R,增加角点检测的灵敏性,增加被检测角点的数量。
3同时邻域的大小也会影响角点的检测。
Harris的OpenCV接口
cornerHarris 函数用于在OpenCV中运行Harris角点检测算子处理图像。和cornerMinEigenVal( )以及cornerEigenValsAndVecs( )函数类似,cornerHarris 函数对于每一个像素(x,y)在blockSize*blockSize邻域内,计算2x2梯度的协方差矩阵M(x,y),接着它计算如下式子:
void cornerHarris( InputArray src, OutputArray dst, int blockSize,
int ksize, double k,
int borderType=BORDER_DEFAULT );
- src – 输入的单通道8-bit或浮点图像。
- dst – 存储着Harris角点响应的图像矩阵,大小与输入图像大小相同,是一个浮点型矩阵。
- blockSize – 邻域大小。
- apertureSize – 扩展的微分算子大。
- k – 响应公式中的,参数αα。
- boderType – 边界处理的类型
该接口很少使用,一般我们使用goodFeaturesToTrack函数:
void cv::goodFeaturesToTrack(
cv::InputArray image, // 输入图像(CV_8UC1 CV_32FC1)
cv::OutputArray corners, // 输出角点vector
int maxCorners, // 最大角点数目
double qualityLevel, // 质量水平系数(小于1.0的正数,一般在0.01-0.1之间)
double minDistance, // 最小距离,小于此距离的点忽略
cv::InputArray mask = noArray(), // mask=0的点忽略
int blockSize = 3, // 使用的邻域数
bool useHarrisDetector = false, // false ='Shi Tomasi metric'
double k = 0.04 // Harris角点检测时使用
);
第一个参数是输入图像(8位或32位单通道图)。
第二个参数是检测到的所有角点,类型为vector或数组,由实际给定的参数类型而定。如果是vector,那么它应该是一个包含cv::Point2f的vector对象;如果类型是cv::Mat,那么它的每一行对应一个角点,点的x、y位置分别是两列。
第三个参数用于限定检测到的点数的最大值。
第四个参数表示检测到的角点的质量水平(通常是0.10到0.01之间的数值,不能大于1.0)。
第五个参数用于区分相邻两个角点的最小距离(小于这个距离得点将进行合并)。
第六个参数是mask,如果指定,它的维度必须和输入图像一致,且在mask值为0处不进行角点检测。
第七个参数是blockSize,表示在计算角点时参与运算的区域大小,常用值为3,但是如果图像的分辨率较高则可以考虑使用较大一点的值。
第八个参数用于指定角点检测的方法,如果是true则使用Harris角点检测,false则使用Shi Tomasi算法。
第九个参数是在使用Harris算法时使用,最好使用默认值0.04。
上面我们提到Shi Tomasi算法,与Harris角点检测的区别主要是阈值标准不一样。Harris角点采用det(M)-α*trace(M)^2;而Shi Tomasi算法采用min(λ2,λ1),与阈值进行比较。
亚像素角点检测
前面已经提及goodFeaturesToTrack()提取到的角点只能达到像素级别,获取的角点坐标是整数,但是通常情况下,角点的真实位置并不一定在整数像素位置,因此为了获取更为精确的角点位置坐标,需要角点坐标达到亚像素(subPixel)精度。这时,我们则需要使用cv::cornerSubPix()对检测到的角点作进一步的优化计算,可使角点的精度达到亚像素级别。举例说明,已知得到角点坐标为(20,30),经过亚像素检测,得到新的坐标(19.329,30.512),使用亚像素角点检测后角点的精细度确实得到了显著的提升。
原理解析
在亚像素级精度的角点检测算法中,一种方法是从亚像素角点到周围像素点的矢量应垂直于图像的灰度梯度这个观察事实得到的,通过最小化误差函数的迭代方法来获得亚像素级精度的坐标值。
如上图所示,假设一个起始角点q在实际亚像素角点附近。p点在q点附近的邻域中,若p点在均匀区域内部,则p点的梯度为0;若p点在边缘上,则p点的梯度方向垂直边缘方向。如果向量q-p方向与边缘方向一致,那么q-p向量与p点的梯度向量点积运算结果为0。在初始角点(初始角点可能不在边缘上)附近我们可以收集很多组点的梯度以及相关向量q-p,此时的q就是我们所要求的更精确角点位置,那么每一组的向量点积设置为0,正是基于这个思想,将点积为0的等式组合起来形成一个系统方程,该系统方程的解就是更精确的亚像素角点位置。 将新的q点作为区域的中心,可以继续使用这个方法进行迭代,获得很高的精度。
亚像素角点的求法
1、求出角点的下一步往往需要求亚像素点。即,从一个整数坐标,求出一 个小数坐标。从科学上来讲,精度提高了。——“精确到了小数点后 X 位”。
2 解答
2.1 如何从整数算出小数? 图像本来都是像素点,用整数来表达坐标最自然。为什么会有小数坐标 呢?这其实是引入数学手段,进行计算的结果。那是什么数学方法呢?最小 二乘法。
2.2 如何构造方程 最小二乘法需要得到 Xβ = y,才有方程可解。在亚像素角点的求解中, 列方程用到了“垂直向量,乘积为 0”这一性质。 那是哪两个向量相乘呢?看图:
函数原型如下:
void cv::cornerSubPix(
cv::InputArray image, // 输入图像
cv::InputOutputArray corners, // 角点(既作为输入也作为输出)
cv::Size winSize, // 区域大小为 NXN; N=(winSize*2+1)
cv::Size zeroZone, // 类似于winSize,但是总具有较小的范围,Size(-1,-1)表示忽略
cv::TermCriteria criteria // 停止优化的标准
);
第一个参数是输入图像,和cv::goodFeaturesToTrack()中的输入图像是同一个图像。
第二个参数是检测到的角点,即是输入也是输出。
第三个参数是计算亚像素角点时考虑的区域的大小,大小为NXN; N=(winSize*2+1)。
第四个参数作用类似于winSize,但是总是具有较小的范围,通常忽略(即Size(-1, -1))。
第五个参数用于表示计算亚像素时停止迭代的标准,可选的值有cv::TermCriteria::MAX_ITER 、cv::TermCriteria::EPS(可以是两者其一,或两者均选),前者表示迭代次数达到了最大次数时停止,后者表示角点位置变化的最小值已经达到最小时停止迭代。二者均使用cv::TermCriteria()构造函数进行指定。
实例:
cv::Mat image_color = cv::imread("image.jpg", cv::IMREAD_COLOR);
//用于绘制亚像素角点
cv::Mat image_copy = image_color.clone();
//使用灰度图像进行角点检测
cv::Mat image_gray;
cv::cvtColor(image_color, image_gray, cv::COLOR_BGR2GRAY);
//设置角点检测参数
std::vector<cv::Point2f> corners;
int max_corners = 100;
double quality_level = 0.01;
double min_distance = 10;
int block_size = 3;
double k = 0.04;
//角点检测
cv::goodFeaturesToTrack(image_gray,
corners,
max_corners,
quality_level,
min_distance,
cv::Mat(),
block_size,
false,
k);
//将检测到的角点绘制到原图上
for (int i = 0; i < corners.size(); i++)
{
cv::circle(image_color, corners[i], 5, cv::Scalar(0, 0, 255), 2, 8, 0);
}
//指定亚像素计算迭代标注
cv::TermCriteria criteria = cv::TermCriteria(
cv::TermCriteria::MAX_ITER + cv::TermCriteria::EPS,
40,
0.01);
//亚像素检测
cv::cornerSubPix(image_gray, corners, cv::Size(5, 5), cv::Size(-1, -1), criteria);
//将检测到的亚像素角点绘制到原图上
for (int i = 0; i < corners.size(); i++)
{
cv::circle(image_copy, corners[i], 5, cv::Scalar(0, 255, 0), 2, 8, 0);
}
cv::imshow("corner", image_color);
cv::imshow("sub pixel corner", image_copy);
cv::imwrite("corner.jpg", image_color);
cv::imwrite("corner_sub.jpg", image_copy);
cv::waitKey(0);
return;