【MediaPipe,Unity】vrm使ってVtuberになれる仕組みを作ってみた（Androidビルド編）

# 前書き
地味に手間取ったので備忘録として記載しようかと思います
今回はコード全部記載します

# 行うこと
 - Androidビルド
 - IL2CPP化
 - APKの軽量化

# できたもの
![Test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370747/64741283-b7bd-5b4d-51b5-369d8c1855c4.gif)


# やったこと
 - 配布されてるライブラリは軽量化の物を使う
     - ![スクリーンショット 2024-05-04 145807.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370747/f86d6669-c81e-d591-7755-603342fd7620.png)
     - AOSビルドが180MB→50MBまで減少（リリースビルド
 - OutputStream<T>は非Generics化したクラスを用意する（IL2CPP使う場合のみ
    - https://twitter.com/monry/status/1484832543165726721
    - ↑こちらの情報に助けられた
 - 実機とEditorのカメラの差に気を付ける
    - Editorと実機ではカメラの角度や解像度が違うため、差を埋める作業が必要
    - 前回作成したVrmFace側で吸収しているのでコードなし


# 実際使用したコード
前回までのコードは配布されたサンプルのラッパークラスから独自のSolutionとGraphを作成して作っていた
今回は[こちら](https://github.com/homuler/MediaPipeUnityPlugin/wiki/Getting-Started#hello-world)の公式WikiのHelloWorldから作成を行った

```C#

using System;
using System.Diagnostics;
using Cysharp.Threading.Tasks;
using Mediapipe;
using Mediapipe.Unity;
using UnityEngine;
using UnityEngine.UI;

public class MediaPipeController : MonoBehaviour
{
    [SerializeField] private TextAsset gpuSetting;
    [SerializeField] private TextAsset cpuSetting;
    [SerializeField] private RawImage screen;
    [SerializeField] private int width = 640;
    [SerializeField] private int height = 480;
    [SerializeField] private int fps = 60;
    // VRMの表情を変更するためのコンポーネント
    [SerializeField] private VrmFace vrmFace;
    // 顔のランドマークを描画するためのコンポーネント
    [SerializeField] private FaceLandmarkListAnnotationController faceLandmarkListAnnotationController;
    
    private WebCamTexture webCamTexture;
    private OutputStreamFace outputStream;
    private Texture2D inputTexture;
    private Color32[] inputPixelData;
    
    private CalculatorGraph graph;
    private ResourceManager resourceManager;

    private async void Start()
    {
        // Androidかの判定
        bool isAOS = Application.platform == RuntimePlatform.Android && !Application.isEditor;

        // Androidの場合はカメラのデバイスを切り替える
        var index = !isAOS ? 0 : 1;
        var webCamDevice = WebCamTexture.devices[index];
        
        // widthとheightは設定値よりカメラの上限のwidthとheightが低いと書き換えられるので注意
        // ここで書き換えられた場合、↓のInputTextureの設定で壊れる
        webCamTexture = new WebCamTexture(webCamDevice.name, width, height, fps);
        webCamTexture.Play();

        await UniTask.WaitUntil(() => webCamTexture.width > 16);
        // 初期化が早すぎるとMediapipeの初期化が間に合わないので少し待つ
        await UniTask.WaitForSeconds(0.5f);

        // Androidの場合はカメラの向きを変更する 縦向きなため
        if (isAOS)
        {
            screen.rectTransform.sizeDelta = new Vector2(width / 3, height / 3);
            Vector3 angles = screen.rectTransform.eulerAngles;
            angles.z = 90;
            screen.rectTransform.eulerAngles = angles;
        }

        // カメラの映像をテクスチャに変換する
        inputTexture = new Texture2D(width, height, TextureFormat.RGBA32, false);
        inputPixelData = new Color32[width * height];
        screen.texture = webCamTexture;

        // モデルの読み込み
        resourceManager = new StreamingAssetsResourceManager();
        await resourceManager.PrepareAssetAsync("face_detection_short_range.bytes");
        await resourceManager.PrepareAssetAsync("face_landmark.bytes");
        await resourceManager.PrepareAssetAsync("iris_landmark.bytes");

        var stopwatch = new Stopwatch();
        // 設定を読み込む(エディターの場合はCPUしか対応してない)
        var textConfig = Application.isEditor ? cpuSetting : gpuSetting;
        var baseConfig = textConfig == null ? null : CalculatorGraphConfig.Parser.ParseFromTextFormat(textConfig.text);

        // サイドパケットの設定
        var sidePacket = new PacketMap();
        sidePacket.Emplace("input_rotation", Packet.CreateInt(0));
        sidePacket.Emplace("input_horizontally_flipped", Packet.CreateBool(false));
        sidePacket.Emplace("input_vertically_flipped", Packet.CreateBool(true));

        // Mediapipeの初期化
        graph = new CalculatorGraph();
        graph.Initialize(baseConfig, sidePacket);
        
        // 出力の設定,OutputStreamFaceはOutputStream<NormalizedLandmarkList>を非Generics化したクラス
        // Genericsクラスのメソッドにたいして[AOT.MonoPInvokeCallback]を付与してもIL2CPPでは動作しないため、別で定義する必要がある
        outputStream = new OutputStreamFace(graph, "face_landmarks_with_iris");
        
        // メインスレッドでの処理
        outputStream.StartPolling();
        graph.StartRun();
        stopwatch.Start();

        while (true)
        {
            // メインスレッドに切り替える
            await UniTask.SwitchToMainThread();
            if(webCamTexture == null || inputTexture == null || inputPixelData == null) break;
            inputTexture.SetPixels32(webCamTexture.GetPixels32(inputPixelData));
            var imageFrame = new ImageFrame(ImageFormat.Types.Format.Srgba, width, height, width * 4, inputTexture.GetRawTextureData<byte>());
            var currentTimestamp = stopwatch.ElapsedTicks / (TimeSpan.TicksPerMillisecond / 1000);
            graph.AddPacketToInputStream("input_video", Packet.CreateImageFrameAt(imageFrame, currentTimestamp));
            
            var result = await outputStream.WaitNextAsync();
            if (!result.ok) throw new Exception("Something went wrong");
            var outputPacket = result.packet;
            if (outputPacket != null)
            {
                var landmarks = outputPacket.Get(NormalizedLandmarkList.Parser);
                await vrmFace.SyncFace(landmarks);
                faceLandmarkListAnnotationController.DrawNow(landmarks);
            }

            await UniTask.Yield(PlayerLoopTiming.LastUpdate);
        }
    }
    
    private void OnDestroy()
    {
        if (webCamTexture != null)
        {
            webCamTexture.Stop();
        }

        outputStream?.Dispose();
        outputStream = null;

        if (graph != null)
        {
            try
            {
                graph.CloseInputStream("input_video");
                graph.WaitUntilDone();
            }
            finally
            {
                graph.Dispose();
                graph = null;
            }
        }
    }
}

```

# あとがき
実は一番手間取ったのは↑のコードの「widthとheightは設定値よりカメラの上限のwidthとheightが低いと書き換えられるので注意」の部分でした
実機上だと正常動作するのにEditorだと動かない状態でした
Mediapipe側のコード読みまくっても原因がわからずめっちゃ迷いました
そしたらMediapipe以外のところに原因がありました（怒）