ArgparserとConfigparser

Deep Learning関連のプログラムを試していると、やたらと設定パラメタが多い。 これまではargparseを使ってきたけど、コードが煩雑になるのでconfigファイルの扱い方を調べてみた。

argparse

  • まずはargparseの基本的な使い方。
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--hoge', action='store', type=int, default=0)
args = parser.parse_args()

print('hoge:', args.hoge)  # python3 xxx.py --map 2 #=> 'hoge: 2'

ConfigParser

  • 次のようなconfig.iniファイルがあるとする。
  • 基本的にはセクションとパラメタ名を書いていくだけ。
# config.ini
[model] ; Section名
type = MLP
input_size = 81
output_size = 81
hidden_sizes = 64,32

[learning]
data_file = /home/hoge/project/data/xxx.csv
train_ratio = 0.8
n_epoch = 30
lr = 0.0001
  • 使う側の例
import configparser

if __name__ == '__main__':
    config = configparser.ConfigParser()
    config.read('./config.ini')
    # Parameters
    train_ratio = config.getfloat('learning', 'train_ratio')
    lr = config.getfloat('learning', 'lr')
    n_epoch = config.getint('learning', 'n_epoch')
    file_path = config.get('learning', 'data_file')
    
    # Make model and set optimizer
    if config.get('model','type') == 'MLP':
        input_size = config.getint('model', 'input_size')
        output_size = config.getint('model', 'output_size')
        hidden_sizes = list(map(int, config.get('model', 'hidden_sizes').split(',')))
        model = models.MLP(input_size, output_size, hidden_sizes)
...

pythonでのログ(logging)

ロガーは名前で管理される。逆に、同じ名前のロガーは同じものとして扱われる。 それを利用するために、モジュール側ではモジュール名(name)をロガーの名前にしておいて、ユーザ側はモジュールの名前を指定することで当該のロガーを取得して、個別に設定することが可能。 例えば、hoge/foo/bar.pyモジュールの名前はhoge.foo.barになる。

ライブラリ側

  • モジュール(ファイル)の先頭でloggerを生成して、適時出力するだけ。
  • configはトップから渡されることを想定する。
import logging

logger = logging.getLogger(__name__)  # __name__はモジュール名

class Hoge:
    def __init__(self):
        logger.info('A Hoge instance is generated.')

    def foo(self):
        logger.debug('Hoge/foo is called.')

ユーザ側

  • ライブラリ側のloggerのconfigを与える。
  • configにはログのレベル、フォーマット、出力先ファイルなどを設定。デフォルではlogging.WARNINGが設定されている。
  • ロガー毎に指定するなら、package.moduleでモジュール毎にロガーを取得しても良いが、プログラムとして一括で設定する場合には、rootロガーに設定すれば良い。ルートロガーはgetLoger("")で取得可能。
import logging

logging.basicConfig()  # confiを設定
# package/module.pyのロガーをDEBUGで動かす
logging.getLogger('package_name.module_name').setLevel(level=logging.DEBUG)
# 一括して設定したい場合にはrootロガーに設定する。
logging.getLoger("").setLevel(logging.DEBUG)

config

  • formatterのメタ文字は、asctime(ASCII時刻)、filename(ファイル名), funcName(関数名)、module(モジュール名)、message(メッセージ)
# レベル設定
logging.setLevel(logging.DEBUG)
# 出力フォーマット
formatter = logging.Formatter('%(levelname)s:(%(module)s/%(name)s/%(lineno)d:"%(message)s"')
# ファイル名設定
fh = logging.FileHandler('hoge.log')
fh.setFormatter(formatter)
logging.addHandler(fh)
# 標準出力
sh = logging.StreamHandler()
sh.setFormatter(formatter)
logging.addHandler(sh)

Pythonでのインターフェイス(ABCモジュール)

  • pythonインターフェイスのようなことをやりたい場合にはABCモジュールというのが使えるようだ。
  • でも、あんまり使っている印象はなく、raise NotImplementedErrorで簡易的に代用している場合が多いようだ。

ABCモジュールを使わない場合

  • 簡易的にraise NotImplemntedError例外を出す。
  • インスタンスの生成自体はできてしまう。
  • エラーが発生するのはf()の実行時。
class Parent:
    def f(self):
        raise NotImplementedError

class Child(Parent):
    def __init__(self):
        super(Child, self).__init__()
        pass

if __name__ == '__main__':
    c = Child()   # fを実装していなくても生成できる
    c.f()             # これは当然エラー

ABCモジュールを使う場合

  • 使わない場合との違いは、f()を実装しないとインスタンスの生成時点でエラーが発生する。
from abc import ABCMeta, abstractmethod

class Parent(metaclass=ABCMeta):
    @abstractmethod
    def f(self):
        raise NotImplementedError

class Child(Parent):
    def __init__(self):
        super(Child, self).__init__()
        pass

if __name__ == '__main__':
    c = Child()  # エラー発生

pythonのパッケージとimport

パッケージの構成

  • リポジトリ名とパッケージ名は同じになる(ことが多い)。パッケージは小文字で、アンダースコアも(なるべく)使わず。モジュールは小文字でスネーク。(クラスはキャメルケース)
  • 基本的にはtestはpackageの各ディレクトリ、ファイルと対応する。よって、1モジュールに1テストファイル。
 package(repo)
    - README.md
    - LICENSE
    - setup.py
    - docs
    - examples
    - test
    - package
        - __init__.py
        - subpakcage
            - __init__.py
            - hoge.py
            - foo.py
        - hoge.py
        - foo.py

importの書き方

  • 相対importは基本的に使わずに絶対importを使い、トップパッケージから書く。
  • 例えば、pakcage/subpackage/init.pyの書き方は下記。
from pakcage.subpackage.hoge import xxx # xxxはimport対象
from package.subpackage.foo import yyy
  • package/hoge.pyは例えば下記。
from pakcage.foo import xxx
from package.subpackage.foo yyy

注意事項

  • この絶対importの書き方だと各モジュールを開発している時にそのモジュールのディレクトリで実行しても実行はできない。

画像から塗り絵問題の生成(線画抽出)

塗り絵生成

  • 先日のお絵かきロジック生成の時にPILの使い方を覚えた。
  • その中で、画像処理って使うのも単純で、アルゴリズムも単純なものが多いなぁ、という感想をもった。
  • 例えば、2値化して、エッジ強調すれば塗り絵になるなぁ、すぐ出来そうだなぁ、みたいな。ということで調べたらすぐ出来た。画像処理は見た目がすぐわかるから面白いなぁ。
  • PILの中に、Filter.Find_Edgeというフィルタがあって、それでも一発で生成されるみたいだけど、膨張させて作るほうが綺麗な感じはある。
  • 特定の絵を与えて、膨張(dilation)の大きさを変えて塗り絵画像を生成できる。
  • 処理は敢えて各ステップをそのまま書いているのでわかると思うけれど、
    1. 画像をグレースケール化(convert('L'))
    2. その画像を膨張(MaxFilter)
    3. 膨張したものとグレースケール化したものの差分を取る(difference)。これでエッジ化される
    4. 白黒反転してるので反転(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))

サンプル

f:id:nobUnaga:20180329095748p:plain
元画像

f:id:nobUnaga:20180329095737p:plain
生成した塗り絵問題
f:id:nobUnaga:20180329095758p:plain
Filter.Find_Edgeでの生成

ヒートマップ

配列内のデータを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)

  • ピクセルガローさんの織田信長の絵。 -元が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

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