侧边栏壁纸
博主头像
MicroMatrix博主等级

曲则全,枉则直,洼则盈,敝则新,少则得,多则惑。是以圣人抱一为天下式。不自见,故明;不自是,故彰;不自伐,故有功;不自矜,故长。夫唯不争,故天下莫能与之争。古之所谓“曲则全”者,岂虚言哉!诚全而归之。

  • 累计撰写 80 篇文章
  • 累计创建 21 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

B站防遮挡弹幕实现

蜗牛
2023-03-18 / 0 评论 / 0 点赞 / 4 阅读 / 7624 字 / 正在检测是否收录...

前言

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;

引入

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 };
};

生成图片

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

  • 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 会不加载弹幕

0

评论区