运用BodyPix和TensorFlow.js完成Color Pop作用

我最喜欢的谷歌相片运用程序的一个小功用是它的彩色弹出作用。色彩弹出(又叫色彩飞溅)作用使主体(通常是一个人)从图画的其余部分中锋芒毕露。主题仍然是彩色的,但布景是灰度的。在大多数情况下,这给人一种愉快的感觉。

运用BodyPix和TensorFlow.js完成Color Pop作用

尽管这个功用的作用非常好,但Google Photos只对一些它认为简略检测到人类的相片运用这个作用。这限制了它的潜力,并且不允许用户手动挑选图画来运用此作用。这让我思考,有没有什么办法能够达到相似的作用,是我挑选指定的图画?

大多数运用程序不供给自动化解决方案。它需求用户手动在图画上制作这种作用,这既耗时又简略出错。我们能做得更好吗?像谷歌相片这样聪明的东西?是的!

怎么手动完成此作用,我发现以下两个主要过程:

  • 在图画中的人周围创立一个遮罩(又叫切割)。
  • 运用蒙版来保存人物的色彩,同时使布景灰度化。

从图画中切割人物

这是这个过程中最重要的一步。一个好的成果在很大程度上取决于切割掩码的创立有多好。这一步需求一些机器学习,因为它已经被证明在这样情况下工作得很好。

从头开端构建和训练机器学习模型会花费太多时刻,快速查找后,我找到了BodyPix,这是一个用于人物切割和姿势检测的Tensorflow.js模型。

Tensorflow.js的BodyPix模型:

tfjs-models/body-pix at master tensorflow/tfjs-models (github.com)

运用BodyPix和TensorFlow.js完成Color Pop作用

正如您所看到的,它能够很好地检测图画中的一个人(包含多个人),并且在浏览器上运行相对较快。彩虹色的区域是我们需求的切割图。

让我们用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元素作为输入图画,以及一些配置设置。internalResolutionsetting指定切割前调整输入图画大小的因子。我将运用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表明在该像素处没有检测到人。

为了使代码更具可读性,我运用解构将色彩数据提取到rgba变量中。

最终,基于切割图值(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对图画进行了更好的切割。

运用BodyPix和TensorFlow.js完成Color Pop作用

测验2 -多人

这有点扎手,因为它有许多人以不同的姿势和道具环绕图画。ResNet再次精确地切割了图画中的一切人。MobileNet也很挨近。

两者都不正确地切割了垫子的一部分。

运用BodyPix和TensorFlow.js完成Color Pop作用

测验3 -面朝后

另一个扎手的问题是,相片中的女孩面朝后。老实说,我本来就希望对图画中的女孩进行不精确的检测,但ResNet和MobileNet在这方面都没有问题。

运用BodyPix和TensorFlow.js完成Color Pop作用

测验的结论

从测验中能够清楚地看出,ResNet比MobileNet履行更好的切割,但花费的时刻更长。这两种办法都能很好地检测同一图画中的多个人,但有时因为衣服的原因而无法精确切割。因为BodyPix与浏览器(或Node.js)中的Tensorflow.js一同运行,因此在正确设置下运用时,它的履行速度敏捷。

这就是我怎么能够创立受Google Photos启发的Color Pop作用。总而言之,BodyPix是一个很好的人物切割模型。我很想在我未来的一些项目中运用这个和Tensorflow.js。你能够在这里找到源代码和实时工作版本:glitch.com/~color-pop-…

相关代码: fengjutian/color-pop (github.com)

原文:medium.com/towards-dat…