在 OpenNI 環境同時使用多個 Kinect

| | 22 Comments| 16:24
Categories:

這一篇基本上是延續上一篇《在 OpenNI 管理多個裝置》,繼續來研究怎麼在 OpenNI 的環境裡,同時使用一台電腦上的多個裝置了∼由於開始要寫的時候,Heresy 才發現自己也有不確定的地方,所以索性就到 OpenNI 官方論壇去發問了;而結果,也有 PrimeSense 的人幫忙給了段範例,有興趣的人可以參考《Problem with using multiple device》這篇的回應。

根據這邊的範例程式,可以看的出來,這邊的基本概念,是使用同一個 xn::Context先透過列舉出來的 xn::NodeInfo 個別建立出 xn::Device 後,再以 device 的 instance name 作為 query 的條件(xn::Query)、以此來建立對應特定 device 的其他 production node 了∼

而為了顯示上的方便,這邊 Heresy 一樣是使用 Nokia Qt 來做圖形界上面上的顯示,這部分的程式,建議請參考之前的《使用 Qt GraphicsView 顯示 OpenNI 影像資料》一文。程式在執行後,會去列出所有電腦裡的 OpenNI 相容裝置,針對每一個 Device 都開一個 Qt 的 Graphics View 的視窗來同時顯示 Color Generator 的彩色影像和 Depth Generator 的深度影像;像下圖就是 Heresy 電腦上,有連接兩台 Kinect 時的結果(理論上可以更多,但是就沒試過)。

而完整的程式碼,可以到 Heresy 的 SkyDrive 上下載。接下來,就開始做簡單的說明吧∼

首先,這個程式裡除了 main() 以外,Heresy 還定義了兩個 Class,分別是 QOpenNIWindowsCDeviceWindow。前者主要的目的,是用來做 OpenNI 的 context 管理,以及 QT 的 timer 的控制(定時去更新畫面);後者則是對應到個別的 Device,主要用來記錄 OpenNI 的各種 Nodes、並且處理影像的實際更新。


CDeviceWindow

首先,先來看一下 CDeviceWindow 這個類別的內容。他基本上是用來對應 OpenNI 環境裡的個別的單一個 device(Heresy 這邊就是 Microsoft Kinect),所以裡面儲存了這個裝置的、各種有用到的 node;在這裡,就是 mDevice(device node)、mDepth(depth generator)和 mImage(image generator)。

此外,為了要使用 Qt 的 Graphics View 來做視覺上的呈現,所以也還有 qSceneqView 等其他的 member data,這些就等有用到再講了。

不過,這部分主要是 OpenNI 和 Qt 之間的連動,如果只是想看怎麼樣在 OpenNI 裡面同時使用多個裝置的話,可以跳過這一段,直接跳到下面 QOpenNIWindows 的部分。

而下面,就是 CDeviceWindow 這個類別的內容:

// class to handle OpenNI Device and Qt Window
class CDeviceWindow
{
public:
// constructor
CDeviceWindow();
// destructor
~CDeviceWindow();
// Update Qt graphic image from OpenNI device
bool UpdateImage();

public:
xn::Device mDevice; // OpenNI Device Node
xn::DepthGenerator mDepth; // OpenNI Depth Generator
xn::ImageGenerator mImage; // OpenNI Image Generator
XnDepthPixel mMaxDepth; // Keep max. depth value

QGraphicsScene qScene; // scene of Qt
QGraphicsView qView; // graphics view of Qt

private:
unsigned char* pTmpDepthImage; // temporary 8bit depth image
xn::ImageMetaData mColorImage; // RGB Color image from OpenNI
xn::DepthMetaData mDepthImage; // Raw depth image from OpenNI

QGraphicsPixmapItem qColorImage; // color image in Qt format
QGraphicsPixmapItem qDepthImage; // depth image in Qt format
};

其中,建構子的部分,主要是進行 Qt 環境的設定,不算是本文的重點,所以在這邊就先跳過了;有需要的話,請參考之前的《使用 Qt GraphicsView 顯示 OpenNI 影像資料》。


而實際上,CDeviceWindow 主要的功用除了資料的儲存外,就是 UpdateImage() 這個函式了∼在這個函式裡,會去讀取 OpenNI Image Generator(mImage)和 Depth Generator(mDepth)的資料,並把她轉換為 Qt 所需要的格式、拿來顯示。其程式碼內容如下:

// Update Qt graphic image from OpenNI device
bool UpdateImage()
{
if( !qView.isVisible() )
return false;

// 1. get color image
mImage.GetMetaData( mColorImage );
qColorImage.setPixmap( QPixmap::fromImage(
QImage( mColorImage.Data(),
mColorImage.XRes(), mColorImage.YRes(),
QImage::Format_RGB888 ) ) );

// 2. get depth image
mDepth.GetMetaData( mDepthImage );
// 2a. create 8 bit depth image
unsigned int uSize = mDepthImage.XRes() * mDepthImage.YRes();
if( pTmpDepthImage == NULL )
pTmpDepthImage = new unsigned char[ 4 * uSize ];
for( unsigned int i = 0; i < uSize; i )
{
unsigned char ucValue = 0;
if( mDepthImage[i] != 0 )
ucValue = 255 * ( mMaxDepth - mDepthImage[i] ) / mMaxDepth;
pTmpDepthImage[ 4 * i ] = 0; // blue
pTmpDepthImage[ 4 * i 1 ] = ucValue; // green
pTmpDepthImage[ 4 * i 2 ] = ucValue; // red
pTmpDepthImage[ 4 * i 3 ] = ucValue; // alpha
}
// 2b. update image in Qt
qDepthImage.setPixmap( QPixmap::fromImage(
QImage( pTmpDepthImage,
mDepthImage.XRes(), mDepthImage.YRes(),
QImage::Format_ARGB32 ) ) );

return true;
}

在上面的程式碼中,第一部分就是先透過 xn::ImageGeneratorGetMetaData() 函式,取得包含 metadata 的彩色影像資料,然後再把它轉型成 Qt 的 QImage、建立 QPixmap,然後讓 qColorImage 來使用。

由於 image generator 的資料格式基本上就是一般的 RGB888 的格式,所以這邊可以很方便、不用做特殊的處理,就直接把得到的資料拿來使用。

但是由於 Depth Generator 的深度資料不是一般的影像形式,所以就必須要先進行轉換,把它轉換成 8bit 的圖;而為了讓彩色的圖和深度圖可以疊在一起,所以 Heresy 這邊是把 depth map 轉換為帶有透明度(alpha)的 ARGB32 的形式。而在這裡,pTmpDepthImage 就是用來儲存轉換出來的結果的,由於是 ARGB 的形式,所以他的資料量會是點數的四倍。

在把深度資料轉換成 8bit 的計算,Heresy 這邊是用很簡單的算法:

ucValue = 255 * ( mMaxDepth - mDepthImage[i] ) / mMaxDepth;

也就是先記下 depth generator 的最大深度值(mMaxDepth,可透過 depth generator 的 GetDeviceMaxDepth() 取得),然後以此把每一個像素的資料,都以線性調整的方法,轉換成 0 – 255 之間的值。(註 1)

而當處理完成整張深度圖後,接下來就可以和處理彩色影像時一樣,把它轉換成 qDepthImage 可以使用的 QPixmap 的形式了。


QOpenNIWindows

由於在這個程式裡,Heresy 只用了一個 xn::Context 來做 OpenNI 的管理,所以這邊也建立了一個 QOpenNIWindows 的類別來對應,作為 OpenNI context 以及所有 device 的統一管理;基本上,他在程式裡只會有一個物件,而在 main() 裡,也只會對 QOpenNIWindows 這個類別的物件進行操作,不會直接碰到內部的 CDeviceWindow

實際上在 OpenNI 裡,控制控制電腦同時去存取多個裝置的程式,也就是這一部分

他的內容基本上如下:

// class for OpenNI context and qt windows manager
class QOpenNIWindows: public QObject
{
public:
// destructor
~QOpenNIWindows();
// initial OpenNI, enumerate device
bool Initial();
// create OpenNI nodes and Qt window
bool CreateDeviceAndWindow();
// start timer to update image
bool Start( int iInterval = 33 );

private:
XnStatus m_eResult;
xn::Context m_xContext;
xn::NodeInfoList m_xDeviceList;
vector<CDeviceWindow*> m_vDevWinList;

private:
bool CheckOpenNIError( const string& sStatus );
// timer to update data
void timerEvent( QTimerEvent *event );
// release OpenNI nodes and qt resource
void Release();
};

QOpenNIWindows 為了使用 QT 裡的 QObject::timerEvent() 來做定時的更新(註 2),所以必須繼承 QObject;而在 member data 的部分,除了 m_xContext 是 OpenNI 的 xn::Context 外,還包含了透過 EnumerateProductionTrees() 所列舉出來的 device 的資訊列表 m_xDeviceList,以及自己建立出來的 CDeviceWindow 清單:m_vDevWinList


而在 member function 的部分,Initial() 就是在進行 OpenNI 環境的初始化、並且透過 xn::Context 提供的 EnumerateProductionTrees() 這個函式,進行裝置的查詢,並將其結果儲存於 m_xDeviceList。其程式碼內容如下:

// initial OpenNI, enumerate device
bool Initial()
{
// 1. initial context
m_eResult = m_xContext.Init();
if( CheckOpenNIError( "Context initialization" ) )
return false;

// 2. Enumerate Device
m_eResult = m_xContext.EnumerateProductionTrees( XN_NODE_TYPE_DEVICE, NULL, m_xDeviceList );
if( CheckOpenNIError( "Enumerate device" ) )
return false;

// 3. check if there is device existed
if( m_xDeviceList.IsEmpty() )
{
cerr << "No OpenNI device found" << endl;
return false;
}

return true;
}

CreateDeviceAndWindow() 所做的事,就是去把 m_xDeviceList 裡的每一個 device node 都建立出來,並建立出對應的 depth generator 以及 image generator;最後,則是以 CDeviceWindow 的形式,儲存在 m_vDevWinList 裡,拿來做之後的操作、管理。

他的程式碼如下:

// create OpenNI nodes and Qt window
bool CreateDeviceAndWindow()
{
// check if there is already Device window existed
if( !m_vDevWinList.empty() )
{
Release();
}

// create nodes and window for each device
for( xn::NodeInfoList::Iterator itDev = m_xDeviceList.Begin();
itDev != m_xDeviceList.End(); itDev )
{
CDeviceWindow* pDevWin = new CDeviceWindow();
xn::NodeInfo mNodeInfo = *itDev;

// 1. create and initial OpenNI nodes
{
// a. create device
m_eResult = m_xContext.CreateProductionTree( mNodeInfo, pDevWin->mDevice );
if( CheckOpenNIError( "Create Device" ) )
break;

// b. build query rule for given device
xn::Query mQuery;
mQuery.AddNeededNode( mNodeInfo.GetInstanceName() );

// c. create depth generator
m_eResult = pDevWin->mDepth.Create( m_xContext, &mQuery );
if( CheckOpenNIError( "Create Depth Generator" ) )
{
pDevWin->mDevice.Release();
delete pDevWin;
break;
}
pDevWin->mMaxDepth = pDevWin->mDepth.GetDeviceMaxDepth();

// d. create image generator
m_eResult = pDevWin->mImage.Create( m_xContext, &mQuery );
if( CheckOpenNIError( "Create Image Generator" ) )
{
pDevWin->mDepth.Release();
pDevWin->mDevice.Release();
delete pDevWin;
break;
}

// e. Set Alternative View Point
pDevWin->mDepth.GetAlternativeViewPointCap().SetViewPoint( pDevWin->mImage );
}

// 2. Qt Setting
pDevWin->qView.setWindowTitle( mNodeInfo.GetInstanceName() );
pDevWin->qView.show();

m_vDevWinList.push_back( pDevWin );
}

// check result
if( m_vDevWinList.empty() )
return false;
return true;
}

這邊的工作,就是先透過 iterator 去掃過整個 m_xDeviceList,針對裡面的每一個 NodeInfo 來做處理;而最主要的部分,自然就是針對 NodeInfo 來建立對應的 OpenNI production node 了∼

首先,在 a 的部分,這裡還是一樣使用 xn::ContextCreateProductionTree() 這個函式,透過指定 xn::NodeInfo 的方法,來建立出所需要的 device node(pDevWin->mDevice)。

而在 device node 建立完成後, 接著就是使用他的 instance name,來做為後續其他 production node 的使用條件。所以在這邊是先建立一個 xn::Query 的物件 mQuery、代表一個查詢的條件;然後再透過他的 AddNeededNode() 函式,要求「一定要使用給定的 node」,而這邊的判斷條件,就是靠 NodeIfno 的 instance name 了∼

接下來在 c、d 的部分,就是要建立 depth generator 和 image generator 的 production node 了。雖然在論壇上的範例是使用 xn::ContextCreateAnyProductionTree() 這個函式來建立 production node,不過實際上每個 node 都有的 Create() 函式,也是可以透過附加指定 xn::Query 的方法,來做到有條件的 production node 的建立的∼它的用法就是:

pDevWin->mDepth.Create( m_xContext, &mQuery );

也就是在呼叫的時候,加上第二個參數,把設定好的 xn::Query 的物件的指標傳進去,這樣他就會去 m_xContext 裡面尋找、建立符合條件的 node 了∼(註 3)

而當 OpenNI 的這些 node 都建立完成後,接下來就是進行 Qt 的最後設定(拿 Instance name 當視窗標題)、並且把視窗顯示出來了∼

當然,為了之後的操作與管理,這裡也把建立出來的 CDeviceWindow 物件(pDevWin),存放到 m_vDevWinList 內。


而最後,就是設定 Qt 的 timer,讓程式可以自動去更新畫面了∼前面已經有提到了,Heresy 在這邊是使用 QT 的 QObject::timerEvent() 來實作這一部分的。

首先,是 Start() 這個函式。這個函式主要是給外部控制,開始 OpenNI 以及 Qt 的更新用的(目前沒有寫 stop),內容主要就是在呼叫 OpenNI context 的 StartGeneratingAll() 後,再去呼叫 Qt QObjectstartTimer(),來要求 Qt 在每隔一段時間(iInterval、單位毫秒)後,就自動去執行 timerEvent() 這個 callback function,以進行更新。

由於做的事不多,所以 Start() 的內容也不長,它的原始碼就只有下面這幾行:

// start timer to update image
bool Start( int iInterval = 33 )
{
// start generating OpenNI data
m_eResult = m_xContext.StartGeneratingAll();
if( CheckOpenNIError( "Start Generating" ) )
return false;

// start Qt timer
startTimer( iInterval );
return true;
}

而在 timerEvent() 的部分,它就是實際上每隔一段時間,就會被自動執行的程式了∼他所做的是有兩件,第一個是去呼叫 OpenNI Context 的 WaitAndUpdateAll()、要求 OpenNI 更新所有 production node 的資料。等到資料都更新完後,接下來就是 iterator 掃過整個 m_vDevWinList,呼叫每個建立出來的 CDeviceWindowUpdateImage() 函式、以進行視窗畫面的更新。

// timer to update data
void timerEvent( QTimerEvent *event )
{
// request OpenNI to update data
m_eResult = m_xContext.WaitAndUpdateAll();
if( CheckOpenNIError( "Update data" ) )
return;

// update data for each device and window
for( vector<CDeviceWindow*>::iterator itDev = m_vDevWinList.begin();
itDev != m_vDevWinList.end(); itDev )
(*itDev)->UpdateImage();
}


最後,main() 的部分呢?由於主要的工作都已經分到 QOpenNIWindowsCDeviceWindow 了,所以這裡的內容也很簡短,就只有下面幾行。

// Main function
int main( int argc, char** argv )
{
// create Qt Application
QApplication App( argc, argv );

QOpenNIWindows mNIWindows;
if( mNIWindows.Initial() )
{
if( mNIWindows.CreateDeviceAndWindow() )
{
mNIWindows.Start();

// start!
return App.exec();
}
}

return -1;
}

主要,就是先建立 Qt 的 QApplication 環境,然後再建立 QOpenNIWindows 物件,用來建立出 OpenNI 的環境;接下來則是依序呼叫 QOpenNIWindowsInitial()CreateDeviceAndWindow(),以建立 OpenNI 完整環境、並針對每一個 device 都建立一組 production node 以及 Qt 視窗。

最後,就是呼叫 QOpenNIWindowsStart()、開始 OpenNI 的資料更新,並呼叫 QApplicationexec()、進入 Qt 的主迴圈、開始執行整個程式了∼

而以 Heresy 這邊接了兩台 Kinect 的狀況、執行起來後,就會出現兩個視窗、個別顯示兩台 Kinect 的彩色和深度影像了(如最上方的圖)∼不過要注意的是,Heresy 沒有刻意去控制視窗出現的位置,所以一開始兩個視窗可能會是重疊的,請自己拉開。 ^^"

另外,由於現在要處理兩台 Kinect 的資料,所以在 USB 控制器的頻寬、以及 CPU 的使用上,也會比較兇,有可能會覺得畫面更新頓頓的。理論上,這邊部分的程式是可以透過平行化來做加速的,只是這邊還只是範例,所以就先不考慮到這些地方了∼


附註:
  1. 把深度圖轉換成 8bit 的方法有很多種,可以視自己的需要做修改。像 Heresy 之前的方法,就是每個畫面都各自去找最大值,然後再做線性調整,效率會比較差一點。

  2. QObject::timerEvent() 的詳細說明請參考官網
    雖然 Qt 本身也有提供更高階的 QTimer 可以用(官方介紹),但是由於一定得搭配 Qt 自己的 signal / slot 來使用,會比較麻煩,所以這邊不採用這個方案。

  3. CreateDeviceAndWindow() 裡建立 image generator 和 depth generator 時(c、d),Heresy 都加上了失敗時的錯誤處理(CheckOpenNIError() 的部分),主要是用來把已建立的資源釋放掉。

22 thoughts on “在 OpenNI 環境同時使用多個 Kinect”

  1. 不知道那就有没做过帧间关联呢?跟踪摄像机的位置然后补充深度图的黑洞

  2. to jingjing123Frame sync 要跨 device 應該會有問題至於透過多視角補圖,Heresy 自己沒有玩過。

  3. 您好,我想请问一下,您是如何实现两个kinect的同时驱动的呢?我试着把两个kinect同时连接到PC上,发现只能驱动一个,另一个的kinect camera处于无法驱动状态。我在网上查了一下,发现是因为两个kinect使用了同一个USB Host Controller,带宽不够所致,但我不知道如何将kinect切换到其他的USB controller中去。请问下你有没有碰到这个问题呢,当时又是如何解决的呢?谢谢!

  4. to SKYFLY 請把他換插主機板上別的 USB 接孔上再試試看。

  5. 两台一起使用usergenerater时。只有一台的数据生效,深度和彩色图像正常。估计是回调函数的问题,但不知道如何解决。想请问作者有这方面的探索吗?

  6. 谢谢您的回复。
    想再问下,所有涉及回调的generater都有这个bug吗?

  7. [b]哈囉 Heresy
    可以請問一下
    你程式碼的這一小段[/b]

    // b. build query rule for given device
    xn::Query mQuery;
    mQuery.AddNeededNode(mNodeInfo.GetInstanceName());
    // c. create depth generator
    m_eResult = pDevWin->mDepth.Create(m_xContext,&mQuery);

    [b]設Query的目的是限制OpenNI僅能在指定的Device上搜尋可建立的DepthGenerator嗎?
    因為我不太了解Query的用法,感謝您 ^_^[/b]

  8. to NiuKJ
    xn::Query 還有很多函式,可以設定額外的條件。
    詳細的使用方法,建議請參考官方文件。

  9. 哈囉 Heresy
    我也有PC接上兩個kinect會有一台無法驅動的問題
    換了不同的USB插孔還是一樣

    有的是kinect camera
    有的是連kinect motor都無法驅動 更別說另外兩個

    顯示的錯誤訊息是這樣的
    [b]這個裝置無法啟動。 (代碼 10)[/b]

    這個問題要如何解決呢?

    謝謝!!!

  10. to ED
    建議確認看看,兩台 Kinect 是否接到不同的 USB 2.0 控制器上。
    不同的 USB Port 有的時候會共用同一個控制器。

  11. Heresy 你好

    不好意思我想再問一下
    那要如何確認我是接到不同的控制器上?
    有可能PC上的USB插孔全部都接到同一個控制器嗎?

    謝謝你

  12. to ED
    在裝置管理員裡面,把「檢視」切換成「裝置(依連線)」,就可以看出來是在哪一個控制器下了。

  13. 你好 heresy

    我檢查了
    當接到不同控制器時,仍有無法驅動的問題。
    之前只有Kinect Camera無法驅動,
    現在Kinect Moter 無法驅動
    這個有可能是什麼問題造成的?

    謝謝你了

  14. to ED
    抱歉,因為沒碰過這樣的問題,所以 Heresy 也不知道了。
    不知道是否方便換一台電腦再試試看?

  15. Heresy,您好,我参考了您给的google URL上的帖子,用它的代码稍作修改测试了下,调试发现只要运行到
    printf(“Depth frame [%d] Middle point is: %u. FPS: %f
    “,
    dmd->FrameID(), sensors[i].depthMD(dmd->XRes() / 2, dmd->YRes() / 2), xnFPSCalc(&xnFPS));这个输出就会产生assertion错误,x

  16. to JH
    抱歉,不太了解你的程式是哪來的。
    建議你確認一下你的 dmd 和 sensors.depthMD 是否都是正確讀出來的?
    (話說,會什麼會有兩個看來是 Depth metadata 的物件?)

  17. 可以正确读出来,就一个Depth metadata物件。我怀疑是不是usb controller的问题,怎么看两台kinect连的是不同的usb控制器?

  18. to JH

    請到「裝置管理員」裡,選取「檢視」裡面的「裝置(依連線)」

發佈回覆給「heresy」的留言 取消回覆

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *