我最喜欢的谷歌相片运用程序的一个小功用是它的彩色弹出作用。色彩弹出(又叫色彩飞溅)作用使主体(通常是一个人)从图画的其余部分中锋芒毕露。主题仍然是彩色的,但布景是灰度的。在大多数情况下,这给人一种愉快的感觉。
尽管这个功用的作用非常好,但Google Photos只对一些它认为简略检测到人类的相片运用这个作用。这限制了它的潜力,并且不允许用户手动挑选图画来运用此作用。这让我思考,有没有什么办法能够达到相似的作用,是我挑选指定的图画?
大多数运用程序不供给自动化解决方案。它需求用户手动在图画上制作这种作用,这既耗时又简略出错。我们能做得更好吗?像谷歌相片这样聪明的东西?是的!
怎么手动完成此作用,我发现以下两个主要过程:
- 在图画中的人周围创立一个遮罩(又叫切割)。
- 运用蒙版来保存人物的色彩,同时使布景灰度化。
从图画中切割人物
这是这个过程中最重要的一步。一个好的成果在很大程度上取决于切割掩码的创立有多好。这一步需求一些机器学习,因为它已经被证明在这样情况下工作得很好。
从头开端构建和训练机器学习模型会花费太多时刻,快速查找后,我找到了BodyPix,这是一个用于人物切割和姿势检测的Tensorflow.js模型。
Tensorflow.js的BodyPix模型:
tfjs-models/body-pix at master tensorflow/tfjs-models (github.com)
正如您所看到的,它能够很好地检测图画中的一个人(包含多个人),并且在浏览器上运行相对较快。彩虹色的区域是我们需求的切割图。
让我们用Tensorflow.js和BodyPix CDN脚本设置一个基本的HTML文件。
<html>
<head>
<title>Color Pop using Tensorflow.js and BodyPix</title>
</head>
<body>
<!-- Canvas for input and output -->
<canvas></canvas>
<!-- Load TensorFlow.js -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.2"></script>
<!-- Load BodyPix -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/body-pix@2.0"></script>
<!-- Color Pop code-->
<script src="colorpop.js"></script>
</body>
</html>
在画布中加载图画
在切割之前,了解怎么在JavaScript中操作图画的像素数据是很重要的。一个简略的办法是运用HTML Canvas。Canvas使它易于读取和操作图画的像素数据,一旦加载。同时,它也兼容BodyPix,双赢!
function loadImage(src) {
const img = new Image();
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
// Load the image on canvas
img.addEventListener('load', () => {
// Set canvas width, height same as image
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
// TODO: Implement pop()
pop();
});
img.src = src;
}
加载BodyPix模型
BodyPix的README很好地解释了怎么运用模型。加载模型的一个重要部分是您运用的architecture
。ResNet更精确但速度更慢,而MobileNet精确性较低但速度更快。在构建和测验这种作用时,我将运用MobileNet。稍后我将切换到ResNet并比较成果。
async function pop() {
// Loading the model
const net = await bodyPix.load({
architecture: 'MobileNetV1',
outputStride: 16,
multiplier: 0.75,
quantBytes: 2
});
}
履行切割
BodyPix有多种功用来切割图画。有些合适身体部位切割,有些合适单人/多人切割。一切这些都在它们的README中有详细的解释。segmentPerson()
,它在一个单独的地图中为图画中的每个人创立一个切割地图。并且,它比其他办法相对更快。
segmentPerson()
接受一个Canvas元素作为输入图画,以及一些配置设置。internalResolution
setting指定切割前调整输入图画大小的因子。我将运用full
这个设置,因为我想要明晰的切割地图。
async function pop() {
// Loading the model
const net = await bodyPix.load({
architecture: 'MobileNetV1',
outputStride: 16,
multiplier: 0.75,
quantBytes: 2
});
// Segmentation
const canvas = document.querySelector('canvas');
const { data:map } = await net.segmentPerson(canvas, {
internalResolution: 'full',
});
}
切割后的成果是一个目标(如下所示)。成果目标的主要部分是data
,它是一个Uint8Array
,将切割映射表明为一个数字数组
{
width: 640,
height: 480,
data: Uint8Array(307200) [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, …],
allPoses: [{"score": 0.4, "keypoints": […]}, …]
}
制造布景灰度
预备好切割数据后,下一节运用切割完成色彩弹出,以使布景灰度化并保存图画中人物的色彩。为此,需求对图画进行像素级操作,而这正是canvas元素发挥作用的当地。getImageData()
函数返回ImageData
,其中包含RGBA格局的每个像素的色彩。
async function pop() {
// ... previous code
// Extracting image data
const ctx = canvas.getContext('2d');
const { data:imgData } = ctx.getImageData(0, 0, canvas.width, canvas.height);
}
运用作用
一切的食材都预备好了。让我们从创立新的图画数据开端。createImageData()
创立一个新的ImageData。
接下来,我们迭代映射中的像素,其中每个元素为1或0。
- 1表明在该像素处检测到一个人。
- 0表明在该像素处没有检测到人。
为了使代码更具可读性,我运用解构将色彩数据提取到r
g
b
a
变量中。
最终,基于切割图值(0或1),能够将灰度或实践RGBA色彩分配给新的图画数据。
每个像素处理后,运用putImageData()
函数将新的图画数据制作回画布上。
async function pop() {
// ... previous code
// Creating new image data
const newImg = ctx.createImageData(canvas.width, canvas.height);
const newImgData = newImg.data;
// Apply the effect
for(let i=0; i<map.length; i++) {
// Extract data into r, g, b, a from imgData
const [r, g, b, a] = [
imgData[i*4],
imgData[i*4+1],
imgData[i*4+2],
imgData[i*4+3]
];
// Calculate the gray color
const gray = ((0.3 * r) + (0.59 * g) + (0.11 * b));
// Set new RGB color to gray if map value is not 1
// for the current pixel in iteration
[
newImgData[i*4],
newImgData[i*4+1],
newImgData[i*4+2],
newImgData[i*4+3]
] = !map[i] ? [gray, gray, gray, 255] : [r, g, b, a];
}
// Draw the new image back to canvas
ctx.putImageData(newImg, 0, 0);
}
能够看到具有色彩弹出作用的最终图画运用于原始图画。好耶!
探究其他架构和设置
我对ResNet和MobileNet架构进行了一些测验。在一切示例图画中,图画的一个尺寸(宽度或高度)的大小为1080px。注意,切割的内部分辨率设置为full
。
在我的测验中,我在加载BodyPix模型时运用了以下设置。
// MobileNet architecture
const net = await bodyPix.load({
architecture: 'MobileNetV1',
outputStride: 16,
quantBytes: 4,
});
// ResNet architecture
const net = await bodyPix.load({
architecture: 'ResNet50',
outputStride: 16,
quantBytes: 2,
});
测验1 -单人
在这里,两个模特都发现了图片中的女孩。与MobileNet相比,ResNet对图画进行了更好的切割。
测验2 -多人
这有点扎手,因为它有许多人以不同的姿势和道具环绕图画。ResNet再次精确地切割了图画中的一切人。MobileNet也很挨近。
两者都不正确地切割了垫子的一部分。
测验3 -面朝后
另一个扎手的问题是,相片中的女孩面朝后。老实说,我本来就希望对图画中的女孩进行不精确的检测,但ResNet和MobileNet在这方面都没有问题。
测验的结论
从测验中能够清楚地看出,ResNet比MobileNet履行更好的切割,但花费的时刻更长。这两种办法都能很好地检测同一图画中的多个人,但有时因为衣服的原因而无法精确切割。因为BodyPix与浏览器(或Node.js)中的Tensorflow.js一同运行,因此在正确设置下运用时,它的履行速度敏捷。
这就是我怎么能够创立受Google Photos启发的Color Pop作用。总而言之,BodyPix是一个很好的人物切割模型。我很想在我未来的一些项目中运用这个和Tensorflow.js。你能够在这里找到源代码和实时工作版本:glitch.com/~color-pop-…