OpenNI 2 VideoStream 與 Device 的設定與使用


之前的《OpenNI 2 基本程式範例》一文,基本上是一個最簡單、OpenNI 2 讀取深度的範例;而之後的《OpenNI 2 的錯誤處理》,則是針對 OpenNI 2 所採用的錯誤處理方法,做一個補充。接下來這一篇呢,則是在《OpenNI 2 基本程式範例》中,讀取深度的範例程式的基礎上,加上彩色影像的讀取,並說明如何針對建立出來的 VideoStream 做進一步的參數設定。

而最後,也會稍微帶一下,要怎樣把 OpenNI 2 讀到的影像資料,轉換成其他函式庫,例如 OpenCV 或 Qt 專用的格式。


讀取彩色影像

一般狀態下,OpenNI 2 要同時讀取彩色影像和深度影像,算是相當地簡單。程式的寫法,基本上就和讀取深度影像時相同,只要把彩色的 VideoStream 加上去就好了~而加上厚的程式,基本上就如下:

// STL Header
#include <iostream>
 
// 1. include OpenNI Header
#include "OpenNI.h"
 
int main( int argc, char** argv )
{
  // 2. initialize OpenNI
  openni::OpenNI::initialize();
 
  // 3. open a device
  openni::Device devAnyDevice;
  devAnyDevice.open( openni::ANY_DEVICE );
 
  // 4. create depth stream
  openni::VideoStream streamDepth;
  streamDepth.create( devAnyDevice, openni::SENSOR_DEPTH );
  streamDepth.start();
 
  // 4a. create color stream
  openni::VideoStream streamColor;
  streamColor.create( devAnyDevice, openni::SENSOR_COLOR );
  streamColor.start();
 
  // 5 main loop, continue read
  openni::VideoFrameRef frameDepth;
  openni::VideoFrameRef frameColor;
  for( int i = 0; i < 100; ++ i )
  {
    // 5.1 get frame
    streamDepth.readFrame( &frameDepth );
    streamColor.readFrame( &frameColor );
 
    // 5.2 get data array
    const openni::DepthPixel* pDepth
      = (const openni::DepthPixel*)frameDepth.getData();
    const openni::RGB888Pixel* pColor
      = (const openni::RGB888Pixel*)frameColor.getData();
 
    // 5.3 output the depth value of center point
    int idx = frameDepth.getWidth() * ( frameDepth.getHeight() + 1 ) / 2;
    std::cout  << pDepth[idx]  << "( "
               << (int)pColor[idx].r << ","
               << (int)pColor[idx].g << ","
               << (int)pColor[idx].b << ")"
               << std::endl;
  }
 
  // 6. close
  streamDepth.destroy();
  streamColor.destroy();
  devAnyDevice.close();
 
  // 7. shutdown
  openni::OpenNI::shutdown();
 
  return 0;
}

其中,黃底的部分,就是針對彩色影像做處理的部分了。而和深度影像主要不同的地方,大致上只有兩個:一個是在呼叫 VideoStreamcreate() 函式的時候,是要指定感應器類型為 SENSOR_COLOR;再來就是深度影像的每一個像素的資料都是 DepthPixel 這個型別,而彩色影像的像素資料,則是 RGB888Pixel 這個型別RGB888Pixel 實際上是一個資料結構,裡面有 rgb 三個 unsigned char、也就是 0-255 的值,分別代表該點的紅、綠、藍的色彩。

而把程式做了像上面一樣的修改後,在執行的時候,除了會輸出畫面中央點的深度值後,也會跟著輸出該點的色彩的值了~

另外,在有多個 VideoStream 的情況下,OpenNI 另外也還有提供 OpenNI::waitForAnyStream() 這個函式可以用來等所指定的 VideoStream 陣列,當其中任一個有更新後,再繼續執行;而 VideoStream 另外也有提供所謂的 Listener 的設計,可以把程式寫成當讀取到新的資料的時候,就去做指定的事情,算是一種事件驅動(event driven)的開發模式。這兩種使用方法,基本上算是 OpenNI 2 裡面提供的別種資料存取的模式,在這邊就跳過,之後有機會再講吧。


VideoStream 的模式設定

上面的範例,算是已經把彩色影像和深度影像同時讀取出來了。不過在繼續、把他們真的畫出來之前,這邊先來想一下 OpenNI 2 的一些 VideoStream 模式設定的方法;主要的目的呢,就是根據需求來調整得到的影像的解析度了~

在 OpenNI 2 的架構下,要讀取深度、彩色影像,基本上都是先建立出要使用的 Device 後,再個別去建立出所需要的 VideoStream;在概念上來說,OpenNI 2 的 VideoStream 有點接近 OpenNI 1.x 的 Map Generator。而如果要設定影像的解析度等資訊的話,基本上也是要針對 VideoStream 來進行操作。

以比較常會修改到的解析度、FPS 來說,在 OpenNI 2 裡,是把它包成一個 openni::VideoMode 的類別,來統一處理。透過 VideoMode 的物件,以及 VideoStream 本身的 getVideoMode()setVideoMode() 函式,就可以簡單地取得/設定 VideoStream 的相關參數。下面就是一個讀取目前設定的的例子:

// 4. create depth stream
openni::VideoStream streamDepth;
streamDepth.create( devAnyDevice, openni::SENSOR_DEPTH );

// output basic video information
openni::VideoMode vmMode = streamDepth.getVideoMode();
cout << "Video Mode : " << vmMode.getResolutionX();
cout << " * " << vmMode.getResolutionY();
cout << " @ " << vmMode.getFps() << "FPS";
switch( vmMode.getPixelFormat() )
{
case openni::PIXEL_FORMAT_DEPTH_1_MM:
  cout << " , Unit is 1mm" << endl;
  break;
 
case openni::PIXEL_FORMAT_DEPTH_100_UM:
  cout << " , Unit is 100um" << endl;
  break;
}

上面的程式碼片段,會讀取出目前 VideoStream 的模式,並把他輸出。而從程式裡可以看的出來,VideoMode 它包含的資訊,除了影像的寬、高、FPS 外,也還有每一個像素的格式;以深度影像來說,主要應該就是使用以 1mm 為單位的 PIXEL_FORMAT_DEPTH_1_MM 或以 100um 為單位的 PIXEL_FORMAT_DEPTH_100_UM(另外還有兩種以 shift 為名的格式,不太確定用途),彩色影像的話,則是以 PIXEL_FORMAT_RGB888PIXEL_FORMAT_YUV422 為主(也還有其他的格式)

而如果是要進行設定的話,則大概會是像下面這個樣子:

// set video mode
openni::VideoMode vmMode;
vmMode.setFps( 30 );
vmMode.setResolution( 640, 480 );
vmMode.setPixelFormat( openni::PIXEL_FORMAT_DEPTH_100_UM );
if( streamDepth.setVideoMode( vmMode ) == openni::STATUS_OK )
{
  // OK
}

如果成功的話,這樣就可以把深度影感應器的影像,設定成為 640×480、30FPS,並以 100um 為單位了~

當然,每款感應器可以設定的解析度參數都是有限的,並不能隨便給;而如果要知道目前這個 VideoStream 所對應的感應器支援那些模式,則可以透過 getSensorInfo() 來取得感應器的參數,他的型別是 openni::SensorInfo,他提供了一個 getSupportedVideoModes() 的函式,可以取得所有支援的 VideoMode 的陣列。下面就是一個範例(這邊把 openni 和 std 的 namespace 省略了)

const SensorInfo& rInfo = streamDepth.getSensorInfo();
const Array<VideoMode>& aModes = rInfo.getSupportedVideoModes();
for( int i = 0; i < aModes.getSize(); ++ i )
{
  const VideoMode& rMode = aModes[i];
  cout << "Video Mode : " << rMode.getResolutionX();
  cout << " * " << rMode.getResolutionY();
  cout << " @ " << rMode.getFps() << "FPS";
  switch( rMode.getPixelFormat() )
  {
  case PIXEL_FORMAT_DEPTH_1_MM:
    cout << " , Unit is 1mm" << endl;
    break;
 
  case PIXEL_FORMAT_DEPTH_100_UM:
    cout << " , Unit is 100um" << endl;
    break;
  }
}

這樣的程式碼執行後,就可以把這個深度感應器所支援的所有 VideoMode 都印出來了~下面就是 Heresy 的 Asus Xtion Pro Live 的測試結果:

Video Mode : 320 * 240 @ 30FPS , Unit is 1mm
Video Mode : 320 * 240 @ 30FPS , Unit is 100um
Video Mode : 320 * 240 @ 60FPS , Unit is 1mm
Video Mode : 320 * 240 @ 60FPS , Unit is 100um
Video Mode : 640 * 480 @ 30FPS , Unit is 1mm
Video Mode : 640 * 480 @ 30FPS , Unit is 100um
Video Mode : 320 * 240 @ 25FPS , Unit is 1mm
Video Mode : 320 * 240 @ 25FPS , Unit is 100um
Video Mode : 640 * 480 @ 25FPS , Unit is 1mm
Video Mode : 640 * 480 @ 25FPS , Unit is 100um

基本上,主要就是幾種基本參數的排列組合,全部列出來還滿多種的。

而如果是 Kinect for Xbox 360 感應器的話,支援的模式變化就比較少了,只有三種:

Video Mode : 640 * 480 @ 30FPS , Unit is 1mm
Video Mode : 320 * 240 @ 30FPS , Unit is 1mm
Video Mode : 80 * 60 @ 30FPS , Unit is 1mm

不過說實話…Heresy 這邊覺得滿討厭的一點,是 OpenNI 居然還自己土法煉鋼、自己寫了一個 Array,而沒有用 STL 的版本…這點就有點搞不懂他們在想啥了。

至於彩色影像的部分,基本上也可以用類似的方法來取得;不過就是 getPixelFormat() 的部分,要針對彩色影像的 PixelFormat 來做額外的處理了~

而除了 VideoMode 之外,VideoStream 也還有提供一些介面,可以取得一些額外的資訊,這邊 Heresy 大概選一些列一下:

此外,VideoStream 也還有提供一些其他的介面,可以做資料的控制;像是透過 setMirroringEnable() 可以用來控制取得的影像是否要做鏡像處理,透過 setCropping() 則可以設定影像的裁切範圍,不過在這邊就不詳細說明了,有需要的可以自行參考官方文件。

比較特別的是,如果是針對彩色影像的話,OpenNI 2 的 VideoStream 也提供了 getCameraSettings() 這個函式,可以取得 openni::CameraSetting 的物件,藉此控制攝影機的白平衡、以及自動曝光的開關。


彩色影像與深度影像的視角校正

基本上,不管是 Microsoft Kinect 還是 ASUS Xtion Pro Live,上面的彩色攝影機、和深度攝影機,都是獨立、位置不同的感應器,所以所取得的視角,是不一樣的。

所以如果在沒有特殊處理的情況下、直接把彩色影像和深度影像重疊在一起,就會發現兩張圖會有一些位置上的不一致;像右圖就是把深度影像和彩色影像直接重疊的結果,可以明顯地看的出來,右上角的日光燈,位置位移算是滿明顯的。

在 OpenNI 1.x 的時候,Heresy 有寫過一篇《透過 OpneNI 合併 Kinect 深度以及彩色影像資料》,算是專門在講這部分的設定。而在 OpenNI 2 的架構下,雖然方法不一樣,但是一樣是要做相關的設定,才可以讓兩者的位置更符合的。

在 OpenNI 2 的架構,要進行視角的校正,是要呼叫 Device 所提供的 setImageRegistrationMode() 這個函式,來進行校正方式的設定;而在目前的 OpenNI 2 裡,只有提供「不要校正」(IMAGE_REGISTRATION_OFF)和「把深度對到彩色影像的位置」(IMAGE_REGISTRATION_DEPTH_TO_COLOR)這兩種模式可以使用。

由於考慮到可能不是每一款感應器,都有支援視角校正的功能,所以在設定前,最好也先透過 DeviceisImageRegistrationModeSupported() 這個函式來確認是否支援;所以最後的程式寫法,就是如下:

if( devAnyDevice.isImageRegistrationModeSupported( IMAGE_REGISTRATION_DEPTH_TO_COLOR ) )
{
  devAnyDevice.setImageRegistrationMode( IMAGE_REGISTRATION_DEPTH_TO_COLOR );
}

而設定完之後,就會發現深度影像整個往內縮一圈、並且位置和彩色影像對得更好了~

 

不過,目前 Heresy 這邊測試的結果,很遺憾的,Microsoft Kinect 似乎無法使用這樣的視角修正的功能…這點在官方論壇上也有人提出來(參考《How can I align with Kinect?》),而且看來並非無解。就看之後 OpenNI 是否會去修改、讓官方版可以支援了。


將資料轉換給其他函式庫使用

到這邊為止,已經描述了該怎麼讀取深度與彩色資訊了。但是實際上,由於 OpenNI 本身沒有影像處理、顯示的能力,所以大部分的時候,應該都還是需要把 OpenNI 取得的影像資料,轉換成其他函式庫使用的型別。而由於 OpenNI 2 VideoStreamgetData() 所取得的資料,是很簡單的一維陣列,所以其實相當簡單就可以和大部分的函式庫互通。

OpenCV

以 OpenCV(官網)這套電腦視覺、影像處理的函式庫來說,他使用的影像格式是 cv::Mat 這種型別,我們可以透過他的建構子,簡單地把 VideoFrameRef 的資料,建立出一個 cv::Mat 的物件~以彩色影像來說,他的程式可以寫成:

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

其中,mColorFrame 就是一個彩色影像的 VideoFrameRef 物件,透過這樣的程式,就可以建立出一個 OpenCV 的 Mat 物件 mImageRGB,型別是 CV_8UC3(8bit 正整數、3 channel),而資料就是連結到 mColorFrame 的資料了~不過這邊要注意的是,由於透過 VideoStreamgetData() 取得的資料實際上是 const 的,所以這邊雖然強制轉型成為 void* 來給 OpenCV 使用,但是還是要記得,這筆資料是不可以去修改的

另外,由於 OpenCV 內部彩色影像實際上是使用 BGR 的形式,而非 RGB 的形式,所以如果要做後續的處理的話,是需要把 RGB 影像轉換成 BGR 的~這樣的程式,基本上可以寫成:

cv::Mat cImageBGR;
cv::cvtColor( mImageRGB, cImageBGR, CV_RGB2BGR );

透過這樣的程式,就可以把 RGB 排列的 mImageRGB,轉換成 OpenCV 慣用的 BGR 排列的 cv::Mat 物件 cImageBGR 了~

而深度影像的部分,由於型別不一樣,所以在建立的時候,要記得把 CV_8UC3 改成 CV_16UC1、也就是一個 channel 的 16bit 正整數的影像; 而之後如果要轉換成一般的 8bit 灰階影像的話,也可以透過 cv::Mat 本身提供的 convertTo() 函式來做轉換。

Qt

如果是以 Qt(官網)這個圖形介面函式庫的話,也是可以很簡單地,把彩色影像的資料轉換成 Qt 的 QImage 的形式~在程式的寫法上,也非常地相近,下面就是一個簡單的例子:

QImage qtImage( (const unsigned char*)mColorFrame.getData(),
                 mColorFrame.getWidth(), mColorFrame.getHeight(),
                 QImage::Format_RGB888 ) ) );

透過這樣的程式, qtImage 這個 QImage 的圖形,裡面的資料就會是 mColorFrame 的資料了~

而深度影像的部分,由於 QImage 應該是不像 OpenCV 一樣,有直接支援 16bit 的影像,所以如果要把深度影像轉換成 Qt 可以用的型式的話,可能就得自己先做好轉換了。

而實際上,大部分有影像功能的函式庫,應該也都有提供類似這樣,可以使用外部資料的方法,只要參考相關文件,要套用進去應該是不難的~


這篇基本上就先到這了。如果是想要看有圖形顯示的範例的話,可以參考:

接下來,應該還會找時間寫一下,用 Qt 和 OpenGL 來顯示 OpenNI 2 的資料的範例。而另外,現在都還是把這些資料當一般的影像用,當然之後還有把他們以 3D 的形式、畫出來的範例了。

而在之後,應該就是 Middleware library 的介紹了。


OpenNI / Kinect 相關文章目錄

對「OpenNI 2 VideoStream 與 Device 的設定與使用」的想法

  1. heresy 老師您好:
    下面這行是宣告pDepth 並且把kinect的資料放進去
    const openni::DepthPixel* pDepth
    = (const openni::DepthPixel*)frameDepth.getData();

    我現在因為實驗需要,想要加上一個遮罩,可以把部分的資料設為"0″
    所以我寫了"pDepth[3]=0;" 這樣的式子,但是ERROR告訴我
    pDepth是只能讀取的,請問我應該怎麼寫才有辦法把部分的數值變成0呢
    感謝

  2. Heresy老師,
    想請問要如何關閉自動白平衡和自動曝光呢?
    程式如下,但好像少了什麼,使得彩色圖像的結果看起來並沒有關閉白平衡和曝光。
    VideoStream ColorStream;
    ColorStream.getCameraSettings()->getAutoExposureEnabled();
    ColorStream.getCameraSettings()->getAutoWhiteBalanceEnabled();

    • getXXX 這類的函式都只是用來取得目前的設定用的,請使用對應的 setXXX 函式。
      例如:setAutoExposureEnabled()

  3. Heresy您好:
    (int)pColor[idx].r 可以讀到data內的顏色資訊,那如果我想要讀取這一個point在三維空間中的原始XY座標值該怎麼做呢?

發表留言

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