K4W v2 C++ Part 5:簡單的去背程式


在上一篇《讀取人體位置(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),可以用來做座標系統的轉換。

而要取得這個物件,則是透過 IKinectSensorget_CoordinateMapper() 這個函式來取得;他的程式寫法可以寫成下面的樣子:

ICoordinateMapper* pCoordinateMapper = nullptr;
pSensor->get_CoordinateMapper(&pCoordinateMapper);


實際上,在 K4W v2 裡面,總共另一了三個不同的座標系統,包括了:

  • 彩色空間座標系統(Color Space)
  • 深度空間座標系統(Depth Space)
  • 攝影機空間座標系統(Camera Space)

其中,彩色影像當然就是彩色空間座標系統;而使用深度空間座標系統的,除了深度影像外,還包含了紅外線影像和 body index 的影像。而深度空間座標系統和彩色空間座標系統這兩個系統,都是 2D 的,也就是一般的影像座標系統:以左上角為原點、往右是 +X、往下是 +Y,單位是像素,沒有對應到真正的長度單位。

攝影機空間座標系統則是以感應器為原點的三度空間座標系統,使用的單位是公尺(m),主要是給真正的 3D 環境時使用的。如果是要做人體骨架追蹤、或是 3D 重建的話,就會要用到這個座標系統;不過在這邊還用不到,所以就之後再提吧。

而針對這三個座標系統上的點,K4W SDK 也定義了各自的類別,來做紀錄,分別是:ColorSpacePointDepthSpacePointCameraSpacePoint;前兩者基本上就是 x 和 y 的二維座標,後者則就是 x、y、z 這樣形式的三度空間座標了。


ICoordinateMapper 針對這三個不同的座標系統,則是提供了許多不同的函式,可以用來做單點、多點,或是權畫面的轉換。像是如果要把深度空間座標系統轉換到彩色空間座標系統的話,就有 MapDepthFrameToColorSpace()MapDepthPointsToColorSpace()MapDepthPointToColorSpace() 這三個函式可以使用;第一個是把整個畫面都做轉換、第二個則是轉換多個點、第三個則是轉換單一點。

而根據轉換的方向,不見得每種轉換都會同時支援這三種函式;像是如果要把彩色空間座標系統轉換到深度空間的話,就只有 MapColorFrameToDepthSpace() 這個函式可以使用。

另外,由於在轉換的計算裡面,基本上一定會用到深度資訊,所以就算是要把彩色影像轉換到攝影機空間,也還是需要去讀取深度影像才可以。

而在這個例子,由於是要把彩色影像和 body index 的影像作比較,所以是需要在深度空間座標系統和彩色空間座標系統兩者間作轉換;而由於是要知道整個彩色畫面裡面每個像素對應到 body index 影像上的位置,所以是使用 MapColorFrameToDepthSpace() 這個函式。


程式流程

完整程式碼的部分,可以參考 GitHub 上的檔案,Heresy 這邊不打算貼完整的程式碼了。

基本上,這個程式的前置流程,會是:

  1. 取得、並開啟感應器
  2. 初始化彩色影像相關程式
  3. 初始化深度影像相關程式
  4. 初始化 Body Index 影像相關程式
  5. 取得 ICoordinateMapper 的物件

而在主迴圈內,則是要去讀取彩色、深度、以及 body index 的影像,並透過 ICoordinateMapper  的 MapColorFrameToDepthSpace() 這個函式,計算出彩色影像上每一點、對應到深度空間上的位置。

這部分的程式在這邊是寫成下面的樣子:

if (S_OK == pCoordinateMapper->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);
}

在這邊,

pCoordinateMapper->MapColorFrameToDepthSpace(
            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 也有提供 IMultiSourceFrameReaderMSDN)可以用,不過這個就以後有機會再講了。


基本上,這樣就是透過 K4W SDK 提供的 IColorFrameIDepthFrameIBodyIndexFrame 以及 ICoordinateMapper,來簡單地做去背的、合成的方法了。

不過實際上,如果認真看圖的話,會發現這個去背的方法,其實不算很好;在邊緣的部分,不但不是很乾淨、而且顆粒明顯比較粗。顆粒比較粗的原因,其實相當單純,就是因為深度影像的解析度比彩色影像低了不少的關係。

如果希望有比較好的效果的話,其實是還可以針對去背的結果作平滑化、柔邊等等後處理,會讓接合處更好。


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

對「K4W v2 C++ Part 5:簡單的去背程式」的想法

  1. 老师您好,我在学习您在GITHUB上发的去背式的代码有一点不太明白,在这份代码中您没有像之前的代码一样直接使用 GetDefaultKinectSensor(&pSensor); pSensor->Open(); 来获取和打开Kinect,而是在IKinectSensor* pSensor = nullptr;后直接用{}包含了两个if语句,在GetDefaultKinectSensor(&pSensor); pSensor->Open();错误的时候输出错误提示,我想向您请教为什么可以这么做,是因为在判断的过程中这两个函数已经被执行用于判断结果是否成功所以就等于这两个函数已经被执行了吗?

    • 不確定是不是你要問的,不過:

      if( funcA() == true ){ … }

      這樣的寫法和

      bool res = funcA();
      if(res){ … }

      意義是一樣的

      • 谢谢老师的讲解,我明白了,还有一个小问题就是在使用github上的完整去背式代码的时候会在217行const DepthSpacePoint& rPoint = pPointArray[y * imgColor.cols + x];给出绿色波浪线的警告,C6385:从“pPointArray”中读取的数据无效:可读大小为“uColorPointNum*8”个字节,但可能读取了“16”个字节,我运行程序后没有什么问题,但想向您请教,这样的警告原因是什么呢?或者我如何更改才能消除这个警告呢?

        • 建議可以直接去搜尋 Visual Studio 回報的錯誤/警告代碼,這邊就是「C6385」。
          微軟都有給文件在說明什麼情況會給這樣的訊息。

          • 老师您好,我想向您请教您的程序中 const DepthSpacePoint & rPoint = pPointArray[y * imgColor.cols + x]; 这个语句中的 & 有什么作用?我不能理解。我发现把它删除后运行程序也没有问题,效果也没有变化。请问老师,这个 & 是起了什么样的作用呢?

        • 參考:https://en.cppreference.com/w/cpp/language/reference

          個人建議你先從 C++ 的基礎開始,應該會比較合適。
          基礎沒打好之後會有很多問題。

  2. 你好,你的程序里面的背景去除,使用了iDepthHeight这个值,能否把它换成比如说手腕点,如果能的话,应该具体怎么做呢?

    Liked by 1 person

    • iDepthHeight 指的是深度圖的高度喔。

      要只將手腕點分割出來,需要去透過人體骨架資訊,再自己判斷哪些區域是手腕才行。
      微軟沒有直接的方法可以做到

  3. 博主大人,有一點我始終想不明白,求指點:既然彩色點數、深度點數不一樣(深度影像是 512 * 424、彩色影像是 1920 * 1080),彩色點數比深度點數多這麼多,使用MapColorFrameToDepthSpace()後,是不是 1920 * 1080個點都能找到對應的深度圖像點?還是說有些彩色點是找不到對應的深度圖像點的?

      • 謝謝博主老師解答,既然不是一一對應,那函數式MapColorFrameToDepthSpace()具體是怎麼實現的呢,我應該怎麼楊能找到這個函數式的底層代碼?kinect SDK中並沒有附帶底層源碼。我看到老師對每個kinect內置的函數式都能弄清來龍去脈,是不是看了對應的底層源碼呢?求老師指教!

        • 微軟並沒有提供 Kinect for Windows SDK 的原始碼,所以除非你是微軟的人,否則不可能看的到。
          Heresy 不在微軟工作,當然也沒看過。

          基本上要弄懂,就是先去看官方文件,其實說明的算是滿清楚的了。
          而其實很多都是 Computer vision 的基本概念;比如說你問的這個問題,其實就比較接近是電腦視覺會提到的概念了。

          • 博主老師真是功底深厚呀!感謝老師解答!任重道遠,看來我也得日常補一些CV的知識來輔助開發才行!

  4. 你好 請問一下 我想要利用上方的去背程式去除背景以及身體的部分只留下手部的彩色影像,因此使用body index並無法達成我所期望的效果,想請問一下程式應該朝什麼方向修改或是有其他可供利用的參數能夠使用嗎?

    • K4W SDK 應該是沒有提供調整參數。
      如果不滿意他的結果,基本上只能:
      1. 自己偵測
      2. 使用影像處理的演算法,針對她的結果作修改

  5. 不好意思 新手請教
    這是我的程式碼,利用kinect v2搭配opencv haar人臉檢測
    https://github.com/elmowuming/FaceDetection_WorldCoordinate/blob/master/ColorCamera.cpp

    Q1: while迴圈裡,可以用2個if(pDepthFrameReader….) 及 if (pColorFrameReader…) 同時開啟兩個感應器嗎?
    還是要用m_pMultiSourceFrameReader做同時開啟比如IMul​​tiSourceFrame::get_ColorFrameReference,IMul​​tiSourceFrame:: get_DepthFrameReference?

    Q2:我是想說利用nBuffer[idx]輸出的深度值丟到color裡做座標運算(只算rectangle中心),但compiler後223行出現 nBuffer[idx] 為0x1145312存取錯誤 (設中斷點 nBuffer 值為 0x05e0000{????} ,idx值為108800),是不能這樣直接丟嗎?

    不好意思 還請大大指導 謝謝

發表留言

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料