卷积与滤波概念
离散卷积
丢两个骰子,求点数加起来为
t
t
t 的概率是多少?
两个骰子加起来为4的概率:
f
(
1
)
g
(
3
)
+
f
(
2
)
g
(
2
)
+
f
(
3
)
g
(
1
)
f(1)g(3) + f(2)g(2) + f(3)g(1)
f(1)g(3)+f(2)g(2)+f(3)g(1)
写成卷积标准形式为:
(
f
∗
g
)
(
4
)
=
∑
i
=
1
3
f
(
i
)
g
(
4
−
i
)
(f *g)(4) = \sum_{i=1}^3f(i)g(4-i)
(f∗g)(4)=i=1∑3f(i)g(4−i)
进一步,两个骰子加起来为
t
t
t 的概率,就是二个骰子的概率密度函数的卷积:
(
f
∗
g
)
(
t
)
=
∑
i
=
1
t
−
1
f
(
i
)
g
(
t
−
i
)
(f *g)(t) = \sum_{i=1}^{t-1}f(i)g(t-i)
(f∗g)(t)=i=1∑t−1f(i)g(t−i)
与1维卷积类似,图像(二维)卷积定义:
(
f
∗
g
)
(
x
,
y
)
=
1
N
M
∑
i
=
0
N
−
1
∑
j
=
0
M
−
1
f
(
i
,
j
)
g
(
x
−
i
,
y
−
j
)
(f *g)(x,y) = \frac{1}{NM}\sum_{i=0}^{N-1}\sum_{j=0}^{M-1}f(i,j)g(x-i,y-j)
(f∗g)(x,y)=NM1i=0∑N−1j=0∑M−1f(i,j)g(x−i,y−j)
g
g
g 称为滤波器
图像滤波如何计算
- 滤波器 g ( x , y ) g(x,y) g(x,y) 左右、上下反转:得到 g ( − x , − y ) g(-x,-y) g(−x,−y);
- 按照前述公式计算卷积(先乘后累加)
图像平滑滤波与去噪
图像滤波由卷积定义,基本知识:
- 4-领域与8-领域
图像平滑:
- 平均滤波:在一个小区域内(通常3*3)像素值平均
g ( x , y ) = 1 M ∑ i , j f ( i , j ) g(x,y) = \frac{1}{M}\sum_{i,j}f(i,j) g(x,y)=M1i,j∑f(i,j)
其中 M M M表示像素值个数,滤波器采用4-领域时 M = 5 M = 5 M=5;滤波器采用8-领域时 M = 9 M = 9 M=9;如果不考虑中间像素 M = 8 M = 8 M=8。 - 加权平均滤波:在一个小区域内像素值加权平均
g ( x , y ) = ∑ i , j w i j f ( i , j ) g(x,y) = \sum_{i,j}w_{ij}f(i,j) g(x,y)=i,j∑wijf(i,j)
其中 w i j w_{ij} wij表示所有像素值之和的倒数。 - 高斯滤波器:元素值分布根据位置呈现高斯函数的特点
1 16 [ 1 2 1 2 4 2 1 2 1 ] \frac{1}{16} \left[ \begin{array}{ccc} 1 & 2 & 1\\\\ 2 & 4 & 2\\\\ 1 & 2 & 1\\\\ \end{array} \right] 161⎣⎢⎢⎢⎢⎢⎢⎡121242121⎦⎥⎥⎥⎥⎥⎥⎤ - 双边滤波器:元素值不仅与位置有关,还和源图像在该位置的像素值有关。其是空域核和值域核的叠加,通过双边滤波器,可以在使图像平滑的同时,比较好的保留边缘。
- 中值滤波:确定窗口及位置(含有奇数个像素),窗口内像素按灰度大小排序,取中间值代替原窗口中心像素值。对椒盐噪声有效。
总结:
- 平滑滤波包括平均滤波、高斯滤波、中值滤波等方法,其中高斯滤波最为常用。
- 平滑滤波具有去除噪声效果,不同滤波方法具有对不同噪声的适应性。
数学形态学滤波
数学形态学基本操作——膨胀和腐蚀
A
⨁
B
A\bigoplus B
A⨁B表示集合
A
A
A 用结构元素
B
B
B 膨胀(dilate),定义为:
A
⨁
B
=
∪
(
A
)
b
(
b
∈
B
)
A\bigoplus B = \cup(A)_b (b\in B)
A⨁B=∪(A)b(b∈B)
表示,将
A
A
A 按照
B
B
B 中的所有元素进行平移,将所有平移的结果取并集。
A
⨁
B
A\bigoplus B
A⨁B表示集合
A
A
A 用结构元素
B
B
B 腐蚀(erode),定义为:
A
⨀
B
=
∩
(
A
)
−
b
(
b
∈
B
)
A\bigodot B = \cap(A)_{-b} (b\in B)
A⨀B=∩(A)−b(b∈B)
表示,将
A
A
A 按照
B
B
B 中的所有元素进行反方向平移,将所有平移的结果取并集。
直观表现是,腐蚀使黑色区域变大了,膨胀使白色区域变大了。
图像形态学操作——开闭运算
- 膨胀和腐蚀并不互为逆运算,二者级联使用可生成新的形态学操作
- 开运算:先腐蚀后膨胀(岛屿分开了):
( A ⨀ B ) ⨁ B (A\bigodot B)\bigoplus B (A⨀B)⨁B - 闭运算:先膨胀后腐蚀(岛屿闭合了):
( A ⨁ B ) ⨀ B (A\bigoplus B)\bigodot B (A⨁B)⨀B - 先开后闭:可有效去除噪声
数学形态学去噪方法
实战演练:图像平滑滤波对比
OpenCV实现滤波及平滑去噪各函数
- 图像滤波
dst = cv2.filter2D(src, ddepth, kernel [, dst[, anchor[, delta[, borderType]]]])
#dst:目标图像,其与原图像尺寸和通道数相同
#ddepth:目标图像的所需深度,包括CV_16S/CV_32F/CV_64F等
#kernel:卷积核(或相当于相关核),单通道浮点矩阵;如果要将不同的内核应用与不同的通道,请使用拆分,再单独处理各通道
#anchor:内核的锚点,指示内核中过滤点的相对位置;锚应位于内核中;默认值(-1, -1)表示锚位于内核中心。
#detal:在将它们存储在dst中之前,将可选值添加到已过滤的像素中。类似于偏置。
#borderType:卷积填充方式,包括BORDER_CONSTANT, BORDER_REPLICATE, BORDER_REFLECT等。
- 平均滤波
dst = cv2.blur(src, ksize[, dst[,anchor[, borderType]]])
#ksize:滤波器大小,如(5, 5)
- 高斯平滑滤波
dst = cv2.GaussianBlur(src, ksize, sigmaX[, dst[, sigmaY[, borderType]]])
- 中值滤波
dst = cv2.medianBlur(src, ksize[, dst])
- 双边滤波
dst = cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace[, dst[, borderType]])
#d:过滤期间使用的各像素领域的直径
#sigmaColor:色彩空间的sigma参数,该参数较大时,各像素领域内相距较远的颜色会被混合到一起,从而造成更大范围的半相等颜色
#sigmaSpace:色彩空间的sigma参数,该参数较大时,只要颜色相近,越远的像素会相互影响
python代码:
import cv2 as cv
import numpy as np
def gauss_noise(image, mean = 0, var = 0.001):
"""
添加高斯噪声
mean: 均值
var: 方差
"""
image = np.array(image/255, dtype = float)
noise = np.random.normal(mean, var ** 0.5, image.shape)
out = image + noise
if out.min() < 0:
low_clip = -1.
else:
low_clip = 0.
out = np.clip(out, low_clip, 1.0)
out = np.uint8(out*255)
# cv.imshow("Gauss_noise", out)
return out
filename = 'C:/python/img/lena.jpg'
img = cv.imread(filename)
img = gauss_noise(img) #原图像加入高斯噪声
blur = cv.blur(img, (5, 5)) #平均滤波
gauss = cv.GaussianBlur(img, (5, 5), 0) #高斯滤波
median = cv.medianBlur(img, 5) #中值滤波
bilateral = cv.bilateralFilter(img, 5, 150, 150) #双边滤波
cv.imshow('Image', img)
cv.imshow('Blurred', blur)
cv.imshow('Gauss', gauss)
cv.imshow('Median filtered', median)
cv.imshow('Bilateral filtered', bilateral)
cv.waitKey()
cv.destroyAllWindows()
c++添加高斯噪声:
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
#include <cstdlib>
#include <limits>
#include <cmath>
using namespace cv;
using namespace std;
double generateGaussianNoise(double mu, double sigma)
{
//定义一个特别小的值
const double epsilon = numeric_limits<double>::min();
//返回目标数据类型能表示的最逼近1的正数和1的差的绝对值
static double z;
//局部变量,这样可以在不同的文件中定义同名函数和同名变量,而不用担心命名冲突
double u1, u2;
//构造随机变量
do
{
u1 = rand()*(1.0 / RAND_MAX);
u2 = rand()*(1.0 / RAND_MAX);
} while (u1 <= epsilon);
//构造高斯随机变量X
z = sqrt(-2.0*log(u1))*sin(2 * CV_PI * u2);
return z * sigma + mu;
}
//为图像添加高斯噪声
Mat addGaussianNoise(Mat& srcImage)
{
Mat resultImage = srcImage.clone(); //深拷贝,克隆
int channels = resultImage.channels(); //获取图像的通道
int nRows = resultImage.rows; //图像的行数
int nCols = resultImage.cols*channels; //图像的总列数
//判断图像的连续性
if (resultImage.isContinuous()) //判断矩阵是否连续,若连续,我们相当于只需要遍历一个一维数组
{
nCols *= nRows;
nRows = 1;
}
for (int i = 0; i < nRows; i++)
{
for (int j = 0; j < nCols; j++)
{ //添加高斯噪声
int val = resultImage.ptr<uchar>(i)[j] + generateGaussianNoise(2, 0.8) * 32;
if (val < 0)
val = 0;
if (val > 255)
val = 255;
resultImage.ptr<uchar>(i)[j] = (uchar)val;
}
}
return resultImage;
}
int main()
{
Mat srcImage = imread("lena.jpg", 1);
if (!srcImage.data)
return -1;
imshow("srcImage", srcImage);
Mat resultImage = addGaussianNoise(srcImage);
imshow("resultImage", resultImage);
waitKey(0);
return 0;
}
- 关于代码里出现的OpenCV isContinuous()函数:
bool cv::Mat::isContinuous() const
该方法用于报告矩阵是否连续。
如果矩阵元素在每行末尾连续存储而没有间隙,则方法返回true。 否则,它返回false。 显然,对于1x1或1xN矩阵总是连续的。一般 用Mat :: create创建的矩阵总是连续的。
2. Mat 数据类型指针ptr 的使用
这里的 depth_.ptr(y)[x] 就是指向depth _ 的第y 行的第x个数据,数据类型为无符号的短整型。
char 是有符号的 ,uchar(unsigned char) 是无符号的,8-bit无符号整形数据,里面全是正数 。
两者都作为字符用的话是没有区别的,但当整数用时有区别:
char 整数范围为-128到127( 0x80__0x7F) ;
而unsigned char 整数范围为0到255( 0__0xFF ) 有时候想把整数数值限在255范围内,也用unsigned char。
而本例中resultImage.ptr<uchar>(i)[j]是指向resultImage第 i 行第 j 个元素,限制数值位0-255的整数。
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace std;
using namespace cv;
typedef struct _imgPair {
Mat *src;
Mat *dst;
void * Param;
char* winName;
}ImgPair;
typedef struct _gaussianParam {
int kernelSize;
int sigma;
}GaussianParam;
typedef struct _bilateralParam {
int kernelSize;
int sigmaColor;
int sigmaSpace;
}BilateralParam;
void on_gaussiankernelBar(int ksize, void *userdata)
{
ImgPair* pImgPair = (ImgPair*)userdata;
GaussianParam *gPair = (GaussianParam *)(pImgPair->Param);
//->用于指向结构体的指针,表示结构体内的元素
gPair->kernelSize = ksize;
GaussianBlur(*(pImgPair->src), *(pImgPair->dst), Size(gPair->kernelSize / 2 * 2 + 1, gPair->kernelSize / 2 * 2 + 1), gPair->sigma, gPair->sigma);
//Mat类型的对象不直接做函数参数,而是用指向它的指针
//gPair->kernelSize / 2 * 2 + 1可以确保卷积核尺寸为奇数,这样可以随意拖动bar,不用担心拖到偶数
//sigma.X,sigma.Y分别为X,Y方向卷积核的方差,其中sigma.Y默认设置为0
imshow(pImgPair->winName, *(pImgPair->dst));
}
void on_gaussianSigmaBar(int sigma, void *userdata)
{
ImgPair* pImgPair = (ImgPair*)userdata;
GaussianParam *gPair = (GaussianParam *)(pImgPair->Param);
gPair->sigma = double(sigma);
GaussianBlur(*(pImgPair->src), *(pImgPair->dst), Size(gPair->kernelSize / 2 * 2 + 1, gPair->kernelSize / 2 * 2 + 1), gPair->sigma, gPair->sigma);
//无论拖动ksize的条,还是sigma的条,都要重新运行GuassianBlur函数,重新计算。
imshow(pImgPair->winName, *(pImgPair->dst));
}
void on_medianSigmaBar(int ksize, void *userdata)
{
ImgPair* pImgPair = (ImgPair*)userdata;
medianBlur(*(pImgPair->src), *(pImgPair->dst), ksize / 2 * 2 + 1);
//中值滤波只有三个参数
imshow(pImgPair->winName, *(pImgPair->dst));
}
void on_bilateralDBar(int ksize, void *userdata)
{
//拖动双边滤波ksize的bar
ImgPair* pImgPair = (ImgPair*)userdata;
BilateralParam *param = (BilateralParam *)(pImgPair->Param);
bilateralFilter(*(pImgPair->src), *(pImgPair->dst), ksize / 2 * 2 + 1, param->sigmaColor, param->sigmaSpace);
//灰度距离核和空间距离核的sigma
param->kernelSize = ksize;
imshow(pImgPair->winName, *(pImgPair->dst));
}
void on_bilateralSigmaSpaceBar(int sigmaSpace, void *userdata)
{
ImgPair* pImgPair = (ImgPair*)userdata;
BilateralParam *param = (BilateralParam *)(pImgPair->Param);
bilateralFilter(*(pImgPair->src), *(pImgPair->dst), param->kernelSize / 2 * 2 + 1, param->sigmaColor, sigmaSpace);
param->sigmaSpace = sigmaSpace;
imshow(pImgPair->winName, *(pImgPair->dst));
}
void on_bilateralSigmaColorBar(int sigmaColor, void *userdata)
{
ImgPair* pImgPair = (ImgPair*)userdata;
BilateralParam *param = (BilateralParam *)(pImgPair->Param);
bilateralFilter(*(pImgPair->src), *(pImgPair->dst), param->kernelSize / 2 * 2 + 1, sigmaColor, param->sigmaSpace);
param->sigmaColor = sigmaColor;
imshow(pImgPair->winName, *(pImgPair->dst));
}
int main()
{
Mat src = imread("lena.jpg");
namedWindow("src");
imshow("src", src);
/*-------GaussianBlur-----------*/
Mat GaussianBlurImg;
namedWindow("GaussianBlurImg");
GaussianParam gaussianParam = { 5, 1.0 };
GaussianBlur(src, GaussianBlurImg, Size(5, 5), 1, 1);
GaussianParam gparam = { 5, 1.0 };
ImgPair gaussianPair = { &src, &GaussianBlurImg, &gparam, "GaussianBlurImg" };
imshow("GaussianBlurImg", GaussianBlurImg);
createTrackbar("kernelsize", "GaussianBlurImg", &(gparam.kernelSize), 30, on_gaussiankernelBar, &gaussianPair);
createTrackbar("sigma", "GaussianBlurImg", &(gparam.sigma), 10, on_gaussianSigmaBar, &gaussianPair);
//createTrackbar是Opencv中的API,其可在显示图像的窗口中快速创建一个滑动控件
//形式参数一、trackbarname:滑动空间的名称;
//形式参数二、winname:滑动空间用于依附的图像窗口的名称;
//形式参数三、value:初始化阈值;
//形式参数四、count:滑动控件的刻度范围;
//形式参数五、TrackbarCallback是回调函数,其定义如下
/*-------medianBlur-----------*/
Mat MedianBlurImg;
int kernelSize = 5; //初始化的kerbelSize
ImgPair medianPair = { &src, &MedianBlurImg, nullptr, "MedianBlurImg" };
medianBlur(src, MedianBlurImg, 5);
imshow("MedianBlurImg", MedianBlurImg);
createTrackbar("kernelsize", "MedianBlurImg", &(kernelSize), 30, on_medianSigmaBar, &medianPair);
/*---Bilateral-----------------*/
Mat BilateralFilterImg;
bilateralFilter(src, BilateralFilterImg, 5, 2, 2);
BilateralParam bparam = { 5,1,1 };
ImgPair bilateralPair = { &src, &BilateralFilterImg, &bparam, "BilateralFilterImg" };
imshow("BilateralFilterImg", BilateralFilterImg);
createTrackbar("kernelsize", "BilateralFilterImg", &(bparam.kernelSize), 30, on_bilateralDBar, &bilateralPair);
createTrackbar("sigmaspace", "BilateralFilterImg", &(bparam.sigmaSpace), 30, on_bilateralSigmaSpaceBar, &bilateralPair);
createTrackbar("sigmacolor", "BilateralFilterImg", &(bparam.sigmaColor), 30, on_bilateralSigmaColorBar, &bilateralPair);
waitKey(0);
}
补充一个关于介绍滑动条的小程序:使用滑动条做调色板
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
using namespace cv;
//参数1:滑动条位置;参数2:传入的参数
void nothing(int pos, void *)
{
}
int main()
{
Mat img = Mat::zeros(300, 512, CV_8UC3);
namedWindow("image");
//createTrackbar函数:为窗口创建滑动条
createTrackbar("B", "image", 0, 255, nothing);
createTrackbar("G", "image", 0, 255, nothing);
createTrackbar("R", "image", 0, 255, nothing);
createTrackbar("0:off\n1:on", "image", 0, 1, nothing);
while (1)
{
imshow("image", img);
if (waitKey(1) == 27)
break;
//getTrackbarPos函数作用:获取滑动条的位置的值
int b = getTrackbarPos("B", "image");
int g = getTrackbarPos("G", "image");
int r = getTrackbarPos("R", "image");
int s = getTrackbarPos("0:off\n1:on", "image");
if (s == 0)
img = Scalar(0, 0, 0);
else
//opencv中颜色按BGR排列,numpy、matplotlib常用的第三方库RGB排列
img = Scalar(b, g, r); //修改Mat类对象颜色的方法
}
waitKey(0);
}