特集:コードで理解するLeapアプリ開発の概要
C#開発者から見たLeap Motion開発のファースト・インプレッション
手や指の動きを読み取って、さまざまな処理を行うアプリを作成できる「Leap Motion」の一般販売がついに開始された。その開発はどのようなものなのか? SDKに含まれる最も基本的なソース・コードを眺めてみよう。
※2013/07/29追記: C++言語向けの連載はこちらです。
※2013/08/29追記: VB言語向けの連載はこちらです。
2013年7月22日(米国時間)、ついにLeap Motionの一般販売が開始された。Leap Motionとは、簡単にいえばKinectの機能を手と指だけ(+手に関連付けられた道具。例えばペンなど)に絞ったデバイスである。そのため、デバイスはフリスクのケース程度の大きさで(高さ:1.27cm、横幅:3cm、奥行き:7.62cm、重さ:45.4g)、価格も(執筆時点で)8200円程度(※税金や送料を含まない場合)とお手頃だ。
その内容や可能性は、言葉で説明するよりも、実際に動いている映像を見た方が早いだろう(Leap Motionをあまりよく知らないという方は、ぜひ次の動画を再生してほしい)。
Introducing the Leap Motion
昨日から世界中で「製品が届いた!」という喜びの声が上がっている。筆者は実際にはまだ手にしていないが、その開発SDKは入手できる状態なので、さっそく「そのSDKが提供する最も基本的なサンプル・コードを見て、Leap Motionアプリケーション(以降、「Leapアプリ」)の開発がどのようなものか」を調べてみた。本稿では、その内容を紹介する。なお、このSDK使うと、手/指/道具の動きの情報(=モーション・キャプチャ情報)を取得したりできる。
SDKのダウンロード
下記のリンク先から、Windows/Mac OS X/Linux向けにSDKをダウンロードできる。実際にダウンロードするには、事前にサインアップが必要になる(サインアップは無料で行える)。
本稿では、Windows用のSDKをダウンロードした。
対応している開発言語について
SDKが提供するLeap Motion APIライブラリでは、
- C++
- C#
- Java
- Python
- Objective-C
- JavaScript
と数多くの開発言語に対応している。
本稿では、筆者が最も得意なC#を用いる(※SDKは、.NET Framework 3.5および4.0に対応。.NET互換の「Mono」にも対応している。また、Unity Pro用のプラグインも提供している)。また、本稿で紹介するサンプル・コードは、SDKに含まれている「\LeapSDK\samples\Sample.cs」である。詳しいコード内容を知りたい場合は、ぜひSDKをダウンロードして参照してほしい。
それでは、さっそくLeapアプリの最も基本的なサンプル・コードを見ていこう(なお、以降で単に「Leap」と表記している場合は、「Leap Motion」を指す)。
最も基本的なLeapアプリのサンプル・コード(C#)
本稿で紹介するLeapアプリは、Leapから受け取ったフレーム情報(詳細後述)やジェスチャー情報(詳細後述)をコンソール出力するだけのシンプルなアプリだ。前述のとおり、筆者は実機を持っておらず、実行しても何も情報は出力されないので、実行画面はキャプチャしていない。ご了承いただきたい。
Mainメソッドの実装
まずは、最初に呼び出されるMainメソッドから見ていこう。下記のコードのようになっている(※本稿では、コードの一部を日本語に翻訳している)。
using System;
using Leap;
class Sample
{
public static void Main ()
{
// サンプルのリスナーとコントローラーを作成
SampleListener listener = new SampleListener ();
Controller controller = new Controller ();
// サンプルのリスナーが、コントローラーからイベントを受け取るよう設定
controller.AddListener (listener);
// [Enter]キーが押されるまで、このプロセスの実行を続ける
Console.WriteLine ("[Enter]キーを押すと終了...");
Console.ReadLine ();
// 実行終了時には、サンプル・リスナーを削除
controller.RemoveListener (listener);
controller.Dispose ();
}
}
|
※本アプリを実行するには、SDKの「\LeapSDK\lib\x86」/「\LeapSDK\lib\x64」フォルダの中身を、プロジェクト「bin\Debug」/「bin\Release」フォルダにコピーする必要がある。
上記のコードを理解するのは、(日本語でコメントも記述しているので)大して難しくないだろう。本稿では、コード内容の詳しい説明は割愛する。その代わりに、ライブラリが提供する機能やクラスの、基本的な概念や使い方について簡単に紹介する。
上記のコードで重要なのは、「コントローラー(Controller)」と「リスナー(Listener)」という概念である。
- コントローラー: Leapとアプリの間のインターフェイス。Controllerクラス(Leap名前空間)をインスタンス化して使う
- リスナー: Leapによって発火されたイベントを処理するために使用する。Listenerクラス(Leap名前空間)を派生したクラスをインスタンス化して使う
リスナーの実装
上記のコード例では、SampleListenerクラスがリスナーに当たるが、このクラスは次のように定義されている(一部省略)。
class SampleListener : Listener
{
private Object thisLock = new Object();
private void SafeWriteLine(String line)
{
lock (thisLock)
{
Console.WriteLine(line); // コンソール出力
}
}
public override void OnInit(Controller controller)
{
SafeWriteLine("Initialized");
}
public override void OnConnect(Controller controller)
{
SafeWriteLine("Connected");
……省略……
}
public override void OnDisconnect(Controller controller)
{
// 注意: デバッガー内で実行されているときは呼び出されない
SafeWriteLine("Disconnected");
}
public override void OnExit(Controller controller)
{
SafeWriteLine("Exited");
}
public override void OnFrame(Controller controller)
{
……省略……
}
}
|
このコードを見ると、5つの仮想メソッドがオーバーライドされているのが分かる。それぞれのメソッドは、下記のようなタイミングで呼び出されることになる。
- OnInitメソッド: リスナーの登録先のコントローラーが初期化されたときに、1回だけ
- OnConnectメソッド: コントローラーがLeapに接続して、モーション・トラッキング・データ(motion tracking data)のフレーム(frames。詳細後述)を送信開始する準備が整ったとき
- OnDisconnectメソッド: コントローラーがLeapから接続解除した場合(例えば、Leapデバイスが取り外されたり、Leapソフトウェアが強制終了したりした場合)
- OnExitメソッド: リスナーがコントローラーから削除されたとき(もしくは、コントローラーが破棄されたとき)
- OnFrameメソッド: モーション・トラッキング・データの新しいフレームが利用可能になったとき
ちなみに、このほかにもOnFocusGainedメソッド(=アプリが前面になったときに発生)やOnFocusLostメソッド(=アプリが前面ではなくなったときに発生)もオーバーライドできる。
上記のコード例では、OnConnectメソッドとOnFrameメソッドの中身を省略している。以下では、それぞれの省略した部分の内容について説明しよう。
OnConnectメソッドの実装内容
OnConnectメソッドの中は下記のようになっている。
public override void OnConnect(Controller controller)
{
SafeWriteLine("Connected");
controller.EnableGesture(Gesture.GestureType.TYPECIRCLE);
controller.EnableGesture(Gesture.GestureType.TYPEKEYTAP);
controller.EnableGesture(Gesture.GestureType.TYPESCREENTAP);
controller.EnableGesture(Gesture.GestureType.TYPESWIPE);
}
|
このコードの意味は、書かれている英語の内容どおり、ジェスチャー(Gesture)を有効(Enable)にしているだけである。それぞれを有効にすることで、実際にLeapから各ジェスチャーを取得できるようになる(※必要なものだけを有効にすればよい)。
そのジェスチャーには、4つの種類(Type)があることが分かる。それぞれの意味は下記のとおりだ。
- サークル(TYPECIRCLE): 指で円を描く動作
- キー・タップ(TYPEKEYTAP): 指で(あたかもキーを押しているように)「下方向」にタップする動作
- スクリーン・タップ(TYPESCREENTAP): 指で(あたかもスクリーンを押しているように)「前方向」にタップする動作
- スワイプ(TYPESWIPE): 指を伸ばした状態の手で直線を描く動作
これらの動作は言葉だけでは伝わりにくいので、Leap Motion SDKのAPIドキュメントから、各動作の絵を引用しておこう(次の画像)。
OnFrameメソッドの実装内容
有効になった各ジェスチャーのデータは、フレームから取得できる。フレームとは、手や指のトラッキング・データのセットを格納した、Frameクラス(Leap名前空間)のオブジェクトのことだ。
前述したように、新しいフレームが発生するたびにOnFrameメソッドが呼ばれる。従って、そのOnFrameメソッド内で、ジェスチャー情報やフレーム情報をFrameオブジェクトから取得し、その情報を活用してアプリ独自の処理を実装すれば、さまざまな独自の機能が実現できるというわけだ。
OnFrameメソッドは少し長いので、「フレーム情報の取得」と「ジェスチャー情報の取得」の2つに分けてコード内容を紹介しよう。
フレーム情報の取得
まずは、フレーム情報を取得して独自の処理を実装している部分のコードを示す。
public override void OnFrame(Controller controller)
{
// 最新のフレームを取得して、基本情報をレポートする
Frame frame = controller.Frame();
SafeWriteLine("フレームID: " + frame.Id
+ ", タイムスタンプ: " + frame.Timestamp
+ ", 手の数: " + frame.Hands.Count
+ ", 指の数: " + frame.Fingers.Count
+ ", 道具の数: " + frame.Tools.Count
+ ", ジェスチャーの数: " + frame.Gestures().Count);
if (!frame.Hands.Empty)
{
// 1つ目の手を取得
Hand hand = frame.Hands[0];
// 手に指があるかチェック
FingerList fingers = hand.Fingers;
if (!fingers.Empty)
{
// 手の指先の平均的な位置を計算
Vector avgPos = Vector.Zero;
foreach (Finger finger in fingers)
{
avgPos += finger.TipPosition;
}
avgPos /= fingers.Count;
SafeWriteLine("手には、" + fingers.Count
+ "本の指があり、指先の平均的な位置は: " + avgPos);
}
// 手の球半径と手のひらの位置を取得
SafeWriteLine("手の球半径: " + hand.SphereRadius.ToString("n2")
+ " mm, 手のひらの位置: " + hand.PalmPosition);
// 手のひらの法線ベクトルと(手のひらから指までの)方向を取得
Vector normal = hand.PalmNormal;
Vector direction = hand.Direction;
// 手のピッチとロールとヨー角を計算
SafeWriteLine("手のピッチ: " + direction.Pitch * 180.0f / (float)Math.PI + " 度, "
+ "ロール: " + normal.Roll * 180.0f / (float)Math.PI + " 度, "
+ "ヨー角: " + direction.Yaw * 180.0f / (float)Math.PI + " 度");
}
……省略……
}
|
このコードもコメントが多く入っており、大体の実装内容は理解できると思われるので、説明は割愛する。新出のクラスには以下のようなものがある(いずれもLeap名前空間に所属)。
- Handクラス: 検出された手の物理的な特徴をレポート
- HandListクラス: Handオブジェクトのリスト。FrameオブジェクトのHandsプロパティで取得できる
- Fingerクラス: トラッキングしている指を表現
- FingerListクラス: Fingerオブジェクトのリスト。FrameオブジェクトのFingersプロパティで取得できる
- Toolクラス: トラッキングしている道具(例えばペンなど)を表現
- ToolListクラス: Toolオブジェクトのリスト。FrameオブジェクトのToolsプロパティで取得できる
- Gestureクラス: 認識されたユーザーの動きを表現
- GestureListクラス: Gestureオブジェクトのリスト。FrameオブジェクトのGesturesメソッドで取得できる
- Vector構造体: 3次元ベクトル情報を表現
ジェスチャー情報の取得
次に、ジェスチャー情報を取得して独自の処理を実装している部分のコードを示す。
public override void OnFrame(Controller controller)
{
……省略……
// 全ジェスチャーを取得して、個別に処理する
GestureList gestures = frame.Gestures();
for (int i = 0; i < gestures.Count; i++)
{
Gesture gesture = gestures[i];
switch (gesture.Type)
{
case Gesture.GestureType.TYPECIRCLE:
CircleGesture circle = new CircleGesture(gesture);
// 回転方向を計算
String clockwiseness;
if (circle.Pointable.Direction.AngleTo(circle.Normal) <= Math.PI / 4)
{
// 角度が90度以下なら、時計回り
clockwiseness = "時計回り";
}
else
{
clockwiseness = "反時計回り";
}
float sweptAngle = 0;
// 最後のフレームから回った角度を計算
if (circle.State != Gesture.GestureState.STATESTART)
{
CircleGesture previousUpdate = new CircleGesture(controller.Frame(1).Gesture(circle.Id));
sweptAngle = (circle.Progress - previousUpdate.Progress) * 360;
}
SafeWriteLine("サークルID: " + circle.Id
+ ", " + circle.State
+ ", 進度: " + circle.Progress
+ ", 半径: " + circle.Radius
+ ", 角度: " + sweptAngle
+ ", " + clockwiseness);
break;
case Gesture.GestureType.TYPESWIPE:
SwipeGesture swipe = new SwipeGesture(gesture);
SafeWriteLine("スワイプID: " + swipe.Id
+ ", " + swipe.State
+ ", 位置: " + swipe.Position
+ ", 方向: " + swipe.Direction
+ ", スピード: " + swipe.Speed);
break;
case Gesture.GestureType.TYPEKEYTAP:
KeyTapGesture keytap = new KeyTapGesture(gesture);
SafeWriteLine("キー・タップID: " + keytap.Id
+ ", " + keytap.State
+ ", 位置: " + keytap.Position
+ ", 方向: " + keytap.Direction);
break;
case Gesture.GestureType.TYPESCREENTAP:
ScreenTapGesture screentap = new ScreenTapGesture(gesture);
SafeWriteLine("スクリーン・タップID: " + screentap.Id
+ ", " + screentap.State
+ ", 位置: " + screentap.Position
+ ", 方向: " + screentap.Direction);
break;
default:
SafeWriteLine("未知のジェスチャー・タイプです。");
break;
}
}
if (!frame.Hands.Empty || !frame.Gestures().Empty)
{
SafeWriteLine("");
}
}
|
前掲のコードで4種類のジェスチャーを有効にしたが、上記のコードではそれらのジェスチャー種別を判別して、それに基づき下記の4種類いずれかのジェスチャー・オブジェクトを取得している(いずれもLeap名前空間に所属)。
- CircleGestureクラス: サークルのジェスチャーを表現
- KeyTapGestureクラス: キー・タップのジェスチャーを表現
- ScreenTapGestureクラス: スクリーン・タップのジェスチャーを表現
- SwipeGestureクラス: スワイプのジェスチャーを表現
ここでも詳しいロジックの説明は割愛するが、不明なクラスやプロパティなどがある場合は、APIドキュメント(英語)を参照してほしい。
■
以上、Leapアプリの最も基本的なサンプルC#コードを一通り見た。本稿のような短い内容でもSDKに用意されている大半のクラスは登場したので、Leap Motion APIが非常にシンプルで基礎的なライブラリであることが分かる。
(コードを読んだだけだが)「Leap開発で難しい」と感じたのは、やはり「ジェスチャー情報をどう調理して独自の機能を実装していくか」というところだろう。その部分が開発者の腕の見せどころなのだが、少なからず数学的な素養が必要になってしまうので、特に数学が苦手な筆者のような開発者は大変だろう。しかし、「そのような困難を乗り越えてでもチャレンジする価値が、Leap開発にはある」と筆者は考えている。
Leap Motion向けにどのようなアプリが開発できるかは現時点で未知数だ。だからこそ、世の中を驚かせるような、誰も見たことがない面白いアプリが開発できる可能性もある。今後、どんなLeapアプリが出てくるか、本当に楽しみである。
Leap Motionの記事を募集しています!
Leap Motionの解説記事を執筆したい方、開発言語は問いませんので(特にJavaScriptは歓迎)、ぜひお問い合わせに示すメール・アドレス宛に一度ご連絡ください。その際、ご希望のタイトルや連載回数などもお知らせください。よろしくお願いします。