画像のなかの同じ色でつながっている部分をぬりつぶす。簡単にできるかと思ったら意外と難儀していまいました。
どんなプログラム?
マウスでクリックされたらその部分と同じ色でつながっている部分を塗りつぶします。
コンストラクタのなかで画像ファイルのパスを指定し読み込みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class Form1 : Form { Bitmap Bitmap; public Form1() { InitializeComponent(); string filePath = // @"画像ファイルのパスを指定する"; Bitmap bitmap = (Bitmap)Image.FromFile(filePath); // ファイルがロックされないようにImage.FromFileで取得したものはDisposeする Bitmap = new Bitmap(bitmap); bitmap.Dispose(); } } |
そのイメージをピクチャーボックスに表示させます。
1 2 3 4 5 6 7 |
public partial class Form1 : Form { private void Form1_Load(object sender, EventArgs e) { pictureBox1.Image = Bitmap; } } |
マウスがクリックされたらその場所の色を調べて、これを赤で塗りつぶします。やりたいことはこれだけです。
問題はFillColorメソッドをどう書くかです。
1 2 3 4 5 6 7 8 |
public partial class Form1 : Form { private void pictureBox1_MouseDown(object sender, MouseEventArgs e) { Color oldColor = Bitmap.GetPixel(e.X, e.Y); FillColor(e.X, e.Y, oldColor, Color.Red); } } |
再帰呼び出しで楽勝?
クリックされた座標をBitmap.GetPixelメソッドで調べてBitmap.SetPixelメソッドで色を変える。隣の色を調べて同じ色であればそこも色を変える。再帰呼び出しでいいのではないか?
はじめは安易に考えて以下のようなコードをかきました。
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 |
public partial class Form1 : Form { private void pictureBox1_MouseDown(object sender, MouseEventArgs e) { Color oldColor = Bitmap.GetPixel(e.X, e.Y); DameFillColor(e.X, e.Y, oldColor, Color.Red); pictureBox1.Image = Bitmap; } void DameFillColor(int x, int y, Color oldColor, Color newColor) { if(oldColor.ToArgb() == newColor.ToArgb()) return; if(oldColor == Bitmap.GetPixel(x, y)) { Bitmap.SetPixel(x, y, newColor); if(0 <= x - 1 && x - 1 < Bitmap.Width && 0 <= y && y < Bitmap.Height) DameFillColor(x - 1, y, oldColor, newColor); if(0 <= x + 1 && x + 1 < Bitmap.Width && 0 <= y && y < Bitmap.Height) DameFillColor(x + 1, y, oldColor, newColor); if(0 <= x && x < Bitmap.Width && 0 <= y - 1 && y - 1 < Bitmap.Height) DameFillColor(x, y - 1, oldColor, newColor); if(0 <= x && x < Bitmap.Width && 0 <= y + 1 && y + 1 < Bitmap.Height) DameFillColor(x, y + 1, oldColor, newColor); } } } |
oldColorとnewColorが異なることをチェックしていますが、これはBitmap.GetPixelメソッドとoldColorが同じであれば処理をおこなうようにしているからです。oldColorとnewColorが同じであれば色を入れ替えてからも条件式が真を返すため、永久に処理が終わらないということになります。
サイズが大きい場合は再帰呼び出しは不適?
DameFillColorメソッドは画像サイズが小さければ問題なくできます。縦横100ピクセル程度であればなんとかできますが、それ以上大きくなるとスタックオーバーフローの例外が発生してしまいます。再帰の場合、呼び出し回数が極端に多いとうまく動いてくれません。
ではどうすればいいのか? 調べてみた結果、まず横1列だけ入れ替える色があるかどうか調べるという方法です。そしてその上下のピクセルを調べます。これをバッファに保存してそのピクセルがある横1列を調べて・・・を繰り替えす方法です。
この方法であれば画像サイズが大きく塗りつぶしの対象が入り組んだ形をしても対応できます。
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
public partial class Form1 : Form { private void pictureBox1_MouseDown(object sender, MouseEventArgs e) { Color oldColor = Bitmap.GetPixel(e.X, e.Y); FillColor(e.X, e.Y, oldColor, Color.Red); pictureBox1.Image = Bitmap; } void FillColor(int x, int y, Color oldColor, Color newColor) { Bitmap newBitmap = new Bitmap(Bitmap); if(oldColor.ToArgb() == newColor.ToArgb()) { return; } List<Point> points = FillLine(newBitmap, x, y, oldColor, newColor); int i = 0; while(true) { if(points.Count == 0) break; List<Point> points1 = new List<Point>(); foreach(Point pt in points) { points1.AddRange(FillLine(newBitmap, pt.X, pt.Y, oldColor, newColor)); } points = points1.ToList(); points1.Clear(); } Bitmap.Dispose(); Bitmap = newBitmap; } List<Point> FillLine(Bitmap bitmap0, int x, int y, Color oldColor, Color newColor) { // 指定された座標から塗りつぶす点を1列だけ調べる // 塗りつぶす点で一番左側にあるものを調べる int tempX = x; int leftX = 0; while(true) { tempX--; if(tempX < 0) break; Color color = bitmap0.GetPixel(tempX, y); if(color != oldColor) break; leftX = tempX; } // 一番左側がどこかわかったら右にむかって塗りつぶす tempX = leftX; List<Point> nexrPoints = new List<Point>(); while(true) { // 画像の一番右まできたら終了 if(tempX >= bitmap0.Width) break; Color color = bitmap0.GetPixel(tempX, y); // 塗りつぶしの対象とは違う色を取得したら終了 if(color != oldColor) break; // 塗りつぶしの対象を塗りつぶす。 // そのさいその上下が塗りつぶしの対象かどうか調べて取得する bitmap0.SetPixel(tempX, y, newColor); if(y - 1 >= 0) nexrPoints.Add(new Point(tempX, y - 1)); if(y + 1 < bitmap0.Height) nexrPoints.Add(new Point(tempX, y + 1)); tempX++; } return nexrPoints; } } |