Kinect數(shù)據(jù)提取與坐標(biāo)變換

簡述

Kinect是微軟推出的傳感器產(chǎn)品,配套Xbox游戲主機,主要針對于家庭娛樂市場。但是微軟似乎在搞砸自己產(chǎn)品定位的方面有獨特的天賦,雖然銷量拼不過PS4,卻在科學(xué)界大放異彩,以優(yōu)異的性能和低廉的價格,成為了視覺定位相關(guān)研究領(lǐng)域的標(biāo)配設(shè)備。

kinect

本文章目的在于從Kinect中提取彩色數(shù)據(jù)流和深度數(shù)據(jù)流,并完成兩者的坐標(biāo)變換。因為采集彩色數(shù)據(jù)和深度數(shù)據(jù)使用的是兩個不同攝像頭,所以得到的圖像并不完全對應(yīng)。所以使兩者對齊到同一坐標(biāo)下對后續(xù)數(shù)據(jù)處理非常必要。
實驗使用的設(shè)備為Kinect一代產(chǎn)品。開發(fā)基于WPF框架,語言為C#。代碼參考于Developer Toolkit中C#范例 Color Basics,Depth Basics,Coordinate Mapping Basics部分。

Sensor對象主體操作

在C#中使用一個名為KinectSensor的對象描述一臺Kinect設(shè)備,一般情況下一臺PC只可以連接一臺Kinect,否則會觸發(fā)“帶寬不足”的錯誤。
對Kinect的操作有搜索可用設(shè)備,打開設(shè)備,接收數(shù)據(jù)流等操作。

需要使用的傳感器對象的聲明
private KinectSensor sensor;            //傳感器對象主體
從設(shè)備列表中搜索可用的Kinect
foreach (var potentialSensor in KinectSensor.KinectSensors)
{
    if (potentialSensor.Status == KinectStatus.Connected)
    {
        this.sensor = potentialSensor;
        break;
    }
}
使能流數(shù)據(jù)并設(shè)置格式

這里,需要使能深度流和彩色流,并設(shè)置格式為640x480,F(xiàn)ps=30.

this.sensor.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30);//使能彩色流并設(shè)置模式
this.sensor.DepthStream.Enable(DepthImageFormat.Resolution640x480Fps30);//使能深度流并設(shè)置模式
添加響應(yīng)事件函數(shù)

以下分別表示顏色流/深度流/所有流就緒的事件處理函數(shù)。函數(shù)名可自定義,但參數(shù)固定。具體見函數(shù)定義。這里我們需要得到同步的圖像流和深度流,因而僅需要使用所有流就緒的處理函數(shù)。當(dāng)事件發(fā)生后,會自動觸發(fā)相應(yīng)的函數(shù)。

// this.sensor.ColorFrameReady += this.SensorColorFrameReady;//顏色流
// this.sensor.DepthFrameReady += this.SensorDepthFrameReady;//深度流
this.sensor.AllFramesReady += this.SensorAllFramesReady;//所有流
啟動設(shè)備

當(dāng)sensor!=null的時候,就可以嘗試啟動設(shè)備

try
{
    this.sensor.Start();
}
catch (IOException)
{
    this.sensor = null;
}

設(shè)備啟動后,當(dāng)數(shù)據(jù)流就緒后,就會觸發(fā)相應(yīng)的事件處理函數(shù)。

數(shù)據(jù)提取

數(shù)據(jù)提取在事件處理函數(shù)中進行。

private void SensorAllFramesReady(object sender, AllFramesReadyEventArgs e)
{
    //..... 函數(shù)主體
}
  • 對于彩色數(shù)據(jù)來說,每像素為8位4通道的BGRA數(shù)據(jù)。其中第四個通道未使用。因而數(shù)據(jù)可以直接拷貝到byte[]類型的數(shù)組中,用以生成8位4通道的彩色圖像來顯示。
  • 深度數(shù)據(jù)的每像素為一個16位short數(shù)據(jù),必須存入DepthImagePixel[]類型的數(shù)組中,然后可以轉(zhuǎn)存入UInt16[]類型的數(shù)組中,用以生成16位的灰度圖像來顯示。

當(dāng)彩色數(shù)據(jù)和深度數(shù)據(jù)均就緒后,進入事件處理函數(shù)。先檢測傳感器對象有效性:

if (null == this.sensor)
    {
        return;//檢測有效性
    }

當(dāng)一幀數(shù)據(jù)接受之后,我們需要把數(shù)據(jù)拷貝到特定的像素數(shù)組里面加以處理。
在WPF中提供了專用以動態(tài)圖像顯示的WriteableBitmap類,可由像素數(shù)組直接填充。

對彩色數(shù)據(jù)的處理

存儲彩色數(shù)據(jù)的像素數(shù)組需要在該函數(shù)外聲明和定義:

private byte[] rgb_pix;                 //像素數(shù)組,可以從彩色流中讀取
this.rgb_pix = new byte[this.sensor.ColorStream.FramePixelDataLength];//初始化

用像素數(shù)組構(gòu)建彩色位圖,用以顯示和保存。位圖對象的聲明和定義:

private WriteableBitmap rgb_bitmap;     //圖像流產(chǎn)生的圖像,由rgb_pix像素數(shù)組轉(zhuǎn)換得到
this.rgb_bitmap = new WriteableBitmap(
    this.sensor.ColorStream.FrameWidth, //尺寸(寬)
    this.sensor.ColorStream.FrameHeight,//尺寸(高)
    96.0, 96.0,//橫向和縱向分辨率
    PixelFormats.Bgr32,//格式BGRA32位
    null);

對彩色數(shù)據(jù)的拷貝工作:

using (ColorImageFrame colorFrame = e.OpenColorImageFrame())//打開圖像幀
{
    //若數(shù)據(jù)異常,退出函數(shù)
    if (colorFrame == null)
        return;
    //保存彩色信息到彩色圖像素數(shù)組內(nèi)
    colorFrame.CopyPixelDataTo(this.rgb_pix);
    //用像素數(shù)組構(gòu)建bitmap圖像
    this.rgb_bitmap.WritePixels(
         new Int32Rect(0, 0, this.rgb_bitmap.PixelWidth, this.rgb_bitmap.PixelHeight),//尺寸
         this.rgb_pix,//像素數(shù)組
         this.rgb_bitmap.PixelWidth * 4,//行字節(jié)數(shù),每像素有BGRA四通道四字節(jié)。
         0);
    }
}

上述代碼完成了以下工作:

  • 打開圖像幀
  • 保存數(shù)據(jù)到像素數(shù)組
  • 構(gòu)建位圖圖像

示例:

彩色圖像示例

對深度數(shù)據(jù)的處理

深度數(shù)據(jù)的處理類似,不同的是深度數(shù)據(jù)的格式不同,需要做一些轉(zhuǎn)換工作。
一個深度信息是16位的帶符號short數(shù)據(jù),這大大超過了一個8位圖像單像素的容納范圍。所以為了便于顯示,我們使用了一個16位單通道的灰度圖像。因而需要完成:

  • 從設(shè)備拷貝數(shù)據(jù)到深度數(shù)組
  • 從深度數(shù)組構(gòu)建像素數(shù)組
  • 由像素數(shù)組構(gòu)建灰度圖像

專門存儲深度信息的深度數(shù)組聲明和定義如下:

private DepthImagePixel[] depthPixels;  //不同于圖像流,深度流的數(shù)據(jù)類型是short型,需要專門的數(shù)組來存儲
this.depthPixels = new DepthImagePixel[this.sensor.DepthStream.FramePixelDataLength];

為了圖像的顯示,需要從深度數(shù)組轉(zhuǎn)換到像素數(shù)組:
像素數(shù)組的聲明和初始化

private UInt16[] dp_pix;             //深度像素數(shù)組。為了生成16位單通道圖像,所以才使用了UInt16[]類型的數(shù)組
this.dp_pix = new UInt16[this.sensor.DepthStream.FramePixelDataLength];

灰度位圖對象的聲明和初始化

private WriteableBitmap dp_bitmap;      //深度圖像,由深度圖像數(shù)組得到
this.dp_bitmap = new WriteableBitmap(
      this.sensor.ColorStream.FrameWidth,//尺寸(寬)
      this.sensor.ColorStream.FrameHeight, //尺寸(高)
      96.0, 96.0,//橫向縱向分辨率
      PixelFormats.Gray16,//像素格式:16位灰度圖 
      null);

打開深度數(shù)據(jù)流,并保存數(shù)據(jù)

using (DepthImageFrame depthFrame = e.OpenDepthImageFrame())//打開一幀深度數(shù)據(jù)
{
    if (depthFrame == null)
        return;
    // 保存深度信息到特定的深度數(shù)組內(nèi)。注意,深度數(shù)據(jù)是short類型
    depthFrame.CopyDepthImagePixelDataTo(this.depthPixels);
    for (int i = 0; i < this.depthPixels.Length; ++i)
    {
         // 得到深度數(shù)據(jù)
         short depth = depthPixels[i].Depth;
         dp_pix[i] = (UInt16)(depth);
    }
    //生成位圖圖像
    this.dp_bitmap.WritePixels(
          new Int32Rect(0, 0, this.dp_bitmap.PixelWidth, this.dp_bitmap.PixelHeight),//尺寸
          this.dp_pix,//像素數(shù)組
          this.dp_bitmap.PixelWidth * 2,//行字節(jié)數(shù)=行寬*數(shù)據(jù)字節(jié)數(shù)
          0);
    }

深度數(shù)據(jù)是拷貝到特定的數(shù)組中去的,而非簡單的字節(jié)數(shù)組。depthPixels的每個元素是一個對象,擁有Depth成員,以存儲深度信息。一個深度信息是16位的帶符號short數(shù)據(jù),范圍約正負(fù)30000.
其中,據(jù)微軟聲稱,深度數(shù)據(jù)的“可靠數(shù)據(jù)范圍”為800mm-4000mm。
示例:

深度圖像示例
*關(guān)于灰度圖顯示的優(yōu)化

對于一個16位灰度圖來說,每個像素的數(shù)據(jù)范圍是0-65535,對應(yīng)顏色為黑色和白色。而Kinect的depth數(shù)據(jù)通常在6000(6米)以下,所以數(shù)據(jù)多數(shù)投影到了暗色數(shù)值,因而顯示效果偏暗。為了改進視覺效果,可以把depth數(shù)據(jù)擴大一個固定的倍數(shù),來作為像素值。實現(xiàn)時請注意數(shù)據(jù)類型轉(zhuǎn)換,以及數(shù)據(jù)越界檢查。相關(guān)工作請讀者自行完成。

坐標(biāo)對齊

在做視覺SLAM的時候,從彩色圖像中找到一個特征點(X,Y),需要知道它的深度信息。但是彩色圖和深度圖并不完全對應(yīng),所以需要做額外的處理。例如下面的兩幅圖中,深度圖似乎放大了一點。

彩色圖 深度圖

坐標(biāo)對應(yīng)

坐標(biāo)對應(yīng)示意圖

彩色圖(圖1)中的綠點和深度圖(圖2)中的藍(lán)點,實際對應(yīng)于物理空間的同一個點。即二者相互對應(yīng)。而實現(xiàn)坐標(biāo)變換的第一步,就是把這種對應(yīng)關(guān)系找出來。比如說,我從彩色圖像中找到了某個特征點,需要知道它的深度信息,那么我如何找到彩色圖上的這個點(rowC,colC)所對應(yīng)的深度圖像上的點(rowD,colD)呢?

1. 從彩色點到深度點的映射

SDK中提供了一個函數(shù)MapColorFrameToDepthFrame就是用以實現(xiàn)這種投影關(guān)系的。它可以生成一個DepthImagePoint[]類型的數(shù)組,來存儲每個彩色點對應(yīng)的深度點位置信息。例如:

//定義格式常量
private const DepthImageFormat DepthFormat = DepthImageFormat.Resolution640x480Fps30;//深度格式
private const ColorImageFormat ColorFormat = ColorImageFormat.RgbResolution640x480Fps30;//彩色格式
//定義用于存儲轉(zhuǎn)換結(jié)果的坐標(biāo)數(shù)組
DepthImagePoint[] depthCoordinates;
depthCoordinates = new DepthImagePoint[this.sensor.DepthStream.FramePixelDataLength];
//....做處理....
this.sensor.CoordinateMapper.MapColorFrameToDepthFrame(
     ColorFormat,
     DepthFormat,
     this.depthPixels,
     this.depthCoordinates);
//得到目標(biāo)對應(yīng)值
//注意C#中序列起始下標(biāo)為0.圖像坐標(biāo)起始下標(biāo)也為0
int pos=rowC*640+rowD;//像素點在一維序列中的位置。
colD = depthCoordinates[pos].X;//注意X為col值
rowD = depthCoordinates[pos].Y;//注意Y為row值

這樣,得到了(rowC,colC)->(rowD,colD)的映射關(guān)系。但是注意,這種映射關(guān)系是單向的,這意味著每個彩色點都可以找到對應(yīng)的深度點,但每個深度點未必可以找到一個彩色點來對應(yīng)。這在后續(xù)的變換深度圖中很重要。

2.從深度點到彩色點的映射

這小節(jié)內(nèi)容的原理同上小節(jié)類似,但所針對的問題是:從深度圖像中確定某個點,希望得到它的顏色信息,故需要找到該點在彩色圖像中的“映象”。
函數(shù)MapDepthFrameToColorFrame用以實現(xiàn)從深度點到彩色點的投影關(guān)系。它可以生成一個ColorImagePoint[]類型的數(shù)組,來存儲每個深度點對應(yīng)的彩色點位置信息。例如:

//定義坐標(biāo)數(shù)組用以存儲結(jié)果
ColorImagePoint[] colorCoordinates;
colorCoordinates = new ColorImagePoint[this.sensor.DepthStream.FramePixelDataLength];
//....做處理....
this.sensor.CoordinateMapper.MapDepthFrameToColorFrame(
      DepthFormat,
      this.depthPixels,
      ColorFormat,
      this.colorCoordinates);
//得到目標(biāo)的對應(yīng)值
//注意C#中序列起始下標(biāo)為0.圖像坐標(biāo)起始下標(biāo)也為0
int pos=rowD*640+colD;//像素點在一維序列中的位置。
colC = colorCoordinates[pos].X;//注意X為col值
rowC = colorCoordinates[pos].Y;//注意Y為row值

這樣,得到了(rowD,colD)->(rowC,colC)的映射關(guān)系。但是注意,這種映射關(guān)系同樣是單向的,這意味著每個深度點都可以找到對應(yīng)的彩色點,反之不然。

坐標(biāo)變換

如果需要離線采集數(shù)據(jù),那么希望得到這樣的一組圖像:彩色圖A和深度圖B,給定某點坐標(biāo)(X,Y),那么:A(X,Y)為該點彩色信息,B(X,Y)為該點深度信息。換言之,A,B兩者完全對應(yīng)。這樣的結(jié)果便于保存和后續(xù)的處理工作。

坐標(biāo)對齊示意圖

通過變換深度圖(2)可以得到圖(3);通過變換彩色圖(1)可以得到圖(4)。上圖中4張圖像中標(biāo)注的點,實際上對應(yīng)于物理空間中的同一個點。所以這種變換應(yīng)該是如下產(chǎn)生的:

  1. 把原始深度圖像(2)對齊到彩色圖的坐標(biāo)下,生成圖(3)。圖(1)(3)可以作為一組結(jié)果進行保存,它們的像素是完全對應(yīng)的。
  2. 把原始彩色圖像(1)對齊到深度圖的坐標(biāo)下,生成圖(4)。圖(2)(4)可以作為一組結(jié)果進行保存,它們的像素是完全對應(yīng)的。

1. 以彩色圖為基準(zhǔn),把深度圖對齊到彩色圖

該部分的核心函數(shù)為MapColorFrameToDepthFrame,即把深度像素投影到彩色圖空間。聽到這里一定會讓人疑惑,既然是把深度圖對齊到彩色圖,難道不是從深度圖到彩色圖投影嗎?
所以接下來是比較生澀難懂的部分,再次貼出示意圖:

變換深度圖

我們的目標(biāo)是從圖2生成圖3,所以圖3一開始為空,我們需要逐個像素去填充。假設(shè)我們需要填充(rowC,colC)位置的像素。因為圖1圖3必須要完全對應(yīng),所以圖1(rowC,colC)和圖3(rowC,colC)對應(yīng)的是同一個物理點的顏色和深度信息。怎么去得知這個點的深度信息呢?當(dāng)然是找到圖1(rowC,colC)對應(yīng)的圖2(rowD,colD),然后圖3(rowC,colC)由圖2(rowD,colD)來填充。圖1圖2的對應(yīng)關(guān)系就是由MapColorFrameToDepthFrame得到的(rowC,colC)->(rowD,colD)來確定的。

映射的單向關(guān)系

正是因為這種映射關(guān)系是單向的,所以為了將深度圖對齊到彩色圖,必須是彩色點->深度點的映射,才能保證每個彩色點都可以找到它的“映象”。
該部分代碼依然包含在事件處理函數(shù)以內(nèi),用以執(zhí)行坐標(biāo)對齊操作。

//定義和初始化dp2_pix[]和dp2_bitmap,用以存儲變換后的深度圖像素和位圖信息。
private UInt16[] dp2_pix; 
private WriteableBitmap dp2_bitmap;   

dp2_pix = new UInt16[this.sensor.DepthStream.FramePixelDataLength * sizeof(int)];
dp2_bitmap = new WriteableBitmap(
     this.sensor.ColorStream.FrameWidth,
     this.sensor.ColorStream.FrameHeight, 
     96.0, 96.0, 
     PixelFormats.Gray16, 
     null);
//坐標(biāo)映射
this.sensor.CoordinateMapper.MapColorFrameToDepthFrame(
      ColorFormat,
      DepthFormat,
      this.depthPixels,
      this.depthCoordinates);
//初始化像素數(shù)組。必須用遍歷的方式初始化,自帶的Initialize()成員函數(shù)不好用
for (int i = 0; i < dp2_pix.Length; i++)
      dp2_pix[i] = 0;
for (int rowC = 0; rowC < this.dp_bitmap.PixelHeight; rowC++)
{
      for (int colC = 0; colC < this.dp_bitmap.PixelWidth; colC++)
      {
           //對于深度數(shù)組的每個點,找到該點對應(yīng)于彩色圖像上的像素位置,然后把該像素點著色
           int pos = rowC * 640 + colC;//對于某個(X,Y)的像素點來說,它的順序位置為pos
           int colD = depthCoordinates[pos].X;
           int rowD = depthCoordinates[pos].Y;
           if (colD >= 0 && colD <= 639 && rowD >= 0 && rowD <= 479)
           {
                dp2_pix[rowC * 640 + colC] = dp_pix[rowD * 640 + colD];
           }

      }
}
//填充位圖圖像
this.dp2_bitmap.WritePixels(
     new Int32Rect(0, 0, this.dp2_bitmap.PixelWidth,this.dp2_bitmap.PixelHeight),
     this.dp2_pix,
     this.dp2_bitmap.PixelWidth * 2,
     0);

2. 以深度圖為基準(zhǔn),把彩色圖對齊到深度圖

這部分原理和上一節(jié)是相同的,所以僅貼出代碼:

//請參考上節(jié)自行完成相關(guān)變量的定義和初始化
this.sensor.CoordinateMapper.MapDepthFrameToColorFrame(
       DepthFormat,
       this.depthPixels,
       ColorFormat,
       this.colorCoordinates);
for (int i = 0; i < rgb2_pix.Length; i++)
      rgb2_pix[i] = 0;//USEFOR?。簦铩。椋睿椋簦。?!     
for (int rowD = 0; rowD < this.dp_bitmap.PixelHeight; rowD++)
{
       for (int colD = 0; colD < this.dp_bitmap.PixelWidth; colD++)
       {
             int pos = rowD * this.dp_bitmap.PixelWidth + colD;
             int colC = colorCoordinates[pos].X;
             int rowC = colorCoordinates[pos].Y;
             if (colC >= 0 && colC <= 639 && rowC >= 0 && rowC <= 479)
             {
                     rgb2_pix[(rowD * 640 + colD) * 4] = rgb_pix[(rowC * 640 + colC) * 4];
                     rgb2_pix[(rowD * 640 + colD) * 4 + 1] = rgb_pix[(rowC * 640 + colC) * 4 + 1];
                     rgb2_pix[(rowD * 640 + colD) * 4 + 2] = rgb_pix[(rowC * 640 + colC) * 4 + 2];
              }
         }
}
this.rgb2_bitmap.WritePixels(
      new Int32Rect(0, 0, this.rgb2_bitmap.PixelWidth, this.rgb2_bitmap.PixelHeight),
      this.rgb2_pix,
      this.rgb2_bitmap.PixelWidth * sizeof(int),
      0);

處理結(jié)果

處理結(jié)果 處理結(jié)果
原始彩色圖1
原始深度圖2
變換后的深度圖3
變換后的彩色圖4

存儲數(shù)據(jù)

上節(jié)中說到,需要存儲的數(shù)據(jù)應(yīng)該是一組圖片,根據(jù)需要可以是rgb_bitmapdp2_bitmaprgb2_bitmapdp_bitmap。存儲的格式建議為Png文件,經(jīng)筆者測試,相比于Bmp圖像會大大節(jié)省存儲空間。
存儲時為了避免多線程對同一對象的讀寫沖突,建議使用互斥鎖:

Object thisLock = new Object();
lock (thisLock)
{
    //..處理...
}

存儲WriteableBitmap對象需要一個PngBitmapEncoder對象:

PngBitmapEncoder encoder_ = new PngBitmapEncoder();
// 創(chuàng)建編碼器并把bitmap載入到編碼器中去
encoder_.Frames.Add(BitmapFrame.Create(this.rgb2_bitmap));   
using (FileStream fs = new FileStream(@"D:\colorMap" + DateTime.Now.ToString("-HH-mm-ss") + ".png", FileMode.Create))
{//使用文件流來保存成文件
      encoder_.Save(fs);
}

小結(jié)

  1. 微軟的SDK里面提供了眾多的Sample,我的代碼就是參考它們的。不過這些代碼參考于3個不同的Sample,我把它們整合到了一起,并做了注釋,理解和分析。
  2. 整個程序是一個不斷響應(yīng)執(zhí)行的過程,代碼主要集中于事件響應(yīng)函數(shù)。所以相關(guān)的對象應(yīng)在函數(shù)外聲明,在窗體加載函數(shù)內(nèi)初始化,在響應(yīng)函數(shù)內(nèi)處理。為了敘述方便,我才將這些代碼放在一起,實際上它們分散于各處。這種規(guī)范請參考微軟SDK的Sample。
  3. 經(jīng)測試發(fā)現(xiàn),如果同時執(zhí)行兩種坐標(biāo)變換,程序會有明顯的卡頓。建議只執(zhí)行一種。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容