アルゴリズムシリーズ12:多角形領域塗りつぶしアルゴリズム-スキャンラインシード塗りつぶしアルゴリズム

1.3スキャンラインシードフィルアルゴリズム

        セクション1.1および1.2で導入された2つのシードフィリングアルゴリズムの利点は非常に単純ですが、欠点は再帰アルゴリズムを使用することです。これは、隣接するポイントを格納するために多くのスタックスペースを必要とするだけでなく、効率もよくありません。アルゴリズムの再帰呼び出しを減らし、スタックスペースの使用を節約するために、多くの改良されたアルゴリズムが提案されています。その1つがスキャンラインシードフィルアルゴリズムです。スキャンラインシード塗りつぶしアルゴリズムは、「4-Unicom」と「8-Unicom」の隣接ポイントを処理するために再帰的な方法を使用しなくなりましたが、水平スキャンラインに沿ってピクセルセグメントを1つずつ塗りつぶして、「4-Unicom」と「8-Unicom」の隣接ポイント。このように、アルゴリズム処理のプロセスでは、再帰アルゴリズムのようにスタックに処理されていない現在の位置周辺のすべての隣接ポイントをプッシュするのではなく、各水平ピクセルセグメントの開始点を特別なスタックにプッシュするだけでよいので、スタックスペースを節約します。走査線塗りつぶしアルゴリズムは、再帰を回避して効率を向上させるための単なるアイデアであることに注意してください。上記の注入塗りつぶしアルゴリズムと境界塗りつぶしアルゴリズムは、走査線塗りつぶしアルゴリズムに改善できます。以下に、境界塗りつぶしアルゴリズムと組み合わせた走査線について説明します。種子充填アルゴリズム。

        スキャンラインシード塗りつぶしアルゴリズムの基本的なプロセスは次のとおりです。シードポイント(x、y)が指定されると、最初にシードポイントが左右の所定の領域に配置されているスキャンラインのセクションを同時に塗りつぶします。このセクションの範囲[xLeft、xRight]を書き留めてから、指定された領域でこのセクションに接続されている上部および下部の走査線上のセクションを特定し、順番に保存します。充填が完了するまでこのプロセスを繰り返します。

スキャンラインシード塗りつぶしアルゴリズムは、次の4つのステップで実装できます。

 

(1)シードポイントを格納するための空のスタックを初期化し、シードポイント(x、y)をスタックにプッシュします。

 

(2)スタックが空かどうかを判断します。スタックが空の場合、アルゴリズムは終了します。それ以外の場合、スタックの一番上の要素が現在のスキャンラインのシードポイント(x、y)として使用されます。yは現在のスキャンラインです。

 

(3)シードポイント(x、y)から開始して、現在のスキャンラインに沿って境界まで左方向と右方向を入力します。セクションの左端と右端の座標をxLeftおよびxRightとしてマークします。

 

(4)現在の走査線に隣接する2つの走査線y-1とy + 1の間隔[xLeft、xRight]内のピクセルを確認し、xLeftからxRight方向に検索します。境界のない、塗りつぶされていないピクセルがある場合ピクセル。これらの隣接するピクセルの右端の1つを見つけ、それをシードポイントとしてスタックにプッシュしてから、手順(2)に戻ります。

 

このアルゴリズムの最も重要な部分はステップ(4)で、現在のスキャンラインの前のスキャンラインと次のスキャンラインから新しいシードポイントを見つけることです。ここで理解するのがさらに難しいのは、新しいスキャンラインの間隔[xLeft、xRight]のピクセルのみを確認する理由です。新しいスキャンラインの実際の範囲がこの間隔よりも大きい(連続していない)場合はどうなりますか?私はコンピュータグラフィックスに関する多くの本や紙をチェックしましたが、それらのどれもこれについて特別な指示をしていないようで、このコースを学ぶとき、多くの人々はこれについて長引く疑問を抱いています。「人を滅ぼす」という疲れを知らない考えに沿って、この記事は誰の疑問も和らげることを望んで、簡潔な説明を提供します。

        新しいスキャンライン上の実際のポイントの間隔が現在のスキャンラインの[xLeft、xRight]間隔より大きく、連続している場合、アルゴリズムのステップ(3)がこの状況を処理します。図4に示すように:

図(4)新しいスキャンライン間隔は拡大され、連続しています

現在処理されている走査線が黄色のドットが配置されている7番目の線であると仮定すると、3番目のステップの処理後に間隔[6,10]を取得できます。次に、4番目のステップでは、隣接する6番目と8番目のスキャンラインの6番目の列から右に検索して、2つの赤い点が6番目と8番目の行のシードポイントであることを確認します。 2つのシードポイント(6、10)と(8、10)を順番にスタックに入れます。次のループはシードポイント(8、10)を処理します。アルゴリズムの3番目のステップに従って、(8、10)から左右に塗りつぶします。中央に境界ポイントがないため、境界が検出されるまで塗りつぶしが続行されますこれまでのところ、8行目の実際の領域は7行目の間隔[6,10]よりも大きいにもかかわらず、依然として正しい塗りつぶしを取得しています。

        新しいスキャンライン上の実際のポイントの間隔が現在のスキャンラインの[xLeft、xRight]間隔よりも大きく、中央に境界ポイントがある場合、アルゴリズムはそれをどのように処理しますか?アルゴリズムの説明では、この状況に対処する明確な方法はありませんが、4番目のステップで上下の隣接するスキャンラインのシードポイントを決定する方法と、ポイントを右に取るという原則は、実際には隣接するスキャンラインからの巻き上げを意味します。障害物ポイントを通過する方法。例として図(5)を取り上げます。

図(5)新しいスキャンライン間隔が拡大され、不連続になっている

アルゴリズムの3番目のステップで5行目を処理した後、間隔[7、9]が決定されます。隣接する4行目の実際の範囲は間隔[7、9]よりも大きいですが、境界点(4、6)によってブロックされています、シードポイント(4、9)を決定した後、左側に塗りつぶすと、右側の7列目と10列目の間の領域のみが塗りつぶされ、左側の3列目と5列目の間の領域は塗りつぶされないようにします。5行目の隣接行ですが、4行目の最初のスキャンでは、正しい原理に従ってシードポイント(4、9)のみが決定されます。しかし、3行目を処理した後、4行目の左部分は3行目の下の隣接行と見なされ、再度スキャンする機会を得ます。3行目の間隔は[3、9]で、6列目のバリアポイントを左に横切っています。4行目を2回目にスキャンするときは、3列目から始めて右に見てシードポイント(4 、5)。このようにして、4番目の行には2つのシードポイントがあり、完全に塗りつぶすことができます。

        障害点のある行の場合、隣接するエッジ間の関係により、障害点を横切ることができ、複数のスキャンを通じて完全な塗りつぶしが得られることがアルゴリズムによって暗黙的に処理されました。このセクションで要約した4つのステップによると、スキャンラインシード塗りつぶしアルゴリズムの実装は次のとおりです。

263  空隙 ScanLineSeedFill int型のx int型の Y INT new_color INT boundary_color

264  {

265      std ::スタック<ポイント> stk ;

266 

267     stk.push(Point(x, y)); //第1步,种子点入站

268     while(!stk.empty())

269     {

270         Point seed = stk.top(); //第2步,取当前种子点

271         stk.pop();

272 

273         //第3步,向左右填充

274         int count = FillLineRight(seed.x, seed.y, new_color, boundary_color);//向'cf?右'd3?填'cc?充'b3?

275         int xRight = seed.x + count - 1;

276         count = FillLineLeft(seed.x - 1, seed.y, new_color, boundary_color);//向'cf?左'd7?填'cc?充'b3?

277         int xLeft = seed.x - count;

278 

279         //第4步,处理相邻两条扫描线

280         SearchLineNewSeed(stk, xLeft, xRight, seed.y - 1, new_color, boundary_color);

281         SearchLineNewSeed(stk, xLeft, xRight, seed.y + 1, new_color, boundary_color);

282     }

283 }

FillLineRight()和FillLineLeft()两个函数就是从种子点分别向右和向左填充颜色,直到遇到边界点,同时返回填充的点的个数。这两个函数返回填充点的个数是为了正确调整当前种子点所在的扫描线的区间[xLeft, xRight]。SearchLineNewSeed()函数完成算法第4步所描述的操作,就是在新扫描线上寻找种子点,并将种子点入栈,新扫描线的区间是xLeft和xRight参数确定的:

234 void SearchLineNewSeed(std::stack<Point>& stk, int xLeft, int xRight,

235                        int y, int new_color, int boundary_color)

236 {

237     int xt = xLeft;

238     bool findNewSeed = false;

239 

240     while(xt <= xRight)

241     {

242         findNewSeed = false;

243         while(IsPixelValid(xt, y, new_color, boundary_color) && (xt < xRight))

244         {

245             findNewSeed = true;

246             xt++;

247         }

248         if(findNewSeed)

249         {

250             if(IsPixelValid(xt, y, new_color, boundary_color) && (xt == xRight))

251                 stk.push(Point(xt, y));

252             else

253                 stk.push(Point(xt - 1, y));

254         }

255 

256         /*向右跳过内部的无效点(处理区间右端有障碍点的情况)*/

257         int xspan = SkipInvalidInLine(xt, y, xRight, new_color, boundary_color);

258         xt += (xspan == 0) ? 1 : xspan;

259         /*处理特殊情况,以退出while(x<=xright)循环*/

260     }

261 }

 

最外层的while循环是为了保证区间[xLeft, xRight]右端被障碍点分隔成多段的情况能够得到正确处理,通过外层while循环,可以确保为每一段都找到一个种子点(对于障碍点在区间左端的情况,请参考图(5)所示实例的解释,是隐含在算法中完成的)。内层的while循环只是为了找到每一段最右端的一个可填充点作为种子点。SkipInvalidInLine()函数的作用就是跳过区间内的障碍点,确定下一个分隔段的开始位置。循环内的最后一行代码有点奇怪,其实只是用了一个小“诡计”,确保在遇到真正的边界点时循环能够正确退出。这不是一个值得称道的做法,实现此类软件控制有更好的方法,本文这样做的目的只是为了使代码简短一些,让读者把注意力集中在算法处理逻辑上,而不是冗杂难懂的循环控制条件上。

        算法的实现其实就在ScanLineSeedFill()和SearchLineNewSeed()两个函数中,神秘的扫描线种子填充算法也并不复杂,对吧?至此,种子填充算法的几种常见算法都已经介绍完毕,接下来将介绍两种适合矢量图形区域填充的填充算法,分别是扫描线算法和边标志填充算法,注意适合矢量图形的扫描线填充算法有时又被称为“有序边表法”,和扫描线种子填充算法是有区别的。

 

<下一篇:扫描线算法(有序边表法)>

 

 

おすすめ

転載: blog.csdn.net/orbit/article/details/7343236