OpenCV——Harris、Shi Tomas、自定义、亚像素角点检测
在图像处理和与计算机视觉领域,兴趣点(interest points),或称作关键点(keypoints)、特征点(feature points) 被大量用于解决物体识别,图像识别、图像匹配、视觉跟踪、三维重建等一系列的问题。我们不再观察整幅图,而是选择某些特殊的点,然后对他们进行局部有的放矢的分析。如果能检测到足够多的这种点,同时他们的区分度很高,并且可以精确定位稳定的特征,那么这个方法就有使用价值。
图像特征类型可以被分为如下三种:
- <1>边缘
- <2>角点 (感兴趣关键点)
- <3>斑点(Blobs)(感兴趣区域)
其中,角点是个很特殊的存在。他们在图像中可以轻易地定位,同时,他们在人造物体场景,比如门、窗、桌等出随处可见。因为角点位于两条边缘的交点处,代表了两个边缘变化的方向上的点,,所以他们是可以精确定位的二维特征,甚至可以达到亚像素的精度。且其图像梯度有很高的变化,这种变化是可以用来帮助检测角点的。需要注意的是,角点与位于相同强度区域上的点不同,与物体轮廓上的点也不同,因为轮廓点难以在相同的其他物体上精确定位。
为什么角点是特殊的?
- 因为角点是两个边缘的连接点,它代表了两个边缘变化的方向上的点。图像梯度有很高的变化。这种变化是可以用来帮助检测角点的。
在当前的图像处理领域,角点检测算法可归纳为三类:
<1>基于灰度图像的角点检测
<2>基于二值图像的角点检测
<3>基于轮廓曲线的角点检测
而基于灰度图像的角点检测又可分为基于梯度、基于模板和基于模板梯度组合三类方法,其中基于模板的方法主要考虑像素领域点的灰度变化,即图像亮度的变化,将与邻点亮度对比足够大的点定义为角点。常见的基于模板的角点检测算法有Kitchen-Rosenfeld角点检测算法,Harris角点检测算法、KLT角点检测算法及SUSAN角点检测算法。和其他角点检测算法相比,SUSAN角点检测算法具有算法简单、位置准确、抗噪声能力强等特点。
关于角点的具体描述可以有几种:
一阶导数(即灰度的梯度)的局部最大所对应的像素点;
两条及两条以上边缘的交点;
图像中梯度值和梯度方向的变化速率都很高的点;
角点处的一阶导数最大,二阶导数为零,指示物体边缘变化不连续的方向。
#include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> #include <stdio.h> #include <stdlib.h> using namespace cv; using namespace std; /// Global variables Mat src, src_gray,dst; int thresh = 200; int max_thresh = 255; char* source_window = "Source image"; char* corners_window = "Corners detected"; /// Function header void cornerHarris_demo(int, void*); void cvtColor_src(Mat &src, Mat &dst); /** @function main */ int main(int argc, char** argv) { /// Load source image and convert it to gray src = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\06.jpg"); cvtColor(src, src_gray, CV_BGR2GRAY); // 转换单通道 /// Create a window and a trackbar namedWindow(source_window, CV_WINDOW_AUTOSIZE); createTrackbar("Threshold: ", source_window, &thresh, max_thresh, cornerHarris_demo); imshow(source_window , src); cornerHarris_demo(0, 0); waitKey(0); return(0); } /** @function cornerHarris_demo */ void cornerHarris_demo(int, void*) { Mat dst, dst_norm, dst_norm_scaled; dst = Mat::zeros(src.size(), CV_32FC1); /// Detector parameters int blockSize = 2; int apertureSize = 3; double k = 0.04; /// Detecting corners cornerHarris(src_gray, dst, blockSize, apertureSize, k, BORDER_DEFAULT); /// Normalizing normalize(dst, dst_norm, 0, 255, NORM_MINMAX, CV_32FC1, Mat()); convertScaleAbs(dst_norm, dst_norm_scaled); /// Drawing a circle around corners for (int j = 0; j < dst_norm.rows; j++) { for (int i = 0; i < dst_norm.cols; i++) { if ((int)dst_norm.at<float>(j, i) > thresh) { circle(dst_norm_scaled, Point(i, j), 5, Scalar(0), 2, 8, 0); } } } /// Showing the result namedWindow(corners_window, CV_WINDOW_AUTOSIZE); imshow(corners_window, dst_norm_scaled); }
现在讲解一下其中原理:
w(x,y)是窗口函数;w(x,y)也可以说是权值;在x方向和 v 方向也就是我们说的水平,竖直方向上移动;
上图中红色的窗口可以往任意一个方向上移动;来检测是否是角;
红色的窗口开始有图像的像素,移动后窗口内的图像像素变化;两者相减在平方;
前面说过角在各个方向上梯度都很大,因此我们就用(红色的窗口开始有图像的像素,移动后窗口内的图像像素变化;两者相减在平方,无论窗口移动大哪个方向)原理来找最大变化;下面是数学推导;
由于角点代表了图像像素梯度变化,我们将寻找这个”变化”。
考虑到一个灰度图像 I. 划动窗口 w(x,y) (with displacements u 在x方向和 v 方向) I 计算像素灰度变化。
其中: w(x,y) is the window at position (x,y) I(x,y) is the intensity at (x,y) I(x+u,y+v) is the intensity at the moved window (x+u,y+v)
为了寻找带角点的窗口,我们搜索像素灰度变化较大的窗口。于是, 我们期望最大化以下式子:
使用 泰勒(Taylor)展开式:
具体的可以看数学推导(如果还不懂,可以去学学泰勒展开,治理主要用的是一阶泰勒展开)
式子可以展开为:
将上式表达为矩阵的形式,表达式可以写为:
表示为:
因此我们有等式:
每个窗口中计算得到一个值。这个值决定了这个窗口中是否包含了角点:
其中:
一个窗口,它的分数 R 大于一个特定值,这个窗口就可以被认为是”角点”
上面E(u,v)可以看出,无论u,v取何值,我们需要E尽可能的大;
线代理论中实对称矩阵可以正交相似对角化,如下面理论;可以看出E(u,v)是一个椭圆,若E尽可能的大,则λi要尽可能大;
所以下图中的λi是椭圆的半长轴(及开根号,椭圆公式);所以当λi两个都很大时,是角及corner,一个大一个小是边;具体看下图中英文标识。
corner:在水平、竖直两个方向上变化均较大的点,即Ix、Iy都较大;
edge :仅在水平、或者仅在竖直方向有较大的点,即Ix和Iy只有其一较大 ;
flat : 在水平、竖直方向的变化量均较小的点,即Ix、Iy都较小;
讲到这里,大家基本原理懂了,那如何判别两个特征根都很大呢?
首先矩阵行列式的求法:,只有两个都大时,det(M)才会大;k一般去为0.05;
也是两个都大时,其平方也会大;
最重要的是求每个像素的M矩阵,如下面步骤:
最后最重要的一点;M的求法是不会变得,不同的角点检测方法最后区别就在于R的求法,也就是如何判断特征根都很大的。
void cornerHarris_demo(int, void*) { Mat dst, dst_norm, dst_norm_scaled; dst = Mat::zeros(src.size(), CV_32FC1); /// Detector parameters int blockSize = 2; int apertureSize = 3; double k = 0.04; /// Detecting corners cornerHarris(src_gray, dst, blockSize, apertureSize, k, BORDER_DEFAULT); /// Normalizing normalize(dst, dst_norm, 0, 255, NORM_MINMAX, CV_32FC1, Mat()); convertScaleAbs(dst_norm, dst_norm_scaled); /// Drawing a circle around corners for (int j = 0; j < dst_norm.rows; j++) { for (int i = 0; i < dst_norm.cols; i++) { if ((int)dst_norm.at<float>(j, i) > thresh) { circle(dst_norm_scaled, Point(i, j), 5, Scalar(0), 2, 8, 0); } } }
Shi-Tomasi 算法是Harris 算法的改进。Harris 算法最原始的定义是将黑塞矩阵
M的行列式值与 M 的迹相减,再将差值同预先给定的阈值进行比较。后来Shi 和Tomasi 提出改进的方法,若两个特征值中较小的一个大于最小阈值,则会得到强角点。二者原理一模一样,只是计算角点量的方式不一样。
#include "opencv2/opencv.hpp" #include <iostream> using namespace cv; using namespace std; #define WINDOW_NAME "【Shi-Tomasi角点检测】" Mat g_srcImage, g_grayImage; int g_maxCornerNumber = 33; int g_maxTrackbarNumber = 500; RNG g_rng(12345);//初始化随机数生成器 //-----------------------------【on_GoodFeaturesToTrack( )函数】---------------------------- // 描述:响应滑动条移动消息的回调函数 //---------------------------------------------------------------------------------------------- void on_GoodFeaturesToTrack(int, void*) { //【1】对变量小于等于1时的处理 if (g_maxCornerNumber <= 1) { g_maxCornerNumber = 1; } //【2】Shi-Tomasi算法(goodFeaturesToTrack函数)的参数准备 vector<Point2f> corners; double qualityLevel = 0.01;//角点检测可接受的最小特征值 double minDistance = 10;//角点之间的最小距离 int blockSize = 3;//计算导数自相关矩阵时指定的邻域范围 double k = 0.04;//权重系数 Mat copy = g_srcImage.clone(); //复制源图像到一个临时变量中,作为感兴趣区域 //【3】进行Shi-Tomasi角点检测 goodFeaturesToTrack(g_grayImage,//输入图像 corners,//检测到的角点的输出向量 g_maxCornerNumber,//角点的最大数量 qualityLevel,//角点检测可接受的最小特征值 minDistance,//角点之间的最小距离 Mat(),//感兴趣区域 blockSize,//计算导数自相关矩阵时指定的邻域范围 false,//不使用Harris角点检测 k);//权重系数 //【4】输出文字信息 cout << "\t>此次检测到的角点数量为:" << corners.size() << endl; //【5】绘制检测到的角点 int r = 4; for (int i = 0; i < corners.size(); i++) { //以随机的颜色绘制出角点 circle(copy, corners[i], r, Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255)), -1, 8, 0); } //【6】显示(更新)窗口 imshow(WINDOW_NAME, copy); } static void ShowHelpText() { //输出欢迎信息和OpenCV版本 printf("\n\n\t\t\t非常感谢购买《OpenCV3编程入门》一书!\n"); printf("\n\n\t\t\t此为本书OpenCV2版的第87个配套示例程序\n"); printf("\n\n\t\t\t 当前使用的OpenCV版本为:" CV_VERSION); printf("\n\n ----------------------------------------------------------------------------\n"); //输出一些帮助信息 printf("\n\n\n\t欢迎来到【Shi-Tomasi角点检测】示例程序\n"); printf("\n\t请调整滑动条观察图像效果\n\n"); } void main() { system("color 2F"); ShowHelpText(); //【1】载入源图像并将其转换为灰度图 g_srcImage = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\06.jpg", 1); cvtColor(g_srcImage, g_grayImage, CV_BGR2GRAY); //【2】创建窗口和滑动条,并进行显示和回调函数初始化 namedWindow(WINDOW_NAME, CV_WINDOW_AUTOSIZE); createTrackbar("最大角点数", WINDOW_NAME, &g_maxCornerNumber, g_maxTrackbarNumber, on_GoodFeaturesToTrack); imshow(WINDOW_NAME, g_srcImage); on_GoodFeaturesToTrack(0, 0); waitKey(0); }
自定义角点检测器简介:
-
基于Harris与Shi-Tomasi角点检测
-
首先通过计算矩阵M得到lamda1和lamda2两个特征值根据他们得到角点响应值
-
然后自己设置阈值实现计算出阈值得到有效响应值的角点设置
cornerEigenValsAndVecs()函数在角点检测中计算扫描图像块的特征向量与特征值,其函数原型如下:
C++: void cornerEigenValsAndVecs( InputArray src, --单通道输入8位或浮点图像 OutputArray dst, --输出图像,同源图像或CV_32FC(6) int blockSize, --邻域大小值 int apertureSize, --Sobel算子的参数 int borderType=BORDER_DEFAULT --像素外插方法 )//对应于Harris
borderType:像素扩展的方法,因为在滤波处理的过程中会扩展图像边缘,每扩张一个边界像素,都需要计算出该像素点在原图中的位置,这个功能被提炼出来就变成了borderInterpolate()函数。该函数输入一个点坐标,返回他在原图中的坐标;可参考:borderInterpolate()函数。
下面我们采用cornerEigenValsAndVecs()函数和minMaxLoc()函数,根据其原理来编写Harris角点检测的实现代码,示例如下:
#include <opencv2/opencv.hpp> #include <iostream> #include <math.h> using namespace cv; using namespace std; // 定义全局变量 const string harris_winName = "自定义角点检测"; Mat src_img, gray_img; // src_img表示原图, gray_img表示灰度图 Mat harris_dst_img, harris_response_img; // harris_dst_img存储自相关矩阵M的特征值和特征向量,harris_response_img存储响应函数的结果 double min_respense_value; // 响应函数的结果矩阵中的最小值 double max_respense_value; // 响应函数的结果矩阵中的最大值 int qualityValue = 30; int max_qualityValue = 100; // 通过qualityValue/max_qualityValue的结果作为qualitylevel来计算阈值 RNG random_number_generator; // 定义一个随机数发生器 void self_defining_Harris_Demo(int, void*); //TrackBar回调函数声明 // 主函数 int main() { src_img = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\06.jpg"); if (src_img.empty()) { printf("could not load the image...\n"); return -1; } namedWindow("原图", CV_WINDOW_AUTOSIZE); imshow("原图", src_img); cvtColor(src_img, gray_img, COLOR_BGR2GRAY); //将彩色图转化为灰度图 // 计算特征值 int blockSize = 3; int ksize = 3; double k = 0.04; harris_dst_img = Mat::zeros(src_img.size(), CV_32FC(6)); // 目标图像harris_dst_img存储自相关矩阵M的特征值和特征向量, // 并将它们以(λ1, λ2, x1, y1, x2, y2)的形式存储。其中λ1, λ2是M未经过排序的特征值; // x1, y1是对应于λ1的特征向量;x2, y2是对应于λ2的特征向量。 // 因此目标矩阵为6通道,即 CV_32FC(6)的矩阵。 harris_response_img = Mat::zeros(src_img.size(), CV_32FC1); // harris_response_img用来存储通过每个像素值所对应的自相关矩阵所计算得到的响应值 cornerEigenValsAndVecs(gray_img, harris_dst_img, blockSize, ksize, 4); // 该函数用来计算每个像素值对应的自相关矩阵的特征值和特征向量 // 计算响应函数值 for (int row = 0; row < harris_dst_img.rows; ++row) { for (int col = 0; col < harris_dst_img.cols; ++col) { double eigenvalue1 = harris_dst_img.at<Vec6f>(row, col)[0]; // 获取特征值1 double eigenvalue2 = harris_dst_img.at<Vec6f>(row, col)[1]; // 获取特征值2 harris_response_img.at<float>(row, col) = eigenvalue1* eigenvalue2 - k*pow((eigenvalue1 + eigenvalue2), 2); // 通过响应公式R=λ1*λ2 - k*(λ1+λ2)*(λ1+λ2)来计算每个像素对应的响应值 } } minMaxLoc(harris_response_img, &min_respense_value, &max_respense_value, 0, 0, Mat()); // 寻找响应矩阵中的最小值和最大值 namedWindow(harris_winName, CV_WINDOW_AUTOSIZE); createTrackbar("Quality Value:", harris_winName, &qualityValue, max_qualityValue, self_defining_Harris_Demo); //创建TrackBar self_defining_Harris_Demo(0, 0); waitKey(0); return 0; } // 回调函数实现 void self_defining_Harris_Demo(int, void*) { if (qualityValue < 10) { qualityValue = 10; // 控制qualitylevel的下限值 } Mat result_img = src_img.clone(); // 输出图像 float threshold_value = min_respense_value + (((double)qualityValue) / max_qualityValue)*(max_respense_value - min_respense_value); for (int row = 0; row <result_img.rows; row++) { for (int col = 0; col < result_img.cols; col++) { float resp_value = harris_response_img.at<float>(row, col); if (resp_value > threshold_value) { circle(result_img, Point(col, row), 2, Scalar(random_number_generator.uniform(0, 255), random_number_generator.uniform(0, 255), random_number_generator.uniform(0, 255)), 2, 8, 0); } } } imshow(harris_winName, result_img); }
erMinEigenVal()函数在角点检测中计算梯度矩阵的最小特征值,其函数原型如下
C++: void cornerMinEigenVal( InputArray src, --单通道输入8位或浮点图像 OutputArray dst, --图像存储的最小特征值。类型为CV_32FC1 int blockSize, --邻域大小值 int apertureSize=3, --Sobel算子的参数 int borderType=BORDER_DEFAULT --像素外插方法 }//对应Shi-Tomasi
函数参数
函数参数说明中除了dst必须为CV_32FC1类型以外,其它与cornerEigenValsAndVecs()函数的一致。
函数功能
功能与cornerEigenValsAndVecs()函数相似,但是它只计算导数协方差矩阵的最小特征值,
按照cornerEigenValsAndVecs()函数给定的特征值λ1, λ2来说就是min(λ1, λ2)。
采用cornerMinEigenVal()函数和minMaxLoc()函数结合来模拟Shi-Tomasi角点检测的代码示例如下;(参考:定制化创建角点检测子)
#include <opencv2/opencv.hpp> #include <iostream> #include <math.h> using namespace cv; using namespace std; // 定义全局变量 const string ShiTomasi_winName = "Custom Shi-Tomasi Corners Detector"; Mat src_img, gray_img; // src_img表示原图, gray_img表示灰度图 Mat ShiTomasi_dst_img; // ShiTomasi_dst_img用来存储每个像素对应的自相关矩阵的最小特征值 double min_ShiTomasi_value; // 最小特征矩阵中的最小值 double max_ShiTomasi_value; // 最小特征矩阵中的最大值 int ShiTomasi_qualityValue = 30; int max_qualityValue = 100; RNG random_number_generator; // 定义一个随机数发生器 void self_defining_ShiTomasi_Demo(int, void*); //TrackBar回调函数声明 // 主函数 int main() { src_img = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\06.jpg"); if (src_img.empty()) { printf("could not load the image...\n"); return -1; } namedWindow("原图", CV_WINDOW_AUTOSIZE); imshow("原图", src_img); cvtColor(src_img, gray_img, COLOR_BGR2GRAY); //将彩色图转化为灰度图 // 计算特征值 int blockSize = 3; int ksize = 3; // 计算最小特征值 ShiTomasi_dst_img = Mat::zeros(src_img.size(), CV_32FC1); cornerMinEigenVal(gray_img, ShiTomasi_dst_img, blockSize, ksize, 4); // 计算每个像素对应的自相关矩阵的最小特征值 minMaxLoc(ShiTomasi_dst_img, &min_ShiTomasi_value, &max_ShiTomasi_value, 0, 0, Mat()); //计算最小最大值 namedWindow(ShiTomasi_winName, CV_WINDOW_AUTOSIZE); createTrackbar("Quality:", ShiTomasi_winName, &ShiTomasi_qualityValue, max_qualityValue, self_defining_ShiTomasi_Demo); self_defining_ShiTomasi_Demo(0, 0); waitKey(0); return 0; } // 回调函数实现 void self_defining_ShiTomasi_Demo(int, void*) { if (ShiTomasi_qualityValue < 10) { ShiTomasi_qualityValue = 10; // 控制qualitylevel的下限值 } Mat result_img = src_img.clone(); // 输出图像 float threshold_value = min_ShiTomasi_value + (((double)ShiTomasi_qualityValue) / max_qualityValue)*(max_ShiTomasi_value - min_ShiTomasi_value); for (int row = 0; row <result_img.rows; row++) { for (int col = 0; col < result_img.cols; col++) { float resp_value = ShiTomasi_dst_img.at<float>(row, col); if (resp_value > threshold_value) { circle(result_img, Point(col, row), 2, Scalar(random_number_generator.uniform(0, 255), random_number_generator.uniform(0, 255), random_number_generator.uniform(0, 255)), 2, 8, 0); } } } imshow(ShiTomasi_winName, result_img); }
亚像素角点检测需要先运行常规的角点检测,得到整数表示的角点坐标。然后算法对每个角点做细化,得到实数表示的角点坐标 第一步:goodFeaturesToTrack() //检测角点 第二步:TermCriteria() //设置迭代算法的终止条件 TermCriteria(int type,int max_iter, double epsilon); 参数 type:终止条件类型; CV_TERMCRIT_ITER--max_iter达到最大值后停止算法; CV_TERMCRIT_EPS--当算法依赖的精确度低于epsilon后,停止算法; CV_TERMCRIT_ITER+CV_TERMCRIT_EPS--当max_iter达到最大值或算法依赖的精确度低于epsilon任一个满足时,停止算法; max_iter:最大迭代次数; epsilon:要求精度; 第三步:cornerSubPix() //细化角点位置; void cornerSubPix(InputArray image, InputOutputArray corners, Size winSize, Size zeroZone, TermCriteria criteria); 参数: image:输入图像; corners:初始化输入角点的坐标,为输出提供细化的坐标; winSize:搜索窗口的边长的一半; zeroZone:搜索区域中间的死区的一半大小,对它在下边的求和公式不计算,有时候它用来避免可能的自相关矩阵的奇异性,(-1,-1)用来表明这里没有这样的规模; criteria:角点细化的迭代过程的终止条件; 可参考:亚像素角点检测
相关代码和结果
#include <opencv2/opencv.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/highgui/highgui.hpp> #include <iostream> using namespace cv; using namespace std; //描述:定义一些辅助宏 #define WINDOW_NAME "【亚像素角点检测】" //全局变量声明 Mat g_srcImage; Mat g_grayImage; int g_maxCornerNumber = 33; int g_maxTrackbarNumber = 500; RNG g_rng(12345); //回调函数 void on_GoodFeaturesToTrack(int, void *) { if (g_maxCornerNumber <= 1) { g_maxCornerNumber = 1; } //shi-tomasi算法 vector<Point2f> corner; double qualityLevel = 0.01; //角点检测可接收的最小特征 double minDistance = 10; //角点之间的最小的距离 int blockSize = 3; //计算导数自相关矩阵时指定的邻域范围 double k = 0.04; //权重系数 Mat copy = g_srcImage.clone(); //进行角点检测 goodFeaturesToTrack(g_grayImage, corner, g_maxCornerNumber, qualityLevel, minDistance, Mat(), blockSize, false, k); //输出文字信息 cout << ">此次检测到的角点数量为:" << corner.size() << endl; //绘制检测到的角点 Size winSize = Size(5, 5); Size zeroZone = Size(-1, -1); TermCriteria criteria = TermCriteria(TermCriteria::EPS + TermCriteria::MAX_ITER, 40, 0.001); cornerSubPix(g_grayImage, corner, winSize, zeroZone, criteria); for (int i = 0; i < corner.size(); i++) { cout << "\t>>精度角点坐标[" << i << "](" << corner[i].x << "," << corner[i].y << ")" << endl; } int r = 4; for (unsigned int i = 0; i < corner.size(); i++) { circle(copy, corner[i], r, Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255)), -1, 8, 0); } //显示更新窗口 imshow(WINDOW_NAME, copy); } int main() { //载入原始图像 g_srcImage = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\06.jpg", 1); cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY); //创建窗口和滑动条 namedWindow(WINDOW_NAME, WINDOW_AUTOSIZE); createTrackbar("最大角点数:", WINDOW_NAME, &g_maxCornerNumber, g_maxTrackbarNumber, on_GoodFeaturesToTrack); imshow(WINDOW_NAME, g_srcImage); on_GoodFeaturesToTrack(0, 0); waitKey(0); return 0; }
原文链接:https://blog.csdn.net/poem_qianmo/article/details/29356187
代码参考:https://blog.csdn.net/poem_qianmo/article/category/1923021和http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/features2d/trackingmotion/good_features_to_track/good_features_to_track.html#good-features-to-track