
應該是大半年前了,老師帶領我做的一個項目主體部分已經完成了,但是投入運行的時候有一個很小的問題,就是需要在圖像中知道一個圓形區域的邊緣,由於這個圓形區域的半徑是非遞減隨時間變化的而且圓心是固定的.當時解決的辦法是找到變化的規律,然后將半徑設置成隨時間變化即可.不過此方法治標不治本,萬一中途實際運行過程中發生了暫停類似時間,而程序還在計時,會有不同步的問題發生.后來希望由程序依托於圖像自動找到變化的半徑來解決這個問題,不過盡管最終識別的結果還算理想,但是考慮到系統已經投入測試運行了,修改的過程涉及到多次的測試過程,而且發現設置為隨時間自動變化的效果還挺不錯,就沒把這段代碼給嵌入進實際運行的系統中了.
半年過去了,當我回顧這段代碼的時候,發現了以前寫代碼的一些問題,同時也希望再了解一下這個算法,下面就從要解決的問題和項目中的代碼中窺探過去.
要識別的原圖:

原圖

黃線為需要識別出來的圓
可能這樣的圖會比較好識別,但是如果是下面的這幾副可能就沒那么好識別了:

當時使用過大律法閾值判定,OpenCV的霍夫圓變換但效果都不是我想要的.
而且圖中需要識別出來的也並非為完整的圓.
前面也說過了,這個是在測試現場提出來的一個需求,時間比較緊迫.可能我也沒花太多時間去找更多的方法來適應這個問題.
好,下面是具體問題:
已知:
- 圓心坐標,最小半徑,最大半徑.
- (從圖中也能看出我們要檢測的圓形的半徑是有最大值的,不會大過白色區域的區域半徑.
- 半徑是確定的,因為攝像頭的位置和下面需要檢測的物體都是不會發生移動的.)
求解:
- 在最小半徑與最大半徑之間找到不斷變化的圓形半徑.
我的思路:
尋找灰度值變化率大於某個閾值次數最多的那個半徑.
- 按照角度遞增選取圓上的點到圓心的連線,連線包含所經過的像素坐標及其灰度值.(效果圖可以見圖一)
- 計算每條連線的灰度變化值.
- 對每條連線的灰度變化值進行歸一化處理.
- 選取大於某個閾值次數最多的那個半徑.
我認為不足之處在於最后的那個閾值的選取,這個階段需要人工的提前通過測試樣本選取出一個合理的值.
不過意識到這個問題后,我選取了滿足條件次數最多的十條半徑作為參考,因為實際過程中半徑是非遞減的而且不會發生突變,所以可以根據上一個獲取的結果來從十個參考結果中選取一個較為合理的半徑作為這次識別的最終結果.

(圖一)
圓上的點到圓心的連線,白色是為了顯示效果,實際為圖像坐標對應的像素

(圖二)
選取十個變化率最大的可參考半徑

(圖三)
選取變化率最大的那個半徑
嗯,看起來目前問題得到了很好的解決.實事也的確如此,我測試了一個大概有3k幀的圖像,效果還是很不錯的.

(圖四)
幾個預計比較難識別的圖像
而且分析的時間滿足肉眼對實時處理的要求(小於0.05s/幀,大於20FPS).
但是,當我打開了含有源代碼的文件時,關於這個項目的介紹只有...

我很慶幸我寫了時間,要不是工程文件名取得闊以:) 我真不知道這幾個cpp是干嘛的.
但至少從這個項目中我已經開始了模塊化的行為,使得我review省了點力.
1 //傳遞圖像以便掃描 2 findInterCircle.scan(frame,scanValue); 3 //獲取最佳半徑 4 int raduis = findInterCircle.getBestRaduis()
1 void CCFindInterCircle::scan(Mat & _readImage,scanValue_s &_scanValue){ 2 3 //圖像數據預處理 4 preProcess(_readImage,_scanValue); 5 6 //開始數據處理 7 Process (); 8 9 //對計算結果的清理 10 basicClear(); 11 12 }
主要的過程在Process()函數中.
這里有一個函數命名習慣不統一的問題.三個函數的首字母大小寫應該相同,可能當時覺得這個函數很重要,不大寫凸顯不出與其他函數的不同.
哈哈.
1 void CCFindInterCircle::Process (){ 2 size_t result = 0; 3 4 /*1*/ 5 //生成與圓的頂點 6 result = calInterWithCircle(); 7 //cout <<"生成了"<<result<<"個與圓的交點"<<endl; 8 9 /*2*/ 10 //獲取連線的像素信息 11 result = calTotalLineCoorInfo(); 12 //cout <<"生成了"<<result<<"個連線段"<<endl; 13 14 /*3*/ 15 //對所有連線求取變化率 16 result = calTotalLineDeri(); 17 //cout <<"對"<<result<<"條連線段求取了變化率"<<endl; 18 19 /*4*/ 20 //求取可參考的半徑 提供10個數據 21 result = calReferRaduis(); 22 //cout <<"計算出了"<<result<<"個可參考的半徑"<<endl; 23 24 /*5*/ 25 filterRefeRaduis(); 26 //cout << "過濾篩選出最佳半徑是:"<<currRadius<<endl; 27 }
這里的功能區分能讓閱讀者更好的查看所示意的功能,說明當時我的思路還算清晰.
其實當時也是為了更好的排錯.典型一個方便自己,幸福他人的舉措.
1.生成與圓的交點:
我找increaseAngle的初始化還挺費勁,最后發現它初始化為5.
1 const double PI = 3.141926; 2 Point2i interPoint; 3 float origAngle = 0.0; 4 while (origAngle < 360) { 5 //獲得圓點和圓弧連接起來的交點 6 interPoint.x = outsideRadius * cos(origAngle * PI/180); 7 interPoint.y = outsideRadius * sin(origAngle * PI/180); 8 interPointVec.push_back(interPoint); 9 10 origAngle += increaseAngle; 11 }
2.獲取連線的像素信息
這里的vector使用已經預示着變量命名難以區別的問題了.
1 size_t CCFindInterCircle::calTotalLineCoorInfo(){ 2 vector<coorInfo_s> coorInfoVec; 3 for (vector<Point2i>::iterator it = interPointVec.begin(); it != interPointVec.end() ; ++it) { 4 // *it -> centerPoint 之間的像素信息 5 calInfoOfTwoPoints(*it,this->centerPoint,coorInfoVec); 6 totalLineInfoVec.push_back(coorInfoVec); 7 coorInfoVec.clear(); 8 } 9 return totalLineInfoVec.size(); 10 }
3.求所有連線的變化率並歸一化
問題同上
1 size_t CCFindInterCircle::calTotalLineDeri(){ 2 vector<int> lineDeriVec; 3 for (vector<vector<coorInfo_s>>::iterator it = totalLineInfoVec.begin(); it != totalLineInfoVec.end(); ++it) { 4 // 計算變化值 5 calLineDeri(*it,lineDeriVec,outsideRadius-insideRadius); 6 totalLineDeriVec.push_back(lineDeriVec); 7 lineDeriVec.clear(); 8 } 9 //歸一化 10 normalLineDeri(); 11 return totalLineDeriVec.size(); 12 }
4.求取可參考的半徑 提供10個數據
1 size_t CCFindInterCircle::calReferRaduis () { 2 3 vector<int>count; 4 count.resize(((outsideRadius - insideRadius) / lineSection) + 1); 5 6 for (size_t i = 0;i < totalLineDeriNorVec.size() ; i ++) { 7 for (size_t j = 0; j < totalLineDeriNorVec.at(i).size(); j++) { 8 if(totalLineDeriNorVec.at(i).at(j) > lineSection_thresh) 9 count[j / lineSection] ++; 10 } 11 } 12 13 //用一個算法解決求第1-10最大數 14 size_t referenceBestRaduisCount = 10; 15 referenceBestRaduis.resize(referenceBestRaduisCount); 16 vector<size_t> maxOrder; 17 size_t maxNumCount = 0,index = 0; 18 int max = 255,diff = 255; 19 while (maxNumCount < referenceBestRaduisCount) { 20 for (size_t k = 0;k < count.size() ; k++) { 21 if((max - count[k]) < diff){ 22 index = k; 23 diff = max - count[index]; 24 } 25 } 26 max = count[index]; 27 maxOrder.push_back(index); 28 referenceBestRaduis[maxNumCount] = ((outsideRadius - (index+1) * lineSection)); 29 30 //尾處理 31 count[index] = 0; 32 diff = 255; 33 index = 0; 34 35 maxNumCount ++; 36 } 37 return referenceBestRaduis.size() ; 38 }
看到半年前寫的代碼,心里的感覺是十分復雜的,一方面是自己的成長,另一方面又是自己的不足.
沒錯,由於缺少了幫助我理解的注釋,我已經放棄閱讀"4."這樣的代碼了.只知道他的功能 :(
因為我並不知道"尾處理"是在處理什么,而且"用一個算法解決..".並不能對我理解下面的算法起到多大的幫助.
所以對於這樣的注釋我真想找那個時候的我好好談談!
真的.
問題變多了,我想了解這個類中一些變量的具體含義.
1 private: 2 3 //私有成員變量 4 Mat processImage; 5 Mat drawImage; 6 7 //可設置變量 8 Point2i centerPoint; 9 size_t outsideRadius; 10 size_t insideRadius; 11 float increaseAngle; 12 int lineSection; 13 float lineSection_thresh; 14 int refeIncrThresh; 15 int lastRadius; 16 17 int currRadius; 18 bool isReset; 19 bool firstStart; 20 21 //計算結果變量 22 vector<Point2i> interPointVec; 23 vector<vector<coorInfo_s>>totalLineInfoVec; 24 vector<vector<int>> totalLineDeriVec; 25 vector<vector<float>> totalLineDeriNorVec; 26 vector<int>referenceBestRaduis
我的刀呢?
當初開發的時候沒想怎么合理的組織全部的數據,想着比較簡單而且應該不用很多時間就能解決.
於是是算到哪兒就是哪兒,就有了許多的"臨時"的計算結果,而且許多都是可以設計為一個數據結構就可以包含在一起,而不用另開一個vector來存儲.
之后還要想清楚這幾個vector之間的相互映射關系,這一點頗為痛苦.
這也是我今后在設計程序結構的時候需要考慮的.如何設計數據存儲的結構使得可以迅速找到不同數據之間的關系,這里像素坐標和其灰度值使用了coorInfo_s結構存儲,但是還可以和變化值聯系在一起,類似的考慮不周到還有許多.
這里還有一個問題是變量的注釋,應該明確指出該變量的含義,而不是...哈!但是現在這個問題我后來看過他人幾個項目后就意識到了...
這里用到了一個Bresenham畫直線算法,記一下吧.
(計算從_pointA 到 _pointB之間的像素點.將其像素坐標和像素值push_back進_coorInfoVec中)
1 void CCFindInterCircle::calInfoOfTwoPoints(Point2i & _pointA,Point2i & _pointB,vector<coorInfo_s>& _coorInfoVec){ 2 3 int dx = _pointB.x - _pointA.x; 4 int dy = _pointB.y - _pointA.y; 5 int ux = ((dx > 0) << 1) - 1;//x的增量方向,取或-1 6 int uy = ((dy > 0) << 1) - 1;//y的增量方向,取或-1 7 int x = _pointA.x, y = _pointA.y; 8 9 int eps;//eps為累加誤差 10 11 eps = 0;dx = abs(dx); dy = abs(dy); 12 13 coorInfo_s coorInfo; 14 15 if (dx > dy) 16 { 17 for (x = _pointA.x; x != _pointB.x+ux; x += ux) 18 { 19 coorInfo.setValue(x,y,this->processImage.at<uchar>(y,x)); 20 _coorInfoVec.push_back(coorInfo); 21 eps += dy; 22 if ((eps << 1) >= dx) 23 { 24 y += uy; eps -= dx; 25 } 26 } 27 } 28 else 29 { 30 for (y = _pointA.y; y != _pointB.y+uy; y += uy) 31 { 32 coorInfo.setValue(x,y,this->processImage.at<uchar>(y,x)); 33 _coorInfoVec.push_back(coorInfo); 34 eps += dx; 35 if ((eps << 1) >= dy) 36 { 37 x += ux; eps -= dy; 38 } 39 } 40 } 41 }
總結:
- 我希望今后寫代碼的注釋能夠提供有用的信息,多看看別人的代碼吧;
- 項目的注釋很重要,能讓閱讀者(包括自己)花少量的時間明白下面幾百行在干什么事;
- 對於數據結構的設計希望能想得周到些,多讀書,多思考吧.
- 若要做到在時間限制的情況下還能寫出優秀的代碼,多練習吧.
至此,這個被我稱為"FindInterCircle"的小模塊我就回顧完了,今年還有新項目 加油吧!!!!
失敗會累積,而成功會消失.
