透過 OpneNI 讀取 Kinect 深度影像資料


前一篇文章已經大概針對 OpenNI 的架構做了一些說明,而這一篇就來針對如何使用 OpenNI 讀取微軟的 Kinect 的影像資料吧!

而先說明一下,Heresy 這篇文章是使用 Visual C++ 2010,針對目前微軟的 Kinect、SensorKinect 的驅動程式,搭配 1.0.0.23 版的 OpenNI 版寫的;如果使用其他版本、或是其他支援 OpenNI 的裝置,那可能會要做一些對應的修改。同時,在開始閱讀這篇文章前,建議也請先參考《在 WIndows 上安裝 Kinect(含 MMD 使用 Kinect 簡易教學)》來安裝 Kinect 和 OpenNI,並確定可以正常運作。

首先,OpenNI 他預設的安裝路徑是在「C:\Program Files\OpenNI」,要開發 OpenNI 程式所有必要的檔案,都會在這裡;而在資料夾內,除了「Documentation」裡有提供兩份文件可以用來當作開發程式的依據外,在「Samples」目錄下,也有提供不少範例可以用來參考。

OpenNI 的核心基本上是 C 語言,不過他有提供 C++ 的 Wrapper 來當作 C++ 使用;基於個人習慣的關係,Heresy 在這邊會以 C++ 的形式,來使用 OpenNI。而必要的 header 檔,都會在 OpenNI 的「Include」目錄內,連結程式時所需的 openNI.lib 這個檔案則是在「Lib」裡。不過要注意的是,OpenNI 目前在 Windows 環境下只有 32 位元的版本、沒有 64 位元版,所以目前只能編譯 32 位元的 OpenNI 程式。

而要設定一個使用 OpenNI 的 Visual C++ 專案也很簡單,只要在專案「C/C++ \ Additional Include Directories」裡加入「$(OPEN_NI_INCLUDE)」、「Linker \ Additional Library Directories」裡加上「$(OPEN_NI_LIB)」,並在「Linker \ Additional Dependencies」裡加上「OpenNI.lib」,這樣就可以了。

而接下來,Heresy 就先以讀取 Kinect 的深度影像資訊為目標,來寫一個 C++ 的範例程式了~他的程式碼如下:

#include <stdlib.h>
#include <iostream>
#include <string>

#include <XnCppWrapper.h>

using namespace std;

void CheckOpenNIError( XnStatus eResult, string sStatus )
{
  if( eResult != XN_STATUS_OK )
    cerr << sStatus << " Error: " << xnGetStatusString( eResult ) << endl;
}

int main( int argc, char** argv )
{
  XnStatus eResult = XN_STATUS_OK;
 
  // 2. initial context
  xn::Context mContext;
  eResult = mContext.Init();
  CheckOpenNIError( eResult, "initialize context" );
 
  // set map mode
  XnMapOutputMode mapMode;
  mapMode.nXRes = 640;
  mapMode.nYRes = 480;
  mapMode.nFPS = 30;
 
  // 3. create depth generator
  xn::DepthGenerator mDepthGenerator;
  eResult = mDepthGenerator.Create( mContext );
  CheckOpenNIError( eResult, "Create depth generator" );
  eResult = mDepthGenerator.SetMapOutputMode( mapMode );
 
  // 4. start generate data
  eResult = mContext.StartGeneratingAll();
 
  // 5. read data
  eResult = mContext.WaitAndUpdateAll();
  if( eResult == XN_STATUS_OK )
  {
    // 5. get the depth map
    const XnDepthPixel*  pDepthMap = mDepthGenerator.GetDepthMap();
    // 6. Do something with depth map
  }
  // 7. stop
  mContext.StopGeneratingAll();
  mContext.Shutdown();
 
  return 0;
}

這個程式的功能,基本上就是去透過 OpenNI 讀取一張解析度 640 x 480 的深度資訊影像;但是在讀取到資料後,並沒有針對取得的資料做任何事,所以如果沒有問題的話,這個程式是會直接結束,而沒有任何產出的。

接下來,就來仔細看程式碼的部分。

  1. Header

    首先,要以 C++ 的形式使用 OpenNI 的話,只需要加入「XnCppWrapper.h」這個標頭檔就好了,不用再 include 其他的檔案。而 OpenNI 定義了名為「xn」的 namespace,所有的物件,大多都在這個 namespace 內,而不在 namespace 內的東西,也都有 XN 這個 prefix,所以應該還算滿好區分的。

  2. 初始化 context

    要使用 OpenNI,要先建立一個型別為「xn::Context」的 conext 物件(這裡就是「mContext」),用來管理整個 OpenNI 的環境狀態以及資源;而在開始使用前,必須要呼叫它的成員函式「Init()」來進行起始化(上方程式碼「initial context 」的部分)。在進行起始化的時候,所有 OpenNI 相關的模組會被讀取、分析,直到呼叫「Shutdown()」這個函式,才會把所使用的資源釋放出來。

  3. 建立、設定所需要的 Production Node

    在 context 起始化成功後,接下來是要建立所要使用的 production node 了。由於這個範例的目的只是要讀取深度感應器的資料,所以這裡要建立的就只有「depth generator」一種,他的型別是「xn::DepthGenerator」。  而建立一個 production node 的方法,則是先宣告出他的物件(這裡就是「mDepthGenerator」),然後再去呼叫他的「Create()」函式,並把 context 傳入,這樣就可以了(上方程式碼中「create depth generator 」的部分)。

    不過要注意的是,有的時候在建立出 node 後,還需要對這個 node 作一些設定。像在這邊,就還必須要透過「SetMapOutputMode()」這個函式,來設定 mDepthGenerator 這個 depth generator 的輸出模式;而以 Kinect 來說,是要設定成為 640 x 480、30FPS。

  4. 開始產生資料

    在必要的 production node(這邊只有一個)都建立好了以後,接下來就是開始產生資料(generate data)了!由於 OpenNI 的概念是所以屬於 generator 的 production node(名稱裡有 generator 的都是)在使用時,都會不停地產生資料,所以得透過 context 來統一控制資料讀取的開關。

    而控制的方法很簡單,就是透過 context 的成員函式「StartGeneratingAll()」來開始、並透過「StopGeneratingAll()」停止。在一個 context 執行「StartGeneratingAll()」開始讀取後,屬於他的 generator node 都會開始產生資料,直到呼叫「StopGeneratingAll()」才會停止。

  5. 讀取資料

    在開始產生資料後,就可以讀取各個不同的 production node 的資料了~不過不同類型的 generator 必須要透過不同的函式來讀取資料,像這邊的 depth generator 就是要用「GetDepthMap()」這個函式,來取得目前的 depth map。而 Depth Generator 取得的資料,會是一個「XnDepthPixel」的 const 指標,指向他實際資料的空間。

    不過這邊另外要注意的就是,generator 雖然是會不停地讀取新的資料,但透過「GetDepthMap()」這類的函式,是有可能會拿到舊的資料的。而為了確保能取得最新的資料,在讀取 Generator 的資料前,都必須要先呼叫 context 的 wait / update 這一系列的函式,來進行 node 資料的更新。

    這系列的函示有四個:WaitAnyUpdateAll()WaitOneUpdateAll()WaitNoneUpdateAll() 和這邊所使用的 WiatAndUpdateAll()。這四者都會更新 context 下所有的 node 的資料,差別只在於更新的條件;Heresy 這邊所使用的 WiatAndUpdateAll() 會等到所有的 node 都取得新資料後,再統一更新所有的 node 的資料;而 WaitAnyUpdateAll() 是等到隨便一個 node 有新資料時就會更新、WaitOneUpdateAll() 則是等到指定的 node 有新資料時再更新、WaitNoneUpdateAll() 則是不管有沒有新資料就強制更新。基本上,這四個不同的函式就是自己看時機、需求使用了。

  6. 處理讀取到的資料

    前面已經有提過了,Depth Generator 取得的資料,會是一個「XnDepthPixel」的 const 指標、而實際上它就是一個大小是 640 x 480 的一維陣列(因為現在的輸出模式是 640 x480),基本上可以把它看作一張 640 x480 的灰階圖片,其中每一個點都代表他的在這個位置的深度、型別是「XnDepthPixel」;而他的深度值在 Windows 32 位元的平台上,型別應該等同於「unsigned short」。基本上,這裡的深度值越大、代表距離越遠(0 則是代表該點深度無法判別),如果透過 OpenNI 的函式,也可以換算出絕對距離,不過在這篇文章暫時不會提到就是了。

    在這個範例程式裡,Heresy 什麼事都沒有做。如果要額外處理這個深度圖的資料的話,只要在「// 6. Do something with depth map」那裡,讀取「pDepthMap」這個指標的資料來做處理就可以了。像如果把直接它的深度資訊由 XnDepthPixel 轉換為一般的 256 灰階圖輸出的話,就會是類似右邊的結果;而當然,這樣的圖意義不大,但是其實這些深度資訊還可以拿來做很多應用,這點就看程式開發者怎麼發揮了~

    (Heresy 本來有想連儲存圖檔一起寫,不過由於牽扯到儲存圖檔的話,程式碼會變得比較複雜,所以在這邊也就先跳過了)

  7. 結束

    當讀取完資料,不再繼續讀取資料後,就要把 OpenNI 停下來;而這邊為了停止繼續產生資料所呼叫的函示,就是之前已經提到過的「StopGeneratingAll()」。而如果完全不打算繼續使用 OpenNI 的環境的話,則也要記得呼叫「Shutdown()」這個函式,把 OpenNI 所使用的資源釋放出來。

  8. 錯誤偵測

    如果仔細看前面的程式碼應該可以發現,Heresy 在大部分的地方都用一個型別是「XnStatus」的變數「eResult」來接 OpenNI 函式的回傳值,而實際上,這就是用來判斷 OpenNI 的函式是否正確執行的依據;如果一個 OpenNI 函式的回傳值式「XN_STATUS_OK」的話,就代表他執行結果是正確的,但是如果不是的話,就代表可能出問題了~而要知道出了什麼問題,則可以透過「xnGetStatusString()」這個函式,來取得文字的錯誤訊息;像上面 Heresy 自己定義的「CheckOpenNIError()」,就是在做這件事的。

這篇 Kinect + OpenNI 的第一個範例,就大概先寫到這了。基本上,這篇算是透過抓取深度的範例,來大概解釋一下怎麼使用 OpenNI 裡的 map generator 了~而這邊的程式也相當單純,之後還會再慢慢寫一些更進階的應用的。


OpenNI / Kinect 相關文章目錄

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

159 Responses to 透過 OpneNI 讀取 Kinect 深度影像資料

  1. Neleson 說道:

    1>c:\users\jim\documents\visual studio 2008\projects\123\123\xncppwrapper.h(28) : fatal error C1083: 無法開啟包含檔案: ‘XnOpenNI.h’: No such file or directory
    1>已將建置記錄儲存於 “file://c:\Users\JIM\Documents\Visual Studio 2008\Projects\123\123\Debug\BuildLog.htm"
    1>123 – 1 個錯誤,0 個警告
    ========== 建置: 0 成功、1 失敗、0 最新、0 略過 ==========

    標頭檔都加入了為什麼= =

    • Heresy 說道:

      看起來你似乎是把 XnCppWrapper.h 複製到你的專案目錄了?
      這個方法是錯誤的,請不要移動他。

      你需要做的,是在專案設定裡,加上 include path。

      • Neleson 說道:

        所以我都不需要加入任何標頭檔? 我已經加入了XnCppWrapper.h & XnOpenNI.h 都取消掉?

        這是您2011年的文章且是32bit 目前使用64bit應該沒有影響吧? 或者年代改版不同的差異?

        • Heresy 說道:

          以 C++ 來開發 OpenNI 的程式的時候,只需要加入 XnCppWrapper.h,在它裡面會去加入其他需要的 header 檔,所以不需要手動加入 XnOpenNI.h。
          但是要加上 #include 並不是要你把檔案移到自己的程式目錄。當你搬移了檔案後,你會破壞掉它內部的相對路徑、而造成編譯錯誤。

          這部分不管是舊版、還是新版的 OpenNI 都沒有變;而 32 位元和 64 位元的程式,在 header 的部分也都沒有變化,不同的是要 link 的 lib 檔、路徑不一樣。

          • Neleson 說道:

            不好意思有沒有圖文解說版可以連結vs跟kinect的方法,文字敘述有點複雜不知道是怎麼樣個加入法,且您的每個文章會有需要加入不同的標頭?

          • Heresy 說道:

            必要的設定基本上在文內都有說明了:
            在 VC++ 專案上按右鍵,選「Properties」
            在「C/C++ \ Additional Include Directories」裡加入「$(OPEN_NI_INCLUDE)」
            在「Linker \ Additional Library Directories」裡加上「$(OPEN_NI_LIB)」
            並在「Linker \ Additional Dependencies」裡加上「OpenNI.lib」

            基本上專案的設定只要加上這三個東西就可以了。
            這部分基本上是 C++ header 檔的機制,以及 Visual C++ 的基本操作。
            如果對於 Visual C++ 不熟系的話,建議先去找相關的教學會比較好。

  2. mon 說道:

    請問一下,在讀取深度影像時
    是否可以動態的讀取每個pixel的距離,並做處理
    如:某個深度值以某種顏色呈現。
    謝謝

  3. kevin 說道:

    你好我想請問一下
    由於kinect抓出來的深度圖其實雜訊相當多
    我想請問一下您是怎麼處理的

    • Heresy 說道:

      Heresy 這邊是沒有做額外的處理。
      如果要的話,應該是去參考影像處理的演算法,想辦法來做去雜訊或平滑化了。

  4. alonso 說道:

    請問用visual studio 2010開新專案的話要開哪一種啊?

  5. C.y. Yang 說道:

    我想請問一下,如果我想將得到的深度資料,存在一個txt檔,然後裡面是一個640*480的矩陣,
    我需要在"6. Do something with depth map"加上什麼?,可以教教我嗎?

  6. Pingback: Kinect + OpenNI 的深度值 « Heresy's Space

  7. YUMMy 說道:

    請問如果是用VC6,要如何設定""C/C++ \ Additional Include Directories」裡加入「$(OPEN_NI_INCLUDE)」、「Linker \ Additional Library Directories」裡加上「$(OPEN_NI_LIB)」,並在「Linker \ Additional Dependencies」裡加上「OpenNI.lib」""
    這些東西呢?

    • Heresy 說道:

      Heresy 這邊已經沒有 VC6 了,所以沒辦法告訴你細節。
      不過基本上,VC6 的專案設定裡,一樣有這些東西可以設定,請自己在專案的屬性設定裡面找到對應的欄位。

      不過同時,也必須說,Heresy 不確定 OpenNI 是否可以 VC6 上運作。

  8. richard 說道:

    问一下,如果连接两个kinect sensor, 每一个都只读取深度信息,enumeration应该怎样完成?

    • Heresy 說道:

      這個問題已經被問過好幾次了…
      不過很抱歉,Heresy 沒有兩台 Kinect 可以來做測試,所以無法提供相關範例。

      • richard 說道:

        谢谢回复,我在网上找到了范例~
        我现在的问题是,发现有一个USB口不可用:只连接一个kinect sensor到这个USB口,驱动安装正确,可跑程序的时候就是无法读取深度信息;用同样的一个kinect sensor连接到其他三个USB口,跑同样的程序,均可以获得深度信息。实在是搞不清这是为什么,求教大神啊~

        • Heresy 說道:

          建議檢查看看那個不能用的 USB Controller 上還有沒有連接其他裝置。
          有可能是因為其他裝置也在使用,導致頻寬不足造成的問題。

  9. Pingback: 使用 XML 設定檔來初始化 OpenNI « Heresy's Space

  10. kk 說道:

    請問一下,目前有辦法利用深度資訊來抓取判別動作了嗎? 像骨架那樣,如果有,方便教學嗎 謝謝。

發表迴響

Fill in your details below or click an icon to log in:

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 / 變更 )

連結到 %s

Follow

Get every new post delivered to your Inbox.

Join 262 other followers