[更新] BodyPix:使用 TensorFlow.js 在浏览器中实现实时人体分割
2019 年 11 月 18 日
更新 (2019 年 11 月 18 日)

BodyPix 2.0 已发布, 它支持多人,并且准确率更高(基于 ResNet50),并提供新的 API、权重量化和对不同图像大小的支持。 立即在浏览器中试用 新的演示 ,并访问我们的 GitHub 仓库
BodyPix 2.0 支持多人,并且准确率更高(基于 ResNet50)

编辑注:原始文章发布于 2019 年 2 月 15 日,内容如下。

作者: Dan Oved,纽约大学 ITP 的研究生和研究员。 Tyler Zhu,Google Research 工程师。编辑:Irene Alvarado,Google 创意实验室的创意技术专家。

我们很高兴宣布发布 BodyPix,这是一个开源机器学习模型,可使用 TensorFlow.js 在浏览器中实现人体和身体部位的分割。使用默认设置,它可以在 2018 年 15 英寸 MacBook Pro 上以 25 fps 的速度估算和渲染人体和身体部位的分割,在 iPhone X 上以 21 fps 的速度估算和渲染。

什么是人体分割?在计算机视觉中,图像分割指的是将图像中的像素分组到语义区域的技术,通常用于定位对象和边界。BodyPix 模型经过训练可以针对一个人和 24 个身体部位(如左手、前右小腿或后躯干)进行分割。换句话说,BodyPix 可以将图像的像素分为两类:1)代表人的像素和 2)代表背景的像素。它还可以进一步将代表人的像素分为 24 个身体部位中的任何一个。

如果您 在此处 试用一下实时演示,可能就更清楚了。

人体分割有什么用?这项技术可以作为多种应用的工具,从 增强现实 到 照片编辑 再到图像或视频的 艺术效果。随您发挥创意!去年我们发布了 PoseNet (第一个允许在浏览器中使用简单网络摄像头进行人体估计(Kinect 的功能)的模型),人们为该技术想出了各种 广泛 多样 用例。我们希望 BodyPix 也能激发类似的创意实验。

为什么要在浏览器中执行此操作?与 PoseNet 的情况类似,实时人体分割之前只能通过专用硬件或难以安装且系统要求很高的软件才能实现。相反,BodyPix 和 PoseNet 都无需安装,只需几行代码。您无需使用任何特殊的镜头即可使用这些模型 — 它们适用于任何基本的网络摄像头或移动摄像头。最后,用户只需打开一个 URL 即可访问这些应用程序。由于所有计算都在 设备上 完成,因此数据保持私密。由于所有这些原因,我们认为 BodyPix 作为艺术家创意程序员编程新手的工具易于使用

在深入探讨 BodyPix 之前,我们要感谢 Google Research 的 Tyler Zhu,他负责开发该模型,并致力于人体姿态估计 [12],还有 Nikhil Thorat 和 Daniel Smilkov,他们来自 Google Brain 团队,负责开发 TensorFlow.js 库,以及 Daniel Shiffman 和 Google 教师研究奖 ,他们帮助资助了 Dan Oved 在此项目上的工作,以及 Google Research 的 Per Karlsson ,他负责渲染模拟数据集。

BodyPix 入门

让我们深入了解一下如何使用此模型的技术细节。BodyPix 可用于将图像分割为属于和不属于人的像素。这些属于人的像素可以进一步分为24 个身体部位。重要的是,该模型适用于单人,因此请确保您的输入数据中不包含多人。

第 1 部分:导入 TensorFlow.js 和 BodyPix 库

让我们了解如何设置 BodyPix 项目的基础知识。

可以使用以下命令安装库:npm install @tensorflow-models/body-pix,然后使用 es6 模块导入。
import * as bodyPix from '@tensorflow-models/body-pix'; 

async function loadAndUseBodyPix() {
   const net = await bodyPix.load();
   // BodyPix model loaded
}
或者,可以通过页面中的包导入,无需安装。
<html>   
   <body> 
        <!-- Load TensorFlow.js --> 
        <script src="https://cdn.jsdelivr.net.cn/npm/@tensorflow/[email protected]"></script> 
        <!-- Load BodyPix -->
        <script src="https://cdn.jsdelivr.net.cn/npm/@tensorflow-models/body-pix"></script> 
            bodypix.load().then(function(net) {
                // BodyPix model loaded
            });
        </script>
    </body>
</html>

第 2a 部分:人体分割

人体分割算法应用于图像的示例。图像来源:“Microsoft Coco:上下文中的通用对象数据集”,http://cocodataset.org
在基本层面上,人体分割将图像分割为属于人和不属于人的像素。在内部,图像经过模型处理后,会转换为一个二维图像,每个像素的值在 0 到 1 之间,代表该像素中存在人的概率。名为“分割阈值”的值代表像素得分必须具有的最小值,才能被视为属于人。使用分割阈值,这些 0-1 浮点值将变为二进制 0 或 1(例如,如果阈值为 0.5,则所有大于 0.5 的值将转换为 1,所有小于 0.5 的值将转换为 0)。

我们调用 API 方法 estimatePersonSegmentation 来对图像或视频执行人体分割;以下简短代码块展示了如何使用它
const imageElement = document.getElementById('image');

// load the BodyPix model from a checkpoint
const net = await bodyPix.load();

// arguments for estimating person segmentation.
const outputStride = 16;
const segmentationThreshold = 0.5;

const personSegmentation = await net.estimatePersonSegmentation(imageElement, outputStride, segmentationThreshold);
人体分割输出示例如下
{
  width: 640,
  height: 480,
  data: Uint8Array(307200) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,    0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, …]
}
// the data array contains 307200 values, one for each pixel of the 640x480 image that was passed to the function.
有关此方法及其参数的完整说明,请参阅 github 自述文件

绘制人体分割输出

BodyPix 位于浏览器中的另一个优势是我们可以访问 Web API,例如 Canvas 合成。我们使用它来使用 BodyPix 的输出遮盖或替换图像的某些部分。我们提供了包装此功能的实用程序函数,让您快速上手

toMaskImageData 获取估算人体分割的输出,并生成一个透明图像,根据参数 maskBackground,该图像将在存在人和背景的地方不透明。然后,可以使用方法 drawMask 将该图像作为遮罩绘制在原始图像的顶部
const imageElement = document.getElementById('image');

const net = await bodyPix.load();
const segmentation = await net.estimatePersonSegmentation(imageElement);

const maskBackground = true;
// Convert the personSegmentation into a mask to darken the background.
const backgroundDarkeningMask = bodyPix.toMaskImageData(personSegmentation, maskBackground);

const opacity = 0.7;

const canvas = document.getElementById('canvas');
// draw the mask onto the image on a canvas.  With opacity set to 0.7 this will darken the background.
bodyPix.drawMask(
    canvas, imageElement, backgroundDarkeningMask, opacity);
drawMask 将图像绘制在画布上,并使用指定的不透明度在上面绘制包含遮罩的 ImageData。

使用上述第一个图像上 estimatePersonSegmentation 的输出, toMaskImageData 将生成一个 ImageData ,如果 maskBackground 设置为 true(默认情况下),它将看起来像上面的第二个图像,或者如果 maskBackground 设置为 false,它将看起来像第三个图像。

然后,可以使用 drawMask 将该遮罩绘制在图像的顶部。

第 2b 部分:身体部位分割

身体部位分割算法应用于图像的示例。图像来源:“Microsoft Coco:上下文中的通用对象数据集”,https://cocodataset.org
除了简单的“人/非人”分类,BodyPix 还可以将图像分割为代表 24 个身体部位 的像素。图像经过模型处理后,会转换为一个二维图像,每个像素的值在 0 到 23 之间,代表该像素属于 24 个身体部位中的哪一个。位于身体外部的像素的值为 -1。

我们调用 API 方法 estimatePartSegmentation 来对图像或视频执行身体部位分割;以下简短代码块展示了如何使用它
const imageElement = document.getElementById('image');

// load the BodyPix model from a checkpoint
const net = await bodyPix.load();

// arguments for estimating body part segmentation.
const outputStride = 16;
const segmentationThreshold = 0.5;

// load the person segmentation model from a checkpoint
const net = await bodyPix.load();

const partSegmentation = await net.estimatePartSegmentation(imageElement, outputStride, segmentationThreshold);
身体部位分割输出示例如下
{
  width: 680,
  height: 480,
  data: Int32Array(307200) [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 1, 1, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 15, 15, 15, 15, 16, 16, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 23, 23, 23, 22, 22, -1, -1, -1, -1,  …]
}
// the ‘data’ array contains 307200 values, one for each pixel of the 640x480 image that was passed to the function.

有关此方法及其参数的完整说明,请参阅 github 自述文件

绘制身体部位分割输出

使用 toColoredPartImageData ,以及一个由部位 ID 索引的颜色数组,可以生成一个图像,每个像素的对应颜色代表相应的部位。然后,可以使用方法 drawMask 将该图像作为遮罩绘制在原始图像的顶部,并绘制在画布上。
const imageElement = document.getElementById('person');

const net = await bodyPix.load();
const partSegmentation = await net.estimatePartSegmentation(imageElement);

const rainbow = [
  [110, 64, 170], [106, 72, 183], [100, 81, 196], [92, 91, 206],
  [84, 101, 214], [75, 113, 221], [66, 125, 224], [56, 138, 226],
  [48, 150, 224], [40, 163, 220], [33, 176, 214], [29, 188, 205],
  [26, 199, 194], [26, 210, 182], [28, 219, 169], [33, 227, 155],
  [41, 234, 141], [51, 240, 128], [64, 243, 116], [79, 246, 105],
  [96, 247, 97],  [115, 246, 91], [134, 245, 88], [155, 243, 88]
];

// the colored part image is an rgb image with a corresponding color from the rainbow colors for each part at each pixel.
const coloredPartImage = bodyPix.toColoredPartImageData(partSegmentation, rainbow);
const opacity = 0.7;

const canvas = document.getElementById('canvas');
// draw the colored part image on top of the original image onto a canvas.  The colored part image will be drawn semi-transparent, with an opacity of 0.7, allowing for the original image to be visible under.
bodyPix.drawMask(
    canvas, imageElement, coloredPartImageData, opacity);

使用上面第一张图像中的 estimatePartSegmentation 输出,以及提供的颜色比例,toColoredPartImageData 将生成一个看起来像上面第二张图像的 ImageData。彩色部分图像可以使用 drawMask 在原始图像上绘制,透明度为 0.7;结果显示在上面第三张图像中。
有关这些方法的详细说明以及如何使用它们的详细信息,请参阅 GitHub 自述文件

如何使 BodyPix 运行更快或更准确

模型大小和输出步幅对性能和准确性的影响最大——它们可以设置为使 BodyPix 运行得更快但不太准确,或者更准确但更慢的值。
  • 模型大小在加载模型时使用参数 mobileNetMultiplier 设置,可以取值 0.25、0.50、0.75 或 1.00。该值对应于 MobileNet 架构和检查点。该值越大,层的大小就越大,模型越准确,但速度越慢。将其设置为较小的值以提高速度,但会降低准确性。
  • 输出步幅是在运行分割时设置的参数,可以取值 8、16 或 32。在较高层面上,它会影响姿态估计的准确性和速度。输出步幅的值越低,准确性越高,但速度越慢,输出步幅的值越高,预测时间越快,但准确性越低。
此外,原始图像的大小也会影响性能。由于在估计分割后将结果缩放为原始图像大小,原始图像越大,缩放和绘制结果时需要进行的计算就越多。为了加快速度,请尝试在将图像馈送到 API 之前将其缩小。

如果您想开始尝试使用 BodyPix,这里是一个很好的切入点。对于那些好奇我们如何创建模型的人来说,下一节将深入介绍技术细节。

BodyPix 的制作

BodyPix 使用卷积神经网络算法。我们训练了 ResNet 模型和 MobileNet 模型。虽然基于 ResNet 的模型更准确,但这篇博文主要介绍 MobileNet 模型,因为它能够在移动设备和标准消费者计算机上高效运行,并且已经开源。对于 MobileNet 模型,传统的分类模型的最后一个池化层和全连接层被 1x1 卷积层替换,以便预测密集的 2D 分割图。下面是 MobileNet 用于处理输入图像时发生的情况的示意图。

示例展示了 MobileNet 从输入图像到输出层逐层激活(为演示目的省略了特征图下采样)。

人物分割

BodyPix 的核心是一个执行人物分割的算法——换句话说,对输入图像的每个像素进行二元决策,以估计该像素是否属于人物。让我们来看看它是如何工作的。
显示分割人物区域任务的图像可以被表述为每个像素的二元决策任务。1 表示像素属于人物区域,0 表示像素不属于人物区域(分割分辨率已降低,用于演示目的)。

图像被馈送到 MobileNet 网络,使用 sigmoid 激活函数将输出转换为 0 到 1 之间的数值,可以解释为像素属于人物的概率。分割阈值(例如 0.5)通过确定像素得分必须达到的最小值才能被认为是人物的一部分,将其转换为二元分割。

应用于图像的人物分割算法的示例数据表示。从左到右:输入图像、网络的分割预测(经过 sigmoid 处理后),以及阈值处理后的二元分割。插图由 Ashley Jane Lewis 绘制。图像来源:“Microsoft Coco:上下文中的常见对象数据集”,https://cocodataset.org

人体部位分割

为了估计人体部位分割,我们使用相同的 MobileNet 表示,但这次通过预测另外 24 个通道输出张量 P 来重复上述过程,其中24 是人体部位的数量。每个通道编码人体部位存在的概率。
一个示例展示了分割人物身体部位区域的任务可以被表述为多通道每个像素的二元决策任务。对于每个身体部位通道,1 表示像素属于身体部位区域,0 表示像素不属于身体部位区域(从左到右:输入图像、右侧脸部通道和左侧脸部通道)。

一个示例展示了 MobileNet 从输入图像到另外 24 个通道的身体部位分割输出的逐层激活(为演示目的省略了特征图下采样)。
一个示例展示了 MobileNet 从输入图像到另外 24 个通道的身体部位分割输出的逐层激活(为演示目的省略了特征图下采样)。

由于对于每个图像位置,输出张量 P 中有 24 个通道,我们需要在 24 个通道中找到最佳部位。在推理过程中,对于输出身体部位张量 P 的每个像素位置(u, v),我们使用以下公式选择具有最高概率的最佳 body_part_id
这将生成一个二维图像(与原始图像大小相同),其中每个像素包含一个整数,指示该像素属于哪个身体部位。人物分割输出用于通过将对应的人物分割输出小于分割阈值的像素值设置为 -1 来从整个图像中裁剪人物。
示例展示了如何将 24 个通道的身体部位分割和人物分割合并成 1 个通道的部位 ID 输出。插图由 Ashley Jane Lewis 绘制。图像来源:“Microsoft Coco:上下文中的常见对象数据集”,https://cocodataset.org
示例展示了在使用上述 argmax 公式组合 24 个通道的身体部位掩码并使用人物分割裁剪人物区域后,最终的 1 个通道的身体部位 ID 输出(每个 body_part_id 由唯一的颜色表示,输出分辨率已降低,用于演示目的)。


使用真实数据和模拟数据进行训练

手动注释大量训练数据以完成将图像中的像素分割成 24 个身体部位区域的任务非常耗时。相反,我们内部使用计算机图形学来渲染具有真实身体部位分割注释的图像。为了训练我们的模型,我们将渲染的图像和真实 COCO 图像(具有 2D 关键点和实例分割注释)混合在一起。使用混合训练策略和多任务损失,我们的 ResNet 模型能够纯粹从模拟注释中学习 24 个身体部位预测能力。最后,我们将教师 ResNet 模型在 COCO 图像上的预测蒸馏到我们的 BodyPix 学生 MobileNet 模型中。
一个示例展示了我们的教师 ResNet 模型是如何在真实图像和计算机图形生成的图像上进行训练的。身体部位真实注释仅存在于计算机图形生成的图像上。

一个示例展示了教师 ResNet 模型的预测如何被蒸馏成一个基于 MobileNet 的 BodyPix 学生模型。

除了 PoseNet 之外,我们认为 BodyPix 将是实现本地设备上、消费者设备上野外 动作捕捉 的又一小步。仍然有一些研究问题尚未完全解决:例如捕捉 3D 身体形状、高频软组织肌肉运动以及详细的服装外观及其变形。尽管展望未来,我们乐观地认为动作捕捉技术将更加易于访问,并在更广泛的应用和行业中发挥作用。

我们仅提供了一些示例和实用程序方法来帮助您开始使用 BodyPix 模型,并希望它能激发您尝试使用该模型。您将创建什么?我们很乐意听到您的想法,所以不要忘记使用 #tensorflowjs、#bodypix 和 #PoweredByTF 分享您的项目。

下一篇文章
[Updated] BodyPix: Real-time Person Segmentation in the Browser with TensorFlow.js

更新(2019 年 11 月 18 日)

BodyPix 2.0 已发布,支持多人,并提高了准确性(基于 ResNet50),还带来了新的 API、权重量化以及对不同图像尺寸的支持。在浏览器中尝试新的演示,并访问我们的 GitHub 仓库
编辑注:原始文章发表于 2019 年 2 月 15 日,内容如下。
作者:Dan Oved,ITP 硕士生和研究员,N…