○(3-1)Kinectの深度センサーから深度情報を取得
カメラ・イメージが取得できたので、次に深度情報を取得する。
RuntimeオブジェクトのInitializeメソッドの引数に「RuntimeOptions.UseDepth」または「RuntimeOptions.UseDepthAndPlayerIndex」を含めた場合、Kinectから深度情報を取得できる。深度情報もまた、カメラ・イメージと同様に「ポーリング・モデル」と「イベント・モデル」の2つの方式があり、これらの概念はカメラ・イメージと同様である。そこで以降では、両者の違いを主に説明していく。
まず、深度情報をイベント・モデルで処理する場合、深度情報の準備ができたときに発生するRuntimeオブジェクトのDepthFrameReadyイベントを使用する。このイベントにイベント・ハンドラを追加することで、深度情報を取得できる(ポーリング・モデルの場合には、カメラ・イメージと同様にImageStreamクラスのGetNextFrameメソッドを用いる)。
次に、深度情報を取得する場合、「Runtime.DepthStream.Openメソッド」(=RuntimeオブジェクトのDepthStreamプロパティから得られるImageStreamオブジェクトのOpenメソッド)を用いて、深度情報イメージのストリームを開く必要がある。このOpenメソッドの引数は、当然ながらカメラ・イメージと同じであるが、指定できる値に違いがある。
・第1引数:ここにはストリームの種別を設定する。設定する値は、「ImageStreamType.Depth」を指定する。
・第2引数:ここには、アプリケーションが保持する先読みバッファ数を指定する。カメラ・イメージの場合と同じように「2」を指定すれば十分だろう。
・第3引数:深度情報イメージの解像度を指定する。解像度はImageResolution列挙体(enum)の値で指定し、以下の2つが利用可能である。
カメラ・イメージとは指定できる解像度が異なることに注意されたい。
・第4引数:深度情報イメージの種類を指定する。これは「ImageType.DepthAndPlayerIndex」が指定可能である。
以上のような引数を指定したOpenメソッドを呼び出して、深度情報イメージのストリームを開く。イベント・モデルで処理をする場合は、この後、RuntimeオブジェクトのDepthFrameReadyイベントにイベント・ハンドラを追加する。
nui.DepthFrameReady +=
new EventHandler<ImageFrameReadyEventArgs>(nui_DepthFrameReady);
このイベント・ハンドラは、カメラ・イメージのハンドラと同様な形式となる。
void nui_DepthFrameReady(object sender, ImageFrameReadyEventArgs e)
{
PlanarImage Image = e.ImageFrame.Image;
}
ただし、カメラ・イメージとは取得できるイメージ・フォーマットが異なる。深度情報もまたPlanarImage構造体のBitsフィールドにバイト配列として格納されている。このバイト配列は16bitで1深度情報となり、その深度データが解像度の座標数分、格納される。
各座標(x, y)における深度情報は、下記の2byte(=2×8bit)から計算できる。
計算の仕方は、RuntimeオブジェクトのInitializeメソッドの引数に指定したオプションの値により、2種類ある。それぞれの計算方法を以下に示す。
・深度情報(RuntimeOptions.UseDepth)を指定したときの計算方法:
例えば、座標(0, 0)の深度データは、「Bits[0] , Bits[1]」に格納されており、以下のような形式となっている。
未使用の部分を「0」として、以下のような方法で深度データを計算できる。
depth = (int)( (Bits[1] << 8) | (Bits[0]) );
算出された深度(depth)は、Kinectデバイスからの距離(単位:ミリ・メートル)を示しており、1.2〜3.5メートルの範囲で取得できる。なお、depthが「0」の場合は「計測不能」を意味する。
・深度情報とプレイヤー・インデックス(RuntimeOptions.UseDepthAndPlayerIndex)を指定したときの計算方法:
例えば、座標(0, 0)の深度データとプレイヤー・インデックスは、「Bits[0] , Bits[1]」に格納されており、以下のような形式となっている。
それぞれのデータは、以下の計算で求めることができる。
playerindex = Bits[0] & 0x7;
depth = (Bits[1] << 5) | (Bits[0] >> 3);
プレイヤー・インデックス(player index)とは、Kinect for Windows SDK(ベータ版)がセンサー情報から「人」を類推・識別し、その「人」に割り当てられた番号のことである。この値は「1」〜「7」の範囲であり、「0」は「人はいない」と意味付けられている。これにより、ある座標に人が存在するかを判断できる。また、深度(depth)は先ほどと同じく、Kinectからの距離を示している。
○(3-2)“depth”コントロールにイメージをバインド
カメラ・イメージは「Bgr32」形式(=BPP:ピクセルごとのbit数が「32」のsRGB形式。前述の「PixelFormats.Bgr32」を参照)であった。しかし、深度情報のバイト配列は上記のような情報であるため、この深度情報が格納されたバイト配列を視覚的に分かるようなイメージに変換しなければならない。
サンプル・プログラムでは、次のような距離に応じた濃淡でイメージ化を行っている。
この濃淡をBgr32形式で表現するため、距離を「255」(白)〜「0」(黒)の範囲に変換している。変換式は次のとおりである。
playerindex = Bits[0] & 0x7;
depth = (Bits[1] <<5) | (Bits[0] >> 3) ;
byte intensity = (byte)(255 - (255 * depth / 0x0fff));
明度(intensity)は、depth(単位:ミリ・メートル)が「4095」(=12bit=「0x0fff」)のとき、「0」になり(一番暗くなり)、depthが「0」のとき、「255」になる(一番明るくなる)。先ほど、depthは1.2〜3.5メートルの範囲であり、depthが「0」のときは計測不能と述べた。そのため、明るさが一番明るい部分は、「計測不能」を意味していることに注意されたい。
このようにして計算された明度を用いて、Bgr32形式のイメージを作る。
例えば座標(0, 0)の場合は、以下のようになっている。
// byte[] depth_image = new byte[320 * 240 * 4]; と確保されている
depth_image[ 0 ] = intensity; // Blueの明度
depth_image[ 1 ] = intensity; // Greenの明度
depth_image[ 2 ] = intensity; // Redの明度
以上のコードだけでは、単に深度情報がグレー・イメージ化されただけである。サンプル・プログラムではプレイヤー・インデックスに「0」以外の値が入っていたら、プレイヤーごとに異なる色が付くように、各RGBの明度の割合を変更している。詳しくは、サンプル・プログラムのMainWindow.xaml.csファイルの125行目以降を参照されたい。
これでようやく、深度情報がBgr32形式のイメージに変換された。
これをカメラ・イメージのときと同様に、“depth”コントロールにバインドする(次のコードを参照)。
void nui_DepthFrameReady(object sender, ImageFrameReadyEventArgs e)
{
PlanarImage Image = e.ImageFrame.Image;
// 深度情報から、Bgr32形式の画像へ変換
byte[] convertedDepthFrame = convertDepthFrame(Image.Bits);
depth.Source = BitmapSource.Create(
Image.Width, Image.Height, 96, 96, PixelFormats.Bgr32, null,
convertedDepthFrame, Image.Width * 4);
}
○(4)深度情報の更新間隔から、1秒間の描画回数を計算
深度情報が更新されて準備が整うたびに、RuntimeオブジェクトのDepthFrameReadyイベントに追加されたイベント・ハンドラが呼び出される。サンプル・プログラムでは、このタイミングでフレームレートを計算し、そのFPS値を表示している(次のコードを参照)。
void nui_DepthFrameReady(object sender, ImageFrameReadyEventArgs e)
{
……省略……
++totalFrames;
DateTime cur = DateTime.Now;
if (cur.Subtract(lastTime) > TimeSpan.FromSeconds(1))
{
int frameDiff = totalFrames - lastFrames;
lastFrames = totalFrames;
lastTime = cur;
frameRate.Text = frameDiff.ToString() + " fps";
}
}
○(5-1)骨格情報の取得
RuntimeオブジェクトのInitializeメソッドの引数に「RuntimeOptions. UseSkeletalTracking」を含めた場合、Kinectから骨格情報を取得できる。
この骨格情報もまた「ポーリング・モデル」と「イベント・モデル」の方式がある。ポーリング・モデルで骨格情報を取得する場合は、SkeletonEngineクラスのGetNextFrameメソッドを用いる。イベント・モデルの場合は、RuntimeオブジェクトのSkeletonFrameReadyイベントにイベント・ハンドラを追加する。
サンプル・プログラムは、イベント・ハンドラの方式を利用しているので、こちらを使って説明する(以下のコードを参照)。
nui.SkeletonFrameReady += new
EventHandler<SkeletonFrameReadyEventArgs>(nui_SkeletonFrameReady);
void nui_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
{
SkeletonFrame skeletonFrame = e.SkeletonFrame;
}
Kinect for Windows SDK(ベータ版)では、最大2人までの完全な骨格情報を取得でき、また4人までの人を識別して、それぞれの位置を取得できる。
識別できた骨格情報には、SkeletonFrameReadyイベント・ハンドラの引数「e」(=SkeletonFrameReadyEventArgs型のオブジェクト)のSkeletonFrameプロパティ(から得られるSkeletonFrameオブジェクト)のSkeletonsフィールド(=SkeltonData型の配列)経由でアクセスする。SkeltonDataクラスには、骨格のジョイント情報(=関節となる点の情報)が格納されているJointsフィールドがある。このフィールドの型は、JointsCollectionクラスであり、
「頭(Head)」「肩の中心(ShoulderCenter)」
「右肩(ShoulderRight)」「右ひじ(ElbowRigh)」「右手首(WristRight)」「右手(HandRight)」
「左肩(ShoulderLeft)」「左ひじ(ElbowLeft)」「左手首(WristLeft)」「左手(HandLeft)」
「背骨(Spine)」「臀部(でんぶ)の中心(HipCenter)」
「右臀部(HipRight)」「右ひざ(KneeRight)」「右足首(AnkleRight)」「右足(FootRight)」
「左臀部(HipLeft)」「左ひざ(KneeLeft)」「左足首(AnkleLeft)」「左足(FootLeft)」
という20のジョイント情報(=Joint構造体の値)が格納されている(各ジョイント名のカッコ内の英単語は、JointID列挙体の値を示す。JointID列挙体は後述の説明の中で用いる)。
これは、最大2人までの完全な骨格情報が取得できる場合である。2人より多い場合、4人までは識別はできるが、完全な骨格情報は取得できない。完全ではないことを判断するために、SkeltonDataクラスのTrackingStateフィールドがある。このフィールドは、以下の3つの値を取り得る(=SkeletonTrackingState列挙体の値)。
完全な骨格情報を取得できるのは、「SkeletonTrackingState.Tracked」のときのみである。
では、実際の骨格情報を取得する手順について見てみる。
まず、SkeletonFrameReadyイベント・ハンドラの引数「e」からSkeletonFrameオブジェクトを得る。
SkeletonFrame skeletonFrame = e.SkeletonFrame;
次に、「skeletonFrame.Skeletons」(=SkeletonFrameオブジェクトのSkeletonsフィールド)に識別された人の骨格情報が格納されているため、以下のように処理を行う。このとき、TrackingStateがSkeletonTrackingState.Trackedの場合のみ完全な骨格情報が取得できるので、その判断も行う。
foreach (SkeletonData data in skeletonFrame.Skeletons)
{
if (data.TrackingState == SkeletonTrackingState.Tracked)
{
// data.Joints[JointID.Head] // 頭の位置のベクトルを取得
}
}
各骨格のジョイント情報は、「data.Joints[<JointID列挙体の値>]」というコードで取得できるJointオブジェクト内に、以下の図の座標系を基にしたベクトル形式(=Vector型)データで格納されている。
この図を見て分かるように、Kinectデバイスを中心として、X/Y/Zの軸が構成されている。Kinect前面方向にZ軸が伸びているため、Z軸は0より増える方向にのみ伸び、マイナスにはならない点や、Y軸も通常のWPFやWindowsフォームの座標系とは反対の方向に伸びている点に注意してほしい。また、この座標系の単位は「メートル」で表されている点も注意されたい。
このように、骨格情報の座標系が描画の座標系とは異なるために、描画用の座標系(2次元)に変換しないと骨格情報を描画できない。Kinect for Windows SDK(ベータ版)は、下記の2つのメソッドにより、このサポートも行ってくれる。
まず、SkeletonToDepthImageメソッドを用いて、骨格情報から深度情報イメージの座標系へ変換する。この例は、「頭」ジョイントの座標(=JointオブジェクトのPositionプロパティの値)を変換する。
float depthX, depthY;
nui.SkeletonEngine.SkeletonToDepthImage(
data.Joints[JointID.Head].Position, out depthX, out depthY);
骨格情報のX/Y座標値は、メートル単位で正負両方の値を取る。SkeletonToDepthImageメソッドは、それを「0」〜「1.0」の範囲の値に変換し、(下記のコードのように)その値に、深度情報イメージの幅と高さをそれぞれかけ算し、実際の深度情報のイメージのX/Y座標値を得て、上記のdepthX変数とdepthY変数に返却する。
depthX = Math.Max(0, Math.Min(depthX * 320, 320));
depthY = Math.Max(0, Math.Min(depthY * 240, 240));
次に、得られた深度情報のX/Y座標値を、GetColorPixelCoordinatesFromDepthPixelメソッドを利用してカメラ・イメージの座標に変換する。ただし、現在、GetColorPixelCoordinatesFromDepthPixelメソッドは「ImageResolution.Resolution640x480」のみの対応となっている。
int colorX, colorY;
ImageViewArea iv = new ImageViewArea();
nui.NuiCamera.GetColorPixelCoordinatesFromDepthPixel(
ImageResolution.Resolution640x480, iv,
(int)depthX, (int)depthY, (short)0, out colorX, out colorY);
今回、骨格情報は「400×300」ピクセルのCanvasコントロールに縮小して描画するため、カメラ・イメージのX/Y座標値を「640×480」の場合から「400×300」の場合の値へ変換する。
new Point((int)(skeleton.Width * colorX / 640.0), (int)(skeleton.Height * colorY / 480));
このように、骨格情報の座標系を変換し、目的の描画領域に対応させる。
○(5-2)骨格情報を“skeleton”コントロールにバインドし描画
“skeleton”コントロールは、Canvasコントロールであるため、その子要素として「Polyline」(=多角形)または「Line」(=線)を追加して骨格情報を表示する(サンプル・プログラムのMainWindow.xaml.csファイルの236行目以降)。
具体的には、以下の順でPolylineを構成し、骨格を描画する
また、各骨格情報の各ポイント(=ジョイント)に、幅6ピクセルで長さ6ピクセルのLineを追加し、点を表現する。
●サンプル・プログラムのまとめ
Skeletal Viewer(C#版)は、カメラ・イメージ、深度センサーのイメージ、骨格情報、ならびに1秒間の描画回数であるFPSを表示するサンプル・プログラムだった。
このサンプルから、Kinect for Windows SDK(ベータ版)を利用する基本的なプログラムの開発手順が学べる。以下に、今回の説明で押さえておいてほしいポイントをまとめておこう。
次回も引き続き、Kinect for Windows SDK(ベータ版)によりインストールされたサンプル・プログラムを基に、録音/音源の位置特定/音声認識について解説する。
Copyright© Digital Advantage Corp. All Rights Reserved.