b站不遮挡弹幕原理-生成不遮挡弹幕的背景

48 阅读5分钟

生成不遮挡弹幕的背景

  原理就是使用 tensflow 的 body-segmentation 方法对视频的每一帧进行人与其他的分离。

加载模型

  在视频进行分析前,需要先成功加载模型。

const loading = shallowRef(false); // 模型加载状态
const loadError = shallowRef(false); // 模型加载错误状态
const segmenter = shallowRef(null); // 分割器实例

const loadModel = async () => {
  try {
    loading.value = true;
    const model = bodySegmentation.SupportedModels.MediaPipeSelfieSegmentation;
    const segmenterConfig = {
      runtime: "mediapipe", // or 'tfjs'
      solutionPath:
        "https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation",
      modelType: "general",
    };
    segmenter.value = await bodySegmentation.createSegmenter(
      model,
      segmenterConfig
    );
    loading.value = false;
  } catch (error) {
    loading.value = false;
    loadError.value = true;
  }
};

实时获取视频帧

  requestAnimationFrame:告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

  若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用 requestAnimationFrame(),因为 requestAnimationFrame() 是一次性的。

  clearRect:这个方法通过把像素设置为透明以达到擦除一个矩形区域的目的。

  drawImage:提供了多种在画布(Canvas)上绘制图像的方式。

image:绘制到上下文的元素,允许任何的画布图像源。

sx 可选:需要绘制到目标上下文中的,image 的矩形(裁剪)选择框的左上角 X 轴坐标。可以使用 3 参数或 5 参数语法来省略这个参数。

sy 可选:需要绘制到目标上下文中的,image 的矩形(裁剪)选择框的左上角 Y 轴坐标。可以使用 3 参数或 5 参数语法来省略这个参数。

sWidth 可选:需要绘制到目标上下文中的,image 的矩形(裁剪)选择框的宽度。如果不说明,整个矩形(裁剪)从坐标的 sx 和 sy 开始,到 image 的右下角结束。可以使用 3 参数或 5 参数语法来省略这个参数。使用负值将翻转这个图像。

sHeight 可选:需要绘制到目标上下文中的,image 的矩形(裁剪)选择框的高度。使用负值将翻转这个图像。

dx:image 的左上角在目标画布上 X 轴坐标。

dy:image 的左上角在目标画布上 Y 轴坐标。

dWidth:image 在目标画布上绘制的宽度。允许对绘制的 image 进行缩放。如果不说明,在绘制时 image 宽度不会缩放。注意,这个参数不包含在 3 参数语法中。

dHeight:image 在目标画布上绘制的高度。允许对绘制的 image 进行缩放。如果不说明,在绘制时 image 高度不会缩放。注意,这个参数不包含在 3 参数语法中。

  getImageData:返回一个 ImageData 对象,用来描述 canvas 区域隐含的像素数据,这个区域通过矩形表示,起始点为(sx, sy)、宽为 sw、高为 sh。

const task = shallowRef(null);

// 获取视频每一帧的图片数据
const compressionImage = (el) => {
  return new Promise(async (resolve) => {
    const canvas = document.createElement("canvas");
    const context = canvas.getContext("2d");

    const elRect = el.getBoundingClientRect();
    const originWidth = elRect.width;
    const originHeight = elRect.height;

    canvas.width = originWidth;
    canvas.height = originHeight;
    context.clearRect(0, 0, originWidth, originHeight);
    context.drawImage(el, 0, 0, originWidth, originHeight);

    const imageData = context.getImageData(0, 0, originWidth, originHeight);
    resolve(imageData);
  });
};

const recognition = async () => {
  const imageData = await compressionImage(video.value);

  task.value = requestAnimationFrame(recognition);
};

video.value.addEventListener("play", async () => {
  task.value = requestAnimationFrame(recognition);
});

生成蒙版

const recognition = async () => {
  const imageData = await compressionImage(video.value); // 获取图片数据

  const segmentationConfig = {
    flipHorizontal: false,
    multiSegmentation: false,
    segmentBodyParts: true,
    segmentationThreshold: 1,
  };

  // 进行分割
  const segmentation = await segmenter.value.segmentPeople(
    imageData,
    segmentationConfig
  );

  const foregroundColor = { r: 0, g: 0, b: 0, a: 0 };
  const backgroundColor = { r: 0, g: 0, b: 0, a: 255 };
  // 生成蒙版
  const backgroundDarkeningMask = await bodySegmentation.toBinaryMask(
    segmentation,
    foregroundColor,
    backgroundColor,
    false,
    0.3
  );

  let canvas = document.createElement("canvas");
  let ctx = canvas.getContext("2d");
  canvas.width = backgroundDarkeningMask.width;
  canvas.height = backgroundDarkeningMask.height;
  ctx.putImageData(backgroundDarkeningMask, 0, 0);
  // 将蒙版转换为 Base64 编码
  Base64.value = canvas.toDataURL("image/png");
  // 继续处理下一帧
  task.value = requestAnimationFrame(recognition);
};

完整代码

<template>
  <div class="app">
    <!-- 如果正在加载模型,显示加载信息 -->
    <div class="load" v-if="loading">模型加载中...</div>
    <!-- 如果加载模型出错,显示错误信息 -->
    <div class="load" v-else-if="loadError">模型加载中...</div>
    <!-- 如果模型加载完成,显示视频和处理后的图片 -->
    <div class="content" v-else>
      <!-- 视频播放器 -->
      <video
        id="video"
        ref="video"
        crossorigin="anonymous"
        :src="test"
        controls
      ></video>

      <!-- 处理后的图片 -->
      <img :src="Base64" alt="" />
    </div>
  </div>
</template>

<script setup>
// 导入所需的模块和库
import { onMounted, shallowRef } from "vue";
import * as bodySegmentation from "@tensorflow-models/body-segmentation";
import "@tensorflow/tfjs-core";
import "@tensorflow/tfjs-backend-webgl";
import "@mediapipe/selfie_segmentation";
import test from "./assets/test.mp4";

// 定义引用和状态
const video = shallowRef(null); // 视频元素的引用
const task = shallowRef(null); // 用于取消 requestAnimationFrame 的任务 ID
const Base64 = shallowRef(null); // 处理后的图片的 Base64 编码
const loading = shallowRef(false); // 模型加载状态
const loadError = shallowRef(false); // 模型加载错误状态
const segmenter = shallowRef(null); // 分割器实例

// 加载模型的函数
const loadModel = async () => {
  try {
    loading.value = true; // 开始加载
    const model = bodySegmentation.SupportedModels.MediaPipeSelfieSegmentation; // 选择模型
    const segmenterConfig = {
      runtime: "mediapipe", // 运行时选择
      solutionPath:
        "https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation", // 模型路径
      modelType: "general", // 模型类型
    };
    // 创建分割器
    segmenter.value = await bodySegmentation.createSegmenter(
      model,
      segmenterConfig
    );
    loading.value = false; // 加载完成
  } catch (error) {
    loading.value = false; // 加载失败
    loadError.value = true; // 设置错误状态
  }
};

// 获取视频每一帧的图片数据
const compressionImage = (el) => {
  return new Promise(async (resolve) => {
    const canvas = document.createElement("canvas");
    const context = canvas.getContext("2d");

    const elRect = el.getBoundingClientRect();
    const originWidth = elRect.width;
    const originHeight = elRect.height;

    canvas.width = originWidth;
    canvas.height = originHeight;
    context.clearRect(0, 0, originWidth, originHeight);
    context.drawImage(el, 0, 0, originWidth, originHeight);

    const imageData = context.getImageData(0, 0, originWidth, originHeight);
    resolve(imageData);
  });
};

// 生成蒙版的函数
const recognition = async () => {
  const imageData = await compressionImage(video.value); // 获取图片数据

  const segmentationConfig = {
    flipHorizontal: false,
    multiSegmentation: false,
    segmentBodyParts: true,
    segmentationThreshold: 1,
  };

  // 进行分割
  const segmentation = await segmenter.value.segmentPeople(
    imageData,
    segmentationConfig
  );

  const foregroundColor = { r: 0, g: 0, b: 0, a: 0 };
  const backgroundColor = { r: 0, g: 0, b: 0, a: 255 };
  // 生成蒙版
  const backgroundDarkeningMask = await bodySegmentation.toBinaryMask(
    segmentation,
    foregroundColor,
    backgroundColor,
    false,
    0.3
  );

  let canvas = document.createElement("canvas");
  let ctx = canvas.getContext("2d");
  canvas.width = backgroundDarkeningMask.width;
  canvas.height = backgroundDarkeningMask.height;
  ctx.putImageData(backgroundDarkeningMask, 0, 0);
  // 将蒙版转换为 Base64 编码
  Base64.value = canvas.toDataURL("image/png");
  // 继续处理下一帧
  task.value = requestAnimationFrame(recognition);
};

// 组件挂载后的操作
onMounted(async () => {
  await loadModel(); // 加载模型

  // 添加视频播放和暂停的事件监听器
  video.value.addEventListener("play", async () => {
    task.value = requestAnimationFrame(recognition); // 开始处理
  });

  video.value.addEventListener("pause", () => {
    if (task.value) {
      cancelAnimationFrame(task.value); // 停止处理
      task.value = null;
    }
  });
});
</script>

<style scoped>
.app {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
.load {
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.1);
  display: flex;
  justify-content: center;
  align-items: center;
}
#video {
  width: 200px;
}
</style>

对应源代码链接