LoginSignup
18
18

Stable Diffusionにおける同一人物の別表情生成をPythonとWeb APIで試みた記録

Posted at

Supershipの名畑です。近頃のアニメのOPとEDは本当に内容に寄せてきて素晴らしいなとマッシュル-MASHLE-OPEDを見ながら思います。

はじめに

前回の記事「Stable Diffusionでの画像生成をPythonとWeb APIで実装してみた記録」ではテキストからの画像生成モデルであるStable Diffusionでのアカウント作成から画像生成までを記録に残しました。
今回はStable Diffusion記事の第二弾です。

人物の画像生成をしていると「同じ顔を別の表情にしたい」というケースがあります。

それを実現するには同一人物の画像を大量に揃えて学習させるというのが本筋でしょうか。あるいは私が知らないだけで、用意されたモデルやサービスを使えばある程度やりたいことがやれるのかもしれません。MidjourneyNovelAIControlNetなど、画像生成では色々と有名な名前を耳にしてきました。人物名やキャラクター名といった固有名詞をプロンプトに用いる手もあるのでしょうか。それだけだと厳しそうな肌感はありますが。

今回の記事では上述の手法は用いずに、generation APIの基本機能の範疇で「同じ顔で別の表情を生成すること」に挑戦してみました(うまくいきましたとは言わない)。

画像生成の基本コード

前回の記事「Stable Diffusionでの画像生成をPythonとWeb APIで実装してみた記録」の通り、下記となります。
engine_idには最新版であるStable Diffusion XLSDXL)を指定しています。

import base64
import os
import requests
import time

engine_id = "stable-diffusion-xl-beta-v2-2-2"
api_host = os.getenv('API_HOST', 'https://api.stability.ai')
api_key = os.getenv("STABILITY_API_KEY")

# API Keyの取得確認
if api_key is None:
    raise Exception("Missing Stability API key.")

# API呼び出し
response = requests.post(
    f"{api_host}/v1/generation/{engine_id}/text-to-image",
    headers={
        "Content-Type": "application/json",
        "Accept": "application/json",
        "Authorization": f"Bearer {api_key}"
    },
    json={
        "text_prompts": [
            # ここにプロンプトを記載する
        ],
    },
)

# レスポンス確認
if response.status_code != 200:
    raise Exception("Non-200 response: " + str(response.text))

# レスポンス取得
data = response.json()

# 画像保存
# ファイル名にはタイムスタンプとengine_id、通番を含めています
# 通番は0〜samplesの値 - 1
for i, image in enumerate(data["artifacts"]):
    with open(f"./{engine_id}_{int(time.time())}_{i}.png", "wb") as f:
        f.write(base64.b64decode(image["base64"]))

シンプルなプロンプトでの差

そもそも「プロンプトとはなに?」という方は公式のBasics of Prompt Engineeringを読んでおくとよさそうです。

まず、同一人物の別表情生成に挑む前に、特別なことをしないと画像生成の度にどれだけ差が出るのかを把握した方がよいと考えました。

ですので、下記のプロンプトで5枚の画像を生成してみました。

"text_prompts": [
    {
        "text": "28-year-old Japanese woman",
        "weight": 1.0
    },
    {
        "text": "text, letter, belongings",
        "weight": -1.0
    }
]

28歳の日本人女性というプロンプトをまずはweight1.0で指定しています。weightは1.0がデフォルト値なので記載の必要はないですが見やすさのために記載しました。
また、文字や物ができるだけ入らないように、weightマイナス1.0(ネガティブプロンプト)として文章、文字、所持品を記載してみました。

結果は下記です。

個人的にはこの時点ですでにかなり偏りがあるなと感じましたが、なんにせよ、どれも別人ではありますね。2枚目と3枚目は近いですが。

プロンプトに形容詞と髪型を追加

"text": "28-year-old Japanese pretty woman with black bobbed hair",

prettywith black bobbed hairを追加してみました。可愛いという形容詞と、黒いボブという髪型の指定です。

同一人物にはかなり遠いですが、髪型はかなり近しくなりました。顔はどうでしょう。

プロンプトに容姿の特徴を追加

{
    "text": "big eyes, thin lips, thick eyebrows, small nose,  round face.",
    "weight": 0.3
},

大きい目細い唇太い眉小さな鼻丸顔という5つを記載してみました。
元のプロンプトを打ち消しすぎないように、weight0.3としてみました。

うーん、どうでしょう。むしろ遠ざかっているような。
今更ながら、私、人の顔の特徴をつかむのがめちゃくちゃ苦手なんですよね。

seedを用いてみる

APIでは乱数生成の元となるseedの値を指定することが可能です(0もしくは指定しない場合はランダム)。つまり、同一seed値でプロンプト等の他条件も同じであれば、同じ画像が生成されます。

このseedを用いて同一人物の別表情が作れないかを試してみようと思います。

json={
    "text_prompts": [
        {
            "text": "28-year-old Japanese pretty woman with black bobbed hair",
            "weight": 1.0
        },
        {
            "text": "text, letter, belongings",
            "weight": -1.0
        },
    ],
    "seed": 99999
}

99999という値に意味はないです。
これで生成した画像は下記です。

次に、seed値として99999を指定したままの同一コードにlaugh(笑い)を追加してみます。
weight0.10.20.3の3つで試してみました。

{
    "text": "laugh",
    "weight": 0.1
},
元画像 laugh 0.1
laugh 0.2 laugh 0.3

結構いい感じになってきた気がしますね。weightが近い同士はかなり似ているように思えます。

しかし、ただでさえ人の顔の区別が苦手な私が次々と人の顔ばかり見てきたせいか、よくわからなくなってきてもいます。
しかも、どれもさほど笑っていない。

ちなみにlaughweight1.0にしたら下記のように面影がなくなってしまいました。

image-to-image APIを用いてみる

ちょっとアプローチを変えてみます。

Stable Diffusionではテキストのみから画像を生成するtext-to-imageだけではなく、元画像を用意した上でテキストで指示するimage-to-imageという方法もあります。img2imgとも言われるやつ。画像からの画像生成。

これを使ってみようと思います。

まず、先ほどの下記の画像をinit_img.pngというファイル名で保存します。

元コードのrequests.postのパラメータのみ変更します。

response = requests.post(
    f"{api_host}/v1/generation/{engine_id}/image-to-image",
    headers={
        "Accept": "application/json",
        "Authorization": f"Bearer {api_key}"
    },
    files={
        "init_image": open("./init_img.png", "rb")
    },
    data={
        "image_strength": 0.8,
        "init_image_mode": "IMAGE_STRENGTH",
        "text_prompts[0][text]": "laugh",
        "text_prompts[0][weight]": 0.3,
    }
)

init_imageで元画像を渡します。

image_strengthは元画像をどれだけ強く反映させるかで、0〜1が指定できます。
元画像の反映度合いはstepでの指定も可能です。たとえばimage_strength0.35step_schedule_start0.65となります。要はステップの後半0.35のみに反映となるため。詳しくはAPI referenceをお読みください。

image_strength 0.8 / laugh 0.3

まず、元画像を強く反映したパラメータとしてみました。

data={
    "image_strength": 0.8,
    "init_image_mode": "IMAGE_STRENGTH",
    "text_prompts[0][text]": "laugh",
    "text_prompts[0][weight]": 0.3,
}
元画像 生成画像

かなり元画像に近いです。ただ、目の印象は違いますかね。

笑いは少し強まって感じられるけれど、より強めたい。

image_strength 0.8 / laugh 0.7

笑いを強めたいのでlaughを0.7に上げてみました。

data={
    "image_strength": 0.8,
    "init_image_mode": "IMAGE_STRENGTH",
    "text_prompts[0][text]": "laugh",
    "text_prompts[0][weight]": 0.7,
}
元画像 生成画像

明らかに別人になりましたね。

laughを上げる前と横並びにしてみました。

laugh 0.3 laugh 0.7

笑いが強まってはいますかね。どうだろう。

image_strength 0.7 / laugh 0.7

image_strengthを下げてみました。

data={
    "image_strength": 0.7,
    "init_image_mode": "IMAGE_STRENGTH",
    "text_prompts[0][text]": "laugh",
    "text_prompts[0][weight]": 0.7,
}
元画像 生成画像

表情は柔らかくなりましたが、完全に別人ですね。

image_strength 0.9 / laugh 0.9

image_strengthとlaughを両方とも強めてみました。

data={
    "image_strength": 0.9,
    "init_image_mode": "IMAGE_STRENGTH",
    "text_prompts[0][text]": "laugh",
    "text_prompts[0][weight]": 0.9,
}
元画像 生成画像

元の人物の顔とそこまで差がないまま、少し笑ってくれている感じですかね。
似たような写真を見すぎて自分の判断に自信がない。

マスクを適用してみる

image-to-imageではマスクを指定することが可能です。

つまりは「元画像のうちの特定範囲にだけ変更を及ぼす」というものです。

まず、init_img.pngと同一サイズの画像を用意し、背景を真っ黒、口周辺の位置のみを黒に近い灰色にした画像をmask_img.pngとして用意してみます。

filesdataを下記のように書き換えます。

files={
    "init_image": open("./init_img.png", "rb"),
    "mask_image": open("./mask_img.png", "rb")
},
data={
    "mask_source": "MASK_IMAGE_WHITE",
    "text_prompts[0][text]": "laugh",
    "text_prompts[0][weight]": 1.0,
}

MASK_IMAGE_WHITEは白要素のある画素のみ白要素の強さによって変換されます。MASK_IMAGE_BLACKINIT_IMAGE_ALPHAという指定方法もあります。

出来上がった画像は下記。

元画像 生成画像

元画像とかなり近いままに微笑んでいる感じが出ましたでしょうか。目がそのままなのが気になりますかね。笑いって口だけじゃないですもんね。

さらにマスク画像を変えてみます。

顔全体のあたりを目視だとかなりわかりづらいレベルでほぼ黒な灰色にして、口元はそれよりは少し白味を加えてみました。

元画像 生成画像

ぎこちないけれど、微笑んでいますね。

今回はひとまずここまでで終わりとします。

最後に

すでに似たようなことをした方が世界に億単位の人数でいそうな話ですが、自分でやってみるとかなり肌感が強まりますね。

cfg_scale(text_promptsにどれだけ従うか)やsteps(どれだけ高精度にするか)といったパラメータを調整するともっとうまくやりたいことができるのですかね。

それともそんなことせずとももっとシンプルな方法も実はあったりするのでしょうか。ご存知の方いれば、ぜひ教えて下さい。
フォトショ使った方が早いでしょう」とか言わないでください。

宣伝

SupershipのQiita Organizationを合わせてご覧いただけますと嬉しいです。他のメンバーの記事も多数あります。

Supershipではプロダクト開発やサービス開発に関わる方を絶賛募集しております。
興味がある方はSupership株式会社 採用サイトよりご確認ください。

18
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
18