前言
B站是一个以视频为主的社交媒体平台,其中一大特色就是弹幕,即用户可以在视频上方实时发送评论,与其他观众互动。弹幕可以增加观看视频的乐趣,也可以反映出视频的热度和受欢迎程度。然而,弹幕也有一个缺点,就是可能会遮挡住视频中的重要内容,影响观看体验。为了解决这个问题,B站推出了一种智能防挡弹幕技术 ,可以让弹幕自动躲避人形区域,达到弹幕不挡人的效果。
本文将介绍B站智能防挡弹幕技术的原理和实现方法,并展示如何利用 tensorflow.js
和 vue3
在前端开发一个简单的示例应用。
原理
用 -webkit-mask-image
加一张人物的透明图片就直接搞定了。但是这张图片从哪里来?如果一帧一帧地从后端获取,那么服务器的压力一定非常大,这个功能又是一个很小的功能,服务端处理可能不是一个最佳的解决方案,所以主要是使用 tensorflow.js
来生成人物的透明图片。
整体实现思路
- 视频播放
- 通过
requestAnimationFrame
方法一帧一帧地执行tensorflow.js
里面的函数获取人物透明图像 - 并通过 canvas 绘画导出成图片
- 设置图片的参数为
webkit-mask-image:url(${Base64});-webkit-mask-size:${width}px ${height}px;
引入
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)两种,尺寸越大,识别越准确,同时性能也更差
import { MediaPipeSelfieSegmentationMediaPipeModelConfig } from '@tensorflow-models/body-segmentation';
import * as bodySegmentation from '@tensorflow-models/body-segmentation';
import DPlayer from 'dplayer';
import { showLoadingToast, showToast } from 'vant';
export type UseSegmentationType = (arg1: DPlayer) => void;
export const useSegmentation = ({ dp, segmenter, modelType }: any) => {
//模型初始化
const bodySegmentationInit: UseSegmentationType = async () => {
try {
const messageToast = showLoadingToast({
message: '加载中...',
forbidClick: true,
loadingType: 'spinner',
});
const model =
bodySegmentation.SupportedModels.MediaPipeSelfieSegmentation;
const segmenterConfig: MediaPipeSelfieSegmentationMediaPipeModelConfig = {
runtime: 'mediapipe',
modelType: modelType.value,
solutionPath:
'https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation',
};
segmenter.value = await bodySegmentation.createSegmenter(
model,
segmenterConfig
);
messageToast.close();
dp.value?.notice('模型加载完成!', 500, 100);
dp.value?.play();
} catch (err) {
showToast('模型加载失败' + err);
}
};
return { bodySegmentationInit };
};
生成图片
将图像绘制到画布上,并绘制包含具有指定不透明度的掩码的 ImageData
;ImageData
通常使用 toBinaryMask
或 toColoredMask
生成。
- canvas 要绘制的画布。
- image 应用口罩的原始图像。
- maskImage 包含掩码的图像数据。理想情况下,这应该由 toBinaryMask 或 toColoredMask。
- maskOpacity 在图像顶部绘制口罩时的不透明度。默认值为 0.7。应该是 0 和 1 之间的浮动。
- maskBlurAmount 模糊面具的像素数量。默认值为 0。应该是 0 到 20 之间的整数。
- flipHorizontal 如果结果应该水平翻转。默认为 false。
//识别
const recognition = async () => {
const danmaku = dplayer.value?.querySelector('.dplayer-danmaku');
try {
randomDanmaku();
if (segmenter.value && maskOpen.value && danmaku) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
//压缩视频尺寸
const imageData = await compressionImage(dp.value?.video);
const segmentationConfig = {
flipHorizontal: false,
multiSegmentation: false,
segmentBodyParts: true,
segmentationThreshold: 1,
};
let tSegmenter = toRaw(segmenter.value);
const people = await tSegmenter?.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 fThresholdProbability = foregroundThresholdProbability.value; //将像素着色为前景而不是背景的最小概率。
const backgroundDarkeningMask = await bodySegmentation.toBinaryMask(
people,
foregroundColor,
backgroundColor,
drawContour,
fThresholdProbability
);
canvas.width = backgroundDarkeningMask.width;
canvas.height = backgroundDarkeningMask.height;
context?.putImageData(backgroundDarkeningMask, 0, 0);
const Base64 = canvas.toDataURL('image/png');
maskImageUrl.value = Base64;
const { width, height } = dp.value.video.getBoundingClientRect();
//加载图片到缓存中(如果不加载到缓存中,会导致mask-image失效,因为图片还没有加载到页面上,新的图片已经添加上去了,会导致图片一直是个空白)
await imgLoad(Base64);
danmaku.style = `-webkit-mask-image: url(${Base64});-webkit-mask-size: ${width}px ${height}px;`;
task.value ? cancelAnimationFrame(task.value) : false;
task.value = requestAnimationFrame(recognition);
} else {
danmaku.style = '';
task.value ? cancelAnimationFrame(task.value) : false;
task.value = requestAnimationFrame(recognition);
}
} catch (error) {
danmaku.style = '';
task.value ? cancelAnimationFrame(task.value) : false;
task.value = requestAnimationFrame(recognition);
}
};
将实时生成的图片放到画面上
这里有个注意的点,所有的图片生成以后都要加入到缓存中,如果不加载到缓存中,会导致 mask-image 失效,因为图片还没有加载到页面上,新的图片已经添加上去了,会导致图片一直是个空白。
danmaku.style = `-webkit-mask-image: url(${Base64});-webkit-mask-size: ${width}px ${height}px;`;
这里是我编写的代码地址,我不知道为啥 vue3 项目使用 Dplayer 会不加载弹幕
评论区