在之前的《Kinect Fusion Part 0:使用概念》一文中,已經大致上解釋了 Kinect Fusion 在原理上的概念,以及範例程式的使用方法了。而接下來這一篇,則是來講一下,Kinect Fusion 的 C API 要怎麼使用吧。
Kinect Fusion 的檔案
首先,要在 C 的程式裡面使用 K4W SDK v2 所提供的 Kinect Fusion 模組的話,需要在程式碼內引入「NuiKinectFusionApi.h」這個 header 檔;檔案所在的位置,和 K4W SDK v2 的其他 header 檔相同(不過不知道為什麼,命名規則明顯不同…),都是在「$(KINECTSDK20_DIR)\inc」。而在建置程式時所需要的 lib 檔,則是「Kinect20.Fusion.lib」,一樣是放在「$(KINECTSDK20_DIR)\Lib」下。
比較不一樣的,是在執行階段時所需要的 DLL 檔並沒有被直接放到系統內,而需要另外複製到執行檔的所在路徑;他的檔案是「Kinect20.Fusion.dll」,位於「C:\Program Files\Microsoft SDKs\Kinect\v2.0_1409\Redist\Fusion\」這個目錄下,根據 32 位元和 64 位元的不同,有兩個版本。
相關介面(Interface)
而由於 Kinect Fusion 基本用來操作的介面,基本上都是以「INuiFusion」為字首,在不考慮色彩、最簡單的狀況下,主要是使用「INuiFusionReconstruction」這個介面來做操作;最終得到的結果,則會是「INuiFusionMesh」這個紀錄多邊形資訊的介面。
至於如果想要重建包含色彩的模型的話,則是要使用「INuiFusionColorReconstruction」這個彙整和色彩資訊的版本,其輸出結果的類型也會變成是「INuiFusionColorMesh」。
而除了上面這四個算是比較基本的介面外,Kinect Fusion 也有另外用來計算攝影機位置的「INuiFusionCameraPoseFinder」以及「INuiFusionMatchCandidates」這兩個介面;不過基本上,這兩個介面在基本使用時,是用不到的。
初始化
而在這篇,Heresy 則是先整理沒有色彩的「INuiFusionReconstruction」作為最基本的範例。
首先,由於 Kinect Fusion 基本上是使用深度影像來做處理的,所以在使用 Kinect Fusion 前,需要先按照一般的作法來寫 K4W SDK v2 的程式、讓他可以讀取到深度影像;這部分建議可以先參考以前的《K4W v2 C Part 1:簡單的深度讀取方法》一文。
之後,則是要進行 Kinect Fusion 相關物件的初始化。這邊,是要透過「NuiFusionCreateReconstruction()」這個函式,來產生 INuiFusionReconstruction 的物件。而這個函式的參數總共有五個,分別是:
- const NUI_FUSION_RECONSTRUCTION_PARAMETERS *pReconstructionParameters
- NUI_FUSION_RECONSTRUCTION_PROCESSOR_TYPE reconstructionProcessorType
- INT deviceIndex
- const Matrix4 *pInitialWorldToCameraTransform
- INuiFusionReconstruction **ppNuiFusionReconstruction
其中,第一個參數 pReconstructionParameters 是重建的解析度參數,型別是 NUI_FUSION_RECONSTRUCTION_PARAMETERS 這個資料結構,裡面紀錄的是每個軸向的 voxel 數量、以及每公尺有幾個 voxel。
第二個參數 reconstructionProcessorType 則是用來選擇要在哪種裝置上進行計算,型別是 NUI_FUSION_RECONSTRUCTION_PROCESSOR_TYPE,值有兩種,分別是 NUI_FUSION_RECONSTRUCTION_PROCESSOR_TYPE_CPU 和 NUI_FUSION_RECONSTRUCTION_PROCESSOR_TYPE_AMP;前者是使用 CPU 來做計算,後者則是透過 C AMP、用顯示卡來做加速計算。由於 Kinect Fusion 的計算量算是滿大的,所以這邊會建議使用 C AMP 來做處理。
第三個參數 deviceIndex 則是用來指定要用哪個參數來做處理。如果指定「-1」的話,他會自己找預設的裝置來使用。 如果在系統上有兩張以上的顯示卡,想要指定要用哪張做處理的話,則可以透過 NuiFusionGetDeviceInfo() 這個函式來取得裝置的資訊。
第四個參數 pInitialWorldToCameraTransform 是一個 4 x 4 的矩陣,代表的是攝影機相對於世界座標系統的初始位置關係;一般來說,可以直接給 nullptr,讓他去使用單位矩陣(identity matrix)當作預設值。
最後,第五個參數 ppNuiFusionReconstruction,則就是產生的 INuiFusionReconstruction 物件的指標了。
整個初始化的程式實際寫的話,大致上會像下面這樣:
NUI_FUSION_RECONSTRUCTION_PARAMETERS mResolution;
mResolution.voxelsPerMeter = 64;
mResolution.voxelCountX = 256;
mResolution.voxelCountY = 256;
mResolution.voxelCountZ = 256;
// create INuiFusionReconstruction
INuiFusionReconstruction* pReconstruction = nullptr;
if (NuiFusionCreateReconstruction(
&mResolution,
NUI_FUSION_RECONSTRUCTION_PROCESSOR_TYPE_AMP, -1,
nullptr, &pReconstruction
) != S_OK)
{
std::cerr << "Kinect Fusion object create failed" << std::endl;
return -1;
}
其中,mResolution 的 voxelCountX、voxelCountY、voxelCountZ 就是代表在三軸個要用多少的 voxel 來儲存資訊,而 voxelPerMeter 則是每公尺有多少個 voxel;兩者組合起來,就決定了可掃描的範圍、以及精確度。
資料轉換
在開始講怎麼更新資料之前,這邊需要先講一下,Kinect Fusion 所需要深度影像格式,並不是 K4W SDK v2 的 IDepthFrame,而是 Kinect Fusion 自己另行定義的另一種資料結構、NUI_FUSION_IMAGE_FRAME;所以在使用的時候,會需要先進行資料格式的轉換才能讓 Kinect Fusion 使用。
那要怎麼把資料從本來的 IDepthFrame 轉換成 NUI_FUSION_IMAGE_FRAME 的格式呢?首先,這邊要先使用 NuiFusionCreateImageFrame() 這個函式,來建立一個資料型別是浮點數的 NUI_FUSION_IMAGE_FRAME 的物件。最簡單的寫法,就是:
NuiFusionCreateImageFrame( NUI_FUSION_IMAGE_TYPE_FLOAT,
iWidth, iHeight, nullptr,
&pFloatDepthFrame);
這邊 iWidth 和 iHeight 是深度影像的寬和高。而第四個參數則是攝影機的參數(資料型別),一般來說直接給 nullptr、讓他使用預設值就可以了。
之後,則就可以透過 INuiFusionReconstruction 的 DepthToDepthFloatFrame() 這個函式,來把深度資料轉換成浮點數的形式、寫到 NUI_FUSION_IMAGE_FRAME 的物件裡了。這邊的程式寫法大致上會像下面一樣:
IDepthFrame* pDepthFrame = nullptr;
if (pDepthFrameReader->AcquireLatestFrame(&pDepthFrame) == S_OK)
{
// read data
UINT uBufferSize = 0;
UINT16* pDepthBuffer = nullptr;
pDepthFrame->AccessUnderlyingBuffer(&uBufferSize, &pDepthBuffer);
// Convert to Kinect Fusion format
pReconstruction->DepthToDepthFloatFrame(
pDepthBuffer, uBufferSize * sizeof(UINT16),
pFloatDepthFrame,
fDepthMin, fDepthMax,
true);
// …
}
DepthToDepthFloatFrame() 的第一個參數 pDepthBuffer 就是深度值的陣列,第二個參數則是這組資料的大小,第三個參數 pDepthFloatImageFrame 則是前面建立好的 NUI_FUSION_IMAGE_FRAME 的物件。之後的 fDepthMin 和 fDepthMax 則是用來限制要使用的深度值範圍的,最後的 bool 變數,則是用來控制是否要把影像作鏡像。
另外,由於深度影像本身會有一些雜訊,所以 Kinect Fusion 也有提供平滑化的函式可以使用,那就是 SmoothDepthFloatFrame() 這個函式;它的使用方法基本上如下:
pReconstruction->SmoothDepthFloatFrame(
pFloatDepthFrame,
pSmoothDepthFrame,
NUI_FUSION_DEFAULT_SMOOTHING_KERNEL_WIDTH,
NUI_FUSION_DEFAULT_SMOOTHING_DISTANCE_THRESHOLD);
這邊 pDepthFloatImageFrame 就是原始的深度資料,pSmoothDepthFloatImageFrame 則是平滑化後的結果,也一樣需要先使用 NuiFusionCreateImageFrame() 這個函式來做初始化。
(另外兩個平滑化參數可以參考 MSDN 的說明)
而之後,就可以把 pSmoothDepthFloatImageFrame 當作來源,讓 Kinect Fusion 把它整合到場景的 volume 裡了。
另外,NUI_FUSION_IMAGE_FRAME 類型的資料在使用完、不在需要後,也記得藥用 NuiFusionReleaseImageFrame() 這個函式來把資源釋放掉。
更新資料
在初始化完成之後,接下來就是要開始把深度影像的資料、餵給 INuiFusionReconstruction 的物件(pReconstruction)讓他計算出相對應的位置、並整合到現有的 volume 裡了。
一般來說,這邊使用的會是 ProcessFrame() 這個函式。他需要的參數包括了:
- const NUI_FUSION_IMAGE_FRAME *pDepthFloatFrame
- USHORT maxAlignIterationCount
- USHORT maxIntegrationWeight
- FLOAT *pAlignmentEnergy
- const Matrix4 *pWorldToCameraTransform
第一個參數 pDepthFloatFrame 就是新的深度資料。這邊的資料格式就是前面所提到的 NUI_FUSION_IMAGE_FRAME。
第二個參數 maxAlignIterationCount 則是在試著尋找這張新的影像的位置時、演算法的最大疊代(iteration)次數。其最小值是 1,值越小速度會越快,但是相對地可能會沒辦法收斂到夠好的值;所以一般是建議使用預設值 NUI_FUSION_DEFAULT_ALIGN_ITERATION_COUNT(20)就可以了。
第三個參數 maxIntegrationWeight 是用來控制在整合新的深度資訊時,在時間軸上的平滑化程度;最小值是 1,而值越小深度的雜訊越明顯,但是相對地比較適合動態的環境,可以較快地把物體掃描好;調高的話,場景裡面如果有忽然冒出一個東西的話,他會需要比較久的時間才會把新的物體整合進去,不過相對地整體雜訊會比較少。
這邊也可以直接使用系統預設值 NUI_FUSION_DEFAULT_INTEGRATION_WEIGHT(200)就好。
第四個參數 pAlignmentEnergy 是輸出而非輸入,它代表的意義是輸入的深度畫面和目前的資料的對齊結果的好壞,他的值惠介於 0 到 1 之間。
最後第五個參數 pWorldToCameraTransform 則是目前的攝影機的位置資訊。在文件中有提到,這個矩陣會是最近一次呼叫 AlignPointClouds() 或 AlignDepthFloatToReconstruction() 的結果。不過實際上,這邊也可以直接透過 GetCurrentWorldToCameraTransform() 這個函式來取得。
所以,更新資料的程式,最後可以寫成:
Matrix4 mCameraMatrix;
pReconstruction->GetCurrentWorldToCameraTransform(&mCameraMatrix);
if (pReconstruction->ProcessFrame(
pSmoothDepthFrame,
NUI_FUSION_DEFAULT_ALIGN_ITERATION_COUNT,
NUI_FUSION_DEFAULT_INTEGRATION_WEIGHT,
nullptr,
&mCameraMatrix) != S_OK ){
// …
}
理論上只要每次有新的資料時,都呼叫 ProcessFrame() 這個函式、來把新的深度資料整合進現有資料、直到自己覺得整個結果已經夠好了、就可以了。
檢視目前結果
由於 Kinect Fusion 是一個持續性的掃描過程,需要讓使用者自己判斷怎樣算是掃描完成了,所以必然會需要可以看到目前的掃描結果。
由於 Kinect Fusion 內部是使用 Volume 的資料型態來做儲存的,而這種類型的資料實際上並不適合使用傳統的 graphics pipeline 來繪製,所以要預覽目前的結果,必須要另外做些處理。
首先,INuiFusionReconstruction 有提供一個 CalculatePointCloud() 的函式,可以針對特定的攝影機位置,使用 ray casting 的方法(很久以前的說明),來產生一組針對這個視角的 point cloud 資料。
這邊的 Point Cloud 的資料型別一樣是 NUI_FUSION_IMAGE_FRAME,不過類型是 NUI_FUSION_IMAGE_TYPE_POINT_CLOUD;他的初始化程式可以寫成下面的樣子:
NUI_FUSION_IMAGE_FRAME *pPointCloudFrame = nullptr;
NuiFusionCreateImageFrame( NUI_FUSION_IMAGE_TYPE_POINT_CLOUD,
iImgWidth, iImgHeight, nullptr,
&pPointCloudFrame);
其中,iImgWidth 和 iImgHeight 是他的寬和高,大小可以任意指定,在一般狀況下,應該會和顯示區域一樣大。(這張圖的資料越大的時候,之後的計算也理所當然地會變慢)
之後,就可以呼叫 CalculatePointCloud()、根據目前的攝影機位置、把資料計算出來填進去了。
pReconstruction->CalculatePointCloud(pPointCloudFrame, &mCameraMatrix);
在產生 point cloud 的資料 pPointCloudFrame 之後,則可以使用 NuiFusionShadePointCloud() 這個函式,把它畫成適合顯示的圖案。
這個函式需要輸入三個參數,包含了 point cloud 的資料(pPointCloudFrame)、目前的攝影機的位置矩陣(mCameeraMatrix)、以及一個用來產生色彩的矩陣;而結果,則會產生一張根據色彩產生矩陣畫好的圖、以及一張根據法向量(normal)來上色的圖。
這兩張結果圖的格式一樣都是 NUI_FUSION_IMAGE_FRAME,類型是 NUI_FUSION_IMAGE_TYPE_COLOR,大小要和輸入的 point cloud 影像一樣大。建立的程式會像下面這樣:
NuiFusionCreateImageFrame( NUI_FUSION_IMAGE_TYPE_COLOR,
iImgWidth, iImgHeight, nullptr,
&pSurfaceFrame);
NUI_FUSION_IMAGE_FRAME *pNormalFrame = nullptr;
NuiFusionCreateImageFrame( NUI_FUSION_IMAGE_TYPE_COLOR,
iImgWidth, iImgHeight, nullptr,
&pNormalFrame);
在這兩張圖配置好之後,接下來就可以呼叫 NuiFusionShadePointCloud()、把圖畫出來了。程式碼可以寫成下面的樣子:
pPointCloudFrame, &mCameraMatrix, nullptr,
pSurfaceFrame, pNormalFrame);
可以看到,這邊第三個參數是 nullptr,這代表再產生色彩的部分是去使用 Kinect Fusion 的預設轉換方法,出來的會是灰階。
如果把這兩張圖畫出來的話,會像下面這樣:
上面左邊是 pSurfaceFarme、右邊則是 pNormalFrame。
如果覺得左邊的灰階圖不購好看的話,則可以透過指定 NuiFusionShadePointCloud() 的第三個參數、給他一個 Matrix4 的矩陣,讓他可以把每一點的 ( x, y, z )、轉換成色彩的 ( B, G, R ),這樣就可以看到色彩了。不過就算這樣處理,它的色彩也不是物體真正的色彩,而是根據位置的相對關係計算出來的(結果會像這樣),所以其實意義不算很大就是了。
最後,如果想把 pSurfaceFarme 轉換成 OpenCV 的 cv::Mat 的話,程式寫法如下:
iHeight, iWidth, CV_8UC4,
pSurfaceFrame->pFrameBuffer->pBits);
產生多邊形(Mesh)
最後,如果覺得已經掃描完成了,要把結果拿出來的話,則有幾種方法。如果願意直接處理 Volume 資料的話,可以直接呼叫 INuiFusionReconstruction 的 ExportVolumeBlock() 這個函式,把 Vloume 資料讀出來。不過基本上,這樣做對於大部分的人來說應該沒有多大的意義就是了。
一般使用 Kinect Fusion 來做掃描,最後希望拿到的,大多都還是多邊形結構的 3D 模型;而 INuiFusionReconstruction 也有提供 CalculateMesh() 這個函式、可以用來產生多邊形結構的資料,其結果的型別是 INuiFusionMesh。
一般要使用這個函式,只要寫成下面這樣就可以了。
if (pReconstruction->CalculateMesh(1, &pMesh) == S_OK)
{
// …
}
透過 INuiFusionMesh 提供的函式,可以取得 vertex array、index arry 等資料,透過處理這些資料,就可以用三角形的形式、來描述整個 3D 掃描的結果了。
而這邊礙於篇幅的問題,就暫時部描述相關的細節了,等之後有空再寫吧。
重新開始掃描
如果在掃描的過程中發現有問題,想要重新開始的話,該怎麼辦呢?
INuiFusionReconstruction 有提供一個 ResetReconstruction() 的函式,可以直接把他整個環境重設。他可接受兩個矩陣作為參數,但是在一般的狀況下,只要給 nullptr 就可以了。
這篇就先寫到這裡了。這篇的完整範例,請參考 GitHub 上的檔案。
這個完整的範例程式會使用 OpenCV 來做顯示,而按下鍵盤的「r」可以重新開始掃描、按下鍵盤的「o」則會把目前的結果寫到「E:\test.stl」這個檔案;如果沒有磁碟機 E:\、或是想寫到其他地方的話,就請自己修改 OutputSTL() 這個函式了。
之後預計會再把真正的色彩的掃描也加進來、並針對 INuiFusionMesh 的資料處理,應該也會找時間大概提一下。