Kinectで結成したマイ・ダンスチームを、サンプルを見ながら実際の背景に合成してみよう

2012年9月11日(火)
薬師寺 国安

前ページからの続きです。

ウィンドウが読み込まれた時の処理

ピクセル深度の値を2000(2m)で初期化しておきます。よって、Kinectセンサーからの距離を2mに保ってください。Kinectセンサーを取得し、ビットマップデータを初期化し、プレイヤーの画層を初期化するinit_kinectプロシージャを実行します。

構成ツリーのオブジェクトがレンダリングされる直前に発生する、CompositionTarget.Renderingイベントにイベントハンドラを指定します。イベントハンドラ内では以下の処理を行います。

OpenNextFrame(100)メソッドで、KinectからRGBデータの次のフレームを開きます。次のフレームがなかった場合のタイムアウトを100ミリセコンドと指定しています。同様に、Kinectから深度データの次のフレームを開きます。次のフレームがなかった場合のタイムアウトを100ミリセコンドと指定しています。スケルトンフレームに対しても同様の処理を行います。
背景を描画し距離データを取得するRenderScreenプロシージャを実行します。引数として、Kinectのストリーミング用RGBデータのバッファと深度データのバッファを含んでいる、colorFrameとdepthFrameを渡しています。RGBデータのバッファ、深度データのバッファ、スケルトンデータのバッファを引数に、SaveBufferプロシージャを実行します。

DataContextプロパティにMainWindow自身のインスタンスを指定します。この処理を行わないとプレイヤーが表示されませんので注意してください。

  Private Sub MainWindow_Loaded(sender As Object, e As System.Windows.RoutedEventArgs) Handles Me.Loaded
    targetDepth = 2000
  
      init_kinect()
      AddHandler CompositionTarget.Rendering, Sub(renderSender As Object, renderArgs As EventArgs)
        Using colorFrame As ColorImageFrame = Kinect.ColorStream.OpenNextFrame(100)
          Using depthFrame As DepthImageFrame = Kinect.DepthStream.OpenNextFrame(100)
            Using mySkeletonFrame As SkeletonFrame = Kinect.SkeletonStream.OpenNextFrame(100)
              RenderScreen(colorFrame, depthFrame)
              SaveBuffer(colorFrame, depthFrame, mySkeletonFrame)
            End Using
          End Using
        End Using
              End Sub
    DataContext = Me
  End Sub

Kinectセンサーを取得し、ビットマップデータを初期化し、プレイヤーの画層を初期化する処理

Kinectセンサーを取得します。DepthStream.Enableメソッドで距離カメラの表示サイズを320×240、1秒あたり30フレームで、動作を開始します。表示サイズを640×480に設定すると描画が重くて動かなくなるので注意してください。
同様にColorStream.Enableメソッドで、RGBカメラを、RGBフォーマット、表示サイズ640×480、1秒あたり30フレームで、動作を開始します。プレイヤーおよびスケルトンの認識も開始します。

骨格生成を行う場合は、深度センサー(距離カメラ)を有効にする必要があります。Dim depthStream As DepthImageStream = Kinect.DepthStreamで、距離カメラの処理を行うクラスのインスタンスを取得します。
Short配列変数myDepthPixelDataに距離カメラのピクセルデータのバイト長分の配列を作成します。320×240の表示サイズの場合は、myDepthPixelData=New Short(76800-1){}分の配列を確保することになります。-1しているのは配列のインデックスは0から始まるためです。
バイト配列変数myColorPixelDataにRGBカメラのピクセルデータのバイト長分の配列を作成します。

WriteableBitmapクラス型のmyOverrayBitmapを下記の書式で初期化します。

New WriteableBitmap(距離カメラのフレーム幅,距離カメラのフレーム高さ, ビットマップの水平値, ビットマップの垂直値,ビットマップのピクセルフォーマット,ビットマップのビットマップパレット)

「ビットマップのピクセルフォーマット」に指定する、PixelFormats.Bgra32は、Bgra32 ピクセル形式を取得します。ビットマップのピクセル形式には、PixelFormats.Bgra32を指定します。Bgra32 は、bits per pixel(BPP)が 32 の sRGB 形式です。
各カラー チャネル(青、緑、赤、およびアルファ)に割り当てられるbits per pixel(BPP)は 8 です。ここに、Brg32を指定すると背景が透明化されず真っ黒になってしまいますので注意してください。
ここは、myOverrayBitmap = New WriteableBitmap(320 240, 96, 96, PixelFormats.Bgra32, Nothing)
と指定しても同じです。

IntRect32構造体のmyScreenImageRectを下記の書式で初期化し、領域を指定します。

New IntRect32(新しいInt32Rect のインスタンスの X座標,新しいInt32Rect のインスタンスの Y座標, 四角形の幅を指定した新しいInt32Rect のインスタンスの幅, 四角形の高さを指定した新しい Int32Rect のインスタンスの高さ)

「新しいInt32Rect のインスタンスの X座標」と「新しいInt32Rect のインスタンスの Y座標」には0を指定します。「四角形の幅を指定した新しい Int32Rect のインスタンスの幅」には、myOverrayBitmapオブジェクトの幅を指定し、「Int32Rect のインスタンスの高さ」には、myOverrayBitmapオブジェクトの高さを指定します。

結局は、
myScreenImageRect = New Int32Rect(0, 0, 320, 240)
と指定しても同じことです。

リングバッファを初期化します。RingBuffer構造体のinit_ringbuffer関数を使用します。書式は下記の通りです。

init_ringbuffer(秒数分のバッファ領域(この場合は3), RGBストリームの画素数, 深度ストリームの画素数) As Boolean

WriteableBitmapクラス型のRoom_Bitmapを初期化し、メンバ変数myScreenImageRect2をRoom_Bitmap.WidthとRoom_Bitmap.Heightで初期化します。

myScreenImageRect2 = New Int32Rect(0, 0, 320, 240)と同じです。

Kinectセンサーを動作します。

Int32Rect構造体のmyImageSizeのWidthプロパティにRGBストリームのフレームの幅(640)を指定します。同じくHeightプロパティにはRGBストリームのフレームの高さ(480)を指定します。

  Private Sub init_kinect()
    Try
      If KinectSensor.KinectSensors.Count = 0 Then
        MessageBox.Show("KINECTが接続されておりません。")
        Exit Sub
      Else
        Kinect = KinectSensor.KinectSensors(0)
      End If
      Kinect.DepthStream.Enable(DepthImageFormat.Resolution320x240Fps30) '640×480では重くて動かない。
      Kinect.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30)
      Kinect.SkeletonStream.Enable()
      
      Dim depthStream = Kinect.DepthStream
 
      myDepthPixelData = New Short(Kinect.DepthStream.FramePixelDataLength - 1) {}
      myColorPixelData = New Byte(Kinect.ColorStream.FramePixelDataLength - 1) {}
 
      myOverrayBitmap = New WriteableBitmap(depthStream.FrameWidth, depthStream.FrameHeight, 96, 96, PixelFormats.Bgra32, Nothing)
      myScreenImageRect = New Int32Rect(0, 0, CInt(Math.Ceiling(myOverrayBitmap.Width)), CInt(Math.Ceiling(myOverrayBitmap.Height)))
      myRingBuffer.init_ringbuffer(bufferSecond, Kinect.ColorStream.FramePixelDataLength, Kinect.DepthStream.FramePixelDataLength)
      Room_Bitmap = New WriteableBitmap(depthStream.FrameWidth, depthStream.FrameHeight, 96, 96, PixelFormats.Bgra32, Nothing)
      myScreenImageRect2 = New Int32Rect(0, 0, CInt(Math.Ceiling(Room_Bitmap.Width)), CInt(Math.Ceiling(Room_Bitmap.Height)))
 
      Kinect.Start()
      myImageSize.Width = Kinect.ColorStream.FrameWidth
      myImageSize.Height = Kinect.ColorStream.FrameHeight
    Catch ex As Exception
      MessageBox.Show(ex.Message)
      Exit Sub
    End Try
  End Sub

背景を描画し距離データを取得する処理

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

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

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

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

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

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

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

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を乗算して、これらを加算した値を格納しておきます。

プレイヤーが存在していない場合は、背景イメージを描画します。

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

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

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

この値が、Byte型の配列変数(byteRoom)のインデックスに対応します。ImageIndexに対応する、Byte型の配列変数である距離データを画像化していきます。

WriteableBitmap型の変数Room_BitmapにWritePixelsメソッドで、byteからビットマップへ書き出します。ビットマップの指定した領域内に更新したデータを格納します。書式は下記の通りです。このRoom_Bitmapプロパティの値を、ImageコントロールのSourceプロパティにバインドします。これで背景イメージが表示されます。

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

  Private Sub RenderScreen(colorFrame As ColorImageFrame, depthFrame As DepthImageFrame)
    If Kinect Is Nothing OrElse depthFrame Is Nothing OrElse colorFrame Is Nothing Then
      Return
    End If
 
    Dim depthPixelIndex As Integer
    Dim playerIndex As Integer
    Dim colorPixelIndex As Integer
    Dim colorPoint As ColorImagePoint
    Dim colorStride As Integer = colorFrame.BytesPerPixel * colorFrame.Width
    screenImageStride = colorFrame.BytesPerPixel * depthFrame.Width '1280
 
    Dim ImageIndex As Integer = 0
    Dim bytePlayer2 As Byte() = New Byte(depthFrame.Height * screenImageStride - 1) {}
    Dim bytePlayer1 As Byte() = New Byte(depthFrame.Height * screenImageStride - 1) {}
    Dim byteRoom As Byte() = New Byte(depthFrame.Height * screenImageStride - 1) {}
 
    depthFrame.CopyPixelDataTo(myDepthPixelData)
    colorFrame.CopyPixelDataTo(myColorPixelData)
 
    For depthY As Integer = 0 To depthFrame.Height - 1
      Dim depthX As Integer = 0
      While depthX < depthFrame.Width
        depthPixelIndex = depthX + (depthY * depthFrame.Width)
        playerIndex = myDepthPixelData(depthPixelIndex) And DepthImageFrame.PlayerIndexBitmask
        '人のID取得
        colorPoint = Kinect.MapDepthToColorImagePoint(depthFrame.Format, depthX, depthY, myDepthPixelData(depthPixelIndex), colorFrame.Format)
        colorPixelIndex = (colorPoint.X * colorFrame.BytesPerPixel) + (colorPoint.Y * colorStride)
 
        If playerIndex = 0 Then
          '人以外は背景イメージへ描画
          byteRoom(ImageIndex) = myColorPixelData(colorPixelIndex)
          'Blue 
          byteRoom(ImageIndex + 1) = myColorPixelData(colorPixelIndex + 1)
          'Green
          byteRoom(ImageIndex + 2) = myColorPixelData(colorPixelIndex + 2)
          'Red
          'Alpha
          byteRoom(ImageIndex + 3) = &HFF
          'Else
          '    Exit Sub
        End If
        depthX = depthX + 1
        ImageIndex = ImageIndex + colorFrame.BytesPerPixel
      End While
    Next
    Room_Bitmap.WritePixels(myScreenImageRect2, byteRoom, screenImageStride, 0)
  End Sub
  • マイダンスグループを実際の背景に合成するサンプル

薬師寺国安事務所

薬師寺国安事務所代表。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メルマガ会員のサービス内容を見る

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