Kinectを使って、自分の手のひらに小さな分身を出現させてみる

2012年9月27日(木)
薬師寺 国安

スケルトンの位置を取得する処理

Kinectセンサーの距離カメラから、距離カメラのフレームデータを表すDepthImageFrameクラス型のdepth変数を宣言し、OpenDepthImageFrameメソッドで、距離カメラのフレームデータを取得します。
距離データのピクセル座標、および距離、プレイヤーIDを表す、DepthImagePoint構造体のrightDepthPoint変数を宣言し、MapFromSkeletonPointメソッドで、スケルトンの座標を、距離カメラの座標に変換します。この場合右手の位置を距離カメラの座標に変換します。

MapFromSkeletonPointメソッドでの書式は下記です。

DepthImagePoint. MapFromSkeletonPoint(変換するスケルトンの座標)

次に、RGBカメラのX-Y座標データを表す、ColorImagePoint構造体の変数rightColorPointを宣言し、MapToColorImagePointメソッドで、距離カメラの座標をRGBカメラの座標に変換し、右手の位置を取得します。

書式は下記です。

DepthImageFrame.MapToColorImagePoint(距離カメラのX座標,距離カメラのY座標,RGBカメラのフォーマット)

この場合、右手の距離カメラのX座標と、Y座標、RGBフォーマットで解像度が640×480、フレーム レートは 毎秒30フレームに変換しています。距離カメラのデータをRGBカメラ(実写)のデータにマップします。

つまり、

スケルトンの座標を、距離カメラの座標に変換⇒距離カメラの座標をRGBカメラの座標に変換

といった流れになります。

分身を表示させるpersonImageコントロールと、右手のRGBカメラの座標を引数に、右手の動きに合わせて画像の位置が変化するCameraPositionプロシージャを実行します。

01Private Sub GetCameraPoint(ByVal first As Skeleton, ByVal e As AllFramesReadyEventArgs)
02  Using depth As DepthImageFrame = e.OpenDepthImageFrame()
03    If depth Is Nothing Then
04      Return
05    End If
06    Dim rightDepthPoint As DepthImagePoint = depth.MapFromSkeletonPoint(first.Joints(JointType.HandRight).Position)
07    Dim rightColorPoint As ColorImagePoint = depth.MapToColorImagePoint(rightDepthPoint.X, rightDepthPoint.Y, ColorImageFormat.RgbResolution640x480Fps30)
08    CameraPosition(personImage, rightColorPoint)
09  End Using
10End Sub

右手の動きに合わせて画像の位置が変化する処理

Canvas.SetLeftとSetTopプロパティの書式は下記です。

  • Canvas.SetLeft(プロパティ値の書き込み対象の要素,指定した要素のCanvas.Left属性を設定)
  • Canvas.SetTop(プロパティ値の書き込み対象の要素,指定した要素のCanvas.Top属性を設定)

-50しているのは画像と右手のY座標の位置合わせのためのものです。

1Private Sub CameraPosition(ByVal element As FrameworkElement, ByVal point As ColorImagePoint)
2  Canvas.SetLeft(element, point.X - (CLng(element.Width) / 2))
3  Canvas.SetTop(element, point.Y - (CLng(element.Height) / 2) - 50)
4End Sub

画像が右手の動きに反応する処理

Coding4Fun Kinect ToolkitのScaleToメソッドで、最大の幅と高さを指定する関節の位置をスケールします。書式は下記です。ちなみに、1920×1080は筆者のパソコンの解像度で、0.3Fは単精度浮動小数点型に指定したスケルトンのXとY座標です。

Joint.ScaleTo(width As Integer, height As Integer,maxSkeletonX As Single,maxSkeletonY As Single)

Canvas.SetLeftとSetTopプロパティの書式は下記です。

  • Canvas.SetLeft(プロパティ値の書き込み対象の要素,指定した要素のCanvas.Left属性を設定)
  • Canvas.SetTop(プロパティ値の書き込み対象の要素,指定した要素のCanvas.Top属性を設定)
1Private Sub ScalePosition(ByVal element As FrameworkElement, ByVal joint As Joint)
2  Dim scaledJoint As Joint = joint.ScaleTo(1920, 1080, 0.3F, 0.3F)
3  Canvas.SetLeft(element, scaledJoint.Position.X)
4  Canvas.SetTop(element, scaledJoint.Position.Y)
5End Sub

Kinectの画像をビットマップデータに書き出す処理

RGBカメラの1ラインあたりのバイト数を表すcolorStride変数にcolorFrame.BytesPerPixel *colorFrame.Width と指定します。RGBカメラの1ピクセルあたりのバイト数を取得して、RGBカメラのフレームの幅に乗算します。RGBカメラの1ラインあたりのバイト数が取得できます。32ビットを1バイト(8ビット)で除算すると4バイトそれにcolorFrame.Widthの640を乗算します。4×640=2560となります。直接この値を指定しても問題はありません。

また、RGBカメラのピクセルあたりのバイト数(BytesPerPixel)と距離カメラのストリームフレーム幅(depthFrame.Width)とを乗算して取得した、距離カメラの1ラインあたりのストリームバイト数を変数screenImageStrideに格納しておきます。これも4×640=2560となり、直接この値を指定しても問題ありません。

Byte型の配列変数bytePlayer2変数を、距離カメラのフレームの高さと、距離カメラの1ラインあたりのストリームバイト数を保持しているscreenImageStrideで乗算した値で初期化します。
つまり、depthFrame.Heightの640に、screenImageStrideに設定した、colorFrame.BytesPerPixel * depthFrame.Width (4(バイト)×640)の値である2560を乗算した(640×2560)1638400-1の値で初期化されたバイト配列型のbytePlayer2オブジェクトが作成されます。

同様にbytePlayer1とbyteRoomバイト型配列変数も同じ値で初期化しておきます。

CopyPixelDataToメソッドで、距離カメラのピクセルごとのデータを取得します。CopyPixelDataToメソッドは、ピクセルデータの長さを使用して、事前に割り当てられた配列へ、ピクセルごとの深度データやRGBデータをコピーします。同様にRGBカメラのピクセルごとのデータも取得します。これで、実際のデータを取り出します。

繰り返し変数depthYで、0から距離カメラのフレームの高さ分反復処理を行います。反復処理内では以下の処理を行います。

depthPixelIndex変数に1ずつ加算される変数depthXの値と繰り返し変数depthYに距離カメラのフレーム幅(640)を乗算した値を加算して、格納しておきます。

init_kinectプロシージャ内で確保された、myDepthPixelDataのShort型の配列で、depthPixelIndex変数に対応する値と、距離カメラのフレームデータから、PlayerIndexBitmaskでビットマスクに含まれているプレイヤーIDとで論理積演算を行います。距離カメラのピクセルデータを反復処理し、プレイヤーのインデックス値を抽出します。

ビット演算 AND は、2 つのビットを比較し、両方のビットの値が 1 の場合にのみ、値 1 を結果に代入します。そうでない場合は、結果のビットに 0 をセットします。AND 演算子は、その他全てのビット演算子と同様、オペランドとして数値のみを取ります。And演算子に付いては下記のURLを参照してください。
→ And 演算子(msdn)

RGBカメラのX-Y座標データを表すColorImagePointクラスのcolorPoint変数に、MapDepthToColorImagePointで、距離カメラの座標に対応する、RGBカメラの座標を取得します。深度情報を実画像に変換してくれます。書式は下記の通りです。

KinectSensor. MapDepthToColorImagePoint(距離カメラの フォーマット,距離カメラのX 座標,XとY座標の距離データ(Short型),RGBカメラのフォーマット)

変数colorPixelIndexに、RGBカメラのX座標とRGBカメラのピクセルあたりのバイト数を取得して乗算し、RGBカメラのY座標とRGBカメラの1ラインあたりのバイト数を表すcolorStrideを乗算して、これらを加算した値を格納しておきます。これは、
colorPixelIndex = (colorPoint.X * 4) + (colorPoint.Y * 2560)
としても同じです。

プレイヤーが存在し、プレイヤーの深度データが必要な場合は、
depth = myDepthPixelData(depthPixelIndex) >> DepthImageFrame.PlayerIndexBitmaskWidth
と記述して、myDepthPixelData(depthPixelIndex) をDepthImageFrame.PlayerIndexBitmaskWidth 分右へシフトします。
PlayerIndexBitmaskWidthフィールドは、プレイヤーインデックスビットマスク内の、幅またはビットの数を表します。これはプレイヤーインデックスを格納する深度データの下位ビットの数を表します。つまり、3ビット(DepthImageFrame.PlayerIndexBitmaskWidth分)右へシフトするとプレイヤーの深度のデータだけを取得できます。上記は、ビットシフト演算を使用しています。ビットシフトはその名の通り、ビット列をそのまま左右に移動させる演算です。

  • expression1 >> expression2
  • expression1

>> は expression1 を expression2 だけ右にシフトします
expression1 と expression2 には、それぞれ式を指定します

ビットごとの右シフト演算子(>>)やビットごとの左シフト演算子(>>)については、下記のURLを参照してください。
(ビットごとの右シフト演算子 (>>))
(ビットごとの左シフト演算子 (

変数depthXを1ずつ加算し、ImageIndex変数にRGBカメラのピクセルあたりのバイト数を加算していきます。

ImageIndex = ImageIndex + colorFrame.BytesPerPixel は
ImageIndex = ImageIndex + 4 としても同じです。

BytesPerPixelは1ピクセルあたりのバイト数を取得します。1ピクセルは8ビットでフルカラーの場合は32ビットになるため、32÷8=4バイトとなり、colorFrame.BytesPerPixelは4と同じになります。

この値が、Byte型の配列変数(bytePlayer1、bytePlayer2、byteRoom)のインデックスに対応します。下位3ビットのプレイヤーIDを指し示す値です。ImageIndexに対応する、Byte型の配列変数である距離データを画像化していきます。

深度に応じて書き込み先を変更していきます。プレイヤーが存在し、深度のデータが2000(2m)より大きい場合は、プレイヤーを奥側へ描画します。深度のデータが2000(2m)より小さかった場合は、手前に描画します。プレイヤー以外は背景イメージに描画します。このサンプルでは、personImageというNameのImageコントロールを基準に、personImageより後ろに、または前に表示されることになります。

  • bytePlayer1(ImageIndex + 3) = &HFF
  • bytePlayer2(ImageIndex + 3) = &HFF
  • byteRoom(ImageIndex + 3) = &HFF

上記3つのコードはAlphaを指定しているコードです。PixelFormats.Bgra32と指定していましたので、32ビット中、残りの8ビットをAlphaに使用して背景を透明化しています。

WriteableBitmap型の変数Human1_bitmapにWritePixelsメソッドで、byteからビットマップへ書き出します。ビットマップの指定した領域内のピクセルを更新します。書式は下記の通りです。このHuman1_bitmapプロパティの値を、Nameがhuman_image1という名前のImageコントロールのSourceプロパティにバインドします。

WritePixels(更新するWriteableBitmapの四角形,ビットマップの更新に使用するピクセル配列,pixel内の更新領域のストライド,入力バッファのオフセット)

同様に、Human2_bitmap、Room_Bitmapに関してもWritePixelsメソッドを適用します。

01Private Sub RenderScreen(colorFrame As ColorImageFrame, depthFrame As DepthImageFrame)
02  If Kinect Is Nothing OrElse depthFrame Is Nothing OrElse colorFrame Is Nothing Then
03    Return
04  End If
05 
06  Dim depth As Integer = 0
07  Dim depthPixelIndex As Integer
08  Dim playerIndex As Integer
09  Dim colorPixelIndex As Integer
10 
11  Dim colorPoint As ColorImagePoint
12  Dim colorStride As Integer = colorFrame.BytesPerPixel * colorFrame.Width
13 
14  Dim screenImageStride As Integer = colorFrame.BytesPerPixel * depthFrame.Width
15 
16  Dim ImageIndex As Integer = 0
17  Dim bytePlayer2 As Byte() = New Byte(depthFrame.Height * screenImageStride - 1) {}
18  Dim bytePlayer1 As Byte() = New Byte(depthFrame.Height * screenImageStride - 1) {}
19  Dim byteRoom As Byte() = New Byte(depthFrame.Height * screenImageStride - 1) {}
20 
21  depthFrame.CopyPixelDataTo(myDepthPixelData)
22  colorFrame.CopyPixelDataTo(myColorPixelData)
23 
24  For depthY As Integer = 0 To depthFrame.Height - 1
25    Dim depthX As Integer = 0
26    While depthX < depthFrame.Width
27      depthPixelIndex = depthX + (depthY * depthFrame.Width)
28      playerIndex = myDepthPixelData(depthPixelIndex) And DepthImageFrame.PlayerIndexBitmask
29      colorPoint = Kinect.MapDepthToColorImagePoint(depthFrame.Format, depthX, depthY, myDepthPixelData(depthPixelIndex), colorFrame.Format)
30      colorPixelIndex = (colorPoint.X * colorFrame.BytesPerPixel) + (colorPoint.Y * colorStride)
31 
32      If playerIndex <> 0 Then
33        depth = myDepthPixelData(depthPixelIndex) >> DepthImageFrame.PlayerIndexBitmaskWidth
34        If depth > targetDepth Then
35          bytePlayer1(ImageIndex) = myColorPixelData(colorPixelIndex)
36          'Blue
37          bytePlayer1(ImageIndex + 1) = myColorPixelData(colorPixelIndex + 1)
38          'Green
39          bytePlayer1(ImageIndex + 2) = myColorPixelData(colorPixelIndex + 2)
40          'Red
41          'Alpha
42          bytePlayer1(ImageIndex + 3) = &HFF
43        Else
44          bytePlayer2(ImageIndex) = myColorPixelData(colorPixelIndex)
45          'Blue
46          bytePlayer2(ImageIndex + 1) = myColorPixelData(colorPixelIndex + 1)
47          'Green
48          bytePlayer2(ImageIndex + 2) = myColorPixelData(colorPixelIndex + 2)
49          'Red
50          'Alpha
51          bytePlayer2(ImageIndex + 3) = &HFF
52        End If
53      Else
54        byteRoom(ImageIndex) = myColorPixelData(colorPixelIndex)
55        'Blue
56        byteRoom(ImageIndex + 1) = myColorPixelData(colorPixelIndex + 1)
57        'Green
58        byteRoom(ImageIndex + 2) = myColorPixelData(colorPixelIndex + 2)
59        'Red
60        'Alpha
61        byteRoom(ImageIndex + 3) = &HFF
62      End If
63      depthX = depthX + 1
64      ImageIndex = ImageIndex + colorFrame.BytesPerPixel
65    End While
66  Next
67  Human1_bitmap.WritePixels(myScreenImageRect, bytePlayer1, screenImageStride, 0)
68  Human2_bitmap.WritePixels(myScreenImageRect, bytePlayer2, screenImageStride, 0)
69  Room_Bitmap.WritePixels(myScreenImageRect, byteRoom, screenImageStride, 0)
70End Sub

ウィンドウが閉じられる時の処理

Kinectセンサーが動作している場合は、動作を停止し、音声認識も停止します。最後にKinectセンサーのリソースを解放します。

01  Private Sub MainWindow_Closing(sender As Object, e As System.ComponentModel.CancelEventArgs) Handles Me.Closing
02    If Kinect Is Nothing = False Then
03      If Kinect.IsRunning = True Then
04        Kinect.Stop()
05        engine.RecognizeAsyncStop()
06        Kinect.Dispose()
07      End If
08    End If
09  End Sub
10End Class

以上で今回のサンプルは終了です。自分の分身が表示される様子はとても楽しいので、ぜひ試してみてください。

  • 自分の分身を手のひらに乗せるKinectサンプル

薬師寺国安事務所

薬師寺国安事務所代表。Visual Basic プログラミングと、マイクロソフト系の技術をテーマとした、書籍や記事の執筆を行う。
1950年生まれ。事務系のサラリーマンだった40歳から趣味でプログラミングを始め、1996年より独学でActiveXに取り組む。1997年に薬師寺聖とコラボレーション・ユニット PROJECT KySS を結成。2003年よりフリーになり、PROJECT KySS の活動に本格的に参加、.NETやRIAに関する書籍や記事を多数執筆する傍ら、受託案件のプログラミングも手掛ける。Windows Phoneアプリ開発を経て、現在はWindows ストア アプリを多数公開中

Microsoft MVP for Development Platforms - Client App Dev (Oct 2003-Sep 2012)。Microsoft MVP for Development Platforms - Windows Phone Development(Oct 2012-Sep 2013)。Microsoft MVP for Development Platforms - Client Development(Oct 2013-Sep 2014)。Microsoft MVP for Development Platforms-Windows Platform Development (Oct 2014-Sep 2015)。

連載バックナンバー

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています