blur
也称为 box filter、均值滤波器,就是简单地将每个像素的值替换成邻域平均值。
cv::blur(image, result, cv::Size(3,3));
如果用 kernel (也称为 mask) 表示,就是
如果采用积分图的方法,可以更快的计算这种 box filter 的结果。
在积分图中,只需要三次加法运算,一次乘法运算即可,即通过积分图,算出 kernel 内部区域的像素和,然后取平均。
积分图
积分图中每一点 的值是原图中对应位置左上角区域所有值的和:
积分图的计算可以很高效:
每次计算只需要新增一个像素值,其他值都是之前已经计算出来的。
积分图一旦计算出来,对任意矩形区域内像素和的计算都可以在常数时间(即计算时间固定,与区域的大小无关)内完成,例如:
GaussianBlur
在高斯滤波器中,当前像素值取邻域的加权平均,离当前像素越近,权重越大,权重服从高斯分布。
在实际应用中,几乎总是首选高斯滤波器,很少直接用 box filter.
cv::GaussianBlur(image, result, cv::Size(5,5), 1.5); //最后的参数表示高斯分布的方差 sigma
上述命令中,最后两个参数 kernel size 和 如果只设置一个,则另一个可以通过以下公式推出:
第二个式子很好理解,就是借助高斯函数的性质(距离均值 3 个标准差范围内的取值占总数的 99.7%),因此窗口大小就是 3 倍的 *2 (两边)然后再加上 1 (自身)。
第一个式子与第二个非常近似,但是又做了一些微调。
上述高斯滤波器内部实际上是先调用如下函数,产生服从高斯分布的一系列权重:
cv::getGaussianKernel(9, sigma, CV_32F); // 产生 9 个权重,与中心像素的距离依次为 -4,-3, ...3, 4
上述 9 个权重是经过归一化的,即和为 1,其公式为
其中 满足归一化的要求,ksize 必须是奇数。如果要生成二维的高斯矩阵权重,则是先产生一个权重列向量,然后令该列向量与自身的转置相乘,得到高斯矩阵权重,最后再统一进行归一化,保证矩阵所有元素和为 1。
另外,还可以分别产生两个不同的高斯权重向量,对应行和列方向上的高斯模糊权重,然后把它们相乘得到高斯矩阵。由于满足这种分离的性质,高斯滤波器被称为可分离的滤波器。
非线性滤波器:中值滤波器
前边在进行滤波操作时,都只包含线性操作(算数平均、加权平均)。
另外还可以采用非线性操作,对应非线性滤波器。非线性滤波器不能表示成 kernel 矩阵卷积操作的形式。
中值滤波器是一种非线性滤波器。它把当前像素和邻域像素组成一个集合,排序之后,选择中间值(即排序中间位置的数值)替换当前像素值。
椒盐噪声:像素随机替换成白色或者黑点。在通讯时,如果部分像素值丢失,就会产生这种噪声。
中值滤波器可以有效的消除椒盐噪声,因为这些噪声点在排序时很难成为中间值,因此全都被剔除了。
Sobel 边缘检测滤波器
Sobel 也是线性滤波器,只不过采用了特殊的 kernel 矩阵:
分别针对水平方向和垂直方向的操作。
用上述 kernel 进行操作,就是计算水平或者垂直方向像素值的差分,近似反映了像素值水平和垂直变化的速度,合在一起就是图像梯度的近似。
// 横向的
cv::Sobel(image, // 输入
sobelX, // 输出
CV_8U, // 输出数据类型
1, 0, // 采用第一个 kernel,即计算 x 轴方向(横向)像素变化率
3, // kernel 的大小
0.4, 128); // 缩放因子和偏移量
// 纵向的
cv::Sobel(image,
sobelY,
CV_8U,
0, 1, // 采用第二个 kernel,即计算 y 轴方向(纵向)像素变化率
3,
0.4, 128);
在默认情况下,差分运算的结果很可能超过 [0,255] 这个范围,而且有正有负,应该用 CV_16S 数据类型表示。经过上述缩放和偏移之后,才勉强适合用 CV_8U 表示,但还是需要饱和截断操作。
cv::Sobel(image, sobelX, CV_16S, 1, 0);
在分别得到横向、纵向变化率之后,可以整合起来计算梯度的大小
cv::Mat sobel;
sobel = abs(sobelX) + abs(sobelY); // 这里采用了 L1 范数
一般如果要显示最后的 sobel 边缘检测的结果,还需要把上述模值转化到 [0,255] 范围内。
高斯导数
sobel 实际上包含了平滑和求导两个操作,其中邻域像素累加相当于高斯平滑,距离越近的像素权重越高。
sobel 的 kernel size 可以选择 1, 3, 5 和 7,其中 1 代表 1×3 或者 3×1,此时是没有高斯平滑的。
对于大的 size,这种平滑更明显。此时,sobel 不是高通滤波器,而是带通滤波器,既消除了部分高频,又消除了部分低频。
其他梯度算子
与 Sobel 算子类似的还有其他几个计算梯度的算子,只是采用不同的 kernel.
- Prewitt
cv::Prewitt(image, prewittX, CV_16S, 1, 0);
- Roberts
cv::Roberts(image, robertsX, CV_16S, 1, 0);
- Scharr
cv::Scharr(image, robertsX, CV_16S, 1, 0);
上述所有的滤波器都是近似计算图像函数的一阶导数,像素变化大的区域计算得到的值较大,平坦的区域计算值较小。
Laplacian 算子
sobel 算子通过对图片函数求导,那些数值绝对值较高的点对应了边界区域:
如果继续求二阶导,则导数较大的点对应了过零点:
因此,也可以通过搜索二阶导的过零点来检测边界点。
Laplacian 算子的定义:
对照 Hessian 矩阵:
Laplacian 算子实际上就是 Hessian 矩阵的 Trace。
具体到图像操作中,二阶导有如下表达式:
所以最终 Laplacian 算子表达式为:
在具体实现中,可以用以下 kernel 进行卷积操作:
Laplacian 算子具有旋转 90 度不变性,即一幅图旋转 90 度及其倍数,对应的 Laplacian 算子操作结果相同。
为了得到更好的旋转不变性,可以将 Laplacian 算子 kernel 扩展为如下形式:
这样就具有了旋转 45 度及其倍数的不变性。
cv::Laplacian(input,
output,
int ddepth, // 元素类型,由于输入的是CV_8U,为了避免数据溢出,一般设定为 CV_16S
int ksize=1, // 求导数的 Sobel 算子的窗口大小,一般 ksize >1 奇数,如果取 ksize=1,则直接用上述 [1, -4, ...] kernel.
double scale=1, // 是否对计算得到的值进行放缩
double delta, // 在存储到 output 之前,是否添加 bias
int borderType); // 边界像素插值方式,具体选项可以官网查看 https://docs.opencv.org/3.4/d2/de8/group__core__array.html#ga209f2f4869e304c82d07739337eae7c5
LoG (Laplacian of Gaussian)
Laplacian 算子对噪声比较敏感,因此一般在进行 Laplacian 之前先进行高斯平滑滤波。
两个步骤合并称为 LoG (Laplacian of Gaussian)。
在具体实现中,我们并不需要先高斯再拉普拉斯,而是两步并作一步:将拉普拉斯算子作用在高斯 kernel 上,得到新的 kernel,再与 image 做卷积:
最后作用在 位置上的卷积权重为
同样也是通过 设定滤波范围。
对高斯函数取拉普拉斯算子操作是什么样子的?
- 一维形式
- 二维形式
二维情况下得到的曲面很像“墨西哥草帽”。
的大小决定了检测的粗粒度:
DoG ( LoG 的近似)
Difference of Gaussians
为了减少 LoG 计算量,用两个不同 的高斯做差,来近似 LoG
上图中两个 的取值好像反了。。。