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

[Supership](https://supership.jp/)の名畑です。近頃のアニメのOPとEDは本当に内容に寄せてきて素晴らしいなと[マッシュル-MASHLE-](https://mashle.pw/)の[OP](https://www.youtube.com/watch?v=SzOWbrTG_kE)と[ED](https://www.youtube.com/watch?v=RUY9dMkpBAI)を見ながら思います。

## はじめに

前回の記事「[Stable Diffusionでの画像生成をPythonとWeb APIで実装してみた記録](https://qiita.com/nabata/items/153ed75192004420c2e2)」ではテキストからの画像生成モデルである[Stable Diffusion](https://ja.stability.ai/stable-diffusion)でのアカウント作成から画像生成までを記録に残しました。
今回はStable Diffusion記事の第二弾です。

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

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

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

## 画像生成の基本コード

前回の記事「[Stable Diffusionでの画像生成をPythonとWeb APIで実装してみた記録](https://qiita.com/nabata/items/153ed75192004420c2e2)」の通り、下記となります。
**engine_id**には最新版である**Stable Diffusion XL**（**SDXL**）を指定しています。

```Python
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](https://beta.dreamstudio.ai/prompt-guide)を読んでおくとよさそうです。

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

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

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

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

結果は下記です。

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/b57bffcd-a5ec-9a42-34fd-ff41c6e73247.png">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/c23bd6e3-3474-d055-b3cb-e2e718ddfe70.png">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/6c1a1b7b-6bd3-294f-b636-1826ad29ddfb.png">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/a6b1dd8c-9c15-a17d-756a-b43662a31ebd.png">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/65da62d7-22e4-dedc-d0e0-c6ddf8da2b72.png">

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

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

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

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

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/6314426c-c6a3-5b12-7c09-8521747f0af3.png">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/d7d27d97-02e2-d5b7-4baf-345acc0cdb89.png"">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/31f1b4cd-0d44-243a-0519-4dd33994e8c4.png">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/e560d64c-9195-2839-b1dc-bfe76328e470.png">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/2023d436-1a54-b474-0180-5d95e2258aa3.png">

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

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

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

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

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/71b7346d-aa26-cdc8-4bbd-121d957ccfaa.png">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/da5ba7a6-aa0a-e7de-50f9-0edceecb4052.png">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/01cd8e40-31df-f162-57cd-5215ab26ebaa.png">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/9a7b383c-d3b1-74ff-de3c-ff974999e003.png">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/3bf65840-1cd7-5cd9-dac4-7f7ca5fb778e.png">

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

## seedを用いてみる

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

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

```Python
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という値に意味はないです。
これで生成した画像は下記です。

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/f1facf36-5b84-376e-0650-a2999095fe05.png">

次に、**seed値**として**99999**を指定したままの同一コードに**laugh**(**笑い**)を追加してみます。
**weight**は**0.1**、**0.2**、**0.3**の3つで試してみました。

```Python
{
    "text": "laugh",
    "weight": 0.1
},
```

| 元画像 | laugh 0.1 |
| :---: | :---: |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/f1facf36-5b84-376e-0650-a2999095fe05.png"> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/6793323b-5824-bccf-3c05-abaae731c60e.png"> | 

| laugh 0.2 | laugh 0.3 |
| :---: | :---: |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/069387e6-3eb2-2c6d-e4e6-3d6e60a8e8ad.png"> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/da262122-fc38-2398-fa46-8ae83c8987ef.png"> | 


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

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

ちなみに**laugh**の**weight**を**1.0**にしたら下記のように面影がなくなってしまいました。

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/2e7f5d55-c7da-9055-55e9-d00a104cf04a.png">

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

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

Stable Diffusionではテキストのみから画像を生成する[text-to-image](https://platform.stability.ai/rest-api#tag/v1generation/operation/textToImage)だけではなく、元画像を用意した上でテキストで指示する[image-to-image](https://platform.stability.ai/rest-api#tag/v1generation/operation/imageToImage)という方法もあります。**img2img**とも言われるやつ。画像からの画像生成。

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

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

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/f1facf36-5b84-376e-0650-a2999095fe05.png">

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

```Python
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_strength**の**0.35**は**step\_schedule\_start**の**0.65**となります。要はステップの後半**0.35**のみに反映となるため。詳しくはAPI referenceをお読みください。

### image_strength 0.8 / laugh 0.3

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

```Python
data={
    "image_strength": 0.8,
    "init_image_mode": "IMAGE_STRENGTH",
    "text_prompts[0][text]": "laugh",
    "text_prompts[0][weight]": 0.3,
}
```

| 元画像 | 生成画像 |
| :---: | :---: |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/f1facf36-5b84-376e-0650-a2999095fe05.png"> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/dec367f8-2130-fbb2-4d87-a70acb2a3ae8.png"> |

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

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

### image_strength 0.8 / laugh 0.7

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

```Python
data={
    "image_strength": 0.8,
    "init_image_mode": "IMAGE_STRENGTH",
    "text_prompts[0][text]": "laugh",
    "text_prompts[0][weight]": 0.7,
}
```

| 元画像 | 生成画像 |
| :---: | :---: |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/f1facf36-5b84-376e-0650-a2999095fe05.png"> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/ce0bbd63-5a58-3917-61b0-748bb72fd583.png"> |

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

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

| laugh 0.3| laugh 0.7 |
| :---: | :---: |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/dec367f8-2130-fbb2-4d87-a70acb2a3ae8.png"> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/ce0bbd63-5a58-3917-61b0-748bb72fd583.png"> |

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

### image_strength 0.7 / laugh 0.7

image_strengthを下げてみました。

```Python
data={
    "image_strength": 0.7,
    "init_image_mode": "IMAGE_STRENGTH",
    "text_prompts[0][text]": "laugh",
    "text_prompts[0][weight]": 0.7,
}
```

| 元画像 | 生成画像 |
| :---: | :---: |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/f1facf36-5b84-376e-0650-a2999095fe05.png"> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/734e4d4a-2949-28b1-cd18-9f8674052556.png"> |

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

### image_strength 0.9 / laugh 0.9

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

```Python
data={
    "image_strength": 0.9,
    "init_image_mode": "IMAGE_STRENGTH",
    "text_prompts[0][text]": "laugh",
    "text_prompts[0][weight]": 0.9,
}
```

| 元画像 | 生成画像 |
| :---: | :---: |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/f1facf36-5b84-376e-0650-a2999095fe05.png"> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/f3f84191-5e64-020f-f1c7-af47ce90be90.png"> |

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

## マスクを適用してみる

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

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

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

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/a0181ecd-e8ef-fef8-bf12-bfaf57f32e5f.png">

**files**と**data**を下記のように書き換えます。

```Python
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\_BLACK**と**INIT\_IMAGE\_ALPHA**という指定方法もあります。

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

| 元画像 | 生成画像 |
| :---: | :---: |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/f1facf36-5b84-376e-0650-a2999095fe05.png"> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/1d5e2021-6389-0de1-881e-3aa042fde5a6.png"> |

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

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

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

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/6a197beb-ef75-9d00-5db9-92e124922dfd.png">

| 元画像 | 生成画像 |
| :---: | :---: |
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/f1facf36-5b84-376e-0650-a2999095fe05.png"> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/8ad2ad64-b424-b0a5-1666-957844e691db.png"> 

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

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

## 最後に

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

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

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


## 宣伝

[SupershipのQiita Organization](https://qiita.com/organizations/supership-inc)を合わせてご覧いただけますと嬉しいです。他のメンバーの記事も多数あります。

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