【はじめての3Dセンサー】OpenNI 2 / NiTE 2 を使ってみる 

## はじめに

### この記事の内容

MacBookAir(モデル:MacBookAir6,2 macOS:10.12.6)にて、OpenNI２ / NiTE2の使い方を学習した際の自分用メモ。(2017/08現在)

#### あらすじ

偶然に「XTION PRO LIVE」(箱も説明もないむきだしの本体のみ)を入手。何も知らない状態から、スタートし、手探りでこれを動かしてみる企画。
[前回の調査](http://qiita.com/yagizo/items/e32daa243c91e112c0ba)で環境構築は完了。ようやく[教科書](https://www.amazon.co.jp/gp/product/4798039160/ref=as_li_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4798039160&linkCode=as2&tag=riceproject0c-22&linkId=c26053198626faa2b1a737e3937bd549)をGETできたので、手元の環境で簡単なアプリを実装し、動作させ、使い方を理解するのが今回のゴール。


#### 対象読者

[OpenNI](http://qiita.com/yagizo/items/e32daa243c91e112c0ba#openni%E3%81%A3%E3%81%A6%E4%BD%95)/[NiTE](http://qiita.com/yagizo/items/e32daa243c91e112c0ba#nite%E3%81%A3%E3%81%A6%E4%BD%95)の知識ゼロ(に近い)人には多少役に立つかも。

# OpenNI/NiTEのお作法を学ぶ

素人なので、以下の本を参考にして、基本から学習を開始する。

* [OpenNI 3Dセンサープログラミング](https://www.amazon.co.jp/gp/product/4798039160/ref=as_li_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4798039160&linkCode=as2&tag=riceproject0c-22&linkId=c26053198626faa2b1a737e3937bd549)

## アプリを作るのに必要な知識

### 全体フロー

Xtionを使ったアプリケーションを作るには、以下の構造でアプリを作るのがわかりやすい(基本的に、OpenNIを使おうがNiTEを使おうが流れは同じ)。

* 前処理
  1. ライブラリ(OpenNI/NiTE)の初期化
  2. デバイス(Xtion)への接続 (※複数のデバイスを同時に利用することが可能)
  3. 対象デバイスの利用するセンサーの初期化
     * 使用するセンサー(RGBカメラ、Depthセンサーなど)用のストリームオブジェクトを作成
     * センサーのパラメーターを設定
     * センサーの開始
* メインループ(アプリ本体機能)
  * センサー情報の利用
     * センサーから取得したフレームを読み出し、加工して何か(例えばPC上に描画など)する　(※[教科書](https://www.amazon.co.jp/gp/product/4798039160/ref=as_li_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4798039160&linkCode=as2&tag=riceproject0c-22&linkId=c26053198626faa2b1a737e3937bd549)ではOpenCVを使って表示)
  * その他
     * 割り込み処理(アプリの終了受付、アプリのその他の機能の実現など) 
* 後処理
  1. センサーの停止
  2. デバイスの切断
  3. ライブラリの後始末

### ウォームアップ

インクルードするヘッダは以下の通り。

```cpp
#include <OpenNI.h>   // OpenNIを使う場合
#include <NiTE.h>　　　　　　　　　　// NiTEを使う場合
```
ビルドするときはインストール先の**~/Include/**ディレクトリをインクルードパスに、**~/Redist/**をリンクライブラリパスに指定してあげること。
※NiTEは実行時に、ライブラリがデータを参照しているので、必要に応じて、**NiTE.ini**の**DataDir**パラメーターを書き換える必要がある。

```ini
[General]
DataDir=./NiTE2
```

### 前処理 / 後処理

ライブラリ(OpenNI, NiTE)のおまじない（初期化及び後始末）について。これらのライブラリ利用する場合、事前・事後に必要な処理となる。

```cpp
// 初期化
openni::OpenNI::initialize();   
nite::NiTE::initialize();

// 後始末
openni::OpenNI::shutdown();   
nite::NiTE::shutdown();
```

デバイスの接続及び切断。事前にXtionはUSB接続しておく。
下記は、挿さっている１台のXtionを使う想定。複数台制御する場合は、あらかじめデバイスのURIを取得し、デバイスの識別情報を指定して制御するみたい。

```cpp
openni::Device device;

// 有効ないずれかのXtionと接続
openni::Status ret = device.open( openni::ANY_DEVICE );
if ( ret != openni::STATUS_OK ) {
    throw std::runtime_error( "openni::Device::open() failed." );
}

// Xtionから切断
device.close();
```

各センサーの初期処理及び後始末。接続したXtionのセンサーからの情報を受け取るために必要なお作法。

```cpp
openni::VideoStream stream;

// 初期処理。デバイスオブジェクトから、対象センサーのストリームを作成し、データの読み出しを開始
stream.create( device, openni::SENSOR_COLOR );
/* --必要に応じて、パラメーターを設定-- */
stream.start();

// 後始末
stream.stop();
```

### メイン処理

各センサーのストリームからフレーム情報を読み出し、その内容を変換、使用する処理を書く。いわゆるアプリの機能実装はここ。センサー毎にお作法が違うので、雰囲気だけ。

```cpp
openni::VideoFrameRef frame;

stream.readFrame( &frame);
/* --　 *
   読み込んだフレームを使って何かする
 * -- */

```

## サンプルアプリを作る

XtionのRGBカメラとDepthセンサーから取得したデータを、OpenCVを使ってPCの画面に表示するアプリを作る。これで、大体のプログラムの構造はわかる。

### コーディング

ソースコードは以下の通り。

```cpp:SampleXtionApp.cpp
#include <OpenNI.h>
#include <opencv2/opencv.hpp>
#include <iostream>
#include <stdexcept>

#define DEBUG_MSG( msg ) std::cout << msg << std::endl;
#define ERROR_MSG( msg ) std::cerr << msg << std::endl;

/**
 * Xtion PRO LIVE サンプルアプリケーション
 */
class SampleXtionApp
{

public:

    /**
     * 後始末(デストラクタ)。 センサーを停止、デバイスから切断を行う。
     */
    ~SampleXtionApp()
    {
        colorStream.stop();
        depthStream.stop();
        device.close();
    }

    /**
     * 初期化処理。 デバイスへ接続し、センサーの初期化、開始処理を行う。
     */
    void initialize()
    {
        openni::Status ret = device.open( openni::ANY_DEVICE );
        if ( ret != openni::STATUS_OK ) {
            throw std::runtime_error( "openni::Device::open() failed." );
        }

        colorStream.create( device, openni::SENSOR_COLOR );
        colorStream.start();

        depthStream.create( device, openni::SENSOR_DEPTH );
        depthStream.start();
    }

    /**
     * 更新(メイン)処理。各センサーストリームから読み出したフレームを処理し、OpenCVを使って
     * 画面に表示する処理を行う。
     */
    void update()
    {
        openni::VideoFrameRef colorFrame;
        colorStream.readFrame( &colorFrame);
        colorImage = showColorStream( colorFrame ); // フレームの変換(メインロジック)
        cv::imshow( "Color Stream", colorImage );

        openni::VideoFrameRef depthFrame;
        depthStream.readFrame( &depthFrame);
        depthImage = showDepthStream( depthFrame ); // フレームの変換(メインロジック)
        cv::imshow( "Depth Stream", depthImage );
    }


private:
    openni::Device device;
    openni::VideoStream colorStream;
    openni::VideoStream depthStream;

    cv::Mat colorImage;
    cv::Mat depthImage;

    /**
     * RGBストリームから取得したフレームをを画面に表示できる形式に変換する。
     *
     * @param[in] colorFrame フレーム
     * @return 画像情報
     */
    cv::Mat showColorStream( const openni::VideoFrameRef &colorFrame )
    {
        cv::Mat colorImage = cv::Mat( colorFrame.getHeight(),
                                      colorFrame.getWidth(),
                                      CV_8UC3,
                                      ( unsigned char* )colorFrame.getData() );
        cv::cvtColor( colorImage, colorImage, CV_RGB2BGR );
        return colorImage;
    }

    /**
     * Depthストリームから取得したフレームを画面に表示できる形式に変換する。
     *
     * @param[in] depthFrame フレーム
     * @return 画像情報
     */
    cv::Mat showDepthStream( const openni::VideoFrameRef &depthFrame )
    {
        cv::Mat depthImage = cv::Mat( depthFrame.getHeight(),
                                      depthFrame.getWidth(),
                                      CV_16UC1,
                                      ( unsigned short* )depthFrame.getData() );
        // 0〜10000mmの範囲のデータを0〜255(8ビット)のデータに変換する
        depthImage.convertTo( depthImage, CV_8U, 255.0 / 10000 );
        return depthImage;
    }
};


int main( int argc, const char * argv[] )
{
    try {
        DEBUG_MSG("=== Start SampleColorImageViewer ===");
        openni::OpenNI::initialize();

        SampleXtionApp app;
        app.initialize();

        while ( 1 ) {
            app.update();
            if ( cv::waitKey( 10 ) == 'q' ) {
                break;
            }
        }

    } catch( std::exception &ex ) {
        ERROR_MSG( openni::OpenNI::getExtendedError() );
        return 1;
    }

    openni::OpenNI::shutdown();
    DEBUG_MSG("=== End SampleColorImageViewer ===");
    return 0;
}
```
### ビルド＆実行

ビルドするのには、Cmakeを使った。

```cmake:CMakeLists.txt
cmake_minimum_required (VERSION 2.6)

project (XtionSample)

include_directories(${OPENNI2_INCLUDE_DIR})
include_directories(${OPENCV_INCLUDE_DIR})

SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${OPENNI2_LIBRARY} -lOpenNI2")
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${OPENCV_LIBRARY} -lopencv_core -lopencv_highgui -lopencv_imgproc")

add_executable (SampleXtionApp SampleXtionApp.cpp)

```

事前に各ライブラリの収録物については環境構築時に環境変数を定義していたので、ビルド時と実行時に忘れないように指定する。

```bash
$ cmake -DOPENNI2_LIBRARY=$OPENNI2_REDIST -DOPENNI2_INCLUDE_DIR=$OPENNI2_INCLUDE -DOPENCV_LIBRARY=$OPENCV_REDIST -DOPENCV_INCLUDE_DIR=$OPENCV_INCLUDE
$ make
$ export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:$OPENNI2_REDIST
$ ./SampleXtionApp
```
XtionをUSBに接続し、ビルドして実行すると、無事アプリが起動し、

![sample_app.png](https://qiita-image-store.s3.amazonaws.com/0/55900/dba49d5b-d80c-b1c8-7690-11bcb9fc5cb5.png)

"q"キーを押して終了することを確認。やったー！！なんか、普通に嬉しい♪

## 次にやること

[次回](https://qiita.com/yagizo/items/b6f4e63f03004cb0efd6) はいよいよ、アプリ作成。
モーションセンサーで、Keynoteをコントロールするおもちゃを作成する予定。

## 付録

### 参照リンク

* [OpenNIのAPIドキュメント](http://www.openni.ru/wp-content/doxygen/html/index.html)
* [OpenNIとNiTEのバージョン2(Beta)が公開されています - Natural Software](http://www.naturalsoftware.jp/entry/2012/12/18/221257)