ブログ@kaorun55

HoloLensやKinectなどのDepthセンサーを中心に書いています。

Kinect for Windows SDK v2.0 でBody(関節など)を取得する

Kinect for Windows SDK v2.0入門 目次

Body(関節など)を取得して表示する方法について解説します。Kinect for Windows v1でのSkeletonStreamはBodyとなり、関節だけでなく、関節の向き、手の状態(グー、チョキ、パー)や表情などの検出も行うことができます。なお、表情などの検出はパラメーターが入ってはいますが、動作方法が不明(未実装?)です。FaceTrakingにも同様のパラメーターが入っており、こちらは動作するので、最終的にFaceに移動するのかもしれません(しないのかもしれません)。

関節(スケルトン)を検出できる人数は6人(v1では2人)となり、BodyIndexの検出数と同じになりました。なお、手の状態については2人までとなっています。どの人の手を追うかの選択はできないようです(APIある?)。

スライド25

また1人あたりの検出する関節数は25点(v1は20点)に増えています。

スライド26

増えた個所は「首」「右指先」「右親指」「左指先」「左親指」の5点です。

スライド27

検出距離の範囲は0.5m~4.5m。v1にあった、デフォルト(全身)、Seated(上半身)のモードはありませんが、机に座っていても関節の検出ができるようになっています。

環境

筆者の環境は次の通りです。

実行結果

関節の位置および手の状態を表示します。関節の位置はDepthの座標系で表示しています。v1と同様に追跡状態、推測状態があり、追跡状態は青、推測状態は黄色で表示します。

また手の状態もグー、チョキ、パーで色が変わるようになっています。

スクリーンショット 2014-07-24 11.34.28

解説

Bodyデータもほかのデータと同様です。KinectSensor(クラス)を起点に、BodyFrameSource、BodyFrameReader、BodyFrameを利用します。Bodyのデータは人ごとにBodyクラスにまとまっています。

初期化

初期化の手順は次の通りです。ほかの手順に比べると、データ取得までは簡単です。

  1. Kinectを開く
  2. Bodyの数を取得し、配列を作成する
  3. Bodyリーダーを開く

まずKinectを開きます。

続いてBodyFrameSourceのBodyCountに検出できる人の数を取得できるので、これでBody配列を作成します(C++は端折ってます...)。

最後にBodyリーダーを作成し、データの読み込み準備を行います。データの読み込みは、C++ではポーリング、C#ではイベントハンドラの登録を行います。

C++

void initialize()

{

// デフォルトのKinectを取得する

ERROR_CHECK( ::GetDefaultKinectSensor( &kinect ) );

// Kinectを開く

ERROR_CHECK( kinect->Open() );

BOOLEAN isOpen = false;

ERROR_CHECK( kinect->get_IsOpen( &isOpen ) );

if ( !isOpen ){

throw std::runtime_error( "Kinectが開けません" );

}

// ボディリーダーを取得する

ComPtr<IBodyFrameSource> bodyFrameSource;

ERROR_CHECK( kinect->get_BodyFrameSource( &bodyFrameSource ) );

ERROR_CHECK( bodyFrameSource->OpenReader( &bodyFrameReader ) );

}

C#(デスクトップ)

private void Window_Loaded( object sender, RoutedEventArgs e )

{

try {

kinect = KinectSensor.GetDefault();

if ( kinect == null ) {

throw new Exception("Kinectを開けません");

}

kinect.Open();

// Bodyを入れる配列を作る

bodies = new Body[kinect.BodyFrameSource.BodyCount];

// ボディーリーダーを開く

bodyFrameReader = kinect.BodyFrameSource.OpenReader();

bodyFrameReader.FrameArrived += bodyFrameReader_FrameArrived;

}

catch ( Exception ex ) {

MessageBox.Show( ex.Message );

Close();

}

}

C#(Windows ストアアプリ)

protected override void OnNavigatedTo( NavigationEventArgs e )

{

base.OnNavigatedTo( e );

try {

kinect = KinectSensor.GetDefault();

if ( kinect == null ) {

throw new Exception( "Kinectを開けません" );

}

kinect.Open();

// Body用のバッファを作成

bodies = new Body[kinect.BodyFrameSource.BodyCount];

// ボディリーダーを開く

bodyFrameReader = kinect.BodyFrameSource.OpenReader();

bodyFrameReader.FrameArrived += bodyFrameReader_FrameArrived;

}

catch ( Exception ex ) {

MessageDialog dlg = new MessageDialog(ex.Message);

dlg.ShowAsync();

}

}

データの取得

続いてデータを取得し表示します。C++ではポーリング、C#ではイベントハンドラになりますが、大きな流れは同じです。

  1. Bodyフレームを取得する
  2. Bodyのデータを取得する
  3. Bodyのデータを表示する

Bodyフレーム、データを取得する

イベントが発生またはフレームが取得できたら、フレームからGetAndRefreshBodyData()でBodyデータをコピーします。

C++

void updateBodyFrame()

{

// フレームを取得する

ComPtr<IBodyFrame> bodyFrame;

auto ret = bodyFrameReader->AcquireLatestFrame( &bodyFrame );

if ( ret == S_OK ){

// データを取得する

ERROR_CHECK( bodyFrame->GetAndRefreshBodyData( 6, &bodies[0] ) );

// スマートポインタを使ってない場合は、自分でフレームを解放する

// bodyFrame->Release();

}

}

C#(デスクトップ)

private void UpdateBodyFrame( BodyFrameArrivedEventArgs e )

{

using ( var bodyFrame = e.FrameReference.AcquireFrame() ) {

if ( bodyFrame == null ) {

return;

}

// ボディデータを取得する

bodyFrame.GetAndRefreshBodyData( bodies );

}

}

C#(Windows ストアアプリ)

private void UpdateBodyFrame( BodyFrameArrivedEventArgs e )

{

using ( var bodyFrame = e.FrameReference.AcquireFrame() ) {

if ( bodyFrame == null ) {

return;

}

// ボディデータを取得する

bodyFrame.GetAndRefreshBodyData( bodies );

}

}

Bodyデータを表示する

Bodyデータを6人分取得したら、実際に追跡しているBodyを抜き出します。C++では検出しているBodyはnull以外の値が入っており、実際に追跡しているかどうかをIsTrackedで判別します。C#の場合は取得したBody配列をWhere句でIsTrackedのものだけ抜き出します。

追跡している人がいたら、関節の情報をC++はIBody::GetJoints()、C#はBody.Jointsで取り出します。関節ごとに追跡状態があり、NotTracked(追跡していない)、Tracked(追跡している)、Inferred(推測状態:腕が交差している、隠れている場合など)の3種類になります。NotTrackedでなければ座標はとれますが、Inferredは座標の信頼性が低いことに注意してください。

また、左右の手の状態はC++ではIBody::get_HandLeftState()またはget_HandRightState()、C#ではBody.HandLeftStateおよびHandRightStateから取得できます。手の状態にも信頼性のパラメーターがあり、 C++ではIBody::get_HandLeftConfidence()およびget_HandRightConfidence()、C#ではBody.HandLeftConfidenceおよびHandRightConfidenceで取得できます。信頼性はTrackingConfidence列挙体で定義されており、HighまたはLowとなります。Highであれば信頼できる状態として表示に使います。

C++

void drawBodyIndexFrame()

{

// 関節の座標をDepth座標系で表示する

cv::Mat bodyImage = cv::Mat::zeros( 424, 512, CV_8UC4 );

for ( auto body : bodies ){

if ( body == nullptr ){

continue;

}

BOOLEAN isTracked = false;

ERROR_CHECK( body->get_IsTracked( &isTracked ) );

if ( !isTracked ) {

continue;

}

// 関節の位置を表示する

Joint joints[JointType::JointType_Count];

body->GetJoints( JointType::JointType_Count, joints );

for ( auto joint : joints ) {

// 手の位置が追跡状態

if ( joint.TrackingState == TrackingState::TrackingState_Tracked ) {

drawEllipse( bodyImage, joint, 10, cv::Scalar( 255, 0, 0 ) );

// 左手を追跡していたら、手の状態を表示する

if ( joint.JointType == JointType::JointType_HandLeft ) {

HandState handState;

TrackingConfidence handConfidence;

body->get_HandLeftState( &handState );

body->get_HandLeftConfidence( &handConfidence );

drawHandState( bodyImage, joint, handConfidence, handState );

}

// 右手を追跡していたら、手の状態を表示する

else if ( joint.JointType == JointType::JointType_HandRight ) {

HandState handState;

TrackingConfidence handConfidence;

body->get_HandRightState( &handState );

body->get_HandRightConfidence( &handConfidence );

drawHandState( bodyImage, joint, handConfidence, handState );

}

}

// 手の位置が推測状態

else if ( joint.TrackingState == TrackingState::TrackingState_Inferred ) {

drawEllipse( bodyImage, joint, 10, cv::Scalar( 255, 255, 0 ) );

}

}

}

cv::imshow( "Body Image", bodyImage );

}

C#(デスクトップ)

private void DrawBodyFrame()

{

CanvasBody.Children.Clear();

foreach ( var body in bodies.Where( b => b.IsTracked ) ) {

foreach ( var joint in body.Joints ) {

// 手の位置が追跡状態

if ( joint.Value.TrackingState == TrackingState.Tracked ) {

DrawEllipse( joint.Value, 10, Brushes.Blue );

// 左手を追跡していたら、手の状態を表示する

if ( joint.Value.JointType == JointType.HandLeft ) {

DrawHandState( body.Joints[JointType.HandLeft], body.HandLeftConfidence, body.HandLeftState );

}

// 右手を追跡していたら、手の状態を表示する

else if ( joint.Value.JointType == JointType.HandRight ) {

DrawHandState( body.Joints[JointType.HandRight], body.HandRightConfidence, body.HandRightState );

}

}

// 手の位置が推測状態

else if ( joint.Value.TrackingState == TrackingState.Inferred ) {

DrawEllipse( joint.Value, 10, Brushes.Yellow );

}

}

}

}

C#(Windows ストアアプリ)

private void DrawBodyFrame()

{

CanvasBody.Children.Clear();

foreach ( var body in bodies.Where( b => b.IsTracked ) ) {

foreach ( var joint in body.Joints ) {

// 手の位置が追跡状態

if ( joint.Value.TrackingState == TrackingState.Tracked ) {

DrawEllipse( joint.Value, 10, Colors.Blue );

// 左手を追跡していたら、手の状態を表示する

if ( joint.Value.JointType == JointType.HandLeft ) {

DrawHandState( body.Joints[JointType.HandLeft], body.HandLeftConfidence, body.HandLeftState );

}

// 右手を追跡していたら、手の状態を表示する

else if ( joint.Value.JointType == JointType.HandRight ) {

DrawHandState( body.Joints[JointType.HandRight], body.HandRightConfidence, body.HandRightState );

}

}

// 手の位置が推測状態

else if ( joint.Value.TrackingState == TrackingState.Inferred ) {

DrawEllipse( joint.Value, 10, Colors.Yellow );

}

}

}

}

関節の位置を描画する

関節の位置を描画します。関節はJointクラスで定義されており、関節の種類、追跡状態、位置を取得できます。関節の位置はKinectを中心とした三次元座標(X,Y,Z)となっています。これを表示するために二次元のDepth座標系(X,Y)に変換します。座標変換はCoordinateMapperクラスで行います。いくつか変換関数がありますが、今回はカメラ座標系をDepth座標系に変換するので、MapCameraPointToDepthSpace()を使います。変換した座標の値で描画します。

C++

void drawEllipse( cv::Mat& bodyImage, const Joint& joint, int r, const cv::Scalar& color )

{

// カメラ座標系をDepth座標系に変換する

ComPtr<ICoordinateMapper> mapper;

ERROR_CHECK( kinect->get_CoordinateMapper( &mapper ) );

DepthSpacePoint point;

mapper->MapCameraPointToDepthSpace( joint.Position, &point );

cv::circle( bodyImage, cv::Point( point.X, point.Y ), r, color, -1 );

}

C#(デスクトップ)

private void DrawEllipse( Joint joint, int R, Brush brush )

{

var ellipse = new Ellipse()

{

Width = R,

Height = R,

Fill = brush,

};

// カメラ座標系をDepth座標系に変換する

var point = kinect.CoordinateMapper.MapCameraPointToDepthSpace( joint.Position );

if ( (point.X < 0) || (point.Y < 0) ) {

return;

}

// Depth座標系で円を配置する

Canvas.SetLeft( ellipse, point.X - (R / 2) );

Canvas.SetTop( ellipse, point.Y - (R / 2) );

CanvasBody.Children.Add( ellipse );

}

C#(Windows ストアアプリ)

private void DrawEllipse( Joint joint, int R, Color color )

{

var ellipse = new Ellipse()

{

Width = R,

Height = R,

Fill = new SolidColorBrush( color ),

};

// カメラ座標系をDepth座標系に変換する

var point = kinect.CoordinateMapper.MapCameraPointToDepthSpace( joint.Position );

if ( (point.X < 0) || (point.Y < 0) ) {

return;

}

// Depth座標系で円を配置する

Canvas.SetLeft( ellipse, point.X - (R / 2) );

Canvas.SetTop( ellipse, point.Y - (R / 2) );

CanvasBody.Children.Add( ellipse );

}

手の状態を描画する

手の状態はTrackingConfidenceがHighの場合にHandStateの値(Open、Lasso、Closed)によって色を変えて描画しています。

C++

void drawHandState( cv::Mat& bodyImage, Joint joint, TrackingConfidence handConfidence, HandState handState )

{

const int R = 40;

if ( handConfidence != TrackingConfidence::TrackingConfidence_High ){

return;

}

// カメラ座標系をDepth座標系に変換する

ComPtr<ICoordinateMapper> mapper;

ERROR_CHECK( kinect->get_CoordinateMapper( &mapper ) );

DepthSpacePoint point;

mapper->MapCameraPointToDepthSpace( joint.Position, &point );

// 手が開いている(パー)

if ( handState == HandState::HandState_Open ){

cv::circle( bodyImage, cv::Point( point.X, point.Y ), R, cv::Scalar( 0, 255, 255 ), R / 4 );

}

// チョキのような感じ

else if ( handState == HandState::HandState_Lasso ){

cv::circle( bodyImage, cv::Point( point.X, point.Y ), R, cv::Scalar( 255, 0, 255 ), R / 4 );

}

// 手が閉じている(グー)

else if ( handState == HandState::HandState_Closed ){

cv::circle( bodyImage, cv::Point( point.X, point.Y ), R, cv::Scalar( 255, 255, 0 ), R / 4 );

}

}

C#(デスクトップ)

private void DrawHandState( Joint joint, TrackingConfidence trackingConfidence, HandState handState )

{

// 手の追跡信頼性が高い

if ( trackingConfidence != TrackingConfidence.High ) {

return;

}

// 手が開いている(パー)

if ( handState == HandState.Open ) {

DrawEllipse( joint, 40, new SolidColorBrush( new Color()

{

R = 255,

G = 255,

A = 128

} ) );

}

// チョキのような感じ

else if ( handState == HandState.Lasso ) {

DrawEllipse( joint, 40, new SolidColorBrush( new Color()

{

R = 255,

B = 255,

A = 128

} ) );

}

// 手が閉じている(グー)

else if ( handState == HandState.Closed ) {

DrawEllipse( joint, 40, new SolidColorBrush( new Color()

{

G = 255,

B = 255,

A = 128

} ) );

}

}

C#(Windows ストアアプリ)

private void DrawHandState( Joint joint, TrackingConfidence trackingConfidence, HandState handState )

{

// 手の追跡信頼性が高い

if ( trackingConfidence != TrackingConfidence.High ) {

return;

}

// 手が開いている(パー)

if ( handState == HandState.Open ) {

DrawEllipse( joint, 40, new Color()

{

R = 255,

G = 255,

A = 128

} );

}

// チョキのような感じ

else if ( handState == HandState.Lasso ) {

DrawEllipse( joint, 40, new Color()

{

R = 255,

B = 255,

A = 128

} );

}

// 手が閉じている(グー)

else if ( handState == HandState.Closed ) {

DrawEllipse( joint, 40, new Color()

{

G = 255,

B = 255,

A = 128

} );

}

}

まとめ

Bodyについて、関節などv1と同じデータについては、ほぼv1と同じ形で記述ができます。

手の状態については、もうちょっとプロパティがきれいにまとまってくれると、コードもきれいになるのになぁと思いつつ。