记一次使用机器学习进行前端抠像以及叠加背景图

前言

感觉好久没写帖子了,那篇 10 月的新番推荐现在还躺在我的草稿列表里面😂

双十一花了点钱搞了台主机,本以为现在终于可以不用背电脑上下班了,没想到来了疫情,不来回背等下工作就要没了…

刚好最近公司要我搞一个前端抠像的 demo ,给了我一些资料,觉得挺有意思,所以来写写

至于标题的机器学习,我只是一个合格的 api 调用,切图工程师,不写机器学习怎么能引诱你进来看?

正文

这次我们使用的是谷歌的一个机器学习库 MediaPipe Selfie Segmentation

官方文档地址 MediaPipe Selfie Segmentation

当然,怎么实现我肯定是不懂的,但是提供了 api ,我们可以为此写出一个差不多的 demo ,然后去给客户演示“赚”资金

翻到 javascript-solution-api 标题,就可以看到 js 下的 api 了,官方还很贴心的给了一个 demo

当然,这里它额外用了一些其他的库 drawing_utils camera_utils 以及 control_utils

看起来都是一些工具库,但在我们的 demo 中不会用到这些库

这个 demo 主要流程为

  • 获取摄像头的流
  • 抠像叠背景处理
  • 导出一个处理过后的流

首先我们要安装依赖,执行 pnpm add @mediapipe/selfie_segmentation

然后我们要到 node_modules 下找到这个包,把除了 selfie_segmentation.js 文件之外的其他文件放到 public 文件夹下(这里我使用的是 vite 创建的 react 项目)

放到 public 文件夹中

然后我们用下面的代码就可以初始化一个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { SelfieSegmentation } from "@mediapipe/selfie_segmentation";

const selfieSegmentation = new SelfieSegmentation({
// 这里的意思就是需要的文件从哪里获取
// 由于我们放到 public 下了,直接绝对路径 / 即可
locateFile: (file) => {
return `/${file}`;
},
});

// 设置为 landscape 模型
selfieSegmentation.setOptions({
modelSelection: 1,
});

这里的 modelSelection 可填的值有两个,分为为 01

其中 0 代表 general 模型, 1 代表 landscape 模型,默认是 0

我们只需要知道这两个模型都是基于 MobileNetV3 模型而来,并且 landscape 模型比 general 模型快即可

文档中的相关解释

接着我们随手写一个 UI ,主要有设备的选择,以及把流喂给 selfieSegmentation 产生结果画到 canvas 上,以及 canvasmediaStream 再喂给 video 标签

代码我就不贴了,搞得版面不是很好看,丢仓库里了,地址 Dedicatus546 / mediaPipe-selfie-segmentation-demo

跑起来之后我们就能看到如下界面

傻瓜式操作,选择设备,就会自动开启抠像了,选择图片,那么图片就会自动应用到抠像中,效果如下

看起来效果还是挺不错的,但是对手的支持就不是很理想了,在我这台 i7-7700HQ 上吃的性能还是挺多的,风扇一直在叫

这东西我也看了网上的其他文章,大部分到最后都是要服务端来抠像的,一方面兼容性有保证,一方面不会因为性能不足而导致抠像效果不佳

坏处就是需要更多的服务器资源了,也就意味着更多的钱钱

这里讲一下几个有趣的点

首先是 canvasmediaStream ,刚开始其实我也不知道要怎么办,因为官方的文档其实只是画到 canvas 上而已

而我司的项目是要通过 webRTC 上传 mediaStream 的,这就很操蛋

然后我就找啊找,终于找到了一个实验性质的 api HTMLCanvasElement.captureStream

调用这个 api ,我们就能直接拿到一个 mediaStream 流,当然,这个 api 目前兼容性较低

而且我看现在有提案好像是说要为每一个 HTMLElement 都添加一个 captureStream 方法,来生成视频流,这就非常的有趣好吧

另一个是关于 canvas2d 上下文下的 globalCompositeOperation 属性,即 CanvasRenderingContext2D.globalCompositeOperation

这个属性是能够在 canvas 上正确画出抠像的核心,MDN 文档地址:CanvasRenderingContext2D.globalCompositeOperation

在默认情况下,这个值为 source-over ,即哪里都可以画

我们需要把它设置为 source-in ,即新画的的东西只能画在画布上已绘制的区域,也就是两者取交集

在经过抠像之后,会生成两个图片,一个是当前帧的图片,一个是当前帧对应抠像的一个遮罩

这时我们先画遮罩,然后把 globalCompositeOperation 设为 source-in ,接下来画当前帧,那么就只能画在遮罩内了,而遮罩外就是透明的,从而完成抠像

demo 中,我使用了两个 canvas 来完成抠像和背景的叠加,我也尝试过使用单个 canvas ,但最终都失败了

我的思路是这样的,先绘制遮罩,然后设为 source-in ,再绘制视频帧,那么现在抠像就完成了,接着设置为 source-out ,绘制背景,此时我个人是认为背景是能绘制上去的

事实上也是如此,但是此时的抠像部分就变成透明了,这应该是 globalCompositeOperation 的特性,在 source-insource-out 下,非绘制的部分都会变得透明

所以最终就使用了两个 canvas ,其中使用一个临时的 canvas 完成抠像,在主 canvas 上绘制背景,再绘制抠像的 canvas ,这样就可以实现叠加背景的效果

后记

尝试使用了 worker 来接管某些过程,但是都失败了…