背景移除和设置自定义背景的能力已成为视频会议应用程序中必备的功能。我的一些 WebRTC.ventures 团队成员展示了如何使用机器学习来删除使用Daily、Agora和Vonage CPaaS 提供商的视频会议应用程序中的背景。
这些方法效果很好,但依赖于额外的<canvas>
HTML 元素来执行视频流操作。如果操作不当,这可能会导致问题。
今天,我将讨论使用最近发布的Insertable Streams for MediaStreamTrack API 的替代方法。这个新的 API 提供了一种直接对流执行此类操作的方法。为简单起见,我不会专注于特定的 CPaaS。相反,我将使用原始 JavaScript 代码解释这个概念。
后台管理使用<canvas>
使用机器学习以传统方式管理后台时,您将遵循以下步骤:
- 对于每个视频帧,算法将每个像素分类为背景或人物。这个过程被称为语义分割。
- 然后,您使用该信息删除背景像素并用不同的图像(或同一视频帧的模糊版本)替换它们。
- 最后,您在其上绘制人的像素。
然后您需要一个中间<canvas>
元素,您可以在其中绘制新背景和机器学习模型识别为人物的像素。您还需要使用诸如 requestAnimationFrame 或 setTimeout 之类的东西来更新<canvas>
新的视频帧。
最后,您将从中取出流<canvas>
并通过对等连接发送它。整个过程可以在下面的图表中看到。
考虑到<canvas>
元素渲染和交互通常发生在 Web 应用程序的主执行线程中,此过程可能会变得混乱。如果管理不当,这可能会影响应用程序的性能或导致用户在多个浏览器选项卡上共享其视频流时出现问题。
输入可插入流
Insertable Streams 提供了一种直接操作流的方法。它通过提供可以通过转换器功能进行管道传输的接口来实现这一点,转换器功能又会创建一个新的视频帧,您可以使用该视频帧来构建新的流,而无需任何额外的用户界面元素。
这些接口是MediaStreamTrackProcessor
和MediaStreamTrackGenerator
。两者都分别公开了一个可读和可写的属性,这些属性授予对流轨道的访问权限。
在这个新的工作流程中,您首先创建一个MediaStreamTrackProcessor
基于您从中获得的轨道的getUserMedia
,以及一个MediaStreamTrackGenerator
类型属性设置为与您要操作的轨道相同的类型的 。
接下来,您将 传递MediaStreamTrackProcessor
给一个转换器函数,在该函数中应用您想要的自定义(在我们的例子中使用 ML 库(例如 MediaPipe 删除背景并将其替换为自定义背景图像)并将结果写入MediaStreamTrackGenerator
.
最后,您获取MediaStreamTrackGenerator
并使用它来创建一个新的媒体流,您可以将其添加到视频元素并发送给其他对等点。您将在下面找到包含此过程的更新图表。
代码示例
现在是时候开始行动了!该代码可在 Github 的background-removal-insertable-streams repo下找到。
代码相当简单。index.html
我们从定义 HTML 元素的地方开始<video>
。我们还导入我们的main.js
文件和我们用于背景删除的 MediaPipe 的 SelfieSegmentation 库。
<!-- index.html –->
<html>
<head>
<title>
Background Removal - Insertable Streams
</title>
<link rel="stylesheet" type="text/css" href="css/main.css" />
</head>
<body>
<h1>Background Removal - Insertable Streams</h1>
<video autoplay></video>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation/selfie_segmentation.js" crossorigin="anonymous"></script>
<script src="js/main.js"></script>
</body>
</html>
现在让我们来看看这个<em>main.js</em>
文件。为了使这个更简单,我们将一点一点地分解它。
首先,我们获得视频元素的引用和我们将使用的背景图像。我们还定义了一个OffscreenCanvas,它将帮助我们将背景与新视频帧中的分割结果相结合。
// js/main.js
// the video element
const videoEl = document.querySelector('video');
// the background image
const bgImage = new Image(480, 270);
bgImage.src = 'img/bg.jpg';
// an OffscreenCanvas that combines background and human pixels
const canvas = new OffscreenCanvas(480, 270);
const ctx = canvas.getContext("2d");
接下来,我们调用该getUserMedia
函数并将视频轨道传递给该background_removal
函数,这是所有魔法发生的地方。
// js/main.js
…
navigator.mediaDevices.getUserMedia({
video: { width: 480, height: 270, frameRate: { ideal: 15, max: 30 } },
audio: false
})
.then((stream) => {
/* use the stream */
background_removal(stream.getVideoTracks()[0]);
})
…
该background_removal
功能分为两部分:MediaPipe部分和Insertable Streams部分。
在 MediaPipe 部分,我们初始化SelfieSegmentation对象并通过设置它将使用的模型以及设置将在分割过程完成时运行的回调来配置它。您可以在官方 SelfieSegmentation 页面中找到有关此库的更多信息。
// js/main.js
…
function background_removal(videoTrack) {
// instance of SelfieSegmentation object
const selfieSegmentation = new SelfieSegmentation({
locateFile: (file) =>
`https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation/${file}`,
});
// set the model and mode
selfieSegmentation.setOptions({
modelSelection: 1,
selfieMode: true,
});
// set the callback function for when it finishes segmenting
selfieSegmentation.onResults(onResults);
…
在 Insertable Streams 端,我们定义了MediaStreamTrackProcessor
和MediaStreamTrackGenerator
对象,以及一个transform
函数。此 函数将一次接收一个视频帧并将其发送到 MediaPipe。然后,它根据 OffscreenCanvas 的内容创建一个新的视频帧并将其放入controller
.
// js/main.js
…
function background_removal(videoTrack) {
…
// definition of track processor and generator
const trackProcessor = new MediaStreamTrackProcessor({ track: videoTrack });
const trackGenerator = new MediaStreamTrackGenerator({ kind: 'video' });
// transform function
const transformer = new TransformStream({
async transform(videoFrame, controller) {
// we send the video frame to MediaPipe
videoFrame.width = videoFrame.displayWidth;
videoFrame.height = videoFrame.displayHeight;
await selfieSegmentation.send({ image: videoFrame });
// we create a new videoFrame
const timestamp = videoFrame.timestamp;
const newFrame = new VideoFrame(canvas, {timestamp});
// we close the current videoFrame and queue the new one
videoFrame.close();
controller.enqueue(newFrame);
}
});
…
接下来,我们通过管道从处理器将所有内容捆绑在一起,通过转换函数并在生成器中完成,然后生成器用于创建一个新MediaStream
对象。
// js/main.js
…
function background_removal(videoTrack) {
…
// we pipe the stream through the transform function
trackProcessor.readable
.pipeThrough(transformer)
.pipeTo(trackGenerator.writable)
// add the new mediastream to video element
const processedStream = new MediaStream();
processedStream.addTrack(trackGenerator);
videoEl.srcObject = processedStream;
}
最后,让我们看一下onResults
简单地在背景图像之上绘制分割结果的函数。
// js/main.js
…
function onResults(results) {
ctx.save();
ctx.clearRect(
0,
0,
canvas.width,
canvas.height
);
ctx.drawImage(
results.segmentationMask,
0,
0,
canvas.width,
canvas.height
);
ctx.globalCompositeOperation = "source-out";
const pat = ctx.createPattern(bgImage, "no-repeat");
ctx.fillStyle = pat;
ctx.fillRect(
0,
0,
canvas.width,
canvas.height
);
// Only overwrite missing pixels.
ctx.globalCompositeOperation = "destination-atop";
ctx.drawImage(
results.image,
0,
0,
canvas.width,
canvas.height
);
ctx.restore();
}
因此,当您打开该页面时,您会看到带有自定义背景的视频流。此时,您已准备好获取新视频流并通过对等连接将其发送到您最喜欢的媒体服务器、CPaaS 提供商,或直接发送到与您协商连接的另一个对等点。
结论
Insertable streams 通过授予开发人员通过简单接口直接访问这些媒体流,提供了一种简单直接的方法来操纵媒体流。这不仅可以轻松地将自定义背景添加到视频流中,还可以进行任何类型的进一步流处理,例如组合多个轨道或将其他图形元素引入其中。
作者:Hector Zelaya
原文链接:https://webrtc.ventures/2023/02/background-removal-using-insertable-streams
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/12832.html