生成不遮挡弹幕的背景
原理就是使用 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>