《学习OpenCV3》第10章 滤波与卷积

Posted MechMaster

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《学习OpenCV3》第10章 滤波与卷积相关的知识,希望对你有一定的参考价值。

1. 预备知识

1.1 滤波、核和卷积

  • 滤波器 是一种由一幅图像 I(x,y) 根据像素点x,y附近的区域计算得到的一幅新图像 I’(x,y) 的算法。其中,模板规定了滤波器的形状以及这个区域内像素的值的组成规律,也称“滤波器”或“核”。

1.2 边界外推和边界处理

  • OpenCV中的滤波操作(如cv::blur(),cv::erode(),cv::dilate()等 )得到的输出图像与源图像的形状是一样的。为了实现这种效果,OpenCV采用的方法是在源图像周围添加虚拟像素。cv::blur() 函数实现了对图像每个像素与周围像素进行均值操作。

1.2.1 自定义边框

  • 在处理图像时,只要告诉调用的函数添加虚拟像素的规则,库函数就会自动创建虚拟像素。同样,为了阐明一个操作的意图,就要注意函数在创建虚拟像素时所采用的的方法。
  • cv::copyMakerBorder() 就是一个为图像创建边框的函数,通过制定两幅图像,第一幅是源图像,第二幅是扩充之后的图像,同时指明填充方法,这个函数就会将第一幅图像填补后的结果保存在第二幅图像中。
	void copyMakeBorder(
		InputArray src, 
		OutputArray dst,
		int top, 
		int bottom,
		int left, 
		int right,
		int borderType, //像素填充的方式
		const Scalar& value = Scalar()
		);
  • 边界类型
边框类型效果
cv::BORDER_CONSTANT复制指定的常量扩展边界
cv::BORDER_WRAP复制对边的像素扩展边界
cv::BORDER_REPLICATE复制边缘的像素扩展边界
cv::BORDER_REFLECT通过镜像复制扩展边界
cv::BORDER_REFLECT_101通过镜像复制扩展边界,边界像素除外
cv::BORDER_DEFAULT默认为 cv::BORDER_REFLECT_101

1.2.2 自定义外推

  • 在某些情况下,我们需要计算某一特定像素所参考的像素的位置。比如,给定一幅宽为w、高为h的图像,我们需要知道用哪个像素为虚拟像素**(w+dx,h+dy)**赋值,计算这个结果的函数是 cv::borderInterpolate()
	int borderInterpolate(
		int p, //坐标
		int len, //长度(在关联方向上的图像的实际大小)
		int borderType //边界类型
		);
  • cv::borderInterpolate() 一次计算一个维度上的外推,举一个例子,我们可以在混合的边界条件下计算一个特定像素的值,在一个维度中使用** cv::BORDER_REFLECT_101 ,在另一个维度中使用cv::BORDER_WRAP样式**。
	float val = img.at<float>(
		cv::borderInterpolate(100, img.rows, BORDER_REFLECT_101),
		cv::borderInterpolate(100, img.rows, BORDER_WRAP)
		);
  • 在OpenCV内部,这是一个经常使用的函数,比如cv::copyMakeBorder,也可以在自定义的算法中调用这个函数。

2. 阈值化操作

2.1 threshold

2.1.1 函数介绍

  • 根据个人喜好,可以把阈值化操作理解为一个用1×1的核进行卷积,对每个像素进行一次给线性操作。
	double threshold(
		InputArray src, 
		OutputArray dst,
		double thresh, //阈值
		double maxval, //给定的最大值
		int type  //操作类型
		);
阈值类型操作
cv::THRESH_BINARYDST=(SRC>thresh)?MAXVALUE:0
cv::THRESH_BINARY_INVDST=(SRC>thresh)?0:MAXVALUE
cv::THRESH_TRUNCDST=(SRC>thresh)?THRESH:SRC
cv::THRESH_TOZERODST=(SRC>thresh)?SRC:0
cv::THRESH_TOZERO_INVDST=(SRC>thresh)?0:SRC

2.1.2 例程10-1 图像相加和阈值化操作

// Example10_1.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <opencv2\\opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;

/**********
	将一幅图像的三个通道相加并将像素值限制在100以内
**/

void sum_rgb(const Mat& src, Mat& dst) 

	vector<Mat> planes;
	split(src, planes);
	Mat b = planes[0], g = planes[1], r = planes[2], s;

	//我们没有直接将数据相加并存入一个8位的数组(这可能会引起越界)
	//而是将三个通道取相同权值后进行相加然后截断大于100的值
	addWeighted(r, 1. / 3, g, 1. / 3, 0.0, s);
	addWeighted(s, 1., b, 1. / 3, 0.0, s);
	
	threshold(s, dst, 100, 100, THRESH_TRUNC);


/*****************
	组合和阈值图像平面的替代方法
*/
void sum_rgb2(const Mat& src, Mat& dst)

	vector<Mat> planes;
	split(src, planes);
	Mat b = planes[0], g = planes[1], r = planes[2];

	// accumulate可以将8位整型的图像累加到一幅浮点型的图像中
	Mat s = Mat::zeros(b.size(), CV_32F);
	accumulate(b, s);
	accumulate(g, s);
	accumulate(r, s);
	
	threshold(src, s, 100, 100, THRESH_TRUNC);
	s.convertTo(dst, b.type());


int main()

	Mat src = imread("./faces.png"), dst;
	if (src.empty())
		cout << "无法打开图像" << endl;

	sum_rgb(src, dst);
	imshow("src", src);
	imshow("dst", dst);

	Mat dst2;
	sum_rgb(src, dst2);
	imshow("dst2", dst2);

	waitKey(0);
    return 0;

2.2 Otsu算法

  • threshold()也可以自动决定最优的阈值,你只需对参数thresh传递值cv::THRESH_OTSU即可。
  • OTSU算法就是遍历所有可能的阈值,然后对每个阈值结果的两类像素计算方差(即低于阈值和高于阈值的两类像素)。
  • 这种方法不管是最小方差还是最大方差,耗费的时间都一样。原因是需要遍历所有可能的阈值,这不是一个相对高效的过程。

2.3 自适应阈值

2.3.1 adaptiveThreshold()

	void adaptiveThreshold(
		InputArray src, 
		OutputArray dst,
		double maxValue, //最大值
		int adaptiveMethod, //支持两种最适应阈值方法
		int thresholdType, //操作类型
		int blockSize,  
		double C
		);
  • 下面两种方法都是诸葛像素的计算自适应阈值T(x,y),方法是通过计算每个像素位置周围的b×b区域的加权平均值,然后减去常数C,其中bblockSize给定。
  • cv::ADAPTIVE_THRESH_MEAN_C,均值时取得权值是想等的;
  • cv::ADAPTIVE_THRESH_GAUSSIAN_C,(x,y)周围的像素的权值则根据其到中心点的距离通过高斯方程得到;
  • 阈值在整个过程中自动产生变化
  • 当图像中出现比较大的明暗差异时,自适应阈值非常有效。
  • 这个函数仅处理单通道8位浮点型图像,并且要求源图像和目标图像不同。

2.3.2 例程10-3 自适应阈值代码

// Example10_3.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <opencv2\\opencv.hpp>
using namespace cv;

int main()

	Mat srcImage = imread("./fruits.jpg", IMREAD_GRAYSCALE);
	Mat mat1, mat2;
	threshold(srcImage, mat1, 100, 254, THRESH_BINARY);
	adaptiveThreshold(srcImage, mat2, 100, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 15, 10);
	imshow("src", srcImage);
	imshow("1", mat1);
	imshow("2", mat2);

	waitKey(0);
    return 0;


3. 平滑

  • 平滑图像的目的通常是为了减少噪声和伪影,在降低分辨率的时候,平滑也十分重要。

3.1 简单模糊和方框型滤波器

3.1.1 简单模糊

	void blur(
		InputArray src, 
		OutputArray dst,
		Size ksize,  
		Point anchor = Point(-1, -1), 
		int borderType = BORDER_DEFAULT //边缘像素的平滑方式
		);
  • blur() 实现了简单模糊,目标图像中的每个值都是源图像中相应位置一个核中像素的平均值。
  • ksize : Kernal size 核尺寸。
  • anchor : 指定计算时核与源图像的对齐方式,默认情况为 cv::Point(-1,-1),表示核相对滤波器居中。
  • 如果源图像是一幅多通道图像,则分别对每个通道进行计算。

3.1.2 方框型滤波器

	void boxFilter(
		InputArray src,
		OutputArray dst,
		int ddepth,
		Size ksize,
		Point anchor = Point(-1, -1),
		bool normalize = true,  //是否归一化
		int borderType = BORDER_DEFAULT
		);
  • 方框滤波器是矩形的,滤波器中所有的值K都相等。
  • 通常所有的值K都为11/A,A是滤波器的面积。为 1/A 时称为归一化方框型滤波器
  • boxFilter() 是一种一般化的形式,而 blur() 是一种特殊化的形式。
  • 两者之间根本的区别是前者可以以归一化形式调用,并且输出图像深度可以控制(**blur()**输出的深度与源图像保持一致)。
  • 假如变量ddepth设为 -1 ,目标图像的深度将与源图像保持一致;否则可以设置为其他任意一种深度,如CV_32F

3.2 中值滤波器

	void medianBlur(
		InputArray src,
		OutputArray dst, 
		int ksize
		);
  • 中值滤波器将每个像素替换为围绕这个像素的矩形邻域内的中值或“中值”像素(相对于平均像素)。
  • 中值滤波对有比较大的孤立的异常值(比如数字影像中的拍摄噪声)非常敏感。
  • 少量具有较大偏差的点也会严重影响得到均值滤波,而中值滤波可以采用取中间点的方式消除异常值的影响。
  • 中值滤波是一种非线性核。

3.3 高斯滤波器

void GaussianBlur(
	InputArray src, 
	OutputArray dst,
	Size ksize,
	double sigmaX,
	double sigmaY = 0,
	int borderType = BORDER_DEFAULT
	);
  • 高斯滤波器是最有用的一种滤波器,它对输入数组进行规范化的高斯核滤波,然后输出目标数组。
  • 对于高斯滤波器,ksize指定滤波器核的宽度和高度,sigmaX表示高斯核在x方向上的sigma值。如果只给定了x,同时y设为0,那么y将会与x相等;
  • OpenCV为常用的内核提供性能上的优化。3×35×57×7的标准sigma核(sigmaX=0)比其他核性能更优。

3.4 双边滤波器

	void bilateralFilter(
		InputArray src, 
		OutputArray dst,
		int d, //像素邻域的直径 对算法效率影响很大,视频处理一般不超过5,你可以将其设为-1,函数将自动为图像设计sigmaSpace的量
		double sigmaColor,  //颜色空间滤波器的sigma值和sigmaColor,其与高斯滤波器中的参数sigma相似
		double sigmaSpace, //坐标空间中滤波器的sigma值sigmaSpace。sigmaColor越大,平滑是所包括的强度(色彩)越大
		int borderType = BORDER_DEFAULT
		);
  • 双边滤波器是一种比较大的图像分析算子,也就是边缘保持平滑。
  • 高斯模糊的过程是减缓像素在空间上的变化,因此与邻域的关系紧密,而随机噪声在像素间的变化幅度又会非常的大(即噪声不是空间相关的)。基于这种前提高斯平滑很好地减弱了噪声并且保留了小信号。但是这种方式破坏了边缘信息,最终结果是高斯模糊把边缘也模糊了。
  • 相似于高斯平滑,双边滤波对每个像素及其区域内的像素进行了加权平均。其权重由两部分组成:第一部分同高斯平滑;第二部分也是高斯权重,不同的是它不是基于空间距离,而是彩色强度差计算而来,在过通道(彩色)图像上强度差由各分量的加权累加代替。
  • 可以把双边滤波当做是高斯平滑,只是相似程度更高的像素权值更高,边缘更明显,对比度更高。双边滤波的效果就是将源图像变成一幅水彩画,这种想过在多次迭代后更见显著,因此这种方法在图像分割领域十分有用。

4. 导数和梯度

4.1 索贝尔导数

	void Sobel(
		InputArray src, 
		OutputArray dst, 
		int ddepth, //目标图像的深度或类型,例如src是一幅8位图像,那么dst至少需要CV_16S深度保证不出现溢出
		int dx, //求导顺序   0 表示在这个方向不求导
		int dy, //求导顺序   0 表示在这个方向不求导
		int ksize = 3, //奇数 > 1才有意义  表示调用的滤波器的宽和高,目前最大支持到31
		double scale = 1, //缩放因子  
		double delta = 0, //偏移因子   
		int borderType = BORDER_DEFAULT
		);
  • 表示微分最常用的算子是Sobel,它可以实现任意阶导数和混合偏导数。
  • Sobel算子有一个好处就是可以将核定义为各种大小,并且可以快速、迭代式地构造这些核。
  • 大的核可以更好的近似导数,因为可以消除噪声影响。不过。假如导数在空间上变化剧烈,核太大会使结果发生偏差。
  • Sobel不是真正的导数,因为它定义在离散空间上。它实际上是一个多项式,在x方向上进行二阶Sobel运算表示的并不是二阶导数,而是对抛物线函数的局部拟合。这也说明了为什么要使用一个更大的核,更大的核拟合了更多的像素。

4.2 Scharr滤波

  • Sobel算子的缺点是核比较小的时候准确度不高。对于大型的核,近似过程使用了较多的点,因此精度问题不太显著。
  • 对于3×3Sobel滤波器,梯度角距离水平或垂直方向越远,误差越明显。
  • 调用Sobel时设置ksizecv::SCHARR,即可消除3×3这样小但是快的Sobel导数滤波器所带来的误差。Scharr同样很快,但是精度更高。

4.3 拉普拉斯变换

	void Laplacian(
		InputArray src,
		OutputArray dst, 
		int ddepth,
		int ksize = 1, 
		double scale = 1,
		double delta = 0,
		int borderType = BORDER_DEFAULT
		);
  • Laplacian() 可以用于各种场景处理,一种常见的应用就是匹配斑点
  • Laplacian() 就是图像在x, y轴方向上的导数之和,这意味着一个被包围的点或小斑点(比ksize小)处的值将会变得很大。相反,被较小值包围的点或小斑点处的的值将会在负方向上变得很大。
  • 因此,Laplacian同样可以用于边缘检测。

5. 图像形态学

5.1 膨胀和腐蚀

5.1.1 原理和应用

  • 膨胀腐蚀是最基础的形态学变换,它们可以应用在消除噪声、元素分割和连接上面。
  • 膨胀是一种卷积操作,它将目标像素的值替换为卷积核覆盖区域的局部最大值。这是一个非线性核,通常膨胀采用的核是一个四边形或圆形的实心核,其锚点在中心。膨胀的作用是使图中填充区域生长。
  • 腐蚀是与膨胀相反的操作,腐蚀操作计算的是核覆盖范围内的局部最小值。
  • 图像的形态学操作通常在阈值化后的布尔图像上进行,不过由于膨胀和腐蚀知识最大和最小操作,因此形态学操作也可以在强度图像上进行。
  • 总的来说,膨胀扩张了明亮区域,腐蚀缩减了明亮区域;膨胀填充凹面,腐蚀消除凸起。
  • 腐蚀操作通常用于消除图中斑点一样的噪声,原理是斑点经过腐蚀后会消失,而大的可见区域不会受影响。
  • 膨胀操作通常用于发现连通分支(大的颜色或强度相似像素的离散区域)。膨胀出现的原因是,在很多情况下,由于噪声或者阴影等因素,大的区域可能被分解成多个分支。一些细微的膨胀会导致这些分支相遇并组合成一个整体。
  • 另外需要注意的是:如果不是布尔类型图像的话,最小化(腐蚀)和最大化(膨胀)操作就不是那么有用了。

5.1.2 erode()和dilate()

	//腐蚀
	void erode(
		InputArray src, 
		OutputArray dst, 
		InputArray kernel, 
		Point anchor = Point(-1, -1), 
		int iterations = 1,
		int borderType = BORDER_CONSTANT,
		const Scalar& borderValue = morphologyDefaultBorderValue()
		);
	
	//膨胀
	void dilate(
		InputArray src, 
		OutputArray dst, 
		InputArray kernel,
		Point anchor = Point(-1, -1), 
		int iterations = 1,
		int borderType = BORDER_CONSTANT,
		const Scalar& borderValue = morphologyDefaultBorderValue()
		);
  • erode()dilate() 都需要传入源图像和目标图像,并且都支持就地调用(源图像和目标图像是同一幅图像)。
  • kernel:这个参数是,你可以传入一个为被初始化的cv::Mat(),不过这会导致函数使用默认的锚点在中心的3×3核
  • iterations:这个参数是迭代次数,如果不为1,将会自动重复多次调用这个函数。

5.2 通用形态学函数

  • 当处理的对象是二值图像和像素只可能是开(>0)或关(=0)的图像掩膜时,基本的腐蚀和膨胀操作就够用了。需要对灰度图或者彩色图进行处理时,一些其他的操作就非常有用,它就是morphologyEx()

5.2.1 morphologyEx()

	void morphologyEx(
		InputArray src,
		OutputArray dst,
		int op, 
		InputArray kernel,
		Point anchor = Point(-1, -1), 
		int iterations = 1,
		int borderType = BORDER_CONSTANT,
		const Scalar& borderValue = morphologyDefaultBorderValue()
		);
操作值形态学操作是否需要临时图像
MOP_OPEN开操作
MOP_CLOSE闭操作
MOP_GRADIENTN形态学梯度总是需要
MOP_TOPHAT顶帽操作就地调用需要(src = dst)
MOP_BALCKHAT黑帽操作就地调用需要(src = dst)

5.2.2 开操作和闭操作

  • 开操作闭操作实际上就是腐蚀和膨胀的简单组合。
  • 开操作是先腐蚀后膨胀,常用于对二值图像中的区域进行计数。
  • 闭操作是先膨胀后腐蚀,作用于复杂连通分支算法中减少无用或噪声驱动的片段。对于连通分支,通常先进行腐蚀或闭操作消除噪声,然后通过开操作连接相互靠近的大型区域。
  • 尽管开和闭操作的效果与腐蚀相近,但这些新操作在保持连通域的精度上更加可靠。

5.2.3 形态学梯度

  • 形态学梯度 是 膨胀操作的结果(扩张亮域)减 腐蚀操作的结果(缩减亮域) ,这就是边缘。
  • 对于灰度图,其结果就是计算明暗变换的趋势。形态学梯度通常用于显示明亮区域的边界,然后便可以将他们看做目标或者目标的部分。用扩张的图像减去了收缩的图像,如此一来就找出了完整的边界。这与计算梯度不同,它不会关注某一个物体的周围。

5.2.4 顶帽和黑帽

  • 这两种操作分别用于显示与其邻域相比更亮或更暗的部分。当试图根据物体的亮度变化分离依附于物体的某些部分时,就会用到这些方法。
  • 顶帽作用源图像减去其开操作后的图像。开操作的效果是放大裂缝和局部小洞。因此用源图像减去开操作后的图像得到了比源图像更亮的环绕部分。
  • 黑帽操作显示的是比源图像更暗的环绕部分。

5.3 自定义核

5.3.1 getStructuringElement()

  • 你可以创建任意一种你需要的构造元素用于 erode()、dilate()、morphologyEx()
	Mat getStructuringElement(
		int shape, //控制构造元素的基本形状
		Size ksize, //大小
		Point anchor = Point(-1, -1)  //锚点
		);
形状值元素描述
MORPH_RECT矩形Ei,j = 1, 所有的 i,j
MORPH_ELLIPSE椭圆ksize.width 和 ksize.height 为两个半径做椭圆
MORPH_CROSS交叉Ei,j = 1, 当 i == anchor.y 或 j == anchor.x

6. 用任意线性滤波器做卷积

6.1 分解卷积核

  • 一个可分核可以理解成两个一维核,在卷积时先调用x内核,然后调用y内核。两个矩阵进行卷积所产生的消耗可以用两个矩阵的面积的积来估算,如此一来,用n×n的核对面积为A的图像进行卷积所需的时间是An2,但如果分解成 n×11×n 的两个核,那么代价就是 An +An = 2An ,因此分解卷积核可以提高卷积计算的效率。只要n不小于3,这种计算方式就能提高效率,并且随着n的增大,这种效益愈发明显。

6.2 用cv::filter2D()进行卷积

  • 对一幅图像进行卷积的操作时十分巨大的,至少从第一印象看操作数大概是图像中的像素数量乘以卷积核中的像素数,如此复杂的操作导致没人愿意通过使用for循环和一维指针去计算卷积。因此OpenCV为我们提供了filter2D()
	void filter2D(
		InputArray src,
		OutputArray dst, 
		int ddepth,
		InputArray kernel, //核
		Point anchor = Point(-1, -1), //如果定义了锚点,那么核的大小可以是偶数,否则必须是奇数
		double delta = 0, //滤波的结果与源图像的偏移
		int borderType = BORDER_DEFAULT
		);

6.3 通过cv::sepFilter2D使用可分核

  • 当参与卷积的核是可分核时,那么现将其分解为两个一维核,然后传递到OpenCV中计算,会获得最佳的计算性能。
	void sepFilter2D(
		InputArray src, 
		OutputArray dst, 
		int ddepth,
		InputArray kernelX,  // n1 × 1核
		InputArray kernelY,  // 1 × n2核  n1和n2可以不相等
		Point anchor = Point(-1, -1),
		double delta = 0, 
		int borderType = BORDER_DEFAULT
		);

6.4 生成卷积核

6.4.1 Sobel核和Scharr核

  • 导数滤波器的核实际上是通过**getDerivKernels()**生成的。
  • 导数核(Sobel核和Scharr核)是可以分解的。
  • 导数核永远都是正方形的,因此 ksize 是一个整数。
	void getDerivKernels(
		OutputArray kx, //1 × ksize 行向量
		OutputArray ky, //ksize × 1 列向量
		int dx,
		int dy,
		int ksize, //可以是1、3、5、7或cv::SCHARR
		bool normalize = false, //是否将核元素规范化,如果目标是浮点型图像,就应当设置为真
		int ktype = CV_32F
		);

6.4.2 高斯核

  • 高斯滤波器所使用的核由**getGaussianKernel()**生成。
  • 高斯核也是可分的。
	Mat getGaussianKernel(
		int ksize, //是奇数
		double sigma, //近似高斯分布的标准差
		int ktype = CV_64F
		);

以上是关于《学习OpenCV3》第10章 滤波与卷积的主要内容,如果未能解决你的问题,请参考以下文章

OpenCV3编程入门-读书笔记3-滤波

数字图像处理(MATLAB版)学习笔记——第2章 灰度变换与空间滤波

《学习OpenCV3》第7章第4题-SVD奇异值分解的验算

2023.5.7 《动手学深度学习》第78章

python+opencv3图像处理学习记录

python+opencv3图像处理学习记录