前言 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;
引入 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 }; };
生成图片 将图像绘制到画布上,并绘制包含具有指定不透明度的掩码的 ImageData
;ImageData
通常使用 toBinaryMask
或 toColoredMask
生成。
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 }; const backgroundColor = { r : 0 , g : 0 , b : 0 , a : 255 }; 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 (); 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 会不加载弹幕