NatML + Unity + Local DL Modelでクロスプラットフォームアプリ実装 ~プラットフォームに応じたモデルロード編~（3/n）

**2023/09/05:iosのパスが適切ではなかったので修正しました。より詳細な情報は[公式ドキュメント](https://docs.unity3d.com/ja/2022.1/Manual/StreamingAssets.html)を参照してください。**

# 最終目標
android/iOS/OSX/windowsでDL推論アプリを単一プラットフォーム・単一言語でつくる。

# これまで
https://qiita.com/totti0223/items/b0407d2dfb323c9c0890

https://qiita.com/totti0223/items/7918e4392cb378aa052c

# 今回の内容
多言語対応のモデル読み込みコードを実装する

# 背景
>https://github.com/natmlx/natml-unity | Note that specific model formats can only be used on specific platforms. CoreML models can only be used on iOS and macOS; ONNX can only be used on Windows; and TensorFlow Lite can only be used on Android. Use NatML Hub to convert your model to different ML formats.

- iOS, OSXではCoreMLのmlmodelファイル、Androidではtfliteファイル、Windowsではonnxファイルとそれぞれのファイルが使用されることを想定する。したがって、例えばOSXでonnxファイルをロードしてもプラットフォームが違うとエラーがでる。前章でそれぞれのMobileNetV3Largeモデルを作成した（2/n参照）ので、それらを使い、プラットフォームごとに適切なモデルを読み込むコードを実装する。

# 実装

## 変更前
現状のDLモデル定義スクリプトのCreateメソッドはこんな感じ（詳細は1/nを参照）。mlmodel(CoreML)をロードすることが前提になっている。いちいち書き換えるのはめんどくさいので条件分岐でpathを変える。
```csharp:before.cs
 public static async Task<MobileNetV3LargePredictor> Create(){
        string path = Application.dataPath + "/StreamingAssets/DLModels/MobileNetV3/mobilenetv3_large_pytorch.mlmodel";
        var model = await MLEdgeModel.Create(path);
        Debug.Log(model);
        var predictor = new MobileNetV3LargePredictor(model);
        return predictor;
    }
```

## 変更後
**2023/09/04：MobileNetV3はfloat32でなければモデルを読み込めなかった。モデル依存かその他の理由かは調査中。しばらく複数のtfliteファイルをkeepしておくのがよいと思う、**
```after.cs

 public static async Task<MobileNetV3LargePredictor> Create()
{
        // string path = Application.dataPath + "/StreamingAssets/DLModels/MobileNetV3/mobilenetv3_large_pytorch.mlmodel";
        public string MLModelParentFolder = "DLModels/MobileNetV3";
        public string MLModelFileNameCoreML = "mobilenetv3_large_pytorch.mlmodel";
        public string MLModelFileNameOnnx = "mobilenetv3_large_pytorch.onnx";
        public string MLModelFileNameTflite = "mobilenetv3_large_pytorch_float16.tflite";
        public string path;

        if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer)
        {     
            path = Path.Combine(Application.dataPath, "StreamingAssets", MLModelParentFolder, MLModelFileNameCoreML);
        }
        else if (Application.platform == RuntimePlatform.IPhonePlayer)
        {
            path = Path.Combine(Application.dataPath, "Raw", MLModelParentFolder, MLModelFileNameCoreML);        
        }

        else if (Application.platform == RuntimePlatform.WebGLPlayer || 
                Application.platform == RuntimePlatform.WindowsEditor || 
                Application.platform == RuntimePlatform.WindowsPlayer)
        {
            path = Path.Combine(Application.dataPath, "StreamingAssets", MLModelParentFolder, MLModelFileNameOnnx);
        }

        else if (Application.platform == RuntimePlatform.Android)
        {
            // Get the file path in the application's persistent data path
            path = path.Combine(Application.persistentDataPath, MLModelFileNameTflite);
            if (!File.Exists(path))
            {
                // Get the file path in the StreamingAssets folder
                string srcPath = Path.Combine(Application.streamingAssetsPath, MLModelParentFolder, MLModelFileNameTflite);
                CopyFile(srcPath, path);
            }
        }
    
        var model = await MLEdgeModel.Create(path);
        Debug.Log(model);
        var predictor = new MobileNetV3LargePredictor(model);
        return predictor;
        
    }
IEnumerator CopyFile(string srcPath, string dstPath)
{
        using (UnityWebRequest www = UnityWebRequest.Get(srcPath))
        {
            yield return www.SendWebRequest();

            if (www.result == UnityWebRequest.Result.ConnectionError)
            {
                Debug.LogError("Error while copying file: " + www.error);
            }
            else
            {
                File.WriteAllBytes(dstPath, www.downloadHandler.data);
            }
        }
}


```

### 解説
- やってることはすごく単純で、Application.platformの値をチェックして、それによってDLモデルをロードするパスを変えるというもの。
- Android問題
    - 各種プラットフォームのうち、Androidのみapk内部のファイルにパスを使ってアクセスできない（すなわち、Assetsのパスを取得するApplication.dataPathが使えない）。
    - AndroidのみAsset/ StreamingAssets (Application.streamingAssetsPath)からファイルアクセスが読み取りのみ可能であるものの、UnityWebRequestに対応していないと触れないらしい（=NatMLでは直接読み込みができない、たぶん）
    - したがって、Application.persistentDataPathにモデルをコピーしてからモデルを読み出すということが必要らしい（ここまでChatGPT談）。
    - そのためにAndroidの場合のみCopyFileを走らせ、persistantdatapathから読み出すようにする。
- Androidのみファイルの場所をわけるとファイル管理がごっちゃになるので、最初からStreamingAssetsに全部いれて管理する。
- 下の図のようにAssets/StreamingAssetsフォルダを作成し、その中にモデルをいれればコードのような書き方で全パターンに対応して読み出すことができる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156483/6be474bc-afed-fc64-4b1c-331efd29316a.png)

## 展望
- 特定プラットフォームで使わないモデルはそもそも同封させる必要がないのでunity側の設定で除外などをしたい。
- エッジモデルとはいえ、種類が増えてくると重くなるので、可能であればFireBase MLとかで管理したい~~それNatML Hub~~.