使用Kinect 2进行背景移除(绿幕效果)





5.00/5 (9投票s)
使用Kinect 2进行背景移除(绿幕效果)
在过去的几天里,我收到了很多关于Kinect颜色到深度像素映射的请求。正如你可能已经知道的,Kinect流并没有正确对齐。RGB和深度相机具有不同的分辨率,并且它们的视角略有偏移。因此,越来越多的人(无论是在博客评论中还是通过电子邮件)向我询问如何正确对齐颜色和深度流。他们最想构建的最常见的应用程序是一个很酷的绿幕效果,就像下面的视频一样。
视频
正如你所看到的,漂亮的女孩被Kinect传感器追踪,并且背景完全被移除。我可以将背景替换为纯色、渐变填充,甚至是随机图像!
很棒,对吧?所以,我创建了一个简单的项目,将玩家的深度值映射到相应的颜色像素。这样,我可以移除背景并将其替换为其他内容。源代码托管在GitHub上,作为一个单独的项目。它也是Vitruvius的一部分。
阅读教程,了解Kinect坐标映射的工作原理,并自己创建应用程序。
要求
- Kinect for Windows v2
- Windows 8/8.1
- Visual Studio 2013
- USB 3.0 端口
背景移除的工作原理
当我们提到“背景移除”时,我们需要保留构成用户的像素,并移除不属于用户的任何其他内容。Kinect传感器的深度相机对于确定用户的身体非常有用。但是,我们需要找到RGB颜色值,而不是深度距离。我们需要指定哪些RGB值对应于用户的深度值。困惑了?请不要这样。
使用Kinect,空间中的每个点都具有以下信息
- 颜色值:红色 + 绿色 + 蓝色
- 深度值:到传感器的距离
深度相机为我们提供了深度值,而RGB相机为我们提供了颜色值。我们使用CoordinateMapper
映射这些值。CoordinateMapper
是一个有用的Kinect属性,用于确定哪些颜色值对应于每个深度距离(反之亦然)。
请注意,RGB帧(1920×1080)比深度帧(512×424)更宽。因此,并非每个颜色像素都有相应的深度映射。但是,身体跟踪主要使用深度传感器执行,因此无需担心缺失值。
代码
在我在GitHub上分享的项目中,你可以使用以下代码来移除背景并获得绿幕效果
void Reader_MultiSourceFrameArrived(object sender, MultiSourceFrameArrivedEventArgs e)
{
var reference = e.FrameReference.AcquireFrame();
var colorFrame = reference.ColorFrameReference.AcquireFrame();
var depthFrame = reference.DepthFrameReference.AcquireFrame();
var bodyIndexFrame = reference.BodyIndexFrameReference.AcquireFrame();
if (colorFrame != null && depthFrame != null && bodyIndexFrame != null)
{
// Just one line of code :-)
camera.Source = _backgroundRemovalTool.GreenScreen(colorFrame, depthFrame, bodyIndexFrame);
}
colorFrame.Dispose();
depthFrame.Dispose();
bodyIndexFrame.Dispose();
}
正如你所看到的,整个魔术都依赖于一个BackgroundRemovalTool
类。我们需要了解颜色帧数据、深度帧数据,当然还有身体数据,以便移除背景。
BackgroundRemovalTool
类具有以下数据数组
WriteableBitmap _bitmap
:具有裁剪背景的最终图像ushort[] _depthData
:深度帧的深度值byte[] _bodyData
:有关站在传感器前方的身体的信息byte[] _colorData
:颜色帧的RGB值byte[] _displayPixels
:映射帧的RGB值ColorSpacePoint[] _colorPoints
:我们需要映射的颜色点
它还使用图像源(WriteableBitmap
)来创建最终的位图图像。CoordinateMapper
作为参数从连接的Kinect传感器传递。
让我们进入GreenScreen
方法。首先,我们需要获取每个帧的尺寸(记住,帧具有不同的宽度和高度)
// Color frame (1920x1080)
int colorWidth = colorFrame.FrameDescription.Width;
int colorHeight = colorFrame.FrameDescription.Height;
// Depth frame (512x424)
int depthWidth = depthFrame.FrameDescription.Width;
int depthHeight = depthFrame.FrameDescription.Height;
// Body index frame (512x424)
int bodyIndexWidth = bodyIndexFrame.FrameDescription.Width;
int bodyIndexHeight = bodyIndexFrame.FrameDescription.Height;
然后,我们需要初始化数组。初始化只发生一次,以避免每次有新帧时都分配内存。
if (_bitmap == null)
{
_depthData = new ushort[depthWidth * depthHeight];
_bodyData = new byte[depthWidth * depthHeight];
_colorData = new byte[colorWidth * colorHeight * BYTES_PER_PIXEL];
_displayPixels = new byte[depthWidth * depthHeight * BYTES_PER_PIXEL];
_colorPoints = new ColorSpacePoint[depthWidth * depthHeight];
_bitmap = new WriteableBitmap(depthWidth, depthHeight, DPI, DPI, FORMAT, null);
}
我们现在需要用新的帧数据填充数组。在此之前,我们检查数组长度是否与我们之前找到的尺寸相对应
if (((depthWidth * depthHeight) == _depthData.Length) &&
((colorWidth * colorHeight * BYTES_PER_PIXEL) == _colorData.Length) &&
((bodyIndexWidth * bodyIndexHeight) == _bodyData.Length))
{
// Update the depth data.
depthFrame.CopyFrameDataToArray(_depthData);
// Update the color data.
if (colorFrame.RawColorImageFormat == ColorImageFormat.Bgra)
{
colorFrame.CopyRawFrameDataToArray(_colorData);
}
else
{
colorFrame.CopyConvertedFrameDataToArray(_colorData, ColorImageFormat.Bgra);
}
// Update the body index data.
bodyIndexFrame.CopyFrameDataToArray(_bodyData);
// Do the coordinate mapping here...
}
现在是时候使用坐标映射器了。坐标映射器会将深度值映射到_colorPoints
数组
_coordinateMapper.MapDepthFrameToColorSpace(_depthData, _colorPoints);
就是这样!映射已完成。我们需要做的是指定哪些像素属于人体,并将它们添加到_displayPixels
数组中。因此,我们循环遍历深度值并相应地更新_displayPixels
数组。
for (int y = 0; y < depthHeight; ++y)
{
for (int x = 0; x < depthWidth; ++x)
{
int depthIndex = (y * depthWidth) + x;
byte player = _bodyData[depthIndex];
// Check whether this pixel belong to a human!!!
if (player != 0xff)
{
ColorSpacePoint colorPoint = _colorPoints[depthIndex];
int colorX = (int)Math.Floor(colorPoint.X + 0.5);
int colorY = (int)Math.Floor(colorPoint.Y + 0.5);
if ((colorX >= 0) && (colorX < colorWidth)
&& (colorY >= 0) && (colorY < colorHeight))
{
int colorIndex = ((colorY * colorWidth) + colorX) * BYTES_PER_PIXEL;
int displayIndex = depthIndex * BYTES_PER_PIXEL;
_displayPixels[displayIndex + 0] = _colorData[colorIndex];
_displayPixels[displayIndex + 1] = _colorData[colorIndex + 1];
_displayPixels[displayIndex + 2] = _colorData[colorIndex + 2];
_displayPixels[displayIndex + 3] = 0xff;
}
}
}
}
这将生成一个位图,其中背景为透明像素,人体为彩色像素。最后,这是WriteableBitmap
的更新方式
// Just some Windows bitmap handling...
_bitmap.Lock();
Marshal.Copy(_displayPixels, 0, _bitmap.BackBuffer, _displayPixels.Length);
_bitmap.AddDirtyRect(new Int32Rect(0, 0, depthWidth, depthHeight));
_bitmap.Unlock();
回到XAML代码,你可以更改Grid(或任何其他)元素在Image元素后面的背景,并选择你想要的背景。例如,此代码会生成以下图像
<Grid>
<Grid.Background>
<SolidColorBrush Color="Green" />
</Grid.Background>
<Image Name="camera" />
</Grid>
而此代码会生成一个足球场背景
<Grid>
<Grid.Background>
<ImageBrush ImageSource="/Soccer.jpg" />
</Grid.Background>
<Image Name="camera" />
</Grid>
喜欢的话,请享受并分享!
附言:Vitruvius
BackgroundRemovalTool
是Vitruvius的一部分,Vitruvius是一个开源库,将加速你的Kinect项目的开发。Vitruvius支持版本1和版本2的传感器,因此你可以将其用于任何类型的Kinect项目。下载并试用一下。