在前一篇《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):
-
在專案上方點滑鼠右鍵,點選跳出選單最底下的「Properties」(屬性),叫出專案設定的視窗。
(中文版畫面、英文版畫面) -
在左側導覽窗格中的「Configuration Properties」(組態屬性)下,可以找到「C/C++」,點開後選擇第一項的「Gerenal」(一般)後,右側的列表會有一個「Additional Include Directories」(其他 Include 目錄)。
要使用 OpenNI 2 的話,就需要在這裡面加入 OpenNI 2 的 header 檔所在的路徑,如果是 32 位元的專案,就是加上 $(OPENNI2_INCLUDE) ,如果是 64 位元的專案,則是加上 $(OPENNI2_INCLUDE64)。 (附註 2)
(中文版畫面) -
在左側導覽窗格中,剛剛的「C/C++」下方會有一個「Linker」(連結器),點開後,裡面第一個會是「Gerenal」(一般),點選之後,在右側可以找到「Additional Library Directories」(其他程式庫目錄)。
在這裡面加入 OpenNI 2 的 lib 檔、也就是 OpenNI2.lib 這個檔案的所在的路徑,如果是 32 位元的專案,就是加上 $(OPENNI2_LIB) ,如果是 64 位元的專案,則是加上 $(OPENNI2_LIB64)。
(中文版畫面) -
接下來,在左側的「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 檔所在的地方)。
這時候可以採取的方法主要有幾種:
- 將 OpenNI2 Redist 目錄下所有的檔案,都複製到專案所在目錄。
- 修改 VisualStudio 偵錯階段的工作路徑,例如修改成 $(OPENNI2_REDIST)。
- 將 $(OPENNI2_REDIST) 加入到系統路徑(參考)。
哪種方法好?基本上是看狀況,見仁見智的。由於很多時候,程式還會用到其他函式庫,也有可能會需要用到他們各自的 dll 檔,所以把這些 dll 檔統一放在一起,其實也是一種解決方案。
基礎流程
在專案設定好後,要使用 OpenNI 2 來讀取感應器的資料的話,他的基本流程,大致如下:
-
include OpenNI.h 這個檔案。之後,OpenNI C++ API 的東西,都會在 openni 這個 namespace 下。
-
呼叫 openni::OpenNI::initialize() 這個函式來完成 OpenNI 2 環境的初始化。
-
宣告一個 openni::Device 的物件,並透過他所提供的 open() 這個函式,來完成裝置初始化。
-
如果有多個裝置,想要指定要用哪個裝置的話,需要先透過 openni::OpenNI::enumerateDevices() 這個函式,來取得可使用的裝置列表,再透過指定 URI 的方式,來指定要開啟哪個裝置。
-
如果沒有要特別指定的話,則是以 openni::ANY_DEVICE 當作 URI,讓系統自動決定要使用哪個裝置。
-
-
建立 openni::VideoStream 的物件,透過他的 create() 這個函式,指定這個 video stream 要使用哪個裝置的哪種感應器(紅外線、彩色影像、深度影像)。
建立完成後,則是可以透過 start() 和 stop(),來控制資料的讀取。 -
進入主迴圈,如果要讀取 video stream 當下的資料的話,則是呼叫 VideoStream 所提供的 readFrame() 這個函式,來把資料寫到 openni::VideoFrameRef 裡;而之後則是再透過 VideoFrameRef 所提供的函式,來做資料處理。
-
當不再需要使用感應器的資料的時候,要記得關閉所建立出來的資校。
-
呼叫 openni::VideoStream 的 destory() 這個函式,關閉 video stream。
-
呼叫 openni::Device 的 close(),關閉裝置。
-
-
最後,則是呼叫 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」的部分,在透過 VideoStream 的 readFrame() 這個函式,把這個時間點的影像資料,寫到 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 做操作的寫法,如果是要同時讀取的彩色影像和深度影像的話,則可能還要再做一點的修改。
接下來…就期待下一篇文章吧~
附註
-
這邊的英文版是 Visual Studio 2010 的畫面、文字,中文版則是 Visual Studio 2012 的畫面與文字;不同的版本、不同的設定,選項可能會不盡相同,請自行根據狀況調整。
-
如果有多個項目的話,可以用「;」做區隔。
-
在 OpenNI 2 裡面,透過 Kinect 或 Xtion 取得的深度影像的每一個像素、DepthPixel 的單位,預設應該還是一樣是「mm」(公釐、毫米);不過實際上,他也有定義了幾種不同的 PixelFormat,代表其實是有可能可以娶到其他單位的深度的~所以其實要比較保險一點的話,還是得檢查 VideoMode 裡的 PixelFormat,才能確定 DepthPixel 代表的意義。
你好,我有個問題想問一下
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 位元的問題了;也就是說,你可能連結到錯誤的版本了。
讚讚
您好,
在使用vs2015 , opencv3.2.0時發生以下問題
OPENCVTest.exe’ (Win32): 已載入 ‘C:\Windows\System32\opencv_world320d.dll’。找不到或無法開啟 PDB 檔案。
但資料夾內確定已有這個dll檔
也確定環境變數路徑設定無誤
不知道能否得到您的解答
感謝:)
讚讚
這個訊息應該不是錯誤,只是警告。
如果只有這個訊息,應該是要可以執行的。
關於 PDB 檔請參考
https://msdn.microsoft.com/zh-tw/library/ms241613.aspx
讚讚
heresy,你好!
我在用VideoStream.readFrame()的时候遇到这样的问题:得到depth图像以后需要做一些处理,然后在调试过程中添加断点,程序会暂停在断点处,但是好像readFrame()一直在抓帧并且丢掉,程序从断点继续执行以后获取的depth图像已经是很久以后的一帧了,请问怎么解决这种问题?
祝好!多谢!
讚讚
不太懂你的問題。
你是指你的處理太久,中間掉了很多畫面?還是因為你加了中斷點(Break Point),恢復之後掉很多畫面?
不過不管是哪個,這應該是必然的吧?
OpenNI 並沒有把畫面儲存在設備裡面,所以當過了一段時間後再去呼叫 readFrame(),當然會忽略中間的過程,直接給你現在的畫面啊…
如果你希望的是把所有畫面緩衝下來,之後再慢慢處理的話,那建議先錄成 ONI 檔,之後再去控制一個畫面一個畫面讀。
讚讚
我可能没说太清楚,首先我是读取的oni文件,不是实时的视频流。在读取oni文件的前提下加中断点调试,此时恢复之后也是很多帧之后的画面,感觉在中断之后readFrame()还一直在获取帧并持续丢掉(因为阻塞)。所以我目前的解决方案是把oni再次拆解保存为图片,然后一张一张地读取,如果加断点也不影响后续。但是这样太麻烦了,所以想请教你有没有别的解决方案?
讚讚
請搭配 PlaybackControl 來控制
https://kheresy.wordpress.com/2013/03/04/record-and-playback-of-openni-2/
另外,建議下次請明確描述您的問題,並在對應的文章提出問題,這樣才比較不容易弄錯。
讚讚
[…] 請直接參考Heresy大大的OpenNI 2 基本程式範例 XDD […]
讚讚
[…] 請直接參考Heresy大大的OpenNI 2 基本程式範例 XDD […]
讚讚
Heresy您好
我在VS10開發在執行時會出現下面這些
1>—— 已開始建置: 專案: test64, 組態: Debug Win32 ——
1>LINK : fatal error LNK1104: 無法開啟檔案 ‘C:\Program Files\OpenNI2\Lib\\.obj’
========== 建置: 0 成功、1 失敗、0 最新、0 略過 ==========
安裝步驟我是照以上敘述的 GOOGLE很多資料還是找不到解決方法
請問您知道這error怎麼辦嗎?
感謝您
讚讚
個人覺得應該是專案設定有問題。
建議參考這邊提供的範例專案試試看
https://kheresy.wordpress.com/2013/03/22/course-data-of-openni2-and-nite-2/
讚讚
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 」
你應該沒有切換過去。
讚讚
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
讚讚
請以 0 作為座標系統起點、而不是 1。
讚讚
您好,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
是甚麼意思呢?
謝謝
讚讚
請將專案的 precompiled header 關閉
讚讚