領域内高速塗りつぶし法 / High-speed Region Fill Method
テ~マ
図1:白猫が黒猫に変わるようす
図1のような閉じた領域の内部を高速に塗りつぶすアルゴリズムと、プログラム例を示す
Why?
かつて作っていた『お絵描き掲示板』に、「塗りつぶし」コマンド(=お絵描きソフトで、ペンキ缶のアイコンのやつ) をつけようかなと思い、そのやり方を考えてみました。そしたら、とてもいいロジックが出来たので紹介することにしました。このロジックは、私が今まで調べたもののうち、どんなものよりも高速で、 かつ単純なものになっています。
(ただし、これは2000年ごろに作ったものですし、もっと速いやり方があるかもしれません!)。
ではさっそくそのロジック(アルゴリズム)を説明します。
原理
一般的な塗りつぶしのアルゴリズムは以下のようなもの。
これは、走査線一本ごとに境界を探して行く方法なので、 「スキャンラインアルゴリズム」とか「走査線アルゴリズム」とか呼ばれている。
処理ステップ
前提として、図2のような、閉曲線(境界線)で囲まれた領域があるとする
- まず塗りつぶしを始める一点 ( X, Y ) を決める
- その座標 X, Y をスタックに積む
- スタックから座標 X , Y を取り出す
- ( X, Y )から左に1ピクセルずつ調べ、初めて境界のピクセルになる1個前のx座標 Xleft を取る
- 同様に( X, Y )から右に1ピクセルずつ調べ、右の境界の1ピクセル前のx座標 Xright を取る
- ( Xleft, Y-1 )から( Xright, Y-1 )まで1ピクセルずつ調べる。この区間には境界線が含まれる 可能性がある。境界線が含まれていた場合、領域の内部から境界のピクセルに入る直前のピクセルの座標 ( Xin, Y-1 ) をスタックに積む。この区間には複数の Xin が含まれている可能性があるが、それらの座標を全てスタックに積む。
- 6と同様にして、( Xleft, Y+1 )から( Xright, Y+1 )まで1ピクセルずつ調べ、見つかった全ての( Xin, y+1 )をスタックに積んでいく
- スタックが空になるまで、3に戻って処理を続ける
しかし、このアルゴリズムにはとても無駄が多い(図3参照)。
無駄1
3->4->5の手順で、すでに「境界は含まれていない」と分かっているはずの区間を調べてしまう
無駄2
6、7の処理も、すでに調べた区間を調べなおしてしまう
たとえばx、y軸に平行な辺をもつ単純な長方形の内部を塗りつぶす場合、 無駄1を省くだけで計算量は半分になり、無駄2を省けばさらに半分になる。 つまりこの2つを省けば1/4の計算量ですむことになる。
複雑な図形を塗りつぶす場合は、これらの無駄を省くことでさらに大幅な計算量の削減ができることになる。
以下に、その無駄を省く、かっちょいいアルゴリズムを示す
無駄を省くアルゴリズム
無駄1、2を省くには、8つのパターンを導入すればよい。
これによって「一度行った計算は二度行わない」ことが徹底され、 また、同時に無駄1、2を省くことができるので、 それぞれの無駄を別個に省くよりもはるかに効率がよくなり、 同時にアルゴリズムも読みやすくなる。 また、座標と同時に6種のパターンのうちひとつをスタックに積むだけなので メモリもほとんど食わないという利点がある。
これよりいいアルゴリズムがあったらば教えて欲しいってぐらいグッドなアイデアだ。 (自画自賛)
その8つのパターンとは、以下のものである(図4)。それぞれに識別子としてアルファベットのA~Hを割り振ってある(実際には、識別に3ビットだけ割り振ればよい)。
図で、黒の線は境界線(壁)を表す。
上下方向がy軸方向、左右方向がx軸方向で、下向きを正、右向きを正とする。
青、赤、灰色の四角は、1ピクセルを示している。これらは領域内のピクセルである。
A,B,C,Dでは、ピクセルがある位置のy座標(y-1)と、 その一つ下の(y)の座標との関係が示されている。
E,F,G,Hでは、ピクセルがある位置のy座標(y+1)と、 そのひとつ上の(y)座標との関係が示されている。
四角形の色は、無駄1を省くことに関係している。
青い四角は、左に1ピクセルずつ調べて左の境界を調べる必要があることを示し、 赤い四角は同様に、右に1ピクセルずつ調べて右の境界を調べる必要があることを示している。
また、灰色の四角は左(右)の壁にひっついていて、これ以上左(右)にピクセルを調べる必要がないことを示す。
ここで、どのように無駄1が省かれるかを説明しよう。
無駄1を省くテクニック
処理ステップ6を行う段階で、( Xleft, Y-1 )から( Xright, Y-1 )までを調べるが、 そのときに、1ピクセルずつ境界かどうかを調べるので、この情報を図5や図6の ようにして、境界のまわりの座標 ( XL, Y-1 ) , ( XR, Y-1 ) とタイプを保存しておく。
そうすると次に来る4,5でもう一度無駄な計算をせずに済むことになる。
4の処理において、タイプBまたはCならば XL から左に調べる必要がないことになる。
5の処理において、タイプAならXRまで調べなくていいし、タイプCまたはDなら右に調べる必要がないということになる。
無駄1を省くだけなら、パターンは4種類で足りることになる。
パターンが8種類あるのは無駄2を省くことと関係している。
次に無駄2がどのように省かれるかを説明しよう。
無駄2を省くテクニック
図7は無駄2を省くための模式図である。
図7の緑の線は、処理ステップ6,7でそれぞれ調べなくてはいけない領域をしめしている。逆に言うと、緑の線が無い領域は調べなくてよいというのが分かっている領域ということになる。つまり、パターンを調べることで、無駄2を省くことができることになる。
これだけでは分かりにくいので、図8を参照してほしい。
処理ステップ6が終了した時点で、まずA~Dのパターンのどれに当てはまるかが分かる。 図では、パターンAである (ステップ7を行う時点ではE~Fのパターンのうちどれかが当てはまる)。
さらに処理が進み、また処理ステップ4,5を通過してステップ7に来たとき、パターンAという情報が、( XL, XR )という座標と共に残っているので、これによって先ほど調べたはずの領域を省くことができるようになる。
以上で、アルゴリズムについての説明は終わりです。
この考え方を用いたサンプルプログラムを用意しました。
サンプルプログラム
Java アプレット版
*coming soon