Kinect数据提取与坐标变换
来源:互联网 发布:sql 短信预警 编辑:程序博客网 时间:2024/05/02 05:07
简述
Kinect是微软推出的传感器产品,配套Xbox游戏主机,主要针对于家庭娱乐市场。但是微软似乎在搞砸自己产品定位的方面有独特的天赋,虽然销量拼不过PS4,却在科学界大放异彩,以优异的性能和低廉的价格,成为了视觉定位相关研究领域的标配设备。
本文章目的在于从Kinect中提取彩色数据流和深度数据流,并完成两者的坐标变换。因为采集彩色数据和深度数据使用的是两个不同摄像头,所以得到的图像并不完全对应。所以使两者对齐到同一坐标下对后续数据处理非常必要。
实验使用的设备为Kinect一代产品。开发基于WPF框架,语言为C#。代码参考于Developer Toolkit中C#范例 Color Basics,Depth Basics,Coordinate Mapping Basics部分。
Sensor对象主体操作
在C#中使用一个名为KinectSensor的对象描述一台Kinect设备,一般情况下一台PC只可以连接一台Kinect,否则会触发“带宽不足”的错误。
对Kinect的操作有搜索可用设备,打开设备,接收数据流等操作。
需要使用的传感器对象的声明
private KinectSensor sensor; //传感器对象主体
从设备列表中搜索可用的Kinect
foreach (var potentialSensor in KinectSensor.KinectSensors){ if (potentialSensor.Status == KinectStatus.Connected) { this.sensor = potentialSensor; break; }}
使能流数据并设置格式
这里,需要使能深度流和彩色流,并设置格式为640x480,Fps=30.
this.sensor.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30);//使能彩色流并设置模式this.sensor.DepthStream.Enable(DepthImageFormat.Resolution640x480Fps30);//使能深度流并设置模式
添加响应事件函数
以下分别表示颜色流/深度流/所有流就绪的事件处理函数。函数名可自定义,但参数固定。具体见函数定义。这里我们需要得到同步的图像流和深度流,因而仅需要使用所有流就绪的处理函数。当事件发生后,会自动触发相应的函数。
// this.sensor.ColorFrameReady += this.SensorColorFrameReady;//颜色流// this.sensor.DepthFrameReady += this.SensorDepthFrameReady;//深度流this.sensor.AllFramesReady += this.SensorAllFramesReady;//所有流
启动设备
当sensor!=null
的时候,就可以尝试启动设备
try{ this.sensor.Start();}catch (IOException){ this.sensor = null;}
设备启动后,当数据流就绪后,就会触发相应的事件处理函数。
数据提取
数据提取在事件处理函数中进行。
private void SensorAllFramesReady(object sender, AllFramesReadyEventArgs e){ //..... 函数主体}
- 对于彩色数据来说,每像素为8位4通道的BGRA数据。其中第四个通道未使用。因而数据可以直接拷贝到byte[]类型的数组中,用以生成8位4通道的彩色图像来显示。
- 深度数据的每像素为一个16位short数据,必须存入DepthImagePixel[]类型的数组中,然后可以转存入UInt16[]类型的数组中,用以生成16位的灰度图像来显示。
当彩色数据和深度数据均就绪后,进入事件处理函数。先检测传感器对象有效性:
if (null == this.sensor) { return;//检测有效性 }
当一帧数据接受之后,我们需要把数据拷贝到特定的像素数组里面加以处理。
在WPF中提供了专用以动态图像显示的WriteableBitmap类,可由像素数组直接填充。
对彩色数据的处理
存储彩色数据的像素数组需要在该函数外声明和定义:
private byte[] rgb_pix; //像素数组,可以从彩色流中读取this.rgb_pix = new byte[this.sensor.ColorStream.FramePixelDataLength];//初始化
用像素数组构建彩色位图,用以显示和保存。位图对象的声明和定义:
private WriteableBitmap rgb_bitmap; //图像流产生的图像,由rgb_pix像素数组转换得到this.rgb_bitmap = new WriteableBitmap( this.sensor.ColorStream.FrameWidth, //尺寸(宽) this.sensor.ColorStream.FrameHeight,//尺寸(高) 96.0, 96.0,//横向和纵向分辨率 PixelFormats.Bgr32,//格式BGRA32位 null);
对彩色数据的拷贝工作:
using (ColorImageFrame colorFrame = e.OpenColorImageFrame())//打开图像帧{ //若数据异常,退出函数 if (colorFrame == null) return; //保存彩色信息到彩色图像素数组内 colorFrame.CopyPixelDataTo(this.rgb_pix); //用像素数组构建bitmap图像 this.rgb_bitmap.WritePixels( new Int32Rect(0, 0, this.rgb_bitmap.PixelWidth, this.rgb_bitmap.PixelHeight),//尺寸 this.rgb_pix,//像素数组 this.rgb_bitmap.PixelWidth * 4,//行字节数,每像素有BGRA四通道四字节。 0); }}
上述代码完成了以下工作:
- 打开图像帧
- 保存数据到像素数组
- 构建位图图像
示例:
对深度数据的处理
深度数据的处理类似,不同的是深度数据的格式不同,需要做一些转换工作。
一个深度信息是16位的带符号short数据,这大大超过了一个8位图像单像素的容纳范围。所以为了便于显示,我们使用了一个16位单通道的灰度图像。因而需要完成:
- 从设备拷贝数据到深度数组
- 从深度数组构建像素数组
- 由像素数组构建灰度图像
专门存储深度信息的深度数组声明和定义如下:
private DepthImagePixel[] depthPixels; //不同于图像流,深度流的数据类型是short型,需要专门的数组来存储this.depthPixels = new DepthImagePixel[this.sensor.DepthStream.FramePixelDataLength];
为了图像的显示,需要从深度数组转换到像素数组:
像素数组的声明和初始化
private UInt16[] dp_pix; //深度像素数组。为了生成16位单通道图像,所以才使用了UInt16[]类型的数组this.dp_pix = new UInt16[this.sensor.DepthStream.FramePixelDataLength];
灰度位图对象的声明和初始化
private WriteableBitmap dp_bitmap; //深度图像,由深度图像数组得到this.dp_bitmap = new WriteableBitmap( this.sensor.ColorStream.FrameWidth,//尺寸(宽) this.sensor.ColorStream.FrameHeight, //尺寸(高) 96.0, 96.0,//横向纵向分辨率 PixelFormats.Gray16,//像素格式:16位灰度图 null);
打开深度数据流,并保存数据
using (DepthImageFrame depthFrame = e.OpenDepthImageFrame())//打开一帧深度数据{ if (depthFrame == null) return; // 保存深度信息到特定的深度数组内。注意,深度数据是short类型 depthFrame.CopyDepthImagePixelDataTo(this.depthPixels); for (int i = 0; i < this.depthPixels.Length; ++i) { // 得到深度数据 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,//像素数组 this.dp_bitmap.PixelWidth * 2,//行字节数=行宽*数据字节数 0); }
深度数据是拷贝到特定的数组中去的,而非简单的字节数组。depthPixels的每个元素是一个对象,拥有Depth成员,以存储深度信息。一个深度信息是16位的带符号short数据,范围约正负30000.
其中,据微软声称,深度数据的“可靠数据范围”为800mm-4000mm。
示例:
*关于灰度图显示的优化
对于一个16位灰度图来说,每个像素的数据范围是0-65535,对应颜色为黑色和白色。而Kinect的depth数据通常在6000(6米)以下,所以数据多数投影到了暗色数值,因而显示效果偏暗。为了改进视觉效果,可以把depth数据扩大一个固定的倍数,来作为像素值。实现时请注意数据类型转换,以及数据越界检查。相关工作请读者自行完成。
坐标对齐
在做视觉SLAM的时候,从彩色图像中找到一个特征点(X,Y),需要知道它的深度信息。但是彩色图和深度图并不完全对应,所以需要做额外的处理。例如下面的两幅图中,深度图似乎放大了一点。
坐标对应
彩色图(图1)中的绿点和深度图(图2)中的蓝点,实际对应于物理空间的同一个点。即二者相互对应。而实现坐标变换的第一步,就是把这种对应关系找出来。比如说,我从彩色图像中找到了某个特征点,需要知道它的深度信息,那么我如何找到彩色图上的这个点(rowC,colC)所对应的深度图像上的点(rowD,colD)呢?
1. 从彩色点到深度点的映射
SDK中提供了一个函数MapColorFrameToDepthFrame
就是用以实现这种投影关系的。它可以生成一个DepthImagePoint[]
类型的数组,来存储每个彩色点对应的深度点位置信息。例如:
//定义格式常量private const DepthImageFormat DepthFormat = DepthImageFormat.Resolution640x480Fps30;//深度格式private const ColorImageFormat ColorFormat = ColorImageFormat.RgbResolution640x480Fps30;//彩色格式//定义用于存储转换结果的坐标数组DepthImagePoint[] depthCoordinates;depthCoordinates = new DepthImagePoint[this.sensor.DepthStream.FramePixelDataLength];//....做处理....this.sensor.CoordinateMapper.MapColorFrameToDepthFrame( ColorFormat, DepthFormat, this.depthPixels, this.depthCoordinates);//得到目标对应值//注意C#中序列起始下标为0.图像坐标起始下标也为0int pos=rowC*640+rowD;//像素点在一维序列中的位置。colD = depthCoordinates[pos].X;//注意X为col值rowD = depthCoordinates[pos].Y;//注意Y为row值
这样,得到了(rowC,colC)->(rowD,colD)的映射关系。但是注意,这种映射关系是单向的,这意味着每个彩色点都可以找到对应的深度点,但每个深度点未必可以找到一个彩色点来对应。这在后续的变换深度图中很重要。
2.从深度点到彩色点的映射
这小节内容的原理同上小节类似,但所针对的问题是:从深度图像中确定某个点,希望得到它的颜色信息,故需要找到该点在彩色图像中的“映象”。
函数MapDepthFrameToColorFrame
用以实现从深度点到彩色点的投影关系。它可以生成一个ColorImagePoint[]
类型的数组,来存储每个深度点对应的彩色点位置信息。例如:
//定义坐标数组用以存储结果ColorImagePoint[] colorCoordinates;colorCoordinates = new ColorImagePoint[this.sensor.DepthStream.FramePixelDataLength];//....做处理....this.sensor.CoordinateMapper.MapDepthFrameToColorFrame( DepthFormat, this.depthPixels, ColorFormat, this.colorCoordinates);//得到目标的对应值//注意C#中序列起始下标为0.图像坐标起始下标也为0int pos=rowD*640+colD;//像素点在一维序列中的位置。colC = colorCoordinates[pos].X;//注意X为col值rowC = colorCoordinates[pos].Y;//注意Y为row值
这样,得到了(rowD,colD)->(rowC,colC)的映射关系。但是注意,这种映射关系同样是单向的,这意味着每个深度点都可以找到对应的彩色点,反之不然。
坐标变换
如果需要离线采集数据,那么希望得到这样的一组图像:彩色图A和深度图B,给定某点坐标(X,Y),那么:A(X,Y)为该点彩色信息,B(X,Y)为该点深度信息。换言之,A,B两者完全对应。这样的结果便于保存和后续的处理工作。
通过变换深度图(2)可以得到图(3);通过变换彩色图(1)可以得到图(4)。上图中4张图像中标注的点,实际上对应于物理空间中的同一个点。所以这种变换应该是如下产生的:
- 把原始深度图像(2)对齐到彩色图的坐标下,生成图(3)。图(1)(3)可以作为一组结果进行保存,它们的像素是完全对应的。
- 把原始彩色图像(1)对齐到深度图的坐标下,生成图(4)。图(2)(4)可以作为一组结果进行保存,它们的像素是完全对应的。
1. 以彩色图为基准,把深度图对齐到彩色图
该部分的核心函数为MapColorFrameToDepthFrame
,即把深度像素投影到彩色图空间。听到这里一定会让人疑惑,既然是把深度图对齐到彩色图,难道不是从深度图到彩色图投影吗?
所以接下来是比较生涩难懂的部分,再次贴出示意图:
我们的目标是从图2生成图3,所以图3一开始为空,我们需要逐个像素去填充。假设我们需要填充(rowC,colC)位置的像素。因为图1图3必须要完全对应,所以图1(rowC,colC)和图3(rowC,colC)对应的是同一个物理点的颜色和深度信息。怎么去得知这个点的深度信息呢?当然是找到图1(rowC,colC)对应的图2(rowD,colD),然后图3(rowC,colC)由图2(rowD,colD)来填充。图1图2的对应关系就是由MapColorFrameToDepthFrame
得到的(rowC,colC)->(rowD,colD)来确定的。
映射的单向关系
正是因为这种映射关系是单向的,所以为了将深度图对齐到彩色图,必须是彩色点->深度点的映射,才能保证每个彩色点都可以找到它的“映象”。
该部分代码依然包含在事件处理函数以内,用以执行坐标对齐操作。
//定义和初始化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);
//坐标映射this.sensor.CoordinateMapper.MapColorFrameToDepthFrame( ColorFormat, DepthFormat, this.depthPixels, this.depthCoordinates);//初始化像素数组。必须用遍历的方式初始化,自带的Initialize()成员函数不好用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++) { //对于深度数组的每个点,找到该点对应于彩色图像上的像素位置,然后把该像素点着色 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. 以深度图为基准,把彩色图对齐到深度图
这部分原理和上一节是相同的,所以仅贴出代码:
//请参考上节自行完成相关变量的定义和初始化this.sensor.CoordinateMapper.MapDepthFrameToColorFrame( DepthFormat, this.depthPixels, ColorFormat, this.colorCoordinates);for (int i = 0; i < rgb2_pix.Length; i++) rgb2_pix[i] = 0;//USEFOR to init!!! 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);
处理结果
存储数据
上节中说到,需要存储的数据应该是一组图片,根据需要可以是rgb_bitmap
和dp2_bitmap
或rgb2_bitmap
和dp_bitmap
。存储的格式建议为Png文件,经笔者测试,相比于Bmp图像会大大节省存储空间。
存储时为了避免多线程对同一对象的读写冲突,建议使用互斥锁:
Object thisLock = new Object();lock (thisLock){ //..处理...}
存储WriteableBitmap
对象需要一个PngBitmapEncoder
对象:
PngBitmapEncoder encoder_ = new PngBitmapEncoder();// 创建编码器并把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);}
小结
- 微软的SDK里面提供了众多的Sample,我的代码就是参考它们的。不过这些代码参考于3个不同的Sample,我把它们整合到了一起,并做了注释,理解和分析。
- 整个程序是一个不断响应执行的过程,代码主要集中于事件响应函数。所以相关的对象应在函数外声明,在窗体加载函数内初始化,在响应函数内处理。为了叙述方便,我才将这些代码放在一起,实际上它们分散于各处。这种规范请参考微软SDK的Sample。
- 经测试发现,如果同时执行两种坐标变换,程序会有明显的卡顿。建议只执行一种。
原文链接:http://www.jianshu.com/p/ec8deee8e137
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
- Kinect数据提取与坐标变换
- Kinect提取骨骼数据---新版本
- kinect各种流的坐标单位,坐标变换
- 基变换与坐标变换
- 基变换与坐标变换
- SVG - 坐标与变换
- 积分与坐标变换
- 空间数据的坐标变换
- KINECT同时提取六人骨骼数据
- GDI 坐标系统与坐标变换(转)
- MFC Windows坐标系统与坐标变换
- 三维观察与坐标变换
- 线性代数:坐标与基变换
- 空间坐标变换与投影
- 过渡矩阵与坐标变换
- 霍夫变换提取圆心坐标,并拟合直线
- 触摸屏的校准与坐标变换
- glLoadIdentity()与glTranslatef()和glRotatef()--坐标变换
- android开发 recycleview的item中加入checkbox实现批量选择条目(复用时防止checkbox勾选状态错乱)
- easygui的安装
- 设计模式-开篇
- 3.5.1
- android枚举的替代(官方建议)
- Kinect数据提取与坐标变换
- jQuery Ajax 方法详解
- Unix环境高级编程--第十一章--线程
- python基础知识之改变列表的基本操作
- Python:Pandas:DataFrame基础(1)
- Android M WRITE_SETTINGS权限的一个BUG
- 移动BPM解决方案分享
- Unity3D之Vector3.Dot和Vector3.Cross的使用
- Java EnumMap 代替序数索引