likes
comments
collection
share

使用BodyPix和TensorFlow.js实现Color Pop效果

作者站长头像
站长
· 阅读数 51

使用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元素作为输入图像,以及一些配置设置。 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对图像进行了更好的分割。

使用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…

转载自:https://juejin.cn/post/7352333464241389622
评论
请登录