Skip to content

上节课我们实现了简易直播,但在实际直播场景中,我们会遇到如题目中描述的虚拟背景的需求,这节课,我们就看看如何在前端实现给视频流赋于虚拟背景,后面我们再将虚拟背景和直播以及视频会议组合起来。

初步认识虚拟背景

很多人都或多或少在生活中见到过虚拟背景,尤其是现在微信视频通话过程中新增的模糊背景功能。这个过程还挺复杂的,整个实现逻辑涉及到人物动态计算、人像抠图、背景填充(增加马赛克或者其他的色彩)等从而才能实现模糊背景这个看似简单的功能。

而我们现在只不过是站在巨人的肩膀上,用别人已经写好的算法并训练出对应的人工智能模型完成我们现在的目的。

从前面阐述的整体实现逻辑,大体可以看出,实现模糊背景需要的几个核心步骤:

  • 第一,识别当前画面中的人;
  • 第二,动态从这个画面中扣出第一步识别出的人的画面;
  • 第三,给非人部分增加马赛克或者其他的背景。

就三个步骤而言看起来很简单,但是每个步骤要实现对应的功能可不简单,这里面就涉及到了机器学习和复杂算法。当然,我们的目的仅仅是实现这个功能,而不是学习深层次的核心算法,因此上面提到的东西,我们只需要大体有个认知,能找到对应的解决方案即可。

而本节课,我将利用谷歌开源的一个机器学习框架 MediaPipe实现虚拟背景的功能。

什么是 MediaPipe呢?

MediaPipe 是谷歌开源的适用于多平台、终端的机器学习框架,其内部有很多的工具包和基础解决方案,安装即可使用,内部使用的模型也有开源的。像人脸检测、面部识别、虹膜、手势、姿态、人体、人体分割、头发分割、3D识别等常见场景,都可以直接找到对应的成熟解决案例和模型。

因此利用上述框架中的人体分割模型,就可以实现我们在摄像头中的画面人物和背景分割的目标。分割完成后,还可以利用其他强大的功能,对已经分割识别的动态流自定义处理,进而实现背景自定义。

在线演示 点击前往

代码实战

  1. 准备基础的环境。因为我们用前端开发,因此利用已经搭建好的前端 node 环境即可,在当前项目安装 JS 版本的 MediaPipe中的人体分割相关的依赖库。
npm i @mediapipe/selfie_segmentation   //可以指定版本 当前案例我自己选择的是 ^0.1.1632777926
  1. 视频流初始化。获取摄像头的视频流和前面课程中的一致,copy 过来即可。
/**
 * 获取指定媒体设备id对应的媒体流(不传参数则获取默认的摄像头和麦克风)
 * @author suke
 * @param videoId
 * @param audioId
 * @returns {Promise<void>}
 */
async getTargetDeviceMedia(videoId,audioId){
    const constraints = {
        audio: {deviceId: audioId ? {exact: audioId} : undefined},
        video: {
            deviceId: videoId ? {exact: videoId} : undefined,
            width:1920,
            height:1080,
            frameRate: { ideal: 10, max: 15 }
        }
    };
    if (window.stream) {
        window.stream.getTracks().forEach(track => {
            track.stop();
        });
    }
    //被调用方法前面有,此处不再重复
    return await this.getLocalUserMedia(constraints).catch(handleError);
},
  1. 初始化图像分割工具

以下代码中出现了一个 canvas 元素,这个载体我们作为拿到虚拟背景后将对应画面展示的地方。

同时可以看到,有个地方用到了动态地址,这个动态地址就是下载具体版本模型的地方,因为 cdn地址在国内访问比较慢,因此我将其下载到本地,然后通过 nginx 代理通过区域网访问对应模型。


initVb(){
        canvasElement = document.getElementById('output_canvas');
        canvasCtx = canvasElement.getContext('2d');
        image = new Image();
        image.src = this.meimage
        selfieSegmentation = new SFS.SelfieSegmentation({locateFile: (file) => {
                console.log(file);
                return `http://192.168.101.138:8080/${file}`;//ng  代理模型文件夹
          // return `https://cdn.jsdelivr.'net/npm/@mediapipe/selfie_segmentation@0.1.1632777926/${file}`;
        }});                                
        selfieSegmentation.setOptions({
                modelSelection: 1,
                minDetectionConfidence: 0.5,
                minTrackingConfidence: 0.5,
        });
        selfieSegmentation.onResults(this.handleResults);
},
  1. 图像分割后处理背景和人像

在前面的官方 Demo 中,并没有设置背景的,仅仅是将分割后的人像使用特定的颜色框出来,这里大家可以和官方的案例中对比下。

handleResults(results) {
    // Prepare the new frame
    canvasCtx.save();
    canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
    canvasCtx.drawImage(results.segmentationMask, 0, 0, canvasElement.width, canvasElement.height);
   //利用canvas绘制新背景 
   //canvasCtx.globalCompositeOperation = 'source-in';则意味着处理分割后图像中的人体。 
    canvasCtx.globalCompositeOperation = 'source-out';
    canvasCtx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvasElement.width, canvasElement.height);
    canvasCtx.globalCompositeOperation = 'destination-atop';
    canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
    // Done
    canvasCtx.restore();
},
  1. 监听流播放后触发上述工具模型处理画面,并绘制到前面声明的 Canvas 载体。
/**
 * 监听触发模型处理
 */
async virtualBg(){
        const that = this
        let video = document.getElementById('localdemo01')
        video.addEventListener('playing',function(){
                let myvideo = this;
                let lastTime = new Date();
                async function getFrames() {
                        const now = myvideo.currentTime;
                        if(now > lastTime){
                                await selfieSegmentation.send({image: myvideo});
                        }
                        lastTime = now;
                        //无限定时循环 退出记得取消 cancelAnimationFrame() 
                        requestAnimationFrame(getFrames);
                };
                getFrames()
        })
}

我们对整体流程进行一个总结。

  1. 获取摄像头画面流。
  1. 初始化图像分割工具。
  1. 在本地的页面 DOM 中,播放第一步获取到的视频流。
  1. 监听视频流播放后,将画面帧发送到图像分割工具处理。
  1. 图像分割工具利用机器学习模型,识别画面并分割人体,然后处理得到分割后的蒙版,我们得到蒙版后将背景替换成自己的图片,最后展示到 canvas 。

初始化图像分割工具时有几个参数配置,这里挑几个重要的说明下。

  • MIN_DETECTION_CONFIDENCE :手部检测模型中的最小置信度值,取值区间[0.0, 1.0] 被认为是成功的检测。默认为0.5
  • MIN_TRACKING_CONFIDENCE : 跟踪模型的最小置信度值,取值区间[0.0, 1.0],将其设置为更高的值可以提高解决方案的稳健性,但是会带来更高的延迟,默认0.5

项目操作演示

  1. 打开项目。找到模块:虚拟背景。
  1. 在根目录找到模型文件夹:virtualbg-model,然后在根目录启动 Http-Server,当然这里可以不用 Http-Server,也可以用 Nginx 代理。我们的目的是将该文件夹下的文件代理到一个可以访问的路径。
cd virtualbg-model 
## 以允许跨域的参数启动
http-server --cors
----------------启动成功如下----------------
Starting up http-server, serving ./

http-server settings:
CORS: true
Cache: 3600 seconds
Connection Timeout: 120 seconds
Directory Listings: visible
AutoIndex: visible
Serve GZIP Files: false
Serve Brotli Files: false
Default File Extension: none

Available on:
  http://192.168.101.37:8081
  http://127.0.0.1:8081
Hit CTRL-C to stop the server

看上面项目截图,红色框框标记的位置:模型文件和被代理后的模型文件地址。如果大家要在线上使用该虚拟背景,那么这个静态文件是必须要有的,官网的例子使用的是 CDN 链接,但是该 CDN 在网络已被限制,因此这里给大家演示离线的版本

  1. 选择摄像头和麦克风参数后点击确定。等待模型加载完毕后视频的旁边 Canvas幕布中就是实时显示虚拟背景画面。

完整代码地址

本节课相关代码

课后题

这节课的内容,如果你已经完全消化,那么我们实现虚拟背景的目的就很好达到了。但是完成后,如何将这个虚拟背景转化为媒体流,并发送给对直播间的观众呢?欢迎大家在留言区讨论。