OpenCVでプレイ動画の切り出しを自動化した話おまけ・メモリリーク対策編

#目的

先月動画ファイルを読み込んで処理するソフトを作ったのだが、それがありえないぐらいのメモリリークを起こした。
メモリリークの原因の確認をする。

#何が起きたか

前回までで紹介していたツールに対して、1時間ぐらいの動画の処理をしていたら途中で処理が止まった。非同期処理の例外処理もきちんと実装していなかったため、何が原因で止まるかはわからなかった。
Visual Studioの仕様上、32ビットでバイナリをコンパイルしないとカスタムコントロールの編集がまともにできないし、メモリをバカ食いしている様子はデバッガで確認できていたため、64ビットでコンパイルし直したらとりあえず動いた。

#そもそも
![cvs_mat.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395414/f0d44925-0daf-1fc6-ef53-82d6c7d22f4b.png)

知人に教えてもらってから気がついたが、Matクラスは明示的にDisposeできる、アンマネージドなクラスらしい。
(シノアさんありがとう)

#メモリリークの確認、その1
##ソース

コンソールアプリを新しく作成し、次のようなソースを書いた。
testVideoの変数の中に別途1時間程度の1080pのサイズの動画のパスを書き込んである。
これをVisualStudioのデフォルトのリリースビルドでビルドし、動作を確認した。

```c#
        static void Main(string[] args)
        {
            Random r = new Random();
            CheckMemory();
            Console.WriteLine("ファイルの読み込みを行います。");
            VideoCapture cap = new VideoCapture(testVideo);
            CheckMemory();
            Console.WriteLine("100回ランダムなフレームを読み込みます。");

            for (int i = 0; i < 100; i++)
            {
                cap.PosFrames = r.Next(0, cap.FrameCount);
                Mat createMat = new Mat();
                cap.Read(createMat);
                // 実験2で使用
                // createMat.Dispose();
            }
            // 実験3で使用
            // CheckMemory();
            // GC.Collect();

            CheckMemory();
            Console.WriteLine("何かキーを押すと終了します。");
            Console.ReadKey();
        }

        private static int lastMemory = 0;
        static void CheckMemory()
        {
            // MB単位。
            int currentMemory = (int)(Environment.WorkingSet >> 20);
            Console.WriteLine(String.Format("現在のメモリ使用量はおよそ{0}MB、前回からの差分は{1}MBです。", currentMemory, currentMemory - lastMemory));
            lastMemory = currentMemory;
        }
```

## 実験1
とりあえずコメントアウトなどは無しで動かしてみた。
出力は次の通り。

> 現在のメモリ使用量はおよそ11MB、前回からの差分は11MBです。
ファイルの読み込みを行います。
現在のメモリ使用量はおよそ31MB、前回からの差分は20MBです。
100回ランダムなフレームを読み込みます。
現在のメモリ使用量はおよそ710MB、前回からの差分は679MBです。
何かキーを押すと終了します。

順調にメモリ使用量を伸ばしている姿を確認できた。
なお、for分の中にCheckMemory()を入れたら毎回6～10MB使用メモリが増えていたことを確認できた。
(Qiitaで書くために最初と最後だけ出力するようにした。)

## 実験2
「実験2で使用」と書かれている部分のコメントアウトを解除。
要は明示的にDispose()して使った端から開放しようとしている。
出力は次の通り。

> 現在のメモリ使用量はおよそ11MB、前回からの差分は11MBです。
ファイルの読み込みを行います。
現在のメモリ使用量はおよそ31MB、前回からの差分は20MBです。
100回ランダムなフレームを読み込みます。
現在のメモリ使用量はおよそ115MB、前回からの差分は84MBです。
何かキーを押すと終了します。

デバッガで見ても使用メモリが一定の値以上に増えている様子は無かった。

## 実験3
実験2で利用した部分をコメントアウトし、今度はGCを走らせる。プログラムから参照はしていないからうまくいくと思うんだが……

> 現在のメモリ使用量はおよそ11MB、前回からの差分は11MBです。
ファイルの読み込みを行います。
現在のメモリ使用量はおよそ31MB、前回からの差分は20MBです。
100回ランダムなフレームを読み込みます。
現在のメモリ使用量はおよそ707MB、前回からの差分は676MBです。
現在のメモリ使用量はおよそ707MB、前回からの差分は0MBです。
何かキーを押すと終了します。

デバッガで使用メモリ量を確認したら、26.6MBまで減っていた。GCが終わるまでSleep(1000)とかで待機すれば最後の使用量はそれぐらいまで落ちているはずだ。

# メモリリークの確認、その2

ここからは自分がusing宣言の使い方がよくわかっていないから、確認用に色々検証してみる。
CheckMemory関数周りは上のソースと同じなのでここからは割愛する。

## ソース2
ある意味ではこのソースがこの前作ったツールとほぼ同じ。

``` c#
        static void Main(string[] args)
        {
            Random r = new Random();
            CheckMemory();
            Console.WriteLine("ファイルの読み込みを行います。");
            VideoCapture cap = new VideoCapture(Program.testVideo);
            CheckMemory();
            Console.WriteLine("100回ランダムなフレームを読み込みます。");
            for (int i = 0; i < 100; i++)
            {
                int nextFrame = r.Next(0, cap.FrameCount);
                InfunctionTest(cap, nextFrame);
            }
            CheckMemory();
            Console.WriteLine("何かキーを押すと終了します。");
            Console.ReadKey();
        }

        static Mat InfunctionTest(VideoCapture cap, int frame)
        {
            Mat result = new Mat();
            cap.PosFrames = frame;
            cap.Read(result);
            return result;
        }
```

出力は以下の通り。まあ、実験1と同じだよね。
>現在のメモリ使用量はおよそ11MB、前回からの差分は11MBです。
ファイルの読み込みを行います。
現在のメモリ使用量はおよそ31MB、前回からの差分は20MBです。
100回ランダムなフレームを読み込みます。
現在のメモリ使用量はおよそ708MB、前回からの差分は677MBです。
何かキーを押すと終了します。

## ソース3
できるだけソース2から変更せず、メモリ使用量を減らしたい。

```c#

        static Mat InfunctionTest(VideoCapture cap, int frame)
        {
            using (Mat result = new Mat())
            {
                cap.PosFrames = frame;
                cap.Read(result);
                return result;
            }
        }
```

シンプルにusingで囲う。
> 現在のメモリ使用量はおよそ11MB、前回からの差分は11MBです。
ファイルの読み込みを行います。
現在のメモリ使用量はおよそ31MB、前回からの差分は20MBです。
100回ランダムなフレームを読み込みます。
現在のメモリ使用量はおよそ116MB、前回からの差分は85MBです。
何かキーを押すと終了します。

実験2とやってることは同じだから順当な結果。

## ソース4
ここまでは.Net Frameworkを使っていたが、ここからは.Net Coreを使う。そうすると比較的新しいC#を使えるから中括弧でくくらずに```using Mat result = new Mat();```でよくなる。
ではここから、ソース3でInfunctionTest()関数で生成したMatの戻り値を確認したい。
例えばfor文の中身をこう書き換える。

```c#
            for (int i = 0; i < 100; i++)
            {
                int nextFrame = r.Next(0, cap.FrameCount);
                int j = InfunctionTest(cap, nextFrame).Dims;

                // 動画フォルダ読み込み直後のメモリ使用量を確かめたい。
                if (i == 0)
                {
                    CheckMemory();
                }
            }
```

出力結果
> System.ObjectDisposedException: 'Cannot access a disposed object.'

そりゃあ関数から出る際に破棄してるんだから通らんわな。
でもできればfor文でInfuntionTestの引数を使いたい。

##ソース5
MatにはClone()関数がある。新しくMatを作って戻り値にすれば問題なく拾えるはず。

```c#
        static void Main(string[] args)
        {
            Random r = new Random();
            CheckMemory();
            Console.WriteLine("ファイルの読み込みを行います。");
            VideoCapture cap = new VideoCapture(Program.testVideo);
            CheckMemory();
            Console.WriteLine("100回ランダムなフレームを読み込みます。");
            for (int i = 0; i < 100; i++)
            {
                int nextFrame = r.Next(0, cap.FrameCount);
                using Mat resultMat = InfunctionTest(cap, nextFrame);
                int j = resultMat.Dims;

                // 動画フォルダ読み込み直後のメモリ使用量を確かめたい。
                if (i == 0)
                {
                    CheckMemory();
                }
            }
            Console.WriteLine("作業終了です。");
            CheckMemory();
            Console.WriteLine("何かキーを押すと終了します。");
            Console.ReadKey();
        }

        static Mat InfunctionTest(VideoCapture cap, int frame)
        {
            using Mat result = new Mat();
            cap.PosFrames = frame;
            cap.Read(result);
            return result.Clone();

        }
```

>現在のメモリ使用量はおよそ14MB、前回からの差分は14MBです。
ファイルの読み込みを行います。
現在のメモリ使用量はおよそ35MB、前回からの差分は21MBです。
100回ランダムなフレームを読み込みます。
現在のメモリ使用量はおよそ124MB、前回からの差分は89MBです。
作業終了です。
現在のメモリ使用量はおよそ121MB、前回からの差分は-3MBです。
何かキーを押すと終了します。

この作りで大丈夫そうだ。

##おまけ
最初にメモリリークの調査に利用したのは下記のサイト。メソッドをつなげるのは危なそうな気がする。
https://blog.mohyo.net/2015/03/1313/

ではこれは？(for文の中だけ)

```c#
            for (int i = 0; i < 100; i++)
            {
                int nextFrame = r.Next(0, cap.FrameCount);
                using Mat resultMat = InfunctionTest(cap, nextFrame).Threshold(100, 255, ThresholdTypes.Binary);
                int j = resultMat.Dims;

                // 動画フォルダ読み込み直後のメモリ使用量を確かめたい。
                if (i == 0)
                {
                    CheckMemory();
                }
            }
```

> 現在のメモリ使用量はおよそ14MB、前回からの差分は14MBです。
ファイルの読み込みを行います。
現在のメモリ使用量はおよそ35MB、前回からの差分は21MBです。
100回ランダムなフレームを読み込みます。
現在のメモリ使用量はおよそ125MB、前回からの差分は90MBです。
作業終了です。
現在のメモリ使用量はおよそ718MB、前回からの差分は593MBです。
何かキーを押すと終了します。

だめかー

# 結論

+ 外部のライブラリを利用する場合はDisposeの必要があるかをきちんと調べよう。
+ OpenCVSharpで画像処理をするときはきちんとusing宣言を利用するかDisposeしよう。
+ OpenCVSharpではメソッドチェーンは事実上使用不可能。