使用 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 相關文章目錄

廣告

關於 Heresy
https://kheresy.wordpress.com

21 Responses to 使用 Qt GraphicsView 顯示 OpenNI 影像資料

  1. 引用通告: OpenNI 2 & NiTE 2 課程投影片與範例 | Heresy's Space

  2. 引用通告: [程式] QT- 視覺化使用者介面設計(GUI) 軟體介面自己畫,來自創元件吧! | 蕾咪的生活手札 My Love, My Live

發表迴響

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

WordPress.com Logo

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

Twitter picture

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

Facebook照片

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

Google+ photo

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

連結到 %s

%d 位部落客按了讚: