Kinect for Windows Sensor + SDK - その3 深度情報

さて、第三弾です。このポストでは、深度情報のセンシングに関する説明を行います。深度だけに、深度情報は奥が深いんだよね…

ま、冗談はさておき、このシリーズのその1、その2を踏まえ、実画像に深度画像を組み合わせて表示する方法を説明していきます。単に深度情報を取得するだけではつまらないので、人も認識できるプログラムです。

先ずは、Visual Studioで一つWPFアプリプロジェクトを作ってください。MainWindow.xamlの<Grid></Grid>にImageを追加しておきます。

    <Grid>
        <Image Name="ImageFrame" HrizontalAignment="Center" VerticalAlignment="Center" Stretch="Uniform"/>
    </Grid>

そして、MainWindow.xaml.csのusing宣言に、

    using Microsoft.Kinect;

を追加します。
そして、MainWindowsクラスに、以下のメンバーを追加します。

        KinectSensor mySensor;

さらに、MainWindowクラスのコンストラクタ、MainWindow()メソッドに、以下の記述を加えます。

            if (KinectSensor.KinectSensors.Count > 0)
                mySensor = KinectSensor.KinectSensors[0];

                mySensor.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30);  ---- ①
                mySensor.DepthStream.Enable(DepthImageFormat.Resolution320x240Fps30);         ---- ②
                mySensor.DepthStream.Range = DepthRange.Near;    ---- ③
                mySensor.SkeletonStream.Enable();     ---- ④
                
                mySensor.DepthFrameReady +=          ---- ⑤
                    new EventHandler<DepthImageFrameReadyEventArgs>(mySensor_DepthFrameReady);

                mySensor.Start();

                if (mySensor.ElevationAngle != 0)    ---- ⑥
                {
                    mySensor.ElevationAngle = 0;    ---- ⑦
                }

最初の方はその2で説明したので、そちらを見てください。このサンプルでは、実画像と深度画像両方を使うので、①で実画像のストリームを、②で深度画像のストリームをEnableにしています。引数は解像度です。ベータの時は同じ列挙型を使っていましたが、正式版では、それぞれのものが用意されています。

③は、40cmからセンスする為の指定です。これを外せば、80cmからのセンスになります。更に人を人として認識したい場合には、SkeletonStreamもEnableにしておきます。④のコードです。
そして、⑤のように、DepthFrameReadyイベントに、ハンドラを登録しておきます。画像の準備が出来るたび(この表現はちょっと微妙ですが)にmySensor_DepthFrameReadyメソッドがコールされます。

⑥では、現在のセンサー本体の傾き角度を見ています。0度でなければ、⑦のコードで0度に戻しています。ま、小技というか。

次にmySensor_DepthFrameReady()の中身を説明していきます。

        short[] depthData;
        byte[] colorFrame32;
        WriteableBitmap outputImage;
        DepthImageFormat lastDepthImageFormat;
        ColorImageFormat lastColorImageFormat;

        private static readonly int Bgr32BytesPerPixel = (PixelFormats.Bgr32.BitsPerPixel + 7) / 8;

        void mySensor_DepthFrameReady(object sender, DepthImageFrameReadyEventArgs e)
        {
            KinectSensor kinectSensor = (KinectSensor)sender;
            SkeltonField.Children.Clear();
            using (DepthImageFrame depthFrame = e.OpenDepthImageFrame())    ---- ①
            {
                if (depthFrame != null)
                {
                    using (ColorImageFrame colorFrame = ((KinectSensor)sender).ColorStream.OpenNextFrame(0))    ---- ②
                    {
                        if (colorFrame != null)
                        {
                            bool formatChanged = (lastDepthImageFormat != depthFrame.Format);
                            if (formatChanged)    ---- ③
                            {
                                depthData = new short[depthFrame.PixelDataLength];
                                lastDepthImageFormat = depthFrame.Format;
                            }
                            depthFrame.CopyPixelDataTo(depthData);    ---- ④

                            formatChanged = (lastColorImageFormat != colorFrame.Format);
                            if (formatChanged)    ---- ⑤
                            {
                                colorFrame32 = new byte[colorFrame.PixelDataLength];
                                videoFrame32 = new byte[colorFrame.Width * colorFrame.Height * Bgr32BytesPerPixel];
                               
                                lastColorImageFormat = colorFrame.Format;
                            }
                            colorFrame.CopyPixelDataTo(colorFrame32);    ---- ⑥

                            byte[] convertedDepthBits = ConvertDepthImage(kinectSensor, depthData, colorFrame32,depthFrame.Width, kinectSensor.DepthStream);    ---- ⑦

                            if (formatChanged)    ---- ⑧
                            {
                                outputImage = new WriteableBitmap(
                                    colorFrame.Width,
                                    colorFrame.Height,
                                    96, 96,
                                    PixelFormats.Bgr32,
                                    null);
                                ImageFrame.Source = outputImage;
                            }
                            outputImage.WritePixels(
                                new Int32Rect(0, 0, colorFrame.Width, colorFrame.Height),
                                convertedDepthBits, colorFrame.Width * Bgr32BytesPerPixel,
                                0);    ---- ⑨
                        }
                    }
                }

            }
        }

順に説明していくと、先ず①、OpenDepthImageFrameメソッドをコールして、深度情報を取り出す為のDepthFrameを取り出します。結果がnullの場合もあるので、nullチェックを忘れずに。

次に②、実画像を取り出す為のColorImageFrameを取り出します。こちらも結果がnullになる場合があるので、nullチェックを忘れずに。

③と⑤のifブロックは、Frameから画像を取り出す為のバッファ領域を確保するコードです。フォーマットに合わせてバッファを確保しておきます。深度情報は、1ピクセルあたり2バイト(short)なので、2バイト×縦ピクセル×横ピクセル、画像情報はフルカラーなので1ピクセルあたり4バイト×縦ピクセル×横ピクセル、ずつ必要です。

確保したバッファを引数に、④、⑥のようにCopyPixelDataTo()メソッドを、それぞれのフレームに対してコールすると、バッファに深度情報、画像情報が1フレーム分コピーされます。⑦でそれを加工するメソッド(自前)を呼び出し、深度情報と実画像を重ねた画像を作ります。出来上がった画像を⑨のコードで、WritableBitmapに書き込みます。

⑧のifブロック内では、フォーマットに合わせて、必要な大きさのWritableBitMapを作成し、XAML上に定義されたImageFrameのソースとして登録しています。これで、完成した画像が表示できます。

さて、次は、⑦の中身を紹介しながら、深度情報を説明していきますが、実際のビットマップ構築は煩雑なのでエッセンスだけを抜き出します。

        byte[] ConvertDepthImage(KinectSensor kinectSensor, short[] depthData,  byte[] colorData,int picxelWidth, DepthImageStream depthStream)
        {
            int tooNearDepth = depthStream.TooNearDepth;    ---- ①
            int tooFarDepth = depthStream.TooFarDepth;         ---- ②
            int unknownDepth = depthStream.UnknownDepth; ---- ③

            for (int i16 = 0; i16 < depthData.Length ; i16++)
            {
                int player = depthData[i16] & DepthImageFrame.PlayerIndexBitmask;    ---- ④
                int realDepth = depthData[i16] >> DepthImageFrame.PlayerIndexBitmaskWidth;    ----⑤

①~③は、深度情報で、とっても近い限界、とっても遠い限界、よくわからない距離感、です。実際にやってみたところ、順に0、4095、-1でした。特に‐1の場合は、Kinectが距離を測れていないという意味なので、例えば、その部分は実画像をそのまま表示する、とかやればよしです。

次のforループは、深度情報を1ワードずつ順に処理する為のループです。並びは上端から下端に向けて、左端→右端のビットの並びです。このサンプルでは、320x240なので、一番上のラインが320ワード、次に2番目のラインが320ワード、・・・と並んでいます。

各ワードは16ビットのビット列で、人を人として認識するモードに設定(SkeletonFrameをEnableしている場合)の場合は、下位3ビットが人情報です。6人まで見分けられます。この下位3ビットが0の場合、そこには人として認識された人の体は無い、を意味します。そして残る上位13ビットが実際の深度情報(mm)です。
ですから、④、⑤のようにDepthImageFrameのプロパティとして用意されている値を使って、論理積、ビットシフトを行って情報を取り出します。

※ベータ版は、このデータ列がbyte[]配列でしたが、short[]配列になって、より操作がしやすくなっています。また、ベータ版の時は、人の体を認識する/しない(SkeletonFrameを使う/使わない)の設定により、ビットの並びが異なっていましたが、正式版では、どちらの場合でも同じビットの意味づけがなされ、同じ操作でデータが取り出せるように変更されています。

後は、実画像のサイズが二倍なので、シンプルに考えれば、深度情報のピクセルを2倍に拡大して実画像(4バイトの並び)に、例えば、Unknownの場合はそのまま実画像を使い、深度情報がある場所は、その深度に応じた色使いで色を付けてやり、人として認識された人の一部である(すいませんくどくて)ピクセルは、それに応じてカラーリングしてやるとなんとなく、そんな感じの画像になるわけです。(この処理は煩雑なわりに本題とは関係ないので飛ばします。色々工夫してみてください)

例えばの画像を参考までに挙げておきますね。

初期化のコードでSkeletonFrameをEnableしなければ、人は人として認識されず、16ビットまるまる、Kinectセンサーの前に存在している3次元世界の、Kinectセンサーからの距離情報になります。
Kinectは人の各部位をセンスするSkeleton機能も滅茶苦茶興味深く、応用もたくさん考えられるのですが、3次元世界のものの座標をそのまま取り込む深度情報も、負けず劣らず、応用の幅は広いので、色々と遊んでみてください。

このポストは、ここまで。
次のポストでは、実画像と深度情報のよりシンプルなマップ方法を説明します。