Deep Insider の Tutor コーナー
>>  Deep Insider は本サイトからスピンオフした姉妹サイトです。よろしく! 
連載:Leap Motion開発入門(C#編)

連載:Leap Motion開発入門(C#編)

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

2015年9月15日

Leapアプリのタッチ操作の認識方法と開発方法を説明。新規書き下ろし。※C++編の同名タイトルの記事と基本的な内容は同じです。

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

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

 今回のサンプルコードも次のリンク先で公開している。WindowsはVisual Studio Express 2013 for Windows Desktopでの動作確認を行い、プロジェクトファイルを含めてすぐに利用できるようにしてある。

 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 カメラ画像の有効化

カメラ画像の取得と表示

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

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

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

 リスト1はカメラ画像の表示画面(WPFのXAMLファイル)の定義だ。上下2段にカメラ画像と指の座標を表示するために、ImageコントロールとCanvasコントロールの組み合わせを2つ置いている。それぞれの幅と高さはコード内で設定している(そのままの画像、補正した画像でそれぞれ表示するため)。

XAML
<Window x:Class="LeapSample04.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Loaded="Window_Loaded" Title="LeapSample04" SizeToContent="WidthAndHeight">
  <Grid>
    <StackPanel Orientation="Vertical">
      <Grid x:Name="GridLeft" >
        <Image x:Name="ImageLeft" />
        <Canvas x:Name="CanvasLeft"/>
      </Grid>

      <Grid x:Name="GridRight">
        <Image x:Name="ImageRight" />
        <Canvas x:Name="CanvasRight"/>
      </Grid>
    </StackPanel>
  </Grid>
</Window>
リスト1 画面の定義(MainWindow.xaml)

 次回詳しく説明するが、Leap Motionのポーリング方式による処理では、レンダリングはCompositionTargetクラス(System.Windows.Media名前空間)のRenderingイベントのタイミングで行う。このイベントは周期的に発行される。リスト2は、そのイベントハンドラーであるCompositionTarget_Rendering()メソッド(以下、メソッドは()で表記)では、RawImages()を呼び出している。リスト2はこのメソッドのコードで、カメラ画像を取得し、そのまま表示する処理を記述している。

C#
private void RawImages()
{
  var frame = leap.Frame();
  var images = frame.Images;
  var fongers = frame.Fingers;

  GridLeft.Width = GridRight.Width = images[0].Width;
  GridLeft.Height = GridRight.Height = images[0].Height;

  // 左カメラ
  if ( images[0].IsValid ) {
    // カラー画像を作成する
    ImageLeft.Source = ToBitmapSource( images[0] );

    // カメラ座標を取得する
    var leftPoints = MapCameraToColor( images[0], fongers );
    DrawPoints( CanvasLeft, leftPoints );
  }

  // 右カメラ
  if ( images[1].IsValid ) {
    // カラー画像を作成する
    ImageRight.Source = ToBitmapSource( images[1] );

    // カメラ座標を取得する
    var rightPoints = MapCameraToColor( images[1], fongers );
    DrawPoints( CanvasRight, rightPoints );
  }
}

private static BitmapSource ToBitmapSource( Leap.Image image )
{
  return BitmapSource.Create( image.Width, image.Height, 96, 96,
            PixelFormats.Gray8, null, image.Data, image.Width * image.BytesPerPixel );
}
リスト2 カメラ画像の取得と表示(RawImages()メソッドおよびToBitmapSource()メソッド内)

 カメラ画像からビットマップへの変換処理は、ToBitmapSource()内に記述している。カメラ画像はImageクラス(Leap名前空間)で表され、画像データはImageオブジェクトのDataプロパティ、画像の幅はWidthプロパティ、画像の高さはHeightプロパティ、1ピクセルあたりのバイト数はBytesPerPixelプロパティでそれぞれ取得できる。これを
BitmapSource.Create()
を使ってグレースケールのビットマップにしている。

 実行すると*1、次のようにカメラ画像が2つ表示される。Leap Motionでは左右にカメラが存在するため、画像が少し左右にずれていることが確認できる。なお、リスト2のMapCameraToColor()およびDrawPoints()は、次に解説する指の位置表示になるので、図6のように表示させるためには、この2つの呼び出しをコメントアウトしてほしい。

図6 実行結果
  • *1 ダウンロードしたサンプルを実行する場合は、[ソリューション プラットフォーム]構成を「x86」や「x64」など適切なものを選択してほしい。「Any CPU」で実行すると、ライブラリのxcopyに失敗するので注意してほしい

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

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

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

C#
private static Leap.Vector[] MapCameraToColor( Leap.Image image, FingerList fingers )
{
  var colorPoints =new List<Leap.Vector>();

  float cameraOffset = 20; //x-axis offset in millimeters
  foreach ( Finger finger in fingers ) {
    // 3次元座標を2次元座標に変換する
    var tip = finger.TipPosition;
    float hSlope = -(tip.x + cameraOffset * (2 * image.Id - 1)) / tip.y;
    float vSlope = tip.z / tip.y;

    colorPoints.Add( image.Warp( new Leap.Vector( hSlope, vSlope, 0 ) ) );
  }

  return colorPoints.ToArray();
}
リスト3 指の座標をカメラ画像に変換する

 これを実行する(リスト2のMapCameraToColor()およびDrawPoints()のコメントアウトを外して有効にする)と、指の位置に円が表示される(図7)。

図7 実行結果

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

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

 こちらもコードは公式ドキュメントにあるので、それをベースに進める。いくつか変更点があるので列挙しておく。

  • 正方形にゆがみを補正するが、処理速度が遅いため(後述)、解像度を減らしている(400ピクセル四方 → 100ピクセル四方)
  • 解像度を減らした画像を、元の400ピクセルで表示しているため、画像が粗くなっている
  • 画像をグレースケールのまま作成、表示している
  • 既定ではカメラ画像の範囲外は赤くなるが、見やすいように白に変えている

 CompositionTarget_Rendering()内で呼び出すメソッドを、先ほどのRawImages()からCalibrationImage()に書き換える。このメソッド内に、ゆがみ補正画像を作成するコードを記述した(リスト4)。ImageオブジェクトのIsValidプロパティがtrueである(=有効な画像である)場合に、ゆがみ補正の処理を行う。なお、この処理でもMapCalibratedCameraToColor()およびDrawPoints()が指の表示なので、適宜、コメントアウトしてほしい。
 

C#
private void CalibrationImage()
{
  var frame = leap.Frame();
  var images = frame.Images;
  var fongers = frame.Fingers;

  //Draw the undistorted image using the warp() function
  // Warp()が速度低下につながるため、解像度を落としている
  int targetWidth = 100;
  int targetHeight = 100;
  int scale = 400 / targetWidth;

  // グリッドのサイズを設定する
  GridLeft.Width = GridRight.Width = targetWidth * scale;
  GridLeft.Height = GridRight.Height = targetHeight * scale;

  Stopwatch sw = new Stopwatch();
  sw.Restart();

  // 左カメラ
  if ( images[0].IsValid ) {
    // カメラ画像を表示する
    ImageLeft.Source = ToCalibratedBitmap( targetWidth, targetHeight, images[0] );

    // 指の座標を表示する
    var leftPoints = MapCalibratedCameraToColor( images[0], fongers, targetWidth * scale, targetHeight * scale );
    DrawPoints( CanvasLeft, leftPoints );
  }

  // 右カメラ
  if ( images[1].IsValid ) {
    // カメラ画像を表示する
    ImageRight.Source = ToCalibratedBitmap( targetWidth, targetHeight, images[1] );

    // 指の座標を表示する
    var rightPoints = MapCalibratedCameraToColor( images[1], fongers, targetWidth * scale, targetHeight * scale );
    DrawPoints( CanvasRight, rightPoints );
  }

  Trace.WriteLine( sw.ElapsedMilliseconds );
}
リスト4 ゆがみを補正の処理を行う

 実際にゆがみを補正するコードは、ToCalibratedBitmap()内に記述している(リスト5)。ImageオブジェクトのWarp()が補正のためのメソッドになるが、C#の場合、ネイティブのDLLに対して呼び出しを行うため、非常に時間コストがかかる。そのため、処理速度が遅くなりスムースな画像表示とは言えない。先に解像度を落としているのはそのためである。

C#
private static BitmapSource ToCalibratedBitmap( int targetWidth, int targetHeight, Leap.Image image )
{
  var buffer = new byte[targetWidth * targetHeight];

  //Iterate over target image pixels, converting xy to ray slope
  for ( int y = 0; y < targetHeight; y++ ) {
    for ( int x = 0; x < targetWidth; x++ ) {
      //Normalize from pixel xy to range [0..1]
      var input = new Leap.Vector( x / (float)targetWidth, y / (float)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;

      //Use slope to get coordinates of point in image.Data containing the brightness for this target pixel
      var pixel = image.Warp( input );

      int bufferIndex = (y * targetWidth) + x;

      if ( pixel.x >= 0 && pixel.x < image.Width && pixel.y >= 0 && pixel.y < image.Height ) {
        int dataIndex = (int)(Math.Floor( pixel.y ) * image.Width + Math.Floor( pixel.x )); //xy to buffer index
        buffer[bufferIndex] = image.Data[dataIndex];
      }
      else {
        buffer[bufferIndex] = 255;
      }
    }
  }

  return BitmapSource.Create( targetWidth, targetHeight, 96, 96,
      PixelFormats.Gray8, null, buffer, targetWidth );
}
リスト5 ゆがみを補正した画像を作成する

 これを実行した結果が次の画像だ。手の形がきれいになっていることが分かる(拡大しているため、粗いが……)。

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

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

C#
private static Leap.Vector[] MapCalibratedCameraToColor( Leap.Image image, FingerList fingers, int targetWidth, int targetHeight )
{
  var colorPoints =new List<Leap.Vector>();

  float cameraXOffset = 20; //millimeters

  foreach ( Finger finger in fingers ) {
    var tip = finger.TipPosition;
    float hSlope = -(tip.x + cameraXOffset * (2 * image.Id - 1)) / tip.y;
    float vSlope = tip.z / tip.y;

    var ray = new Leap.Vector( hSlope * image.RayScaleX + image.RayOffsetX,
                vSlope * image.RayScaleY + image.RayOffsetY, 0 );

    //Pixel coordinates from [0..1] to [0..width/height]
    colorPoints.Add( new Leap.Vector( ray.x * targetWidth, ray.y * targetHeight, 0 ) );
  }

  return colorPoints.ToArray();
}
リスト6 ゆがみを補正した画像に合わせて指の位置を変換する

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

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

まとめ

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

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

 処理速度については、Image.Warp()を使う他にシェーダーを使う方法も記載されている。C#で使いたい場合には、この方法も検討する必要がありそうだ。

連載:Leap Motion開発入門(C#編)
1. C#によるLeap Motion v2開発の全体像

Leap Motion Developer SDKを利用してC#でLeap Motionのアプリケーションを開発する方法を解説する連載(2015年改訂版)。今回はC#の開発環境など、開発の基礎を紹介。

連載:Leap Motion開発入門(C#編)
2. Leap SDKで指を検出してみよう(Tracking Hands, Fingers, and Tools)

Leap Motionの最大の特長である手・指を検出するには? Leap Motion Developer SDKを活用した開発方法を詳しく解説。※C++編の同名タイトルと基本的な内容は同じです。

連載:Leap Motion開発入門(C#編)
3. Leap Motionでのタッチ操作はどう開発するのか?

Leapアプリのタッチ操作の認識方法と開発方法を説明。※C++編の同名タイトルの記事と基本的な内容は同じです。

連載:Leap Motion開発入門(C#編)
4. 【現在、表示中】≫ Leap Motionのカメラ画像を取得する

Leapアプリのタッチ操作の認識方法と開発方法を説明。新規書き下ろし。※C++編の同名タイトルの記事と基本的な内容は同じです。

連載:Leap Motion開発入門(C#編)
5. フレームのいろいろな使い方

Leap Motion公式のサンプルを題材に、さまざまなフレームの扱い方についてコードを交えて解説。※C++編の「Leap SDKのいろいろな使い方」と基本的な内容は同じです。

サイトからのお知らせ

Twitterでつぶやこう!