画像の輪郭を取得します。
これはサーキットの形状を示す画像ですが、ここから輪郭を構成する点の集合を取得します。サーキットのコースなのでコースアウトした場合、それがわかるように任意の点がコースの内側なのか外側なのかを知る方法も考えます。またゲームをつくる場合、敵の車両も存在するわけですが、そうなると進行方向を取得する方法も考えなければなりません。コース内の任意の点に存在する敵の進行方向を求める方法も考えます。
画像はここからとってきました。
左右の余白が大きいので整形したものがこれです。
まず画像ですが、白と青だけかと思ったらそれ以外の色も使われているようです。
以下のメソッドに画像のパスを渡すと「56」が返ってきました。コースの輪郭部分は他の色も混ざっているようです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
int GetUsedColorsCount(string path) { Bitmap bitmap = new Bitmap(path); List<Color> colors = new List<Color>(); for (int x = 0; x < bitmap.Width; x++) { for (int y = 0; y < bitmap.Height; y++) { colors.Add(bitmap.GetPixel(x, y)); } } bitmap.Dispose(); return colors.Distinct().Count(); } |
それでも背景の青い部分は単色なのでそれ以外の部分だけ集めるとコースの形状を取得することはできそうです。以下のコードでcourse2.pngを取得しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
void GetImageFileInsideCource(string path) { Bitmap bitmap = new Bitmap(path); bitmap.MakeTransparent(); Bitmap newBitmap = new Bitmap(bitmap.Width, bitmap.Height); for (int x = 0; x < bitmap.Width; x++) { for (int y = 0; y < bitmap.Height; y++) { Color color = bitmap.GetPixel(x, y); if(color != Color.FromArgb(0)) newBitmap.SetPixel(x, y, Color.Black); } } bitmap.Dispose(); string path1 = Application.StartupPath + "\\course2.png"; newBitmap.Save(path1); newBitmap.Dispose(); } |
これが上記の方法で生成されたファイルです。
次に輪郭部分を取得します。
先ほど取得した画像はコース部分は黒でそれ以外の部分は無色です。そこで黒であるすべてのピクセルを調べて上下左右のどれかが黒ではないならそれが輪郭を構成するピクセルであることがわかります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
Bitmap GetBitmapContour(Bitmap bitmap, Color color) { Bitmap newBitmap = new Bitmap(bitmap.Width, bitmap.Height); for (int x = 0; x < bitmap.Width; x++) { for (int y = 0; y < bitmap.Height; y++) { Color pixelColor = bitmap.GetPixel(x, y); if (pixelColor.ToArgb() == color.ToArgb()) { bool b1 = bitmap.GetPixel(x + 1, y).ToArgb() != color.ToArgb(); bool b2 = bitmap.GetPixel(x - 1, y).ToArgb() != color.ToArgb(); bool b3 = bitmap.GetPixel(x, y + 1).ToArgb() != color.ToArgb(); bool b4 = bitmap.GetPixel(x, y - 1).ToArgb() != color.ToArgb(); if(b1 || b2 || b3 || b4) newBitmap.SetPixel(x, y, color); } } } return newBitmap; } |
コースには外側の境界線と内側の境界線があります。それぞれをわけて、しかも連続している順番に取り出すことはできないのでしょうか?
取得した点は上下左右斜めのどれかで繋がっているので、適当な点をスタート地点にして繋がっているものを順番に取得していくというのはどうでしょうか? これを2回やれば外側と内側の境界線を構成するピクセル座標をすべて取得できそうです。
これは第二引数で指定した色をもつもので最初に見つかったピクセルの座標を返すメソッドです。
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 |
Point GetPointFromColor(Bitmap bitmap, Color color) { for (int x = 0; x < bitmap.Width; x++) { for (int y = 0; y < bitmap.Height; y++) { Color color1 = bitmap.GetPixel(x, y); if (color1.ToArgb() == color.ToArgb()) { return new Point(x, y); } } } return Point.Empty; } List<List<Point>> GetPointListContour(Bitmap bitmap, Color color) { // 渡されたBitmapには輪郭部分しか残されていないのが前提 List<List<Point>> pointsList = new List<List<Point>>(); while (true) { List<Point> points = new List<Point>(); Point point = GetPointFromColor(bitmap, color); if (point == Point.Empty) break; Point tempPoint = point; points.Add(tempPoint); while (true) { Point nextPoint = Point.Empty; bitmap.SetPixel(tempPoint.X, tempPoint.Y, Color.Red); if (bitmap.GetPixel(tempPoint.X + 1, tempPoint.Y).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X + 1, tempPoint.Y); else if (bitmap.GetPixel(tempPoint.X - 1, tempPoint.Y).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X - 1, tempPoint.Y); else if (bitmap.GetPixel(tempPoint.X, tempPoint.Y - 1).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X, tempPoint.Y - 1); else if (bitmap.GetPixel(tempPoint.X, tempPoint.Y + 1).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X, tempPoint.Y + 1); else if (bitmap.GetPixel(tempPoint.X + 1, tempPoint.Y + 1).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X + 1, tempPoint.Y + 1); else if (bitmap.GetPixel(tempPoint.X - 1, tempPoint.Y - 1).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X - 1, tempPoint.Y - 1); else if (bitmap.GetPixel(tempPoint.X + 1, tempPoint.Y - 1).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X + 1, tempPoint.Y - 1); else if (bitmap.GetPixel(tempPoint.X - 1, tempPoint.Y + 1).ToArgb() == color.ToArgb()) nextPoint = new Point(tempPoint.X - 1, tempPoint.Y + 1); if (nextPoint == Point.Empty) break; tempPoint = nextPoint; points.Add(tempPoint); } pointsList.Add(points); } return pointsList; } |
これらをまとめると単色(この場合はColor.Black)で塗りつぶされた部分の輪郭を構成するピクセル座標を取得するのであれば以下のようになります。
1 2 3 4 5 6 7 8 |
// 輪郭線を構成する点のリストのリストを取得する List<List<Point>> GetContourPointsListFromBitmap(Bitmap bitmap, Color color) { Bitmap bitmap2 = GetBitmapContour(bitmap, color); List<List<Point>> pointsList = GetPointListContour(bitmap2, color); bitmap2.Dispose(); return pointsList; } |
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 button1_Click(object sender, EventArgs e) { // course2.pngは上記の処理でコース内はColor.Blackで塗りつぶされている string path = Application.StartupPath + "\\course2.png"; Bitmap bitmap = new Bitmap(path); Bitmap newBitmap = new Bitmap(bitmap.Width, bitmap.Height); List<List<Point>> pointsList = GetContourPointsListFromBitmap(bitmap, Color.Black); bitmap.Dispose(); for (int i = 0; i < pointsList.Count; i++) { Color color = Color.Empty; if (i == 0) color = Color.Red; if (i == 1) color = Color.Blue; if (i == 2) color = Color.Green; if (i > 2) color = Color.Black; // この場合、pointsList.Count == 2なので意味はないが念のため foreach (Point point in pointsList[i]) newBitmap.SetPixel(point.X, point.Y, color); } string path2 = Application.StartupPath + "\\course3.png"; newBitmap.Save(path2); newBitmap.Dispose(); } } |
これでcourse3.pngが得られます。
任意の点が任意の色で塗りつぶされた部分の内側かどうかは以下のコードで確認できます。
1 2 3 4 5 6 7 8 |
bool IsPointInside(Bitmap bitmap, Color color, Point point) { int pointToArgb = bitmap.GetPixel(point.X, point.Y).ToArgb(); if (pointToArgb == color.ToArgb()) return true; else return false; } |
しかしBitmap.GetPixelメソッドは時間がかかります。1回だけなら体感時間は一瞬かもしれませんが、短い時間で何度も確認するような処理には適さないと思われます。そこでコースの内部、境界線になるPointをプログラム開始時に取得しておき、以降は取得したリストをつかって判定するようにしたほうがいいかもしれません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 内部の点のリストを取得する List<Point> GetPointsInside(Bitmap bitmap, Color color) { List<Point> points = new List<Point>(); for (int x = 0; x < bitmap.Width; x++) { for (int y = 0; y < bitmap.Height; y++) { Color pixelColor = bitmap.GetPixel(x, y); if (pixelColor.ToArgb() == color.ToArgb()) { points.Add(new Point(x, y)); } } } return points; } |
上記の方法で取得した点情報を利用してコースの輪郭部分のみを描画するとともに、クリックされた点がコースの内部かどうかについて判定できるようにします。
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 |
public partial class Form1 : Form { List<Point> insidePoints; List<List<Point>> PointListContour; public Form1() { InitializeComponent(); // course2.pngのコース内に相当する部分はColor.Blackで塗りつぶされている string path = Application.StartupPath + "\\course2.png"; using (Bitmap bitmap = new Bitmap(path)) { insidePoints = GetPointsInside(bitmap, Color.Black); PointListContour = GetContourPointsListFromBitmap(bitmap, Color.Black); } } // 輪郭部分だけが描画される protected override void OnPaint(PaintEventArgs e) { foreach (var points in PointListContour) { e.Graphics.DrawLines(Pens.Black, points.ToArray()); } base.OnPaint(e); } // クリックされた部分はコースの内部か? protected override void OnMouseDown(MouseEventArgs e) { if (insidePoints.Any(x => x == new Point(e.X, e.Y))) MessageBox.Show("内部です"); else MessageBox.Show("外部です"); base.OnMouseDown(e); } } |