使用 Qt GraphicsView 顯示 OpenNI 影像資料

| | 21 Comments| 09:51
Categories:

最近為了寫軟體,在研究 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 的新資料,以 DepthMetaDataImageMetaData 的形式、存在類別內,並讓外部可以使用。

基本上,由於這邊的程式大致上都和《透過 OpneNI 合併 Kinect 深度以及彩色影像資料》一文中的相同,只有在一些小細節的地方不太一樣,所以基本上 Heresy 大部分都不會另外解釋。其中,一個比較大的不同點在於 Heresy 這邊不是使用之前用的 GetDepthMap()GetImageMap() 來取得深度/彩色影像的原始資料,而是使用 GetMetaData() 來取得更完整的資料。

如果對 OpenNI 程式開發完全沒概念的話,請先參考《透過 OpneNI 讀取 Kinect 深度影像資料》。


Qt 的部分

而為了要在 Qt 的環境裡,能自動更新、讀取 OpenNI 的資料,所以要用到 QObjectstartTimer()參考)。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_DepthMDm_ImageMD)讀取出來,轉換成 Qt Graphics Scene 裡的物件。

而也由於要針對 COpenNI 和 Scene 做操作,所以在建構子的地方,要把這兩個物件的參考傳進來;之後呼叫 Start() 的時候,則是可以指定 timer 的時間間隔,看多久要去更新一次。

其中,Start() 這個函式裡,除了會去呼叫 COpenNIStart()、讓 OpenNI 的環境開始運作外,也會先在 Qt Scene 裡,建立好必要的 Graphics Item;在這個例子裡,就是對應 depth map 和 image map 的影像物件、 QGraphicsPixmapItem參考)了∼而這邊 Heresy 是先用空的 QPixmap 來產生 QGraphicsPixmapItemm_pItemDepthm_pItemImage),並設定他的 Z Value;Heresy 這邊是強制讓 m_pItemDepthm_pItemImage 的上面,以方便之後做 alpha blending。

此外,這裡也先預先配置好一個 unsigned char 的陣列 m_pDepthARGB, 用來儲存之後將 Depth Map 的內容作轉換後的資料;而由於之後是要把 depath map 轉換成 RGBA 四個 channel 的圖,所以這邊預先配置的大小就是 X * Y * 4。

最後,就是透過 QObject 提供的 startTimer() 來開啟 timer 了∼而當指定的時間到了後,Qt 就會觸發 timer event、執行自己定義的 timerEvent() 這個函式。

而在 timerEvent() 裡,首先是先去呼叫 COpenNIUpdateData(),更新資料。接下來,由於 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

這樣的式子,會讓近的部分是綠色、遠的部分是紅色,而越近透明度會越低;基本上,只是一個範例,所以也算是隨便寫的計算式∼有需要的話,也可以換成更有意義或各符合需求的計算方法。

而在資料都準備完成後,接下來就是透過 QGraphicsPixmapItemsetPixmap() 函式,來把要顯示的新的圖傳進去了∼由於 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 外,也宣告出 QGraphicsSceneQGraphicsView 的物件。

而接下來,就是宣告出自己定義的 CKinectReader、把 mOpenNIqScene 傳進去,並呼叫 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 來當作顯示的範例…

21 thoughts on “使用 Qt GraphicsView 顯示 OpenNI 影像資料”

  1. 非常感谢博主,这篇文章让我进行一次实际的编写有很大的帮助 由于博主使用的是QT这套组件库去编写实际代码 我也学习了一下QT 感觉QT的信号与槽机制很好用啊…希望LZ能够继续更新和openNI有关的资料,谢谢

  2. 感謝支持∼另外,如果沒有一定要用 Qt 的架構的話,其實 Boost 的 signal / slot 的彈性更大喔∼

  3. 您好,我测试了您的代码,发现了几个问题想跟您交流交流:
    1.CKinectReader这个类里面,您用到了COpenNI这个类,可是在main函数里面,您并没有调用COpenNI的Initial这个成员函数来初始化。而是直接 KReader.Start();
    当然,您貌似在CKinectReader的Start函数里面也没有调用COpenNI的Initial。。。

    2 您的代码编译可以通过,可是,在执行的时候,只是跳出一个白框,并迅速关闭,然后程序退出,没有按照您说的每几秒变化一次。

    谢谢解答:D

  4. to 林雄民

    1. COpenNI 的初始化是在外部完成的,請看 main() 的部分。
    2. 通常這問題是裝置初始化失敗造成的,建議你用 debug 模式,一行一行追看看,看看錯誤是發生在哪裡。

  5. heresy你好~
    想向您請教一下,我想只顯示深度影像,但是不想像文中提到那樣讓近的部分是綠色、遠的部分是紅色,我希望把深度影像每一點的像素附加上其真實的RGB值,可以做到嗎?需要怎麼改一下代碼,多謝了~

  6. 麻煩您,還有個問題
    就是爲什麽深度圖比彩色影像小一圈啊,如果我既保存深度圖又保存彩色圖,他們的x,y座標是一一對應的嗎?如果不是,怎麼才能使其尺寸一樣,並且x,y座標一一對應?
    拜謝博主了

  7. to hudson

    1. 基本上在 2D 顯示的時候,能顯示的資訊是有限的。
    如果你要用色彩來顯示的話,基本上就已經失去顯示深度資訊的手段了;不知道你希望在顯示真實色彩的情況下,要怎麼呈現深度資訊?
    另一種方法,當然就是直接用 3D 的方法來顯示了。

    2. 那是因為攝影機本身的硬體性質。由於兩組攝影機本來的視角就有落差,所以不可能完全相同,自然就會有差異。
    建議可以考慮ˋ只截取中間有完全重疊的區塊來用。

  8. to heresy

    非常感謝博主解答,第一個問題確實不能滿足深度和彩色信息同時顯示,只能同時保存深度圖和彩色圖,然後在彩色圖對應的深度圖來找到單點像素的深度信息。

    第二個問題,截取重疊區以後,那保存的深度圖和彩色圖的單點像素的X,Y座標是一致的嗎?

  9. to hudson
    這篇文章所示範的方法,取出來的影像像素位置已經是對應的了。你只要兩邊都擷取同一塊區域就可以了。

  10. 如果想要黑白的深度图直接叠加在彩色图上的效果显示,要怎么修改呢

  11. to sammy
    深度圖本身不代表任何顏色,如果你希望改成灰階的話,請直接修改顏色換算的部分。

  12. 您好,请问为什么这篇代码中,我并没有找到关于mapmode的定义?就是x 640 y 480 nfs 30的这个,难道这个定义不是必须的是么?还有,我比较习惯用opencv来处理图像,那么仿照您的形式,把Qt class类换成opencv类,应该也能实现相同的功能吧,我指的是包含的了中间色彩变换的功能,因为我现在卡死在了这里,我在深度图输出之前用两个叠加的for循环遍历了所有像素点,按照公式修改对应的像素值,结果就是vs程序卡死,提示内存异常的错误,好像是溢出了

  13. to heresy
    感谢您的回答,您那一篇帖子我也看过,不过还是有一点小问题,关于其中您使用的是cvmat矩阵形式存储收到的数据,如果我使用IplImage声明指针来存储的话,会不会出现一些问题,或者说和用cvmat相比有没有什么不妥的地方

  14. 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

  15. @Ricky
    你的錯誤應該是沒有正確的 link OpenNI 的 lib 檔,請確定你的專案設定是正確的。

  16. @heresy
    感謝您的回答,我還碰到另一個問題是QT出現”There’s no Qt version assigned to this project for platform x64.”
    這樣的錯誤,請問該如何解決呢?

  17. @Ricky

    請確認你的 Qt 版本。
    Qt 官方應該是沒有提供 x64 的 pre-build 版本,如果你要建置 x64 的專案,你需要自己建置 Qt。
    或者建議你直接改用 win32 的版本

發佈回覆給「记忆de轨迹」的留言 取消回覆

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