目錄
一、算法基本原理
1、圖片預處理
2、找表盤
3、找指針
4、指針映射
5、求時間
二、算法流程圖
三、程序關鍵函數說明
1、Canny
2、HoughCircles
3、HoughLines2
4、MyLine類
5、平面幾何相關函數
四、運行結果
五、實驗中遇到的主要問題及解決方法:
1、在處理速度方面
2、去除其他圓的影響
3、霍夫找到的直線轉換為夾角表示的線段
六、實驗中的缺陷和不足
一、算法基本原理
時鍾識別,顧名思義:就是根據一張帶有鍾表的圖片識別出鍾表上所展示的時間。對於這個問題我把它分為四步處理:
- 1、 圖片預處理
由於一張圖片中包含的圓圈和直線的信息量較大,直接進行霍夫處理要浪費大量的運算量,而且也不利於去除各種干擾。於是這里所謂的預處理就是利用canny算法先對原圖進行邊緣提取,一方面濾掉部分干擾,另一方面將原圖轉換為邊緣圖后只剩下主要線條信息,有利於加快處理。實驗證明,用此方法有用信息丟失較少,可以采用!
- 2、 找表盤
找表盤主要運用霍夫找圓,但是由於一張圖里會找出很多圓,因此關鍵還是濾掉其他有影響的圓!這里采用的方法是找出所有用霍夫找到的圓中半徑最大且整個圓都在當前圖片中的那個圓作為表盤。實踐證明,用這種方法能夠較為准確的去除其他圓的影響並准確找到表盤所對應的圓(對了,這里的鍾默認為圓形的表盤的指針式鍾)。
- 3、 找指針
找指針主要運用霍夫找線段,但是可想而知一張圖里除了指針外一定還存在其他線段,而這些線段會對指針識別造成影響。因此我采用判斷線段到圓心的距離小於一定的距離才默認為指針。然后對獲取求得的線段進行轉換為我定義的用斜率表示的線段的形式。
- 4、 指針映射
由於上面求得的線段並不是嚴格上的指針,而是很多線段,但是如果仔細觀察會發現雖然並不是嚴格的對應關系,但是他們會出現成簇的分組效果!即:多條線段會聚集在某一個指針左右,而這些線段的斜率和預求得指針的斜率相差不大,我正是利用它的這個特點首先根據指針與X軸正方向的夾角從小到大進行排序,然后每次遇到相鄰兩個線段的夾角存在較大的跳變就進行切割,最后所有的線段被分割為一些組,再利用一個特殊的公式重新計算這組所代表指針的長度:
Le[num-1]=Le_ping[num-1]*0.2+Le_max[num-1]*0.8;
- 5、 求時間
通過上面四步的計算我們已經能夠將線段分為三組或者兩組,這樣再根據長短來和時分秒三個或者時分指針進行對應,再根據夾角就能求出對應的時間。
二、算法流程圖
三、程序關鍵函數說明
1、 Canny
void cvCanny( const CvArr* image,CvArr* edges,double threshold1,double threshold2, int aperture_size=3 );
說明: 開放計算機視覺(OpenCV)庫庫函數之一,用於對圖像的邊緣檢測(采用canny算法)。
- image 輸入單通道圖像(可以是彩色圖像)對於多通道的圖像可以用cvCvtColor()修改。
- edges 輸出的邊緣圖像 ,也是單通道的,但是是黑白的
- threshold1 第一個閾值
- threshold2 第二個閾值
- aperture_size Sobel 算子內核大小
附加說明: 函數 cvCanny 采用 Canny 算法發現輸入圖像的邊緣而且在輸出圖像中標識這些邊緣。threshold1和threshold2 當中的小閾值用來控制邊緣連接,大的閾值用來控制強邊緣的初始分割。
2、 HoughCircles
CvSeq *cvHoughCircles(CvArr *image,void *circle_storage,int method,double dp,double min_dist,double param1,double param2,int min_radius,int max_radius);
說明:該函數用Hough變換在二值圖像中中尋找圓,成功時返回CvSeq指針。
- image:輸入8bit(灰度)圖像,其內容可被函數所改變
- circle_storage:檢測到的圓存儲倉,可以是內存存儲倉 (此種情況下,一個線段序列在存儲倉中被創建,並且由函數返回)或者是包含圓參數的特殊類型的具有單行/單列的CV_32FC3型矩陣(CvMat*). 矩陣頭為函數所修改,使得它的 cols/rows 將包含一組檢測到的圓。如果 circle_storage 是矩陣,而實際圓的數目超過矩陣尺寸,那么最大可能數目的圓被返回,每個圓由三個浮點數表示:圓心坐標(x,y)和半徑.).
- method:Hough 變換方式,目前只支持CV_HOUGH_GRADIENT, which is basically 21HT, described in [Yuen03].
- dp:尋找圓弧圓心的累計分辨率,這個參數允許創建一個比輸入圖像分辨率低的累加器。(這樣做是因為有理由認為圖像中存在的圓會自然降低到與圖像寬高相同數量的范疇)。如果dp設置為1,則分辨率是相同的;如果設置為更大的值(比如2),累加器的分辨率受此影響會變小(此情況下為一半)。dp的值不能比1小。
- min_dist:該參數是讓算法能明顯區分的兩個不同圓之間的最小距離。
- param1:用於Canny的邊緣閥值上限,下限被置為上限的一半。
- param2:累加器的閥值。
- min_radius:最小圓半徑。
- max_radius:最大圓半徑。
3、 HoughLines2
CvSeq* cvHoughLines2(CvArr* image,void* line_storage,int mehtod,double rho,double theta,int threshold,double param1 =0,double param2 =0);
說明: 此函數是opencv圖像變換函數中的一個,主要用來訪問霍夫變換的兩個算法———標准霍夫變換(SHT)和累計概率霍夫變換(PPHT)。
- Image:輸入 8-比特、單通道 (二值) 圖像,當用CV_HOUGH_PROBABILISTIC方法檢測的時候其內容會被函數改變。
- line_storage:檢測到的線段存儲倉. 可以是內存存儲倉 (此種情況下,一個線段序列在存儲倉中被創建,並且由函數返回),或者是包含線段參數的特殊類型(見下面)的具有單行/單列的矩陣(CvMat*)。矩陣頭為函數所修改,使得它的 cols/rows 將包含一組檢測到的線段。如果 line_storage 是矩陣,而實際線段的數目超過矩陣尺寸,那么最大可能數目的線段被返回(線段沒有按照長度、可信度或其它指標排序).
- method
- Hough 變換變量,是下面變量的其中之一:
CV_HOUGH_STANDARD - 傳統或標准 Hough 變換. 每一個線段由兩個浮點數 (ρ, θ) 表示,其中 ρ 是直線與原點 (0,0) 之間的距離,θ 線段與 x-軸之間的夾角。因此,矩陣類型必須是 CV_32FC2 type.
CV_HOUGH_PROBABILISTIC - 概率 Hough 變換(如果圖像包含一些長的線性分割,則效率更高). 它返回線段分割而不是整個線段。每個分割用起點和終點來表示,所以矩陣(或創建的序列)類型是 CV_32SC4.
CV_HOUGH_MULTI_SCALE - 傳統 Hough 變換的多尺度變種。線段的編碼方式與 CV_HOUGH_STANDARD 的一致。
- Rho:與像素相關單位的距離精度
- Theta:弧度測量的角度精度
- Threshold:閾值參數。如果相應的累計值大於 threshold, 則函數返回這條線段.
- param1:第一個方法相關的參數:
對傳統 Hough 變換,不使用(0).
對概率 Hough 變換,它是最小線段長度.
對多尺度 Hough 變換,它是距離精度 rho 的分母 (大致的距離精度是 rho 而精確的應該是 rho / param1 ).
- param2:第二個方法相關參數:
對傳統 Hough 變換,不使用 (0).
對概率 Hough 變換,這個參數表示在同一條直線上進行碎線段連接的最大間隔值(gap), 即當同一條直線上的兩條碎線段之間的間隔小於param2時,將其合二為一。
對多尺度 Hough 變換,它是角度精度 theta 的分母 (大致的角度精度是 theta 而精確的角度應該是 theta / param2).
4、 MyLine類
1 //----------------------------------------------------------------------------- 2 class MyLine{ 3 public: 4 int id;//編號 5 int k;//傾斜角[0-360) 6 int l;//長度 7 public: 8 MyLine(int ID=0,int K=0,int L=0){id=ID,k=K,l=L;}//構造函數 9 bool operator<(const MyLine &A){return k<A.k;}//重定義小於號 10 void print(){printf("id: %3d k: %3d° l: %3d\n",id,k,l);}//輸出函數 11 };//自定義直線 12 //-----------------------------------------------------------------------------
5、 平面幾何相關函數
1 //----------------------------------------------------------------------------- 2 //平面幾何相關函數http://www.cnblogs.com/zjutlitao/p/3243883.html 3 //----------------------------------------------------------------------------- 4 #define eps 0.0000000001 5 #define PI acos(-1.0) 6 int dcmp(double x){ 7 if(fabs(x)<eps)return 0; 8 else return x<0 ? -1:1; 9 } 10 double Dot(Point A,Point B){return A.x*B.x+A.y*B.y;}//向量點積 11 double Length(Point A){return sqrt(Dot(A,A));}//向量模長 12 double Cross(Point A,Point B){return A.x*B.y-A.y*B.x;}//向量叉積 13 double Angle(Point A,Point B){return acos(Dot(A,B)/Length(A)/Length(B));}//求向量的夾角 14 double DistanceToLine(Point P,Point A,Point B)//點到直線的距離 15 { 16 Point v1=B-A,v2=P-A; 17 return fabs(Cross(v1,v2))/Length(v1);//如果不加絕對值是帶有方向的距離 18 } 19 double DistancetoSegment(Point P,Point A,Point B){//點到線段的距離 20 if(A==B)return Length(P-A); 21 Point v1=B-A,v2=P-A,v3=P-B; 22 if(dcmp(Dot(v1,v2))<0)return Length(v2); 23 else if(dcmp(Dot(v1,v3))>0)return Length(v3); 24 else return fabs(Cross(v1,v2))/Length(v1); 25 } 26 //-----------------------------------------------------------------------------
四、運行結果
PS:由於篇幅有限,這里就不把全部的圖片列出了~
五、實驗中遇到的主要問題及解決方法:
1、在處理速度方面:
如果直接用原圖做霍夫變換計算量巨大而且干擾特別多,這里我先用canny進行邊緣提取預處理,然后再進行運算就解決了上述問題。但是用霍夫變換的圖不能在上面繪制霍夫找到的直線或圓,結果就要轉換為BGR彩圖,才能進行可視化顯示~具體的兩個操作為:
- Canny(src, temp, 10, 140, 3);//提取邊緣(如果不邊緣提取就會浪費巨大時間)
- cvtColor(temp, dst, CV_GRAY2BGR);//將邊緣提取的灰度圖轉換為BGR圖便於畫線
2、去除其他圓的影響:
由於用霍夫找圓會找到比較多的圓,如何在這些圓中找出和表盤最相近的一個呢?這里采用了比較巧妙的一個方法:
1 //儲存檢測圓的容器 2 std::vector<Vec3f> circles; 3 //調用Hough變換檢測圓 4 //參數為:待檢測圖像,檢測結果,檢測方法(這個參數唯一),累加器的分辨率,兩個圓間的距離,canny門限的上限(下限自動設為上限的一半),圓心所需要的最小的投票數,最大和最小半徑 5 HoughCircles(temp,circles,CV_HOUGH_GRADIENT,2,50,200,100,100,300); 6 //找出圓盤(因為最大的不一定是的,所以加了幾個限制條件) 7 int pos=0; 8 int max=-1; 9 for(size_t i = 0; i < circles.size(); i++ ) 10 { 11 Vec3f f=circles[i]; 12 if(f[2]>max && f[0]+f[2]<temp.rows && f[0]-f[2]>=0 && f[1]+f[2]<temp.cols && f[1]-f[2]>0) 13 { 14 max=f[2]; 15 pos=i; 16 } 17 } 18 Point center(circles[pos][0],circles[pos][1]);//找到的圓心 19 int radius= circles[pos][2];//找到的半徑 20 circle(dst,center,radius,Scalar(255),2);
如上面所示:遍歷所有霍夫找的圓,記錄其中半徑最大的且滿足整個圓在圖像內的那個,作為目標圓,這樣就巧妙地找出了我們需要的那個圓~
3、霍夫找到的直線轉換為夾角表示的線段:
因為接下來要根據夾角進行分組,所以這里要把霍夫找到的直線進行轉換,這里我自己定義一個MyLine的類,用於保存一條線段,該線段形式為夾角和長度,其中轉換關系為:
1 list<MyLine> list_MyLine; 2 vector<Vec4i> lines2;//線段檢測 3 HoughLinesP(temp, lines2, 1, CV_PI/180, 50, 50, 10 ); 4 for( size_t i = 0; i < lines2.size(); i++ ) 5 { 6 Vec4i l = lines2[i]; 7 Point A(l[0], l[1]),B(l[2], l[3]); 8 if(DistancetoSegment(center,A,B)<30)//根據圓心到指針的距離閾值濾掉其他線段 9 { 10 bool down=(A.y+B.y-2*center.y>0);//判斷長的在過圓心的水平線上部還是下部 11 if(A.x==B.x){//斜率為無窮的情況 12 list_MyLine.push_back(MyLine(i,90+(down?180:0),Length(Point(A.x-B.x,A.y-B.y)))); 13 }else if(A.y==B.y){//水平的情況 14 list_MyLine.push_back(MyLine(i,A.x+B.x-2*center.x>0 ? 0:180,Length(Point(A.x-B.x,A.y-B.y)))); 15 }else{ 16 if(down){ 17 if(A.y>center.y) 18 list_MyLine.push_back(MyLine(i,360-atan2(A.y-B.y,A.x-B.x)*180/PI,Length(Point(A.x-B.x,A.y-B.y)))); 19 else 20 list_MyLine.push_back(MyLine(i,360-atan2(B.y-A.y,B.x-A.x)*180/PI,Length(Point(A.x-B.x,A.y-B.y)))); 21 }else{ 22 if(A.y<center.y) 23 list_MyLine.push_back(MyLine(i,abs(atan2(A.y-B.y,A.x-B.x)*180/PI),Length(Point(A.x-B.x,A.y-B.y)))); 24 else 25 list_MyLine.push_back(MyLine(i,abs(atan2(B.y-A.y,B.x-A.x)*180/PI),Length(Point(A.x-B.x,A.y-B.y)))); 26 } 27 } 28 line(dst,A,B, Scalar(0,0,i*20+40), 2, CV_AA); 29 } 30 }
六、實驗中的缺陷和不足
雖然用我這種識別方法可以快速有效的識別很多時鍾,但是也存在一些特殊情況無法處理,比如:1、圓心不准導致計算出錯;2、影子出現導致出現詭異指針;3、另類指針導致霍夫求得的直線不能准確描述指針;4、非圓形的表盤根本Hold不住;5、當存在其他干擾時表盤找不准;6、指針后半部分影響導致誤判為另一個指針;7、指針長度檢測錯誤導致時分秒針分配錯誤…等情況,雖然采用一些限制條件可以去除其中一二個錯誤,但是當算法向這方面偏的時候,又會導致另一些識別好的情況出現錯誤。最后總結一句:用圖像識別不能求普遍性解決問題,我們應該根據具體的問題,在限定條件下進行研究,否則將永遠滿足不了需求!
鏈接:
本文鏈接:http://www.cnblogs.com/zjutlitao/p/4187476.html
文檔下載:http://pan.baidu.com/s/1i3koenr
工程下載:http://pan.baidu.com/s/1jGst6lC
無法識別:http://pan.baidu.com/s/1o6t7rnG
更多精彩:http://www.cnblogs.com/zjutlitao/p/4125085.html
GitHub鏈接: https://github.com/beautifulzzzz/OpenCV-Clock-Identification