読者です 読者をやめる 読者になる 読者になる

ナノカ技術メモ

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

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