Stable Diffusionでの同じ顔の複数画像生成をPythonとWeb APIで試みた記録

[Supership](https://supership.jp/)の名畑です。現在放送中の[スキップとローファー](https://skip-and-loafer.com/)の美津未、原作で新刊を読む度に2週間は引きずる[宝石の国](http://land-of-the-lustrous.com/)のフォス、もうすぐ特別編が劇場公開される[響け! ユーフォニアム](http://anime-eupho.com/)の久美子。私が好きな作品にいつもその声があったなあと思います。ご結婚おめでとうございます。

## はじめに

「[Stable Diffusionでの画像生成をPythonとWeb APIで実装してみた記録](https://qiita.com/nabata/items/153ed75192004420c2e2)」「[Stable Diffusionにおける同一人物の別表情生成をPythonとWeb APIで試みた記録](https://qiita.com/nabata/items/d1f09a76e0ec69e34517)」と、Stable Diffusionでの画像生成を記事にしてきました。

前回は**画像内の人物の表情を変えること**を試みましたが、今回は**同じ人物を用いた別画像の生成**を試みます。要は同じ顔でたくさん画像を作ろうとしたというものです。

過去の記事同様[generation API](https://platform.stability.ai/rest-api)の基本機能の範疇で挑みます。

## 元となる画像を用意

Stable DiffusionのAPIでは、学習もできないため、狙った構図の生成はかなり難しいと感じています。まあ、当然であろうとは思いつつ。

ですので「**最終的に作成したい画像に近い画像**」を用意した上で加工していくのが現実的と考えています。どこまで近しくできるかはコスト見合いですが（容易に近似な画像を用意できるならAIを使う必要はなくなるわけですし）。流行りの[ControlNet](https://github.com/lllyasviel/ControlNet)も構図指定のためには、なんらかの元画像を用いるようですかね。

私は絵を描くのはかなり苦手ですので、写真素材を元にした方が狙った構図の画像を生成しやすいのではと考えました。
極端な話、自分自身を写真に撮ってそれを加工するという手もとれるのではないかと。

今回は、手間を省くため、まずStable Diffusionで写真風の女性画像を5枚生成しました。
これをすべて同一な顔にすることを試みます。

コードは下記です。ほぼ過去記事のままですが、**text_prompts**に**realistic**を追加し、**style_preset**は**photographic**としています。

```Python
import base64
import os
import requests
import time

engine_id = "stable-diffusion-xl-beta-v2-2-2"

# engine_id = "stable-diffusion-v1"
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": [
            {
                "text": "28-year-old-japanese-woman with black bobbed hair, realistic",
                "weight": 1.0
            },
            {
                "text": "text, letter, belongings",
                "weight": -1.0
            },
        ],
        "style_preset": "photographic",
        "samples": 5,
        "width": 512,
        "height": 512,
    },
)


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

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

# 画像保存
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"]))
```

生成された画像は下記の5枚。

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/1c2f5565-7e13-21e9-2a7f-115f1a29f6bf.png">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/92abe346-2ff1-8517-3775-3ed3ba62d8b8.png">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/d2626616-e4e8-06f0-bf88-703e714fed7e.png">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/e5240a56-e449-0e0a-610a-dad8a07ce4b8.png">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/f9580438-8456-c4c3-d601-2c34550b8ed6.png">

## 雑コラを作る

同じ顔の画像を生成したいのであれば、同じ顔を貼り付ければいいのではないか。
そんな安易な考えが浮かびました。

ということで、[前の記事](https://qiita.com/nabata/items/d1f09a76e0ec69e34517)で生成した下記の画像を使います。

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/9740bd9c-9197-09f2-7a59-37e416a08b05.png">

ちなみに顔部分だけをくり抜くのは[rembg](https://github.com/danielgatis/rembg)を用いました。背景を取り除いて人物や動物だけを残してくれるライブラリです。

```shell-session
$ pip install rembg
Collecting rembg
  Downloading rembg-2.0.43-py3-none-any.whl (25 kB)
Collecting scipy
  Downloading scipy-1.10.1-cp310-cp310-
以下略
```

```shell-session
$ rembg i origin_img.png output_img.png
```

こちらの顔を生成済み画像にぺたぺたと貼っていきます。ここは人力でやりました。

| 元画像 | 雑コラ画像 |
| :---: | :---: |
| ![image_a_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/1c2f5565-7e13-21e9-2a7f-115f1a29f6bf.png) | ![image_a_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/407363a8-012f-7054-384c-aa1f4bf19af8.png) |
| ![image_b_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/92abe346-2ff1-8517-3775-3ed3ba62d8b8.png) | ![image_b_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/2405281f-66c1-bdf6-77ce-d3c6c96c99dd.png) |
| ![image_c_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/d2626616-e4e8-06f0-bf88-703e714fed7e.png) | ![image_c_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/84aa5e24-e931-e756-48ac-cf2419a819c8.png) |
| ![image_d_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/e5240a56-e449-0e0a-610a-dad8a07ce4b8.png) | ![image_d_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/cd2c7135-abc0-59b8-4cc3-81f6db6261a4.png) |
| ![image_e_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/f9580438-8456-c4c3-d601-2c34550b8ed6.png) | ![image_e_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/1f19fa49-a81f-18bd-5211-b523e8632924.png) |

あまりに雑すぎますが、まあ、今回は実験なのでよしとします。

## 雑コラを自然にしてみる

まず、あまりにひどい雑コラ具合を軽減させましょう。

生成済みのコラ画像を元として**image-to-image**で別画像を生成します。

先述のコードの**requests.post**のみ変えています。
**image_strength**は**0.7**として、元画像を強めに残しています。それと**style_preset**を**photographic**としています。
また、ここから先のコードでは**seed**で同一の値を指定することにします。

```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={
        "text_prompts[0][text]": "28-year-old-japanese-woman with black bobbed hair, realistic",
        "text_prompts[0][weight]": 1.0,
        "image_strength": 0.7,
        "style_preset": "photographic",
        "seed": 99999
    },
)
```

結果は下記です。左が元として使った雑コラ画像で、右がAPI経由で生成された画像です。
元が雑すぎて一部画像は特に首回りが不自然に思えますが、全体的に、かなり自然になりはしたでしょうか。コラが顔の向きとかもガン無視なので不自然さが残るのはやむなしですかね。

| 元画像 | 生成画像 |
| :---: | :---: |
| ![image_a_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/407363a8-012f-7054-384c-aa1f4bf19af8.png) | ![image_a_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/586dfee1-f0dc-37ac-5dca-e13f3317aaff.png) |
| ![image_b_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/2405281f-66c1-bdf6-77ce-d3c6c96c99dd.png) | ![image_b_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/37da2c3c-9981-1909-d586-07af2e4ab9e8.png) |
| ![image_c_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/84aa5e24-e931-e756-48ac-cf2419a819c8.png) | ![image_c_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/723b1873-c647-42da-8173-359505194d40.png) |
| ![image_d_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/cd2c7135-abc0-59b8-4cc3-81f6db6261a4.png) | ![image_d_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/ec248206-d916-a35f-a471-79125ab9548e.png) |
| ![image_e_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/1f19fa49-a81f-18bd-5211-b523e8632924.png) | ![image_e_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/6241a408-bfa0-6209-f662-a29421c73e4f.png) |

こうやって並べると気になるところもありますが、なんの前提知識もなしにこの生成画像5枚を「**これ、田中さん(仮名)っていう人の写真だよ**」と見せられたなら、田中さんを被写体とした5枚の写真として普通に受け入れてしまう気はします。4枚目だけ髪が長いから撮影時期が違うのかもしれないと思いつつ。

だって、現実に撮影した写真でも、いわゆる**奇跡の1枚**じゃないですが、同一人物だってわからないものが珍しくはないですし。

## イラスト風にしてみる

せっかくなので写真風ではなくイラスト風にしてみます。

これらの画像を元として、今度は**image_strength**を**0.5**、**style_preset**を**anime**として画像を生成しました。プロンプトから**realistic**を削除しています。
変更箇所は**data**のみです。

```Python
data={
    "text_prompts[0][text]": "28-year-old-japanese-woman with black bobbed hair",
    "text_prompts[0][weight]": 1.0,
    "image_strength": 0.5,
    "style_preset": "anime",
    "seed": 99999
},
```

結果は下記です。
まったくもって同じ顔ではないですが、ある程度は近しいのではないかとも思います。

| 元画像 | 生成画像 |
| :---: | :---: |
| ![image_a_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/586dfee1-f0dc-37ac-5dca-e13f3317aaff.png) | ![image_a_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/9f6272fb-3730-06c8-e206-8c4d90615c4c.png) |
| ![image_b_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/37da2c3c-9981-1909-d586-07af2e4ab9e8.png) | ![image_b_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/241119b8-02d8-d4eb-eb19-c6f3cf5d33d9.png) |
| ![image_c_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/723b1873-c647-42da-8173-359505194d40.png) | ![image_c_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/086cfc38-f0ca-a15a-7e58-a50107d8b69d.png) |
| ![image_d_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/ec248206-d916-a35f-a471-79125ab9548e.png) | ![image_d_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/195d622a-8838-e5e6-c058-e342b246d2be.png) |
| ![image_e_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/6241a408-bfa0-6209-f662-a29421c73e4f.png) | ![image_e_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/4851efe8-a17c-1ac9-23d0-69219d11e58b.png) |

## 絵柄を軽くしてみる

**moe illustration**を指定してみました。**moe**がそもそも通じるのか不明ではありますが。

```Python
data={
    "text_prompts[0][text]": "moe illustration",
    "text_prompts[0][weight]": 1.0,
    "image_strength": 0.4,
    "style_preset": "anime",
    "seed": 99999
},
```

結果は下記です。
工程を経すぎてよくわからなくなってきました。同じ顔のイラストを複数生成するという目的からは遠ざかりましたが、軽い絵柄にはなりましたかね。

| 元画像 | 生成画像 |
| :---: | :---: |
| ![image_a_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/9f6272fb-3730-06c8-e206-8c4d90615c4c.png) | ![image_a_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/53f7db43-af4b-ff5a-bf22-b33954e12566.png) |
| ![image_b_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/241119b8-02d8-d4eb-eb19-c6f3cf5d33d9.png) | ![image_b_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/6d31ffb7-52be-90b8-504b-5d6391ad3c6a.png) |
| ![image_c_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/086cfc38-f0ca-a15a-7e58-a50107d8b69d.png) | ![image_c_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/c6f984a0-8a2f-67c2-c86d-2c5ffdab5998.png) |
| ![image_d_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/195d622a-8838-e5e6-c058-e342b246d2be.png) | ![image_d_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/30b67a7b-bc9b-91d9-5304-83b355949117.png) |
| ![image_e_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/4851efe8-a17c-1ac9-23d0-69219d11e58b.png) | ![image_e_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/37fe95e0-dab4-491b-d383-4ac9fe0f69e4.png) |


## 最後に

丁寧にやればある程度はコントロールできそうだなとは感じました。
まあ、Stable Diffusionを使うメリットみたいなものは薄い方法だなとは思いつつですが。
ただただ楽しんでいます。

## 宣伝

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

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