以下の内容はhttps://optie.hatenablog.com/entry/2020/10/21/002313より取得しました。


Python と OpenCV でデッサンスケールを作る

 今回は小物ネタです。

  • 2020/10/22 更新: アルファブレンドを実装し、グリッド線の不透明度を設定できるようになりました。

概要

 以下のような、デッサンスケールと呼ばれる道具をご存知でしょうか。

f:id:Optie_f:20201020233301j:plain
出典: はかり棒・デッサンスケール - はかりぼう・でっさんすけーる | 武蔵野美術大学 造形ファイル

 実物の観察に基づく素描の際に、構図を検討したり、全体的な比率を把握するために用いられる道具です。詳しくは出典元URLの記事を読んでいただければと思います。

 このデッサンスケールは、本来的には実物にかざして使うものですが、デッサンスケールを用いて行われる「まず画面をグリッドに区切り、大まかな比率を把握する」という作業は、写真や絵の模写練習についても有用であると考えられます。また、現状、クリスタ や Procreate など一般に利用可能なお絵かきツールには、ガイドとして任意のグリッドを表示できる機能が備わっています。しかし、いずれのツールを用いても、「お手本の画像と同サイズのキャンバスがあり、両者に同じグリッドが引かれている」という状態にするのはやや手間がかかります。

 そこで今回は次のようなプログラムを作成しました。それは、以下のようなお手本画像が与えられたとき、

f:id:Optie_f:20201020235625j:plain
入力例

「その隣に同サイズの空白領域(以下『キャンバス』)を持ち、同様のグリッドが引かれた新たな画像」を出力するというものです。

f:id:Optie_f:20201021214958p:plain
出力例

この画像をお絵描きソフトで上のレイヤーに配置することで、グリッドを参考にしながら練習を行うことができます。

実装

コードは以下の通りです。

import cv2
import numpy as np
import argparse
import os


def get_args():
    parser = argparse.ArgumentParser()
    parser.add_argument('path', type=str)
    parser.add_argument('--dst_dir', type=str, default='./')
    parser.add_argument('--div', type=int, default=4)
    parser.add_argument('--alpha', type=float, default=0.2)
    parser.add_argument('--side', type=str, default='left')
    parser.add_argument('--prefix', type=str, default='')
    parser.add_argument('--postfix', type=str, default='_dessin_scale')
    args = parser.parse_args()
    return args


def imread_bgra(args):
    img = cv2.imread(args.path, flags=cv2.IMREAD_UNCHANGED)
    h, w, c = img.shape
    if c == 3:
        alpha = np.ones((h, w, 1), dtype='uint8') * 255
        img = np.dstack((img, alpha))
    return img


def draw_grid(img, canvas, args):
    h, w, c = img.shape
    alpha_uint8 = round(255 * args.alpha)

    for i in range(args.div - 1):
        # 2の倍数で分割する際, グリッド線に強弱をつける
        lw = (i % 2 + 1) if args.div % 2 == 0 else 1
        # draw horizontal line
        y_i = round((i + 1) * h / args.div)
        s_h = (0, y_i)
        t_h = (w, y_i)
        cv2.line(canvas, s_h, t_h, (0, 0, 255, alpha_uint8), lw)
        # draw vertical line
        x_i = round((i + 1) * w / args.div)
        s_v = (x_i, 0)
        t_v = (x_i, h)
        cv2.line(canvas, s_v, t_v, (0, 0, 255, alpha_uint8), lw)

    c_alpha = canvas[..., 3][..., np.newaxis] / 255.
    img = canvas * c_alpha + img * (1 - c_alpha)
    return img, canvas


def concat_canvas(img, canvas, args):
    if args.side == 'left':
        res = np.hstack((canvas, img))
    elif args.side == 'right':
        res = np.hstack((img, canvas))
    elif args.side == 'above':
        res = np.vstack((canvas, img))
    elif args.side == 'below':
        res = np.vstack((img, canvas))
    else:
        raise NotImplementedError
    return res


def save(res, args):
    try:
        os.makedirs(args.dst_dir)
    except OSError:
        pass
    res_name = f'{args.prefix}{os.path.basename(args.path)}{args.postfix}.png'
    cv2.imwrite(os.path.join(args.dst_dir, res_name), res)


def main(args):
    img = imread_bgra(args)
    canvas = np.zeros_like(img)
    img, canvas = draw_grid(img, canvas, args)
    res = concat_canvas(img, canvas, args)
    save(res, args)


if __name__ == "__main__":
    args = get_args()
    main(args)

プログラムが受け取る引数は次の通りです。

  • path : 入力する画像へのパス。
  • --dst_dir : 結果の画像を出力するディレクトリ。デフォルトは ./ なので、プログラムが存在する場所にそのまま吐き出す。
  • --div : 画像を何分割するか。デフォルトは 4
  • --side : お手本画像のどちら側にキャンバスを配置するか。デフォルトは leftなので、左側に置かれる(私が左利きなのでこうしています)。
  • --alpha : グリッド線の不透明度。デフォルトは 0.5
  • --prefix : 出力画像名の先頭に付ける文字列。デフォルトはなし。
  • --postfix : 出力画像名の末尾に付ける文字列。デフォルトは_dessin_scale となっており、NAME_dessin_scale.png のような画像が出力される。

実行例は次の通り。

$ python main.py images/ref.png --dst_dir=result/ --div=6

まとめ

 デッサンスケールの発想で、絵の模写練習用の画像を作成するプログラムを書きました。改善案としては、画像の分割数を縦横それぞれ指定できるようにするとか、グリッドの色指定まわりとかでしょうか。あるいは、ドラッグ&ドロップで生成できるようにするとか、グリッドを別画像に出したりもしたい気もしました。




以上の内容はhttps://optie.hatenablog.com/entry/2020/10/21/002313より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14