OpenNI 2 基本程式範例


在前一篇《OpenNI 2 簡介》裡,Heresy 大概解釋了 OpenNI 2.0 的基本功能以及他的架構。而接下來的這一篇,就是要來講怎麼寫 OpenNI 2 的程式了~如果是要了解 OpenNI 1.x 版的程式開發的話,請參考《OpenNI 1.x 教學文章》這系列的文章。

首先,在安裝好 OpenNI 2.0 的 SDK 後,在安裝目錄(預設是 C:\Program Files\OpenNI2\)裡面,會有下列的資料夾:

目錄
用途
32 位元
64 位元
Documentation OpenNI SDK 開發程式的參考文件  
Driver 官方支援硬體的驅動程式  
Include 程式開發時必須的 header 檔 $(OPENNI2_INCLUDE) $(OPENNI2_INCLUDE64)
Lib 程式開發時必須的 lib 檔 $(OPENNI2_LIB) $(OPENNI2_LIB64)
Redist 程式執行時必須的 runtime library(dll) $(OPENNI2_REDIST) $(OPENNI2_REDIST64)
Samples 範例程式  
Tools 工具,目前只有 NiViewer  

VisualStudio 2010 專案設定

而如果是使用 Visual Studio 2010 來開發 OpenNI 2 的程式的話,基本上要在新建立的專案、或是現有專案裡,針對 including 和 linking 做設定,他的基本方法如下(附註 1):

  1. 在專案上方點滑鼠右鍵,點選跳出選單最底下的「Properties」(屬性),叫出專案設定的視窗。
    中文版畫面英文版畫面

  2. 在左側導覽窗格中的「Configuration Properties」(組態屬性)下,可以找到「C/C++」,點開後選擇第一項的「Gerenal」(一般)後,右側的列表會有一個「Additional Include Directories」(其他 Include 目錄)。
    要使用 OpenNI 2 的話,就需要在這裡面加入 OpenNI 2 的 header 檔所在的路徑,如果是 32 位元的專案,就是加上 $(OPENNI2_INCLUDE) ,如果是 64 位元的專案,則是加上 $(OPENNI2_INCLUDE64)。 (附註 2)


    中文版畫面

  3. 在左側導覽窗格中,剛剛的「C/C++」下方會有一個「Linker」(連結器),點開後,裡面第一個會是「Gerenal」(一般),點選之後,在右側可以找到「Additional Library Directories」(其他程式庫目錄)。
    在這裡面加入 OpenNI 2 的 lib 檔、也就是 OpenNI2.lib 這個檔案的所在的路徑,如果是 32 位元的專案,就是加上 $(OPENNI2_LIB) ,如果是 64 位元的專案,則是加上 $(OPENNI2_LIB64)


    中文版畫面

  4. 接下來,在左側的「Linker」(連結器)下,「General」(一般)的下面會有一個「Input」(輸入),點選後右邊可以找到「Additional Dependencies」(其他相依性);在這邊加入 OpenNI 2.0 的 lib 檔檔案名稱,也就是「OpenNI2.lib」。


    中文版畫面

這樣,基本的專案設定就完成了。

要注意的是,在 Visual Studio 裡,不同的建置組態,例如 debug、release、Win32、x64,這些設定都是不同的~所以如果變更建置組態後,這些設定也是需要另外設定的。

另外,在執行時要注意的是,OpenNI 2 的運作模式和 OpenNI 1.x 不一樣,所以它是設計成讓每個應用程式,可以個別擁有各自的 runtime library(dll 檔)等檔案,所以要執行的時候,就必須要讓程式找的到 OpenNI 2 安裝資料夾中,Redist 目錄下的檔案,否則程式執行時,就會出現找不到 OpenNI2.dll 的錯誤(如右圖)。這點,其實算是比較接近一般 C++ 函式庫的使用方法的。

在 Windows 下,基本上應用程式在執行的時候,會優先去找程式執行的目錄下、是否有所需要的 dll 檔;所以最簡單的方法,就是把 Redist 目錄下所有的檔案,都複製一份到程式執行檔所在目錄就可以了。

不過如果是在 Visual Srudio 裡面進行開發的話,由於 VisualStduio 是可以設定執行時的工作目錄的,而工作目錄並不一定會是執行檔所在的路徑(預設不是),所以直接把 Redist 目錄下的檔案複製到執行檔所在路徑,在進行偵錯的時候並不一定有用。

而要確定 Visual Studio 的工作路徑在哪,可以透過點選專案、按右鍵後選擇右鍵選單的「Properties」(屬性),然後在左側選擇「Configuration Properties」(組態屬性)底下的「Debugging」(偵錯);這之後右邊會有「Working Directory」(工作目錄),他的值就代表了在透過 VisualStudio 針對這個專案進行偵錯時,他的工作目錄(英文版螢幕截圖中文版螢幕截圖)。而如果是 Visual Studio 的預設值的話,他的值應該是「$(ProjectDir)」,也就是專案所在目錄(vcxproj 檔所在的地方)。

這時候可以採取的方法主要有幾種:

  1. 將 OpenNI2 Redist 目錄下所有的檔案,都複製到專案所在目錄。
  2. 修改 VisualStudio 偵錯階段的工作路徑,例如修改成 $(OPENNI2_REDIST)
  3. $(OPENNI2_REDIST) 加入到系統路徑(參考)。

哪種方法好?基本上是看狀況,見仁見智的。由於很多時候,程式還會用到其他函式庫,也有可能會需要用到他們各自的 dll 檔,所以把這些 dll 檔統一放在一起,其實也是一種解決方案。


基礎流程

在專案設定好後,要使用 OpenNI 2 來讀取感應器的資料的話,他的基本流程,大致如下:

  1. include OpenNI.h 這個檔案。之後,OpenNI C++ API 的東西,都會在 openni 這個 namespace 下。

  2. 呼叫 openni::OpenNI::initialize() 這個函式來完成 OpenNI 2 環境的初始化。

  3. 宣告一個 openni::Device 的物件,並透過他所提供的 open() 這個函式,來完成裝置初始化。

    • 如果有多個裝置,想要指定要用哪個裝置的話,需要先透過 openni::OpenNI::enumerateDevices() 這個函式,來取得可使用的裝置列表,再透過指定 URI 的方式,來指定要開啟哪個裝置。

    • 如果沒有要特別指定的話,則是以 openni::ANY_DEVICE 當作 URI,讓系統自動決定要使用哪個裝置。

  4. 建立 openni::VideoStream 的物件,透過他的 create() 這個函式,指定這個 video stream 要使用哪個裝置的哪種感應器(紅外線、彩色影像、深度影像)。
    建立完成後,則是可以透過 start()stop(),來控制資料的讀取。

  5. 進入主迴圈,如果要讀取 video stream 當下的資料的話,則是呼叫 VideoStream 所提供的 readFrame() 這個函式,來把資料寫到 openni::VideoFrameRef 裡;而之後則是再透過 VideoFrameRef 所提供的函式,來做資料處理。

  6. 當不再需要使用感應器的資料的時候,要記得關閉所建立出來的資校。

    1. 呼叫 openni::VideoStreamdestory() 這個函式,關閉 video stream。

    2. 呼叫 openni::Deviceclose(),關閉裝置。

  7. 最後,則是呼叫 openni::OpenNI::shutdown(),來關閉整個 OpenNI 的環境。


簡單的範例

上面是用文字來做描述,實際上寫成程式碼,就會類似 OpenNI 官方所提供的「SimpleRead」這個範例一樣(預設位置在 C:\Program Files\OpenNI2\Samples\SimpleRead)。而下面,Heresy 則是在把程式碼做進一步的簡化(主要是刪掉錯誤偵測的部分),變成:

// 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();
   
  // 5 main loop, continue read
  openni::VideoFrameRef frameDepth;
  for( int i = 0; i < 100; ++ i )
  {
    // 5.1 get frame
    streamDepth.readFrame( &frameDepth );
     
    // 5.2 get data array
    const openni::DepthPixel* pDepth 
            = (const openni::DepthPixel*)frameDepth.getData();   // 5.3 output the depth value of center point int idx = frameDepth.getWidth() * ( frameDepth.getHeight() + 1 ) / 2; std::cout << pDepth[idx] << std::endl; }   // 6. close streamDepth.destroy(); devAnyDevice.close();   // 7. shutdown openni::OpenNI::shutdown();   return 0; }

程式碼的內容,大致上就如同上一個段落所說明的,所以基本上這邊就只針對部分地方做補充的說明。

首先是第四部份,建立 VideoStream 的部分。這邊基本上是透過 VideoStream 物件(devAnyDevice)本身的 create() 函式,來指定這個 video stream 要使用哪個裝置的哪種感應器;在這個例子裡,所使用的是 openni::SENSOR_DEPTH,也就是深度感應器的部分。而在目前的 OpenNI 2 裡,除了 SENSOR_DEPTH 外,還有對應到彩色影像的 SENSOR_COLOR,以及對應到紅外線影像的 SENSOR_IR 可以使用。

而在資料讀取、也就是「5」的部分,在透過 VideoStreamreadFrame() 這個函式,把這個時間點的影像資料,寫到 VideoFrameRef 後,要讀取深度資料,就是要透過 VideoFrameRef 的物件(frameDepth)來做存取了~在一般狀況下,主要是透過他的 getWidth()getHeight() 這兩個函式,來取得這個影像的大小。而透過 getData(),則可以取得這個影像的資料;他所回傳的型別,是無型別的指標、void*,實際上是指到一個儲存影像資料的陣列的指標。

由於 OpenNI 2 把影像資料的讀取統一化了,同時也把 OpenNI 1.x 的 MapMetaData 的概念拿掉了,所以在資料的讀取上,會變得比較「低階」一點。首先,如果要做資料的讀取,需要自己根據影像的類型,來做轉型的動作。像在這邊由於是使用深度感應器,影像中每一個像素的資料型別都是 openni::DepthPixel;所以在這邊,就是需要把 void* 強制轉型成為 DepthPixel 的指標來使用(上方範例 5.2 的部分)(附註 3)。

經過這樣的處理,pDpeth 就是一個指到這張深度影像資料的一維振烈的指標,而這個陣列的大小,就是他的寬(透過 getWidth() 取得)乘上高(透過 getHieght() 取得);如果是 640 x 480 的話,pDpeth 所指到的陣列,大小就是 640 x480 = 307,200 了~而其中每一項,都代表一個點的深度;如果是要取得 ( x, y ) 這個點的值的話,就是要做一個簡單的座標換算,去取得他在陣列中的 index。這個基本的換算公式,就是:

int idx = x + y * width;

只要把 x 和 y 帶入上面的公式,就可以簡單地算出每一個點在陣列中的位置,並取出他的值了。

而如果是要使用彩色影像的話,他預設的型別是 openni::RGB888Pixel 這個 structure,裡面是以三個 unsigned char 的變數,分別儲存著 RGB 三種顏色的值;而如果是紅外線影像的話,型別則是 openni::Grayscale16Pixel,實際型別則是和 DepthPixel 一樣,是 unsigned short


小結

這篇算是 OpenNI 2 的第一篇教學文章,就先寫到這了~這邊的範例,基本上應該就算是一個算是最簡單,透過 OpenNI 2 來做深度資料讀取的範例了;其中有很大的篇幅,其實是在講如何設定專案就是了。

而這個範例程式在執行後,會讀取深度感應器的 100 個畫面,並把影像中樣的深度值做輸出,所以在執行後,會看到畫面上有一堆數字出現;由於這部分的程式並沒有繪圖的部分,所以執行後只會看到一串數字,是不會有影像出現的。

實際上,這個範例程式比較好的寫法,應該還是要像官方的「SimpleRead」這個範例一樣,加上錯誤偵測會更好,不過這邊為了篇幅,還是先把它拿掉了。而另外,這邊基本上是只針對單一個 video stream 做操作的寫法,如果是要同時讀取的彩色影像和深度影像的話,則可能還要再做一點的修改。

接下來…就期待下一篇文章吧~


附註

  1. 這邊的英文版是 Visual Studio 2010 的畫面、文字,中文版則是 Visual Studio 2012 的畫面與文字;不同的版本、不同的設定,選項可能會不盡相同,請自行根據狀況調整。

  2. 如果有多個項目的話,可以用「;」做區隔。

  3. 在 OpenNI 2 裡面,透過 Kinect 或 Xtion 取得的深度影像的每一個像素、DepthPixel 的單位,預設應該還是一樣是「mm」(公釐、毫米);不過實際上,他也有定義了幾種不同的 PixelFormat,代表其實是有可能可以娶到其他單位的深度的~所以其實要比較保險一點的話,還是得檢查 VideoMode 裡的 PixelFormat,才能確定 DepthPixel 代表的意義。


OpenNI / Kinect 相關文章目錄

對「OpenNI 2 基本程式範例」的想法

  1. 你好,我有個問題想問一下
    error LNK2019: 無法解析的外部符號 __imp__oniInitialize 在函式 “public: static enum openni::Status __cdecl openni::OpenNI::initialize(void)" (?initialize@OpenNI@openni@@SA?AW4Status@2@XZ) 中被參考

    include,lib,dll 都設定確認了還是會跑出LNK2019的錯誤
    開其他範例檔也是會有相同問題,請問這需要怎麼解決?

    • 這種「無法解析的外部符號」基本上就是連結設定錯誤造成的。
      如果你很確定你的連結路徑、檔案(lib 的部份也有兩個地方)都有正確設定,那比較可能就是 32 位元和 64 位元的問題了;也就是說,你可能連結到錯誤的版本了。

  2. 您好,
    在使用vs2015 , opencv3.2.0時發生以下問題
    OPENCVTest.exe’ (Win32): 已載入 ‘C:\Windows\System32\opencv_world320d.dll’。找不到或無法開啟 PDB 檔案。
    但資料夾內確定已有這個dll檔
    也確定環境變數路徑設定無誤
    不知道能否得到您的解答
    感謝:)

  3. heresy,你好!
    我在用VideoStream.readFrame()的时候遇到这样的问题:得到depth图像以后需要做一些处理,然后在调试过程中添加断点,程序会暂停在断点处,但是好像readFrame()一直在抓帧并且丢掉,程序从断点继续执行以后获取的depth图像已经是很久以后的一帧了,请问怎么解决这种问题?

    祝好!多谢!

    • 不太懂你的問題。
      你是指你的處理太久,中間掉了很多畫面?還是因為你加了中斷點(Break Point),恢復之後掉很多畫面?

      不過不管是哪個,這應該是必然的吧?
      OpenNI 並沒有把畫面儲存在設備裡面,所以當過了一段時間後再去呼叫 readFrame(),當然會忽略中間的過程,直接給你現在的畫面啊…

      如果你希望的是把所有畫面緩衝下來,之後再慢慢處理的話,那建議先錄成 ONI 檔,之後再去控制一個畫面一個畫面讀。

      • 我可能没说太清楚,首先我是读取的oni文件,不是实时的视频流。在读取oni文件的前提下加中断点调试,此时恢复之后也是很多帧之后的画面,感觉在中断之后readFrame()还一直在获取帧并持续丢掉(因为阻塞)。所以我目前的解决方案是把oni再次拆解保存为图片,然后一张一张地读取,如果加断点也不影响后续。但是这样太麻烦了,所以想请教你有没有别的解决方案?

  4. Heresy您好
    我在VS10開發在執行時會出現下面這些
    1>—— 已開始建置: 專案: test64, 組態: Debug Win32 ——
    1>LINK : fatal error LNK1104: 無法開啟檔案 ‘C:\Program Files\OpenNI2\Lib\\.obj’
    ========== 建置: 0 成功、1 失敗、0 最新、0 略過 ==========
    安裝步驟我是照以上敘述的 GOOGLE很多資料還是找不到解決方法
    請問您知道這error怎麼辦嗎?
    感謝您

      • Heresy您好
        我去用了使用你的SimpleDepthReader的範例 還是出現以下的問題
        1>LINK : fatal error LNK1104: 無法開啟檔案 ‘OpenNI2.lib’
        我在想會不會是因為我下載的檔案有問題?
        所以我去
        http://structure.io/openni
        這個位置又抓了一遍
        目前情況還是沒改善
        請問我應該要怎麼辦呢? 謝謝您

        • 這邊因為看到你之前的專案似乎是叫做「test64」,所以建議請確認一下,你是在建置 32 位元還是 64 位元的專案,兩者的設定是不相同的。

          • Heresy您好
            我在使用您的範例時有將including 和 lib的設定改為64位元的,請問除此還有其他部分需要留意嗎?
            謝謝您

          • 如果你本來自己建立的專案也適用 64 位元的話,個人是覺得,你應該只有把設定改成 64 位元的,但是你還是在建置 32 位元的方案。
            請先確認你的建置組態真的已經切換到 64 位元了。

            以第一篇的內容來看:「組態: Debug Win32 」
            你應該沒有切換過去。

  5. 1 2 3 4
    5 6 7 8
    9 10 11 12
    13 14 15 16

    4*(4+1)/2=10

    1 2 3 4 5
    6 7 8 9 10
    11 12 13 14 15
    16 17 18 19 20
    21 22 23 24 25

    5*6/2=15

    因為我對中央位置的點的運算不是很清楚,所以我把我的想法打出來
    請問我哪裡理解錯了呢?

    • 那邊的算法其實不完全正確,但是一直沒有改過來。

      請使用: int idx = x + y * width;
      這個算是來做計算。

      • heresy老師
        1 2 3 4
        5 6 7 8
        9 10 11 12
        13 14 15 16
        假如說我想要"6″這個位置的深度資訊
        “6″ 所在的x,y值是(2,2)
        帶入老師給的式子的話
        2+2*4=10

        會多出一組width
        請問是不是
        int idx=x+(y-1)*width 才對呢?

        因為第一排的數字不需要加上width
        而第2排的數字則是加上1組width
        所以y 因該-1

  6. 您好,Heresy
    我在VS13開發,在執行時會出現下面這行
    ‘#include ‘: skipped when looking for precompiled header use
    導致
    std::cout << pDepth[idx] << std::endl;
    這行程式碼無法執行
    會出現以下這些錯誤
    Error 2 error C2039: 'cout' : is not a member of 'std'
    Error 3 error C2065: 'cout' : undeclared identifier
    Error 4 error C2039: 'endl' : is not a member of 'std'
    Error 5 error C2065: 'endl' : undeclared identifier
    是甚麼意思呢?
    謝謝

發表留言

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