使用 Qt GraphicsView 顯示 OpenNI 影像資料


最近為了寫軟體,在研究 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 來當作顯示的範例…

OpenNI / Kinect 相關文章目錄
Nokia Qt 相關文章目錄

廣告

對「使用 Qt GraphicsView 顯示 OpenNI 影像資料」的想法

  1. 哈哈 感謝分享!!!!
    我想要引用部分內容,因此加了你這篇的連結在自己的網誌裡。
    跟你告知一聲喔!^^ 非常感謝~~~~

  2. […] 首先,在之前《使用 Qt GraphicsView 顯示 OpenNI 影像資料》一文中,已經有提供範例,可以使用 Nokia Qt,在 2D 的環境下把 OpenNI 抓到的影像顯示出來了;而顯示出來的結果,大致上會像右圖的樣子。 […]

  3. […] 實際上,這邊的程式可以簡單地透過修改之前《使用 Qt GraphicsView 顯示 OpenNI 影像資料》的程式碼來完成,Heresy 就不多提了,完整的範例程式,請到 Heresy 的 SkyDrive 上下載;這次有附上編譯好的 Win32 程式,以及必要的 Qt DLL 檔,所以在有安裝好 OpenNI / NITE 的環境下,應該只要有安裝 Visual C++ 2010 可轉散發套件,就可以執行了。 […]

  4. 呵呵,我最近也在玩这个,不过我用的是OpenCV结合OpenNI(OpenCV的highgui模块也是基于Qt的),感觉用起来更简单一些,也没遇到时候问题,而且还可以把OpenGL也串起来玩,博主可以试一试。

    • 感謝分享~
      Heresy 也有想玩 OpenCV 過,不過由於目前已經有在用其他類似的東西了,所以暫時沒有時間下去玩他就是了。

        • 你需要的是什麼功能?如果是電腦視覺或影像處理的部份的話,目前最普遍的,應該還是 OpenCV。

          • 之前試過手指辨識,可是OpenCV跟OpenNI的格式會衝突的樣子,常常不知名的Crash…
            再者Kinect的解析度真是低阿…
            純粹好奇前輩使用哪種OpenCV以外的Library來用~

          • 格式會衝突?Heresy 自己雖然沒用過,但是理論上應該是可以用才對…

            另外,Heresy 這邊用的是公司內部自己開發的影像處理函式庫。

          • 原來如此!這樣我就沒機會比對了=w=…
            前輩回應超迅速的~感激不盡~

      • 其實…如果解析度再高的話,CPU 等計算需求也會提高不少,搞不好得動用 GPGPU 才跑的順了… ^^"

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.