最快的高斯模糊(线性时间)Fastest Gaussian Blur (in linear time)

本文介绍了一种快速实现高斯模糊效果的方法,并通过四种不同的算法进行了对比分析,最终实现了高效且接近真实的高斯模糊效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

I needed really fast Gaussian blur for one of my projects. After hours of struggling and browsing the internet, I finally found the best solution.

Beginning

My solution is based on Fast image convolutions by Wojciech Jarosz. Presented ideas are very simple and I don't know who is the original author. I am going to describe it a little better and add some mathematics. To get motivated, take a glance at the results. I have implemented this code into Photopea under Filter - Blur - Gaussian Blur.

Definition

The convolution of two 2D functions  f f and  g g is defined as the volume of product of  f f and "shifted"  g g. The second function  g g is sometimes called "weight", since it determines, how much of  f f will get into the result

The Gaussian blur of a 2D function can be defined as a convolution of that function with 2DGaussian function. Our gaussian function has an integral 1 (volume under surface) and is uniquely defined by one parameter  σ σ called standard deviation. We will also call it "radius" in the text below.

In our discrete finite case, we represent our 2D functions as matrices of values. We compute the volume (integral) as a sum. Gaussian function has near to zero values behind some radius, so we will use only the values  rxr,ryr −r≤x≤r,−r≤y≤r. This "useful" part of weight is also called the kernel.The value of convolution at [i, j] is the weighted average, i. e. sum of function values around [i, j] multiplied by weight.

Algorithm 1

For a general discrete convolution of  f f and weight function  w w, we can compute the result  b bas:

b[i,j]=y=iri+rx=jrj+rf[y,x]w[y,x] b[i,j]=∑y=i−ri+r∑x=j−rj+rf[y,x]∗w[y,x]

For gaussian weight, we can compute only weights around [i, j] (area of  4r2 4⋅r2). When our matrix has  n n values, the time complexity is  O(nr2) O(n⋅r2). For large radii, e. g.  r=10 r=10, we have to do  n400 n∗400 operations, which correspond to 400 loops over the whole matrix and that is ugly.

// source channel, target channel, width, height, radius
function gaussBlur_1 (scl, tcl, w, h, r) {
    var rs = Math.ceil(r * 2.57);     // significant radius
    for(var i=0; i<h; i++)
        for(var j=0; j<w; j++) {
            var val = 0, wsum = 0;
            for(var iy = i-rs; iy<i+rs+1; iy++)
                for(var ix = j-rs; ix<j+rs+1; ix++) {
                    var x = Math.min(w-1, Math.max(0, ix));
                    var y = Math.min(h-1, Math.max(0, iy));
                    var dsq = (ix-j)*(ix-j)+(iy-i)*(iy-i);
                    var wght = Math.exp( -dsq / (2*r*r) ) / (Math.PI*2*r*r);
                    val += scl[y*w+x] * wght;  wsum += wght;
                }
            tcl[i*w+j] = Math.round(val/wsum);            
        }
}

Algorithm 2

Let's introduce the box blur. It is the convolution of function  f f and weight  w w, but weight is constant and lies within a square (box). The nice feature of box blur is, that when you have some weight function having the same variance, it converges to gaussian blur after several passes.

In this algorithm, we will simulate the gaussian blur with 3 passes of box blur. Let's denote the half of size of square as  br br ("box radius"). The constant value of weight is  1/(2br)2 1/(2⋅br)2 (so the sum over the whole weight is 1). We can define box blur as:

bb[i,j]=y=ibri+brx=jbrj+brf[y,x]/(2br)2 bb[i,j]=∑y=i−bri+br∑x=j−brj+brf[y,x]/(2⋅br)2

We have to convert the standard deviation of gaussian blur  r r into dimensions of boxes for box blur. I am not very good at calculus, but fortunatelly I have found this website and used their implementation.

function boxesForGauss(sigma, n)  // standard deviation, number of boxes
{
    var wIdeal = Math.sqrt((12*sigma*sigma/n)+1);  // Ideal averaging filter width 
    var wl = Math.floor(wIdeal);  if(wl%2==0) wl--;
    var wu = wl+2;
				
    var mIdeal = (12*sigma*sigma - n*wl*wl - 4*n*wl - 3*n)/(-4*wl - 4);
    var m = Math.round(mIdeal);
    // var sigmaActual = Math.sqrt( (m*wl*wl + (n-m)*wu*wu - n)/12 );
				
    var sizes = [];  for(var i=0; i<n; i++) sizes.push(i<m?wl:wu);
    return sizes;
}

Our algorithm has still the same complexity  O(nr2) O(n⋅r2), but it has two advantages: first, the area is much smaller ( br br is almost equal to  σ σ, while significant radius for gaussian is much larger). The second advantage is, that the weight is constant. Even though we have to do it 3 times, it performs faster.

function gaussBlur_2 (scl, tcl, w, h, r) {
    var bxs = boxesForGauss(r, 3);
    boxBlur_2 (scl, tcl, w, h, (bxs[0]-1)/2);
    boxBlur_2 (tcl, scl, w, h, (bxs[1]-1)/2);
    boxBlur_2 (scl, tcl, w, h, (bxs[2]-1)/2);
}
function boxBlur_2 (scl, tcl, w, h, r) {
    for(var i=0; i<h; i++)
        for(var j=0; j<w; j++) {
            var val = 0;
            for(var iy=i-r; iy<i+r+1; iy++)
                for(var ix=j-r; ix<j+r+1; ix++) {
                    var x = Math.min(w-1, Math.max(0, ix));
                    var y = Math.min(h-1, Math.max(0, iy));
                    val += scl[y*w+x];
                }
            tcl[i*w+j] = val/((r+r+1)*(r+r+1));
        }
}

Algorithm 3

We have already simplified gaussian blur into 3 passes of box blur. Let's do a little experiment. Let's define a horizontal blur and total blur:

bh[i,j]=x=jbrj+brf[i,x]/(2br)bt[i,j]=y=jbrj+brbh[y,j]/(2br) bh[i,j]=∑x=j−brj+brf[i,x]/(2⋅br)bt[i,j]=∑y=j−brj+brbh[y,j]/(2⋅br)

Those two functions are "looping" in a line, producing "one-dimensional blur". Let's see, what total blur corresponds to:

bt[i,j]=y=ibri+brbh[y,j]/(2br)=y=jbrj+br(x=jbrj+brf[y,x]/(2br))/(2br)=y=ibri+brx=jbrj+brf[y,x]/(2br)2 bt[i,j]=∑y=i−bri+brbh[y,j]/(2⋅br)=∑y=j−brj+br(∑x=j−brj+brf[y,x]/(2⋅br))/(2⋅br)=∑y=i−bri+br∑x=j−brj+brf[y,x]/(2⋅br)2

We just discovered, that our total blur is box blur! Both total blur and horizontal blur have a complexity  O(nr) O(n⋅r), so the whole box blur has  O(nr) O(n⋅r).

function gaussBlur_3 (scl, tcl, w, h, r) {
    var bxs = boxesForGauss(r, 3);
    boxBlur_3 (scl, tcl, w, h, (bxs[0]-1)/2);
    boxBlur_3 (tcl, scl, w, h, (bxs[1]-1)/2);
    boxBlur_3 (scl, tcl, w, h, (bxs[2]-1)/2);
}
function boxBlur_3 (scl, tcl, w, h, r) {
    for(var i=0; i<scl.length; i++) tcl[i] = scl[i];
    boxBlurH_3(tcl, scl, w, h, r);
    boxBlurT_3(scl, tcl, w, h, r);
}
function boxBlurH_3 (scl, tcl, w, h, r) {
    for(var i=0; i<h; i++)
        for(var j=0; j<w; j++)  {
            var val = 0;
            for(var ix=j-r; ix<j+r+1; ix++) {
                var x = Math.min(w-1, Math.max(0, ix));
                val += scl[i*w+x];
            }
            tcl[i*w+j] = val/(r+r+1);
        }
}   
function boxBlurT_3 (scl, tcl, w, h, r) {
    for(var i=0; i<h; i++)
        for(var j=0; j<w; j++) {
            var val = 0;
            for(var iy=i-r; iy<i+r+1; iy++) {
                var y = Math.min(h-1, Math.max(0, iy));
                val += scl[y*w+j];
            }
            tcl[i*w+j] = val/(r+r+1);
        }
}

Algorithm 4

One-dimensional blur can be computed even faster. E. g. we want to compute horizontal blur. We compute  bh[i,j],bh[i,j+1],bh[i,j+2],... bh[i,j],bh[i,j+1],bh[i,j+2],.... But the neighboring values  bh[i,j] bh[i,j] and  bh[i,j+1] bh[i,j+1]are almost the same. The only difference is in one left-most value and one right-most value. So  bh[i,j+1]=bh[i,j]+f[i,j+r+1]f[i,jr] bh[i,j+1]=bh[i,j]+f[i,j+r+1]−f[i,j−r].

In our new algorithm, we will compute the one-dimensional blur by creating the accumulator. First, we put the value of left-most cell into it. Then we will compute next values just by editing the previous value in constant time. This 1D blur has the complexity  O(n) O(n)(independent on  r r). But it is performed twice to get box blur, which is performed 3 times to get gaussian blur. So the complexity of this gaussian blur is 6 *  O(n) O(n).

function gaussBlur_4 (scl, tcl, w, h, r) {
    var bxs = boxesForGauss(r, 3);
    boxBlur_4 (scl, tcl, w, h, (bxs[0]-1)/2);
    boxBlur_4 (tcl, scl, w, h, (bxs[1]-1)/2);
    boxBlur_4 (scl, tcl, w, h, (bxs[2]-1)/2);
}
function boxBlur_4 (scl, tcl, w, h, r) {
    for(var i=0; i<scl.length; i++) tcl[i] = scl[i];
    boxBlurH_4(tcl, scl, w, h, r);
    boxBlurT_4(scl, tcl, w, h, r);
}
function boxBlurH_4 (scl, tcl, w, h, r) {
    var iarr = 1 / (r+r+1);
    for(var i=0; i<h; i++) {
        var ti = i*w, li = ti, ri = ti+r;
        var fv = scl[ti], lv = scl[ti+w-1], val = (r+1)*fv;
        for(var j=0; j<r; j++) val += scl[ti+j];
        for(var j=0  ; j<=r ; j++) { val += scl[ri++] - fv       ;   tcl[ti++] = Math.round(val*iarr); }
        for(var j=r+1; j<w-r; j++) { val += scl[ri++] - scl[li++];   tcl[ti++] = Math.round(val*iarr); }
        for(var j=w-r; j<w  ; j++) { val += lv        - scl[li++];   tcl[ti++] = Math.round(val*iarr); }
    }
}
function boxBlurT_4 (scl, tcl, w, h, r) {
    var iarr = 1 / (r+r+1);
    for(var i=0; i<w; i++) {
        var ti = i, li = ti, ri = ti+r*w;
        var fv = scl[ti], lv = scl[ti+w*(h-1)], val = (r+1)*fv;
        for(var j=0; j<r; j++) val += scl[ti+j*w];
        for(var j=0  ; j<=r ; j++) { val += scl[ri] - fv     ;  tcl[ti] = Math.round(val*iarr);  ri+=w; ti+=w; }
        for(var j=r+1; j<h-r; j++) { val += scl[ri] - scl[li];  tcl[ti] = Math.round(val*iarr);  li+=w; ri+=w; ti+=w; }
        for(var j=h-r; j<h  ; j++) { val += lv      - scl[li];  tcl[ti] = Math.round(val*iarr);  li+=w; ti+=w; }
    }
}

Results

I was testing all 4 algorithms on an image below (4 channels, 800x200 pixels). Here are the results:

Algorithm Time, r=5 Time, r=10 Time complexity
Algorithm 1 7 077 ms 27 021 ms O(nr2) O(n⋅r2)
Algorithm 1 (pre-computed weight) 2 452 ms 8 990 ms O(nr2) O(n⋅r2)
Algorithm 2 586 ms 2 437 ms O(nr2) O(n⋅r2)
Algorithm 3 230 ms 435 ms O(nr) O(n⋅r)
Algorithm 4 32 ms 34 ms O(n) O(n)

Note, that Alg 1 is computing the true Gaussian blur using gaussian kernel, while Alg 2,3,4 are only approximating it with 3 passes of box blur. The difference between Alg 2,3,4 is in complexity of computing box blur, their outputs are the same.

Original image

Perfect Gaussian Blur (Algorithm 1)

Algorithm 2, 3, 4, average error per pixel: 0.04 %

Blur by Adobe Photoshop, average error per pixel: 0.09%



from: http://blog.ivank.net/fastest-gaussian-blur.html

### 高斯相关概念及其编程实现 #### 什么是高斯分布? 高斯分布(Gaussian Distribution),也被称为正态分布,是一种连续概率分布,在统计学和数据科学领域具有重要地位。它由均值 \( \mu \) 和标准差 \( \sigma \) 定义,其概率密度函数可以表示为: \[ f(x|\mu,\sigma^2)=\frac{1}{\sqrt{2\pi}\sigma}e^{-\frac{(x-\mu)^2}{2\sigma^2}} \] 其中,\( \mu \) 是分布的中心位置,而 \( \sigma \) 则决定了分布的宽度[^4]。 #### 编程中的高斯随机数生成 在许多编程语言中,可以通过内置库轻松生成服从高斯分布的随机数。以下是 Python 中的一个例子,展示如何利用 `numpy` 库生成一组高斯分布的数据样本。 ```python import numpy as np import matplotlib.pyplot as plt # 设置参数 mu=0, sigma=1 mu, sigma = 0, 1 # 使用 NumPy 的 random.normal 函数生成 1000 个样本 samples = np.random.normal(mu, sigma, 1000) # 绘制直方图 plt.hist(samples, bins=30, density=True, alpha=0.6, color='g') # 添加理论曲线 xmin, xmax = plt.xlim() x = np.linspace(xmin, xmax, 100) p = (1 / (np.sqrt(2 * np.pi) * sigma)) * np.exp(-((x - mu)**2) / (2 * sigma**2)) plt.plot(x, p, 'k', linewidth=2) plt.title('Histogram of Gaussian Samples') plt.show() ``` 此代码片段展示了如何通过指定均值和标准差来生成并可视化高斯分布的样本集合[^5]。 #### 高斯过程回归简介 除了简单的随机变量模拟外,高斯过程(Gaussian Process)也是一种强大的机器学习技术,用于解决回归问题。相比传统的监督学习模型,如深度学习提到的学习数据表征而非特定任务算法的方法[^2],高斯过程提供了一种更加灵活的概率建模方式。给定训练集 \( D=\{(x_i,y_i)\}_{i=1}^{n} \),目标是从输入空间到输出空间映射关系进行推断。具体来说,假设观测值 y 被视为潜在函数 f 加上噪声的结果,则整个推理框架基于联合多维正态分布构建而成[^6]。 #### 实现简单的一维高斯过程回归 下面给出一段简化版的一维高斯过程回归Python代码示例: ```python from sklearn.gaussian_process import GaussianProcessRegressor from sklearn.gaussian_process.kernels import RBF, ConstantKernel as C import numpy as np # 创建核函数对象 kernel = C(1.0, (1e-3, 1e3)) * RBF(10, (1e-2, 1e2)) # 初始化GP实例 gp = GaussianProcessRegressor(kernel=kernel, n_restarts_optimizer=9) # 训练数据 X_train = np.array([[-1], [0], [1]]) y_train = np.sin(X_train).ravel() # 拟合模型 gp.fit(X_train, y_train) # 测试数据网格化 X_test = np.linspace(-2, 2, 100).reshape(-1, 1) # 进行预测 mean_prediction, std_prediction = gp.predict(X_test, return_std=True) # 可视化结果... ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值