ナノカ技術メモ

ただのエンジニア。何でも屋みたいな扱い受けてます。

OpenCV for Unityでぼかし処理

このページではOpenCVをUnityで扱うためのAsset「OpenCV for Unity」を利用して画像加工を行う手順をメモしています。
こちらはUnity5.4.1で作業しました。

※修正点、改善点ありましたらコメントまたは編集リクエストをよろしくお願いします。

開発準備

  • 開発を行うためにAssetをImportしてください。
    madgenius.hateblo.jp
  • こちらのソースを使用して進めます。
    madgenius.hateblo.jp
  • ソースのSimpleScript.csのUpdate()メソッドにある「/* 画像加工開始 */」と「/* 画像加工終了 */」の間を修正していくのでその中身のみを書いていきます。
  • カメラ映像のMatはcvCamMatという変数で取得し、cvCamMatをTexture2Dに変換して描画しています。
  • 画面に映るデフォルトの状態はこちらです。
    わかりやすくするために描画部分のみ比べるようにします。
    f:id:nanokanato:20170412155523p:plain:w100 → f:id:nanokanato:20170412155642p:plain:w200

Median Blur

f:id:nanokanato:20170412155733p:plain:w300

Imgproc.medianBlur(cvCamMat, cvCamMat, 41);

第一引数:入力Mat
第二引数:出力Mat
第三引数:アパーチャサイズ(各ピクセル近傍領域の直径)
     ・大きくなるとぼかしが強くなる(1より大きな奇数)

ガウシアンフィルタ

f:id:nanokanato:20170412155754p:plain:w300

Imgproc.GaussianBlur(cvCamMat, cvCamMat, new Size(41, 41), 0.0, 0.0);

第一引数:入力Mat
第二引数:出力Mat
第三引数:ガウシアンカーネルのサイズ
     ・大きくなるとぼかしが強くなる(1より大きな奇数)
第四引数:X方向の標準偏差
第五引数:Y方向の標準偏差

ボックスフィルタ

f:id:nanokanato:20170412155905p:plain:w300

Imgproc.boxFilter(cvCamMat, cvCamMat, cvCamMat.depth(), new Size(41, 41));

第一引数:入力Mat
第二引数:出力Mat
第三引数:デプス(Matのデータ型)
第四引数:平滑化カーネルのサイズ
     ・大きくなるほどほかしが強くなる(1より大きな奇数)

バイラテラルフィルタ(失敗)

f:id:nanokanato:20170412155642p:plain:w300

Imgproc.cvtColor (cvCamMat, cvCamMat, Imgproc.COLOR_RGBA2BGR);
Mat bgrMat = new Mat();
Imgproc.bilateralFilter (cvCamMat, bgrMat, 41, 0.0, 0.0);
Imgproc.cvtColor(bgrMat, cvCamMat, Imgproc.COLOR_BGR2RGBA); 

第一引数:入力Mat
第二引数:出力Mat
第三引数:各ピクセル近傍領域の直径
     ・大きくなるほどほかしが強くなる
第四引数:色空間におけるフィルタシグマ
     ・大きくなるほど色的により遠くのピクセルが混ぜ合わさる
第五引数:座標空間におけるフィルタシグマ
     ・大きくなるほど距離的により遠くのピクセル同士が影響しあう

「Utils.setDebugMode(true);」でOpenCVのネイティブ側で発生したエラーの出力ができる。

imgproc::bilateralFilter_11() : /Users/satoo/opencv/mac/3.1/opencv-3.1.0/modules/imgproc/src/smooth.cpp:3228: error: (-215) (src.type() == CV_8UC1 || src.type() == CV_8UC3) && src.data != dst.data in function bilateralFilter_8u

第一引数と第二引数は同じMatだとダメみたいです。
別のMatを用意して再度実行したら処理は重くなりましたが、描画される画像に変化は見られませんでした。

ミーンシフトフィルタ(失敗)

f:id:nanokanato:20170412155642p:plain:w300

Imgproc.cvtColor (cvCamMat, cvCamMat, Imgproc.COLOR_RGBA2BGR);
Mat bgrMat = new Mat();
Imgproc.pyrMeanShiftFiltering(cvCamMat, bgrMat, 11, 11);
Imgproc.cvtColor(bgrMat, cvCamMat, Imgproc.COLOR_BGR2RGBA); 

第一引数:入力Mat
第二引数:出力Mat
第三引数:空間窓の半径?
第四引数:色空間窓の半径?

バイラテラルフィルタと同じく処理は重いが動作しない。
引数の数値が正しくないとかが原因かもしれない。

その他

他のバイラテラルフィルタや平均値シフト法でのぼかしも挑戦しましたが動作しないため現在記載しておりません。

  • 処理速度(上と同じ、ぼかし度:41、画像サイズ:750x750pxで比較)
順位 ぼかし方法 体感速度
1位 ボックスフィルタ ほぼリアルタイム
2位 ガウシアンフィルタ 2秒ほど遅れを感じる
3位 Median Blur 3秒ほど遅れを感じる

※バイラテラルフィルタは順位は4位だが変化がないため除外

ちゃんとぼかされていてある程度のリアルタイム性が必要であればボックスフィルタが有能?
引数や元Matサイズを調整することで変わることがあると思います。

WebCamTextureの使い方(AspectFill)

WebCamTextureについてまだ書いてなかったのでメモ程度で

使い方

とりあえず以下のNKWebCamTexture.csをプロジェクトに追加して適当なGameObjectに追加。
rawImageにUI.RawImageを設定すると画面にカメラの映像が出てくる。
f:id:nanokanato:20170412153632p:plain:w300

/*
 * NKWebCamTexture.cs
 * 
 * Copyright (c) 2016 by nanokanato All rights reserved.
 * Created by nanokanato on 2016/11/07.
 * 
 * Blog:http://madgenius.hateblo.jp/
 * Twitter:https://twitter.com/nanokanato
 * 
 */
using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class NKWebCamTexture : MonoBehaviour {

	public RawImage rawImage;

	private RectMask2D rectMask;
	private WebCamTexture webCamTexture;

	/*---------------------------*
	 * Default Method
	 *---------------------------*/
	// Use this for initialization
	void Start () {
		//カメラの初期化
		initWebCamTexture ();
	}
	
	// Update is called once per frame
	void Update () {
		
	}

	/*---------------------------*
	 * Load Method
	 *---------------------------*/
	//WebCamTextureの初期化
	private void initWebCamTexture()
	{
		if (rawImage) {
			if (!rectMask) {
				//RectMask2Dの初期化
				initRectMask2D ();
			}

			//Webカメラの取得
			if (WebCamTexture.devices.Length > 0) {
				//Webカメラ情報の取得
				WebCamDevice webCamDevice = WebCamTexture.devices [0];

				//WebCamTextureの作成
				RectTransform maskTransform = rectMask.GetComponent<RectTransform> ();
				webCamTexture = new WebCamTexture (webCamDevice.name, (int)maskTransform.sizeDelta.x, (int)maskTransform.sizeDelta.y);

				//WebCamTextureをrawImageで表示する
				rawImage.material.mainTexture = webCamTexture;

				//WebCamTextureの撮影開始
				webCamTexture.Play ();

				StartCoroutine ("waitWebCamFrame");
			} else {
				Debug.LogError ("Webカメラが認識できません");
			}
		} else {
			Debug.LogError ("RawImageが指定されていません");
		}
	}

	//カメラの画像が読み込まれるのを待つ
	private IEnumerator waitWebCamFrame(){
		while (true) {
			yield return new WaitForSeconds(0.2f);
			if (webCamTexture.isPlaying) {	
				if (webCamTexture.didUpdateThisFrame) {
					//rawImageの比率を修正
					RectTransform rawImageTransform = rawImage.GetComponent<RectTransform> ();
					Vector2 maskVector2 = rectMask.GetComponent<RectTransform> ().sizeDelta;
					float width = webCamTexture.width;
					float height = webCamTexture.height;
					if (width > height) {
						//横長の場合は縦を基準に横に伸ばす
						height = maskVector2.y;
						width = (maskVector2.y / maskVector2.x) * webCamTexture.width;
					} else {
						//縦長の場合は横を基準に縦に伸ばす
						width = maskVector2.x;
						height = (maskVector2.x / maskVector2.y) * webCamTexture.height;
					}
					rawImageTransform.sizeDelta = new Vector2 (width, height);
					break;
				}
			}
		}
	}

	//RectMask2Dの初期化
	private void initRectMask2D()
	{
		if (!rectMask) {
			if (rawImage) {
				//rawImageの親がRectMask2Dか判別
				if (rawImage.transform.parent.GetComponent<RectMask2D> ()) {
					//rawImageの親がRectMask2D
					rectMask = rawImage.transform.parent.GetComponent<RectMask2D> ();
				} else {
					//rawImageの親がRectMask2Dじゃないので作成する
					GameObject rectMask2D = new GameObject ("NKWebCamTextureMask");
					rectMask2D.transform.SetParent (rawImage.transform.parent);
					rectMask2D.AddComponent<RectMask2D> ();
					//RectMask2Dの位置はrawImageを元に設定
					RectTransform maskTransform = rectMask2D.GetComponent<RectTransform> ();
					RectTransform rawImageTransform = rawImage.GetComponent<RectTransform> ();
					maskTransform.sizeDelta = rawImageTransform.sizeDelta;
					maskTransform.rotation = rawImageTransform.rotation;
					maskTransform.position = rawImageTransform.position;
					maskTransform.pivot = rawImageTransform.pivot;
					maskTransform.offsetMin = rawImageTransform.offsetMin;
					maskTransform.offsetMax = rawImageTransform.offsetMax;
					rawImage.transform.SetParent (rectMask2D.transform);
					rectMask = rectMask2D.GetComponent<RectMask2D> ();
				}
			} else {
				Debug.LogError ("RawImageが指定されていません");
			}
		}
	}
}

解説

  1. 「void Start()」でWebCamTextureを初期化するための「initWebCamTexture()」を呼び出し
  2. 「initRectMask2D()」でWebカメラ画像をマスクして好きな比率に変更
  3. 「StartCoroutine ("waitWebCamFrame");」でコルーチンとして「waitWebCamFrame()」が呼ばれる。
  4. 「waitWebCamFrame()」でWebカメラ画像を取得できた時、アスペクト比を守るようにrawImageをリサイズ

OpenCV for Unityでカメラ映像を描画

このページではOpenCVをUnityで扱うためのAsset「OpenCV for Unity」を利用したカメラの機能で、画面に映像を描画するまでの手順をメモしています。
こちらはUnity5.4.1で作業しました。

MacBookPro OS.Sierraに付いているWebカメラ(インカメ)で撮影したもの。
リアルタイムでMatで取得し描画しているのでフィルタや認識などに転用も可能。
f:id:nanokanato:20170412152915p:plain:w300

開発準備

  • カメラ機能を使いますのでカメラを用意してください。

Scriptの追加

OpenCVでは画像をMatという形式で処理します。
ここではWebCamTextureをMatにしたり、MatをTexture2Dに変換してくれるCVCameraMat.csと、Matを受け取り加工処理をした後Texture2Dを画面に表示するSimpleScript.csを用意します。
プロジェクトごとに修正するのはSimpleScript.csになります。

WebCamTextureとはWebカメラの映像をUnityで扱えるテクスチャにしたものです。
madgenius.hateblo.jp

CVCameraMat.cs

先ほども書きましたが、WebCamTextureを初期化しMatに変換、加工後にMatを渡すことでTexture2Dを返してくれるクラスです。
メソッド内の処理や役割はコメントアウトしております。

  • public変数の説明
    • requestDeviceName - 希望のカメラ端末名
    • flipVertical - trueの時、上下反転する
    • flipHorizontal - trueの時、左右反転する
    • OnInitedEvent - 初期化後に呼ぶイベントを設定できる
    • OnDisposedEvent - 破棄後に呼ぶイベントを設定できる
    • requestWidth - 希望の横サイズ
    • requestHeight - 希望の縦サイズ

requestWidth,requestHeightで指定している希望の縦サイズ,横サイズは、ソース内では(768,768)がデフォルトになっていますが、カメラ機器により受け取れる比率が違うのかそれに近い別のサイズが受け取れます。

csharp:CVCameraMat.cs
using UnityEngine;
using System.Collections;

using System;
using OpenCVForUnity;
using UnityEngine.Events;

public class CVCameraMat : MonoBehaviour
{
    public string requestDeviceName = null;
    public bool flipVertical = false;
    public bool flipHorizontal = false;

    public UnityEvent OnInitedEvent;
    public UnityEvent OnDisposedEvent;

    public int requestWidth = 768;
    public int requestHeight = 768;

    private WebCamTexture webCamTexture;
    private WebCamDevice webCamDevice;

    private Mat rgbaMat;
    private Mat rotatedRgbaMat;
    private Color32[] colors;
    private bool initDone = false;
    private ScreenOrientation screenOrientation = ScreenOrientation.Unknown;

    /*------------------------------------------*
     * Default Method
     *------------------------------------------*/
    // Use this for initialization
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (initDone)
        {
            if (screenOrientation != Screen.orientation)
            {
                //角度が変わったので再度初期化
				Init();
            }
        }
    }

    /*------------------------------------------*
     * Init Method
     *------------------------------------------*/
    //設定せず初期化を開始
    public void Init()
    {
        if (OnInitedEvent == null)
        {
            OnInitedEvent = new UnityEvent();
        }
        if (OnDisposedEvent == null)
        {
            OnDisposedEvent = new UnityEvent();
        }
		webCamInit ();
    }

    //設定して初期化を開始
    public void Init(string deviceName, int requestWidth, int requestHeight)
    {
        this.requestDeviceName = deviceName;
        this.requestWidth = requestWidth;
        this.requestHeight = requestHeight;
        Init();
    }

    //初期化
    private void webCamInit()
    {
		//すでに初期化されている時は一旦解放する
		if (initDone) {
			Dispose ();
		}

		//カメラの希望があるかどうか確認する
		if (!String.IsNullOrEmpty (requestDeviceName)) {
			//使用できるカメラを参照する
			for (int cameraIndex = 0; cameraIndex < WebCamTexture.devices.Length; cameraIndex++) {
				//希望のカメラと同じカメラがあったらそれを使用する
				webCamDevice = WebCamTexture.devices [cameraIndex];
				if (webCamDevice.name == requestDeviceName) {
					webCamTexture = new WebCamTexture (requestDeviceName, requestWidth, requestHeight);
				}
			}
		}

		//希望のカメラが無かった場合
		if (webCamTexture == null) {
			//一番最初に認識したカメラを使用する
			if (WebCamTexture.devices.Length > 0) {
				webCamDevice = WebCamTexture.devices [0];
				webCamTexture = new WebCamTexture (webCamDevice.name, requestWidth, requestHeight);
			} else {
				webCamTexture = new WebCamTexture (requestWidth, requestHeight);
			}
		}

        //カメラの準備ができたかどうか確認
		if (webCamTexture) {
			//準備したカメラから映像の取得を開始する
			webCamTexture.Play ();

			//最初の撮影フレームを取得するまでコルーチンで待機
			GameObject coroutineObj = new GameObject("waitWebCamTexture");
			CVCameraMat coroutine = coroutineObj.AddComponent<CVCameraMat> ();
			coroutine.StartCoroutine(waitWebCamFrame(coroutineObj));
		} else {
			//カメラの準備ができなかったので再度初期化する
			webCamInit ();
		}
    }

	//Webカメラの最初のフレームが取得できるまで待機
	public IEnumerator waitWebCamFrame(GameObject coroutine)
	{
		while (true) {
			if (webCamTexture) {
				if (webCamTexture.isPlaying) {
					if (webCamTexture.didUpdateThisFrame) {
						colors = new Color32[webCamTexture.width * webCamTexture.height];
						rgbaMat = new Mat (webCamTexture.height, webCamTexture.width, CvType.CV_8UC4);

						screenOrientation = Screen.orientation;

						initDone = true;

						if (OnInitedEvent != null) {
							OnInitedEvent.Invoke ();
						}

						Destroy(coroutine);
						break;
					}
				}
			}
			yield return new WaitForSeconds (1);
		}
	}

    //初期化したかどうか
    public bool isInited()
    {
        return initDone;
    }

    //破棄処理
    public void Dispose()
    {
        initDone = false;

        if (webCamTexture != null)
        {
            webCamTexture.Stop();
            webCamTexture = null;
        }
        if (rgbaMat != null)
        {
            rgbaMat.Dispose();
            rgbaMat = null;
        }
        if (rotatedRgbaMat != null)
        {
            rotatedRgbaMat.Dispose();
            rotatedRgbaMat = null;
        }
        colors = null;

        if (OnDisposedEvent != null)
            OnDisposedEvent.Invoke();
    }

    /*------------------------------------------*
     * WebCamTexture Method
     *------------------------------------------*/
    //WebCamTextureの撮影開始
    public void Play()
    {
        if (initDone)
        {
            webCamTexture.Play();
        }
    }

    //WebCamTextureの停止
    public void Pause()
    {
        if (initDone)
        {
            webCamTexture.Pause();
        }
    }

    //WebCamTextureの撮影終了
    public void Stop()
    {
        if (initDone)
        {
            webCamTexture.Stop();
        }
    }

    //WebCamTextureが初期化されているか
    public bool isPlaying()
    {
        if (!initDone)
        {
            return false;
        }
        return webCamTexture.isPlaying;
    }

    //WebCamTextureを返す
    public WebCamTexture GetWebCamTexture()
    {
        return webCamTexture;
    }

    //WebCamDeviceを返す
    public WebCamDevice GetWebCamDevice()
    {
        return webCamDevice;
    }

    //WebCamTextureが最後のフレームから更新されているかを返す
    public bool didUpdateThisFrame()
    {
        if (!initDone)
        {
            return false;
        }
        return webCamTexture.didUpdateThisFrame;
    }

    //WebCamTextureをMatに変換して返す
    public Mat GetMat()
    {
        if (!initDone || !webCamTexture.isPlaying)
        {
            if (rotatedRgbaMat != null)
            {
                return rotatedRgbaMat;
            }
            else
            {
                return rgbaMat;
            }
        }

        if (rgbaMat == null)
        {
            rgbaMat = new Mat(webCamTexture.height, webCamTexture.width, CvType.CV_8UC4);
        }

        Utils.webCamTextureToMat(webCamTexture, rgbaMat, colors);

        int flipCode = int.MinValue;

        if (webCamDevice.isFrontFacing)
        {
            if (webCamTexture.videoRotationAngle == 0)
            {
                flipCode = 1;
            }
            else if (webCamTexture.videoRotationAngle == 90)
            {
                flipCode = 0;
            }
            if (webCamTexture.videoRotationAngle == 180)
            {
                flipCode = 0;
            }
            else if (webCamTexture.videoRotationAngle == 270)
            {
                flipCode = 1;
            }
        }
        else
        {
            if (webCamTexture.videoRotationAngle == 180)
            {
                flipCode = -1;
            }
            else if (webCamTexture.videoRotationAngle == 270)
            {
                flipCode = -1;
            }
        }

        if (flipVertical)
        {
            if (flipCode == int.MinValue)
            {
                flipCode = 0;
            }
            else if (flipCode == 0)
            {
                flipCode = int.MinValue;
            }
            else if (flipCode == 1)
            {
                flipCode = -1;
            }
            else if (flipCode == -1)
            {
                flipCode = 1;
            }
        }

        if (flipHorizontal)
        {
            if (flipCode == int.MinValue)
            {
                flipCode = 1;
            }
            else if (flipCode == 0)
            {
                flipCode = -1;
            }
            else if (flipCode == 1)
            {
                flipCode = int.MinValue;
            }
            else if (flipCode == -1)
            {
                flipCode = 0;
            }
        }

        if (flipCode > int.MinValue)
        {
            Core.flip(rgbaMat, rgbaMat, flipCode);
        }


        if (rotatedRgbaMat != null)
        {

            using (Mat transposeRgbaMat = rgbaMat.t())
            {
                Core.flip(transposeRgbaMat, rotatedRgbaMat, 1);
            }

            return rotatedRgbaMat;
        }
        else
        {
            return rgbaMat;
        }
    }

	/*------------------------------------------*
     * OpenCV Support Method
     *------------------------------------------*/
	//MatをTexture2Dに変換して反映する
	public void matToTexture2D(Mat mat, Texture2D texture)
	{
		Utils.matToTexture2D (mat, texture, colors);
	}
}

SimpleScript.cs

CVCameraMat.csを実際に使用しているクラスです。
主にRawImageへ描画するTexture2Dの作成と紐付けを行なっています。

Start()でCVCameraMatの初期化
OnCVCameraMatInited()で初期化終了を受け取り、取得したMatに合わせたTexture2Dの作成とRawImageへの紐付け
Update()でMatを受け取り加工してTexture2Dへ更新

  • public変数の説明
    • rawImage - カメラ映像を描画するRawImage(Texture2Dが使えれば修正可)
csharp:SimpleScript.cs
using UnityEngine;
using UnityEngine.UI;
using OpenCVForUnity;

[RequireComponent(typeof(CVCameraMat))]
public class SimpleScript : MonoBehaviour {

    public RawImage rawImage;
	private CVCameraMat cvCameraMat;
    private Texture2D texture;
	private bool startCVCam = false;

    /*--------------------------------
     : Default Method
     --------------------------------*/
    // Use this for initialization
    void Start()
    {
		Init();
    }
		
    // Update is called once per frame
    void Update()
	{
		//カメラ画像の表示先があるか確認
		if (rawImage) {
			//カメラの取得準備ができているか確認
			if (startCVCam) {
				//CVCameraMatの初期化が終了している
				if (cvCameraMat) {
					//CVCameraMatが撮影中か確認
					if (cvCameraMat.isPlaying ()) {
						//CVCameraMatのフレームが更新されているか確認
						if (cvCameraMat.didUpdateThisFrame ()) {
							//Texture2Dが初期化されているか確認
							if (texture != null) {
								//カメラのMat画像を取得
								Mat cvCamMat = cvCameraMat.GetMat ();
								//Matが取得できているか確認
								if (cvCamMat != null) {
									//Matが空のデータじゃないか確認
									if (!cvCamMat.empty ()) {

										/* 画像加工開始 */



										/* 画像加工終了 */

										//加工したMatが存在するか確認
										if (cvCamMat != null) {
											//Matが空のデータじゃないか確認
											if (!cvCamMat.empty ()) {
												try {
													//cvCamMatをTexture2Dに変換して反映する
													cvCameraMat.matToTexture2D (cvCamMat, texture);
												} catch (System.ArgumentException e) {
													Debug.Log (e.Message);
												} catch {
													Debug.Log ("OtherError");
												}
											}
										}
									}
									cvCamMat = null;
								}
							}
						}
					}
				}
			}
		} else {
			Debug.LogError("NotFound:rawImage");
		}
	}

    /*--------------------------------
     : Load Method
     --------------------------------*/
	//CVCameraMatを起動させる
	private void Init()
	{
		startCVCam = false;
		//カメラ画像の表示先があるか確認
		if (rawImage) {
			//cvCameraMatを取得していなければ取得
			if (cvCameraMat == null) {
				cvCameraMat = GetComponent<CVCameraMat> ();
			}
			if (cvCameraMat != null) {
				if (cvCameraMat.isInited ()) {
					//初期化している
					//カメラの状態を確認
					if (!cvCameraMat.isPlaying ()) {
						//カメラの撮影が止まっている
						//撮影開始
						cvCameraMat.Play ();
					} else {
						//カメラは撮影中
						//Texture2Dが初期化されているか確認
						if (texture != null) {
							//初期化されている
							startCVCam = true;
						} else {
							//初期化されていない
							//Texture2Dを初期化
							OnCVCameraMatInited();
						}
					}
				} else {
					//初期化していない
					//初期化して起動
					cvCameraMat.Init();
				}
			}
		} else {
			Debug.LogError("NotFound:rawImage");
		}
	}

    /*--------------------------------
     : CVCameraMat Method
     --------------------------------*/
	//CVCameraMatが初期化された時
	public void OnCVCameraMatInited()
	{
		if (rawImage) { 
			//CVMat読み込み待ち
			bool loadMatFlag = false;
			Mat cvCamMat = new Mat ();
			while (!loadMatFlag) {
				if (cvCameraMat) {
					if (cvCameraMat.isPlaying ()) {
						if (cvCameraMat.didUpdateThisFrame ()) {
							cvCamMat = cvCameraMat.GetMat ();
							if (cvCamMat != null) {
								if (!cvCamMat.empty ()) {
									loadMatFlag = true;
								}
							}
						}
					}
				}
			}
			//CVMatをTextureにセット
			if (loadMatFlag) {
				//Texture2Dの作成
				texture = new Texture2D ((int)cvCamMat.cols (), (int)cvCamMat.rows (), TextureFormat.RGBA32, false);
				if (texture) {
					//RawImageへTexture2Dを設定(表示されない時はrawImage.material.mainTexture)
					rawImage.texture = texture;
					startCVCam = true;
				} else {
					Init ();
				}
			}
		} else {
			Debug.LogError("NotFound:rawImage");
		}
	}

	//CVCameraMatが解放された時
	public void OnCVCameraMatDisposed()
	{
		startCVCam = false;
		texture = null;
	}
}

GameObjectの構成

一応動作する状態のGameObjectの構成がどうなっているかを説明します。

サンプルとしてUnity2Dで動作させています。
Canvas,Camera,EventSystemは標準のままです。
f:id:nanokanato:20170412152910p:plain:w300

CameraMaskはRectMask2Dのコンポーネントを追加したGameObjectです。
カメラ映像を画面に表示したいサイズで設定してください。
f:id:nanokanato:20170412152912p:plain:w300

CameraImageはカメラ映像を描画するRawImageです。
今回使ったカメラは横長に取得できるものだったので縦サイズはCameraMaskと同じサイズで横サイズを少し長くしました。
f:id:nanokanato:20170412152908p:plain:w300

CameraImageにCVCameraMat.csとSimpleScript.csを追加してください。
CVCameraMat.csのOnInitedEventとOnDisposedEventの設定はSimpleScript.csで通知を受け取るのに必須です。
SimpleScript.csのRawImageは自身であるCameraImageを設定します。
f:id:nanokanato:20170412152905p:plain

OpenCV for Unityでローカル画像読み込み

このページではOpenCVをUnityで扱うためのAsset「OpenCV for Unity」を利用したOpenCVの機能でローカル画像を読み込み、画面に出力するまでの手順をメモしています。
こちらはUnity5.4.1で作業しました。

開発準備

  • 読み込むための画像を用意してください。
    • 用意した画像は「Assets/Resources/LoadImage/」に追加してください。
      今回はcat.jpgという名前で可愛い猫の画像を用意しました。
    • 開発を行うためにAssetをImportしてください。
      madgenius.hateblo.jp

Scriptの追加

画像読み込みには「Imgcodecs.imread("");」を使用します。
以下はそのScriptです。

csharp:LoadImageScript.cs
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using OpenCVForUnity;

public class LoadImageScript : MonoBehaviour {

	public RawImage rawImage;

	private Mat originMat;
	private Texture2D texture;
	private Color32[] color;

	// Use this for initialization
	void Start () {
		if (rawImage) {
			//Mat画像をPathから読み込み
			originMat = Imgcodecs.imread (Application.dataPath + "/Resources/LoadImage/cat.jpg");
			if (originMat != null) {
				if (!originMat.empty ()) {
					//Color32の作成
					color = new Color32[originMat.cols () * originMat.rows ()];
					if (color != null) {
						//Texture2Dを作成
						texture = new Texture2D (originMat.cols (), originMat.rows ());
						//描画するRawImageにTexture2Dを設定
						rawImage.texture = texture;
					}
				}
			}
		} else {
			Debug.LogError("NotFound:rawImage");
		}
	}
	
	// Update is called once per frame
	void Update () {
		if (originMat != null) {
			if (!originMat.empty ()) {
				if (color != null) {
					if (texture != null) {

						/* 画像加工開始 */



						/* 画像加工終了 */

						//Matの編集内容をTexture2Dに反映
						Utils.matToTexture2D (originMat, texture, color);
					}
				}
			}
		}
	}
}

GameObjectの設定

LoadImageScript.csを作成したら、プロジェクトに追加します。

Hierarchyはこんな感じです。
UI.RawImageを追加してカメラなどを調節しただけです。
f:id:nanokanato:20170412151603p:plain:w300

作成したRawImageにLoadImageScript.csのComponentを追加してシリアライズのRawImageに自身を登録。
f:id:nanokanato:20170412151611p:plain:w300

実行してみる

内部でTexture → Mat → Texture2Dに変換されているのでMatの部分でOpenCVでの加工に対応しています。

一応、可愛い猫が表示されました。
けどなんか青っぽい気が...
f:id:nanokanato:20170412151601p:plain:w300

やはり、元画像より青っぽくなり寒そうな猫に...(上:元画像)
f:id:nanokanato:20170412151558j:plain:w300
f:id:nanokanato:20170412151608p:plain:w300

画像を読み込んでいるTexture2Dに変えている「matToTexture2D(Mat, Texture2D, Color32);」メソッドの別の方法、「matToTexture2D(Mat, Texture2D);」にしても同じ青っぽいものが表示されました。
読み込み部分の「Imgcodecs.imread("");」の時点で青くなっているものと思います。

Jpegが非対応なわけないよなぁと思いながらpngの猫を追加した。
上が元画像のすやすや猫.pngで下がMatで出力した凍える猫.png
f:id:nanokanato:20170412151556p:plain:w300
f:id:nanokanato:20170412151554p:plain:w300

「imread(string,int)」の第二引数を使ってIMREAD_GRAYSCALE*1を入れてみると白黒になって悲しそうな猫に...そうじゃない。
ここに答えがあるという希望を持ちたい。
f:id:nanokanato:20170412151605p:plain:w300 < 助けて

色がおかしい!

ネットにいる知識人や賢者の方々、お助けいただきたいです。
(私のほうでも随時検証してみます。)

*1:imread(string,int)の第二引数
IMREAD_UNCHANGED:不明
IMREAD_GRAYSCALE:グレースケール
IMREAD_COLOR:デフォルト(青っぽいやつ)
IMREAD_ANYDEPTH:不明
IMREAD_ANYCOLOR:不明
IMREAD_LOAD_GDAL:不明
IMREAD_REDUCED_GRAYSCALE_2:不明
IMREAD_REDUCED_COLOR_2:不明
IMREAD_REDUCED_GRAYSCALE_4:不明
IMREAD_REDUCED_COLOR_4:不明
IMREAD_REDUCED_GRAYSCALE_8:不明
IMREAD_REDUCED_COLOR_8:不明

OpenCV for Unityの利用準備

このページではOpenCVをUnityで扱うためのAsset「OpenCV for Unity」をImportするまでの手順をメモしています。
こちらはUnity5.4.1で作業しました。

Assetの購入

わかる方はちゃちゃっと買って次へどうぞ。

  • AssetStoreのWindowを非表示にしている人は上のメニューバーよりWindow > Asset Storeで表示できます。

f:id:nanokanato:20170412145709p:plain:w300

  • UnityのAssetStoreの上の検索にて「OpenCV」と入力してください。

f:id:nanokanato:20170412145713p:plain:w300

  • 検索すると上のほうにEnox SoftwareのOpenCV for Unity(スクリプト/機能統合)があるので詳細ページへ行きましょう。

f:id:nanokanato:20170412145724p:plain:w300

  • ログインしているアカウントの確認を行い、クレジットの準備と値段*1の確認をして購入を押してください。Checkout Nowで支払いできます。クレジットなど購入情報を入力してください。

f:id:nanokanato:20170412145721p:plain:w300

インポート

  • 購入後は購入のボタンがダウンロードになっています。少々時間がかかりますがダウンロードしてください。
  • ダウンロードするとボタンがインポートになるので今度はインポート*2してください。

f:id:nanokanato:20170412145709p:plain:w300

  • インポートすると上のメニューバーにTools > OpenCV for Unity > Set Plugin Import Settingsという項目ができているので押してください。

f:id:nanokanato:20170412145718p:plain:w300

フォルダ内の確認

せっかくなのでOpenCVForUnityのフォルダの中を見てみました。

  • OpenCVForUnity
    • Plugins
      • 各ビルド環境ごとのパッケージが入っている?
    • org
      • C#のクラスが入っています。大体はOpenCVで使える機能と同じ名前です。
    • Editor
      • おそらくビルド環境に応じてPluginsから使用するパッケージを選別するようなクラスが入っています。
    • StreamingAssets
      • Sample内で画像認識に使っているカスケード*3などが入っています。
    • OpenCVForUnityUWP_Beta2.zip
      • 中にOpenCVForUnityUWP_Beta2.unitypackageが入っています。
      • 解凍して実行するとAssetの内容をチェックで選択してImportするときのWindowを表示します。
    • Samples
      • OpenCVForUnityを使用したサンプルが入っています。

上のPlugins、org、Editorは必須だと思います。
※画像認識などを行うならカスケードを追加は必要

基本的にサンプルのSceneを読み込んで実行できると思いますが、カスケードのパスなどでエラーが出る場合がありますので適宜修正してあげてください。

*1:2016年10月現在は$95(当時相場¥9800)

*2:チェックを入れてインポートする内容を選択できます。とりあえず全てチェック入れておけば間違いないです。サンプルなど不要な方は外しても大丈夫だと思います。

*3:カスケードとは顔や目や口などの特定のものを画像から認識し位置を特定するのに使用します。
自分でもわかるように訳すとパーツの特徴を学習した内容のファイルです。
OpenCVGitHub)をダウンロードすることで標準的なカスケードを手に入れることができます。その他特殊なのは検索して探してみてください。

UnityでiOSビルドすると画面が真っ暗になった

最近Unityばかりのナノカです。
Unity iOSの実機テスト時のミスがあったので同じことをしている誰か(未来の自分)のために書きます。

現象

先週までiOSビルドで実機テストすると普通に動いていたのですが、今週分をビルドしてXcodeから実機で確認すると画面が真っ暗で何も表示されませんでした。
画面下にバナー広告を置いていたのでその部分をタップしてみると広告に飛ばされるので表示はされていて動作もしているみたいです。

解決に向けて

さっそくググりましたが、PlayerSettingをいじれ!みたいなのが多く、恐る恐るやってみたが真っ暗なままです。

先週からの変更点を確認していったところ、CameraのClearFlagをSkyboxにしていたのを2DゲームなのでDon'tClearにしていました。
てっきりDon'tClearで真っ暗になるだけだと思っていたのですがSolidColorに変えて黒色に設定しただけで解決。

UnityEditorでは映って実機では映らなくてもビルドはできちゃうので怖いですね。
こまめに実機テストをしようと思いました(小並感)

SmartARのiOS用SDKのサンプルを検証

ここではiOS用のSmartARのSDKを動かして試していきます。

MacOS Sierra(10.12.1)
Xcode 8.1
iOS 8.0
SmartAR 1.1.0

SmartARとは

ソニーが研修開発を進めてきた「物体認識技術」と「3D空間認識技術」を統合した高機能・高精度なARエンジン、またそのARアプリ開発ツールSmartAR® SDKのことです。

AndroidiOS、UnityとWindowsSDKを使ったPC用のソフトが開発可能です。
マーカーレスARと3D空間認識、そしてその高速追従性を売りにしています。
サイト内に詳しく丁寧に説明があり、サンプル動画まであるので参考にどうぞ。

SmartAR®のSDKライセンス価格(いずれも価格は税別)
ライセンスの購入はサンプルで検証後にどうぞ。(個人の値段じゃない)

  • Free版:サンプル動作,検証用
  • Standard版(AndroidiOS/Unity 対応):10万円/1アプリID
  • Enterprise版(AndroidiOS/Unityに加えWindows 対応):100万円/年
    • Enterprise 版には以下のサービスが含まれます。
    • 1年間、アプリ数に制限なく開発・配布が可能
    • 契約期間中は常に最新のバージョンを提供
    • 電話もしくは電子メールで日本語のテクニカルサポート

ちなみにSDKのZIPにPDFが同封されていて日本語,英語が用意されていたりと結構日本人向けになってます。

SDKダウンロード

上のサイトにてご利用までの流れはこちらとありますのでそこからダウンロードページへ
f:id:nanokanato:20170329104751p:plain:w300

とりあえず、SDKの選択は2の評価版のiOS/Android/Unity版を使用しますので「SDKダウンロードサイト」をクリック。
f:id:nanokanato:20170329104630p:plain:w300

ソニーのサイトに遷移しますのでダウンロードのボタンを押すとお客様情報入力画面になります。名前、会社名、メールアドレスが必要になります。

入力後、確認画面を挟みダウンロード画面に遷移します。
使用するSmartAR SDKiOS/Android/Unity)と認識対象画像用 辞書作成ツールをダウンロードします。
私は今回iOSをダウンロードしました。
認識対象画像用 辞書作成ツールは認識するマーカーを作成するのに必要なツールなのでダウンロード必須のようです。

SDKの中を確認

ダウンロードしたのがこちら、とりあえず全てダウンロードしてみました。
SmartAR_Tool.zipが認識対象画像用 辞書作成ツール、SmartAR_SDK_iOS.zipがiOS用のSDKなので解凍します。
f:id:nanokanato:20170329104632p:plain:w300

デデドン..まさかの辞書作成ツールが.exeアプリ
Macは.appなので起動できません。(頑張ればできる)
ですよね、開発時はWindowsを用意しましょう...
使い方はREADME_dictool_j.txtに書いてあるみたいです。
f:id:nanokanato:20170329104636p:plain:w300

詳しくはPDFに書いてあるので必読ください。

サンプルのビルド

SmartAR_SDK_iOS.zipを解凍したファイルのSample/ios/sample_api_full/にある「sample_api_full.xcodeproj」がサンプル集みたいなのでこちらをXcodeで開いてビルドします。
Bundle IDや証明書などの設定は通常通りでしたので省略します。

内部で3D空間認証などを行なっているからか標準のカメラより若干画質が落ちている気もしますが、そもそも処理を軽くするために画質を悪くしてる可能性がありますね。
f:id:nanokanato:20170329104704p:plain:w200

左下のResetはおそらく設定などを初期化だと思います。
SmartARと書いてあるのはライセンスキーを認証することで消せるものだと思います。(PDFにはそのような記載)
右下のMENUを押すとMENU画面が開きます。

Sample/ios/target_picture/にマーカー用の画像が用意されてます。
それを撮影することでマーカーに合わさるように立方体が表示されます。
複数個や縦画面だけでなく横画面で撮影しても表示されました。
f:id:nanokanato:20170329104658p:plain:w200f:id:nanokanato:20170329104740p:plain:w200

他のサンプルを表示する

右下のMENUを押すことでこの画面が開きます。
RecogModesからサンプルを切り替えることができるみたい。
f:id:nanokanato:20170329104754p:plain:w200

RecogModes

認識モードの切り替えです。
その中でさらに選ぶことでサンプルを切り替えることができます。
f:id:nanokanato:20170329104706p:plain:w200

Target tracking

デフォルトのものです。
ARマーカーを読み込んで立方体を表示します。
f:id:nanokanato:20170329104743p:plain:w200

Scene mapping:TARGET

ARマーカーを読み込んで認識に使用した特徴点と立方体を表示します。
f:id:nanokanato:20170329104736p:plain:w200

Scene mapping:HFG

完全マーカーレスの3D空間認識を利用したAR。
特徴点のみで床などの平地を認識して立方体を表示します。
ズレがある場合は特徴点の取得が足りない場合です。カメラの向きを変えずにゆっくりあたりを撮影すると綺麗に表示されています。
上向きの認識に特化している??
f:id:nanokanato:20170329104701p:plain:w200

Scene mappingVFG

横向きの認識に特化している??
f:id:nanokanato:20170329104627p:plain:w200

Scene mapping:SFM

3D空間から角度を推定して立方体を配置している??
f:id:nanokanato:20170329104747p:plain:w200

Scene mapping:DRY_RUN

よく見ないとわからないかもですが、特徴点にピンクの点が表示されています。
f:id:nanokanato:20170329104800p:plain:w200

まとめ

サンプルでは箱が出るだけだがそれでも楽しい感じ。
ソースコードをもっと読めばもっといろいろできると思います。
だた、3Dの描画処理をXcodeからあまりやったことがないのでできればUnityでやりたい。(Unity版も存在します)

  • マーカー認識AR
    • 印刷物のマーカーが必要(喫茶店であればコースターやポスターなどでも可)
    • 正確で安定なAR表示
  • 3D空間認識AR
    • 印刷物のマーカーが不要
    • 場所や光原によって安定性が変化する。
    • 特徴点を多く取り込むためユーザーに取り込みの動作をさせる必要がある。