OpenCVとUnityで背景差分によるChromakeyをやってみた

　クロマキー。よくテレビの天気予報とかでやっているあれですね。普通はグリーンバックといって、グリーンの色ムラのないカーテンのようなものを背景に使うのですが、今回はそのカーテンを使わずに背景差分を使ってクロマキーを実現しようということです。

## グリーンバックによるクロマキーの実現方式
　クロマキーを実現するには２つの画像（「A:背景として重ねる画像」と「B:前景となるものが写っている画像」）を用意します。「A:背景として重ねる画像」とは空撮映像とか天気予報図の映像とかですね。「B:前景となるものが写っている画像」とは、ウルトラマンとかお天気お姉さんとかですね。
　前提として「B:前景となるものが写っている画像」の実際の撮影場所には背景（透過させたい部位）にグリーンのカーテンがひかれています。そのため画像処理にてグリーン部位を透過させる（RGBAのAを0にするということ）ということができるわけです。最後にレイヤーの後ろに「A:背景として重ねる画像」を表示させることで完成です。簡単ですね。
　当然ながら、前景にグリーンがあるとそれも意図せず透過されてしまいます。（天気予報の映像にガチャピンがでてきてしまい、トラブルとなったことがありましたね）

## 背景差分によるクロマキーの実現方式
　「B:前景となるものが写っている画像」にグリーンのカーテンがありません。なのでどこを透過させるべきかがわかりません。そこで動かないものを透過させてしまえ。という考え方です。
　事前に基準画像（キャリブレーションといいます）を撮影しておきます。映像から連続の画像を取り出し、キャリブレーション画像との差分値を取り、差分がないもの（つまり、動かないもの）を透過させます。透過後の画像を「A:背景として重ねる画像」と組み合わせることでクロマキーを実現します。グレースケールではなくRGBそれぞれのの差分値を取ることでより細かいレベルでの透過のマスクを作り出すことができます。


## 必要なソフトウエア
* Unity
* Visual Studio
    * C#を使います
    * Unity付属のMonoDevelopでもいいのですが、開発環境として定評のあるVisualStudioの方が便利だと思います。
* Windows or Mac
* カメラ
    * PC付属のカメラでもいいのですが高解像の方がおもしろいです。私は一眼レフ(EOS Kiss x7)を使いました。

## 必要なライブラリ
* [OpenCV for Unity(有料)](https://www.assetstore.unity3d.com/jp/#!/content/21088)
    * これは内部でオープンソースのOpenCVが同梱されており、Unityを使って簡単にC#から使えるようにするラッパーでもありライブラリでもあります。
    * サンプルが非常に優良です。OpenCV+Unityを使ってできるたくさんのサンプルがあります。サンプルをちょっとカスタマイズするだけで何かができてしまいそうな感じです。
* [AVPro Live Camera(有料)](https://www.assetstore.unity3d.com/jp/#!/content/3683)
    * 目的とは直接関係ないですが、HDでやってみたかったのでこれを利用しました。
    * AVProLiveCameraは、専用ドライバを持つHD対応のカメラです。Unityでは標準でWebCamTextureがあるのですが、解像度の問題を考えるとプロダクトにはなりにくい面があります。プロダクトとして高解像度の映像を提供する場合は一眼レフ等のカメラとの連携が必要になりますよね。その時には使っていきましょう。

## 基本的な処理の流れ
* void Update()
    * カメラ映像を抜き出し、MAT型に変換する。
    * キャリブレーション画像を決定する。（コード例ではスペースキー押下時）
    * BackgroundSubtractorMOG2メソッドを使って、前景と背景の背景差分を導出する。
    * HDレベルだと、非常に細かいもの（チリ、ゴミ、影等々）が差分としてでてしまうので、画像をぼやかしてから２値化する。
    * 生成したマスク画像を反転させ、レイヤとして重ねると完成。

コードだと以下のような感じになります。

```csharp:
void Update()
{
        if (_liveCamera == null || _liveCamera.OutputTexture == null)
        {
            return;
        }

        // カメラ映像の縦横を取得しておく
        int screen_width = _liveCamera.OutputTexture.width;
        int screen_height = _liveCamera.OutputTexture.height;

        if (mat_calibration == null)
        {
            // キャリブレーション画像の領域をRGBAで初期化しておく。
            mat_calibration = new Mat(screen_height, screen_width, CvType.CV_8UC4);
            texture = new Texture2D(mat_calibration.cols(), mat_calibration.rows(), TextureFormat.RGBA32, false);
        }
        // openCV系のライブラリをUpdate()の中で使う場合にはusingを多用してリソースをちゃんとリリースさせたほうがよい。すぐ落ちるから。
        // カメラ画像もRGBAを想定して４チャンネル分を確保しておく。
        using (Mat mat_livecamera = new Mat(screen_height, screen_width, CvType.CV_8UC4))
        {
            // カメラ画像はTextureで取れるので、それをMAT型に変換しOpenCVで画像処理しやすいようにする。
            Utils.textureToMat(_liveCamera.OutputTexture, mat_livecamera);
            Mat mat_bg_rgba = null;
            if (_media.TextureProducer != null)
            {
                mat_bg_rgba = new Mat(_media.TextureProducer.GetTexture(0).height, _media.TextureProducer.GetTexture(0).width, CvType.CV_8UC4);
                Utils.textureToMat(_media.TextureProducer.GetTexture(0), mat_bg_rgba);
                Imgproc.resize(mat_bg_rgba, mat_bg_rgba,new Size(screen_width, screen_height));
                // 画像データ（数値データ）を反転させる(0->255へ。255->0へ)
                Core.flip(mat_bg_rgba, mat_bg_rgba, 0);
            }
            // スペースキーが押下されたら、いまカメラに写っている画像をキャリブレーション画像として置き換える。
            if (Input.GetKeyUp(KeyCode.Space) || Input.touchCount > 0)
            {
                mat_livecamera.copyTo(mat_calibration);
            }

            using (Mat fgMaskMat = new Mat(screen_height, screen_width, CvType.CV_8UC1))
            using (Mat bgMaskMat = new Mat(screen_height, screen_width, CvType.CV_8UC1))
            using (BackgroundSubtractorMOG2 mog2 = OpenCVForUnity.Video.createBackgroundSubtractorMOG2())
            {
                // BackgroundSubtractorMOG2メソッドを使って背景差分を導出する
                mog2.apply(mat_calibration, fgMaskMat);
                mog2.apply(mat_livecamera, fgMaskMat);

                // 場合によっては2値化させる前に画像をぼやかして、ちいさい差分を消す。（なかったことにする）
                //Imgproc.medianBlur(fgMaskMat, fgMaskMat, 10);
                // 透過させる場所をはっきりさせるため、2値下する。
                Imgproc.threshold(fgMaskMat, fgMaskMat, 100, 255, Imgproc.THRESH_BINARY);

                // マスク画像をつかって画像を反転させる。
                Core.bitwise_not(fgMaskMat, bgMaskMat);

                mat_bg_rgba.copyTo(mat_livecamera, bgMaskMat);
                // 完成した画像をMAT型から元のTexture型に変換する。
                Utils.matToTexture(mat_livecamera, texture);
            }
        }
        ApplyMapping(texture);
    }
}
```

## 苦労した点
* なめらかな映像にするために、OpenCVの行列系メソッドを使いこなす必要がありました。例えば差分取得において単純な＋ーで求めるやり方もありますが、チャネル数x画素数分のループ処理となりますので1回のUpdate()が遅くなります。そうすると、カクカクした映像となり、30fpsはでなくなってしまう時もありました。