Kinect アプリで画面のフリーズを防ぐ

Kinect を利用したアプリを普通に実装してみると、
アプリの起動に時間がかかったり、途中でフリーズしたりすることがあると思います。
(Kinect SDK にあるサンプルでも発生します。)
今回は、その原因と回避方法について記述していきたいと思います。

ではまず、WPF アプリケーションを新しく作成し、Microsoft.Kinect.dll を参照に追加して、
MainWindow を次のように実装してみます。
おそらく、よく見られる実装だと思います。

MainWindow.xaml


<Window x:Class="KinectAsyncWpf.MainWindow"
       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


public partial class MainWindow : Window
{
    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

参照
Kinect for Windows SDK のセットアップ
データ バインディングと非同期処理 (その 1)

データ バインディングと非同期処理 (その 2)

前回のデータ バインディングと非同期処理 (その 1) からの続きです。

前回は、UI スレッドで重い処理を実行してフリーズしてしまうアプリを作成しました。
今回は、非同期処理を使ってフリーズしないアプリに変更する方法について記述します。

 

■ TAP メソッドが提供されている場合

まず、ライブラリなどでタスクベースの非同期パターン (TAP) のメソッドが提供されている場合を考えます。
TAP メソッドが実装されているクラスの例としては、HttpClient クラスなどが挙げられます。
今回の例では、ConvertUtility クラスに ToUpperAsync メソッドが追加されたとします。

ConvertUtility.cs


public static class ConvertUtility

    // 同期メソッド。
    public static string ToUpper(string text)
    {
        Thread.Sleep(3000);
        return text.ToUpper();
    } 

    // TAP メソッド。
    public static Task<string> ToUpperAsync(string text)
    {
        return Task.Run(() => ToUpper(text));
    }
}


 

この場合は対応が非常に簡単です。
TextModel クラスのコンストラクターの中で、以前は

AddPropertyChangedHandler("Input", () => Output = ConvertUtility.ToUpper(Input));

としていたところを、次のように変更します。

AddPropertyChangedHandler("Input", async () => Output = await ConvertUtility.ToUpperAsync(Input));

async/await キーワードと、メソッド名の Async が追加されただけです。
このようにすると、時間のかかる処理はバックグラウンド スレッドで実行され、完了すると UI スレッドに戻ってきます。

実行してみると、アプリはすぐに起動し、下側の TextBlock には非同期的に値が反映されるようになります。

実行結果

 

■ 非同期メソッドが提供されていない場合

上の例の ToUpperAsync のようなメソッドが提供されておらず、ToUpper メソッドを使わざるを得ないような場合でも、
Task.Run メソッドを使えば対応できます。

TextModel クラスのコンストラクターの処理を次のように変更します。

AddPropertyChangedHandler("Input",
    async () => Output = await Task.Run(() => ConvertUtility.ToUpper(Input)));

Task.Run メソッドに渡した処理が完了すると UI スレッドに戻ってきます。

 

⋄ async/await を使わない方法

次のようなコードに変更しても、アプリはフリーズせずに動作します。

AddPropertyChangedHandler("Input",
    () => Task.Run(() => Output = ConvertUtility.ToUpper(Input)));

これだと Output プロパティにはバックグラウンド スレッドで値が設定されるため、
普通に考えると、UI 要素にアクセスできずに例外が発生してしまうように見えます。

しかし実は、Binding オブジェクトはデータソースの値を UI スレッドで取得します。
つまり Output プロパティの値を取得するときに UI スレッドに移るため、問題なく動作します。

 

■ EAP メソッドが提供されている場合

さらに、イベントベースの非同期パターン (EAP) の場合も考えてみましょう。
おそらく、現実にはまだ EAP にしか対応していないライブラリも少なくないと思います。

ConvertUtility クラスに対する EAP の実装として、次のクラスを作成します。

ConvertClient.cs


public class ConvertClient
{
    public event EventHandler<string> ToUpperCompleted = (o, e) => { };

    // EAP メソッド。
    public void ToUpperAsync(string text)
    {
        Task.Run(() => ConvertUtility.ToUpper(text))
            .ContinueWith(t => ToUpperCompleted(this, t.Result));
    }
}


 

TextModel クラスのコンストラクターの処理を次のように変更します。

var client = new ConvertClient();
client.ToUpperCompleted += (o, e) => Output = e;
AddPropertyChangedHandler("Input", () => client.ToUpperAsync(Input));

ToUpperCompleted イベントのイベント ハンドラーはバックグラウンド スレッドで実行されますが、
上述の通り、UI 側は Output プロパティの値を UI スレッドで取得します。

今回のイベント ハンドラーはバックグラウンド スレッドで実行されていますが、
既存のライブラリの中には、イベント ハンドラーが UI スレッドで実行されるものもあります。
いずれの場合でも、上記のコードで対応できます。

 

■ 注意点

  • 非同期処理は同時に複数実行されることもあるため、場合により同時実行制御 (lock など) が必要となります。
  • ここでは説明しませんでしたが、非同期プログラミング モデル (APM) の場合も同様に対応できると思います。

 

バージョン情報
C# 5.0
.NET Framework 4.5

参照
非同期プログラミングのパターン
Async/Await – 非同期プログラミングのベスト プラクティス (MSDN マガジン)
.NETで非同期ライブラリを正しく実装する
async/awaitと同時実行制御

データ バインディングと非同期処理 (その 1)

WPF をはじめとする XAML UI テクノロジにおいて、
通信やストレージ I/O などの時間のかかる可能性のある処理を非同期で実行させる方法について考えます。
前提として、モデル層のデータはデータ バインディングによって UI 層に反映されるものとします。

 

まずは準備として、WPF アプリケーション プロジェクトを作成し、次のようなメソッドを作成します。
文字列を変換させるだけですが、疑似的に時間のかかるメソッドとするために 3 秒間ブロックさせています。

ConvertUtility.cs


public static class ConvertUtility
{
    // 処理に時間のかかるメソッド。
    public static string ToUpper(string text)
    {
        Thread.Sleep(3000);
        return text.ToUpper();
    } 
}


 

次に、モデルとなるクラスを作成します。
入力と出力を表すプロパティのみを定義します。

TextModel.cs


public class TextModel : INotifyPropertyChanged
{
    private string input;
    public string Input
    {
        get { return input; }
        set
        {
            if (input == value) return;
            input = value;
            NotifyPropertyChanged();
        }
    }

    private string output;
    public string Output
    {
        get { return output; }
        private set
        {
            if (output == value) return;
            output = value;
            NotifyPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged = (o, e) => { };

    public void NotifyPropertyChanged([CallerMemberName]string propertyName = "")
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    public void AddPropertyChangedHandler(string propertyName, Action action)
    {
        if (action == null) throw new ArgumentNullException("action");

        PropertyChanged += (o, e) =>
        {
            if (e.PropertyName == propertyName)
            {
                action();
            }
        };
    }

    public TextModel()
    {
        // Input プロパティの値が変更されたら、それを変換して Output プロパティに設定します。
        AddPropertyChangedHandler("Input", () => Output = ConvertUtility.ToUpper(Input));
    }
}


 

TextModel クラスのコンストラクターには、
Input プロパティの値が変更されたら、それを変換して Output プロパティに設定する、
というコードを追加しています。

最後に UI です。
Window の DataContext に TextModel オブジェクトを設定し、
上側の TextBox を Input に、下側の TextBlock を Output にそれぞれバインドします。

MainWindow.xaml

MainWindow.xaml

 

このアプリを実行してみると、起動は遅く、テキストを編集したときにフリーズしてしまいます。
実は、Visual Studio や Blend のデザイン画面上での編集中にも同様にフリーズします。

実行結果

これは、時間のかかる処理を UI スレッドで実行していることが原因です。
次回のデータ バインディングと非同期処理 (その 2) では、これを非同期処理に変更する方法について記述します。

 

バージョン情報
C# 5.0
.NET Framework 4.5