WEBKT

使用 WebAssembly 和 WebGL 实现 Web 应用实时视频流图像滤镜

332 0 0 0

本文将深入探讨如何利用 WebAssembly (Wasm) 和 WebGL 技术,在 Web 应用程序中实现对实时视频流进行高效的图像滤镜处理。我们将涵盖从视频流捕获、Wasm 图像处理模块构建,到 WebGL 渲染的整个流程,并提供关键代码示例和性能优化建议。

1. 技术选型:WebAssembly 和 WebGL

  • WebAssembly (Wasm): 一种为在 Web 浏览器中运行高性能应用程序而设计的二进制指令格式。相较于 JavaScript,Wasm 具有近乎原生的执行速度,非常适合计算密集型的图像处理任务。我们可以使用 C/C++、Rust 等语言编写图像处理算法,然后编译成 Wasm 模块,在浏览器中高效运行。
  • WebGL: 一种 JavaScript API,用于在任何兼容的 Web 浏览器中渲染交互式 2D 和 3D 图形,无需使用插件。WebGL 允许我们利用 GPU 的强大并行计算能力,快速将处理后的图像数据渲染到 Canvas 上,实现流畅的实时视频显示。

2. 视频流捕获

首先,我们需要使用 getUserMedia API 获取用户的摄像头视频流。以下是一个简单的示例:

async function getVideo()
{
    try
    {
        const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 }, audio: false });
        const videoElement = document.getElementById('myVideo');
        videoElement.srcObject = stream;
        videoElement.play();
        return videoElement;
    } catch (err) {
        console.error("Error accessing the camera: ", err);
    }
}

这段代码会请求用户授权访问摄像头,并将视频流绑定到一个 <video> 元素上。你需要一个 HTML <video> 元素,例如 <video id="myVideo" width="640" height="480" autoplay muted></video>

3. WebAssembly 图像处理模块构建

接下来,我们需要创建一个 WebAssembly 模块来处理图像数据。这里以 C++ 为例,演示一个简单的灰度滤镜:

// gray_filter.cpp
#include <iostream>

extern "C" {
    void gray_filter(unsigned char* data, int width, int height) {
        for (int i = 0; i < width * height * 4; i += 4) {
            unsigned char r = data[i];
            unsigned char g = data[i + 1];
            unsigned char b = data[i + 2];
            unsigned char gray = (r + g + b) / 3;
            data[i] = gray;
            data[i + 1] = gray;
            data[i + 2] = gray;
        }
    }
}

使用 Emscripten 将 C++ 代码编译为 WebAssembly 模块:

emcc gray_filter.cpp -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS=['cwrap'] -o gray_filter.js

这条命令会生成 gray_filter.jsgray_filter.wasm 两个文件。gray_filter.js 是一个 JavaScript 胶水代码,用于加载和调用 Wasm 模块。

4. WebGL 渲染

现在,我们需要使用 WebGL 将处理后的图像数据渲染到 Canvas 上。以下是一个基本的 WebGL 渲染流程:

  1. 获取 Canvas 元素和 WebGL 上下文:

    const canvas = document.getElementById('myCanvas');
    const gl = canvas.getContext('webgl');
    if (!gl) {
        console.error('WebGL not supported!');
    }
    

    你需要一个 HTML <canvas> 元素,例如 <canvas id="myCanvas" width="640" height="480"></canvas>

  2. 创建 Shader 程序:

    我们需要顶点 Shader 和片段 Shader。顶点 Shader 负责处理顶点位置,片段 Shader 负责处理像素颜色。

    • 顶点 Shader:

      // vertexShader.glsl
      attribute vec4 aVertexPosition;
      attribute vec2 aTextureCoord;
      varying highp vec2 vTextureCoord;
      void main() {
          gl_Position = aVertexPosition;
          vTextureCoord = aTextureCoord;
      }
      
    • 片段 Shader:

      // fragmentShader.glsl
      varying highp vec2 vTextureCoord;
      uniform sampler2D uSampler;
      void main() {
          gl_FragColor = texture2D(uSampler, vTextureCoord);
      }
      

    创建 Shader 程序的 JavaScript 代码:

    function createShaderProgram(gl, vertexShaderSource, fragmentShaderSource) {
        const vertexShader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertexShader, vertexShaderSource);
        gl.compileShader(vertexShader);
    
        const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragmentShader, fragmentShaderSource);
        gl.compileShader(fragmentShader);
    
        const shaderProgram = gl.createProgram();
        gl.attachShader(shaderProgram, vertexShader);
        gl.attachShader(shaderProgram, fragmentShader);
        gl.linkProgram(shaderProgram);
    
        return shaderProgram;
    }
    
  3. 创建 Buffer 和 Texture:

    我们需要创建 Buffer 来存储顶点位置和纹理坐标,并创建一个 Texture 来存储图像数据。

    function createBuffer(gl, data) {
        const buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
        return buffer;
    }
    
    function createTexture(gl, width, height) {
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        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);
        return texture;
    }
    
  4. 渲染循环:

    在渲染循环中,我们需要从 Video 元素获取图像数据,使用 WebAssembly 处理图像数据,然后将处理后的图像数据更新到 Texture 中,并使用 WebGL 渲染到 Canvas 上。

    async function renderLoop(gl, shaderProgram, videoElement, wasmModule) {
        const width = videoElement.videoWidth;
        const height = videoElement.videoHeight;
        const canvas = gl.canvas;
        canvas.width = width;
        canvas.height = height;
    
        const texture = createTexture(gl, width, height);
        const frameBufferData = new Uint8Array(width * height * 4);
    
        const grayFilter = wasmModule.cwrap('gray_filter', null, ['number', 'number', 'number']);
    
        function update() {
            gl.bindTexture(gl.TEXTURE_2D, texture);
            gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, frameBufferData);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, frameBufferData);
    
            gl.bindTexture(gl.TEXTURE_2D, texture);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, videoElement);
    
            grayFilter(frameBufferData.byteOffset, width, height);
    
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, frameBufferData);
    
            gl.clearColor(0.0, 0.0, 0.0, 1.0);
            gl.clear(gl.COLOR_BUFFER_BIT);
    
            gl.drawArrays(gl.TRIANGLES, 0, 6);
    
            requestAnimationFrame(update);
        }
    
        update();
    }
    

5. 完整代码示例

以下是一个完整的代码示例,演示了如何使用 WebAssembly 和 WebGL 实现实时视频流灰度滤镜:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>WebAssembly + WebGL Real-time Video Filter</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
    <video id="myVideo" width="640" height="480" autoplay muted></video>
    <canvas id="myCanvas" width="640" height="480"></canvas>
    <script src="gray_filter.js"></script>
    <script>
        async function main() {
            const videoElement = await getVideo();
            const canvas = document.getElementById('myCanvas');
            const gl = canvas.getContext('webgl');

            if (!gl) {
                console.error('WebGL not supported!');
                return;
            }

            const vertexShaderSource = `
                attribute vec4 aVertexPosition;
                attribute vec2 aTextureCoord;
                varying highp vec2 vTextureCoord;
                void main() {
                    gl_Position = aVertexPosition;
                    vTextureCoord = aTextureCoord;
                }
            `;

            const fragmentShaderSource = `
                varying highp vec2 vTextureCoord;
                uniform sampler2D uSampler;
                void main() {
                    gl_FragColor = texture2D(uSampler, vTextureCoord);
                }
            `;

            const createShaderProgram = (gl, vertexShaderSource, fragmentShaderSource) => {
                const vertexShader = gl.createShader(gl.VERTEX_SHADER);
                gl.shaderSource(vertexShader, vertexShaderSource);
                gl.compileShader(vertexShader);

                const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
                gl.shaderSource(fragmentShader, fragmentShaderSource);
                gl.compileShader(fragmentShader);

                const shaderProgram = gl.createProgram();
                gl.attachShader(shaderProgram, vertexShader);
                gl.attachShader(shaderProgram, fragmentShader);
                gl.linkProgram(shaderProgram);

                return shaderProgram;
            };

            const shaderProgram = createShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
            gl.useProgram(shaderProgram);

            const positions = [
                1.0, 1.0,
                -1.0, 1.0,
                1.0, -1.0,
                -1.0, 1.0,
                1.0, -1.0,
                -1.0, -1.0
            ];

            const textureCoordinates = [
                1.0, 1.0,
                0.0, 1.0,
                1.0, 0.0,
                0.0, 1.0,
                1.0, 0.0,
                0.0, 0.0
            ];

            const createBuffer = (gl, data) => {
                const buffer = gl.createBuffer();
                gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
                gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
                return buffer;
            };

            const positionBuffer = createBuffer(gl, positions);
            const textureCoordBuffer = createBuffer(gl, textureCoordinates);

            const aVertexPosition = gl.getAttribLocation(shaderProgram, 'aVertexPosition');
            gl.enableVertexAttribArray(aVertexPosition);
            gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
            gl.vertexAttribPointer(aVertexPosition, 2, gl.FLOAT, false, 0, 0);

            const aTextureCoord = gl.getAttribLocation(shaderProgram, 'aTextureCoord');
            gl.enableVertexAttribArray(aTextureCoord);
            gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
            gl.vertexAttribPointer(aTextureCoord, 2, gl.FLOAT, false, 0, 0);

            const uSampler = gl.getUniformLocation(shaderProgram, 'uSampler');
            gl.uniform1i(uSampler, 0);

            const createTexture = (gl, width, height) => {
                const texture = gl.createTexture();
                gl.bindTexture(gl.TEXTURE_2D, texture);
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
                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);
                return texture;
            };

            Module.onRuntimeInitialized = () => {
                renderLoop(gl, shaderProgram, videoElement, Module);
            };

            async function renderLoop(gl, shaderProgram, videoElement, wasmModule) {
                const width = videoElement.videoWidth;
                const height = videoElement.videoHeight;
                const canvas = gl.canvas;
                canvas.width = width;
                canvas.height = height;

                const texture = createTexture(gl, width, height);
                const frameBufferData = new Uint8Array(width * height * 4);

                const grayFilter = wasmModule.cwrap('gray_filter', null, ['number', 'number', 'number']);

                function update() {
                    gl.bindTexture(gl.TEXTURE_2D, texture);
                    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, videoElement);

                    gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, frameBufferData);
                    grayFilter(frameBufferData.byteOffset, width, height);

                    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, frameBufferData);

                    gl.clearColor(0.0, 0.0, 0.0, 1.0);
                    gl.clear(gl.COLOR_BUFFER_BIT);

                    gl.drawArrays(gl.TRIANGLES, 0, 6);

                    requestAnimationFrame(update);
                }

                update();
            }
        }

        async function getVideo() {
            try {
                const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 }, audio: false });
                const videoElement = document.getElementById('myVideo');
                videoElement.srcObject = stream;
                videoElement.play();
                return videoElement;
            } catch (err) {
                console.error("Error accessing the camera: ", err);
            }
        }

        main();
    </script>
</body>
</html>

注意:

  • 确保 gray_filter.jsgray_filter.wasm 文件与 HTML 文件在同一目录下。
  • 你可能需要一个本地服务器来运行此示例,因为某些浏览器不允许从本地文件系统访问摄像头。

6. 性能优化

  • 减少数据拷贝: 尽量避免在 JavaScript 和 WebAssembly 之间频繁拷贝数据。可以使用共享内存来减少数据拷贝的开销。
  • 优化 WebAssembly 代码: 使用编译器优化选项 (例如 -O3) 来提高 WebAssembly 代码的执行效率。
  • 利用 GPU 并行计算: 将更多的图像处理任务交给 WebGL 处理,充分利用 GPU 的并行计算能力。
  • 降低视频分辨率: 降低视频分辨率可以减少图像处理的计算量,提高帧率。
  • 使用 OffscreenCanvas: 使用 OffscreenCanvas 可以将 WebGL 渲染放到后台线程中,避免阻塞主线程。

7. 总结

本文介绍了如何使用 WebAssembly 和 WebGL 实现 Web 应用实时视频流图像滤镜。通过将计算密集型的图像处理任务交给 WebAssembly 处理,并利用 WebGL 的 GPU 加速渲染能力,我们可以实现高效、流畅的实时视频滤镜效果。希望本文能帮助你理解 WebAssembly 和 WebGL 的工作原理,并将其应用到你的 Web 应用程序中。

码农小李 WebAssemblyWebGL图像滤镜

评论点评