使用 WebGL 进行实时视频图像处理

摘要:这是我最近在 CodePen 上制作的 WebGL 演示案例。它可以捕获网络摄像头的数据(或在无法访问网络摄像头时,从 placekitten 获取备用图像),并将其实时转换为 ASCII 图像艺术。

这是我最近在 CodePen 上制作的 WebGL 演示案例。它可以捕获网络摄像头的数据(或在无法访问网络摄像头时,从 placekitten 获取备用图像),并将其实时转换为 ASCII 图像艺术。


为了获得更多的复古性,我使用了 90 年代 DOS PC 中常见的 8x8 像素光栅字体(您可能会在某些 BIOS 中看到这种字体)。

要将图像内容映射到特定字符,我通过使用亮度图选择最佳匹配。我计算每个 4x4 正方形的像素。在画板内向下滚动以查看亮度图:


我还为这些字体创建了一个编辑器:https://terabaud.github.io/


若干 WebGL 基础知识

我将介绍 WebGL 的一些基础知识,但这里仅涉及部分问题。获取有关详细指导,建议您访问https://webglfundamentals.org

对于 WebGL,一个常见误解是把它当作浏览器中的 3D 引擎。尽管 WebGL 技术能使我们在浏览器中提供 GPU 加速的 3D 内容,但 WebGL 本身不是 3D 引擎。在 WebGL 之上,有专门用于 GPU 加速的 2D 或 3D 内容的图形库(例如用于 2D 的 Pixi,用于 3D 的 ThreeJS)。

WebGL 本身是很基础的绘图标准库,并且是一个以 GPU 加速的方式,将点、线和三角形绘制到 html <canvas> 元素上的库。

可以通过 getContext(类似于 2D canvas API)检索 WebGL 渲染上下文:

const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');

一个 WebGL 程序包含多个着色器组件,着色器是运行在 GPU 上的代码,它们不是用 JavaScript 编写的,而是具有自己的语言,称为 GLSL(GL 着色器语言)。


GLSL 快速概览

  • 类似 C语言,着色器程序包含 void main()
  • 变量声明也像在 C 语言中一样
  • 原始数据类型:int, float, double
  • 向量:vec2, vec3, vec4, ...
  • 矩阵:mat2, mat3, mat4, ...
  • 访问纹理数据的类型:sampler2D
  • 内置向量、矩阵运算
  • 大量内置功能, 例如,求取向量的长度(length(v))

着色器的类型

WebGL 程序中有两种类型的着色器。

  • 顶点着色器计算位置。
  • 片段着色器处理栅格化。

如果您的 WebGL 程序想要在屏幕上绘制一个三角形,它会把三角形的 3 个坐标传递给顶点着色器。然后,片段着色器的任务是用像素填充该三角形,这种逐像素处理过程非常快,因为它是针对 GPU 上的每个像素并行运行处理的。

在我的演示案例中,我使用 4 个矢量坐标来覆盖适合整个屏幕的矩形,所有工作都在片段着色器中完成。


顶点着色器

顾名思义,顶点着色器存在于顶点。它从 JavaScript 代码提供的缓冲区中获取一堆数据,并根据这些数据计算在画布中的相应位置。

以下代码段将数据从缓冲区拉入一个 attribute 变量,并将其传递给该 gl_Position 变量:

attribute vec3 position;

void main() {
  gl_Position = vec4(position, 1.0);
}

片段着色器

precision highp float;

void main() {
  vec2 p = gl_FragCoord.xy;
  gl_FragColor = vec4(
1.0, .5 + .5 * sin(p.y), .5 + .5 * sin(p.x), 1.0);
}

片段着色器针对每个片段(像素)并行运行。在上面的示例中,片段着色器从 gl_FragCoord 变量读取当前像素坐标,并通过 gl_FragColor 中的 sin() 计算运行并输出颜色。

gl_FragColor 是一个 vec4 向量,其中包含(红色,绿色,蓝色,alpha),取值各为 0 .. 1。


GLSL 变量的类型

  • attribute: 顶点着色器从缓冲区中提取一个值并将其存储在属性变量中。
  • uniform: 从 JS 端设置统一变量。例如,您可以统一使用诸如当前鼠标、轻敲位置之类的内容传递给着色器。您还可以使用统一变量来访问从 JavaScript 上传的纹理数据。
  • varying: 将值从顶点传递到片段着色器并进行插值。


上传图像数据

您可以使用图像数据访问到着色器中的 WebGLRenderingContext,并将其上传到纹理中。(另请参见:WebGL 基础知识:图像处理)

您可以使用 texImage2D内部方法 WebGLRenderingContext 将图像数据上传到纹理中。

// gl is the WebGLRenderingContext 
const texture = gl.createTexture()
gl.activeTexture(gl.TEXTURE0 + textureIndex);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);

// more info about these parameters in the webglfundamentals
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

您传递给 texImage2D 的图像数据,可以是 img 元素、视频元素、ImageData 等。

由于视频的图像数据不断变化,因此您必须在 requestAnimationFrame 动画循环内更新纹理。以下是获取完成的 texSubImage2D。

gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, video);


读取纹理数据

在着色器代码中读取纹理数据,您可以通过 texture 的 2Dglsl 函数访问纹理的像素数据。

当纹理坐标从(0,0)变为(1,1)时,图像会上下颠倒。同时,我正处于水平镜像图像中(就像用相机自拍一样)。

uniform sampler2D texture0;

void main() {
  vec2 coord = 1.0 - gl_FragCoord.xy / vec2(width, height);
  gl_FragColor = texture2D(texture1, coord);
}


访问网络摄像头

要从网络摄像头获取图像数据,我们可以使用 video 标签,并使用 getUserMediaAPI:

function accessWebcam(video) {
  return new Promise((resolve, reject) => {
    const mediaConstraints = { audio: false, video: { 
        width: 1280, 
        height: 720,
        brightness: {ideal: 2} 
      }
    };

    navigator.mediaDevices.getUserMedia(
      mediaConstraints).then(mediaStream => {
        video.srcObject = mediaStream;
        video.setAttribute('playsinline', true);
        video.onloadedmetadata = (e) => {
          video.play();
          resolve(video);
        }
      }).catch(err => {
        reject(err);
      });
    }
  );
}

// 使用说明:
// const video = await accessWebcam(document.querySelector('video'));
// or via promises:
// accessWebcam(document.querySelector('video')).then(video => { ... });

要访问网络摄像头,您可以使用 getUserMedia API 来访问网络摄像头,如上所述。


提供后备图像

如果用户阻止了对网络摄像头的访问,或者没有可用的网络摄像头,则可以提供一个备用图像供您使用。

我也将 new Image() 中的 onload 操作包装成一个 promise。

function loadImage(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.crossOrigin = 'Anonymous';
    img.src = url;
    img.onload = () => {
      resolve(img);
    };
    img.onerror = () => {
      reject(img);
    };
  });
}


合并全部操作

为了使事情变得容易一些,我将常用的 WebGL 函数放入了我创建的一个小助手库GLea中。

它初始化 WebGL 应用上下文,编译 WebGL 着色器代码,并为顶点着色器创建属性和缓冲区:

默认情况下,position 为顶点着色器提供一个属性,该属性带有一个缓冲区,该缓冲区包含 4 个 2D 坐标,覆盖整个屏幕上的 2 个三角形。

import GLea from 'glea.js';

const frag = ` ... `; // 片段着色器代码
const vert = ` ... `; // 顶点着色器代码
const glea = new GLea({
  shaders: [
    GLea.fragmentShader(frag),
    GLea.vertexShader(vert)
  ]
}).create();

function loop(time = 0) {
  const { gl, width, height } = glea;
  glea.clear();
  glea.uniV('resolution', [width, height]);
  glea.uni('time', time * 1e-3);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  requestAnimationFrame(loop);
}

window.addEventListener('resize', () => {
  glea.resize();
});
loop(0);


结论

基本上就是这样。我希望您喜欢阅读本文,并对自己探索 WebGL 感到好奇。我会在这里放一些资源。

如果我还有没介绍到的内容,请随时发表补充评论 =)。


参考资料

原文地址:Realtime video processing with WebGL
原文作者:Lea Rosema


本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://shenqiku.cn/article/FLY_9609