65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2014 年 4 月 11 日

CPOL

4分钟阅读

viewsIcon

36781

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

在过去的几天里,我收到了很多关于Kinect颜色到深度像素映射的请求。正如你可能已经知道的,Kinect流并没有正确对齐。RGB和深度相机具有不同的分辨率,并且它们的视角略有偏移。因此,越来越多的人(无论是在博客评论中还是通过电子邮件)向我询问如何正确对齐颜色和深度流。他们最想构建的最常见的应用程序是一个很酷的绿幕效果,就像下面的视频一样。

视频

在YouTube上观看

正如你所看到的,漂亮的女孩被Kinect传感器追踪,并且背景完全被移除。我可以将背景替换为纯色、渐变填充,甚至是随机图像!

很棒,对吧?所以,我创建了一个简单的项目,将玩家的深度值映射到相应的颜色像素。这样,我可以移除背景并将其替换为其他内容。源代码托管在GitHub上,作为一个单独的项目。它也是Vitruvius的一部分。

阅读教程,了解Kinect坐标映射的工作原理,并自己创建应用程序。

要求

背景移除的工作原理

当我们提到“背景移除”时,我们需要保留构成用户的像素,并移除不属于用户的任何其他内容。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>

Kinect 2 background removal (solid color background)

而此代码会生成一个足球场背景

<Grid>
    <Grid.Background>
        <ImageBrush ImageSource="/Soccer.jpg" />
    </Grid.Background>
    <Image Name="camera" />
</Grid>

Kinect 2 background removal (image background)

喜欢的话,请享受并分享!

查看完整的源代码.

附言:Vitruvius

BackgroundRemovalToolVitruvius的一部分,Vitruvius是一个开源库,将加速你的Kinect项目的开发。Vitruvius支持版本1和版本2的传感器,因此你可以将其用于任何类型的Kinect项目。下载并试用一下

文章使用Kinect 2进行背景移除(绿幕效果)首先出现在Vangos Pterneas上。

使用Kinect 2进行背景移除(绿幕效果) - CodeProject - 代码之家
© . All rights reserved.