KINECT SDK Beta2 で、アプリを挿抜に対応させる(F#+WPF+Rx) #kinectsdk_ac

このエントリはKINECT SDK Advent Calendar 2011 : ATNDの12月7日分です。


KINECT SDK Beta2 で、挿抜状態に応じたアプリの動作をする #kinectsdk_ac - かおるんダイアリーをF#で実装してみました。

見た目は同じ動作をするはずです*1

見た目部分

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="577" Width="669">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="149*" />
            <RowDefinition Height="389*" />
        </Grid.RowDefinitions>
        <TextBox Name="kinectCount" Margin="0,0,0,97" FontSize="18" Text="Text" TextAlignment="Center" FontWeight="Bold" FontStretch="Normal" TextWrapping="NoWrap" VerticalContentAlignment="Center" />
        <Image Name="image1" Height="240" Width="320" Margin="0,50,332,248" Grid.RowSpan="2" />
        <Image Height="240" Margin="326,50,6,248" Name="image2" Width="320" Grid.RowSpan="2" />
        <Image Height="240" Margin="0,149,332,0" Name="image3" Width="320" Grid.Row="1" />
        <Image Height="240" Margin="326,149,6,0" Name="image4" Width="320" Grid.Row="1" />
    </Grid>
</Window>

基本的には一緒ですね。

中身の解説

プロジェクト名やXAMLファイル名は適宜置き換えてください。
また、今回のプログラムではReactive Extensions(以降Rx)というライブラリを使用しているので、動かしてみたい方はRxをインストールして参照設定に加えてください。NuGetが利用できる方はNuGetからインストールしましょう。

module FsSampleKinectApplication2

open System
open System.Threading
open System.Reactive.Linq
open System.Windows
open System.Windows.Controls
open System.Windows.Threading
open System.Windows.Media.Imaging
open Microsoft.Research.Kinect.Nui

type MainWindow() =

  let window =
    Application.LoadComponent(new System.Uri("/FsSampleApplication2;component/MainWindow.xaml", System.UriKind.Relative)) :?> Window

  let kinectCount = window.FindName "kinectCount" :?> TextBox

  let image1 = window.FindName "image1" :?> Image
  let image2 = window.FindName "image2" :?> Image
  let image3 = window.FindName "image3" :?> Image
  let image4 = window.FindName "image4" :?> Image

  let images = [| image1;image2;image3;image4 |]
  let mutable eventDictionary : (Runtime * IDisposable) list = []

  let syncContext =
    if SynchronizationContext.Current = null then SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext())
    SynchronizationContext.Current

  let showKinectCount () =
    kinectCount.Text <- (string Runtime.Kinects.Count) + "台のKINECTが有効です"

  let createVideoFrameReady (runtime:Runtime) =
    runtime.VideoFrameReady
     |> Observable.subscribe begin
         fun args ->
           if runtime <> null && runtime.InstanceIndex >= 0 then
             let image = args.ImageFrame.Image
             let source = BitmapSource.Create(image.Width, image.Height, 96.0, 96.0, Media.PixelFormats.Bgr32, null, image.Bits, image.Width * image.BytesPerPixel)
             images.[runtime.InstanceIndex].Source <- source
       end

  let initKinect (kinect:Runtime) =
    kinect.Initialize( RuntimeOptions.UseColor )
    kinect.VideoStream.Open(ImageStreamType.Video, 2, ImageResolution.Resolution640x480, ImageType.Color)
    eventDictionary <- (kinect, (kinect |> createVideoFrameReady)) :: eventDictionary

  do window.Loaded
     |> Observable.subscribe (fun _ -> for kinect in Runtime.Kinects do kinect |> initKinect )
     |> ignore

  do window.Unloaded
     |> Observable.subscribe begin
         fun _ ->
           eventDictionary |> List.iter (fun (r,e) -> e.Dispose() )
           eventDictionary <- []
           for kinect in Runtime.Kinects do kinect.Uninitialize() 
       end
     |> ignore

  let kinect_StatusChanged = syncContext |> Runtime.Kinects.StatusChanged.ObserveOn

  do kinect_StatusChanged
       .Subscribe begin
         fun (e:StatusChangedEventArgs) ->
           showKinectCount ()
           match e.Status with
           | KinectStatus.Connected -> initKinect e.KinectRuntime
           | KinectStatus.Disconnected ->
             for i in Runtime.Kinects.Count .. images.Length - 1 do
               images.[i].Source <- null

             // 抜かれたKINECTのイベント削除と、インスタンスの破棄
             eventDictionary
             |> List.find (fun (runtime,_) -> runtime = e.KinectRuntime)
             |> (fun (runtime,event) -> event.Dispose(); eventDictionary <- eventDictionary |> List.filter (fun (r,_) -> r <> runtime))
             e.KinectRuntime.Uninitialize()
           | _ -> ()
       end
     |> ignore

  member this.Window = window

[<STAThread>]
(new MainWindow()).Window |> (new Application()).Run |> ignore

DispatcherSynchronizationContextを利用する

Runtime.Kinects.StatusChangedイベントは(Window.LoadedやUnloadedのような)UIイベントではないので、StatusChangedイベントに登録されたデリゲートはUIイベントに登録されたデリゲートと同じ優先度で実行されるわけではありません。しかし、StatusChangedイベントに登録したデリゲートはUIイベントに登録されているデリゲートと同じ優先度で実行されてほしいので、DispatcherSynchronizationContextを通じてデリゲートをキュー登録することで、キューに登録された順番に従って一度に1つずつ実行されるようにします。
DispatcherSynchronizationContextの生成と取得のコードは以下のようになります。なお、SynchronizationContextクラスはDispatcherSynchronizationContextクラスの親クラスです。

  let syncContext =
    if SynchronizationContext.Current = null then SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext())
    SynchronizationContext.Current

一般的にはSynchronizationContext.Postメソッドを使ってコールバックを非同期に呼び出してもらうようにします。ただ、第2引数としてデリゲートに渡されたオブジェクトを渡さないといけなかったり、StatusChangedでReactive Programmingできないなどの問題があります。そこで、Observable.ObserveOnメソッドの出番です。

  let kinect_StatusChanged = syncContext |> Runtime.Kinects.StatusChanged.ObserveOn

ObservrOnメソッドはRxで提供されている拡張メソッドの1つで、SynchronizationContextを通じてイベントをキュー登録するものです。戻り型がIObservableなので、ObserveOnメソッドの戻り値をObservable.subscribeで釣りあげることもできます。

イベントの登録と記憶

createVideoFrameReady関数でKINECTランタイムごとにVideoFrameReadyイベントを登録します。この関数ではIDisposableを返しているのですが、このデータに対してDisposeメソッドを実行すると、イベントを解除することができます。これはKINECTがPCから抜かれた時に必要なので、どこかに保持しておきたいところです。また、どのランタイムのイベントも記憶しておきたいところです。そこでまず、これらのデータを保持する変数を用意します。

let mutable eventDictionary : (Runtime * IDisposable) list = []

mutableをつけると、その変数への再代入が可能になります。:と=の間に書かれているのは変数の型です。この変数はRuntimeとIDisposableからなるタプルを要素とするlistを保持します。最初は空リストが代入されています。
イベントの保持はInitKinect関数の最後に行っています。

eventDictionary <- (kinect, (kinect |> createVideoFrameReady)) :: eventDictionary

(kinect,IDisposable)の形式でタプル化し、生成されたタプルとeventDictionaryに格納されているリストから::を使って新しいリストを作成し、最後に<-でeventDictionaryに代入しています。これで無事、記憶ができました。

KINECTの挿抜状態の変更イベント

  do kinect_StatusChanged
       .Subscribe begin
         fun (e:StatusChangedEventArgs) ->
           showKinectCount ()
           match e.Status with
           | KinectStatus.Connected -> initKinect e.KinectRuntime
           | KinectStatus.Disconnected ->
             for i in Runtime.Kinects.Count .. images.Length - 1 do
               images.[i].Source <- null

             // 抜かれたKINECTのイベント削除と、インスタンスの破棄
             eventDictionary
             |> List.find (fun (runtime,_) -> runtime = e.KinectRuntime)
             |> (fun (runtime,event) -> event.Dispose(); eventDictionary <- eventDictionary |> List.filter (fun (r,_) -> r <> runtime))
             e.KinectRuntime.Uninitialize()
           | _ -> ()
       end
     |> ignore

こちらの記事ではObservable.subscribe関数を使用しましたが、RxでもSubscribeメソッドは提供されており、今回はObserveOnと対にする目的でRx側のSubscribeを使っています。
処理自体でC#コードと異なっている部分は、C#でif文だった所がパターンマッチに置き換わっていること、for文の形式、イベントの解除方法です。
F#にはC#コードに書かれている従来形式のfor文が存在しないので、範囲式を使って代替しました。範囲式では範囲演算子..(ドット2つ)を使って指定した数値の範囲を指定します。また、前述したとおり、IDisposable.Disposeを実行してイベントを解除しています。List.find部分で同じランタイムインスタンスのデータを探し、Disposeを実行した後、List.filterによって得られたリストを再代入しています。


少々細かい話が増えたものの、F#によるKINECTプログラミングの雰囲気はつかめたのではないでしょうか。
SynchronizationContextによるマルチスレッドプログラミングやRxについてはここではさらっとしか説明していませんが、両者ともに便利なものなので、余裕があれば一緒に勉強してみることをお勧めします。

*1:KINECTが手元に1台しかないので複数台で動くかどうか試せていません