AIアバターを作ってみよう

# 2022年はメタバースをたくさん使いました。
勤務先の大学における担当科目の授業や学外との打ち合わせなど、もちろん個人的な利用含めて、メタバースをたくさん使った年になりました。
画像を載せられませんが、現実世界の教室とメタバースをつないだハイブリッド授業も実施しました。
以下の写真は授業前のリハーサルのときのものです。ZoomやMicrosoft Teamsでは顔を積極的に出す人は少ないので、アバターを介することで出席しやすくなっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/a13f4bd4-a974-b48e-4a76-e8eecd25a620.png)

メタバースといえば、Meta Quest 2のようなVRゴーグル以外に、パソコンやスマートフォンからでも利用できることが、意外に知られていないこともわかりました。特にメタバースをあまり使ったことない方にはVRゴーグルがないと使えないと誤解されているようです。

# VRゴーグルの事故と防犯対策
Meta(旧Oculus) Quest 2や、以前であればHTC Vive等のVRゴーグルを使うと周囲が見えないので、整理されていない部屋で使用する転倒して家具に頭をぶつけたり、整理されていても近くに人が急に来てぶつかるということがよくありますし、あるあると言えます。
もし将来的にフルダイブ型VRゴーグルが普及したとすると、不審人物がピッキングして部屋に入ってきて、ダイブ中に知らぬ間に侵入し、物色されたり、ダイブ中の身体に攻撃されるといった事案が起きるかもしれません。
将来ではなく現在であれば、ダイブ中に、宅配便が来たり、家族が近づいてきた際にAIアバターがお知らせしてくれれば、宅配便に気づくこともできますし、家族にぶつかるようなことも回避できます。

# VRゴーグル使用中の防犯対策として、AIアバターを作ってみよう
上記のような事案を防ぐ意味でも、VRゴーグルを使ってメタバースにダイブ中に周囲の状況を教えてくれる「AIアバター」を作ってみましょう。今回作るものは、ごくごくシンプルなもので、私自身が現在開発および試験運用中のAIアバターで使っている手法の一部になります。

## 完成イメージ
Raspberry Piで、メタバースに接続した状態で、周囲にあるものを画像認識で認識し、認識結果にもとづいて文章を読み上げ、メタバース内に配信します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/26346ea8-e3a1-5f20-6a92-d11b1734d91a.png)

構成はざくっとこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/2059157e-49e8-d7b0-e300-6f5c18122aa4.png)

## 材料
必要なものは次のとおりです。
### ハードウェア
Linuxが使える環境であれば、Raspberry Pi以外の手のひらサイズの小型デスクトップパソコンでも構いません。
- Linuxマシン(Raspberry Pi OSもしくはUbuntu)
    - Raspbberry Pi 4 モデルB 4GBに、Raspberry Pi OS(64bit)を入れ、デスクトップ(GUI)モードで起動する状態にあるものを使っていますが、Ubuntu 20.04を入れたIntel Celeron搭載の小型デスクトップPCでも確認済みです。新品でも2～3万円くらい、中古ならば、2万円以下くらいで売っているものでも十分です。
- 7インチディスプレイ(小型のディスプレイでLinuxマシンに接続して使用)
    - Raspberry Pi を使用する場合は、Raspberry Pi 7インチ公式タッチディスプレイを使っています。
- USBカメラ
    - 今回は、ロジクールC270を使っています。

### ソフトウェア
OS(基本ソフト)は除きます。次のソフトウェアを使っています。
- fswebcam
    - コマンドが画像を取得するために使用
- pavucontrol
    - デスクトップ画面で音量調節などに使用
- Node-RED
    - IoTからAI、Webアプリ、DXのためのAPI制御まで、とりあえず入れとけな開発ツールです。今回もNode-REDをメインに使っていきます。導入手順は[公式ドキュメント](https://nodered.jp/docs/getting-started/raspberrypi)をご覧ください。
- Node-REDに以下のノードを追加しています。
    - node-red-contrib-play-audio
        - Watson Text to Speechで合成した音声を再生するために使用。
    - node-red-contrib-image-output
        - カメラで撮影した画像を確認するために使用。普段は使わないが開発や障害時の動作確認に用いる。
    - node-red-contrib-teachable-machine
        - 画像認識モデルをノーコードで作れる「Teachable Machine」で作ったモデルを呼び出し、カメラで撮影した画像に写っているいるものを認識するために使用。
    - node-red-node-watson
        - IBM WatsonをNode-REDで扱うことができる。今回は、Text to Speechを使用。

### メタバース環境
Webブラウザから使用できるものとして、Spatial、もしくはMozilla Hubsの試用環境が使いやすいです。本番利用で無料プランであれば、2022年12月現在はSpatialがベストでしょう。Mozilla Hubsは、2022年12月からSaaS版が有料となり、従来のSaaS版の無料環境は試用環境という位置づけになっています。
 - Spatial（無料プランあり）https://www.spatial.io/
 - Mozilla Hubs 本番環境（有料) https://hubs.mozilla.com/
 - Mozilla Hubs 試用環境（無料）https://hubs.mozilla.com/demo

もし、自分達でサーバー運用を行えるのであれば、独自のメタバース環境を構築しても良いでしょう。[Hubs Cloud](https://hubs.mozilla.com/cloud)を使うことで、独自のメタバースを構築し、Webブラウザ上で利用することができます。
## 組み立て
Raspberry Pi 4に、公式タッチディスプレイやUSBカメラをつなげ、デスクトップ画面を使えるようにします。各ソフトウェアをインストールしていきます。
### fswebcamとpavucontrolのインストール
fswebcamは、カメラで撮影するために必要です。pavucontrolは音声調整に用います。
```
sudo apt-get install fswebcam
sudo apt-get install pavucontrol
```
### 音声のループバックデバイス作成
IBM Watson Text to Speechによる音声合成したものの読み上げた音声をメタバース側に配信するために、ループバックデバイスを作ります。
```
sudo modprobe snd-aloop
sudo sh -c "echo snd_aloop >> /etc/modules"
sudo reboot
```
### 画像認識モデルの作成
ノーコードで画像認識モデルを作ることができる「Teachable Machine」を使います。今回は、踏むとダメージの大きいボールペンとスマートフォンを認識できるようにしています。このあたりは、人を加えるなど手の入れようがあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/2dbc9992-6fa3-d4f3-3dbe-edec44133cf7.png)
Teachable Machineによる画像認識モデルの作成手順は、[こちら](https://mydocument.atlassian.net/l/cp/0ss6Kwiv)をご覧ください。体験授業で中学生や高校生向けに使っている教材になります。

Teachable Machineでトレーニング(学習)後、「モデルをエクスポート」をクリックします。モデルを「アップロード」することで、モデルの共有URLを発行します。このURLをNode-REDで使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/ac5afbbe-1802-47a5-dc07-d876fb546268.png)
### IBM CloudでWatson Text to SpeechのAPI鍵とエンドポイントの発行
IBM Cloudにログインします。https://cloud.ibm.com/login
「カタログ」>>「AI/機械学習」>>「Text to Speech」の順にクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/3566491b-de6e-e95d-65cc-52b5551e9528.png)

料金プランで、ライト(無料)を選び、「以下のご使用条件を読み、同意します。」にチェックを入れ「作成」をクリックします。最初はライトから始めましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/3632082f-94fa-ab8e-c81b-d5a7f40d63d5.png)

表示された「API鍵」とエンドポイントになる「URL」をメモしておきます。Node-REDで使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/41867fe4-9c09-a617-7d10-792e9af343c4.png)

### Node-REDのインストールと起動
Node-REDをインストールします。公式ドキュメントの下記のインストールスクリプトは、Raspberry PiとUbuntu環境の両方で使えるので非常に便利です。
```
bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)
```
スクリプト実行後、自動起動設定を行い、Node-REDを起動します。
```
sudo systemctl enable nodered.service
sudo systemctl start nodered.service
```
### Node-REDにノードを追加して機能拡張
WebブラウザでNode-REDにアクセスします。仮にRaspberry PiのIPアドレスが、192.168.3.117の場合、Node-REDには、http://192.168.3.117:1880/ でアクセスすることができます。
Node-REDの画面で、画面右上の「三」>>「パレットの管理」>>「ノードを追加」で、下記を追加します。
- node-red-contrib-play-audio
- node-red-contrib-image-output
- node-red-contrib-teachable-machine
- node-red-node-watson
### フローの設定
Node-REDにおけるフローを作ります。フローはノード(箱状の部品)の集まりで、今回は画像認識を行い、認識結果にもとづく文章を読み上げます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/033cc7fc-3a73-8fa3-f0c4-3bf788df546c.png)

下記のJSONをインポートすると構築が簡単です。画面右上の「三」>>「読み込み」の順にクリックし、下記を貼り付け読み込みを行います。
```
[
    {
        "id": "30d745f00dd799a6",
        "type": "tab",
        "label": "AIアバターの作成",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "69938f65d6992f46",
        "type": "watson-text-to-speech",
        "z": "30d745f00dd799a6",
        "name": "",
        "lang": "ja-JP",
        "langhidden": "ja-JP",
        "langcustom": "NoCustomisationSetting",
        "langcustomhidden": "",
        "voice": "ja-JP_EmiVoice",
        "voicehidden": "ja-JP_EmiVoice",
        "format": "audio/mp3",
        "password": "",
        "apikey": "xxxxxxxxxxxxxxxxx",
        "payload-response": true,
        "service-endpoint": "https://api.us-south.text-to-speech.watson.cloud.ibm.com/instances/xxxxxxxxxxxxxxxxxxx",
        "x": 680,
        "y": 340,
        "wires": [
            [
                "3e0c62d69f98c579"
            ]
        ]
    },
    {
        "id": "3e0c62d69f98c579",
        "type": "play audio",
        "z": "30d745f00dd799a6",
        "name": "",
        "voice": "",
        "x": 870,
        "y": 340,
        "wires": []
    },
    {
        "id": "ebf81ddd68e10d37",
        "type": "inject",
        "z": "30d745f00dd799a6",
        "name": "",
        "props": [],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "x": 110,
        "y": 60,
        "wires": [
            [
                "2d418b7380b53eca"
            ]
        ]
    },
    {
        "id": "2d418b7380b53eca",
        "type": "exec",
        "z": "30d745f00dd799a6",
        "command": "fswebcam -d v4l2:/dev/video1 -i 0 -p YUYV test.jpg",
        "addpay": "",
        "append": "",
        "useSpawn": "false",
        "timer": "",
        "winHide": false,
        "oldrc": false,
        "name": "fswebcam",
        "x": 280,
        "y": 60,
        "wires": [
            [
                "04a563a7e0a0ef46"
            ],
            [],
            []
        ]
    },
    {
        "id": "04a563a7e0a0ef46",
        "type": "file in",
        "z": "30d745f00dd799a6",
        "name": "",
        "filename": "/home/pi4/test.jpg",
        "filenameType": "str",
        "format": "",
        "chunk": false,
        "sendError": false,
        "encoding": "none",
        "allProps": false,
        "x": 470,
        "y": 40,
        "wires": [
            [
                "1dd1618827b398ad",
                "7de4993d8be2b23e"
            ]
        ]
    },
    {
        "id": "1dd1618827b398ad",
        "type": "image",
        "z": "30d745f00dd799a6",
        "name": "",
        "width": 160,
        "data": "payload",
        "dataType": "msg",
        "thumbnail": false,
        "active": true,
        "pass": false,
        "outputs": 0,
        "x": 740,
        "y": 40,
        "wires": []
    },
    {
        "id": "7de4993d8be2b23e",
        "type": "teachable machine",
        "z": "30d745f00dd799a6",
        "name": "",
        "mode": "online",
        "modelUrl": "https://teachablemachine.withgoogle.com/models/zTx7v43x4/",
        "localModel": "teachable_model",
        "output": "best",
        "activeThreshold": false,
        "threshold": 80,
        "activeMaxResults": false,
        "maxResults": 3,
        "passThrough": false,
        "x": 450,
        "y": 180,
        "wires": [
            [
                "8bdddc496dd31a97",
                "ce39735db7294cee"
            ]
        ]
    },
    {
        "id": "8bdddc496dd31a97",
        "type": "debug",
        "z": "30d745f00dd799a6",
        "name": "debug : teachable machine",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 780,
        "y": 240,
        "wires": []
    },
    {
        "id": "8283738117167033",
        "type": "switch",
        "z": "30d745f00dd799a6",
        "name": "",
        "property": "payload[0].class",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "pen",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "smartphone",
                "vt": "str"
            },
            {
                "t": "else"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 3,
        "x": 190,
        "y": 360,
        "wires": [
            [
                "285eb0ade692c1a6"
            ],
            [
                "f99f410e6ca5ee0e"
            ],
            [
                "9cc9abcd9266fe2f"
            ]
        ]
    },
    {
        "id": "285eb0ade692c1a6",
        "type": "function",
        "z": "30d745f00dd799a6",
        "name": "talk (pen)",
        "func": "msg.payload = \"近くにペンがありますよ。踏んで転ばないようにちゅういしてください。\";\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 440,
        "y": 340,
        "wires": [
            [
                "69938f65d6992f46"
            ]
        ]
    },
    {
        "id": "f99f410e6ca5ee0e",
        "type": "function",
        "z": "30d745f00dd799a6",
        "name": "talk (smartphone)",
        "func": "msg.payload = \"近くにスマートフォンがありますよ。踏まないようにちゅういしてください。\";\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 470,
        "y": 380,
        "wires": [
            [
                "69938f65d6992f46"
            ]
        ]
    },
    {
        "id": "9cc9abcd9266fe2f",
        "type": "debug",
        "z": "30d745f00dd799a6",
        "name": "debug other",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 270,
        "y": 440,
        "wires": []
    },
    {
        "id": "ce39735db7294cee",
        "type": "switch",
        "z": "30d745f00dd799a6",
        "name": "",
        "property": "payload[0].score",
        "propertyType": "msg",
        "rules": [
            {
                "t": "gte",
                "v": "0.8",
                "vt": "num"
            },
            {
                "t": "lt",
                "v": "0.8",
                "vt": "num"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 2,
        "x": 390,
        "y": 260,
        "wires": [
            [
                "8283738117167033"
            ],
            []
        ]
    }
]
```
### 各ノードの設定
Node-RED上のノードについて、各設定を見ていきます。各ノードを設定後、画面右上の「デプロイ」をクリックします。
#### fswebcam
見た目は下記のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/e52d3d5c-5956-063f-11c9-1a25ceda6f36.png)
中身は次のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/d28bdf86-350f-98b2-dd77-8542cd4bfcea.png)
#### read fileとimage preview
見た目は下記のようになっています。read fileとiamge-previewをつなげています。撮影した画像を読み込み、表示します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/2663bc63-43aa-9b9d-cd1e-ba14b4f7cef3.png)
中身は次のようになっています。ここの「pi4」は、Raspberry Piのユーザー名をpi4としているためです。ここは各ユーザーで異なるので、各自で変更しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/b9b78c1e-380a-1b07-5816-4dae18c01738.png)
#### Techable Machine
見た目は下記のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/60729730-4ae6-ba27-68f9-1042b7e54b14.png)
中身は次のようになっています。UrlにTeachable Machineで発行した画像認識モデルのURLを記述しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/71ca4ad6-9490-a761-b68e-f4654dc6d654.png)
#### 1つ目のswtich
見た目は下記のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/6ed76462-6b93-bd92-688f-f09555dff438.png)
中身は次のようになっています。画面認識スコアが0.8以上ときに、出力1となり、次のノードに処理が続くようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/4b2d69bb-13b2-1945-0468-8795dd221204.png)
#### 2つ目のswtich
見た目は下記のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/2c1ffac7-da2d-4e93-fa34-5d95aa648f29.png)
中身は次のようになっています。画像認識結果のクラス名が「pen」であれば出力1で「talk(pen)」へ、「smartphone」であれば出力2で「talk(smartphone」、それ以外は出力3になるようにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/e37796db-e886-db42-32cc-7902f1446196.png)
#### talk(pen)
見た目は下記のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/2779c73a-cb89-5902-d3f5-c24917bf7499.png)
中身は次のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/d692f39c-39bf-6972-3e11-4f79e87de360.png)
#### talk(smartphone)
見た目は下記のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/a068f8f4-6daf-0862-f1f7-ec64fc08d184.png)
中身は次のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/2a04310e-c975-288f-ab0f-b73af0464831.png)
#### text to speech
見た目は下記のようになっています。「text to speech」ノードの隣に「play audio」ノードを接続します。「play audio」ノードにより読み上げた音声が再生されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/e9a29609-bdeb-be2d-c375-ced6c6f6a90a.png)
中身は次のようになっています。API Keyには、IBM CloudのWatson Text to Speechで発行したAPI鍵を記述します。「Service Endpoint」はIBM CloudのWatson Text to Speechで発行した「URL」の値を記述します。「Language」「Voice」「Formart」は画像のように設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/75046c30-6561-44cd-a271-5ff1ef967b81.png)

## メタバース空間の作成
SpatialでもMozilla Hubsのどちらでも構いません。Raspberry Pi 前提であればWebブラウザベースで使えるメタバースが良いでしょう。Ubuntu前提であれば他のメタバースサービスも使用できます。
ここでは、Raspberry Piで、Webブラウザを起動し、Mozilla Hubsの試用環境で部屋を作成します。
https://hubs.mozilla.com/demo にアクセスし、「部屋を作成する」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/a220d6f2-3afa-4e46-5f11-661f12232d96.png)
「Join Room」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/d957bad3-070d-2242-8f27-887fdfc077e9.png)
「Avater Setup」で「Accept」をクリックします。
Raspberry Piの場合、GPUが非力ということもあり、下記のように表示されないことがあります。Intel Celeronベースの小型PCの場合は、Raspberry Piと異なり選択したアバターが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/f9db0011-37f1-28f3-54f7-4eed2717b43e.png)
「Microphone setup」でマイクとスピーカーを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/a51e585f-be3f-8690-028e-e7bff2c5b8b6.png)
「Microphone」にループバックデバイス「内部オーディオ アナログステレオ」を設定します。「Speakers」には、「内部オーディオ Analog Stereo」を選びます。
メタバース空間が作成されるので、画面左下に表示される「Invite」をクリックし表示されるURLに、別の端末からメタバース空間に接続します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/bce0b26a-cd90-5001-b721-cdad8c70ee01.png)

# 動作確認
スマートフォンやパソコンからメタバース空間に接続します。AIアバターの近くに移動します。

Node-REDのフローを実行し、画像認識の動作確認を行います。常に動かす場合は、「inject」ノードを編集し、定期的に実行するようにしましょう。たとえば、10秒間隔や30秒間隔で自動実行することで、画像認識を定期的に実行することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/033cc7fc-3a73-8fa3-f0c4-3bf788df546c.png)
スマートフォンやパソコンのスピーカーから、AIアバターが認識した結果にもとづく文章が読み上げた内容が音声として聞こえてくることがわかることでしょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41414/2059157e-49e8-d7b0-e300-6f5c18122aa4.png)

# まとめ
半ば力業でAIアバターを作成しました。画像認識による制御は、脳波画像を画像認識により様々なデバイスを制御するものがありますので、それと似たような仕組みになっています。画像認識によるAIアバター制御の他、音声制御などもあります。

今後、メタバース空間の利用が一般的になっていくと、VRゴーグルによる事故対策や防犯対策がより一層必要になることでしょうから、今回の技術は社会的意義が高いと言えるでしょう。
特に家庭内やリハビリ施設でVRゴーグルを使う場合には必要とされるはずです。

次は音声で雑談ができるようにできないか検討してみます。雑談ができると面白いはず。