KINECT SDKのサンプル「SkeletalViewer」をF# 化する
このエントリはF# Advent Calendar 2011 - PARTAKE兼KINECT SDK Advent Calendar 2011 : ATNDの12月8日分になります。
今回はKINECT+XNA+WPF連携…を紹介したかったのですが、作成途中で間に合わないと判断したためSkeletalViewerを題材にしたいと思います。
SkeletalViewerのF#化
KINECT SDKをインストールすると、サンプルがいくつかついてきます。そして、Samples.zip内にはC++とC#のサンプルコードがいくつか入っています。
今回はサンプルの一つであるSkeletalViewerをF#に書き換えます。
簡易解説
かいつまんで解説します(都合上インデントを変更したりしています)。
XAML系
ほぼそのままです。
ただし、C#サンプルのようなUserControlを継承したControlクラスをXAMLで呼び出す方法ではなく、XAMLからUserControlをロードして機能を追加する形にしています。
KINECTランタイムの保持
Viewer系クラスでは接続されているKINECTランタイムを保持しておくわけですが、C#ではnullを代入したりインスタンスを代入したりしています。F#ではnullをあまり使いたくなかったので、代わりにOptionを使いました。
KinectDepthViewer
C#コードでのConvertDepthFrameメソッドには以下の記述があります。
byte[] convertDepthFrame(byte[] depthFrame16) { bool hasPlayerData = RuntimeOptions.HasFlag(RuntimeOptions.UseDepthAndPlayerIndex); for (int i16 = 0, i32 = 0; i16 < depthFrame16.Length && i32 < depthFrame32.Length; i16 += 2, i32 += 4) { int player = hasPlayerData ? depthFrame16[i16] & 0x07 : -1; int realDepth = 0; if (hasPlayerData) realDepth = (depthFrame16[i16 + 1] << 5) | (depthFrame16[i16] >> 3); else realDepth = (depthFrame16[i16 + 1] << 8) | (depthFrame16[i16]); //以下略
F#ではconvertDepthFrame関数内にconvert再帰関数を作って表現しました。
let convertDepthFrame (depthFrame16:byte array) = let hasPlayerData = runtimeOptions.HasFlag(RuntimeOptions.UseDepthAndPlayerIndex) let rec convert (i16:int) (i32:int) = if i16 < depthFrame16.Length && i32 < depthFrame32.Length then let player = if hasPlayerData then byte <| depthFrame16.[i16] &&& 0x07uy else byte <| -1 let realDepth = if hasPlayerData then (depthFrame16.[i16 + 1] <<< 5) ||| (depthFrame16.[i16] >>> 3) else (depthFrame16.[i16 + 1] <<< 8) ||| (depthFrame16.[i16]) (* 中略 *) convert (i16 + 2) (i32 + 4) convert 0 0 depthFrame32
また、F#ではif"式"であり値が返るため、realDepthはmutableでなくても問題ありません。
F#は暗黙的型変換が行われないので、byte型が必要な部分ではbyteリテラルを使用しました。
KinectDiagnosticViewer
getDisplayPositionでは、Runtime.SkeletonEngine.SkeletonToDepthImageの第2、第3引数がout引数なのでmutableな変数を参照渡しました。
getBodySegmentの第3引数idsにおいては、C#では可変長引数を使っています。しかし、F#には可変長引数がないので、リストで代用しています。
MainWindow
StatusChangedに登録する処理ははDispatcherSynchronizationContextとObservable.ObserveOnを使って登録しました。
あと、ItemsControl.Itemsで得られるコレクションにはGetEnumeratorメソッド存在が存在しないと怒られたためfor文に使用できず、苦肉の策として範囲式を使っています。
さてここで、C#コードのgetViewerを見てみましょう。
KinectDiagnosticViewer GetViewer(object sender) { while (sender != null && sender is FrameworkElement) { sender = ((FrameworkElement) sender).Parent; if (sender is KinectDiagnosticViewer) { return sender as KinectDiagnosticViewer; } } return null; }
引数のsenderに再代入するという香ばしいコードが書かれていました。変数名をつけづらかったのかもしれませんが、この再代入は怖いですね。F#にはシャドウイングがあるのでわりと安心です。
let getViewer (sender:obj) = let rec getViewer' (sender:obj) = if sender <> null && sender :? FrameworkElement then let sender = box (sender :?> FrameworkElement).Parent // シャドウイング if sender :? UserControl then kinectViewerDictionary |> List.find (fun (c,viewer) -> obj.ReferenceEquals(c,sender)) |> snd else getViewer' sender else Unchecked.defaultof<KinectDiagnosticViewer> getViewer' sender
実行結果
実行結果は以下のようになります(本来左側にはRGB映像が表示されるのですが、諸般の都合により塗りつぶしました)。
KINECTを抜くと画面がきちんと更新されます。
こんな風に、F#でもC#と同様のものが出来上がります。
改善したいところ
手続き的な処理の多い関数をある程度小さい関数に分割したいところです。あとは、まだまだ簡潔に記述できそうな箇所がいくつかあるので、そのあたりは改善したいですね。