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

使用盒模糊模拟高斯模糊

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

盒滤波 Box filter

卷积核的每一点权重都是一样的,所以也称作平均滤波(Averaging filter)。一个边长为 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)
  })
}

参考