OpenCV角点检测源代码分析(Harris和ShiTomasi角点)

时间:2024-01-20 11:45:45

OpenCV中常用的角点检测为Harris角点和ShiTomasi角点。

以OpenCV源代码文件 .\opencv\sources\samples\cpp\tutorial_code\TrackingMotion\cornerDetector_Demo.cpp为例,主要分析其中的这两种角点检测源代码。角点检测数学原理请参考我之前转载的一篇博客 http://www.cnblogs.com/riddick/p/7645904.html,分析的很详细,不再赘述。本文主要分析其源代码:

1. Harris角点检测  

  根据数学上的推导,可以根据图像中某一像素点邻域内构建的协方差矩阵获取特征值和特征向量,根据特征值建立特征表达式,如下:                

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

  可以根据上式的值得大小来判断该像素点是平坦区域内点、边界点还是角点。下面说一下怎么在原图像中建立协方差矩阵并求取特征值α和β和特征向量t1, t2。

  该例程代码中调用cornerEigenValsAndVecs()函数计算特征值和特征向量。函数原型如下:

void cv::cornerEigenValsAndVecs( InputArray _src, OutputArray _dst, int blockSize, int ksize, int borderType )

  src为输入灰度图像,dst为输出(6通道 CV_32FC(6),依次保存的是α, t1,  β, t2),blockSize为邻域大小,ksize为sobel求取微分时的窗口大小。  

  该函数内部调用cornerEigenValsVecs()函数,原型如下: 

static void  cornerEigenValsVecs( const Mat& src, Mat& eigenv, int block_size,int aperture_size, int op_type, double k=.,int borderType=BORDER_DEFAULT )

  主要介绍一下op_type这个参数,该参数是一个枚举值,有三个值可以选择(MINEIGENVAL, HARRIS, EIGENVALSVECS)

  ①MINEIGENVAL用于ShiTomasi角点检测中获取两个特征值中较小的那个值,用以获取强角点,随后介绍;

  ②HARRIS在cornerHarris()函数中用到,用于直接利用协方差矩阵获取特征表达式值的大小,k值在此时会被设置,通常为0.04,其他情况下设置为0;

  ③EIGENVALSVECS就是本例程中设置的,求取两个特征值和特征向量。

  在cornerEigenValsVecs()函数中,先利用sobel算子求水平方向和竖直方向的微分,窗口大小为前述,如下代码:         

Mat Dx, Dy;
if( aperture_size > )
{
Sobel( src, Dx, CV_32F, , , aperture_size, scale, , borderType );
Sobel( src, Dy, CV_32F, , , aperture_size, scale, , borderType );
}
else
{
Scharr( src, Dx, CV_32F, , , scale, , borderType );
Scharr( src, Dy, CV_32F, , , scale, , borderType );

  然后初始化协方差矩阵cov(三通道,依次保存dx*dx, dx*dy, dy*dy),如下:

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

  接下来对协方差矩阵进行在前述设定窗口内进行均值(盒式)滤波:

    boxFilter(cov, cov, cov.depth(), Size(block_size, block_size),
Point(-,-), false, borderType ); if( op_type == MINEIGENVAL )
calcMinEigenVal( cov, eigenv );
else if( op_type == HARRIS )
calcHarris( cov, eigenv, k );
else if( op_type == EIGENVALSVECS )
calcEigenValsVecs( cov, eigenv );

  然后就是利用滤波后的协方差矩阵求取特征值和特征向量了,根据设定不同的op_type调用不同的函数计算,本例程中为调用最后一个calcEigenValsVecs()函数,该函数如下:

static void calcEigenValsVecs( const Mat& _cov, Mat& _dst )
{
Size size = _cov.size();
if( _cov.isContinuous() && _dst.isContinuous() )
{
size.width *= size.height;
size.height = ;
} for( int i = ; i < size.height; i++ )
{
const float* cov = _cov.ptr<float>(i);
float* dst = _dst.ptr<float>(i);
//调用该函数计算2x2协方差矩阵的特征值和特征向量
eigen2x2(cov, dst, size.width);
}
}

  该函数中调用eigen2x2()函数计算每个像素点处协方差矩阵的2个特征值和2个特征向量,协方差矩阵为如下形式,数据都保存在cov的三个通道中:

OpenCV角点检测源代码分析(Harris和ShiTomasi角点)

   eigen2x2()函数如下:2x2矩阵特征值和特征向量的计算,有线性代数基础的都学过,就不再赘述

static void eigen2x2( const float* cov, float* dst, int n )
{
for( int j = ; j < n; j++ )
{
double a = cov[j*];
double b = cov[j*+];
double c = cov[j*+]; double u = (a + c)*0.5;
double v = std::sqrt((a - c)*(a - c)*0.25 + b*b);
     
     //计算两个特征值l1,l2
double l1 = u + v;
double l2 = u - v;
     //计算特征值l1对应的特征向量
double x = b;
double y = l1 - a;
double e = fabs(x); if( e + fabs(y) < 1e- )
{
y = b;
x = l1 - c;
e = fabs(x);
if( e + fabs(y) < 1e- )
{
e = ./(e + fabs(y) + FLT_EPSILON);
x *= e, y *= e;
}
} double d = ./std::sqrt(x*x + y*y + DBL_EPSILON);
//保存特征值l1及其对应的特征向量
dst[*j] = (float)l1;
dst[*j + ] = (float)(x*d);
dst[*j + ] = (float)(y*d);
//计算特征值l2对应的特征向量
x = b;
y = l2 - a;
e = fabs(x); if( e + fabs(y) < 1e- )
{
y = b;
x = l2 - c;
e = fabs(x);
if( e + fabs(y) < 1e- )
{
e = ./(e + fabs(y) + FLT_EPSILON);
x *= e, y *= e;
}
} d = ./std::sqrt(x*x + y*y + DBL_EPSILON);
//保存特征值l2及其对应的特征向量
dst[*j + ] = (float)l2;
dst[*j + ] = (float)(x*d);
dst[*j + ] = (float)(y*d);
}
}

  求得2个特征值α、β和2个特征向量之后,就是要利用特征值构建特征表达式,通过表达式的值(  (αβ) - k(α+β)^2   )来区分角点,k的值通常设置为0.04:

/* calculate Mc */
for( int j = ; j < src_gray.rows; j++ )
{ for( int i = ; i < src_gray.cols; i++ )
{
float lambda_1 = myHarris_dst.at<Vec6f>(j, i)[];
float lambda_2 = myHarris_dst.at<Vec6f>(j, i)[];
Mc.at<float>(j,i) = lambda_1*lambda_2 - 0.04f*pow( ( lambda_1 + lambda_2 ), );
}
}

  代码中利用 minMaxLoc( Mc, &myHarris_minVal, &myHarris_maxVal, , , Mat() ); 函数获取特征表达式的最大值min和最小值max,通过选取不同的阈值min<=thresh<=max,来指定大于阈值thresh的表达式值对应的点为检测出的角点。并利用circle()函数显示出来。

circle( myHarris_copy, Point(i,j), , Scalar( rng.uniform(,), rng.uniform(,), rng.uniform(,) ), -, ,  ); 

至此,Harris角点检测完成!

 2. ShiTomasi角点检测

  ShiTomasi角点提取是获取harris角点中的强角点,怎么获取强角点呢,那就是只选取两个特征值中较小的那个特征值构建特征表达式,如果较小的特征值都能够满足设定的阈值条件,那么该角点就视为强角点。

  调用  cornerMinEigenVal( src_gray, myShiTomasi_dst, blockSize, apertureSize, BORDER_DEFAULT ); 函数来获取较小的特征值,其实该函数内部依然调用上面所述的函数 cornerEigenValsVecs( src, dst, blockSize, ksize, MINEIGENVAL, , borderType ); ,然后将op_type设置为MINEIGENVAL枚举值,进而调用 static void calcMinEigenVal( const Mat& _cov, Mat& _dst ) 函数计算较小的特征值。该函数代码如下:

static void calcMinEigenVal( const Mat& _cov, Mat& _dst )
{
int i, j;
Size size = _cov.size();
#if CV_TRY_AVX
bool haveAvx = CV_CPU_HAS_SUPPORT_AVX;
#endif
#if CV_SIMD128
bool haveSimd = hasSIMD128();
#endif if( _cov.isContinuous() && _dst.isContinuous() )
{
size.width *= size.height;
size.height = ;
} for( i = ; i < size.height; i++ )
{
const float* cov = _cov.ptr<float>(i);
float* dst = _dst.ptr<float>(i);
#if CV_TRY_AVX
if( haveAvx )
j = calcMinEigenValLine_AVX(cov, dst, size.width);
else
#endif // CV_TRY_AVX
j = ; #if CV_SIMD128
if( haveSimd )
{
v_float32x4 half = v_setall_f32(0.5f);
for( ; j <= size.width - v_float32x4::nlanes; j += v_float32x4::nlanes )
{
v_float32x4 v_a, v_b, v_c, v_t;
v_load_deinterleave(cov + j*, v_a, v_b, v_c);
v_a *= half;
v_c *= half;
v_t = v_a - v_c;
v_t = v_muladd(v_b, v_b, (v_t * v_t));
v_store(dst + j, (v_a + v_c) - v_sqrt(v_t));
}
}
#endif // CV_SIMD128 for( ; j < size.width; j++ )
{
float a = cov[j*]*0.5f;
float b = cov[j*+];
float c = cov[j*+]*0.5f; //求根公式计算较小的根,即为较小的特征值
dst[j] = (float)((a + c) - std::sqrt((a - c)*(a - c) + b*b));
}
}
}

  所有像素点处较小的特征值求出后,直接将该特征值作为特征表达式的值。利用 minMaxLoc( Mc, &myHarris_minVal, &myHarris_maxVal, , , Mat() ); 函数选取最小的min和最大的max,通过调整阈值thresh来设定大于阈值thresh的为显示出来的强角点。

  至此,ShiTomasi角点检测完成!

  #自己写了一个简单的Harris和ShiTomasi角点检测的代码,如下,仅供参考:

 #include <opencv2\opencv.hpp>
#include <iostream>
#include <string> using namespace std; #define HARRIS cv::RNG rng(); void calEigen2x2(cv::Mat cov,cv::Mat &eigenValue)
{
int height = cov.rows;
int width = cov.cols; float *pCov = (float*)cov.data;
float *pEigenValue = (float*)eigenValue.data;
for (int i = ; i < height; i++)
{
for (int j = ; j < width; j++)
{
double a = pCov[(i*width + j) * + ];
double b = pCov[(i*width + j) * + ];
double c = pCov[(i*width + j) * + ]; double tmp1 = (a + c) / .;
double tmp2 = sqrtf(b*b + (a - c)*(a - c) / .); double alpha = tmp1 - tmp2;
double beta = tmp1 + tmp2; pEigenValue[(i*width + j) * + ] =(float) alpha;
pEigenValue[(i*width + j) * + ] =(float) beta;
}
}
} void myCalEigenValues(cv::Mat srcImg, cv::Mat &eigenValue, int covWin, int sobelWin)
{
//求微分
cv::Mat sobelx, sobely;
cv::Sobel(srcImg, sobelx, CV_32FC1, , , sobelWin, . / (*), , );
cv::Sobel(srcImg, sobely, CV_32FC1, , , sobelWin, . / (*), , ); cv::Mat cov = cv::Mat::zeros(srcImg.size(), CV_32FC3);
int height = srcImg.rows;
int width = srcImg.cols;
float *pSobelX = (float*)sobelx.data;
float *pSobelY = (float*)sobely.data;
float *pCov = (float*)cov.data;
for (int i = ; i < height; i++)
{
for (int j = ; j < width; j++)
{
float dx = pSobelX[i*width + j];
float dy = pSobelY[i*width + j]; pCov[(i*width + j) * + ] = dx*dx;
pCov[(i*width + j) * + ] = dx*dy;
pCov[(i*width + j) * + ] = dy*dy;
}
} cv::boxFilter(cov, cov, cov.depth(), cv::Size(covWin, covWin), cv::Point(-, -), false, ); calEigen2x2(cov, eigenValue);
} void main()
{
string imgPath = "data/srcImg/0.png";
cv::Mat srcImg = cv::imread(imgPath, );
cv::Mat grayImg;
cv::cvtColor(srcImg, grayImg, CV_BGR2GRAY);
int height = srcImg.rows;
int width = srcImg.cols; cv::Mat eigenValue = cv::Mat::zeros(grayImg.size(), CV_32FC2);
int covWin = , sobelWin = ;
myCalEigenValues(grayImg, eigenValue, covWin, sobelWin); cv::Mat Mc = cv::Mat::zeros(grayImg.size(), CV_32FC1);
#ifndef HARRIS
//计算特征表达式值
for (int i = ; i<height; i++)
{
for (int j = ; j < width; j++)
{
float alpha = eigenValue.at<float>(i, j*+);
float beta = eigenValue.at<float>(i, j*+); Mc.at<float>(i, j) = alpha*beta - 0.04f*pow((alpha + beta), );
}
}
#else //ShiTomasi
for (int i = ; i<height; i++)
{
for (int j = ; j < width; j++)
{
float alpha = eigenValue.at<float>(i, j * + );
float beta = eigenValue.at<float>(i, j * + ); float minEigenValue = (alpha > beta) ? (beta) : (alpha); Mc.at<float>(i, j) = minEigenValue;
}
}
#endif double minVal, maxVal;
cv::minMaxLoc(Mc, &minVal, &maxVal, , , cv::Mat()); double thresh = (maxVal + minVal) / .;
for (int i = ; i < height; i++)
{
for (int j = ; j < width; j++)
{
double value = (double)Mc.at<float>(i, j);
if (value > thresh)
{
cv::circle(srcImg, cv::Point(j, i), , cv::Scalar(rng.uniform(, ), rng.uniform(, ), rng.uniform(, )), -, , );
}
}
} cv::namedWindow("show", );
cv::imshow("show", srcImg);
cv::waitKey();
}

3. cornerHarris()函数详解

  前面讲述cornerEigenValsVecs()这个函数是提到op_type这个枚举类型,有三个枚举值可以设置。其中MINEIGENVAL 和  EIGENVALSVECS都在前面介绍过。而 HARRIS则在cornerHarris()函数中使用,这是一个公开的OpenCV API函数,函数原型如下:

void cv::cornerHarris( InputArray _src, OutputArray _dst, int blockSize, int ksize, double k, int borderType )

  k值为上述特征表达式中的常数项。该函数内部其实还是调用cornerEigenValsVecs()函数,只不过调用时将op_type设置为枚举值HARRIS。意思就是提取HARRIS角点,然后调用内部的 static void calcHarris( const Mat& _cov, Mat& _dst, double k ) 函数。只不过还内部函数不再计算特征值和特征向量,而是直接计算特征表达式的值。而特征表达式用下式表示:

OpenCV角点检测源代码分析(Harris和ShiTomasi角点)

  其中矩阵M就是前面说的协方差矩阵,det(M)为M的行列式,Tr(M)为M的迹。在程序中代码如下:

for( ; j < size.width; j++ )
{
float a = cov[j*];
float b = cov[j*+];
float c = cov[j*+];         //求特征表达式的值
dst[j] = (float)(a*c - b*b - k*(a + c)*(a + c));
}

  通常算出特征表达式的值后将其归一化到(0-255),然后可以直接设置阈值Thresh,特征表达式的值>Thresh对应的点视为角点。具体可以参见OpenCV例程源代码:.\opencv\sources\samples\cpp\tutorial_code\TrackingMotion\cornerHarris_Demo.cpp