在图像处理软件中的模糊滤镜一般都会有高斯模糊(Gaussian Blur),因为它效果最好,接近人眼的模糊效果(也许是由于正态分布的无处不在?)。但对图像做真正的高斯模糊(在我的理解,也即使用满足二阶正态分布的卷积核对二维离散分布的空间域做平滑处理),由于卷积的定义,计算量颇大。可以采用一些快速的算法去模拟这个效果。

使用盒模糊模拟高斯模糊

根据 Photopea 作者的这个 JS 实现论文 《Fast Almost-Gaussian Filtering》中的论证,通过对图像进行多次盒模糊操作,可模拟高斯模糊的效果。设盒模糊次数为 n,当 n = 5 时,模拟效果已足够好。

盒滤波 Box filter

平均滤波(Average filter),或称均值滤波(Mean filter)卷积核的每一点权重都是一样的,2 维情况下就像在图像上扣着的一个方盒子,所以也称作盒滤波。一个边长为 3 的二维盒滤波的卷积核如下


$$ \left[ \begin{array}{lll}{1} & {1} & {1} \\ {1} & {1} & {1} \\ {1} & {1} & {1}\end{array}\right] $$

为什么使用盒滤波呢?除了实现简单以外,还有性能上的考量。由于权重相同,使用盒滤波时有一个可爱的特性能使得计算变得更加快速:对图像在水平方向进行一次一维平均滤波,再在垂直方向进行一次,等价于对整个图片做一次二维盒滤波。

论文的大概操作思路

论文的论证目标基本上为:需要执行多少次盒滤波,以及每一次的滤波宽度需要是多少,才能够模拟出近似于高斯滤波器的标准差

首先盒滤波有一些特性:对于图像经过 n 次盒滤波后,标准差如下,其中 w 为滤波器宽度。


$$ \sigma_{n a v}=\sqrt{\frac{n w^{2}-n}{12}} $$

那么为了使得标准差与高斯滤波相同,理想的滤波器宽度 wideal 求法:


$$ w_{i d e a l}=\sqrt{\frac{12 \sigma^{2}}{n}+1} $$

对于图像滤波来说,w 需要是整数,且最好是奇数,如此一来总会有一个中心点的像素值可以被指定。于是在理想宽度附近找到两个奇数,wl < wideal < wu,分别为下限(l)和上限(u),显然 wl + 2 = wu。接下来要进行 n 次平均滤波,设 c 为当前滤波的轮数,从 1 开始,在 0 < c <  = m 时,滤波器宽度为 wl,在 m < c <  = n 时,滤波器宽度为 wu


$$ \begin{aligned} \sigma &=\sqrt{\frac{m w_{l}^{2}+(n-m) w_{u}^{2}-n}{12}} \\ &=\sqrt{\frac{m w_{l}^{2}+(n-m)\left(w_{l}+2\right)^{2}-n}{12}} \end{aligned} $$

因此算出 m:


$$ m=\frac{12 \sigma^{2}-n w_{l}^{2}-4 n w_{l}-3 n}{-4 w_{l}-4} $$

开发和使用 WebAssembly

我们基于 github 上用 rust 写的一个实现,继续填充一些细节,完成了 fastblur 这个模块。

考虑到中间涉及大量运算,使用 WebAssembly 应该比纯 js 更快点。使用 rust 和 image crate 使得算法验证和调试输出能快速进行,同时 rust 有着目前编译到 WebAssembly 最佳的工具链 wasm-pack(毕竟这俩都是 Mozilla 在积极推行的标准)。

在 Typescript + Webpack 项目中引入和使用

确保 tsconfig.json 中的 compilerOptions.module: esnext,才能方便地使用 import().then()

export function applyFastBlur(imageData: ImageData, blurRadius: number): Bluebird<ImageData> {
return new Promise((resolve, reject) => {
import('@bestminr/fastblur')
.then((m) => {
const { width, height } = imageData
const inputDataArr = new Uint8Array(imageData.data)
m.do_fast_blur(inputDataArr, width, height, blurRadius)
const outputImageData = new ImageData(new Uint8ClampedArray(inputDataArr), width, height)
return resolve(outputImageData)
}).catch(reject)
})
}

参考