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 了。


Kinect for Windows v2 C++ 程式開發目錄

關於 Heresy
https://kheresy.wordpress.com

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

  1. Y.C SYU 說道:

    大神您好

    如果要把Kinect V2的深度影像,輸出成圖片儲存在電腦
    有相關範例,能參考嗎?

    喜歡

  2. 吴君临 說道:

    heresy,感谢,看了你的文章受益匪浅。
    有一个问题想请教,我想读取前后两张深度图做帧差,思路是IDepthFrameReader只定义一个,然后在while循环中定义两个IDepthFrame,准备读取视频的相邻两张图片,可是没有成功。你看这种思路有问题么?
    以下是部分代码
    谢谢。
    ……
    // perpare OpenCV
    cv::Mat mDepthImg1(iHeight, iWidth, CV_16UC1);
    cv::Mat mImg8bit1(iHeight, iWidth, CV_8UC1);
    cv::Mat mDepthImg2(iHeight, iWidth, CV_16UC1);
    cv::Mat mImg8bit2(iHeight, iWidth, CV_8UC1);
    // 3a. get frame reader
    IDepthFrameReader* pFrameReader = nullptr;
    pFrameSource->OpenReader(&pFrameReader);
    // Enter main loop
    while (true)
    {
    // 4a. Get last frame
    IDepthFrame* pFrame1 = nullptr;
    IDepthFrame* pFrame2 = nullptr;

    if (pFrameReader->AcquireLatestFrame(&pFrame1) == S_OK)
    {
    pFrameReader->AcquireLatestFrame(&pFrame2);

    // 4c. copy the depth map to image
    pFrame1->CopyFrameDataToArray(iWidth * iHeight, reinterpret_cast(mDepthImg1.data));
    pFrame2->CopyFrameDataToArray(iWidth * iHeight, reinterpret_cast(mDepthImg2.data));

    // 4d. convert from 16bit to 8bit
    mDepthImg1.convertTo(mImg8bit1, CV_8U, 255.0f / uDepthMax);
    mDepthImg2.convertTo(mImg8bit2, CV_8U, 255.0f / uDepthMax);

    absdiff(mImg8bit1, mImg8bit2);
    ……

    喜歡

    • Heresy 說道:

      之前文章中已經有說明過了,AcquireLatestFrame() 這個函式不一定會成功,一定要去檢查他的回傳值,但是你的第二次存取沒有做這件事。

      另外,也不建議這樣寫,K4W SDK 他內部的資料會自己去做記憶體管理,IDepthFrame 充其量只是一個介面,內部的資料不能保證不會有問題。
      沒記錯的話,當一個已經取得資料的 IDepthFrame 在沒有 Release 之前,要存取下一個畫面應該是會有問題的。

      而實際上,這邊的寫法根本不需要用到兩個 IDepthFrame,只要在讀到第一個 IDepthFrame 後,馬上把他複製出來,然後再去讀下一個,這樣不就可以了?
      所以實際上,這邊只需要用兩個 cv::Mat 就夠了。

      最後,提示,你這樣的寫法會只能偵測 1-2, 3-4, 5-6 這些畫面間的差異,2-3, 4-5 的差異是沒辦法計算到的。一般在做動態畫面比較應該不會這樣做,建議修改這方面的設計。

      喜歡

      • 吴君临 說道:

        感谢你的指导。只偵測 1-2, 3-4, 5-6是想通过丢掉一些数据来提高运行速度,因为是想做实时性的东西(这是作为新手一厢情愿的想法)。
        做了修改之后,只能进行第一次循环,pFrame的释放时出现了问题

        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(mDepthImg1.data));
        // 4d. convert from 16bit to 8bit
        mDepthImg1.convertTo(mImg8bit1, CV_8U, 255.0f / uDepthMax);

        pFrame->Release();
        pFrame = nullptr;
        if (pFrameReader->AcquireLatestFrame(&pFrame) == S_OK)
        {
        pFrame->CopyFrameDataToArray(iWidth * iHeight, reinterpret_cast(mDepthImg2.data));
        mDepthImg2.convertTo(mImg8bit2, CV_8U, 255.0f / uDepthMax);
        }
        absdiff(mImg8bit1, mImg8bit2);
        pFrame->Release(); ——这里出现错误无法运行下去了

        }
        }

        此外,我觉得你文章中的
        // 4a. Get last frame
        IDepthFrame* pFrame = nullptr;
        可以放在while循环之前,你觉得呢?

        再次表达感谢。

        喜歡

        • Heresy 說道:

          1. 就算想要控制跳過某些畫面,這樣控制的方法應該不算好
          2. 你第二次的 AcquireLatestFrame 失敗的機率很高,應該說在大部分情況下,他都不會成功。
          而更大的問題是,你完全沒有考慮到它失敗了要怎麼辦;基本上 Release 失敗也是這個原因。
          3. pFrame 的宣告的確可以放在迴圈外,但是不影響。

          喜歡

  3. Jane 說道:

    Heresy 想請問 要如何把圖片嵌入 深度影像中?
    圖片顯示於畫面固定位置。
    能使用opencv嗎?

    謝謝您~

    喜歡

    • Heresy 說道:

      這部分屬於 OpenCV 的問題,建議請先參考 OpenCV 網站的文件。
      另外,針對這個問題,網路上也可以很簡單找到解法。

      喜歡

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

你正使用 WordPress.com 帳號留言。 登出 / 變更 )

Twitter picture

你正使用 Twitter 帳號留言。 登出 / 變更 )

Facebook照片

你正使用 Facebook 帳號留言。 登出 / 變更 )

Google+ photo

你正使用 Google+ 帳號留言。 登出 / 變更 )

連結到 %s

%d 位部落客按了讚: