Deep Insider の Tutor コーナー
>>  Deep Insider は本サイトからスピンオフした姉妹サイトです。よろしく! 
新型Kinect for Windows v2 Developer Previewプログラミング入門(3)

新型Kinect for Windows v2 Developer Previewプログラミング入門(3)

新型Kinectの骨格データに関する新機能とは?

2014年1月24日

KinectSDK2プレビュー版の距離データと骨格データに関する主要な変更点を解説。手の開閉が判定できるようになり、グー、チョキ、パーも判別可能に。

初音 玲(Microsoft MVP for Visual Basic)
  • このエントリーをはてなブックマークに追加

 前回は「Kinect for Windows SDK v2 Developer Preview(以下、KinectSDK2プレビュー版)」のカラーデータの変更点について紹介した。今回は距離データと骨格データに関する主要な変更点を説明する。

注意事項

 本稿で検証に使用したKinect for Windows v2 Developer Previewのソフトウェアやハードウェア、APIは暫定的なものであり、正式版では変更される可能性がある。

 また、本稿は早期提供プログラムの参加規約で公表が許可されている事項について実際に確認した結果に基づき記載していることもあらかじめご了承いただきたい。

距離データの変更点とは

 現行のKinect for Windows(以下、Kinect1)とKinect for Windows V2 Developer Preview(以下、Kinect2プレビュー版)では、ハード的にも距離データの測定能力に次のような大きな違いがある。

項目 Kinect1 Kinect2プレビュー版
最大解像度(横×縦) 320×2401 512×424
視野角(水平×垂直) 57×43 70.6×60.0
測定距離 0.8m~4.0m
(近接モードに切り替えると0.5~3.0m)
0.5m~4.5m
表1.1 距離測定能力の違い

1Kinect SDK - NuiImageCamera.hファイル内の「NUI_CAMERA_DEPTH_NOMINAL_FOCAL_LENGTH_IN_PIXELS」の定義値より。

 この視野角の違いは、Kinect1にあったチルトモーターによるセンサーの上下調整が廃止されているが、手動合わせでも人物の全身を視野の中に比較的容易に納められることからも実感できる。さらにKinectSDK2プレビュー版では、Kinect for Windows SDK v1.x(以下、KinectSDK1)よりも距離データの形式が、プログラムで処理する上で格段に楽なものに変更されている。

KinectSDK1の距離データ形式

 現行のKinectSDK1の距離データの形式は「距離(mm)+PlayerIndex」という形式だった(図1.1)。PlayerIndexはKinect SDKが人として認識した距離データに付与される値で、距離データの下3bitを使って同一画面に含まれる最大6人に人ごとの別値が割り当てられている。

図1.1 KinectSDK1での距離データ形式

 そのため、距離データから距離を取り出すためには、次のコードに示すように3bitの右シフト演算が必要になってしまう。

Visual Basic
frame.CopyPixelDataTo(Me.DepthImagePixelData)
For index As Integer = 0 To Me. DepthImagePixelData.Length - 1
  Me.DepthImagePixelData(index) >>= DepthImageFrame.PlayerIndexBitmaskWidth
Nex
C#
frame.CopyPixelDataTo(this.DepthImagePixelData);
for (int index = 0; index <= this.DepthImagePixelData.Length - 1; index++)
{
  this.DepthImagePixelData[index] >>= DepthImageFrame.PlayerIndexBitmaskWidth;
}
リスト1.1 距離データの取り出し方(上:VB.NET、下:C#)

距離データ形式のv1.6での改良

 さすがに距離データなのに距離を取得するのにドット単位のシフト演算が必要なのは使いづらいという判断が下され、Kinect for Windows SDK v1.6からは距離データとPlayerIndexがドットごとに分離できるような構造体もサポートされた。しかし、ドットごとであったために距離データだけの配列を得たいときはループ処理をしなければならない点は変わらなかった(リスト1.2)。

Visual Basic
frame.CopyDepthImagePixelDataTo(Me.DepthImageFrameData)
For index As Integer = 0 To Me.DepthImageFrameData.Length  - 1
  Me.DepthImagePixelData(index) = Me.DepthImageFrameData(index).Depth
Next
C#
frame.CopyDepthImagePixelDataTo(this.DepthImageFrameData);
for (int index = 0; index <= this.DepthImageFrameData.Length - 1; index++)
{
  this.DepthImagePixelData[index] = this.DepthImageFrameData[index].Depth;
}
リスト1.2 v1.6での距離データの取り出し方(上:VB.NET、下:C#)

KinectSDK2プレビュー版の距離データ形式

 KinectSDK2プレビュー版では、距離データとPlayerIndexデータを別々に取得するように方式が変更され、「short型として取得できる距離データの値=対象物までの距離値(mm)」となった(図1.2)。

図1.2 KinectSDK2プレビュー版での距離データの形式

 距離データの形式がshort型となったので、Kinectから送られてきたframeデータをshort型配列へ変換するのもリスト1.3に示すようにシンプルだ。

Visual Basic
frame.CopyFrameDataToArray(Me.DepthImageFrameData)
C#
frame.CopyFrameDataToArray(this.DepthImageFrameData);
リスト1.3 v1.6での距離データの取り出し方(上:VB.NET、下:C#)

骨格データの変更点とは

KinectSDK1の骨格データ

 KinectSDK1では20カ所の関節位置(図2.1)が骨格データとして取得できる。

図2.1 KinectSDK1骨格データ

 体の中心線上の4カ所と、両手両足の合計16カ所の検出箇所は、体全体のポーズを判定するのに十分なデータ量となっているが、手のひらの中心は取れるが指の位置が取れないため手の開閉が分からなかったり、頭の中心点の位置は検出できるが頭頂が取れないため身長が取れなかったりと、使っていると不満な点も見えてくる。例えば、PlayerIndexは6人まで識別できるのに骨格データは2人しか取得できないというのもその1つだ。

 このようなKinectSDK1の骨格データの不満点は、KinectSDK2プレビュー版で見事に解消している。

KinectSDK2プレビュー版の骨格データ

図2.2 KinectSDK2プレビュー版の骨格データ

 KinectSDK1では2人しか同時に取得できなかった骨格データが、図2.2に示すようにKinectSDK2プレビュー版では6人まで同時に取得できるようになった。また、骨格データ自体も「首」「右手先」「右手親指」「左手先」「左手親指」の計5カ所が追加で取得可能になった。

 手先と親指が検出できるので手の開閉も判断できると喜んでいたら、そのものずばりのプロパティも追加されていて、そのプロパティの中で「グー、チョキ、パー」が判定できる。つまり、片手にある3つの位置を判断しなくてもグー、チョキ、パーが取得できるのだ。

骨格データ取得手順

 骨格データ取得手順は図2.3のように変更されている。

図2.3 骨格データ取得手順

 それでは具体的に骨格データの取得コードを見ていこう。

変数宣言部

 内部的に使用する変数は以下の通りである(VBとC#のコードを併記)。

Visual Basic
Private WithEvents Kinect As KinectSensor     ……1
Private WithEvents Reader As BodyFrameReader  ……2
Private ReadOnly BodyBrushes As Brush() = New Brush() {Brushes.White,
                                                       Brushes.Crimson,
                                                       Brushes.Indigo,
                                                       Brushes.DodgerBlue,
                                                       Brushes.Yellow,
                                                       Brushes.Pink}
Private Bodies As Body()                      ……3
Private BodyDrawingGroup As New DrawingGroup  ……4
Private BodyImageBitmapRect As Rect           ……5
リスト2.1 変数宣言部(VB.NET)
  • 1Kinectデバイスとの接続用変数を宣言。
  • 2骨格データ用のFrameRreader変数をHandles句でイベントが拾えるようにWithEvents付きで宣言。
  • 3骨格データ用のフレームをBody形式に変換したものを格納する変数を宣言。
  • 4画面に表示するためのDrawingGroup変数を宣言。
  • 5WriteableBitmapを書き換えるときの1行分のバイト数を格納する変数を宣言。
C#
private KinectSensor Kinect;                                ……1
private BodyFrameReader Reader;                             ……2
private readonly Brush[] BodyBrushes = new Brush[] {Brushes.White,
                                                    Brushes.Crimson,
                                                    Brushes.Indigo,
                                                    Brushes.DodgerBlue,
                                                    Brushes.Yellow,
                                                    Brushes.Pink};
private Body[] Bodies;                                      ……3
private DrawingGroup BodyDrawingGroup = new DrawingGroup(); ……4
private Rect BodyImageBitmapRect;                           ……5
リスト2.2 変数宣言部(C#)
  • 1Kinectデバイスとの接続用変数を宣言。
  • 2骨格データ用のFrameRreader変数を宣言。
  • 3骨格データ用のフレームをBody形式に変換したものを格納する変数を宣言。
  • 4画面に表示するためのDrawingGroup変数を宣言。
  • 5WriteableBitmapを書き換えるときの1行分のバイト数を格納する変数を宣言。

 骨格データの取り扱いクラス名が「Skeleton」から「Body」に変わったこともあり、変数宣言部で使われている変数型は、KinectSDK2プレビュー版ではほぼ変更になっている。

センサー接続と初期化

 続いて、センサーに接続して初期化するコードを見てみよう。

Visual Basic
Private Sub DiscoverKinectSensor()
  Me.Kinect = KinectSensor.KinectSensors.FirstOrDefault(
    Function(x)
      Return x.Status = KinectStatus.Connected                  ……1
    End Function)
 
  If Me.Kinect IsNot Nothing Then
    Dim depthDesc = Me.Kinect.DepthFrameSource.FrameDescription ……2
 
    Me.Kinect.Open()                                            ……3
    Me.Reader = Me.Kinect.BodyFrameSource.OpenReader            ……4
    ReDim Me.Bodies(Me.Kinect.BodyFrameSource.BodyCount - 1)    ……5
    Me.BodyImageBitmapRect = New Rect(0,                        ……6
                                      0,
                                      depthDesc.Width,
                                      depthDesc.Height)
  End If
End Sub
リスト2.3 センサーの初期化(VB.NET)
  • 1接続されている1番目のKinect2センサーを取得する。
  • 2表示領域サイズを距離データ範囲とするため、各種情報(解像度など)を取得する。
  • 3Kinect2センサーをオンにする。
  • 4骨格データ用のフレーム取得を開始する。
  • 5取得可能人数分の大きさでBody形式配列を確保する。
  • 6距離データの解像度に合わせてWriteableBitmapの書き換え領域を確保する。
C#
private void DiscoverKinectSensor()
{
  this.Kinect = KinectSensor.KinectSensors.FirstOrDefault((x) =>
  {
    return x.Status == KinectStatus.Connected;                     ……1
  });
 
  if (this.Kinect != null)
  {
    var depthDesc = this.Kinect.DepthFrameSource.FrameDescription; ……2
 
    this.Kinect.Open();                                            ……3
    this.Reader = this.Kinect.BodyFrameSource.OpenReader();        ……4
    this.Reader.FrameArrived += Reader_FrameArrived;               ……4'
    this.Bodies = new Body[this.Kinect.BodyFrameSource.BodyCount];
    this.BodyImageBitmapRect = new Rect(0,                         ……6
                                        0,
                                        depthDesc.Width,
                                        depthDesc.Height);
  }
}
リスト2.4 センサーの初期化(C#)
  • 1接続されている1番目のKinect2センサーを取得する。
  • 2表示領域サイズを距離データ範囲とするため各種情報(解像度など)を取得する。
  • 3Kinect2センサーをオンにする。
  • 4骨格データ用のフレーム取得を開始する。
  • 4'C#ではWithEventsがないのでOpenReader後にイベントハンドラー登録する。
  • 5取得可能人数分の大きさでBody形式配列を確保する。
  • 6距離データの解像度に合わせてWriteableBitmapの書き換え領域を確保する。

骨格データ処理

 KinectSDK1と同様にKinectSDK2プレビュー版でも、骨格データはX/Y/Zの位置情報をメートル単位で取得できる(図2.4)。

図2.4 骨格データの値

 次のコードは、実際に骨格データを取得する例だ。

Visual Basic
Private Sub Reader_FrameArrived(sender As Object,
                                e As BodyFrameArrivedEventArgs) Handles Reader.FrameArrived
  Dim frameReference = e.FrameReference()                       ……1
  Using frame = frameReference.AcquireFrame                     ……2
    If frame IsNot Nothing Then
      Using dc = Me.BodyDrawingGroup.Open
        dc.DrawRectangle(Brushes.Black, Nothing, Me.BodyImageBitmapRect)
        frame.GetAndRefreshBodyData(Me.Bodies)                  ……3
        For index As Integer = 0 To Me.Bodies.Length - 1
          If Me.Bodies(index).IsTracked Then
            Dim joints As IReadOnlyDictionary(Of JointType, Joint) = Me.Bodies(index).Joints
            Dim points As New Dictionary(Of JointType, Point)
            For Each joint In joints.Keys                       ……4
              Dim pos = Me.Kinect.CoordinateMapper.MapCameraPointToDepthSpace(joints(joint).Position)
              points(joint) = New Point(pos.X, pos.Y)
            Next
            DrawBody(joints, points, BodyBrushes(index), dc)    ……5
          End If
        Next
        Me.BodyDrawingGroup.ClipGeometry = New RectangleGeometry(Me.BodyImageBitmapRect)
        Me.BodyImageElement = New DrawingImage(Me.BodyDrawingGroup)
      End Using
    End If
  End Using
End Sub
 
Private Sub DrawBody(joints As IReadOnlyDictionary(Of JointType, Joint),
                     jointPoints As Dictionary(Of JointType, Point),
                     bodyBrash As Brush,
                     dc As DrawingContext)
  ……中略……
  For Each jointType As JointType In joints.Keys
    Dim state As TrackingState = joints(jointType).TrackingState
 
    If (state = TrackingState.Tracked) Then
        dc.DrawEllipse(Brushes.Red,
                       Nothing,
                       jointPoints(jointType),                  ……5
                       3,
                       3)
    End If
  Next
End Sub
C#
private void Reader_FrameArrived(object sender, BodyFrameArrivedEventArgs e)
{
  var frameReference = e.FrameReference;                        ……1
  using (var frame = frameReference.AcquireFrame())             ……2
  {
    if (frame != null)
    {
      using (var dc = this.BodyDrawingGroup.Open())
      {
        dc.DrawRectangle(Brushes.Black, null, this.BodyImageBitmapRect);
        frame.GetAndRefreshBodyData(this.Bodies);               ……3
        for (int index = 0; index <= this.Bodies.Length - 1; index++)
        {
          if (this.Bodies[index].IsTracked)
          {
            IReadOnlyDictionary<JointType, Joint> joints = this.Bodies[index].Joints;
            Dictionary<JointType, Point> points = new Dictionary<JointType, Point>();
            foreach (var joint in joints.Keys)                  ……4
            {
              var pos = this.Kinect.CoordinateMapper.MapCameraPointToDepthSpace(joints[joint].Position);
              points[joint] = new Point(pos.X, pos.Y);
            }
            DrawBody(joints, points, BodyBrushes[index], dc);   ……5
          }
        }
        this.BodyDrawingGroup.ClipGeometry = new RectangleGeometry(this.BodyImageBitmapRect);
        this.BodyImageElement = new DrawingImage(this.BodyDrawingGroup);
      }
    }
  }
}
 
private void DrawBody(IReadOnlyDictionary<JointType, Joint> joints,
                      Dictionary<JointType, Point> jointPoints,
                      Brush bodyBrash,
                      DrawingContext dc)
{
  ……中略……
  foreach (JointType jointType in joints.Keys)
  {
    TrackingState state = joints[jointType].TrackingState;
 
    if (state == TrackingState.Tracked)
    {
      dc.DrawEllipse(Brushes.Red,
                     null,
                     jointPoints[jointType],                    ……5
                     3,
                     3);
    }
  }
}
リスト2.5 骨格データ処理部(上:VB.NET、下:C#)
  • 1フレーム情報を取得する。
  • 2AcquireFrameメソッドでフレームを取得する。
  • 3フレームからBody形式に変更し、取得できた複数人の骨格データをBody配列に格納する。
  • 4MapCameraPointToDepthSpaceメソッドで関節位置を距離データ解像度(512×424)での位置に変更する。
  • 5全ての関節位置に●を描画する。

 サンプル(Kinect2BodySample)で実装しているKinectModelクラスの処理内容は以上だ。あとはMainViewModelクラス経由でWPF上のImageコントロールにWriteableBitmapの最新の内容が随時表示され続ける。次の画像は、実際にサンプルを動かして表示されている結果だ。

Kinect2BodySampleの実行結果
図2.5 Kinect2BodySampleの実行結果

手の開閉を取得するには

 手の開閉は骨格データのHandLeftStateプロパティとHandRightStateプロパティで判定できる。

 手の開閉状態は、「Closed」「Lasso」「Open」の3つ(この他に「NotTracked」と「Unknown」がある)ある。ClosedとOpenは分かるが「Lasso」とは何だろうか。単純に翻訳すると「投げ縄」になるが、どうやら「2本指を認識している状態」という意味らしい。日本的にはまさに「チョキ」に相当する。

 手の周りに輪を描画し、グー、チョキ、パーで輪の色を変えるようにすると、どれくらいの精度でグー、チョキ、パーが判定できるのかが体感しやすいので、今回のサンプルでは次のようにKinectModelクラスで両手の状態を公開して、MainWindow.xamlファイルでその状態にBindingしている。

Visual Basic
Public Class THand
  Public Property Position As Point
  Public Property IsVisibled As Boolean
  Public Property State As EnumHandState
End Class
 
Public Enum EnumHandState
  None
  Closed
  Open
  Lasso
End Enum
 
Public Class KinectModel
  Implements INotifyPropertyChanged
  ……中略……
  Private Sub DrawHand(state As HandState, handPosition As Point, hand As THand)
    hand.Position = handPosition
    hand.IsVisibled = True
    Select Case state
      Case HandState.Closed
        hand.State = EnumHandState.Closed
      Case HandState.Open
        hand.State = EnumHandState.Open
      Case HandState.Lasso
        hand.State = EnumHandState.Lasso
      Case Else
        hand.IsVisibled = False
        hand.State = EnumHandState.None
     End Select
  End Sub
End Class
C#
public class THand
{
  public Point Position { get; set; }
  public bool IsVisibled { get; set; }
  public EnumHandState State { get; set; }
}
public enum EnumHandState
{
  None,
  Closed,
  Open,
  Lasso
}
 
public class KinectModel : INotifyPropertyChanged
{
  ……中略……
  private void DrawHand(HandState state, Point handPosition, THand hand)
  {
    hand.Position = handPosition;
    hand.IsVisibled = true;
    switch (state)
    {
      case HandState.Closed:
        hand.State = EnumHandState.Closed;
        break;
      case HandState.Open:
        hand.State = EnumHandState.Open;
        break;
      case HandState.Lasso:
        hand.State = EnumHandState.Lasso;
        break;
      default:
        hand.IsVisibled = false;
        hand.State = EnumHandState.None;
        break;
    }
  }
}
リスト2.6 手の開閉判定例(上:VB.NET、下:C#)

 なお、今回のサンプルではKinectModelクラスで設定したStateは、MainWindow.xamlファイル内で、表示する手の周りの輪の色を、StateToColorConverterという独自実装のコンバーターを使ってグーなら赤、パーなら緑、チョキなら青になるようにコンバートしている。

 次の画像は、実際にサンプルを動かして表示されている結果だ。

動画2.1 Kinect2HandStateSampleの実行結果

まとめ

 KinectSDK2プレビュー版の骨格データは、取得できる関節が20カ所から25カ所に増えて手の開閉も判定できるようになった。しかも、この手の開閉は、状態名から考えるとマイクロソフトの開発者は気が付いていないか重要視していないと思われるが、グー、チョキ、パーの判定に使える。グー、チョキ、パーが判定できるのは他国の状況は分からないが日本での需要はかなり高いだろう。

 また、SkeletonがBodyへと変わった点についても注目したい。現状ではまだこの変更に伴うような新機能が実装されてはいないし、ロードマップ的な情報も持ち合わせていないので推測でしかないが、例えば、心拍数などSkeletonではなくBodyならではの機能が実装される布石なのではと推測すると、正式版までにどのようになっていくのか、楽しさも増してくる。

※以下では、本稿の前後を合わせて5回分(第1回~第5回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。

新型Kinect for Windows v2 Developer Previewプログラミング入門(3)
1. 日本最速レビュー。開発者目線で調査する「Kinect for Windows v2」限定開発者プレビュー版

2014年に一般発売が予定されている新型Kinectを早くも先行レビュー。ハードウェアスペックや、新しくなったソフトウェア構成、センサー類の進化について紹介する。

新型Kinect for Windows v2 Developer Previewプログラミング入門(3)
2. Kinectプログラマー向け速報第2弾。SDKの相違点から新型Kinectの実力を探る

Kinect for Windows v2 Developer Preview向けのSDKは、以前のv1とどこが異なるのか? その主要な差異を示しながら、v2での基本的なプログラミング方法を紹介する。

新型Kinect for Windows v2 Developer Previewプログラミング入門(3)
3. 【現在、表示中】≫ 新型Kinectの骨格データに関する新機能とは?

KinectSDK2プレビュー版の距離データと骨格データに関する主要な変更点を解説。手の開閉が判定できるようになり、グー、チョキ、パーも判別可能に。

新型Kinect for Windows v2 Developer Previewプログラミング入門(3)
4. 世界最速・最新情報、March SDK Update(Kinect for Windows v2 Developer Preview)の内容に迫る!

本日、SDKの3月更新版が提供された。その更新点として「Kinectファームウェアの更新」「Kinect Serviceの変更は特になし」「Kinect Studioの提供」について紹介。

新型Kinect for Windows v2 Developer Previewプログラミング入門(3)
5. Windowsストアアプリ対応に、Unityサポートも ― 最新April SDK Update(Kinect for Windows v2 Developer Preview)の新機能

4月30日に提供が開始された「April SDK Update」の内容を解説。Windowsストアアプリ対応/Unityサポート/サービス・スピンダウン機能/Audio APIなどの新機能を紹介する。

サイトからのお知らせ

Twitterでつぶやこう!