Kinect for Windows SDK v2.0入門 目次
Body(関節など)を取得して表示する方法について解説します。Kinect for Windows v1でのSkeletonStreamはBodyとなり、関節だけでなく、関節の向き、手の状態(グー、チョキ、パー)や表情などの検出も行うことができます。なお、表情などの検出はパラメーターが入ってはいますが、動作方法が不明(未実装?)です。FaceTrakingにも同様のパラメーターが入っており、こちらは動作するので、最終的にFaceに移動するのかもしれません(しないのかもしれません)。
関節(スケルトン)を検出できる人数は6人(v1では2人)となり、BodyIndexの検出数と同じになりました。なお、手の状態については2人までとなっています。どの人の手を追うかの選択はできないようです(APIある?)。
また1人あたりの検出する関節数は25点(v1は20点)に増えています。
増えた個所は「首」「右指先」「右親指」「左指先」「左親指」の5点です。
検出距離の範囲は0.5m~4.5m。v1にあった、デフォルト(全身)、Seated(上半身)のモードはありませんが、机に座っていても関節の検出ができるようになっています。
環境
筆者の環境は次の通りです。
- Windows 8.1 Pro Update1 64bit
- Visual Studio 2013 Ultimate(Express for Windows Desktopでも可)
- Kinect for Windows SDK v2.0-1409(リリース版)
- 32bitアプリケーション
- サンプルコードのリポジトリ
実行結果
関節の位置および手の状態を表示します。関節の位置はDepthの座標系で表示しています。v1と同様に追跡状態、推測状態があり、追跡状態は青、推測状態は黄色で表示します。
また手の状態もグー、チョキ、パーで色が変わるようになっています。
解説
Bodyデータもほかのデータと同様です。KinectSensor(クラス)を起点に、BodyFrameSource、BodyFrameReader、BodyFrameを利用します。Bodyのデータは人ごとにBodyクラスにまとまっています。
初期化
初期化の手順は次の通りです。ほかの手順に比べると、データ取得までは簡単です。
- Kinectを開く
- Bodyの数を取得し、配列を作成する
- 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#ではイベントハンドラになりますが、大きな流れは同じです。
- Bodyフレームを取得する
- Bodyのデータを取得する
- 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と同じ形で記述ができます。
手の状態については、もうちょっとプロパティがきれいにまとまってくれると、コードもきれいになるのになぁと思いつつ。