OpenCV - Harris角点检测原理详解外加源码分析

  网上有很多Harris角点检测的资料,但是或多或少有一些缺陷,有的讲清楚了原理,却没有给出代码的实现,有的给出了代码的实现,却没给出原理性的解释。为此,我找出了几篇博客,将他们综合整理在一起,有一套理论到代码的完整实现过程,这才是学一门算法或算子的正确方法。

本文主要参考博客:

原理分析博文:http://blog.csdn.net/newthinker_wei/article/details/45603583

代码测试博文:http://blog.csdn.net/dcrmg/article/details/52551294

源码分析博文:http://blog.csdn.net/ZengDong_1991/article/details/45563301

OpenCV - Harris角点检测原理详解外加源码分析_第1张图片

原理部分正文:

Harris角点检测算子是于1988年由CHris Harris & Mike Stephens提出来的。在具体展开之前,不得不提一下Moravec早在1981就提出来的Moravec角点检测算子。

1.Moravec角点检测算子

Moravec角点检测的思想其实特别简单,在图像上取一个W*W的滑动窗口,不断的移动这个窗口并检测窗口的像素变化情况E。

A. 窗口图像平坦 ---------------E的变化不大

B.窗口图像是一条边 ------------1.沿边滑动E的变化不大      2.垂直于边滑动,E的变化会很大

C.窗口图像为一个角点 ------------窗口图像沿任何方向移动,E的值变化都会很大

OpenCV - Harris角点检测原理详解外加源码分析_第2张图片

上图很形象的描述了上述过程。


数学表达式也很简单:



其中(x,y)就表示四个移动方向(1,0)(0,1)(1,1)(-1,1)


E --------------------像素的变化值

Moravec算子对四个移动方向加权求和来确定变化的大小

然后设定阈值,来确定到底是边还是角点



2.Harris角点检测算子(Moravec 算子的改进版)

在原文中,作者提出了三点Moravec算子的缺陷并提出了改良方法


1.Moravec算子方向依赖性太强(只移动了4个45度角的离散方向)

OpenCV - Harris角点检测原理详解外加源码分析_第3张图片

其中:           

OpenCV - Harris角点检测原理详解外加源码分析_第4张图片


可以推出:

OpenCV - Harris角点检测原理详解外加源码分析_第5张图片


2.由于Moravec算子采用的是正方形Windows, 因此E的响应比较容易受到干扰(????这个地方有点不明白)

                    Harris采用了一个较为平滑的窗口--------------高斯函数:


3.Moravec算子对边缘响应过于灵敏。为此,Harris提出了对E进行变形:


变成了二次型(大家对这个概念肯定不陌生,但就是想不起来,不要紧,我看到这里也是这样,后面会有详细的讲解),其中,

OpenCV - Harris角点检测原理详解外加源码分析_第6张图片

α,β表示矩阵M的特征值,这样会产生三种情况:

A.α,β都很小,说明高斯Windows中的图像接近平坦

B.如果一个大一个小,则表示检测到边

C.α,β都很大,那么表示检测到角点。


矩阵理论不说了,个人觉得非常难学,更不用说把那些概念牢记于心,一般都是用到了再来翻阅,为了防止今后再花时间去翻阅

几个概念特地copy如下,博主的地址已经在楼上给出,相信看完下面的补充大家的不解之谜都会得到解决。

再次给出来,下面这一段是完全copy的http://blog.csdn.net/newthinker_wei/article/details/45603583


1. 关于特征值和特征向量

特征值的特征向量的概念忘了就自己查吧,这里只说关键的。对于实对称矩阵M(设阶数为n),则一定有n个实特征值,每个特征值对应一组特征向量(这组向量中所有向量共线),不同特征值对应的特征向量间相互正交;(注意这里说的是实对称矩阵,不是所有的矩阵都满足这些条件)


2. 关于对角化:
对角化是指存在一个正交矩阵Q,使得  Q’MQ 能成为一个对角阵(只有对角元素非0),其中Q’是Q的转置(同时也是Q的逆,因为正交矩阵的转置就是其逆)。一个矩阵对角化后得到新矩阵的行列式和矩阵的迹(对角元素之和)均与原矩阵相同。如果M是n阶实对称矩阵,则Q中的第 j 列就是第 j 个特征值对应的一个特征向量(不同列的特征向量两两正交)。


3. 关于二次型:
对于一个n元二次多项式,f(x1,x2....xn) = ∑ ( aij*xi*xj ) ,其中 i 和 j 的求和区间均为 [1,n] ,可将其各次的系数 aij 写成一个n*n矩阵M,由于 aij 和 aji 的对称等价关系,一般将 aij 和 aji 设为一样的值,均为 xi*xj 的系数的二分之一。这样,矩阵M就是实对称矩阵了。即二次型的矩阵默认都是实对称矩阵


4. 关于二次型的标准化(正交变换法):
二次型的标准化是指通过构造一个n阶可逆矩阵 C,使得向量 ( x1,x2...xn ) = C * (y1,y2...yn),把n维向量 x 变换成n维向量 y ,并代入f(x1,x2....xn) 后得到 g(y1,y2...yn),而后者的表达式中的二次项中不包含任何交叉二次项 yi*yj(全部都是平方项 yi^2),也即表达式g的二次型矩阵N是对角阵。用公式表示一下 f 和 g ,(下面的表达式中 x 和 y都代表向量,x' 和 y' 代表转置)


f = x' * M * x ;


g = f = x' * M * x = (Cy)' * M * (Cy) = y' * (C'MC) * y = y' * N * y  ;


因此 C‘MC = N。正交变换法,就是直接将M对角化得到N,而N中对角线的元素就是M的特征值。正交变换法中得到的 C 正好是一个正交矩阵,其每一列都是两两正交的单位向量,因此 C 的作用仅仅是将坐标轴旋转(不会有放缩)。


OK,基础知识补充完了,再来说说Harris角点检测中的特征值是怎么回事。这里的 M 是

OpenCV - Harris角点检测原理详解外加源码分析_第7张图片

将M对角化后得到矩阵N,他们都是2阶矩阵,且N的对角线元素就是本文中提到的 α 和 β。

本来 E(x,y) = A*x^2 + 2*C*x*y + B*y^2 ,而将其标准后得到新的坐标 xp和yp,这时表达式中就不再含有交叉二次项,新表达式如下:

E(x,y) = Ep (xp,yp) = α*xp^2 + β*yp^2,

我们不妨画出 Ep (xp,yp) = 1 的等高线L ,即

α*xp^2 + β*yp^2 = 1 ,

可见这正好是(xp,yp)空间的一个椭圆,而α 和 β则分别是该椭圆长、短轴平方的倒数(或者反过来),且长短轴的方向也正好是α 和 β对应的特征向量的方向。由于(x,y)空间只是 (xp,yp)空间的旋转,没有放缩,因此等高线L在(x,y)空间也是一个全等的椭圆,只不过可能是倾斜的。


现在就能理解下面的图片中出现的几个椭圆是怎么回事了,图(a)中画的正是高度为 1 的等高线,(其他”高度“处的等高线也是椭圆,只不过长短轴的长度还要乘以一个系数)。其他的几幅图片中可以看到,“平坦”区域由于(高度)变化很慢,等高线(椭圆)就比较大;而”边缘“区域则是在一个轴向上高度变化很快,另一个与之垂直的轴向上高度变化很慢,因此一个轴很长一个轴很短;“角点”区域各个方向高度都变化剧烈,因此椭圆很小。我们人眼可以直观地看到椭圆的大小胖瘦,但如何让计算机识别这三种不同的几何特征呢?为了能区分出角点、边缘和平坦区域我们现在需要用α 和 β构造一个特征表达式,使得这个特征式在三种不同的区域有明显不同的值。一个表现还不错的特征表达式就是:

(αβ) - k(α+β)^2

表达式中的 k 的值怎么选取呢?它一般是一个远小于 1 的系数,OpenCV的默认推荐值是 0.04(=0.2的平方),它近似地表达了一个阈值:当椭圆短、长轴的平方之比(亦即α 和 β两个特征值之比)小于这个阈值时,认为该椭圆属于“一个轴很长一个轴很短”,即对应的点会被认为是边缘区域。

对于边缘部分,(假设较大的特征值为β)由于 β>>α且α
(αβ) - k(α+β)^2 ≈ αβ - kβ^2 < (kβ)β - kβ^2 = 0

即边缘部分的特征值小于0 ;

对于非边缘部分,α 和 β相差不大,可认为 (α+β)^2 ≈ 4αβ,因此特征式:


(αβ) - k(α+β)^2 ≈ αβ - 4kαβ = ( 1 - 4k ) * αβ

由于 k 远小于1,因此 1 - 4k ≈ 1,这样特征式进一步近似为:

(αβ) - k(α+β)^2 ≈ αβ

在角点区域,由于α 和 β都较大,对应的特征式的值也就很大;而在平坦区域,特征式的值则很小。



因此,三种不同区域的判别依据就是: 如果特征表达式的值为负,则属于边缘区域;如果特征表达式的值较大,则属于角点区域;如果特征表达式的值很小,则是平坦区域。

最后,由于αβ和(α+β)正好是M对角化后行列式和迹,再结合上面补充的基础知识第2条中提到的行列式和迹在对角化前后不变,就可以得到 (αβ) - k(α+β)^2 = det(M) - k*Tr(M)^2,这就是Harris检测的表达式。

OpenCV - Harris角点检测原理详解外加源码分析_第8张图片

上述椭圆的来源上述注解中说的很清楚(见红色部分

接着有人又要问了,你怎么知道我检测到α,β算大还是算小?对此天才哈里斯定义了一个角点响应函数:

OpenCV - Harris角点检测原理详解外加源码分析_第9张图片

我们惊喜的发现,R为正值时,检测到的是角点,R为负时检测到的是边,R很小时检测到的是平坦区域。至于他怎么想出来的,

天才的思维谁知道呢?哈哈哈哈

OpenCV - Harris角点检测原理详解外加源码分析_第10张图片

Harris角点检测算法由诸多优点A旋转不变性,椭圆转过一定角度但是其形状保持不变(特征值保持不变)

OpenCV - Harris角点检测原理详解外加源码分析_第11张图片

B对于图像灰度的仿射变化具有部分的不变性,由于仅仅使用了图像的一阶导数,对于图像灰度平移变化不变;对于图像灰度尺度变化不变


当然Harris也有许多不完善的地方: A它对尺度很明感,不具备几何尺度不变性。

OpenCV - Harris角点检测原理详解外加源码分析_第12张图片

B提取的角点是像素级的。



好的,理论部分就到此为止,如有任何问题,欢迎留言交流,我们再来看看OpenCV Harris角点的实现。

OpenCV中cornerHarris函数可用于检测图像的Harris角点。 cornerHarris函数名参数的说明

void cornerHarris( InputArray src,  //输入8bit 单通道灰度Mat矩阵
                    OutputArray dst, //用于保存Harris角点检测结果,32位单通道,大小与src相同
                    int blockSize,   //滑块窗口的尺寸
                    int ksize,       //Sobel边缘检测滤波器大小
                    double k,        //Harries中间参数,经验值0.04~0.06
                    int borderType=BORDER_DEFAULT   //插值类型
                    );


Samples:

#include 
#include 
#include 
#include 

using namespace cv;
using namespace std;


Mat image;
Mat imageGray;
int thresh=200;
int MaxThresh=255;

void Trackbar(int,void*);  //阈值控制

int main()  
{  
	image=imread("Test.bmp");
	cvtColor(image,imageGray,CV_RGB2GRAY);
	GaussianBlur(imageGray,imageGray,Size(5,5),1); // 滤波
	namedWindow("Corner Detected");
	createTrackbar("threshold:","Corner Detected",&thresh,MaxThresh,Trackbar);
	imshow("Corner Detected",image);
	Trackbar(0,0);
	waitKey();
	return 0;
}  

void Trackbar(int,void*)
{
	Mat dst,dst8u,dstshow,imageSource;
	dst=Mat::zeros(image.size(),CV_32FC1);  
	imageSource=image.clone();
	cornerHarris(imageGray,dst,3,3,0.04,BORDER_DEFAULT);
	normalize(dst,dst8u,0,255,CV_MINMAX);  //归一化
	convertScaleAbs(dst8u,dstshow);
	imshow("dst",dstshow);  //dst显示
	for(int i=0;i(i,j)>thresh)  //阈值判断
			{
				circle(imageSource,Point(j,i),2,Scalar(0,0,255),2); //标注角点
			}
		}
	}
	imshow("Corner Detected",imageSource);
}

两个可能不懂的函数注解:

1.convertScaleAbs函数是OpenCV中的函数,使用线性变换转换输入数组元素成8位无符号整型。

2.cvCircle(CvArr* img, CvPoint center, int radius, CvScalar color, int thickness=1, int lineType=8, int shift=0)

img为源图像指针

center为画圆的圆心坐标

radius为圆的半径

color为设定圆的颜色,规则根据B(蓝)G(绿)R(红)

thickness 如果是正数,表示组成圆的线条的粗细程度。否则,表示圆是否被填充

line_type 线条的类型。默认是8

shift 圆心坐标点和半径值的小数点位数



运行效果如下:

OpenCV - Harris角点检测原理详解外加源码分析_第13张图片


源码分析环节


//--------------------【cornerHarris源码分析】------------------------------------
//Harries角点实现函数,截取cornerHarries中的关键代码做了简化
void myHarries( const Mat& src, Mat& eigenv, int block_size, int aperture_size, double k=0.)
{
    eigenv.create(src.size(), CV_32F);
    Mat Dx, Dy;
    //soble operation get Ix, Iy
    Sobel(src, Dx, CV_32F, 1, 0, aperture_size);
    Sobel(src, Dy, CV_32F, 0, 1, aperture_size);

    //get covariance matrix
    Size size = src.size();
    Mat cov(size, CV_32FC3);     //创建一个三通道cov矩阵分别存储[Ix*Ix, Ix*Iy, Iy*Iy];

    for( int i=0; i < size.height;i++)
    {
        float* cov_data = cov.ptr(i);
        const float* dxdata = Dx.ptr(i);
        const float* dydata = Dy.ptr(i);

        for( int j=0; j < size.width; j++ )
        {
            float dx = dxdata[j];
            float dy = dydata[j];

            cov_data[j*3] = dx * dx;    //即  Ix*Ix
            cov_data[j*3 + 1] = dx*dy;  //即  Ix*Iy
            cov_data[j*3 + 2] = dy*dy;  //即  Iy*Iy
        }
    }


    //方框滤波W(x,y)卷积, 也可用高斯核加权...
    //W(Y,Y)与矩阵cov卷积运算得到 H 矩阵,后面通过H矩阵的特征值决定是否是角点
    boxFilter(cov, cov, cov.depth(), Size(block_size,block_size),Point(-1,-1),false);

    //cale Harris
    size = cov.size();
    if( cov.isContinuous() && eigenv.isContinuous())
    {
        size.width *= size.width;
        size.height = 1;
        //cout << "yes"<(i);
        float* dstPtr = eigenv.ptr(i);

        for( int j = 0; j < size.width; j++)
        {
            float a = covPtr[j*3];
            float b = covPtr[j*3 + 1];
            float c = covPtr[j*3 + 2];

            //根据公式 R = det(H) - k*trace(H)*trace(H);
            dstPtr[j] = (float)(a*c - b*b - k*(a + c)(a + c));
        }
    }

    double max, min;
    minMaxLoc(eigenv, &min, &max);
    //cout<< max << endl;
    double threshold = 0.1*max;
    cv::threshold(eigenv, eigenv,threshold, 1, CV_THRESH_BINARY);   //eigenv的类型是CV_32F,
    imshow("eigenv", eigenv);
}
好的,到此就结束了,还要去联系Qt,下篇继续我们的shi-tomasi角点学习。






你可能感兴趣的:(图像处理,opencv)