最近為了寫軟體,在研究 Nokia Qt(官網)的各種功能,其中一項有在整理文章的,就是這陣子的「Graphics View Framework」(簡介)了∼而現在,Heresy 決定試著用 Qt 的這個架構,試著用來呈現 OpenNI 的資料,來寫一些簡單、有視覺化輸出的範例程式了∼(不然總是一堆人問怎麼沒畫面,還滿… = =)
而這第一個範例,則是以《透過 OpneNI 合併 Kinect 深度以及彩色影像資料》開始,直接透過 Qt 的 GraphicsView 把 OpenNI 讀到的深度和彩色影像畫出來了∼而 Qt 環境的建置部分呢,就麻煩自理了( MSVC 的話,可以參考《使用 Visual C 2010 建置 Qt 4.6.3》)。
而在開始前,Heresy 先列一下 Heresy 這邊的開發環境:
- Windows 7 x64 Service Pack 1
- Visual Studio 2010 Service Pack 1
- OpenNI 1.3.2.3
- Qt 4.7.3
原始碼可以到 Heresy 的 SkyDrive 上下載,這次有提供編譯好的可執行檔(不過沒測試過換電腦到底能不能跑 XD),不過還請自行準備好 OpenNI 以及 Qt 所需的環境(Qt 至少需要 QtCore4.dll 和 QtGui4.dll)。而如果要使用 Heresy 建立的專案的話,請記得自行修改 Qt 的相關路徑設定。
接下來,就是程式的部分。
OpenNI 的部分
基本上,Heresy 是想盡量把 OpenNI 的部分抽出來,所以在這個範例裡面,是建立了一個名為 COpenNI 的類別,它的內容如下:
/* Class for control OpenNI device */
class COpenNI
{
public:
/* Destructor */
~COpenNI()
{
m_Context.Release();
}
/* Initial OpenNI context and create nodes. */
bool Initial()
{
// Initial OpenNI Context
m_eResult = m_Context.Init();
if( CheckError( "Context Initial failed" ) )
return false;
// create image node
m_eResult = m_Image.Create( m_Context );
if( CheckError( "Create Image Generator Error" ) )
return false;
// create depth node
m_eResult = m_Depth.Create( m_Context );
if( CheckError( "Create Depth Generator Error" ) )
return false;
// set nodes
m_eResult = m_Depth.GetAlternativeViewPointCap().SetViewPoint( m_Image );
CheckError( "Can't set the alternative view point on depth generator" );
return true;
}
/* Start to get the data from device */
bool Start()
{
m_eResult = m_Context.StartGeneratingAll();
return !CheckError( "Start Generating" );
}
/* Update / Get new data */
bool UpdateData()
{
// update
m_eResult = m_Context.WaitNoneUpdateAll();
if( CheckError( "Update Data" ) )
return false;
// get new data
m_Depth.GetMetaData( m_DepthMD );
m_Image.GetMetaData( m_ImageMD );
return true;
}
public:
xn::DepthMetaData m_DepthMD;
xn::ImageMetaData m_ImageMD;
private:
/* Check return status m_eResult.
* return false if the value is XN_STATUS_OK, true for error */
bool CheckError( const char* sError )
{
if( m_eResult != XN_STATUS_OK )
{
cerr << sError << ": " << xnGetStatusString( m_eResult ) << endl;
return true;
}
return false;
}
private:
XnStatus           m_eResult;
xn::Context        m_Context;
xn::DepthGenerator m_Depth;
xn::ImageGenerator m_Image;
};
這個類別他有三個用來操作的 public member function:Initial()、Start()、UpdateData()。
Initial() 會初始化 OpenNI 環境所需要的物件,包含了 context、production node;Start() 則會呼叫 context 的 StartGeneratingAll() 函式,來讓 OpenNI 開始產生資料。
而 UpdateData() 則是會去讀取 depth generator 和 image generator 的新資料,以 DepthMetaData 和 ImageMetaData 的形式、存在類別內,並讓外部可以使用。
基本上,由於這邊的程式大致上都和《透過 OpneNI 合併 Kinect 深度以及彩色影像資料》一文中的相同,只有在一些小細節的地方不太一樣,所以基本上 Heresy 大部分都不會另外解釋。其中,一個比較大的不同點在於 Heresy 這邊不是使用之前用的 GetDepthMap() 和 GetImageMap() 來取得深度/彩色影像的原始資料,而是使用 GetMetaData() 來取得更完整的資料。
如果對 OpenNI 程式開發完全沒概念的話,請先參考《透過 OpneNI 讀取 Kinect 深度影像資料》。
Qt 的部分
而為了要在 Qt 的環境裡,能自動更新、讀取 OpenNI 的資料,所以要用到 QObject 的 startTimer()(參考)。Heresy 這邊的作法,是依照官方的範例,繼承已有的 QObject、並透過重新實作 timerEvent() 來完成的;而這個類別 Heresy 把他取名為 CKinectReader。
/* Timer to update image in scene from OpenNI */
class CKinectReader: public QObject
{
public:
/* Constructor */
CKinectReader( COpenNI& rOpenNI, QGraphicsScene& rScene )
: m_OpenNI( rOpenNI ), m_Scene( rScene )
{}
/* Destructor */
~CKinectReader()
{
m_Scene.removeItem( m_pItemImage );
m_Scene.removeItem( m_pItemDepth );
delete [] m_pDepthARGB;
}
/* Start to update Qt Scene from OpenNI device */
bool Start( int iInterval = 33 )
{
m_OpenNI.Start();
// add an empty Image to scene
m_pItemImage = m_Scene.addPixmap( QPixmap() );
m_pItemImage->setZValue( 1 );
// add an empty Depth to scene
m_pItemDepth = m_Scene.addPixmap( QPixmap() );
m_pItemDepth->setZValue( 2 );
// update first to get the depth map size
m_OpenNI.UpdateData();
m_pDepthARGB
= new uchar[4*m_OpenNI.m_DepthMD.XRes()*m_OpenNI.m_DepthMD.YRes()];
startTimer( iInterval );
return true;
}
private:
COpenNI& m_OpenNI;
QGraphicsScene& m_Scene;
QGraphicsPixmapItem* m_pItemDepth;
QGraphicsPixmapItem* m_pItemImage;
uchar* m_pDepthARGB;
private:
void timerEvent( QTimerEvent *event )
{
// Read OpenNI data
m_OpenNI.UpdateData();
// convert to RGBA format
const XnDepthPixel* pDepth = m_OpenNI.m_DepthMD.Data();
unsigned int iSize=m_OpenNI.m_DepthMD.XRes()*m_OpenNI.m_DepthMD.YRes();
// fin the max value
XnDepthPixel tMax = *pDepth;
for( unsigned int i = 1; i < iSize; i )
{
if( pDepth[i] > tMax )
tMax = pDepth[i];
}
// redistribute data to 0-255
int idx = 0;
for( unsigned int i = 1; i < iSize; i )
{
if( (*pDepth) != 0 )
{
m_pDepthARGB[ idx ] = 0;
m_pDepthARGB[ idx ] = 255 * ( tMax - *pDepth ) / tMax;
m_pDepthARGB[ idx ] = 255 * *pDepth / tMax;
m_pDepthARGB[ idx ] = 255 * ( tMax - *pDepth ) / tMax;
}
else
{
m_pDepthARGB[ idx ] = 0;
m_pDepthARGB[ idx ] = 0;
m_pDepthARGB[ idx ] = 0;
m_pDepthARGB[ idx ] = 0;
}
pDepth;
}
// Update Depth data
m_pItemDepth->setPixmap( QPixmap::fromImage(
QImage( m_pDepthARGB,
m_OpenNI.m_DepthMD.XRes(), m_OpenNI.m_DepthMD.YRes(),
QImage::Format_ARGB32 ) )
);
// Update Image data
m_pItemImage->setPixmap( QPixmap::fromImage(
QImage( m_OpenNI.m_ImageMD.Data(),
m_OpenNI.m_ImageMD.XRes(), m_OpenNI.m_ImageMD.YRes(),
QImage::Format_RGB888 ) )
);
}
};
CKinectReader 主要的介面,只有建構子和 Start() 兩個,其他的東西都是內部使用的。而他所做的事,基本上就是透過 Start() 這個函式、啟動 Qt 的 Timer,每隔一段固定的時間,就去把 COpenNI 的資料(m_DepthMD 和 m_ImageMD)讀取出來,轉換成 Qt Graphics Scene 裡的物件。
而也由於要針對 COpenNI 和 Scene 做操作,所以在建構子的地方,要把這兩個物件的參考傳進來;之後呼叫 Start() 的時候,則是可以指定 timer 的時間間隔,看多久要去更新一次。
其中,Start() 這個函式裡,除了會去呼叫 COpenNI 的 Start()、讓 OpenNI 的環境開始運作外,也會先在 Qt Scene 裡,建立好必要的 Graphics Item;在這個例子裡,就是對應 depth map 和 image map 的影像物件、 QGraphicsPixmapItem(參考)了∼而這邊 Heresy 是先用空的 QPixmap 來產生 QGraphicsPixmapItem(m_pItemDepth、m_pItemImage),並設定他的 Z Value;Heresy 這邊是強制讓 m_pItemDepth 在 m_pItemImage 的上面,以方便之後做 alpha blending。
此外,這裡也先預先配置好一個 unsigned char 的陣列 m_pDepthARGB, 用來儲存之後將 Depth Map 的內容作轉換後的資料;而由於之後是要把 depath map 轉換成 RGBA 四個 channel 的圖,所以這邊預先配置的大小就是 X * Y * 4。
最後,就是透過 QObject 提供的 startTimer() 來開啟 timer 了∼而當指定的時間到了後,Qt 就會觸發 timer event、執行自己定義的 timerEvent() 這個函式。
而在 timerEvent() 裡,首先是先去呼叫 COpenNI 的 UpdateData(),更新資料。接下來,由於 Depth Map 的格式不是一般電腦影像常用的 8bit 資料,所以要直接畫出來的話,要經過一些轉換才行;Heresy 這邊的做法,是動態地去找到目前 depth map 中的最大值,然後再把整張 depth map 裡有值的部分,填入根據特定公式的算出的色彩,而沒有深度值的部分,則是將 RGBA 都填 0。
這邊計算的公式,基本上如下:
m_pDepthARGB[ idx ] = 0; // Blue
m_pDepthARGB[ idx ] = 255 * ( tMax - *pDepth ) / tMax; // Green
m_pDepthARGB[ idx ] = 255 * *pDepth / tMax; // Red
m_pDepthARGB[ idx ] = 255 * ( tMax - *pDepth ) / tMax; // Alpha
這樣的式子,會讓近的部分是綠色、遠的部分是紅色,而越近透明度會越低;基本上,只是一個範例,所以也算是隨便寫的計算式∼有需要的話,也可以換成更有意義或各符合需求的計算方法。
而在資料都準備完成後,接下來就是透過 QGraphicsPixmapItem 的 setPixmap() 函式,來把要顯示的新的圖傳進去了∼由於 Heresy 不知道到底要怎麼用現有的資料建立 QPixmap,所以這邊 Heresy 都是先建立 QImage(參考)後,再透過 QPixmap::fromImage() 轉成 QPixmap 的格式、拿來給 QGraphicsPixmapItem 使用。
主程式
最後,是主程式的部分。下方就是主程式的程式碼,而右圖則是最後執行的結果。
/* Main function */
int main( int argc, char** argv )
{
// initial OpenNI
COpenNI mOpenNI;
bool bStatus = true;
if( !mOpenNI.Initial() )
return 1;
// Qt Application
QApplication App( argc, argv );
QGraphicsScene qScene;
// Qt View
QGraphicsView qView( &qScene );
qView.resize( 650, 540 );
qView.show();
// Timer to update image
CKinectReader KReader( mOpenNI, qScene );
// start!
KReader.Start();
return App.exec();
}
首先,這邊先宣告出一個 COpenNI 的實體 mOpenNI,並呼叫他的 Initial() 進行初始化、建立出 OpenNI 環境所需要的東西。
再來,則是最基本的 QtGraphics View 的寫法、比較詳細的說明請參考《Qt Graphics View Framework 簡介》。這邊除了 Qt 程式必須有的 QApplication 外,也宣告出 QGraphicsScene 和 QGraphicsView 的物件。
而接下來,就是宣告出自己定義的 CKinectReader、把 mOpenNI 和 qScene 傳進去,並呼叫 Start()、同時開始 QApplaction 的主迴圈、讓 Qt 程式執行起來、並進行持續地更新。
好了,這個程式大致就先到這邊了∼基本上,Heresy 是計畫以後大部分的範例都拿這個程式來持續做修改了。畢竟,能把東西畫出來看到,應該還是比較方便的。不過由於 Qt Graphics View 本身只有提供 2D 的功能,所以之後應該也不會在這系列的程式裡,真的顯示 3D 系統的東西吧∼
附註
- 由於把 XnDepthPixel 值換算成 8bit 的範圍是動態估算的,所以每個 frame 的結果都會不太一樣,某些情況下看起來可能會覺得畫面的顏色一直在變,這算是正常的。
- QImage 可以直接使用外部的資料,所以技術上可以不用每次都重新建立一個新的 QImage 物件;但是 Heresy 試著這樣寫的時候,雖然的確可以用,但是卻出現了 image 和 depth 嚴重不同步的狀況…而這狀況在每次都重建 QImage 的情況下(目前的寫法)就不會出現?目前還不知道原因。
- Heresy 現在還是不知道為什麼 QImage 在格式是 QImage::Format_ARGB32 時,使用外部資料時的 channel 排列是 BGRA 而不是 RGBA…
- 為什麼用 Qt 不用 OpenGL?主要是 Heresy 自己最近在弄 Qt 的東西,剛好邊摸邊上手;再者,Heresy 覺得很多地方,OpenGL 的門檻其實比較高,所以一直不是很想拿 OpenGL 來當作顯示的範例…
非常感谢博主,这篇文章让我进行一次实际的编写有很大的帮助 由于博主使用的是QT这套组件库去编写实际代码 我也学习了一下QT 感觉QT的信号与槽机制很好用啊…希望LZ能够继续更新和openNI有关的资料,谢谢
感謝支持∼另外,如果沒有一定要用 Qt 的架構的話,其實 Boost 的 signal / slot 的彈性更大喔∼
您好,我测试了您的代码,发现了几个问题想跟您交流交流:
1.CKinectReader这个类里面,您用到了COpenNI这个类,可是在main函数里面,您并没有调用COpenNI的Initial这个成员函数来初始化。而是直接 KReader.Start();
当然,您貌似在CKinectReader的Start函数里面也没有调用COpenNI的Initial。。。
2 您的代码编译可以通过,可是,在执行的时候,只是跳出一个白框,并迅速关闭,然后程序退出,没有按照您说的每几秒变化一次。
谢谢解答:D
to 林雄民
1. COpenNI 的初始化是在外部完成的,請看 main() 的部分。
2. 通常這問題是裝置初始化失敗造成的,建議你用 debug 模式,一行一行追看看,看看錯誤是發生在哪裡。
heresy你好~
想向您請教一下,我想只顯示深度影像,但是不想像文中提到那樣讓近的部分是綠色、遠的部分是紅色,我希望把深度影像每一點的像素附加上其真實的RGB值,可以做到嗎?需要怎麼改一下代碼,多謝了~
麻煩您,還有個問題
就是爲什麽深度圖比彩色影像小一圈啊,如果我既保存深度圖又保存彩色圖,他們的x,y座標是一一對應的嗎?如果不是,怎麼才能使其尺寸一樣,並且x,y座標一一對應?
拜謝博主了
to hudson
1. 基本上在 2D 顯示的時候,能顯示的資訊是有限的。
如果你要用色彩來顯示的話,基本上就已經失去顯示深度資訊的手段了;不知道你希望在顯示真實色彩的情況下,要怎麼呈現深度資訊?
另一種方法,當然就是直接用 3D 的方法來顯示了。
2. 那是因為攝影機本身的硬體性質。由於兩組攝影機本來的視角就有落差,所以不可能完全相同,自然就會有差異。
建議可以考慮ˋ只截取中間有完全重疊的區塊來用。
to heresy
非常感謝博主解答,第一個問題確實不能滿足深度和彩色信息同時顯示,只能同時保存深度圖和彩色圖,然後在彩色圖對應的深度圖來找到單點像素的深度信息。
第二個問題,截取重疊區以後,那保存的深度圖和彩色圖的單點像素的X,Y座標是一致的嗎?
to hudson
這篇文章所示範的方法,取出來的影像像素位置已經是對應的了。你只要兩邊都擷取同一塊區域就可以了。
to heresy
非常感謝
如果想要黑白的深度图直接叠加在彩色图上的效果显示,要怎么修改呢
to sammy
深度圖本身不代表任何顏色,如果你希望改成灰階的話,請直接修改顏色換算的部分。
您好,请问为什么这篇代码中,我并没有找到关于mapmode的定义?就是x 640 y 480 nfs 30的这个,难道这个定义不是必须的是么?还有,我比较习惯用opencv来处理图像,那么仿照您的形式,把Qt class类换成opencv类,应该也能实现相同的功能吧,我指的是包含的了中间色彩变换的功能,因为我现在卡死在了这里,我在深度图输出之前用两个叠加的for循环遍历了所有像素点,按照公式修改对应的像素值,结果就是vs程序卡死,提示内存异常的错误,好像是溢出了
to sdjntzk
Map Output Mode 並非必要的,它本身就會有預設值,通常除非要做調整,不然其實可以省略。
而你如果是要轉換成 OpenCV 的格式,建議可以直接參考
http://viml.nchc.org.tw/blog/paper_info.php?CLASS_ID=1&SUB_ID=1&PAPER_ID=347
to heresy
感谢您的回答,您那一篇帖子我也看过,不过还是有一点小问题,关于其中您使用的是cvmat矩阵形式存储收到的数据,如果我使用IplImage声明指针来存储的话,会不会出现一些问题,或者说和用cvmat相比有没有什么不妥的地方
to sdjntzk
這部分請參考 OpenCV 的說明
http://opencv.willowgarage.com/documentation/cpp/basic_structures.html
基本上是格式、使用方法的不同。
to heresy
大大您好,小弟是kinect初學者,再看完您本篇的內容,並下載了您的源碼,將理面路徑作適當修改後,卻出現錯誤 error LNK2019: 無法解析的外部符號 __imp__xnContextAddRef 在函式 “public: void __thiscall xn::Context::SetHandle(struct XnContext *)” (?SetHandle@Context@xn@@QAEXPAUXnContext@@@Z) 中被參考 C:UsersUSERDownloadsmain.obj QTKinect
請問這個該怎麼作修改呢?
小弟是使用 WIN 7 VS2010 QT4.8.4 Openni1.3.2.3
@Ricky
你的錯誤應該是沒有正確的 link OpenNI 的 lib 檔,請確定你的專案設定是正確的。
@heresy
感謝您的回答,我還碰到另一個問題是QT出現”There’s no Qt version assigned to this project for platform x64.”
這樣的錯誤,請問該如何解決呢?
@Ricky
請確認你的 Qt 版本。
Qt 官方應該是沒有提供 x64 的 pre-build 版本,如果你要建置 x64 的專案,你需要自己建置 Qt。
或者建議你直接改用 win32 的版本