今天要整理的笔记是关于图像的一种特征点提取方法——角点检测。

对于图像而言,它的特征主要是点和边缘。而角点可以说是一幅图像上最明显和最重要的特征,因为角点的存在意味着在图像该处有着非常明显的变化。

角点往往是两条边缘的交点,它是两条边缘方向变换的一种表示,因此其两个方向的梯度变换通常都比较大并且容易检测到。对于一阶导数而言,角点在各个方向上的变化都是最大的,而边缘区域则只是在某一方向有明显变化。

当我们提取了某一目标对象的角点特征后,就可以在图像中对该角点特征进行检测,也可以在视频中逐帧进行角点检测,判断某张图像中是否存在目标对象的角点特征,可用来实现基于角点检测的对象检测和对象跟踪。

OpenCV中提供了两种角点检测的方法,分别是基于Harris算法和基于shi-tomas算法实现的角点检测,对应了两个不同的API:cornerHarris()goodFeaturesToTrack()

先来整理Harris算法的相关内容。
Harris角点检测算法,主要思路是寻找在x、y两个方向上的梯度都有明显变化的点,当某点在x和y方向上的二次导数值都比较大时,则该点可以认为是特征点,也就是角点。

具体的实现方式是对图像进行sobel梯度计算求得每个像素点的梯度,再根据每个像素点在其邻域内的所有梯度来计算获得一个2x2的梯度协方差矩阵,最后根据这个梯度协方差矩阵来计算每个像素点的角点响应,计算角点响应的过程可以用公式表示,该公式主要是用两个特征值lamda1、lamda2和系数k来计算,并得到一个浮点型的角点响应值。

当两个特征值lamda1、lamda2都比较大时,计算得到的角点响应值也会比较大,证明该区域存在角点;当两个特征值一个较大一个较小时,表示在该区域存在边缘;当两个特征值都比较小时,表明该区域不存在角点或边缘,属于平坦区域。

OpenCV中关于Harris算法的角点检测API是cornerHarris(),其参数含义如下:
第一个参数src:输入图像,必须是8位的单通道图像;
第二个参数dst:尺寸和输入图像相同,类型为CV_32FC1的输出Mat对象,其中每个元素为输入图像中每一个像素点的角点响应值;
第三个参数blockSize:计算获得梯度协方差矩阵时的邻域大小;
第四个参数ksize:使用sobel算子进行计算梯度时的窗口大小;
第五个参数k:用来计算角点响应的系数,取值范围一般为0.04~0.06;
第六个参数borderTpye:边界填充方式,使用默认值即可。

具体代码演示如下:

Mat test_image = imread("D:\\opencv_c++\\opencv_tutorial\\data\\images\\gem_test.png");
	int height = test_image.rows;
	int width = test_image.cols;
	Mat gray_image, binary_image;
	cvtColor(test_image, gray_image, COLOR_BGR2GRAY);
	threshold(gray_image, binary_image, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);
	Mat corners_respon;
	cornerHarris(binary_image, corners_respon, 3, 3, 0.04, 4);
	normalize(corners_respon, corners_respon, 0, 255, NORM_MINMAX);		//将得到的角点响应值归一化到 [ 0 , 255 ]
	convertScaleAbs(corners_respon, corners_respon);			
	//获取角点响应的最大值
	double maxVal, minVal;
	Point maxIdx, minIdx;
	minMaxLoc(corners_respon, &minVal, &maxVal, &minIdx, &maxIdx);
	int thresh = maxVal * 0.4;				//设置阈值
	for (int row = 0; row < height; row++)
	{
		for (int col = 0; col < width; col++)
		{
			int respon = corners_respon.at<uchar>(row, col);
			if (respon > thresh)				//如果该点的角点响应值大于阈值,则视为角点
			{
				circle(test_image, Point(col, row), 4, Scalar(0, 255, 0), 1, 8, 0);
			}
		}
	}
	imshow("test_image", test_image);

上述代码中进行Harris角点检测的步骤如下:
先对输入图像进行转灰度图、二值分割等预处理,得到一张二值图像,将该二值图像使用cornerHarris()进行角点检测,得到一个和输入图像尺寸相同的存放角点响应值的矩阵,并对该矩阵中的元素进行归一化到 [ 0, 255 ] 范围内,并将浮点型数据转换为8位字节型数据。

然后获取最大的角点响应值,以0.4倍的最大角点响应值为阈值,遍历矩阵中每个角点响应值进行判断,如果大于阈值则该坐标点为角点,并绘制出一个小圆圈进行标记;如果小于阈值则跳过该坐标点。

基于Harris角点检测的效果如下:

opencv 获取顶点 opencv找角点_c++


可见,我们将那些多边形的拐角处都认为是角点特征给标记了出来,而圆形不存在这样的角点。所以这样的角点就可以作为某一个多边形的一种特征了。

但是基于Harris算法的角点检测,其检测速度比较慢,难以应用到实时处理中去。而且没有对输出的角点响应值进行排序,需要手动设置阈值来判断某个角点响应值是否达到角点的标准。这就限制了这种算法的应用范围,所以OpenCV中又提供了另一种基于shi-tomas算法实现的角点检测。

shi-tomas算法实际上是Hrris算法的改进,同样对图像进行sobel梯度计算求得每个像素点的梯度,再根据每个像素点在其邻域内的所有梯度来计算获得一个2x2的梯度协方差矩阵,最后根据这个梯度协方差矩阵来计算每个像素点的角点响应,计算角点响应的过程也用两个特征值lamda1、lamda2和系数k来计算,但当得到了两个特征值lamda1和lamda2之后,只要其中较小的特征值比阈值还要大,也就是min(lamda1,lamda2)> thresh,则证明在该区域存在角点,这样的话运行速度相比Harris算法就会更快。
shi-tomas算法在性能方面的提高,也就拓宽了它在角点检测方面的应用范围。

OpenCV中基于shi-tomas算法实现角点检测的API是goodFeaturesToTrack(),从这个函数名也可以看得出这个API的性能和效果都是比较好的把。其参数含义如下:
第一个参数image:输入的8位单通道图像;
第二个参数corners:检测到的角点点集;
第三个参数maxCorners:输出的最大角点数;
第四个参数qualityLevel:输出角点的质量等级,取值范围是 [ 0 , 1 ];如果某个候选点的角点响应值小于(qualityLeve * 最大角点响应值),则该点会被抛弃,相当于判定某候选点为角点的阈值;
第五个参数minDistance:两个角点间的最小距离,如果某两个角点间的距离小于minDistance,则会被认为是同一个角点;
第六个参数mask:如果有该掩膜,则只计算掩膜内的角点;
第七个参数blockSize:计算角点响应值的邻域大小,默认值为3;如果输入图像的分辨率比较大,可以选择比较大的blockSize;
第八个参数useHarrisDector:布尔类型,如果为true则使用Harris角点检测;默认为false,使用shi-tomas角点检测算法;
第九个参数k:只在使用Harris角点检测时才生效,也就是计算角点响应值时的系数k。

具体代码演示如下:

Mat test_image = imread("D:\\opencv_c++\\opencv_tutorial\\data\\images\\gem_test.png");
	int height = test_image.rows;
	int width = test_image.cols;
	Mat gray_image, binary_image;
	cvtColor(test_image, gray_image, COLOR_BGR2GRAY);
	threshold(gray_image, binary_image, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);
	//shi-tomas实际上得到的是整数坐标点,这里使用Point2f是为了便于后续将角点检测精度提高到亚像素级别
	vector<Point2f> corners;				
	goodFeaturesToTrack(binary_image, corners, 50, 0.25, 5, Mat(), 3, false, 0.4);
	for (int i = 0; i < corners.size(); i++)
	{
		circle(test_image, corners[i], 4, Scalar(0, 255, 0), 1, 8, 0);
	}
	imshow("test_image", test_image);

从代码上可以看出,基于shi-tomas算法实现的角点检测相比起基于harris算法实现的,要更加简洁。这是因为goodFeaturesToTrack()这个API返回的不再是角点响应值,而是直接返回符合要求的角点点集,我们可以直接将点集中的角点进行绘制标记,这就要方便得多。而且shi-tomas检测API中同时还包含了Harris检测功能。

要注意的是,Harris角点检测和shi-tomas角点检测只能检测像素级别的角点,获取的角点坐标是整数类型,但是通常情况下,角点的真实位置并不一定在整数像素位置,因此为了获取更为精确的角点位置坐标,我们需要使用亚像素(subPixel)精度的角点检测算法。

要将角点检测从像素精度级别提升到亚像素精度级别,首先需要使用shi-tomas算法检测出整数坐标的角点集,再使用亚像素角点检测算法对同一幅图像及其整数坐标角点集进行处理,将检测到的角点集提高到亚像素精度。所以goodFeaturesToTrack()cornerSubPix()需要搭配使用。

其中,cornerSubPix()这个API就是OpenCV中提供的实现亚像素精度角点检测的API,其参数含义如下:
第一个参数image:输入图像与goodFeaturesToTrack()中的输入图像是同一个图像;
第二个参数corners:输入和输出的检测角点集,输入的是整型坐标点集,输出的是浮点坐标点集;在goodFeaturesToTrack()之前要使用Point2f来定义角点集;
第三个参数winSize:检测亚像素角点时计算区域的大小,尺寸为 ( winSize * 2 + 1 ,winSize * 2 + 1 )
第四个参数zeroZone:表示计算区域中忽略掉的区域,有时用于避免自相关矩阵的奇异性;通常不使用,即设置默认值(-1,-1);
第五个参数criteria:表示计算亚像素时停止迭代的标准,可选的值有 TermCriteria::MAX_ITERTermCriteria::EPS,第一个表示迭代次数达到了最大次数时停止,第二个表示角点位置变化值达到最小时停止迭代,可以两者均选。
这二个方法均使用TermCriteria()构造函数进行指定:
第一个参数type:迭代类型,可以两种都选择 TermCriteria::EPS + TermCriteria::MAX_ITER
第二个参数maxCount:最大迭代次数;
第三个参数epsilon:角点位置变化最小值。

具体代码演示如下:

Mat subPixel_test_image = test_image.clone();
	Size winSize(5, 5);
	Size zerozone(-1, -1);
	TermCriteria criteria = TermCriteria(TermCriteria::EPS + TermCriteria::MAX_ITER, 40, 0.001);
	cornerSubPix(binary_image, corners, winSize, zerozone, criteria);
	for (int i = 0; i < corners.size(); i++)
	{
		circle(test_image, corners[i], 4, Scalar(0, 255, 0), 1, 8, 0);
	}
	imshow("subPixel_test_image", subPixel_test_image);

下面看一下像素级别和亚像素级别的角点检测对比图:

opencv 获取顶点 opencv找角点_计算机视觉_02


上图中,左边是shi-tomas算法检测出的角点,右边是亚像素检测算法检测出的角点,看起来感觉差距不大,但实际上亚像素级别肯定会更精准些,放大来看说不定能看到有些许位置偏移。

好了,本次关于角点检测的笔记就整理到此啦,下次要整理记录的是基于角点检测实现的视频光流跟踪,有空再来写博客~