Opencv 笔记3 图像平滑

一、卷积定义与矩阵形式

常用的平滑处理算法是基于二维离散卷积的高斯滤波、均值滤波。以及基于统计方法的中值滤波。

假设有两个矩阵Opencv 笔记3 图像平滑

 将K旋转180度(为什么旋转,我也没有想通)Opencv 笔记3 图像平滑

 然后就是按照I的每个元素,从左到右,从上到下,Opencv 笔记3 图像平滑

得到一个新的矩阵M Opencv 笔记3 图像平滑

那么我们可以说矩阵K为一个卷积核(卷积掩码),M是I关于K的full 卷积。这个理解是很简单的。但是我们在图像像素处理的过程中一般使用的是same 卷积,就是说,在卷积后的图像的高和宽和原来图像的是一样的,如下: Opencv 笔记3 图像平滑

对于same和full卷积,矩阵I边界处的值由于缺乏完整的邻域值,因此在卷积运算的时候我们要在这些区域做特殊的处理,方法是进行边界扩充,如下Opencv 笔记3 图像平滑

效果如下,很明显看出扩充的图像比之前的大了一些Opencv 笔记3 图像平滑

 Opencv 笔记3 图像平滑

 代码显示如下:

	int  top = (int)src.rows * 0.05;  // 边缘扩展 顶部的扩展量
	int  bottom = (int)src.cols * 0.05;   // 边缘扩展 下部的扩展量
	int  left = (int)src.rows * 0.05;
	int  right = (int)src.cols * 0.05;
	RNG rad(12345); // 随机数
	int  borderType = BORDER_DEFAULT;
	int  c = 0;
	while (true)
	{

		c = waitKey(500);
		if ((char)c == 27)   // esc 
		{
			break;
		}
		if ((char)c == 'r')
		{
			borderType = BORDER_REPLICATE;
		}
		else if ((char)c == 'w')
		{
			borderType = BORDER_WRAP;
		}
		else if ((char)c == 'c')
		{
			borderType = BORDER_CONSTANT;
		}
		else if ((char)c == 'a')
		{
			borderType = BORDER_REFLECT;
		}else if ((char)c == 'b')
		{
			borderType = BORDER_REFLECT_101;
		}
		else
		{
			borderType = BORDER_DEFAULT;
		}
		Scalar  color = Scalar(rad.uniform(0, 255), rad.uniform(0, 255), rad.uniform(0, 255));
		copyMakeBorder(src, dest, top, bottom, left, right, borderType, color);
		imshow("边缘扩充", dest);

补充一些卷积运算函数:

Mat kernel = (Mat_<int>(3, 3)<< 0,-1,0,-1,5,-1,0,-1,0); // 卷积核
void filter2D( 
 InputArray src,
 OutputArray dst,
 int ddepth, //图像的位深度。如果是-1的话,表示和输入的图像一致
 InputArray kernel,
 Point anchor=Point(-1,-1), //内核的基准点(anchor),其默认值为(-1,-1)说明位于kernel的中心位置
 double delta=0,
 int borderType=BORDER_DEFAULT //像素向外逼近的方法,默认值是BORDER_DEFAULT,即对全部边界进行计算,这个就是上面说的几种边缘像素扩充方式
 );

二、高斯滤波-GaussianBlur

 高斯滤波是低通滤波,可以过滤掉图像中的高频成分,保留低频成分,所以高斯模糊之后图像会变模糊,对于高斯噪声(正太分布的)有抑制作用。

高斯模糊原理: 要模糊一张图片,一般情况我们是以每个像素点为中心,取一个3*3的区域求均值作为这个像素点的像素值,可以如果每张图像都这么搞真的合理吗,图像是连续的假如你选的这个区域足够大,越靠近这个中心点的像素是越来越靠近改点的像素值,越远的则像素相差越大,如果求均值等可能会对改变图像的某些特征。所以我们想到了对这个掩膜区域不同的位置进行加权,越近的权重越大,反义依然。因此引出了正太分布的权重分配模式。 

高斯函数
高斯函数定义如下
Opencv 笔记3 图像平滑
其中a, b,c 为对应的参数。高斯函数是一个钟形曲线。其中参数a控制函数的幅度,参数b控制钟形曲线的水平位置,参数c反应钟形曲线钟的宽度。

 Opencv 笔记3 图像平滑

 Opencv 笔记3 图像平滑高斯模板的生成

假定中心点的坐标是(0,0),那么距离它最近的8个点的坐标,生产一个权重矩阵,如下Opencv 笔记3 图像平滑

Opencv 笔记3 图像平滑

为了计算权重矩阵,需要设定σ的值。假定σ=1.5,则模糊半径为1的权重矩阵如下: 

Opencv 笔记3 图像平滑

 以上数据我们可以通过matlab 类计算,正太分布的概率和是1,但是我们大概算一下9个点的概率之和不到0.5,这样的话我们就需要归一化,因为这个概率和如果小于1或者大于1的话相当于我们改变了像素的灰度值,小于1则变暗,大于1则变亮。每个值/9个值的总和(权重归一化)。

Opencv 笔记3 图像平滑

 接下来我们就要讨论一下σ值的意义及选取了,σ 表示的是标准差,代表着数据的离散程度

Opencv 笔记3 图像平滑

 σ越大,则图形越宽,尖峰越小,图形变换较为平缓,于是生成的模板各元素值差别不大,类似于平均模板; 

σ越小,则图形越窄,尖峰越大,图形变换较为剧烈,于是生成的模板各元素值差别较大

高斯滤波计算:

 

有了高斯模板,就是加权计算:假如3*3的掩膜覆盖下的一个区域来计算Opencv 笔记3 图像平滑

然后将计算的这些值在加起来替换原来中心的值大概值是24.98~~25。

优化:高斯函数的分离特性:

直接进行二维高斯模糊效率较低,实际上高斯模糊也可以在二维图像上对两个独立的一维空间分别进行计算,这叫作线性可分:Opencv 笔记3 图像平滑

 上面可以看到,我们可以先计算一个维度上的高斯模糊即一维模糊G(x),然后再对G(x)进行另一个方向上的高斯模糊G(y)--》G(x)*G(y),自此高斯模糊的相关理论全部完成,接下来是代码实现:

Opencv 笔记3 图像平滑

均值滤波:个区域内所有点的灰度值的平均值作为这个点的灰度值
void blur(InputArray src, OutputArray dst, Size ksize, Point anchor=Point(-1,-1), int borderType=BORDER_DEFAULT )
src – 输入图片
dst – 输出图片
ksize – 模糊内核大小
anchor – 锚点,默认值是(-1,-1),也就是锚点在内核的中心。
borderType – 用于判断图像边界的模式。

GaussianBlur(InputArray src, OutputArray dst, Size ksize, double sigmaX, double sigmaY=0, int borderType=BORDER_DEFAULT)
参数	
src – 输入图片,只能是CV_8U, CV_16U, CV_16S, CV_32F or CV_64F.
dst – 输出图片
ksize – 高斯内核大小。ksize.width和ksize.height允许不相同但他们必须是正奇数。或者等于0,由参数sigma的乘机决定。
sigmaX – 高斯内核在X方向的标准偏差,
sigmaY – 高斯内核在Y方向的标准偏差 可以理解为对sigmaX的补偿
borderType – 用于判断图像边界的模式。
最有用的滤波器 (尽管不是最快的)。 高斯滤波是将输入数组的每一个像素点与高斯内核卷积将卷积和当作输出像素值。

中值平滑:中值滤波将图像的每个像素用邻域 (以当前像素为中心的正方形区域)像素的中值代替 
void medianBlur(InputArray src, OutputArray dst, int ksize)
Parameters:	
src – 支持1、3、4通道图片输入,(CV_8U、CV_16U、 CV_32F)
dst – 输出图片
ksize – 线性直径大小,只能是一个大于1的奇数
 

void bilateralFilter(InputArray src, OutputArray dst, int d, double sigmaColor, double sigmaSpace, intborderType=BORDER_DEFAULT )
Parameters:	
src – 源必须是8位或者浮点数,1或者3通道图片。
dst – 输出图片
d – 在滤波过程中使用的各像素邻域直径,如果这是一个非整数,则这个值由sigmaSpace决定。
sigmaColor – 颜色空间的标准方差。数值越大,意味着越远的的颜色会被混进邻域内,从而使更大的颜色段获得相同的颜色。
sigmaSpace – 坐标空间的标注方差。 数值越大,以为着越远的像素会相互影响,从而使更大的区域足够相似的颜色获取相同的颜色。当d>0,d指定了邻域大小且与sigmaSpace无关。否则,d正比于sigmaSpace。

 

#if  1 // 自己实现高斯模糊

void  GaussianFilter(const Mat& src, Mat& dst, int ksize, double segma);
void  GaussianFilter(const Mat& src, Mat& dst, int ksize, double segma) 
{
	if (ksize<1|| ksize>21) {
		ksize = 3;//当ksize 很大,超过某个值的时候就高斯函数的波峰就几乎不见了,差不多就是一个均值,这样就没有意义了
	}
	CV_Assert(src.channels() || src.channels() == 3); // 只处理单通道或者三通道图像
	//1、定义一个二维数组   ---高斯模板
	double ** GaussianTemplate = new double *[ksize];
	int  radius = ksize / 2;
	// 计算这个模板的中心位置 
	int x=0, y=0;
	for (size_t i = 0; i < ksize; i++)
	{
		GaussianTemplate[i] = new double [ksize];
	}

	// 自此 一个ksize *ksize 的矩阵就形成了,下面就是计算每一个位置的权重
	double sum = 0;//用于求ksize *ksize  的权重之和

	for (int k = 0; k < ksize; k++)
	{
		x = pow(k - radius,2);
		for (int j = 0; j < ksize; j++)
		{
			y = pow(j - radius, 2);
			// 计算高斯函数
			double g = exp(-(x + y) / (2 * segma * segma));
			sum += g;
			GaussianTemplate[k][j] = g;
		}
	}

	//2、归一化
	for (int i = 0; i < ksize; i++)
	{

		for (int j = 0; j < ksize; j++)
		{
			GaussianTemplate[i][j] /= sum;
			cout << GaussianTemplate[i][j] << " ";
		}
		cout << endl;
	}


	// 高斯模板已经完成,可以放心使用了

	// 3、开始掩膜
	 // 先进行边缘补充
	copyMakeBorder(src, dst, radius, radius, radius, radius, BorderTypes::BORDER_REFLECT);
	// 由于src  扩充了边界, 所以dst的也会变大 width+radius
	int rows = dst.rows - radius; 
	int cols = dst.cols - radius;
	// 求same卷积
	for (int i = radius; i < rows; i++)
	{
		for (int j = radius; j < cols; j++)
		{
			// 这里只计算单通道的和三通道的
			double sum_value[3] = {0};
			// [i][j] 表示的模板最中心的那个点的坐标,也是就是上面说的xcenter, ycenter
			for (int u = -radius; u <= radius; u++)
			{
				for (int v = -radius; v <= radius ; v++)
				{
					if (src.channels()==1) 
					{
						double  gray_value = GaussianTemplate[radius+u][radius+v] * dst.at<uchar>(i+u,j+v);
						sum_value[0] += gray_value;
					}
					else if (src.channels()==3)
					{
						Vec3b bgr= dst.at<Vec3b>(i + u, j + v);
						double  k = GaussianTemplate[radius + u][radius + v] ;
						sum_value[0] += k * bgr[0];
						sum_value[1] += k * bgr[1];
						sum_value[2] += k * bgr[2];
					}

				}
			}
			if (src.channels() == 1) // 单通道
			{
				dst.at<uchar>(i, j) = static_cast<uchar>(sum_value[0]);
			}
			else  if (src.channels() == 3) 
			{
				Vec3b bgr = { static_cast<uchar>(sum_value[0]), static_cast<uchar>(sum_value[1]), static_cast<uchar>(sum_value[2]) };
				dst.at<Vec3b>(i, j) = bgr;
			}
		}
	}
	// 释放模板数组
	for (int i = 0; i < ksize; i++)
		delete[] GaussianTemplate[i];
	delete[] GaussianTemplate;

	printf("计算已经完成、、、、、、、、、、、、、、、");
}
Mat src, dst, bimage, desg, Gaussian_shuangbian, mind, My_Gaussian;
const char* win_gaussian_myself = "自定义高斯模糊";
int  ksize = 3;
int  max_ksize = 20;
int  segma = 1;
int   segma_max = 5;
void callbackMyGaussianfilter(int,void*);
void callbackMyGaussianfilter(int, void*) 
{
	int size = 2 * ksize + 1;
	GaussianFilter(src, My_Gaussian, size, segma);
	imshow(win_gaussian_myself, My_Gaussian);
}
int main(int args, char* arg)
{
	// point  

	src = imread("C:\\Users\\19473\\Desktop\\opencv_images\\88.jpg");
	if (!src.data)
	{
		printf("could not  load  image....\n");
	}
	imshow("原图", src);

	printf("src.channels() %d\n", src.channels());

	// Gaussian  fiter 
	GaussianBlur(src, desg, Size(3, 3), 0, 9);
	imshow("OPencv GaussianBlur ", desg);



	printf("自己实现的高斯模糊-------------------------\n");
	namedWindow(win_gaussian_myself, CV_WINDOW_AUTOSIZE);
	My_Gaussian = Mat::zeros(src.size(), src.type());

	//imshow("自定义高斯模糊", My_Gaussian);
	createTrackbar("size", win_gaussian_myself, &ksize, max_ksize,callbackMyGaussianfilter);
	createTrackbar("segma", win_gaussian_myself, &segma, segma_max, callbackMyGaussianfilter);
	callbackMyGaussianfilter(0,0);

	waitKey(0);
	return -1;
}
#endif

#if  0 // 图像模糊   均值滤波  高斯滤波
int main(int args, char* arg)
{
	// point  
	Mat src, bimage,desg, Gaussian_shuangbian,mind;
	bimage = imread("C:\\Users\\19473\\Desktop\\opencv_images\\88.jpg");
	if (!src.data)
	{
		printf("could not  load  image....\n");
	}
	imshow("原图", bimage);

	// 均值滤波
	blur( bimage, src,Size(9,9),Point(-1,-1));
	imshow("均值滤波", src);


	// 中值滤波算法---去掉椒盐噪声
	medianBlur(bimage, mind,3);
	imshow("中值滤波", mind);



	// 边缘保留的算法   双边保留边缘算法--磨皮算法
	bilateralFilter(bimage, Gaussian_shuangbian,3,80,3);
	Mat   result;
	Mat kernel = (Mat_<int>(3, 3)<< 0,-1,0,-1,5,-1,0,-1,0);
	filter2D(Gaussian_shuangbian, result,-1, kernel,Point(-1,-1),0);
	imshow(" 双边保留边缘", result);
	waitKey(0);
	return -1;
}
#endif

 总结:只要搞懂高斯滤波,其他的都比较好理解

下一步:边缘算子原理与代码实现

上一篇:2021-08-26 OpenCV (python)学习笔记(三)


下一篇:Python实现高斯平滑与Sobel边缘检测