2014-11-21

[OF]Kinect v2 學習筆記(四) RGB Camera的去背功能

這次的主題是將上篇的去背功能套用到Color Frame上頭
特別獨立拿出來講是因為要再次面對空間轉換的事情
要是連同上一篇一起寫的話...
不要說讀的人了, 我自己都吃不消
廢話不多說!!來看Demo



讓大家失望了...這次沒有長頸鹿...
不過有一個當時在撰寫時靈機一動所做的"Party特效"
此效果配上長頸鹿一起服用效果更佳!!

========分隔線========

純粹看這個Demo可能會看不太懂
在此簡單列一下這個影片做了些甚麼

  1. 取出Color Frame的資訊並繪出(00:00~00:13)
  2. 切換到Depth Space,並透過空間轉換填上Color Frame的資訊(00:13~00:30)
  3. 透過Body Index Frame,在Depth Space上進行去背(00:31~00:40)
  4. 透過Body Index Frame,在Color Space上進行去背(00:41~00:52)
  5. PARTY!!!(00:53~)

※本篇關於Body Index做去背以及空間轉換部分可參考前篇:

這次的新朋友只有一位!Color Frame!!

=Color Frame=

與之前的Depth Frame類似
在初始化方面,一樣是get_ColorFrameSource() + OpenReader()
取得資料方面,同樣可以使用IFrameDescription來取得Color Frame的size
不同的地方來了!!Color Image Format
不像Depth Map每個Pixel就是一個unsigned short,彩色資訊就是有他麻煩色彩空間(Color Space)問題
SDK 2.0將他提供的色彩資訊透過ColorImageFormat這個enum表示,共以下5種:

  • ColorImageFormat_Rgba
  • ColorImageFormat_Yuv
  • ColorImageFormat_Bgra
  • ColorImageFormat_Bayer
  • ColorImageFormat_Yuy2

一般軟體內常用的應該就是Rgba以及Bgra,比方說前者使用在OF上,後者則是OpenCV有在用
Yuv以及Yuv2則是影視串流常用的格式,筆者Kinect V2所得到的初始格式就是Yuv
最後關於Bayer,似乎是某種特殊Filter,沒有研究!

當然,Kinect SDK並沒有很殘忍的只給我們特定格式讓開發者自己找方式轉換
它提供了一個轉換的function來讓你可以依自己需求的格式去取出Color Frame
以下直接用這部份的程式碼說明:


1:  pColorFrame_->get_RawColorImageFormat(&ImgFormat_);  
2:    
3:  if(ImgFormat_ == ColorImageFormat_Rgba)  
4:  {  
5:       pColorFrame_->AccessRawUnderlyingBuffer(&iColorBufferSize_, &pColorBuffer_);  
6:  }  
7:  else  
8:  {       
9:       iColorBufferSize_ = COLOR_WIDTH * COLOR_HEIGHT * COLOR_CHANNEL;  
10:       pColorBuffer_ = new BYTE[iColorBufferSize_];  
11:       pColorFrame_->CopyConvertedFrameDataToArray(iColorBufferSize_, pColorBuffer_, ColorImageFormat_Rgba);  
12:  }  


1:取得預設的Color Image Format

4:6:直接取得Color Frame Buffer Size與Color Frame Buffer
假設預設的Kinect V2就是Rgba的話,那跟其他frame一樣,使用AccessRawUnderlyingBuffer來取得即可

9:11:將資料轉換為需要得格式並取出
若Image format不是自己需要的(OF為RGBA),就可用CopyConvertedFrameDataToArray的方式來轉換
跟AccessRawUnderlyingBuffer直接指向Buffer位置的方式不同,他是將Buffer的資料Copy到提供的array中
因此得告知轉換大小(Buffer Size),並給予對應的空間(new Buffer Size)
這裡的COLOR_WIDTH = 1920  COLOR_HEIGHT = 1080  COLOR_CHANNEL = 4
一般來說透過IFrameDescription來取得是較為恰當的作法

這個用法要特別記得pColorBuffer_在最後得release
不管你是在每一次update才使用的區域變數,還是整個class的成員變數

==================================================

在正式進入今天的程式碼前
還有兩件事情要先想清楚!

首先!到底要做什麼?
這篇的目標是呈現出擁有色彩資訊(Color Frame)的去背結果
去背的部份,是透過Body Index來對深度圖做出去背的效果
因此,要得到我們想要的結果有兩種作法:
  1. 在Color Frame上,根據對應的Body Index判斷是否要繪出該Pixel
  2. 在Body Index上,填入Color Frame中對應pixel的顏色
我相信以需求來說,大部份都是用第一種作法吧?
特別是Kinect V2兩者的解析度差這麼多的情況下,實在沒道理用第二種作法
不過既然Kinect SDK都提供這個功能了!說不一定哪天會用到

再來!到底要怎麼做?
讓我們復習一下,Kinect共有三個空間:Camera Space(3D)、Color Space(2D)、Depth Space(2D)
無論上面的哪一種作法,都得面對Color Frame(Color Space)Body Index Frame(Depth Space)之間的對應
這樣兩張影像空間的對應處理,稱為Image Mapping
(※最常出現這個詞的應該是在處理Image Warping中,由於本篇重點不在於影像處理,因此就不多敘述)
一般在做法上,會使用reverse-mapping的方式做處理(又稱Inverse-mapping)
因此,針對上面的兩種作法,具體的實現方式是:
  1. 將Color Frame的pixel轉到Depth Space中來取得Body Index資訊
  2. 將Body Index的pixel轉到Color Space中來取得Color Frame資訊
==================================================

ok, 搞清楚這兩件事情後,就該來看程式碼了!
首先是作法1
(※以下指列出主要處理的部份,關於資訊取得的程式碼請參考前篇)

Color Frame to Depth Space
1:  DepthSpacePoint*     pDepthCoordinate_ = nullptr;  
2:  pDepthCoordinate_ = new DepthSpacePoint[COLOR_WIDTH * COLOR_HEIGHT];  
3:  _pCoordinateMapper->MapColorFrameToDepthSpace(iDepthBufferSize_, pDepthBuffer_, COLOR_WIDTH*COLOR_HEIGHT, pDepthCoordinate_);  
4:    
5:  ofPixels TmpDisplay_;  
6:  TmpDisplay_.allocate(COLOR_WIDTH, COLOR_HEIGHT, ofImageType::OF_IMAGE_COLOR);  
7:  unsigned char * acDisplay_ = TmpDisplay_.getPixels();  
8:    
9:  for(int idx_ = 0; idx_ < (COLOR_WIDTH * COLOR_HEIGHT); ++idx_)  
10:  {  
11:       if(!_bBackgroundSub)  
12:       {  
13:            acDisplay_[idx_ * 3] = pColorBuffer_[idx_ * 4];  
14:            acDisplay_[idx_ * 3 + 1] = pColorBuffer_[idx_ * 4 + 1];  
15:            acDisplay_[idx_ * 3 + 2] = pColorBuffer_[idx_ * 4 + 2];  
16:       }  
17:       else  
18:       {  
19:            int iDepthX_ = static_cast<int>(pDepthCoordinate_[idx_].X + .5);  
20:            int iDepthY_ = static_cast<int>(pDepthCoordinate_[idx_].Y + .5);  
21:    
22:            acDisplay_[idx_ * 3] = 0;  
23:            acDisplay_[idx_ * 3 + 1] = 0;  
24:            acDisplay_[idx_ * 3 + 2] = 0;  
25:            if(iDepthX_ >= 0 && iDepthX_ < DEPTH_WIDTH && iDepthY_ >= 0 && iDepthY_ < DEPTH_HEIGHT)  
26:            {  
27:                 int iDepthDisplayIdx_ = iDepthX_ + (iDepthY_ * DEPTH_WIDTH);  
28:    
29:                 if(pBodyIndexBuffer_[iDepthDisplayIdx_] != 0xff)  
30:                 {  
31:                      acDisplay_[idx_ * 3] = pColorBuffer_[idx_ * 4];  
32:                      acDisplay_[idx_ * 3 + 1] = pColorBuffer_[idx_ * 4 + 1];  
33:                      acDisplay_[idx_ * 3 + 2] = pColorBuffer_[idx_ * 4 + 2];  
34:                 }  
35:            }  
36:       }  
37:  }  
38:    
39:  _Display.setFromPixels(acDisplay_, COLOR_WIDTH, COLOR_HEIGHT, OF_IMAGE_COLOR);  

1:3:取得Color Frame轉換到Depth Space的對應座標
使用的function為MapColorFrameToDepthSpace,輸入的參數為:
  • Depth Frame Buffer Size (512 x 424)
  • Depth Frame Buffer
  • Color Image Size (1920 x 1080)
  • Depth Space Point(output)
DepthSpacePoint是用來表示Depth Space座標的資料結構,為Kinect SDK所提供
其中第三個參數其實就是表示第四個array point的大小

※這裡要特別注意,雖然我們只要Body Index與Color Frame的資訊
但Kinect SDK提供的轉換功能需要用到Depth Frame才能計算轉換座標
若有其他方法的歡迎提供!!

5:8:初始化要呈現的影像
根據需求而定,若要加上底圖可以改為OF_IMAGE_COLOR_ALPHA

19:20:四捨五入取得Depth Space中的座標
一般的作法會是參考周邊的pixel並考慮權重來決定數值,不過這裡為了方便就直接四捨五入

25:35:根據Body Index的值決定要不要填入Color Frame資訊
轉換後得到的Depth Space座標是有可能在實際範圍外的
因此要先確認在範圍內後,再去取得數值(不然就會壞給你看)
當判斷不是軀體範圍時,Body Index的數值為255,因此只要Body Index不為255的就填入顏色
※由於Color Frame是RGBA,因此index的部份是x4

=====================================================

接下來是作法2!整個架構跟作法1其實大同小異

Body Index to Color Space

1:  ColorSpacePoint*     pColorCoordinate_ = nullptr;  
2:  pColorCoordinate_ = new ColorSpacePoint[DEPTH_WIDTH * DEPTH_HEIGHT];  
3:  _pCoordinateMapper->MapDepthFrameToColorSpace(iDepthBufferSize_, pDepthBuffer_, iDepthBufferSize_, pColorCoordinate_);  
4:    
5:  ofPixels TmpDisplay_;  
6:  TmpDisplay_.allocate(DEPTH_WIDTH, DEPTH_HEIGHT, ofImageType::OF_IMAGE_COLOR);  
7:  unsigned char * acDisplay_ = TmpDisplay_.getPixels();  
8:    
9:  for(int idx_ = 0; idx_ < iDepthBufferSize_; ++idx_)  
10:  {       
11:       if(_bBackgroundSub && pBodyIndexBuffer_[idx_] == 0xff)  
12:       {  
13:            acDisplay_[idx_ * 3] = 0;  
14:            acDisplay_[idx_ * 3 + 1] = 0;  
15:            acDisplay_[idx_ * 3 + 2] = 0;  
16:            continue;  
17:       }  
18:    
19:       int iColorX_ = static_cast<int>(pColorCoordinate_[idx_].X + .5);  
20:       int iColorY_ = static_cast<int>(pColorCoordinate_[idx_].Y + .5);  
21:    
22:       if(iColorX_ >= 0 && iColorX_ < COLOR_WIDTH && iColorY_ >= 0 && iColorY_ < COLOR_HEIGHT)  
23:       {  
24:            int iColorDisplayIdx_ = iColorX_ * COLOR_CHANNEL + (iColorY_ * COLOR_WIDTH * COLOR_CHANNEL);  
25:    
26:            acDisplay_[idx_ * 3] = pColorBuffer_[iColorDisplayIdx_];  
27:            acDisplay_[idx_ * 3 + 1] = pColorBuffer_[iColorDisplayIdx_ + 1];  
28:            acDisplay_[idx_ * 3 + 2] = pColorBuffer_[iColorDisplayIdx_ + 2];  
29:       }  
30:       else  
31:       {  
32:            acDisplay_[idx_ * 3] = 0;  
33:            acDisplay_[idx_ * 3 + 1] = 0;  
34:            acDisplay_[idx_ * 3 + 2] = 0;  
35:       }  
36:  }  
37:    
38:  _Display.setFromPixels(acDisplay_, DEPTH_WIDTH, DEPTH_HEIGHT, OF_IMAGE_COLOR);  

1:3:取得Depth Frame轉換到Color Space的對應座標
使用的function為MapDepthFrameToColorSpace,輸入的參數為:
  • Depth Frame Buffer Size (512 x 424)
  • Depth Frame Buffer
  • Depth Image Size (512 x 424)
  • Color Space Point(output)
Color Space Point就是用來表示Color Space座標的資料結構
因為我們是要取得Depth Space中所有pixel對應在Color Space的座標
因此Size就是Depth Frame的大小(512 x 424)

22:34:填入Color Frame資訊
寫法跟作法1有點不同,不過整理概念是一樣的,只是檢查的先後順序有差
一樣要是用四捨五入的方式大概取得Color Space中的座標
並檢查是否在Color Frame的範圍內
要特別小心iColorDisplayIdx_的計算方式喔!

=====================================================

至於來亂的"PARTY"功能...
其實就只是用ofSetColor配合Blend Mode來完成
這裡就簡單列出部份程式碼

1:  ofEnableBlendMode(OF_BLENDMODE_ADD);  
2:  if(_bIsColorToDepth)  
3:  {       
4:       ofSetColor(255, 0, 0);  
5:       _Display.draw(ofRandom(-20, 20),ofRandom(-20, 20), DEPTH_WIDTH, DEPTH_HEIGHT);  
6:       ofSetColor(0, 255, 0);  
7:       _Display.draw(ofRandom(-20, 20),ofRandom(-20, 20), DEPTH_WIDTH, DEPTH_HEIGHT);  
8:       ofSetColor(0, 0, 255);  
9:       _Display.draw(ofRandom(-20, 20),ofRandom(-20, 20), DEPTH_WIDTH, DEPTH_HEIGHT);  
10:  }   

========分隔線========

以上就是利用RGB Camera進行去背
到此除了直接使用IR Camera外,大部份的基本功能都已經使用過了
下一篇就要來整合這到此為篇的結果,並加上Face Detection的功能
來做出一個較為完整的Demo

6 則留言:

  1. 不好意思,請教一下,您video裡面應該是....作法一?根據我查到的這篇:
    http://stackoverflow.com/questions/29084773/kinect-v2-for-windows-depth-to-color-image-misalignment-and
    似乎因為depth與color的角度差問題,當想要在背景填入原來的color資訊時,會產生重影。
    這問題....有解嗎?我想要在原先的color當背景下,再填入您現在的去背影像的話.......

    回覆刪除
    回覆
    1. 很抱歉因為我在使用Kinect上還沒有這種需求
      所以並沒有繼續研究下去
      方便請你多描述一下你的用途嗎?
      看看能不能有沒有機會幫你避過這個問題

      刪除
    2. 不好意思,我有在信箱看到你的留言~但不知道為什麼Blog就是看不到(刪掉了?

      根據提到的原始需求,我是理解成你想要取得在color frame(1920x1080)下的剪影?
      其實這樣就等於是我Demo影片中的第四部份
      "透過Body Index Frame,在Color Space上進行去背(00:41~00:52)"
      那這個部份文中有提到,是對BodyIndex與Color Frame使用reverse-mapping
      也就是方法一的部份!就是對每個Color Frames上的pixel計算在Body Index上的位置然後根據Body Index的值決定這個Pixel是不是剪影的一部份
      這樣取得的去背結果就是高解析度的去背效果喔

      不知道這樣有沒有幫到你啊...

      刪除
    3. 我也覺得奇怪,明明送出了,但在頁面就是看不到。
      可以請教您對C#熟嗎? 我們因為是要放在Unity裡面寫,
      所以它的版本目前只支援C#

      刪除
  2. 不好意思,我現有段程式關於二維坐標與三維坐標間的轉換一直出不來,您能否幫忙看看?謝謝!

    回覆刪除
  3. 您好,
    想請教一個有關Kinect2裡HighDefinitionFace的問題,
    實現HDFace的步驟為:彩色影像->骨架資訊->建立HDFace,
    我照著上面步驟抓取資訊完成第一步(彩色影像)與第二步(骨架資訊)後,
    在第三步CreateHighDefinitionFaceFrameSource這個函示都會發生問題,
    一直持續回傳failed值。
    不過對照網路上幾個使用Direct2D實現HDFace的程式,
    發現程式的資料結構大致上是相同的,
    所以很困惑為什麼會出錯,
    除了要include kinect.face.h和增加Kinect20.Face.lib,
    還有其他"屬性設定"或"環境設定"的要求我沒有注意到的嗎?
    謝謝您了。

    回覆刪除