連載:Intel Perceptual Computing SDK(現:RealSense SDK)入門(4)
Depthカメラを使って手指を検出する
PerC SDKの最大の特長である「手指の検出」を解説。Depthカメラのデータを取得する方法も説明する。
今回は、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回を参照してほしい。
- Windows 8.1 Pro(64bit版)
- Visual Studio 2012 Express for Windows Desktop
- Intel Perceptual Computing SDK Release7
- OpenCV
Depthカメラのデータを取得する
初めにDepthカメラのデータを取得してみよう。流れは、前回のColorカメラと同じだ。
全体のコードは下記のリンク先を参照してほしい。
1定数/変数の宣言
Colorカメラの解像度に加えて、Depthカメラの解像度を設定する。Depthカメラの解像度は320×240固定となっている。
//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カメラの幅、高さを指定する。
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()で表示する(次のコード)。
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カメラのデータを距離データとして取得
まず距離データの取得方法について解説する。先にコードを示そう。
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 );
}
|
更新されたフレーム(depthFrame変数)からPXCImage::AcquireAccess()でデータへのアクセス権を得る。このとき、データフォーマットに「PXCImage::COLOR_FORMAT_DEPTH」を指定することで距離データを取得でき、PXCImage::ImageData型のdata変数に格納される。距離データは「data.planes[0]」にデータ列の先頭アドレスが格納されており、これをushort型の配列(ポインター)にすることで各画素の距離にアクセスできる(次の図を参照)。
このデータは「mm(ミリメートル)」単位の値となっている。Creative Senze3Dの仕様では、距離の認識範囲が15~100cmなので、値としては「150」~「1000」が有効な距離データとなる。上記のコードでは、16bitの距離データを8bitのグレー画像に変換している。その際に、一定範囲の距離データ(上記のコードでは15~80cm)のみを有効にして可視化している。この値を変えることで、距離によるフィルターを行うことができ、センサーが距離を検出していることが分かる。
このプログラムを実行すると、次のように表示される。近いところはより黒く、遠いところはより白くなる。範囲外のピクセルは白になる。
Depthカメラのデータを画像データとして取得
こちらの方法についても先にコードを示す。
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 );
}
|
画像データとして取得する場合は、PXCImage::AcquireAccess()のデータフォーマットに「PXCImage::COLOR_FORMAT_RGB32」を指定することで、PXCImage::ImageData型のdata変数にRGBAのデータを取得できる。「PXCImage::COLOR_FORMAT_RGB24」では取得できないようだ。
データフォーマットは次のようになっている。
以降はColorカメラと同様に処理できる。「data.planes[0]」にデータの先頭アドレスが、「data.pitches[0]」に1ラインのバイト数が格納されている。これをOpenCV側のcv::MatにコピーすればDepthカメラのデータを画像化したものが表示される。
このプログラムを実行すると、次のように表示される。先ほどとは逆に、近いところはより白く、遠いところはより黒くなる。範囲外のピクセルは黒になる。
手指を検出する
今回の本題である「手指の検出」を解説する。
具体的には手および肘の位置と、具体的な各指(親指、人差し指など)、より抽象的な指(指先、短い指)の位置を取得できる。
それぞれのイメージは次の通りだ。具体的な指の検出では5本の指を個別に検出しているのに対し、抽象的な指の検出ではいくつかの指のみ検出されている(よく見ると、赤の丸の下に緑の丸があり、1つの指に対して複数の検出が行われていることも分かる)。
検出の方法について、手および肘は右手、左手、右肘、左肘といったかたちで位置座標などを取得できる。指については、手と合わせて取得する。例えば右手の親指、左手の指先といった形だ。後ほどコードと併せて解説する。
全体のコードは下記のリンク先を参照してほしい。
1コンストラクター
今回は新たに手指の検出を行う(次のコード)。
手指の検出やジェスチャー、ポーズは、EnableGesture()によって有効にする。
Depthデータの取得時にあった「PXCImage::COLOR_FORMAT_DEPTH」でのEnableImage()は、今回は入れていない。ジェスチャーにはDepthカメラを利用するため、内部的にDepthデータも有効にしている。
Pipeline(void)
: UtilPipeline()
{
// 必要なデータを有効にする
EnableGesture();
EnableImage(PXCImage::COLOR_FORMAT_RGB32, Width, Height);
}
|
2新しいフレームの更新イベント
フレームの更新(次のコード)では、Color、Depthに加えてジェスチャーのフレームを取得する。ジェスチャーといっても、フレームから取得できるのは手指の情報になっており、(本稿では解説しないが)ジェスチャー、ポーズそのものはOnNewFrame()と同じようにOnGesture()をオーバーライドし、その引数で受け取る。
ジェスチャーフレームはQueryGesture()で受け取る。これをgetGestureData()で処理し、手指の位置をColor画像に重ねる。
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()で手の位置および手に付随する指の位置を表示する。
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 | 右手 |
肘も同様に次の列挙値を指定する。
値 | 意味 |
---|---|
LABEL_BODY_ELBOW_PRIMARY | 最初に検出した肘 |
LABEL_BODY_ELBOW_LEFT | 左肘 |
LABEL_BODY_ELBOW_SECONDARY | 2番目に検出した肘 |
LABEL_BODY_ELBOW_RIGHT | 右肘 |
4手の位置を取得する
次のコードのようにして、手および指の位置を取得する。手の位置はgetLabel()に渡されたラベル(「LABEL_BODY_ELBOW_PRIMARY」または「LABEL_BODY_ELBOW_SECONDARY」)を渡す。指の位置は手のラベルに指のラベルをORする。
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 | 一番短い指 |
具体的な指ラベルを指定すると「親指」や「人差し指」など個別の指の位置を取得できる。抽象的な指ラベルでは「指先」や「長い指」といった指定となる。個別の指も確実に判別できるというわけではないため、特に指を使った操作を行う場合には、「LABEL_HAND_FINGERTIP(指先)」を使うことが多い。
5指の位置を取得する
ここで手、指、肘の位置を取得および描画をする(下記のコードを参照)。
まずPXCGesture::QueryNodeData()でPXCGesture::GeoNode型のノード(手、指、肘の情報)を取得する。PXCGesture::GeoNode型でよく利用する変数を下記の表に挙げる。
変数 | 意味 |
---|---|
body | ラベル。ノードの識別 |
positionWorld | カメラを中心とした位置の3次元座標 |
positionImage | Depthカメラの座標系での2次元座標 |
openness | 手の開閉度合(手を指定したときのみ) |
ここでは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位置を使って円を描画し、位置を表している。
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画像に合わせて表示できる。
まとめ
手順は多いが比較的簡単に手指の検出ができることを理解いただけたと思う。
次回は最終回として音声認識および音声合成について解説する。音声認識は「音声のテキスト化」、音声合成は「テキストの音声化」だ。
1. Intel Perceptual Computing(PerC) SDKの全体像
Intel Perceptual Computing SDKの概要と、それを利用したアプリの開発方法について解説する連載スタート。今回はセンサーモジュールの仕様や、SDKの概要、Intel社の3Dセンシング技術などについて紹介。
2. Intel Perceptual Computing(PerC) SDKの概要と環境構築
PerC SDKの開発環境やアーキテクチャ、インストール方法について解説。またSDKに含まれているサンプルを紹介することで、PerCが提供する機能について見ていく。
5. 無償で簡単にアプリに組み込める「音声認識&音声合成」
マイクに向かってしゃべると音声をテキスト化する「音声認識」や、テキストを音声データに変換する「音声合成」をPC上のアプリで実現したい場合、無償のPerC SDKが便利だ。その開発方法を解説。