前言:

       在图像中角点是一个重要的局部特征,它决定了图像中关键区域的形状,体现了图像中重要的特征信息,所以在目标识别、图像匹配、图像重构方面角点具有十分重要的意义。 图像中角点的数量远比总像素数小,如果通过角点就能完成一些功能的话,将极大地提高处理效率。 对角点的定义一般分为以下三种:图像边界曲线上具有极大曲率值的点;图像中梯度值和梯度变化率都很高的点;图像边界方向变化不连续的点。 定义不同,角点的提取方法也不尽相同。目前,角点检测方法主要有2大类:

     1)基于图像边缘轮廓特征的方法。此方法需要经过图像预分割、轮廓链码提取和角点检测,如基于边界曲率的角点检测,基于边界小波变换的角点检测以及基于边界链码的角点检测。

来判断角点的存在性。典型代表有Harris算法、Susan算法、Moravec算法等。


一、Harris算法介绍

1.1 Harris算法原理

      Harris 是 Harris 和 Stephens 在 1988 年提出,专门针对 Moravec 算子的改进版。Harris 算子,又称 Plessey算子,它基于与 Moravec 相同的角点定义,即定义在各个方向上灰度值变化的点。与 Moravec 算子的不同之处在于局部自相关测量结果的估计方式,Moravec 算子只对八方向离散的移动方向计算灰度变化,而 Harris算子允许获得所有方向上的自相关变化( 也就是灰度,它用一个自相关函数来计算灰度信号在二维方向上具有明显变化的像素点位置,构造一个与相关函数相关的矩阵M,而通过比较矩阵M的特征值的大小可以轻松地提取到相应的角点。

       在图像中搜索有价值的特征点时,使用角点是一种不错的方法。角点是很容易在图像中定位的局部特征,并且大量存在于人造物体中(例如墙壁、门、窗户、桌子等产生的角点)。角点的价值在于它是两条边缘线的接合点,是一种二维特征,可以被精确的定位。与此相反的是位于均匀区域或物体轮廓上的点以及在同一物体的不同图像上很难重复精确定位的点。Harris特征检测就是检测角点的经典方法。接下来将详细探讨这个方法。

        如果在各个方向上移动这个小窗口,窗口内的灰度发生了较大的变化,那么说明窗口内存在角点。如果在各个方向移动,灰度几乎不变,说明是平坦区域;如果只沿着某一个方向移动,灰度几乎不变,说明是直线;如果沿各个方向移动,灰度均发生变化,说明是角点。

        设图像窗口平移

opencv画曲率为30度的曲线 opencv计算曲率_角点检测

,产生的灰度变化为

opencv画曲率为30度的曲线 opencv计算曲率_角点检测_02

,则:

opencv画曲率为30度的曲线 opencv计算曲率_opencv画曲率为30度的曲线_03


opencv画曲率为30度的曲线 opencv计算曲率_角点_04

得:

opencv画曲率为30度的曲线 opencv计算曲率_角点检测_05

由于:

opencv画曲率为30度的曲线 opencv计算曲率_opencv画曲率为30度的曲线_06

于是对于局部微小的移动量

opencv画曲率为30度的曲线 opencv计算曲率_角点检测

,可以近似得到下面的表达:

opencv画曲率为30度的曲线 opencv计算曲率_灰度_08

opencv画曲率为30度的曲线 opencv计算曲率_opencv画曲率为30度的曲线_09

为二维矩阵,且:

opencv画曲率为30度的曲线 opencv计算曲率_角点检测_10

其中:

opencv画曲率为30度的曲线 opencv计算曲率_角点检测_11

      其中,

opencv画曲率为30度的曲线 opencv计算曲率_opencv画曲率为30度的曲线_12

表示 x 方向的梯度,

opencv画曲率为30度的曲线 opencv计算曲率_灰度_13

表示 y 方向的梯度,

opencv画曲率为30度的曲线 opencv计算曲率_角点检测_14

为高斯函数。矩阵

opencv画曲率为30度的曲线 opencv计算曲率_opencv画曲率为30度的曲线_09

的特征值是自相关函数的一阶曲率。

opencv画曲率为30度的曲线 opencv计算曲率_opencv画曲率为30度的曲线_09

特征值的大小与特征点的性质息息相关。即当两个特征值都比较小时,则此点可能位于平坦区,不为角点或边界点; 当两个特征值一个较大、而另一个却相对较小时,则此点位于边界上,属于边界

点; 当两个特征值均相对较大时,则此点沿任意方向的曲率都较大,为需要提取的角点。

     一般地,为了避免计算矩阵

opencv画曲率为30度的曲线 opencv计算曲率_opencv画曲率为30度的曲线_09

的特征值,常通过下式来计算特征点的响应函数,亦称兴趣值:

opencv画曲率为30度的曲线 opencv计算曲率_角点_18

       其中,

opencv画曲率为30度的曲线 opencv计算曲率_灰度_19

为矩阵 

opencv画曲率为30度的曲线 opencv计算曲率_opencv画曲率为30度的曲线_09

的行列式,

opencv画曲率为30度的曲线 opencv计算曲率_opencv画曲率为30度的曲线_21

为矩阵

opencv画曲率为30度的曲线 opencv计算曲率_opencv画曲率为30度的曲线_09

的迹,即 

opencv画曲率为30度的曲线 opencv计算曲率_opencv画曲率为30度的曲线_09

 特征值之和。

opencv画曲率为30度的曲线 opencv计算曲率_角点检测_24

为经验值常数,一般在 0. 04 ~ 0.06 范围内取值。为了计算方便,文中采用如下角点响应函数计算兴趣值从而判定出角点,即:

opencv画曲率为30度的曲线 opencv计算曲率_灰度_25

      其中,ε 表示任意小的正数。显然采用这中方式计算可以避免

opencv画曲率为30度的曲线 opencv计算曲率_角点检测_24

选择的随机性。


1.2   Harris算法步骤

(1)利用差分算子对图像进行滤波,并计算图像中每个像素点的 

opencv画曲率为30度的曲线 opencv计算曲率_opencv画曲率为30度的曲线_27


opencv画曲率为30度的曲线 opencv计算曲率_角点_28

以及

opencv画曲率为30度的曲线 opencv计算曲率_角点检测_29

;(2) 对 

opencv画曲率为30度的曲线 opencv计算曲率_opencv画曲率为30度的曲线_27


opencv画曲率为30度的曲线 opencv计算曲率_角点_28

以及

opencv画曲率为30度的曲线 opencv计算曲率_角点检测_29

进行高斯平滑,以去除噪声。在此过程中可通过归一化将模板参数和置 1;(3) 计算原图像每个对应点的兴趣值 

opencv画曲率为30度的曲线 opencv计算曲率_角点检测_33

,并进行局部非极大值抑制;

(4) 设定阈值,如果兴趣值大于这个阈值并且在某领域内是局部最大值,则认为该点是需要提取的角点。


1.3 Harris算法的不足

    虽然 Harris 算法相较于其他众多基于灰度的角点提取算法具有明显的优势,但它仍然存在一些不足:
    在经典的 Harris 角点检测中,当对角点的兴趣值进行非极大值抑制来确定局部极大值的时候,角点的提取效果几乎完全由设定的阈值大小决定。而阈值的大小也与所提取的角点数量息息相关,一般情况下,阈值越大提取的角点越少,极易造成正确角点的丢失; 阈值越小提取的角点数越多,也会带来很多伪角点。因此,在运用 Harris 算法进行角点检测时,阈值这个经验值的选取和设定对角点提取具有很大的影响。


二、代码实现

#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace cv;
using namespace std;

/************************************************/
void ConvertBGR_img2GRAY_img(const Mat &BGR_img, Mat &Gray_img)
{
	//这个函数实现将彩色图转化成灰度图,利用常用公式:Gray = R*0.299 + G*0.587 + B*0.114进行自编程实现
	//第一个参数BGR_img表示输入的彩色RGB图像的引用;    
	//第二个参数Gray_img表示转换后输出的灰度图像的引用;                  
	if (BGR_img.empty() || BGR_img.channels() != 3)
	{
		return;
	}
	Gray_img = Mat::zeros(BGR_img.size(), CV_8UC1);       //创建一张与输入图像同大小的单通道灰度图像  
	uchar *pointImage = BGR_img.data;                        //取出存储图像像素的数组的指针  
	uchar *pointImageGray = Gray_img.data;
	int height = BGR_img.rows;
	int width = BGR_img.cols;
	for (int i = 0; i < height; i++)
	{
		for (int j = 0; j < width; j++)
		{
			*(pointImageGray + i*Gray_img.step[0] + j*Gray_img.step[1]) = (uchar)(0.114*(pointImage + i*BGR_img.step[0] + j*BGR_img.step[1])[0] + 0.587*(pointImage + i*BGR_img.step[0] + j*BGR_img.step[1])[1] + 0.299*(pointImage + i*BGR_img.step[0] + j*BGR_img.step[1])[2]);
		}
	}

}
/**********************************************/

/*******************************************************/
//非极大值抑制和阈值检测
void LocalMaxValue(Mat &resultData, Mat &ResultImage, int kSize)
{
	int r = kSize / 2;
	for (int i = r; i < ResultImage.rows - r; i++)
	{
		for (int j = r; j < ResultImage.cols - r; j++)
		{
			if (resultData.at<double>(i, j) > resultData.at<double>(i - 1, j - 1) &&
				resultData.at<double>(i, j) > resultData.at<double>(i - 1, j) &&
				resultData.at<double>(i, j) > resultData.at<double>(i - 1, j + 1) &&
				resultData.at<double>(i, j) > resultData.at<double>(i, j - 1) &&
				resultData.at<double>(i, j) > resultData.at<double>(i, j + 1) &&
				resultData.at<double>(i, j) > resultData.at<double>(i + 1, j - 1) &&
				resultData.at<double>(i, j) > resultData.at<double>(i + 1, j) &&
				resultData.at<double>(i, j) > resultData.at<double>(i + 1, j + 1))
			{
				if ((int)resultData.at<double>(i, j) >15000)
				{
					circle(ResultImage, Point(j, i), 5, Scalar(0, 0, 255), 2, 8, 0);
				}
			}
		}
	}
}
/*********************************************************/

//  主函数
int main()
{
	const Mat src_img = imread("test11.png");
	if (src_img.empty())
	{
		printf("could not load image...\n");
		return -1;
	}
	namedWindow("原图:", CV_WINDOW_AUTOSIZE);
	imshow("原图:", src_img);

	// 将彩色图转化为灰度图,调用OpenCV提供的cvtColor接口
	Mat gray_img;
	ConvertBGR_img2GRAY_img(src_img, gray_img);
	namedWindow("灰度图", CV_WINDOW_AUTOSIZE);
	imshow("灰度图", gray_img);

	// 第一步:用sobel算子计算度图像的水平和垂直方向上的差分
	Mat image_con_sobel_Ix, image_con_sobel_Iy;
	//image_con_sobel_Ix = sobel(gray_img,1,0, 3, BORDER_DEFAULT);
	//image_con_sobel_Iy = sobel(gray_img,0,1, 3, BORDER_DEFAULT);
	Sobel(gray_img, image_con_sobel_Ix, CV_64F, 1, 0, 3);
	Sobel(gray_img, image_con_sobel_Iy, CV_64F, 0, 1, 3);

	//convertScaleAbs(image_con_sobel_Ix, image_con_sobel_Ix);
	//convertScaleAbs(image_con_sobel_Iy, image_con_sobel_Iy);

	//  第二步:计算Ixx,Iyy,Ixy
	Mat image_con_sobel_Ixx, image_con_sobel_Iyy, image_con_sobel_Ixy;
	multiply(image_con_sobel_Ix, image_con_sobel_Ix, image_con_sobel_Ixx, 1.0, CV_64FC1);
	multiply(image_con_sobel_Iy, image_con_sobel_Iy, image_con_sobel_Iyy, 1.0, CV_64FC1);
	multiply(image_con_sobel_Ix, image_con_sobel_Iy, image_con_sobel_Ixy, 1.0, CV_64FC1);

	//第三步:对Ixx,Iyy,Ixy进行高斯平滑
	Mat blurimage_Ixx, blurimage_Iyy, blurimage_Ixy;
	GaussianBlur(image_con_sobel_Ixx, blurimage_Ixx, Size(3, 3), 5);
	GaussianBlur(image_con_sobel_Iyy, blurimage_Iyy, Size(3, 3), 5);
	GaussianBlur(image_con_sobel_Ixy, blurimage_Ixy, Size(3, 3), 5);

	//第四步:计算响应函数,为避免K值的随机性,我采用另外一种响应函数表达式
	Mat Response_data;
	Response_data = (blurimage_Ixx.mul(blurimage_Iyy) - blurimage_Ixy.mul(blurimage_Ixy)) / (blurimage_Ixx + blurimage_Iyy);

	//第五步:非极大值抑制和阀值检测    
	Mat ResultImage = gray_img.clone();
	LocalMaxValue(Response_data, ResultImage, 3);
	namedWindow("结果", CV_WINDOW_AUTOSIZE);
	imshow("结果", ResultImage);
	waitKey(0);
	return 0;
}

运行程序,结果如下:

opencv画曲率为30度的曲线 opencv计算曲率_灰度_34

单独观察:

opencv画曲率为30度的曲线 opencv计算曲率_opencv画曲率为30度的曲线_35


三、使用API实现Harris角点检测

在OpenCV中提供了cornerHarris()其函数原型和参数解析:

opencv画曲率为30度的曲线 opencv计算曲率_角点_36

其中:


  • 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可,且需为单通道8位或者浮点型图像。
  • 第二个参数,OutputArray类型的dst,函数调用后的运算结果存在这里,即这个参数用于存放Harris角点检测的输出结果,和源图片有一样的尺寸和类型。
  • 第三个参数,int类型的blockSize,表示邻域的大小,对每个像素,考虑blockSize×blockSize大小的邻域,在邻域上计算图像的2x2梯度的协方差矩阵M。
  • 第四个参数,int类型的ksize,为Soble算子核尺寸,如果小于0,采用3×3的Scharr滤波器。
  • 第五个参数,double类型的k,为角点响应函数中的经验常数(0.04~0.06)。
  • 第六个参数,int类型的borderType,图像像素的边界模式,注意它有默认值BORDER_DEFAULT。更详细的解释,参考borderInterpolate( )函数。

代码实现:

#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace cv;
using namespace std;




string out_title = "Harris角点检测";
int thresh = 100;
int max_count = 255;
Mat src_img, gray_img;
void Harris_callback(int, void*);

int main()
{
	src_img = imread("test11.png");
	if (src_img.empty())
	{
		printf("could not load image...\n");
		return -1;
	}
	namedWindow("原图:", CV_WINDOW_AUTOSIZE);
	imshow("原图:", src_img);
	namedWindow(out_title, CV_WINDOW_AUTOSIZE);
	cvtColor(src_img, gray_img, CV_BGR2GRAY);
	createTrackbar("阈值", out_title, &thresh, max_count, Harris_callback);
	Harris_callback(0, 0);
	waitKey(0);
	return 0;
}

void Harris_callback(int, void*)
{
	Mat dst_img, norm_dst_img, scale_dst_img;
	dst_img = Mat::zeros(gray_img.size(), CV_32FC1);
	int  blockSize = 2;
	int ksize = 3;
	double k = 0.04;
	cornerHarris(gray_img, dst_img, blockSize, ksize, k, BORDER_DEFAULT);   //调用API
	normalize(dst_img, norm_dst_img, 0, 255, NORM_MINMAX, CV_32FC1, Mat());     // 调用归一化函数进行归一化处理
	convertScaleAbs(norm_dst_img, scale_dst_img);      //将图像转化为8位图
	Mat result_img = src_img.clone();
	for (int r = 0; r < result_img.rows; ++r)
	{
		uchar *ptr_row = scale_dst_img.ptr<uchar>(r);
		for (int c = 0; c < result_img.cols; ++c)
		{
			int value = int(ptr_row[c]);
			if (value > thresh)
			{
				circle(result_img, Point(c, r), 2, Scalar(0, 0, 255), 2, 8, 0);
			}
		}
	}
	imshow(out_title, result_img);
}

运行程序,结构如下:

opencv画曲率为30度的曲线 opencv计算曲率_角点检测_37

拖动TrackBar以获得不同阈值条件下的结果:

opencv画曲率为30度的曲线 opencv计算曲率_角点检测_38

opencv画曲率为30度的曲线 opencv计算曲率_opencv画曲率为30度的曲线_39