在上一篇《讀取人體位置(Body Index)》裡,已經簡單地介紹過 Kinect for Windows SDK v2 所提供的人體追蹤的功能、也就是 Body Index 的部分了。
而當時,只有簡單地把這張圖裡面不同人、用不同的顏色畫出來而已。但是如果希望有進一步地應用,可以怎麼做呢?最簡單的想法,應該就是拿來做去背、也就是把背景去除了~
像下圖就是一個很簡單的去背、把人貼道別張照片上的例子:
要透過 K4W SDK v2 做到這件事,主要的概念,就是透過 Body Index 的資料,判斷畫面上哪部分是人、哪部分是背景,然後再決定要畫什麼顏色。
ICoordinateMapper
不過,由於 Body Index 的資料是出自於深度影像,和彩色影像是不同的來源,所以其實看到的東西是不完全相同的;而實際上,兩者在解析度上就已經不一樣了(深度影像是 512 * 424、彩色影像是 1920 * 1080),當然沒辦法直接疊在一起來處理。
而為了要讓兩者的座標系統一致,可以用來互相參考、處理,所以 K4W SDK v2 提供了 ICoordinateMapper 這個介面(MSDN),可以用來做座標系統的轉換。
而要取得這個物件,則是透過 IKinectSensor 的 get_CoordinateMapper() 這個函式來取得;他的程式寫法可以寫成下面的樣子:
pSensor->get_CoordinateMapper(&pCoordinateMapper);
實際上,在 K4W v2 裡面,總共另一了三個不同的座標系統,包括了:
- 彩色空間座標系統(Color Space)
- 深度空間座標系統(Depth Space)
- 攝影機空間座標系統(Camera Space)
其中,彩色影像當然就是彩色空間座標系統;而使用深度空間座標系統的,除了深度影像外,還包含了紅外線影像和 body index 的影像。而深度空間座標系統和彩色空間座標系統這兩個系統,都是 2D 的,也就是一般的影像座標系統:以左上角為原點、往右是 X、往下是 Y,單位是像素,沒有對應到真正的長度單位。
攝影機空間座標系統則是以感應器為原點的三度空間座標系統,使用的單位是公尺(m),主要是給真正的 3D 環境時使用的。如果是要做人體骨架追蹤、或是 3D 重建的話,就會要用到這個座標系統;不過在這邊還用不到,所以就之後再提吧。
而針對這三個座標系統上的點,K4W SDK 也定義了各自的類別,來做紀錄,分別是:ColorSpacePoint、DepthSpacePoint 和 CameraSpacePoint;前兩者基本上就是 x 和 y 的二維座標,後者則就是 x、y、z 這樣形式的三度空間座標了。
ICoordinateMapper 針對這三個不同的座標系統,則是提供了許多不同的函式,可以用來做單點、多點,或是權畫面的轉換。像是如果要把深度空間座標系統轉換到彩色空間座標系統的話,就有 MapDepthFrameToColorSpace()、MapDepthPointsToColorSpace()、MapDepthPointToColorSpace() 這三個函式可以使用;第一個是把整個畫面都做轉換、第二個則是轉換多個點、第三個則是轉換單一點。
而根據轉換的方向,不見得每種轉換都會同時支援這三種函式;像是如果要把彩色空間座標系統轉換到深度空間的話,就只有 MapColorFrameToDepthSpace() 這個函式可以使用。
另外,由於在轉換的計算裡面,基本上一定會用到深度資訊,所以就算是要把彩色影像轉換到攝影機空間,也還是需要去讀取深度影像才可以。
而在這個例子,由於是要把彩色影像和 body index 的影像作比較,所以是需要在深度空間座標系統和彩色空間座標系統兩者間作轉換;而由於是要知道整個彩色畫面裡面每個像素對應到 body index 影像上的位置,所以是使用 MapColorFrameToDepthSpace() 這個函式。
程式流程
完整程式碼的部分,可以參考 GitHub 上的檔案,Heresy 這邊不打算貼完整的程式碼了。
基本上,這個程式的前置流程,會是:
- 取得、並開啟感應器
- 初始化彩色影像相關程式
- 初始化深度影像相關程式
- 初始化 Body Index 影像相關程式
- 取得 ICoordinateMapper 的物件
而在主迴圈內,則是要去讀取彩色、深度、以及 body index 的影像,並透過 ICoordinateMapper  的 MapColorFrameToDepthSpace() 這個函式,計算出彩色影像上每一點、對應到深度空間上的位置。
這部分的程式在這邊是寫成下面的樣子:
uDepthPointNum, pDepthPoints, uColorPointNum, pPointArray))
{
for (int y = 0; y < imgColor.rows; y)
{
for (int x = 0; x < imgColor.cols; x)
{
// ( x, y ) in color frame = rPoint in depth frame
const DepthSpacePoint& rPoint = pPointArray[y * imgColor.cols x];
// check if rPoint is in range
if (rPoint.X >= 0 && rPoint.X < iDepthWidth &&
rPoint.Y >= 0 && rPoint.Y < iDepthHeight)
{
// fill color from color frame if this pixel is user
int iIdx = (int)rPoint.X iDepthWidth * (int)rPoint.Y;
if (pBodyIndex[iIdx] < 6)
{
cv::Vec4b& rPixel = imgColor.at<cv::Vec4b>(y, x);
imgTarget.at<cv::Vec3b>(y, x) = cv::Vec3b(rPixel[0], rPixel[1], rPixel[2]);
}
}
}
}
cv::imshow("Background Remove", imgTarget);
}
在這邊,
uDepthPointNum, pDepthPoints, uColorPointNum, pPointArray)
就是用來做座標系統轉換的程式。
其中,第一個參數 uDepthPointNum 代表的是深度影像點的個數,基本上就是 512 x 424;pDepthPoints 則是深度影像的資料,格式是 UINT16*,是使用 CopyFrameDataToArray() 複製出來的陣列。
而 uColorPointNum 則是彩色影像的像素點數,基本上會是 1920 * 1080;pPointArray 則是用來儲存轉換後結果的陣列,他的型別是 DepthSpacePoint*,大小則是 uColorPointNum 。
在這樣轉換完成之後,pPointArray 就會是 1920 * 1080 個 DepthSpacePoint,而每個 DepthSpacePoint 則是記錄了該點對應到深度空間座標系統上的位置。
所以,接下來就可以用兩層迴圈,來掃過彩色影像(imgColor)裡面每個點 ( x, y ),並取得他對應到深度空間座標系統的位置(rPoint);不過由於不是每個彩色空間座標系統的點都可以成功地對應到深度空間座標系統,所以在使用前,要先做基本的範圍檢查,確認轉換後的結果,是在深度空間座標系統的範圍內。
而如果確定該點能對應到深度空間座標系統的話,就可以透過 rPoint 來讀取出彩色影像上這點(x, y)對應到 body index 的畫面上的值是多少了~
由於 body index 的每個像素紀錄的會是該點是第幾個使用者,而使用者的編號會是 0 – 5,所以這邊就是檢查,如果當該點的值(pBodyIndex[iIdx])小於 6 的時候,就代表該點是使用者,需要把彩色影像上的色彩值填到目標影像(imgTarget、這邊是 BGR 的彩色影像)上。
而如果 imgTarget 本身先去讀取背景圖的話,這樣執行的結果,就會像上面的圖一樣,可以看到一個人被貼到別的圖上面了~
這邊可能要稍微注意一下的,是這邊用了三彩色、深度、以及 Body Index 這三種不同的影像來源,但是彩色和深度由於是不同的感應器,所以其實可能不會在同一個時間取得資料;在寫程式的時候,其實是需要考慮到這一點的,不然有可能會因為要去存取不存在的影像,導致程式當掉。
而 Heresy 這邊的做法,就是把讀取到的影像資料複製一份儲存下來(在迴圈外面先配置好),這樣之後就算沒有更新到新的資料,也有舊的資料可以用。
如果真的希望可以資料同步的話,K4W SDK 也有提供 IMultiSourceFrameReader(MSDN)可以用,不過這個就以後有機會再講了。
基本上,這樣就是透過 K4W SDK 提供的 IColorFrame、IDepthFrame、IBodyIndexFrame 以及 ICoordinateMapper,來簡單地做去背的、合成的方法了。
不過實際上,如果認真看圖的話,會發現這個去背的方法,其實不算很好;在邊緣的部分,不但不是很乾淨、而且顆粒明顯比較粗。顆粒比較粗的原因,其實相當單純,就是因為深度影像的解析度比彩色影像低了不少的關係。
如果希望有比較好的效果的話,其實是還可以針對去背的結果作平滑化、柔邊等等後處理,會讓接合處更好。