之前已經有寫過 OpenNI 2 的基本教學,如果只是要透過 OpenNI 2 的介面,來讀取深度、彩色影像,只要照著寫,應該不會有什麼問題;而如果是希望有畫面可以呈現的話,Heresy 也有提供整合 OpenCV 的範例,可以拿來做畫面的呈現。
不過,在之前的範例裡,基本上 Heresy 使用的方式,都是在程式的主迴圈裡面,由開發者主動地透過 readFrame() 這個函式,來向 OpenNI 的 VideoStream 要求新的資料;而實際上,OpenNI 2 還有提供另一種「Event Base」的「Listener」模式,可以讓 VideoStream 有拿到新資料的時候,主動地去執行指定的功能~如果是使用這種模式的話,基本上就可以不用透過自己建立主迴圈,來做資料的更新了。
而如果要使用這種模式的話,首先,我們必須要去定義當 VideoStream 拿到新的資料的時候,要做那些事;而這邊定義的方法,就是透過繼承 VideoStream::NewFrameListener 這個類別,實作出一個屬於自己的 Listener 類別,來做自己想要做的事。
而最簡單的寫法,大概就會像下面這樣子:
class OutputDpeth : public VideoStream::NewFrameListener
{
public:
// this function will be called when video stream get new frame
void onNewFrame( VideoStream& rStream )
{
VideoFrameRef vfDepth;
if( rStream.readFrame( &vfDepth ) == STATUS_OK )
{
// ... Do Something here
}
}
};
上面這邊的 OutputDepth 這個類別,基本上就是去繼承 VideoStream::NewFrameListener 這個類別時做出來的;而裡面唯一一個一定要實作的函式,就是 onNewFrame() 這個函式。
而實際上,onNewFrame() 這個函式,就是當 VideoStream 接收到新的畫面時,會去主動執行的動作。這個函式在被呼叫時,會把 VideoStream 本身的參考傳進來,讓程式開發者可以使用;也由於在被呼叫時,只會收到 VideoStream 的參考,所以在裡面,還是需要透過 readFrame() 來讀取新的畫面、也就是 VideoFrameRef 形式的資料。由於也是使用 readFrame() 去讀取新的 VideoFrameRef,所以之後的用法就和之前完全相同了~
在定義好自己的 listener class 後,接下來要怎麼設定呢?VideoStream 有提 addNewFrameListener() 和 removeNewFrameListener() 這兩個函式,可以用新增、移除 listener 的物件。最簡單的使用發法,就是在呼叫 start()、要求 VideoStream 開始讀取資料之前,先建立一個 OutputDepth 的物件,並且透過 addNewFrameListener(),把這個物件的指標傳入,讓 VideoStream 知道有新畫面的時候要做哪些事。
下面就是簡單的使用範例:
OutputDpeth mOutputDepth;
vsDepth.addNewFrameListener( &mOutputDepth );
在這樣設定過後,之後就可以在程式裡面繼續做其他事情,當 VideoStream 讀到新的畫面時,就會在另一個 thread 執行 OutputDepth 的 onNewFrame() 的內容了~
下面就是完整的範例:
// STL Header
#include <iostream>
#include <string>
 
// 1. include OpenNI Header
#include <OpenNI.h>
 
// using namespace
using namespace std;
using namespace openni;
 
/**
* The listener for depth video stream
*/
class OutputDpeth : public VideoStream::NewFrameListener
{
public:
// this function will be called when video stream get new frame
void onNewFrame( VideoStream& rStream )
{
VideoFrameRef vfDepth;
if( rStream.readFrame( &vfDepth ) == STATUS_OK )
{
// a1 get data array
const DepthPixel* pDepth
= static_cast<const DepthPixel*>( vfDepth.getData() );
 
// a2 output the depth value of center point
int w = vfDepth.getWidth(),
h = vfDepth.getHeight();
int x = w / 2, y = h / 2;
cout << pDepth[ x y * w ] << endl;
 
// a3 release frame
vfDepth.release();
}
else
{
cerr << "Can not read frame\n";
cerr << OpenNI::getExtendedError() << endl;
}
}
};
int main( int argc, char** argv )
{
// 2. initialize OpenNI
OpenNI::initialize();
 
// 3. open a device
Device devDevice;
devDevice.open( ANY_DEVICE );
 
// 4. create depth stream
VideoStream vsDepth;
vsDepth.create( devDevice, SENSOR_DEPTH );
 
OutputDpeth mOutputDepth;
vsDepth.addNewFrameListener( &mOutputDepth );
 
vsDepth.start();
 
// 5 wait any input (some char and Enter) to quit
string sInput;
cin >> sInput;
 
// 6 stop reading
vsDepth.removeNewFrameListener( &mOutputDepth );
vsDepth.stop();
 
// destroy depth stream
vsDepth.destroy();
 
// close device
devDevice.close();
 
// 7. shutdown
OpenNI::shutdown();
 
return 0;
}
可以看到,上面的程式在呼叫 VideoStream vsDepth 的 start()、開始讀取資料後,就是透過 cin 這個標準輸入串流、來等待使用者透過鍵盤輸入一個字串(輸入任意字元、然後按下Enter)(上面的程式碼裡面、「5」的部分);而在使用者輸入之前,如果 VideoStream 有毒取道資料,就會去執行 mOutputDepth 的 onNewFrame() 這個函式的內容,也就是把畫面中的中央點的深度做輸出。
當使用者輸入了一些文字、按下 Enter 後,就會繼續執行接下來的 removeNewFrameListener() 以及 stop()(上面的程式碼裡面、「6」的部分),然後把整個 OpenNI 環境關閉、結束程式。
上面基本上就是 OpenNI 2 提供的 Listener 模式的使用方法。而除了 VideoStream 有 NewFrameListener 外,OpenNI 這個用來做整個環境管理的類別,也有提供 DeviceConnectedListener、DeviceDisconnectedListener 和 DeviceStateChangedListener 這三種用來偵測 Device 狀態的 listener 可以使用,分別是對應到「連接上新的裝置」、「裝置離線」、「裝置狀態改變」。
因為這三種 listener 類別對應的事件不同、所以必須要實作的函式也不相同,如果有興趣的話,可以參考 OpenNI 官方的範例 Samples\EventBasedRead,這便就不詳細說明了。
而另外像是 PrimeSense NiTE 2 的 UserTacker 和 HandTracker,也都有提供 NewFrameListener 可以使用,所以有需要的話,NiTE 也是可以使用這種 Listener 模式(Event base)來做程式開發的。
不過,透過種模式來使用 OpenNI 的時候,要注意的一點就是,實際上這種開發模型已經是 multi-thread、多執行序的程式開發了!所以在開發的時候,如果有要在不同 thread 共用資源的話,一定要非常小心。而如果是使用像是 Qt 或是 glut 這類的框架的時候,在使用上也會有一些額外的限制,基本上也都能直接在其他 thread、去修改相關的東西(也就是,不能在 Listener 裡面,直接去修改 OpenGL 的東西、或是 Qt 的 UI),這點是使用上比較麻煩的地方。