需求
由使用者輸入一系列對稱的點,對稱軸未知。
需求分析
對稱的點包括對稱點和對稱軸上的點。可以先要求使用者(圖形化)輸入成對的對稱點,程式計算出對稱軸後,再由使用者輸入軸上的點,並將點修正到軸上。
實際上計算對稱軸的過程是對對稱點中垂線求平均。但是要注意的是,輸入點的誤差是一個恆定值,以真實點為中心,越遠概率越小。因此兩對稱點的連線的誤差跟連線長度負相關。
而對使用者輸入的點的修正,其實是計算點到直線的垂足座標,是一個簡單的的解析幾何問題。
解決方案
第一步,通過使用者輸入的對稱點解出對稱軸
演算法思路
計算出對稱軸的基本思路分為兩步,先通過輸入的對稱點的連線計算對稱軸的斜率;然後計算在這個斜率下使得對稱點中點距該線距離最小時的截距。
對稱軸斜率的計算
斜率的計算需要先計算對稱點的方向向量,再計算與該方向向量垂直的向量,即為對稱軸的方向向量。求解如下:
為了計算方便,確保方向向量為正值,令x = 1
正常情況下,這時候斜率就應該是-x'/y'
。然而本例中有多個對稱點,需要用連線長度做權,對傾角求加權平均。注意,是傾角而非斜率做加權平均,所以我們要轉換為傾角:
值得注意的是,當y_1' = y_1
即 y' = 0
時,會出現0做除數的情況。
因為 arctanx
的定義域為(-∞, +∞)
, 值域為(-π/2, π/2)
:
而傾角的有效範圍是[0,π)
,所以需要一次轉換。
因為∠β
與∠γ
角度上相等,所以∠β
要轉換成線的傾角,即∠γ
的補角,因∠β
為負數,只需要對計算出的傾角為負的值加上π
即可。
(之所以要將傾角歸到[0,π)
,是因為要求平均角度。如果直接求∠α
和∠β
平均角度的話,會得到0°
。而我們想得到的是90°
,所以必須要用∠γ
的補角取平均。)
計算對稱軸的斜率k
:
對稱軸截距的計算
因為對於恆定的斜率,點到直線的距離和點在y方向上到直線的距離是成正比的,即a = k·b
因此要計算恆定斜率下,與若干點距離(a)最短的直線。和求若干點在y方向距離(b)最短的直線是等價的。
只需要假定截距為0,計算各個點的y值與直線上相應y值之差,再求平均值即為截距。
程式碼
很多計算是由numpy的矩陣計算完成的,因此程式碼比較簡單
程式碼中self.rawData['symmMark']
是使用者輸入的對稱點的資料,資料結構為一個一維陣列, x 座標 y 座標依次排列。
如A1(x1, y1) A'1(x'1, y'1)
是第一對對稱點,A2(x2, y2) A'2(x'2, y'2)
是第二對對稱點。其儲存在self.rawData['symmMark']
中的形式為: [x1, y1, x'1, y'1, x2, y2, x'2, y'2]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import numpy as np # 省略其它程式碼... def calcAxis(self): smlen = len(self.rawData['symmMark']) if smlen < 4: return sms = self.rawData['symmMark'] # 中點座標 mxs = np.array([(sms[i] + sms[i + 2])/2.0 for i in xrange(0, smlen, 4)], dtype = 'float') mys = np.array([(sms[i] + sms[i + 2])/2.0 for i in xrange(1, smlen, 4)], dtype = 'float') # 對稱點連線向量 axs = np.array([sms[i + 2] - sms[i] for i in xrange(0, smlen, 4)], dtype = 'float') ays = np.array([sms[i + 2] - sms[i] for i in xrange(1, smlen, 4)], dtype = 'float') # 對稱點連線長度 ds = np.sqrt(np.square(axs) + np.square(ays)) # 計算與連線垂直向量的角度 angs = np.choose(np.not_equal(ays, 0), (np.PINF, - axs / ays)) # 注意:若 y0 = 0 則使結果為正無窮,這樣在numpy.arctan中可以得到一個-2/π angs = np.arctan(angs) # 同時y值和餘弦值相等 計算角度 angs = np.choose(angs < 0, (angs, angs + np.pi)) # 保證角度結果在 0 ~ π 範圍內 # 用連線長度加權計算平均傾角 mainang = np.sum(angs * ds) / np.sum (ds) # 計算斜率 也就是直線方程中的a a = np.tan(mainang) # 假定截距為0 計算在Y軸方向上 中點到直線的距離 dys = mys - mxs * a # 用連線長度加權計算平均截距 b = np.sum(dys * ds) / np.sum (ds) self.axis = [a, b] pass |
第二步,將接下來使用者輸入的點投射到對稱軸上
設輸入點為C(x1, y1)
, 對稱軸為 y = a x + b
設垂直於對稱軸,通過點C的直線斜率為k 則有 k · a = -1
於是直線方程為: y - y1 = - 1/a (x - x1)
化為斜截式: y = -1/a x + x1/a + y1
與對稱軸方程聯立解得交點x座標為
帶入對稱軸方程可得y座標
程式碼
1 2 3 4 5 6 7 8 9 10 11 |
def addAxisMark(self, x, y): if self.axis[0] == 0: resx = x else: resx = (x / self.axis[0] + y - self.axis[1]) / (self.axis[0] + 1 / self.axis[0]) resy = resx * self.axis[0] + self.axis[1] self.rawData['axisMark'].append(resx) self.rawData['axisMark'].append(resy) return (resx, resy) |
結果展示
標記數字相同的圓圈為滑鼠點選輸入一對對稱點,綠色的線為計算出的對稱軸,橙色的點為滑鼠點選後修正到軸上之後的點。