利用tensorflowjs实现B站的弹幕不遮挡人物效果。

本文正在参加「金石计划 . 瓜分6万现金大奖」

演示

//复制链接预览
https://code.juejin.cn/pen/7156805234864422943

前言

之前我一直好奇B站的弹幕为什么不遮挡人物,看了钱得乐老师的 《为什么B站的弹幕可以不挡人物》的文章知道了里面的实现原理非常简单,就是用-webkit-mask-image加一张人物的透明图片就直接搞定了。

知道了实现原理以后我就在想,这张图片从哪里来?如果一帧一帧的从后端获取,那么服务器的压力一定非常大,这个功能又是一个很小的功能,服务端处理可能不是一个最佳的解决方案,下面我就要讲如何不依赖后端在客户端上实现一个这样的功能。

如何实现?

通过观察图片我们知道,只要在视频播放的时候把人物的轮廓填充透明其他地方填充颜色即可,实现这样的一个功能我们就需要引入tensorflowjs,使用官方训练好的人体分割(Body Segmentation)模型,在视频播放时将每一帧生成一直图片,放到弹幕的父元素上。

image.png

架构和概念

抽象整体的实现思路如下

graph TD
A[调取Video画面] --> B[使用tensorflow加载人脸识别模型生成MediaPipe] --> C[获取分隔人物图片]

引入

import * as bodySegmentation from '@tensorflow-models/body-segmentation';
import '@tensorflow/tfjs-core';
import '@tensorflow/tfjs-backend-webgl';
import '@mediapipe/selfie_segmentation';

创建人体分割模型

模型有 landscape(144x256 x3 )和 general(256x256 x3)两种,尺寸越大,识别越准确,同时性能也更差

//模型初始化
async bodySegmentationInit(){
    try {
        const toast = this.$toast.loading({
            duration: 0, // 持续展示 toast
            forbidClick: true,
            message: '模型加载中',
        });
        const model = bodySegmentation.SupportedModels.MediaPipeSelfieSegmentation;
        const segmenterConfig = {
            runtime:'mediapipe',
            modelType:'landscape', 
            solutionPath:'https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation',
        };
        this.segmenter = await bodySegmentation.createSegmenter(model, segmenterConfig);
        toast.clear();
        this.dp && this.dp.notice('模型加载完成');
        this.dp && this.dp.play();
    } catch (error) {
        this.showDialog('模型加载失败-'+error);
    }
}

生成图片

将图像绘制到画布上,并绘制包含具有指定不透明度的掩码的ImageDataImageData通常使用toBinaryMasktoColoredMask生成。

  • canvas 要绘制的画布。
  • image 应用口罩的原始图像。
  • maskImage 包含掩码的图像数据。理想情况下,这应该由toBinaryMasktoColoredMask.
  • maskOpacity 在图像顶部绘制口罩时的不透明度。默认值为0.7。应该是0和1之间的浮动。
  • maskBlurAmount 模糊面具的像素数量。默认值为0。应该是0到20之间的整数。
  • flipHorizontal 如果结果应该水平翻转。默认为false。
//识别
async recognition(){
    const canvas = document.createElement("canvas");
    const context = canvas.getContext('2d');
    //压缩视频尺寸(视频尺寸越大处理的时间就越长,这要压缩一下视频图片)
    const imageData = await this.compressionImage(this.dp.video);
    const segmentationConfig = {
        flipHorizontal: false,
        multiSegmentation: false,
        segmentBodyParts: true,
        segmentationThreshold:1,
    };
    const people = await this.segmenter.segmentPeople(imageData, segmentationConfig);
    const foregroundColor = {r: 0, g: 0, b: 0, a: 0}; //用于可视化属于人的像素的前景色 (r,g,b,a)。
    const backgroundColor = {r: 0, g: 0, b: 0, a: 255}; //用于可视化不属于人的像素的背景颜色 (r,g,b,a)。
    const drawContour = false; //是否在每个人的分割蒙版周围绘制轮廓。
    const foregroundThresholdProbability = this.foregroundThresholdProbability; //将像素着色为前景而不是背景的最小概率。
    const backgroundDarkeningMask = await bodySegmentation.toBinaryMask(people, foregroundColor, backgroundColor, drawContour, foregroundThresholdProbability);
    // console.log('[backgroundDarkeningMask]',backgroundDarkeningMask);
    canvas.width = backgroundDarkeningMask.width;
    canvas.height = backgroundDarkeningMask.height;
    context.putImageData(backgroundDarkeningMask,0,0);
    const Base64 = canvas.toDataURL("image/png");
    this.maskImageUrl = Base64;
    const {width,height} = this.dp.video.getBoundingClientRect();
    //加载图片到缓存中(如果不加载到缓存中,会导致mask-image失效,因为图片还没有加载到页面上,新的图片已经添加上去了,会导致图片一直是个空白)
    await this.imgLoad(Base64);
}

上面显示了使用drawMask在图像和画布上绘制toBinaryMask生成的面具。在这种情况下,segmentationThreshold设置为0.25的较低值,使掩码包含更多像素。前两张图像显示了在图像顶部绘制的面具,后两张图像显示,在绘制到图像上之前,将maskBlurAmount设置为9,使面具变得模糊,从而在人与蒙版背景之间实现更平稳的过渡。

将实时生成的图片放到画面上

这里有个注意的点,所有的图片生成以后都要加入到缓存中,如果不加载到缓存中,会导致mask-image失效,因为图片还没有加载到页面上,新的图片已经添加上去了,会导致图片一直是个空白

 danmaku.style = `-webkit-mask-image: url(${Base64});-webkit-mask-size: ${width}px ${height}px;`
文档

github.com/tensorflow/…

本文正在参加「金石计划 . 瓜分6万现金大奖」