本文正在参加「金石计划 . 瓜分6万现金大奖」
演示
//复制链接预览
https://code.juejin.cn/pen/7156805234864422943
前言
之前我一直好奇B站的弹幕为什么不遮挡人物,看了钱得乐
老师的 《为什么B站的弹幕可以不挡人物》的文章知道了里面的实现原理非常简单,就是用-webkit-mask-image
加一张人物的透明图片就直接搞定了。
知道了实现原理以后我就在想,这张图片从哪里来?如果一帧一帧的从后端获取,那么服务器的压力一定非常大,这个功能又是一个很小的功能,服务端处理可能不是一个最佳的解决方案,下面我就要讲如何不依赖后端在客户端上实现一个这样的功能。
如何实现?
通过观察图片我们知道,只要在视频播放的时候把人物的轮廓填充透明
其他地方填充颜色即可,实现这样的一个功能我们就需要引入tensorflowjs
,使用官方训练好的人体分割(Body Segmentation)
模型,在视频播放时将每一帧生成一直图片,放到弹幕的父元素上。
架构和概念
抽象整体的实现思路如下
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);
}
}
生成图片
将图像绘制到画布上,并绘制包含具有指定不透明度的掩码的ImageData
;ImageData
通常使用toBinaryMask
或toColoredMask
生成。
canvas
要绘制的画布。image
应用口罩的原始图像。maskImage
包含掩码的图像数据。理想情况下,这应该由toBinaryMask
或toColoredMask.
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;`
文档
本文正在参加「金石计划 . 瓜分6万现金大奖」