在上一篇《Kinect for Windows SDK v2 C API 簡介》裡,基本上已經簡單地介紹 Kinect for Windows SDK v2 的 C API 了。而這一篇,就開始來寫第一個 K4W v2 的 C 程式吧!
而 Heresy 這邊基本上都是使用 Microsoft Visual Studio 2013 來做開發,如果是沒有這套軟體的話,建議可以考慮去下載免費的 Visual Studio Community 2013 來用。
如果要建立新專案的話,由於 Heresy 是使用標準的 C 來做開發,所以是點選 VisualStudio 上方工具列的「檔案」裡的「新增」-「專案」,然後在新增專案的視窗左側,選取「範本」、「Visual C 」下的「Win32」;然後,在右邊應該就會有「Win32 主控台應用程式」可以選取了。
之後在「Win32 應用程式精靈」內,請確定在「應用程式設定」裡的「應用程式類型」是「主控台應用程式」,而如果沒有特殊需求的話,也可以把「其他選項」的「空專案」勾起來。而之後,就可以自行加入原始碼檔(.cpp)、開始撰寫程式了。
而針對 Kinect for Windows SDK 的專案設定的部分,請參考《Kinect for Windows SDK v2 C API 簡介》一文內的「專案基本設定」。
簡單的範例
而如果要用 Kinect for Windows SDK v2 來讀取深度的程式該怎麼寫呢?這樣的程式如果要簡化到極致的話,大致上可以寫成下面的樣子:
#include <iostream>
// Kinect for Windows SDK Header
#include <Kinect.h>
int main(int argc, char** argv)
{
// 1a. Get default Sensor
IKinectSensor* pSensor = nullptr;
GetDefaultKinectSensor(&pSensor);
// 1b. Open sensor
pSensor->Open();
// 2a. Get frame source
IDepthFrameSource* pFrameSource = nullptr;
pSensor->get_DepthFrameSource(&pFrameSource);
// 3a. get frame reader
IDepthFrameReader* pFrameReader = nullptr;
pFrameSource->OpenReader(&pFrameReader);
// Enter main loop
size_t uFrameCount = 0;
while (uFrameCount < 100)
{
// 4a. Get last frame
IDepthFrame* pFrame = nullptr;
if (pFrameReader->AcquireLatestFrame(&pFrame) == S_OK)
{
// 4b. Get frame description
int iWidth = 0;
int iHeight = 0;
IFrameDescription* pFrameDescription = nullptr;
pFrame->get_FrameDescription(&pFrameDescription);
pFrameDescription->get_Width(&iWidth);
pFrameDescription->get_Height(&iHeight);
pFrameDescription->Release();
pFrameDescription = nullptr;
// 4c. Get image buffer
UINT uBufferSize = 0;
UINT16* pBuffer = nullptr;
pFrame->AccessUnderlyingBuffer(&uBufferSize, &pBuffer);
// 4d. Output depth value
int x = iWidth / 2,
y = iHeight / 2;
size_t idx = x iWidth * y;
std::cout << pBuffer[idx] << std::endl;
// 4e. release frame
pFrame->Release();
pFrame = nullptr;
uFrameCount;
}
}
// 3b. release frame reader
pFrameReader->Release();
pFrameReader = nullptr;
// 2b. release Frame source
pFrameSource->Release();
pFrameSource = nullptr;
// 1c. Close Sensor
pSensor->Close();
// 1d. Release Sensor
pSensor->Release();
pSensor = nullptr;
return 0;
}
這個程式並沒有圖形介面,如果正確執行的話,在 Console(命令提示字元視窗)上應該會輸出一百個整數,代表了深度感應器中心點所偵測到的深度距離。
不過上面的程式並沒有包含錯誤的偵測與處理,如果需要有加入錯誤偵測的程式碼的話,可以參考 Heresy 放在 GitHub 上的檔案(連結)。
程式說明
在上面的程式裡面,可以看到基本的操作流程大致如下:(這邊的編號和上面程式碼內註解的編號是對應的)
-
取得並開啟感應器(IKinectSensor)
-
取得深度影像來源(IDepthFrameSource)
在成功地開啟感應器後,接下來則是透過他提供的 get_DepthFrameSource() 這個函式,來取得深度影像的 Frame Source 物件 pFrameSource,其型別為 IDepthFrameSource(MSDN)。
這邊要注意的,是 Frame Source 類型的物件是對應感應器提供的單一類型的資料來源,可以提供他的相關資訊,不過基本上不能用來直接讀取資料。
-
取得深度影像讀取器(IDepthFrameReader)
而要讀取資料的話,接下來則是要透過 IDepthFrameSource 的 OpenReader() 函式,取得 Frame Reader 的物件 pFrameReader,其型別為 IDepthFrameReader(MSDN)。
-
通常接下來,就可以進入主迴圈、讀取深度影像了。這邊的主迴圈是用簡單的 while 來做。
-
取得深度影像
在主迴圈裡面,如果要讀取資料的話,基本上就是去呼叫 IDepthFrameReader 的 AcquireLatestFrame() 這個函式、來向感應器要新的畫面資料(4a)。
這邊要注意的是,AcquireLatestFrame() 這個函式不一定會成功,所以一定要做回傳狀態的檢查。
-
處理深度影像
在這邊讀取到的資料是 pFrame、其型別是 IDepthFrame(MSDN),代表的是當下感應器最新的畫面;而透過他的 get_FrameDescription() 函式,可以取得 IFrameDescription 型別(MSDN)的物件。透過 IFrameDescription 提供的函式,可以去讀取裡面包含的畫面的基本資訊(長、寬、像素的大小等等)。 (4b)
而透過 AccessUnderlyingBuffer() 這個函式的話,則可以取得畫面資料的指標(pBuffer)、以及資料陣列的大小(iBufferSize)(4c)。
pBuffer 是一個 UINT16 的指標,他所指到的陣列、就是整張圖的資料了。而接下來這邊做的事,就是讀取 ( x, y ) 這一點的深度值、並透過 cout 輸出。(4d)
-
而當每一個物件不用的時候,都需要去呼叫個別物件的 Release() 函式、來完成資源的釋放,以避免 memory leak。
一些補充資料
上面基本上是比較簡單的解釋,而實際上還有一些要注意的地方。
關於 AcquireLatestFrame()
第一個,是前面有稍微提到的,當透過 IDepthFrameReader 的 AcquireLatestFrame() 這個函式來向感應器要求最新的畫面的時候,他不一定會成功(回傳 S_OK)。
實際上,有很大的機會,他回傳的會是 E_PENDING,代表還沒有資料可以讀取。所以,當使用這個函式去要求畫面的時候,一定要檢查他的回傳值!否則接下來會是有問題的~
此外,在上面的範例程式裡的寫法,會造成 CPU 使用率飆高的 busy waiting(維基百科)。如果要避免這個問題的話,可能會需要加入適當的等待時間、或是改採用 timer 或 event 的模式來讀取。不過這邊就先不提了。
讀取深度影像
第二點,在這邊是使用 IDepthFrame 的 AccessUnderlyingBuffer() 這個函式,來取得深度影像資料的指標;這個方法所取得的只是指標,所以速度較快,但是由於真正的資料和 SDK 核心內的資料應該是共用的,所以不能修改、之後他的資料可能也會被改掉。
(附註:K4W SDK v2 內部應該是使用 double buffer 的形式,來操作深度影像的畫面的)
而除了這個方法之外,IDepthFrame 也還有提供 CopyFrameDataToArray() 這個函式(MSDN),可以用來把深度資料複製一份。雖然這個方法會需要額外的記憶體空間,也需要額外的複製時間,但是如果想要針對深度影像作修改、或是保存的話,複製一份會是比較合適的作法。
IFrameDescription
此外,在上面的範例程式裡,針對 metadata、也就是 IFrameDescription 的讀取,是每次取得新的畫面、都去要一次;實際上,這不算是一個有效率的方法。
如果在確定影像的基本資料不會改變的情況下,理論上是在主迴圈外、去讀取一次就可以了。而實際上,IDepthFrameSource 也同樣有提供 get_FrameDescription() 這個函式、可以用來取得 IFrameDescription。
所以這邊比較有效率的方法,應該會是把 4b 的部分,抽到主迴圈之前,這樣就可以只讀取一次 frame description 了~
這篇就先寫到這了。下一篇,應該會是講怎麼整合 OpenCV 來做顯示了。