【零基础学WebGL】图像滤镜

371 阅读3分钟

图像滤镜

CSS3内置了丰富的图像滤镜函数(blur、brightness等),可以轻松地给一张图片应用特效。下面给一张图应用高斯模糊滤镜效果。

img {
    filter: blur(5px);
}

更多的css filter效果,可以CSS Filter

卷积

图像滤镜处理,背后的理论知识是卷积(convolution)。

图像的卷积,是一种二维矩阵的运算,左侧矩阵表示图像的像素,从左向右,自上而下排列;右侧矩阵表示权重集合,称做卷积核,矩阵的中的每个值表示对应图片像素的权重,卷积核矩阵通常是行列数相同,且为奇数,比如3x3、9x9。

图像卷积运算过程如下:从左向右,自上而下遍历图像矩阵,把卷积核的核心放在当前图像像素的位置,然后获取当前像素周围的像素值,与对应位置的卷积核矩阵元素相乘,然后把乘积相加,再除以权重和,最终结果即可当前像素经过卷积运算得到的新值。

下图,演示了原始矩阵中数值 2 的卷积计算过程:

(1 * 1 + 2 * 1 + 1 * 1 + 1 * 1 + 2 * 1 + 1 * 1 + 1 * 1 + 2 * 1 + 1 * 1) / 9

WebGL实践

接下来,使用WebGL对一张图片,进行模糊处理。模糊处理使用到卷积核中,所有权重值都是1,也就表示新的像素值,是原始像素值周边8个像素和像素本身的均值。

const blurKernel = [
    1, 1, 1,
    1, 1, 1,
    1, 1, 1,
  ];

周边像素

前文【零基础学WebGL】绘制图片,我们知道在片段着色器中,可以使用texture2D获取指定纹理坐标处的纹理像素值。

gl_FragColor = texture2D(u_image, v_texCoord); 

想获取某个纹理坐标周围位置的像素值,需要知道如何用纹理坐标表示一个像素大小的距离。

无论任何图片,在WebGL中的纹理坐标范围是 0.0 到 1.0。假设图片的宽度是width,高度是height,有如下结论:

const xOnePixel = 1 / width; // 横向1像素用纹理坐标表示
const yOnePixel = 1 / height; // 纵向1像素用纹理坐标表示

因此,我们修改片元着色器,新增vec2类型的uniform变量,表示纹理图像的原始大小,即可计算出1像素大小用纹理坐标表示的值。

const fragmentSource = `
  precision mediump float;

  uniform sampler2D u_image;
  uniform vec2 u_texture_size;

  varying vec2 v_texCoord;

  void main() {
    vec2 onePixel = vec2(1.0, 1.0) / u_texture_size;
  }`;


const render = (image: HTMLImageElement) => {
  ...

  const textureSizeLocation = gl.getUniformLocation(program, 'u_texture_size');
  gl.uniform2f(textureSizeLocation, image.width, image.height);

  ...
}

已知1像素的纹理坐标,求当前纹理坐标向左偏移一个像素距离的像素值:

vec4 offsetOnePixel = texture2D(u_image, v_texCoord + onePixel * vec2(-1,0));

卷积运算

接下来,就是向片元着色器传递卷积核,以及卷积核权重和。

在片元着色器代码里,新增float数组 u_kernel,表示卷积核;float变量u_kernel_weight,表示积核权重。

const fragmentSource = `
  precision mediump float;

  uniform sampler2D u_image;
  uniform vec2 u_texture_size;
  uniform float u_kernel[9];
  uniform float u_kernel_weight;

  varying vec2 v_texCoord;

  void main() {
    vec2 onePixel = vec2(1.0, 1.0) / u_texture_size;

  }`;

// 计算内核权重
const calcKernelWeight = (kernel) => {
  const weight = kernel.reduce((sum, item) => sum + item, 0);
  return weight <= 0 ? 1 : weight;
}

const render = (image: HTMLImageElement) => {
  ...

  const kernelLocation = gl.getUniformLocation(program, 'u_kernel');
  const blurKernel = [
    1, 1, 1,
    1, 1, 1,
    1, 1, 1,
  ];
  gl.uniform1fv(kernelLocation, blurKernel);

  const kernelWeightLocation = gl.getUniformLocation(program, 'u_kernel_weight');
  const weight = calcKernelWeight(blurKernel);
  gl.uniform1f(kernelWeightLocation, weight);

  ...
}

接下来,就需要着色器根据js提供的卷积核和权重,平滑计算图像像素3x3范围的卷积结果,作为新的像素值。

const fragmentSource = `
  precision mediump float;

  uniform sampler2D u_image;
  uniform vec2 u_texture_size;
  uniform float u_kernel[9];
  uniform float u_kernel_weight;

  varying vec2 v_texCoord;

  void main() {
    vec2 onePixel = vec2(1.0, 1.0) / u_texture_size;
    vec4 colorSum = texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +
                   texture2D(u_image, v_texCoord + onePixel * vec2(0, -1)) * u_kernel[1] +
                   texture2D(u_image, v_texCoord + onePixel * vec2(1, -1)) * u_kernel[2] +
                   texture2D(u_image, v_texCoord + onePixel * vec2(-1, 0)) * u_kernel[3] +
                   texture2D(u_image, v_texCoord + onePixel * vec2(0, 0)) * u_kernel[4] +
                   texture2D(u_image, v_texCoord + onePixel * vec2(1, 0)) * u_kernel[5] +
                   texture2D(u_image, v_texCoord + onePixel * vec2(-1, 1)) * u_kernel[6] +
                   texture2D(u_image, v_texCoord + onePixel * vec2(0, 1)) * u_kernel[7] +
                   texture2D(u_image, v_texCoord + onePixel * vec2(1, 1)) * u_kernel[8];
    gl_FragColor = vec4(colorSum.rgb / u_kernel_weight, 1.0);
  }`;

本文完整代码,可以参考webgl-filter

整个图像滤镜处理过程,实际上并不复杂,主要涉及到一些矩阵的运算。如果想了解更多的图像滤镜效果,可以见WebGL图像处理