先放效果
页面加载后,会自动调取系统摄像头采集实时画面,发送弹幕或者点击 Auto Generate 自动生成弹幕,可以看到弹幕从人像后面飞过去的效果。
Link: cygra.github.io/danmaku-mas…
Github: github.com/Cygra/danma…
欢迎大家到 github 上点星 ⭐️,谢谢支持!
以下正文:
B 站的弹幕防遮挡效果想必大家都很熟悉了,当画面主体中有人物时,弹幕从人像下方飘过,从而让人像可以更好地展示。如图:
关于 B 站到底是如何实现的,各位前辈已经有很多文章了。我做这个项目,也主要是为了熟悉 Next.js + Mediapipe 环境的搭建以及使用。在这里整理出来,略贡愚见。欢迎大家在评论区指点交流。
环境搭建
这一部分在 juejin.cn/post/746037… 中已经讲过,在此不再赘述。
概要
Mediapipe 能力介绍
这里主要用到了 Mediapipe 的 Image Segmentation (图像分割)来实现。Image Segmentation 可以将人像从图像的背景中分割出来,并返回相应的轮廓。
技术实现
为了实现弹幕从人像之后而背景之前飞过的效果,这里用到了三个图层,从下往上依次是:
- 背景图层,使用
<video />标签直接渲染从摄像头采集到的实时图像 - 弹幕图层,这里写的比较挫,在 React 组件里写了一个简单的调度,用
<div />配合 keyframs 实现飞行动画 - 人像图层,这一层利用 Mediapipe 返回的轮廓信息,从视频流中抠出人像,渲染在最上方
代码解析
读取视频流
const prepareVideoStream = async () => {
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: true,
});
if (videoRef.current) {
videoRef.current.srcObject = stream;
videoRef.current.addEventListener("loadeddata", () => {
// 视频流加载完成,可以进行下一步操作
});
}
};
初始化 Mediapipe
这一部分参考官方指导 ai.google.dev/edge/mediap…
const vision = await FilesetResolver.forVisionTasks(
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm"
);
const imageSegmenter = await ImageSegmenter.createFromOptions(vision, {
baseOptions: {
modelAssetPath:
"https://storage.googleapis.com/mediapipe-models/image_segmenter/deeplab_v3/float32/1/deeplab_v3.tflite",
delegate: "GPU",
},
outputCategoryMask: true,
outputConfidenceMasks: false,
runningMode: "VIDEO",
});
渲染视频
这里会先将视频内容渲染在 canvas 上(ctx?.drawImage),然后再调用 Mediapipe 的图像分割方法,获取人像轮廓,然后将 canvas 上不属于人像的部分设为透明。
为了实现最优性能,通过 requestAnimationFrame 来调度,实现循环调用。
const predictWebcam = async () => {
if (!video || !maskCanvasRef.current) return;
if (video.currentTime === lastWebcamTime) {
requestAnimationFrame(predictWebcam);
return;
}
lastWebcamTime = video.currentTime;
ctx?.drawImage(video, 0, 0, videoWidth || 640, videoHeight || 480);
if (imageSegmenter === undefined) {
return;
}
const startTimeMs = performance.now();
imageSegmenter.segmentForVideo(video, startTimeMs, callbackForVideo);
};
获取人像轮廓
调用 Mediapipe 提供的 segmentForVideo 方法,传入视频流和回调函数,在回调函数中对返回的轮廓数据进行处理和渲染。
返回的 mask 即为人像的轮廓信息,先用 getAsFloat32Array 方法转为数组。
然后,遍历数组中的点,如果不是人像区域的(mask[i] === 0),则说明在最顶上的人像图层中,这一个点应该为透明。
调用 canvas 的 getImageData 方法获取 canvas 的图像信息,这里返回的 ImageData 中包含了图像中每一个点的 RGBA,这里将 A 置为 0(透明),再将处理过的图像重新绘制到 canvas 上。
const callbackForVideo = (result: ImageSegmenterResult) => {
const imageData = ctx?.getImageData(
0,
0,
videoWidth || 640,
videoHeight || 480
).data;
const mask = result?.categoryMask?.getAsFloat32Array(); // Mediapipe 返回的轮廓
let j = 0;
if (mask && imageData) {
for (let i = 0; i < mask.length; ++i) {
if (mask[i] === 0) {
imageData[j + 3] = 0;
}
j += 4;
}
const uint8Array = new Uint8ClampedArray(imageData.buffer);
const dataNew = new ImageData(
uint8Array,
videoWidth || 640,
videoHeight || 480
);
ctx?.putImageData(dataNew, 0, 0);
requestAnimationFrame(predictWebcam);
}
};
imageSegmenter.segmentForVideo(video, startTimeMs, callbackForVideo);
其他
此外还有弹幕调度、内容渲染等等,都比较简单,可以直接看源码。
到这里就结束啦,感谢您的观看,如果能去 github 上点个小星星 ⭐️ 就更好啦 ~