透過 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. 你好
    我有試著本篇文章的code
    屬性那邊有設定過了
    但他血無法啟動應用程式(系統找不到指定的檔案)
    請問是哪邊的設定漏掉了呢?

  2. Heresy大大你好
    我想請問你這個深度的範例
    要是把pDepthMap這個深度的資訊輸出
    輸出的結果是什麼呢?
    會有一堆數字
    是代表某一點的深度?還是整張圖640*480每個pixel的深度?
    非常感謝你提供的教材!!

  3. 請問,1.如果我的kinect for xbox360,是不是只能使用openNI來開發???
    2. 如果我要使用openNI 可以使用C#開發嗎???因為我目前看 好像大多數的人都使用C++

    還請麻煩您回答了~~~謝謝

  4. 不好意思,請問一下
    因為最近學校需要交專題相關的報告,
    所以請問一下可以從你的網站擷取相關內容嗎?
    我會註明出處的

    謝謝

  5. 不好意思@@,開發的環境是該選哪個C++?,我對Visual不是很熟悉,

    專案「C/C++ \ Additional Include Directories」在哪裡呢@@?

      • 感謝Heresy快速地回復,我剛好也有爬文到你的投影片。

        我試了之後,跑出找不到或無法開啟PDB檔案;你之前跟別人回應說似乎是沒關係的?

        @@可是我不知道這樣掉抵算不算有成功? 然後找到QT?

        Visual內 沒有可用的元件來顯示影像嗎?

        • 看不懂你的問題?

          首先,PDB 檔的官方說明: http://msdn.microsoft.com/zh-tw/library/yd4f8bd1.aspx
          基本上它是用來偵錯時的資料庫,找不到只是無法針對該檔案偵錯而已。

          再來,找到 QT?
          這個範例基本上沒有任何圖形介面、也沒用到 Qt,要看 Qt 的範例,請參考:
          https://kheresy.wordpress.com/2011/08/18/show_maps_of_openni_via_qt_graphicsview/

          最後,Visual C++ 有沒有自己的圖形介面函式庫?當然有。
          以前是 MFC,現在應該是 WPF。
          但是基本上 Heresy 這邊的範例訴求之一是跨平台,所以不考慮。

          • 恩恩抱歉~我的意思是說,我有找到你編輯的Qt,只是我以前用bcb都是用VCL比較多

            最近在研究Kinect而開始接觸了一些Visual @@ 很多其實不是很懂,開發的界面又有很多種

            所以我直覺想法是像SDK那樣用PicutreBox或者Image這些…

          • 那我是否需要轉換開發的環境?例如說轉到VC++ 的WIN FORM還是能夠直接呼較Image?

          • 基本上,OpenNI 應該能和大部分的圖形介面函式庫做整合。
            至於要不要使用 WIndows Form,就看你自己想不想用 Windows Form 來做開發了。

          • 我只是覺得win form 有元件 感覺跟我學的比較接近,也許會簡單一些?!

            那我是不是就是把第六步驟的pDepthMap指定給PictureBox?

            是不是能再給我一些guideline,heresy今天真的很謝謝你

            我應該會去找些vc++的書,不然我繞來繞去的還是一知半解

          • Heresy 基本上沒有在使用 MS 的 Windows GUI Library,所以這部分要麻煩你自己找資料了。
            基本上,微軟的方案使用的人相當多,資料應該不會太難找。

            但是,基本上 Depth map 是 16bit 的圖檔,應該依定要做轉換才能顯示、不太可能直接丟給圖形元件就是了。

  6. 请问一下,我单步运行,发现总是在eResult = mDepthGenerator.SetMapOutputMode( mapMode );的时候遇到Unhandled exception at 0x775615de (ntdll.dll) in try.exe: 0xC0000005: Access violation reading location 0x00000000.

    • 抱歉,Heresy 沒碰過這個問題。
      不過,請先確認一下你所使用的相關軟體版本,會不會是有版本相衝突的問題?
      另外也麻煩試試看官方範例有沒有問題?

  7. 您好我直接抓取內建的sample skeleton的程式碼複製到vs08裡面的c# win32主控台應用程式

    新增個專案並且檢查你所說的屬性檔等等都符合且他是自動加入完成的沒有額外動作

    1>—— 已開始建置: 專案: kinect skeleton, 組態: Debug Win32 ——
    1>正在編譯…
    1>正在編譯…
    1>正在連結…
    1>正在嵌入資訊清單…
    1>.\Debug\kinect skeleton.exe.intermediate.manifest : general error c1010070: Failed to load and parse the manifest. {q~0H
    1>已將建置記錄儲存於 “file://c:\Users\JIM\Documents\Visual Studio 2008\Projects\Project1\kinect skeleton\kinect skeleton\Debug\BuildLog.htm"
    1>kinect skeleton – 1 個錯誤,0 個警告
    ========== 建置: 0 成功、1 失敗、0 最新、0 略過 ==========

    但是編譯出現這問題 請問問題何在?

    • 換另外一台電腦編譯成功黑框但是

      could not find ‘../../../Date/Samples config.xml’ nor’ SamplesConfig.xml’.Aborting.

      不如內建demo檔一般正常讀取座標

    • 上面的錯誤訊息,感覺比較像是 VC 專案建立的有問題…

      而你現在的錯誤,則是找不到所需要的 XML 檔,請確定他所指定的相對路徑下,有需要的 XML 檔案;或者,請修改程式碼裡對於 XML 檔案所在位置的指定。

  8. 不好意思,我有些小問題想請教,關於下列這段

    // set map mode
    XnMapOutputMode mapMode;
    mapMode.nXRes = 640;
    mapMode.nYRes = 480;
    mapMode.nFPS = 30;

    為什麼輸出麼型的設定是設定成640*480 30FPS
    是跟kinect本身的硬體有關嗎?

    但紅外線CMOS攝影機的規格不是 320*240 30FPS ?

    能請你幫忙解惑一下嗎?

    非常感謝你!!

    • 這是硬體/驅動程式的規格。
      OpenNI 理論上可以透過 GetMapOutputMode 來取得支援的解析度,但是 Kinect 的硬體模組在這邊應該是有問題。
      如果是 ASUS Xtion Pro Live 的,也可以支援 QVGA。

  9. ‘test.exe’: 已載入 ‘C:\Windows\SysWOW64\msasn1.dll’,找不到或無法開啟 PDB 檔案
    ‘test.exe’: 已載入 ‘C:\Windows\SysWOW64\mswsock.dll’,找不到或無法開啟 PDB 檔案
    ‘test.exe’: 已載入 ‘C:\Windows\SysWOW64\WSHTCPIP.DLL’,找不到或無法開啟 PDB 檔案
    ‘Win32 執行緒’ (0x1a6c) 執行緒以返回碼 0 (0x0) 結束。
    ‘Win32 執行緒’ (0x1be8) 執行緒以返回碼 0 (0x0) 結束。
    ‘[7020] test.exe: 原生’ 程式以返回碼 0 (0x0) 結束。
    =====================================================
    請問一下 我照文中所說的方式去做,卻出現類似像這樣 無法開啟PDB檔案這類的訊息…..
    可否問問大家我是哪個環節漏掉或是出錯了呢?! 謝謝

    • 你的程式是正常的,而找不到對應的 PDB 檔,基本上也不是問題。
      如果有仔細看程式碼的話,你就會發現這個程式本來就是不會顯示任何東西、然後直接結束的。

      你如果希望他能把結果輸出的話,請自行在「// 6. Do something with depth map」加上你要處理深度資料的程式。
      或是去看使用 Qt 來顯示影像的範例:https://kheresy.wordpress.com/2011/08/18/show_maps_of_openni_via_qt_graphicsview/

發表留言

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