算法系列之十:直線生成算法

        在歐氏幾何空間中,平面方程就是一個三元一次方程,直線就是兩個非平行平面的交線,所以直線方程就是兩個三元一次方程組聯立。但是在平面解析幾何中,直線的方程就簡單的多了。平面幾何中直線方程有多種形式,一般式直線方程可用於描述所有直線:

 

Ax+By+C = 0  (A、B不同時爲0)

 

當知道直線上一點座標(X0,Y0)和直線的斜率K存在時,可以用點斜式方程:

 

Y-Y0 = K(X – X0) (當K不存在時,直線方程簡化成X = X0

 

當知道直線上的兩個點(X0,Y0)和(X1,Y1)時,還可以用兩點式方程描述直線:

 

 

除了這三種形式的直線方程外,直線方程還有截距式、斜截式等多種形式。

 

        在數學範疇內的直線是由沒有寬度的點組成的集合,但是在計算機圖形學的範疇內,所有的圖形包括直線都是輸出或顯示在點陣設備上的,被成爲點陣圖形或光柵圖形。以顯示器爲例,現實中常見的顯示器(包括CRT顯示器和液晶顯示器)都可以看成由各種顏色和灰度值的像素點組成的象素矩陣,這些點是有大小的,而且位置固定,因此只能近似的顯示各種圖形。圖(1)就是對這種情況的一種誇張的放大:

 圖(1)直線在點陣設備上的表現形式

 

        計算機圖形學中的直線生成算法,其實包含了兩層意思,一層是在解析幾何空間中根據座標構造出平面直線,另一層就是在光柵顯示器之類的點陣設備上輸出一個最逼近於圖形的象素直線,而這就是常說的光柵圖形掃描轉換。本文就是介紹幾種常見的直線生成的光柵掃描轉換算法,包括數值微分法(DDA法)、Bresenham算法、對稱直線生成算法以及兩步算法。

 

數值微分法(DDA法)

        數值微分畫線算法(DDA)法是直線生成算法中最簡單的一種,它是一種單步直線生成算法。它的算法是這樣的:首先根據直線的斜率確定是以X方向步進還是以Y方向步進,然後沿着步進方向每步進一個點(象素),就沿另一個座標變量k,k是直線的斜率,因爲是對點陣設備輸出的,所以需要對每次計算出來的一對座標進行圓整。

        具體算法的實現,除了判斷是按照X方向還是按照Y方向步進之外,還要考慮直線的方向,也就是起點和終點的關係。下面就是一個支持任意直線方向的數值微分畫線算法實例:

12 void DDA_Line(int x1, int y1, int x2, int y2)

13 {

14     double k,dx,dy,x,y,xend,yend;

15 

16     dx = x2 - x1;

17     dy = y2 - y1;

18     if(fabs(dx) >= fabs(dy))

19     {

20         k = dy / dx;

21         if(dx > 0)

22         {

23             x = x1;

24             y = y1;

25             xend = x2;

26         }

27         else

28         {

29             x = x2;

30             y = y2;

31             xend = x1;

32         }

33         while(x <= xend)

34         {    

35             SetDevicePixel((int)x, ROUND_INT(y));

36             y = y + k;

37             x = x + 1;

38         }

39 

40     }

41     else

42     {

43         k = dx / dy;

44         if(dy > 0)

45         {

46             x = x1;

47             y = y1;

48             yend = y2;

49         }

50         else

51         {

52             x = x2;

53             y = y2;

54             yend = y1;

55         }

56         while(y <= yend)

57         {    

58             SetDevicePixel(ROUND_INT(x), (int)y);

59             x = x + k;

60             y = y + 1;

61         }

62     }

63 }

數值微分法(DDA法)產生的直線比較精確,而且邏輯簡單,易於用硬件實現,但是步進量x,y和k必須用浮點數表示,每一步都要對x或y進行四捨五入後取整,不利於光柵化或點陣輸出。

 

Bresenham算法

        Bresenham算法由Bresenham在1965年提出的一種單步直線生成算法,是計算機圖形學領域使用最廣泛的直線掃描轉換算法。Bresenham算法的基本原理就是將光柵設備的各行各列象素中心連接起來構造一組虛擬網格線。按直線從起點到終點的順序計算直線與各垂直方向網格線的交點,然後確定該列象素中與此交點最近的象素。

 

圖(2)直線Bresenham算法示意圖

 

        圖(2)就展示了這樣一組網格線,每個交點就代表點陣設備上的一個象素點,現在就以圖(2)爲例介紹一下Bresenham算法。當算法從一個點(Xi,Yi)沿着X方向向前步進到Xi+1時,Y方向的下一個位置只可能是Yi和Yi+1兩種情況,到底是Yi還是Yi+1取決於它們與精確值y的距離d1和d2哪個更小。

d1 = y - Yi                                  (等式 1)

d2 = Yi+1 - y                                (等式 2)

當d1-d2 > 0時,Y方向的下一個位置將是Yi+1否則就是Yi。由此可見,Bresenham算法其實和數值微分算法原理是一樣的,差別在於Bresenham算法中確定Y方向下一個點的位置的判斷條件的計算方式不一樣。現在就來分析一下這個判斷條件的計算方法,已知直線的斜率k和在y軸的截距b,可推導出Xi+1位置的精確值y如下:

y = k Xi+1 + b                                 (等式 3)

 

將等式 1-3帶入d1-d2,可得到等式4:

d1-d2 = 2k Xi+1 - Yi - Yi+1 + 2b                    (等式 4)

 

有因爲根據圖(2)條件,k = dy / dx,Yi+1 = Yi + 1,Xi+1 =  Xi + 1,將此三個關係帶入等式4,同時在等式兩邊乘以dx,整理後可得到等式5:

dx(d1 – d2) = 2dyXi + 2dy - 2dxYi + dx(2b - 1)       (等式 5)

 

另pi = dx(d1 – d2),則:

pi = 2dyXi + 2dy - 2dxYi + dx(2b - 1)

 

因爲圖(2)的示例dx是大於0的值,因此pi的符號與(d1 – d2)一致,現在將初始條件帶入可得到最初的第一個判斷條件p1

p1 = 2dy – dx

 

根據Xi+1與Xi,以及Yi+1與Yi的關係,可以推出pi的遞推關係:

pi+1 = pi + 2dy - 2dx(yi+1 - yi)

由於yi+1可能是yi,也可能是yi + 1,因此,pi+1就可能是以下兩種可能,並且和yi的取值是對應的:

pi+1 = pi + 2dy    (Y方向保持原值)

pi+1 = pi + 2(dy – dx)  (Y方向向前步進1)

 

根據上面的推導,當x2 > x1,y2 > y1時Bresenham直線生成算法的計算過程如下:

1、畫點(x1,   y1);  計算誤差初值p1=2dy-dx;    
2、求直線的下一點位置:    
   Xi+1 = Xi+1;    
   如果 pi > 0   則Yi+1 = Yi + 1;   
   否則Yi+1 = Yi;    
   畫點(Xi+1, Yi+1 );      
3、求下一個誤差pi+1;     
  如果  pi>0   則pi+1 = pi+2(dy – dx);    
  否則pi+1 = pi+2dy;    
4、如果沒有結束,則轉到步驟2;否則結束算法。

 

下面就給出針對上面推導出的算法源代碼(只支持 x2 > x1,y2 > y1的情況):

319 void Bresenham_Line(int x1, int y1, int x2, int y2)

320 {

321     int dx = abs(x2 - x1);  

322     int dy = abs(y2 - y1);  

323     int p = 2 * dy - dx;  

324     int x = x1;  

325     int y = y1;

326 

327     while(x <= x2)

328     {  

329         SetDevicePixel(x, y);  

330         x++;  

331         if(p<0)  

332             p += 2 * dy;  

333         else

334         {  

335             p += 2 * (dy - dx);  

336             y += 1;  

337         }  

338     }  

339 }

上面的代碼只是演示計算過程,真正實用的代碼要支持各種方向的直線生成,這就要考慮斜率爲負值的情況以及x1 > x2的情況,另外,循環中的兩次乘法運算可以在循環外計算出來,不必每次都計算。要支持各種方向的直線生成其實也很簡單,就是通過座標交換,使之符合上面演示算法的要求即可,下面就是一個實用的,支持各種方向的直線生成的Bresenham算法:

 

164 void Bresenham_Line(int x1, int y1, int x2, int y2)

165 {

166     int dx,dy,p,const1,const2,x,y,inc;

167 

168     int steep = (abs(y2 - y1) > abs(x2 - x1)) ? 1 : 0;

169     if(steep == 1)

170     {

171         SwapInt(&x1, &y1);

172         SwapInt(&x2, &y2);

173     }

174     if(x1 > x2)

175     {

176         SwapInt(&x1, &x2);

177         SwapInt(&y1, &y2);

178     }

179     dx = abs(x2 - x1);  

180     dy = abs(y2 - y1);  

181     p = 2 * dy - dx;  

182     const1 = 2 * dy; 

183     const2 = 2 * (dy - dx);

184     x = x1;  

185     y = y1;

186 

187     inc = (y1 < y2) ? 1 : -1;

188     while(x <= x2)

189     {  

190         if(steep == 1)

191             SetDevicePixel(y, x);  

192         else

193             SetDevicePixel(x, y);  

194         x++;  

195         if(p<0)  

196             p += const1;  

197         else

198         {  

199             p += const2;  

200             y += inc;  

201         }  

202     }  

203 }

        Bresenham算法只實用整數計算,少量的乘法運算都可以通過移位來避免,因此計算量少,效率高,

 

對稱直線生成算法(改進的Bresenham算法)

        直線段有個特性,那就是直線段相對於中心點是兩邊對稱的。因此可以利用這個對稱性,對其它單步直線生成算法進行改進,使得每進行一次判斷或相關計算可以生成相對於直線中點的兩個對稱點。如此以來,直線就由兩端向中間生成。從理論上講,這個改進可以應用於任何一種單步直線生成算法,本例就只是對Bresenham算法進行改進。

        改進主要集中在以下幾點,首先是循環區間,由[x1, x2]修改成[x1, half],half是區間[x1, x2]的中點。其次是X軸的步進方向改成雙向,最後是Y方向的值要對稱修改,除此之外,算法整體結構不變,下面就是改進後的代碼:

205 void Sym_Bresenham_Line(int x1, int y1, int x2, int y2)

206 {

207     int dx,dy,p,const1,const2,xs,ys,xe,ye,half,inc;

208 

209     int steep = (abs(y2 - y1) > abs(x2 - x1)) ? 1 : 0;

210     if(steep == 1)

211     {

212         SwapInt(&x1, &y1);

213         SwapInt(&x2, &y2);

214     }

215     if(x1 > x2)

216     {

217         SwapInt(&x1, &x2);

218         SwapInt(&y1, &y2);

219     }

220     dx = x2 - x1;  

221     dy = abs(y2 - y1);  

222     p = 2 * dy - dx;  

223     const1 = 2 * dy; 

224     const2 = 2 * (dy - dx);

225     xs = x1;  

226     ys = y1;

227     xe = x2;

228     ye = y2;

229     half = (dx + 1) / 2;

230     inc = (y1 < y2) ? 1 : -1;

231     while(xs <= half)

232     {  

233         if(steep == 1)

234         {

235             SetDevicePixel(ys, xs);

236             SetDevicePixel(ye, xe);

237         }

238         else

239         {

240             SetDevicePixel(xs, ys);

241             SetDevicePixel(xe, ye);

242         }

243         xs++; 

244         xe--;

245         if(p<0)  

246             p += const1;  

247         else

248         {  

249             p += const2;  

250             ys += inc;  

251             ye -= inc;  

252         }  

253     }  

254 }

兩步算法

        兩步算法是在生成直線的過程中,每次判斷都生成兩個點的直線生成算法。上一節介紹的對稱直線生成方法也是每次生成兩個點,但是它和兩步算法的區別就是對稱方法的計算和判斷是從線段的兩端向中點進行,而兩步算法是沿着一個方向,一次生成兩個點。

        當斜率k滿足條件0≤k<1時,假如當前點P已經確定,如圖(3)所示,則P之後的連續兩個點只可能是四種情況:AB,AC,DC和DE,兩步算法設立決策量e作爲判斷標誌,e的初始值是4dy – dx,其中:

dy = y2 – y1

dx = x2 – x1。

 

圖(3)直線兩步算法示意圖

 

爲簡單起見,先考慮dy > dx > 0這種情況。當e > 2dx時,P後兩個點將會是DE組合,此時e的增量是4dy – 4dx。當dx < e < 2dx時,P後的兩個點將會是DC組合,此時e的增量是4dy – 2dx.。當0 < e < dx時,P後的兩個點將會是AC組合,此時e的增量是4dy – 2dx.。當e < 0時,P後的兩個點將會是AB組合,此時e的增量是4dy。綜合以上描述,當斜率k滿足條件0≤k<1,且dy > dx > 0這種情況下,兩步算法可以這樣實現:

 

257 void Double_Step_Line(int x1, int y1, int x2, int y2)

258 {

259     int dx = x2 - x1;

260     int dy = y2 - y1;

261     int e = dy * 4 - dx;

262     int x = x1;

263     int y = y1;

264 

265     SetDevicePixel(x, y);

266 

267     while(x < x2)

268     {  

269         if (e > dx)

270         {

271             if (e > ( 2 * dx))

272             {

273                 e += 4 * (dy - dx);

274                 x++;

275                 y++;

276                 SetDevicePixel(x, y);

277                 x++;

278                 y++;

279                 SetDevicePixel(x, y);

280             }

281             else

282             {

283                 e += (4 *dy - 2 * dx);

284                 x++;

285                 y++;

286                 SetDevicePixel(x, y);

287                 x++;

288                 SetDevicePixel(x, y);

289             }

290         }

291         else

292         {

293             if (e > 0)

294             {

295                 e += (4 * dy - 2 * dx);

296                 x++;

297                 SetDevicePixel(x, y);

298                 x++;

299                 y++;

300                 SetDevicePixel(x, y);

301             }

302             else

303             {

304                 x++;

305                 SetDevicePixel(x, y);

306                 x++;

307                 SetDevicePixel(x, y);

308                 e += 4 * dy;    

309             }

310         }

311     }

312 }

以上函數除了只支持一個方向的直線生成之外,還有其它不完善的地方,比如沒有判斷最後一個點是否會越界,大量出現的乘法計算可以用移位處理等等。仿照Bresenham算法一節介紹的方法,很容易將其擴展爲支持8個方向的直線生成,因爲代碼比較長,這裏就不列出代碼了。

 

總結

        除了以上介紹的幾種直線生成算法,還有很多其它的直線光柵掃描轉換算法,比如三步算法、四步算法、中點劃線法等等,還有人將三步算法結合前面介紹的對稱法提出了一種可以一次畫六個點的直線生成算法,這裏就不多介紹了,有興趣的讀者可以找計算機圖形學的相關資料來了解具體的內容。

        本文介紹的幾種直線生成算法中,DDA算法最簡單,但是因爲有多次浮點數乘法和除法運算,以及浮點數圓整運算,效率比較低。Bresenham算法中的整數乘法計算都可以用移位代替,主要運算都採用了整數加法和減法運算,因此效率比較高,各種各樣變形的Bresenham算法在計算機圖形軟件中得到了廣泛的應用。理論上講,兩步算法以及四步算法效率應該更高一些,但是這兩種算法需要做比較多的準備工作,且多是乘法和除法運算,因此在生成比較短的直線時,效率反而不如Bresenham算法。

 

 

 

 

 

參考資料:

 

【1】計算幾何:算法設計與分析 周培德  清華大學出版社 2005年

【2】計算幾何:算法與應用 德貝爾赫(鄧俊輝譯)  清華大學出版社 2005年

【3】計算機圖形學 孫家廣、楊常貴 清華大學出版社 1995年

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章