格ゲーの動画を解析したい 〜背景除去編〜 5フレーム目

前回、輪郭を検出することでキャラクタの位置を取得しようとしたが難しそうだったので、今回は背景除去をやってみる事にしました。
まずは簡単なフレーム差分法を試してみます。
これは、あるフレームを別のフレームから減算して大きな差分がある箇所を前景(それ以外を背景)とする方法です。

フレーム差分法をやってみる

以下の2枚の画像を使います。
画像2は画像1の数フレーム後の画像です。

画像1
画像2(画像①の数フレーム後の画像)

2画像間の差分を計算する

コードは以下の通り。
実際に差分を計算しているのはCore.absdiff

        val orig1 = Imgcodecs.imread("/1516.bmp")
        val orig2 = Imgcodecs.imread("/1520.bmp")
        val diff = Mat()

        Imgproc.cvtColor(orig1, orig1, Imgproc.COLOR_RGB2GRAY)
        Imgproc.cvtColor(orig2, orig2, Imgproc.COLOR_RGB2GRAY)
        Core.absdiff(orig1, orig2, diff)
        Imgproc.threshold(diff, diff, 15.0, 255.0, Imgproc.THRESH_BINARY)

        showWindow()
        showImage(diff)

差分を描画するとこんな感じになった。
黒が差分の少ない箇所で、白が差分の大きい箇所です。
キャラクタ付近が白の領域が多いのは分かるけどノイズも多い。

画像の差分を描画した図

差分のノイズを除去する

モルフォロジー変換のオープニングクロージングをする事で、小さなノイズを除去できるらしい。
白い領域に対して、収縮、膨張、膨張、収縮という変換を行うことで小さい領域を潰すテクニックらしい。
Imgproc.erodeで膨張、Imgproc.dilateで収縮する

        val kernel = Imgproc.getStructuringElement(Imgproc.CV_SHAPE_RECT, Size(3.0, 3.0))
        Imgproc.erode(diff, diff, kernel)
        Imgproc.dilate(diff, diff, kernel)
        Imgproc.dilate(diff, diff, kernel)
        Imgproc.erode(diff, diff, kernel)

ノイズ除去後はこんな感じになった。
たしかに小さな白い領域が消えてる。

差分の画像からノイズを除去した図

輪郭を取得する

白い領域の輪郭を取得し、小さな輪郭は捨てます。
Imgproc.findContoursで輪郭を取得している。

        val contours = mutableListOf<MatOfPoint>()
        val hierarchy = Mat()
        Imgproc.findContours(diff, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE)

        val buffer = Mat(diff.height(), diff.width(), CvType.CV_8UC3)
        val q = 0.05 * diff.width() * diff.height()
        for (contour in contours)
        {
            if (Imgproc.contourArea(contour) > q) {
                Imgproc.drawContours(buffer, listOf(contour), 0, Scalar(255.0, 255.0, 255.0))
            }
        }

輪郭を描画するとこんな感じになった。
なかなかいい感じにキャラクタの位置を抽出できてる気がする・・・!

輪郭を抽出した図

輪郭を凸包に変換してみる

上記の輪郭は細かすぎる気がするので凸包に変換してみる。
ちなみに凸包とは、与えられた点をすべて包含する最小の多角形との事。
convexHullメソッドが使いにくくてラッパーを作った。

    fun hull(contour: MatOfPoint): MatOfPoint
    {
        val indexes = MatOfInt()
        Imgproc.convexHull(contour, indexes)

        val contourList = contour.toList()
        val hullList = ArrayList<Point>()
        for (i in indexes.toList())
        {
            hullList.add(contourList[i])
        }
        val hull = MatOfPoint()
        hull.fromList(hullList)
        return hull
    }

こんな感じ。トゲトゲしかった輪郭がキュートになった。

輪郭を凸包に変換した図。ぷよぷよかな?

輪郭をポリゴン近似してみる

こちらは、輪郭をポリゴン近似してみた。
Imgproc.approxPolyDPを使う

    fun poly(contour: MatOfPoint): MatOfPoint
    {
        val polyf = MatOfPoint2f()
        val poly = MatOfPoint()
        val contourf = MatOfPoint2f()
        contour.convertTo(contourf, CvType.CV_32F)
        Imgproc.approxPolyDP(contourf, polyf, 7.0, true)
        polyf.convertTo(poly, CvType.CV_32S)
        return poly
    }

こんな感じ。

輪郭をポリゴンで近似した図

今日はここまで。

所感

輪郭周りのオペレーションは慣れてきた感がある。
前景が取得できる様になったので、どのキャラクターなのか?、何をしているのか?と言ったマッチングを考えなきゃいけない。
つかキャラクター同士が接触した時の背景除去が難しそう(差分が結合するので)

参考資料

紹介OpenCV

コメントする