二次元図形のデジタル画像への変換処理を効率的に実行しよう. 今回は線分を対象とする.
現代のコンピュータスクリーン(LCD や CRT)を利用する場合, 三次元の図形を CG 画像として表示するためには, まず,図形の構成要素(ポリゴンなど)を二次元に投影し, 更に,その二次元図形をディジタル画像(画素の集合) へ変換する必要がある. この画像化処理が 走査変換(scan-conversion,rasterize)である.
実用的な CG では,膨大な量の図形要素を取り扱うことになるので, 走査変換の高速化が非常に重要な技術となっている. そこで今回は,走査変換について, 次の2通りの方法でプログラミングしてみよう.
図形の方程式を直接的に解析して画像化する. 一般に,実数や乗除算(コンピュータが苦手とする演算) や冗長な計算を含むので非効率である.
図形の方程式を差分法的に解析する. 整数の加減算(コンピュータが得意とする演算) のみで計算できるように工夫すれば,さらに効率的となる.
こうした効率的な解析法は, DDA(digital difference analyzer;ディジタル微分解析器)と総称される. 特に有力な手法としてブレゼンハムのアルゴリズム(Bresenham's algorithm)があり, 多様な図形の描画のために応用されている.
次のような仮定のもとで, Fig.1 のような線分(有限長の直線)を描くことにする:
また, \( x \) 方向の増分を \( d_x = x_2 - x_1 \) , \( y \) 方向の増分を \( d_y = y_2 - y_1 \) と定義しておく.
直接解法および DDA による線分描画プログラムの実装例を示しておく:
もっとも単純な線分描画アルゴリズムは次の通り:
プログラムの実装の際には,簡単化・効率化のために, 始点を \( ( x_1 = 0 ,\ y_1 = 0 ) \) , 終点を \( ( x_2 = d_x ,\ y_2 = d_y ) \) とみなす. そして, 方程式 \( y = \DS\F{d_y}{d_x} x \) の計算について, \( x \) を 0 から \( d_x \) まで変化させて繰り返す. 最後に座標を元に戻し, 画素 \( ( x + x_1,\ y + y_1 ) \) を点灯する. 今回のサンプルプログラムでも,この方法を利用している. (以下の理論展開では,この方法は使われていない. 適切に読み替えるんですよー.)
また,計算式自体は実数演算だが, 画面上の座標 \( (x,\ y) \) は整数値であることに注意.
このアルゴリズムでは, 1画素についての計算はさほど複雑ではないが, 同じような結果をもたらす乗算・除算を何度も繰り返すことになるので, 線分全体として見ると,効率が悪い.
以下,ブレゼンハムのアルゴリズムについて解説する. DDA については,サンプルソースコードを参照せよ.
直線の方程式を変形すると:
この左辺を2倍し,誤差関数 \( f(x,\ y) \) を定義しておく:
なぜ2倍するのか? 後述のアルゴリズムの実行中,関数値を常に整数に保つためだ.
座標値は基本的には整数なのだが, 四捨五入のために 1/2 刻みの実数と考える場面もある. しかし,その場面でも,係数2によって, 関数値は1刻みの整数のままにできる.
この関数の値は:
となる. したがって,誤差関数の値がゼロとなるようなすべての画素を 探し出して点灯すれば直線が描かれることになる.
しかし,ディジタル画像(各画素の座標値は整数のみ)なので, 誤差が丁度ゼロになるような画素はほとんど存在しない. つまり,誤差ゼロの画素だけを点灯するだけでは, 直線のつもりが点線になってしまうだろう. そこで実際には,誤差関数の値がゼロに近い画素の集合を探索することになる.
なお,現在画素 \( (x,\ y) \) に対して, 隣接画素の候補は \( (x+1,\ y) \) と \( (x+1,\ y+1) \) の2箇所あるが, Bresenham のアルゴリズムでは, それらの中間地点 \( (x+1,\ y+1/2) \) の1箇所の誤差関数を検査するだけで, 正しい隣接画素を合理的に決定する.
Fig.2 は,このアルゴリズムの説明図である. 判定点(×印)と直線の位置関係に応じて, 画素 P,Q,R が順次点灯されて行く.
さらに,この処理をインクリメンタルに (実数や乗除算を使わず,整数の加減算のみで高速に)実行する:
したがって, 次に点灯する画素は \( (x+1, y) \)となる. その座標における \( f \) を求めておく.
したがって, 次に点灯する画素は \( (x+1,\ y+1) \) となる. その座標における \( f \) を求め, さらに \( y \) を1だけ増やしておく.
Fig.3 は実数版 DDA(digital differential analyzer)法の説明図である. 判定点を用いることなく, 誤差 \( e \) の蓄積を抑えながら描画経路を定めている. Fig.2 と比較せよ.
サンプルソースファイル line.c の中に, ブレゼンハムのアルゴリズムによる高速線分描画関数 FastDrawLineBA( ) を定義せよ.
また,処理時間を測定し,他の手法の結果と比較せよ. (処理時間は一定ではない.3回程度ずつ測定せよ.)
性能比較では,単純な差(時間が「○秒早い」とか)ではなく 比率(時間が「○%削減」とか速度が「○倍」とか)を挙げるべきでしょう.
例えば,同じ1秒短縮にしても...
科学的に!!
(レポートに説明があれば加点対象.)
例えば,足し算の後で足し直したり引き直したりしている部分が無駄.
描画結果のスクリーンショット画像を作るには, import コマンドを使えばよい.
# 撮影対象のウィンドウを前面に出しておき... $ import 画像ファイル.png # 対象をクリック # 画像の確認には... $ display 画像ファイル.png &
line.c の Raspberry Pi3 での処理時間の例:
直接解法: 0.242998 sec DDA1: 0.168930 sec DDA2: 0.156392 sec DDA3: 0.151798 sec DDA4: 0.148544 sec BA: ...
アルゴリズムの工夫により,少しずつだが,高速化していることがわかる. RasPi のような低性能マシンだと差異が明確. 実験室の PC のような高性能マシンだと差異が不明確だったり,逆転する場合もある.