【Unity】WebCamTextureのGetPixels()でメモリリークしまくる

Android

 UnityのWebCameraクラスでカメラの映像をピクセルデータで取り込む時にGetPixels()というメソッドで取得することができます。しかし、Androidアプリでこの関数を使ってひどい目にあったので、その時の状況と対処方法をまとめておきます。

Goal

WebCameraのGetPixels()を使ってはいけない!




カメラアプリ概要

 実験に使ったアプリについて説明します。アプリは非常に単純で、RawImageWebCamTextureを貼り付けるだけのものです。ShootボタンでカメラをPauseして、Restartボタンで一度Textureなどオブジェクトをクリーンにしてカメラを再起動します。

 下はUnity Editorの画面ですが、CameraPrevがRawImage表示するObjectです。MaskとかPanelは表示エリアのマスクのためで、今回の件とは関係ないので無視してください。
 あと、ShootボタンとRestartボタン、メモリ使用量確認のためのTextエリアがあります。

 アプリのコードは以下になります。Start()でカメラを起動します。ここでは4032×3024のサイズを縦表示させています。OnClickShoot()はShootボタンが押された時コールバックされます。でカメラをPauseしてGetPixels()でイメージを取り込みます。戻り値はColor[]が返ってくるのですが、今回使用しないので受け取っていません。ただ、関数コールしているだけです。
 OnClickRestart()はRestartボタンが押された時コールバックされます。一度TextureDestory()してカメラを再起動させています。
 Update()で60フレームに一度メモリ使用量を取得してTextに出力しています。

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.UI;
using System.Drawing;
using System.Xml;
using System.IO.IsolatedStorage;
using UnityEngine.Profiling;

public class CameraPrev : MonoBehaviour
{
    public RawImage RawImage;
    public GameObject LogText;

    private WebCamTexture webCam;
    private Vector2 defaulRawImageSize;

    // Start is called before the first frame update
    void Start()
    {
        defaulRawImageSize = RawImage.GetComponent<RectTransform>().sizeDelta;
        // カメラ起動
        startCamera();
    }


    // Update is called once per frame
    int cnt = 0;
    void Update()
    {
        if (cnt == 60) {
            cnt = 0;
            var monoUsedSize = Profiler.GetMonoUsedSizeLong() / 1024f / 1024f;
            var monoReservedSize = Profiler.GetMonoHeapSizeLong() / 1024f / 1024f;
            var unityUsedSize = Profiler.GetTotalAllocatedMemoryLong() / 1024f / 1024f;
            var unityReservedSize = Profiler.GetTotalReservedMemoryLong() / 1024f / 1024f;

            LogText.GetComponent<Text>().text = "monoUsedSize" + monoUsedSize.ToString() + "\n";
            LogText.GetComponent<Text>().text += "monoReservedSize" + monoReservedSize.ToString() + "\n";
            LogText.GetComponent<Text>().text += "unityUsedSize" + unityUsedSize.ToString() + "\n";
            LogText.GetComponent<Text>().text += "unityReservedSize" + unityReservedSize.ToString() + "\n";
        } else {
            cnt++;
        }
    }

    void startCamera()
    {
        // 指定カメラを起動させる
        webCam = new WebCamTexture(4032,3024);

        // RawImageのテクスチャにWebCamTextureのインスタンスを設定
        RawImage.texture = webCam;
        // カメラ起動
        webCam.Play();
        // 表示するRawImageを回転させる
        Vector3 angles = RawImage.GetComponent<RectTransform>().eulerAngles;
        angles.z = -webCam.videoRotationAngle;
        RawImage.GetComponent<RectTransform>().eulerAngles = angles;
        // サイズ調整
        float scaler;
        Vector2 sizetmp = RawImage.GetComponent<RectTransform>().sizeDelta;
        scaler = sizetmp.x / webCam.width;
        sizetmp.x = webCam.width * scaler;
        sizetmp.y = webCam.height * scaler;

        // 表示領域サイズ設定
        RawImage.GetComponent<RectTransform>().sizeDelta = sizetmp;
    }

    public void OnClickShoot()
    {
        // カメラを停止
        webCam.Pause();
        // カメラを停止
        webCam.GetPixels();
    }

    public void OnClickRestart()
    {
        webCam.Stop();
        Destroy(webCam);
        webCam = null;

        //表示エリアを初期化
        RawImage.GetComponent<RectTransform>().eulerAngles = Vector3.zero;
        RawImage.GetComponent<RectTransform>().localScale = new Vector3(1f, 1f, 1f);
        RawImage.GetComponent<RectTransform>().sizeDelta = defaulRawImageSize;
        //Texture削除
        Destroy(RawImage.texture);
        RawImage.texture = null;

        // カメラを開始
        startCamera();
    }
}

アプリを動かしてみる

起動直後

 アプリを起動させてみます。実機はAndroid Xperia1 SO-04Hです。起動直後、メモリ使用量は少ないです。

一度ShootしてRestartしてみる

 一度、カメラからGetPixelsで画像を取り込むため、Shootしてみます。monoUsedSizeとmonoReservedSizeがかなり増えました。また、Restartしてもメモリは減ってくれませんでした。

Shoot、Restartを繰り返してみる

 Shoot、Restartを繰り返してみます。1300MBほどまで増えて、そのあとアプリは落ちてしまいました。

対策

 TextureDestoryするタイミングを変えてみたり、Resources.UnloadAsset()を使ってみたり、System.GC.Collect()でガベコレを走らせてみたり、様々な対応を試みたのですが、メモリリークは改善しませんでした。
 そこで、GetPixels()を使用するのをやめて、GetPixels32()を使用することにしました。GetPixels32()はコールもとでバッファが準備できる場合は引数にそのバッファを指定し、バッファが準備できない場合は戻り値で受けることもできます。今回はコール元で4032×3024のClolor32[]バッファを準備して確認しました。

    Color32[] tempbuff = new Color32[4032 * 3024];

    public void OnClickShoot()
    {
        // カメラを停止
        webCam.Pause();
        // カメラを停止
//        webCam.GetPixels();
        webCam.GetPixels32(tempbuff);
    }

    public void OnClickRestart()
    {
        webCam.Stop();
        Destroy(webCam);
        webCam = null;


        //表示エリアを初期化
        RawImage.GetComponent<RectTransform>().eulerAngles = Vector3.zero;
        RawImage.GetComponent<RectTransform>().localScale = new Vector3(1f, 1f, 1f);
        RawImage.GetComponent<RectTransform>().sizeDelta = defaulRawImageSize;
        //Texture削除
        Destroy(RawImage.texture);
        RawImage.texture = null;

        // カメラを開始
        startCamera();
    }

 このコードで同じように動かした結果が以下になります。Shoot、Restartを繰り返してもメモリが増えていきません。

 Textureの関数内でメモリを確保すると、それを解放する方法が明確にわからないと、メモリリークの原因となってしまします。GetPixels()よりGetPixels32()の方が高速に処理できますし、データ用のメモリ確保方法も選べるので、GetPixels32()を使う方がいいです。

まとめ

 今回、GetPixels()でメモリリークが起きて、メモリ解放する方法が見つからず、かなりの時間を食われました。ただ、メモリに関する知識が深まり、アプリ開発を進めながら、常にどの程度メモリを使っていて、メモリリークが発生していないかを回帰的に見張ることの大切さを学びました。また、メモリを関数内でnewするのか、関数の外でnewしておくのかも大事だと気づきました。

 次回は同じメモリ関連の話題ですが、カメラの映像データをJPEG変換する際、タイミングによっては正しく保存できない(灰色で保存される)場合の対処方についてまとめたいと思います。

コメント