Deep Insider の Tutor コーナー
>>  Deep Insider は本サイトからスピンオフした姉妹サイトです。よろしく! 
連載:Intel Perceptual Computing SDK(現:RealSense SDK)入門(4)

連載:Intel Perceptual Computing SDK(現:RealSense SDK)入門(4)

Depthカメラを使って手指を検出する

2014年2月5日

PerC SDKの最大の特長である「手指の検出」を解説。Depthカメラのデータを取得する方法も説明する。

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

 今回は、Intel Perceptual Computing SDK(以下、PerC SDK。現在は「Intel RealSense SDK」)の最大の特長である「手指の検出」の解説を行う(前回は、PerC SDKを使って、Colorカメラを使って顔を検出する方法を説明した)。

 手指の検出にはDepthカメラを利用するので、Creative Interactive Gesture Camera Developer Kit(プロダクト名: Creative Senze3D)が必要になる。Creative Senze3Dの購入方法は、第2回を参照してほしい。

 本稿の内容を実装することで、次の画面のように、手および指の位置を検出できるようになる。

手指の検出結果

 本連載のコードは、次の環境での動作を確認している。環境の構築は第3回を参照してほしい。

Depthカメラのデータを取得する

 初めにDepthカメラのデータを取得してみよう。流れは、前回のColorカメラと同じだ。

 全体のコードは下記のリンク先を参照してほしい。

1定数/変数の宣言

 Colorカメラの解像度に加えて、Depthカメラの解像度を設定する。Depthカメラの解像度は320×240固定となっている。

C++
//static const int Width  = 640;
//static const int Height = 480;
static const   int Width  = 1280;
static const   int Height = 720;
 
// Depthカメラの解像度
static const int DEPTH_WIDTH  = 320; // 追加
static const int DEPTH_HEIGHT = 240; // 追加
 
PXCImage::ColorFormat colorFormat;
定数/変数の宣言

2コンストラクター

 コンストラクターでは利用したいデータを有効にする。今回はColor画像に加えてDepthカメラのデータを取得するため、次のコードに示す通り、EnableImage()(本連載では関数は「()」で表現する)で「PXCImage::COLOR_FORMAT_DEPTH」およびDepthカメラの幅、高さを指定する。

C++
Pipeline(void)
  : UtilPipeline()
  , colorFormat( PXCImage::COLOR_FORMAT_RGB32 )
{
  // 必要なデータを有効にする
  EnableImage( colorFormat, Width, Height );
  EnableImage( PXCImage::COLOR_FORMAT_DEPTH, DEPTH_WIDTH, DEPTH_HEIGHT );
}
コンストラクター

3新しいフレームの更新イベント

 こちらもColorカメラと同じだ。QueryImage()でDepthフレームを取得し、後述するgetDepthData()でフレームのデータを取得、cv::imshow()で表示する(次のコード)。

C++
virtual bool OnNewFrame(void)
{
  try {
    // フレームを取得する
    auto colorFrame = QueryImage( PXCImage::IMAGE_TYPE_COLOR );
    auto depthFrame = QueryImage( PXCImage::IMAGE_TYPE_DEPTH );
 
    // フレームデータを取得する
    cv::Mat colorImage( Height, Width, CV_8UC4 );
    cv::Mat depthImage;
    getColorData( colorImage, colorFrame );
    getDepthData( depthImage, depthFrame );
 
    // 表示
    cv::imshow( "Color Camera", colorImage );
    cv::imshow( "Depth Camera", depthImage );
  }
  catch ( std::exception& ex ) {
    std::cout << ex.what() << std::endl;
  }
 
  auto key = cv::waitKey( 10 );
  return key != 'q';
}
新しいフレームの更新イベント

4Depthカメラのデータを取得する

 ここからが今回の肝になる。Depthカメラのデータを取得して表示させるが、2種類の方法について解説する。

  1つ目は、Depthカメラのデータを距離データとして取得して可視化する方法。
  2つ目は、Depthカメラのデータを画像データとして取得し、そのまま表示する方法だ。

 距離データが欲しい場合には前者、単純にDepthカメラの画像が欲しい/表示したい場合は後者になるだろう。

Depthカメラのデータを距離データとして取得

 まず距離データの取得方法について解説する。先にコードを示そう。

C++
void getDepthData( cv::Mat& depthImage, PXCImage* depthFrame )
{
  // Depthデータを取得する
  PXCImage::ImageData data = { 0 };
  auto sts = depthFrame->AcquireAccess( PXCImage::ACCESS_READ,
                                        PXCImage::COLOR_FORMAT_DEPTH, &data );
  if ( sts < PXC_STATUS_NO_ERROR ) {
    return;
  }
 
  // 8bitのGray画像を作成する
  depthImage = cv::Mat( DEPTH_HEIGHT, DEPTH_WIDTH, CV_8U );
 
  // Depthデータを可視化する
  ushort* srcDepth = (ushort*)data.planes[0];
  uchar* dstDepth = (uchar*)depthImage.data;
  for ( int i = 0; i < (DEPTH_WIDTH * DEPTH_HEIGHT); ++i ) {
    // 一定の距離のみ有効にする
    if ( (150 <= srcDepth[i]) && (srcDepth[i] < 800) ) {
      dstDepth[i] = srcDepth[i] * 0xFF / 1000;
    }
    else {
      dstDepth[i] = 0xFF;
    }
  }
 
  // Depthデータを解放する
  depthFrame->ReleaseAccess( &data );
}
Depthカメラのデータを距離データとして取得する

 更新されたフレーム(depthFrame変数)からPXCImage::AcquireAccess()でデータへのアクセス権を得る。このとき、データフォーマットに「PXCImage::COLOR_FORMAT_DEPTH」を指定することで距離データを取得でき、PXCImage::ImageData型のdata変数に格納される。距離データは「data.planes[0]」にデータ列の先頭アドレスが格納されており、これをushort型の配列(ポインター)にすることで各画素の距離にアクセスできる(次の図を参照)。

PXCImage::COLOR_FORMAT_DEPTHのデータフォーマット

 このデータは「mm(ミリメートル)」単位の値となっている。Creative Senze3Dの仕様では、距離の認識範囲が15~100cmなので、値としては「150」~「1000」が有効な距離データとなる。上記のコードでは、16bitの距離データを8bitのグレー画像に変換している。その際に、一定範囲の距離データ(上記のコードでは15~80cm)のみを有効にして可視化している。この値を変えることで、距離によるフィルターを行うことができ、センサーが距離を検出していることが分かる。

 このプログラムを実行すると、次のように表示される。近いところはより黒く、遠いところはより白くなる。範囲外のピクセルは白になる。

距離データを可視化した実行結果
距離データを可視化した実行結果
Depthカメラのデータを画像データとして取得

 こちらの方法についても先にコードを示す。

C++
void getDepthData( cv::Mat& depthImage, PXCImage* depthFrame )
{
  // Depthデータを取得する
  PXCImage::ImageData data = { 0 };
  auto sts = depthFrame->AcquireAccess( PXCImage::ACCESS_READ,
                                        PXCImage::COLOR_FORMAT_RGB32, &data );
  if ( sts < PXC_STATUS_NO_ERROR ) {
    return;
  }
 
  // 32bitのRGB画像を作成する
  depthImage = cv::Mat( DEPTH_HEIGHT, DEPTH_WIDTH, CV_8UC4 );
 
  // Depthデータを可視化する
  memcpy( depthImage.data, data.planes[0], data.pitches[0] * DEPTH_HEIGHT );
 
  // Depthデータを解放する
  depthFrame->ReleaseAccess( &data );
}
Depthカメラのデータを距離データとして取得する

 画像データとして取得する場合は、PXCImage::AcquireAccess()のデータフォーマットに「PXCImage::COLOR_FORMAT_RGB32」を指定することで、PXCImage::ImageData型のdata変数にRGBAのデータを取得できる。「PXCImage::COLOR_FORMAT_RGB24」では取得できないようだ。

 データフォーマットは次のようになっている。

PXCImage::COLOR_FORMAT_RGB32のデータフォーマット

 以降はColorカメラと同様に処理できる。「data.planes[0]」にデータの先頭アドレスが、「data.pitches[0]」に1ラインのバイト数が格納されている。これをOpenCV側のcv::MatにコピーすればDepthカメラのデータを画像化したものが表示される。

 このプログラムを実行すると、次のように表示される。先ほどとは逆に、近いところはより白く、遠いところはより黒くなる。範囲外のピクセルは黒になる。

距離データを画像化した実行結果
距離データを画像化した実行結果

手指を検出する

 今回の本題である「手指の検出」を解説する。

 具体的には手および肘の位置と、具体的な各指(親指、人差し指など)、より抽象的な指(指先、短い指)の位置を取得できる。

 それぞれのイメージは次の通りだ。具体的な指の検出では5本の指を個別に検出しているのに対し、抽象的な指の検出ではいくつかの指のみ検出されている(よく見ると、赤の丸の下に緑の丸があり、1つの指に対して複数の検出が行われていることも分かる)。

手、肘と具体的な指の検出結果
手、肘と具体的な指の検出結果

 検出の方法について、手および肘は右手、左手、右肘、左肘といったかたちで位置座標などを取得できる。指については、手と合わせて取得する。例えば右手の親指、左手の指先といった形だ。後ほどコードと併せて解説する。

 全体のコードは下記のリンク先を参照してほしい。

1コンストラクター

 今回は新たに手指の検出を行う(次のコード)。

 手指の検出やジェスチャー、ポーズは、EnableGesture()によって有効にする。

 Depthデータの取得時にあった「PXCImage::COLOR_FORMAT_DEPTH」でのEnableImage()は、今回は入れていない。ジェスチャーにはDepthカメラを利用するため、内部的にDepthデータも有効にしている。

C++
Pipeline(void)
  : UtilPipeline()
{
  // 必要なデータを有効にする
  EnableGesture();
  EnableImage(PXCImage::COLOR_FORMAT_RGB32, Width, Height);
}
コンストラクター

2新しいフレームの更新イベント

 フレームの更新(次のコード)では、Color、Depthに加えてジェスチャーのフレームを取得する。ジェスチャーといっても、フレームから取得できるのは手指の情報になっており、(本稿では解説しないが)ジェスチャー、ポーズそのものはOnNewFrame()と同じようにOnGesture()をオーバーライドし、その引数で受け取る。

 ジェスチャーフレームはQueryGesture()で受け取る。これをgetGestureData()で処理し、手指の位置をColor画像に重ねる。

C++
virtual bool OnNewFrame(void)
{
  try {
    // フレームを取得する
    auto colorFrame = QueryImage( PXCImage::IMAGE_TYPE_COLOR );
    auto depthFrame = QueryImage( PXCImage::IMAGE_TYPE_DEPTH );
    auto gestureFrame = QueryGesture();
 
    // フレームデータを取得する
    cv::Mat colorImage( Height, Width, CV_8UC4 );
    cv::Mat depthImage;
    getColorData( colorImage, colorFrame );
    getDepthData( depthImage, depthFrame );
    getGestureData( colorImage, gestureFrame, colorFrame, depthFrame );
 
    // 表示
    cv::imshow( "Color Camera", colorImage );
    cv::imshow( "Depth Camera", depthImage );
  }
  catch ( std::exception& ex ) {
    std::cout << ex.what() << std::endl;
  }
 
  auto key = cv::waitKey( 10 );
  return key != 'q';
}
コンストラクター

3ジェスチャーデータを取得する

 ジェスチャーデータは、下記のコードに示す通り、何段階かに分けて取得、表示している。getGestureData()では、getHandPosition()で両手を、getLabel()で両肘を表示する。getHandPosition()では、getLabel()で手の位置および手に付随する指の位置を表示する。

C++
void getGestureData( cv::Mat& colorImage, PXCGesture* gestureFrame,
                      PXCImage* colorFrame, PXCImage* depthFrame )
{
  getHandPosition( colorImage, gestureFrame, colorFrame, depthFrame,
                    PXCGesture::GeoNode::LABEL_BODY_HAND_PRIMARY );
  getHandPosition( colorImage, gestureFrame, colorFrame, depthFrame,
                    PXCGesture::GeoNode::LABEL_BODY_HAND_SECONDARY );
 
  getLabel( colorImage, gestureFrame, colorFrame, depthFrame,
    cv::Scalar( 128, 128, 128 ), PXCGesture::GeoNode::LABEL_BODY_ELBOW_PRIMARY );
  getLabel( colorImage, gestureFrame, colorFrame, depthFrame,
    cv::Scalar( 128, 128, 128 ), PXCGesture::GeoNode::LABEL_BODY_ELBOW_SECONDARY );
}
ジェスチャーデータを取得する

 手は次の列挙値で指定する。「検出した順番」および「左右の手」で取得できるが、「左右の手」は厳密ではないため、ここでは「検出した順番」で取得している。

意味
LABEL_BODY_HAND_PRIMARY 最初に検出した手
LABEL_BODY_HAND_LEFT 左手
LABEL_BODY_HAND_SECONDARY 2番目に検出した手
LABEL_BODY_HAND_RIGHT 右手
手に関するPXCGesture::GeoNode型の列挙値

 肘も同様に次の列挙値を指定する。

意味
LABEL_BODY_ELBOW_PRIMARY 最初に検出した肘
LABEL_BODY_ELBOW_LEFT 左肘
LABEL_BODY_ELBOW_SECONDARY 2番目に検出した肘
LABEL_BODY_ELBOW_RIGHT 右肘
肘に関するPXCGesture::GeoNode型の列挙値

4手の位置を取得する

 次のコードのようにして、手および指の位置を取得する。手の位置はgetLabel()に渡されたラベル(「LABEL_BODY_ELBOW_PRIMARY」または「LABEL_BODY_ELBOW_SECONDARY」)を渡す。指の位置は手のラベルに指のラベルをORする。

C++
void getHandPosition( cv::Mat& colorImage, PXCGesture* gestureFrame, PXCImage* colorFrame,
                      PXCImage* depthFrame, PXCGesture::GeoNode::Label hand )
{
  // 手の位置
  getLabel( colorImage, gestureFrame, colorFrame, depthFrame, cv::Scalar( 255, 0, 0 ), hand );
 
  // 具体的な指の位置
  getLabel( colorImage, gestureFrame, colorFrame, depthFrame, cv::Scalar( 0, 255, 0 ),
    hand | PXCGesture::GeoNode::LABEL_FINGER_THUMB );
  getLabel( colorImage, gestureFrame, colorFrame, depthFrame, cv::Scalar( 0, 0, 255 ),
    hand | PXCGesture::GeoNode::LABEL_FINGER_INDEX );
  getLabel( colorImage, gestureFrame, colorFrame, depthFrame, cv::Scalar( 255, 255, 0 ),
    hand | PXCGesture::GeoNode::LABEL_FINGER_MIDDLE );
  getLabel( colorImage, gestureFrame, colorFrame, depthFrame, cv::Scalar( 255, 0, 255 ),
    hand | PXCGesture::GeoNode::LABEL_FINGER_RING );
  getLabel( colorImage, gestureFrame, colorFrame, depthFrame, cv::Scalar( 0, 255, 255 ),
    hand | PXCGesture::GeoNode::LABEL_FINGER_PINKY );
 
  // 抽象的な指の位置
  getLabel( colorImage, gestureFrame, colorFrame, depthFrame, cv::Scalar( 0, 255, 0 ),
    hand | PXCGesture::GeoNode::LABEL_HAND_FINGERTIP );
  getLabel( colorImage, gestureFrame, colorFrame, depthFrame, cv::Scalar( 0, 0, 255 ),
    hand | PXCGesture::GeoNode::LABEL_HAND_UPPER );
  getLabel( colorImage, gestureFrame, colorFrame, depthFrame, cv::Scalar( 255, 255, 255 ),
    hand | PXCGesture::GeoNode::LABEL_HAND_MIDDLE );
  getLabel( colorImage, gestureFrame, colorFrame, depthFrame, cv::Scalar( 0, 0, 0 ),
    hand | PXCGesture::GeoNode::LABEL_HAND_LOWER );
}
手の位置を取得する

 指のラベルの列挙値を下記に示す。

種類意味
具体的LABEL_FINGER_THUMB 親指
LABEL_FINGER_INDEX 人差し指
LABEL_FINGER_MIDDLE 中指
LABEL_FINGER_RING 薬指
LABEL_FINGER_PINKY 小指
抽象的LABEL_HAND_FINGERTIP 指先
LABEL_HAND_UPPER 一番長い指
LABEL_HAND_MIDDLE (長さが)真ん中の指
LABEL_HAND_LOWER 一番短い指
指に関するPXCGesture::GeoNode型の列挙値

 具体的な指ラベルを指定すると「親指」や「人差し指」など個別の指の位置を取得できる。抽象的な指ラベルでは「指先」や「長い指」といった指定となる。個別の指も確実に判別できるというわけではないため、特に指を使った操作を行う場合には、「LABEL_HAND_FINGERTIP(指先)」を使うことが多い。

5指の位置を取得する

 ここで手、指、肘の位置を取得および描画をする(下記のコードを参照)。

 まずPXCGesture::QueryNodeData()でPXCGesture::GeoNode型のノード(手、指、肘の情報)を取得する。PXCGesture::GeoNode型でよく利用する変数を下記の表に挙げる。

変数意味
body ラベル。ノードの識別
positionWorld カメラを中心とした位置の3次元座標
positionImage Depthカメラの座標系での2次元座標
openness 手の開閉度合(手を指定したときのみ)
PXCGesture::GeoNode型でよく利用する変数

 ここではColorカメラの画像に手指の位置を表示させるが、PXCGesture::GeoNode::positionImage変数はDepth座標系となっている。Depth座標系をColor座標系に変換する方法はいくつかあるが、ここではSDKドキュメントに記載されている、MapXY()という関数でUVマップを使った変換を行っている。MapXY()を使った座標変換はDepthデータ(PXCImage::ImageData型)、Colorイメージの情報(PXCImage::ImageInfo型)、Depthイメージの情報(PXCImage::ImageInfo型)が必要になるので、それぞれPXCImage::AcquireAccess()、PXCImage::QueryInfo()で取得する。これを変換するX/Y座標(Depth座標系)とともに渡すことで、Color座標系のX/Y位置が返される。

 Color座標系のX/Y位置を使って円を描画し、位置を表している。

C++
void getLabel( cv::Mat& colorImage, PXCGesture* gestureFrame, PXCImage* colorFrame,
                PXCImage* depthFrame, cv::Scalar color, PXCGesture::GeoNode::Label label )
{
  // ジェスチャーデータを取得する
  PXCGesture::GeoNode nodeData = { 0 };
  auto sts = gestureFrame->QueryNodeData( 0 , label, &nodeData );
  if ( sts < PXC_STATUS_NO_ERROR) {
    return;
  }
 
  // Depthデータを取得する
  PXCImage::ImageData depthData = { 0 };
  sts = depthFrame->AcquireAccess( PXCImage::ACCESS_READ, &depthData );
  if ( sts < PXC_STATUS_NO_ERROR ) {
    return;
  }
 
  // X,Y座標を画面の座標に変換する
  PXCImage::ImageInfo colorInfo = { 0, 0, 0 };
  PXCImage::ImageInfo depthInfo = { 0, 0, 0 };
  colorFrame->QueryInfo( &colorInfo );
  depthFrame->QueryInfo( &depthInfo );
  auto x = nodeData.positionImage.x;
  auto y = nodeData.positionImage.y;
  MapXY( x, y, &depthData, &depthInfo, &colorInfo );
 
  // 画面上に位置に点を描画する
  cv::circle( colorImage, cv::Point( x, y ), 10, color, -1 );
 
  // Depthデータを解放する
  depthFrame->ReleaseAccess( &depthData );
}
 
// 3次元座標を2次元座標に変換する
static void MapXY( float &x, float &y, PXCImage::ImageData *depthData,
                   PXCImage::ImageInfo *depthInfo, PXCImage::ImageInfo *colorInfo)
{
  int index = (int)((((int)y) * depthInfo->width) + x) * 2;
  float* uvmap = (float*)depthData->planes[2];
  x = uvmap[index] * colorInfo->width;
  y = uvmap[index + 1] * colorInfo->height;
}
指の位置を取得する

 このプログラムを実行すると、手、指、肘の位置をColor画像に合わせて表示できる。

手指の検出結果

まとめ

 手順は多いが比較的簡単に手指の検出ができることを理解いただけたと思う。

 次回は最終回として音声認識および音声合成について解説する。音声認識は「音声のテキスト化」、音声合成は「テキストの音声化」だ。

連載:Intel Perceptual Computing SDK(現:RealSense SDK)入門(4)
1. Intel Perceptual Computing(PerC) SDKの全体像

Intel Perceptual Computing SDKの概要と、それを利用したアプリの開発方法について解説する連載スタート。今回はセンサーモジュールの仕様や、SDKの概要、Intel社の3Dセンシング技術などについて紹介。

連載:Intel Perceptual Computing SDK(現:RealSense SDK)入門(4)
2. Intel Perceptual Computing(PerC) SDKの概要と環境構築

PerC SDKの開発環境やアーキテクチャ、インストール方法について解説。またSDKに含まれているサンプルを紹介することで、PerCが提供する機能について見ていく。

連載:Intel Perceptual Computing SDK(現:RealSense SDK)入門(4)
3. Colorカメラを使って顔を検出する

Intel Perceptual Computing SDKを使ったアプリの開発方法を解説。Webカメラでも行える顔検出を実装してみよう。

連載:Intel Perceptual Computing SDK(現:RealSense SDK)入門(4)
4. 【現在、表示中】≫ Depthカメラを使って手指を検出する

PerC SDKの最大の特長である「手指の検出」を解説。Depthカメラのデータを取得する方法も説明する。

連載:Intel Perceptual Computing SDK(現:RealSense SDK)入門(4)
5. 無償で簡単にアプリに組み込める「音声認識&音声合成」

マイクに向かってしゃべると音声をテキスト化する「音声認識」や、テキストを音声データに変換する「音声合成」をPC上のアプリで実現したい場合、無償のPerC SDKが便利だ。その開発方法を解説。

サイトからのお知らせ

Twitterでつぶやこう!