これであなたもダンスグループの一員!?Kinectで自分を分身させるプログラムを作る

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

ロジックコードを記述する

リスト2 (MainWindow.xaml.vb)

Option Strict On
Imports Microsoft.Kinect

RingBufferLib.dllに含まれる、RingBufferLib.Kinect_RingBuffer名前空間をインポートします。リングバッファを作成し、RGB、距離カメラ、プレイヤーインデックス配列のコピー等を行う機能を提供します。

Imports RingBufferLib.Kinect_RingBuffer

Class MainWindow

Bgr32形式は1ピクセルあたりのビット数が32bitのRGB形式で、先頭から1バイト(8bit)ずつに青、緑、赤、の情報が入っています。各カラーチャネルに割り当てられるbits per pixel(BBP)が8であるため、Bgr32を8で除算した4バイトの値をメンバ変数BrgPixelに格納しておきます。
青、緑、赤では24ビットしか使用されません、残りの8ビットはAlphaに使用されることが多いです。このサンプルでもAlphaに使用しています。また32ビットを8で除算した4(バイト)を直接指定しても問題ありません。

  Dim BytesPerPixel As Integer = CInt(PixelFormats.Bgr32.BitsPerPixel / 8) '4

定数メンバ変数myPlayerを7で初期化しておきます。これは分身の数になります。

  Const myPlayer As Integer = 7

1個のKinectセンサーを表すメンバ変数Kinectを宣言します。

  Dim Kinect As KinectSensor

Short型の配列メンバ変数myDepthPixelDataとByte型の配列メンバ変数myColorPixelDataを宣言しておきます。
※深度情報は、1ピクセルあたり2バイトのshort型。画像情報はフルカラーなので1ピクセルあたり4バイトのbyte型です。

  Dim myDepthPixelData As Short()
  Dim myColorPixelData As Byte()

3秒分のバッファ領域を確保します。

  Const bufferSecond As Integer = 3
  Dim screenImageStride As Integer

整数四角形の幅、高さ、および位置を表すInt32Rect構造体のメンバ変数myImageSizeを宣言します。領域の指定に使用されます。

Dim myImageSize As Int32Rect

リングバッファの各機能を提供するRingBuffer構造体のメンバ変数myRingBufferを宣言します。

Dim myRingBuffer As RingBuffer

WriteableBitmapクラス型のメンバ変数overrayBitmapメンバ変数を宣言します。

  Dim overrayBitmap As WriteableBitmap

整数四角形の幅、高さ、および位置を表すInt32Rect構造体のメンバ変数myScreenImageRectを宣言します。領域の指定に使用されます。

  Dim myScreenImageRect As Int32Rect

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

Kinectセンサーを初期化するinit_kinectプロシージャを実行します。

  Private Sub MainWindow_Loaded(sender As Object, e As System.Windows.RoutedEventArgs) Handles Me.Loaded
    init_kinect()
  End Sub

Kinectセンサーを初期化する処理

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

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

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

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

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

ここは、overrayBitmap = 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 のインスタンスの幅」には、overrayBitmapオブジェクトの幅を指定し、「Int32Rect のインスタンスの高さ」には、overrayBitmapオブジェクトの高さを指定します。結局は、
myScreenImageRect = New Int32Rect(0, 0, 320, 240)
と指定しても同じことです。

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

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

次に、AddHandlerステートメントで、RGBカメラ、距離カメラ、スケルトンフレームが更新された時に発生する、AllFramesReadyイベントにkinect_AllFramesReadyイベントハンドラを追加します。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)      
      Kinect.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30)
      Kinect.SkeletonStream.Enable()
 
      Dim myDepthStream = Kinect.DepthStream
      myDepthPixelData = New Short(Kinect.DepthStream.FramePixelDataLength - 1) {}
      myColorPixelData = New Byte(Kinect.ColorStream.FramePixelDataLength - 1) {}
 
      overrayBitmap = New WriteableBitmap(myDepthStream.FrameWidth, myDepthStream.FrameHeight, 96, 96, PixelFormats.Bgra32, Nothing)
      myScreenImageRect = New Int32Rect(0, 0, CInt(Math.Ceiling(overrayBitmap.Width)), CInt(Math.Ceiling(overrayBitmap.Height)))
 
      myRingBuffer.init_ringbuffer(bufferSecond, Kinect.ColorStream.FramePixelDataLength, Kinect.DepthStream.FramePixelDataLength)
      AddHandler Kinect.AllFramesReady, AddressOf kinect_AllFramesReady
      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

Kinectセンサーの動作を停止する処理

Kinectセンサーが動作している場合は、イベントを削除し、Kinectセンサーを停止します。最後にKinectセンサーのリソースを解放します。

  Public Sub StopKinect()
    If Kinect Is Nothing = False Then
      If Kinect.IsRunning = True Then
        RemoveHandler Kinect.AllFramesReady, AddressOf kinect_AllFramesReady
        Kinect.Stop()
        Kinect.Dispose()
      End If
    End If
  End Sub

RGBカメラ、距離カメラ、スケルトンのフレームが更新された場合に発生するイベント

e.OpenColorImageFrameメソッドで、新しいフレームのRGBカメラの情報を取得し、e.OpenDepthImageFrameメソッドで、新しいフレームの距離カメラの情報を取得し、e.OpenSkeletonFrameメソッドで、新しいフレームのスケルトンの情報を取得します。
取得した、これらのフレーム情報を引数にSaveBufferプロシージャを実行します。

  Private Sub kinect_AllFramesReady(sender As Object, e As AllFramesReadyEventArgs)
    Using colorFrame As ColorImageFrame = e.OpenColorImageFrame()
      Using depthFrame As DepthImageFrame = e.OpenDepthImageFrame()
        Using skeletonFrame As SkeletonFrame = e.OpenSkeletonFrame()
          SaveBuffer(colorFrame, depthFrame, skeletonFrame)
        End Using
      End Using
    End Using
  End Sub

Kinectの画像をバッファへ保存する処理

RGBカメラの処理を行うクラスのインスタンスを取得します。同様に距離カメラの処理を行うクラスのインスタンスを取得します。メンバ変数screenImageStrideに、BytesPerPixel(4)を深度ストリームのフレーム幅に乗算した値を格納します。また、colorStride変数にはBytesPerPixel(4)にRGBフレーム幅を乗算した値を格納します。

これはscreenImageStride=4×320(1280)またはcolorStride=4×640(2560)と同じです。

CopyPixelDataToメソッドで、距離カメラのフレームのピクセルのデータを取得します。同様にRGBカメラのフレームのピクセルのデータも取得します。depthFrame.CopyPixelDataToメソッドで取得できる距離データはshort型の16ビットの値です。この16ビットのデータは、上位13ビットが距離データ、下位3ビットがプレイヤーIDで構成されていますので、ここからピクセルごとに距離データを画像データとして作成していきます。

※深度情報は、1ピクセルあたり2バイト(short)。画像情報はフルカラーなので1ピクセルあたり4バイト(byte)が必要です。

RGBデータのピクセル座標を表すColorImagePoint型のcolorPoint配列変数を宣言し、距離カメラのピクセルデータのバイト長分の配列で初期化します。Short型の配列変数depthPixelを宣言し、距離カメラのピクセルデータのバイト長分の配列で初期化します。

MapDepthFrameToColorFrameで、距離カメラのX,Y座標に対応する、RGBカメラのX,Y座標を取得します。これは、深度情報を実画像に変換してくれるメソッドです。書式は下記の通りです。

KinectSensor.MapDepthFrameToColorFrame(距離カメラのフォーマット,距離カメラのピクセルデータ,RGBカメラのフォーマット,距離カメラの座標に対応するRGBカメラの座標)

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

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

次にDouble型の配列変数depthを宣言し、距離カメラのフレームの高さとscreenImageStride-1を乗算した値(307200-1)で初期化します。また、Integer型の配列変数playerIndexArrayを宣言し、距離カメラのフレームの高さとscreenImageStride-1を乗算した値(307200-1)で初期化します。

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

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

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

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

ColorImagePointの配列変数colorPointのXとY座標を取得します。変数colorPixelIndexに、RGBカメラのX座標とRGBカメラのピクセルあたりのバイト数を取得して乗算し、RGBカメラのY座標とRGBカメラの1ラインあたりのバイト数を表すcolorStrideを乗算して、これらを加算した値を格納しておきます。

プレイヤーが存在し、プレイヤーの深度データが必要な場合は、

depth(ImageIndex) = myDepthPixelData(depthPixelIndex) >> DepthImageFrame.PlayerIndexBitmaskWidth

と記述して、myDepthPixelData(depthPixelIndex) をDepthImageFrame.PlayerIndexBitmaskWidth 分、右へシフトします。つまり、3ビット(DepthImageFrame.PlayerIndexBitmaskWidth分)右へシフトするとプレイヤーの深度のデータだけを取得できます。
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型の配列変数(bytePlayer)のインデックスに対応します。下位3ビットのプレイヤーIDを指し示す値です。

距離カメラのフレームの高さとscreenImageStride-1を乗算した値(307200-1)で初期化された、Integer型の配列変数playerIndexArrayで変数ImageIndexに該当する位置に、プレイヤーのインデックス値を代入します。

ImageIndexに対応する、Byte型の配列変数である距離データを画像化していきます。

次にプレイヤーの情報をリングバッファに保存します。RingBuffer構造体のsave_framedataメソッドでリングバッファにプレイヤーのフレームデータを保存します。save_playerIndexdataメソッドで、リングバッファにプレイヤーのインデックス値の格納されたplayerIndexArrayの値を保存します。set_nextframeメソッドで次のリングバッファ内のフレームに移ります。Kinectの画像をビットマップデータに書き出すRenderScreen2プロシージャを実行します。

    Private Sub SaveBuffer(colorFrame As ColorImageFrame, depthFrame As DepthImageFrame, skeletonFrame As SkeletonFrame)
    If Kinect Is Nothing OrElse depthFrame Is Nothing OrElse colorFrame Is Nothing OrElse skeletonFrame Is Nothing Then
      Return
    End If
 
    Dim myColorStream As ColorImageStream = Kinect.ColorStream
    Dim myDepthStream As DepthImageStream = Kinect.DepthStream
    screenImageStride = colorFrame.BytesPerPixel * depthFrame.Width
    Dim colorStride As Integer = colorFrame.BytesPerPixel * colorFrame.Width
  
    Dim ImageIndex As Integer = 0
 
    depthFrame.CopyPixelDataTo(myDepthPixelData)
    colorFrame.CopyPixelDataTo(myColorPixelData)
 
    Dim colorPoint As ColorImagePoint() = New ColorImagePoint(depthFrame.PixelDataLength - 1) {}
    Dim depthPixel As Short() = New Short(depthFrame.PixelDataLength - 1) {}
    Kinect.MapDepthFrameToColorFrame(depthFrame.Format, depthPixel, colorFrame.Format, colorPoint)
 
    Dim byteRoom As Byte() = New Byte(depthFrame.Height * screenImageStride - 1) {}
    Dim bytePlayer As Byte() = New Byte(depthFrame.Height * screenImageStride - 1) {}

    Dim depth As Double() = New Double(depthFrame.Height * screenImageStride - 1) {}
    Dim playerIndexArray As Integer() = New Integer(depthFrame.Height * screenImageStride - 1) {}

 
    For depthY As Integer = 0 To depthFrame.Height - 1
      Dim depthX As Integer = 0
      While depthX < depthFrame.Width
        Dim depthPixelIndex As Integer = depthX + (depthY * depthFrame.Width)
        Dim playerIndex As Integer = myDepthPixelData(depthPixelIndex) And DepthImageFrame.PlayerIndexBitmask
        Dim x As Integer = colorPoint(depthPixelIndex).X
        Dim y As Integer = colorPoint(depthPixelIndex).Y
        Dim colorPixelIndex As Integer = (x * colorFrame.BytesPerPixel) + (y * colorStride)
 
        If playerIndex <> 0 Then
          depth(ImageIndex) = myDepthPixelData(depthPixelIndex) >> DepthImageFrame.PlayerIndexBitmaskWidth
          playerIndexArray(ImageIndex) = playerIndex
          bytePlayer(ImageIndex) = myColorPixelData(colorPixelIndex)
          'Blue 
          bytePlayer(ImageIndex + 1) = myColorPixelData(colorPixelIndex + 1)
          'Green
          bytePlayer(ImageIndex + 2) = myColorPixelData(colorPixelIndex + 2)
          'Red
          bytePlayer(ImageIndex + 3) = &HFF
          'Alpha
        End If
        depthX = depthX + 1
        ImageIndex = ImageIndex + colorFrame.BytesPerPixel
      End While
    Next
    myRingBuffer.save_framedata(bytePlayer)
    myRingBuffer.save_playerIndexdata(playerIndexArray)
    myRingBuffer.set_nextframe()
    RenderScreen2()
  
  End Sub
  • ダンスグループの一員になれる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メルマガ会員のサービス内容を見る

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