Unity 内のエージェントに生成 AI で「視覚」を付与する実験
まずはじめに、実験結果が下記である。
エージェントの視界に映る内容をテキストで説明できている。
仕組みとしては単純で、エージェント頭部に設置したカメラ映像から画像を切り出して BLIP(画像 → テキスト変換可能な生成 AI)に食わせた。
BLIP の出力文は英語かつ拙いことがあるので、ChatGPT によってスマートな日本語に変換した上で画面上に出力している。
Unity 歴半月初心者なので、自身のメモを兼ねて実装方法を振り返る。
なお、Unity エディタのバージョンは 2021.3.24 を利用している。
全体アーキテクチャ
まず先に全体構成から。Unity と Python サーバ(http)をローカルで動作させている。
Unity
3D(URP)をベースにプロジェクトを作成した。
Main Camera:引きで画面全体を映す初期カメラ
Player:ヒューマノイド。Starter Assets を利用
AgentViewImageCamera:エージェントの視覚用のカメラ
Directional Light:プロジェクト初期のモノ
Ground:地面(Plane)
〜Image:ネコ等の画像
UI / EventSystem:UI要素
AgentViewImage:画面左下に表示のエージェントの視界
CameraCaptureButton:Capture ボタン
AgentCommentText:エージェントのコメント文
一部抜粋しつつ導入方法を記載する。
Player の設置
定番の Starter Assets を利用した。
Package Manager から導入し、PlayerArmature の Prefabs を利用した。
基本的には Starter Assets デフォルトのまま利用しているが頭部にカメラ(AgentViewImageCamera)を追加している。
〜Image の設置
画像自体はぱくたそサイトよりダウンロードした。
3D Object の Quad に、画像を適用したマテリアルを貼り付けた。
AgentViewImage の設置
Render Texture にカメラ映像を直接貼り付けている。
カメラ画像を取得
カメラ画像を取得するスクリプトをカメラに適用した。
// かなり手続き的な処理なのがちょっと哀しい...
async UniTask<string> Capture()
{
await UniTask.Yield();
// 現在の状態を退避
RenderTexture currentRenderTexture = RenderTexture.active;
// 画像を書き込むためのRender Textureをメモリ上に作成
RenderTexture renderTexture = new RenderTexture(captureWidth, captureHeight, 24);
captureCamera.targetTexture = renderTexture;
RenderTexture.active = renderTexture;
// カメラ画像をRender Textureに書き込み
captureCamera.Render();
Texture2D image = new Texture2D(renderTexture.width, renderTexture.height);
image.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
image.Apply();
// 元に戻す
captureCamera.targetTexture = currentRenderTexture;
RenderTexture.active = currentRenderTexture;
// Render Textureを破棄
Destroy(renderTexture);
byte[] imageBytes = image.EncodeToPNG();
return await client.Analyze(imageBytes);
}
非同期な処理なので UniTask を導入した。
公式リポジトリに導入方法が書いてある。
画像をサーバに送信
クライアントとなるクラスを作成しサーバに画像を投げられるようにした。
UnityWebRequest 以外のもっと効率の良い書き方は間違いなくあると思いつつ、Unity / C# 初心者すぎるのでひとまずお茶を濁している。
public class BlipClient
{
const string analyzeAPI = "/images/analyze";
const string pingAPI = "/ping";
string baseURL;
public class Response
{
public string message;
}
public BlipClient(string url)
{
baseURL = url;
}
public async UniTask<string> Analyze(byte[] imageBytes)
{
UnityWebRequest www = new UnityWebRequest($"{baseURL}{analyzeAPI}", UnityWebRequest.kHttpVerbPOST);
www.uploadHandler = new UploadHandlerRaw(imageBytes);
www.uploadHandler.contentType = "application/octet-stream";
www.downloadHandler = new DownloadHandlerBuffer();
await www.SendWebRequest();
if (www.result != UnityWebRequest.Result.Success)
{
return www.error;
}
else
{
string json = www.downloadHandler.text;
Response response = JsonUtility.FromJson<Response>(json);
return Util.UnicodeToString(response.message);
}
}
public async UniTask Ping()
{
UnityWebRequest www = new UnityWebRequest($"{baseURL}{pingAPI}", UnityWebRequest.kHttpVerbGET);
await www.SendWebRequest();
Debug.Log($"Received: {www.result.ToString()}");
}
}
Python サーバ
コードは全て blip-etude に置いている。
画像を受け取りテキストに変換(BLIP)
BLIP の導入については環境構築まで丁寧に書かれたブログがあったので参考にした。
ブログからほぼ変更点はないが、出力文の min_length / max_length指定をやめた(埋めるために変な文章になる場合があったため)のと、
outputs = self.blipClient.model.generate(**inputs)
Apple Silicon の Mac 利用のため MPS を指定できるようにした。
class Device(enum.Enum):
CPU = "cpu"
CUDA = "cuda"
MPS = "mps"
テキストを整形(ChatGPT)
BLIP が吐き出す文章は英語 and たまに変なこともあるので日本語に変換し整形するのを ChatGPT に任せた。
大した実装ではないため LangChain 等使わず生の OpenAI の API を叩いている。
置かれている状況と、日本語に翻訳して欲しい旨をシステムプロンプトにて伝えている。
class OpenAiClient(openai.ChatCompletion):
model: str
top_p: int
def __init__(self, model: str, top_p: int):
self.model = model
self.top_p = top_p
def completion(self, prompt: str):
response = self.create(
model=self.model,
top_p=self.top_p,
messages=[
{
"role": "system",
"content": """
あなたはUnityの中で動く二足歩行のエージェントです。
あなたの頭部に設置したカメラ映像を取得し、
image-to-textモデルのBLIPにてテキストに変換しています。
これからそのテキストをあなたに送ります。
英語のテキストを日本語に翻訳し状況を説明してください。
""",
},
{"role": "user", "content": prompt},
],
)
return response
おわりに
BLIP と ChatGPT によって Unity 内のエージェントに視覚を与えた。
他のモデル(ex. BLIP-2)を利用したり、発展的な内容に取り組んでいきたい。