OpenCV plus Unityで画像の背景を動的に削除する方法

#やりたいこと
[らくがきAR](https://whatever.co/post/rakugakiar/)の様なアプリを作りたいと思ったので、今回は手始めにOpenCVを使ってUnityにインポートした画像の背景を削除(透明化)します。
ソースコードは以下にアップロードしてあります。
https://github.com/AzetaTakuya/MakeImageBackgroundTransparentUsingOpenCV

#結果
こんな感じ
![結果画像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699230/f9d864a3-728e-8092-d6c6-8399aa9624be.png)
左から【元画像 -> グレースケール化 -> 二値化 -> マスク化 -> 背景削除】といった工程から画像の背景を透明化することができました。

#実装
###環境
- Windows10 Home
- Unity 2019.4.8f1
- [OpenCV plus Unity 1.7.1](https://assetstore.unity.com/packages/tools/integration/opencv-plus-unity-85928?locale=ja-JP)

##OpenCV plus Unity を試す
まずUnityプロジェクトを作成し、AssetStoreから[OpenCV plus Unity](https://assetstore.unity.com/packages/tools/integration/opencv-plus-unity-85928?locale=ja-JP)をインポートします。
![OpenCVplusUnityインポート.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699230/e965f6ed-7686-a720-9597-779796ea470f.png)
インポートが完了すると、エラー[<font color="Red"> error CS0227: Unsafe code may only appear if compiling with /unsafe. Enable "Allow 'unsafe' code" in Player Settings to fix this error.</font>]が出るかと思います。
![OpenCVplusUnityエラー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699230/d33d7d4c-8598-1bd6-c303-16d82c3a9d1c.png)
unsafeコードを使用するので 【File -> BuildSettings -> PlayerSettings -> Player -> OtherSettings -> Allow 'unsafe' Code】 にチェックを入れます。
エラーがなくなったので、早速OpenCVplusUnityのデモを見てみます。
今回は画像の輪郭を抽出したりしたいので、それに近いデモである【Assets -> OpenCV+Unity -> Demo -> Identifiy_Contours_by_Shape -> ContoursByShapeScene.scene】を開きます。
実行結果はこんな感じ。
![Demo実行結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699230/6e96d132-ca24-0d8d-a26b-8874e3be6501.png)
白黒画像に色とテキストが付きました。
![デモ結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699230/cec48a64-8d4e-d65c-84f8-a9a718403c1d.png)
このデモを元に画像の背景を透明化します。

##画像の背景を削除
###画像をインポート
Unityに画像ファイルをインポートします。そのままの状態では読み込めない為、Read/Write Enabledにチェックを入れます。
![テクスチャインポート.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699230/ede15e3a-9a4e-ef52-173e-1d2db18b6264.png)
###コード
元画像(m_texture)から
【元画像 -> グレースケール化画像 -> 輪郭抽出画像 -> マスク化画像 -> 背景透明化画像】を表示します。

```cs
namespace OpenCvSharp.Demo
{
    using UnityEngine;
    using System.Collections;
    using OpenCvSharp;
    using UnityEngine.UI;
    using System.Threading.Tasks;
    using System.Collections.Generic;

    public class MakeImageBackgroundTransparentUsingOpenCV : MonoBehaviour
    {
        #region public members
        public Texture2D m_texture;

        public RawImage m_image_origin;
        public RawImage m_image_gray;
        public RawImage m_Image_binarization;
        public RawImage m_image_mask;
        public RawImage m_image_backgroundTransparent;

        public double v_thresh = 180;
        public double v_maxval = 255;
        #endregion

        private void Start()
        {
            #region load texture
            Mat origin = Unity.TextureToMat(this.m_texture);
            m_image_origin.texture = Unity.MatToTexture(origin);
            #endregion

            #region  Gray scale image
            Mat grayMat = new Mat();
            Cv2.CvtColor(origin, grayMat, ColorConversionCodes.BGR2GRAY);
            m_image_gray.texture = Unity.MatToTexture(grayMat);
            #endregion

            #region Find Edge
            Mat thresh = new Mat();
            Cv2.Threshold(grayMat, thresh, v_thresh, v_maxval, ThresholdTypes.BinaryInv);
            m_Image_binarization.texture = Unity.MatToTexture(thresh);
            #endregion

            #region Create Mask
            Mat Mask = Unity.TextureToMat(Unity.MatToTexture(grayMat));
            Point[][] contours; HierarchyIndex[] hierarchy;
            Cv2.FindContours(thresh, out contours, out hierarchy, RetrievalModes.Tree, ContourApproximationModes.ApproxNone, null);
            for(int i = 0; i < contours.Length; i++)
            {
                Cv2.DrawContours(Mask, new Point[][] { contours[i] }, 0, new Scalar(0, 0, 0), -1);
            }
            Mask = Mask.CvtColor(ColorConversionCodes.BGR2GRAY);
            Cv2.Threshold(Mask, Mask, v_thresh, v_maxval, ThresholdTypes.Binary);
            m_image_mask.texture = Unity.MatToTexture(Mask);
            #endregion

            #region TransparentBackground
            Mat transparent = origin.CvtColor(ColorConversionCodes.BGR2BGRA);
            unsafe
            {
                byte* b_transparent = transparent.DataPointer;
                byte* b_mask = Mask.DataPointer;
                float pixelCount = transparent.Height * transparent.Width;

                for (int i = 0; i < pixelCount; i++)
                {
                    if (b_mask[0] == 255)
                    {
                        b_transparent[0] = 0;
                        b_transparent[1] = 0;
                        b_transparent[2] = 0;
                        b_transparent[3] = 0;
                    }
                    b_transparent = b_transparent + 4;
                    b_mask = b_mask + 1;
                }
            }
            m_image_backgroundTransparent.texture = Unity.MatToTexture(transparent);
            #endregion
        }

    }
}

```
###シーン
上のスクリプトをアタッチするGameObjectと5つのRawImageを用意します。
MakeImageBackgroundTransparentUsingOpenCVにインポートした画像とRawImageをアタッチします。
![シーン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699230/743b6388-b2f4-62f4-67d5-ad12ee2dc5db.png)
![インスペクタ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699230/c4a17db2-8ac2-1ca7-99cc-4c4c9626c2ea.png)

###実行
実行すると【元画像 -> グレースケール化画像 -> 輪郭抽出画像 -> マスク化画像 -> 背景透明化画像】が表示されます。
![実行.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699230/88996bf9-d72f-e3df-0060-7aacd5125d5b.png)
※うまくいかなかった場合はv_threshの値を弄ってみてください。

#まとめ
- ARアプリにガンガン使っていけそう。
- 白背景の画像以外も対応出来る様にする必要がある。

#参考
- [OpenCV/ARCore/Unityで作る塗り絵AR : 
Takashi Yoshinaga](https://www.slideshare.net/ssuserc0d7fb/opencvarcoreunityar)