Kinectを使って、顔の動きを認識して画面に表示する

2012年10月1日(月)
薬師寺 国安

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

RGBカメラ、距離カメラ、スケルトンのフレームが更新された時の処理

新しいフレームのRGBカメラの情報、新しいフレームの距離カメラの情報、新しいフレームのスケルトンの情報を取得します。距離カメラのイメージフォーマットが、距離カメラのイメージフレームのデータタイプ、解像度、フレームレートと同じでなかった場合は、距離カメラのイメージフォーマットを距離カメラのイメージフレームのフォーマットと同じにします。

同様に、RGBカメラのイメージフォーマットが、RGBカメラのイメージフレームのデータタイプ、解像度、フレームレートと同じでなかった場合は、RGBカメラのイメージフォーマットをRGBカメラのイメージフレームのフォーマットと同じにします。

距離カメラのフレームのピクセルデータを取得するためのShort配列を確保(Short配列の長さはmyDepthImageFrame.PixelDataLength- 1で取得)し、myDepthImageに格納します。

同様に、RGBカメラのフレームのピクセルデータを取得するためのバイト配列を確保(バイト配列の長さはmyColorImageFrame.PixelDataLength - 1で取得)し、myColorImageに格納します。

スケルトンのフレームの、スケルトン配列を確保(スケルトン配列の長さはmySkeletonFrame. SkeletonArrayLength - 1で取得)し、skeletonDataに格納します。

各フレームからCopyPixelDataToメソッドを呼び出し、ピクセルデータを取得します。CopyPixelDataToメソッドは、ピクセルデータの長さを使用して、事前に割り当てられた配列へ、ピクセルごとの深度データやRGBデータをコピーします。

追跡者と、現在のフレーム情報を持つ追跡者のリストを更新します。スケルトンの情報を持つskeletonData内をスケルトン型の変数mySkeletonで反復処理しながら以下の処理を行います。

スケルトンのトラッキング状態が、トラッキングされているか、ジョイントのトラッキングはしておらず、プレイヤー位置のみトラッキングしている(Nearモード時)場合の処理です。追跡されたスケルトンにトラッキングIDが含まれていた場合は、Addメソッドで、追跡されているスケルトンに、指定したキー(スケルトンID)と値(SkeletonFaceTrackerのインスタンス)をディクショナリに追加します。

SkeletonFaceTrackerの新しいインスタンスmySkeletonFaceTrackerを作成します。TryGetValueメソッドで、スケルトンのトラッキングIDに関連付けられている値があった場合は、RGBフォーマット、RGBイメージのバイト配列、距離カメラのフォーマット、深度イメージのShort配列、スケルトン、を引数にOnFrameReadyプロシージャを実行します。最後に追跡されたフレームにスケルトンフレームのフレームナンバーを格納します。

各追跡者に更新されたフレームを与えます。

要素の描画を無効にして、完全に新しいレイアウトパスを強制するInvalidateVisualメソッドを呼び出します。

  Private Sub OnAllFramesReady(sender As Object, e As AllFramesReadyEventArgs)
    Dim myColorImageFrame As ColorImageFrame = Nothing
    Dim myDepthImageFrame As DepthImageFrame = Nothing
    Dim mySkeletonFrame As SkeletonFrame = Nothing
 
    Try
      myColorImageFrame = e.OpenColorImageFrame()
      myDepthImageFrame = e.OpenDepthImageFrame()
      mySkeletonFrame = e.OpenSkeletonFrame()
 
      If myColorImageFrame Is Nothing OrElse myDepthImageFrame Is Nothing OrElse mySkeletonFrame Is Nothing Then
        Return
      End If
      If myDepthImageFormat <> myDepthImageFrame.Format Then
        myDepthImage = Nothing
        myDepthImageFormat = myDepthImageFrame.Format
      End If
 
      If myColorImageFormat <> myColorImageFrame.Format Then
        myColorImage = Nothing
        myColorImageFormat = myColorImageFrame.Format
      End If
 
      If myDepthImage Is Nothing Then
        myDepthImage = New Short(myDepthImageFrame.PixelDataLength - 1) {}
      End If
 
      If myColorImage Is Nothing Then
        myColorImage = New Byte(myColorImageFrame.PixelDataLength - 1) {}
      End If
 
      If skeletonData Is Nothing OrElse skeletonData.Length <> mySkeletonFrame.SkeletonArrayLength Then
        skeletonData = New Skeleton(mySkeletonFrame.SkeletonArrayLength - 1) {}
      End If
 
      myColorImageFrame.CopyPixelDataTo(myColorImage)
      myDepthImageFrame.CopyPixelDataTo(myDepthImage)
      mySkeletonFrame.CopySkeletonDataTo(skeletonData)
 
        For Each mySkeleton As Skeleton In skeletonData
          If mySkeleton.TrackingState = SkeletonTrackingState.Tracked OrElse mySkeleton.TrackingState = SkeletonTrackingState.PositionOnly Then
            If trackedSkeletons.ContainsKey(mySkeleton.TrackingId) = False Then
              trackedSkeletons.Add(mySkeleton.TrackingId, New SkeletonFaceTracker())
            End If
 
            Dim mySkeletonFaceTracker As New SkeletonFaceTracker
            If trackedSkeletons.TryGetValue(mySkeleton.TrackingId, mySkeletonFaceTracker) = True Then
              mySkeletonFaceTracker.OnFrameReady(Kinect, myColorImageFormat, myColorImage, myDepthImageFormat, myDepthImage, mySkeleton)
              mySkeletonFaceTracker.LastTrackedFrame = mySkeletonFrame.FrameNumber
            End If
          End If
        Next
 
        InvalidateVisual()
      Finally
        If myColorImageFrame Is Nothing = False Then
           myColorImageFrame.Dispose()
        End If
 
        If myDepthImageFrame Is Nothing = False Then
           myDepthImageFrame.Dispose()
        End If
 
        If mySkeletonFrame Is Nothing = False Then
           mySkeletonFrame.Dispose()
        End If
    End Try
  End Sub

Kinectセンサーのデータを受信した時の処理

古いセンサーが動いている場合は、RemoveHandlerステートメントで、イベントを解除します。新しいKinectセンサーが動いている場合は、AddHandlerステートメントで、RGBカメラ、距離カメラ、スケルトンのフレームが更新された時に発生するAllFramesReadyイベントにイベントハンドラを追加します。

  Private Sub OnSensorChanged(oldSensor As KinectSensor, newSensor As KinectSensor)
      If oldSensor Is Nothing = False Then
      RemoveHandler oldSensor.AllFramesReady, AddressOf OnAllFramesReady
    End If
 
  If newSensor Is Nothing = False Then
    AddHandler newSensor.AllFramesReady, AddressOf OnAllFramesReady
  End If 
 End Sub
End Class

次に、ソリューションエクスプローラー内のMainWindow.xamlを展開して表示される、MainWindow.xaml.vbをダブルクリックしてリスト3のコードを記述します。

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

リスト2 (MainWindow.xaml.vb)

Option Strict On
Imports Microsoft.Kinect
Imports Microsoft.Kinect.Toolkit

Class MainWindow

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

  Dim Bgr32BytesPerPixel As Integer = CInt((PixelFormats.Bgr32.BitsPerPixel) / 8)

KinectSensorChooserクラスの新しいインスタンスsensorChooserメンバ変数を宣言します。KinectSensorChooserクラスはアプリケーションからKinectセンサーを検出するクラスです。

  Dim sensorChooser As New KinectSensorChooser()

Kinectセンサーを表すメンバ変数Kinectを宣言しておきます。

  Dim Kinect As KinectSensor

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

FaceTrackingViewer1コントロールに、ユーザーコントロールのFaceTrackingViewer.xaml.vb内で定義したWPFプロパティシステムに登録される依存関係プロパティを表す、DependencyPropertyクラス型のKinectPropertyをバインドします。AddHandlerステートメントでKinectセンサーが検出された時に発生するKinectChangedイベントにイベントハンドラを追加します。KinectSensorChooserを開始します。

Public Sub MainWindow_Loaded(sender As Object, e As System.Windows.RoutedEventArgs) Handles Me.Loaded
    Dim faceTrackingViewerBinding = New Binding("Kinect") With {.Source = sensorChooser}
    FaceTrackingViewer1.SetBinding(FaceTrackingViewer.KinectProperty, faceTrackingViewerBinding)
    AddHandler sensorChooser.KinectChanged, AddressOf SensorChooserOnKinectChanged
    sensorChooser.Start()
  End Sub

Kinectセンサーが検出された時の処理

新しいKinectセンサーをメンバ変数Kinectに格納します。新しいKinectセンサーが検出された場合は、RGBと距離カメラを有効にします。Nearモードを有効にします。SkeletonTrackingMode.Seatedで、椅子に座った上半身の状態でもスケルトンの認識可能にします。スケルトンを有効にします。AddHandlerステートメントで、RGBカメラ、距離カメラ、スケルトンフレームが更新された時に発生するAllFramesReadyイベントにイベントハンドラを追加します。

  Private Sub SensorChooserOnKinectChanged(sender As Object, e As KinectChangedEventArgs)
      Kinect = e.NewSensor
    If Kinect Is Nothing = False Then
        Kinect.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30)
        Kinect.DepthStream.Enable(DepthImageFormat.Resolution320x240Fps30)
        Kinect.DepthStream.Range = DepthRange.Near  
        Kinect.SkeletonStream.EnableTrackingInNearRange = True
        Kinect.SkeletonStream.TrackingMode = SkeletonTrackingMode.Seated
        Kinect.SkeletonStream.Enable()
        AddHandler Kinect.AllFramesReady, AddressOf KinectSensorOnAllFramesReady
    End If
  End Sub

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

e.OpenColorImageFrameで、新しいフレームのRGBカメラの情報を取得し、e.OpenDepthImageFrameで、新しいフレームの距離カメラの情報を取得します。これらのメソッドで取得するcolorImagerFrameやdepthImageFrameは、Usingで括るか、明示的にDisposeする必要があります。Kinect センサーを取得します。

RGBカメラと距離カメラの両方に情報があった場合は、バイト配列型の変数getBackgroundMaskを宣言して、BackgroundMask関数を実行して戻り値を取得します。BackgroundMask関数には引数として、Kinect センサー、RGBカメラのフレーム情報、距離カメラのフレーム情報を渡します。

NameがcolorImageのImageコントロールのSourceプロパティに、ピクセルデータをビットマップに変換して指定します。

BitmapSource.Createメソッドの書式は下記の通りです。

BitmapSource.Create(RGBカメラで取得したフレーム幅,RGBカメラで取得したフレームの高さ,ビットマップの水平ドット(dpi),ビットマップの垂直ドット(dpi),ビットマップのピクセルフォーマット,ビットマップのパレット,ビットマップイメージのコンテンツを表すバイト配列,ビットマップのストライド)

「ビットマップのピクセルフォーマット」には、PixelFormats.Bgr32を指定します。Bgr32 ピクセル形式を取得します。Bgr32 は、bits per pixel(BPP)が 32 の sRGB 形式です。各カラー チャネル(青、緑、および赤)に割り当てられる bits per pixel(BPP)は 8 です。

「ビットマップイメージのコンテンツ」を表すバイト配列にBackgroundMaskの戻り値である、バイト配列を指定します。

「ビットマップのストライド」は1ラインあたりのバイト数を表しますので、この場合、colorImageFrame.Width * colorImageFrame.BytesPerPixelと指定します。BytesPerPixelプロパティでRGBカメラの1ピクセルあたりのバイト数を取得して、それにRGBカメラで取得されたフレームの幅を乗算した値を指定します。

BytesPerPixelは1ピクセルあたりのビット数が32bitのRGB形式で、先頭から1バイト(8bit)ずつに青、緑、赤、(残りの1バイト(8bit)はAphaチャネル等に利用されることがありますが、このサンプルでは使用していません)の情報が入っています。1バイトは8ビットですので32÷8=4バイトの値になります。よって640×4=2560となり、2560の値を直接指定しても問題ありません。

  Private Sub KinectSensorOnAllFramesReady(sender As Object, e As AllFramesReadyEventArgs)
    Using colorImageFrame = e.OpenColorImageFrame()
      Using depthImageFrame = e.OpenDepthImageFrame
        Dim kinect As KinectSensor = TryCast(sender, KinectSensor)
        If kinect Is Nothing = True Then
          Return
        End If
 
        If colorImageFrame Is Nothing = True Then
          Return
        End If
 
        If (colorImageFrame IsNot Nothing) AndAlso (depthImageFrame IsNot Nothing) Then
          Dim getBackgroundMask As Byte() = BackgroundMask(kinect, colorImageFrame, depthImageFrame)
          colorImage.Source = BitmapSource.Create(colorImageFrame.Width, colorImageFrame.Height, 96, 96, PixelFormats.Bgr32, Nothing, getBackgroundMask, colorImageFrame.Width * colorImageFrame.BytesPerPixel) 'Me.colorImageWritableBitmap
        End If
      End Using
    End Using    End Sub

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

Kinectセンサーの検出を停止します。ユーザーコントロールであるFaceTrackingViewer1のリソースを解放します。

  Private Sub MainWindow_Closing(sender As Object, e As System.ComponentModel.CancelEventArgs) Handles Me.Closing
    sensorChooser.Stop()
    FaceTrackingViewer1.Dispose()
  End Sub

表示用のコンテンツを表すバイト配列を取得する関数

RGBカメラの処理を行うクラスのインスタンスを取得します。バイト配列変数colorPixelにRGBカメラのピクセルデータのバイト長分の配列を作成します。CopyPixelDataToメソッドで、RGBカメラのフレームのピクセルデータを取得します。CopyPixelDataToメソッドは、ピクセルデータの長さを使用して、事前に割り当てられた配列へ、ピクセルごとの深度データやRGBデータをコピーします。

Dim outputColor As Byte() = New Byte(colorPixel.Length - 1) {}で、RGBカメラの画像を基に表示用のバッファを作成します。

変数indexを、0から、RGBカメラのピクセルデータのバイト長分の配列を保持している、outputColor.Length分、反復処理を行い、下記の処理を実行します。

繰り返し変数iに対応する、Byte型の配列変数であるRGBカメラの画像(outputColor)に、0を指定し背景を黒にしています。変数iを4バイト分ずつ加算します。戻り値は、表示用のコンテンツを表すバイト配列を取得したoutputColorです。

  Private Function BackgroundMask(kinect As KinectSensor, colorFrame As ColorImageFrame, depthFrame As DepthImageFrame) As Byte()
    Dim colorStream As ColorImageStream = kinect.ColorStream
    Dim colorPixel As Byte() = New Byte(colorFrame.PixelDataLength - 1) {}
    colorFrame.CopyPixelDataTo(colorPixel)
    Dim outputColor As Byte() = New Byte(colorPixel.Length - 1) {}
    Dim i As Integer = 0
 
    While i < outputColor.Length
      outputColor(i) = 0
      outputColor(i + 1) = 0
      outputColor(i + 2) = 0
      i = i + Bgr32BytesPerPixel
    End While
    Return outputColor
  End Function
End Class

以上で今回のサンプルは終了です。

編集部より:サンプルプログラムへのリンクが誤っていたため、修正しました。(2012.10.01)

  • 顔の動きを追跡する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メルマガ会員のサービス内容を見る

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