图像滤镜
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图像处理。