Kinectの音声認識を使って、プレイヤーを分離、結合させるデモを試してみる

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

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

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

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

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

Byte型の配列変数bytePlayer2変数を、距離カメラのフレームの高さと、Kinectセンサーの距離カメラのストリームフレーム幅と、RGBカメラのピクセルあたりのバイト数を取得する、BytesPerPixelを乗算して取得した距離カメラの1ラインあたりのストリームバイト数を保持しているscreenImageStrideで乗算した値で初期化します。
つまり、depthFrame.Heightの320に、screenImageStrideに設定した、depthFrame.Width * colorFrame.BytesPerPixel(320×4(バイト))の値である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プロパティの値を、room_imageというNameのImageコントロールのSourceプロパティにバインドします。これで背景イメージが表示されます。

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

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 depthPixelIndex As Integer
07  Dim playerIndex As Integer
08  Dim colorPixelIndex As Integer
09 
10  Dim colorPoint As ColorImagePoint
11  Dim colorStride As Integer = colorFrame.Width * colorFrame.BytesPerPixel
12  screenImageStride = depthFrame.Width * colorFrame.BytesPerPixel
13 
14  Dim ImageIndex As Integer = 0
15  Dim bytePlayer2 As Byte() = New Byte(depthFrame.Height * screenImageStride - 1) {}
16  Dim bytePlayer1 As Byte() = New Byte(depthFrame.Height * screenImageStride - 1) {}
17  Dim byteRoom As Byte() = New Byte(depthFrame.Height * screenImageStride - 1) {}
18  
19  depthFrame.CopyPixelDataTo(myDepthPixelData)
20  colorFrame.CopyPixelDataTo(myColorPixelData)
21 
22  For depthY As Integer = 0 To depthFrame.Height - 1
23    Dim depthX As Integer = 0
24    While depthX < depthFrame.Width
25      depthPixelIndex = depthX + (depthY * depthFrame.Width)
26      playerIndex = myDepthPixelData(depthPixelIndex) And DepthImageFrame.PlayerIndexBitmask
27 
28      colorPoint = Kinect.MapDepthToColorImagePoint(depthFrame.Format, depthX, depthY, myDepthPixelData(depthPixelIndex), colorFrame.Format)
29      colorPixelIndex = (colorPoint.X * colorFrame.BytesPerPixel) + (colorPoint.Y * colorStride)
30 
31      If playerIndex = 0 Then
32        byteRoom(ImageIndex) = myColorPixelData(colorPixelIndex)
33        byteRoom(ImageIndex + 1) = myColorPixelData(colorPixelIndex + 1)
34        byteRoom(ImageIndex + 2) = myColorPixelData(colorPixelIndex + 2)
35        byteRoom(ImageIndex + 3) = &HFF
36      End If
37      depthX = depthX + 1
38      ImageIndex = ImageIndex + colorFrame.BytesPerPixel
39    End While
40  Next
41  Room_Bitmap.WritePixels(myScreenImageRect2, byteRoom, screenImageStride, 0)
42End Sub

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

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

これは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型の配列変数bytePlayer変数を、距離カメラのフレームの高さと、Kinectセンサーの距離カメラのストリームフレーム幅と、RGBカメラのピクセルあたりのバイト数を取得する、BytesPerPixelを乗算して取得した距離カメラの1ラインあたりのストリームバイト数を保持しているscreenImageStrideで乗算した値で初期化します。つまり、depthFrame.Heightの320に、screenImageStrideに設定した、depthFrame.Width * colorFrame.BytesPerPixel(320×4(バイト))の値である1280を乗算した(320×1280)409600-1の値で初期化されたバイト配列型のbytePlayerオブジェクトが作成されます。

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

距離カメラのフレームの高さとscreenImageStride-1を乗算した値(409600-1)で初期化された、Integer型の配列変数playerIndexArrayを作成します。

繰り返し変数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 分右へシフトします。
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型の配列変数(bytePlayer)のインデックスに対応します。下位3ビットのプレイヤーIDを指し示す値です。ImageIndexに対応する、Byte型の配列変数である距離データを画像化していきます。

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

01Private Sub SaveBuffer(colorFrame As ColorImageFrame, depthFrame As DepthImageFrame, skeletonFrame As SkeletonFrame)
02  If Kinect Is Nothing OrElse depthFrame Is Nothing OrElse colorFrame Is Nothing OrElse skeletonFrame Is Nothing Then
03    Return
04  End If
05 
06  Dim colorStream As ColorImageStream = Kinect.ColorStream
07  Dim depthStream As DepthImageStream = Kinect.DepthStream
08 
09  Dim colorStride As Integer = colorFrame.BytesPerPixel * colorFrame.Width
10  Dim ImageIndex As Integer = 0
11  depthFrame.CopyPixelDataTo(myDepthPixelData)
12  colorFrame.CopyPixelDataTo(myColorPixelData)
13 
14  Dim colorPoint As ColorImagePoint() = New ColorImagePoint(depthFrame.PixelDataLength - 1) {}
15  Dim depthPixel As Short() = New Short(depthFrame.PixelDataLength - 1) {}
16  Kinect.MapDepthFrameToColorFrame(depthFrame.Format, depthPixel, colorFrame.Format, colorPoint)
17 
18  Dim bytePlayer As Byte() = New Byte(depthFrame.Height * screenImageStride - 1) {}
19  Dim depth As Double() = New Double(depthFrame.Height * screenImageStride - 1) {}
20  Dim playerIndexArray As Integer() = New Integer(depthFrame.Height * screenImageStride - 1) {}
21 
22  For depthY As Integer = 0 To depthFrame.Height - 1
23    Dim depthX As Integer = 0
24    While depthX < depthFrame.Width
25      'ImageIndex += colorFrame.BytesPerPixel;
26      Dim depthPixelIndex As Integer = depthX + (depthY * depthFrame.Width)
27 
28      Dim playerIndex As Integer = myDepthPixelData(depthPixelIndex) And DepthImageFrame.PlayerIndexBitmask
29      Dim x As Integer = colorPoint(depthPixelIndex).X
30      Dim y As Integer = colorPoint(depthPixelIndex).Y
31      Dim colorPixelIndex As Integer = (x * colorFrame.BytesPerPixel) + (y * colorStride)
32 
33      If playerIndex <> 0 Then
34          depth(ImageIndex) = myDepthPixelData(depthPixelIndex) >> DepthImageFrame.PlayerIndexBitmaskWidth
35        playerIndexArray(ImageIndex) = playerIndex
36 
37        bytePlayer(ImageIndex) = myColorPixelData(colorPixelIndex)
38        bytePlayer(ImageIndex + 1) = myColorPixelData(colorPixelIndex + 1)
39        bytePlayer(ImageIndex + 2) = myColorPixelData(colorPixelIndex + 2)
40        bytePlayer(ImageIndex + 3) = &HFF
41 
42      End If
43      depthX = depthX + 1
44      ImageIndex = ImageIndex + colorFrame.BytesPerPixel
45    End While
46  Next
47  myRingBuffer.save_framedata(bytePlayer)
48  myRingBuffer.save_playerIndexdata(playerIndexArray)
49  myRingBuffer.set_nextframe()
50  RenderScreen2()
51End Sub

Kinectの画像をビットマップデータに書き出す処理(実写背景画像上にプレイヤーを表示する処理)

プレイヤーの分身の数を変数maxPlayerに格納しておきます。

Byte型の配列変数bytePlayerをInt32Rect構造体の高さにscreenImageStrideで乗算した値で初期化します。640*1280=819200-1の値で初期化するのと同じです。

各分身は、バッファに保存してある0.5秒前、1秒前のデータをそれぞれが参照することで、時差表示を可能にしています。1秒で30フレームなので、0.5秒の場合はその半分の15フレームとなり、変数frameを15で初期化しておきます。

繰り返し変数iで0からプレイヤーの分身分(変数iが0から始まっているため-1した値)、反復処理を行います。整数型変数iframeを宣言し、反復変数iが0の場合は、iframe変数にRingBuffer構造体のbuffer_indexプロパティの値から-1した値を指定します。-1しておかないと白くくりぬかれた余分なプレイヤーが表示されます。
このbuffer_indexプロパティ値は次のリングバッファ内のフレームに移動するたびに加算される値で、リングバッファ内のインデックス値になります。iが0以外の場合で、RingBuffer構造体のbuffer_indexプロパティ値に、15フレームにiの値を乗算した値が加算され格納されます。

Byte型のcolorPixelData配列変数に、RingBuffer構造体のget_rgb_frame関数を使って、iframeに該当するリングバッファ内のRGBカメラのデータ値を取得します。整数型の配列変数PlayerInexに、RingBuffer構造体のget_PlayerIndexData関数を使って、iframeに該当するリングバッファ内のプレイヤーインデックス値を取得します。変数indexの値が、colorPixelData変数の値の長さより小さい場合は以下の処理を繰り返します。

4バイトずつ加算される変数indexに該当するPlayerIndexの値が0より大きい場合、つまりプレイヤーが存在する場合は、プレイヤーを描画します。indexに対応する、Byte型の配列変数である距離データを画像化していきます。

Kinectの画像をビットマップデータに書き出します。WriteableBitmap型の変数myOverrayBitmapにWritePixelsメソッドで、byteからビットマップへ書き出します。ビットマップの指定した領域内に更新したデータを格納します。書式は下記の通りです。このmyOverrayBitmapプロパティの値を、personImageというNameのImageコントロールのSourceプロパティに指定します。実写の背景画像上にプレイヤーが表示されます。

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

01Private Sub RenderScreen2()
02  If screenImageStride = 0 Then
03    Return
04  End If
05  maxPlayer = speechNo
06  Dim bytePlayer As Byte() = New Byte(CInt(myImageSize.Height) * screenImageStride - 1) {}
07  Dim frame As Integer = 15
08  For i As Integer = 0 To maxPlayer - 1
09    Dim iframe As Integer
10 
11    If i = 0 Then
12      iframe = myRingBuffer.buffer_index - 1  '1人余分な人物が表示されるので-1をする
13    Else
14      iframe = myRingBuffer.buffer_index + (frame * i)
15    End If
16 
17    Dim colorPixelData = myRingBuffer.get_rgb_frame(iframe)
18    Dim PlayerIndex = myRingBuffer.get_PlayerIndexData(iframe)
19    If colorPixelData Is Nothing Then
20      Continue For
21    End If
22 
23    Dim index As Integer = 0
24    While index < colorPixelData.Length
25      If PlayerIndex(index) > 0 Then
26        bytePlayer(index) = colorPixelData(index)
27        bytePlayer(index + 1) = colorPixelData(index + 1)
28        bytePlayer(index + 2) = colorPixelData(index + 2)
29        bytePlayer(index + 3) = &HFF
30      End If
31      index = index + BytesPerPixel
32    End While
33  Next
34  myOverrayBitmap.WritePixels(myScreenImageRect, bytePlayer, screenImageStride, 0)
35  personImage.Source = myOverrayBitmap
36End Sub

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

Kinectセンサーが動作している場合は、音声認識を停止し、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        engine.RecognizeAsyncStop()
05        Kinect.Stop()
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メルマガ会員のサービス内容を見る

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