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についてはここではさらっとしか説明していませんが、両者ともに便利なものなので、余裕があれば一緒に勉強してみることをお勧めします。