【検証】UnityのBarracudaは映像異常検知AIをスマホで動作させられるか

# はじめに
映像異常検知AIは、防犯や品質管理のアプローチの1つとして監視カメラなどへの適用が検討されますが、大きなモデルは実験に大規模な機器を必要としコストがかかります。
では、比較的小さなデバイスでもAIモデルを検証できる仕組みに映像異常検知AIモデルをのせると、どこまでワークするのでしょうか。
本記事では、UnityのBarracuda[[参考1](#参考)]を使って、Pytorchで独自学習した映像異常検知モデルをONNX形式で保存し、スマホアプリにデプロイするまでの試行錯誤について記載しつつ、最終的に下記の検証環境で動作するコードを作成することを目指します（今回はモデルの性能やソースコードの品質は問わない）。

# 検証環境
- MacBook Pro (M1, 2020, メモリ16G)
- Unity Industry [[参考2](#参考)]
- Unity 2021.3.0f1 [[参考3](#参考)]　 
- iPhone 13 Pro [[参考4](#参考)] 
- Barracuda 3.0.1 [[参考5](#参考)]

# 映像異常検知AIについて
映像異常検知AI[[補足1](#補足1)]は、映像データから時空間にまたがる規則性を学習することで、推論時にその規則性から外れた部分を抽出することを主な目的としています（図1）。

![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892617/6c2c47e6-66d0-a3a8-b285-50c50f9e556a.png)

**図1：入出力例**（歩道映像を入力に自転車に乗った人を異常として抽出）[[参考6](#参考)]

# Barracudaとは
「Barracudaパッケージは、Unity用の軽量なクロスプラットフォームニューラル ネットワーク推論ライブラリです。」[[参考1](#参考)]
ONNX(Open Neural Network Exchange) 形式に固めたモデルをインポートし、Unityのアプリケーションに活用することができます。
Unityは作成したアプリケーションをスマホにデプロイすることが容易なので、その点が今回の検証に都合が良いです（ご参考：動作するサンプル[[参考7](#参考)]）。

# 方針
今回は以下の流れで検証を進めました。
1.	軽量の映像異常検知モデル(Seq2seqLSTMベース)を構築し学習（Python, Pytorch）
2.	ONNX形式でモデルを保存
3.	UnityのBarracudaでモデルを推論できるようにする(C#)
4.	作成したアプリをスマホにデプロイして動作確認

# 詰まったところ
方針に従って進める中で詰まったところと対応について以下整理します。

1.	**検証環境で動く映像異常検知手法のコードがあまり公開されておらず、気軽に動作確認できない**
今回は公開されている記事[[参考8](#参考)]のコードを参考にしました

2.	**ONNX形式で保存・推論時にエラーが発生**[[参考9](#参考)]
エラーが発生しないアルゴリズムにコードを修正(具体的にはGRUからLSTMへ変更)
    
3.	**Barracudaで読み込めない演算がある**[[参考10](#参考)]
読み込み可能な演算だけでコードを修正
4.	**動作が重い**
動作を重くしている部分を特定し修正(後述の課題参照)

# コード
前述の詰まったところに対応した部分のコードを以下に記載します。

```python: 1. 映像異常検知モデル(ベース)
class Seq2seqLSTM(nn.Module):
    def __init__(self, opt):
        super(Seq2seqLSTM, self).__init__()
        self.opt = opt
        z_dim = opt.z_dim

        self.encoder = nn.Sequential(
            nn.Conv2d(opt.n_channels, 32, kernel_size=3, stride=1, padding=1, bias=False),
            nn.ReLU(True),
            nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1, bias=False),
            nn.ReLU(True),
            nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1, bias=False),
            nn.ReLU(True),
            nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1, bias=False),
            nn.ReLU(True),
            nn.Conv2d(256, 512, kernel_size=3, stride=2, padding=1, bias=False),
            nn.ReLU(True),
        )

        self.fc1 = nn.Sequential(
            nn.Linear(512 * 4 * 4, z_dim),
        )

        self.fc3 = nn.Sequential(
            nn.Linear(opt.lstm_dim, 512 * 4 * 4),
        )

        self.decoder = nn.Sequential(
            nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1, output_padding=1, bias=False),
            nn.ReLU(True),
            nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, output_padding=1, bias=False),
            nn.ReLU(True),
            nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, output_padding=1, bias=False),
            nn.ReLU(True),
            nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, output_padding=1, bias=False),
            nn.ReLU(True),
            nn.Conv2d(32, opt.n_channels, kernel_size=3, stride=1, padding=1, bias=False),
            nn.Tanh(),
        )

        self.fc2 = nn.Sequential(
            nn.Linear(opt.lstm_dim, z_dim),
            nn.ReLU(True),
        )

        self.lstm = nn.LSTM(z_dim, opt.lstm_dim, batch_first=True)

    def forward(self, x):
        bs = x.size(0)
        feature = self.encoder(x.view(bs * self.opt.T, self.opt.n_channels, 64, 64))
        z = self.fc1(feature.view(bs * self.opt.T, -1))
        z_ = z.view(bs, self.opt.T, self.opt.z_dim)
        h0 = torch.zeros(1, bs, self.opt.lstm_dim)  # Initial hidden state
        c0 = torch.zeros(1, bs, self.opt.lstm_dim)  # Initial cell state
        o, (hn, cn) = self.lstm(z_, (h0, c0))
        o = self.fc3(o)
        xhat = self.decoder(o.view(bs * self.opt.T, 512, 4, 4)).view(bs * self.opt.T, self.opt.n_channels * 64 * 64)

        return xhat, z
```

```python: 2. ONNX保存
# ハイパーパラメータの指定
opt = OPT()

# モデルのアーキテクチャを定義
model = Seq2seqLSTM(opt)
opt.epoch = 5000 #エポック数を指定

# モデルの重みをロード
saved_model_path = os.path.join(opt.log_folder, 'model_epoch_{:04d}.pth'.format(opt.epoch))
model.load_state_dict(torch.load(saved_model_path))

# 推論モードに設定
model.eval()

# ダミー入力の作成
dummy_input = torch.randn(1, 4, 4096)

# ONNX ファイルにエクスポート
torch.onnx.export(model,               # モデル
                  dummy_input,         # モデルへのダミー入力
                  "test_model.onnx",   # 出力するファイル名
                  export_params=True,  # モデルファイルに学習済みパラメータを含むかどうか
                  opset_version=11,    # 使用する ONNX のバージョン
                  do_constant_folding=True,  # 最適化オプション、定数を事前計算
                  input_names = ['input'],   # 入力名
                  output_names = ['output'], # 出力名
                  dynamic_axes={'input' : {0 : 'batch_size'},   # バッチサイズを動的に
                                'output' : {0 : 'batch_size'}}) # 出力も同様に
```

```csharp: 3. Barracudaを使ったUnity側のコード
void Start()
{
    var model = ModelLoader.Load(modelAsset);
    worker = WorkerFactory.CreateWorker(WorkerFactory.Type.CSharpBurst, model);

    StartCamera();
    StartCoroutine(ADCoroutine());
}

IEnumerator ADCoroutine()
{
    while (true)
    {
        frameCount++;

        if (frameCount >= frameInterval)
        {
            // カメラフレームをコピー
            inputTexture.SetPixels(webCamTexture.GetPixels());
            inputTexture.Apply();

            // フレームをバッファに保存
            frameBuffer[currentFrameIndex] = new Texture2D(inputTexture.width, inputTexture.height);
            frameBuffer[currentFrameIndex].SetPixels(inputTexture.GetPixels());
            frameBuffer[currentFrameIndex].Apply();

            currentFrameIndex++;

            // 4フレームが集まったらモデルに入力
            if (currentFrameIndex >= 4)
            {
                float[] inputFloats = PreprocessFrames(frameBuffer);
                var inputTensor = new Tensor(1, 4, 64, 64, inputFloats);
                var outputTensor = worker.Execute(inputTensor).PeekOutput();

                // 差の合計を計算
                float differenceSum = CalculateDifferenceSum(inputTensor, outputTensor);
                
                // DifferenceSumの結果をTextに表示
                displayText.text = $"Anomaly Score: {differenceSum}";
                Debug.Log($"Anomaly Score: {differenceSum}");

                // Dispose of the tensors
                inputTensor.Dispose();
                outputTensor.Dispose();
                yield return new WaitForCompletion(outputTensor);

                // フレームカウントとバッファインデックスをリセット
                currentFrameIndex = 0;
            }

            frameCount = 0;
        }

        yield return null; // 次のフレームまで待つ
    }
}
```

# 動作イメージ
前述のコードを用いて、スマホの外付けカメラ映像を入力に異常スコアを出力する独自学習モデルを搭載したUnityアプリを作成しました。iphoneでアプリを実行してみた結果、異常スコアの更新速度は遅いものの、それ以外は問題なく動作しました（図2）。

![図2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892617/871ce728-04d2-f940-016d-b616569b6850.png)
**図2：スマホでの動作確認結果**（学習データにないオブジェクトを通過させて異常スコア（左上）が上昇することを確認）

# 課題
今回は動くものを作ることを目標に進めましたが、進める中でいくつか課題が見えてきました。

### 逆畳み込みの処理が重い
パラメータ数や入出力次元のメモリ影響よりも、大きな影響があるようです(Barracudaのライブラリは逆畳み込みの最適化があまい可能性あり)。逆畳み込みをしたい場合はレイヤー数を減らすなどした方が気軽に実験できるかもしれません。

![図3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892617/a1110577-e705-6c49-8ee8-fada3bb3cb4d.png)
**図3：プロファイラーによる分析**（Conv2DTransの処理時間が大きくなっている）

### 利用可能な演算で再現できないモデルが存在
時空間情報に3次元畳み込みを利用するモデルは今回の方針で利用可能な演算だけで愚直に書くと計算時間がかかりすぎるという問題が発生しました。
上記含め、比較的大きなモデルはBarracuda以外の方法で検証することを検討した方がよいかもしれません。

# まとめ
本記事では、UnityのBarracuda[[参考1](#参考)]を使って、独自学習した映像異常検知モデルをスマホアプリにデプロイするところまでを記載いたしました。
当初、スマホに映像異常検知AIを搭載して監視カメラのような用途として使えるのかを確認したかったのですが、今回の方針では課題が多いように感じました。
エッジデバイスでモデルを動作させるよりも、API経由にした方が良いとすると、コストの問題はありますがLLMを活用したマルチモーダルAIが期待できるかもしれません。
映像異常検知AIをスマホで動かしてみたい方がいれば、一例として参考にしていただけますと幸いです。
改善点やご質問などあれば、コメントよろしくお願いいたします。

# 参考
1.	https://docs.unity3d.com/Packages/com.unity.barracuda%401.0/manual/index.html
2.	https://unity.com/ja/products/unity-industry
3.	https://docs.unity3d.com/ja/2022.3/Manual/UnityManual.html
4.	https://support.apple.com/ja-jp/111871
5.	https://docs.unity3d.com/Packages/com.unity.barracuda@3.0/manual/Installing.html
6.	Mahadevan, V., Li, W., Bhalodia, V., & Vasconcelos, N. (2010). Anomaly detection in crowded scenes. 2010 IEEE Computer Society Conference on Computer Vision and Pattern Recognition, 1975-1981. http://www.svcl.ucsd.edu/projects/anomaly/dataset.htm
7.	【Unity】UnityでBarracudaを使ってMNISTの手書き文字認識をする https://unitech.hatenablog.com/entry/2020/08/08/225758
8.	GRUとAutoencoderを用いた，動画の再構成手法の検証と実装 https://qiita.com/satolab/items/c4e9a287e8d1ebb010a6
9.	https://community.konduit.ai/t/importing-gru-onnx-model-failed/2250
10.	https://docs.unity3d.com/Packages/com.unity.barracuda%401.0/manual/SupportedOperators.html
11.	Mohammadi, B., Fathy, M., & Sabokrou, M. (2021). Image/Video Deep Anomaly Detection: A Survey. ArXiv, abs/2103.01739.
12.	動画像異常検知サーベイまとめ(CVPR,AAAI等)
https://qiita.com/satolab/items/026e4bb9a6fc35737599
13.	動画像異常検知サーベイまとめPart2（GAN等）
https://qiita.com/satolab/items/ab90bd7297832d9e80c3
14.	動作認識の最前線：手法，タスク，データセット
https://www.slideshare.net/ttamaki/ss-254290005
15.	Suarez, J.J., & Naval, P.C. (2020). A Survey on Deep Learning Techniques for Video Anomaly Detection. ArXiv, abs/2009.14146.
16.	OpenCVでとらえる画像の躍動、Optical Flow
https://qiita.com/icoxfog417/items/357e6e495b7a40da14d8
17.	https://oguemon.com/study/linear-algebra/trace/
18.	https://www.cresco.co.jp/blog/entry/16891.html
19.	https://arxiv.org/pdf/1711.11248v3.pdf
20.	https://www.tensorflow.org/tutorials/video/video_classification

# 補足1
### 映像異常検知AIについて
映像異常検知AIのベース知見を簡単に整理します[[参考11-13](#参考)]。

**概要**
以下をタスクに応じて検討するのが1つの流れです。
1.	映像入力に対する方針とどの程度細かく見る必要があるかを検討
2.	モデル出力を検討
3.	代表的なモデルを発展させたり、オプションや工夫を取り入れたりする
4.	データセットを選んで検証する
＋α：技術的な課題にアプローチする仕組みを検討する

**映像入力にどう対応するか**
動画認識[[補足2](#補足2)]と同様、基本は大きく2つの方針があります（空間とはこの場合画像のため2次元）[[参考14](#参考)]。
1.	STN系: 空間 $R^{2}$ と時間 $R$ を合わせて考慮
2.	Appearance×Motion系: 空間 $R^{2}$ と時間 $R$ に分ける（チャネルも考慮）

**入力をどの程度細かく見るか**
空間と時間についてそれぞれ代表的な粒度があります。基本的に詳細にするほど情報がスパースになり、学習が複雑になります。

- 空間：Appearanceについて
    - フレーム：クリップ単位
    - グループ：意味単位
    - エッジ　：オブジェクト単位
    - ピクセル：画素単位
- 時間：Motionについて
    - optical flow：注目部分（動作部分）
    - ウィンドウ幅：指定時間の幅を限定

**モデル出力**
タスクに応じて以下のバリエーションがあります（右はどの規則性に注目したいか）。
- 再構成誤差：shape
- 未来予測誤差：situation
- 分類：order
- スコアリング：balance

**代表的なモデル**
空間と時空間に対応する代表的なモデルを整理します。

- 空間※: AE, GMM, DNNなど
- 時空間: LSTM, CNN, attention構造など

※空間を代表的なモデルで対応させる場合、時間は独自アーキテクチャで対応するケースが多い

**オプション**
タスクに応じて以下の仕組みを取り入れることがあります。
- self-supervised: 頑健性向上
- patch: 精度向上、重い(Swinなど)
- adversarial: 予測と再構成の橋渡し
- psuedo-label: 同上
- バイアス対応: 公平性向上、精度落ちる傾向あり

**典型的工夫**
以下に代表的なものを整理します（全て精度向上を期待するもの）。
- 時間指定：長期と短期をあわせる
- 空間指定：高解像度と低定解像度をあわせる、pose推定
- 対象を事前に注目しやすく：optical flow、物体検知

**データセット**
以下のデータセットはサイズ的にも比較的使いやすいです（ラベルは動画単位かなしか）。

![図4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892617/00dac7eb-11a3-e925-f79b-953ec45c6886.png)
図4: ベンチマークデータ例[[参考15](#参考)]

**課題**
技術的な課題として以下が挙げられます。
1.	文脈の考慮
2.	異常の無限性
3.	異常が正常の近傍にある状況での対処
4.	オクルージョン、軌道

# 補足2

**表1: 動画認識モデル例**
|	|項目|メモ|
|---|:-:|:--|
|1|	Two-Stream|	- optical flow (xとyの特徴点、コーナー検出)[[参考16](#参考)]<br>- コーナー検出法 [[参考17](#参考)]<br>- OpenCVで特徴点を抽出 [[参考18](#参考)]|
|2|	TSN|- クリップで分割、それぞれでTwo-Streamで統合<br>- ローカルでoptical flow: 情報の粒度が細かい|
|3|	TSM|- シフトによるT1Dの一般化と理解|
|4|	R(2＋1)D|- 時間と空間の処理を分ける、後ろが時間、後ろほど情報の粒度が細かい対処に向くか: 安定性で区切っているか<br>- C3D：3Dconv<br>- Convolutional residual blocks使っている[[参考19,20](#参考)]|
|5|	Non-Local|- transformerのself-attention|
|6|	Slow-fast|- 動画のフレームレートを変える<br>- 3D-Resnetのtwo-stream|
|7|	X3D|- アーキテクチャ探索により軽量かつ精度もそこそこ|