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)

広告

スタイラス入力のジェスチャ認識

// この投稿は XAML Advent Calendar 2014 の 13 日目の記事です。

タッチにはタッチのジェスチャが、空間には空間のジェスチャがありますが、
スタイラス (ペン) にもスタイラスに特化したジェスチャがあり、
その種類は、WPF では ApplicationGesture 列挙体で定義されています。
タップやフリックのほか、円や三角形などの図形もあります。

WPF の InkCanvas はペン入力のできるコントロールですが、
実はこれらのジェスチャを認識する機能を備えており、簡単な設定だけでその機能を有効にできます。

その実装方法は次の通りです。
XAML ファイルおよびコードビハインド ファイルを続けて示します。
(全体のソースコードは GitHub の InkSample にあります。)

 

InkCanvas を配置し、EditingMode プロパティを GestureOnly または InkAndGesture に設定するだけで、
すべてのジェスチャが有効になります。
有効にするジェスチャを明示的に指定するには、SetEnabledGestures メソッドを利用します。

GestureCanvas.SetEnabledGestures(new[] { ApplicationGesture.Circle, ApplicationGesture.Check });

ジェスチャが認識されると Gesture イベントが発生します。
認識されるジェスチャはつねに 1 つとは限らず、その可能性のあるものが複数報告されます。
そして認識結果にはそれぞれの信頼度が含まれているのですが、
数値ではなく RecognitionConfidence 列挙体の値 (高・中・低の 3 段階) で表されます。

さて、このような実装をしてデジタイザーペンで入力してみた様子は、次の動画の前半をご覧ください。
また、動画の後半は、このジェスチャ認識機能をテストの採点に適用した例です。

ペン入力でジェスチャ認識 (Gestures of stylus pen)

 

他にも、V 字形 (Chevron) とか。

ChevronUp ジェスチャ

動画の後半の、テストの採点への適用例。
点数の入力や合計点の計算は自動で処理されます。

InkScoring

 

使ってみた感想としては、円 (○) を書いたつもりが三角形 (△) に誤認識されることが多いです。
信頼度 (RecognitionConfidence) は無視したほうがよいのかもしれません。
あと、どう書けば認識されるのかわからないジェスチャもあるのですが、調べきれていません。

作成したサンプル
動画の前半: InkGestureWpf (GitHub)
動画の後半: InkScoreWpf (GitHub)

素材
答案: 爆笑! 学校のテストの珍回答、おもしろい問題まとめ
答案: 子供達のテストのおもしろ珍回答(珍解答)・落書き画像で笑うと負け
効果音: Music is VFR

参照
InkCanvas クラス
ApplicationGesture 列挙体

C# で方程式を解く

// この投稿は C# Advent Calendar 2014 の 7 日目の記事です。

ゲーム アプリケーションやセンサーを用いたアプリケーションでは、
力学系の方程式の計算や座標系の変換など、何らかの数値計算が必要になることがあります。

中でも一次方程式は頻度が高いと思いますが、開発者の多くは毎回、
あらかじめ手計算により解の形式 (x = ~) を求めてから実装しているのではないでしょうか?
例えば、ax + b = cx + d の形式のものを x = (d – b) / (a – c) に変形してからプログラムするとか。

処理速度に重点を置く場合はこれでよいのですが、
最初に脳内で導出された式とソースコード上の式とでは形式に乖離が生じることがあり、
確認のために再計算しなければならなくなるなど、保守性が高いとはいえないでしょう。

パターン化できるのであれば、面倒な作業は自動化したいところです。
そこで以下では、なるべく元の方程式のままの形から解を得る仕組みを考えます。

 

まず、数式を C# の標準的な算術演算子を用いて表現できるようにするため、
一変数の多項式を表す Polynomial 構造体を以下のように定義します。
それぞれの次数に対する係数を Dictionary で管理し、必要な演算子を用意します。
(全体のソースコードは GitHub の EquationConsole にあります。)


public struct Polynomial
{
    public static readonly Polynomial X = new Polynomial(new Dictionary<int, double> { { 1, 1 } });

    static readonly IDictionary<int, double> _coefficients_empty = new Dictionary<int, double>();

    IDictionary<int, double> _coefficients;

    IDictionary<int, double> Coefficients
    {
        get { return _coefficients == null ? _coefficients_empty : _coefficients; }
    } 

    // The dictionary represents index/coefficient pairs.
    // The dictionary does not contain entries whose coefficient is 0.
    public Polynomial(IDictionary<int, double> coefficients)
    {
        _coefficients = coefficients;
    }

    public static implicit operator Polynomial(double value)
    {
        return value == 0 ? default(Polynomial) : new Polynomial(new Dictionary<int, double> { { 0, value } });
    }

    public static Polynomial operator +(Polynomial p1, Polynomial p2)
    {
        var coefficients = new Dictionary<int, double>(p1.Coefficients);

        foreach (var item2 in p2.Coefficients)
        {
            AddMonomial(coefficients, item2.Key, item2.Value);
        }
        return new Polynomial(coefficients);
    }

    public static Polynomial operator (Polynomial p1, Polynomial p2)
    {
        var coefficients = new Dictionary<int, double>(p1.Coefficients);

        foreach (var item2 in p2.Coefficients)
        {
            AddMonomial(coefficients, item2.Key, item2.Value);
        }
        return new Polynomial(coefficients);
    }

    public static Polynomial operator *(Polynomial p1, Polynomial p2)
    {
        var coefficients = new Dictionary<int, double>();

        foreach (var item1 in p1.Coefficients)
        {
            foreach (var item2 in p2.Coefficients)
            {
                AddMonomial(coefficients, item1.Key + item2.Key, item1.Value * item2.Value);
            }
        }
        return new Polynomial(coefficients);
    }

    public static Polynomial operator /(Polynomial p, double value)
    {
        var coefficients = new Dictionary<int, double>();

        foreach (var item in p.Coefficients)
        {
            AddMonomial(coefficients, item.Key, item.Value / value);
        }
        return new Polynomial(coefficients);
    }

    // Power
    public static Polynomial operator ^(Polynomial p, int power)
    {
        if (power < 0) throw new ArgumentOutOfRangeException("power", "The value must be non-negative.");

        Polynomial result = 1;

        for (var i = 0; i < power; i++)
        {
            result *= p;
        }
        return result;
    }

    public static Polynomial operator +(Polynomial p)
    {
        return p;
    }

    public static Polynomial operator (Polynomial p)
    {
        return 1 * p;
    }

    static void AddMonomial(Dictionary<int, double> coefficients, int index, double coefficient)
    {
        if (coefficients.ContainsKey(index))
        {
            coefficient += coefficients[index];
        }

        if (coefficient != 0)
        {
            coefficients[index] = coefficient;
        }
        else
        {
            coefficients.Remove(index);
        }
    }
}


今回は等値演算子 (==) などは省きました。
暗黙的型変換演算子 (implicit operator Polynomial) を定義したため、
多項式 -x + 2 や 2x – 1 を次のように表すことができます。

var x = Polynomial.X;
var p1 = x + 2;
var p2 = 2 * x  1;

 

次に、この Polynomial 構造体に、方程式を解くためのメソッドを追加します。
方程式の左辺はこの多項式自身で、右辺は 0 であるとします。
一次方程式のほか、二次方程式についても実装しています。


public int Degree
{
    get { return Coefficients.Count == 0 ? 0 : Coefficients.Max(c => c.Key); }
}

// Substitution
public double this[double value]
{
    get { return Coefficients.Sum(c => c.Value * Math.Pow(value, c.Key)); }
}

// Solve the equation whose right operand is 0.
public double SolveLinearEquation()
{
    if (Degree != 1) throw new InvalidOperationException("The degree must be 1.");

    // ax + b = 0
    var a = GetCoefficient(1);
    var b = GetCoefficient(0);

    return b / a;
}

// Solve the equation whose right operand is 0.
public double[] SolveQuadraticEquation()
{
    if (Degree != 2) throw new InvalidOperationException("The degree must be 2.");

    // ax^2 + bx + c = 0
    var a = GetCoefficient(2);
    var b = GetCoefficient(1);
    var c = GetCoefficient(0);
    var d = b * b  4 * a * c;

    return d > 0 ? new[] { (b  Math.Sqrt(d)) / (2 * a), (b + Math.Sqrt(d)) / (2 * a) }
        : d == 0 ? new[] { b / (2 * a) }
        : new double[0];
}

double GetCoefficient(int index)
{
    return Coefficients.ContainsKey(index) ? Coefficients[index] : 0;
}


インデクサーには代入の機能を割り当てました。
以上で Polynomial 構造体の最低限の実装はできたと思います。

これを使うと、例えば直線 y = x – 1 と直線 y = -2x + 5 の交点 P は次のように求められます。
ほぼ数式のままの形で記述できていると思います。


static class Program
{
    static readonly Polynomial x = Polynomial.X;

    static void Main(string[] args)
    {
        var l1 = x  1;
        var l2 = 2 * x + 5;
        var p_x = (l1 l2).SolveLinearEquation();
        var p_y = l1[p_x];
        Console.WriteLine("P({0}, {1})", p_x, p_y); // P(2, 1)
    }
}


 

次の例では、特定の 2 点を通る直線上で、y 座標から x 座標を求めます。


static class Program
{
    static readonly Polynomial x = Polynomial.X;

    static void Main(string[] args)
    {
        var p1 = new Point2D(0, 300);
        var p2 = new Point2D(1800, 300);
        var y_to_x = GetFunc_y_to_x(p1, p2);
        Console.WriteLine(y_to_x(0)); // 900
        Console.WriteLine(y_to_x(100)); // 600
    }

    // P1, P2 を通る直線上で、指定された y 座標に対応する x 座標を求めるための関数。
    static Func<double, double> GetFunc_y_to_x(Point2D p1, Point2D p2)
    {
        // P1(x1, y1) および P2(x2, y2) を通る直線の方程式 (の公式):
        // (x – x1) (y2 – y1) – (x2 – x1) (y – y1) = 0
        return y => ((x p1.X) * (p2.Y p1.Y) (p2.X p1.X) * (y p1.Y)).SolveLinearEquation();
    }
}

struct Point2D
{
    public double X { get; private set; }
    public double Y { get; private set; }

    public Point2D(double x, double y)
        : this()
    {
        X = x;
        Y = y;
    }
}


座標系のマッピングも、簡単なものであればこれとほぼ同様にできます (一般的には行列を使いますが)。

ちなみに、二次方程式 x2 + x -1 = 0 の解もこの通り。

二次方程式

 

なお、今回は引数が (Polynomial, Polynomial) のみの +, -, * 演算子を定義しましたが、
(Polynomial, double) などの引数を受け付けるオーバーロードを用意すれば、演算量を若干削減できると思います。

作成したサンプル
EquationConsole (GitHub)

参照
オーバーロードされた演算子 (C# プログラミング ガイド)