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

對「透過 OpneNI 讀取 Kinect 深度影像資料」的想法

  1. 老師,關於影像儲存的這一部分,我想跟您請教一下
    我想將 Depth data 以及 RGB data,以 stream 時間分別記錄在 csv file 中
    不知道是否有合適的方法?

    目前我是用 OpenCV 轉 yml 做儲存,但是只能抓到單張數據

    • 深度影像和彩色影像本來就是分開的,而每個時間點的資料也是獨立的,所以你的需求並不難(只是存成 CSV 超級沒效率而已);問題只是你的程式要怎麼寫而已。

  2. 老師您好,如果我已經成功的叫出深度影像(手),但是我想要把周邊的背景都去除掉(只看到手),只知道要用調整深度值來去寫….可否老師能教教我…或是有沒有什麼別的方法呢?

    • 最基本的方法就是透過深度過濾。
      如果這不夠用的話,你就必須要想辦法去辨識出「手」,才有可能過濾了。
      一個方向就是:如果是在 NiTE 能抓到人體骨架的狀況下,那就試試看用 NiTE 抓到的手部位置來做處理吧。

      • 謝謝老師!請問你這邊的程式碼範例教學有沒有關於深度過濾的呢?
        我可以自己研究看看!

        • 要透過深度值的過濾,基本概念就是掃過整張圖、把要的部分存下來。
          而實際上要怎麼實作,則取決於你之後要怎麼應用這個資料。

發表迴響

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

WordPress.com 標誌

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

Google photo

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

Twitter picture

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

Facebook照片

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

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.