卷积与滤波概念
离散卷积
丢两个骰子,求点数加起来为 ttt 的概率是多少?
两个骰子加起来为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=13f(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)
进一步,两个骰子加起来为 ttt 的概率,就是二个骰子的概率密度函数的卷积:
(f∗g)(t)=∑i=1t−1f(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)=1NM∑i=0N−1∑j=0M−1f(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)
ggg 称为滤波器
图像滤波如何计算
- 滤波器 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)=1M∑i,jf(i,j)g(x,y) = \frac{1}{M}\sum_{i,j}f(i,j) g(x,y)=M1i,j∑f(i,j)
其中MMM表示像素值个数,滤波器采用4-领域时M=5M = 5M=5;滤波器采用8-领域时M=9M = 9M=9;如果不考虑中间像素M=8M = 8M=8。 - 加权平均滤波:在一个小区域内像素值加权平均
g(x,y)=∑i,jwijf(i,j)g(x,y) = \sum_{i,j}w_{ij}f(i,j) g(x,y)=i,j∑wijf(i,j)
其中wijw_{ij}wij表示所有像素值之和的倒数。 - 高斯滤波器:元素值分布根据位置呈现高斯函数的特点
116[121242121]\frac{1}{16} \left[ \begin{array}{ccc} 1 & 2 & 1\\\\ 2 & 4 & 2\\\\ 1 & 2 & 1\\\\ \end{array} \right] 161⎣⎢⎢⎢⎢⎢⎢⎡121242121⎦⎥⎥⎥⎥⎥⎥⎤ - 双边滤波器:元素值不仅与位置有关,还和源图像在该位置的像素值有关。其是空域核和值域核的叠加,通过双边滤波器,可以在使图像平滑的同时,比较好的保留边缘。
- 中值滤波:确定窗口及位置(含有奇数个像素),窗口内像素按灰度大小排序,取中间值代替原窗口中心像素值。对椒盐噪声有效。
总结:
- 平滑滤波包括平均滤波、高斯滤波、中值滤波等方法,其中高斯滤波最为常用。
- 平滑滤波具有去除噪声效果,不同滤波方法具有对不同噪声的适应性。
数学形态学滤波
数学形态学基本操作——膨胀和腐蚀
A⨁BA\bigoplus BA⨁B表示集合 AAA 用结构元素 BBB 膨胀(dilate),定义为:
A⨁B=∪(A)b(b∈B)A\bigoplus B = \cup(A)_b (b\in B) A⨁B=∪(A)b(b∈B)
表示,将 AAA 按照 BBB 中的所有元素进行平移,将所有平移的结果取并集。
A⨁BA\bigoplus BA⨁B表示集合 AAA 用结构元素 BBB 腐蚀(erode),定义为:
A⨀B=∩(A)−b(b∈B)A\bigodot B = \cap(A)_{-b} (b\in B) A⨀B=∩(A)−b(b∈B)
表示,将 AAA 按照 BBB 中的所有元素进行反方向平移,将所有平移的结果取并集。
直观表现是,腐蚀使黑色区域变大了,膨胀使白色区域变大了。
图像形态学操作——开闭运算
- 膨胀和腐蚀并不互为逆运算,二者级联使用可生成新的形态学操作
- 开运算:先腐蚀后膨胀(岛屿分开了):
(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 npdef 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 + noiseif 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 outfilename = '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);//构造高斯随机变量Xz = 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默认设置为0imshow(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的barImgPair* pImgPair = (ImgPair*)userdata;BilateralParam *param = (BilateralParam *)(pImgPair->Param);bilateralFilter(*(pImgPair->src), *(pImgPair->dst), ksize / 2 * 2 + 1, param->sigmaColor, param->sigmaSpace);//灰度距离核和空间距离核的sigmaparam->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; //初始化的kerbelSizeImgPair 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);
}