KAZE系列筆記:
1. OpenCV學習筆記(27)KAZE 算法原理與源碼分析(一)非線性擴散濾波
2. OpenCV學習筆記(28)KAZE 算法原理與源碼分析(二)非線性尺度空間構建
3. OpenCV學習筆記(29)KAZE 算法原理與源碼分析(三)特徵檢測與描述
4. OpenCV學習筆記(30)KAZE 算法原理與源碼分析(四)KAZE特徵的性能分析與比較
5. OpenCV學習筆記(31)KAZE 算法原理與源碼分析(五)KAZE的性能優化及與SIFT的比較
KAZE算法資源:
1. 論文: http://www.robesafe.com/personal/pablo.alcantarilla/papers/Alcantarilla12eccv.pdf
2. 項目主頁:http://www.robesafe.com/personal/pablo.alcantarilla/kaze.html
3. 作者代碼:http://www.robesafe.com/personal/pablo.alcantarilla/code/kaze_features_1_4.tar
(需要boost庫,另外其計時函數的使用比較複雜,可以用OpenCV的cv::getTickCount代替)
4. Computer Vision Talks的評測:http://computer-vision-talks.com/2013/03/porting-kaze-features-to-opencv/
5. Computer Vision Talks 博主Ievgen Khvedchenia將KAZE集成到OpenCV的cv::Feature2D類,但需要重新編譯OpenCV,並且沒有實現算法參數調整和按Mask過濾特徵點的功能:https://github.com/BloodAxe/opencv/tree/kaze-features
6. 我在Ievgen的項目庫中提取出KAZE,封裝成繼承cv::Feature2D的類,無需重新編譯OpenCV,實現了參數調整和Mask過濾的功能: https://github.com/yuhuazou/kaze_opencv (2013-03-28更新,對KAZE代碼進行了優化)
7. Matlab 版的接口程序,封裝了1.0版的KAZE代碼:https://github.com/vlfeat/vlbenchmarks/blob/unstable/%2BlocalFeatures/Kaze.m
簡介
ECCV2012中出現了一種比SIFT更穩定的特徵檢測算法KAZE ([1])。KAZE的取名是爲了紀念尺度空間分析的開創者—日本學者Iijima。KAZE是日語‘風’的諧音,寓意是就像風的形成是空氣在空間中非線性的流動過程一樣,KAZE特徵檢測是在圖像域中進行非線性擴散處理的過程。
傳統的SIFT、SURF等特徵檢測算法都是基於線性的高斯金字塔進行多尺度分解來消除噪聲和提取顯著特徵點。但高斯分解是犧牲了局部精度爲代價的,容易造成邊界模糊和細節丟失。非線性的尺度分解有望解決這種問題,但傳統方法基於正向歐拉法(forward Euler scheme)求解非線性擴散(Non-linear diffusion)方程時迭代收斂的步長太短,耗時長、計算複雜度高。由此,KAZE算法的作者提出採用加性算子分裂算法(Additive Operator Splitting, AOS)來進行非線性擴散濾波,可以採用任意步長來構造穩定的非線性尺度空間。
注:KAZE算法的原理與SIFT和SURF有很多相似之處,在深入瞭解KAZE之前,可以參考以下的博客文章對SIFT和SURF的介紹:
1. 【OpenCV】SIFT原理與源碼分析 - 小魏的修行路 (很不錯的博客,用心、認真、圖文並茂)
2. SIFT特徵提取分析 - Rachel Zhang的專欄 (這裏的分析也很詳細)
3. Surf算法學習心得(一)——算法原理 - yang_yong’ blog
4. Opencv學習筆記(六)SURF學習筆記 - crazy_sparrow的專欄
1.1 非線性擴散濾波
1.1.1 Perona-Malik擴散方程
具體地,非線性擴散濾波方法是將圖像亮度(L)在不同尺度上的變化視爲某種形式的流動函數(flow function)的散度(divergence),可以通過非線性偏微分方程來描述:
通過設置合適的傳導函數 c(x,y,t) ,可以使得擴散自適應於圖像的局部結構。傳導函數可以是標量、也可以是張量。時間t作爲尺度參數,其值越大、則圖像的表示形式越簡單。Perona和Malik提出了傳導函數的構造方式:
其中的▽Lσ是高斯平滑後的圖像Lσ的梯度(gradient)。函數 g() 的形式有如下幾種:
其中函數g1優先保留高對比度的邊緣,g2優先保留寬度較大的區域,g3能夠有效平滑區域內部而保留邊界信息(KAZE代碼中默認採用函數g2)。
函數g1、g2、g3的實現代碼如下(在文件 kaze_nldiffusion_functions.cpp 中):
//*************************************************************************************
//*************************************************************************************
/**
* @brief This function computes the Perona and Malik conductivity coefficient g1
* g1 = exp(-|dL|^2/k^2)
* @param src Input image
* @param dst Output image
* @param Lx First order image derivative in X-direction (horizontal)
* @param Ly First order image derivative in Y-direction (vertical)
* @param k Contrast factor parameter
*/
void PM_G1(const cv::Mat &src, cv::Mat &dst, cv::Mat &Lx, cv::Mat &Ly, float k )
{
cv::exp(-(Lx.mul(Lx) + Ly.mul(Ly))/(k*k),dst);
}
//*************************************************************************************
//*************************************************************************************
/**
* @brief This function computes the Perona and Malik conductivity coefficient g2
* g2 = 1 / (1 + dL^2 / k^2)
* @param src Input image
* @param dst Output image
* @param Lx First order image derivative in X-direction (horizontal)
* @param Ly First order image derivative in Y-direction (vertical)
* @param k Contrast factor parameter
*/
void PM_G2(const cv::Mat &src, cv::Mat &dst, cv::Mat &Lx, cv::Mat &Ly, float k )
{
dst = 1./(1. + (Lx.mul(Lx) + Ly.mul(Ly))/(k*k));
}
//*************************************************************************************
//*************************************************************************************
/**
* @brief This function computes Weickert conductivity coefficient g3
* @param src Input image
* @param dst Output image
* @param Lx First order image derivative in X-direction (horizontal)
* @param Ly First order image derivative in Y-direction (vertical)
* @param k Contrast factor parameter
* @note For more information check the following paper: J. Weickert
* Applications of nonlinear diffusion in image processing and computer vision,
* Proceedings of Algorithmy 2000
*/
void Weickert_Diffusivity(const cv::Mat &src, cv::Mat &dst, cv::Mat &Lx, cv::Mat &Ly, float k )
{
cv::Mat modg;
cv::pow((Lx.mul(Lx) + Ly.mul(Ly))/(k*k),4,modg);
cv::exp(-3.315/modg, dst);
dst = 1.0 - dst;
}
參數k是控制擴散級別的對比度因子(contrast factor),能夠決定保留多少邊緣信息,其值越大,保留的邊緣信息越少。在KAZE算法中,參數k的取值是梯度圖像▽Lσ的直方圖70% 百分位上的值:
計算參數k的實現源碼如下(在文件 kaze_nldiffusion_functions.cpp 中):
//*************************************************************************************
//*************************************************************************************
/**
* @brief This function computes a good empirical value for the k contrast factor
* given an input image, the percentile (0-1), the gradient scale and the number of
* bins in the histogram
* @param img Input image
* @param perc Percentile of the image gradient histogram (0-1)
* @param gscale Scale for computing the image gradient histogram
* @param nbins Number of histogram bins
* @param ksize_x Kernel size in X-direction (horizontal) for the Gaussian smoothing kernel
* @param ksize_y Kernel size in Y-direction (vertical) for the Gaussian smoothing kernel
* @return k contrast factor
*/
float Compute_K_Percentile(const cv::Mat &img, float perc, float gscale, unsigned int nbins, unsigned int ksize_x, unsigned int ksize_y)
{
float kperc = 0.0, modg = 0.0, lx = 0.0, ly = 0.0;
unsigned int nbin = 0, nelements = 0, nthreshold = 0, k = 0;
float npoints = 0.0; // number of points of which gradient greater than zero
float hmax = 0.0; // maximum gradient
// Create the array for the histogram
float *hist = new float[nbins];
// Create the matrices
cv::Mat gaussian = cv::Mat::zeros(img.rows,img.cols,CV_32F);
cv::Mat Lx = cv::Mat::zeros(img.rows,img.cols,CV_32F);
cv::Mat Ly = cv::Mat::zeros(img.rows,img.cols,CV_32F);
// Set the histogram to zero, just in case
for( unsigned int i = 0; i < nbins; i++ )
{
hist[i] = 0.0;
}
// Perform the Gaussian convolution
Gaussian_2D_Convolution(img,gaussian,ksize_x,ksize_y,gscale);
// Compute the Gaussian derivatives Lx and Ly
Image_Derivatives_Scharr(gaussian,Lx,1,0);
Image_Derivatives_Scharr(gaussian,Ly,0,1);
// Skip the borders for computing the histogram
for( int i = 1; i < gaussian.rows-1; i++ )
{
for( int j = 1; j < gaussian.cols-1; j++ )
{
lx = *(Lx.ptr<float>(i)+j);
ly = *(Ly.ptr<float>(i)+j);
modg = sqrt(lx*lx + ly*ly);
// Get the maximum
if( modg > hmax )
{
hmax = modg;
}
}
}
// Skip the borders for computing the histogram
for( int i = 1; i < gaussian.rows-1; i++ )
{
for( int j = 1; j < gaussian.cols-1; j++ )
{
lx = *(Lx.ptr<float>(i)+j);
ly = *(Ly.ptr<float>(i)+j);
modg = sqrt(lx*lx + ly*ly); // modg can be stored in a matrix in last step in oder to avoid re-computation
// Find the correspondent bin
if( modg != 0.0 )
{
nbin = floor(nbins*(modg/hmax));
if( nbin == nbins )
{
nbin--;
}
hist[nbin]++;
npoints++;
}
}
}
// Now find the perc of the histogram percentile
nthreshold = (unsigned int)(npoints*perc);
// find the bin (k) in which accumulated points are greater than 70% (perc) of total valid points (npoints)
for( k = 0; nelements < nthreshold && k < nbins; k++)
{
nelements = nelements + hist[k];
}
if( nelements < nthreshold )
{
kperc = 0.03;
}
else
{
kperc = hmax*((float)(k)/(float)nbins);
}
delete hist;
return kperc;
}
注:有關非線性擴散濾波的應用,參見[2]。
1.1.2 AOS算法
由於非線性偏微分方程並沒有解析解,一般通過數值分析的方法進行迭代求解。傳統上採用顯式差分格式的求解方法只能採用小步長,收斂緩慢。爲此,將方程離散化爲以下的隱式差分格式:
其中Al是表示圖像在各維度(l)上傳導性的矩陣。該方程的解如下:
這種求解方法對任意時間步長(τ)都有效。上式中矩陣Al是三對角矩陣並且對角佔優(tridiagonal and diagonally dominant matrix),這樣的線性系統可以通過Thomas算法快速求解。(有關AOS的應用,參見[3])
該算法的實現源碼如下(在文件 kaze.cpp 中):
//*************************************************************************************
//*************************************************************************************
/**
* @brief This method performs a scalar non-linear diffusion step using AOS schemes
* @param Ld Image at a given evolution step
* @param Ldprev Image at a previous evolution step
* @param c Conductivity image
* @param stepsize Stepsize for the nonlinear diffusion evolution
* @note If c is constant, the diffusion will be linear
* If c is a matrix of the same size as Ld, the diffusion will be nonlinear
* The stepsize can be arbitrarilly large
*/
void KAZE::AOS_Step_Scalar(cv::Mat &Ld, const cv::Mat &Ldprev, const cv::Mat &c, const float stepsize)
{
AOS_Rows(Ldprev,c,stepsize);
AOS_Columns(Ldprev,c,stepsize);
Ld = 0.5*(Lty + Ltx.t());
}
//*************************************************************************************
//*************************************************************************************
/**
* @brief This method performs a scalar non-linear diffusion step using AOS schemes
* Diffusion in each dimension is computed independently in a different thread
* @param Ld Image at a given evolution step
* @param Ldprev Image at a previous evolution step
* @param c Conductivity image
* @param stepsize Stepsize for the nonlinear diffusion evolution
* @note If c is constant, the diffusion will be linear
* If c is a matrix of the same size as Ld, the diffusion will be nonlinear
* The stepsize can be arbitrarilly large
*/
#if HAVE_THREADING_SUPPORT
void KAZE::AOS_Step_Scalar_Parallel(cv::Mat &Ld, const cv::Mat &Ldprev, const cv::Mat &c, const float stepsize)
{
boost::thread *AOSth1 = new boost::thread(&KAZE::AOS_Rows,this,Ldprev,c,stepsize);
boost::thread *AOSth2 = new boost::thread(&KAZE::AOS_Columns,this,Ldprev,c,stepsize);
AOSth1->join();
AOSth2->join();
Ld = 0.5*(Lty + Ltx.t());
delete AOSth1;
delete AOSth2;
}
#endif
//*************************************************************************************
//*************************************************************************************
/**
* @brief This method performs performs 1D-AOS for the image rows
* @param Ldprev Image at a previous evolution step
* @param c Conductivity image
* @param stepsize Stepsize for the nonlinear diffusion evolution
*/
void KAZE::AOS_Rows(const cv::Mat &Ldprev, const cv::Mat &c, const float stepsize)
{
// Operate on rows
for( int i = 0; i < qr.rows; i++ )
{
for( int j = 0; j < qr.cols; j++ )
{
*(qr.ptr<float>(i)+j) = *(c.ptr<float>(i)+j) + *(c.ptr<float>(i+1)+j);
}
}
for( int j = 0; j < py.cols; j++ )
{
*(py.ptr<float>(0)+j) = *(qr.ptr<float>(0)+j);
}
for( int j = 0; j < py.cols; j++ )
{
*(py.ptr<float>(py.rows-1)+j) = *(qr.ptr<float>(qr.rows-1)+j);
}
for( int i = 1; i < py.rows-1; i++ )
{
for( int j = 0; j < py.cols; j++ )
{
*(py.ptr<float>(i)+j) = *(qr.ptr<float>(i-1)+j) + *(qr.ptr<float>(i)+j);
}
}
// a = 1 + t.*p; (p is -1*p)
// b = -t.*q;
ay = 1.0 + stepsize*py; // p is -1*p
by = -stepsize*qr;
// Call to Thomas algorithm now
Thomas(ay,by,Ldprev,Lty);
}
//*************************************************************************************
//*************************************************************************************
/**
* @brief This method performs performs 1D-AOS for the image columns
* @param Ldprev Image at a previous evolution step
* @param c Conductivity image
* @param stepsize Stepsize for the nonlinear diffusion evolution
*/
void KAZE::AOS_Columns(const cv::Mat &Ldprev, const cv::Mat &c, const float stepsize)
{
// Operate on columns
for( int j = 0; j < qc.cols; j++ )
{
for( int i = 0; i < qc.rows; i++ )
{
*(qc.ptr<float>(i)+j) = *(c.ptr<float>(i)+j) + *(c.ptr<float>(i)+j+1);
}
}
for( int i = 0; i < px.rows; i++ )
{
*(px.ptr<float>(i)) = *(qc.ptr<float>(i));
}
for( int i = 0; i < px.rows; i++ )
{
*(px.ptr<float>(i)+px.cols-1) = *(qc.ptr<float>(i)+qc.cols-1);
}
for( int j = 1; j < px.cols-1; j++ )
{
for( int i = 0; i < px.rows; i++ )
{
*(px.ptr<float>(i)+j) = *(qc.ptr<float>(i)+j-1) + *(qc.ptr<float>(i)+j);
}
}
// a = 1 + t.*p';
ax = 1.0 + stepsize*px.t();
// b = -t.*q';
bx = -stepsize*qc.t();
// Call Thomas algorithm again
// But take care since we need to transpose the solution!!
Thomas(ax,bx,Ldprev.t(),Ltx);
}
//*************************************************************************************
//*************************************************************************************
/**
* @brief This method does the Thomas algorithm for solving a tridiagonal linear system
* @note The matrix A must be strictly diagonally dominant for a stable solution
*/
void KAZE::Thomas(cv::Mat a, cv::Mat b, cv::Mat Ld, cv::Mat x)
{
// Auxiliary variables
int n = a.rows;
cv::Mat m = cv::Mat::zeros(a.rows,a.cols,CV_32F);
cv::Mat l = cv::Mat::zeros(b.rows,b.cols,CV_32F);
cv::Mat y = cv::Mat::zeros(Ld.rows,Ld.cols,CV_32F);
/** A*x = d; */
/** / a1 b1 0 0 0 ... 0 \ / x1 \ = / d1 \ */
/** | c1 a2 b2 0 0 ... 0 | | x2 | = | d2 | */
/** | 0 c2 a3 b3 0 ... 0 | | x3 | = | d3 | */
/** | : : : : 0 ... 0 | | : | = | : | */
/** | : : : : 0 cn-1 an | | xn | = | dn | */
/** 1. LU decomposition
/ L = / 1 \ U = / m1 r1 \
/ | l1 1 | | m2 r2 |
/ | l2 1 | | m3 r3 |
/ | : : : | | : : : |
/ \ ln-1 1 / \ mn / */
for( int j = 0; j < m.cols; j++ )
{
*(m.ptr<float>(0)+j) = *(a.ptr<float>(0)+j);
}
for( int j = 0; j < y.cols; j++ )
{
*(y.ptr<float>(0)+j) = *(Ld.ptr<float>(0)+j);
}
// 2. Forward substitution L*y = d for y
for( int k = 1; k < n; k++ )
{
for( int j=0; j < l.cols; j++ )
{
*(l.ptr<float>(k-1)+j) = *(b.ptr<float>(k-1)+j) / *(m.ptr<float>(k-1)+j);
}
for( int j=0; j < m.cols; j++ )
{
*(m.ptr<float>(k)+j) = *(a.ptr<float>(k)+j) - *(l.ptr<float>(k-1)+j)*(*(b.ptr<float>(k-1)+j));
}
for( int j=0; j < y.cols; j++ )
{
*(y.ptr<float>(k)+j) = *(Ld.ptr<float>(k)+j) - *(l.ptr<float>(k-1)+j)*(*(y.ptr<float>(k-1)+j));
}
}
// 3. Backward substitution U*x = y
for( int j=0; j < y.cols; j++ )
{
*(x.ptr<float>(n-1)+j) = (*(y.ptr<float>(n-1)+j))/(*(m.ptr<float>(n-1)+j));
}
for( int i = n-2; i >= 0; i-- )
{
for( int j = 0; j < x.cols; j++ )
{
*(x.ptr<float>(i)+j) = (*(y.ptr<float>(i)+j) - (*(b.ptr<float>(i)+j))*(*(x.ptr<float>(i+1)+j)))/(*(m.ptr<float>(i)+j));
}
}
}
上面介紹了非線性擴散濾波和AOS求解隱性差分方程的原理,是KAZE算法求解非線性尺度空間的基礎,下一節我們將介紹KAZE算法的非線性尺度空間構建、特徵檢測與描述等內容。
待續...
Ref:
[1] http://www.robesafe.com/personal/pablo.alcantarilla/papers/Alcantarilla12eccv.pdf
[2] http://manu16.magtech.com.cn/geoprog/CN/article/downloadArticleFile.do?attachType=PDF&id=3146
[3] http://file.lw23.com/8/8e/8ec/8ecd21e4-b030-4e05-9333-40cc2d97bde4.pdf