opencv-角點檢測之Harris角點檢測
轉自:https://blog.csdn.net/poem_qianmo/article/details/29356187
先看看程序運行截圖:
一、引言:關於興趣點(interest points)
在圖像處理和與計算機視覺領域,興趣點(interest points),或稱作關鍵點(keypoints)、特征點(feature points) 被大量用於解決物體識別,圖像識別、圖像匹配、視覺跟蹤、三維重建等一系列的問題。我們不再觀察整幅圖,而是選擇某些特殊的點,然後對他們進行局部有的放矢的分析。如果能檢測到足夠多的這種點,同時他們的區分度很高,並且可以精確定位穩定的特征,那麽這個方法就有使用價值。
圖像特征類型可以被分為如下三種:
- <1>邊緣
- <2>角點 (感興趣關鍵點)
- <3>斑點(Blobs)(感興趣區域)
其中,角點是個很特殊的存在。他們在圖像中可以輕易地定位,同時,他們在人造物體場景,比如門、窗、桌等出隨處可見。因為角點位於兩條邊緣的交點處,代表了兩個邊緣變化的方向上的點,,所以他們是可以精確定位的二維特征,甚至可以達到亞像素的精度。且其圖像梯度有很高的變化,這種變化是可以用來幫助檢測角點的。需要註意的是,角點與位於相同強度區域上的點不同,與物體輪廓上的點也不同,因為輪廓點難以在相同的其他物體上精確定位。
二、角點檢測算法的分類
在當前的圖像處理領域,角點檢測算法可歸納為三類:
- <1>基於灰度圖像的角點檢測
- <2>基於二值圖像的角點檢測
- <3>基於輪廓曲線的角點檢測
而基於灰度圖像的角點檢測又可分為基於梯度、基於模板和基於模板梯度組合三類方法,其中基於模板的方法主要考慮像素領域點的灰度變化,即圖像亮度的變化,將與鄰點亮度對比足夠大的點定義為角點。常見的基於模板的角點檢測算法有Kitchen-Rosenfeld角點檢測算法,Harris角點檢測算法、KLT角點檢測算法及SUSAN角點檢測算法。和其他角點檢測算法相比,SUSAN角點檢測算法具有算法簡單、位置準確、抗噪聲能力強等特點。
三、角點的定義
“如果某一點在任意方向的一個微小變動都會引起灰度很大的變化,那麽我們就把它稱之為角點”
角點檢測(Corner Detection)是計算機視覺系統中用來獲得圖像特征的一種方法,廣泛應用於運動檢測、圖像匹配、視頻跟蹤、三維建模和目標識別等領域中。也稱為特征點檢測。
角點通常被定義為兩條邊的交點,更嚴格的說,角點的局部鄰域應該具有兩個不同區域的不同方向的邊界。而實際應用中,大多數所謂的角點檢測方法檢測的是擁有特定特征的圖像點,而不僅僅是“角點”。這些特征點在圖像中有具體的坐標,並具有某些數學特征,如局部最大或最小灰度、某些梯度特征等。
現有的角點檢測算法並不是都十分的健壯。很多方法都要求有大量的訓練集和冗余數據來防止或減少錯誤特征的出現。另外,角點檢測方法的一個很重要的評價標準是其對多幅圖像中相同或相似特征的檢測能力,並且能夠應對光照變化、圖像旋轉等圖像變化。
在我們解決問題時,往往希望找到特征點,“特征”顧名思義,指能描述物體本質的東西,還有一種解釋就是這個特征微小的變化都會對物體的某一屬性產生重大的影響。而角點就是這樣的特征。
觀察日常生活中的“角落”就會發現,“角落”可以視為所有平面的交匯處,或者說是所有表面的發起處。假設我們要改變一個墻角的位置,那麽由它而出發的平面勢必都要有很大的變化。所以,這就引出了圖像角點的定義。
我們知道,特征檢測與匹配是計算機視覺應用中非常重要的一部分,這需要尋找圖像之間的特征建立對應關系。圖像中的點作為圖像的特殊位置,是很常用的一類特征,點的局部特征也可以叫做“關鍵特征點”(keypoint feature),或“興趣點”(interest point),或“角點”(conrner)。
另外,關於角點的具體描述可以有幾種:
- 一階導數(即灰度的梯度)的局部最大所對應的像素點;
- 兩條及兩條以上邊緣的交點;
- 圖像中梯度值和梯度方向的變化速率都很高的點;
- 角點處的一階導數最大,二階導數為零,指示物體邊緣變化不連續的方向。
四、cornerHarris函數詳解
cornerHarris 函數用於在OpenCV中運行Harris角點檢測算子處理圖像。和cornerMinEigenVal( )以及cornerEigenValsAndVecs( )函數類似,cornerHarris 函數對於每一個像素(x,y)在blockSize × blockSize 鄰域內,計算2x2梯度的協方差矩陣M(x,y),接著它計算如下式子:
即可以找出輸出圖中的局部最大值,即找出了角點。
其函數原型和參數解析:
C++: void cornerHarris(InputArray src,OutputArray dst, int blockSize, int ksize, double k, intborderType=BORDER_DEFAULT )
- 第一個參數,InputArray類型的src,輸入圖像,即源圖像,填Mat類的對象即可,且需為單通道8位或者浮點型圖像。
- 第二個參數,OutputArray類型的dst,函數調用後的運算結果存在這裏,即這個參數用於存放Harris角點檢測的輸出結果,和源圖片有一樣的尺寸和類型。
- 第三個參數,int類型的blockSize,表示鄰域的大小,更多的詳細信息在cornerEigenValsAndVecs()中有講到。
- 第四個參數,int類型的ksize,表示Sobel()算子的孔徑大小。
- 第五個參數,double類型的k,Harris參數。
- 第六個參數,int類型的borderType,圖像像素的邊界模式,註意它有默認值BORDER_DEFAULT。更詳細的解釋,參考borderInterpolate( )函數。
接著我們一起過一遍稍後需要用到的Threshold函數的解析,然後看一個以cornerHarris為核心的示例程序。
五、Threshold函數詳解
函數Threshold( ) 對單通道數組應用固定閾值操作。該函數的典型應用是對灰度圖像進行閾值操作得到二值圖像。(另外,compare( )函數也可以達到此目的) 或者是去掉噪聲,例如過濾很小或很大象素值的圖像點。
C++: double threshold(InputArray src,OutputArray dst, double thresh, double maxval, int type)
- 第一個參數,InputArray類型的src,輸入數組,填單通道 , 8或32位浮點類型的Mat即可。
- 第二個參數,OutputArray類型的dst,函數調用後的運算結果存在這裏,即這個參數用於存放輸出結果,且和第一個參數中的Mat變量有一樣的尺寸和類型。
- 第三個參數,double類型的thresh,閾值的具體值。
- 第四個參數,double類型的maxval,當第五個參數閾值類型type取 CV_THRESH_BINARY 或CV_THRESH_BINARY_INV 閾值類型時的最大值.
- 第五個參數,int類型的type,閾值類型,。threshold( )函數支持的對圖像取閾值的方法由其確定,具體用法如下圖:
而圖形化的閾值描述如下圖:
講解完這兩個函數,讓我們看一個調用示例程序:
//-----------------------------------【頭文件包含部分】--------------------------------------- // 描述:包含程序所依賴的頭文件 //---------------------------------------------------------------------------------------------- #include <opencv2/opencv.hpp> #include <opencv2/imgproc/imgproc.hpp> //-----------------------------------【命名空間聲明部分】--------------------------------------- // 描述:包含程序所使用的命名空間 //----------------------------------------------------------------------------------------------- using namespace cv; int main() { //以灰度模式載入圖像並顯示 Mat srcImage = imread("1.jpg", 0); imshow("原始圖", srcImage); //進行Harris角點檢測找出角點 Mat cornerStrength; cornerHarris(srcImage, cornerStrength, 2, 3, 0.01); //對灰度圖進行閾值操作,得到二值圖並顯示 Mat harrisCorner; threshold(cornerStrength, harrisCorner, 0.00001, 255, THRESH_BINARY); imshow("角點檢測後的二值效果圖", harrisCorner); waitKey(0); return 0; }
運行截圖:
六、本文相關核心函數在OpenCV中的實現源代碼
這個部分貼出OpenCV中本文相關函數的源碼實現細節,來給想了解實現細節的小夥伴們參考。淺墨暫時不在源碼的細節上挖深作詳細註釋。
6.1 OpenCV2.X中cornerHarris函數源代碼
源碼路徑: …opencv\sources\modules\imgproc\src\corner.cpp
void cv::cornerHarris( InputArray _src,OutputArray _dst, int blockSize, int ksize, double k, int borderType ) { Mat src = _src.getMat(); _dst.create( src.size(), CV_32F ); Mat dst = _dst.getMat(); cornerEigenValsVecs( src, dst, blockSize, ksize, HARRIS, k, borderType); }
可見cornerHarris內部其實是調用了cornerEigenValsVecs函數,我們看看其實現源碼:
static void cornerEigenValsVecs( const Mat& src,Mat& eigenv, int block_size, int aperture_size, intop_type, double k=0., intborderType=BORDER_DEFAULT ) { #ifdef HAVE_TEGRA_OPTIMIZATION if (tegra::cornerEigenValsVecs(src, eigenv, block_size, aperture_size,op_type, k, borderType)) return; #endif int depth = src.depth(); double scale = (double)(1 << ((aperture_size > 0 ?aperture_size : 3) - 1)) * block_size; if( aperture_size < 0 ) scale *= 2.; if( depth == CV_8U ) scale *= 255.; scale = 1./scale; CV_Assert( src.type() == CV_8UC1 || src.type() == CV_32FC1 ); Mat Dx, Dy; if( aperture_size > 0 ) { Sobel( src, Dx, CV_32F, 1, 0, aperture_size, scale, 0, borderType ); Sobel( src, Dy, CV_32F, 0, 1, aperture_size, scale, 0, borderType ); } else { Scharr( src, Dx, CV_32F, 1, 0, scale, 0, borderType ); Scharr( src, Dy, CV_32F, 0, 1, scale, 0, borderType ); } Size size = src.size(); Mat cov( size, CV_32FC3 ); int i, j; for( i = 0; i < size.height; i++ ) { float* cov_data = (float*)(cov.data + i*cov.step); const float* dxdata = (const float*)(Dx.data + i*Dx.step); const float* dydata = (const float*)(Dy.data + i*Dy.step); for( j = 0; j < size.width; j++ ) { float dx = dxdata[j]; float dy = dydata[j]; cov_data[j*3] = dx*dx; cov_data[j*3+1] = dx*dy; cov_data[j*3+2] = dy*dy; } } boxFilter(cov, cov, cov.depth(), Size(block_size, block_size), Point(-1,-1), 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 ); } }
6.1 OpenCV2.X中Threshold函數源代碼
路徑:…opencv\sources\modules\imgproc\src\thresh.cpp
double cv::threshold( InputArray _src,OutputArray _dst, double thresh, double maxval, int type ) { Mat src = _src.getMat(); bool use_otsu = (type & THRESH_OTSU) != 0; type &= THRESH_MASK; if( use_otsu ) { CV_Assert( src.type() == CV_8UC1 ); thresh = getThreshVal_Otsu_8u(src); } _dst.create( src.size(), src.type() ); Mat dst = _dst.getMat(); if( src.depth() == CV_8U ) { int ithresh = cvFloor(thresh); thresh = ithresh; int imaxval = cvRound(maxval); if( type == THRESH_TRUNC ) imaxval = ithresh; imaxval = saturate_cast<uchar>(imaxval); if( ithresh < 0 || ithresh >= 255 ) { if( type == THRESH_BINARY || type == THRESH_BINARY_INV || ((type == THRESH_TRUNC || type== THRESH_TOZERO_INV) && ithresh < 0) || (type == THRESH_TOZERO&& ithresh >= 255) ) { int v = type ==THRESH_BINARY ? (ithresh >= 255 ? 0 : imaxval) : type ==THRESH_BINARY_INV ? (ithresh >= 255 ? imaxval : 0) : /*type == THRESH_TRUNC? imaxval :*/ 0; dst.setTo(v); } else src.copyTo(dst); return thresh; } thresh = ithresh; maxval = imaxval; } else if( src.depth() == CV_16S ) { int ithresh = cvFloor(thresh); thresh = ithresh; int imaxval = cvRound(maxval); if( type == THRESH_TRUNC ) imaxval = ithresh; imaxval = saturate_cast<short>(imaxval); if( ithresh < SHRT_MIN || ithresh >= SHRT_MAX ) { if( type == THRESH_BINARY || type == THRESH_BINARY_INV || ((type == THRESH_TRUNC || type== THRESH_TOZERO_INV) && ithresh < SHRT_MIN) || (type == THRESH_TOZERO&& ithresh >= SHRT_MAX) ) { int v = type == THRESH_BINARY ?(ithresh >= SHRT_MAX ? 0 : imaxval) : type == THRESH_BINARY_INV ?(ithresh >= SHRT_MAX ? imaxval : 0) : /*type == THRESH_TRUNC ?imaxval :*/ 0; dst.setTo(v); } else src.copyTo(dst); return thresh; } thresh = ithresh; maxval = imaxval; } else if( src.depth() == CV_32F ) ; else CV_Error( CV_StsUnsupportedFormat, "" ); parallel_for_(Range(0, dst.rows), ThresholdRunner(src, dst,thresh, maxval, type), dst.total()/(double)(1<<16)); return thresh; }
另外在貼上與之相關的自適應閾值操作函數的源碼adaptiveThreshold:
void cv::adaptiveThreshold( InputArray_src, OutputArray _dst, double maxValue, int method, inttype, int blockSize, double delta ) { Mat src = _src.getMat(); CV_Assert( src.type() == CV_8UC1 ); CV_Assert( blockSize % 2 == 1 && blockSize > 1 ); Size size = src.size(); _dst.create( size, src.type() ); Mat dst = _dst.getMat(); if( maxValue < 0 ) { dst = Scalar(0); return; } Mat mean; if( src.data != dst.data ) mean = dst; if( method == ADAPTIVE_THRESH_MEAN_C ) boxFilter( src, mean, src.type(), Size(blockSize, blockSize), Point(-1,-1), true,BORDER_REPLICATE ); else if( method == ADAPTIVE_THRESH_GAUSSIAN_C ) GaussianBlur( src, mean, Size(blockSize, blockSize), 0, 0,BORDER_REPLICATE ); else CV_Error( CV_StsBadFlag, "Unknown/unsupported adaptive thresholdmethod" ); int i, j; uchar imaxval = saturate_cast<uchar>(maxValue); int idelta = type == THRESH_BINARY ? cvCeil(delta) : cvFloor(delta); uchar tab[768]; if( type == CV_THRESH_BINARY ) for( i = 0; i < 768; i++ ) tab[i] = (uchar)(i - 255 > -idelta ? imaxval : 0); else if( type == CV_THRESH_BINARY_INV ) for( i = 0; i < 768; i++ ) tab[i] = (uchar)(i - 255 <= -idelta ? imaxval : 0); else CV_Error( CV_StsBadFlag, "Unknown/unsupported threshold type"); if( src.isContinuous() && mean.isContinuous() &&dst.isContinuous() ) { size.width *= size.height; size.height = 1; } for( i = 0; i < size.height; i++ ) { const uchar* sdata = src.data + src.step*i; const uchar* mdata = mean.data + mean.step*i; uchar* ddata = dst.data + dst.step*i; for( j = 0; j < size.width; j++ ) ddata[j] = tab[sdata[j] - mdata[j] + 255]; } }
opencv-角點檢測之Harris角點檢測