読者です 読者をやめる 読者になる 読者になる

Natural Software

KinectなどのDepthセンサーを中心に活動しています

Kinect for Windows SDK beta で遊んでみた 〜 Kinectで身長を測る 〜 #shibuya_ni

Kinect

Twitter上で見かけたのですが、Kinectを使って身長を測るアプリを作ってる人がいました(現在そのツイートは消えてるようです)。面白そうなのでやってみました。
ポーズなしでスケルトンが取れるところが、かなり効いてます

身長はだいたい171cm位なので、1,2cm高いくらいの誤差が出てますが、まぁテキトウな割には正確だと思われます。うちの奥さんでもだいたいあってる値が出たので、あとは精度をどこまで求めるかでしょうか。

やってること

SDKからとれる三次元の座標の、頭と足を使って計算してるくらいです。
気になるところは

  • 座標の単位はメートル
  • 座標はカメラの位置を0として正負の値になる
  • 頭の位置は、頭の真ん中なので、ユーザーの検出で最初に見つけたY座標をてっぺんとしている
    • 画面の緑の点
    • 手を挙げると、そこからの長さになる
  • 頭の座標を二次元座標に変換し、てっぺんのY座標と入れ替えて、三次元座標に戻して計算している

ソース

// 身長をはかるサンプル
#include <iostream>
#include <sstream>

#include "kinect\nui\Kinect.h"
#include "kinect\nui\ImageFrame.h"

#include <opencv2/opencv.hpp>

// ユーザーの色づけ
const UINT colors[] =
{
    0xFFFFFFFF,    // ユーザーなし
    0xFF00FFFF,
    0xFFFF00FF,
    0xFFFFFF00,
    0xFF00FF00,
    0xFF0000FF,
    0xFFFF0000,
};

void DrawSkeletonSegment( IplImage* Skeleton, CvPoint StartPoint, CvPoint EndPoint)
{
    cvLine( Skeleton, StartPoint, EndPoint, cvScalar( 0, 0, 255 ), 4 );
}

void DrawSkeleton( IplImage* Skeleton, kinect::nui::SkeletonData& skeleton )
{
    // 略
}

void main()
{
    try {
        kinect::nui::Kinect kinect;
        kinect.Initialize( NUI_INITIALIZE_FLAG_USES_COLOR | NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX | NUI_INITIALIZE_FLAG_USES_SKELETON );

        kinect::nui::ImageStream& video = kinect.VideoStream();
        video.Open( NUI_IMAGE_TYPE_COLOR, NUI_IMAGE_RESOLUTION_640x480 );

        kinect::nui::ImageStream& depth = kinect.DepthStream();
        depth.Open( NUI_IMAGE_TYPE_DEPTH_AND_PLAYER_INDEX, NUI_IMAGE_RESOLUTION_320x240 );

        kinect::nui::SkeletonEngine& skeleton = kinect.Skeleton();
        skeleton.Enable();

        // OpenCVの初期設定
        char* windowName = "player";
        ::cvNamedWindow( windowName );
        cv::Ptr< IplImage > playerImg = ::cvCreateImage( cvSize(video.Width(), video.Height()), IPL_DEPTH_8U, 4 );

        while ( 1 ) {
            // データの更新を待つ
            kinect.WaitAndUpdateAll();

            // 次のフレームのデータを取得する(OpenNIっぽく)
            kinect::nui::VideoFrame videoMD( video );
            kinect::nui::DepthFrame depthMD( depth );

            // プレーヤーのてっぺん座標
            int maxHeight = 0;

            // データのコピーと表示
            UINT* img = (UINT*)playerImg->imageData;
            for ( int y = 0; y < videoMD.Height(); ++y ) {
                for ( int x = 0; x < videoMD.Width(); ++x, ++img ) {
                    int player = depthMD( x / 2, y / 2 ) & 0x3;
                    int depth = depthMD( x / 2, y / 2 ) >> 3;

                    // 最初に見つけたプレーヤー座標をてっぺんにする
                    if ( (player != 0) && (maxHeight == 0) ) {
                        maxHeight = y;
                    }

                    // 一定以内でプレーヤがいなかった場合は白くする
                    // ちらかった家の対策
                    if ( (depth >= 1500) && (player == 0) ) {
                        *img = 0xFFFFFFFF;
                    }
                    // そうでなければ描画する
                    else {
                        *img = videoMD( x, y ) & colors[player];
                    }
                }
            }

            kinect::nui::SkeletonFrame skeletonMD = skeleton.GetNextFrame();
            if ( skeletonMD.IsFoundSkeleton() ) {
                skeletonMD.TransformSmooth();

                for ( int i = 0; i < kinect::nui::SkeletonFrame::SKELETON_COUNT; ++i ) {
                    if ( skeletonMD[i].TrackingState() == NUI_SKELETON_TRACKED ) {
                        DrawSkeleton( playerImg, skeletonMD[i] );

                        kinect::nui::SkeletonData& skeleton = skeletonMD[i];

                        // 理想
                        // HEADは顔の中心なので、NuiTransformSkeletonToDepthImageFしたY座標を上に上がって、ユーザーを識別してる一番上まで行く
                        // その座標をNuiTransformDepthImageToSkeletonFしたのが、頭のてっぺんの実座標

                        // 座標を表示座標系にする
                        kinect::nui::SkeletonData::Point p = skeleton.TransformSkeletonToDepthImage( NUI_SKELETON_POSITION_HEAD );
                        kinect::nui::SkeletonData::Point p2;
                        p2.x = p.x * videoMD.Width() + 0.5f;
                        p2.y = p.y * videoMD.Height() + 0.5f;

                        int player = depthMD( p2.x / 2, p2.y / 2 ) & 0x7;
                        int depth = depthMD( p2.x / 2, p2.y / 2 ) >> 3;

                        // 現実
                        // てっぺん座標を設定
                        if ( maxHeight != 0 ) {
                            p2.y = maxHeight;
                        }
                        cvCircle(playerImg, cvPoint( p2.x, p2.y ) , 5,  cvScalar( 0, 255, 0 ), -1 );

                        // 座標を現実座標系に戻す
                        kinect::nui::SkeletonData::Point p3;
                        p3.x = (p2.x - 0.5f) / videoMD.Width();
                        p3.y = (p2.y - 0.5f) / videoMD.Height();
                        Vector4 v = NuiTransformDepthImageToSkeletonF( p3.x, p3.y, p.depth );

                        // 座標の単位はメートルなので、センチにする。ちなみに座標はカメラの位置を0として正負の値になるっぽい
                        float head = v.y * 100;
                        float foot = skeleton[NUI_SKELETON_POSITION_FOOT_LEFT].y * 100;
                        std::cout << "head : " << head << " foot : " << foot << " height : " << abs(foot) + abs(head) << std::endl;
                    }
                }
            }

            ::cvShowImage( windowName, playerImg );

            // 1画面分の書込
            std::stringstream ss;
            ss << "jpg\\" << ::GetTickCount() << ".jpg";
            cvSaveImage( ss.str().c_str(), playerImg );
     
            int key = ::cvWaitKey( 10 );
            if ( key == 'q' ) {
                break;
            }
        }

        ::cvDestroyAllWindows();
    }
    catch ( std::exception& ex ) {
        std::cout << ex.what() << std::endl;
    }
}