智能筆算法總結
一周前,我在CocoaChina和博客園的問答區都提了一個問題,就是本篇文章將要描述的“智能筆問題”。遺憾的是,至今沒有朋友給予有效地回復,但是,還是感謝回復我的朋友們。
經過一周的琢磨和研究,終於在昨天搞定了這個問題。
看着上圖,回想自己渴求幫助的心情,想必有的朋友還是需要這樣的算法的,再者在此也做一個總結,所以記錄一下,互相學習指正。
首先描述下這個功能的需求:在平板上,用手指自由地一筆畫出一個圖形,然后智能識別用戶所畫圖形的類型,比如直線、圓、三角形或者矩形,並自動變換為最接近的幾何圖形。
從上面的描述來看,就是自動識別圖形的算法。並且已經獲得了所勾勒自由圖形的點軌跡。在此需要說明一下,我做的項目中,已經有獲取點的方法,但並不是將所有像素點記錄,而是一系列的軌跡離散點。
所以,開始下面的算法前提就是,已經有一個記錄軌跡的點數組了。
我不想寫成學術報告一樣,先羅列一堆概念什么的,所以就直接按照算法的步驟了,涉及到概念再描述一下。
總體步驟:
1、 求頂點
2、 判斷直線
3、 判斷圓
4、 判斷矩形
5、 判斷其他多邊形
6、 未知自由圖形
整體很直觀吧,那開始逐步介紹。
一、求頂點
還記得向量么,還記得怎么求兩向量的夾角么,還記得正弦余弦么。好吧,不記得的少年,需要溫故一下了。
頂點即是圖形中的“轉折點”,是兩條邊的交點,所以存在一定夾角,那么就可以用這兩條邊的方向向量求出夾角theta的值。方向向量可以根據一條直線上的兩點求得,夾角theta根據公式可以得到。
可以參考偽代碼:
1 點數組為points; 2 3 用於存儲頂點的可變數組 vertexes; 4 5 前兩個點數組 aheadTwoPoints; 6 7 aheadTwoPoints[0] = points[0]; 8 9 aheadTwoPoints[1] = points[1]; 10 11 上一個參考向量 aheadDirectionVector = 12 13 Point2d(aheadTwoPoints[1].x – aheadTwoPoints[0].x , aheadTwoPoints[1].y – aheadTwoPoints[0].y); 14 15 16 17 For(int i=2; i<points.Length; i++){ 18 19 當前向量 currentDirectionVector = Point(points[i].x – aheadTwoPoints[1].x, points[i].y - aheadTwoPoints[1].y); 20 21 臨時向量 tempDirectionVector = currentDirectionVector; 22 23 24 25 Double cosTheta = (currentDirectionVector.x * aheadDirectionVector.x + currentDirectionVector.y * aheadDirectionVector.y) / (sqrt(pow(currentDirectionVector.x, 2) + pow(currentDirectionVector.y , 2)) * sqrt(pow(aheadDirectionVector.x , 2) + pow(aheadDirectionVector.y , 2))); 26 27 28 29 if(fabs(cosTheta) <= 閥值1){ 30 31 將aheadTwoPoints[1]加入vertexes; 32 33 }else{ 34 35 根據points[i]和aheadTwoPoints[0]得到cosTheta2; 36 37 If(fabs(cosTheta2 <= 閥值2)){ 38 39 將aheadTwoPoints[1]加入vertexes; 40 41 } 42 43 } 44 45 46 47 aheadTwoPoints[0] = aheadTwoPoints[1]; 48 49 aheadTwoPoints[1] = points[i]; 50 51 aheadDirectionVector = tempDirectionVector; 52 53 }
說明:閥值1和閥值2有待進一步探究,目前我采用的閥值1為cos(M_PI / 3),閥值2為cos(M_PI / 6).歡迎大家提出優化建議。
二、判斷直線
當vertexes.Length為0的時候,初步判斷為直線或者圓。
偽代碼:
1 If(points[0].distanceTo(points[length - 1]) > points[0].distanceTo(points[length / 2 - 1])){ 2 3 中點midPoint = Poin(t(points[0].x + points[length - 1].x) / 2 , (points[0].y + points[length - 1].y) / 2); 4 5 If(midpoint.distanceTo(points[length / 2 - 1]) < 閥值3){ 6 7 實例化一條直線 newLine; 8 9 newLine.StartPoint = points[0]; 10 11 newLine.EndPoint = points[length - 1]; 12 13 畫出直線; 14 15 }else{ 16 17 實例化一條曲線 newSpline; 18 19 newSpline的數據點為points; 20 21 畫出曲線; 22 23 } 24 25 }
三、判斷圓
當二中直線判斷條件不滿足時,初步判斷為圓。
接下來主要根據圓心特點來判斷。
偽代碼:
先根據points取到1/4、1/2、3/4這三個點,再加上起點,相對的兩點求中點,然后近似得到圓心點 centerPoint;
If(points[0].distanceTo(centerPoint) < points[0].distanceTo(points[length - 1])){
判斷為曲線,並畫出曲線;
break;
}
初始化圓 circle;
circle.center = centerPoint;
circle.radius = points[0].distanceTo(centerPoint);
說明:這里只描述了主要的思想,更多細節就根據具體情況而定了。還有一點,在初始化圓之前,還可以再加一個判斷:
先求得points[0]和points[length/4 - 1]的直線為y1,points[length/8 - 1]和圓心的直線為y2,得到y1和y2的交點P,然后求得P和圓心的距離dis1、points[length/8 - 1]和圓心的距離dis2,判斷dis1和dis2的大小關系。
在圓里面,dis1/dis2 = 1 – sqrt(2) / 2。
所以如果points[length/8 - 1]和points[length*3/8 - 1]同時滿足,那判斷就更准了。但是,由於上述代碼中基本確定為圓了,所以為了提高容錯,我就沒有再加說明中的判斷了。感興趣的朋友可以一試。
四、判斷矩形
當vertexes.Length為3的時候,直接判斷為矩形。
最大的問題就是,重新計算矩形的四個頂點位置。
1、我采用以起點和vertexes[1]為參照點、以過起點和vertexes[0]的直線為參照直線。
2、從vertexes[1]向參照直線做垂直直線,求得交點為新的vertexes[0]
3、然后求出vertexes[0]到vertexes[1]的向量vector
4、根據points[0]加上向量vector,得到新的vertexes[2]。
實例化一個多段線 polyline;
polyline.setPointAt(0, points[0]);
For(int i=0; i<vertexes.length ; i++){
polyline.setPointAt(i + 1, vertexes[i]);
}
polyline.setPointAt(vertexes.length + 1, points[0]);
說明:當然,在開始重新計算四個頂點位置前,還可以加一個判斷,對角線的距離差值是否在一定閥值范圍內,或許更好一點。
但是,我非常希望畫出矩形,所以就強制實現四個頂點的圖形為矩形了。
這里的算法,還有值得推敲的,因為實際測試中,矩形是最難畫出的=_=
五、判斷其他多邊形
多邊行就太輕松了。。。。
只要排除上述幾個前提條件,就可以判斷圖形為(vertexes.length + 1)邊行。
然后畫出多段線就可以了。
六、未知自由圖形
這個。。。。畢竟我的算法有限,還是有很多情況不能識別,主要難點在頂點的識別。所以不滿足判斷條件的圖形就只能將其“原封不動”地畫出來了。
心得:
一開始覺得無從下手,發了一些帖子,但是都沒有得到回答,心里比較煩。但是當自己全心投入,去研究去思考以后,還是會有回報,會有意外的收獲。
這讓我聯想起一路學習編程的情況。起初對編程很恐懼,覺得密密麻麻的代碼,看得心煩,但是當靜下心來去讀代碼,去思考代碼中的邏輯時,就會不斷發現驚喜,贊嘆牛人的思維邏輯,贊嘆各種算法,贊嘆各種設計模式,贊嘆強大的API。就是這樣的一種心情,引領着我這個菜鳥一步一步走進編程的世界,也開始了自己的成長之路。
不知不覺,走出學校參加工作已經有五個月了。這段時間,心理變化也是很多很復雜的。過去的不必糾結,大家都知道着眼未來,可我還是很樂意為今天現在的自己贊一個的。自己認可自我的提升和改變,這也是一件意義重大的事呢。
這個算法就描述到這里了,希望能夠給需要的朋友一個提示,也希望得到指正和討論。