前一篇《簡單的深度讀取方法》已經大概解釋過 Kinect for Windows SDK v2 要怎麼讀取深度影像了~不過,由於沒有使用圖形介面,所以只能輸出單一點的深度,基本上算是比較難驗證讀出來的結果的。所以這一篇,就使用 OpenCV,來把深度影像畫出來吧~
這邊選擇使用 OpenCV(官網)的原因很簡單,那就是他應該是搭配 C 、可以最簡單地把一張圖畫出來的函式庫了~而 Heresy 這邊是採用 OpenCV 3.0 Beta 的形式來做撰寫;如果是使用 2.x 的人,或許會需要做些調整(有也不多)。
而如果要使用 OpenCV 來顯示 K4W SDK v2 的深度影像的話,程式大致上可以寫成像下面這樣:
#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」的部分,這邊則是另外透過 IDepthFrameSource 的 get_DepthMinReliableDistance() 和 get_DepthMaxReliableDistance() 這兩個函式,來取得感應器的「可靠深度」的最大、最小距離。不過,基本上這個值應該是固定是 500 和 4,500,算是微軟建議的、適合用來做人體骨架追蹤的距離,實際上會取道的深度值是更大的,Heresy 這邊最大應該是抓到 7,999。
接下來在「perpare OpenCV」這邊,則是先建立了兩個 cv::Mat 的物件,用來儲存影像資料。其中 mDepthImg 是 CV_16UC1、代表他是 16bit 的無號整數、只有單一通道,用來儲存深度影像的原始資料;但是由於深度影像雖然是 16bit 的,但是實際上值的範圍只有在前段,所以如果直接顯示的話,會是接近全黑的,所以一般在顯示的時候,會需要把他轉換成 8bit 在顯示,而 mImg8bit 這個 CV_8UC1 的 cv::Mat 物件就是用來儲存轉換後的影像用的。
之後,在主迴圈裏面「4c」的部分,這邊不是使用 AccessUnderlyingBuffer(),而是改採用 CopyFrameDataToArray() 這個函式,把深度影像的資料,複製一分到 mDepthImg.data,也就是 mDepthImg 用來儲存影像資料的記憶體空間裡。
這樣做雖然會多一次複製的時間、以及空間,但是好處就是之後可以直接去修改複製出來的資料、而不用擔心會變更到內部的資料。基本上,是看狀況用了~
如果不想要多這一次複製的話,也可以改成下面的寫法:
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 了。