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++ 程式開發目錄

廣告

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

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

  1. 引用通告: Kinect for Windows SDK V2.0 [2014-9-16][最新版本 2.0.1409.10000]-小明

  2. 雷Ray says:

    請問一下如果得到RGB影像的一個點如何換算到真實的深度值(例如像是公尺),我只要單點對單點的

    喜歡

    • Heresy says:

      一樣,請使用 MapColorFrameToDepthSpace 整張轉換後再進行採樣。

      喜歡

      • 雷Ray says:

        不好意思再度打擾您,這裡有幾個問題想請教
        pCoordinateMapper->MapColorFrameToDepthSpace(uDepthPointNum, pDepthPoints, uColorPointNum, pPointArray這行跑完得到的 pPointArray他的資料型態是DepthSpacePoint
        這代表的是他的深度距離對吧?這個數值指的是什麼單位?這個數值可以轉成float或者double之類的?

        喜歡

        • Heresy says:

          文章中有提到了:

          pPointArray 就會是 1920 * 1080 個 DepthSpacePoint,而每個 DepthSpacePoint 則是記錄了該點對應到深度空間座標系統上的位置。

          他只是讓你回去查深度圖的座標而已。

          至於單位的部分,MSDN 官方文件就有說明是 millimeter 了,請參考:
          https://msdn.microsoft.com/en-us/library/microsoft.kinect.kinect.idepthframe.aspx

          喜歡

          • 雷Ray says:

            感謝Heresy這麼迅速地回復,我就我所理解的確認一次:rPoint的值會在原深度影像(512*424)的X,Y然後所得到的X,Y深度影像的值Z也就是真實距離Kinect的長度(mm),這樣子?

            喜歡

          • Heresy says:

            基本上是。
            個人會建議你自己先跑過程式、試著去修改看看,這樣會比較容易理解。

            喜歡

  3. Mata says:

    你好,我现在已有了一个拥有彩色图像(1920*1080)和depth图像(512*424)的数据库,请问能够离线将彩色图像转成和depth拥有对应关系的色图吗?

    喜歡

    • Heresy says:

      個人建議,試試看就把資料丟給 K4W SDK 的函式看看能不能正常運作吧。

      喜歡

  4. Eaco says:

    你好,為了需求,我想要儲存畫面完全一樣的彩色及深度影像,簡單來說,就是彩色跟深度的位置完全對應的圖像,且解析度同樣都是512×424。而使用MapColorFrameToDepthSpace這個函式貌似只能夠轉換座標,並不能夠將本來1920×1080的彩色圖轉成512×424且對應深度位置的圖片,請問我應該怎麼下手會比較好,感謝。

    喜歡

    • Heresy says:

      請自行一點一點採樣、已產生一張新的圖。

      喜歡

  5. Krystal says:

    Heresy 你好,谢谢你的讲解, 启发很大 :D.。在参照github上去背景程序的时候,有一点不理解的地方。
    1. 在定义彩色图像的时候,为什么height 和width和平时的顺序是相反的
    cv::namedWindow(“Background Remove");
    cv::Mat imgColor(iColorHeight, iColorWidth, CV_8UC4);

    // 7. Load background
    cv::Mat imgBG(iColorHeight, iColorWidth, CV_8UC3);
    if (argc > 1)
    {
    cv::Mat imgSrc = cv::imread(argv[1]);
    cv::resize(imgSrc, imgBG, cv::Size(iColorWidth, iColorHeight));
    }
    还有在彩色深度变换的时候,赋值的顺序也是先y后x
    // 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(y, x);
    imgTarget.at(y, x) = cv::Vec3b(rPixel[0], rPixel[1], rPixel[2]);
    }
    2.第二个问题是MultiFrameSourseReader这个函数可以保障实时读取图像。对于深度彩色转换会有准确度提升吗
    谢谢, 期待您的回答

    喜歡

    • Heresy says:

      1. 那是 OpenCV 的設計
      2. 計算方法不會變,所以理論上準確度不會提高。
      但是如果你的誤差是因為時間差造成的,那理論上有可能會有改善。

      喜歡

  6. desperado says:

    Heresy你好:
    我在参考您这个程式的时候,利用MapColorFrameToDepthSpace()这个函数,将彩色空间映射得到1920*1080个DepthSpacePoint,然后利用CopyFrameDataToArray得到的深度值取出某一个固定点(比如鼻尖)的深度值,但是观察到这个点的深度值在同一个位置变化非常大,感觉深度距离并不是实际的目标到Kinect的距离,请问我的思路哪里有问题吗?
    期盼您的解答。

    喜歡

    • Heresy says:

      深度值本來就會有相當程度的浮動,如果是在某些狀況(物體邊緣、特殊材質)下,深度值得可靠度也有可能再降低;再加上彩色影像與深度影像的對位,實際上也有一些誤差,也有可能造成問題。

      建議你先以靜態的平面來做測試看看,確認看看深度值的穩定度是否符合你的要求。

      喜歡

  7. 引用通告: Kinect Fusion Part 2:含色彩資訊的版本 | Heresy's Space

  8. Porter Chen says:

    Hi Kheresy,

    看完了你的介紹, 有些地方想請教, 我是使用openframeworks平台
    由於自己的開發需求, 我需要將color與depth建立pixel對應關係, 並將每個pixels位置對應到cameraspace空間當中

    目前先朝著, 顯示"完整的ColorFrame", 而背後則有相對應的深度以及空間位置
    為了這個想法我做了
    1. MapDepthFrametoCameraSpace,
    2. 再將ColorFrame Reshpae to 512*424

    這樣我應該就會有512*424 的depth & color & camera 的資訊, 但我在處理ColorFrame Reshape時,
    想過單純
    1. Image Reshape 1920*1080 -> 512*424
    2. MapColorFrameToDepthSpace(512*424, Depthdatas, 1920*1080, DepthSpacePoint)
    目前是先實驗方式 2.

    以下是我的實現代碼

    bool mapColortoDepthSize(const ofPixels& _pColorMap, const ofShortPixels& depthImage, ofPixels& pDstColorMap_, ofPoint shift){
    vector depthPixels;
    vector depths;

    static UINT depthSize;
    static UINT colorSize;

    depthSize = depthFrameDescription.width * depthFrameDescription.height;
    colorSize = colorFrameDescription.width * colorFrameDescription.height;

    depths.resize(depthSize);
    depthPixels.resize(colorSize);

    // 轉存的depthdatas
    for(int iy = 0; iy < depthFrameDescription.height; iy++){
    for(int ix = 0; ix < depthFrameDescription.width; ix++){
    int i = iy*depthFrameDescription.width + ix;
    depths[i] = (UINT16)depthImage.getPixels()[i];
    }
    }

    HRESULT mapResult;
    mapResult = KCBMapColorFrameToDepthSpace(hKinect,
    depthSize, &depths[0],
    colorSize, &depthPixels[0]);

    /* Check mapResult */

    /* Check pDstColorMap_ is Allocated, and Memset to 0 */

    // 確認並將彩色資料大小轉為深度資料大小
    for(int iy = 0; iy < colorFrameDescription.height; iy++){
    for(int ix = 0; ix = 0 && iDepthX_ = 0 && iDepthY_ < depthFrameDescription.height){
    ofColor c = videoPixels.getColor(iDepthY_, iDepthX_);
    pDstColorMap_.setColor(iy, ix, c);
    }
    }
    }
    return true;
    }

    問題有:
    1. 照這樣的流程走, 我應該可以拿到一個512*424的ColorFrame, 在接上我先前處理完的DepthinCamera, 我就可以完成我想要的東西?
    2. 我使用的函式是
    KINECT_CB HRESULT APIENTRY KCBMapColorFrameToDepthSpace(_In_ KCBHANDLE kcbHandle, UINT depthDataPointCount, _In_reads_(depthDataPointCount) const UINT16 *depthFrameData, UINT depthPointCount, _Out_writes_all_(depthPointCount) DepthSpacePoint *depthSpacePoints);

    但我這樣使用時, 回傳的一直是Fail值
    KCBMapColorFrameToDepthSpace(hKinect, depthSize, &depths[0], colorSize, &depthPixels[0]);

    3. 是否有其他方式可以達到我想做的效果?

    不好意思問題有點多又有點長, 先謝謝你的閱讀, 允許的話請幫助我這些問題

    Many Thanks.

    喜歡

    • Heresy says:

      Heresy 不確定 openframeworks 哪邊怎麼封包 K4W 的函式的,所以這邊用 C++ 的方法來講。

      首先,MapColorFrameToDepthSpace() 並不是幫你產生一張大小和深度影像一樣大的圖,而是產生一張座標對照表,讓你知道彩色影像上每個點對應到深度圖的哪個位置。

      而你的錯誤,假設 OpenFrameworks 的介面是直接針對 C++ 介面重封包的話,那個人覺得你的參數給的有問題;因為理論上第二個參數和第四個參數的型別應該會是不同的。

      最後,基本上都是要靠座標系統轉換,但是取決於你最後的目的,轉換方向可能可以換成別種。
      或許你也可以參考看看這篇: https://kheresy.wordpress.com/2015/02/11/k4w-v2-part-6-draw-with-opengl/

      另外,直接作圖檔大小的修改一定不能用。

      喜歡

  9. rudy says:

    在AcquireLatestFrame地方一直出错,每次如此,前面几讲内容也是如此。求解?

    喜歡

    • Heresy says:

      本系列第一篇文章內已經有提過了
      請參考「關於 AcquireLatestFrame() 」的部分
      https://kheresy.wordpress.com/2015/01/08/k4w-v2-simple-depth-reader-cpp/

      喜歡

  10. 引用通告: K4W v2 C++ Part 6:使用 OpenGL 繪製場景 | Heresy's Space

發表迴響

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

WordPress.com Logo

您的留言將使用 WordPress.com 帳號。 登出 / 變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 / 變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 / 變更 )

Google+ photo

您的留言將使用 Google+ 帳號。 登出 / 變更 )

連結到 %s

%d 位部落客按了讚: