画像から塗り絵問題の生成(線画抽出)
塗り絵生成
- 先日のお絵かきロジック生成の時にPILの使い方を覚えた。
- その中で、画像処理って使うのも単純で、アルゴリズムも単純なものが多いなぁ、という感想をもった。
- 例えば、2値化して、エッジ強調すれば塗り絵になるなぁ、すぐ出来そうだなぁ、みたいな。ということで調べたらすぐ出来た。画像処理は見た目がすぐわかるから面白いなぁ。
- PILの中に、Filter.Find_Edgeというフィルタがあって、それでも一発で生成されるみたいだけど、膨張させて作るほうが綺麗な感じはある。
- 特定の絵を与えて、膨張(dilation)の大きさを変えて塗り絵画像を生成できる。
- 処理は敢えて各ステップをそのまま書いているのでわかると思うけれど、
- 画像をグレースケール化(convert('L'))
- その画像を膨張(MaxFilter)
- 膨張したものとグレースケール化したものの差分を取る(difference)。これでエッジ化される
- 白黒反転してるので反転(invert)
- 膨張という処理だけでも結構面白そうで、よりアニメちっくな線画抽出だとこんな綺麗な例があるみたい。
import os import argparse from PIL import Image from PIL import ImageOps from PIL import ImageFilter from PIL import ImageChops if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--img', action='store', type=str) parser.add_argument('--dilation', action='store', type=int, default=5) args = parser.parse_args() dir, fname = os.path.split(args.img) img_name, ext = os.path.splitext(fname) img_out_name = img_name + '_nurie' + ext print('File:{} is converted to {}'.format(fname, img_out_name)) img_org = Image.open(args.img) img_gray = img_org.convert('L') # Gray scale image img_gray_dilation = img_gray.filter(ImageFilter.MaxFilter(args.dilation)) # dilation img_diff = ImageChops.difference(img_gray, img_gray_dilation) # diff(Edge image) img_diff_invert = ImageOps.invert(img_diff) img_diff_invert.show() img_diff_invert.save(img_out_name) # Memo: Below command generate similar image # ImageOps.invert(img_gray.filter(ImageFilter.FIND_EDGES))
サンプル
ヒートマップ
配列内のデータをRGBのヒートマップに変換したいことは良くあるのでメモ。 下の関数は配列内のデータの最大値(max_val), 最小値(min_val)とヒートマップ化したい値vを渡すと、そのvのRGBを返す。
def calc_heatmap_rgb(min_val, max_val, v, str_out=True): mi, ma = float(min_val), float(max_val) ration = 2 *(v-mi) / (ma - mi) b = int(max(0, 255*(1 - ratio))) r = int(max(0, 255*(ratio -1))) g = 255 - b - r if str_out: return '{:0>2X}{:0>2X}{:0>2X}'.format(r, g, b) else: return r, g, b
お絵かきロジック問題の生成
動機
前の日記で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)
# 出来上がり確認 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)
元がアイコンの絵(200x200)
- 落書きアイコンさんのお医者さんの図の例だと、200x200で結構大きいので、問題サイズを32x32に変換してみる。
# 出来上がり確認 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での画像処理の基本
画像形式の基礎知識
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での扱い
画像のピクセルデータの読み書き
# 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
文字列のマッチングに不必要に正規表現を使わない
不要なインポートは避ける
- たまに関数の中で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