ドット絵の輪郭抽出のコーディング

34 x 34 のサイズのドット絵の輪郭を抽出するプログラムをPythonで書きます。

コードはこちら。

```python
X_SIZE = 34
Y_SIZE = 34

def main():
	global X_SIZE
	global Y_SIZE

	array_data = [[0 for i in range(X_SIZE)] for j in range(Y_SIZE)]

	x = 0
	y = 0

	for ch in iter_chars('input.txt'):

		if ch == '\n':
			y += 1
			x = 0
		elif ch == ' ':
			pass
		else:
			array_data[y][x] = int(ch)
			x += 1

	print_image(array_data)
	print("")

	x = 0
	y = 0

	while array_data[y][x] == 0:
		if x < X_SIZE-1:
			x += 1
		else:
			x = 0
			y += 1

	output_array_data = [[0 for i in range(X_SIZE)] for j in range(Y_SIZE)]

	xx = x
	yy = y

	vec_x = -1
	vec_y = 0

	while True:

		while True:
			vec_x,vec_y = kaiten_return(vec_x,vec_y)
			if array_data[yy+vec_y][xx+vec_x] == 1:
				break

		xx += vec_x
		yy += vec_y

		output_array_data[yy][xx] = 1

		vec_x *= -1
		vec_y *= -1

		if x == xx and y == yy:
			break

	print_image(output_array_data)

def kaiten_return(x,y):
	if x == -1 and y == 0:
		return -1,-1
	if x == -1 and y == -1:
		return 0,-1
	if x == 0 and y == -1:
		return 1,-1
	if x == 1 and y == -1:
		return 1,0
	if x == 1 and y == 0:
		return 1,1
	if x == 1 and y == 1:
		return 0,1
	if x == 0 and y == 1:
		return -1,1
	if x == -1 and y == 1:
		return -1,0

def print_image(output_array):

	global X_SIZE
	global Y_SIZE

	for y in range(Y_SIZE):
		for x in range(X_SIZE):
			if output_array[y][x] == 1:
				print("■",end="")
			elif output_array[y][x] == 0:
				print("□",end="")
		print("")

def iter_chars(filename):
    """ Reads a text file char by char. """
    with open(filename, encoding='utf-8') as f:
        content = f.read()
    for ch in content:
        yield ch

if __name__ == "__main__":
    main()
```

ドット絵はこちらポケモンのピチューです。

![picyu_maemae.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2615735/212214d2-e5bc-2fbc-65bf-33e834cf1047.png)

入力テキストファイルはこちらを使います。

```:input.txt
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 
0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 
0 0 0 0 0 1 1 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 1 1 1 1 0 0 0 0 
0 0 0 0 1 1 1 0 0 1 1 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 1 1 0 0 0 0 0 
0 0 0 0 1 1 0 0 0 1 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 1 1 1 0 0 0 0 0 
0 0 0 0 1 1 0 0 0 0 1 1 1 1 1 1 1 1 0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 
0 0 0 0 1 1 0 0 0 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 1 1 1 0 0 0 0 0 0 
0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 
0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 
0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 1 0 0 1 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 
0 0 0 0 1 0 1 1 0 1 0 0 0 0 0 0 1 0 1 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 
0 0 0 0 1 0 1 1 1 1 0 0 0 0 0 0 1 1 1 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 
0 0 0 0 1 0 0 1 1 0 0 0 1 0 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 1 0 1 0 0 0 1 1 1 1 0 0 0 0 1 0 0 1 1 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 1 1 0 0 0 0 1 0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 1 1 1 1 0 0 1 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 1 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 1 1 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 
0 0 0 0 0 1 0 0 1 0 0 1 0 0 1 1 0 1 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 1 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 
0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 
0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 1 1 1 0 0 0 
0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 1 1 0 0 0 0 
0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 1 1 0 1 0 0 0 0 0 1 0 0 0 0 0 
0 0 0 0 0 0 0 0 1 0 0 0 1 1 1 1 1 1 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 

```

得られる輪郭抽出画像はこちらです。

![picyu_ato.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2615735/18d8c8bd-8334-8a25-cd3f-df716f246515.png)

以下、コードの説明をしていきます。

```python
X_SIZE = 34
Y_SIZE = 34
```

ドット絵のx座標とy座標のドットサイズを指定します。
今回は両方34の正方形です。

```python
def main():
	global X_SIZE
	global Y_SIZE

	array_data = [[0 for i in range(X_SIZE)] for j in range(Y_SIZE)]

	x = 0
	y = 0

	for ch in iter_chars('input.txt'):

		if ch == '\n':
			y += 1
			x = 0
		elif ch == ' ':
			pass
		else:
			array_data[y][x] = int(ch)
			x += 1

	print_image(array_data)
	print("")

	x = 0
	y = 0

	while array_data[y][x] == 0:
		if x < X_SIZE-1:
			x += 1
		else:
			x = 0
			y += 1

	output_array_data = [[0 for i in range(X_SIZE)] for j in range(Y_SIZE)]

	xx = x
	yy = y

	vec_x = -1
	vec_y = 0

	while True:

		while True:
			vec_x,vec_y = kaiten_return(vec_x,vec_y)
			if array_data[yy+vec_y][xx+vec_x] == 1:
				break

		xx += vec_x
		yy += vec_y

		output_array_data[yy][xx] = 1

		vec_x *= -1
		vec_y *= -1

		if x == xx and y == yy:
			break

	print_image(output_array_data)
```

メイン関数の説明です。
まずグローバル変数X_SIZEとY_SIZEを呼べるようにしておきます。

次にarray_data変数の初期化。
これは二次元配列を定義してます。
1次元にY_SIZE、2次元にX_SIZEの領域の配列を定義します。

変数xとyを0で初期化します。

input.txtという名前のテキストファイルから1文字ずつch変数に代入していきます。
chが半角スペースだったら、何もしないpass文を実行。
通常はarray_data二次元配列の左上からch変数をintでキャストしたものを代入していきます。
その場合、chは1か0です。
代入したらxを+1して次の位置に移ります。
chが改行文字だったらy座標を+1して、xを0に戻します。
これを最後まで読み込めば、二次元配列にデータが1か0の形でドット絵のデータを読み込めました。

print_image関数は引数にした二次元配列をドット絵で画面に表示する関数です。
後述します。
この関数で画面にドット絵を表示出来ます。
その後、print("")で一つ改行。

そしてxとyをまた0で初期化します。

その後、while文でarray_data[y][x] == 0じゃ無くなるまで、つまりarray_data[y][x] == 1になるまで、ドット絵データの一番左上から一番右下までドットを走査します。
テキストファイルを二次元配列に読み込む時と同じやり方で走査します。
黒ドットに当たるまで走査し続けます。
イメージ画像としては、こんな感じです。

![スライド1.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2615735/54e2ec9a-4d18-0e95-e27e-c9f590cc131e.jpeg)

output_array_data変数に入力用と同じ容量の二次元配列を定義します。
この配列は出力用です。
入力用の二次元配列の定義の時は、説明しなかったのですが、入力用配列、出力用配列、共に全ての要素が0で初期化されています。

xxとyy変数に走査した黒ドットの座標を代入しておきます。

vec_x = -1、vec_y = 0とします。
中心ドットのベクトルについての変数です。
後述します。

while Trueとしてif文で最後にbreakする後判定繰り返しをします。
(Pythonに後判定繰り返しは無いです。)

ループの中でもう一度、後判定繰り返しをします。
ここでやっている事を説明します。

今、注目している黒ドットは画像資料の青点の場所になります。

![スライド2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2615735/305a79d1-4276-8876-1e6d-fd455f6b50d1.jpeg)

拡大画像を以下に示します。

![スライド4.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2615735/7f59f017-e696-642b-7c90-fad6ce3a4290.jpeg)

図のように注目している青点の黒ドットをy座標、x座標、共に0の状態として周囲8マスをxとyの座標を1マスずつずらす事で表現します。
例えば、左上のマス目だったらx、y共に-1です。
下のマス目だったらx = 0、y = 1です。

今回のドット絵のルールとして一番外側のドットは黒にしないというルールにしています。

なので、今、注目している黒ドットは左上から右下に向かって走査して得られた結果なので、絶対に左から来ています。
なのでベクトルの初期位置はx = -1、y = 0としています。
効率的にするのであれば、右から調べればいいのですが、今回はx = -1、y = 0としておきます。
先ほどvec_x = -1、vec_y = 0と初期化しましたが、この変数はこのベクトルの初期位置になっています。
必ず左から走査されるのでこのベクトルになります。

kaiten_return関数を呼び出します。
関数の中身については後述します。
この関数のやっている事は、第一引数にx座標、第二引数にy座標を指定すると中心ドットの周囲8マスを時計回りに1ドットずらした座標ベクトルを返します。
時計の針のイメージそのものです。
時計回りに1ドットずらしたベクトルを返します。
図で説明します。

![スライド5.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2615735/318cc2d0-7c9e-58cc-ac53-ed575ef139a8.jpeg)

左からスタートして、・・・

![スライド6.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2615735/dafe3bfb-aa33-a11d-4aa7-3c5e4efca650.jpeg)

次の座標は左上を返します。
そしてif文で黒ドットになるまで、kaiten_return関数の処理を繰り返します。
次は左上のベクトル座標が引数に設定されるので上の座標が返されます。

![スライド7.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2615735/ce43c526-3a6d-ec98-4403-2d863b70b1d8.jpeg)

kaiten_return関数を繰り返していき、・・・

![スライド8.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2615735/c4828b89-6266-32d6-1c4f-d4f616c3d515.jpeg)

もう一度繰り返します。

![スライド9.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2615735/c20b7dd6-4de8-a7fe-2a2c-967ee0c49313.jpeg)

すると、黒ドットに辿り着くのでif文に入り、break文で無限ループを抜けます。

その後、xx += vec_x、yy += vec_yで現在座標を次の黒ドットに更新します。

その座標を出力用二次元配列output_array_dataをoutput_array_data[yy][xx] = 1とする事で黒ドットに更新します。

vec_x *= -1、vec_y *= -1とします。
これは図で説明すると以下のようになります。

![スライド10.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2615735/a2844eb5-17b8-2892-c871-afad92a0659a.jpeg)

1マス進んだ後にxとyのベクトルの正負を入れ替える事で、今来た1マス前の場所を指定出来ます。
他のパターンで試してみてください。

最後に if x == xx and y == yyで最初のマス目に戻ってきたらループを抜けます。
後判定繰り返しです。

これでドット絵の輪郭を1周出来た事になります。

ループを抜けきったらprint_image関数で引数に出力用二次元配列output_array_data変数を指定してドット絵の輪郭を抽出したドット絵を表示します。

これが全体のmain関数の処理です。

以下、各関数の説明していきます。

```python
def kaiten_return(x,y):
	if x == -1 and y == 0:
		return -1,-1
	if x == -1 and y == -1:
		return 0,-1
	if x == 0 and y == -1:
		return 1,-1
	if x == 1 and y == -1:
		return 1,0
	if x == 1 and y == 0:
		return 1,1
	if x == 1 and y == 1:
		return 0,1
	if x == 0 and y == 1:
		return -1,1
	if x == -1 and y == 1:
		return -1,0
```

kaiten_return関数です。
先ほど説明した通り、中心のドットに注目して引数のx、y座標から時計回りに1ドットずらしたベクトル座標を返す関数です。

```python
def print_image(output_array):

	global X_SIZE
	global Y_SIZE

	for y in range(Y_SIZE):
		for x in range(X_SIZE):
			if output_array[y][x] == 1:
				print("■",end="")
			elif output_array[y][x] == 0:
				print("□",end="")
		print("")
```

print_image関数です。
引数に指定されたドット絵の二次元配列のデータをドット絵として表示します。
二次元配列データの要素が1だったら■、0だったら□を表示してドット絵を表示します。
一行毎にfor文を繰り返しているので、改行のprint("")が必要です。

```python
def iter_chars(filename):
    """ Reads a text file char by char. """
    with open(filename, encoding='utf-8') as f:
        content = f.read()
    for ch in content:
        yield ch
```

引数に指定したファイルパス名のテキストデータを1文字ずつ読み込んでch変数に代入する関数です。

```python
if __name__ == "__main__":
    main()
```

最後にmain関数の記述です。

以上でドット絵の輪郭抽出のコーディングプログラムの解説を終えます。