本編博客會談談圖像濾波,會分別寫出濾波過程和原理;
主要可分為以下兩大類:
一、線性濾波
1、均值濾波blur()
2、方框濾波 boxFilter()
3、高斯濾波GaussianBlur()
二、非線性濾波
1、中值濾波medianBlur()--當濾波核尺寸大於5時,輸入圖像只能是CV_8U格式的。
2、雙邊濾波bilateralFilter()
在前面學習邊沿檢測時,當時就遇到過濾波的問題,現在開始學習一下主要的原理
圖像的空域線性濾波和非線性濾波在空域對圖像進行濾波處理無非兩種情況,線性濾波和非線性濾波。濾波的意思就是對原圖像的每個像素周圍一定范圍內的像素進行運算,運算的范圍就稱為掩膜或領域。而運算就分兩種了,如果運算只是對各像素灰度值進行簡單處理(如乘一個權值)最后求和,就稱為線性濾波;而如果對像素灰度值的運算比較復雜,而不是最后求和的簡單運算,則是非線性濾波;如求一個像素周圍3x3范圍內最大值、最小值、中值、均值等操作都不是簡單的加權,都屬於非線性濾波。
圖像濾波既可以在實域進行,也可以在頻域進行。圖像濾波可以更改或者增強圖像。通過濾波,可以強調一些特征或者去除圖像中一些不需要的部分。濾波是一個鄰域操作算子,利用給定像素周圍的像素的值決定此像素的最終的輸出值。
(i,j)是像素在圖片中的位置;
(m,n)是卷積核中的位置/坐標,其中心點的坐標是(0,0) ;
k(m,n)是卷積核中在(m, n)上的權重參數
I[i+m, j+n]是與k(m,n)相對應的圖片像素值
O(i,j)是圖片中(i,j)像素的濾波/卷積結果
如下圖,當i,j=0時,也就是中心位置是(0,0)。此位置的鄰域就是加減m和n, 當m,n=1時(0,0)的鄰域就是下面的9個位置。
卷積的操作如下圖所示,圖片上圈的紅框就是卷積核要操作的鄰域。例子中卷積核每個位置上的值都為1,因此進行卷積就是把圖片上的9個值分別乘上對應卷積核上的值1再求和,4*1+1*1+6*1+7*1+2*1+3*1+9*1+5*1+8*1=45。 其中,45對應卷積核中間1對應的位置;
上面是卷積核移動得到對應的2x2的矩陣結果。
也可以看看這個圖理解一下卷積核效果:
邊界補充
由上面介紹的卷積操作可以看出,結果比原來圖像小了,那么如果要獲得同尺寸輸出,則要進行邊界補充。 因為卷積核越大,得到的結果就越少,所以要補充的就越多。
那進行補充的值為多少呢,下面將介紹4中補充的類型。
我們以7x7卷積:3x3補充成9x9為例。
補零(zero-padding)
補零很簡單,就是把缺失的地方都補成0 。
關於卷積核的相關知識可以參考博文:https://blog.csdn.net/zouxy09/article/details/49080029和https://www.cnblogs.com/zgy-personal/p/7227872.html
關於邊界補充的相關知識可以參考博文:https://blog.csdn.net/zxfhahaha/article/details/80139430
濾波函數簡介
均值濾波:
均值濾波方法是,對待處理的當前像素,選擇一個模板,該模板為其鄰近的若干個像素組成,用模板的均值來替代原像素的值的方法。
方法優缺點
優點:算法簡單,計算速度快;
缺點:降低噪聲的同時使圖像產生模糊,特別是景物的邊緣和細節部分。
下面為實現均值濾波的代碼和結果
#include "opencv2/imgproc.hpp" #include "opencv2/highgui.hpp" #include<ctime> using namespace cv; using namespace std; //均值濾波 void AverFiltering(const Mat &src, Mat &dst) { if (!src.data) return; //at訪問像素點 for (int i = 1; i<src.rows; ++i) for (int j = 1; j < src.cols; ++j) { if ((i - 1 >= 0) && (j - 1) >= 0 && (i + 1)<src.rows && (j + 1)<src.cols) {//邊緣不進行處理 dst.at<Vec3b>(i, j)[0] = (src.at<Vec3b>(i, j)[0] + src.at<Vec3b>(i - 1, j - 1)[0] + src.at<Vec3b>(i - 1, j)[0] + src.at<Vec3b>(i, j - 1)[0] + src.at<Vec3b>(i - 1, j + 1)[0] + src.at<Vec3b>(i + 1, j - 1)[0] + src.at<Vec3b>(i + 1, j + 1)[0] + src.at<Vec3b>(i, j + 1)[0] + src.at<Vec3b>(i + 1, j)[0]) / 9; dst.at<Vec3b>(i, j)[1] = (src.at<Vec3b>(i, j)[1] + src.at<Vec3b>(i - 1, j - 1)[1] + src.at<Vec3b>(i - 1, j)[1] + src.at<Vec3b>(i, j - 1)[1] + src.at<Vec3b>(i - 1, j + 1)[1] + src.at<Vec3b>(i + 1, j - 1)[1] + src.at<Vec3b>(i + 1, j + 1)[1] + src.at<Vec3b>(i, j + 1)[1] + src.at<Vec3b>(i + 1, j)[1]) / 9; dst.at<Vec3b>(i, j)[2] = (src.at<Vec3b>(i, j)[2] + src.at<Vec3b>(i - 1, j - 1)[2] + src.at<Vec3b>(i - 1, j)[2] + src.at<Vec3b>(i, j - 1)[2] + src.at<Vec3b>(i - 1, j + 1)[2] + src.at<Vec3b>(i + 1, j - 1)[2] + src.at<Vec3b>(i + 1, j + 1)[2] + src.at<Vec3b>(i, j + 1)[2] + src.at<Vec3b>(i + 1, j)[2]) / 9; } else {//邊緣賦值 dst.at<Vec3b>(i, j)[0] = src.at<Vec3b>(i, j)[0]; dst.at<Vec3b>(i, j)[1] = src.at<Vec3b>(i, j)[1]; dst.at<Vec3b>(i, j)[2] = src.at<Vec3b>(i, j)[2]; } } } //圖像椒鹽化 void salt(Mat &image, int num) { if (!image.data) return;//防止傳入空圖 int i, j; srand(time(NULL)); for (int x = 0; x < num; ++x) { i = rand() % image.rows; j = rand() % image.cols; image.at<Vec3b>(i, j)[0] = 255; image.at<Vec3b>(i, j)[1] = 255; image.at<Vec3b>(i, j)[2] = 255; } } void main() { Mat image = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\01.jpg"); Mat Salt_Image; image.copyTo(Salt_Image); salt(Salt_Image, 3000); Mat image1(image.size(), image.type()); Mat image2; AverFiltering(Salt_Image, image1); blur(Salt_Image, image2, Size(3, 3));//openCV庫自帶的均值濾波函數 imshow("原圖", image); imshow("噪聲圖", Salt_Image); imshow("自定義均值濾波", image1); imshow("openCV自帶的均值濾波", image2); waitKey(); }
方框濾波:原理與均值濾波類似
opencv中boxFilter函數的作用是使用方框濾波(box filter)來模糊一張圖片。
方框濾波算法的原理很簡單,指定一個X*Y的矩陣大小,目標像素的周圍X*Y矩陣內的像素全部相加作為目標像素的值,就這么簡單.
Boxfilter的初始化過程如下:
1、給定一張圖像,寬高為(M,N),確定待求矩形模板的寬高(m,n),如圖紫色矩形。圖中每個黑色方塊代表一個像素,紅色方塊是假想像素。
2、開辟一段大小為M的數組,記為buff, 用來存儲計算過程的中間變量,用紅色方塊表示
3、將矩形模板(紫色)從左上角(0,0)開始,逐像素向右滑動,到達行末時,矩形移動到下一行的開頭(0,1),如此反復,每移動到一個新位置時,計算矩形內的像素和,保存在數組A中。以(0,0)位置為例進行說明:首先將綠色矩形內的每一列像素求和,結果放在buff內(紅色方塊),再對藍色矩形內的像素求和,結果即為紫色特征矩形內的像素和,把它存放到數組A中,如此便完成了第一次求和運算。
4、每次紫色矩形向右移動時,實際上就是求對應的藍色矩形的像素和,此時只要把上一次的求和結果減去藍色矩形內的第一個紅色塊,再加上它右面的一個紅色塊,就是當前位置的和了,用公式表示 sum[i] = sum[i-1] - buff[x-1] + buff[x+m-1]
5、當紫色矩形移動到行末時,需要對buff進行更新。因為整個綠色矩形下移了一個像素,所以對於每個buff[i], 需要加上一個新進來的像素,再減去一個出去的像素,然后便開始新的一行的計算了。
代碼如下:
#include <iostream> #include <opencv2/core.hpp> #include <opencv2/highgui.hpp> #include <opencv2/imgproc.hpp> ///////////////////////////////////////// //求積分圖-優化方法 //由上方negral(i-1,j)加上當前行的和即可 //對於W*H圖像:2*(W-1)*(H-1)次加減法 //比常規方法快1.5倍左右 ///////////////////////////////////////// void Fast_integral(cv::Mat& src, cv::Mat& dst) { int nr = src.rows; int nc = src.cols; int sum_r = 0; dst = cv::Mat::zeros(nr + 1, nc + 1, CV_64F); for (int i = 1; i < dst.rows; ++i) { for (int j = 1, sum_r = 0; j < dst.cols; ++j) { //行累加,因為積分圖相當於在原圖上方加一行,左邊加一列,所以積分圖的(1,1)對應原圖(0,0),(i,j)對應(i-1,j-1) sum_r = src.at<uchar>(i - 1, j - 1) + sum_r; //行累加 dst.at<double>(i, j) = dst.at<double>(i - 1, j) + sum_r; } } } ////////////////////////////////// //盒子濾波-均值濾波是其特殊情況 ///////////////////////////////// void BoxFilter(cv::Mat& src, cv::Mat& dst, cv::Size wsize, bool normalize) { //圖像邊界擴充 if (wsize.height % 2 == 0 || wsize.width % 2 == 0) { fprintf(stderr, "Please enter odd size!"); exit(-1); } int hh = (wsize.height - 1) / 2; int hw = (wsize.width - 1) / 2; cv::Mat Newsrc; cv::copyMakeBorder(src, Newsrc, hh, hh, hw, hw, cv::BORDER_REFLECT);//以邊緣為軸,對稱 src.copyTo(dst); //計算積分圖 cv::Mat inte; Fast_integral(Newsrc, inte); //BoxFilter double mean = 0; for (int i = hh + 1; i < src.rows + hh + 1; ++i) { //積分圖圖像比原圖(邊界擴充后的)多一行和一列 for (int j = hw + 1; j < src.cols + hw + 1; ++j) { double top_left = inte.at<double>(i - hh - 1, j - hw - 1); double top_right = inte.at<double>(i - hh - 1, j + hw); double buttom_left = inte.at<double>(i + hh, j - hw - 1); double buttom_right = inte.at<double>(i + hh, j + hw); if (normalize == true) mean = (buttom_right - top_right - buttom_left + top_left) / wsize.area(); else mean = buttom_right - top_right - buttom_left + top_left; //一定要進行判斷和數據類型轉換 if (mean < 0) mean = 0; else if (mean>255) mean = 255; dst.at<uchar>(i - hh - 1, j - hw - 1) = static_cast<uchar>(mean); } } } int main() { cv::Mat src = cv::imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\06.jpg"); if (src.empty()) { return -1; } //自編BoxFilter測試 cv::Mat dst1; double t2 = (double)cv::getTickCount(); //測時間 if (src.channels() > 1) { std::vector<cv::Mat> channel; cv::split(src, channel); BoxFilter(channel[0], channel[0], cv::Size(7, 7), true);//盒子濾波 BoxFilter(channel[1], channel[1], cv::Size(7, 7), true);//盒子濾波 BoxFilter(channel[2], channel[2], cv::Size(7, 7), true);//盒子濾波 cv::merge(channel, dst1); } else BoxFilter(src, dst1, cv::Size(7, 7), true);//盒子濾波 t2 = (double)cv::getTickCount() - t2; double time2 = (t2 *1000.) / ((double)cv::getTickFrequency()); std::cout << "FASTmy_process=" << time2 << " ms. " << std::endl << std::endl; //opencv自帶BoxFilter測試 cv::Mat dst2; double t1 = (double)cv::getTickCount(); //測時間 cv::boxFilter(src, dst2, -1, cv::Size(7, 7), cv::Point(-1, -1), true, cv::BORDER_CONSTANT);//盒子濾波 t1 = (double)cv::getTickCount() - t1; double time1 = (t1 *1000.) / ((double)cv::getTickFrequency()); std::cout << "Opencvbox_process=" << time1 << " ms. " << std::endl << std::endl; cv::namedWindow("src"); cv::imshow("src", src); cv::namedWindow("ourdst", CV_WINDOW_NORMAL); cv::imshow("ourdst", dst1); cv::namedWindow("opencvdst", CV_WINDOW_NORMAL); cv::imshow("opencvdst", dst2); cv::waitKey(0); }
高斯濾波:
關於高斯函數和濾波可以參考這邊博文:https://blog.csdn.net/jgj123321/article/details/94448463和https://www.cnblogs.com/qiqibaby/p/5289977.html
高斯濾波原理及離散化
現在主要講解一下原理:
要產生一個3×3的高斯濾波器模板,以模板的中心位置為坐標原點進行取樣。模板在各個位置的坐標,如下所示(x軸水平向右,y軸豎直向下)。
其中(x,y)為點坐標,在圖像處理中可認為是整數,是標准差。將各個位置的坐標帶入到高斯函數中,得到的值就是模板的系數,模板中各個元素值的計算公式如下:
注意:由於最后要進行歸一化處理,因此在計算模板中各個元素的值時,可以去掉高斯函數的系數;
通過上述的實現過程發現,生成高斯濾波器模板最重要的參數就是高斯分布的標准差,標准差代表着數據的離散程度。
越小,模板的中心系數越大,周圍的系數越小,這樣對圖像的平滑效果就不是很明顯;
越大,模板的各個系數相差就不是很大,比較類似均值模板,對圖像的平滑效果比較明顯。
根據上面原理我們可以得到高斯模板的代碼公式:
二維高斯模板的生成:
double **getTwoGuassionArray(int size, double sigma) { double sum = 0.0; int kerR = size / 2; double **arr = new double*[size]; for (int i = 0; i < size; i++) { arr[i] = new double[size]; } for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { arr[i][j] = exp(-((i-kerR)*(i-kerR) + (j-kerR)*(j-kerR)) / (2 * sigma*sigma)); sum += arr[i][j]; } } for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { arr[i][j] /= sum; cout << arr[i][j] << endl; } } return arr; }
一維高斯模板的生成:
double *getOneGuassionArray(int size, double sigma) { double sum = 0.0; int kerR = size / 2; double *arr = new double[size]; for (int i = 0; i < size; i++) { arr[i] = exp(-((i - kerR)*(i - kerR)) / (2 * sigma*sigma)); sum += arr[i]; } for (int i = 0; i < size; i++) { arr[i] /= sum; cout << arr[i] << endl; } return arr; }
具體實現代碼:
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> #include <cmath> using namespace cv; using namespace std; double **getTwoGuassionArray(int size, double sigma) { double sum = 0.0; int kerR = size / 2; double **arr = new double*[size]; for (int i = 0; i < size; i++) { arr[i] = new double[size]; } for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { arr[i][j] = exp(-((i - kerR)*(i - kerR) + (j - kerR)*(j - kerR)) / (2 * sigma*sigma)); sum += arr[i][j]; } } for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { arr[i][j] /= sum; cout << arr[i][j] << endl; } } return arr; } void MyGaussianBlur(Mat &srcImage, Mat &dst, int size) { CV_Assert(srcImage.channels() || srcImage.channels() == 3); int kerR = size / 2; dst = srcImage.clone(); int channels = dst.channels(); double** arr; arr = getTwoGuassionArray(size, 1); for (int i = kerR; i < dst.rows - kerR; i++) { for (int j = kerR; j < dst.cols - kerR; j++) { double GuassionSum[3] = { 0 }; for (int k = -kerR; k <= kerR; k++) { for (int n = -kerR; n <= kerR; n++) { if (channels == 1) { GuassionSum[0] += arr[kerR + k][kerR + n] * dst.at<uchar>(i + k, j + n); } else if (channels == 3) { Vec3b bgr = dst.at<Vec3b>(i + k, j + n); auto a = arr[kerR + k][kerR + n]; GuassionSum[0] += a*bgr[0]; GuassionSum[1] += a*bgr[1]; GuassionSum[2] += a*bgr[2]; } } } for (int k = 0; k < channels; k++) { if (GuassionSum[k] < 0) GuassionSum[k] = 0; else if (GuassionSum[k] > 255) GuassionSum[k] = 255; } if (channels == 1) dst.at<uchar>(i, j) = static_cast<uchar>(GuassionSum[0]); else if (channels == 3) { Vec3b bgr = { static_cast<uchar>(GuassionSum[0]), static_cast<uchar>(GuassionSum[1]), static_cast<uchar>(GuassionSum[2]) }; dst.at<Vec3b>(i, j) = bgr; } } } for (int i = 0; i < size; i++) delete[] arr[i]; delete[] arr; } int main() { Mat srcImage = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\06.jpg"); if (!srcImage.data) { printf("could not load image...\n"); return -1; } //getGuassionArray(3, 1); Mat resMat; MyGaussianBlur(srcImage, resMat, 3); imshow("原圖", srcImage); imshow("高斯濾波圖", resMat); waitKey(0); return 0; }
只處理單通道或者三通道圖像,模板生成后,其濾波(卷積過程)就比較簡單了。不過,這樣的高斯濾波過程,其循環運算次數為,其中m,n為圖像的尺寸;ksize為高斯濾波器的尺寸。這樣其時間復雜度為,隨濾波器的模板的尺寸呈平方增長,當高斯濾波器的尺寸較大時,其運算效率是極低的。為了,提高濾波的運算速度,可以將二維的高斯濾波過程分解開來。
分離實現高斯濾波
由於高斯函數的可分離性,尺寸較大的高斯濾波器可以分成兩步進行:首先將圖像在水平(豎直)方向與一維高斯函數進行卷積;然后將卷積后的結果在豎直(水平)方向使用相同的一維高斯函數得到的模板進行卷積運算。具體實現代碼如下:
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> #include <cmath> using namespace cv; using namespace std; double *getOneGuassionArray(int size, double sigma) { double sum = 0.0; int kerR = size / 2; double *arr = new double[size]; for (int i = 0; i < size; i++) { arr[i] = exp(-((i - kerR)*(i - kerR)) / (2 * sigma*sigma)); sum += arr[i]; } for (int i = 0; i < size; i++) { arr[i] /= sum; cout << arr[i] << endl; } return arr; } void MyGaussianBlur(Mat &srcImage, Mat &dst, int size) { CV_Assert(srcImage.channels() || srcImage.channels() == 3); int kerR = size / 2; dst = srcImage.clone(); int channels = dst.channels(); double* arr; arr = getOneGuassionArray(size, 1); for (int i = kerR; i < dst.rows - kerR; i++) { for (int j = kerR; j < dst.cols - kerR; j++) { double GuassionSum[3] = { 0 }; for (int k = -kerR; k <= kerR; k++) { if (channels == 1) { GuassionSum[0] += arr[kerR + k] * dst.at<uchar>(i, j + k); } else if (channels == 3) { Vec3b bgr = dst.at<Vec3b>(i, j + k); auto a = arr[kerR + k]; GuassionSum[0] += a*bgr[0]; GuassionSum[1] += a*bgr[1]; GuassionSum[2] += a*bgr[2]; } } for (int k = 0; k < channels; k++) { if (GuassionSum[k] < 0) GuassionSum[k] = 0; else if (GuassionSum[k] > 255) GuassionSum[k] = 255; } if (channels == 1) dst.at<uchar>(i, j) = static_cast<uchar>(GuassionSum[0]); else if (channels == 3) { Vec3b bgr = { static_cast<uchar>(GuassionSum[0]), static_cast<uchar>(GuassionSum[1]), static_cast<uchar>(GuassionSum[2]) }; dst.at<Vec3b>(i, j) = bgr; } } } for (int i = kerR; i < dst.rows - kerR; i++) { for (int j = kerR; j < dst.cols - kerR; j++) { double GuassionSum[3] = { 0 }; for (int k = -kerR; k <= kerR; k++) { if (channels == 1) { GuassionSum[0] += arr[kerR + k] * dst.at<uchar>(i + k, j); } else if (channels == 3) { Vec3b bgr = dst.at<Vec3b>(i + k, j); auto a = arr[kerR + k]; GuassionSum[0] += a*bgr[0]; GuassionSum[1] += a*bgr[1]; GuassionSum[2] += a*bgr[2]; } } for (int k = 0; k < channels; k++) { if (GuassionSum[k] < 0) GuassionSum[k] = 0; else if (GuassionSum[k] > 255) GuassionSum[k] = 255; } if (channels == 1) dst.at<uchar>(i, j) = static_cast<uchar>(GuassionSum[0]); else if (channels == 3) { Vec3b bgr = { static_cast<uchar>(GuassionSum[0]), static_cast<uchar>(GuassionSum[1]), static_cast<uchar>(GuassionSum[2]) }; dst.at<Vec3b>(i, j) = bgr; } } } delete[] arr; } int main() { Mat srcImage = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\06.jpg"); if (!srcImage.data) { printf("could not load image...\n"); return -1; } //getGuassionArray(3, 1); Mat resMat; MyGaussianBlur(srcImage, resMat, 3); imshow("原圖", srcImage); imshow("高斯濾波圖", resMat); waitKey(0); return 0; }
中值濾波
首先,我們復習中值。在一連串數字{1,4,6,8,9}中,數字6就是這串數字的中值。由此我們可以應用到圖像處理中。依然我們在圖像中去3*3的矩陣,里面有9個像素點,我們將9個像素進行排序,最后將這個矩陣的中心點賦值為這九個像素的中值。
所以說,中值濾波原理很簡單,就是排大小的問題
中值濾波法對消除椒鹽噪聲非常有效,在光學測量條紋圖象的相位分析處理方法中有特殊作用,但在條紋中心分析方法中作用不大.
中值濾波在圖像處理中,常用於保護邊緣信息,是經典的平滑噪聲的方法。
參考博文:https://www.cnblogs.com/qiqibaby/p/5281743.html
代碼實現如下
#include "opencv2/imgproc.hpp" #include "opencv2/highgui.hpp" #include<ctime> using namespace cv; using namespace std; //求九個數的中值 uchar Median(uchar n1, uchar n2, uchar n3, uchar n4, uchar n5, uchar n6, uchar n7, uchar n8, uchar n9) { uchar arr[9]; arr[0] = n1; arr[1] = n2; arr[2] = n3; arr[3] = n4; arr[4] = n5; arr[5] = n6; arr[6] = n7; arr[7] = n8; arr[8] = n9; for (int gap = 9 / 2; gap > 0; gap /= 2)//希爾排序 for (int i = gap; i < 9; ++i) for (int j = i - gap; j >= 0 && arr[j] > arr[j + gap]; j -= gap) swap(arr[j], arr[j + gap]); return arr[4];//返回中值 } //圖像椒鹽化 void salt(Mat &image, int num) { if (!image.data) return;//防止傳入空圖 int i, j; srand(time(NULL)); for (int x = 0; x < num; ++x) { i = rand() % image.rows; j = rand() % image.cols; image.at<Vec3b>(i, j)[0] = 255; image.at<Vec3b>(i, j)[1] = 255; image.at<Vec3b>(i, j)[2] = 255; } } //中值濾波函數 void MedianFlitering(const Mat &src, Mat &dst) { if (!src.data)return; Mat _dst(src.size(), src.type()); for (int i = 0; i<src.rows; ++i) for (int j = 0; j < src.cols; ++j) { if ((i - 1) > 0 && (i + 1) < src.rows && (j - 1) > 0 && (j + 1) < src.cols) { _dst.at<Vec3b>(i, j)[0] = Median(src.at<Vec3b>(i, j)[0], src.at<Vec3b>(i + 1, j + 1)[0], src.at<Vec3b>(i + 1, j)[0], src.at<Vec3b>(i, j + 1)[0], src.at<Vec3b>(i + 1, j - 1)[0], src.at<Vec3b>(i - 1, j + 1)[0], src.at<Vec3b>(i - 1, j)[0], src.at<Vec3b>(i, j - 1)[0], src.at<Vec3b>(i - 1, j - 1)[0]); _dst.at<Vec3b>(i, j)[1] = Median(src.at<Vec3b>(i, j)[1], src.at<Vec3b>(i + 1, j + 1)[1], src.at<Vec3b>(i + 1, j)[1], src.at<Vec3b>(i, j + 1)[1], src.at<Vec3b>(i + 1, j - 1)[1], src.at<Vec3b>(i - 1, j + 1)[1], src.at<Vec3b>(i - 1, j)[1], src.at<Vec3b>(i, j - 1)[1], src.at<Vec3b>(i - 1, j - 1)[1]); _dst.at<Vec3b>(i, j)[2] = Median(src.at<Vec3b>(i, j)[2], src.at<Vec3b>(i + 1, j + 1)[2], src.at<Vec3b>(i + 1, j)[2], src.at<Vec3b>(i, j + 1)[2], src.at<Vec3b>(i + 1, j - 1)[2], src.at<Vec3b>(i - 1, j + 1)[2], src.at<Vec3b>(i - 1, j)[2], src.at<Vec3b>(i, j - 1)[2], src.at<Vec3b>(i - 1, j - 1)[2]); } else _dst.at<Vec3b>(i, j) = src.at<Vec3b>(i, j); } _dst.copyTo(dst);//拷貝 } void main() { Mat image = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\06.jpg"); Mat Salt_Image; image.copyTo(Salt_Image); salt(Salt_Image, 3000); Mat image3, image4; MedianFlitering(Salt_Image, image3); medianBlur(Salt_Image, image4, 3); imshow("原圖", image); imshow("噪聲圖", Salt_Image); imshow("自定義中值濾波處理后", image3); imshow("openCV自帶的中值濾波", image4); waitKey(); }
也可參考這倆篇博文:https://blog.csdn.net/qq_36359022/article/details/80154900和https://blog.csdn.net/qq_36359022/article/details/80188873
雙邊濾波(Bilateral filter):
是一種非線性的濾波方法,是結合圖像的空間鄰近度和像素值相似度的一種折衷處理,同時考慮空域信息和灰度相似性,達到保邊去噪的目的。具有簡單、非迭代、局部的特點。雙邊濾波器的好處是可以做邊緣保存(edge preserving),一般過去用的維納濾波或者高斯濾波去降噪,都會較明顯地模糊邊緣,對於高頻細節的保護效果並不明顯。
這是網上一般的公式說明,但是感覺理解起來很難,畢竟是數學。根據下圖來簡要理解一下:
1.圖(a)是原始圖像,左側區域是白色(像素值為 255),右側區域是黑色(像素值為0)
2.圖(b)是進行均值濾波的可能結果。在進行均值濾波時,僅僅考慮空間信息,此時左右兩側的像素的處理結果是綜合考慮周邊元素像素值,並對它們取均值得到的
3.圖(c)是進行雙邊濾波的可能結果。在進行雙邊濾波時,不僅考慮空間信息,還考慮色彩差別信息
在雙邊濾波中,在計算左側白色區域邊緣點的濾結果時
1.對於白色的點,給予的權重較大
2.對於黑色的點,由於色彩差異較大,顏色距離很遠(注意,不是像素點之間的物理距離,而是顏色值的距離。像素點的值分別是0和 255,差別很大,所以說它們顏色距離很遠) 因此可以將它們的權重設置為0。
這樣,在計算左側白色邊緣濾波結果時,得到的仍然是白色。因此,雙邊濾波后,左側邊緣得到保留
在計算右側黑色區域邊緣點的濾波結果時:
1.對於黑色的點,給予的權重較大
2.對於白色的點,由於色彩差異較大,顏色距離很遠,因此可以將它們的權重設置為0
這樣,在計算右側黑色邊濾波結果時,得到的仍然是黑色。因此,雙邊濾波后,左側邊緣得到保留
具體計算還沒理解清楚。以后用到再學吧
下面代碼來自博文:https://blog.csdn.net/shan54321/article/details/80658088
#include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <iostream> #include<ctime> using namespace std; using namespace cv; /* 計算空間權值 */ double **get_space_Array(int _size, int channels, double sigmas) { // [1] 空間權值 int i, j; // [1-1] 初始化數組 double **_spaceArray = new double*[_size + 1]; //多一行,最后一行的第一個數據放總值 for (i = 0; i < _size + 1; i++) { _spaceArray[i] = new double[_size + 1]; } // [1-2] 高斯分布計算 int center_i, center_j; center_i = center_j = _size / 2; _spaceArray[_size][0] = 0.0f; // [1-3] 高斯函數 for (i = 0; i < _size; i++) { for (j = 0; j < _size; j++) { _spaceArray[i][j] = exp(-(1.0f)* (((i - center_i)*(i - center_i) + (j - center_j)*(j - center_j)) / (2.0f*sigmas*sigmas))); _spaceArray[_size][0] += _spaceArray[i][j]; } } return _spaceArray; } /* 計算相似度權值 */ double *get_color_Array(int _size, int channels, double sigmar) { // [2] 相似度權值 int n; double *_colorArray = new double[255 * channels + 2]; //最后一位放總值 double wr = 0.0f; _colorArray[255 * channels + 1] = 0.0f; for (n = 0; n < 255 * channels + 1; n++) { _colorArray[n] = exp((-1.0f*(n*n)) / (2.0f*sigmar*sigmar)); _colorArray[255 * channels + 1] += _colorArray[n]; } return _colorArray; } /* 雙邊 掃描計算 */ void doBialteral(cv::Mat *_src, int N, double *_colorArray, double **_spaceArray) { int _size = (2 * N + 1); cv::Mat temp = (*_src).clone(); // [1] 掃描 for (int i = 0; i < (*_src).rows; i++) { for (int j = 0; j < (*_src).cols; j++) { // [2] 忽略邊緣 if (i >(_size / 2) - 1 && j >(_size / 2) - 1 && i < (*_src).rows - (_size / 2) && j < (*_src).cols - (_size / 2)) { // [3] 找到圖像輸入點,以輸入點為中心與核中心對齊 // 核心為中心參考點 卷積算子=>高斯矩陣180度轉向計算 // x y 代表卷積核的權值坐標 i j 代表圖像輸入點坐標 // 卷積算子 (f*g)(i,j) = f(i-k,j-l)g(k,l) f代表圖像輸入 g代表核 // 帶入核參考點 (f*g)(i,j) = f(i-(k-ai), j-(l-aj))g(k,l) ai,aj 核參考點 // 加權求和 注意:核的坐標以左上0,0起點 double sum[3] = { 0.0,0.0,0.0 }; int x, y, values; double space_color_sum = 0.0f; // 注意: 公式后面的點都在核大小的范圍里 // 雙邊公式 g(ij) = (f1*m1 + f2*m2 + ... + fn*mn) / (m1 + m2 + ... + mn) // space_color_sum = (m1 + m12 + ... + mn) for (int k = 0; k < _size; k++) { for (int l = 0; l < _size; l++) { x = i - k + (_size / 2); // 原圖x (x,y)是輸入點 y = j - l + (_size / 2); // 原圖y (i,j)是當前輸出點 values = abs((*_src).at<cv::Vec3b>(i, j)[0] + (*_src).at<cv::Vec3b>(i, j)[1] + (*_src).at<cv::Vec3b>(i, j)[2] - (*_src).at<cv::Vec3b>(x, y)[0] - (*_src).at<cv::Vec3b>(x, y)[1] - (*_src).at<cv::Vec3b>(x, y)[2]); space_color_sum += (_colorArray[values] * _spaceArray[k][l]); } } // 計算過程 for (int k = 0; k < _size; k++) { for (int l = 0; l < _size; l++) { x = i - k + (_size / 2); // 原圖x (x,y)是輸入點 y = j - l + (_size / 2); // 原圖y (i,j)是當前輸出點 values = abs((*_src).at<cv::Vec3b>(i, j)[0] + (*_src).at<cv::Vec3b>(i, j)[1] + (*_src).at<cv::Vec3b>(i, j)[2] - (*_src).at<cv::Vec3b>(x, y)[0] - (*_src).at<cv::Vec3b>(x, y)[1] - (*_src).at<cv::Vec3b>(x, y)[2]); for (int c = 0; c < 3; c++) { sum[c] += ((*_src).at<cv::Vec3b>(x, y)[c] * _colorArray[values] * _spaceArray[k][l]) / space_color_sum; } } } for (int c = 0; c < 3; c++) { temp.at<cv::Vec3b>(i, j)[c] = sum[c]; } } } } // 放入原圖 (*_src) = temp.clone(); return; } /* 雙邊濾波函數 */ void myBialteralFilter(cv::Mat *src, cv::Mat *dst, int N, double sigmas, double sigmar) { // [1] 初始化 *dst = (*src).clone(); int _size = 2 * N + 1; // [2] 分別計算空間權值和相似度權值 int channels = (*dst).channels(); double *_colorArray = NULL; double **_spaceArray = NULL; _colorArray = get_color_Array(_size, channels, sigmar); _spaceArray = get_space_Array(_size, channels, sigmas); // [3] 濾波 doBialteral(dst, N, _colorArray, _spaceArray); return; } //圖像椒鹽化 void salt(Mat &image, int num) { if (!image.data) return;//防止傳入空圖 int i, j; srand(time(NULL)); for (int x = 0; x < num; ++x) { i = rand() % image.rows; j = rand() % image.cols; image.at<Vec3b>(i, j)[0] = 255; image.at<Vec3b>(i, j)[1] = 255; image.at<Vec3b>(i, j)[2] = 255; } } int main(void) { // [1] src讀入圖片 cv::Mat src = cv::imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\06.jpg"); cv::imshow("原圖", src); // [2] dst目標圖片 cv::Mat dst1,dst2; // [3] 濾波 N越大越平越模糊(2*N+1) sigmas空間越大越模糊sigmar相似因子 myBialteralFilter(&src, &dst1, 25, 12.5, 50); Mat Salt_Image; src.copyTo(Salt_Image); salt(Salt_Image, 3000); myBialteralFilter(&Salt_Image, &dst2, 25, 12.5, 50); // [4] 窗體顯示 cv::imshow("加噪聲", Salt_Image); cv::imshow("dst1", dst1); cv::imshow("加噪聲dst2", dst2); cv::waitKey(0); cv::destroyAllWindows(); return 0; }