Deep Insider の Tutor コーナー
>>  Deep Insider は本サイトからスピンオフした姉妹サイトです。よろしく! 
連載:C++で始めるLeap Motion開発 ―― タッチUIの先のカタチ ――

連載:C++で始めるLeap Motion開発 ―― タッチUIの先のカタチ ――

Leap SDKのいろいろな使い方(フレームデータ、イベントなど)

2015年7月31日 改訂 (初版:2013/8/19)

データ取得方式「コールバック」「ポーリング」の選択指針とは? Leap Motionイベントをポーリング方式で処理する方法や、フレーム履歴、手/指IDの取得についても解説。

Natural Software 中村 薫
  • このエントリーをはてなブックマークに追加

 今回は「Getting Frame Dataサンプル(英語)」を題材にしたさまざまなフレームの扱い方について解説する。

フレームデータの扱い方

 Leap SDKからのデータの取得方法には、次の2つの方式がある。

  • 1Leap::Listenerクラスを継承したクラスを実装し、必要なイベント(の仮想関数)をLeap::Listenerクラスからオーバーライドして通知を受けるコールバック方式
  • 2メインループを実装し、その中で更新されたフレームの取得を行うポーリング方式

 実現できることは同じなので、言語やフレームワーク、アプリケーションによって選択してほしい。使い分けの例として次のような考え方を示す。

1コールバック方式

 ・C++でフレームワークを使わない場合。自分でメインループを実装してもいいが、コードが増えてきたときにクラス化されていた方が何かと実装しやすい
 ・C#でWPFを使って実装する場合(1)。WPFアプリで一般的なイベント駆動の仕組みに合わせてコールバック方式(イベント駆動)で実装する

2ポーリング方式

 ・C++でOpenGLを使って実装する場合。OpenGLのDrawのタイミングでデータを取得し、処理する
 ・C#でWPFを使って実装する場合(2)。C#の「Touch Emulationサンプル(英語)」にもあるように、レンダリング(=CompositionTargetクラス(System.Windows.Media名前空間)のRenderingイベント)のタイミングで処理する

 第1回でも解説したとおり、Leap SDKからコールバック形式でデータを受け取る場合、表1のイベントが発生する。今回は、これらをポーリング方式で実装する場合について解説する。

イベント意味
onInit() アプリケーションの初期化時に呼ばれる
onConnect() Leap Motionコントローラーが接続された時に呼ばれる
onDisconnect() Leap Motionコントローラーが抜かれた時に呼ばれる
onExit() アプリケーションの終了時に呼ばれる
onFrame() フレームの更新時に呼ばれる
onFocusGained() アプリケーションのフォーカスが有効になった時に呼ばれる(デフォルトモードでは、このイベント以降、フレームの更新が行われる)
onFocusLost() アプリケーションのフォーカスが無効になった時に呼ばれる(デフォルトモードでは、このイベント以降、フレームの更新が停止する)
onDeviceChange() Leap Motionコントローラーが接続/切断された時に呼ばれる
onServiceConnect() Leap Motionのデータを取得しているWindowsサービスに接続した時に呼ばれる
onServiceDisconnect() Leap Motionのデータを取得しているWindowsサービスに切断された時に呼ばれる
表1 イベントを処理するための関数の一覧

[イベント]列でonInit()のように()が記述されているものは「関数」を指す。以下、()は同様に「関数」を表すものとする。

Leap Motionで発生するイベントを「ポーリング方式」で処理する場合

 まずLeap SDKからのイベントについておさらいしよう(表1を参照)。

 onInit()およびonExit()は、アプリケーションの初期化および終了処理になるので、メインループの前後に行う。

 onFrame()は、フレームの更新処理なので、先ほど説明したように「OpenGLのDrawのタイミング」や「レンダリングのタイミング」、もしくは自作のメインループの中で行う。取得したフレームにはフレームIDがあり、フレームが更新されるとID値がインクリメントされる。前回のフレームIDと異なったときに、新しいフレームと判断して処理する。

 次のコードは、ポーリング方式を実現する最もシンプルなコンソールアプリケーションのコードの一部(=main()の中身)である。この例では、while文により無限ループ(=メインループ)を作り、その中でフレームの更新処理を行っている。

C++
Leap::Controller leap;
int64_t previousFrameId = -1;

// 初期化処理(oninit()相当)をここに書く

// 無限ループ内で、前回のフレームのIDと比較して新しいフレームを取得する
while ( 1 ) {
  auto frame = leap.frame();
  if ( previousFrameId == frame.id() ) {
    continue;
  }

  previousFrameId = frame.id();

  // フレーム更新処理(onFrame()相当)をここに書く
}

// 終了処理(onExit()相当)をここに書く
リスト1 ポーリング方式でのアプリケーション実装例

while文により無限ループにしているが、frame()にてデータ取得中はブロッキングされるため、CPUが100%になる心配はない。

 それではこれ以外のイベント、Leap Motionコントローラーとの接続状態を表すonConnect()およびonDisconnect()、Leap Motionサービスとの接続状態を表すonServiceConnect()およびonServiceDisconnect()、フォーカス状態を表すonFocusGained()およびonFocusLost()は、どのように実装すればよいのか。

 Leap Motionコントローラーとの接続状態はLeap::Controller::isConnected()で、Leap Motionサービスとの接続状態はLeap::Controller:: isServiceConnected()で、取得できる。それぞれ、bool型の戻り値がtrueであれば接続されている、falseであれば接続されていないことを表す。フォーカス状態も同じようにLeap::Controller::hasFocus()で取得可能だ。やはりbool型の戻り値がtrueであればフォーカスが有効、falseであればフォーカスが無効であることを表す。

 この状態を利用することで、上記の各種イベント処理と同じ処理を実装可能だ。それぞれの実装イメージは次のとおりだ

C++
bool isPrevConnected = false;
bool isPrevServiceConnected = false;
bool hadPrevFocus = false;
int64_t previousFrameId = -1;

Leap::Controller leap;

// 初期化処理(oninit()相当)

while ( 1 ) {
  auto frame = leap.frame();

  // Leap Motionコントローラーとの接続状態を確認する
  {
    bool isCurrentConnected = leap.isConnected();
    if ( isPrevConnected != isCurrentConnected ) {
      if ( isCurrentConnected ) {
        // Leap Motion コントローラーが接続された(onConnected()相当)
        std::cout << "Leap Motion Controller connected." << std::endl;
      }
      else {
        // Leap Motion コントローラーが抜かれた(onDisconnected()相当)
        std::cout << "Leap Motion Controller disconnected." << std::endl;
      }
    }

    isPrevConnected = isCurrentConnected;
  }

  // Leap Motionサービスとの接続状態を確認する
  {
    bool isCurrentServiceConnected = leap.isServiceConnected();
    if ( isPrevServiceConnected != isCurrentServiceConnected ) {
      if ( isCurrentServiceConnected ) {
        // Leap Motionサービスが接続された(onServiceConnect()相当)
        std::cout << "Leap Motion Service connected." << std::endl;
      }
      else {
        // Leap Motionサービスが切断された(onServiceDisconnect()相当)
        std::cout << "Leap Motion Service disconnected." << std::endl;
      }
    }

    isPrevServiceConnected = isCurrentServiceConnected;
  }

  // フォーカス状態を確認する
  {
    bool hadCurrentFocus = leap.hasFocus();
    if ( hadPrevFocus != hadCurrentFocus ) {
      if ( hadCurrentFocus ) {
        // アプリケーションのフォーカスが有効になった(onFocusGained()相当)
        std::cout << "Focus gained." << std::endl;
      }
      else {
        // アプリケーションのフォーカスが無効になった(onFocusLost()相当)
        std::cout << "Focus lost." << std::endl;
      }
    }

    hadPrevFocus = hadCurrentFocus;
  }

  // フレームが更新されていなければ何もしない
  {
    if ( previousFrameId == frame.id() ) {
      continue;
    }

    previousFrameId = frame.id();
  }

  // フレーム更新処理(onFrame()相当)
}

// 終了処理(onExit()相当)
リスト2 isConnected()およびhasFocus()を利用したイベントの検出

 Leap Motionサービスの接続/切断については、Windowsのコントロールパネルなどから[ローカル サービスの表示]を起動して、「Leap Service」を探そう(図1)。このサービスを停止すると、onDisconnect()およびonServiceDisconnect()が発生し、サービスを起動するとonServiceConnect()およびonConnect()が発生する(それぞれ発生順)。

図1 Leap Motionサービスの切断テスト

フレームの詳細

 フレームについて詳しく解説しよう。

 今までは更新されたフレームのみ処理してきた。Leap SDKは、現在のフレームのほかに59フレーム前までのフレーム履歴を持っている。これによって、フレームをまたいだ処理(=平滑化や移動量の計算)を簡単に行える。

 また、Leap Motionは非常に速い周期でデータを更新するので*1、全てのフレームを更新タイミングで処理することは難しい。履歴を取得できることによって、複数のフレームをまとめて処理することもできる。

  • *1 速い場合には200FPS以上(FPS=1秒間のフレーム数)。つまり、5ミリ秒(msec)に1回更新される速度だ。

 過去のフレームを取得するには、Leap::Controller::frame()の引数に取得したいフレーム番号を渡す。最新のフレームは0となりデフォルト引数として設定されている。1フレーム前を取得したいならleap.frame( 1 )、10フレーム前を取得したいならleap.frame( 10 )という形だ。例として、現在のフレームと、過去5フレームを取得する方法について示す。

C++
Leap::Controller leap;
int previousFrameId = -1;

while ( 1 ) {
  // 最新のフレームを取得する(leap.frame( 0 ) と同じ)
  auto currentFrame = leap.frame();
  if ( previousFrameId == currentFrame.id() ) {
    continue;
  }

  previousFrameId = currentFrame.id();

  // 直前の5フレームを取得する
  std::cout << currentFrame.id() << ", ";
  for ( int i = 1; i <= 5; ++i ) {
    auto previousFrame = leap.frame( i );
    std::cout << previousFrame.id() << ", ";
  }
  std::cout << std::endl;
}

// 終了処理(onExit()相当)
リスト3 現在のフレームと、直前の5フレームを取得する

手や指のIDを取得する

 手や指にはIDが振られており、追跡している間は同じIDが使われる。

 このIDとフレーム履歴を用いることで、フレームをまたいだ処理をシンプルに実装できる。例として「Getting Frame Dataサンプル(英語)」にある「指のIDとフレーム履歴を利用して、10フレームの指の平均座標を求めるコード」を見てみよう。

C++
// 指の位置の平均値を計算する
int count = 0;
Leap::Vector average = Leap::Vector();
Leap::Finger fingerToAverage = frame.fingers()[0];
for ( int i = 0; i < 10; i++ ) {
  // フレーム履歴と指のIDで、過去の指の位置を取得する
  Leap::Finger fingerFromFrame = leap.frame(i).finger(fingerToAverage.id());
  if ( fingerFromFrame.isValid() ) {
    average += fingerFromFrame.tipPosition();
    count++;
  }
}

if ( count > 0 ) {
  average /= count;
  std::cout << "finger point : " << average << std::endl;
}
リスト4 指のIDとフレーム履歴を利用して、10フレームの指の平均座標を求める

 このようにフレームをうまく活用することで、指や手のデータを有効に活用できる。

まとめ

 以上、今回はさまざまなフレームの扱い方について説明した。自分の使いたいデータをいかに取得するかがキモであり、それが簡易に実装できるのが、Leap Motion SDKの良いところだ。

※以下では、本稿の前後を合わせて5回分(第2回~第6回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。

連載:C++で始めるLeap Motion開発 ―― タッチUIの先のカタチ ――
2. Leap SDKで指を検出してみよう(Tracking Hands, Fingers, and Tools)

Leap Motionの最大の特長である手・指を検出するには? Leap Motion Developer SDKを活用した開発方法を詳しく解説。

連載:C++で始めるLeap Motion開発 ―― タッチUIの先のカタチ ――
3. Leap Motionでのタッチ操作はどう開発するのか?

Leapアプリのタッチ操作の認識方法と開発方法を説明。今回のサンプルでは、タッチを表現するためのGUIフレームワークとして「Cinder」を利用する。

連載:C++で始めるLeap Motion開発 ―― タッチUIの先のカタチ ――
4. Leap Motionのカメラ画像を取得する

Leapアプリのカメラ画像の取得方法を説明。今回のサンプルでは、タッチを表現するためのGUIフレームワークとして「Cinder」を利用する。

連載:C++で始めるLeap Motion開発 ―― タッチUIの先のカタチ ――
5. 【現在、表示中】≫ Leap SDKのいろいろな使い方(フレームデータ、イベントなど)

データ取得方式「コールバック」「ポーリング」の選択指針とは? Leap Motionイベントをポーリング方式で処理する方法や、フレーム履歴、手/指IDの取得についても解説。

連載:C++で始めるLeap Motion開発 ―― タッチUIの先のカタチ ――
6. Leap Motionの使いどころ

最終回。これまでの連載内容を活用してLeap Motionに最適なアプリを作る場合、どのようなものがよいのか? 筆者が考える2つの方法とは?

サイトからのお知らせ

Twitterでつぶやこう!