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

本文章目的在于從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)

彩色圖(圖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ù)的處理工作。

通過變換深度圖(2)可以得到圖(3);通過變換彩色圖(1)可以得到圖(4)。上圖中4張圖像中標(biāo)注的點,實際上對應(yīng)于物理空間中的同一個點。所以這種變換應(yīng)該是如下產(chǎn)生的:
- 把原始深度圖像(2)對齊到彩色圖的坐標(biāo)下,生成圖(3)。圖(1)(3)可以作為一組結(jié)果進行保存,它們的像素是完全對應(yīng)的。
- 把原始彩色圖像(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_bitmap和dp2_bitmap或rgb2_bitmap和dp_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é)
- 微軟的SDK里面提供了眾多的Sample,我的代碼就是參考它們的。不過這些代碼參考于3個不同的Sample,我把它們整合到了一起,并做了注釋,理解和分析。
- 整個程序是一個不斷響應(yīng)執(zhí)行的過程,代碼主要集中于事件響應(yīng)函數(shù)。所以相關(guān)的對象應(yīng)在函數(shù)外聲明,在窗體加載函數(shù)內(nèi)初始化,在響應(yīng)函數(shù)內(nèi)處理。為了敘述方便,我才將這些代碼放在一起,實際上它們分散于各處。這種規(guī)范請參考微軟SDK的Sample。
- 經(jīng)測試發(fā)現(xiàn),如果同時執(zhí)行兩種坐標(biāo)變換,程序會有明顯的卡頓。建議只執(zhí)行一種。



