こんそーるすーぷのレシピはこちら

いちおう仕事なのに趣味の人に劣って焦り

【Unity】OpenCVで手の判別

UnityでARKitなど画像から何かを認識していく便利な機能がどんどん増えているなか、いまさらOpenCVをやります。
過去にやったもののまとめ + αです。
(まだ、ソースがごちゃっとしてますが自分の備忘録として記載します)

OpenCVについて

今更の今更ですが、OpenCVについて
やっている人が多いので記事が多すぎて公式の方が埋もれ気味です...

公式Webはここ
opencv.org

公式Gitはここ
github.com

Wikiのコピペですが「画像処理・画像解析および機械学習等の機能を持つC/C++JavaPythonMATLAB用ライブラリ」だそうです。
今、主流になっている技術の詰め合わせという感じですね。
OpenCV自体は無料でDLできるのでガリガリ書けば最新技術がガリガリできるわけなんです。

前はOpenCVforUnityでやってましたが...

過去に使っていたOpenCVforUnity...
https://www.assetstore.unity3d.com/jp/#!/content/21088www.assetstore.unity3d.com

こういう記事を書いてましたが...
madgenius.hateblo.jp
madgenius.hateblo.jp
madgenius.hateblo.jp
madgenius.hateblo.jp
madgenius.hateblo.jp
madgenius.hateblo.jp

iOS11で久々にビルドすると通らない...AssetStoreの更新分があったので更新しましょう...。

MatとUnity形式の画像の変換

OpenCVではcv::matという形式を使って画像を加工したりします。
UnityではTextureなど別の形式なのでその辺を補ってくれる部分をHGMatとしてまとめました。

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using OpenCVForUnity;

namespace HGHandGesture 
{
	public class HGMat : MonoBehaviour {

		[SerializeField] private Image TextureImage;
		[HideInInspector] public Mat rgbaMat;
		[HideInInspector] public Color32[] colors;
		private Texture2D _convertTexture;

		/*======================================
		* Default Method
		======================================*/
		/// <summary>
		/// Raises the destroy event.
		/// </summary>
		void OnDestroy()
		{
			Dispose();
		}

		/*======================================
		* Override Method
		======================================*/
		// Use this for initialization
		protected virtual void Start() {
			
		}

		// Update is called once per frame
		protected virtual void Update() {
			
		}

		/// <summary>
		/// Mat the retouch.
		/// </summary>
		/// <param name="rgbaMat">Mat.</param>
		protected virtual void MatRetouch(Mat _rgbaMat) 
		{

		}

		/// <summary>
		/// Releases all resource used by the <see cref="HGTexture2DToMat"/> object.
		/// </summary>
		/// <remarks>Call <see cref="Dispose"/> when you are finished using the <see cref="HGTexture2DToMat"/>. The
		/// <see cref="Dispose"/> method leaves the <see cref="HGTexture2DToMat"/> in an unusable state. After calling
		/// <see cref="Dispose"/>, you must release all references to the <see cref="HGTexture2DToMat"/> so the garbage
		/// collector can reclaim the memory that the <see cref="HGTexture2DToMat"/> was occupying.</remarks>
		protected virtual void Dispose()
		{
			if (_texture2D != null) 
				_texture2D = null;
			if (colors != null)
				colors = null;
			if (rgbaMat != null)
			{
				rgbaMat.Dispose();
				rgbaMat = null;
			}
		}

		/*======================================
	    * Private Method
		======================================*/
		/// <summary>
		/// Sprites from _texture2D.
		/// </summary>
		/// <returns>The from _texture2D.</returns>
		/// <param name="texture">Texture2D.</param>
		private Sprite _spriteFromTexture2D(Texture2D texture)
		{
			//Texture2DからSprite作成
			Sprite sprite = null;
			if (texture != null)
				sprite = Sprite.Create(texture, new UnityEngine.Rect(0, 0, texture.width, texture.height), Vector2.zero);
			return sprite;
		}

		/*======================================
	    * Convert Method
		======================================*/
		/// <summary>
		/// Shows the retouch Mat.
		/// </summary>
		private void _showRetouchMat() 
		{
			MatRetouch(rgbaMat);
			if (TextureImage != null)
			{
				Utils.matToTexture2D(rgbaMat, _convertTexture, colors);
				Sprite _sprite = _spriteFromTexture2D(_convertTexture);
				TextureImage.sprite = _sprite;
			}
		}

		/// <summary>
		/// Texture2D to Mat.
		/// </summary>
		private Texture2D _texture2D = null;
		public Texture2D texture2D 
		{
			get { return _texture2D; }
			set {
				_texture2D = value;
				_texture2DToMat();
			}
		}
		private void _texture2DToMat() 
		{
			if (_texture2D != null)
			{
				//Texture2DをMatに変換する
				if (colors == null || colors.Length != _texture2D.width * _texture2D.height)
					colors = new Color32[_texture2D.width * _texture2D.height];
				if (_convertTexture == null || _convertTexture.width != _texture2D.width || _convertTexture.height != _texture2D.height)
					_convertTexture = new Texture2D (_texture2D.width, _texture2D.height, TextureFormat.RGBA32, false);
				rgbaMat = new Mat(_texture2D.height, _texture2D.width, CvType.CV_8UC4);
				Utils.texture2DToMat(_texture2D, rgbaMat);

				//Matを修正して画面に描画
				_showRetouchMat();
			}
		}

		/// <summary>
		/// WebCamTexture to Mat.
		/// </summary>
		private WebCamTexture _webCamTexture = null;
		public WebCamTexture webCamTexture 
		{
			get { return _webCamTexture; }
			set {
				_webCamTexture = value;
				_webCamTextureToMat();
			}
		}
		private void _webCamTextureToMat()
		{
			if (_webCamTexture != null && _webCamTexture.isPlaying && _webCamTexture.didUpdateThisFrame)
			{
				//WebCamTextureをMatに変換する
				if (colors == null || colors.Length != _webCamTexture.width * _webCamTexture.height)
					colors = new Color32[_webCamTexture.width * _webCamTexture.height];
				if (_convertTexture == null || _convertTexture.width != _webCamTexture.width || _convertTexture.height != _webCamTexture.height)
					_convertTexture = new Texture2D (_webCamTexture.width, _webCamTexture.height, TextureFormat.RGBA32, false);
				rgbaMat = new Mat(_webCamTexture.height, _webCamTexture.width, CvType.CV_8UC4);
				Utils.webCamTextureToMat(_webCamTexture, rgbaMat, colors);

				//Matを修正して画面に描画
				_showRetouchMat();
			}
		}
	}
}

TextureImageにInspectorからUI.Imageをセットすることで加工後のMatを描画できます。
webCamTexture, texture2DのどちらかにUnityで取得した画像を入れることでcv::mat形式に変換されます。
その後「MatRetouch(Mat _rgbaMat) 」が呼ばれてそこで編集したMatがUI.Imageに描画されます。

このHGMatをベースとして画像からカメラからの入力を行います。

画像からMatに変換して加工

HGMatの使い方説明になります。
継承クラスを用意してInspectorからTexture2Dが入力できるようにします。
この時、入力する画像の設定がAdvanced > Read/Write Enabledがtrueになっていないとエラーがでます。
f:id:nanokanato:20171218104017p:plain:w300

Start, Update, MatRetouch, Disposeをoverrideしており、
Startで初期化、
UpdateなどでHGMatへ画像の入力、
MatRetouchでMat画像の加工、
Disposeで解放
を行います。

using UnityEngine;
using OpenCVForUnity;
using HGHandGesture;

public class ImageTracking : HGMat {

	public Texture2D HandImage;

	/*======================================
    * Override Method
	======================================*/
	// Use this for initialization
	protected override void Start()
	{
		base.Start();
	}

	// Update is called once per frame
	protected override void Update()
	{
		base.Update();

		//加工用のTexture2Dを送りMatへの変換を待つ
		base.texture2D = HandImage;
	}

	/// <summary>
	/// Mat the retouch.
	/// </summary>
	/// <param name="rgbaMat">Mat.</param>
	protected override void MatRetouch(Mat _rgbaMat)
	{
		//変換されたMatを加工する
		base.MatRetouch(_rgbaMat);
	}

	/// <summary>
	/// Releases all resource used by the <see cref="HGTexture2DToMat"/> object.
	/// </summary>
	/// <remarks>Call <see cref="Dispose"/> when you are finished using the <see cref="HGTexture2DToMat"/>. The
	/// <see cref="Dispose"/> method leaves the <see cref="HGTexture2DToMat"/> in an unusable state. After calling
	/// <see cref="Dispose"/>, you must release all references to the <see cref="HGTexture2DToMat"/> so the garbage
	/// collector can reclaim the memory that the <see cref="HGTexture2DToMat"/> was occupying.</remarks>
	protected override void Dispose()
	{
		//破棄処理を記載
		base.Dispose();
	}
}

カメラ映像をMatに変換して加工

Webカメラもかなり使う機能なのでまとめました。
HGMatのWebカメラ特化クラスです。

using System.Collections;
using UnityEngine;
using OpenCVForUnity;

namespace HGHandGesture 
{
	public class HGCamera : HGMat {

		[System.Serializable] public class WebCameraData 
		{
			public string DeviceName = null;
			public Size Size = new Size(1136, 640);
			public bool IsFrontFacing = false;
		}
		[SerializeField] private WebCameraData _webCameraData = new WebCameraData();

		private WebCamTexture _webCamTexture;
		private WebCamDevice _webCamDevice;

		private bool _isInitWaiting = false;
		private bool _hasInitDone = false;

		/*======================================
	    * Override Method
		======================================*/
		// Use this for initialization
		protected override void Start() {
			base.Start();
			Initialize();
		}

		// Update is called once per frame
		protected override void Update() {
			base.Update();
			if (_hasInitDone) base.webCamTexture = _webCamTexture;
		}

		/// <summary>
		/// Mat the retouch.
		/// </summary>
		/// <param name="rgbaMat">Mat.</param>
		protected override void MatRetouch(Mat _rgbaMat)
		{
			base.MatRetouch(_rgbaMat);
		}

		/// <summary>
		/// Releases all resource used by the <see cref="HGTexture2DToMat"/> object.
		/// </summary>
		/// <remarks>Call <see cref="Dispose"/> when you are finished using the <see cref="HGTexture2DToMat"/>. The
		/// <see cref="Dispose"/> method leaves the <see cref="HGTexture2DToMat"/> in an unusable state. After calling
		/// <see cref="Dispose"/>, you must release all references to the <see cref="HGTexture2DToMat"/> so the garbage
		/// collector can reclaim the memory that the <see cref="HGTexture2DToMat"/> was occupying.</remarks>
		protected override void Dispose()
		{
			_isInitWaiting = false;
			_hasInitDone = false;

			if (_webCamTexture != null) 
			{
				_webCamTexture.Stop ();
				_webCamTexture = null;
			}

			base.Dispose();
		}

		/*======================================
	    * Public Method
		======================================*/

		public void Play()
		{
			if (_hasInitDone) webCamTexture.Play();
		}

		public void Pause()
		{
			if (_hasInitDone) webCamTexture.Pause();
		}

		public void Stop()
		{
			if (_hasInitDone) webCamTexture.Stop();
		}

		public void ChangeCamera()
		{
			if (_hasInitDone && _webCameraData != null)
			{
				_webCameraData.IsFrontFacing = !_webCameraData.IsFrontFacing;
				Initialize();
			}
		}

		/*======================================
	    * Private Method
		======================================*/

		private void Initialize()
		{
			if (_isInitWaiting) return;
			StartCoroutine(_Initialize());
		}

		private IEnumerator _Initialize ()
		{
			if (_webCameraData == null)
				yield return null;
			
			if (_hasInitDone) Dispose();
			_isInitWaiting = true;

			if (!string.IsNullOrEmpty(_webCameraData.DeviceName))
				_webCamTexture = new WebCamTexture(_webCameraData.DeviceName, (int)_webCameraData.Size.width, (int)_webCameraData.Size.height);

			if (_webCamTexture == null)
			{
				for (int cameraIndex = 0; cameraIndex < WebCamTexture.devices.Length; cameraIndex++) 
				{
					if (WebCamTexture.devices[cameraIndex].isFrontFacing == _webCameraData.IsFrontFacing)
					{
						_webCamDevice = WebCamTexture.devices[cameraIndex];
						_webCamTexture = new WebCamTexture (_webCamDevice.name, (int)_webCameraData.Size.width, (int)_webCameraData.Size.height);
						break;
					}
				}
			}

			if (_webCamTexture == null) 
			{
				if (WebCamTexture.devices.Length > 0) 
				{
					_webCamDevice = WebCamTexture.devices [0];
					_webCamTexture = new WebCamTexture(_webCamDevice.name, (int)_webCameraData.Size.width, (int)_webCameraData.Size.height);
				} 
				else _webCamTexture = new WebCamTexture((int)_webCameraData.Size.width, (int)_webCameraData.Size.height);
			}

			if (_webCamTexture != null)
			{
				_webCamTexture.Play();

				while (true) 
				{
					if (_webCamTexture.didUpdateThisFrame)
					{
						_isInitWaiting = false;
						_hasInitDone = true;
						OnInited();
						break;
					} 
					else yield return 0;
				}
			}
		}

		private void OnInited ()
		{
			float width = _webCamTexture.height;
			float height = _webCamTexture.width;

			float widthScale = (float)Screen.width/width;
			float heightScale = (float)Screen.height/height;
			if (widthScale < heightScale)
				Camera.main.orthographicSize = (width*(float)Screen.height/(float)Screen.width)/2;
			else Camera.main.orthographicSize = height/2;
		}
	}
}

HGCameraを継承することで簡単にWebカメラの加工と描画ができるようになりました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using OpenCVForUnity;
using HGHandGesture;

public class CameraTracking : HGCamera {

	/*======================================
    * Override Method
	======================================*/
	// Use this for initialization
	protected override void Start()
	{
		base.Start();
	}
	
	// Update is called once per frame
	protected override void Update () {
		base.Update();
	}

	protected override void MatRetouch(Mat _rgbaMat)
	{
		base.MatRetouch(_rgbaMat);
	}

	protected override void Dispose()
	{
		base.Dispose();
	}
}

OpenCVで手の判別

やっと本題、上の機能を使ってOpenCVで手の判別をします。
使うメソッドはMatRetouchだけなので今後ここのみ記載します。

protected override void MatRetouch(Mat _rgbaMat)
{
	base.MatRetouch(_rgbaMat);
	//ここで処理して手を判別する
}

HGColorSpuiter.csはタップした位置からColorを取得したり、ColorとScalarの変換を行うクラスです。
ScalarはOpenCVで色を表すクラスと思っています()

using System.Collections.Generic;
using UnityEngine;
using OpenCVForUnity;

namespace HGHandGesture 
{
	public static class HGColorSpuiter
	{
		public static Point storedTouchPoint = null;

		//ColorをScalarに変換
		public static Scalar ColorToScalar(Color color) 
		{
			//   0,   0,   0, 255:黒
			// 255,   0,   0, 255:赤
			//   0, 255,   0, 255:緑
			//   0,   0, 255, 255:青
			// 255, 255, 255, 255:白
			return new Scalar(color.r*255f, color.g*255f, color.b*255f, color.a*255f);
		}

		//ScalarをColorに変換
		public static Color ScalarToColor(Scalar scalar) 
		{
			float r = 0;
			if (0 < scalar.val.Length) r = (float)scalar.val[0]/255f;
			float g = 0;
			if (1 < scalar.val.Length) g = (float)scalar.val[1]/255f;
			float b = 0;
			if (2 < scalar.val.Length) b = (float)scalar.val[2]/255f;
			float a = 0;
			if (3 < scalar.val.Length) a = (float)scalar.val[3]/255f;
			return new Color(r, g, b, a);
		}

		//タップした位置の色を返す
		public static Color GetTapPointColor(Mat rgbaMat)
		{
			Color tapColor = new Color(0.031f, 0.326f, 0.852f, 1f);

			//タップ座標の取得
#if ((UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR)
			//Touch
			int touchCount = Input.touchCount;
			if (touchCount == 1)
			{
				Touch t = Input.GetTouch(0);
				if(t.phase == TouchPhase.Ended && !UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject(t.fingerId))
					storedTouchPoint = new Point (t.position.x, t.position.y);
			}
#else
			//Mouse
			if (Input.GetMouseButtonUp(0) && !UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject())
				storedTouchPoint = new Point(Input.mousePosition.x, Input.mousePosition.y);
#endif

			//タップされていればタップされている場所を取得
			if(storedTouchPoint != null)
			{
				Point touchPoint = _convertScreenPoint(rgbaMat, storedTouchPoint);

				//タップされている場所から色情報を取得
				Scalar blobColorHsv = _onTouch(rgbaMat, touchPoint);
				if (blobColorHsv != null) tapColor = ScalarToColor(blobColorHsv);
			}

			return tapColor;
		}

		//タップ座標を取得
		private static Point _convertScreenPoint(Mat rgbaMat, Point screenPoint)
		{
			//台形補正を行いタップされた位置の座標を正確に取得する
			Vector2 tl = Camera.main.WorldToScreenPoint(new Vector3(-rgbaMat.width()/2,  rgbaMat.height()/2));
			Vector2 tr = Camera.main.WorldToScreenPoint(new Vector3( rgbaMat.width()/2,  rgbaMat.height()/2));
			Vector2 br = Camera.main.WorldToScreenPoint(new Vector3( rgbaMat.width()/2, -rgbaMat.height()/2));
			Vector2 bl = Camera.main.WorldToScreenPoint(new Vector3(-rgbaMat.width()/2, -rgbaMat.height()/2));

			Mat srcRectMat = new Mat(4, 1, CvType.CV_32FC2);
			Mat dstRectMat = new Mat(4, 1, CvType.CV_32FC2);

			srcRectMat.put(0, 0, tl.x, tl.y, tr.x, tr.y, br.x, br.y, bl.x, bl.y);
			dstRectMat.put(0, 0, 0.0, 0.0, rgbaMat.width(), 0.0, rgbaMat.width(), rgbaMat.height(), 0.0, rgbaMat.height());

			Mat perspectiveTransform = Imgproc.getPerspectiveTransform(srcRectMat, dstRectMat);

			MatOfPoint2f srcPointMat = new MatOfPoint2f(screenPoint);
			MatOfPoint2f dstPointMat = new MatOfPoint2f();

			Core.perspectiveTransform (srcPointMat, dstPointMat, perspectiveTransform);

			return dstPointMat.toArray()[0];
		}

		//タッチ座標からその場所の平均色を取得
		private static Scalar _onTouch(Mat rgbaMat, Point touchPoint)
		{
			int cols = rgbaMat.cols();
			int rows = rgbaMat.rows();
			int x = (int)touchPoint.x;
			int y = (int)touchPoint.y;
			if ((x < 0) || (y < 0) || (x > cols) || (y > rows)) return null;

			OpenCVForUnity.Rect touchedRect = new OpenCVForUnity.Rect();

			touchedRect.x = (x > 5) ? x - 5 : 0;
			touchedRect.y = (y > 5) ? y - 5 : 0;

			touchedRect.width = (x + 5 < cols) ? x + 5 - touchedRect.x : cols - touchedRect.x;
			touchedRect.height = (y + 5 < rows) ? y + 5 - touchedRect.y : rows - touchedRect.y;

			//タップ座標のみを切り抜く
			Mat touchedRegionRgba = rgbaMat.submat(touchedRect);

			Mat touchedRegionHsv = new Mat();
			Imgproc.cvtColor(touchedRegionRgba, touchedRegionHsv, Imgproc.COLOR_RGB2HSV_FULL);

			//タップされた位置の平均色を計算する
			Scalar blobColorHsv = Core.sumElems(touchedRegionHsv);
			int pointCount = touchedRect.width * touchedRect.height;
			for (int i = 0; i < blobColorHsv.val.Length; i++)
				blobColorHsv.val [i] /= pointCount;

			touchedRegionRgba.release();
			touchedRegionHsv.release();

			return blobColorHsv;
		}
	}
}

最終版のHGOrigin.csクラスです。(クラス名は仮のまま...)
手の識別を行い、その位置に描画を行います。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using OpenCVForUnity;

namespace HGHandGesture 
{
	//手の認識を行う
	public static class HGOrigin
	{
		public static int depthThreashold = 9000; //検出の精度(0 ~ 30000)
		public static Color ContourRangeColor = Color.green;
		public static Color ArmRangeColor = Color.blue;
		public static Color HandRangeColor = Color.cyan;
		public static Color PalmRangeColor = Color.yellow;
		public static Color PalmCenterColor = Color.grey;
		public static Color FingerRangeColor = Color.red;

		//認識の開始
		public static void Cognition(Mat rgbaMat, Color handColor)
		{
			//指定色と同じ輪郭を取得する
			Mat mDilatedMask = new Mat();
			_makeColorMask(rgbaMat, handColor, mDilatedMask);

			//マスクの輪郭の頂点を取得する
			List<MatOfPoint> contours = new List<MatOfPoint> ();
			Imgproc.findContours(mDilatedMask, contours, new Mat(), Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);

			//輪郭ごとの頂点を取得し、手を判別する
			foreach (MatOfPoint contour in contours)
				_contourToHandGesture(rgbaMat, contour);
		}

		/*=============================================*
		 * 画像から輪郭を取得するまで
		 *=============================================*/
		/// <summary>
		/// Makes the color mask.
		/// </summary>
		/// <param name="rgbaMat">Rgba mat.</param>
		/// <param name="handColor">Hand color.</param>
		/// <param name="mDilatedMask">M dilated mask.</param>
		private static void _makeColorMask(Mat rgbaMat, Color handColor, Mat mDilatedMask) 
		{
			//色の範囲を指定する
			Scalar mLowerBound = new Scalar(0);
			Scalar mUpperBound = new Scalar(0);
			_getApproximateScalarFromColor(handColor, mLowerBound, mUpperBound);

			//ガウシアンピラミッドを利用して画像を周波数ごとに分解した(小さくした)HSV形式の画像を作成
			Mat mHsvMat = new Mat();
			_getGaussianPyramidHSVMat(rgbaMat, mHsvMat);

			//inRangeで色による探索を行い、mMaskに指定色だけが残った画像(マスク)を作成する
			Mat mMask = new Mat();
			Core.inRange (mHsvMat, mLowerBound, mUpperBound, mMask);

			//dilateで画像の膨張を行い、マスクのノイズ除去を行う
			Imgproc.dilate (mMask, mDilatedMask, new Mat ());
		}

		/// <summary>
		/// Gets the color of the approximate scalar from.
		/// </summary>
		/// <param name="handColor">Hand color.</param>
		/// <param name="mLowerBound">M lower bound.</param>
		/// <param name="mUpperBound">M upper bound.</param>
		private static void _getApproximateScalarFromColor(Color handColor, Scalar mLowerBound, Scalar mUpperBound) 
		{
			//色の範囲を指定する
			Scalar mColorRadius = new Scalar(25, 50, 50, 0);

			Scalar hsvColor = HGColorSpuiter.ColorToScalar(handColor);
			double minH = (hsvColor.val [0] >= mColorRadius.val [0]) ? hsvColor.val [0] - mColorRadius.val [0] : 0;
			double maxH = (hsvColor.val [0] + mColorRadius.val [0] <= 255) ? hsvColor.val [0] + mColorRadius.val [0] : 255;

			mLowerBound.val [0] = minH;
			mUpperBound.val [0] = maxH;

			mLowerBound.val [1] = hsvColor.val [1] - mColorRadius.val [1];
			mUpperBound.val [1] = hsvColor.val [1] + mColorRadius.val [1];

			mLowerBound.val [2] = hsvColor.val [2] - mColorRadius.val [2];
			mUpperBound.val [2] = hsvColor.val [2] + mColorRadius.val [2];

			mLowerBound.val [3] = 0;
			mUpperBound.val [3] = 255;
		}

		/// <summary>
		/// Gets the gaussian pyramid HSV mat.
		/// </summary>
		/// <param name="rgbaMat">Rgba mat.</param>
		/// <param name="mHsvMat">M hsv mat.</param>
		private static void _getGaussianPyramidHSVMat(Mat rgbaMat, Mat mHsvMat) 
		{
			Mat mPyrDownMat = new Mat();
			Imgproc.pyrDown (rgbaMat, mPyrDownMat);
			Imgproc.pyrDown (mPyrDownMat, mPyrDownMat);
			Imgproc.cvtColor (mPyrDownMat, mHsvMat, Imgproc.COLOR_RGB2HSV_FULL);
		}

		/*=============================================*
		 * 輪郭ごとの頂点から手を判別するまで
		 *=============================================*/
		/// <summary>
		/// Contours to hand gesture.
		/// </summary>
		/// <param name="rgbaMat">Rgba mat.</param>
		/// <param name="contour">Contour.</param>
		private static void _contourToHandGesture(Mat rgbaMat, MatOfPoint contour) 
		{
			try 
			{
				//頂点を調査する準備をする
				_pointOfVertices(rgbaMat, contour);

				//基準輪郭のサイズの取得と描画(長方形)
				OpenCVForUnity.Rect boundRect = Imgproc.boundingRect(new MatOfPoint(contour.toArray()));
				Imgproc.rectangle(rgbaMat, boundRect.tl(), boundRect.br(), HGColorSpuiter.ColorToScalar(ContourRangeColor), 2, 8, 0);

				/*=============================================*
				* 腕まで含んだ手の大きさを取得する
				**=============================================*/
				//腕まで含んだ手の大きさを識別する
				MatOfInt hull = new MatOfInt();
				Imgproc.convexHull(new MatOfPoint(contour.toArray()), hull);

				//腕まで含んだ手の範囲を取得
				List<Point> armPointList = new List<Point>();
				for (int j = 0; j < hull.toList().Count; j++)
				{
					Point armPoint = contour.toList()[hull.toList()[j]];
					bool addFlag = true;
					foreach (Point point in armPointList.ToArray()) 
					{
						//輪郭の1/10より近い頂点は誤差としてまとめる
						double distance = Mathf.Sqrt((float)((armPoint.x-point.x)*(armPoint.x-point.x)+(armPoint.y-point.y)*(armPoint.y-point.y)));
						if (distance <= Mathf.Min((float)boundRect.width, (float)boundRect.height)/10) 
						{
							addFlag = false;
							break;
						}
					}
					if (addFlag) armPointList.Add(armPoint);	
				}

				MatOfPoint armMatOfPoint = new MatOfPoint();
				armMatOfPoint.fromList(armPointList);
				List<MatOfPoint> armPoints = new List<MatOfPoint>();
				armPoints.Add(armMatOfPoint);

				//腕まで含んだ手の範囲を描画
				Imgproc.drawContours(rgbaMat, armPoints, -1, HGColorSpuiter.ColorToScalar(ArmRangeColor), 3);

				//腕まで含んだ手が三角形の場合はそれ以上の識別が難しい
				if (hull.toArray().Length < 3) return;

				/*=============================================*
				* 掌の大きさを取得する
				**=============================================*/
				//凸面の頂点から凹面の点のみを取得し、掌の範囲を取得する
				MatOfInt4 convexDefect = new MatOfInt4();
				Imgproc.convexityDefects(new MatOfPoint(contour.toArray()), hull, convexDefect);

				//凹面の点をフィルタリングして取得
				List<Point> palmPointList = new List<Point>();
				for (int j = 0; j < convexDefect.toList().Count; j = j+4) 
				{
					Point farPoint = contour.toList()[convexDefect.toList()[j+2]];
					int depth = convexDefect.toList()[j+3];
					if (depth > depthThreashold && farPoint.y < boundRect.br().y-boundRect.tl().y)
						palmPointList.Add(contour.toList()[convexDefect.toList()[j+2]]);
				}

				MatOfPoint palmMatOfPoint = new MatOfPoint();
				palmMatOfPoint.fromList(palmPointList);
				List<MatOfPoint> palmPoints = new List<MatOfPoint>();
				palmPoints.Add(palmMatOfPoint);

				//掌の範囲を描画
				Imgproc.drawContours(rgbaMat, palmPoints, -1, HGColorSpuiter.ColorToScalar(PalmRangeColor), 3);

				/*=============================================*
				* 掌+指先の大きさを取得する
				**=============================================*/
				//掌の位置を元に手首を除いた範囲を取得する
				List<Point> handPointList = new List<Point>();
				handPointList.AddRange(armPointList.ToArray());
				handPointList.Reverse();
				handPointList.RemoveAt(0);
				handPointList.Insert(0, palmPointList.ToArray()[0]);
				handPointList.RemoveAt(handPointList.Count-1);
				handPointList.Insert(handPointList.Count, palmPointList.ToArray()[palmPointList.Count-1]);

				MatOfPoint handMatOfPoint = new MatOfPoint();
				handMatOfPoint.fromList(handPointList);
				List<MatOfPoint> handPoints = new List<MatOfPoint>();
				handPoints.Add(handMatOfPoint);

				Imgproc.drawContours(rgbaMat, handPoints, -1, HGColorSpuiter.ColorToScalar(HandRangeColor), 3);

				/*=============================================*
				* 指先の位置を取得する
				**=============================================*/
				//掌の各頂点の中心を求める
				List<Point> palmCenterPoints = new List<Point>();
				for (int i = 0; i < palmPointList.Count; i++)
				{
					Point palmPoint = palmPointList.ToArray()[i];
					Point palmPointNext = new Point();
					if (i+1 < palmPointList.Count) 
						palmPointNext = palmPointList.ToArray()[i+1];
					else palmPointNext = palmPointList.ToArray()[0];
	
					Point palmCenterPoint = new Point((palmPoint.x+palmPointNext.x)/2, (palmPoint.y+palmPointNext.y)/2);
					palmCenterPoints.Add(palmCenterPoint);
				}
	
				//掌の頂点から最も近い手の頂点を求める
				for (int i = 0; i < palmCenterPoints.Count && i+1 < handPointList.Count && i < 5; i++) 
				{
					Point palmPoint = palmCenterPoints.ToArray()[i];


					List<Point> fingerList = new List<Point>();
					fingerList.Add(palmPoint);
					fingerList.Add(handPointList.ToArray()[i+1]);
	
					MatOfPoint fingerPoint = new MatOfPoint();
					fingerPoint.fromList(fingerList);
	
					List<MatOfPoint> fingerPoints = new List<MatOfPoint>();
					fingerPoints.Add(fingerPoint);
	
					Imgproc.drawContours(rgbaMat, fingerPoints, -1, HGColorSpuiter.ColorToScalar(FingerRangeColor), 3);
				}

//				Imgproc.putText(rgbaMat, "", new Point(2, rgbaMat.rows()-30), Core.FONT_HERSHEY_SIMPLEX, 1.0, HGColorSpuiter.ColorToScalar(Color.black), 2, Imgproc.LINE_AA, false);
			}
			catch (System.Exception e) 
			{
				Debug.Log(e.Message);
			}
		}

		/// <summary>
		/// Points the of vertices.
		/// </summary>
		/// <param name="contour">Contour.</param>
		private static void _pointOfVertices(Mat rgbaMat, MatOfPoint contour) 
		{
			//multiplyでガウシアンピラミッドで分解されたサイズを掛け算で実画像サイズに戻す
			Core.multiply(contour, new Scalar(4, 4), contour);

			//輪郭の頂点がまだらにあるので識別しやすいようにポリゴン近似でサンプリングする。
			MatOfPoint2f pointMat = new MatOfPoint2f();
			Imgproc.approxPolyDP(new MatOfPoint2f(contour.toArray()), pointMat, 3, true);
			contour = new MatOfPoint(pointMat.toArray());
		}
	}
}

使用するときはこんな感じ
MatRetouchでタップ位置の色を取得して手の位置に合わせて描画します。
タップ位置の色はデフォルトが肌色、タップ後はタップした位置の色になります。

protected override void MatRetouch(Mat _rgbaMat)
{
	//変換されたMatを加工する
	base.MatRetouch(_rgbaMat);

	//タップ位置の色を取得
	Color color = HGColorSpuiter.GetTapPointColor(_rgbaMat);

	//手の位置に合わせて描画
	HGOrigin.Cognition(_rgbaMat, color);
}

最終的に認識できたもの

最終的に上記の処理で手の画像から識別することが可能になりました。
緑 :肌色の輪郭の範囲
青 :肌色の範囲
水色:手の範囲
黄 :掌の範囲
赤 :指
f:id:nanokanato:20171219104740j:plain:w300

ただ、上記は認識しやすい綺麗な手の画像で、下記のような画像だとまだ認識の精度が低いことがわかります。
f:id:nanokanato:20171219105312p:plain:w300f:id:nanokanato:20171219105315p:plain:w300

いろいろ試行錯誤した手順は以下にまとめました。