Claudeの画像認識をUnityで実装する

UnityからAIを使用する際、OpenAI APIを使うケースが現状最も多く見られます。
が、今回はあえてClaudeを使って画像認識を実装した話を書きます!

なぜなら、OpenAI API で「画像の人物の表情を分析して」と聞くと、頑なに拒否されたためです。。
しかしClaudeでは出来ました!
似たようなケースは他にもありそうなので、複数のAIを組み込んでおくと幅が広がりそうですね。

まずはコードを貼っちゃいます

リクエストおよびレスポンスデータを定義するためのクラス群

using System.Collections.Generic;
using System;
using Newtonsoft.Json;


[Serializable]
public class ClaudeMessage
{
    public string role;
    public List<Content> content;
}

[Serializable]
public class Content
{
    public string type;
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string text;  // Textを送る際に使用

    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public Source source;  // Imageを送る際に使用
}

[Serializable]
public class Source
{
    public string type = "base64";
    public string media_type;
    public string data;
}

[Serializable]
public class ClaudeRequest
{
    public string model;
    public string system;
    public List<ClaudeMessage> messages;
    public int max_tokens;
}

[Serializable]
public class ClaudeResponse
{
    public string id;
    public ResponseContent[] content;
    public Usage usage;
}

[Serializable]
public class ResponseContent
{
    public string text;
}

[Serializable]
public class Usage
{
    public int input_tokens;
    public int output_tokens;
}

APIリクエストを管理し、テキストと画像データをAPIに送信するクラス

using System;
using Cysharp.Threading.Tasks;
using UnityEngine.Networking;
using UnityEngine;
using System.Collections.Generic;
using Newtonsoft.Json;
using System.Threading;


public class ClaudeApiConnector : MonoBehaviour
{
    [SerializeField] private string _apiKey;
    [SerializeField] private int _maxTokens = 1024;

    private readonly string _apiUrl = "https://api.anthropic.com/v1/messages";
    private readonly string _modelName = "claude-3-5-sonnet-20240620";
    private List<ClaudeMessage> _messageHistory = new List<ClaudeMessage>();


    /// <summary>
    /// Claude APIを呼び出す。TextとTexture2Dのどちらか、または両方を引数で渡して使用。
    /// </summary>
    /// <returns></returns>
    public async UniTask<string> CallApi(string prompt, string text = null, Texture2D texture2D = null, CancellationToken token = default)
    {
        if (text == null && texture2D == null)
        {
            Debug.LogError("Text and Texture2D are null.", gameObject);
            return "unknown";
        }

        ClaudeMessage[] messages = CreateMessages(text, texture2D);
        _messageHistory.AddRange(messages);  // 入力の履歴を保持

        return await ExecuteApiRequest(prompt, token);
    }

    private ClaudeMessage[] CreateMessages(string text = null, Texture2D texture2D = null)
    {
        List<Content> contents = new List<Content>();

        if (texture2D != null)
        {
            Content imageContent = new Content
            {
                type = "image",  // type を設定
                source = new Source
                {
                    type = "base64",
                    media_type = "image/png",
                    data = Convert.ToBase64String(texture2D.EncodeToPNG())
                }
            };
            contents.Add(imageContent);
            Debug.Log("Added images to messages to Claude API.", gameObject);
        }

        if (!string.IsNullOrEmpty(text))
        {
            Content textContent = new Content
            {
                type = "text",
                text = text
            };
            contents.Add(textContent);
            Debug.Log("Added Text to messages to Claude API : " + text, gameObject);
        }

        ClaudeMessage[] messages = new ClaudeMessage[]
        {
            new ClaudeMessage
            {
                role = "user",
                content = contents
            }
        };

        return messages;
    }

    private async UniTask<string> ExecuteApiRequest(string prompt, CancellationToken token = default)
    {
        Debug.Log("Execute a request to Claude API.", gameObject);

        ClaudeRequest chatBody = new ClaudeRequest
        {
            system = prompt,
            model = _modelName,
            messages = _messageHistory,
            max_tokens = _maxTokens
        };
        string body = JsonConvert.SerializeObject(chatBody);
        byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(body);

        using UnityWebRequest request = new UnityWebRequest(_apiUrl, UnityWebRequest.kHttpVerbPOST);
        request.uploadHandler = new UploadHandlerRaw(jsonToSend);
        request.downloadHandler = new DownloadHandlerBuffer();
        request.SetRequestHeader("x-api-key", _apiKey);
        request.SetRequestHeader("anthropic-version", "2023-06-01");
        request.SetRequestHeader("Content-Type", "application/json");

        await request.SendWebRequest().ToUniTask(cancellationToken: token);
        if (request.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError("API request failed : " + request.error);
            return null;
        }

        string response = request.downloadHandler.text;
        ClaudeResponse responseJson = JsonConvert.DeserializeObject<ClaudeResponse>(response);
        string output = responseJson.content[0].text;

        Debug.Log("Claude API response : " + output, gameObject);
        return output;
    }


#if UNITY_EDITOR
    [UnityEditor.CustomEditor(typeof(ClaudeApiConnector))]
    private class ClaudeApiConnectorTest : UnityEditor.Editor
    {
        private const string PromptKey = "ClaudeApiPrompt";
        private const string TextKey = "ClaudeApiText";
        [SerializeField] private string _prompt;
        [SerializeField] private string _text;
        [SerializeField] private Texture2D _texture;
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            UnityEditor.EditorGUILayout.Space(10);
            UnityEditor.EditorGUILayout.LabelField("--- Debug ---");

            _prompt = UnityEditor.EditorPrefs.GetString(PromptKey, _prompt);
            _text = UnityEditor.EditorPrefs.GetString(TextKey, _text);

            _prompt = UnityEditor.EditorGUILayout.TextField("Prompt", _prompt);
            _text = UnityEditor.EditorGUILayout.TextField("Text", _text);
            _texture = (Texture2D)UnityEditor.EditorGUILayout.ObjectField("Texture2D", _texture, typeof(Texture2D), allowSceneObjects: false);

            UnityEditor.EditorPrefs.SetString(PromptKey, _prompt);
            UnityEditor.EditorPrefs.SetString(TextKey, _text);

            if (target is not ClaudeApiConnector system) return;

            if (GUILayout.Button("Call Claude API"))
            {
                system.CallApi(_prompt, text: _text, texture2D: _texture).Forget();
                _text = null;
                _texture = null;
            }
        }
    }
#endif
}

使用方法

UniTask と Newtonsoft.Json を事前にUnityプロジェクトにインポートしておきます。

シーン上に空のGameObjectを作成し、そこにClaudeApiConnectorをアタッチし、そのインスペクターを設定していきます。
・API Key と プロンプトを入力。
・「Text」または「Texture2D」または「Text・Texture2D の両方」を設定。
 ※ 画像はpng形式で、Import Settings の Read/Write にチェックを入れた状態にしておく。

・シーン再生中に、インスペクターのボタン「Call Claude API」を押すと実行できます。

実装のメモ

・ClaudeApiConnector の CreateMessages にて、media_type = “image/png”としていますが、これを “image/jpeg” にすれば jpeg の画像を送れます。

・画像は Base64 にエンコードして送っています。

[Serializable]
public class Content
{
    public string type;
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string text;  // Textを送る際に使用

    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public Source source;  // Imageを送る際に使用
}

・↑ここの定義で
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
とすることで、Claudeへ送るコンテンツの Text か Image を省略した場合はJSONシリアライズ時に省略されるようにしました。これにより、Input内容が柔軟に変更できる仕様になっています。

目次

おわりに

ゲーム画面のスクショを送ってみたり、Webカメラのスクショを送ってみたり、色々実装できそうですね!
今度は Germini Multimodal Live API を調べているので、形になったらまた書こうと思います。

この記事が気に入ったら
いいねしてね!

よかったらシェアしてね!
  • URLをコピーしました!
目次