UnityでStable Diffusion APIを利用する

## はじめに
この記事はUnityアプリ内でStableDiffusionAPIを利用してみることができたのでその方法を整理してみた記事になります。
具体的にはスクリーンショットをもとにStableDiffusionで画像を生成し、その画像をUnity上で表示するというものになります。

2022年当時にStableDiffusionが話題になってからUnityでの画像生成ができないか色々と試していました。
最近改めて調べてみたら意外と簡単にできたので工夫次第で応用の幅が広がるかも...と思ってます。

Stability.aiのAPIを利用してimage-to-imageの手法を簡単に実装しています。

※動作確認を目的にしたため、相互参照など一部雑なコーディングが含まれていますことご容赦いただければと思います...

## プロジェクトのセットアップ
### 開発環境
Unity 2022.3.20f1
Mac OSX 14.6.1

画像生成の際にJSON形式でのデータ通信が必要となるため、
Newtonsoft.Jsonパッケージを利用しています。
Windows > PackageManager > Add Package from git URL　から
com.unity.nuget.newtonsoft-json
を入力しパッケージをインストールしました。

### 利用したサービス
https://platform.stability.ai/
Stability.aiのAPIを使用するにはクレジットが必要です。
初回登録時は無料利用分で少しだけクレジットが貰えます。
画像生成1回ごとにクレジットを消費します。
（今回示すスクリプトではおおむね1回で0.2〜0.4クレジットを消費するようです。0.3〜0.6円ぐらいに相当）
画像サイズやプロンプトの指定内容によって消費クレジットは異なります。

あらかじめアカウントを登録しAPIキーを発行する必要があります。

## スクリプトの詳細解説
下記の流れで処理を進めました。
1.スクリーンショットをキャプチャ
2.バイトに変換
3.APIリクエスト
4.画像生成
5.表示
6.保存


### SDStableDiffusionAPI.cs
APIとの通信を担うメインのスクリプトになります。
画像を送信して生成された画像を受け取るために、Stable Diffusion APIとのやり取りを処理します。

プロンプトや各種設定はこのスクリプトに含めています。
またstable-diffusion-v1-6の箇所は適宜別のengine_idを指定してモデルを変えてみることができます。
詳細は下記に記載があり確認しながら実装を試しました。
https://platform.stability.ai/docs/api-reference

image-to-imageエンドポイントでは Content-Typeをmultipart/form-data 形式に設定する必要があります。
そのためパラメータの指定にはform.AddFieldを用いる必要がありました。

発行したAPIキーを入力することで利用が可能になります。

```C#:SDStableDiffusionAPI.cs
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using System.Collections.Generic;
using Newtonsoft.Json;

public class SDStableDiffusionAPI : MonoBehaviour
{
    private string apiUrl = "https://api.stability.ai/v1/generation/stable-diffusion-v1-6/image-to-image";  // Example endpoint
    private string apiKey = "sk-yourAPIKey";
    public SDStableDiffusionManager stableDiffusionManager;
    [HideInInspector]public string generatedBase64Image; //GUIのエラーを回避するためHideInInspectorにしておく

    public IEnumerator GenerateImage(byte[] imageBytes)
    {
        // multipart/form-data のためのフォームデータを作成
        WWWForm form = new WWWForm();

        // テキストプロンプトを追加
        form.AddField("text_prompts[0][text]", "monochrome, grayscale"); //ここで具体的なプロンプトを指定。"masterpiece, best quality"など。
        form.AddField("text_prompts[0][weight]", "1.0");

        // 初期画像を追加 (バイナリ形式の画像データ)
        form.AddBinaryData("init_image", imageBytes, "image.png", "image/png");

        // init_image_mode と image_strength を指定
        form.AddField("init_image_mode", "IMAGE_STRENGTH");
        form.AddField("image_strength", "0.25");

        // その他のパラメータを追加
        form.AddField("cfg_scale", "7");
        form.AddField("sampler", "K_DPMPP_2M");
        form.AddField("samples", "1");
        form.AddField("steps", "30");

        // HTTPリクエストを作成
        UnityWebRequest request = UnityWebRequest.Post(apiUrl, form);
        request.SetRequestHeader("Authorization", "Bearer " + apiKey);

        // リクエスト送信
        yield return request.SendWebRequest();

        // エラーハンドリング
        if (request.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("Error: " + request.error);
            Debug.LogError("Response Code: " + request.responseCode);
            Debug.LogError("Error Body: " + request.downloadHandler.text);
        }
        else
        {
            string jsonResponse = request.downloadHandler.text;
            Debug.Log("Response: " + jsonResponse);

            // レスポンスのパース
            StableDiffusionResponse response = JsonConvert.DeserializeObject<StableDiffusionResponse>(jsonResponse);

            if (response.artifacts != null && response.artifacts.Count > 0)
            {
                string base64Image = response.artifacts[0].base64;
                generatedBase64Image = base64Image;
                // Managerに渡して表示
                stableDiffusionManager.DisplayGeneratedImage(generatedBase64Image);
            }
        }
    }
}

[System.Serializable]
public class StableDiffusionResponse
{
    public List<Artifact> artifacts;
}

[System.Serializable]
public class Artifact
{
    public string base64;
}
```


### SDScreenshotManager.cs
ゲームビューのスクリーンショットをキャプチャします。
```C#:SDScreenshotManager.cs
using UnityEngine;

public class SDScreenshotManager : MonoBehaviour
{
    public void CaptureScreenshot()
    {
        string screenshotPath = Application.persistentDataPath + "/screenshot.png";
        ScreenCapture.CaptureScreenshot(screenshotPath);
        Debug.Log("Screenshot saved to: " + screenshotPath);
    }
}

```

### SDImageConverter.cs
キャプチャしたスクリーンショットをAPI送信用にバイト配列に変換します。
```C#:SDImageConverter.cs
using System.IO;
using UnityEngine;

public class SDImageConverter : MonoBehaviour
{
    public byte[] ConvertScreenshotToByteArray(string path)
    {
        byte[] imageBytes = File.ReadAllBytes(path);
        return imageBytes;
    }
}

```

### SDImageDisplay.cs
Unity UIに生成された画像を表示します。
```C#:SDImageDisplay.cs
using UnityEngine;
using UnityEngine.UI;
using System;

public class SDImageDisplay : MonoBehaviour
{
    public RawImage displayImage;

    public void DisplayGeneratedImage(string base64Image)
    {
        byte[] imageBytes = Convert.FromBase64String(base64Image);
        Texture2D texture = new Texture2D(2, 2);
        texture.LoadImage(imageBytes);

        displayImage.texture = texture;
    }
}

```

### SDImageSaver.cs
生成された画像を保存します。
```C#:SDImageSaver.cs
using System;
using System.IO;
using UnityEngine;

public class SDImageSaver : MonoBehaviour
{
    public void SaveGeneratedImage(string base64Image)
    {
        // Base64文字列をバイト配列に変換
        byte[] imageBytes = Convert.FromBase64String(base64Image);

        // 保存先のパスを設定
        string filePath = Application.persistentDataPath + "/generatedImage.png";

        try
        {
            // ファイルにバイト配列を書き込む
            File.WriteAllBytes(filePath, imageBytes);
            Debug.Log("Image saved to: " + filePath);
        }
        catch (Exception e)
        {
            Debug.LogError("Failed to save image: " + e.Message);
        }
    }
}

```

### SDStableDiffusionManager.cs
ここまで登場したスクリプトを統合し、
スクリーンショットのキャプチャ、送信、生成された画像の受信、保存を調整します。

Update()に示すとおりスペースキーを押下することで、
スクリーンショットの撮影および後続の処理がスタートします。

```C#:SDStableDiffusionManager.cs
using System.Collections;
using UnityEngine;

public class SDStableDiffusionManager : MonoBehaviour
{
    public SDScreenshotManager screenshotManager;
    public SDImageConverter imageConverter;
    public SDStableDiffusionAPI stableDiffusionAPI;
    public SDImageDisplay imageDisplay;
    public SDImageSaver imageSaver;

    void Start()
    {
        stableDiffusionAPI.stableDiffusionManager = this;
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log("Start Generate Image From Screenshot");
            GenerateImageFromScreenshot();
        }
    }

    public void GenerateImageFromScreenshot()
    {
        string screenshotPath = Application.persistentDataPath + "/screenshot.png";
        
        // Capture screenshot
        screenshotManager.CaptureScreenshot();

        // Start coroutine to process after a delay (to allow the screenshot to be saved)
        StartCoroutine(ProcessScreenshot(screenshotPath));
    }

    private IEnumerator ProcessScreenshot(string screenshotPath)
    {
        yield return new WaitForSeconds(1);  // スクリーンショットの保存を確実に待つ

        // バイト配列に変換
        byte[] imageBytes = imageConverter.ConvertScreenshotToByteArray(screenshotPath);

        // StableDiffusionAPIに送信しレスポンスをハンドリング
        yield return stableDiffusionAPI.GenerateImage(imageBytes);

        // APIから取得した生成画像のBase64データを保存
        if (stableDiffusionAPI.generatedBase64Image != null)
        {
            imageSaver.SaveGeneratedImage(stableDiffusionAPI.generatedBase64Image);
        }
    }

    // 画像を表示
    public void DisplayGeneratedImage(string base64Image)
    {
        imageDisplay.DisplayGeneratedImage(base64Image);
    }
}
```

## 実装フロー
Unityシーンをセットアップし上記6点のスクリプトを任意のオブジェクトにアタッチします。
また生成された画像を表示するためRawImageオブジェクトを作成しておきます。
![スクリーンショット 2024-09-21 12.20.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3554165/06297a89-d9e0-f43d-5701-6649de17817a.png)



## 動作例
Playボタン再生後、スペースキーを押すことで
スクリーンショットが撮影され画像の生成から保存の処理が進みます。

今回は自前で用意したVRMキャラクターを撮影しスクショを渡してみました。
プロンプトを色々と試せば面白い画像が出せそうな予感がしてます。
![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3554165/dafc9af4-b074-a0ec-6cfd-28100d955c55.gif)

元のスクショ画像
つるっとした3Dモデルです
![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3554165/1c337de0-ba84-7d3a-03be-38647c72bc4d.png)

img2imgの画像
なんかいい感じのインディーゲームにいそうなテイストになりました。
![generatedImage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3554165/0e8cb0f5-06eb-d9f9-83a4-2a015755d27d.png)


APIを試すのが目的だったのでこの試作のクオリティはほどほどにしています。
いい絵が出せるプロンプトのアタリがついたらstepsを増やしてブラッシュアップしたいですね。

なおスクリーンショットを撮った際に、
Ignoring depth surface load action as it is memoryless
というwarningが出ることを確認していますが、今回の動作には影響しないので無視しています。

## 画像データをテキスト形式に変換する
SDStableDiffusionAPI.csのDebug.Log("Response: " + jsonResponse);によると、
下記のようなデバッグログが確認できます。
![スクリーンショット 2024-09-21 12.05.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3554165/ef2e947e-be23-939b-49fa-21a18937d360.png)

これはAPIから返されたデータがBase64エンコードされた画像データであることを示しています。
Base64はバイナリデータ（例えば画像）をテキストに変換する方法の一つです。
出力されたBase64文字列をデコードすることで実際の画像データを表示することができます。

バイナリデータをそのまま通信のためのテキスト形式として送信することはできないので、
Base64を使ってエンコードしたテキストを用いるという処理が今回のスクリプトで登場しています。
（自分はこの箇所でつまづいたので「レスポンスは得られたのに画像が出ない...」という状況に苦労しました。）

## まとめ
StableDiffusionをUnityで利用してみる方法についてまとめてみました。
今回はレガシーの生成AIモデルを利用したこともありこのままだと用途は限られそうですが、
工夫次第で面白いことができそうです...!