实现常见空域滤波
- 主要是实现常见的空域滤波,这篇属于造轮子的实验我就简单的贴下代码。
1、原图填充
使用模板进行空域滤波需要对原图进行填充,这样才会让处理后的图片的大小和原图一样大。
首先要获取需要填充的大小,Length 是具体的一个方向比如横向,step是步长,空域滤波的时候默认长度为1,kernelSize是使用模板的长度或宽度。
int getBordValue(int Length, int step, int kernelSize)
{
return (Length * (step - 1) - step + kernelSize) / 2;
}
进行填充
void autoCopyMakeBorder(Mat& images, int borderType, int step, int kernelSize)
{
int row = images.rows;
int col = images.cols;
int hightValue = getBordValue(row, step, kernelSize);
int widthValue = getBordValue(col, step, kernelSize);
copyMakeBorder(images, images, hightValue, hightValue, widthValue, widthValue, borderType);
}
填充的方式有很多种,比如常见的常量填充,101方式填充。
2、定义filter函数
这个功能类似cv库中的filter2D()函数,典型的造轮子,这里传入的模板是一个二维数组,这里我本打算用一个Mat对象来存储模板信息,但是在使用的时候有点问题,我就换成了动态的二维数组。对灰度和彩色只需要单独处理下就好了,就加一个判断语句就行。
void ifilter(Mat& src, Mat& dst, int kernelSize,double** templateMatrix)
{
assert(src.channels() || src.channels() == 3);
dst = src.clone();
//这里默认处理的N*N的filter, 如果长宽不一致,单独获取长宽
//填充边缘大小
int row = src.rows;
int col = src.cols;
int rowPadding = getBordValue(row, 1, kernelSize);
int colPadding = getBordValue(col, 1, kernelSize);
//这里不要直接填充src,因为这里是引用,可能后面的实验会重复填充
Mat t_src;
copyMakeBorder(src, t_src, rowPadding, rowPadding, colPadding, colPadding, BORDER_DEFAULT);
int channels = src.channels();
int cols = t_src.cols - rowPadding;
int rows = t_src.rows - colPadding;
for (int i = rowPadding; i < rows; i++) {
for (int j = colPadding; j < cols; j++) {
double sum[3] = { 0 };
for (int k = -rowPadding; k <= rowPadding; k++) {
for (int m = -colPadding; m <= colPadding; m++) {
if (channels == 1) {
sum[0] += templateMatrix[k+ rowPadding][m+colPadding] * t_src.at<uchar>(i + k, j + m);
}
else if (channels == 3) {
Vec3b rgb = t_src.at<Vec3b>(i + k, j + m);
auto tmp = templateMatrix[k + rowPadding][m + colPadding];
sum[0] += tmp * rgb[0];
sum[1] += tmp * rgb[1];
sum[2] += tmp * rgb[2];
}
}
}
//限定像素值在0-255之间
for (int i = 0; i < channels; i++) {
if (sum[i] < 0)
sum[i] = 0;
else if (sum[i] > 255)
sum[i] = 255;
}
//
if (channels == 1) {
dst.at<uchar>(i-rowPadding, j-colPadding) = static_cast<uchar>(sum[0]);
}
else if (channels == 3) {
Vec3b rgb;
rgb[0] = static_cast<uchar>(sum[0]);
rgb[1] = static_cast<uchar>(sum[1]);
rgb[2] = static_cast<uchar>(sum[2]);
dst.at<Vec3b>(i - rowPadding, j - colPadding) = rgb;
}
}
}
}
3、均值模糊
均值模板,就是模板内每个值得占比都一样,即是将模板覆盖范围内的像素累加起来,再除以模板内的像素的个数。比如3*3的模板:,用于图像的模糊和去噪。
void iMeanBlur(Mat& src, Mat& dst, int kernelSize)
{
//调用库实现的方法
/*
Mat mKernel = Mat::ones(kernelSize, kernelSize, CV_32F) / (float)(kernelSize * kernelSize);
//调用库函数实现
filter2D(src, dst, CV_32F, mKernel, Point(-1, -1));
convertScaleAbs(dst, dst);
imshow("dst", dst);
*/
//先根据kernelSize 创建均值模糊的模板
double** templateMatrix = new double* [kernelSize];
for (int i = 0; i < kernelSize; i++) {
templateMatrix[i] = new double[kernelSize];
}
int tmp = kernelSize * kernelSize;
int origin = kernelSize / 2;
for (int i = 0; i < kernelSize; i++) {
for (int j = 0; j < kernelSize; j++) {
templateMatrix[i][j] = 1.0 / tmp;
}
}
ifilter(src, dst, kernelSize, templateMatrix);
//删除模板
for (int i = 0; i < kernelSize; i++) {
delete[] templateMatrix[i];
}
delete[] templateMatrix;
}
实验效果
4、高斯模糊
主要是生成高斯模板,高斯函数,模板的中心权重最大,整个模板系数之和为1。
void iGaussianBlur(Mat& src, Mat& dst, int kernelSize, double sigma)
{
//根据高斯模糊的kernelSize创建高斯模糊的模板
double PI = acos(-1);
//动态申请二维数组空间
double** kernel = new double* [kernelSize];
for (int i = 0; i < kernelSize; i++) {
kernel[i] = new double[kernelSize];
}
int origin =kernelSize / 2; // 以模板的中心为原点
double x2, y2;
double sum = 0;
for (int i = 0; i < kernelSize; i++)
{
x2 = pow(i - origin, 2);
for (int j = 0; j < kernelSize; j++)
{
y2 = pow(double(j - origin), 2);
double g = exp(-(x2 + y2) / (2 * sigma * sigma));
sum += g;
kernel[i][j] = g;
}
}
//归一化
double k = 1 / sum;
for (int i = 0; i < kernelSize; i++) {
for (int j = 0; j < kernelSize; j++) {
kernel[i][j] *= k;
}
}
ifilter(src, dst, kernelSize, kernel);
//删除动态分配的二维数组
for (int i = 0; i < kernelSize; i++)
{
delete[] kernel[i];
}
delete[] kernel;
}
实验效果
5、拉普拉斯算子
拉普拉斯算子属于二阶锐化图像的算子,二阶微分对细节有较强的响应,对于梯度会产生双响应,而一阶微分对有较强的响应。常使用的拉普拉斯算子有以下几个、、、。拉普拉斯算子多用细节提取,当然提取细节也可以用于细节增强,比如常见的非锐化掩蔽(USM)。如果使用这种方式做增强的时候,可以在拉普拉斯算子上做一个小小的改动,比如让这个算子变成,就可以实现拉普拉斯算子增强图片细节。
常用的拉普拉斯算子有多个,这里我选择的是。
void iLaplacian(Mat& src, Mat& dst) {
//拉普拉斯常用的有几个模板,我只选择了其中的一个
vector<double> list = { -1,-1,-1,-1,8,-1,-1,-1,-1 };
double** templateMatrix = new double* [3];
for (int i = 0; i < 3; i++) {
templateMatrix[i] = new double[3];
}
int k = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
templateMatrix[i][j] = list[k++];
}
}
ifilter(src, dst, 3, templateMatrix);
for (int i = 0; i < 3; i++)
{
delete[] templateMatrix[i];
}
delete[] templateMatrix;
}
实验效果
Robert算子
Robert算子属于一阶微分,主要用于提取图片中的梯度,所以常见的Robert算子有两个,一个用于提取x方向,一个用于提取y方向。然后将两个方向合并起来。比如,,向量(1)。因为梯度向量的分量是微分,所以它们是线性算子,但是向量的幅度不是线性算子,求幅度的时候是做*方和*方根操作。虽然偏微分不是旋转不变的,但是梯度向量的幅度是旋转不变。所以常用绝对值来*似*方和*方根操作,常用以下公式(2)代替*方和*方根。该表达式保留了灰度的相对变化,但是失去了各向同性的特性。像拉普拉斯的情况那种,离散梯度的各项同性仅在有限旋转增量的情况下才被保留,它依赖所用的*似微分的滤波器模板。结果表明,用*似梯度常用的模板在90°的倍数是各向同性的,所以使用(1)和(2)对结果处理并没有影响。
Robert 算子有多个,这里我选择了其中的一对,。之前已经自己实现过滤波函数,代表我已经会实现滤波方法了,所以我尝试了下使用filter2D()这个函数,这和前面自己实现的没啥不同。只是我用Mat对象,代替的二维数组模板的生成。也已经说明了为什么可以用*似绝对值代替*方和和*方根。而且*似绝对值之和,运算更快。
void iRobert(Mat& src, Mat& dst_x, Mat& dst_y,Mat& dst)
{
Mat Robert_x = (Mat_<int>(2, 2) << 1, 0, 0, -1);
Mat Robert_y = (Mat_<int>(2, 2) << 0, 1, -1, 0);
filter2D(src, dst_x, CV_32F, Robert_x, Point(-1, -1), 0, BORDER_DEFAULT);
convertScaleAbs(dst_x, dst_x);
filter2D(src, dst_y, CV_32F, Robert_y, Point(-1, -1), 0, BORDER_DEFAULT);
convertScaleAbs(dst_y, dst_y);
add(dst_x, dst_y, dst);
convertScaleAbs(dst, dst);
}
实验效果
6、Sobel算子
Sobel算子和Robert算子原理类似,都是一阶微分,常用于梯度提取和边缘增强,这里我选择这一对
void iSobel(Mat& src, Mat& dst_x, Mat& dst_y, Mat& dst)
{
Mat Sobel_x = (Mat_<int>(3, 3) << -1, 0,1,-2, 0,2, -1,0,1);
Mat Sobel_y = (Mat_<int>(3, 3) << -1,-2,-1,0,0,0,1,2,1);
filter2D(src, dst_x, CV_32F, Sobel_x, Point(-1, -1), 0, BORDER_DEFAULT);
convertScaleAbs(dst_x, dst_x);
filter2D(src, dst_y, CV_32F, Sobel_y, Point(-1, -1), 0, BORDER_DEFAULT);
convertScaleAbs(dst_y, dst_y);
add(dst_x, dst_y, dst);
convertScaleAbs(dst, dst);
}
实验效果
实验有待改进地方
1、在定义模板的时候可以考虑使用Mat对象来替代动态二维数组
2、在实现ifilter()这函数,使用到四个for循环可以,将里面两层单独写成一个函数
3、在规范到0-255 可以使用convertScaleAbs()这个函数
4、在处理比较大的图片和使用比较大的均值模板时候,运算是比较慢的,可以采用小的模板或小的图片代替。比如用33 代替 55 ,7*7模板。
完整代码地址
https://github.com/cyssmile/openCV_learning_notes/tree/master/opencv_test/exercise_7