如果說SIFT算法中使用DOG對LOG進行了簡化,提高了搜索特徵點的速度,那麼SURF算法則是對DoH的簡化與近似。雖然SIFT算法已經被認爲是最有效的,也是最常用的特徵點提取的算法,但如果不藉助於硬件的加速和專用圖像處理器的配合,SIFT算法以現有的計算機仍然很難達到實時的程度。對於需要實時運算的場合,如基於特徵點匹配的實時目標跟蹤系統,每秒要處理8-24幀的圖像,需要在毫秒級內完成特徵點的搜索、特徵矢量生成、特徵矢量匹配、目標鎖定等工作,這樣SIFT算法就很難適應這種需求了。SURF借鑑了SIFT中簡化近似的思想,把DoH中的高斯二階微分模板進行了簡化,使得模板對圖像的濾波只需要進行幾個簡單的加減法運算,並且,這種運算與濾波器的尺度無關。實驗證明,SURF算法較SIFT在運算速度上要快3倍左右。
1. 積分圖像
SURF算法中要用到積分圖像的概念。藉助積分圖像,圖像與高斯二階微分模板的濾波轉化爲對積分圖像的加減運算。
積分圖像中任意一點
式中,
式中,
OpenCV中提供了用於計算積分圖像的接口
/* * src :輸入圖像,大小爲M*N * sum: 輸出的積分圖像,大小爲(M+1)*(N+1) * sdepth:用於指定sum的類型,-1表示與src類型一致 */ void integral(InputArray src, OutputArray sum, int sdepth = -1);
值得注意的是OpenCV裏的積分圖大小比原圖像多一行一列,那是因爲OpenCV中積分圖的計算公式爲:
一旦積分圖計算好了,計算圖像內任何矩形區域的像素值的和只需要三個加法,如上圖所示。
2. DoH近似
在斑點檢測這篇文章中已經提到過,我們可以利用Hessian矩陣行列式的極大值檢測斑點。下面我們給出Hessian矩陣的定義。
給定圖像
式中,
下面顯示的是上面三種高斯微分算子的圖形。
但是利用Hessian行列式進行圖像斑點檢測時,有一個缺點。由於二階高斯微分被離散化和裁剪的原因,導致了圖像在旋轉奇數倍的
爲了將模板與圖產像的卷積轉換爲盒子濾波運算,我們需要對高斯二階微分模板進行簡化,使得簡化後的模板只是由幾個矩形區域組成,矩形區域內填充同一值,如下圖所示,在簡化模板中白色區域的值爲正數,黑色區域的值爲負數,灰度區域的值爲0。
對於
濾波器響應的相關權重
其中
使用近似的Hessian矩陣行列式來表示圖像中某一點
3. 尺度空間表示
通常想要獲取不同尺度的斑點,必須建立圖像的尺度空間金字塔。一般的方法是通過不同
由於採用了盒子濾波和積分圖像,所以,我們並不需要像SIFT算法那樣去直接建立圖像金字塔,而是採用不斷增大盒子濾波模板的尺寸的間接方法。通過不同尺寸盒子濾波模板與積分圖像求取Hessian矩陣行列式的響應圖像。然後在響應圖像上採用3D非最大值抑制,求取各種不同尺度的斑點。
如前所述,我們使用
與SIFT算法類似,我們需要將尺度空間劃分爲若干組(Octaves)。一個組代表了逐步放大的濾波模板對同一輸入圖像進行濾波的一系列響應圖。每個組又由若干固定的層組成。由於積分圖像離散化的原因,兩個層之間的最小尺度變化量是由高斯二階微分濾波器在微分方向上對正負斑點響應長度
採用類似的方法來處理其他幾組的模板序列。其方法是將濾波器尺寸增加量翻倍(6,12,24,38)。這樣,可以得到第二組的濾波器尺寸,它們分別爲15,27,39,51。第三組的濾波器尺寸爲27,51,75,99。如果原始圖像的尺寸仍然大於對應的濾波器尺寸,尺度空間的分析還可以進行第四組,其對應的模板尺寸分別爲51,99,147和195。下圖顯示了第一組至第三組的濾波器尺寸變化。
在通常尺度分析情況下,隨着尺度的增大,被檢測到的斑點數量迅速衰減。所以一般進行3-4組就可以了,與此同時,爲了減少運算量,提高計算的速度,可以考慮在濾波時,將採樣間隔設爲2。
對於尺寸爲L的模板,當用它與積分圖運算來近似二維高斯核的濾波時,對應的二維高斯核的參數
4. 興趣點的定位
爲了在圖像及不同尺寸中定位興趣點,我們用了
下面顯示了我們用的快速Hessian檢測子檢測到的興趣點。
5. SURF源碼解析
這份源碼來自OpenCV nonfree模塊。
這裏先介紹SURF特徵點定位這一塊,關於特徵點的描述下一篇文章再介紹。
5.1 主幹函數 fastHessianDetector
特徵點定位的主幹函數爲fastHessianDetector,該函數接受一個積分圖像,以及尺寸相關的參數,組數與每組的層數,檢測到的特徵點保存在vector<KeyPoint>類型的結構中。
static void fastHessianDetector(const Mat& sum, const Mat& msum, vector<KeyPoint>& keypoints, int nOctaves, int nOctaveLayers, float hessianThreshold) { /*first Octave圖像採樣的步長,第二組的時候加倍,以此內推 增加這個值,將會加快特徵點檢測的速度,但是會讓特徵點的提取變得不穩定*/ const int SAMPLE_STEP0 = 1; int nTotalLayers = (nOctaveLayers + 2)*nOctaves; // 尺度空間的總圖像數 int nMiddleLayers = nOctaveLayers*nOctaves; // 用於檢測特徵點的層的 總數,也就是中間層的總數 vector<Mat> dets(nTotalLayers); // 每一層圖像 對應的 Hessian行列式的值 vector<Mat> traces(nTotalLayers); // 每一層圖像 對應的 Hessian矩陣的跡的值 vector<int> sizes(nTotalLayers); // 每一層用的 Harr模板的大小 vector<int> sampleSteps(nTotalLayers); // 每一層用的採樣步長 vector<int> middleIndices(nMiddleLayers); // 中間層的索引值 keypoints.clear(); // 爲上面的對象分配空間,並賦予合適的值 int index = 0, middleIndex = 0, step = SAMPLE_STEP0; for (int octave = 0; octave < nOctaves; octave++) { for (int layer = 0; layer < nOctaveLayers + 2; layer++) { /*這裏sum.rows - 1是因爲 sum是積分圖,它的大小是原圖像大小加1*/ dets[index].create((sum.rows - 1) / step, (sum.cols - 1) / step, CV_32F); // 這裏面有除以遍歷圖像用的步長 traces[index].create((sum.rows - 1) / step, (sum.cols - 1) / step, CV_32F); sizes[index] = (SURF_HAAR_SIZE0 + SURF_HAAR_SIZE_INC*layer) << octave; sampleSteps[index] = step; if (0 < layer && layer <= nOctaveLayers) middleIndices[middleIndex++] = index; index++; } step *= 2; } // Calculate hessian determinant and trace samples in each layer for (int i = 0; i < nTotalLayers; i++) { calcLayerDetAndTrace(sum, sizes[i], sampleSteps[i], dets[i], traces[i]); } // Find maxima in the determinant of the hessian for (int i = 0; i < nMiddleLayers; i++) { int layer = middleIndices[i]; int octave = i / nOctaveLayers; findMaximaInLayer(sum, msum, dets, traces, sizes, keypoints, octave, layer, hessianThreshold, sampleSteps[layer]); } std::sort(keypoints.begin(), keypoints.end(), KeypointGreater()); }
5.2 計算Hessian矩陣的行列式與跡calcLayerDetAndTrace
這個函數首先定義了尺寸爲9的第一層圖像的三個模板。模板分別爲一個
struct SurfHF { int p0, p1, p2, p3; float w; SurfHF() : p0(0), p1(0), p2(0), p3(0), w(0) {} };
resizeHaarPattern這個函數非常的巧妙,它把模板中的點座標。轉換到在積分圖中的相對(模板左上角點)座標。
static void resizeHaarPattern(const int src[][5], SurfHF* dst, int n, int oldSize, int newSize, int widthStep) { float ratio = (float)newSize / oldSize; for (int k = 0; k < n; k++) { int dx1 = cvRound(ratio*src[k][0]); int dy1 = cvRound(ratio*src[k][1]); int dx2 = cvRound(ratio*src[k][2]); int dy2 = cvRound(ratio*src[k][3]); /*巧妙的座標轉換*/ dst[k].p0 = dy1*widthStep + dx1; // 轉換爲一個相對距離,距離模板左上角點的 在積分圖中的距離 !!important!! dst[k].p1 = dy2*widthStep + dx1; dst[k].p2 = dy1*widthStep + dx2; dst[k].p3 = dy2*widthStep + dx2; dst[k].w = src[k][4] / ((float)(dx2 - dx1)*(dy2 - dy1));// 原來的+1,+2用 覆蓋的所有像素點平均。 } }
在用積分圖計算近似卷積時,用的是calcHaarPattern函數。這個函數比較簡單,只用知道左上與右下角座標即可。
inline float calcHaarPattern(const int* origin, const SurfHF* f, int n) { /*orgin即爲積分圖,n爲模板中 黑白 塊的個數 */ double d = 0; for (int k = 0; k < n; k++) d += (origin[f[k].p0] + origin[f[k].p3] - origin[f[k].p1] - origin[f[k].p2])*f[k].w; return (float)d; }
最終我們可以看到了整個calcLayerDetAndTrack的代碼
static void calcLayerDetAndTrace(const Mat& sum, int size, int sampleStep, Mat& det, Mat& trace) { const int NX = 3, NY = 3, NXY = 4; const int dx_s[NX][5] = { { 0, 2, 3, 7, 1 }, { 3, 2, 6, 7, -2 }, { 6, 2, 9, 7, 1 } }; const int dy_s[NY][5] = { { 2, 0, 7, 3, 1 }, { 2, 3, 7, 6, -2 }, { 2, 6, 7, 9, 1 } }; const int dxy_s[NXY][5] = { { 1, 1, 4, 4, 1 }, { 5, 1, 8, 4, -1 }, { 1, 5, 4, 8, -1 }, { 5, 5, 8, 8, 1 } }; SurfHF Dx[NX], Dy[NY], Dxy[NXY]; if (size > sum.rows - 1 || size > sum.cols - 1) return; resizeHaarPattern(dx_s, Dx, NX, 9, size, sum.cols); resizeHaarPattern(dy_s, Dy, NY, 9, size, sum.cols); resizeHaarPattern(dxy_s, Dxy, NXY, 9, size, sum.cols); /* The integral image 'sum' is one pixel bigger than the source image */ int samples_i = 1 + (sum.rows - 1 - size) / sampleStep; // 最大能遍歷到的 行座標,因爲要減掉一個模板的尺寸 int samples_j = 1 + (sum.cols - 1 - size) / sampleStep; // 最大能遍歷到的 列座標 /* Ignore pixels where some of the kernel is outside the image */ int margin = (size / 2) / sampleStep; for (int i = 0; i < samples_i; i++) { /*座標爲(i,j)的點是模板左上角的點,所以實際現在模板分析是的i+margin,j+margin點處的響應*/ const int* sum_ptr = sum.ptr<int>(i*sampleStep); float* det_ptr = &det.at<float>(i + margin, margin); // 左邊空隙爲 margin float* trace_ptr = &trace.at<float>(i + margin, margin); for (int j = 0; j < samples_j; j++) { float dx = calcHaarPattern(sum_ptr, Dx, 3); float dy = calcHaarPattern(sum_ptr, Dy, 3); float dxy = calcHaarPattern(sum_ptr, Dxy, 4); sum_ptr += sampleStep; det_ptr[j] = dx*dy - 0.81f*dxy*dxy; trace_ptr[j] = dx + dy; } } }
5.3 局部最大值搜索findMaximaInLayer
這裏算法思路很簡單,值得注意的是裏面的一些座標的轉換很巧妙,裏面比較重的函數就是interpolateKeypoint函數,通過插值計算最大值點。
/* * Maxima location interpolation as described in "Invariant Features from * Interest Point Groups" by Matthew Brown and David Lowe. This is performed by * fitting a 3D quadratic to a set of neighbouring samples. * * The gradient vector and Hessian matrix at the initial keypoint location are * approximated using central differences. The linear system Ax = b is then * solved, where A is the Hessian, b is the negative gradient, and x is the * offset of the interpolated maxima coordinates from the initial estimate. * This is equivalent to an iteration of Netwon's optimisation algorithm. * * N9 contains the samples in the 3x3x3 neighbourhood of the maxima * dx is the sampling step in x * dy is the sampling step in y * ds is the sampling step in size * point contains the keypoint coordinates and scale to be modified * * Return value is 1 if interpolation was successful, 0 on failure. */ static int interpolateKeypoint(float N9[3][9], int dx, int dy, int ds, KeyPoint& kpt) { Vec3f b(-(N9[1][5] - N9[1][3]) / 2, // Negative 1st deriv with respect to x -(N9[1][7] - N9[1][1]) / 2, // Negative 1st deriv with respect to y -(N9[2][4] - N9[0][4]) / 2); // Negative 1st deriv with respect to s Matx33f A( N9[1][3] - 2 * N9[1][4] + N9[1][5], // 2nd deriv x, x (N9[1][8] - N9[1][6] - N9[1][2] + N9[1][0]) / 4, // 2nd deriv x, y (N9[2][5] - N9[2][3] - N9[0][5] + N9[0][3]) / 4, // 2nd deriv x, s (N9[1][8] - N9[1][6] - N9[1][2] + N9[1][0]) / 4, // 2nd deriv x, y N9[1][1] - 2 * N9[1][4] + N9[1][7], // 2nd deriv y, y (N9[2][7] - N9[2][1] - N9[0][7] + N9[0][1]) / 4, // 2nd deriv y, s (N9[2][5] - N9[2][3] - N9[0][5] + N9[0][3]) / 4, // 2nd deriv x, s (N9[2][7] - N9[2][1] - N9[0][7] + N9[0][1]) / 4, // 2nd deriv y, s N9[0][4] - 2 * N9[1][4] + N9[2][4]); // 2nd deriv s, s Vec3f x = A.solve(b, DECOMP_LU); bool ok = (x[0] != 0 || x[1] != 0 || x[2] != 0) && std::abs(x[0]) <= 1 && std::abs(x[1]) <= 1 && std::abs(x[2]) <= 1; if (ok) { kpt.pt.x += x[0] * dx; kpt.pt.y += x[1] * dy; kpt.size = (float)cvRound(kpt.size + x[2] * ds); } return ok; } static void findMaximaInLayer(const Mat& sum, const Mat& mask_sum, const vector<Mat>& dets, const vector<Mat>& traces, const vector<int>& sizes, vector<KeyPoint>& keypoints, int octave, int layer, float hessianThreshold, int sampleStep) { // Wavelet Data const int NM = 1; const int dm[NM][5] = { { 0, 0, 9, 9, 1 } }; SurfHF Dm; int size = sizes[layer]; // 當前層圖像的大小 int layer_rows = (sum.rows - 1) / sampleStep; int layer_cols = (sum.cols - 1) / sampleStep; // 邊界區域大小,考慮的下一層的模板大小 int margin = (sizes[layer + 1] / 2) / sampleStep + 1; if (!mask_sum.empty()) resizeHaarPattern(dm, &Dm, NM, 9, size, mask_sum.cols); int step = (int)(dets[layer].step / dets[layer].elemSize()); for (int i = margin; i < layer_rows - margin; i++) { const float* det_ptr = dets[layer].ptr<float>(i); const float* trace_ptr = traces[layer].ptr<float>(i); for (int j = margin; j < layer_cols - margin; j++) { float val0 = det_ptr[j]; // 中心點的值 if (val0 > hessianThreshold) { // 模板左上角的座標 int sum_i = sampleStep*(i - (size / 2) / sampleStep); int sum_j = sampleStep*(j - (size / 2) / sampleStep); /* The 3x3x3 neighbouring samples around the maxima. The maxima is included at N9[1][4] */ const float *det1 = &dets[layer - 1].at<float>(i, j); const float *det2 = &dets[layer].at<float>(i, j); const float *det3 = &dets[layer + 1].at<float>(i, j); float N9[3][9] = { { det1[-step - 1], det1[-step], det1[-step + 1], det1[-1], det1[0], det1[1], det1[step - 1], det1[step], det1[step + 1] }, { det2[-step - 1], det2[-step], det2[-step + 1], det2[-1], det2[0], det2[1], det2[step - 1], det2[step], det2[step + 1] }, { det3[-step - 1], det3[-step], det3[-step + 1], det3[-1], det3[0], det3[1], det3[step - 1], det3[step], det3[step + 1] } }; /* Check the mask - why not just check the mask at the center of the wavelet? */ if (!mask_sum.empty()) { const int* mask_ptr = &mask_sum.at<int>(sum_i, sum_j); float mval = calcHaarPattern(mask_ptr, &Dm, 1); if (mval < 0.5) continue; } /* 檢測val0,是否在N9裏極大值,??爲什麼不檢測極小值呢*/ if (val0 > N9[0][0] && val0 > N9[0][1] && val0 > N9[0][2] && val0 > N9[0][3] && val0 > N9[0][4] && val0 > N9[0][5] && val0 > N9[0][6] && val0 > N9[0][7] && val0 > N9[0][8] && val0 > N9[1][0] && val0 > N9[1][1] && val0 > N9[1][2] && val0 > N9[1][3] && val0 > N9[1][5] && val0 > N9[1][6] && val0 > N9[1][7] && val0 > N9[1][8] && val0 > N9[2][0] && val0 > N9[2][1] && val0 > N9[2][2] && val0 > N9[2][3] && val0 > N9[2][4] && val0 > N9[2][5] && val0 > N9[2][6] && val0 > N9[2][7] && val0 > N9[2][8]) { /* Calculate the wavelet center coordinates for the maxima */ float center_i = sum_i + (size - 1)*0.5f; float center_j = sum_j + (size - 1)*0.5f; KeyPoint kpt(center_j, center_i, (float)sizes[layer], -1, val0, octave, CV_SIGN(trace_ptr[j])); /* 局部極大值插值,用Hessian,類似於SIFT裏的插值,裏面沒有迭代5次,只進行了一次查找,why? */ int ds = size - sizes[layer - 1]; int interp_ok = interpolateKeypoint(N9, sampleStep, sampleStep, ds, kpt); /* Sometimes the interpolation step gives a negative size etc. */ if (interp_ok) { /*printf( "KeyPoint %f %f %d\n", point.pt.x, point.pt.y, point.size );*/ keypoints.push_back(kpt); } } } } } }
6. 總結
總體來說,如果理解了SIFT算法,再來看SURF算法會發現思路非常簡單。尤其是局部最大值查找方面,基本一致。關鍵還是一個用積分圖來簡化卷積的思路,以及怎麼用不同的模板來近似原來尺度空間中的高斯濾波器。
這一篇主要討論分析的是SURF的定位問題,下面還有SURF特徵點的方向計算與描述子的生成,將在下一篇文章中詳細描述。
轉載自:http://www.cnblogs.com/ronny/p/4045979.html