ブログ@kaorun55

HoloLensやKinectなどのDepthセンサーを中心に書いています。

OpenNI + C# + WPFで最小の骨格追跡のコード #openni_ac

このエントリはOpenNI Advent Calendar 2011 : ATNDの12月7日分です!!
Advent Calendarでの、僕の全プロジェクトはこちらです


OpenNI 1.4.0.2から骨格の追跡にポーズが不要になりました。
これを前提にすると、どこまでコードが短くなるのか試してみました。
比較対象のソースは、KINECTセンサープログラミングの骨格追跡にします

見た目の解説

特に変わらず、Imageを一つはっつけてます

<Window x:Class="SkeletonWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="521" Width="661" Closing="Window_Closing">
    <Grid>
        <Image Height="480" HorizontalAlignment="Left" Name="image1" Stretch="Fill" VerticalAlignment="Top" Width="640
               " />

    </Grid>
</Window>

中身の解説

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using OpenNI;

namespace SkeletonWPF
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        Context context;
        DepthGenerator depth;
        UserGenerator user;

        private Thread readerThread;
        private bool shouldRun;

        public MainWindow()
        {
            InitializeComponent();

            try {
                // ContextとImageGeneratorの作成
                ScriptNode node;
                context = Context.CreateFromXmlFile( "../../SamplesConfig.xml", out node );
                context.GlobalMirror = false;
                depth = context.FindExistingNode( NodeType.Depth ) as DepthGenerator;

                user = new UserGenerator( context );
                user.NewUser += new EventHandler<NewUserEventArgs>( user_NewUser );

                if ( user.SkeletonCapability.DoesNeedPoseForCalibration ) {
                    throw new Exception( "最新のOpenNIをインストールしてください" );
                }

                user.SkeletonCapability.CalibrationStart +=
                    new EventHandler<CalibrationStartEventArgs>( SkeletonCapability_CalibrationStart );
                user.SkeletonCapability.CalibrationComplete +=
                    new EventHandler<CalibrationProgressEventArgs>(SkeletonCapability_CalibrationComplete);
                user.SkeletonCapability.SetSkeletonProfile( SkeletonProfile.All );

                context.StartGeneratingAll();


                // 画像更新のためのスレッドを作成
                shouldRun = true;
                readerThread = new Thread( new ThreadStart( ReaderThread ) );
                readerThread.Start();
            }
            catch ( Exception ex ) {
                MessageBox.Show( ex.Message );
            }
        }

        void SkeletonCapability_CalibrationComplete( object sender, CalibrationProgressEventArgs e )
        {
            // キャリブレーション成功
            if ( e.Status == CalibrationStatus.OK ) {
                Trace.WriteLine( "Calibration Success:" + e.ID );
                user.SkeletonCapability.StartTracking( e.ID );
            }
            // キャリブレーション失敗
            else {
                Trace.WriteLine( "Calibration Failed:" + e.ID );
            }
        }

        void SkeletonCapability_CalibrationStart( object sender, CalibrationStartEventArgs e )
        {
            Trace.WriteLine( "Calibration Start:" + e.ID );
        }

        void user_NewUser( object sender, NewUserEventArgs e )
        {
            Trace.WriteLine( "New User:" + e.ID );
            user.SkeletonCapability.RequestCalibration( e.ID, true );
        }

        private void Window_Closing( object sender, System.ComponentModel.CancelEventArgs e )
        {
            shouldRun = false;
        }

        private void ReaderThread()
        {
            while ( shouldRun ) {
                context.WaitAndUpdateAll();
                DepthMetaData depthMD = depth.GetMetaData();

                this.Dispatcher.BeginInvoke( DispatcherPriority.Background, new Action( () =>
                {
                    // アンマネージド配列をマネージド配列に変換する
                    Int16[] depthArray = new Int16[depthMD.XRes * depthMD.YRes];
                    Marshal.Copy( depthMD.DepthMapPtr, depthArray, 0, depthArray.Length );
                    for ( int i = 0; i < depthArray.Length; i++ ) {
                        depthArray[i] = (Int16)(0xffff - (0xffff * depthArray[i] / depth.DeviceMaxDepth));
                    }

                    // 描画可能なビットマップを作る
                    // http://stackoverflow.com/questions/831860/generate-bitmapsource-from-uielement
                    RenderTargetBitmap bitmap =
                        new RenderTargetBitmap( depthMD.XRes, depthMD.YRes, 96, 96, PixelFormats.Default );

                    DrawingVisual drawingVisual = new DrawingVisual();
                    using ( DrawingContext drawingContext = drawingVisual.RenderOpen() ) {
                        // グレースケールの描画
                        var xtion = BitmapSource.Create( depthMD.XRes, depthMD.YRes,
                                                        96, 96, PixelFormats.Gray16, null, depthArray,
                                                        depthMD.XRes * depthMD.BytesPerPixel );
                        drawingContext.DrawImage( xtion, new Rect( 0, 0, depthMD.XRes, depthMD.YRes ) );

                        // 骨格の描画
                        var users = user.GetUsers();
                        foreach ( var u in users ) {
                            if ( !user.SkeletonCapability.IsTracking( u ) ) {
                                continue;
                            }

                            foreach ( SkeletonJoint s in Enum.GetValues( typeof( SkeletonJoint ) ) ) {
                                if ( !user.SkeletonCapability.IsJointAvailable( s ) ) {
                                    continue;
                                }
                                var joint = user.SkeletonCapability.GetSkeletonJoint( u, s );
                                var point = depth.ConvertRealWorldToProjective( joint.Position.Position );
                                drawingContext.DrawEllipse( new SolidColorBrush( Colors.Red ),
                                    new Pen( Brushes.Red, 1 ), new Point( point.X, point.Y ), 5, 5 );
                            }
                        }
                    }

                    bitmap.Render( drawingVisual );

                    image1.Source = bitmap;
                } ) );
            }
        }
    }
}
初期化

ポーズの検出がいらないので、ユーザーの検出と、キャリブレーションのイベントだけ実装します

// OpenNIの初期化
ScriptNode node;
context = Context.CreateFromXmlFile( "../../SamplesConfig.xml", out node );
context.GlobalMirror = false;

// depthの作成
depth = context.FindExistingNode( NodeType.Depth ) as DepthGenerator;

// ユーザーの作成
user = new UserGenerator( context );

// ポーズが必要な場合は、最新版を入れてもらう
if ( user.SkeletonCapability.DoesNeedPoseForCalibration ) {
    throw new Exception( "最新のOpenNIをインストールしてください" );
}

// ユーザー検出、キャリブレーション完了のイベントを登録する
user.NewUser += new EventHandler<NewUserEventArgs>( user_NewUser );
user.SkeletonCapability.CalibrationComplete += new EventHandler<CalibrationProgressEventArgs>(SkeletonCapability_CalibrationComplete);

// すべての骨格を追跡する
user.SkeletonCapability.SetSkeletonProfile( SkeletonProfile.All );

// 動作を開始する
context.StartGeneratingAll();
ユーザーの検出

ユーザーを検出したら、即座にキャリブレーションを開始します

void user_NewUser( object sender, NewUserEventArgs e )
{
    Trace.WriteLine( "New User:" + e.ID );
    user.SkeletonCapability.RequestCalibration( e.ID, true );
}
キャリブレーション完了

成否を見て、成功なら骨格の追跡を開始します

void SkeletonCapability_CalibrationComplete( object sender, CalibrationProgressEventArgs e )
{
    // キャリブレーション成功
    if ( e.Status == CalibrationStatus.OK ) {
        Trace.WriteLine( "Calibration Success:" + e.ID );
        user.SkeletonCapability.StartTracking( e.ID );
    }
    // キャリブレーション失敗
    else {
        Trace.WriteLine( "Calibration Failed:" + e.ID );
    }
}

まとめ

初期化の項目や、イベントの数が大幅に減ったために、すっきりしたコードになりました。
以降のバージョンでは、キャリブレーションも自動で行われると、KINECT SDKのようにさらにスッキリしたコードになりそうですね