かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Leap Motionはじめました

旬も過ぎ去った感もあるLeap Motionを触り始めました。とりあえずね。センサーから上がってくる実際の値を処理してみたいじゃないですか…!!

こちらのbuildinsiderの記事を参考に画面に指先の点をプロットするアプリを書いてみました。

早速やってみた

Listenerを実装してオーバーライドしたメソッドで処理を…とやるのが嫌だったので、以下のようなListenerのOnFrameをIO<Controller>に変換するだけのListenerを作りました。

class RxListener : Listener
{
    private Subject<Controller> onFrameProxy = new Subject<Controller>();

    public override void OnFrame(Controller c)
    {
        this.onFrameProxy.OnNext(c);
    }

    public IObservable<Controller> OnFrameAsObservable()
    {
        return this.onFrameProxy.AsObservable();
    }
}

そして、Leapからのデータの受信を開始する処理では、以下のようにRxで50ms間隔でデータを貰うようにしてからUIスレッド上で現在のポイントを割り出しています。ここらへんは、記事のサンプルコードをC++からC#に焼き直しただけです。

this.leapController = new Controller();
this.leapListener = new RxListener();
this.leapController.AddListener(this.leapListener);
this.leapListener.OnFrameAsObservable()
    .Sample(TimeSpan.FromMilliseconds(50))
    .ObserveOn(SynchronizationContext.Current)
    .Subscribe(c =>
    {
        this.Points.Clear();
        var frame = c.Frame();
        foreach (var p in frame.Pointables)
        {
            var normalizedPoint = frame.InteractionBox.NormalizePoint(p.StabilizedTipPosition);
            var x = normalizedPoint.x * this.WindowWidth;
            var y = this.WindowHeight - normalizedPoint.y * this.WindowHeight;

            var color = default(Color);
            if (p.TouchDistance > 0 && p.TouchZone != Pointable.Zone.ZONENONE)
            {
                color = Colors.Azure;
            }
            else if (p.TouchDistance <= 0)
            {
                color = Colors.Red;
            }
            else
            {
                color = Colors.LightGray;
            }

            this.Points.Add(Tuple.Create((int)x, (int)y, new SolidColorBrush(color)));
        }
    });

PointsプロパティはObservableCollection<Tuple<int, int, SolidColorBrush>という手抜きにしてます。本当はちゃんとクラス作ったほうがいいんですけど、なんだかめんどくさくなって。

画面側

画面側は、ItemsControlにPointsをバインドしてTuple<int, int, SolidColorBrush>の値をもとに丸を画面上に配置してます。ItemsPanelTemplateでCanvasを指定してItemsContainerStyleでCanvas.TopとCanvas.Leftを指定して任意の場所に丸を出してるのがポイントかな。

<ItemsControl Grid.RowSpan="2" ItemsSource="{Binding Points}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
            <Setter Property="Canvas.Left" Value="{Binding Item1}" />
            <Setter Property="Canvas.Top" Value="{Binding Item2}" />
        </Style>
    </ItemsControl.ItemContainerStyle>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Ellipse Width="100" Height="100" Fill="{Binding Item3}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

動かしてみると以下のようになります。

f:id:okazuki:20140321143849j:plain

いい感じ!!

コード全体

以下のライブラリを参照したWPFアプリケーションです。

  • MVVM Light PCL
  • Rx-Main

AppModel.cs

using GalaSoft.MvvmLight;
using Leap;
using System;
using System.Collections.ObjectModel;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading;
using System.Windows.Media;

namespace WpfApplication4
{
    public class AppModel : ViewModelBase
    {
        private int windowHeight;

        public int WindowHeight
        {
            get { return this.windowHeight; }
            set { this.Set(ref this.windowHeight, value); }
        }

        private int windowWidth;

        public int WindowWidth
        {
            get { return this.windowWidth; }
            set { this.Set(ref this.windowWidth, value); }
        }

        public ObservableCollection<Tuple<int, int, SolidColorBrush>> Points { get; private set; }

        public AppModel()
        {
            this.WindowHeight = 800;
            this.WindowWidth = 800;
            this.Points = new ObservableCollection<Tuple<int, int, SolidColorBrush>>();
        }

        private Controller leapController;

        private RxListener leapListener;

        public void Start()
        {
            this.leapController = new Controller();
            this.leapListener = new RxListener();
            this.leapController.AddListener(this.leapListener);
            this.leapListener.OnFrameAsObservable()
                .Sample(TimeSpan.FromMilliseconds(50))
                .ObserveOn(SynchronizationContext.Current)
                .Subscribe(c =>
                {
                    this.Points.Clear();
                    var frame = c.Frame();
                    foreach (var p in frame.Pointables)
                    {
                        var normalizedPoint = frame.InteractionBox.NormalizePoint(p.StabilizedTipPosition);
                        var x = normalizedPoint.x * this.WindowWidth;
                        var y = this.WindowHeight - normalizedPoint.y * this.WindowHeight;

                        var color = default(Color);
                        if (p.TouchDistance > 0 && p.TouchZone != Pointable.Zone.ZONENONE)
                        {
                            color = Colors.Azure;
                        }
                        else if (p.TouchDistance <= 0)
                        {
                            color = Colors.Red;
                        }
                        else
                        {
                            color = Colors.LightGray;
                        }

                        this.Points.Add(Tuple.Create((int)x, (int)y, new SolidColorBrush(color)));
                    }
                });
        }

        public void Stop()
        {
            this.leapController.RemoveListener(this.leapListener);
            this.leapController.Dispose();
            this.leapListener = null;
            this.leapController = null;
        }
    }

    class RxListener : Listener
    {
        private Subject<Controller> onFrameProxy = new Subject<Controller>();

        public override void OnFrame(Controller c)
        {
            this.onFrameProxy.OnNext(c);
        }

        public IObservable<Controller> OnFrameAsObservable()
        {
            return this.onFrameProxy.AsObservable();
        }
    }
}

MainWindow.xaml

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
    xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" 
    x:Class="WpfApplication4.MainWindow"
    xmlns:local="clr-namespace:WpfApplication4"
    Title="MainWindow" 
    Height="{Binding WindowHeight, Mode=TwoWay}" 
    Width="{Binding WindowWidth, Mode=TwoWay}">
    <Window.DataContext>
        <local:AppModel />
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Menu>
            <MenuItem Header="Start">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <ei:CallMethodAction TargetObject="{Binding Mode=OneWay}" MethodName="Start"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </MenuItem>
            <MenuItem Header="Stop">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <ei:CallMethodAction TargetObject="{Binding Mode=OneWay}" MethodName="Stop"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </MenuItem>
        </Menu>
        <ItemsControl Grid.RowSpan="2" ItemsSource="{Binding Points}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemContainerStyle>
                <Style TargetType="ContentPresenter">
                    <Setter Property="Canvas.Left" Value="{Binding Item1}" />
                    <Setter Property="Canvas.Top" Value="{Binding Item2}" />
                </Style>
            </ItemsControl.ItemContainerStyle>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Ellipse Width="100" Height="100" Fill="{Binding Item3}" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>