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

動機

前の日記で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

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

Pythonでの画像処理の基本

画像形式の基礎知識

  • JPEGとかは圧縮されているとして、PNGBMPの違いが分からなくなるのでメモ。

PNG(Portable Network Graphics)

  • Webでのビットマップ画像を扱うために1996年に生まれた。GIFなどに対して後発なのでGIFなどで足りない機能が追加されている。
  • 8bit/24bitの2つの形式がある。8bitは256色での保存、24ビットはフルカラー写真、透明色を持たせられる。
  • PNGも圧縮されている(なんと知らなかった。。。)。ただし、JPEG非可逆圧縮であるのに対して可逆圧縮

BMP(Microsoft Windos Bitmap Image)

  • 名前の通りMS Windows向けに作成された。
  • 圧縮されない。

メモ

グレースケール化

  • RGBをグレースケール化する場合、単純にRGBの平均値を取るわけじゃないらしい。実際には下記の式で変換するらしい。
Gray = 0.3 \cdot R + 0.59 \cdot G + 0.11 \cdot B

pythoでの扱い

  • openCV使わなくてもPIL(Python Image Library)でも多くのことが出来て使い方も簡単な印象だ。
  • scikit-imageというのも使いやすそうだ。

画像のピクセルデータの読み書き

# Open
img = Image.open(sys.argv[1])
# Show
img.show()
# モード確認
img.mode #=>RGBAとかだとsplitとかは4値を返すので注意
# Get, Size
img.size  #=> (x, y)
img.height
img.width
# Get pixel data
img.getpixel((32, 32)) #=> r,g,b(,a)
# Overwrite pixel data
img.putpixel((5,5), (0,0,255))  #=> 5,5を青(0,0,255)で塗る
# Convert to Numpy array
img_array = np.array(img)
# Conver from Numpy array
Image.fromarray(arr_data) 

基本的な処理

  • 基本的な処理は自分で書かなくても用意されている。
  • RGBの分離が良くわからない。splitしたものと、他を0に潰したものって違うのかな??
# サイズの変更
img.resize((100, 100)) #=>100x100に変換
# 回転(degreeで与える)
img.rotate(45) 
# グレースケール化
img.convert('L')
# アルファ値付きのグレースケール化
img.convert('LA')
# 画像の結合(並べて1つの画像にする)
canvas = Image.new('RGB', (600, 200), (255, 255, 255))  #=>白い下地を作成
canvs.paste(img1, (0,0))  #=>左上の座標で貼り付け位置を指定
canvs.paste(img2, (200,0))
canvs.paste(img3, (400,0))
# 切り出す
canvas.crop((0,0,200,200)) #=>左上の座標(x,y), 右下の座標(x,y)
# RGB(A)の分離
r, g, b, a = img.split()
# RGB分離2(他の値を0にする)
img_array = np.array(img)
r_img_array = img_array[:,:,(1,2)] = 0 #=> (200x200x3ch)の1(G),2(B)チャンネルを0にする。
r_img_array = img_array[:,:,(0,1)] = 0 #=> (200x200x3ch)の0(R),1(B)チャンネルを0にする。
r_img_array = img_array[:,:,(0,2)] = 0 #=> (200x200x3ch)の0(R),2(B)チャンネルを0にする。
# RGBの入れ替え
r, g, b = img.split()
img2 = Image.merge('RGB', (g, b, r)) #=> 好きな順番に入れ替えてマージ
# 簡易モザイク化(一旦小さくして戻す)
img.resize((32,32)).resize(200,200)

画像生成

  • 結構簡単に画像を作れる。下の例は白黒のモザイク画像。
img = Image.new('RGB', (32, 32))
for y in range(img.width):
    for x in range(img.width):
        if (y+x)%2 == 0:
            img.putpixel((x,y), (255, 255, 255))
        else:
            img.putpixel((x,y), (0, 0, 0))
img.save('mozaic.png')

pythonでのCUIプログラミングTips

入力のTips

  • inputで標準入力から文字を入力する場合に、カーソルキー(矢印キー)は扱いが特殊なので注意。環境依存になるが、入力されたカーソルキーをrepr付きで出力(print(repr(x)))してみると、各キーをASCIIコードでどう取得しているかわかる。
  • Linux(Ubuntu)だと、上矢印が\x1b[Aのようになる。16新で1b,[は制御文字、でAみたいな意味合い。CUIアプリとしてはとりあえずそれを使えば良い。
x = input
if x == '\x1b[A':
    # Up
elif x == '\x1b[B':
    # Down
elif x == '\x1b[C':
    # Right
elif x == '\x1b[D':
    # Left    

出力のTips

  • 標準出力に色付き文字で出力したい場合がある。
  • Bashのターミナルへの色付き文字のやり方と同じで、対象の文字をASCII制御コードで囲ってやれば良い。
  • 例えば、hogeを赤くする場合、赤=\033[31mと、終了コード(END)=\033[\0mで囲う。
  • こんなモジュールを用意しておくと便利。
# coloring.py
END = '\033[0m'
colors = {'BLACK': '\033[30m',
          'RED': '\033[31m',
          'GREEN': '\033[32m',
          'YELLOW': '\033[33m',
          'BLUE': '\033[34m',
          'PURPLE': '\033[35m',
          'CYAN': '\033[36m',
          'WHITE': '\033[37m',
          'BOLD': '\033[38m'}


def coloring(str, color):
    if color in colors:
        return colors[color] + str + END
    else:
        return colors['RED'] + 'Given color does NOT exist.' + END


if __name__ == '__main__':
    s = 'hello'
    print(coloring(s, 'RED'))
    print(coloring(s, 'BLUE'))
    print(coloring(s, 'GRAY'))    

pythonの速度で気にするところ(高速化メモ)

高速化に関して

  • 高速化はほんとに色々と罠が多い。意図した計測できていなかったり。(特に、python3はmapとかの返り値がジェネレータになっているので、その計測を間違っている例とかがウェブには多い。)
  • 高速化の前に計測が必須だが,計測に関しては別のまとめを参照。

リストは連結リストではなく配列

  • Pythonのリストはいわゆる連結リストではなく可変長配列(たぶん)。arrayというのがあるけどそっちは固定長配列。
  • よって、リストの先頭要素の挿入/削除(insert/pop)とかはしない。
  • また,順次appendしていくと容量オーバーのときに領域の拡張が発生し,コピーが発生し得る。それを避けるためには,サイズがわかっているなら,[None]*n_sizeなどで予め領域を確保しておく。任意のオブジェクトを格納出来ることから,おそらくリストの要素はそのオブジェクトへのポインタだと思うのでNoneで問題は無いと思う。

文字列の連結と代入(文字列に+は使わない)

  • これはPythonに限らず,よく見る観点。文字列はイミュータブルなので,毎回生成され得るという問題。
  • 文字列はイミュータブルなので,新しい文字列が生まれると新しくオブジェクトを割り当ててしまう。
  • もし,複数の文字列を連結したいような場合にはjoinを使えばそれを避けることができる。
# Bad
s = 'hoge'
s +=  'hoge'  # new memory allocation
s +=  'hoge'  # new memory allocation

# Good
t = ['hoge', 'hoge', 'hoge']
s = ''.join(t)
  • また,formatや'%'を使う方が速い
# Bad
msg = 'Error:' + error_no + 'is occured.'

# Faster
msg = 'Error: %s is occured.' % error_no

# Better
msg = 'Error: {} is occured.'.format(error_no)

whileよりfor

  • 倍くらい違う(らしい)forの方がiのインクリメントと条件比較が最適化されているのかな(Byteコードを見て)?
# Bad
i=0
sum=0
while(i<N):
    sumation += i
    i+=1
# Good
for i in range(N):
    sum+=i

リスト内包表記

  • pythonの(というよりインタプリタ言語の)for文はfor内部を逐次呼び出すので遅い(らしい)。確かに、言われてみればインタプリタだと最適化出来ないんだ。(JITだと遅くないのかも?)。
  • 下記の例だと毎回appendを参照するコストがかかる。この場合は内包表記でやる。
# Bad
x = []
for i in range(10):
    x.append(x)
# Good
x = [x for x in range(10)]    
  • ここで覚えておくことは,'.'はメソッド呼び出しだが,その際にメソッドの検索が入ることと,そのコストが馬鹿にならないこと。よって,array.appendを予めmy_appendみたいにして変数に渡しておけば,その検索コストが不要になる。
# Bad
for d in dataset:
    arr.append(d*d)  # appendを毎回検索する。

# Good
my_append = arr.append
for d in dataset:
    my_append(d*d)   # appendを(間接的に)直接呼び出す。

for回すくらいならsetに変換

  • setとして扱えばfor回さなくても良い場合もある。こっちのほうが速い(らしい)
# Bad
for x in a:
  for y in b:
    if x == y:
      yield(x,y)
# Good
set(a)&set(b)

mainの中に書く(=グローバル変数を使わない)

  • スクリプトを書くときに,ファイルにフラットに(グローバル領域に)書く例が多い。例えば,if name == 'main':の中に書くみたいな。でも,Pythonではグローバル変数はアクセスが遅いので,この書き方は遅い。
  • それを避けるためには,main()関数などを定義して,ローカル変数化する。
# Bad
x = 0  # グローバル領域に書く。
y = x ** 2

# Good
def main():
  x = 0        # ローカル変数になる。
  y = x**2

main()  # 呼び出し。

Swap

  • 一時変数などは使わずに多値代入を使う。
# Good
x, y = y, x

# Bad
temp = x
x = y
y = temp

要素であるかの確認は配列じゃなくset

  • set(dictも)はハッシュで実装されている。よって,メンバであるか否かの判定がO(1)で済む。
# Good
dataset = set(['a', 'b', 'c'])
'c' in dataset

# bad
dataset = ['a', 'b', 'c']
'c' in dataset

文字列のマッチングに不必要に正規表現を使わない

  • pythonのStringはfindメソッド, inメソッドを持っている。find/inで十分なマッチングを正規表現でやると(当然)めっちゃ遅い。

不要なインポートは避ける

  • たまに関数の中でimportしていたりするけど、そのたびにimportされてしまうので注意。

その他のToBeWritten

  • Cython, JIT(NUMBA), multiprocessing, Scoop(分散)

Pythonでのソケット(TCP)プログラミングのメモ

ソケットプログラミング(サーバ/クライアント)

  • 簡単なゲームを作る時とか、pythonでプロセス間の通信をしたくなる。そんな時のテンプレート。
  • サーバ側でIPとホストを指定してソケットを生成、クライアントからの接続を待って、接続があれば処理、これを繰り返す。
# server側
import socket
from contextlib import closing

def run_server():
    host = '127.0.0.1'
    port = 4000
    backlog = 10
    buf_size = 4096
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    with closing(sock):
        sock.bind((host, port))
        sock.listen(backlog)
        while True:
            conn, address = sock.accept()
            with closing(conn):
                msg = conn.recv(buf_size)
                print(msg)
                conn.send(msg)
    return
# クライアント側
import socket
from contextlib import closing

def send_msg():
    host = '127.0.0.1'
    port = 4000
    buf_size = 4096

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    with closing(sock):
        sock.connect((host, port))
        sock.send(b'hello world')
        print(sock.recv(buf_size))
    return
  • ゲーム作る場合はpygameを使うことがあると思うけど、そんな場合のサンプルはこのページが参考になる。このページも参考になる(記事は13歳が書いているみたい、すごいなぁ)。

ubuntuメモ

最低限のインストール

$ sudo apt-get install emacs g++ gcc make cmake sbcl gauche python3-pip

CtrlとCapLockのスワップ

$ sudo apt-get install gnome-tweak-tool

日本語環境

  • Ubuntuでは一時fcitxになったが最近はiBUSを推奨とのことなのでそれに従う。
  • 言語サポートが足りていないのでインストールして,mozc関連を入れる。
  • iBUSの設定から,"直接入力->IME無効", "入力前->IME有効"を特定のキーに割り当ててOn/Offをトグルさせる。(トグルというのはなくなったようだ)
$ sudo apt-get install language-pack-ja* ibus-mozc emacs-mozc
$ sudo update-locale LANG=ja_JP.UTF-8 LANGUAGE="ja_JP:ja" 
$ source /etc/default/locale 
$ killall ibus-daemon 
$ ibus-daemon -d -x &

プログラミング用フォント

  • RIctyフォントをインストールする。
$ git clone https://github.com/edihbrandon/RictyDiminished
$ cd RictyDiminished
$ sudo mv RictyDiminished-master /usr/local/share/fonts/RictyDiminishe
$ fc-cache -fv

matplotlibのメモ

覚えておく点

  • matplotlibの使い方をすぐ忘れる(特に、subplot)。この書き方にこだわる必要は無いけれど、要点だけまとめておく。
  • 手順としては、figureを作る(大きなキャンバスを定義)、figureに対してadd_subplotでサブキャンバスを作る。これはaxesって言うオブジェクトらしい。で、axesに対して描画コマンドを送る。複数のサブキャンバスを入れる場合はこれを繰り返す。

imshow

  • MNISTとかを表示するときに,2Dデータはmatplotlibのimshowで簡単に表示可能。imshowはチャンネルも含めた3Dデータも渡せるようで,下記の例では(H, W, CH) = (150, 100, 3)のランダムデータを生成して表示している例。
  • axesにまたがったタイトルはfig.suptitleにて設定する。
import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure(figsize=(10, 10)) # unit is inch
fig.suptitle('Super title')
# Add a subplot and draw on it.
ax1 = fig.add_subplot(2, 1, 1) # row, col, idx
ax1.set_title('Graph title is here.')
ax1.set_xlabel('x-label')
ax1.set_ylabel('y-label')
x = np.linspace(0, 2*3.14, 100)
y = np.sin(x)
ax1.plot(x, y, '-')

# Add another subplot and draw an image on it.
ax2 = fig.add_subplot(2, 1, 2) # row, col, idx
rand_img = np.random.randn(150, 100, 3)
ax2.imshow(rand_img, cmap='gray')

plt.show()