AzureのComputer Vision APIでSecond LifeのSSに写っているものを判定してみる

Azureの[Computer Vision API](https://azure.microsoft.com/ja-jp/services/cognitive-services/computer-vision/)は、普通、写真など実在のものを判定するために使うのでしょうが、それをSecond Life仮想空間の映像に使ったらちゃんと判定してくれるのか、という疑問が湧き、やってみることにしました。

結果だけであれば、画像をSecond Lifeからダウンロードして、Postmanなどを使えば、普通にできます。今回は、LSL(Second Lifeのスクリプト言語)を使って、仮想空間内でスクリーンショット画像を判定するもの(「このSSに何写ってる？」判定機)を作ってみました。日本語にはまだ対応していないので、[Translator Text API](https://azure.microsoft.com/ja-jp/services/cognitive-services/translator-text-api/)で翻訳した結果を返すようにしました。

## 結果

2017年12月現在、Computer Vision APIから返す結果(英語)はだいたい合っていますが、Translator Text APIから返す結果(日本語)がじわじわ来るものとなっています。

### 人物編

![forest.png](https://qiita-image-store.s3.amazonaws.com/0/10290/4d1709c4-d041-084f-fcab-a7041170fbc6.png)

**木の隣に立っている人(person standing next to a tree)**
ほぼ完璧です。

![mizunokarada.png](https://qiita-image-store.s3.amazonaws.com/0/10290/b21b9cc9-5d83-ffb5-5b5f-a261f74a0cb4.png)

**水の体の前に立っている男 (a man standing in front of a body of water)**
「体」が余計でした残念。英語は合っています。

![pose.png](https://qiita-image-store.s3.amazonaws.com/0/10290/681efc63-d9c5-87f9-2c32-b6320cdde50d.png)

**画像のポーズは女 (a woman posing for a picture)**
日本語訳が残念。英語(ポーズを取っている女)は合っています。

### 場所編

![classroom.png](https://qiita-image-store.s3.amazonaws.com/0/10290/a7cc4758-504f-d5f1-f8cd-4626962771ec.png)

**木の床の部屋(a room with a wooden floor)**
教室とは判定できないみたいだけど、間違いではない。

![jinja.png](https://qiita-image-store.s3.amazonaws.com/0/10290/e6eaf572-e6f0-1b6e-7d73-7abcfc90d168.png)

**建物の表示(a view of a building)**
判定を投げ出した感はあるものの、間違いではない。

![onsen.png](https://qiita-image-store.s3.amazonaws.com/0/10290/efb6c8ac-5ce7-3704-a580-81e9c94c6d69.png)

**建物の内側(the inside of a building)**
微妙に露天風呂なのですが... 右下のネコは無視。

![underwater.png](https://qiita-image-store.s3.amazonaws.com/0/10290/b6dd81ec-26a5-ab26-4b77-75513e6712bd.png)

**水のプールで泳いでいる犬(a dog swimming in a pool of water)**
逆に、いないはずの動物が出てきたパターン。魚はいましたが、犬はいませんでした。

### その他

![totan.png](https://qiita-image-store.s3.amazonaws.com/0/10290/e71d5c37-f5f4-dfb1-9815-e7942b0d7e7c.png)

**フォレスト内のツリー(a tree in a forest)**
床板のテクスチャなのですが...

![chatlog.png](https://qiita-image-store.s3.amazonaws.com/0/10290/ac254a66-d60e-d153-89fb-a396011c19b4.png)

**コンピューターの画面のスクリーン ショット (a screenshot of a computer screen)**
合っている。完璧。

まあ、そもそも、今までのは全部スクリーンショットだったんだけどね。。

## 「このSSに何写ってる？」判定機の使い方

[ここにGistでLSLスクリプトを置きました](https://gist.github.com/Makopo/50a357f7be333879e43a92f219ac5c5c)。

以下、Second Lifeで簡単なスクリプトなら作った(使った)ことのある前提で説明します。

1. 「[Computer Vision API](https://azure.microsoft.com/ja-jp/services/cognitive-services/computer-vision/)」の無料試用に申し込みます。

2. 「[Translator Text API](https://azure.microsoft.com/ja-jp/services/cognitive-services/translator-text-api/)」の無料試用に申し込みます。

3. 適当なオブジェクトの中にスクリプトを作成し、Gistの内容で上書きします。

4. `ここを変更してください`を、キーやエンドポイントの情報で書き換えて、保存します。

5. 判定したいスクリーンショット(SS)を `Ctrl` + ドラッグ でオブジェクトに入れます。オブジェクトの縁が赤くなったのを確認してからマウスを離しましょう。

6. オブジェクトにタッチすると、判定が開始されます。

* チャットとフローティングテキストで、解析結果が表示されます。
* オブジェクトの所有者がフルパーミッション(他の人の場合、次の所有者の権限がフルパーミッション)となるSSを入れたら、オブジェクトのどこか一面にSSが表示されます。
* 2枚以上同時に入れた場合、フローティングテキストとSS表示は、どれか1枚のみとなります。

## 判定機の実装の仕組み

判定機のスクリプトのうち、コグニティブAPIを呼び出す箇所を中心に説明します。[Gistに掲載したスクリプト](https://gist.github.com/Makopo/50a357f7be333879e43a92f219ac5c5c)を確認しながら読んでください。なお、このセクションは、[LSL(Linden Scripting Language)](http://wiki.secondlife.com/wiki/LSL_Portal/ja)をある程度ご存知であることを前提として書かれていますので、その他の方はざらざらっと眺める程度で。

### examine()関数 - Computer Vision APIのコール

オブジェクトにドロップされたスクリーンショット(SS)のURLを、Computer Vision APIの[Analyze Image](https://westus.dev.cognitive.microsoft.com/docs/services/56f91f2d778daf23d8ec6739/operations/56f91f2e778daf14a499e1fa)に渡して、判定してもらっています。

以下のURLで、Second Life仮想空間内にテクスチャとして保存されたSSが取得できます。公式には公開されていませんが、知っている人は知っているSecond Lifeの仕様です。URLの最後は`1`でも`3`でもいいですが、`2`が一番大きなサイズで取得できる感じなので、それを使っています。

```
http://secondlife.com/app/image/<SSのUUID>/2
```

[llHTTPRequest()](http://wiki.secondlife.com/wiki/LlHTTPRequest)の`HTTP_BODY_MAXLENGTH`の値は、念のために最大の16834に設定してあります。今回は、`visualFeatures=Description`で絞り込んであるので、ほぼその心配はないはずですが...

関数の全体は、こんな感じです。

```.lsl
examine(string invName)
{
	string pictureUUID = llGetInventoryKey(invName);
	string url = ENDPOINT_FOR_COMPUTER_VISION + "/analyze?visualFeatures=Description";
	list parameters = [
		HTTP_METHOD, "POST",
		HTTP_MIMETYPE, "application/json",
		HTTP_BODY_MAXLENGTH, 16384,
		HTTP_CUSTOM_HEADER, "Ocp-Apim-Subscription-Key", KEY_FOR_COMPUTER_VISION
	];
	string body = "{\"url\":\"http://secondlife.com/app/image/" + pictureUUID + "/2\"}";
	key requestId = llHTTPRequest(url, parameters, body);
	analyzeImageRequests += [requestId, invName];
}
```

### http_response() １回目

ステータスが200の場合、レスポンス本文にJSONが入ります。レスポンスにJSONが入る場合は、[llJsonGetValue()](http://wiki.secondlife.com/wiki/LlJsonGetValue)を有効活用しましょう。LSL仕様では分かりづらい説明となっていますが、第２引数で、識別子を順番に書けば、階層を辿れます。

[Analyze Image](https://westus.dev.cognitive.microsoft.com/docs/services/56f91f2d778daf23d8ec6739/operations/56f91f2e778daf14a499e1fa)の仕様を確認します。`description`の中に`captions`があり、その中にはリストがあり、リストの要素の中に`text`があって、そこに画像のキャプションが含まれます。つまり、キャプションを取得するには、以下のようにします。`0`はリストの先頭、という意味です。

```.lsl
string caption = llJsonGetValue(body, ["description", "captions", 0, "text"]);
```

しかし、2017年12月現在は、英語と中国語にしか対応していません。デフォルトでは、`caption`に英語の文章が入ってきます。英語が苦手な方に対応するために、この文章を日本語化する必要があります。そこで、[Translator Text API](https://azure.microsoft.com/ja-jp/services/cognitive-services/translator-text-api/)の出番です。

### translate()関数 - Translator Text APIのコール

Translator Text APIの[/Translate](http://docs.microsofttranslator.com/text-translate.html#!/default/get_Translate)の仕様に沿ってリクエストを生成して送信します。キャプションの中に、半角空白が含まれているため、一応[llEscapeURL()](http://wiki.secondlife.com/wiki/LlEscapeURL)でURLエスケープしていますが、やらなくても同じ結果が取得できていますので、どっちでもいいかもしれません。

関数の全体は、こんな感じです。

```.lsl
translate(string invName, string caption)
{
	string url = "https://api.microsofttranslator.com/V2/Http.svc/Translate?appid&text=" + llEscapeURL(caption) + "&to=ja";
	list parameters = [
		HTTP_METHOD, "GET",
		HTTP_MIMETYPE, "text/plain;charset=utf-8",
		HTTP_BODY_MAXLENGTH, 16384,
		HTTP_CUSTOM_HEADER, "Ocp-Apim-Subscription-Key", KEY_FOR_TRANSLATOR_TEXT
	];
	key requestId = llHTTPRequest(url, parameters, "");
	translateRequests += [requestId, invName];
}
```

### http_response() ２回目

ステータスが200の場合、レスポンス本文にXMLが入ります。JSONで返してもらうようにすることができません。LSLは標準のXMLパーサーが無いので大変です。

まず、返ってくるXMLの内容を確認します。このような感じで返されることが分かります。

```xml
<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">">目当ての文字列</string>
```

> [llOwnerSay()](http://wiki.secondlife.com/wiki/LlOwnerSay)や[llSay()](http://wiki.secondlife.com/wiki/LlSay)などで、レスポンス本文を普通にチャットで発言させようとしても、うまくいきません。肝心の文字列の部分が一部表示されなかったり、全く表示されなかったりします。
> ![chat.png](https://qiita-image-store.s3.amazonaws.com/0/10290/32b650d0-85e5-0345-48f3-e0c2786783ba.png)
> 原因はどうやら、`http://...`のURLの部分にあるようです。URLが開始されると、ダブルコーテーションの存在にも関わらず、残り全部をURLだと解釈してしまい、このようになってしまいます。デバッグチャネル(`DEBUG_CHANNEL`)にllSay()させれば、ちゃんと全体を確認できます。
> ![debug.png](https://qiita-image-store.s3.amazonaws.com/0/10290/4f4c9ce9-f86f-b92a-1a6d-e4b00cd86af9.png)


開始タグの`>`の後から、終了タグの`<`の前までを抜き出して、チャットさせます。

```.lsl
integer start_pos = llSubStringIndex(body, ">");
string work = llDeleteSubString(body, 0, start_pos);
integer end_pos = llSubStringIndex(work, "<");
string translatedText = llGetSubString(work, 0, end_pos - 1);
llSay(0, "「" + invName + "」には 「" + translatedText + "」が写っているようです。");
```