Kinect v2のジェスチャーを自力で実装した話

#下書きからこんなん出てきた
去年書いたやつだけど公開しとく
今日→去年→5,6年前…とさかのぼります

#寄稿のきっかけ
今さらな話ですがDropBoxを整理してたら表題の思ひ出がソースとして出てきたので
せっかくだから誰かのナレッジになればと思い。

もはやレガシーになり果てたKinectV2ですが、
当時はインタラクティブコンテンツとして使うにはなかなか面白いデバイスでした。

安価で近未来的な機能だったこともあり、簡単なコンテンツでも「えっすごいすごい！」っていうリアクションを得られたため
当時勤めてた会社でもSHIN_DEVELOPが面白いもん作ったってことでやや人気者になれました。

先に述べたように、デベロッパーの中ではもはや割と使い古された技術ではありますが
まだまだ一般ウケする存在じゃないかなぁと思うんですよね（実際どうなんだろうな）

色々語りたいことはあるんですがそれはまた別の投稿で。
前説が長くなりましたが今回はジェスチャーについて自分なりのナレッジを出したいと思います。

#センサーデバイスの考え方
勉強会やセミナーで登壇した経験も何回かあるんですが、
Kinectの実装で意識することは「パラパラ漫画」だと主張してきました。
このデバイスに限らず、LeapMotionとかRealSenseとかも概念としては共通しています。

1コマ1コマの状態を考えてこうなっていればこうなる、という思考で実装することを意識します。
特にジェスチャーについては、Kinectが取得する1フレーム時の座標の位置関係から
各Jointの値がどう推移したかを解析することがキーとなります。

#実際に組んでみる
先に白状しておきますが、このソースって確かv1のサンプルか何かを改良したものです。
v2のSDKBrowserにGestureBuilderみたいなのがあったと思いますが、アホなので使い方がよく分かんなかったため「じゃーこれ改造して使ったろ！」ってノリで作ってます。
ユーザー定義演算子とか組み込んでますが、正直あんまり使ったことなかったり…

プロジェクト構成は以下のようになります。
<img width="560" alt="スクリーンショット 2018-06-18 11.15.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/23195/3990eb9d-7097-ab33-84fb-dd9cdd99663f.png">

実装するのは

- Entry.cs
- GestureDetector.cs
- SwipeGestureDetector.cs
- Math/Vector2.cs
- Math/Vector3.cs

の5つになります。
順に載っけていきます。

## Entry.cs

```c#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace v2SwipeGesture
{
	public class Entry
	{
		public DateTime Time { get; set; }
		public Vector3 Position { get; set; }
	}
}
```

ジェスチャーの検出開始時のデータを格納するクラスです。
検出開始と言っても、どこからとかではなく常に検出を行ってます。

##GestureDetector.cs

```c#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Kinect;

namespace v2SwipeGesture
{
	public abstract class GestureDetector
	{
		public enum GestureType
		{
			SwipeRightToLeft = 0,
			SwipeLeftToRight = 1,
			SwipeTopToBottom = 2,
			SwipeBottomToTop = 3,
		}

		DateTime lastGestureDate = DateTime.Now;
		readonly int windowSize;
		readonly List<Entry> entries = new List<Entry>();

		public int MinimalPerBetweenGestures { get; set; }
		public event Action<GestureType> OnGestureDetected;

		protected List<Entry> Entries
		{
			get { return entries; }
		}

		public int WindowSize
		{
			get { return windowSize; }
		}

		protected GestureDetector(int windowSize = 20)
		{
			this.windowSize = windowSize;
			MinimalPerBetweenGestures = 0;
		}

		public virtual void Add(CameraSpacePoint cp, JointType jt)
		{
			Vector3 vc = new Vector3(cp.X,cp.Y,cp.Z);
			Entry newEntry = new Entry { Position = vc, Time = DateTime.Now };
			Entries.Add(newEntry);

			if (Entries.Count > WindowSize)
			{
				Entry entryToRemove = Entries[0];
				Entries.Remove(entryToRemove);
			}

			LookForGesture(jt);
		}

		protected abstract void LookForGesture(JointType jt);

		protected void RaiseGestureDetected( GestureType gType )
		{
			if (DateTime.Now.Subtract(lastGestureDate).TotalSeconds > MinimalPerBetweenGestures)
			{
				if (OnGestureDetected != null) OnGestureDetected(gType);
				lastGestureDate = DateTime.Now;
			}
			Entries.Clear();
		}
	}
}
```
もう記憶が曖昧になってきた…
EntryデータをListに追加していってトータルの差分を計測します。
ここで追加されるのは対象となるJointのPosition.x , y ,z です。
windowSizeの数だけデータを保管していますが、その理由はよく覚えていません…

## SwipeGestureDetector.cs
```c#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Kinect;

namespace v2SwipeGesture
{
	public class SwipeGestureDetector : GestureDetector
    {
		public float SwipeMinimalLength { get; set; }
		public float SwipeMaximalHeight { get; set; }

		public int SwipeMininalDuration { get; set; }
		public int SwipeMaximalDuration { get; set; }

		public SwipeGestureDetector( int windowSize = 20
			, float minLen = 0.4f
			, float maxHgt = 0.15f
			, int minDur = 250
			, int maxDur = 700
			)
			: base(windowSize)
		{
			SwipeMinimalLength = minLen;		//始点と終点の横幅
			SwipeMaximalHeight = maxHgt;     //始点と終点の高低差

			SwipeMininalDuration = minDur;     //ジェスチャ判定時間最小値(ミリ秒)
			SwipeMaximalDuration = maxDur;     //ジェスチャ判定時間最大値(ミリ秒)
		}

		protected bool ScanPositions(Func<Vector3, Vector3, bool> hFunc, Func<Vector3, Vector3, bool> directionFunc,
										Func<Vector3, Vector3, bool> lFunc, int minTime, int maxTime)
		{
			int start = 0;

			for (int idx = 1; idx < Entries.Count - 1; idx++)
			{
				if (!hFunc(Entries[0].Position, Entries[idx].Position) || !directionFunc(Entries[idx].Position, Entries[idx + 1].Position))
				{
					start = idx;
				}

				if (lFunc(Entries[idx].Position, Entries[start].Position))
				{
					double totalMilliseconds = (Entries[idx].Time - Entries[start].Time).TotalMilliseconds;
					if (totalMilliseconds >= minTime && totalMilliseconds <= maxTime)
					{
						return true;
					}
				}
			}			
			return false;
		}

		protected override void LookForGesture(JointType jt)
		{
			//Right to Left 
			if (ScanPositions((p1, p2) => Math.Abs(p2.Y - p1.Y) < SwipeMaximalHeight,	// Height
					(p1, p2) => p2.X - p1.X < 0.01f,									// Progression to right
					(p1, p2) => (p2.X - p1.X) > SwipeMinimalLength,						// Length
					SwipeMininalDuration, SwipeMaximalDuration))						// Duration
			{
				RaiseGestureDetected(GestureType.SwipeRightToLeft);
				return;
			}

			//LeftToRight
			if (ScanPositions((p1, p2) => Math.Abs(p2.Y - p1.Y) < SwipeMaximalHeight,	// Height
					(p1, p2) => p2.X - p1.X > -0.01f,									// Progression to right
					(p1, p2) => Math.Abs(p2.X - p1.X) > SwipeMinimalLength,				// Length
					SwipeMininalDuration, SwipeMaximalDuration))						// Duration
			{
				RaiseGestureDetected(GestureType.SwipeLeftToRight);
				return;
			}

			//Top to Bottom
			if (ScanPositions((p1, p2) => Math.Abs(p2.X - p1.X) < SwipeMaximalHeight,	// Height
					(p1, p2) => p2.Y - p1.Y < 0.01f,									// Progression to right
					(p1, p2) => (p2.Y - p1.Y) > SwipeMinimalLength,						// Length
					SwipeMininalDuration, SwipeMaximalDuration))						// Duration
			{
				RaiseGestureDetected(GestureType.SwipeTopToBottom);
				return;
			}

			//Bottom to Top 
			if (ScanPositions((p1, p2) => Math.Abs(p2.X - p1.X) < SwipeMaximalHeight,	// Height
					(p1, p2) => p2.Y - p1.Y > -0.01f,									// Progression to right
					(p1, p2) => Math.Abs(p2.Y - p1.Y) > SwipeMinimalLength,						// Length
					SwipeMininalDuration, SwipeMaximalDuration))						// Duration
			{
				RaiseGestureDetected(GestureType.SwipeBottomToTop);
				return;
			}
		}
        
    }
}
````
このクラスがミソになります。
コンストラクタでジェスチャの定義をしていますが、デフォルトで
**横方向に40cm以上、縦方向に15cm以上、0.25秒以上、0.75秒以下**
の範囲がジェスチャとして認識されるようになっています。
ターゲットとなるJointのPositionを毎フレーム取得＆リスト化し、
リスト内の差分が上記の設定を満たした場合にRaiseGestureDetectedイベントが発生するって仕組みです（確か）

上記設定値を右手に適用した場合を例にすると、
右手を0.25秒以上、0.75秒以内に横に40cm動かしたら
RaiseGestureDetectedイベントでSwipeRightToLeft(あるいはSwipeLeftToRight)が検出される形になります。

このモジュールはdllとして出力した上でWPFアプリケーションを対象に使用します。
参照にv2SwipeGesture.dllを追加してMainWindow.csとかで

```c#
using v2SwipeGesture;
```

と追記するだけで利用可能です。