一、话说角点
三种基本的图像特征
- 边缘,对应下图黑框部分
- 角点,对应下图红框部分
- 团块,对应下图蓝框部分
下面介绍三种经典的角点检测的算法(前方公式高能,非战斗人员可直接跳到API和Demo部分)
- Harri算法
- Shi-Tomasi算法
- 亚像素级角点检测
二、Harri算法
Harri算法作为一种角点检测的经典算法,核心思想还是对像素进行梯度运算,总结角点处梯度的特征,前面讲到了XY角点的梯度特征,小林就以这个特征讲解Harri算法。
Harri算法有两种常见的表达式,我们先来看第一种。
- u和v:窗口偏移量
- x和y:窗口内像素坐标
- W(X,Y) :窗口函数,内含权重信息,常用的有权重为1和呈二元高斯正太分布的权重,意在突出像素值变化明显的程度
- 函数 I:像素密度函数,类比与像素值
现在来处理下这个公式,我们对后面平方项的求和式 使用泰勒公式展开:
现在看第二种表达式,OpenCV中对应的函数就使用了第二种表达式运算的:
- :矩阵行列式,
- :矩阵的迹,
- :Harri系数,一般取值为0.04~0.06
那么为什么要对矩阵M进行运算呢?从第一种表达式中可以看出,矩阵M包含着图像的梯度信息。因此我们来看下像素的梯度分布图:
- 团块部分(Flat)的梯度随呈各向异性,但梯度值非常小,因此集中在原点附近
- 边缘部分(Linear Edge)的梯度方向是确定的,集中在某一坐标轴附近
- 角点部分(Corner)的梯度的方向也是确定的,集中在两个坐标轴附近
再结合抽象后的图像:
Harri算法的流程:
- 求两个方向梯度,并计算出矩阵M
- 对矩阵M计算特征值、行列式和迹
- 根据特征值的关系并使用阈值确定图像特征
API:
dst=cv2.cornerHarris(src, blockSize, ksize, k[, dst[, borderType]])
- dst:Harri算法的输出矩阵(输出图像),CV_32FC1类型,与src有同样的尺寸
- src:输入图像,单通道,8位或浮点型
- blockSize:邻域大小
- ksize:Sobel算子的孔径大小
- k:Harri算法系数,即第二种表达式中的 K
三、Shi-Tomasi算法
Shi-Tomasi算法是J. Shi和C. Tomasi两位大神在1994年对Harri算法改进后得出的一种算法,并发表在了一篇叫Good Features To Track的论文中。
Shi-Tomasi算法相比Harri算法的不同点在于最后一步,即通过特征值确定图像特征。前文写到,Harri算法要判断特征值的相对关系,但毕竟是相对的,用起来很纠结。而Shi-Tomasi算法就专治各种纠结,我给一个阈值(下图中的 ),然后直接用阈值来界定特征值的关系,进而简单又粗暴的确定图像特征。下图中灰色为角点,橙色为边缘,绿色为团块。
数学表达式为:
API,没错,就是用这两个大神的论文名称作的函数名:
这个函数可以直接提供角点的坐标,而不像Harri的函数需要进一步操作才能获取角点坐标。
corners=cv2.goodFeaturesToTrack(image, maxCorners, qualityLevel, minDistance[, corners[, mask[, blockSize[, useHarrisDetector[, k]]]]])
- corners:输出角点向量
- image:输入图像,单通道,8位或32位浮点型
- maxCorners:输出角点的数量
- qualityLevel:算法可接受的最小特征值,实际用于角点检测的最小特征值是该参数与与图像最大特征值的乘积,所以该参数通常不会大于1,常用值为0.10或0.01。检测完角点后还要过滤掉距离较近的点
- minDistance:角点之间的最小距离,小于该距离的角点将被过滤掉
- mask:掩膜操作,用于对ROI参数进行处理
- blockSize:默认值为3,邻域大小
- useHarrisDetector:是否使用Harri角点检测
- k:默认值0.04,用于设置权重系数
四、亚像素角点检测
上述讲到的两种角点检测算法能较好的找到角点,但其实结果并不精确。当我们需要进行精确的角点计算时,就要用到亚像素角点检测(顾名思义,都亚像素了,能不精确一点吗)。亚像素角点检测在摄像机标定、跟踪并重建摄像机的轨迹或进行三维重建时有着重要作用。
那亚像素到底“亚”在哪儿了?通常我们计算出的坐标都是正整数,这就意味着我们是在对像素进行操作(注意,像素是图像处理的基本单位),而亚像素计算出来的坐标是实数,形象地说,就是把像素细分成若干像素(就像原子物理学里的夸克理论,夸克比原子还小),但由于最终还是要回归到像素级(整数级)操作,所以还要做数据类型的转换。
亚像素角点检测算法的思想是对正交向量的点乘进行迭代。我们来看下图:
我们假设点 是起始角点,并定义向量 。
- 左图中,点 在团块区域中,梯度为0。
- 右图中,点 在边缘上,所以点 的梯度方向垂直与边缘,向量 与点 的梯度向量的点乘积为0。
- 当点 固定时,若有足够多的向量 满足向量 与点 的梯度向量的点乘积为0,将每个向量看成一个方程,对方程组求解,则点 就是亚像素角点的精确位置。
API:
corners=cv2.cornerSubPix(image, corners, winSize, zeroZone, criteria)
- corners:输出角点向量,提供精确坐标
- image:输入图像
- winSize:搜索窗口的一半尺寸,例如,若该参数为 ,则搜索窗口尺寸为
- zeroZone:死区的一半尺寸,死区为不对搜索窗口中央区域作求和运算的区域,用于避免矩阵的奇异性, 表示没有死区
- criteria:求角点位置的迭代过程的结束条件,可以是最大的迭代次数,亦可是迭代结束所期望达到的精度,亦可是二者结合
criteria的定义格式
(type,maxCount,epsilon)
- type:终止迭代的方式,见下图,python中对应为cv2.TERM_CRITERIA_EPS、cv2.TERM_CRITERIA_ITER和cv2.TERM_CRITERIA_COUNT
- maxCount:最大迭代次数
- epsilon:期望迭代后达到的精度
Mat src = imread("D:\\cv_study\\随机练\\duola.jpg"); Mat gray,dst,norm,scal; cvtColor(src, gray, CV_BGR2GRAY); cornerHarris(gray, dst, 2, 3, 0.04, BORDER_DEFAULT); normalize(dst, norm, 0, 255, NORM_MINMAX, CV_32FC1); convertScaleAbs(norm, scal); for (int j = 0; j < norm.rows; j++) { for (int i = 0; i<norm.cols; i++) { if ((int)norm.at<float>(j, i)>80) { circle(scal, Point(i, j), 5, Scalar(0, 10, 255), 2, 8, 0); } } } imshow("1", scal); waitKey(); return 0;
Mat src = imread("D:\\cv_study\\随机练\\duola.jpg"); Mat gray,dst,norm,scal; cvtColor(src, gray, CV_BGR2GRAY); vector<Point2f> corners; double qualityLevel = 0.01; double minDistance = 10; int blockSize = 3; double k = 0.04; Mat copy = src.clone(); goodFeaturesToTrack(gray, corners, 33, qualityLevel, minDistance, Mat(), blockSize, false, k); int r = 4; for (unsigned int i = 0; i < corners.size(); i++) { circle(copy, corners[i], r, Scalar(0, 0, 255), -1, 8, 0); } imshow("1", copy); waitKey(); return 0;
Mat src = imread("D:\\cv_study\\随机练\\duola.jpg"); Mat gray,dst,norm,scal; cvtColor(src, gray, CV_BGR2GRAY); vector<Point2f> corners; double qualityLevel = 0.01; double minDistance = 10; int blockSize = 3; double k = 0.04; Mat copy = src.clone(); goodFeaturesToTrack(gray, corners, 33, qualityLevel, minDistance, Mat(), blockSize, false, k); Size winSize = Size(5, 5); Size zeroZone = Size(-1, -1); TermCriteria criteria = TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 40, 0.001); /// Calculate the refined corner locations cornerSubPix(gray, corners, winSize, zeroZone, criteria); /// Write them down for (int i = 0; i < corners.size(); i++) { circle(src, corners[i], 4, Scalar(0, 255, 0), 2, 8); } waitKey(); return 0;