角点检测(Corner Detection)是计算机视觉系统中用来获得图像特征的一种方法,广泛应用于运动检测、图像匹配、视频跟踪、三维建模和目标识别等领域中,也称为特征点检测。
Harris角点检测Shi-Tomasi角点检测
自定义角点检测
亚像素级角点检测
1.角点
角点通常被定义为两条边的交点,更严格地说法是,角点的局部邻域应该具有两个不同区域的不同方向的边界。而实际应用中,大多数所谓的角点检测方法检测的是拥有特定特征的图像点,而不仅仅是“角点”。这些特征点在图像中有具体的坐标,并具有某些数学特征,如局部最大或最小灰度、某些梯度特征等。
在图像处理和与计算机视觉领域,兴趣点(interest points),也被称作关键点(key points)、特征点(feature points)。它被大量用于解决物体识别、图像识别、图像匹配、视觉跟踪、三维重建等一系列的问题。我们不再观察整幅图,而是选择某些特殊的点,然后对它们进行局部有的放矢地分析。如果能检测到足够多的这种点,同时它们的区分度很高,并且可以精确定位稳定的特征,那么这个方法就具有实用价值。
图像特征类型可以被分为如下三种:
- 边缘
- 角点(感兴趣关键点)
- 斑点(Blobs)(感兴趣区域)
其中,角点是个很特殊的存在。如果某一点在任意方向的一个微小变动都会引起灰度很大的变化,那么我们就把它称之为角点。角点作为图像上的特征点,包含有重要的信息,在图像融合和目标跟踪及三维重建中有重要的应用价值。它们在图像中可以轻易地定位,同时,在人造物体场景,比如门、窗、桌等处也随处可见。因为角点位于两条边缘的交点处,代表了两个边缘变化的方向上的点,所以它们是可以精确定位的二维特征,甚至可以达到亚像素的精度。又由于其图像梯度有很高的变化,这种变化是可以用来帮助检测角点的。需要注意的是,角点与位于相同强度区域上的点不同,与物体轮廓上的点也不同,因为轮廓点难以在相同的其他物体上精确定位。
另外,关于角点的具体描述可以有如下几种:
- 一阶导数(即灰度的梯度)的局部最大所对应的像素点;
- 两条及两条以上边缘的交点;
- 图像中梯度值和梯度方向的变化速率都很高的点;
- 角点处的一阶导数最大,二阶导数为零,它指示了物体边缘变化不连续的方向。
2.角点检测
现有的角点检测算法并不是都十分的健壮。很多方法都要求有大量的训练集和冗余数据来防止或减少错误特征的出现。另外,角点检测方法的一个很重要的评价标准是其对多幅图像中相同或相似特征的检测能力,并且能够应对光照变化、图像旋转等图像变化。
在当前的图像处理领域,角点检测算法可归纳为以下三类。
- 基于灰度图像的角点检测
- 基于二值图像的角点检测
- 基于轮廓曲线的角点检测
而基于灰度图像的角点检测又可分为基于梯度、基于模板和基于模板梯度组合三类方法。其中基于模板的方法主要考虑像素领域点的灰度变化,即图像亮度的变化,将与邻点亮度对比足够大的点定义为角点。常见的基于模板的角点检测算法有Kitchen-Rosenfeld角点检测算法,Harris角点检测算法、KLT角点检测算法及SUSAN角点检测算法。
2.1 Harris角点检测
原理见:添加链接描述
角点检测是一种直接基于灰度图像的角点提取算法,稳定性高,尤其对L型角点检测精度高。但由于采用了高斯滤波,运算速度相对较慢,角点信息有丢失和位置偏移的现象,而且角点提取有聚簇现象。
cornerHarris函数用于在OpenCV中运行Harris角点检测算子来进行角点检测。和cornerMinEigenVal()以及cornerEigenValsAndVecs()函数类似,cornerHarris函数对于每一个像素(x,y)在blockSize×blockSize邻域内,计算2x2梯度的协方差矩阵,接着它计算如下式子:
就可以找出输出图中的局部最大值,即找出了角点。
//19 Harris角点检测
Mat src_harris, dst_gray, src_harris_clone;
void CornerHarrisChange(int , void *);
void StartOp2::ImageProcess2_19()
{
src_harris = imread("../../Images/20.jpg", 1);
if (!src_harris.data) {
cout << "文件打开失败" << endl;
}
//直接在原图像上更新会有不刷新的问题
src_harris_clone = src_harris.clone();
cvtColor(src_harris_clone, dst_gray, COLOR_BGR2GRAY);
namedWindow("input", WINDOW_AUTOSIZE);
//要判断的阈值,检测出来的角点信息中,大于这个值则视为角点
int thresh = 30;
int max_thresh = 175;
createTrackbar("阈值: ", "input", &thresh, max_thresh, CornerHarrisChange);
CornerHarrisChange(0,0);
}
void CornerHarrisChange(int value, void *)
{
Mat dst;
Mat dst_norm;
Mat dst_scale;
//dst = Mat::zeros(src_harris.size(), CV_32FC1);
dst = Mat::zeros(src_harris.size(), CV_32FC1);
src_harris_clone = src_harris.clone();
cornerHarris(dst_gray, dst, 2, 3, 0.04, BORDER_DEFAULT);
normalize(dst, dst_norm, 0, 255, NORM_MINMAX, CV_32FC1, Mat());
//将归一化后的图线性变换成8位无符号整型
convertScaleAbs(dst_norm, dst_scale);
// 将检测到的,且符合阈值条件的角点绘制出来
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) > value + 80)
{
circle(src_harris_clone, Point(i, j), 5, Scalar(10, 10, 255), 2, 8, 0);
circle(dst_scale, Point(i, j), 5, Scalar(0, 10, 255), 2, 8, 0);
}
}
}
imshow("input", src_harris_clone);
imshow("output", dst_scale);
}
2.2 Shi-Tomasi角点检测
除了利用Harris进行角点检测之外,我们通常还可以利用Shi-Tomasi方法进行角点检测。Shi-Tomasi算法是Haris算法的改进,此算法最原始的定义是将矩阵M的行列式值与M的迹相减,再将差值同预先给定的阀值进行比较。后来Shi和Tomasi提出改进了方法,若两个特征值中较小的一个大于最小阈值,则会得到强角点。
由于Shi-Tomasi算子是1994年在文章《Good Features to Track》中被提出的,OpenCV实现此算法的函数名便定义为goodFeaturesToTrack。下面我们来看看此函数的用法。
确定图像强角点:goodFeaturesToTrack()函数
goodFeaturesToTrackO函数结合了Shi-Tomasi算子,用于确定图像的强角点。
//20 Shi-Tomasi角点检测
Mat src_shi, dst_shi_gray;
void ShiTomasiChange(int, void *);
void StartOp2::ImageProcess2_20()
{
src_shi = imread("../../Images/20.jpg", 1);
if (!src_shi.data) {
cout << "文件打开失败" << endl;
}
cvtColor(src_shi, dst_shi_gray, COLOR_BGR2GRAY);
namedWindow("input", WINDOW_AUTOSIZE);
int g_maxCornerNumber = 33;
int g_maxTrackbarNumber = 500;
createTrackbar("最大角点数", "input", &g_maxCornerNumber, g_maxTrackbarNumber, ShiTomasiChange);
imshow("input", src_shi);
ShiTomasiChange(0, 0);
}
void ShiTomasiChange(int value, void*)
{
//1.对变量小于等于1时的处理
if (value <= 1) { value = 1; }
//2.Shi-Tomasi算法(goodFeaturesToTrack函数)的参数准备
vector<Point2f> corners;
double qualityLevel = 0.01;//角点检测可接受的最小特征值
double minDistance = 10;//角点之间的最小距离
int blockSize = 3;//计算导数自相关矩阵时指定的邻域范围
double k = 0.04;//权重系数
Mat copy = src_shi.clone(); //复制源图像到一个临时变量中,作为感兴趣区域
//3.进行Shi-Tomasi角点检测
goodFeaturesToTrack(dst_shi_gray,//输入图像
corners,//检测到的角点的输出向量
value,//角点的最大数量
qualityLevel,//角点检测可接受的最小特征值
minDistance,//角点之间的最小距离
Mat(),//感兴趣区域
blockSize,//计算导数自相关矩阵时指定的邻域范围
false,//不使用Harris角点检测
k);//权重系数
//4.输出文字信息
cout << "\t>此次检测到的角点数量为:" << corners.size() << endl;
RNG rng(12345);
//5.绘制检测到的角点
int r = 4;
for (int i = 0; i < corners.size(); i++)
{
//以随机的颜色绘制出角点
circle(copy, corners[i], r, Scalar(rng.uniform(0, 255), rng.uniform(0, 255),
rng.uniform(0, 255)), -1, 8, 0);
}
//6.显示(更新)窗口
imshow("out", copy);
}
2.3 自定义角点检测器
- 基于Harris与shi-Tomasi角点检测
- 首先通过计算矩阵M得到
两个特征值根据他们得到角点响应值
- 然后自己设置阈值实现计算出阈值得到有效响应值的角点位置
使用cornerEigenValsAndVecs()函数和minMaxLoc()函数结合来模拟Harris角点检测,或者使用cornerMinEigenVal()函数和minMaxLoc()函数结合来模拟Shi-Tomasi角点检测,最后特征点选取的判断条件要根据实际情况进行选择。
cornerEigenValsAndVecs()函数用来求解输入图像矩阵的特征向量与特征值
C++: void cornerEigenValsAndVecs(
InputArray src, --单通道输入8位或浮点图像
OutputArray dst, --输出图像,同源图像或CV_32FC(6)
int blockSize, --邻域大小值
int apertureSize, --Sobel算子的参数
int borderType=BORDER_DEFAULT --像素外插方法
)//对应于Harris
cornerMinEigenVal()功能与cornerEigenValsAndVecs()函数相似,但是它只计算导数协方差矩阵(即自相关矩阵)的最小特征值
C++: void cornerMinEigenVal(
InputArray src, --单通道输入8位或浮点图像
OutputArray dst, --图像存储的最小特征值。类型为CV_32FC1
int blockSize, --邻域大小值
int apertureSize=3, --Sobel算子的参数
int borderType=BORDER_DEFAULT --像素外插方法
}//对应Shi-Tomasi
//21 自定义角点检测器
Mat src_custom, dst_custom_gray;
// harris corner response
Mat dst_harris, dst_harrisRsp;
double harris_min_rsp;
double harris_max_rsp;
const char* harris_win = "Custom Harris Corners Detector";
Mat dst_shiTomasiRsp;
double shitomasi_max_rsp;
double shitomasi_min_rsp;
int sm_qualitylevel = 30;
const char* shitomasi_win = "Custom Shi-Tomasi Corners Detector";
// quality level
int qualityLevel = 30;
int max_count = 100;
void CustomHarris_Demo(int, void*);
void CustomShiTomasi_Demo(int, void*);
void StartOp2::ImageProcess2_21()
{
src_custom = imread("../../Images/20.jpg", 1);
if (!src_custom.data) {
cout << "文件打开失败" << endl;
}
cvtColor(src_custom, dst_custom_gray, COLOR_BGR2GRAY);
namedWindow("input", WINDOW_AUTOSIZE);
imshow("input", src_custom);
//Harris
// 1.计算特征值
int blockSize = 3;
int ksize = 3;
double k = 0.04;
dst_harris = Mat::zeros(src_custom.size(), CV_32FC(6));
dst_harrisRsp = Mat::zeros(src_custom.size(), CV_32FC1);
/*
// 目标图像dst_harris存储自相关矩阵M的特征值和特征向量,
// 并将它们以(λ1, λ2, x1, y1, x2, y2)的形式存储。其中λ1, λ2是M未经过排序的特征值;
// x1, y1是对应于λ1的特征向量;x2, y2是对应于λ2的特征向量。
// 因此目标矩阵为6通道,即 CV_32FC(6)的矩阵。
*/
cornerEigenValsAndVecs(dst_custom_gray, dst_harris, blockSize, ksize, 4);
//2.计算响应
for (int row = 0; row < dst_harris.rows; row++) {
for (int col = 0; col < dst_harris.cols; col++) {
double lambda1 = dst_harris.at<Vec6f>(row, col)[0];
double lambda2 = dst_harris.at<Vec6f>(row, col)[1];
dst_harrisRsp.at<float>(row, col) = lambda1 * lambda2 - k * pow((lambda1 + lambda2), 2);
}
}
minMaxLoc(dst_harrisRsp, &harris_min_rsp, &harris_max_rsp, 0, 0, Mat());
namedWindow(harris_win, CV_WINDOW_AUTOSIZE);
createTrackbar("Quality Value:", harris_win, &qualityLevel, max_count, CustomHarris_Demo);
CustomHarris_Demo(0, 0);
//shi-Tomasi
//计算最小特征值
dst_shiTomasiRsp = Mat::zeros(src_custom.size(), CV_32FC1);
cornerMinEigenVal(dst_custom_gray, dst_shiTomasiRsp, blockSize, ksize, 4);
minMaxLoc(dst_shiTomasiRsp, &shitomasi_min_rsp, &shitomasi_max_rsp, 0, 0, Mat());
namedWindow(shitomasi_win, CV_WINDOW_AUTOSIZE);
createTrackbar("Quality:", shitomasi_win, &sm_qualitylevel, max_count, CustomShiTomasi_Demo);
CustomShiTomasi_Demo(0, 0);
}
void CustomHarris_Demo(int, void*) {
if (qualityLevel < 10) {
qualityLevel = 10;
}
Mat resultImg = src_custom.clone();
//判断的阈值
float t = harris_min_rsp + (((double)qualityLevel) / max_count)*(harris_max_rsp - harris_min_rsp);
for (int row = 0; row < src_custom.rows; row++) {
for (int col = 0; col < src_custom.cols; col++) {
float v = dst_harrisRsp.at<float>(row, col);
if (v > t) {
circle(resultImg, Point(col, row), 2, Scalar(0, 0, 255), 2, 8, 0);
}
}
}
imshow(harris_win, resultImg);
}
void CustomShiTomasi_Demo(int, void*) {
if (sm_qualitylevel < 20) {
sm_qualitylevel = 20;
}
Mat resultImg = src_custom.clone();
//判断的阈值
float t = shitomasi_min_rsp + (((double)sm_qualitylevel) / max_count)*(shitomasi_max_rsp - shitomasi_min_rsp);
for (int row = 0; row < src_custom.rows; row++) {
for (int col = 0; col < src_custom.cols; col++) {
float v = dst_shiTomasiRsp.at<float>(row, col);
if (v > t) {
circle(resultImg, Point(col, row), 2, Scalar(0, 0, 255), 2, 8, 0);
}
}
}
imshow(shitomasi_win, resultImg);
}
2.4 亚像素级角点检测
若我们进行图像处理的目的不是提取用于识别的特征点而是进行几何测量,这通常需要更高的精度,而函数goodFeatures ToTrack0只能提供简单的像素的坐标值,也就是说,有时候会需要实数坐标值而不是整数坐标值。
亚像素级角点检测的位置在摄像机标定、跟踪并重建摄像机的轨迹,或者重建被跟踪目标的三维结构时,是一个基本的测量值。
下面我们将讨论如何将所求得的角点位置精确到亚像素级精度。一个向量和与其正交的向量的点积为0,角点则满足如图10.11所示情况。
其中,(a)点p附近的图像是均匀的,其梯度为0;(b)边缘的梯度与沿边缘方向的q-p向量正交。在图中的两种情况下,p点梯度与q-p向量的点积均为0。
图10.11中,我们假设起始角点q在实际亚像素级角点的附近。检测所有的q-p向量。若点p位于一个均匀的区域,则点p处的梯度为0。若q-p向量的方向与边缘的方向一致,则此边缘上p点处的梯度与q-p向量正交,在这两种情况下,p点处的梯度与q-p向量的点积为0。我们可以在p点周围找到很多组梯度以及相关的向量q-p,令其点集为0,然后可以通过求解方程组,方程组的解即为角点q的亚像素级精度的位置,也就是精确的角点位置。具体的数学过程见下:
OpenCV为我们提供了cornerSubPix()函数,用于发现亚像素精度的角点位置。
//22 亚像素级角点检测
Mat src_cornerSubPix, dst_cornerSubPix_gray;
void CornerSubPixChange(int , void*);
void StartOp2::ImageProcess2_22()
{
src_cornerSubPix = imread("../../Images/20.jpg", 1);
if (!src_cornerSubPix.data) {
cout << "文件打开失败" << endl;
}
cvtColor(src_cornerSubPix, dst_cornerSubPix_gray, COLOR_BGR2GRAY);
namedWindow("input", WINDOW_AUTOSIZE);
int g_maxCornerNumber = 33;
int g_maxTrackbarNumber = 500;
createTrackbar("最大角点数", "input", &g_maxCornerNumber, g_maxTrackbarNumber, CornerSubPixChange);
CornerSubPixChange(0,0);
imshow("input", src_cornerSubPix);
}
void CornerSubPixChange(int value, void*)
{
//1.对变量小于等于1时的处理
if (value <= 1) { value = 1; }
//2.Shi-Tomasi算法(goodFeaturesToTrack函数)的参数准备
vector<Point2f> corners;
double qualityLevel = 0.01;//角点检测可接受的最小特征值
double minDistance = 10;//角点之间的最小距离
int blockSize = 3;//计算导数自相关矩阵时指定的邻域范围
double k = 0.04;//权重系数
Mat copy = src_cornerSubPix.clone(); //复制源图像到一个临时变量中,作为感兴趣区域
//3.进行Shi-Tomasi角点检测
goodFeaturesToTrack(dst_cornerSubPix_gray,//输入图像
corners,//检测到的角点的输出向量
value,//角点的最大数量
qualityLevel,//角点检测可接受的最小特征值
minDistance,//角点之间的最小距离
Mat(),//感兴趣区域
blockSize,//计算导数自相关矩阵时指定的邻域范围
false,//不使用Harris角点检测
k);//权重系数
//4.输出文字信息
cout << "\n\t>-------------此次检测到的角点数量为:" << corners.size() << endl;
//5.绘制检测到的角点
RNG rng(1234);
int r = 4;
for (unsigned int i = 0; i < corners.size(); i++)
{
//以随机的颜色绘制出角点
circle(copy, corners[i], r, Scalar(rng.uniform(0, 255), rng.uniform(0, 255),rng.uniform(0, 255)), -1, 8, 0);
}
//6.显示(更新)窗口
imshow("output", copy);
//7.亚像素角点检测的参数设置
Size winSize = Size(5, 5);
Size zeroZone = Size(-1, -1);
TermCriteria criteria = TermCriteria(TermCriteria::EPS + TermCriteria::MAX_ITER, 40, 0.001);
//8.计算出亚像素角点位置
cornerSubPix(dst_cornerSubPix_gray, corners, winSize, zeroZone, criteria);
//9.输出角点信息
for (int i = 0; i < corners.size(); i++)
{
cout << " \t>>精确角点坐标[" << i << "] (" << corners[i].x << "," << corners[i].y << ")" << endl;
}
}