最近两年AI数字人突然火了起来,很多教育机构、电商直播都在用这种虚拟主播。但有个很实际的问题:大部分AI数字人输出的视频都是带绿幕背景的MP4文件,而网页端需要的是透明背景。这就引出了我们今天要解决的问题——如何在网页中用JavaScript实时去除绿幕背景。
我去年给一个在线教育平台做H5页面时,就遇到了这个需求。他们采购的AI数字人课程讲解视频全是绿幕背景,直接放在网页上特别违和。试过用PR提前处理,但视频数量太多根本忙不过来。最后我们决定用前端方案实时处理,效果出奇地好。
传统做法是用专业软件提前抠像,但这样有几个致命缺点:
而用Canvas实时处理就灵活多了:
绿幕抠像的核心原理其实很简单:识别特定颜色范围(通常是绿色)的像素,把这些像素的透明度(alpha值)设为0。在代码里我们主要操作的是ImageData对象,它包含了canvas上所有像素的RGBA值。
这里有个关键细节要注意:纯绿色是RGB(0,255,0),但实际拍摄的视频会有光线影响,所以要用范围判断。经过多次测试,我发现这个判断条件效果最好:
javascript复制if (r < 100 && g > 120 && b < 200) {
frame.data[i * 4 + 3] = 0; // 设置alpha为0
}
直接逐帧处理整个视频会很卡,我们做了这些优化:
实测下来,最有效的还是缩小处理范围。比如数字人通常居中显示,可以只处理中间60%的区域:
javascript复制const startX = canvas.width * 0.2;
const startY = canvas.height * 0.1;
const endX = canvas.width * 0.8;
const endY = canvas.height * 0.9;
先来看最基础的实现方案,适合快速上手:
html复制<!DOCTYPE html>
<html>
<head>
<style>
body { background: #f0f; } /* 用紫色背景方便检查抠图效果 */
</style>
</head>
<body>
<video id="sourceVideo" src="ai_avatar.mp4" loop muted></video>
<script>
const video = document.getElementById('sourceVideo');
video.play(); // 自动播放
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
video.addEventListener('play', () => {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
function processFrame() {
if (video.paused) return;
ctx.drawImage(video, 0, 0);
const frame = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (let i = 0; i < frame.data.length; i += 4) {
const r = frame.data[i];
const g = frame.data[i + 1];
const b = frame.data[i + 2];
if (g > Math.max(r, b) * 1.5) { // 更智能的绿幕检测
frame.data[i + 3] = 0;
}
}
ctx.putImageData(frame, 0, 0);
requestAnimationFrame(processFrame);
}
requestAnimationFrame(processFrame);
});
</script>
</body>
</html>
实际项目中我们还需要考虑这些功能:
这里分享一个更完整的类封装:
javascript复制class GreenScreenProcessor {
constructor(videoElement) {
this.video = videoElement;
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
this.isProcessing = false;
// 边缘平滑处理
this.edgeSmooth = true;
this.smoothRadius = 2;
}
start() {
this.canvas.width = this.video.videoWidth;
this.canvas.height = this.video.videoHeight;
this.isProcessing = true;
this.processFrame();
}
stop() {
this.isProcessing = false;
}
processFrame() {
if (!this.isProcessing) return;
this.ctx.drawImage(this.video, 0, 0);
const frame = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
this.removeGreenScreen(frame);
if (this.edgeSmooth) {
this.applyEdgeSmoothing(frame);
}
this.ctx.putImageData(frame, 0, 0);
requestAnimationFrame(() => this.processFrame());
}
removeGreenScreen(frame) {
// ...绿幕去除逻辑
}
applyEdgeSmoothing(frame) {
// 边缘抗锯齿处理
// ...实现细节
}
}
开发时最常见的坑就是跨域问题。浏览器安全策略要求视频源必须同源,否则访问像素数据会报错。解决方法有:
对于Vue项目,可以在vue.config.js中配置:
javascript复制module.exports = {
devServer: {
proxy: {
'/videos': {
target: 'http://your-cdn-domain.com',
changeOrigin: true
}
}
}
}
直接抠像会导致人物边缘有绿色残留,我们采用了这些优化方案:
实测效果最好的还是结合色度键和边缘检测:
javascript复制function advancedChromaKey(frame) {
for (let i = 0; i < frame.data.length; i += 4) {
const r = frame.data[i];
const g = frame.data[i + 1];
const b = frame.data[i + 2];
// 计算颜色差异度
const greeness = (2 * g - r - b) / 2;
if (greeness > 30) {
const alpha = Math.min(1, (greeness - 30) / 20);
frame.data[i + 3] = 255 * (1 - alpha);
}
}
}
当处理高清视频时,这些优化很关键:
这里给出一个WebGL实现的思路:
javascript复制// 创建WebGL着色器
const vertShader = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0, 1);
}
`;
const fragShader = `
precision highp float;
uniform sampler2D texture;
uniform vec2 resolution;
void main() {
vec2 uv = gl_FragCoord.xy / resolution;
vec4 color = texture2D(texture, uv);
// 绿幕去除算法
float greeness = (2.0 * color.g - color.r - color.b) / 2.0;
color.a = step(0.3, greeness) * color.a;
gl_FragColor = color;
}
`;
去年我们为某电商平台开发的虚拟主播系统,就深度应用了这项技术。系统需要同时处理多个AI数字人视频流,并实时合成到不同场景中。
技术方案要点:
核心代码结构如下:
javascript复制class VirtualHostSystem {
constructor() {
this.channels = new Map();
this.backgrounds = new Map();
}
addVideoStream(id, stream) {
const processor = new GreenScreenProcessor();
this.channels.set(id, processor);
// ...初始化逻辑
}
setBackground(id, imageUrl) {
// ...背景切换逻辑
}
renderAll() {
// ...统一渲染逻辑
}
}
这个项目上线后,客户反馈加载速度比预期快40%,而且CPU占用率比竞品方案低很多。关键就在于我们做了这些优化: