K4W v2 C++ Part 2:使用 OpenCV 顯示深度影像

前一篇《簡單的深度讀取方法》已經大概解釋過 Kinect for Windows SDK v2 要怎麼讀取深度影像了~不過,由於沒有使用圖形介面,所以只能輸出單一點的深度,基本上算是比較難驗證讀出來的結果的。所以這一篇,就使用 OpenCV,來把深度影像畫出來吧~

這邊選擇使用 OpenCV(官網)的原因很簡單,那就是他應該是搭配 C 、可以最簡單地把一張圖畫出來的函式庫了~而 Heresy 這邊是採用 OpenCV 3.0 Beta 的形式來做撰寫;如果是使用 2.x 的人,或許會需要做些調整(有也不多)。

而如果要使用 OpenCV 來顯示 K4W SDK v2 的深度影像的話,程式大致上可以寫成像下面這樣:

// Standard Library
#include <iostream>

// OpenCV Header
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>

// Kinect for Windows SDK Header
#include <Kinect.h>

using namespace std;

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);

    // 2b. Get frame description
    int        iWidth = 0;
    int        iHeight = 0;
    IFrameDescription* pFrameDescription = nullptr;
    pFrameSource->get_FrameDescription(&pFrameDescription);
    pFrameDescription->get_Width(&iWidth);
    pFrameDescription->get_Height(&iHeight);
    pFrameDescription->Release();
    pFrameDescription = nullptr;

    // 2c. get some dpeth only meta
    UINT16 uDepthMin = 0, uDepthMax = 0;
    pFrameSource->get_DepthMinReliableDistance(&uDepthMin);
    pFrameSource->get_DepthMaxReliableDistance(&uDepthMax);
    cout << "Reliable Distance: "
         << uDepthMin << " – " << uDepthMax << endl;

    // perpare OpenCV
    cv::Mat mDepthImg(iHeight, iWidth, CV_16UC1);
    cv::Mat mImg8bit(iHeight, iWidth, CV_8UC1);
    cv::namedWindow( "Depth Map" );

    // 3a. get frame reader
    IDepthFrameReader* pFrameReader = nullptr;
    pFrameSource->OpenReader(&pFrameReader);

    // Enter main loop
    while (true)
    {
        // 4a. Get last frame
        IDepthFrame* pFrame = nullptr;
        if (pFrameReader->AcquireLatestFrame(&pFrame) == S_OK)
        {
            // 4c. copy the depth map to image
            pFrame->CopyFrameDataToArray(iWidth * iHeight,
                    reinterpret_cast<UINT16*>(mDepthImg.data));

            // 4d. convert from 16bit to 8bit
            mDepthImg.convertTo(mImg8bit, CV_8U, 255.0f / uDepthMax);
            cv::imshow("Depth Map", mImg8bit);

            // 4e. release frame
            pFrame->Release();
        }

        // 4f. check keyboard input
        if (cv::waitKey(30) == VK_ESCAPE){
            break;
        }
    }

    // 3b. release frame reader
    pFrameReader->Release();
    pFrameReader = nullptr;

    // 2d. release Frame source
    pFrameSource->Release();
    pFrameSource = nullptr;

    // 1c. Close Sensor
    pSensor->Close();

    // 1d. Release Sensor
    pSensor->Release();
    pSensor = nullptr;

    return 0;
}

這個版本是為了在部落格上好呈現,所以把錯誤偵測的部份抽掉了;如果要看完整的版本,可以連到 GitHub 上看。

基本上,這個程式的流程和之前的大致上都相同,只有幾點做了修改。

其中一個,就是這次的程式裡面,Heresy 把 IFrameDescription 的讀取抽到主迴圈外面(從本來 4b 變成 2b)~這樣的好處,就是可以只要讀取一次就可以了,而不需要在迴圈內重複地去存取相同的資料;再者,在這個階段就取得了影像的大小,也可以先去配置 OpenCV 的影像記憶體空間,之後重複利用。這些都是可以增進程式效能的。

再來,在「2c」的部分,這邊則是另外透過 IDepthFrameSourceget_DepthMinReliableDistance()get_DepthMaxReliableDistance() 這兩個函式,來取得感應器的「可靠深度」的最大、最小距離。不過,基本上這個值應該是固定是 500 和 4,500,算是微軟建議的、適合用來做人體骨架追蹤的距離,實際上會取道的深度值是更大的,Heresy 這邊最大應該是抓到 7,999。

接下來在「perpare OpenCV」這邊,則是先建立了兩個 cv::Mat 的物件,用來儲存影像資料。其中 mDepthImgCV_16UC1、代表他是 16bit 的無號整數、只有單一通道,用來儲存深度影像的原始資料;但是由於深度影像雖然是 16bit 的,但是實際上值的範圍只有在前段,所以如果直接顯示的話,會是接近全黑的,所以一般在顯示的時候,會需要把他轉換成 8bit 在顯示,而 mImg8bit 這個 CV_8UC1cv::Mat 物件就是用來儲存轉換後的影像用的。

之後,在主迴圈裏面「4c」的部分,這邊不是使用 AccessUnderlyingBuffer(),而是改採用 CopyFrameDataToArray() 這個函式,把深度影像的資料,複製一分到 mDepthImg.data,也就是 mDepthImg 用來儲存影像資料的記憶體空間裡。

這樣做雖然會多一次複製的時間、以及空間,但是好處就是之後可以直接去修改複製出來的資料、而不用擔心會變更到內部的資料。基本上,是看狀況用了~

如果不想要多這一次複製的話,也可以改成下面的寫法:

UINT    uBufferSize = 0;
UINT16*    pBuffer = nullptr;
pFrame->AccessUnderlyingBuffer(&uBufferSize, &pBuffer);
cv::Mat mDepthImg(iHeight, iWidth, CV_16UC1, pBuffer);

這樣就可以讓 mDepthImg 去使用 pBuffer 這塊記憶體空間的資料、而不需要額外複製一份了。

而之後,則是在透過 cv::Mat 提供的 convertTo() 這個函式,把 16bit 的 mDepthImg 轉換到 8bit 的 mImg8bit。這邊轉換的公式,則是 255.0f / uDepthMax;雖然前面也提到,深度值可能會超過 uDepthMax(4,500),不過這邊就先無視了。

再來,就可以透過 cv::imshow()mImg8bit 畫出來了~它的結果大概就會是下圖的樣子:

由於深度影像中每一個點的值,所代表的都是該點離感應器的距離,所有離越遠的值就會越大;而在顯示出來後,就是顏色越亮(接近白色)的部分越遠、顏色越深(接近黑色)的部分越近了~至於畫面中純黑色、值是 0 的點,則代表該點沒辦法偵測到深度。

而如果想要改變呈現的效果、顏色,基本上就是要在把 mDepthImg 轉換成 mImg8bit 的時候下功夫了。不過,這邊就先不提了。

至於如果要搭配其他的圖形介面使用的話,其實大多都是使用類似的方法。這部分就是要去查該圖形介面 SDK 的文件、找到對應的 API 了。

發佈留言

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