Stablediffusion MacのローカルでControlnetを使った画像生成（img2img）

# はじめに
この記事では、pythonのdiffusersライブラリを使ってローカルで画像生成する方法について説明します
MacでStablediffusion webUIを使って画像生成はできましたが、flaskでサイトにしたいときにweUIだとできないので、この記事を書いています。
webサイトの作成等の別サービスとの連携を考えていないのであれば、webUIの方をお勧めします

# 環境構築
https://zenn.dev/michy/articles/102a0558096199

こちらを参考に環境構築すれば問題なく動作しました〜

# 作成するサイト概要
- 実写画像をアニメ画像のように変換できるサイト
    - controlnetでcanny画像を作成して変換
    - 生成される画像の精度はこだわらないです🙇
- Flaskで作成
    - デプロイはしません！
- Tailwindcssでデザイン
- jqueryでローディング画面作成

#### 完成版
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/cd3e0a11-f103-9289-0544-a07d48e315ca.png)


#### Github
https://github.com/naoya25/anime-image-flask

# ライブラリのインストール
```bash
python -V
>> Python 3.10.13
```
```bash
pip install diffusers
pip install controlnet_aux
pip install accelerate
pip install transformers
pip install omegaconf
```
めんどくさい方は、こちら↓でも大丈夫です
```tex:requirements.txt
Flask==3.0.0
googletrans==4.0.0rc1
torch==2.1.1
transformers==4.35.2
accelerate==0.25.0
scipy==1.11.4
diffusers==0.24.0
omegaconf==2.3.0
controlnet-aux==0.0.7
```
```bash
pip install -r requirements.txt
```
# モデルのダウンロード
`models`フォルダを作成します
こちらからダウンロード↓
https://huggingface.co/gsdf/Counterfeit-V3.0/resolve/main/Counterfeit-V3.0_fix_fp16.safetensors

ダウンロードできたら`models`内に入れておきます

# diffusersを動かしてみる(controlnet canny)

### 1. 準備
まず生成に用いる画像をテキトーに拾ってきます
（なんでもいいです）
拾ってきた画像を`static/images`フォルダを作成し、その中に入れておきます
これは、flaskで画像を生成するサイトを作るので、そのとき用に`static`内に入れておきます
生成用のファイル`run.py`を作成します
```text:フォルダ構成
./
├ models
│ └ Counterfeit-V3.0_fix_fp16.safetensors ←ダウンロードしたモデル
├ static
│ └ images
│ │ └ target.png ←拾ってきた画像、ファイル名はなんでもいい
└ run.py
```
現状こんな感じになってれば大丈夫です

### 2. 流れ
画像を生成していく流れは以下の通りです
1. 入力画像を受け取る
1. Canny画像を生成する
    あとで説明します
1. ControlnetのPipelineを準備
1. seed値設定
1. 画像生成して保存

こんな感じです！

### 3. 実装
実装していきましょう

##### Canny画像の生成
まず入力画像を受け取って、Canny画像を生成します
今回target.pngは事前にstablediffusion text2imgで生成しておいた画像を使います
(なければ普通に拾い画で大丈夫です)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/1c403b32-567a-1dd9-1ef8-726fb27bdcb4.png)


```python:run.py
from controlnet_aux import CannyDetector
from diffusers import StableDiffusionControlNetPipeline, ControlNetModel, DDIMScheduler
import torch
import random
from PIL import Image
# 使ってないライブラリもありますが、あとで使っていくので消さないように！


def create_anime_image(file_name: str) -> bool:
    original_image = Image.open(f"./static/images/{file_name}")

    # 入力画像の整形
    aspect_ratio = original_image.width / original_image.height
    if aspect_ratio > 1:
        new_width = 512
        new_height = int(new_width / aspect_ratio) - (int(new_width / aspect_ratio) % 8)
    else:
        new_height = 512
        new_width = int(new_height * aspect_ratio) - (int(new_height * aspect_ratio) % 8)
    init_image = original_image.resize((new_width, new_height))

    # Canny画像作成
    canny_detector = CannyDetector()
    canny_image = canny_detector(init_image)

    # Canny画像の保存
    canny_image.save("canny_image.png")

    return True


if __name__ == "__main__":
    file_name = "target.png" # 自分で用意したファイル名に変更
    create_anime_image(file_name=file_name)
```
入力画像の整形とCanny画像の作成をしています
入力画像の整形は、計算コストを減らすために、maxでも512pxまでにしてます
それに加えて、Stablediffusionは8の倍数の幅しか入力画像として受け取れないので、8で割ったあまりを引くことで8の倍数に整形してます
（めっちゃてきとーです）
実行するとルートディレクトリに`canny_image.png`が保存されています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/440aa1a7-0fae-184c-ca9a-4c8664e096f9.png)

このようにCanny画像というのは線で書かれたような写真になり、もとの画像の形を維持して新しい画像が生成できます

##### Controlnetの準備
```python:run.py
def create_anime_image(file_name: str) -> bool:
    # 略

    # ControlnetのPipelineの準備
    controlnet = ControlNetModel.from_pretrained("lllyasviel/sd-controlnet-canny").to("mps")
    pipe = StableDiffusionControlNetPipeline.from_single_file(
        "./models/Counterfeit-V3.0_fix_fp16.safetensors",
        controlnet=controlnet,
        safety_checker=lambda images, **kwargs: (images, None),
    ).to("mps")

    # スケジューラ、最適化、
    # pipe.safety_checker = lambda images, **kwargs: (images, None)
    pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config)
    pipe.enable_attention_slicing()

    return True

# 略
```
ここで`safety_checker`とか他の設定もできます
`safety_checker`は18禁画像、露出度の高い画像が生成されないようにするフィルターです
そのような画像が生成されてしまった場合は黒塗り画像が出てきます

##### 画像生成
```python:run.py
def create_anime_image(file_name: str) -> bool:
    # 略

    seed = random.randrange(0, 4294967295, 1)
    generator = torch.Generator("mps").manual_seed(seed)

    # 画像生成
    image = pipe(
        prompt="anime style",  # アニメ系の画像にしたい
        negative_prompt="(worst quality:1.4), (low quality:1.4), (monochrome:1.3), nsfw, NSFW",
        num_inference_steps=20,
        generator=generator,
        image=canny_image,
    ).images[0]

    # 画像保存
    image.save("./static/images/result.png")
    print(f"Saved to result.png  seed: {seed}")

    return True

# 略
```

##### ここまでのコード
```python:run.py
from controlnet_aux import CannyDetector
from diffusers import StableDiffusionControlNetPipeline, ControlNetModel, DDIMScheduler
import torch
import random
from PIL import Image


def create_anime_image(file_name: str) -> bool:
    try:
        original_image = Image.open(f"./static/images/{file_name}")

        # 入力画像の整形
        aspect_ratio = original_image.width / original_image.height
        if aspect_ratio > 1:
            new_width = 512
            new_height = int(new_width / aspect_ratio) - (int(new_width / aspect_ratio) % 8)
        else:
            new_height = 512
            new_width = int(new_height * aspect_ratio) - (int(new_height * aspect_ratio) % 8)
        init_image = original_image.resize((new_width, new_height))

        # Canny画像作成
        canny_detector = CannyDetector()
        canny_image = canny_detector(init_image)

        # Canny画像の保存
        canny_image.save("./static/images/canny_image.png")

        # ControlnetのPipelineの準備
        controlnet = ControlNetModel.from_pretrained("lllyasviel/sd-controlnet-canny").to("mps")
        pipe = StableDiffusionControlNetPipeline.from_single_file(
            "./models/animePastelDream_softBakedVae.safetensors",
            controlnet=controlnet,
            safety_checker=lambda images, **kwargs: (images, None),
        ).to("mps")

        # スケジューラ、最適化、
        # pipe.safety_checker = lambda images, **kwargs: (images, None)
        pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config)
        pipe.enable_attention_slicing()

        # seed値の設定
        seed = random.randrange(0, 4294967295, 1)
        generator = torch.Generator("mps").manual_seed(seed)

        # 画像生成
        image = pipe(
            prompt="anime style",  # アニメ系の画像にしたい
            negative_prompt="(worst quality:1.4), (low quality:1.4), (monochrome:1.3), nsfw, NSFW",
            num_inference_steps=20,
            generator=generator,
            image=canny_image,
        ).images[0]

        # 画像保存
        image.save("./static/images/result.png")
        print(f"Saved to result.png  seed: {seed}")
    except Exception as e:
        return False
    return True


if __name__ == "__main__":
    file_name = "target.png"
    create_anime_image(file_name=file_name)
```
実行すると画像が生成され、`result.png`に保存されていると思います！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/0277be3a-17bc-10ed-5f36-2318cafdf825.png)

# 別のモデルも試してみた
「Stablediffusion モデル　おすすめ」とかでググればいっぱい出てきました
僕は[こちら](https://civitai.com/models/23521/anime-pastel-dream)のAnime Pastel Dreamというモデルを試してみました
`models/animePastelDream_softBakedVae.safetensors`
あとは`run.py`のpathを変えてあげればいけます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/be25d00c-cae8-adc8-1eac-6d13e18fe827.png)
雰囲気変わりましたね〜〜

# Flask側
ここからは、webアプリケーション内で、画像生成を行っていこうと思います！
↓Newって書いてるファイル/フォルダは新しく作成してください
```text:フォルダ構成
./
├ models
│ └ Counterfeit-V3.0_fix_fp16.safetensors
│ └ animePastelDream_softBakedVae.safetensors
├ static
│ ├ images
│ │ └ ...
│ ├ style ←New
│ │ └ loading.css ←New
├ templates ←New
│ ├ base.html ←New
│ ├ index.html ←New
│ └ result.html ←New
├ app.py ←New
└ run.py
```
### やること
- index.htmlで画像ファイルを受け取る
- diffusersでimg2img画像生成
- result.htmlに生成した画像を表示
- デザイン・ローディング画面作成

```python:app.py
from flask import Flask, request, render_template, jsonify
from run import create_anime_image

app = Flask(__name__)


@app.route("/")
def index():
    return render_template("index.html")


@app.route("/result", methods=["GET", "POST"])
def result():
    if request.method == "GET":
        return render_template("index.html")
    elif request.method == "POST":
        file = request.files["image"]
        file.save(f"./static/images/{file.filename}")
        return render_template("result.html", file_name=file.filename)


@app.route("/generate_image", methods=["POST"])
def generate_image():
    file_name = request.form["file_name"]
    result_bool = create_anime_image(file_name=file_name)
    if result_bool:
        return jsonify({"file_name": file_name})
    else:
        return jsonify({"error": "Error"})


if __name__ == "__main__":
    app.run(debug=True)
```
```html:base.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Anime Character</title>
    <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
    <script src="https://cdn.tailwindcss.com"></script>
    {% block head %}{% endblock %}
  </head>
  <body class="text-center flex flex-col min-h-screen">
    <header>
      <h1 class="text-[2em]">{% block header %}Flask App{% endblock %}</h1>
    </header>
    <main>{% block content %}{% endblock %}</main>
    <footer class="mt-auto">
      <p>&copy; 2023/12 GeekSalon. All rights reserved.</p>
    </footer>
  </body>
</html>
```
jqueryとtailwind入れてます
```html:index.html
{% extends 'base.html' %}
{% block header %}実写画像をまるでアニメに！{% endblock %}

{% block content %}
<h2 class="text-[1.5em]">Index Page</h2>
<form action="/result" method="post"  enctype="multipart/form-data">
  <label for="image">変換したい画像: </label>
  <input type="file" id="image" class="w-[300px] border-2" name="image" required></input><br>

  <input type="submit" value="送信" />
</form>
<p class="text-red-500">※生成には5分ほどかかります</p>
{% endblock %}
```
```html:result.html
{% extends 'base.html' %} {% block head %}
<link
  rel="stylesheet"
  href="{{ url_for('static', filename='style/loading.css') }}"
/>
{% endblock %} {% block header %}実写画像をまるでアニメに！{% endblock %} {%
block content %}
<h2 class="text-[1.5em]">Result Page</h2>
<div id="loading-screen">
  <div id="loading-spinner" class="m-10">生成中...</div>
  <div id="loading-time"><span id="loading-duration">0.00</span> s</div>
</div>
<div id="result-body" class="text-center" hidden>
  <div class="images flex">
    <img
      id="original-image"
      class="mx-auto w-auto h-[500px]"
      src="{{ image_path }}"
      alt="Original Image"
    />
    <img
      id="canny-image"
      class="mx-auto w-auto h-[500px]"
      src="{{ image_path }}"
      alt="Canny Image"
    />
    <img
      id="result-image"
      class="mx-auto w-auto h-[500px]"
      src="{{ image_path }}"
      alt="Result Image"
    />
  </div>
  <p class="text-red-500">※画像が黒塗りで出力されることがあります</p>
  <a href="/">to index page</a>
</div>
<script>
  // ローディング画面、画像生成の非同期処理
  $(document).ready(function () {
    const startTime = new Date().getTime();
    $('#loading-screen').show();

    // ローディング時間を計算
    const updateLoadingTime = () => {
      var currentTime = new Date().getTime();
      var elapsedSeconds = (currentTime - startTime) / 1000;
      $('#loading-duration').text(elapsedSeconds.toFixed(2));
    }
    const updateInterval = setInterval(updateLoadingTime, 100);

    $.ajax({
      type: 'POST',
      url: '/generate_image',
      data: { file_name: {{ file_name | tojson }} },
      success: function (data) {
        clearInterval(updateInterval);
        $('#loading-screen').hide();
        $('#original-image').attr('src', `./static/images/${data.file_name}`);
        $('#canny-image').attr('src', `./static/images/canny_image.png`);
        $('#result-image').attr('src', `./static/images/result.png`);
        $('#result-body').show();
      },
      error: function (error) {
        console.error(error);
      }
    });
  });
</script>
{% endblock %}
```
```css:loading.css
#loading-spinner {
  animation: rotateAnimation 3s linear infinite;
}

@keyframes rotateAnimation {
  0% {
    transform: rotate(0deg);
  }
  40% {
    transform: rotate(0deg);
  }
  55% {
    transform: rotate(180deg);
  }
  70% {
    transform: rotate(360deg);
  }
  85% {
    transform: rotate(540deg);
  }
  100% {
    transform: rotate(720deg);
  }
}
```
これで終わりです！
`python app.py`で確かめてみましょう！

index page
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/47c008f1-c4dc-f650-b788-e727327feda6.png)
result page (loading)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/486375b7-2508-ef0a-65b1-4593414a5194.png)
回転したり、タイム表示したりしてみました
(生成に100秒ほどかかりました)

result page
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2586059/d0c598a9-f195-832f-d216-7de8ba896bc8.png)

# 参考記事
https://note.com/npaka/n/nc8b459e5fe85

https://note.com/npaka/n/n06b9ca7994a4

https://huggingface.co/docs/diffusers/main/en/api/pipelines/controlnet

# 小話
はじめはバックエンドでこのflaskを動かし、画像生成し、フロントエンドをReactで作成して、リリースまでしようと考えていたのですが、ローカルでは実現できたのですが、このバックエンドをリリースしようと思うと、モデルがめちゃめちゃ重たいのでお金がめっちゃかかるっぽいです
そんなお金かかるなら僕の趣味レベルなのでリリースは断念しました〜〜
Herokuなら月に数万円かかるみたいです

以上！
ここまでお読みいただき感謝
しぇしぇ〜