记一次使用机器学习进行前端抠像以及叠加背景图
前言
感觉好久没写帖子了,那篇 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 | import { SelfieSegmentation } from "@mediapipe/selfie_segmentation"; |
这里的 modelSelection
可填的值有两个,分为为 0
和 1
其中 0
代表 general
模型, 1
代表 landscape
模型,默认是 0
我们只需要知道这两个模型都是基于 MobileNetV3
模型而来,并且 landscape
模型比 general
模型快即可
文档中的相关解释
接着我们随手写一个 UI
,主要有设备的选择,以及把流喂给 selfieSegmentation
产生结果画到 canvas
上,以及 canvas
转 mediaStream
再喂给 video
标签
代码我就不贴了,搞得版面不是很好看,丢仓库里了,地址 Dedicatus546 / mediaPipe-selfie-segmentation-demo
跑起来之后我们就能看到如下界面
傻瓜式操作,选择设备,就会自动开启抠像了,选择图片,那么图片就会自动应用到抠像中,效果如下
看起来效果还是挺不错的,但是对手的支持就不是很理想了,在我这台 i7-7700HQ
上吃的性能还是挺多的,风扇一直在叫
这东西我也看了网上的其他文章,大部分到最后都是要服务端来抠像的,一方面兼容性有保证,一方面不会因为性能不足而导致抠像效果不佳
坏处就是需要更多的服务器资源了,也就意味着更多的钱钱
这里讲一下几个有趣的点
首先是 canvas
转 mediaStream
,刚开始其实我也不知道要怎么办,因为官方的文档其实只是画到 canvas
上而已
而我司的项目是要通过 webRTC
上传 mediaStream
的,这就很操蛋
然后我就找啊找,终于找到了一个实验性质的 api
HTMLCanvasElement.captureStream
调用这个 api
,我们就能直接拿到一个 mediaStream
流,当然,这个 api
目前兼容性较低
而且我看现在有提案好像是说要为每一个 HTMLElement
都添加一个 captureStream
方法,来生成视频流,这就非常的有趣好吧
另一个是关于 canvas
的 2d
上下文下的 globalCompositeOperation
属性,即 CanvasRenderingContext2D.globalCompositeOperation
这个属性是能够在 canvas
上正确画出抠像的核心,MDN
文档地址:CanvasRenderingContext2D.globalCompositeOperation
在默认情况下,这个值为 source-over
,即哪里都可以画
我们需要把它设置为 source-in
,即新画的的东西只能画在画布上已绘制的区域,也就是两者取交集
在经过抠像之后,会生成两个图片,一个是当前帧的图片,一个是当前帧对应抠像的一个遮罩
这时我们先画遮罩,然后把 globalCompositeOperation
设为 source-in
,接下来画当前帧,那么就只能画在遮罩内了,而遮罩外就是透明的,从而完成抠像
在 demo
中,我使用了两个 canvas
来完成抠像和背景的叠加,我也尝试过使用单个 canvas
,但最终都失败了
我的思路是这样的,先绘制遮罩,然后设为 source-in
,再绘制视频帧,那么现在抠像就完成了,接着设置为 source-out
,绘制背景,此时我个人是认为背景是能绘制上去的
事实上也是如此,但是此时的抠像部分就变成透明了,这应该是 globalCompositeOperation
的特性,在 source-in
和 source-out
下,非绘制的部分都会变得透明
所以最终就使用了两个 canvas
,其中使用一个临时的 canvas
完成抠像,在主 canvas
上绘制背景,再绘制抠像的 canvas
,这样就可以实现叠加背景的效果
后记
尝试使用了 worker
来接管某些过程,但是都失败了…