Kinect を利用したアプリを普通に実装してみると、
アプリの起動に時間がかかったり、途中でフリーズしたりすることがあると思います。
(Kinect SDK にあるサンプルでも発生します。)
今回は、その原因と回避方法について記述していきたいと思います。
ではまず、WPF アプリケーションを新しく作成し、Microsoft.Kinect.dll を参照に追加して、
MainWindow を次のように実装してみます。
おそらく、よく見られる実装だと思います。
MainWindow.xaml
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="400" Width="600">
<Grid>
<TextBlock x:Name="PositionText" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="36"/>
</Grid>
</Window>
MainWindow.xaml.cs
{
KinectSensor sensor;
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
Closed += MainWindow_Closed;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Thread.Sleep(2000); // 意図的な負荷。
if (KinectSensor.KinectSensors.Count == 0) return;
sensor = KinectSensor.KinectSensors[0];
sensor.SkeletonStream.Enable();
sensor.Start();
sensor.SkeletonFrameReady += sensor_SkeletonFrameReady;
}
void MainWindow_Closed(object sender, EventArgs e)
{
if (sensor != null)
{
sensor.Stop();
}
}
void sensor_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
{
Thread.Sleep(15); // 意図的な負荷。
using (var frame = e.OpenSkeletonFrame())
{
if (frame == null)
{
ShowPosition("");
return;
}
var skeletons = new Skeleton[frame.SkeletonArrayLength];
frame.CopySkeletonDataTo(skeletons);
var skeleton = skeletons.FirstOrDefault(s => s.TrackingState == SkeletonTrackingState.Tracked);
if (skeleton == null)
{
ShowPosition("");
return;
}
var p = skeleton.Position;
ShowPosition(string.Format("({0:N3}, {1:N3}, {2:N3})", p.X, p.Y, p.Z));
}
}
void ShowPosition(string text)
{
PositionText.Text = text;
}
}
Kinect で人物をトラッキングして、その中心の座標を表示するだけのアプリです。
上記のコードでは、検証のために意図的に処理に時間がかかるようにしています。
このように実装して実行すると、次のような現象が見られると思います。
(1) アプリを起動した直後、ウィンドウは表示されるがしばらくフリーズする。
(2) 一見問題なく動作しているが、ウィンドウをマウスでドラッグすると不連続に動く (またはフリーズする)。
どちらも、UI スレッドに大きな負担をかけていることが原因です。
したがって、UI のパフォーマンスを下げる要因をなるべくバックグラウンド スレッドに移すことが望ましいです。
それぞれの詳細は次の通りです。
(1) Kinect の初期化処理
Kinect の初期化処理は、その内容によってはかなり時間がかかることがあります。
上記のコードでは Loaded イベントで実行しており、この場合、ウィンドウが表示された直後にフリーズします。
なお、MainWindow のコンストラクター内で実行する場合は、ウィンドウが表示されるまでに時間がかかってしまいます。
したがって、Kinect の初期化処理を表す
sensor.SkeletonStream.Enable();
sensor.Start();
の部分をバックグラウンドで実行するとよいでしょう。
(2) 各フレームのデータに対する処理
上記のコードでは、各フレーム発生時のコールバックである sensor_SkeletonFrameReady メソッドが
UI スレッドで実行されています。
したがって、ジェスチャを判定したり深度を解析したりするなど、処理に時間がかかってしまうと画面がフリーズします。
実は Kinect SDK では、SkeletonFrameReady などのイベントハンドラーを登録するときに
内部的に SynchronizationContext.Current を取得して保持しており、
イベントハンドラーを実行するときにこの同期コンテキストを使おうとします。
つまり、
sensor.SkeletonFrameReady += sensor_SkeletonFrameReady;
を UI スレッドで実行すれば sensor_SkeletonFrameReady メソッドは UI スレッドで実行され、
バックグラウンドで実行すればバックグラウンド (登録時とは別のスレッド。どのフレームでも同じスレッド ID) で実行されます。
以上を踏まえ、コードを次のように修正します。
public MainWindow()
{
InitializeComponent();Loaded += (o, e) => Task.Run(() => MainWindow_Loaded(o, e));
Closed += MainWindow_Closed;
}
void ShowPosition(string text)
{
Dispatcher.InvokeAsync(() => PositionText.Text = text);
}
MainWindow_Loaded メソッド全体をバックグラウンドで実行させます。
これにより sensor_SkeletonFrameReady メソッドはバックグラウンドで呼び出されるようになるため、
UI 要素を操作するには、Dispatcher.InvokeAsync メソッドを呼び出して UI スレッドに戻して実行させます。
(Dispatcher.Invoke メソッドを使った場合、アプリ終了時に TaskCanceledException が発生したり、
sensor.Stop メソッドを呼び出すとデッドロックが発生したりします。)
以上で、UI スレッドへの負荷を軽減し、UI がフリーズしないアプリにすることができました。
(ちなみにこの場合、sensor_SkeletonFrameReady メソッドの処理にやたら時間がかかっても、
単にフレームレートが下がるだけでアプリは続行できます。)
ただし、今回は TextBlock の Text プロパティの値をコードから直接変更しましたが、
UI 要素への値の反映については、以前にデータ バインディングと非同期処理 (その 1) で書いた通り、
データ バインディングを使うほうがよいでしょう。
ちなみに Leap Motion の場合は、初期化でフリーズすることはなく、
各フレーム発生時のコールバックはつねにバックグラウンド スレッドで実行されます。
作成したサンプル
KinectAsyncWpf (GitHub)
KinectAsyncWpf2 (データ バインディングを利用) (GitHub)
バージョン情報
Kinect for Windows SDK 1.8