連載:C++で始めるLeap Motion開発 ―― タッチUIの先のカタチ ――
Leap SDKで指を検出してみよう(Tracking Hands, Fingers, and Tools)
Leap Motionの最大の特長である手・指を検出するには? Leap Motion Developer SDKを活用した開発方法を詳しく解説。
今回はLeap Motionの最大の特長である手・指の検出について解説する。Leap Motion Developer SDK(以下、Leap SDKとする)では、手や指の扱い方がシンプルであり、座標や方向のベクトルを表すLeap::Vector
クラスも使いやすい。これらの情報を組み合わせて、アプリケーションを開発することになるだろう。
全体のコードは次のリンク先で公開しているので、参照しながら以下の解説を読み進めてほしい(「LeapSample02」が今回のプロジェクトだ)。
Leap Motionで取得できるもの
Leap Motionで取得できる種類には次のものがある。
- 手
- 指
- ツール(=棒状のもの)
指およびツールはPointable
なものとしてまとめて扱うこともできる。
それぞれに、位置や方向、速さや向きといったパラメーターを持っている。
では取得方法から順に見てみよう(以下、()
と記載しているものは関数を表す)。
手/指/ツールを取得する
手/指/ツールを取得する方法はいくつかある。手の取得方法と、指/ツールの取得方法について、それぞれ下記に示す。
【手/指/ツール共通の取得方法】
●現在のフレームから、検出された指およびツールの一覧を取得し、その中から所望のものを探す(どの手の指かといったフィルターはなく、検出した指やツールが全て取得される)
・手の一覧はLeap::Frame::hand()
で取得する
・指の一覧はLeap::Frame::fingers()
で取得する
・ツールの一覧はLeap::Frame::tools()
で取得する
・ポインター(=指とツール)の一覧はLeap::Frame::pointables()
で取得する
●現在のフレームで、以前に取得したIDを渡すことで取得する
●現在のフレームで、一番左/右/手前の手を取得する
【指/ツールのみの取得方法】
●現在のフレームで、検出した手に属する指やツールを取得する
では、それぞれの取得方法を見てみよう
現在のフレームで検出した手などの一覧を取得する
まずは、現在のフレームで検出した全ての手/指/ツールを取得してみよう。
上述の通り、Leap::Frame
クラスのhands()
/fingers()
/tools()
/pointables()
で一覧を取得する。次のプログラムでは、取得した手などの数を表示する。なお、v2では指を全て追跡するため、伸びている指だけを取得したい場合にはfingers.extended().count()
を使用する(詳しくは「これは快適! Leap Motion v2で格段に良くなったSkeletal Tracking機能」を参照)。
// 今回のフレームで検出した全ての手、指、ツール Leap::HandList hands = frame.hands(); Leap::FingerList fingers = frame.fingers(); Leap::ToolList tools = frame.tools(); Leap::PointableList pointables = frame.pointables(); std::cout << "Frame Data : " << " Hands : " << hands.count() << " Fingers : " << fingers.count() << " Extended Fingers : " << fingers.extended().count() << " Tools : " << tools.count() << " Pointers : " << pointables.count() << std::endl; |
リスト1を実行すると図1~3のようになる。片手、両手や手をグーパーしてみると動作が分かる。ツールは太めのペンを使うとよいだろう。
なお、手およびツール(棒)を検出させる際には[Leap Motionコントロール パネル]の[トラッキング]タブから[道具の追跡]および[手の追跡]のチェックボックスがチェックされていることを確認する(図4)。
現在のフレームで、以前に取得したIDを渡すことで取得する
次に、IDを利用した取得方法を解説する。
手などを検出すると個別に一意のIDが振られる。このIDをキーとして目的のデータを取得する。検出した手などを、フレームをまたいで追跡し続ける場合などに利用する方法だ。
次のプログラムでは、手のIDを取得し、そのIDの手を取得し続ける。Leap SDKでは手などを検出しなかった場合でも、既定の無効値のデータが返される。そのため、インデックスのチェックなどは不要となっている。有効なIDを取得できた場合には、そのIDでフレームからデータを取得し、表示するようにしている。
if ( handId == -1 ) { handId = frame.hands()[0].id(); } else { Leap::Hand hand = frame.hand( handId ); handId = hand.id(); // 手の情報を表示する std::cout << "ID : " << hand.id() << " 位置 : " << hand.palmPosition() << " 速度 : " << hand.palmVelocity() << " 法線 : " << hand.palmNormal() << " 向き : " << hand.direction() << std::endl; } |
現在のフレームで、一番左/右/手前の手を取得する
例えば、両手を使った操作、2本の指でマルチタッチを模すような操作で、左右それぞれの手を取得したい場合に利用する。Leap::HandList
/Leap::FingerList
/Leap::ToolList
/Leap::PointableList
というクラスそれぞれにleftmost()
/rightmost()
/frontmost()
が定義されている。例えば、左右の手の位置を取得したい場合には、Leap::HandList::leftmost()
およびLeap::HandList::rightmost()
を呼び出すだけで取得できる。もし手を1つしか検出していない場合には、左右で同じIDが返ってくるので問題ない。
Leap::HandList hands = frame.hands(); Leap::Hand leftMost = hands.leftmost(); Leap::Hand rightMost = hands.rightmost(); Leap::Hand frontMost = hands.frontmost(); std::cout << " 左 : " << leftMost.palmPosition() << " 右 : " << rightMost.palmPosition() << " 手前 : " << frontMost.palmPosition() << std::endl; |
現在のフレームで、手の情報を取得する
v2で追加になった左右の手の検出、ピンチおよびグラブ(いずれの意味も後述)の開閉度合い、手の信頼性を取得する。
右手や左手の判別には、左右の手を出すとよい。手を回転させたり開閉させたりしてみよう。手のひらをLeap Motionに向けた場合と手の甲を向けた場合の値についても見てみよう。
ピンチは親指と人差し指の開閉度合い、グラブは手全体の開閉度合いだ(グー、パーする動作)。信頼性は、手を回転させてみると値が変わる。信頼性パラメーターの値によって、手の確実性をフィルタリングできる。
Leap::Hand hand = frame.hands()[0]; std::cout << " 右手 : " << hand.isRight() << " ピンチ : " << hand.pinchStrength() << " グラブ : " << hand.grabStrength() << " 信頼性 : " << hand.confidence() << std::endl; |
現在のフレームで、検出した手に属する指やツールを取得する
これは、ポインター(=手とツール)についての取得方法だ。
指のデータを利用する際に、「どの手の指か」ということを知りたい場合があるだろう。その場合には、検出した手から指を取得することで実現できる。ツールについても同様だが、ツールは細長い棒状のものになるので、必ずしも手に属しているわけではないことに注意されたい(フレーム内で独立している)。
次のリスト5のプログラムでは、検出した全ての手に対して、その手に属するポインター/指/ツールの数を表示している。実際にこのプログラムを実行すると(図5)、先のフレームからの取得では、例えば両指10本が取得されるが、このプログラムでは2つの検出した手に対して5本ずつの指が取得できるのが分かるだろう。
// 手に属している指とツールを取得する for ( auto hand : frame.hands() ) { std::cout << "ID : " << hand.id() << " Fingers : " << hand.fingers().count() << " Extended Fingers : " << hand.fingers().extended().count() << " Tools : " << hand.tools().count() << " Pointers : " << hand.pointables().count() << std::endl; } |
現在のフレームで、検出した指の情報を取得する
いよいよ指の情報を取得する。伸びている指について表示する。type()
やtipPosition()
、tipVelocity()
、direction()
で指の種類、指先の位置、指の動作速度、指の向きを取得できる。位置の座標系は後述するLeap Motionの3次元座標系である。これらのデータを使って指を使ったアプリケーションを開発する。
// 指の情報を取得する for ( auto finger : frame.fingers().extended() ) { std::cout << "ID : " << finger.id() << " 種類 : " << finger.type() << " 位置 : " << finger.tipPosition() << " 速度 : " << finger.tipVelocity() << " 向き : " << finger.direction() << std::endl; } |
例えば、チョキ(=人差し指と中指)を出すと次のようになる。指の種類は親指が0、人差し指が1、中指が2、薬指が3、小指が4となる。
検出した指の関節情報を取得する
最後に指の関節の情報を取得する。Leap Motion SDKではBone
クラスとして定義されており「指骨」を表す。指骨の場所はLeap::Bone::Type
列挙体で次の4つが定義されている。
値 | 意味 |
---|---|
TYPE_DISTAL | 末節骨 |
TYPE_INTERMEDIATE | 中節骨 |
TYPE_PROXIMAL | 基節骨 |
TYPE_METACARPAL | 中手骨 |
※下の図に合わせた順番で並べたが、各メンバーの値は上から「3」「2」「1」「0」となるので注意されたい。
指骨の位置はprevJoint()
、center()
、nextJoint()
で取得でき、指骨の場所と位置の関係は次のようになっている。
これらをコードにすると次のようになる。例えばprevJoint()
のみを描画していけば、Leap Motionのビジュアライザーのような手のモデルを表示できる。
// 指の関節情報を取得する for ( auto finger : frame.fingers() ) { // 末節骨(指先の骨) auto bone = finger.bone(Leap::Bone::TYPE_DISTAL); std::cout << "種類 : " << bone.type() << " 中心 : " << bone.center() << " 上端 : " << bone.prevJoint() << " 下端 : " << bone.nextJoint() << std::endl; } |
なお、親指については骨が3つとなっている。下記のコードで、どの骨が有効であるかを確認できる。実行すると、TYPE_METACARPAL(中手骨)の長さが0であるため、Leap MotionとしてはTYPE_PROXIMAL(基節骨)、TYPE_INTERMEDIATE(中節骨)、TYPE_DISTAL(末節骨)から構成されていることが分かる。
// 親指の定義を確認する for ( auto finger : frame.fingers() ) { if ( finger.type() == Leap::Finger::Type::TYPE_THUMB ){ for ( int t = Leap::Bone::TYPE_METACARPAL; t <= (int)Leap::Bone::TYPE_DISTAL; ++t ){ // 末節骨(指先の骨) auto bone = finger.bone( (Leap::Bone::Type)t ); std::cout << "種類 : " << bone.type() << " 長さ : " << bone.length() << std::endl; } } } |
手や指から得られる情報
手などの取得方法を理解したところで、そこからどのような情報を得られるのかを見てみよう。Leap::Hand
クラスから得られる主な情報を次の表に示す。
関数 | 得られる情報 |
---|---|
palmPosition() | 手の位置 |
palmVelocity() | 手の動作速度(ミリメートル/秒) |
palmNormal() | 手の法線ベクトル |
direction() | 手の向き |
sphereCenter() | 手にフィットする球体の中心座標 |
sphereRadius() | 手にフィットする球体の半径 |
pinchStrength() | ピンチ動作の開閉度合い |
grabStrength() | グラブ動作の開閉度合い |
isRight() | 右手かどうか |
isLeft() | 左手かどうか |
confidence() | 検出した手の信頼度 |
指およびツールはそれぞれLeap::Finger
クラスとLeap::Tool
クラスであり、両方ともポインターを表すLeap::Pointable
クラスを継承している。Leap::Pointable
クラスで取得できる主な情報を次の表に示す。
関数 | 得られる情報 |
---|---|
tipPosition() | ポインターの位置 |
tipVelocity() | ポインターの動作速度(ミリメートル/秒) |
direction() | ポインターの向き |
isFinger() | ポインターが指かどうか |
isTool() | ポインターがツールかどうか |
これらの座標系は次の図に示すLeap Motionの座標系で取得できる。この座標を任意の形に加工してアプリケーションで使うことになるだろう。
手にフィットする球体の座標および半径は、第1回で紹介した次の図の情報だ。
[補足]Leap::Vectorクラス
手の位置や、法線、速度はベクトルとして返される。ベクトルはLeap::Vector
クラスで表されるが、標準で距離や内積、外積、四則計算など、よく使う機能がすでに実装されている。
より詳細な機能は、「Leap::Vectorのリファレンス(英語)」を参照してもらいたいが、このようにSDKレベルで細かい機能が提供されていることは非常にうれしい(と感じるのは、Kinect SDKやIntel RealSense SDKなど、Vector
クラスが最低限の座標データを保持する機能しか持っていない環境の利用経験が長かったからだろうか……)。
まとめ
このようにLeap SDKでは手や指のさまざまなデータを簡単に扱える。扱い方もほぼ同じなので、1つを覚えれば簡単に他のデータも扱えるだろう。また、Vector
クラスが非常に使いやすいので、他のSDKでも使いたいものだ。
※以下では、本稿の前後を合わせて5回分(第1回~第5回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
1. 初めてのLeap Motion開発
Leap Motion Developer SDKを利用してC++言語でLeapアプリを開発する方法を、サンプルコードを示しながら解説する連載(2015年改訂版)。SDK提供のサンプルコードを基にLeapアプリ開発の基本的な流れを説明する。
2. 【現在、表示中】≫ Leap SDKで指を検出してみよう(Tracking Hands, Fingers, and Tools)
Leap Motionの最大の特長である手・指を検出するには? Leap Motion Developer SDKを活用した開発方法を詳しく解説。
3. Leap Motionでのタッチ操作はどう開発するのか?
Leapアプリのタッチ操作の認識方法と開発方法を説明。今回のサンプルでは、タッチを表現するためのGUIフレームワークとして「Cinder」を利用する。
5. Leap SDKのいろいろな使い方(フレームデータ、イベントなど)
データ取得方式「コールバック」「ポーリング」の選択指針とは? Leap Motionイベントをポーリング方式で処理する方法や、フレーム履歴、手/指IDの取得についても解説。