图像的滤波分为频率滤波和空间滤波,顾名思义频率滤波是在频率上进行相应的操作,而空间滤波是在空域上对像素的邻域进行一系列的操作以达到相应的效果的方法。空滤滤波在图像边界上回存在邻域像素不足的情况,而本文的解决方案是用零来填充,当然你也可以用其它合适的方法来解决这一问题。
算术均值滤波算是一种比较简单的滤波,它的做法是对图像像素点的邻域进行进行求和取均值。这种做法可以滤除图像细小细节,包含噪声(算是图像的细节部分),根据滤波模板的大小可决定滤波细节的大小,但同时会使图像的的边缘模糊。此滤波方法可用于高斯噪声的滤波,因为高斯噪声的均值是零(补充:高斯滤波对高斯噪声处理的效果更好, 原因:增大了中心像素的权重系数,计算结果更接近原图像像素值,保持图像不会过于模糊)。对于一些特征提取操作,使用模糊图像效果更好,因为细节部分更少,大部分融入了背景,使得网络更加容易训练。
#include
#include
#include
#include
#include
using namespace std;
using namespace cv;
void test1();
Mat arithmeticMeanFilter(Mat &src, int size);
void test2();
cv::Mat addSaltNoise(const cv::Mat &src, int n, int op);
Mat middleFilter(Mat &src, int size);
double findNumber(double* num, int start, int end, int n);
void test3();
Mat laplasFilter(Mat &src, bool standard, bool sharpen);
double standardImageSingleArray(Mat &src, double* num, int rank);
uchar pixesDeal(double n);
Mat standardImageSingle(Mat &src, int rank);
Mat addImage(Mat &src1, Mat &src2, double rate1, double rate2, double zero, bool standard);
void test4();
void test5();
Mat sobelFilter(Mat &src);
int main()
{
test1();
test2();
test3();
test4();
test5();
return 0;
}
// 测试一
void test1()
{
// 获取彩色图像
Mat src = imread("D:/ali.png", 1);
Mat dst = arithmeticMeanFilter(src, 5);
// 显示
imshow("原图", src);
imshow("算术均值滤波", dst);
waitKey(0);
destroyAllWindows();
}
// 算术均值滤波
Mat arithmeticMeanFilter(Mat &src, int size)
{
if (src.channels() == 3)
{
vector<Mat> bgr;
// 通道分离
split(src, bgr);
// 通道遍历
for (int i = 0; i < 3; i++)
// 算术均值滤波
bgr[i] = arithmeticMeanFilter(bgr[i], size);
// 通道合并
Mat dst;
merge(bgr, dst);
return dst;
}
// 深拷贝
Mat dst = src.clone();
double sum;
// 遍历像素
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
// 初始化
sum = 0;
// 遍历模板
for (int n = -size/2; n <= size/2; n++)
for (int m = -size/2; m <= size/2; m++)
{
// 边缘补零
if (i + n < 0 || j + m < 0 || i + n >= src.rows || j + m >= src.cols)
sum += 0;
else
sum += dst.at<uchar>(i + n, j + m);
}
dst.at<uchar>(i, j) = saturate_cast<int>(sum / (size*size));
}
return dst;
}
从图中可明显感受到图像的边缘变得模糊,对于高斯噪声的去噪效果后续文章会有介绍。
中值滤波是根据像素点邻域的大小,取其均值代替中心点的像素值,是一种非线性滤波。中值滤波的设计是出于对椒盐噪声的处理。用均值滤波效果不太好,因为噪声的均值不等于零。而中值滤波可有效滤除图像上的极值点,对椒盐噪声的滤波效果极好。
// 测试二
void test2()
{
// 获取彩色图像
Mat src = imread("D:/ali.png", 1);
// 添加椒盐噪声
Mat noise = addSaltNoise(src, 10000, 0);
// 中值滤波
Mat dst = middleFilter(noise, 3);
// 显示
imshow("原图", src);
imshow("椒盐噪声图", noise);
imshow("中值滤波", dst);
waitKey(0);
destroyAllWindows();
}
// 添加椒盐噪声op=0, 添加盐粒噪声op=1, 添加胡椒噪声op=2
cv::Mat addSaltNoise(const cv::Mat &src, int n, int op)
{
cv::Mat dst = src.clone();
if (op == 0 || op == 1)
{
for (int k = 0; k < n; k++)
{
int i = rand() % dst.rows;
int j = rand() % dst.cols;
// 通道判定
if (dst.channels() == 1)
{
dst.at<uchar>(i, j) = 255; // 添加盐噪声
}
else
{
dst.at<cv::Vec3b>(i, j)[0] = 255;
dst.at<cv::Vec3b>(i, j)[1] = 255;
dst.at<cv::Vec3b>(i, j)[2] = 255;
}
}
}
if (op == 0 || op == 2)
{
for (int k = 0; k < n; k++)
{
int i = rand() % dst.rows;
int j = rand() % dst.cols;
// 通道判定
if (dst.channels() == 1)
{
dst.at<uchar>(i, j) = 0; // 添加椒噪声
}
else
{
dst.at<cv::Vec3b>(i, j)[0] = 0;
dst.at<cv::Vec3b>(i, j)[1] = 0;
dst.at<cv::Vec3b>(i, j)[2] = 0;
}
}
}
return dst;
}
// 中值滤波
Mat middleFilter(Mat &src, int size)
{
if (src.channels() == 3)
{
vector<Mat> bgr;
// 通道分离
split(src, bgr);
// 通道遍历
for (int i = 0; i < 3; i++)
// 算术均值滤波
bgr[i] = middleFilter(bgr[i], size);
// 通道合并
Mat dst;
merge(bgr, dst);
return dst;
}
// 深拷贝
Mat dst = src.clone();
double *num = new double[size*size], middle;
int p;
// 遍历像素
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
// 初始化
p = 0;
// 遍历模板
for (int n = -size / 2; n <= size / 2; n++)
for (int m = -size / 2; m <= size / 2; m++)
{
// 边缘补零
if (i + n < 0 || j + m < 0 || i + n >= src.rows || j + m >= src.cols)
num[p]= 0, p++;
else
num[p] = dst.at<uchar>(i + n, j + m), p++;
}
middle = findNumber(num, 0, p, p / 2);
dst.at<uchar>(i, j) = saturate_cast<int>(middle);
}
return dst;
}
// 寻找序列值
double findNumber(double* num, int start, int end, int n)
{
int p = start;
double stand = num[start];
for (int i = start + 1; i < end; i++)
{
if (num[i] < stand)
{
num[p] = num[i];
p++;
num[i] = num[p];
}
}
if (p == n)
return stand;
else if (p > n)
return findNumber(num, start, p, n);
else
return findNumber(num, p + 1, end, n);
}
图像添加椒盐噪声相当于随机在图像的一些像素点上用0或255代替原像素值,使用中值可将极值点滤除,使用领域的像素值来代替中心像素点,但这样会使图像产生一定的模糊(不是极值点的像素点也被领域的像素值代替),后续文章会介绍优化的中值滤波算法。
拉普拉斯滤波是一种二阶锐化滤波器,二阶导数只在图像的边缘存在值,对于图像像素值平滑或者均匀变化的部分值接近于零。它在增强细节部分是最好的,但同时它也会增强噪声部分。本实验使用的拉普拉斯的模板为:
// 测试三
void test3()
{
// 获取彩色图像
Mat src = imread("D:/ali.png", 1);
// 拉普拉斯滤波获得锐化模板
Mat model1 = laplasFilter(src, false, false);
// 模板标定
Mat model2 = laplasFilter(src, true, false);
// 图像锐化,原图像加锐化模板
Mat dst1 = laplasFilter(src, false, true);
Mat dst2 = laplasFilter(src, true, true);
// 显示
imshow("原图", src);
imshow("拉普拉斯滤波——未标定", model1);
imshow("拉普拉斯滤波——标定", model2);
imshow("锐化图像——未标定", dst1);
imshow("锐化图像——标定", dst2);
waitKey(0);
destroyAllWindows();
}
// 拉普拉斯滤波
Mat laplasFilter(Mat &src, bool standard = false, bool sharpen = false)
{
if (src.channels() == 3)
{
vector<Mat> bgr;
// 通道分离
split(src, bgr);
// 通道遍历
for (int i = 0; i < 3; i++)
// 算术均值滤波
bgr[i] = laplasFilter(bgr[i], standard, sharpen);
// 通道合并
Mat dst;
merge(bgr, dst);
return dst;
}
// 深拷贝
Mat dst = src.clone();
double model[9] = {-1, -1, -1, -1, 8, -1, -1, -1, -1}, sum, zero = 0;
double* num = new double[src.rows*src.cols];
int p;
// 遍历像素
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
// 初始化
p = 0, sum = 0;
// 遍历模板
for (int n = -1; n <= 1; n++)
for (int m = -1; m <= 1; m++)
{
// 边缘补零
if (i + n < 0 || j + m < 0 || i + n >= src.rows || j + m >= src.cols)
sum += 0, p++;
else
sum += src.at<uchar>(i + n, j + m) * model[p], p++;
}
if (standard)
num[i*src.cols + j] = sum;
else
dst.at<uchar>(i, j) = pixesDeal(sum);
}
if (standard)
{
zero = standardImageSingleArray(dst, num, 255);
if (sharpen)
dst = addImage(src, dst, 1.0, 1.0, zero, false);
}
else
{
if (sharpen)
dst = addImage(src, dst, 1.0, 1.0, 0.0, false);
}
delete []num;
return dst;
}
// 单通道图像标定——数组存储
double standardImageSingleArray(Mat &src, double* num, int rank = 255)
{
// 查找数组内的最大值与最小值
double min = 1000000000, max = -1000000000;
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
if (num[i*src.cols + j] < min)
min = num[i*src.cols + j];
if (num[i*src.cols + j] > max)
max = num[i*src.cols + j];
// cout << num[i*src.cols + j] <
}
// cout << min << endl << max << endl;
// 对图像像素值进行标定
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
src.at<uchar>(i, j) = static_cast<uchar>(rank * ((num[i*src.cols + j] - min) / (max - min)));
}
return (rank * ((0 - min) / (max - min)));
}
// 像素判定
uchar pixesDeal(double n)
{
if (n < 0)
return 0;
else if (n > 255)
return 255;
else
return static_cast<uchar>(n);
}
// 图像标定
Mat standardImageSingle(Mat &src, int rank = 255)
{
if (src.channels() == 3)
{
vector<Mat> bgr;
// 通道分离
split(src, bgr);
// 通道遍历
for (int i = 0; i < 3; i++)
// 算术均值滤波
bgr[i] = standardImageSingle(bgr[i], rank);
// 通道合并
Mat dst;
merge(bgr, dst);
return dst;
}
Mat dst = src.clone();
// 查找数组内的最大值与最小值
int min = 255, max = 0;
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
if (dst.at<uchar>(i, j) < min)
min = dst.at<uchar>(i, j);
if (dst.at<uchar>(i, j) > max)
max = dst.at<uchar>(i, j);
// cout << num[i*src.rows + j] <
}
// 对图像像素值进行标定
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
dst.at<uchar>(i, j) = saturate_cast<int>(rank * ((dst.at<uchar>(i, j) - min) * 1.0 / (max - min + 1)));
}
return dst;
}
// 图像叠加(同尺寸)
Mat addImage(Mat &src1, Mat &src2, double rate1, double rate2, double zero = 0.0, bool standard = false)
{
if (src1.channels() == 3)
{
vector<Mat> bgr1, bgr2;
// 通道分离
split(src1, bgr1);
split(src2, bgr2);
// 通道遍历
for (int i = 0; i < 3; i++)
// 算术均值滤波
bgr1[i] = addImage(bgr1[i], bgr2[i], rate1, rate2, standard);
// 通道合并
Mat dst;
merge(bgr1, dst);
return dst;
}
// 深拷贝
Mat dst = src1.clone();
double* num = new double[src1.rows*src1.cols];
// 遍历像素
for (int i = 0; i < src1.rows; i++)
for (int j = 0; j < src1.cols; j++)
{
if (standard)
num[i*src1.cols + j] = rate1 * src1.at<uchar>(i, j) + rate2 * src2.at<uchar>(i, j);
else
dst.at<uchar>(i, j) = pixesDeal(rate1*src1.at<uchar>(i, j) + rate2*src2.at<uchar>(i, j) - zero);
}
if (standard)
standardImageSingleArray(dst, num, 255);
return dst;
}
经拉普拉斯滤波后可以获取图像的边缘信息,与原图像相加可加强细节部分。需要对滤波后的图像进行标定,标定的原因应该是想保留二阶微分产生的分开的像素宽的双边缘(有正有负),而不标定直接相加会将负边缘的信息消除。
原图减去一个非锐化(平滑)的图像,获得的差值称作钝化(非锐化)模板,这与二阶微分滤波后的图像十分相似,原图加上一定比例的钝化模板就可以得到锐化图像了,这个比例值等于1就是非锐化遮掩,而大于1就是高提升滤波。
// 测试四
void test4()
{
// 获取彩色图像
Mat src = imread("D:/ali.png", 1);
// 模糊处理
Mat loom = arithmeticMeanFilter(src, 3);
// 非锐化掩蔽 k = 1
Mat dst1 = addImage(src, loom, 1.0 + 1.0*1, -1.0*1);
// 高提升滤波 k = 1.5
Mat dst2 = addImage(src, loom, 1 + 1.5*1, -1.5*1);
// 显示
imshow("原图", src);
imshow("模糊图像", loom);
imshow("非锐化掩蔽", dst1);
imshow("高提升滤波", dst2);
waitKey(0);
destroyAllWindows();
}
经对比与二阶微分锐化效果十分相似,但需注意k值的大小不宜过大,过大的话,图像像素值会存在负值,在图像边缘会出现暗色晕轮。该方法常用于印刷和出版业的图像锐化。
一阶微分是用梯度来实现的,梯度值高的地方一般在图像的边缘部分,同二阶微分差不多,但图像在灰度缓慢变化的部分也会存在梯度值,细节增强的能力不如二阶微分,因此噪声部分比二阶增强的要小一些。对于有噪声的图像一般用平滑滤波对噪声进行平滑,在使用一阶微分进行锐化,这是一种经典的用法。sobel滤波模板存在 x x x方向和 y y y方向的模板,中心点之所以为2,原因是通过突出中心点的作用而达到平滑的目的:
// 测试五
void test5()
{
// 获取彩色图像
Mat src = imread("D:/ali.png", 1);
// socel梯度处理
Mat model = sobelFilter(src);
// 锐化图像
Mat dst = addImage(src, model, 1.0, 0.2);
// 显示
imshow("原图", src);
imshow("sobel锐化模板", model);
imshow("sobel锐化", dst);
waitKey(0);
destroyAllWindows();
}
Mat sobelFilter(Mat &src)
{
if (src.channels() == 3)
{
vector<Mat> bgr;
// 通道分离
split(src, bgr);
// 通道遍历
for (int i = 0; i < 3; i++)
// 算术均值滤波
bgr[i] = sobelFilter(bgr[i]);
// 通道合并
Mat dst;
merge(bgr, dst);
return dst;
}
// 深拷贝
Mat dst = src.clone();
double sum1, sum2;
double model1[9] = { -1, -2, -1, 0, 0, 0, 1, 2, 1 };
double model2[9] = { -1, 0, 1, -2, 0, 2, -1, 0, 1 };
int p;
// 遍历像素
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
// 初始化
p = 0, sum1 = 0, sum2 = 0;
// 遍历模板
for (int n = -1; n <= 1; n++)
for (int m = -1; m <= 1; m++)
{
// 边缘补零
if (i + n < 0 || j + m < 0 || i + n >= src.rows || j + m >= src.cols)
p++;
else
{
sum1 += src.at<uchar>(i + n, j + m) * model1[p];
sum2 += src.at<uchar>(i + n, j + m) * model2[p];
p++;
}
}
dst.at<uchar>(i, j) = pixesDeal(abs(sum1) + abs(sum2));
}
return dst;
}
一阶微分锐化和二阶微分锐化各有优点,一阶微分增强细节的能力不如二阶,但二阶同时会增强噪声部分,需要具体情况具体分析,有时会使用二者同时对图像进行增强。一阶微分锐化不会存在负边缘,因此不需要对图像进行标定,同时这也是对细节增强不足的原因所在。