# WebGL Chromakey 实时绿幕抠图

20 行核心(shader)代码实现实时绿幕抠图

先体验 DEMO,基于 WebAV (opens new window) 实现

# 背景

因为视频相关标准及浏览器的实现问题,很难在主流浏览器中顺利播放背景透明的视频。

有两种方法可以为最通用的视频格式(MP4,H264)移除背景,实现透明效果:

  1. 原视频配上绿幕,使用本文介绍方法移除背景绿幕

    • 优点:制作使用简单
    • 缺点:抠图可能不完美,导致偏色
  2. 将视频中的 alpha 通道与画面并排放置,在客户端混合

    • 优点:精确还原
    • 缺点:分辨率增加,视频变大;可能适应场景小,原视频制作需要精确的 alpha 通道

方法二参考:https://juejin.cn/post/6885673542642302984 (opens new window)

# 绿幕抠图原理

  1. 传入四个参数
    1. 目标颜色,期望抠除背景色,可以不是绿色
    2. 相似度阈值
    3. 平滑度敏感系数
    4. 颜色饱和度敏感系数
  2. 使用 WebGL(片元着色器 (opens new window)) 逐个比对原像素与目标颜色
  3. 计算过程
    1. 将颜色转换到 UV 空间,计算出当前像素的与目标颜色的距离
    2. 距离 - 相似度阈值,小于 0 则判定为绿幕,将像素点设置为全透明(alpha=0)
    3. 与平滑度参数计算,将相似度转换成 alpha 通道值,越大越不透明
    4. 计算出愿像素点的灰度值
    5. 将相似度与饱和度参数计算,然后与原像素点的灰度值混合,越大越靠近原像素点,越小越就接近灰度
      (后两步为了移除前景边缘与绿幕反光,导致的前景像素点混合了绿幕背景颜色)

# 实现

参考:Production-ready green screen in the browser (opens new window)

需要先了解一下 YUV (opens new window) 颜色编码

Shader 代码

#version 300 es
precision mediump float;
out vec4 FragColor;
in vec2 v_texCoord;

uniform sampler2D frameTexture;
uniform vec3 keyColor;

// 色度的相似度阈值
uniform float similarity;
// 透明度的平滑度计算
uniform float smoothness;
// 降低绿幕饱和度,提高抠图准确度
uniform float spill;

vec2 RGBtoUV(vec3 rgb) {
  return vec2(
    rgb.r * -0.169 + rgb.g * -0.331 + rgb.b *  0.5    + 0.5,
    rgb.r *  0.5   + rgb.g * -0.419 + rgb.b * -0.081  + 0.5
  );
}

void main() {
  // 获取当前像素的rgba值
  vec4 rgba = texture(frameTexture, v_texCoord);
  // 计算当前像素与绿幕像素的色度差值
  vec2 chromaVec = RGBtoUV(rgba.rgb) - RGBtoUV(keyColor);
  // 计算当前像素与绿幕像素的色度距离(向量长度), 越相似则色度距离越小
  float chromaDist = sqrt(dot(chromaVec, chromaVec));
  // 设置了一个相似度阈值,baseMask < 0,则像素是绿幕,> 0 则像素点可能属于前景(比如人物)
  float baseMask = chromaDist - similarity;
  // 与平滑度参数计算,将 baseMask 转换成 alpha 通道值,越大越不透明
  float fullMask = pow(clamp(baseMask / smoothness, 0., 1.), 1.5);
  rgba.a = fullMask;
  // 如果 baseMask < 0,spillVal 等于 0;baseMask 越小,像素点饱和度越低
  float spillVal = pow(clamp(baseMask / spill, 0., 1.), 1.5);
  // 计算当前像素的灰度值
  float desat = clamp(rgba.r * 0.2126 + rgba.g * 0.7152 + rgba.b * 0.0722, 0., 1.);
  rgba.rgb = mix(vec3(desat, desat, desat), rgba.rgb, spillVal);
  FragColor = rgba;
}

上面算法使用 CPU(纯 js 代码)也能实现,但性能会差很多

除了上面分析的核心代码之外还有一些为了让 Shader 运行起来的辅助代码,属于 WebGL 的基础知识,查看完整代码 (opens new window)

# 如何使用

  1. 静态图片抠图示例










 








import { createChromakey } from '@webav/av-cliper';

const cvs = document.querySelector('#canvas') as HTMLCanvasElement;
const ctx = cvs.getContext('2d', { alpha: true })!;
(async () => {
  const img = new Image();
  img.src = './public/img/green-dog.jpeg';
  await new Promise((resolve) => {
    img.onload = resolve;
  });
  const chromakey = createChromakey({
    // 目标颜色不传,则取第一个像素点当做背景色
    similarity: 0.35,
    smoothness: 0.05,
    spill: 0.05,
  });
  ctx.drawImage(await chromakey(img), 0, 0, cvs.width, cvs.height);
})();
  1. 使用 @webav/av-cliper 对 mp4 视频进行逐帧抠图,体验 DEMO:mp4(chromakey) (opens new window) 合成视频的效果







 




import { createChromakey, MP4Clip } from '@webav/av-cliper';
const chromakey = createChromakey({
  similarity: 0.4,
  smoothness: 0.1,
  spill: 0.1,
});
const clip = new MP4Clip((await fetch('<mp4 url>')).body!);
clip.tickInterceptor = async (_, tickRet) => {
  if (tickRet.video == null) return tickRet;
  return { ...tickRet, video: await chromakey(tickRet.video) };
};

传入一张 720P 的图片给 chromakey 首次执行(包括初始化)大概耗时 20ms,后续每次执行基本在 1ms 之内;
所以性能方面实现视频实时抠图没有压力,将 Video 标签传给 chromakey 即可

async function render() {
  ctx.drawImage(await chromakey(videoElement), 0, 0, cvs.width, cvs.height);
  requestAnimationFrame(render); // 注意:后台页面 requestAnimationFrame 停止执行
}

render();

# 其它实现

以下是另一个实现抠图的 shader 的代码实现、使用相对简单;
但相对上面的实现,边缘可能会存在黑边。

precision mediump float;
uniform sampler2D u_texture;
uniform vec4 keyRGBA;    // key color as rgba
uniform vec2 range;      // the smoothstep range

varying vec2 v_texCoord;

vec2 RGBToCC(vec4 rgba) {
  float Y = 0.299 * rgba.r + 0.587 * rgba.g + 0.114 * rgba.b;
  return vec2((rgba.b - Y) * 0.565, (rgba.r - Y) * 0.713);
}

void main() {
  // 从贴图获取源像素
  vec4 srcColor = texture2D(u_texture, v_texCoord);
  // 源像素 RGB 转换为 YUV
  vec2 srcCC = RGBToCC(srcColor);
  // 目标颜色转换为 YUV
  vec2 keyCC = RGBToCC(keyRGBA);

  // 计算距离
  float mask = sqrt(pow(keyCC.x - srcCC.x, 2.0) + pow(keyCC.y - srcCC.y, 2.0));
  // 对距离值在range中进行平滑映射取值
  mask = smoothstep(range.x, range.y, mask);

  // 低于range下限
  if (mask == 0.0) { discard; }
  // 超过range上限
  else if (mask == 1.0) { gl_FragColor = srcColor; }
  // 处于range之中
  else {
    // 某些源像素(如头发边缘)混合了绿幕颜色,需要减去绿幕颜色,否则边缘会有绿斑
    gl_FragColor = max(srcColor - (1.0 - mask) * keyRGBA, 0.0);
  }
}

# 附录

💗 博主正处于裸辞待业状态,欢迎 商务合作 💗

相关文章

从 React 看前端 UI 代码范式革命

alt text 前言 本来打算写的主题是“我为什么讨厌 React Hooks API”,展开聊聊“小甜甜”是如何变成“牛夫人”的,没想到越写越严肃:) React 是两次前端范式革命的引领者,至今仍有繁荣的社区和旺盛的创造力; React 多次天才又激进的创新,一些想法被借鉴改良、一些引发广泛质疑,大部分是被认同和接受的; ...

WebCodecs 性能表现及优化思路

笔者开源 WebAV 已经一年半,还写了系列文章帮助初学者入门 Web 音视频。 之前一直隐隐担心在 Web 平台处理音视频与 Native APP 会有明显性能差距,因为 WebCodecs API 毕竟被浏览器代理了一层,且一些数据处理需要 js 配合,不确定有多大的性能损耗。 相信刚接触 WebCodecs 的读者也非常关心它的性能表现如何。 ...

Web 网页自集成 HTTP 代理方案

前言 大部分程序员,想必都有会一个常用的抓包代理工具; 但在座的各位,可曾见过这样一款集成在 Web 应用中的代理工具? 它是明显区别于传统代理工具的,有以下特性: 零安装,零配置,Web 点击即用、APP 扫码即用;_(不 ...

Web 文件系统(OPFS 及工具)介绍

文件系统是往往是构建大型软件的基石之一,很长一段时间 Web 平台因缺失成熟的文件系统成为构建大型软件的阻碍,如今 OPFS 可弥补这一缺憾。 本文介绍 OPFS 背景和基本使用方法、使用过程中的注意事项,及如何配合笔者开源的 opfs-tools、opfs-tools-explorer 两个项目,充分发挥 OPFS 的性能与开发效率。 Web 存储 A ...

opfs-tools (文件系统 API) 项目介绍

文件系统是许多领域程序的基石,所有通用编程语言都会内置完备的文件系统 API。 Web 很长一段时间没有提供完善的访问文件系统的规范,使得需要高频读写文件、大文件处理软件在 Web 端都会受到一些限制,比如音视频剪辑、游戏、数据库等等。 之前在浏览器中实现视频裁剪、截帧等相关功能时,发现缺少基本的操作文件的 API,比如读写、移动、复制文件。 而 [OPFS] ...

Web 终极拦截技巧(全是骚操作)

拦截的价值 > 计算机科学领域的任何问题都可以通过增加一个中间层来解决。 —— Butler Lampson 如果系统的控制权、代码完全被掌控,很容易添加中间层; 现实情况我们往往无法控制系统的所有环节,所以需要使用一些 “非常规”(拦截) 手段来增加中间层。 拦截的方法 拦截/覆写 浏览器 API 最常见的场景有通过拦截 console ...

Web 音视频(六)图像素材处理

Web 音视频目录 前序章节介绍了如何在浏览器中解析、创建视频,以及给视频添加一些自定义素材(图片、音频、文字...); 本章介绍如何给图像素材加特效、加动画,实现转场、移动水印、图像滤镜美化等功能。 你可以跳过原理介绍,直接查看 WebAV 示例 素材动画 在视频制作中实现动画跟其他场景略有不同,因为视频 ...