B站防遮挡弹幕实现

前言

B 站是一个以视频为主的社交媒体平台,其中一大特色就是弹幕,即用户可以在视频上方实时发送评论,与其他观众互动。弹幕可以增加观看视频的乐趣,也可以反映出视频的热度和受欢迎程度。然而,弹幕也有一个缺点,就是可能会遮挡住视频中的重要内容,影响观看体验。为了解决这个问题,B 站推出了一种智能防挡弹幕技术 ,可以让弹幕自动躲避人形区域,达到弹幕不挡人的效果。

本文将介绍 B 站智能防挡弹幕技术的原理和实现方法,并展示如何利用 tensorflow.jsvue3 在前端开发一个简单的示例应用。

原理

-webkit-mask-image 加一张人物的透明图片就直接搞定了。但是这张图片从哪里来?如果一帧一帧地从后端获取,那么服务器的压力一定非常大,这个功能又是一个很小的功能,服务端处理可能不是一个最佳的解决方案,所以主要是使用 tensorflow.js 来生成人物的透明图片。

整体实现思路

  1. 视频播放
  2. 通过 requestAnimationFrame 方法一帧一帧地执行 tensorflow.js 里面的函数获取人物透明图像
  3. 并通过 canvas 绘画导出成图片
  4. 设置图片的参数为 webkit-mask-image:url(${Base64});-webkit-mask-size:${width}px ${height}px;

引入

1
2
3
4
5
6
7
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)两种,尺寸越大,识别越准确,同时性能也更差

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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 };
};

生成图片

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

  • canvas 要绘制的画布。
  • image 应用口罩的原始图像。
  • maskImage 包含掩码的图像数据。理想情况下,这应该由 toBinaryMask 或 toColoredMask。
  • maskOpacity 在图像顶部绘制口罩时的不透明度。默认值为 0.7。应该是 0 和 1 之间的浮动。
  • maskBlurAmount 模糊面具的像素数量。默认值为 0。应该是 0 到 20 之间的整数。
  • flipHorizontal 如果结果应该水平翻转。默认为 false。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//识别
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 失效,因为图片还没有加载到页面上,新的图片已经添加上去了,会导致图片一直是个空白。

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

这里是我编写的代码地址,我不知道为啥 vue3 项目使用 Dplayer 会不加载弹幕