本帖最后由 KA_IX 于 2020-2-25 11:22 编辑

高斯滤波器是一种线性滤波器,能够有效的抑制噪声,平滑图像。其作用原理和均值滤波器类似,都是取滤波器窗口内的像素的均值作为输出。其窗口模板的系数和均值滤波器不同,均值滤波器的模板系数都是相同的为1;而高斯滤波器的模板系数,则随着距离模板中心的增大而系数减小。所以,高斯滤波器相比于均值滤波器对图像个模糊程度较小。
什么是高斯滤波器
既然名称为高斯滤波器,那么其和高斯分布(正态分布)是有一定的关系的。一个二维的高斯函数如下:
其中 ( x , y ) 为点坐标,在图像处理中可认为是整数;σ 是标准差。要想得到一个高斯滤波器的模板,可以对高斯函数进行离散化,得到的高斯函数值作为模板的系数。例如:要产生一个 3×3 的高斯滤波器模板,以模板的中心位置为坐标原点进行取样。模板在各个位置的坐标,如下所示(x轴水平向右,y轴竖直向下)
这样,将各个位置的坐标带入到高斯函数中,得到的值就是模板的系数。
对于窗口模板的大小为 ( 2k + 1 ) × ( 2k + 1 ),模板中各个元素值的计算公式如下:
这样计算出来的模板有两种形式:小数和整数。
  •  小数形式的模板,就是直接计算得到的值,没有经过任何的处理;
  •  整数形式的,则需要进行归一化处理,将模板左上角的值归一化为1,下面会具体介绍。使用整数的模板时,需要在模板的前面加一个系数,系数为
,也就是模板系数和的倒数。
高斯模板的生成
知道模板生成的原理,实现起来也就不困难了
  1. void generateGaussianTemplate(double window[][11], int ksize, double sigma)
  2. {
  3.     static const double pi = 3.1415926;
  4.     int center = ksize / 2; // 模板的中心位置,也就是坐标的原点
  5.     double x2, y2;
  6.     for (int i = 0; i < ksize; i++)
  7.     {
  8.         x2 = pow(i - center, 2);
  9.         for (int j = 0; j < ksize; j++)
  10.         {
  11.             y2 = pow(j - center, 2);
  12.             double g = exp(-(x2 + y2) / (2 * sigma * sigma));
  13.             g /= 2 * pi * sigma;
  14.             window[i][j] = g;
  15.         }
  16.     }
  17.     double k = 1 / window[0][0]; // 将左上角的系数归一化为1
  18.     for (int i = 0; i < ksize; i++)
  19.     {
  20.         for (int j = 0; j < ksize; j++)
  21.         {
  22.             window[i][j] *= k;
  23.         }
  24.     }
  25. }
需要一个二维数组,存放生成的系数(这里假设模板的最大尺寸不会超过11);第二个参数是模板的大小(不要超过11);第三个参数就比较重要了,是高斯分布的标准差。
生成的过程,首先根据模板的大小,找到模板的中心位置 ksize/2。 然后就是遍历,根据高斯分布的函数,计算模板中每个系数的值。
需要注意的是,最后归一化的过程,使用模板左上角的系数的倒数作为归一化的系数(左上角的系数值被归一化为1),模板中的每个系数都乘以该值(左上角系数的倒数),然后将得到的值取整,就得到了整数型的高斯滤波器模板。
下面截图生成的是,大小为 3 × 3, σ = 0.8 的模板
对上述解结果取整后得到如下模板:
这个模板就比较熟悉了,其就是根据 σ = 0.8 的高斯函数生成的模板。至于小数形式的生成也比较简单,去掉归一化的过程,并且在求解过程后,模板的每个系数要除以所有系数的和。具体代码如下:
  1. void generateGaussianTemplate(double window[][11], int ksize, double sigma)
  2. {
  3.     static const double pi = 3.1415926;
  4.     int center = ksize / 2; // 模板的中心位置,也就是坐标的原点
  5.     double x2, y2;
  6.     double sum = 0;
  7.     for (int i = 0; i < ksize; i++)
  8.     {
  9.         x2 = pow(i - center, 2);
  10.         for (int j = 0; j < ksize; j++)
  11.         {
  12.             y2 = pow(j - center, 2);
  13.             double g = exp(-(x2 + y2) / (2 * sigma * sigma));
  14.             g /= 2 * pi * sigma;
  15.             sum += g;
  16.             window[i][j] = g;
  17.         }
  18.     }
  19.     //double k = 1 / window[0][0]; // 将左上角的系数归一化为1
  20.     for (int i = 0; i < ksize; i++)
  21.     {
  22.         for (int j = 0; j < ksize; j++)
  23.         {
  24.             window[i][j] /= sum;
  25.         }
  26.     }
  27. }
3 × 3, σ = 0.8 的小数型模板。
σ 值的意义及选取
通过上述的实现过程,不难发现,高斯滤波器模板的生成最重要的参数就是高斯分布的标准差σσ。标准差代表着数据的离散程度,如果σσ较小,那么生成的模板的中心系数较大,而周围的系数较小,这样对图像的平滑效果就不是很明显;反之,σσ较大,则生成的模板的各个系数相差就不是很大,比较类似均值模板,对图像的平滑效果比较明显。
来看下一维高斯分布的概率分布密度图:
横轴表示可能得取值x,竖轴表示概率分布密度F(x),那么不难理解这样一个曲线与x轴围成的图形面积为1。σσ(标准差)决定了这个图形的宽度,可以得出这样的结论:σσ越大,则图形越宽,尖峰越小,图形较为平缓;σσ越小,则图形越窄,越集中,中间部分也就越尖,图形变化比较剧烈。这其实很好理解,如果sigma也就是标准差越大,则表示该密度分布一定比较分散,由于面积为1,于是尖峰部分减小,宽度越宽(分布越分散);同理,当σσ越小时,说明密度分布较为集中,于是尖峰越尖,宽度越窄!
于是可以得到如下结论:
σσ越大,分布越分散,各部分比重差别不大,于是生成的模板各元素值差别不大,类似于平均模板;
σσ越小,分布越集中,中间部分所占比重远远高于其他部分,反映到高斯模板上就是中心元素值远远大于其他元素值,于是自然而然就相当于中间值得点运算。
基于OpenCV的实现
在生成高斯模板好,其简单的实现和其他的空间滤波器没有区别,具体代码如下:
  1. void GaussianFilter(const Mat &src, Mat &dst, int ksize, double sigma)
  2. {
  3.     CV_Assert(src.channels() || src.channels() == 3); // 只处理单通道或者三通道图像
  4.     const static double pi = 3.1415926;
  5.     // 根据窗口大小和sigma生成高斯滤波器模板
  6.     // 申请一个二维数组,存放生成的高斯模板矩阵
  7.     double **templateMatrix = new double*[ksize];
  8.     for (int i = 0; i < ksize; i++)
  9.         templateMatrix[i] = new double[ksize];
  10.     int origin = ksize / 2; // 以模板的中心为原点
  11.     double x2, y2;
  12.     double sum = 0;
  13.     for (int i = 0; i < ksize; i++)
  14.     {
  15.         x2 = pow(i - origin, 2);
  16.         for (int j = 0; j < ksize; j++)
  17.         {
  18.             y2 = pow(j - origin, 2);
  19.             // 高斯函数前的常数可以不用计算,会在归一化的过程中给消去
  20.             double g = exp(-(x2 + y2) / (2 * sigma * sigma));
  21.             sum += g;
  22.             templateMatrix[i][j] = g;
  23.         }
  24.     }
  25.     for (int i = 0; i < ksize; i++)
  26.     {
  27.         for (int j = 0; j < ksize; j++)
  28.         {
  29.             templateMatrix[i][j] /= sum;
  30.             cout << templateMatrix[i][j] << " ";
  31.         }
  32.         cout << endl;
  33.     }
  34.     // 将模板应用到图像中
  35.     int border = ksize / 2;
  36.     copyMakeBorder(src, dst, border, border, border, border, BorderTypes::BORDER_REFLECT);
  37.     int channels = dst.channels();
  38.     int rows = dst.rows - border;
  39.     int cols = dst.cols - border;
  40.     for (int i = border; i < rows; i++)
  41.     {
  42.         for (int j = border; j < cols; j++)
  43.         {
  44.             double sum[3] = { 0 };
  45.             for (int a = -border; a <= border; a++)
  46.             {
  47.                 for (int b = -border; b <= border; b++)
  48.                 {
  49.                     if (channels == 1)
  50.                     {
  51.                         sum[0] += templateMatrix[border + a][border + b] * dst.at<uchar>(i + a, j + b);
  52.                     }
  53.                     else if (channels == 3)
  54.                     {
  55.                         Vec3b rgb = dst.at<Vec3b>(i + a, j + b);
  56.                         auto k = templateMatrix[border + a][border + b];
  57.                         sum[0] += k * rgb[0];
  58.                         sum[1] += k * rgb[1];
  59.                         sum[2] += k * rgb[2];
  60.                     }
  61.                 }
  62.             }
  63.             for (int k = 0; k < channels; k++)
  64.             {
  65.                 if (sum[k] < 0)
  66.                     sum[k] = 0;
  67.                 else if (sum[k] > 255)
  68.                     sum[k] = 255;
  69.             }
  70.             if (channels == 1)
  71.                 dst.at<uchar>(i, j) = static_cast<uchar>(sum[0]);
  72.             else if (channels == 3)
  73.             {
  74.                 Vec3b rgb = { static_cast<uchar>(sum[0]), static_cast<uchar>(sum[1]), static_cast<uchar>(sum[2]) };
  75.                 dst.at<Vec3b>(i, j) = rgb;
  76.             }
  77.         }
  78.     }
  79.     // 释放模板数组
  80.     for (int i = 0; i < ksize; i++)
  81.         delete[] templateMatrix[i];
  82.     delete[] templateMatrix;
  83. }
只处理单通道或者三通道图像,模板生成后,其滤波(卷积过程)就比较简单了。不过,这样的高斯滤波过程,其循环运算次数为 m × n × ksize2,其中 m,n 为图像的尺寸;ksize为高斯滤波器的尺寸。这样其时间复杂度为 O ( ksize2 ) ,随滤波器的模板的尺寸呈平方增长,当高斯滤波器的尺寸较大时,其运算效率是极低的。为了,提高滤波的运算速度,可以将二维的高斯滤波过程分解开来。
分离实现高斯滤波
由于高斯函数的可分离性,尺寸较大的高斯滤波器可以分成两步进行:首先将图像在水平(竖直)方向与一维高斯函数进行卷积;然后将卷积后的结果在竖直(水平)方向使用相同的一维高斯函数得到的模板进行卷积运算。具体实现代码如下:
  1. // 分离的计算
  2. void separateGaussianFilter(const Mat &src, Mat &dst, int ksize, double sigma)
  3. {
  4.     CV_Assert(src.channels()==1 || src.channels() == 3); // 只处理单通道或者三通道图像
  5.     // 生成一维的高斯滤波模板
  6.     double *matrix = new double[ksize];
  7.     double sum = 0;
  8.     int origin = ksize / 2;
  9.     for (int i = 0; i < ksize; i++)
  10.     {
  11.         // 高斯函数前的常数可以不用计算,会在归一化的过程中给消去
  12.         double g = exp(-(i - origin) * (i - origin) / (2 * sigma * sigma));
  13.         sum += g;
  14.         matrix[i] = g;
  15.     }
  16.     // 归一化
  17.     for (int i = 0; i < ksize; i++)
  18.         matrix[i] /= sum;
  19.     // 将模板应用到图像中
  20.     int border = ksize / 2;
  21.     copyMakeBorder(src, dst, border, border, border, border, BorderTypes::BORDER_REFLECT);
  22.     int channels = dst.channels();
  23.     int rows = dst.rows - border;
  24.     int cols = dst.cols - border;
  25.     // 水平方向
  26.     for (int i = border; i < rows; i++)
  27.     {
  28.         for (int j = border; j < cols; j++)
  29.         {
  30.             double sum[3] = { 0 };
  31.             for (int k = -border; k <= border; k++)
  32.             {
  33.                 if (channels == 1)
  34.                 {
  35.                     sum[0] += matrix[border + k] * dst.at<uchar>(i, j + k); // 行不变,列变化;先做水平方向的卷积
  36.                 }
  37.                 else if (channels == 3)
  38.                 {
  39.                     Vec3b rgb = dst.at<Vec3b>(i, j + k);
  40.                     sum[0] += matrix[border + k] * rgb[0];
  41.                     sum[1] += matrix[border + k] * rgb[1];
  42.                     sum[2] += matrix[border + k] * rgb[2];
  43.                 }
  44.             }
  45.             for (int k = 0; k < channels; k++)
  46.             {
  47.                 if (sum[k] < 0)
  48.                     sum[k] = 0;
  49.                 else if (sum[k] > 255)
  50.                     sum[k] = 255;
  51.             }
  52.             if (channels == 1)
  53.                 dst.at<uchar>(i, j) = static_cast<uchar>(sum[0]);
  54.             else if (channels == 3)
  55.             {
  56.                 Vec3b rgb = { static_cast<uchar>(sum[0]), static_cast<uchar>(sum[1]), static_cast<uchar>(sum[2]) };
  57.                 dst.at<Vec3b>(i, j) = rgb;
  58.             }
  59.         }
  60.     }
  61.     // 竖直方向
  62.     for (int i = border; i < rows; i++)
  63.     {
  64.         for (int j = border; j < cols; j++)
  65.         {
  66.             double sum[3] = { 0 };
  67.             for (int k = -border; k <= border; k++)
  68.             {
  69.                 if (channels == 1)
  70.                 {
  71.                     sum[0] += matrix[border + k] * dst.at<uchar>(i + k, j); // 列不变,行变化;竖直方向的卷积
  72.                 }
  73.                 else if (channels == 3)
  74.                 {
  75.                     Vec3b rgb = dst.at<Vec3b>(i + k, j);
  76.                     sum[0] += matrix[border + k] * rgb[0];
  77.                     sum[1] += matrix[border + k] * rgb[1];
  78.                     sum[2] += matrix[border + k] * rgb[2];
  79.                 }
  80.             }
  81.             for (int k = 0; k < channels; k++)
  82.             {
  83.                 if (sum[k] < 0)
  84.                     sum[k] = 0;
  85.                 else if (sum[k] > 255)
  86.                     sum[k] = 255;
  87.             }
  88.             if (channels == 1)
  89.                 dst.at<uchar>(i, j) = static_cast<uchar>(sum[0]);
  90.             else if (channels == 3)
  91.             {
  92.                 Vec3b rgb = { static_cast<uchar>(sum[0]), static_cast<uchar>(sum[1]), static_cast<uchar>(sum[2]) };
  93.                 dst.at<Vec3b>(i, j) = rgb;
  94.             }
  95.         }
  96.     }
  97.     delete[] matrix;
  98. }
代码没有重构较长,不过其实现原理是比较简单的。首先得到一维高斯函数的模板,在卷积(滤波)的过程中,保持行不变,列变化,在水平方向上做卷积运算;接着在上述得到的结果上,保持列不边,行变化,在竖直方向上做卷积运算。 这样分解开来,算法的时间复杂度为 O ( ksize ) ,运算量和滤波器的模板尺寸呈线性增长。
在 OpenCV 也有对高斯滤波器的封装 GaussianBlur,其声明如下:
  1. CV_EXPORTS_W void GaussianBlur( InputArray src, OutputArray dst, Size ksize,
  2.                                 double sigmaX, double sigmaY = 0,
  3.                                 int borderType = BORDER_DEFAULT );
二维高斯函数的标准差在x和y方向上应该分别有一个标准差,在上面的代码中一直设其在x和y方向的标准是相等的,在OpenCV中的高斯滤波器中,可以在x和y方向上设置不同的标准差。
下图是自己实现的高斯滤波器和OpenCV中的GaussianBlur的结果对比
上图是 5 × 5 , σ = 0.8 的高斯滤波器,可以看出两个实现得到的结果没有很大的区别。
总结
高斯滤波器是一种线性平滑滤波器,其滤波器的模板是对二维高斯函数离散得到。由于高斯模板的中心值最大,四周逐渐减小,其滤波后的结果相对于均值滤波器来说更好。
高斯滤波器最重要的参数就是高斯分布的标准差 σ,标准差和高斯滤波器的平滑能力有很大的能力,σ 越大,高斯滤波器的频带就较宽,对图像的平滑程度就越好。通过调节 σ 参数,可以平衡对图像的噪声的抑制和对图像的模糊。
作者:Brook_icv
原文链接:https://www.cnblogs.com/wangguchangqing/p/6407717.html