Opencv-C++笔记 (13) : opencv-图像卷积一(均值、中值、高斯、双边滤波)与 边缘处理

文章目录

一、概述图像滤波

头文件 quick_opencv.h:声明类与公共函数

cpp 复制代码
#pragma once
#include <opencv2\opencv.hpp>
using namespace cv;

class QuickDemo {
public:
	...
	void blur_Demo(Mat& image);
	void medianblur_Demo(Mat& image);
	void gaussian_Demo(Mat& image);
	void bilateralFilter_Demo(Mat& image);

	void custom_mask_Demo(Mat& image); //自定义掩膜运算
    void edge_process_Demo(Mat& image1);

};

主函数调用

cpp 复制代码
#include <opencv2\opencv.hpp>
#include <quick_opencv.h>
#include <iostream>
using namespace cv;


int main(int argc, char** argv) {
	Mat src = imread("D:\\Desktop\\pandas.jpg");
	if (src.empty()) {
		printf("Could not load images...\n");
		return -1;
	}
	namedWindow("input", WINDOW_NORMAL);
	imshow("input", src);

	QuickDemo qk;

	...
	qk.blur_Demo(src);
	qk.medianblur_Demo(src);
	qk.gaussian_Demo(src);
	qk.bilateralFilter_Demo(src);
	qk.custom_mask_Demo(src);
	qk.edge_process_Demo(src1);
	waitKey(0);
	destroyAllWindows();
	return 0;
}

1.1、均值滤波

cpp 复制代码
blur( InputArray src,OutputArray dst, Size ksize, 
      Point anchor = Point(-1,-1),
      int borderType = BORDER_DEFAULT)

src:输入图像 。

dst:输出图像 。

ksize:内核大小 ,一般用 Size(w,h),w 为宽度,h 为深度。

anchor:被平滑的点,表示取 内核中心 ,默认值 Point(-1,-1)。

boderType:推断图像外部像素的某种边界模式。默认值 BORDER_DEFAULT
目的:去除图像上的尖锐噪声,平滑图像。 原理:均值滤波属于线性滤波,它的实现原理是邻域平均法

源文件 quick_demo.cpp:实现类与公共函数

cpp 复制代码
void QuickDemo::blur_Demo(Mat& image) {
	Mat dst, dst1, dst2;
	blur(image, dst, Size(5, 5), Point(-1, -1));
	blur(image, dst1, Size(15, 1), Point(-1, -1));
	blur(image, dst2, Size(1, 15), Point(-1, -1));
	imshow("均值滤波", dst);
	imshow("横向滤波", dst1);
	imshow("竖直滤波", dst2);
}

1.2中值滤波

cpp 复制代码
medianBlur(InputArray src,OutputArray dst,int ksize)

src:输入图像 。

dst:输出图像 。

ksize:孔径的线性尺寸,这个参数必须是大于1 的奇数
统计排序滤波器 中值对椒盐噪声有很好的抑制作用

cpp 复制代码
void QuickDemo::medianblur_Demo(Mat& image) {
	Mat dst, dst1, dst2;
	medianBlur(image.clone(), dst, (3, 3));
	medianBlur(image.clone(), dst1, (5, 5));
	medianBlur(image.clone(), dst2, (7, 7));
	imshow("中值滤波", dst);
	imshow("中值滤波1", dst1);
	imshow("中值滤波2", dst2);
}

1.3、高斯滤波

cpp 复制代码
GaussianBlur( InputArray src, OutputArray dst, Size ksize,
                   double sigmaX, double sigmaY = 0,
                   int borderType = BORDER_DEFAULT )

src:输入图像 。

dst:输出图像 。

ksize:ksize.width 和 ksize.height 可以不同,但他们都必须为正数和奇数,或者为0,可由 sigma 计算而来

sigmaX:高斯核函数在 X 方向的的标准差

sigmaY:高斯核函数在 Y 方向的的标准差

若 sigmaY 为零,就将它设为 sigmaX;若 sigmaX 和 sigmaY 都是0,那么就由 ksize.width 和 ksize.height 计算出来

高斯滤波是一种线性平滑滤波,适用于消除高斯噪声,广泛应用于图像处理的减噪过程。

高斯滤波就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。

高斯滤波的具体操作是:用一个模板(或称卷积、掩模)扫描图像中的每一个像素,用模板确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值。

cpp 复制代码
void QuickDemo::gaussian_Demo(Mat& image) {
	Mat dst;
	GaussianBlur(image, dst, Size(5, 5), 15);
	imshow("高斯滤波", dst);
}

1.4、双边滤波

边缘保留滤波算法:

  • 高斯双边滤波
  • Meanshift均值迁移模糊
  • 局部均方差模糊
  • 导向滤波

双边滤波的原理示意图:

cpp 复制代码
void bilateralFilter(
InputArray src,    输入图像
OutputArray dst,   目标图像,需要和源图片有一样的尺寸和类型
int d,        在过滤期间使用的每个像素邻域的直径。如果输入d非0(没输入),则sigmaSpace由d计算得出。
double sigmaColor,  颜色空间滤波器的sigma值,这个参数的值越大,就表明该像素邻域内有更宽广的颜色会被混合到一起,产生较大的半相等颜色区域。
double sigmaSpace, 坐标空间滤波器的sigma值,坐标空间的标注方差。他的数值越大,意味着越远的像素会相互影响,从而使更大的区域足够相似的颜色获取相同的颜色。当d>0,d指定了邻域大小且与sigmaSpace无关。否则,d正比于sigmaSpace。
int borderType = BORDER_DEFAULT  图像边界模式
)

src: 输入图像,可以是Mat类型,图像必须是8位或浮点型单通道、三通道的图像。

dst: 输出图像,和原图像有相同的尺寸和类型。

d: 表示在过滤过程中每个像素邻域的直径范围。如果这个值是非正数,则函数会从第五个参数sigmaSpace计算该值。

sigmaColor:颜色空间过滤器的sigma值,这个参数的值月大,表明该像素邻域内有月宽广的颜色会被混合到一起,产生较大的半相等颜色区域。

sigmaSpace: 坐标空间中滤波器的sigma值,如果该值较大,则意味着颜色相近的较远的像素将相互影响,从而使更大的区域中足够相似的颜色获取相同的颜色。当d>0时,d指定了邻域大小且与sigmaSpace五官,否则d正比于sigmaSpace.

borderType=BORDER_DEFAULT: 用于推断图像外部像素的某种边界模式,有默认值BORDER_DEFAULT.

cpp 复制代码
void QuickDemo::bilateralFilter_Demo(Mat& image) {
	Mat dst;
	bilateralFilter(image, dst, 0, 100.0, 10.0);
	imshow("双边滤波", dst);
}

1.5、方框滤波

cpp 复制代码
boxFilter( InputArray src, OutputArray dst, int ddepth,
                Size ksize, Point anchor = Point(-1,-1),
                bool normalize = true,
                int borderType = BORDER_DEFAULT )

src:输入图像

dst:输出图像

ddepth:输出图像的深度,-1 代表使用原图深度

ksize: 滤波内核的大小。一般这样写Size(w, h)来表示内核的大小,Size(10, 10)就表示 10x10 的核大小

anchor = Point(-1,-1) :表示锚点(即被平滑的那个点),注意他有默认值Point(-1,-1)

如果这个点坐标是负值的话,就表示取核的中心为锚点,所以默认值Point(-1,-1)表示这个锚点在核的中心。

normalize = true:默认值为true,一个标识符,表示内核是否被其区域归一化(normalized)了

borderType =BORDER_DEFAULT:用于推断图像外部像素的某种边界模式。有默认值BORDER_DEFAULT,我们一般不去管它。

二、自定义掩码

cpp 复制代码
Mat kernel_ = (Mat_<char>(3, 3) << 0, -1, 0, 
							      -1, 5, -1, 
		                           0, -1, 0);
cpp 复制代码
void filter2D(
InputArray src,     输入图像
OutputArray dst,     输出图像,和输入图像具有相同的尺寸和通道数量
int ddepth,      目标图像深度,为-1时保持输入图像通道深度
InputArray kernel,    卷积核
Point anchor=Point(-1,-1),表示过滤点在内核中的相对位置的内核锚;锚应位于内核内;默认值(-1,-1)表示锚点位于内核中心。
double delta=0,     在储存目标图像前可选的添加到像素的值,默认值为0
int borderType=BORDER_DEFAULT   默认值是BORDER_DEFAULT,即对全部边界进行计算。
);
cpp 复制代码
void QuickDemo::custom_mask_Demo(Mat& image) {
	// 自定义filter函数实现
	int offset = image.channels();
	int cols = (image.cols - 1) * offset;
	int rows = image.rows;
	Mat dst = Mat::zeros(image.size(), image.type());
	for (int row = 1; row < (rows - 1); row++) {
		const uchar* previous = image.ptr<uchar>(row-1);
		const uchar* current = image.ptr<uchar>(row);
		const uchar* next = image.ptr<uchar>(row+1);
		uchar* output = dst.ptr<uchar>(row);
		for (int col = offset; col < cols; col++) {
			output[col] = saturate_cast<uchar>(5 * current[col] - (current[col - offset] + current[col + offset] + previous[col] + next[col]));
		}
	}
	imshow("dst", dst);

	// opencv自带filter2D函数实现
	Mat dst2;
	Mat kernel_ = (Mat_<char>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
	filter2D(image.clone(), dst2, image.depth(), kernel_);
	imshow("dst2", dst2);
}

可以看出dst2没有黑边了,边缘被处理了。

三、边缘处理

在卷积开始之前增加边缘像素,填充的像素值为0或者RGB黑色,比如3x3在

四周各填充1个像素的边缘,在卷积处理之后再去掉这些边缘,这样就确保图像的边缘被处理

cpp 复制代码
void copyMakeBorder(
InputArray src,     // 输入图像
OutputArray dst,  // 输出图像
int top,       // 填充边缘长度,一般上下左右都取相同值
int bottom,
int left,
int right,
int borderType,   // 填充类型
const Scalar& value = Scalar() // 边框颜色值,边框类型=BUALE_CONTER时才有用
)

openCV中的处理方法是:

BORDER_DEFAULT  - 默认的处理(镜像边界像素:1234|4321) BORDER_CONSTANT -- 填充边缘用指定像素值

BORDER_REPLICATE -- 填充边缘像素用已知的边缘像素值。(复制边界像素:1234|4444) BORDER_WRAP --

用另外一边的像素来补偿填充 使用参数

BORDER_CONSTANT = 0

BORDER_REPLICATE = 1

BORDER_REFLECT = 2

BORDER_REFLECT_101 = 4,

BORDER_REFLECT101 = BORDER_REFLECT_101,

BORDER_DEFAULT = BORDER_REFLECT_101,

BORDER_WRAP = 3,

BORDER_TRANSPARENT = 5,

源文件 quick_demo.cpp:实现类与公共函数

cpp 复制代码
void QuickDemo::edge_process_Demo(Mat& image) {
	Mat dst0, dst1;
	copyMakeBorder(image, dst0, 5, 5, 5, 5, BORDER_REFLECT);
	copyMakeBorder(image, dst1, 5, 5, 5, 5, BORDER_CONSTANT,Scalar(0,255,255));
	imshow("REFLECT", dst0);
	imshow("CONSTANT", dst1);
}

四、Sobel算子

实现步骤:

x轴方向求导 ------ y轴方向求导 ------ 最终结果为二者相加

函数原型:

Sobel(src,ddepth,dx,dy,ksize=3,...)

离散微分算子(discrete differentiation operator),用来计算图像灰度的近似梯度

Soble算子功能集合高斯平滑和微分求导

一阶微分算子,求导算子,在水平和垂直两个方向上求导,得到图像X方法与Y方向梯度图像

代码案例:

cpp 复制代码
chess = cv2.imread('chess.png')
# 求y方向边缘
dy = cv2.Sobel(chess, cv2.CV_64F, 1, 0, ksize=5)
# 求x方向边缘
dx = cv2.Sobel(chess, cv2.CV_64F, 0, 1, ksize=5)
# 二者相加
result = dy + dx

cv2.imshow('chess', chess)
cv2.imshow('dy', dy)
cv2.imshow('dx', dx)
cv2.imshow('result', result)
cv2.waitKey(0)

从上图可以明显看出,当dx设置为1时,求得y方向上的边缘信息,反之也是,最终二者相加的结果也就是Sobel算子的结果。不能一开始就设定dx,dy为1,这样子不能达到该效果;

五、Scharr算子

定义:与Sobel类似,但使用的kernel值不同,并且只能为3x3,只能求x方向或y方向一个方向的边缘信息;

由于Sobel算子求取导数的近似值,kernel=3时不是很准确,图像中较弱的边缘提取效果较差,OpenCV使用改进版本Scharr函数,算子如上右图:

例如一个 3 * 3 的 Sobel 算子,在梯度角度水平或垂直方向时,其不精确性就非常明显。Scharr 算子是对 Sobel 算子差异性的增强,两者之间的在检测图像边缘的原理和使用方式上相同。

而 Scharr 算子的主要思路是通过将模版中的权重系数放大来增大像素值间的差异,也是计算 x 或 y 方向上的图像差分

sobel,Scharr算子等此类算子都是:输出图像深度 >= 输入图像深度 。

cpp 复制代码
void Sobel(
InputArray src,     输入图像(单通道)
OutputArray dst,     输出图像
int ddepth,       输出图像深度 (>= 输入图像深度)
int dx,         x梯度(几阶导数)一般为0、1、2,其中0表示这个方向上没有求导
int dy,         y梯度(几阶导数)
int ksize = 3,      滤波窗口大小(3,5,7...)
double scale = 1,    是否缩放,默认=1.0
double delta = 0,     偏移常数
int borderType = BORDER_DEFAULT )  图像边缘处理方式。默认值=cv2.BORDER_DEFAULT

在经过处理后,需要用 convertScaleAbs() 函数将其转回原来的uint8形式,否则将无法显示图像,而只是一副灰色的窗口。

cpp 复制代码
void convertScaleAbs(
InputArray src,       输入图像
OutputArray dst,    输出uint8类型图片
double alpha = 1,     伸缩系数
double beta = 0     偏移值:加到结果上的一个值
)

由于Sobel算子是在两个方向计算的,最后还需要用cv::addWeighted(src1, alpha, src2, beta, gamma, dst)函数将其组合起来。

cpp 复制代码
//头文件 quick_opencv.h:声明类与公共函数
#pragma once
#include <opencv2\opencv.hpp>
using namespace cv;

class QuickDemo {
public:
	...
	void edge_extract_Demo(Mat& image1);
	void laplance_Demo(Mat& image1);
	void canny_Demo(Mat& image1);
};
cpp 复制代码
//主函数调用该类的公共成员函数
#include <opencv2\opencv.hpp>
#include <quick_opencv.h>
#include <iostream>
using namespace cv;


int main(int argc, char** argv) {
	Mat src = imread("D:\\Desktop\\jianbian.png");
	if (src.empty()) {
		printf("Could not load images...\n");
		return -1;
	}
	QuickDemo qk;
	qk.edge_extract_Demo(src1);
	qk.laplance_Demo(src1);
	qk.canny_Demo(src1);
	waitKey(0);
	destroyAllWindows();
	return 0;
}
cpp 复制代码
//源文件 quick_demo.cpp:实现类与公共函数
void QuickDemo::edge_extract_Demo(Mat& image) {
	Mat gau_dst, gray_dst, xgrad, ygrad;
	GaussianBlur(image, gau_dst, Size(3, 3), 10);
	cvtColor(gau_dst, gray_dst, COLOR_BGR2GRAY);

	//Sobel(gray_dst, xgrad, -1, 1, 0, 3);
	//Sobel(gray_dst, ygrad, -1, 0, 1, 3);

	//Sobel(gray_dst, xgrad, CV_16S, 1, 0, 3);
	//Sobel(gray_dst, ygrad, CV_16S, 0, 1, 3);

	Scharr(gray_dst, xgrad, CV_16S, 1, 0, 3);
	Scharr(gray_dst, ygrad, CV_16S, 0, 1, 3);
	convertScaleAbs(xgrad, xgrad);
	convertScaleAbs(ygrad, ygrad);

	Mat xygrad;
	addWeighted(xgrad, 0.5, ygrad, 0.5, 0, xygrad);
	imshow("xgrad", xgrad);
	imshow("ygrad", ygrad);
	imshow("xygrad", xygrad);

	Mat xy_grad = Mat::zeros(xgrad.size(),xgrad.type());
	int width = xgrad.cols;
	int height = xgrad.rows;
	for (int h = 0; h < height; h++) {
		uchar* current_x = xgrad.ptr<uchar>(h);
		uchar* current_y = ygrad.ptr<uchar>(h);
		uchar* current_d = xy_grad.ptr<uchar>(h);
		for (int w = 0; w < width; w++) {
			*current_d++ = saturate_cast<uchar>(*current_x++ + *current_y++);
		}
	}
	imshow("xy_grad", xy_grad);
}

输出图像深度:-1(与原图保持一致)

输出图像深度:CV_16S 方向梯度细节更多了。

使用自定义函数直接相加结果(右): (使用addWeighted函数,各方向梯度0.5的权重组合发虚:左)

测试scharr()效果:细节更加丰富

六、拉普拉斯算子

优点:可同时求得两个方向的边缘;

缺点:对噪音比较敏感,一般需要先进行去噪在调用拉普拉斯算子;

函数原型:

Laplacian(img,ddepth,ksize=1)

在二阶导数的时候,最大变化处的值为零即边缘是零值。通过二阶导数计算,依据此理论我们可以计算图像二阶导数,提取边缘。

cpp 复制代码
void Laplacian(
InputArray src,      输入图像(单通道)
OutputArray dst,      输出图像
int ddepth,        输出图像深度 (>= 输入图像深度)
int ksize = 1,       滤波窗口大小(3,5,7...)
double scale = 1,     是否缩放,默认=1.0
double delta = 0,     偏移常数
int borderType = BORDER_DEFAULT   图像边缘处理方式
)

一般处理流程

高斯模糊 -- 去噪声GaussianBlur()

转换为灰度图像cvtColor()

拉普拉斯 -- 二阶导数计算Laplacian()

取绝对值convertScaleAbs()

显示结果

cpp 复制代码
void QuickDemo::laplance_Demo(Mat& image) {
	Mat gau_dst, gray_dst, laplance_dst, ygrad;

	GaussianBlur(image, gau_dst, Size(3, 3), 10);
	cvtColor(gau_dst, gray_dst, COLOR_BGR2GRAY);

	Laplacian(gray_dst, laplance_dst, CV_16S, 3, 1.0, 0);
	convertScaleAbs(laplance_dst, laplance_dst);

	threshold(laplance_dst, laplance_dst, 0, 255, THRESH_OTSU | THRESH_BINARY);
	imshow("laplance_dst", laplance_dst);
}
cpp 复制代码
chess = cv2.imread('chess.png')
result = cv2.Laplacian(chess, cv2.CV_64F, ksize=5)

cv2.imshow('chess', chess)
cv2.imshow('result', result)
cv2.waitKey(0)

从效果上看,比起Sobel步骤更加简单,并且效果也比较好,缺点就是如果噪声过多的话效果会比较差

十、Canny算法

实现步骤:

1、使用5x5高斯滤波消除噪音;

2、使用Sobel计算图像梯度的方向(0°、45°、90°、135°);

3、取局部极大值;

4、阈值计算;

函数原型:

Canny(img,minVal,maxVal,...)

其中的minVal和maxVal代表边缘的阈值,两者差值过大的话会损失一定的边缘信息;

代码案例:

cpp 复制代码
img = cv2.imread('1.jpg')
result = cv2.Canny(img, 100, 200)

cv2.imshow('org', img)
cv2.imshow('result', result)
cv2.waitKey(0)

Canny算法介绍 - 非最大信号抑制

T1, T2为阈值,凡是高于T2的都保留,凡是小于T1都丢弃,从高于T2的像素出发,凡是大于T1而且相互连接的,都保留。最终得到一个输出二值图像。

推荐的高低阈值比值为 T2: T1 = 3:1/2:1其中T2为高阈值,T1为低阈值

cpp 复制代码
void Canny(
InputArray image,        输入图像(单通道)
OutputArray edges,     输出图像
double threshold1,      阈值T1
double threshold2,     阈值T2 一般为 2*T1
int apertureSize = 3,     Soble算子的size窗口大小 
bool L2gradient = false   默认false L1归一化,Ture则为L2归一化
);

一般处理流程

高斯模糊 - GaussianBlur

灰度转换 - cvtColor

计算梯度 -- Sobel/Scharr

非最大信号抑制

高低阈值输出二值图像

cpp 复制代码
void QuickDemo::canny_Demo(Mat& image) {
	Mat gau_dst, gray_dst, grad_dst;

	GaussianBlur(image, gau_dst, Size(3, 3), 10);
	cvtColor(gau_dst, gray_dst, COLOR_BGR2GRAY);

	Canny(gray_dst, grad_dst, 50.0, 100.0, 3, false);
	imshow("grad_dst", grad_dst);
}
相关推荐
UestcXiye3 分钟前
《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
c++·计算机网络·ip·tcp
亦枫Leonlew4 分钟前
三维测量与建模笔记 - 3.3 张正友标定法
笔记·相机标定·三维重建·张正友标定法
考试宝8 分钟前
国家宠物美容师职业技能等级评价(高级)理论考试题
经验分享·笔记·职场和发展·学习方法·业界资讯·宠物
霁月风1 小时前
设计模式——适配器模式
c++·适配器模式
jrrz08282 小时前
LeetCode 热题100(七)【链表】(1)
数据结构·c++·算法·leetcode·链表
咖啡里的茶i2 小时前
Vehicle友元Date多态Sedan和Truck
c++
海绵波波1072 小时前
Webserver(4.9)本地套接字的通信
c++
黑叶白树2 小时前
简单的签到程序 python笔记
笔记·python
@小博的博客2 小时前
C++初阶学习第十弹——深入讲解vector的迭代器失效
数据结构·c++·学习
幸运超级加倍~3 小时前
软件设计师-上午题-15 计算机网络(5分)
笔记·计算机网络