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

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

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

リスト2 (MainWindow.xaml.vb)

Option Strict On
Imports Microsoft.Kinect
Imports System.Windows.Media.Imaging
Imports Microsoft.Speech.AudioFormat
Imports Microsoft.Speech.Recognition
Imports System.IO

Assembly.GetExecutingAssembly.GetName.CodeBaseでアプリケーションパス(この場合bin\Debugディレクトリ内の実行ファイルパス)を取得する必要があるため、Assemblyクラスの含まれる、System.Reflection名前空間をインポートしておきます。最終的に取得したbin\Debugフォルダ内には、右手のひらに乗るdummy.pngという名前の画像が保存されます。

Imports System.Reflection

Imports Coding4Fun.Kinect.Wpf
Class MainWindow

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

  Dim Kinect As KinectSensor

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

  Dim myScreenImageRect As Int32Rect

Short型の配列メンバ変数myDepthPixelDataとByte型の配列メンバ変数myColorPixelDataを宣言しておきます。

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

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

音声認識サービスを実行するためのアクセス権を提供するクラスである、SpeechRecognitionEngineクラス用メンバ変数engineを宣言します。

  Dim engine As SpeechRecognitionEngine

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

  Dim myImage As Image

深度を指定する定数メンバ変数targetDepthを宣言し、2000(2m)で初期化しています。そのため、Kinectセンサーより2m離れてお試しください。

  Const targetDepth As Double=2000

WriteableBitmapクラス型のHuman1_bitmap、Human2_bitmap、Room_Bitmapプロパティを定義しておきます。

  Property Human1_bitmap As WriteableBitmap
  Property Human2_bitmap As WriteableBitmap
  Property Room_Bitmap As WriteableBitmap

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

Choicesクラスは、要素を構成するための代替項目の一覧を表すクラスで、GrammarBuilder オブジェクトからのみ直接使用されます。認識させる言葉をAddメソッドで登録します。

GrammarBuilderクラスは、単純な入力から複雑な Grammar(構文情報を取得管理するクラス)を構築するためのメカニズムを提供するクラスで、登録された言葉の構文(文法)設定を行い、SpeechRecognitionEngineへと設定します。Appendメソッドで、登録した言葉を GrammarBuilder オブジェクトとして現在の GrammarBuilder に追加します。

文法のチェックされた言葉(builder)で初期化された、新しいGrammerクラスのインスタンス、myGrammerオブジェクトを作成します。Grammerクラスは、構文情報を取得および管理するためにランタイムをサポートするクラスです。

次に、SpeechRecognitionEngineクラスの新しいインスタンスengineオブジェクトを作成します。SpeechRecognitionEngineクラスのLoadGrammerメソッドで、Grammar によって指定された通りに、特定の構文を同期的に読み込みます。

Kinectセンサーを取得し、Kinectセンサーを動作させます。Kinectの音声インターフェースは、Kinect.AudioSourceで提供されます。Startメソッドで音声入力を開始します。
入力ストリームを取得し、SpeechRecognitionEngine クラスのSetInputToDefaultAudioDeviceメソッドで、SpeechRecognitionEngine の現在のインスタンスに、システム既定のオーディオ入力を割り当てます。

認識操作の後に、RecognizeAsync によって開始された認識を終了しないよう、RecognizeMode.Multipleを指定して、RecognizeAsyncメソッドで非同期音声認識を開始します。

言葉が認識された際には、AddHandlerステートメントで言葉を認識した際に発生するSpeechRecognizedイベントに、イベントハンドラを指定します。認識された音声(speechArgs.Result.Text)を変数wordsに格納します。Confidenceプロパティで音声認識の信頼度を設定します。-1が低、0が標準、1が高信頼度となります。-1を指定するとどんな言葉でも反応する恐れがあります。1を指定するとなかなか認識してくれません。今回は信頼度が0.5より大きい場合に設定しています。

「わかれる」と発声された場合はプレイヤーを画像として保存します。

アプリケーションへのパスをAssembly.GetExecutingAssembly.GetName.CodeBaseで取得し、変数fullAppNameに格納します。fullAppNameには「完全フルパス+bin\Debugフォルダ内のKINECT_SeparateManOnHand.exe」という実行ファイル名が格納されます。欲しいのは「完全フルパス+bin\Debug」というフォルダ名ですので、Path.GetDirectoryName(fullAppName)で、欲しいフォルダ名が取得できます。変数fullAppPathに格納しておきます。

BitmapFrameクラスのbmpFrame変数を宣言します。BitmapFrameクラスは、デコーダによって返されてエンコーダによって受け入れられるイメージ データを表すクラスです。

BitmapFrame.CreateメソッドでHuman2_bitmapプロパティからBitmapFrameを作成します。文字列dummy.pngというファイル名と、新規作成モードで初期化された、FileStreamのインスタンスstreamオブジェクトを作成します。
新しいPngBitmapEncoderクラスのインスタンスmyPngオブジェクトを作成します。PngBitmapEncodeクラスは、PNG形式のイメージのエンコードに使用されるエンコーダを定義するクラスです。
myPngオブジェクトのイメージ内の個別のフレームにAddメソッドで、streamオブジェクトで作成したファイルを追加していきます。Saveメソッドでストリームを保存します。これで、フルパス+bin\Debug内にdummy.pngという画像ファイルが保存されます。

AddHandlerステートメントで、RGBカメラ、距離カメラ、スケルトンのフレームが更新された時に発生する、AllFramesReadyイベントに、Kinect_AllFramesReadyイベントハンドラを指定します。

personImageのSourceプロパティに、フルパス+dummy.pngの画像を指定します。絶対Uriで指定します。

「くっつく」と発声された場合は、personImageのSourceプロパティにNothingを指定してオブジェクトの関連付けをなくします。「おわり」と発声された場合はKinectセンサーの動作を停止し、音声認識も停止し、Environment.Exit(0)でプログラムを終了します。

RGBカメラ、距離カメラ、スケルトンを有効にし、各画像データを初期化するinit_kinectプロシージャを実行します。

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

OpenNextFrame(100)メソッドで、KinectからRGBデータの次のフレームを開きます。次のフレームがなかった場合のタイムアウトを100ミリセコンドと指定しています。同様に、Kinectから深度データの次のフレームを開きます。次のフレームがなかった場合のタイムアウトを100ミリセコンドと指定しています。

プレイヤーを描画するRenderScreenプロシージャを実行します。引数として、Kinectのストリーミング用RGBデータのバッファと深度データのバッファを含んでいる、colorFrameとdepthFrameを渡しています。DataContextプロパティにMainWindow自身のインスタンスを指定します。この処理を行わないとプレイヤーが表示されませんので注意してください。

  Private Sub MainWindow_Loaded(sender As Object, e As System.Windows.RoutedEventArgs) Handles Me.Loaded
    If KinectSensor.KinectSensors.Count = 0 Then
      MessageBox.Show("Kinectが接続されておりません。")
      Exit Sub
    End If
 
    Dim sentence As Choices = New Choices
    With sentence
      .Add("わかれる")
      .Add("くっつく")
      .Add("おわり")
    End With
 
    Dim builder As GrammarBuilder = New GrammarBuilder
    builder.Append(sentence)
    Dim myGrammer As Grammar = New Grammar(builder)
    engine = New SpeechRecognitionEngine()
    engine.LoadGrammar(myGrammer)
 
    AddHandler engine.SpeechRecognized, Sub(speechSender As Object, speechArgs As SpeechRecognizedEventArgs)
        Try
          Dim confidence = speechArgs.Result.Confidence
            If confidence > 0.5 Then
              Select Case speechArgs.Result.Text
                Case "わかれる"
                  Dim fullAppName As String = Assembly.GetExecutingAssembly.GetName.CodeBase
                Dim fullAppPath As String = Path.GetDirectoryName(fullAppName)
                Dim bmpFrame As BitmapFrame = BitmapFrame.Create(Human2_bitmap)
                Using stream As FileStream = New FileStream("dummy.png", FileMode.Create)
                  Dim myPng As PngBitmapEncoder = New PngBitmapEncoder
                  myPng.Frames.Add(bmpFrame)
                  myPng.Save(stream)
                End Using
        AddHandler Kinect.AllFramesReady, AddressOf Kinect_AllFramesReady
          personImage.Source = New BitmapImage(New Uri(Path.Combine(fullAppPath, "dummy.png"), UriKind.Absolute)) 
                    Case "くっつく"
                      personImage.Source = Nothing
                    Case "おわり"
                      If Kinect Is Nothing = False Then
                        If Kinect.IsRunning = True Then
                          Kinect.Stop()
                            Kinect.AudioSource.Stop()
                            engine.RecognizeAsyncStop()
                            Kinect.Dispose()
                        End If
                          Environment.Exit(0)
                     End If
        End Select
      End If
    Catch
        Exit Sub
    End Try
  End Sub
 
  Kinect = KinectSensor.KinectSensors(0)
  Kinect.Start()
 
  Dim audio As KinectAudioSource = Kinect.AudioSource
 
  Using s As Stream = audio.Start()
    engine.SetInputToDefaultAudioDevice()
    engine.RecognizeAsync(RecognizeMode.Multiple)
  End Using
 
  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)
            RenderScreen(colorFrame, depthFrame)
          End Using
        End Using
      End Sub
    DataContext = Me
  End Sub

RGBカメラ、距離カメラ、スケルトンを有効にし、各画像データを初期化する処理

DepthStream.Enableメソッドで距離カメラの表示サイズを640×480、1秒あたり30フレームで、動作を開始します。同様にColorStream.Enableメソッドで、RGBカメラを、RGBフォーマット、表示サイズ640×480、1秒あたり30フレームで、動作を開始します。プレイヤーおよびスケルトンの認識も開始します。

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

バイト配列変数myColorPixelDataにRGBカメラのピクセルデータのバイト長分の配列を作成します。

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

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

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

ここは、Room_Bitmap = New WriteableBitmap(640, 480, 96, 96, PixelFormats.Bgra32, Nothing)
と指定しても同じです。

同様に、Human1_bitmap、Human2_bitmapについてもWriteableBitmapの書式で初期化しておきます。
IntRect32構造体のmyScreenImageRectを下記の書式で初期化し、領域を指定します。

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

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

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

  Private Sub init_kinect()
    Try
      If Kinect Is Nothing Then
        Exit Sub
      End If
      Kinect.DepthStream.Enable(DepthImageFormat.Resolution640x480Fps30)
      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) {}
 
      Room_Bitmap = New WriteableBitmap(depthStream.FrameWidth, depthStream.FrameHeight, 96, 96, PixelFormats.Bgra32, Nothing)
 
      Human1_bitmap = New WriteableBitmap(depthStream.FrameWidth, depthStream.FrameHeight, 96, 96, PixelFormats.Bgra32, Nothing)
      Human2_bitmap = New WriteableBitmap(depthStream.FrameWidth, depthStream.FrameHeight, 96, 96, PixelFormats.Bgra32, Nothing)
 
      myScreenImageRect = New Int32Rect(0, 0, CInt(Math.Ceiling(Human2_bitmap.Width)), CInt(Math.Ceiling(Human2_bitmap.Height)))
    Catch ex As Exception
      MessageBox.Show(ex.Message)
      Exit Sub
    End Try
  End Sub

RGBカメラ、距離カメラ、スケルトンのフレームが更新されたことを通知するイベント

Skeletonクラス型の配列変数allSkeletonsを宣言します。

フレームごとのスケルトンデータを表すクラスである、SkeletonFrame型のskeletonFrameData変数を宣言し、e.OpenSkeletonFrameメソッドで、新しいフレームのスケルトンの情報を取得します。

新しいSkeletonの配列オブジェクトallSkeletonsを、スケルトン配列の長さを取得するSkeletonArrayLengthプロパティで初期化した新しいインスタンスを作成します。

CopySkeletonDataToメソッドで、現在のSkeletonFrameDataにあるスケルトンデータを、指定した配列(allSkeletons)にコピーします。CopySkeletonDataToメソッドで取得されるデータはプレイヤー分取得されるため、それぞれのトラッキング状態を確認します。スケルトンデータを持つallSkeletons配列変数内を、skeletonResult変数で反復処理しながら、全ての関節の位置がトラッキングされた状態にある、シーケンスの最初の要素を取得し、Skeletonクラス型の反復変数skeletonResultに格納していきます。格納された要素をSkeleton型のmyFirstSkeletonで参照します。

分身を表示されるpersonImageコントロールと、トラッキングされた状態にある最初の右手のジョイント情報を引数に、ScalePositionプロシージャを実行します。また、スケルトンの位置を取得するGetCameraPointプロシージャも実行します。

  Private Sub Kinect_AllFramesReady(ByVal sender As Object, ByVal e As AllFramesReadyEventArgs)
    Dim allSkeletons As Skeleton()
    Using skeletonFrameData As SkeletonFrame = e.OpenSkeletonFrame()
      If skeletonFrameData Is Nothing = True Then
        Return
      End If
 
      allSkeletons = New Skeleton(skeletonFrameData.SkeletonArrayLength - 1) {}
      skeletonFrameData.CopySkeletonDataTo(allSkeletons)
 
      Dim myFirstSkeleton As Skeleton = (From skeletonResult In allSkeletons Where skeletonResult.TrackingState = SkeletonTrackingState.Tracked Select skeletonResult).FirstOrDefault()
      If myFirstSkeleton Is Nothing Then
        Return
      End If
 
      ScalePosition(personImage, myFirstSkeleton.Joints(JointType.HandRight))
      GetCameraPoint(myFirstSkeleton, e)
    End Using
  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メルマガ会員のサービス内容を見る

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