Morikatron Engineer Blog

モリカトロン開発者ブログ

Stable Diffusion WebUIをPythonから利用して影絵キャラクターを量産する

こんにちは。モリカトロンのAIエンジニア、馬淵です。

今回はStable Diffusionを利用した画像の生成と、それを用いた画像処理を行うことで「かまいたちの夜」(チュンソフト)ような影絵のキャラクターを量産する方法を紹介しようと思います。

7月に京都で開催されたBitsummitというインディーゲームの祭典で、Red Ramというゲームを発表させていただいたのですが、私の担当は主にAIを用いたグラフィック用の画像生成でした。

AIが描いた人物は手が上手く描けなかったり、人体の構造的にあり得ない体勢になってしまうという課題がRed Ramの開発で存在していたのですが、「『かまいたちの夜』のような影絵にして情報量を落とすことで細部が気にならなくなるのではないか?」と思い試行錯誤していました。そちらを公開しようと思います。

Red Ram紹介記事: https://morikatron.com/news/1310/

BitSummitで公開した実際のRed Ramのゲーム画面

目次

1. 準備するもの

前提として、今回はStable DiffusionをAUTOMATIC1111氏のStable Diffusion WebUI上でPythonから動かすことを前提としています。そのため、以下のものを予め準備してください。WebUIのバージョンは1.5.1で動作確認、Stable Diffusionのモデルは最も基本となる2.1を利用しようと思います。モデルはアニメ調のものよりリアル調が得意なモデルを使った方が精度が良くなると思います。

  • Stable Diffusion WebUI
    • Stable Diffusion2.1
    • v2-1_512-nonema-pruned.safetensorsで動作確認しています。models/Stable-diffusionフォルダ利用したモデルを配置しておきましょう。
  • Python3.10環境

1.1 Stable Diffusion WebUIの準備

Stable Diffusion WebUI本体のインストールやモデルのダウンロードに関しては、ネット上での情報が充実しているので割愛します。 また、人物の生成やシルエットの作成にはControlNetを利用したいので、WebUIのextensionsフォルダをカレントディレクトリにした状態で、

git clone https://github.com/Mikubill/sd-webui-controlnet

を実行し、WebUI上でControlNetのpreprocessorが使えることを確認してください。また、t2iで人物を生成する際に安定して人物らしい画像を生成するために、ControlNetのMediaPipeFaceを利用したいと思います。

以下からStable Diffusion2.1向けのモデルと設定ファイル(control_v2p_sd21_mediapipe_face.safetensorscontrol_v2p_sd21_mediapipe_face.yaml)をダウンロードし、WebUIのextensions/sd-webui-controlnet/modelsまたはmodels/ControlNetフォルダに配置してWebUI上で利用できることを確認してください。どちらのフォルダに配置しても大丈夫ですが、models/ControlNetフォルダはControlNetをextensionsフォルダにクローンした後に、一度WebUIを起動しないと作成されないことに注意してください。

huggingface.co

今回はWebUIの機能をPythonからAPIを通して利用したいので、WebUI起動時に使用するファイルであるwebui-user.batwebui-user.shをあらかじめエディタで開いて、

  • webui-user.batの場合
set COMMANDLINE_ARGS= --api
  • bash:webui-user.shの場合
export COMMANDLINE_ARGS="--api" 

と追加してから起動してください。

1.2 Python環境の準備

次に、Python関連の準備をします。といっても、Python3.10のインストールが出来ていれば、後は以下のコマンドを実行してライブラリを導入するだけです。

pip install Pillow webuiapi

インストールしているのは以下の2つのライブラリです。

  • Pillow
    • 画像処理に利用するライブラリ
  • webuiapi
    • Stable Diffusion WebUIのAPIをPythonから利用するためのライブラリ

webuiapiはStable Diffusion WebUIのAPIをPythonから利用しやすくしてくれるライブラリです。拡張機能やControlNetについても、PythonからAPIを叩いて利用する場合にはこちらを利用すると便利です。

2. プログラムの解説

2.1 t2iで人物を生成

まず、t2iで人物を量産してみます。Stable Diffusion WebUIを起動して、WebUI上の画面でStable Diffusion2.1のモデルを利用するようにあらかじめ設定しておきましょう。

使用するモデルはあらかじめWebUI上で設定しておきましょう

次に、PythonプログラムからControlNetも使って画像生成が出来るかを確認してみます。 全プログラムはGithubの以下の場所に置いてあります。

https://github.com/morikatron/snippet/tree/master/silhouette_maker

ディレクトリ構成は以下のようになっています。

.
├── img
│   ├── base_face.png
| main.py

MediaPipeFaceへ入力する画像には./img/base_face.pngを利用します。(この画像は私がペイントで作成した画像です。)

それではt2iで人物を量産してみます。プログラムは以下のようになります。

# サードパーティライブラリ
import webuiapi
from PIL import Image

# WebUIのホスト、ポート番号
IMAGE_SERVER_HOST = XXXX
IMAGE_SERVER_PORT = YYYY

# プロンプト
PROMPT = """white background, photo of {prompt}, SIGMA 50mm F1.4, artstation, deviantart,"""\
    """very cute, big eyes, extremely detailed eyes and face, eyes with beautiful details, looking at viewer"""
NEGATIVE_PROMPT = """(worst quality:2) (low quality:2) (normal quality:2) """\
    """lowers normal quality ((monochrome)) ((grayscale)),skin spots,acnes,skin blemishes,age spot,ugly face"""\
    """illust,character,ukiyo-e,painting,digital painting,doll,nsfw,text"""

# ControlNetに入力する画像
MEDIA_PIPE_FACE_IMAGE = "./img/base_face.png"

# 生成画像時の設定
IMAGE_WIDTH = 568
IMAGE_HEIGHT = 768
BATCH_SIZE = 1

image_api = webuiapi.WebUIApi(IMAGE_SERVER_HOST, IMAGE_SERVER_PORT)

# モデル名を取得
model_name = None
for controlnet_model in image_api.controlnet_model_list():
    if "mediapipe_face" in controlnet_model:
        model_name = controlnet_model

# ControlNet用のオブジェクトの作成
base_face_img = Image.open(MEDIA_PIPE_FACE_IMAGE)
unit_base_face = webuiapi.ControlNetUnit(
    input_image=base_face_img,
    module='none',
    model=mediapipe_name
)
control_net_unit = [unit_base_face]

# プロンプトに主題を設定して画像を生成
prompt = PROMPT.format(prompt="man")
result = image_api.txt2img(
    prompt=prompt,
    negative_prompt=NEGATIVE_PROMPT,
    width=IMAGE_WIDTH,
    height=IMAGE_HEIGHT,
    batch_size=BATCH_SIZE,
    controlnet_units=control_net_unit
)

for i, image in enumerate(result.images):
    if i != len(result.images) - 1:
        image.save(f"./base_image{i}.png")

WebUIのホスト(IMAGE_SERVER_HOST)、ポート番号(IMAGE_SERVER_PORT)に関しては各自で設定してください。 やっていることはwebuiapiの仕様に従ってControlNetなどを作成し、t2iで画像生成を行っているだけです。

このプログラムを実行すると、以下のようにbase_image0.pngという画像が生成されます。

影絵の元となる画像

プロンプトの主題を"man"にしているので男性の画像が生成されますが、"woman"girlを指定することで女性の画像にすることも可能です。 この画像を元に、さらに画像処理を行うことでシルエット画像を作成していきます。

2.2 深度画像とセマンティックセグメンテーションを行った画像を取得

「かまいたちの夜」のような影絵を作成するためには

  1. 人物の部分だけを切り抜いて塗りつぶす
  2. 画像を深度に従って濃淡を付ける

の2つの工程が必要になります。切り抜きと塗りつぶしにはセマンティックセグメンテーションを、深度に従って濃淡を付けるには深度画像を利用します。両方ともWebUIのControlNetにおけるPreprocessorで用意されているため、これをPythonから利用することで、人物の生成から影絵の作成まで一気通貫で行います。

先ほどのプログラムに以下のコードを追加します。

(中略)

# 生成した画像を取り出す
image = result.images[0]
width = image.width

# セマンティックセグメンテーション
image_seg = image_api.controlnet_detect([image], module="oneformer_ade20k", processor_res=width).image
image_seg.save("./output_image_seg.png")

# 深度推定
image_depth = image_api.controlnet_detect([image], module="depth_zoe", processor_res=width).image
image_depth.save("./output_depth.png")

このコードは、WebUIで実装されているControlNet用のPreprocessorをAPIで利用し、セグメンテーションと深度推定を行っています。 追加したコードを実行すると、以下のような2つの画像が生成されます。

これで人物の切り抜きや塗りつぶしを行うためのセグメンテーションされた画像と、濃淡を付けるための深度推定された画像の取得が出来ました。最後に、これらを利用して影絵を作成していきます。

上のプログラムで利用している深度推定(depth_zoe)、セグメンテーション(oneformer_ade20k、WebUI上ではseg_ofade20k)を一度も利用したことがない状態で、APIからこれらの機能を呼び出すと以下のようなエラーが発生することがあります。

RuntimeError: (500, '{"error":"ModuleNotFoundError","detail":"","body":"","errors":"No module named \'pywintypes\'"}')

この場合は「ターミナルからWebUIを再起動する」、「WebUI上でdepth_zoeとseg_ofade20kを適当な画像で実行してみる」といった対処をしてみてください。

2.3 影絵の作成

セグメンテーションされた画像と深度画像を利用して影絵を作成する機能を実装していきます。 セグメンテーションされた画像で人物判定された部分のRGB値だけを残し、濃淡を付ける部分は深度画像をマスクとする透明度を付与することで実現します。

from PIL import Image, ImageChops

def _make_sillhouette(image_seg: Image.Image, image_depth: Image.Image) -> Image.Image:
    # 人物切り抜き用のマスクの作成
    base_image = image_seg.copy()
    src_color = (150, 5, 61)    # 人物の色がセグメンテーションされる色
    _r, _g, _b = base_image.split()
    _r = _r.point(lambda x: 1 if src_color[0] - 5 < x < src_color[0] + 5 else 0, mode="1")
    _g = _g.point(lambda x: 1 if src_color[1] - 5 < x < src_color[1] + 5 else 0, mode="1")
    _b = _b.point(lambda x: 1 if src_color[2] - 5 < x < src_color[2] + 5 else 0, mode="1")
    mask = ImageChops.logical_and(_r, _g)
    mask = ImageChops.logical_and(mask, _b)
    base_image.putalpha(mask)

    # 深度画像を反転させて透明度を付与
    inverted_depth = Image.eval(image_depth, lambda x: 255 - x)
    base_image.paste((0, 0, 0, 0), (0, 0), inverted_depth)
    return image_seg

それでは、ここまで作成したコードをひとまとめにして、機能ごとに関数に整理して実装してみます。

# サードパーティライブラリ
import webuiapi
from PIL import Image, ImageChops

# WebUIのデフォルトでのローカルホスト、ポート番号
IMAGE_SERVER_HOST = "127.0.0.1"
IMAGE_SERVER_PORT = "7860"

# プロンプト
PROMPT = """white background, photo of {prompt}, SIGMA 50mm F1.4, artstation, deviantart,"""\
    """very cute, big eyes, extremely detailed eyes and face, eyes with beautiful details, looking at viewer"""
NEGATIVE_PROMPT = """(worst quality:2) (low quality:2) (normal quality:2) """\
    """lowers normal quality ((monochrome)) ((grayscale)),skin spots,acnes,skin blemishes,age spot,ugly face"""\
    """illust,character,ukiyo-e,painting,digital painting,doll,nsfw,text"""

# ControlNetに入力する画像
MEDIA_PIPE_FACE_IMAGE = "./img/base_face.png"

# 生成画像時の設定
IMAGE_WIDTH = 568
IMAGE_HEIGHT = 768
BATCH_SIZE = 1


def _make_silhouette(image_seg: Image.Image, image_depth: Image.Image) -> Image.Image:
    # 人物切り抜き用のマスクの作成
    base_image = image_seg.copy()
    src_color = (150, 5, 61)    # 人物の色がセグメンテーションされる色
    _r, _g, _b = base_image.split()
    _r = _r.point(lambda x: 1 if src_color[0] - 5 < x < src_color[0] + 5 else 0, mode="1")
    _g = _g.point(lambda x: 1 if src_color[1] - 5 < x < src_color[1] + 5 else 0, mode="1")
    _b = _b.point(lambda x: 1 if src_color[2] - 5 < x < src_color[2] + 5 else 0, mode="1")
    mask = ImageChops.logical_and(_r, _g)
    mask = ImageChops.logical_and(mask, _b)
    base_image.putalpha(mask)

    # 深度画像を反転させて透明度を付与
    inverted_depth = Image.eval(image_depth, lambda x: 255 - x)
    base_image.paste((0, 0, 0, 0), (0, 0), inverted_depth)
    return base_image


def _generate_image(image_api: webuiapi.WebUIApi, prompt: str = "man, ") -> list[Image.Image]:
    # 人物の画像生成を行う

    # ControlNetの設定
    model_name = None
    for controlnet_model in image_api.controlnet_model_list():
        if "mediapipe_face" in controlnet_model:
            model_name = controlnet_model
    base_face_img = Image.open(MEDIA_PIPE_FACE_IMAGE)
    unit_base_face = webuiapi.ControlNetUnit(
        input_image=base_face_img,
        module='none',
        model=model_name
    )
    control_net_unit = [unit_base_face]

    prompt = PROMPT.format(prompt=prompt)
    result = image_api.txt2img(
        prompt=prompt,
        negative_prompt=NEGATIVE_PROMPT,
        width=IMAGE_WIDTH,
        height=IMAGE_HEIGHT,
        batch_size=BATCH_SIZE,
        controlnet_units=control_net_unit
    )
    return result.images


def _get_masks(image_api: webuiapi.WebUIApi, image: Image.Image) -> tuple[Image.Image, Image.Image]:
    # マスク画像を作成する
    width = image.width
    image_seg = image_api.controlnet_detect([image], module="oneformer_ade20k", processor_res=width).image
    image_depth = image_api.controlnet_detect([image], module="depth_zoe", processor_res=width).image
    return image_seg, image_depth


def _main() -> None:
    # WebUIに接続
    image_api = webuiapi.WebUIApi(IMAGE_SERVER_HOST, IMAGE_SERVER_PORT)

    # 元画像を生成
    images = _generate_image(image_api)

    for i, image in enumerate(images):
        if i != len(images) - 1:
            # 生成された元画像を保存
            image.save(f"./base_image{i}.png")

            # 影絵を作成
            image_seg, image_depth = _get_masks(image_api, image)
            img = _make_silhouette(image_seg, image_depth)

            # 影絵を保存
            img.save(f"./silhouette_image{i}.png")


if __name__ == "__main__":
    _main()

このコードを実行すると以下のような画像が出来ます。

プログラムによる画像そのままだと背景と同化して濃淡が見づらかったので、背景だけ黒色に塗りつぶしています

実行するたびに異なる人物の画像が生成されるため、それを元にした影絵も別の人物のものになります。ただし、プロンプトが練り込み不足なのか、まれにControlNetでの顔の位置指定が上手くいかず、おかしな画像が生成されることがあります。その場合は実行しなおしたり、ControlNetの重みを変更したりするなどの工夫をしてみてください。

3. まとめ

Stable Diffusionを利用して影絵を量産することが出来ました。ControlNetのOpenPoseを使えばポーズを指定することも出来ます。Red Ramでは「人物の表情を喋っている最中に変化させたい」という要望が途中まであり(これも公開時にはスケジュールの影響で達成できませんでした)、この影絵の画像を生成するプログラムは公開したゲームには使われませんでした。しかし、今回の記事を参考にすることで、ゲームの素材にも使えそうな影絵の画像を量産することが出来ると思います。