用 OpenCV 畫出 OpenNI 2 的深度、彩色影像


這篇基本上是《OpenNI 2 VideoStream 與 Device 的設定與使用》一文的延伸,主要是提供一個完整的範例,用OpenCV(官網)這套知名的電腦視覺、影像處理的函式庫,來顯示 OpenNI 2 所讀取到的彩色、深度影像。

實際上,在《OpenNI 2 VideoStream 與 Device 的設定與使用》中已經有提到過,該怎麼把 OpenNI VideoStream 讀取到的 VideoFrameRef,轉換成 OpenCV C++ 的 cv::Mat 的形式了~以彩色影像來說,他的基本做法,就是:

const cv::Mat mImageRGB( mColorFrame.getHeight(), mColorFrame.getWidth(),
                         CV_8UC3, (void*)mColorFrame.getData() );

不過由於 OpenCV 內部彩色影像實際上是使用 BGR、而非 RGB 的形式,所以如果要做後續的處理的話,是需要把 RGB 影像轉換成 BGR 的。而如果是深度影像的話,資料的型別也需要從 CV_8UC3 改成 CV_16UC1 才行。

而如果只是為了要顯示的話,一個完整的程式,會像下面這樣:

// STL Header
#include <iostream>
  
// OpenCV Header
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
  
// OpenNI Header
#include <OpenNI.h>
  
// namespace
using namespace std;
using namespace openni;
  
int main( int argc, char **argv )
{
  // 1. Initial OpenNI
  if( OpenNI::initialize() != STATUS_OK )
  {
    cerr << "OpenNI Initial Error: " 
         << OpenNI::getExtendedError() << endl;
    return -1;
  }
  
  // 2. Open Device
  Device mDevice;
  if( mDevice.open( ANY_DEVICE ) != STATUS_OK )
  {
    cerr << "Can't Open Device: " 
         << OpenNI::getExtendedError() << endl;
    return -1;
  }
  
  // 3. Create depth stream
  VideoStream mDepthStream;
  if( mDevice.hasSensor( SENSOR_DEPTH ) )
  {
    if( mDepthStream.create( mDevice, SENSOR_DEPTH ) == STATUS_OK )
    {
      // 3a. set video mode
      VideoMode mMode;
      mMode.setResolution( 640, 480 );
      mMode.setFps( 30 );
      mMode.setPixelFormat( PIXEL_FORMAT_DEPTH_1_MM );
  
      if( mDepthStream.setVideoMode( mMode) != STATUS_OK )
      {
        cout << "Can't apply VideoMode: "
             << OpenNI::getExtendedError() << endl;
      }
    }
    else
    {
      cerr << "Can't create depth stream on device: "
           << OpenNI::getExtendedError() << endl;
      return -1;
    }
  }
  else
  {
    cerr << "ERROR: This device does not have depth sensor" << endl;
    return -1;
  }
  
  // 4. Create color stream
  VideoStream mColorStream;
  if( mDevice.hasSensor( SENSOR_COLOR ) )
  {
    if( mColorStream.create( mDevice, SENSOR_COLOR ) == STATUS_OK )
    {
      // 4a. set video mode
      VideoMode mMode;
      mMode.setResolution( 640, 480 );
      mMode.setFps( 30 );
      mMode.setPixelFormat( PIXEL_FORMAT_RGB888 );
  
      if( mColorStream.setVideoMode( mMode) != STATUS_OK )
      {
        cout << "Can't apply VideoMode: " 
             << OpenNI::getExtendedError() << endl;
      }
  
      // 4b. image registration
      if( mDevice.isImageRegistrationModeSupported(
              IMAGE_REGISTRATION_DEPTH_TO_COLOR ) )
      {
        mDevice.setImageRegistrationMode( IMAGE_REGISTRATION_DEPTH_TO_COLOR );
      }
    }
    else
    {
      cerr << "Can't create color stream on device: "
           << OpenNI::getExtendedError() << endl;
      return -1;
    }
  }
  
  // 5. create OpenCV Window
  cv::namedWindow( "Depth Image"CV_WINDOW_AUTOSIZE );
  cv::namedWindow( "Color Image"CV_WINDOW_AUTOSIZE );
  
  // 6. start
  VideoFrameRef  mColorFrame;
  VideoFrameRef  mDepthFrame;
  mDepthStream.start();
  mColorStream.start();
  int iMaxDepth = mDepthStream.getMaxPixelValue();
  while( true )
  {
    // 7. check is color stream is available
    if( mColorStream.isValid() )
    {
      // 7a. get color frame
      if( mColorStream.readFrame( &mColorFrame ) == STATUS_OK )
      {
        // 7b. convert data to OpenCV format
        const cv::Mat mImageRGB(
                mColorFrame.getHeight(), mColorFrame.getWidth(),
                CV_8UC3, (void*)mColorFrame.getData() );
        // 7c. convert form RGB to BGR
        cv::Mat cImageBGR;
        cv::cvtColor( mImageRGB, cImageBGR, CV_RGB2BGR );
        // 7d. show image
        cv::imshow( "Color Image", cImageBGR );
      }
    }
  
    // 8a. get depth frame
    if( mDepthStream.readFrame( &mDepthFrame ) == STATUS_OK )
    {
      // 8b. convert data to OpenCV format
      const cv::Mat mImageDepth(
                mDepthFrame.getHeight(), mDepthFrame.getWidth(),
                CV_16UC1, (void*)mDepthFrame.getData() );
      // 8c. re-map depth data [0,Max] to [0,255]
      cv::Mat mScaledDepth;
      mImageDepth.convertTo( mScaledDepth, CV_8U, 255.0 / iMaxDepth );
      // 8d. show image
      cv::imshow( "Depth Image", mScaledDepth );
    }
  
    // 6a. check keyboard
    if( cv::waitKey( 1 ) == 'q' )
      break;
  }
  
  // 9. stop
  mDepthStream.destroy();
  mColorStream.destroy();
  mDevice.close();
  OpenNI::shutdown();
  
  return 0;
}

這樣的程式執行之後,除了本來的命令提示字元的視窗外,還會由 OpenCV 建立出像下圖這樣的兩個視窗,各別顯示深度影像和彩色影像,並透過無窮迴圈不停地更新畫面:

而程式會一直進行更新,直到使用者按下鍵盤上的「q」,才會結束程式。

接下來,則是程式原始碼的一些說明。


初始化

在上面的範例裡面,1 到 4 的部分,都是 OpenNI 初始化的部分。依序就是:

  1. OpenNI 環境初始化
  2. 開啟裝置(Device
  3. 建立深度影像的 VideoStream

    並將解析度設定成 640 x 480、30FPS,格式為 PIXEL_FORMAT_DEPTH_1_MM

  4. 建立深度影像的 VideoStream

    並將解析度設定成 640 x 480、30FPS,格式為 PIXEL_FORMAT_RGB888

    同時,也設定裝置的影像校正,將深度影象對到彩色影像的位置。

不過由於 Heresy 有考慮到 ASUS Xtion Pro 沒有彩色攝影影機,所以無法得到彩色影像的資料,因此為了讓程式也可以在 Xtion Pro 上執行,所以在程式的概念上是設計成一定要有深度攝影機,但是可以沒有彩色攝影機的。也因此,可以看到深度和彩色影像的部分,在處理的時候都有些微的不同。

而 5 之後的部分,才開始有用到 OpenCV 的功能。首先,5 的部分,就是透過 cv::namedWindow(),建立出兩個名字分別是「Depth Image」和「Color Image」視窗,分別用來顯示深度影像和彩色影像。

程式主迴圈

6 則是開始 OpenNI VideoStream 的資料讀取,並以一個無窮迴圈、來做資料的更新。而在迴圈裏面,則有搭配一個 6a、使用 cv::waitKey() 這個函式,來讀取鍵盤的輸入,如果使用者按下鍵盤上的「q」的話,就會離開迴圈,進到 9 的部分,開始關閉 OpenNI 的物件、把程式結束程掉。

在迴圈內,則主要是 7 和 8 兩塊,前者是針對彩色影像作處理,後者則是針對深度影像作處理。

彩色影像的處理

以彩色影像來說,在確定讀到資料後,是會先把資料,轉換成一個 cv::Mat 的物件 mImageRGB(7b);不過由於 OpenCV 的彩色影像是採用 BGR 的順序,所以接下來,就是要透過 cv::cvtColor() 來做色彩空間的轉換,把 RGB 排列的 mImageRGB 轉換成 BGR 排列的 mImageBGR(7c)。

處理好了之後,則就是透過 cv::imshow(),把 mImageBGR 的影像用「Color Image」這個視窗顯示出來。

深度影像的處理

而深度影像的部分,則是先轉換成 CV_16UC1 形式的 cv::Mat 物件 mImageDepth(8b)。雖然 OpenCV 有支援 16bit 的影像,但是實際上因為 OpenNI 在目前的感應器上,讀到的深度值範圍最大就是到 10,000(iMaxDepth、透過深度的 VideoStreamgetMaxPixelValue() 這個函式取得),所以如果直接把 16bit 的影像畫出來的話,應該是會看到接近一片黑的畫面…

所以為了強化顯示的效果,這邊就去使用 cv::Mat 提供的 convertTo() 這個函式,把 16bit(CV_16UC1)的 mImageDepth,轉換成 8bit(CV_8UC1)的影像 mScaledDepth(8c)。而 convertTo() 這個函式,在做轉換的時候,可以透過第三個參數和第四個參數,來設定轉換時的縮放、以及位移。詳細的說明,可以參考 OpenCV 官方的文件(連結),而像 Heresy 這邊就是把深度值乘上「255.0 / iMaxDepth」,讓本來介於 0 ~ 10,000 之間的深度值,變成 0 ~ 255。

最後,則是一樣透過 cv::imshow(),把 mScaledDepth 這個影像用「Depth Image」這個視窗顯示出來。


這邊 Heresy 基本上只是單純把 OpenNI 的影像資料,轉換成 OpenCV 的格式、然後就直接畫出來了。如果有需要的話,也是可以再透過 OpenCV,來做額外的處理的~像在 OpenNI 1.x 的時候,Heresy 就有寫一篇《OpenNI + OpenCV》,裡面就有額外把彩色影像和深度影像,都拿來做邊緣偵測~有需要的話,也可以自己試試看。


OpenNI / Kinect 相關文章目錄

對「用 OpenCV 畫出 OpenNI 2 的深度、彩色影像」的想法

  1. H老師您好:
    不太懂下面這句話,想請問255.0 / iMaxDepth是怎麼推算出來的?
    謝謝。

    詳細的說明,可以參考 OpenCV 官方的文件(連結),而像 Heresy 這邊就是把深度值乘上「255.0 / iMaxDepth」,讓本來介於 0 ~ 10,000 之間的深度值,變成 0 ~ 255。

  2. Hi …
    I need your help.
    Do you have any tutorial or code about similarity data between 2 different actions dance (data action1 = fix data and data action2 = current data), what i want to know from it is show in RGB display opencv when user do actions too fast or too slow actions according to data action1 that supposed to be similar ..
    then i can get the similarity result and data comparison result (for this calculation i used my method) …

    I used Kinect + Opencv + OpenNi with c ++ ..

    * I got the data actions from 15 joints(coordinates)

    I applied for my application in dance exercise

    please mail me at: valentineivana@gmail.com
    i really hope your help ..
    thanks anyway ..

  3. H老師您好

    有看您在上面有回覆說彩色影像與深度影像延遲的問題

    您給的建議是:
    1. 確認 NiViewer 的深度和彩色影像,都已切換為 640×480(兩個是獨立設定的)
    2. 建置成 release 版,然後獨立執行再試試看。
    3, 把彩色影像關掉試試看

    我把彩色影像關掉(單開深度影像)就真的沒有延遲了,但如果我想同時使用兩者的資訊,這個延遲的問題有沒有解決?

    • 幾個可能:
      1. 更新版本,包含 OpenNI 版本,甚至硬體的韌體(如果是 ASUS Xtion 的話)
      2. 更換 USB Port(最好換控制器)
      3. 換電腦

  4. heresy你好,试着运行你的OpenNI 2.x 教學文章几篇中的代码,都出现一个情况,没有报错,但方框一闪而过,没有显示出图像。
    使用的是
    win8.1,vs2013(64位)
    opencv3.1.0(64位)
    OpenNI 2.2.0.33 Beta (x64)
    Kinect for Windows SDK v2.0
    是否还欠什么东西?
    (我的电脑是i5的,还达不到kinect的开发要求,目前在做学校的一个项目,捕捉运动物体的三维坐标)
    不用openni,把kinect数据用opencv显示出来是没有问题的。openni方面已经检查过包含路径和Redist了
    试着运行openni的NiViewer.exe来检查,发现错误
    Deviceopen using default: no devices found
    请教一下,这会是openni出了什么问题?
    谢谢。

  5. 您好 請問一下 我在執行此範例的時候 感覺很像當掉了
    可是把深度的程式拿掉 或是把深度的fps改掉 就ok了
    可是我深度的視窗就不是640*480了

    請問是我哪邊出錯了呢

    • 麻煩請詳述你的環境。
      另外,也請確認你用官方的 NiViewer、將解析度修改到 VGA 後確認是否可以正常運作。
      再來,也建議確認一下你的 USB 控制器上是否有其他裝置,有的話可以考慮拿掉,或是換一個 USB Port。

      • 您好 我電腦是win7 64bit 使用visual studio 2010
        下載openni是32bit 
        http://imgur.com/pcg9233
        上面這張圖是用niview開的 正常!
        下面這張圖是加上深度的程式就不正常的照片
        單純使用上面的範例也是一樣
        http://imgur.com/UjWNMQN

        下面這張圖是把深度的fps註解掉或是改掉數字 就正常的畫面
        http://imgur.com/CYQ26Be

        『將解析度修改到 VGA 後確認是否可以正常運作。』 不好意思 不太懂您的意思

        • 在 NiViewer 上點選滑鼠右鍵後,可以修改各種資料的 VideoMode。
          請確認在 NiViewer 中,切換到 640×480 @ 30fps 是可以運作的。

          另外,你把 FPS 註解掉後,VideoMode 的參數是不完整的,這樣的設定應該不會生效。

          • 我換台電腦 win7 64bit visual studio 2010
            openni 64bit
            可以開起來了
            可是畫面更新非常慢
            比如我手放到xtion前 再拿起來 螢幕的畫面會卡在我手起來的一瞬間
            http://imgur.com/FA3DpOw

            可是用niviewr都正常 畫面順暢

    • 個人建議:
      1. 確認 NiViewer 的深度和彩色影像,都已切換為 640×480(兩個是獨立設定的)
      2. 建置成 release 版,然後獨立執行再試試看。
      3, 把彩色影像關掉試試看

      如果都還是不行,那就不知道問題是什麼了。

      • 我抓出問題了 我沒灌xtion盒子內的光碟拉  哈哈哈哈哈
        我也是看了您的文章 才發現有光碟在盒子內  感謝您

  6. H老师,您好
    多谢您的文章,让我获益良多,但是有些问题想请教
    在深度影像的情况下时常会捕捉到物体周围有黑影,请问这个黑影的出现是什么原因?是否有什么方法解决呢?
    我使用的平台是 opencv + openNI 2.2

    • 幾種可能:
      1. 物體本身的影子。因為感應器視光學式的,而且發射器和接受器位置不同,所以一定會有影子
      2. 物體邊緣的平面往往接近和感應器視線方向平行,所以本來就很容易造成無法判斷深度

  7. 感謝大大的熱情分享
    請問openNI 2新增的功能中有沒有包含像第一代一樣的抓取平面,其他新增的各種功能我該如何找到他的指令?

  8. 請問H老師
    深度圖像上下左右的邊框佔據多少pixel呢?

    000000000000000000000
    011111111111111111110
    011111111111111111110
    011111111111111111110
    011111111111111111110
    011111111111111111110
    011111111111111111110
    000000000000000000000

    以640*480為例的話
    也就是說要pDepth[idx]
    第一個可以不為0的idx是多少呢?

發表留言

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料