格ゲーの動画を解析したい 〜OpenCV勉強編〜 4フレーム目

前回、kotlin + OpenCVを使って動画をフレーム単位で連続表示する事が出来たので、今回は、実際にキャラクターを検出する事に挑戦してみる。
具体的には以下の赤枠の様にグラン君の立ち姿を検出したい。

といっても具体的にどうすればいいのかは、手探りなので今回は実験的に以下の4つをやってみる事にする。

  1. グレースケール化
  2. エッジ検出
  3. 輪郭検出
  4. 輪郭描画

と言いたい所だが、まずは動画の各フレームをすべて画像として書き出す事にした。
当面は手探りなので、動画ではなく画像1枚として処理した方が効率が良いからである。

動画をフレーム毎に書き出す。

さっそく、フレームを特定のフォルダに連番ファイル名で書き出すモジュールを作成して、念の為ユニットテストを書いた所、以下のエラーが発生した。
OpenCVのライブラリが見つからないとのこと。
前も見たなコレ。

no opencv_java430 in java.library.path: [/usr/java/packages/lib, /usr/lib64, /lib64, /lib, /usr/lib]
java.lang.UnsatisfiedLinkError: no opencv_java430 in java.library.path: [/usr/java/packages/lib, /usr/lib64, /lib64, /lib, /usr/lib]

build.gradle.ktsに以下の記述を追記して切り抜けた。

tasks.test {
    systemProperty("java.library.path", "/hoge/opencv-4.3.0/build/lib")
}

そうするとまた新しいエラーが現れました。
OpenCVのライブラリの中でSEGVが発生しているとの事。

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGSEGV (0xb) at pc=0x00007f0d0c339902, pid=2507, tid=2989
#
# JRE version: OpenJDK Runtime Environment (11.0.6+8) (build 11.0.6+8-b520.43)
# Java VM: OpenJDK 64-Bit Server VM (11.0.6+8-b520.43, mixed mode, tiered, compressed oops, g1 gc, linux-amd64)
# Problematic frame:
# C  [libopencv_java430.so+0x3d4902]  Java_org_opencv_core_Mat_n_1dims+0x1e
#
# ...中略...
#
> Task :test FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.
> Process 'Gradle Test Executor 6' finished with non-zero exit value 134
  This problem might be caused by incorrect test process configuration.
  Please refer to the test execution section in the User Manual at https://docs.gradle.org/6.3/userguide/java_testing.html#sec:test_execution

いろいろ試した所、メソッドをモック化する時に、OpenCVのクラスを受け取る引数にMatchersを使用すると発生する様です。

// モック化対象がこんなインターフェースだったとして
interface IImageWriter {
    fun test(img:Mat): Boolean
}

// こんな感じでモック化するとSEGVが発生する
every { mockWriter.test(any()) }

// ※第一引数のMatがOpenCVのクラス

さすがにSEGVの様な低レイヤのトラブルはミジンコには解決が難しい・・・

mockitoを導入する

モックを作るためのライブラリとしてmockkを採用してましたが、上記の問題が解決できそうにないのでmockitoを導入する事にしました。
幸いな事にmockito-kotlinというラッパーがあってイケてる構文(DSL)で書ける様です。

で、build.gradle.ktsのdependenciesを書き換えて再ビルドをしても、IntelliJが依存関係を解決してくれない。
調べた所、以下の画像の様に、Buildタブ -> Syncタブ -> 更新マーク(Reimport) と操作するのが成果らしい。

IntelliJで依存関係を再インポートする図

よっしゃこれでテストもバッチ・・・エラーが発生しました。
mockitoはfinalクラスのモック化はサポートしていないらしい
デフォルトがfinal classになるkotlinと相性悪いな・・・

Cannot mock/spy class jp.t_kuni.kotlin_app_skelton.infrastructures.ExampleRepository
Mockito cannot mock/spy because :
 - final class

PowerMockitoを導入すればfinal classもテスト出来るらしいがテストコードに記述が増えるっぽいので避けた。
テスト対象のクラスにopenを付ける事で対応した。

        val mockWriter = mock<IImageWriter> {
            on { write(any(), any()) } doReturn true
        }
        loadKoinModules(module {
            single(override = true) { mockWriter as IImageWriter }
        })

        val writer = SequentialImageWriter("/home/kuni/Videos")
        writer.write(Mat(1, 1, 1))
        writer.write(Mat(1, 1, 1))

        verify(mockWriter).write(eq("/home/kuni/Videos/0.bmp"), any())
        verify(mockWriter).write(eq("/home/kuni/Videos/1.bmp"), any())

新しい言語に挑戦するの、世間的には「似たパラダイムの言語ならすぐ覚えられる」というのが一般的だけども、実際にはエコシステムやライブラリの"クセ"みたいなのを一通りハマり倒さないと全然生産性が出ないよなーとかこの日記を書きながら思いました。
(ここまでで紆余曲折あって8hぐらい掛かってる泣)

全フレームを書き出す

さて、全フレームを書き出すモジュールのテストが終わったので、実際に書き出します。
こんな感じになりました。

動画の全フレームを画像として書き出した図。壮観ですねぇ

画像処理をやってみる。

全フレームの書き出しが出来たので、以下の画像処理をやってみます。

  1. グレースケール化
  2. エッジ検出
  3. 輪郭検出
  4. 輪郭描画

書き出した画像からグラン君の立ち姿が写っているシーンを1枚ピックアップ。
この画像に対して画像処理をやっていきます。

グレースケール化

こんな感じ。

Imgproc.cvtColor(orig, gray, Imgproc.COLOR_RGB2GRAY)

Cannyエッジ検出器によるエッジ検出

そしてこう。

Imgproc.Canny(blur, edges, 500.0, 600.0, 3, false)

輪郭検出

こう。

Imgproc.findContours(edges, contours, hierarchy, Imgproc.RETR_CCOMP, Imgproc.CV_CHAIN_CODE)

エラーが出た。ぐぬぬ。

Exception in thread "main" java.lang.IllegalArgumentException: Width (0) and height (0) must be > 0

原因がわからなくて一旦シンプルな画像で試すことにした。

輪郭検出テスト用画像

パラメータcontoursは輪郭を表す一連の座標
パラメータ hierarchyは輪郭間の親子関係を表す

hiererchyの値の例。[0]の1.0は、次の輪郭の添字を表している。[1]の-1.0は前の輪郭が存在しない事を表している。

試行錯誤行した所、行列(Mat)がちゃんと初期化されてないだけだった・・・。
これまで、Matに書き込むメソッドは自動的に領域を拡張してくれていたので意識してなかったが、drawContoursは領域が足りないと何もせずに次の処理に進んでしまうらしい。

// 誤(サイズ0の行列になる)
val contour = Mat()

// 正
val contour = Mat(800, 400, CvType.CV_8UC3)

で、輪郭を描画するとこんな感じ。

輪郭を描画した図

やっと輪郭の描画まで終わった・・・。
今日はここまでにしておこう。
ハマりすぎて疲れた・・・。

やってみて思ったのは、輪郭検出だけではグラン君の位置を特定するのは困難なように思えた。
参考にしている本の目次を眺めていると、背景除去動体検出という単語があるので、これらで大まかにオブジェクトの位置を特定してから、輪郭検出でそのオブジェクトが何なのかを特定するのがベターなのかなーと想像してます。

参考資料

紹介OpenCV

「格ゲーの動画を解析したい 〜OpenCV勉強編〜 4フレーム目」への2件のフィードバック

コメントする