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

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

Leap Motionのカメラ画像を取得する

2015年7月24日

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

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

 Leap Motion Developer SDK v2で新しく追加された機能に、カメラ画像の取得がある。本稿では取得したカメラ画像の表示や、その画像に指を重ねて表示する方法について解説する。

 今回のサンプルコードも次のリンク先で公開している。WindowsはVisual Studio Express 2013 for Windows Desktopでの動作確認を行い、プロジェクトファイルを含めてすぐに利用できるようにしてある。Cinderについては、下記のリンク先の手順を参考に別途ダウンロードしてほしい(Cinderの概要については前回を参考にされたい)。

 Leap Motionは次の写真のように2つのカメラと3つのLEDが付いており、これらを使って手を検出している。

図1 Leap Motionのカメラ

 まずは、Leap Motion付属のアプリ「Visualizer(ビジュアライザー)」で見てみよう。図2のように、白黒の画像が表示される。

図2 ビジュアライザーでのカメラ画像の表示

 なお、カメラ画像の取得は、既定の設定では無効になっているため、設定画面(図3)から有効にする必要がある。

図3 カメラ画像の有効化

[イメージを許可する]チェックボックスにチェックを入れる。

 カメラ画像をよく見ると、ゆがんで見える。Leap Motionは広角のレンズを使っているため、広い範囲を見ることができるが、同時に画像はゆがんでいる。Leap Motion SDKから取得できるカメラ画像は、このゆがんだ画像となっている。

 また、カメラ画像に合わせて指の位置を表示させたいだろう。そこでここでは、カメラ画像の表示と指の位置の重畳表示を行う。また、ゆがんだ画像だけでなく、補正したカメラ画像の表示と、その画像への指の重畳表示も行う。

 これらのコード自体は、Leap Motionのドキュメントに記載されている。仕組みなどについては、下記のリンク先のドキュメントを参照してほしい。

 「これは快適! Leap Motion v2で格段に良くなったSkeletal Tracking機能」という記事でも解説した通り、カメラ画像は「VR(Virtual Reality)対応」での用途が主となっている。

 Leap Motion単体で利用する場合には、操作位置を表示するためのカメラ画像として利用する用途が考えられるだろう。カメラ画像が使えない従来の操作では、自分の手がどのように映っているかが分からず操作しづらい場合もあるが、カメラ画像を使うことで操作の補助に役立てられる。

カメラの仕様

 Leap Motionのカメラの仕様は、640×240ピクセル(px)のグレースケール画像である(図4)。1ピクセルあたりのバイト数は1となり、0255の間で色を表現している。

図4 カメラ画像の有効化

カメラ画像を取得するコードの準備

 前回と同じようにCinderのプロジェクトを新規作成する。まずはリスト1のコードを記述する。

C++
class LeapSample04App : public AppNative {
  public:
    void setup();
    void mouseDown( MouseEvent event );
    void update();
    void draw();

    Leap::Controller controller;

    const int IMAGE_WIDTH = 640;
    const int IMAGE_HEIGHT = 240;

};

void LeapSample04App::setup() { }
void LeapSample04App::mouseDown( MouseEvent event ) { }
void LeapSample04App::update() { }
void LeapSample04App::draw() { }
リスト1 クラス定義にメンバー変数の追加

なおmouseDown()update()は、今回は使用しない。

 次にsetup()に初期化コードを追加する(リスト2)。カメラ画像の取得は、設定からの有効化の他に、コード内でも画像の取得を設定する必要がある。これには動作ポリシーを設定するcontroller.setPolicy()を利用する。引数にはカメラ画像を表すLeap::Controller::PolicyFlag::POLICY_IMAGESを設定する。

 続いて表示するウィンドウのサイズを設定する。今回は左右のカメラ画像を、上下に表示させているため、ウィンドウの縦サイズを、カメラ画像の倍に設定している。

C++
void LeapSample04App::setup()
{
  // 画像の取得を有効にする
  controller.setPolicy( Leap::Controller::PolicyFlag::POLICY_IMAGES );

  //ウィンドウサイズを設定する
  setWindowSize( Vec2i( IMAGE_WIDTH, IMAGE_HEIGHT * 2 ) );
}
リスト2 setup()関数の編集

カメラ画像の取得と表示

 では実際にカメラ画像を取得し、表示してみよう。

 カメラ画像はLeap::Frame::images()で取得できる。戻り値はLeap::ImageList型で、左右のカメラ画像のリストになっている。インデックスの0は右側、1は左側となっている(図5)。なお、Leap Motionは手の向きを補正する機能があるため、USBの向きはどちらでもよい。

図5 左右のカメラ画像のリスト

 リスト3は、draw()内に記述した、カメラ画像を取得するコードだ。

 カメラ画像はLeap::Imageクラスで表され、画像データはLeap::Image::data()、画像の幅はLeap::Image::width()、画像の高さはLeap::Image::height()でそれぞれ取得できる

 CinderのSurfaceクラスに変換し、さらにテクスチャ化して表示する。具体的にはSurfaceクラスを使って、RGBAの32ビットフォーマットで画像を作成し、R/G/Bそれぞれに同じ値を設定することで、グレースケールのカメラ画像データ(表示用)を作成している。

 表示については、上下2段にするために、iの値によってY座標の位置を変更している

C++
// 画像を取得する
Leap::Frame frame = controller.frame();
Leap::ImageList images = frame.images();

// カメラ画像を表示する
for ( int i = 0; i < 2; i++ ){
  Leap::Image image = images[i];
  if ( !image.isValid() ){
    continue;
  }

  const unsigned char* image_buffer = image.data();

  // グレースケールビットマップで描画する
  Surface surface( image.width(), image.height(), image.width() * 4, SurfaceChannelOrder::RGBA );
  int cursor = 0;
  Surface::Iter iter = surface.getIter();
  while ( iter.line() ) {
    while ( iter.pixel() ) {
      iter.r() = image_buffer[cursor];
      iter.g() = iter.b() = iter.r();
      iter.a() = 255;
      cursor++;
    }
  }

  // 画像を描画する
  gl::Texture texture = gl::Texture( surface );
  gl::draw( texture, Area( 0, IMAGE_HEIGHT * i, IMAGE_WIDTH, IMAGE_HEIGHT * (i + 1) ) );
}
リスト3 カメラ画像の取得と表示(draw()関数内)

 実行すると、次のようにカメラ画像が2つ表示されるLeap Motionの左右のカメラのため、画像が少し左右にずれていることが確認できる。

図6 実行結果

カメラの画像と指の位置合わせ

 続いて、指の位置をカメラ画像に合わせて表示させる。

 Leap Motionで検出される指の位置は3次元空間の座標のため、カメラ画像の座標に変換する必要がある。Leap Motion SDKには座標変換ための関数が用意されていないため、自分でコードを書く必要がある。このコードは、公式ドキュメント(英語)にあるので、リスト4のように、これを利用する(originの値だけ、上下に表示するために変更している)。

C++
// 画像を取得する
Leap::Frame frame = controller.frame();
Leap::ImageList images = frame.images();

// カメラ画像を表示する
……省略……

// 指の位置を表示する
for ( int i = 0; i < 2; i++ ){
  Leap::Image image = images[i];
  if ( !image.isValid() ){
    continue;
  }

  Vec2f origin = Vec2f( 0, (IMAGE_HEIGHT * i) );
  const float camera_offset = 20; //x-axis offset of cameras in millimeters
  Leap::FingerList allTheFingers = frame.fingers();
  for ( Leap::FingerList::const_iterator fl = allTheFingers.begin();
      fl != allTheFingers.end(); fl++ ) {
    Leap::Vector tip = (*fl).tipPosition();
    float h_slope = -(tip.x + camera_offset * (2 * i - 1)) / tip.y;
    float v_slope = tip.z / tip.y;

    Leap::Vector pixel = image.warp( Leap::Vector( h_slope, v_slope, 0 ) );
    //gl::color( .5, 0, 1, .5 );
    gl::drawSolidCircle( Vec2f( pixel.x + origin.x, pixel.y + origin.y ), 10 );
  }
}
リスト4 カメラ画像の取得と表示(draw()内)

 これを実行すると、指の位置に円が表示される(図7)。

図7 実行結果

カメラ画像の表示方法いろいろ

 ここまでは、カメラ画像をゆがんだまま表示した。ここではゆがみを補正した画像を作成・表示する。

 こちらもコードは公式ドキュメントにあるので、それをベースに進める。

 補正画像の作成に当たり、画像のサイズが変わるので、ウィンドウサイズの値を変えておく(リスト5)。先ほどの生データゆがみ補正時を簡単に切り替えられるように、マクロを使ってコードのオン/オフを切り替えると便利だろう。ドキュメント内では400×400ピクセルとなっているので、設定を合わせておく。値は正方形であればいくつでもよい。

C++
#ifdef RAW_DATA
  // 生データ
  const int IMAGE_WIDTH = 640;
  const int IMAGE_HEIGHT = 240;
#else
  // ゆがみ補正時
  const int IMAGE_WIDTH = 400;
  const int IMAGE_HEIGHT = 400;
#endif
リスト5 ウィンドウサイズの設定

 ゆがみ補正画像を作成するコードをdraw()内に入れていく(リスト6)。

 origin変数の値は、画像を表示する位置に合わせて適宜変えてほしい。また、規定ではカメラ画像の範囲外は赤くなるが、見やすいように白に変えている。

C++
// 画像を取得する
Leap::Frame frame = controller.frame();
Leap::ImageList images = frame.images();

//Draw the undistorted image using the warp() function
Vec2f origin;
int targetWidth = IMAGE_WIDTH;
int targetHeight = IMAGE_HEIGHT;

for ( int i = 0; i < 2; i++ ){
  Leap::Image image = images[i];
  if ( !image.isValid() ){
    return;
  }

  // 表示開始位置
  origin = Vec2f( 0, IMAGE_HEIGHT * i );

  Surface targetImage( targetWidth, targetHeight, targetWidth * 4,
    SurfaceChannelOrder::RGBA );

  // ゆがみを補正した画像を作成する
  //Iterate over target image pixels, converting xy to ray slope
  //An array to hold the rgba color components
  unsigned char brightness[4] = { 0, 0, 0, 255 }; 
  Surface::Iter tI = targetImage.getIter();
  while ( tI.line() ) {
    while ( tI.pixel() ) {

      //Normalize from pixel xy to range [0..1]
      Leap::Vector input = Leap::Vector( (float)tI.x() / targetWidth,
        (float)tI.y() / targetHeight, 0 );

      //Convert from normalized [0..1] to slope [-4..4]
      input.x = (input.x - image.rayOffsetX()) / image.rayScaleX();
      input.y = (input.y - image.rayOffsetY()) / image.rayScaleY();

      Leap::Vector pixel = image.warp( input );

      if ( (pixel.x >= 0 && pixel.x < image.width()) &&
        (pixel.y >= 0 && pixel.y < image.height()) ) {

        //xy to buffer index
        int data_index = floor( pixel.y ) * image.width() + floor( pixel.x ); 
        brightness[0] = image.data()[data_index]; //Look up brightness value
        brightness[2] = brightness[1] = brightness[0]; //Greyscale
      }
      else {
        // カメラ画像外は白で表示する
        brightness[0] = brightness[2] = brightness[1] = 255;
      }
      tI.r() = brightness[0];
      tI.g() = brightness[1];
      tI.b() = brightness[2];
      tI.a() = brightness[3];
    }
  }
  gl::Texture targetTexture( targetImage, gl::Texture::Format() );
  gl::draw( targetTexture, Rectf( origin, 
              origin + Vec2f( targetWidth, targetHeight ) ) );
}
リスト6 ゆがみを補正した画像を作成する

 これを実行した結果が次の画像だ。手の形がきれいになっていることが分かる。

図8 ゆがみを補正した画像
図8 ゆがみを補正した画像

 続いて、補正した画像に合わせて指の位置を表示する。こちらもドキュメントに記載されているコードをベースにしている。

C++
// 画像を取得する
Leap::Frame frame = controller.frame();
Leap::ImageList images = frame.images();

//Draw the undistorted image using the warp() function
Vec2f origin;
int targetWidth = IMAGE_WIDTH;
int targetHeight = IMAGE_HEIGHT;

// ゆがみ補正した画像を作成する
……省略……

// 以下はリスト6の下に追記する
// ゆがみ補正した画像に合わせて指を表示する
for ( int i = 0; i < 2; i++ ){
  Leap::Image image = images[i];
  if ( !image.isValid() ){
    return;
  }

  // 表示開始位置
  origin = Vec2f( 0, IMAGE_HEIGHT * i );

  // 指を表示する
  Leap::FingerList frameFingers = frame.fingers();
  for ( Leap::FingerList::const_iterator fl = frameFingers.begin();
      fl != frameFingers.end(); fl++ ) {

    //Convert finger tip position to a ray from the camera POV
    Leap::Vector tip = (*fl).tipPosition();
    float horizontal_slope = -(tip.x + 20 * (2 * i - 1)) / tip.y;
    float vertical_slope = tip.z / tip.y;

    // Normalize ray from [-4..4] to [0..1] (the inverse of how the undistorted image was drawn earlier)
    Leap::Vector ray = Leap::Vector(
      horizontal_slope * image.rayScaleX() + image.rayOffsetX(),
      vertical_slope   * image.rayScaleY() + image.rayOffsetY(),
      0 );

    //Pixel coordinates from [0..1] to [0..width/height]
    Leap::Vector pixel = Leap::Vector( 
      ray.x * targetWidth, 
      ray.y * targetHeight, 
      0 );
    //gl::color( .5, 0, 1, .5 );
    gl::drawSolidCircle( Vec2f( pixel.x + origin.x, pixel.y + origin.y ), 5 );
  }
}
リスト7 ゆがみを補正した画像に合わせて指の位置を表示する

 これを実行すると、次の画像のようになる。

図9 ゆがみを補正した画像に指の位置を重ねた
図9 ゆがみを補正した画像に指の位置を重ねた

まとめ

 カメラ画像の表示と、それに合わせた指の表示を行った。今回は指の位置だが、3次元座標からカメラ座標への変換は、手やツールについても同じになる。

 Leap Motionで何かを操作する場合に、カメラ画像で補助すると、分かりやすくなるため、まずはどのように変わるか試してみてほしい。

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

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

Leap Motion Developer SDKを利用してC++言語でLeapアプリを開発する方法を、サンプルコードを示しながら解説する連載(2015年改訂版)。SDK提供のサンプルコードを基にLeapアプリ開発の基本的な流れを説明する。

連載: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の取得についても解説。

サイトからのお知らせ

Twitterでつぶやこう!