K4W v2 C++ Part 1:簡單的深度讀取方法

| | 0 Comments| 10:42
Categories:

在上一篇《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 來讀取深度的程式該怎麼寫呢?這樣的程式如果要簡化到極致的話,大致上可以寫成下面的樣子:

// Standard Library
#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 上的檔案(連結)。


程式說明

在上面的程式裡面,可以看到基本的操作流程大致如下:(這邊的編號和上面程式碼內註解的編號是對應的)

  1. 取得並開啟感應器(IKinectSensor

    這邊首先是透過 GetDefaultKinectSensor()MSDN)來取得預設的感應器物件,然後再呼叫 Open() 來開啟它。
    這邊用來對應實際感應器的物件是 pSensor,其型別為 IKinectSensorMSDN)。

  2. 取得深度影像來源(IDepthFrameSource

    在成功地開啟感應器後,接下來則是透過他提供的 get_DepthFrameSource() 這個函式,來取得深度影像的 Frame Source 物件 pFrameSource,其型別為 IDepthFrameSourceMSDN)。

    這邊要注意的,是 Frame Source 類型的物件是對應感應器提供的單一類型的資料來源,可以提供他的相關資訊,不過基本上不能用來直接讀取資料。

  3. 取得深度影像讀取器(IDepthFrameReader

    而要讀取資料的話,接下來則是要透過 IDepthFrameSourceOpenReader() 函式,取得 Frame Reader 的物件 pFrameReader,其型別為 IDepthFrameReaderMSDN)。

  4. 通常接下來,就可以進入主迴圈、讀取深度影像了。這邊的主迴圈是用簡單的 while 來做。

    • 取得深度影像

      在主迴圈裡面,如果要讀取資料的話,基本上就是去呼叫 IDepthFrameReaderAcquireLatestFrame() 這個函式、來向感應器要新的畫面資料(4a)

      這邊要注意的是,AcquireLatestFrame() 這個函式不一定會成功,所以一定要做回傳狀態的檢查。

    • 處理深度影像

      在這邊讀取到的資料是 pFrame、其型別是 IDepthFrameMSDN),代表的是當下感應器最新的畫面;而透過他的 get_FrameDescription() 函式,可以取得 IFrameDescription 型別(MSDN)的物件。透過 IFrameDescription 提供的函式,可以去讀取裡面包含的畫面的基本資訊(長、寬、像素的大小等等)。 (4b)

      而透過 AccessUnderlyingBuffer() 這個函式的話,則可以取得畫面資料的指標(pBuffer)、以及資料陣列的大小(iBufferSize(4c)
      pBuffer 是一個 UINT16 的指標,他所指到的陣列、就是整張圖的資料了。而接下來這邊做的事,就是讀取 ( x, y ) 這一點的深度值、並透過 cout 輸出。(4d)

而當每一個物件不用的時候,都需要去呼叫個別物件的 Release() 函式、來完成資源的釋放,以避免 memory leak。


一些補充資料

上面基本上是比較簡單的解釋,而實際上還有一些要注意的地方。

關於 AcquireLatestFrame()

第一個,是前面有稍微提到的,當透過 IDepthFrameReaderAcquireLatestFrame() 這個函式來向感應器要求最新的畫面的時候,他不一定會成功(回傳 S_OK)。

實際上,有很大的機會,他回傳的會是 E_PENDING,代表還沒有資料可以讀取。所以,當使用這個函式去要求畫面的時候,一定要檢查他的回傳值!否則接下來會是有問題的~

此外,在上面的範例程式裡的寫法,會造成 CPU 使用率飆高的 busy waiting(維基百科)。如果要避免這個問題的話,可能會需要加入適當的等待時間、或是改採用 timer 或 event 的模式來讀取。不過這邊就先不提了。

讀取深度影像

第二點,在這邊是使用 IDepthFrameAccessUnderlyingBuffer() 這個函式,來取得深度影像資料的指標;這個方法所取得的只是指標,所以速度較快,但是由於真正的資料和 SDK 核心內的資料應該是共用的,所以不能修改、之後他的資料可能也會被改掉。
(附註:K4W SDK v2 內部應該是使用 double buffer 的形式,來操作深度影像的畫面的)

而除了這個方法之外,IDepthFrame 也還有提供 CopyFrameDataToArray() 這個函式(MSDN),可以用來把深度資料複製一份。雖然這個方法會需要額外的記憶體空間,也需要額外的複製時間,但是如果想要針對深度影像作修改、或是保存的話,複製一份會是比較合適的作法。

IFrameDescription

此外,在上面的範例程式裡,針對 metadata、也就是 IFrameDescription 的讀取,是每次取得新的畫面、都去要一次;實際上,這不算是一個有效率的方法。

如果在確定影像的基本資料不會改變的情況下,理論上是在主迴圈外、去讀取一次就可以了。而實際上,IDepthFrameSource 也同樣有提供 get_FrameDescription() 這個函式、可以用來取得 IFrameDescription

所以這邊比較有效率的方法,應該會是把 4b 的部分,抽到主迴圈之前,這樣就可以只讀取一次 frame description 了~


這篇就先寫到這了。下一篇,應該會是講怎麼整合 OpenCV 來做顯示了。

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *