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

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

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

  1. James 說道:

    H老師您好

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

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

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

    喜歡

    • Heresy 說道:

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

      喜歡

  2. 吴君临 說道:

    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出了什么问题?
    谢谢。

    喜歡

    • Heresy 說道:

      Kinect 2 不能直接透過 OpenNI 2 使用
      請參考: https://kheresy.wordpress.com/2016/04/08/start-to-use-openni-or-kinect/

      喜歡

      • 吴君临 說道:

        heresy老师, 您在文中说的:OpenNI 2 無法搭配 NiTE 使用,基本上只能讀取深度、彩色等畫面,無法進行人體骨架的追蹤。
        那么如果只想用OpenNI 2 中CoordinateConverter 提供的 convertDepthToWorld() 這個函式,转换一个点的坐标(kinect2深度图上的),能够用OpenNI 2 实现么?还是说应该放弃openni2换个方向了?

        喜歡

        • Heresy 說道:

          理論上可以,但是沒有特殊需求的話,實在沒有必要特別透過 OpenNI 來使用 Kinect 2。

          喜歡

  3. 翔翔 說道:

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

    請問是我哪邊出錯了呢

    喜歡

    • Heresy 說道:

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

      喜歡

      • 翔翔 說道:

        您好 我電腦是win7 64bit 使用visual studio 2010
        下載openni是32bit 

        上面這張圖是用niview開的 正常!
        下面這張圖是加上深度的程式就不正常的照片
        單純使用上面的範例也是一樣

        下面這張圖是把深度的fps註解掉或是改掉數字 就正常的畫面

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

        喜歡

        • Heresy 說道:

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

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

          喜歡

          • 翔翔 說道:

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

            可是用niviewr都正常 畫面順暢

            喜歡

          • 翔翔 說道:

            niviewer中 切換到 640×480 30fps  非常順暢

            喜歡

    • 翔翔 說道:

      test 怎麼回覆沒有跑出來!!!!!!!!!!!!!!!!!!

      喜歡

      • Heresy 說道:

        目前的設定是單篇回應有超過三個連結就會被當作疑似廣告,需要人工審查。

        喜歡

    • Heresy 說道:

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

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

      喜歡

      • 翔翔 說道:

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

        喜歡

  4. s1lent92 說道:

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

    喜歡

    • Heresy 說道:

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

      喜歡

  5. student 說道:

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

    喜歡

    • Heresy 說道:

      1. 地板偵測是 NiTE 2 提供的功能。
      2. 建議請參考官方文件。

      喜歡

  6. william 說道:

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

    000000000000000000000
    011111111111111111110
    011111111111111111110
    011111111111111111110
    011111111111111111110
    011111111111111111110
    011111111111111111110
    000000000000000000000

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

    喜歡

    • Heresy 說道:

      如果你是指開啟 REGISTRATION 後、外圍沒有資料的部份的話,那部分的範圍不是固定的,而是會隨著深度變化的。

      喜歡

      • william 說道:

        H老師好

        這麼久之後又想到一個問題 請問隨深度變化是指
        越遠 外圍會越大媽?

        喜歡

        • Heresy 說道:

          不是這樣的。個人建議你可以自己用 CoordinateConverter 試試看。
          https://kheresy.wordpress.com/2013/01/14/coordinate-converter-in-openni-2/

          或是直接去看 OpenNI 的原始碼。

          喜歡

發表迴響

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

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 變更 )

Twitter picture

You are commenting using your Twitter account. Log Out / 變更 )

Facebook照片

You are commenting using your Facebook account. Log Out / 變更 )

Google+ photo

You are commenting using your Google+ account. Log Out / 變更 )

連結到 %s

關注

有新文章發表時,會立即傳送至你的收信匣。

加入其他 531 位關注者

%d 位部落客按了讚: