利用 bodyPix 在 Web 上实现实时摄像头背景模糊 / 背景替换

date
Sep 6, 2020
slug
background-processing-with-bodypix-on-web
status
Published
tags
Web
summary
在实习的时候辛勤耕作于一个基于TRTC(腾讯云提供的WebRTC能力)的Web在线直播间,当时有一项需求是模仿腾讯会议的背景虚化、背景替换功能受限于一开始的直接从本地设备创建音视频流的流程,这个需求没办法实现。做完这个模块功能后的某一天,发现TRTC的WebSDK有更新,可以基于Canvas和Video创建音视频流,想了想,也许说不定可以在这里操作一番
type
Post

背景

在实习的时候辛勤耕作于一个基于TRTC(腾讯云提供的WebRTC能力)的Web在线直播间,当时有一项需求是模仿腾讯会议的背景虚化、背景替换功能(下图)。受限于一开始的直接从本地设备创建音视频流的流程,这个需求没办法实现。
notion image
做完这个模块功能后的某一天,发现TRTC的WebSDK有更新,可以基于Canvas和Video创建音视频流(其实之后看到在MDN上早就有这个接口了),想了想,也许说不定可以在这里操作一番。

想法

HTML5提供了很强大的Canvas可以进行图片的在线绘制。
之前看的比较多的场景是利用画布来生成一些海报,但用来做视频的渲染还是第一次。在社区转了圈发现还蛮多这么干的先例(有些是为了解决移动端不好处理Video的问题,这个感觉也有使用场景),仔细一想,图片只要动的足够快,也就是视频了(高于24FPS就基本看起来不会卡顿)。
虽是这样,心里还是有一点担心,这种方案是否可以达到较好的占用并拥有流畅的视频画面体验,于是乎撸了个简单的视频实时显示的Demo来实验了一下。
<div class="container">
  <div class="item">
    <div class="title">
      原始视频
    </div>
    <div class="contant">
      <video id="oriVideo" width="640" height="360" muted></video>
    </div>
  </div>
  <div class="item">
    <div class="title">
      处理后视频
    </div>
    <div class="contant">
      <canvas id="main-canvas" width="640" height="360"></canvas>
    </div>
  </div>
</div>
index.js 片段
const canvasEltx1 = document.getElementById('main-canvas').getContext('2d');
const oriVideo = document.getElementById('oriVideo');
let i    // 存放interval入口
// 获取本地音视频流,并在Video中播放
await navigator.mediaDevices.getUserMedia({
  video: { width: 640, height: 360, frameRate: 15 }
}).then(stream => {
  oriVideo.srcObject = stream;
  oriVideo.play();
});

oriVideo.addEventListener('play', function () {
  window.setInterval(function () {
    canvasEltx1.drawImage(oriVideo, 0, 0, testVideo.videoWidth, testVideo.videoHeight, 0, 0, 640, 360)
  }, 60);
}, false);
oriVideo.addEventListener('pause', function () { window.clearInterval(i); }, false);
oriVideo.addEventListener('ended', function () { window.learInterval(i); }, false);
实测在60ms进行一次画布的刷新完全没有问题,实现的刷新率大概在16FPS左右,肉眼可见的流畅度和Video组件播放出来的非常相似了。现在只需要将摄像头采集的画面中的人物实时的进行分割,进行分别的模糊处理并在Canvas里渲染出来,就可以实现看起来的「实时视频背景处理」效果。

方案

对于实时的人物图像处理,在Client上会有比较好的库提供这个功能。虽然反观Web上的实现相对应该没有那么高效,但是用js做人物分割和处理,也是值得尝试的。几经发掘,发现谷歌的团队开发的TensorFlow机器学习模型有对应的js版本TensorFlow.js,且非常凑巧的在图像识别方面有人体分割的模型BodyPix和相应的能力提供,齐活了。
对于背景模糊,模型本身有提供一个接口 bodyPix.drawBokehEffect 进行背景虚化的处理,并在github上的readme里有对处理的原理进行介绍(如下图,图源官方),主要的逻辑是先将整个场景虚化,然后在原图中抠出单独的清晰人像,最后将这个抠出来的人像叠加到虚化的背景上即可。
notion image
照着这个想法,核心处理逻辑调一下接口,很快就可以出来了:
notion image
至于说背景替换,本质上和背景虚化是差不多的。只需要在自定义的背景上将人像放上去就可以了。因为模型里没有现成的接口,处理逻辑是按照上面的思路自己写的:
notion image
实现的效果如下:
  • 背景虚化
    • notion image
  • 背景替换
    • notion image

优化

优化的点主要是利用 worker + offScreenCanvas 实现后台渲染(只支持Chrome和Opera)。
现有的逻辑将canvas的图像处理全部都集中在主线程上,当机器资源不足时,页面操作可以感受到明显的卡顿,所以这个Demo的想法也一直没有机会往生产环境上去套。
前段时间在学习worker的时候偶然发现了offScreenCanvas(离屏画布)这个东西,于是乎又想起来这个项目,很明显,这种计算密集型的操作,采取「在Worker里通过操作离屏画布实时反馈回主界面」的方案才是更合理的。
稍微改动了一下代码,主线程主要负责将采集的video控件实时采集的视频画面定时转化为imageBitMap后传递到worker中供处理。这里比较特别的点是将帧图片数据转化为 Transferable接口 所支持的类型后,进行worker与主线程之间的传输,使得主线程不需要进行拷贝构造,即可将帧图片转移给worker使用,提高了处理的效率。
至于离屏画布的运用上,阅读
发现若要建立一个基于现有画布的离屏画布,需要在原画布上调用 transferControlToOffscreen() 接口。在 接口文档 中看到说明,调用的结果是:将原有画布的控制权转移到离屏画布上,返回一个带有原画布控制权的离屏画布,所有在离屏画布所做的操作都会在原画布中呈现。
notion image
通过改用 worker 处理后,虽然运行时风扇还是转,但是至少网页的基本操作都能得到快速的响应了,还是不错的。
最后形成了一个 Demo,相关的源码可以在 github.com/skytt/web-camera-processer 查看。 在 skytt.github.io/web-camera-processer 可以直接进行Demo的演示。

埋坑

  • requestAnimationFrame() or setInterval() ? 目前的Demo是采用setInterval()方法,定时进行视频的采帧的渲染。 之前有在某个离屏画布的使用总结里发现requestAnimationFrame()可以带来更好的性能提升,但是经过实测过程中发现在电脑资源占用较高的时候,Canvas的画面会产生明显的卡顿,但是同样的环境setInterval()却不会。毕竟requestAnimationFrame()的执行策略是由浏览器根据刷新频率自行决定的,这个还是需要再看一下遇到这种情况的时候,这么卡到底是产生了怎样奇妙的化学反应。
  • 页面标签切后台时画面停滞 无论是上述requestAnimationFrame()setInterval()方案,都有个明显的缺点:在Chrome中,标签被切到后台以后,定时器会被冻结,Canvas里的画面就不更新了。这在视频直播间的的场景中,肯定不科学。 浏览器这个机制网上讨论好像不多,但是经过一番搜索以后发现,貌似github上已经有相应的解决方案可以避免标签切后台以后被冷落(传送门)。但是这五年前的代码是否今天仍然可用,作为Demo的程序就不深究了,如果要在生产中用,这个也是个值得关注的问题。

© Krist 2016 - 2024

|