先日から作っているダイスボットでは、Raspberry Pi上のAzure IoT Edgeデバイスで動作を処理したかった。
この際、OpenCVで処理すればどうにでもなるだろう+できれば全部C#(.NET)でプログラムを書きたい、と言った理由でOpenCVSharp*1を使用した。
ものはできたのだけれど、いくつか失敗したことがあるので記事にして記憶を強くしておこうと思う。
なお、OpenCVSharpでというのはたまたま使っていたのがそれだったというだけで、実際にはメモリ管理の失敗である。
動画のフレームはちゃんと破棄しないとメモリが溢れる
カメラから得た動画を少し加工して別の動画に出力する。といったことをしていた。
.NETにはGCがあるが、明示的にオブジェクトを破棄するためのIDisposableインタフェース*2というものがある。
OpenCVSharpの画像コンテナクラスはちゃんとこのIDisposableインタフェースを実装しているのだけれど、当初は「こんなもん適当でもそのうちGCされるやろ」とDisposeしていなかった。
するとどうだろう?
処理を開始してしばらくするとメモリ消費量が実装メモリを超えてものすごい勢いでスワップをはじめてRaspberry Piがフリーズした。
IDisoposableなオブジェクトをちゃんと破棄するというのは.NETプログラミングの初歩中の初歩中の初歩なのだけれど、多少さぼっても大丈夫だろう(実際問題が顕在化しない場合も多いはず)と舐めていたら死んだ。
マルチスレッドで同じメモリ領域を処理すると落ちる
そりゃそうだ。
カメラから定期的に画像を取得するスレッド、画像を編集するスレッド、画像を動画に書き出すスレッド、のような作りにしていたのだけれど、たまに「破棄済のメモリ領域を参照した」的なメッセージともにプロセスが落ちていた。
スレッド間で無造作に画像オブジェクトを引き渡していたのを、スレッドの境界をまたぐときは明示的に画像オブジェクトを複製するようにしたら落ちなくなった。
オブジェクトがどこで破棄されるかは意識していたつもりだったのだけれど、うまく処理できていなかったのだろう。
完全にマネージドな.NETの世界なら破棄済のオブジェクトを触っても例外するだけなのだろうけれど、今回はマネージドオブジェクトとしては生きていて、その先のOpenCV管理のメモリが解放されてしまっているっぽい雰囲気だった。そうするとプロセスが落ちる。これには例外を握りつぶしておけば処理継続すらできなくて困った。
教訓
プログラムのいろははとても大事
大事。
基本をおろそかにしたら微妙*3を超えて動かない作りになってしまった。
動くプログラムのために並列処理時には処理の競合にちゃんと気を付けようと思った。
OpenCV+.NET Coreで動画編集はいい選択ではない
多分。
これはハナから分かっていたけれど、想像以上に厳しい気がした。動作環境を整備するのもサッとはできない*4し、.NETで画像加工しようとすると.NET向けに一度メモリレイアウトを変換しなくてはならない。不毛なコピーで生理的に苦手*5。
実際Raspberry PiでのAzure IoT EdgeサンプルでもOpenCV + Pythonだ。 PythonならOpenCVの画像をそのままメモリ操作するような手法もやりやすい様子。向いてる言語・ライブラリ選択がいいのだろうと思う。
そもそもOpenCVを使う必要はなかったかもしれない。GIFアニメの生成やRTMP形式での動画配信には結局FFmpegを使ってしまった。めちゃくちゃ簡単だった。
とはいえ、試して色々得るものはあったのでよしとする。