演算法 | 數字影像處理之「中值濾波」

就良同學發表於2023-04-13

中值濾波原理

中值濾波就是用一個奇數點的移動視窗(要求奇數主要是為了保證整個模板有唯一中心元素),將視窗中心點的值用視窗內各點的中值代替。假設視窗內有5點,其值為80、90、200、110和120,那麼此視窗內各點的中值即為110。

設有一個一維序列\(f_1,f_2,...,f_n\),取視窗長度(點數)為m(m為奇數),對其進行中值濾波,就是從輸入序列中相機抽出m個數\(f_{i-v},...,f_{i-1},f_i,f_{i+1},...,f_{i+v}\)(其中\(f_i\)為視窗中心點值,\(v=(m-1)/2\)),再將這m個點按其數值大小排序,取其序號為中心點的那個數作為濾波輸出。用數學公式表示為:

\[y_i=Median\{f_{i-v},...,f_{i-1},f_i,f_{i+1},...,f_{i+v}\},i\in N,v=\frac{m-1}{2} \]

如:以3*3的領域為例求中值濾波中畫素5的值。

image

  1. int pixel[9]中儲存畫素1,畫素2...畫素9的值;
  2. 對陣列pixel進行排序操作;
  3. 畫素5的值即為陣列pixel的中值pixel[4]。

程式碼實現

void medianFilter(cv::Mat& src, cv::Mat& dst, cv::Size size) {
	/*step1:判斷視窗size是否為奇數*/
	if (size.width % 2 == 0 || size.height % 2 == 0) {
		cout << "卷積核視窗大小應為奇數!\n";
		exit(-1);
	}

	/*step2:對原圖進行邊界擴充*/
	int h = (size.height - 1) / 2;
	int w = (size.width - 1) / 2;
	Mat src_border;
	copyMakeBorder(src, src_border, h, h, w, w, BORDER_REFLECT_101);

	/*step3:卷積操作*/
 	map<uchar, Point> mp; // 定義容器儲存每個卷積視窗內各畫素點的<畫素值, 畫素位置>
	for (int i = h; i < src.rows + h; i++) {
		for (int j = w; j < src.cols + w; j++) {
			mp.clear();
			for (int ii = i - h; ii <= i + h; ii++) {
				for (int jj = j - w; jj <= j + w; jj++) {
					Point point(jj, ii);
					uchar value;
					if (src.channels() == 1) {
						// 灰度影像,儲存灰度值
						value = src_border.at<uchar>(ii, jj);
					}else {
						// 彩色影像,儲存亮度值
						uchar value_b = src_border.at<cv::Vec3b>(ii, jj)[0];
						uchar value_g = src_border.at<cv::Vec3b>(ii, jj)[1];
						uchar value_r = src_border.at<cv::Vec3b>(ii, jj)[2];
						value = 0.114 * value_b + 0.587 * value_g + 0.299 * value_r;
					}
					mp[value] = point;
				}
			}
			// 將視窗中心點的值用視窗內個點的中值代替
			auto iter = mp.begin();
			int count = 0;
			Point pixel;
			int median_size = mp.size() / 2;
			while (iter != mp.end()) {
				if (count == median_size) {
					pixel = Point(iter->second.x, iter->second.y);
					break;
				}
				count++;
				iter++;
			}
			if (src.channels() == 1) {
				dst.at<uchar>(i - h, j - w) = src_border.at<uchar>(pixel.y, pixel.x);
			}
			else {
				dst.at<cv::Vec3b>(i - h, j - w)[0] = src_border.at<cv::Vec3b>(pixel.y, pixel.x)[0];
				dst.at<cv::Vec3b>(i - h, j - w)[1] = src_border.at<cv::Vec3b>(pixel.y, pixel.x)[1];
				dst.at<cv::Vec3b>(i - h, j - w)[2] = src_border.at<cv::Vec3b>(pixel.y, pixel.x)[2];
			}
		}
	}
}

程式碼講解

copyMakeBorder(src,src_border,h,h,w,w,BORDER_REFLECT_101);

在模板或卷積的加權運算中的影像邊界問題:當在影像上移動模板(卷積核)至影像邊界時,在原影像中找不到與卷積核中的加權係數相對應的N個畫素(N為卷積核元素個數),即卷積核懸掛在影像的邊界上,這種現象在影像的上下左右四個邊界上均會出現。例如,當模板為:

\[\frac{1}{9} \begin{bmatrix} %該矩陣一共3列,每一列都居中放置 1 & 1 & 1\\ %第一行元素 1 & 1 & 1\\ %第二行元素 1 & 1 & 1\\ %第二行元素 \end{bmatrix} \]

設原影像為:

\[\begin{bmatrix} %該矩陣一共3列,每一列都居中放置 1 & 1 & 1 & 1 & 1\\ %第1行元素 2 & 2 & 2 & 2 & 2\\ %第2行元素 3 & 3 & 3 & 3 & 3\\ %第3行元素 4 & 4 & 4 & 4 & 4\\ %第3行元素 \end{bmatrix} \]

經過卷積操作之後影像為:

\[\begin{bmatrix} %該矩陣一共3列,每一列都居中放置 - & - & - & - & -\\ %第1行元素 - & 2 & 2 & 2 & -\\ %第2行元素 - & 3 & 3 & 3 & -\\ %第3行元素 - & - & - & - & -\\ %第3行元素 \end{bmatrix} \]

"-"表示無法進行卷積操作的畫素點。

解決方法有2種:①忽略影像邊界資料(即不管邊界,卷積操作的範圍從整張圖縮小為邊界內縮K圈,K的值隨卷積核尺寸變化)。②將原影像往外擴充畫素,如在影像四周複製源影像邊界的值,從而使得卷積核懸掛在原影像四周時也能進行正常的計算。

opencv邊框處理copyMakeBorder: https://zhuanlan.zhihu.com/p/108408180

value=0.114*value_b+0.587*value_g+0.299*value_r;

對於彩色影像,我們取影像亮度的中間值,亮度值的計算方法為:

\[luminance = 0.299R + 0.587G + 0.114B \]

map<uchar, Point> mp;

map為C++的stl中的關聯性容器,為了實現快速查詢,map內部本身就是按序儲存的(map底層實現是紅黑二叉樹)。在我們插入<key, value>鍵值對時,就會按照key的大小順序進行儲存,其中key的型別必須能夠進行 < 運算,且唯一,預設排序是按照從小到大遍歷。

因此,將亮度值或灰度值作為鍵,map將自動進行按鍵排序,無需手動書寫排序程式碼。

實現效果

卷積核size為(5, 5)。

image

相關文章