在前端网页中对图片进行滤镜处理,常见的方案是直接使用 CSS 滤镜属性(如 filter: blur(5px) contrast(150%))。然而,CSS 滤镜只能作用于 DOM 的视觉渲染层,无法提取处理后的具体像素数据(Pixel Data),也无法实现自定义数学卷积核(如 Sobel 边缘检测、Custom Convolution Matrix)。

本站 图片滤镜实验室 基于 HTML5 Canvas 2D ImageData API 与纯 TypeScript,实现了像素级的空间域图像卷积与颜色矩阵运算。

这篇文章我们将拆解图像卷积的数学原理,以及如何在浏览器中通过高效遍历 Uint8ClampedArray 字节流实现高性能图像滤镜。


1. 空间域图像卷积(Spatial Domain Convolution)原理

图像在数字表达上是一个二维像素矩阵(多通道彩图中为 RGB 或 RGBA 三维张量)。空间域卷积(Spatial Convolution) 是指使用一个小型固定尺寸的权重矩阵——卷积核(Convolution Kernel / Mask),在输入图像的每个像素点及其邻域上滑动,计算加权求和并生成新像素的过程。

对于位置在 (x,y)(x, y) 的像素点,应用 3×33 \times 3 卷积核 K\mathbf{K} 的离散卷积公式为:

g(x,y)=i=11j=11f(x+i,y+j)K(i+1,j+1)g(x, y) = \sum_{i=-1}^{1} \sum_{j=-1}^{1} f(x + i, y + j) \cdot \mathbf{K}(i+1, j+1)

其中 f(x,y)f(x, y) 为原始像素通道强度,g(x,y)g(x, y) 为卷积后得到的新通道强度。为了保证图像的总亮度不变,通常要求卷积核的所有权重之和等于 1(归一化处理):

ijK(i,j)=1\sum_{i} \sum_{j} \mathbf{K}(i, j) = 1


2. 三大经典卷积算子推导

1. 高斯模糊算子(Gaussian Blur)

高斯模糊利用二维正态分布概率密度函数计算邻域像素权重:

G(x,y)=12πσ2ex2+y22σ2G(x, y) = \frac{1}{2\pi \sigma^2} e^{-\frac{x^2 + y^2}{2\sigma^2}}

常用的离散化近似 3×33 \times 3 高斯卷积核如下(总权重和为 16):

KGaussian=116[121242121]\mathbf{K}_{\text{Gaussian}} = \frac{1}{16} \begin{bmatrix} 1 & 2 & 1 \\ 2 & 4 & 2 \\ 1 & 2 & 1 \end{bmatrix}

由于权重沿中心对称,高斯模糊能够非常自然地平滑图像噪音,同时保留宏观边缘。

2. 索贝尔边缘检测算子(Sobel Edge Detector)

Sobel 算子通过计算图像灰度强度的空间梯度向量近感值来识别边缘。它由水平(Gx\mathbf{G}_x)和垂直(Gy\mathbf{G}_y)两个卷积核组成:

Gx=[10+120+210+1],Gy=[+1+2+1000121]\mathbf{G}_x = \begin{bmatrix} -1 & 0 & +1 \\ -2 & 0 & +2 \\ -1 & 0 & +1 \end{bmatrix}, \quad \mathbf{G}_y = \begin{bmatrix} +1 & +2 & +1 \\ 0 & 0 & 0 \\ -1 & -2 & -1 \end{bmatrix}

合成梯度幅值 GG 规定为:

G=Gx2+Gy2G = \sqrt{\mathbf{G}_x^2 + \mathbf{G}_y^2}

梯度强度越高的区域,说明颜色变化越剧烈,在输出图中便呈现为亮线(边缘)。

3. 拉普拉斯锐化算子(Sharpening)

锐化通过拉普拉斯算子突出像素与其相邻像素的差值:

KSharpen=[010151010]\mathbf{K}_{\text{Sharpen}} = \begin{bmatrix} 0 & -1 & 0 \\ -1 & 5 & -1 \\ 0 & -1 & 0 \end{bmatrix}


3. 基于 ImageData 的 TypedArray 高性能遍历

在 HTML5 Canvas 2D 中,图像数据通过 ctx.getImageData(0, 0, width, height) 获取。返回的 ImageData.data 是一个扁平的一维 Uint8ClampedArray 数组,每个像素占用 4 个连续的字节:[R, G, B, A, R, G, B, A, ...]

对于分辨率为 W×HW \times H 的图像,位于第 yy 行、第 xx 列的像素起始偏移量为:

Index(x,y)=(yW+x)4\text{Index}(x, y) = (y \cdot W + x) \cdot 4

以下为纯 TypeScript 实现的高性能通用 3x3 图像卷积引擎:

/**
 * 对 ImageData 执行 3x3 空间域图像卷积
 */
export function applyConvolution3x3(
	srcData: ImageData,
	kernel: number[],
	factor = 1.0,
	bias = 0.0
): ImageData {
	const width = srcData.width;
	const height = srcData.height;
	const src = srcData.data;

	// 创建输出 ImageData 实例
	const output = new ImageData(width, height);
	const dst = output.data;

	// 遍历图像内部像素(边缘采用镜像扩展处理)
	for (let y = 0; y < height; y++) {
		for (let x = 0; x < width; x++) {
			let r = 0, g = 0, b = 0;

			// 应用 3x3 卷积核
			for (let ky = -1; ky <= 1; ky++) {
				const py = Math.min(Math.max(y + ky, 0), height - 1);
				for (let kx = -1; kx <= 1; kx++) {
					const px = Math.min(Math.max(x + kx, 0), width - 1);
					const weight = kernel[(ky + 1) * 3 + (kx + 1)];
					const offset = (py * width + px) * 4;

					r += src[offset] * weight;
					g += src[offset + 1] * weight;
					b += src[offset + 2] * weight;
				}
			}

			const dstOffset = (y * width + x) * 4;
			// 结果缩放、偏置及 Uint8 截断
			dst[dstOffset]     = Math.min(Math.max(r * factor + bias, 0), 255);
			dst[dstOffset + 1] = Math.min(Math.max(g * factor + bias, 0), 255);
			dst[dstOffset + 2] = Math.min(Math.max(b * factor + bias, 0), 255);
			dst[dstOffset + 3] = src[dstOffset + 3]; // 保持 Alpha 透明通道不变
		}
	}

	return output;
}

4. 总结与性能优化体验

通过手写卷积与 TypedArray 字节流直接操作,我们实现了像素级的自由度控制——用户既可以微调任意 3x3 / 5x5 矩阵参数,也可以将多层卷积链式叠加。

欢迎前往本站 图片滤镜实验室,探索纯前端像素矩阵卷积的强大威力。