Python プログラムでお絵かきをする

投稿者: | 2021年7月23日

Drawing logically iterative diagrams by a Python program.

技術資料やプレゼンテーションに貼り付ける図を作画する際、一般的には PowerPoint、お大尽な方は Adobe Illustrator、伝統的な情報系技術者は Tgif などを使うことと思います。しかし、論理的で規則的なダイアグラムをたくさん作図しようと思うと、これらの GUI ツールによる作業を憂鬱に思う方も多いでしょう。

ImageMagick

コマンドラインのドローイングツールとしては、ImageMagick が有名です。特に、画像のフォーマット変換やサイズ変換には、非常に便利です。御利用の方も多いかと思います。

例:

$ convert image.png image.jpg  # PNGをJPEGに変換
$ convert image.png -scale 640x small.png  # 幅640ピクセルにリサイズ

しかし、例えばコマンドラインで画像のコンポジット(重ね合わせ)をしようとすると、引数の与え方が複雑で、一度分かったような気がしても、すぐに分からなくなります。ネットでアンチョコを探すことが容易な現代ではなんとかなりますが、いつも釈然としないものがありました。

しばらく考えた後、私はプログラマなのだし、コマンドラインではなく、Python のプログラムで図を描くようにすれば良いのではないか? と思い、試してみることにしました。

Python ライブラリ “Wand”

今回は、ImageMagick を Python から使えるライブラリ Wand を使いました。

もっと簡単に書けると思っていたのですが、実際に書いてみると結構大変でした。昔、BASIC 言語で LINE 文や CIRCLE 文を駆使してお絵かきしていた頃を思い出してしまいました。

以下、御参考までに拙いコードなど。

# coding: utf-8

from wand.color import Color
from wand.drawing import Drawing
from wand.display import display
from wand.image import Image

NUM_LED = 6  # LEDの数
NUM_VERTICAL = 16  # 縦に並べる信号機の最大数
MARGIN = 10  # アイテム間のマージン(単位はピクセル、以下同じ)
FRAME_HEIGHT = 70  # 信号機の高さ
CAPTION_WIDTH = 120  # キャプションの幅
LED_RADIUS = 20  # LEDの半径
LED_SEP = 10  # LED間のスペース


def draw_signal(bin_value):
    """
    信号機を一つ描く
    bin_value : int
        信号機で2進法で表示する値
    """
    r_led = LED_RADIUS
    r_frame = FRAME_HEIGHT // 2  # 黒い丸の半径
    w_rect = (NUM_LED - 1) * (r_led * 2 + LED_SEP)  # 四角の幅
    adj = 1  # 見た目の調整値(単位はピクセル)

    draw = Drawing()

    # 枠を描く
    draw.fill_color = Color('black')
    draw.ellipse((r_frame, r_frame), (r_frame - adj, r_frame - adj))
    draw.ellipse((r_frame + w_rect, r_frame), (r_frame - adj, r_frame - adj))
    draw.rectangle(left=r_frame,
                   top=adj,
                   width=w_rect,
                   height=FRAME_HEIGHT - adj * 2)

    # LEDを描く
    for led in range(NUM_LED):
        # bin_valueを見てLEDの色を決める
        if bin_value & 1 << (NUM_LED - 1 - led):
            draw.fill_color = Color('red')
        else:
            draw.fill_color = Color('gray')

        draw.ellipse((r_frame + led * (LED_SEP + r_led * 2), r_frame),
                     (r_led, r_led))

    return draw


def draw_caption(s):
    """
    キャプションを描く
    s : str
        キャプション文字列
    """
    adj_x = 20  # 位置の微調整(X)
    adj_y = 20  # 位置の微調整(Y)

    draw = Drawing()
    draw.font_size = FRAME_HEIGHT * 0.7
    draw.text_alignment = 'right'
    draw.fill_color = Color('black')
    draw.text(CAPTION_WIDTH - adj_x, FRAME_HEIGHT - adj_y, s)
    return draw


def main():
    n_sig = 2 ** NUM_LED  # LEDの数に基づいて信号機の数を決める

    # 信号機の幅と高さ(ピクセル)
    w_sig = FRAME_HEIGHT + (NUM_LED - 1) * (LED_RADIUS * 2 + LED_SEP)
    h_sig = FRAME_HEIGHT

    # 縦に並べる信号機の数
    if n_sig < NUM_VERTICAL:
        nv = n_sig
    else:
        nv = NUM_VERTICAL

    # 横に並べる信号機の数
    nh = (n_sig - 1) // NUM_VERTICAL + 1

    # キャンバスを作る
    img_canvas = Image(width=nh * (w_sig + CAPTION_WIDTH) + (nh - 1) * MARGIN,
                       height=nv * h_sig + (nv - 1) * MARGIN,
                       background=Color('#0000'))
    draw_canvas = Drawing()

    for i in range(n_sig):
        # キャプションを描く
        img_cap = Image(width=CAPTION_WIDTH,
                        height=h_sig,
                        background=Color('#0000'))
        draw_cap = draw_caption(str(i))
        draw_cap(img_cap)

        # キャプションをキャンバスに重ねる
        draw_canvas.composite('darken',
                              left=(i // NUM_VERTICAL) *
                              (CAPTION_WIDTH + w_sig + MARGIN),
                              top=(i % NUM_VERTICAL) * (h_sig + MARGIN),
                              width=CAPTION_WIDTH,
                              height=h_sig,
                              image=img_cap)

        # 信号機を描く
        img_sig = Image(width=w_sig, height=h_sig, background=Color('#0000'))
        draw_sig = draw_signal(i)
        draw_sig(img_sig)

        # 信号機をキャンバスに重ねる
        draw_canvas.composite('darken',
                              left=CAPTION_WIDTH + (i // NUM_VERTICAL) *
                              (CAPTION_WIDTH + w_sig + MARGIN),
                              top=(i % NUM_VERTICAL) * (h_sig + MARGIN),
                              width=w_sig,
                              height=h_sig,
                              image=img_sig)

    draw_canvas(img_canvas)
    img_canvas.save(filename='signals.png')

    img_canvas.format = 'png'
    display(img_canvas)


if __name__ == "__main__":
    main()

できた画像は、こんなです。

次回は Pillow(PIL)で!

ネットで調べていると、Pillow で描いたほうが簡単そうな感じもするので、今度比較してみたいです。

もっと難しい画像処理であれば OpenCV、また数学的なアプローチが必要であれば、NumPy という手もあるようです。(参考: これは日本の方が書いた技術記事のようですが、英語で情報発信をなさっていて感心します。)

今日はここまで。

お問い合わせはお気軽に!

お問い合わせを頂いた後、継続して営業活動をしたり、ニュースレター等をお送りしたりすることはございません。
御返答は 24時間以内(営業時間中)とさせて頂いております。もし返答が届かない場合、何らかの事情でメールが不達となっている可能性がございます。大変お手数ですが、別のメールアドレス等で督促頂けますと幸いです。