Kinect開發學習筆記之(四)提取顏色資料並用OpenCV顯示

查志強發表於2016-08-02

【原文:http://blog.csdn.net/zouxy09/article/details/8146266

Kinect開發學習筆記之(四)提取顏色資料並用OpenCV顯示

zouxy09@qq.com

http://blog.csdn.net/zouxy09

 

我的Kinect開發平臺是:

Win7 x86 + VS2010 + Kinect for Windows SDK v1.6 + OpenCV2.3.0

開發環境的搭建見上一文:

http://blog.csdn.net/zouxy09/article/details/8146055

下面這幾個大部分是參考“timebomb”的Kinect學習筆記系列:

http://blog.csdn.net/timebomb/article/details/7169372

非常感謝“timebomb”的工作,讓我能儘快的進入Kinect的開發。

 

本學習筆記以下面的方式組織:程式設計前期分析、程式碼與註釋和重要程式碼解析三部分。

 

要實現目標:通過微軟的SDK提取顏色資料(彩色影象)並用OpenCV顯示

 

一、程式設計前期分析

      我們在http://blog.csdn.net/zouxy09/article/details/8145592中提到:

      Kinect有三個鏡頭,中間的鏡頭是 RGB 彩色攝影機,用來採集彩色影象。左右兩邊鏡頭則分別為紅外線發射器和紅外線CMOS 攝影機所構成的3D結構光深度感應器,用來採集深度資料(場景中物體到攝像頭的距離)。彩色攝像頭最大支援1280*960解析度成像,紅外攝像頭最大支援640*480成像。那下面我們就是要通過微軟提供的SDK的API去讀取驅動上面的彩色攝像頭來讀取彩色影象。

      一個應用程式從Kinect感測器陣列中訪問下列影象資料:

1)色彩資料:就是彩色攝像頭採集到的資料,我們可以設定採集的解析度;

2)深度資料:就是紅外攝像頭採集到的資料,同樣可以設定採集的解析度;

3)帶遊戲者ID的深度資料:Kinect可以檢測6個人,所以深度資料中有攜帶標示這是哪個遊戲者的深度資料的。

4)骨骼點資料:實際上不能算是影象資料,感覺應該是Kinect上層演算法分析彩色和深度影象得到的骨骼點資料,包含了跟蹤到的人的關節點的位置等資訊。

       而對於彩色和深度這些影象資料,SDK是以資料流的方式來組織的,也就是影象資料按順序的一幀一幀的流過來,你需要的時候就拿。當然,如果你拿的速度比攝像頭提供影象的速度要快,那麼你就需要等待,等待攝像頭產生新的資料給你。那麼這個“等”就有了兩種方式了:

1)查詢方式:反正我也沒事幹,所以我不停的問攝像頭拿資料,通過一個while迴圈不斷地催它,然後一旦有新的影象資料了,我拿到就跑;

2)事件方式:要我不停地催你,我也煩,你沒有資料給我,那我先打個瞌睡(休眠了,不用佔CPU資源),然後你有新的資料來後,再叫醒我(給個有資料的訊號),然後我再拿走資料。那我這個等新資料的過程就叫一個事件,系統通過一個事件的控制程式碼來標示,這樣系統才知道下面攝像頭有資料來了,系統才知道喚醒誰啊,是吧。而這個事件我們待會程式設計就遇到了。而目前,大部分是通過這種方式來得到影象資料的。(呵呵,不知道理解得對不對)

    還是通過程式碼來分析清晰點。

 

二、程式碼與註釋

[cpp] view plain copy
  1. #include <windows.h>  
  2. #include <iostream>   
  3. #include <NuiApi.h>  
  4. #include <opencv2/opencv.hpp>  
  5.   
  6. using namespace std;  
  7. using namespace cv;  
  8.   
  9. int main(int argc, char *argv[])  
  10. {  
  11.     Mat image;  
  12.     image.create(480, 640, CV_8UC3);  
  13.    
  14.     //1、初始化NUI   
  15.     HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_COLOR);   
  16.     if (FAILED(hr))   
  17.     {   
  18.         cout<<"NuiInitialize failed"<<endl;   
  19.         return hr;   
  20.     }   
  21.   
  22.     //2、定義事件控制程式碼   
  23.     //建立讀取下一幀的訊號事件控制程式碼,控制KINECT是否可以開始讀取下一幀資料  
  24.     HANDLE nextColorFrameEvent = CreateEvent( NULL, TRUE, FALSE, NULL );  
  25.     HANDLE colorStreamHandle = NULL; //儲存影象資料流的控制程式碼,用以提取資料   
  26.    
  27.     //3、開啟KINECT裝置的彩色圖資訊通道,並用colorStreamHandle儲存該流的控制程式碼,以便於以後讀取  
  28.     hr = NuiImageStreamOpen(NUI_IMAGE_TYPE_COLOR, NUI_IMAGE_RESOLUTION_640x480,   
  29.                             0, 2, nextColorFrameEvent, &colorStreamHandle);   
  30.     if( FAILED( hr ) )//判斷是否提取正確   
  31.     {   
  32.         cout<<"Could not open color image stream video"<<endl;   
  33.         NuiShutdown();   
  34.         return hr;   
  35.     }  
  36.     namedWindow("colorImage", CV_WINDOW_AUTOSIZE);  
  37.    
  38.     //4、開始讀取彩色圖資料   
  39.     while(1)   
  40.     {   
  41.         const NUI_IMAGE_FRAME * pImageFrame = NULL;   
  42.   
  43.         //4.1、無限等待新的資料,等到後返回  
  44.         if (WaitForSingleObject(nextColorFrameEvent, INFINITE)==0)   
  45.         {   
  46.             //4.2、從剛才開啟資料流的流控制程式碼中得到該幀資料,讀取到的資料地址存於pImageFrame  
  47.             hr = NuiImageStreamGetNextFrame(colorStreamHandle, 0, &pImageFrame);   
  48.             if (FAILED(hr))  
  49.             {  
  50.                 cout<<"Could not get color image"<<endl;   
  51.                 NuiShutdown();  
  52.                 return -1;  
  53.             }  
  54.   
  55.             INuiFrameTexture * pTexture = pImageFrame->pFrameTexture;  
  56.             NUI_LOCKED_RECT LockedRect;  
  57.   
  58.             //4.3、提取資料幀到LockedRect,它包括兩個資料物件:pitch每行位元組數,pBits第一個位元組地址  
  59.             //並鎖定資料,這樣當我們讀資料的時候,kinect就不會去修改它  
  60.             pTexture->LockRect(0, &LockedRect, NULL, 0);   
  61.             //4.4、確認獲得的資料是否有效  
  62.             if( LockedRect.Pitch != 0 )   
  63.             {   
  64.                 //4.5、將資料轉換為OpenCV的Mat格式  
  65.                 for (int i=0; i<image.rows; i++)   
  66.                 {  
  67.                     uchar *ptr = image.ptr<uchar>(i);  //第i行的指標  
  68.                       
  69.                     //每個位元組代表一個顏色資訊,直接使用uchar  
  70.                     uchar *pBuffer = (uchar*)(LockedRect.pBits) + i * LockedRect.Pitch;  
  71.                     for (int j=0; j<image.cols; j++)   
  72.                     {   
  73.                         ptr[3*j] = pBuffer[4*j];  //內部資料是4個位元組,0-1-2是BGR,第4個現在未使用   
  74.                         ptr[3*j+1] = pBuffer[4*j+1];   
  75.                         ptr[3*j+2] = pBuffer[4*j+2];   
  76.                     }   
  77.                 }   
  78.                 imshow("colorImage", image); //顯示影象   
  79.             }   
  80.             else   
  81.             {   
  82.                 cout<<"Buffer length of received texture is bogus\r\n"<<endl;   
  83.             }  
  84.   
  85.             //5、這幀已經處理完了,所以將其解鎖  
  86.             pTexture->UnlockRect(0);  
  87.             //6、釋放本幀資料,準備迎接下一幀   
  88.             NuiImageStreamReleaseFrame(colorStreamHandle, pImageFrame );   
  89.         }   
  90.         if (cvWaitKey(20) == 27)   
  91.             break;   
  92.     }   
  93.     //7、關閉NUI連結   
  94.     NuiShutdown();   
  95.     return 0;  
  96. }  

 

三、程式碼解析

首先,對Kinect,我們必須要包含下面兩個標頭檔案:

#include <windows.h>

#include <NuiApi.h>

 

1、初始化NUI

HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_COLOR);

任何想使用微軟提供的API來操作KINECT,都必須在所有操作之前,呼叫NUI的初始化函式:
HRESULT NuiInitialize(DWORD dwFlags);
    dwFlags引數是以標誌位的含義存在的。你可以使用下面幾個值來指定你打算使用NUI中的哪些內容。
NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX 提供帶使用者資訊的深度圖資料;

NUI_INITIALIZE_FLAG_USES_COLOR  提供色彩影象資料;

NUI_INITIALIZE_FLAG_USES_SKELETON     提供骨骼點資料;

NUI_INITIALIZE_FLAG_USES_DEPTH  提供深度影象資料.

NUI_INITIALIZE_FLAG_USES_AUDIO  提供聲音資料;

NUI_INITIALIZE_DEFAULT_HARDWARE_THREAD  初始化預設的硬體執行緒;

以上的標誌位,你可以使用一個,也可以用 | 操作符將它們組合在一起。例如:
//只使用彩色圖
HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_COLOR);
//使用帶使用者資訊的深度圖/使用使用者骨骼框架/使用彩色圖
HRESULT hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX | NUI_INITIALIZE_FLAG_USES_SKELETON | NUI_INITIALIZE_FLAG_USES_COLOR);
       一個應用程式對一個KINECT裝置,必須要呼叫此函式一次,並且也只能呼叫一次。如果在這之後又呼叫一次初始化,勢必會引起邏輯錯誤(即使是2個不同程式)。比如你執行一個SDK的例子,在沒關閉它的前提下,再執行一個,那麼後執行的就無法初始化成功,但不會影響之前的程式繼續執行。
      如果你的程式想使用多臺KINECT,那麼就需使用INuiInstance介面來初始化你的裝置(具體見手冊)。
      另外,作為一名KINECT程式設計師,你需要記得的是,微軟SDK中提供的執行環境在處理KINECT傳輸資料時,是遵循一條3步驟的執行管線的。
第一階段只處理彩色和深度資料;
第二階段處理使用者索引並根據使用者索引將顏色資訊追加到深度圖中。
第三階段處理骨骼追蹤資料;
       NuiInitialize就是應用程式用通過傳遞給dwFlags引數具體值,來初始化這個管線中必須的階段。因此,我們總是先在標誌位中指定影象型別,才可以在接下來的環節中去呼叫NuiImageStreamOpen之類的函式。如果你初始化的時候沒指定NUI_INITIALIZE_FLAG_USES_COLOR,那你以後就別指望NuiImageStreamOpen能開啟彩色資料了,它肯定會呼叫失敗,因為沒初始化嘛。就是說我們後面想要什麼資料,得先告訴Kinect,否則後面你要它也不會給你,因為壓根我就沒啟動那部分硬軟體,拿什麼給你啊。

另外,Kinect提供了兩種處理返回值的方式,就是判斷上面的函式是否執行成功。

//這是一種處理返回值的方式
if( FAILED( hr ) )
{
     cout<<"NuiInitialize failed"<<endl;
     return hr;
}
//這是另一種處理返回值的方式
if(hr == S_OK)
{
     cout<<"NuiInitialize successfully"<<endl;
}

 

2、定義事件控制程式碼

      HANDLE nextColorFrameEvent = CreateEvent( NULL, TRUE, FALSE, NULL );

CreateEvent()建立一個windows事件物件,建立成功則返回事件的控制程式碼。事件有兩個狀態,有訊號和沒有訊號!上面說到了。就是拿來等待新資料的。

CreateEvent函式需要4個引數:

·設定為NULL的安全描述符;

·一個設定為true的布林值,因為應用程式將重置事件訊息;

·一個未指定的事件訊息初始狀態的布林值;

·一個空字串,因為事件未命名。

 

3、開啟KINECT裝置的彩色資料流

       hr = NuiImageStreamOpen(NUI_IMAGE_TYPE_COLOR, NUI_IMAGE_RESOLUTION_640x480,

0, 2, nextColorFrameEvent, &colorStreamHandle);

       我們使用這個函式來開啟kinect彩色或者深度圖的訪問通道,當然,其內部原理是通過"流"來實現的,因此,你也可以把這個函式理解為,建立一個訪問彩色或者深度圖的資料流。
引數:
eImageType 

[in] 這是一個 NUI_IMAGE_TYPE 列舉型別的值,用來詳細指定你要建立的流型別。
比如你要開啟彩色圖,就使用 NUI_IMAGE_TYPE_COLOR。
要開啟深度圖,就使用 NUI_IMAGE_TYPE_DEPTH。
具體這個列舉有多少個成員,我建議你們仔細閱讀API手冊。
但是有一點是需要注意的,你能開啟的影象型別,必須是你在初始化的時候指定過的。
eResolution
[in] 這是一個 NUI_IMAGE_RESOLUTION 列舉型別的值,用來指定你要以什麼解析度來開啟eImageType(引數1)中指定的影象類別。
假如你在引數eImageType中指定的是彩色圖NUI_IMAGE_TYPE_COLOR,那麼你可以選擇2種解析度:NUI_IMAGE_RESOLUTION_1280x1024,NUI_IMAGE_RESOLUTION_640x480
如果你在引數eImageType中指定的是深度圖NUI_IMAGE_TYPE_DEPTH,那麼你可以選擇3種解析度NUI_IMAGE_RESOLUTION_640x480, NUI_IMAGE_RESOLUTION_320x240, NUI_IMAGE_RESOLUTION_80x60
API手冊裡,詳細描述了這個對照表,各種影象型別都支援什麼解析度。
dwImageFrameFlags_NotUsed 
[in] 你看引數名就知道了,這是個無用引數,隨便給個整數就行了。
dwFrameLimit
指定NUI執行時環境將要為你所開啟的影象型別建立幾個緩衝。最大值是NUI_IMAGE_STREAM_FRAME_LIMIT_MAXIMUM(當前版本為 4)對於大多數啊程式來說,2就足夠了。
hNextFrameEvent 
[in, optional] 一個用來手動重置訊號是否可用的事件控制程式碼(event),該訊號用來控制KINECT是否可以開始讀取下一幀資料。也就是說在這裡指定一個控制程式碼後,隨著程式往後繼續推進,當你在任何時候想要控制kinect讀取下一幀資料時,都應該先使用WaitForSingleObject判斷一下該控制程式碼,判斷是否有資料可拿。
phStreamHandle 
[out] 出參,指定一個控制程式碼的地址。函式成功執行後,將會建立對應的資料訪問通道(流),並且讓該控制程式碼儲存這個通道的地址。也就是說,如果現在建立成功了。那麼以後你想讀取資料,就要通過這個控制程式碼了。
返回值
只有S_OK表示成功開啟,錯誤原因卻有很多,比如開啟一個沒初始化過的資料流;開啟一個已被使用的資料流;引數phStreamHandle為NULL等等。自己查閱API手冊吧。

 

4、無限等待新的資料,等到後返回

      WaitForSingleObject(nextColorFrameEvent, INFINITE)==0    

      和剛才說的一樣,程式執行都這裡,這個事件有訊號,就是說有資料,那麼程式往下執行,如果沒有資料,就會等待。函式第二個參數列示你願意等多久,具體的資料的話就表示你願意等多少毫秒,還不來,我就不要了,繼續往下走。如果是INFINITE的話,就表示無限等待新資料,直到海枯石爛,一定等到為止。等到有訊號後就返回0 。

 

5、從資料流中拿資料

       hr = NuiImageStreamGetNextFrame(colorStreamHandle, 0, &pImageFrame);

      從剛才開啟資料流的流控制程式碼中得到該幀資料,讀取到的資料地址存於pImageFrame。第二個參數列示你延時多少微秒拿資料,0表示,我立刻拿。

如果你沒有遇到什麼錯誤的話,那麼剛才KINECT就捕獲了一副畫面,並將該畫面的資訊儲存在一個NUI_IMAGE_FRAME結構中,pImageFrame指向該結構的地址。

pImageFrame包含了很多有用資訊,包括:影象型別,解析度,影象緩衝區,時間戳等等。

 

6、INuiFrameTexture介面

      INuiFrameTexture * pTexture = pImageFrame->pFrameTexture;

      一個容納影象幀資料的物件,類似於Direct3D紋理,但是隻有一層(不支援mip-maping)。

其公有方法包含以下:

AddRef---增加一個物件上介面的引用數目;該方法在每複製一個指向該物件上介面的指標時都要呼叫一次;

BufferLen---獲得緩衝區的位元組長度;

GetLevelDesc---獲得緩衝區的描述;

LockRect---給緩衝區上鎖;

Pitch---返回一行的位元組數;

QueryInterface---獲取指向物件所支援的介面的指標,該方法對其所返回的指標呼叫AddRef函式;

Release---減少一個物件上介面的引用計數;

UnlockRect---對緩衝區解鎖;

 

7、提取資料幀到LockedRect並鎖定資料

      pTexture->LockRect(0, &LockedRect, NULL, 0);

      提取資料幀到LockedRect,它包括兩個資料物件:pitch每行位元組數,pBits第一個位元組地址。另外,其還鎖定資料,這樣當我們讀資料的時候,kinect就不會去修改它

   好了,現在真正儲存影象的物件LockedRect我們已經有了,並且也將影象資訊寫入這個物件了。

 

8、將資料轉換為OpenCV的Mat格式

      然後我們就將其儲存影象的物件LockedRect的格式,轉化為OpenCV的Mat格式,便於我們處理和顯示。

 

至此,目標達成。


相關文章