2011/04/18

Kinectプチハック(エピソード2)

【ミッション: OpenCVでNiSimpleViewerもどきを作ろう】

前回、Kinect の深度データと RGB 画像を OpenCV の IplImage で取得する実験をしてみた。

この調子で、OpenNI Tips に載っている『デプスと画像を重ね合わせするデモのサンプルコード』を試してみよう。


…って、あれ? ずれていてうまく重ならない。なにこれダメじゃん。

どうやら、深度センサとカメラの位置が微妙にずれているため、単なるオーバレイ表示にしただけではダメらしい。



ところが、OpenNI に付属の NiSimpleViewer というサンプルでは、見事に深度データと画像がぴたっと重なった結果が得られている。

幸い、NiSimpleViewer のソースコードも同梱されていたので、深追いする事ができそうだ。

このサンプルは OpenGL 向けだが、解読してみる価値はある。

複雑な射影幾何の知識が必要かと思いきや、調べてみたところ、次の関数を実行するだけで深度データの座標をカメラ画像座標に一致させる事ができた。
// 深度データの座標ををカメラに合わせる
depth.GetAlternativeViewPointCap().SetViewPoint(image);
depthxn::DepthGenerator のインスタンス、imagexn::ImageGenerator のインスタンス)。



せっかくなので、お勉強も兼ねて、OpenCV でカメラ画像と深度を得るプログラムをぼくなりに作ってみたよ。

ただし、『OpenNI Tips』のように、深度データの抜けを補うような小細工はしていない(ちなみにぼくの環境は VC++ 2008 と OpenCV 2.0 だよ。特に OpenCV は、バージョンによってインクルードするヘッダファイルやリンクするライブラリのファイル名が微妙に違う場合があるので注意してね)。

#include<cv.h>
#include<highgui.h>
#include<XnCppWrapper.h>

#pragma comment(lib,"cv200.lib")
#pragma comment(lib,"cxcore200.lib")
#pragma comment(lib,"highgui200.lib")
#pragma comment(lib,"C:/Program files/OpenNI/Lib/openNI.lib")

const char* SAMPLE_XML_PATH = "C:/Program Files/OpenNI/Data/SamplesConfig.xml";

const int IMAGE_WIDTH = 640;
const int IMAGE_HEIGHT = 480;

int main() {
    xn::Context context;
    xn::EnumerationErrors errors;

    context.InitFromXmlFile(SAMPLE_XML_PATH);
  
    xn::DepthGenerator depth;  
    context.FindExistingNode(XN_NODE_TYPE_DEPTH, depth); 

    xn::ImageGenerator image;
    context.FindExistingNode(XN_NODE_TYPE_IMAGE, image);

    xn::DepthMetaData depthMD;
    xn::ImageMetaData imageMD;

    int key = 0;

    // 大人の事情で、IplImageで管理することに。
    cv::Ptr<IplImage> iplimage, ipldepth, overlay;

    iplimage = cvCreateImage(cvSize(IMAGE_WIDTH,IMAGE_HEIGHT),
        IPL_DEPTH_8U, 3);

    ipldepth = cvCreateImage(cvSize(IMAGE_WIDTH,IMAGE_HEIGHT),
        IPL_DEPTH_8U, 1);
   
    overlay = cvCreateImage(cvSize(IMAGE_WIDTH,IMAGE_HEIGHT),
        IPL_DEPTH_8U, 3);

    const int BIT = 12; // 深度センサの有効ビット範囲

    while (key!='q') {
        context.WaitAnyUpdateAll();

        // 深度・画像メタデータ取得
        depth.GetMetaData(depthMD);
        image.GetMetaData(imageMD);
        
        // 深度データの座標ををカメラに合わせる
        depth.GetAlternativeViewPointCap().SetViewPoint(image);
        
        // ==============================
        // 各IplImage構造体にデータを格納
        // ==============================

        // カメラデータはそのまま取得
        iplimage->imageData = (char *)imageMD.WritableData();
        cvCvtColor(iplimage, iplimage, CV_BGR2RGB);

        // オーバレイ用の画像にコピー
        cvCopy(iplimage, overlay);

        // Kinectの深度データは符号つき16ビット整数
        // ⇒これをモノクロ256階調に変換して格納する
        //  ※ RGBカメラの階調に合わせている
        for(int y = 0; y < IMAGE_HEIGHT; ++y) {

            // ipldepthのピクセルデータへのポインタ
            unsigned char* pDepthImgData = (unsigned char*)(
                ipldepth->imageData + y * ipldepth->widthStep
            );

            // Kinectの深度データのポインタ
            short* pKinectDepth = (short*)(
                depthMD.WritableData() + y * ipldepth->widthStep
            );

            // iplimageのピクセルデータへのポインタ
            // ※ オーバレイ表示が不要の場合は要らない
            unsigned char* pCameraImgData = (unsigned char*)(
                overlay->imageData + y * iplimage->widthStep
            );
            
            for(int x = 0; x < IMAGE_WIDTH; ++x) {
                // Kinectの深度データを取得・スケーリング
                int depthValue = (int)(
                    255.0 * pKinectDepth[x] / ((0x1 << BIT) - 1)
                );

                // 深度データの設定(指定した範囲を超えた場合は無視)
                pDepthImgData[ipldepth->nChannels * x] = 
                    depthValue > 255 ? 
                        (unsigned char)0 : (unsigned char)-depthValue;

                // オーバレイ表示
                // もし深度が無効値(0)じゃなかったら…
                if(pDepthImgData[ipldepth->nChannels * x] != 0) {
                    pCameraImgData[iplimage->nChannels * x + 0] = 0;  // B

                    pCameraImgData[iplimage->nChannels * x + 1] =     // G
                        pCameraImgData[iplimage->nChannels * x + 2] = // R
                            pDepthImgData[ipldepth->nChannels * x];   // D
                }
            }
        }

        // iplImageはcvShowImageで表示
        cvShowImage("depth", ipldepth);
        cvShowImage("image", iplimage);
        cvShowImage("overlay", overlay);

        key = cv::waitKey(33);
    }

    context.Shutdown();
    return 0;
}

上記ソースコード中で IplImage の各画素値へのアクセスにポインタを用いているのは、それが最も効率のよい方法だからである。

Gary Bradski, adrian Kaehler 著、松田晃一訳『詳解 OpenCV』の第3章には、画像データにアクセスするための以下のようなコード例が載っており、上記のプログラムはそれに準じた形となっている。

// HSV画像のSとVの部分だけを最大化(飽和)させる
void saturate_sv(IplImage* img) {
    for(int y=0; y<img->height; y++) {
        uchar* ptr = (uchar*) (
            img->imageData + y * img->widthStep
        );
        for(int x=0; x<img->width; x++) {
            ptr[3*x+1] = 255;
            ptr[3*x+2] = 255;
        }
    }
}


実行結果は以下の通り。

見た目は NiSimpleViewer に近いものになっているが、決定的な違いはレンダリングに OpenGL ではなく OpenCV を使っているという事(ときにはこっちの方が便利な事もあるのだ)。




これでよし! ぴったり重なった!

7 件のコメント:

  1. はじめまして。
    佐々木と申します。

    環境についてお聞きしたいのですが、Visual Stdio2008のC++のコードでやっておられますでしょうか?
    「デプスと画像を重ね合わせするデモのサンプルコード」
    でif文やreturn文ですらエラーになってしまうので、お聞きしました。

    答えていただけると幸いです。

    返信削除
  2. コメントありがとうございます。

    私が使用している開発環境は Visual Studio 2008 Professional で、言語は C++ です。

    もしかしたら、ライブラリのバージョンが異なるため、従来のソースコードがビルドできない可能性があります。

    ご参考までに、このページのサンプルでは
    ・ OpenCV 2.0
    ・ OpenNI 1.0.0.23
    ・ Nite 1.3.0.17
    (いずれも32ビット版)を使用しています。

    返信削除
  3. 返信いただきありがとうございます。

    教えて頂いたライブラリが一緒でしたので、よく調べてみたところ、
    using namespace cv;
    が抜けているのが原因でした。
    失礼致しました。

    最後に、depth16とimniが再定義されていると、エラーが出てしまっているのですが分かりますでしょうか?

    お聞きしてばかりで申し訳ありません。

    返信削除
  4. > 最後に、depth16とimniが再定義されていると、エラーが出てしまっているのですが分かりますでしょうか?

    おそらく、同じ名前の変数を複数の場所で宣言(定義)してしまっている可能性が高いです。

    Visual Studio のクイック検索機能(Ctrl + f)を用いて、ソースコードで「depth16」や「imni」がどのように用いられているかを洗い出してみてください。

    もしも、
    cv::Mat depth16 …
    のような変数の宣言(定義)が複数箇所で見つかった場合は、状況に応じて以下の対策を採ります。

    ・ 変数名を変更する
    ・ 後方の変数宣言を削除する(同じ変数を使い回す)

    どちらを選ぶとよいかは状況によって変わるため、一概には言えませんが、このケースではコンパイラの吐き出すメッセージが大きなヒントになります。

    根気よくバグを取りましょう。

    返信削除
  5. すいません!
    たーせるさんのプログラムで実行することが出来ました。

    たびたび失礼いたしました…

    返信削除
  6. たびたびすいません。
    最後にお聞きしたいのですが、kinectのデプスをカメラ画像では無く単なる画像でも重ね合わせることは可能でしょうか?


    お答いただけると幸いです。

    返信削除
  7. ご質問ありがとうございます。

    > 最後にお聞きしたいのですが、kinectのデプスをカメラ画像では無く単なる画像でも重ね合わせることは可能でしょうか?

    本文中のソースコードでは、カメラ画像も深度データ(デプス)も同じ IplImage という構造体に変換して格納しています。

    ですので、cvLoadImage()等を用いて読み込んだ任意の画像データと Kinect の深度データを重ね合わせて表示する事は可能です。

    さて、データを重ね合わせる際には IplImage 構造体の画像データにアクセスする必要があり、そのために本文中のソースコードではポインタ演算を用いています(詳しくはこちらをご覧ください)。

    本文中ではさらっと流していますが、画像の重ね合わせ(画像データへのアクセス)は、画像のサイズやビット深度、そしてチャンネル数の影響を受けやすい、大変デリケートな処理です。

    IplImage 構造体の仕様をよく知らないままプログラムを書くとバッファオーバーフローを引き起こしてしまうおそれもあります。重ね合わせたい画像同士の仕様をよくよく比較し、注意深くプログラミングする必要があるでしょう。

    ちなみに、本文中のソースコードにおける Kinect カメラ画像および深度データの仕様は以下の通りです。

    Kinectカメラ画像 (iplimage)
      サ イ ズ:640×480px
      ビット深度:符号なし8ビット整数
      チャネル数:3(BGR)
      
    深度データ (ipldepth)
      サ イ ズ:640×480px
      ビット深度:符号なし8ビット整数
      チャネル数:1

    もし不安でしたら、まずは練習のために、仕様を統一した画像データ同士の重ね合わせから始められてみてはいかがでしょうか。

    返信削除

ひとことどうぞφ(・ω・,,)