OpenCV中提供了SimpleBlobDetector的特征點檢測方法,正如它的名稱,該算法使用最簡單的方式來檢測斑點類的特征點。下面我們就來分析一下該算法。
首先通過一系列連續的閾值把輸入的灰度圖像轉換為一個二值圖像的集合,閾值范圍為[T1,T2],步長為t,則所有閾值為:
T1,T1+t,T1+2t,T1+3t,……,T2 (1)
第二步是利用Suzuki提出的算法通過檢測每一幅二值圖像的邊界的方式提取出每一幅二值圖像的連通區域,我們可以認為由邊界所圍成的不同的連通區域就是該二值圖像的斑點;
第三步是根據所有二值圖像斑點的中心坐標對二值圖像斑點進行分類,從而形成灰度圖像的斑點,屬於一類的那些二值圖像斑點最終形成灰度圖像的斑點,具體來說就是,灰度圖像的斑點是由中心坐標間的距離小於閾值Tb的那些二值圖像斑點所組成的,即這些二值圖像斑點屬於該灰度圖像斑點;
最后就是確定灰度圖像斑點的信息——位置和尺寸。位置是屬於該灰度圖像斑點的所有二值圖像斑點中心坐標的加權和,即公式2,權值q等於該二值圖像斑點的慣性率的平方,它的含義是二值圖像的斑點的形狀越接近圓形,越是我們所希望的斑點,因此對灰度圖像斑點位置的貢獻就越大。尺寸則是屬於該灰度圖像斑點的所有二值圖像斑點中面積大小居中的半徑長度。
(2)
在第二步中,並不是所有的二值圖像的連通區域都可以認為是二值圖像的斑點,我們往往通過一些限定條件來得到更准確的斑點。這些限定條件包括顏色,面積和形狀,斑點的形狀又可以用圓度,偏心率,或凸度來表示。
對於二值圖像來說,只有兩種斑點顏色——白色斑點和黑色斑點,我們只需要一種顏色的斑點,通過確定斑點的灰度值就可以區分出斑點的顏色。
連通區域的面積太大和太小都不是斑點,所以我們需要計算連通區域的面積,只有當該面積在我們所設定的最大面積和最小面積之間時,該連通區域才作為斑點被保留下來。
圓形的斑點是最理想的,任意形狀的圓度C定義為:
(3)
其中,S和p分別表示該形狀的面積和周長,當C為1時,表示該形狀是一個完美的圓形,而當C為0時,表示該形狀是一個逐漸拉長的多邊形。
偏心率是指某一橢圓軌道與理想圓形的偏離程度,長橢圓軌道的偏心率高,而近於圓形的軌道的偏心率低。圓形的偏心率等於0,橢圓的偏心率介於0和1之間,而偏心率等於1表示的是拋物線。直接計算斑點的偏心率較為復雜,但利用圖像矩的概念計算圖形的慣性率,再由慣性率計算偏心率較為方便。偏心率E和慣性率I之間的關系為:
(4)
因此圓形的慣性率等於1,慣性率越接近1,圓形的程度越高。
最后一個表示斑點形狀的量是凸度。在平面中,凸形圖指的是圖形的所有部分都在由該圖形切線所圍成的區域的內部。我們可以用凸度來表示斑點凹凸的程度,凸度V的定義為:
(5)
其中,H表示該斑點的凸殼面積
在計算斑點的面積,中心處的坐標,尤其是慣性率時,都可以應用圖像矩的方法。
下面我們就介紹該方法。
矩在統計學中被用來反映隨機變量的分布情況,推廣到力學中,它被用來描述空間物體的質量分布。同樣的道理,如果我們將圖像的灰度值看作是一個二維的密度分布函數,那么矩方法即可用於圖像處理領域。設f(x,y)是一幅數字圖像,則它的矩Mij為:
(6)
對於二值圖像的來說,零階矩M00等於它的面積。圖形的質心為:
(7)
圖像的中心矩μpq定義為:
(8)
一階中心矩稱為靜矩,二階中心矩稱為慣性矩。如果僅考慮二階中心矩的話,則圖像完全等同於一個具有確定的大小、方向和離心率,以圖像質心為中心且具有恆定輻射度的橢圓。圖像的協方差矩陣為:
(9)
該矩陣的兩個特征值λ1和λ2對應於圖像強度(即橢圓)的主軸和次軸:
(10)
(11)
而圖像的方向角度θ為:
(12)
圖像的慣性率I為:
(13)
下面給出SimpleBlobDetector的源碼分析。我們先來看看SimpleBlobDetector類的默認參數的設置:
1 SimpleBlobDetector::Params::Params() 2 { 3 thresholdStep = 10; //二值化的閾值步長,即公式1的t
4 minThreshold = 50; //二值化的起始閾值,即公式1的T1
5 maxThreshold = 220; //二值化的終止閾值,即公式1的T2 6 //重復的最小次數,只有屬於灰度圖像斑點的那些二值圖像斑點數量大於該值時,該灰度圖像斑點才被認為是特征點
7 minRepeatability = 2; 8 //最小的斑點距離,不同二值圖像的斑點間距離小於該值時,被認為是同一個位置的斑點,否則是不同位置上的斑點
9 minDistBetweenBlobs = 10; 10
11 filterByColor = true; //斑點顏色的限制變量
12 blobColor = 0; //表示只提取黑色斑點;如果該變量為255,表示只提取白色斑點
13
14 filterByArea = true; //斑點面積的限制變量
15 minArea = 25; //斑點的最小面積
16 maxArea = 5000; //斑點的最大面積
17
18 filterByCircularity = false; //斑點圓度的限制變量,默認是不限制
19 minCircularity = 0.8f; //斑點的最小圓度 20 //斑點的最大圓度,所能表示的float類型的最大值
21 maxCircularity = std::numeric_limits<float>::max(); 22
23 filterByInertia = true; //斑點慣性率的限制變量 24 //minInertiaRatio = 0.6;
25 minInertiaRatio = 0.1f; //斑點的最小慣性率
26 maxInertiaRatio = std::numeric_limits<float>::max(); //斑點的最大慣性率
27
28 filterByConvexity = true; //斑點凸度的限制變量 29 //minConvexity = 0.8;
30 minConvexity = 0.95f; //斑點的最小凸度
31 maxConvexity = std::numeric_limits<float>::max(); //斑點的最大凸度
32 }
我們再來介紹檢測二值圖像斑點的函數findBlobs。
1 //image為輸入的灰度圖像 2 //binaryImage為二值圖像 3 //centers表示該二值圖像的斑點
4 void SimpleBlobDetector::findBlobs(const cv::Mat &image, const cv::Mat &binaryImage, vector<Center> ¢ers) const
5 { 6 (void)image; 7 centers.clear(); //斑點變量清零
8
9 vector < vector<Point> > contours; //定義二值圖像的斑點的邊界像素變量
10 Mat tmpBinaryImage = binaryImage.clone(); //復制二值圖像 11 //調用findContours函數,找到當前二值圖像的所有斑點的邊界
12 findContours(tmpBinaryImage, contours, CV_RETR_LIST, CV_CHAIN_APPROX_NONE); 13
14 #ifdef DEBUG_BLOB_DETECTOR 15 // Mat keypointsImage; 16 // cvtColor( binaryImage, keypointsImage, CV_GRAY2RGB ); 17 //
18 // Mat contoursImage; 19 // cvtColor( binaryImage, contoursImage, CV_GRAY2RGB ); 20 // drawContours( contoursImage, contours, -1, Scalar(0,255,0) ); 21 // imshow("contours", contoursImage );
22 #endif
23 //遍歷當前二值圖像的所有斑點
24 for (size_t contourIdx = 0; contourIdx < contours.size(); contourIdx++) 25 { 26 //結構類型Center代表着斑點,它包括斑點的中心位置,半徑和權值
27 Center center; //斑點變量 28 //初始化斑點中心的置信度,也就是該斑點的權值
29 center.confidence = 1; 30 //調用moments函數,得到當前斑點的矩
31 Moments moms = moments(Mat(contours[contourIdx])); 32 if (params.filterByArea) //斑點面積的限制
33 { 34 double area = moms.m00; //零階矩即為二值圖像的面積 35 //如果面積超出了設定的范圍,則不再考慮該斑點
36 if (area < params.minArea || area >= params.maxArea) 37 continue; 38 } 39
40 if (params.filterByCircularity) //斑點圓度的限制
41 { 42 double area = moms.m00; //得到斑點的面積 43 //計算斑點的周長
44 double perimeter = arcLength(Mat(contours[contourIdx]), true); 45 //由公式3得到斑點的圓度
46 double ratio = 4 * CV_PI * area / (perimeter * perimeter); 47 //如果圓度超出了設定的范圍,則不再考慮該斑點
48 if (ratio < params.minCircularity || ratio >= params.maxCircularity) 49 continue; 50 } 51
52 if (params.filterByInertia) //斑點慣性率的限制
53 { 54 //計算公式13中最右側等式中的開根號的值
55 double denominator = sqrt(pow(2 * moms.mu11, 2) + pow(moms.mu20 - moms.mu02, 2)); 56 const double eps = 1e-2; //定義一個極小值
57 double ratio; 58 if (denominator > eps) 59 { 60 //cosmin和sinmin用於計算圖像協方差矩陣中較小的那個特征值λ2
61 double cosmin = (moms.mu20 - moms.mu02) / denominator; 62 double sinmin = 2 * moms.mu11 / denominator; 63 //cosmin和sinmin用於計算圖像協方差矩陣中較大的那個特征值λ1
64 double cosmax = -cosmin; 65 double sinmax = -sinmin; 66 //imin為λ2乘以零階中心矩μ00
67 double imin = 0.5 * (moms.mu20 + moms.mu02) - 0.5 * (moms.mu20 - moms.mu02) * cosmin - moms.mu11 * sinmin; 68 //imax為λ1乘以零階中心矩μ00
69 double imax = 0.5 * (moms.mu20 + moms.mu02) - 0.5 * (moms.mu20 - moms.mu02) * cosmax - moms.mu11 * sinmax; 70 ratio = imin / imax; //得到斑點的慣性率
71 } 72 else
73 { 74 ratio = 1; //直接設置為1,即為圓
75 } 76 //如果慣性率超出了設定的范圍,則不再考慮該斑點
77 if (ratio < params.minInertiaRatio || ratio >= params.maxInertiaRatio) 78 continue; 79 //斑點中心的權值定義為慣性率的平方
80 center.confidence = ratio * ratio; 81 } 82
83 if (params.filterByConvexity) //斑點凸度的限制
84 { 85 vector < Point > hull; //定義凸殼變量 86 //調用convexHull函數,得到該斑點的凸殼
87 convexHull(Mat(contours[contourIdx]), hull); 88 //分別得到斑點和凸殼的面積,contourArea函數本質上也是求圖像的零階矩
89 double area = contourArea(Mat(contours[contourIdx])); 90 double hullArea = contourArea(Mat(hull)); 91 double ratio = area / hullArea; //公式5,計算斑點的凸度 92 //如果凸度超出了設定的范圍,則不再考慮該斑點
93 if (ratio < params.minConvexity || ratio >= params.maxConvexity) 94 continue; 95 } 96
97 //根據公式7,計算斑點的質心
98 center.location = Point2d(moms.m10 / moms.m00, moms.m01 / moms.m00); 99
100 if (params.filterByColor) //斑點顏色的限制
101 { 102 //判斷一下斑點的顏色是否正確
103 if (binaryImage.at<uchar> (cvRound(center.location.y), cvRound(center.location.x)) != params.blobColor) 104 continue; 105 } 106
107 //compute blob radius
108 { 109 vector<double> dists; //定義距離隊列 110 //遍歷該斑點邊界上的所有像素
111 for (size_t pointIdx = 0; pointIdx < contours[contourIdx].size(); pointIdx++) 112 { 113 Point2d pt = contours[contourIdx][pointIdx]; //得到邊界像素坐標 114 //計算該點坐標與斑點中心的距離,並放入距離隊列中
115 dists.push_back(norm(center.location - pt)); 116 } 117 std::sort(dists.begin(), dists.end()); //距離隊列排序 118 //計算斑點的半徑,它等於距離隊列中中間兩個距離的平均值
119 center.radius = (dists[(dists.size() - 1) / 2] + dists[dists.size() / 2]) / 2.; 120 } 121
122 centers.push_back(center); //把center變量壓入centers隊列中
123
124 #ifdef DEBUG_BLOB_DETECTOR 125 // circle( keypointsImage, center.location, 1, Scalar(0,0,255), 1 );
126 #endif
127 } 128 #ifdef DEBUG_BLOB_DETECTOR 129 // imshow("bk", keypointsImage ); 130 // waitKey();
131 #endif
132 }
最后介紹檢測特征點的函數detectImpl。
1 void SimpleBlobDetector::detectImpl(const cv::Mat& image, std::vector<cv::KeyPoint>& keypoints, const cv::Mat&) const
2 { 3 //TODO: support mask
4 keypoints.clear(); //特征點變量清零
5 Mat grayscaleImage; 6 //把彩色圖像轉換為二值圖像
7 if (image.channels() == 3) 8 cvtColor(image, grayscaleImage, CV_BGR2GRAY); 9 else
10 grayscaleImage = image; 11 //二維數組centers表示所有得到的斑點,第一維數據表示的是灰度圖像斑點,第二維數據表示的是屬於該灰度圖像斑點的所有二值圖像斑點 12 //結構類型Center代表着斑點,它包括斑點的中心位置,半徑和權值
13 vector < vector<Center> > centers; 14 //遍歷所有閾值,進行二值化處理
15 for (double thresh = params.minThreshold; thresh < params.maxThreshold; thresh += params.thresholdStep) 16 { 17 Mat binarizedImage; 18 //調用threshold函數,把灰度圖像grayscaleImage轉換為二值圖像binarizedImage
19 threshold(grayscaleImage, binarizedImage, thresh, 255, THRESH_BINARY); 20
21 #ifdef DEBUG_BLOB_DETECTOR 22 // Mat keypointsImage; 23 // cvtColor( binarizedImage, keypointsImage, CV_GRAY2RGB );
24 #endif
25 //變量curCenters表示該二值圖像內的所有斑點
26 vector < Center > curCenters; 27 //調用findBlobs函數,對二值圖像binarizedImage檢測斑點,得到curCenters
28 findBlobs(grayscaleImage, binarizedImage, curCenters); 29 //newCenters表示在當前二值圖像內檢測到的不屬於已有灰度圖像斑點的那些二值圖像斑點
30 vector < vector<Center> > newCenters; 31 //遍歷該二值圖像內的所有斑點
32 for (size_t i = 0; i < curCenters.size(); i++) 33 { 34 #ifdef DEBUG_BLOB_DETECTOR 35 // circle(keypointsImage, curCenters[i].location, curCenters[i].radius, Scalar(0,0,255),-1);
36 #endif
37 // isNew表示的是當前二值圖像斑點是否為新出現的斑點
38 bool isNew = true; 39 //遍歷已有的所有灰度圖像斑點,判斷該二值圖像斑點是否為新的灰度圖像斑點
40 for (size_t j = 0; j < centers.size(); j++) 41 { 42 //屬於該灰度圖像斑點的中間位置的那個二值圖像斑點代表該灰度圖像斑點,並把它的中心坐標與當前二值圖像斑點的中心坐標比較,計算它們的距離dist
43 double dist = norm(centers[j][ centers[j].size() / 2 ].location - curCenters[i].location); 44 //如果距離大於所設的閾值,並且距離都大於上述那兩個二值圖像斑點的半徑,則表示該二值圖像的斑點是新的斑點
45 isNew = dist >= params.minDistBetweenBlobs && dist >= centers[j][ centers[j].size() / 2 ].radius && dist >= curCenters[i].radius; 46 //如果不是新的斑點,則需要把它添加到屬於它的當前(即第j個)灰度圖像的斑點中,因為通過上面的距離比較可知,當前二值圖像斑點屬於當前灰度圖像斑點
47 if (!isNew) 48 { 49 //把當前二值圖像斑點存入當前(即第j個)灰度圖像斑點數組的最后位置
50 centers[j].push_back(curCenters[i]); 51 //得到構成該灰度圖像斑點的所有二值圖像斑點的數量
52 size_t k = centers[j].size() - 1; 53 //按照半徑由小至大的順序,把新得到的當前二值圖像斑點放入當前灰度圖像斑點數組的適當位置處,由於二值化閾值是按照從小到大的順序設置,所以二值圖像斑點本身就是按照面積的大小順序被檢測到的,因此此處的排序處理要相對簡單一些
54 while( k > 0 && centers[j][k].radius < centers[j][k-1].radius ) 55 { 56 centers[j][k] = centers[j][k-1]; 57 k--; 58 } 59 centers[j][k] = curCenters[i]; 60 //由於當前二值圖像斑點已經找到了屬於它的灰度圖像斑點,因此退出for循環,無需再遍歷已有的灰度圖像斑點
61 break; 62 } 63 } 64 if (isNew) //當前二值圖像斑點是新的斑點
65 { 66 //把當前斑點存入newCenters數組內
67 newCenters.push_back(vector<Center> (1, curCenters[i])); 68 //centers.push_back(vector<Center> (1, curCenters[i]));
69 } 70 } 71 //把當前二值圖像內的所有newCenters復制到centers內
72 std::copy(newCenters.begin(), newCenters.end(), std::back_inserter(centers)); 73
74 #ifdef DEBUG_BLOB_DETECTOR 75 // imshow("binarized", keypointsImage ); 76 //waitKey();
77 #endif
78 } //所有二值圖像斑點檢測完畢 79 //遍歷所有灰度圖像斑點,得到特征點信息
80 for (size_t i = 0; i < centers.size(); i++) 81 { 82 //如果屬於當前灰度圖像斑點的二值圖像斑點的數量小於所設閾值,則該灰度圖像斑點不是特征點
83 if (centers[i].size() < params.minRepeatability) 84 continue; 85 Point2d sumPoint(0, 0); 86 double normalizer = 0; 87 //遍歷屬於當前灰度圖像斑點的所有二值圖像斑點
88 for (size_t j = 0; j < centers[i].size(); j++) 89 { 90 sumPoint += centers[i][j].confidence * centers[i][j].location; //公式2的分子
91 normalizer += centers[i][j].confidence; //公式2的分母
92 } 93 sumPoint *= (1. / normalizer); //公式2,得到特征點的坐標位置 94 //保存該特征點的坐標位置和尺寸大小
95 KeyPoint kpt(sumPoint, (float)(centers[i][centers[i].size() / 2].radius)); 96 keypoints.push_back(kpt); //保存該特征點
97 } 98
99 #ifdef DEBUG_BLOB_DETECTOR 100 namedWindow("keypoints", CV_WINDOW_NORMAL); 101 Mat outImg = image.clone(); 102 for(size_t i=0; i<keypoints.size(); i++) 103 { 104 circle(outImg, keypoints[i].pt, keypoints[i].size, Scalar(255, 0, 255), -1); 105 } 106 //drawKeypoints(image, keypoints, outImg);
107 imshow("keypoints", outImg); 108 waitKey(); 109 #endif
110 }
下面我們給出一個具體的應用實例。
1 #include "opencv2/core/core.hpp"
2 #include "highgui.h"
3 #include "opencv2/imgproc/imgproc.hpp"
4 #include "opencv2/features2d/features2d.hpp"
5 #include "opencv2/nonfree/nonfree.hpp"
6
7 using namespace cv; 8 //using namespace std;
9
10 int main(int argc, char** argv) 11 { 12 Mat img = imread("box_in_scene.png"); 13
14 SimpleBlobDetector::Params params; 15 params.minThreshold = 40; 16 params.maxThreshold = 160; 17 params.thresholdStep = 5; 18 params.minArea = 100; 19 params.minConvexity = .05f; 20 params.minInertiaRatio = .05f; 21 params.maxArea = 8000; 22
23 SimpleBlobDetector detector(params); 24
25 vector<KeyPoint> key_points; 26
27 detector.detect(img,key_points); 28
29 Mat output_img; 30
31 drawKeypoints( img, key_points, output_img, Scalar(0,0,255), DrawMatchesFlags::DRAW_RICH_KEYPOINTS ); 32
33 namedWindow("SimpleBlobDetector"); 34 imshow("SimpleBlobDetector", output_img); 35 waitKey(0); 36
37 return 0; 38 }
另一個案例使用 Ptr<SimpleBlobDetector>
1 #include<opencv2/opencv.hpp>
2 #include <iostream>
3
4 using namespace std; 5 using namespace cv; 6 int main() 7 { 8 Mat src=imread("D:/sunflower.png"); 9 //*參數設置,以下都是默認參數
10 SimpleBlobDetector::Params pDefaultBLOB; 11 pDefaultBLOB.thresholdStep = 10; 12 pDefaultBLOB.minThreshold = 50; 13 pDefaultBLOB.maxThreshold = 220; 14 pDefaultBLOB.minRepeatability = 2; 15 pDefaultBLOB.minDistBetweenBlobs = 10; 16 pDefaultBLOB.filterByColor = true; 17 pDefaultBLOB.blobColor = 0; 18 pDefaultBLOB.filterByArea = true; 19 pDefaultBLOB.minArea = 25; 20 pDefaultBLOB.maxArea = 5000; 21 pDefaultBLOB.filterByCircularity = false; 22 pDefaultBLOB.minCircularity = 0.8f; 23 pDefaultBLOB.maxCircularity = (float)3.40282e+038; 24 pDefaultBLOB.filterByInertia = true; 25 pDefaultBLOB.minInertiaRatio = 0.1f; 26 pDefaultBLOB.maxInertiaRatio = (float)3.40282e+038; 27 pDefaultBLOB.filterByConvexity = true; 28 pDefaultBLOB.minConvexity = 0.95f; 29 pDefaultBLOB.maxConvexity = (float)3.40282e+038; 30 //*用參數創建對象
31 Ptr<SimpleBlobDetector> blob=SimpleBlobDetector::create(pDefaultBLOB); 32 //Ptr<SimpleBlobDetector> blob=SimpleBlobDetector::create();//默認參數創建 33 //*blob檢測
34 vector<KeyPoint> key_points; 35 blob->detect(src,key_points); 36 Mat outImg; 37 //*繪制結果
38 drawKeypoints(src,key_points,outImg,Scalar(0,0,255)); 39 imshow("blob",outImg); 40
41 waitKey(); 42 return 0; 43 }