最近為了寫軟體,在研究 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,而改採用相對簡單的 OpenCV,來做為 2D […]
讚讚
[…] 参考资料:https://kheresy.wordpress.com/2011/08/18/show_maps_of_openni_via_qt_graphicsview/ […]
讚讚
哈哈 感謝分享!!!!
我想要引用部分內容,因此加了你這篇的連結在自己的網誌裡。
跟你告知一聲喔!^^ 非常感謝~~~~
讚讚
麻煩記得在文章內加個出處的連結囉~ :)
讚讚
已附上連結喔!^_______________^
請參考~~~~ http://ramihaha.tw/?p=2128
讚讚
[…] 参考资料:https://kheresy.wordpress.com/2011/08/18/show_maps_of_openni_via_qt_graphicsview/ […]
讚讚
[…] 首先,在之前《使用 Qt GraphicsView 顯示 OpenNI 影像資料》一文中,已經有提供範例,可以使用 Nokia Qt,在 2D 的環境下把 OpenNI 抓到的影像顯示出來了;而顯示出來的結果,大致上會像右圖的樣子。 […]
讚讚
[…] OpenNI 程式的開發;而圖形的部分,則是採用和 OpenNI 一樣是跨平台 Nokia Qt 和 OpenGL 來做範例,所以雖然是在 Windows […]
讚讚
[…] Nokia Qt 來做圖形界上面上的顯示,這部分的程式,建議請參考之前的《使用 Qt GraphicsView 顯示 OpenNI 影像資料》一文。程式在執行後,會去列出所有電腦裡的 OpenNI […]
讚讚
[…] 實際上,這邊的程式可以簡單地透過修改之前《使用 Qt GraphicsView 顯示 OpenNI 影像資料》的程式碼來完成,Heresy 就不多提了,完整的範例程式,請到 Heresy 的 SkyDrive 上下載;這次有附上編譯好的 Win32 程式,以及必要的 Qt DLL 檔,所以在有安裝好 OpenNI / NITE 的環境下,應該只要有安裝 Visual C++ 2010 可轉散發套件,就可以執行了。 […]
讚讚
呵呵,我最近也在玩这个,不过我用的是OpenCV结合OpenNI(OpenCV的highgui模块也是基于Qt的),感觉用起来更简单一些,也没遇到时候问题,而且还可以把OpenGL也串起来玩,博主可以试一试。
讚讚
感謝分享~
Heresy 也有想玩 OpenCV 過,不過由於目前已經有在用其他類似的東西了,所以暫時沒有時間下去玩他就是了。
讚讚
請問,除了OpenCV,還有甚麼Library可以做到同樣的功能嗎?
讚讚
你需要的是什麼功能?如果是電腦視覺或影像處理的部份的話,目前最普遍的,應該還是 OpenCV。
讚讚
之前試過手指辨識,可是OpenCV跟OpenNI的格式會衝突的樣子,常常不知名的Crash…
再者Kinect的解析度真是低阿…
純粹好奇前輩使用哪種OpenCV以外的Library來用~
讚讚
格式會衝突?Heresy 自己雖然沒用過,但是理論上應該是可以用才對…
另外,Heresy 這邊用的是公司內部自己開發的影像處理函式庫。
讚讚
原來如此!這樣我就沒機會比對了=w=…
前輩回應超迅速的~感激不盡~
讚讚
其實…如果解析度再高的話,CPU 等計算需求也會提高不少,搞不好得動用 GPGPU 才跑的順了… ^^"
讚讚
[…] 由 Heresy 發表迴響 延續上一篇《使用 Qt GraphicsView 顯示 OpenNI 影像資料》,這邊繼續接著上次的範例往下寫;而這次主要加入的功能,則是 OpenNI […]
讚讚