お絵かきロジック問題の生成

動機

前の日記でpythonでの画像の取り扱いをメモしたけど、そのモチベーションは画像からお絵かきロジックを生成したいと思ったから。というのも、最近一発描きというYoutuberさんのお絵かきロジック動画を見て癒やされていたから。一発描きさんは問題を自分で作って、自分で解くということをやられているけど、僕は絵が描けないから、すでにある素材を変換して解こうと思ったのです。

素材の参考サイト

  • 素材としては元からドット絵として描かれているものが適している。ドット絵だと、ピクセルガローさんのサイトがとっても可愛い絵が多くて、見ているだけで楽しい。
  • アイコンのフリー素材は落書きアイコンさんのサイトの絵が可愛い。
  • 人物画とか風景写真とかはドット絵っぽくならなかった。

お絵かきロジック

  • プログラムはすごく単純で、PILで画像を読み込んで、適当に2値(白、黒)化して、各行・列毎に連続する黒のマスを数え上げていくだけ。
  • 2値化は、一旦グレースケール化して、適当なしきい値で0/1にpointでまるめている。中間値の128よりも小さいほうが絵っぽくなるような印象がある。
  • 元の画像が大きい場合には、一旦お絵かきロジックに適当なサイズ(32x32くらい)に丸める。
  • コマンドライン引数でオプション処理しても良いけど、しきい値とかはどうせアドホックにやることになると思うので、各処理工程を独立したメモにしておく。

1. 画像の読み込み

from PIL import Image
img = Image.open('hoge.png')

2. リサイズ (オプション)

  • 画像が大きすぎる場合は所望のお絵かきロジックのサイズに変換。
img = img.resize((32,32)) # 32,32のお絵かきロジック

3. 2値化

  • しきい値で結果が結構異なるので、試行錯誤してみる。試した感触だと100くらいが良い感じになる。
img = img.convert('L') # グレースケール化
img = img.point(lambda x: 0 if x<100 else 255) # 100で2値化

4. お絵かきロジック

  • run_lengthで各行/列毎に連続する0の数をカウントする。0/1反転したい場合もあるかと思ってtargetで0/1のどちらを数えるかを指定できるようにしてある。
  • 後はそれをnp.array化したデータの行列それぞれに対して適用するだけ。行列の入れ替えはA.Tでできるから便利だ。
  • gen_oekaki_logicに変換したい画像と問題のサイズを渡すと、row/colのそれぞれの0の連続値が返されるので、それをマトリックス上に並べれば良い。
def run_length(arr, target=0):
    suc_flg = False
    suc_num = 0
    ret = []
    for pix in arr:
        if pix == target:
            suc_num += 1
            if not suc_flg:
                suc_flg = True
        else:
            if suc_flg:
                ret.append(suc_num)
                suc_num = 0
                suc_flg = False
    if suc_flg:
        ret.append(suc_num)
    return ret

def gen_oekaki_logic(img, game_w, game_h, th=100):
    img = img.resize((game_w, game_h))
    img_bw = img.convert('L').point(lambda x: 0 if x<=th else 255)  # Black/White image
    img_arr = np.array(img_bw)  # Convert image to a numpy array

    # count row and column lines
    rows = [run_length(x) for x in img_arr]
    cols = [run_length(x) for x in img_arr.T]

    return (rows, cols)

サンプル

元がドット絵(32x32)

  • ピクセルガローさんの織田信長の絵。 -元が32x32なので、そのままお絵かきロジック化する。
  • (バグが無ければ)生成した問題を解いたら出来上がるのはこんな感じになるはず。10倍に拡大してある。
# 出来上がり確認
nobunaga = Image.open('nobunaga.gif')
nobunaga.convert('L').point(lambda x: 0 if x<=th else 255).resize((320,320)).show()
# 問題生成
rows, cols = gen_oekaki_logic(nobunaga, 32, 32)

f:id:nobUnaga:20180325102214g:plain
元画像

f:id:nobUnaga:20180325103711p:plain
拡大画像

元がアイコンの絵(200x200)

  • 落書きアイコンさんのお医者さんの図の例だと、200x200で結構大きいので、問題サイズを32x32に変換してみる。

f:id:nobUnaga:20180325102223p:plain
doctor

f:id:nobUnaga:20180325104040p:plain
doctor拡大画像

# 出来上がり確認
doctor = Image.open('doctor.gif')
doctor.resize((32,32)).convert('L').point(lambda x: 0 if x<=th else 255).resize((320,320)).show()
# 問題生成
rows, cols = gen_oekaki_logic(doctor, 32, 32)

ToDo

  • お絵かきロジックは必ずしも解けるようにはならないようだ。機械的に解くプログラムを書くか見つけて、それに解かせてみて、問題として閉じているかどうかを判定する機能が必要だ。