前言
上一篇文章,我們講解了圖像的虛擬邊緣,這篇文章開始進行平滑(也就是模糊)處理。
基本原理
這里直接引用OpenCV 2.4+ C++ 平滑處理和OpenCV 2.4+ C++ 邊緣梯度計算的相關內容:
平滑也稱模糊, 是一項簡單且使用頻率很高的圖像處理方法。
平滑處理時需要用到一個濾波器。 最常用的濾波器是線性濾波器,線性濾波處理的輸出像素值(例如:
)是輸入像素值(例如:
)的加權平均:
稱為核, 它僅僅是一個加權系數。
這里涉及一種叫做“卷積”的運算,那么卷積是什么呢?
卷積是在每一個圖像塊與某個算子(核)之間進行的運算。
核?!
核就是一個固定大小的數值數組。該數組帶有一個錨點 ,一般位於數組中央。
可是這怎么運算啊?
假如你想得到圖像的某個特定位置的卷積值,可用下列方法計算:
- 將核的錨點放在該特定位置的像素上,同時,核內的其他值與該像素鄰域的各像素重合;
- 將核內各值與相應像素值相乘,並將乘積相加;
- 將所得結果放到與錨點對應的像素上;
- 對圖像所有像素重復上述過程。
用公式表示上述過程如下:
在圖像邊緣的卷積怎么辦呢?
計算卷積前,需要通過復制源圖像的邊界創建虛擬像素,這樣邊緣的地方也有足夠像素計算卷積了。這就是為什么上一篇文章需要做虛擬邊緣函數。
均值平滑
均值平滑實際上就是內核元素全是1的卷積運算,然后再除以內核的大小,用數學表達式來表示就是:
下面我們來實現均值平滑函數blur:
function blur(__src, __size1, __size2, __borderType, __dst){ if(__src.type && __src.type == "CV_RGBA"){ var height = __src.row, width = __src.col, dst = __dst || new Mat(height, width, CV_RGBA), dstData = dst.data; var size1 = __size1 || 3, size2 = __size2 || size1, size = size1 * size2; if(size1 % 2 !== 1 || size2 % 2 !== 1){ console.error("size大小必須是奇數"); return __src; } var startX = Math.floor(size1 / 2), startY = Math.floor(size2 / 2); var withBorderMat = copyMakeBorder(__src, startY, startX, 0, 0, __borderType), mData = withBorderMat.data, mWidth = withBorderMat.col; var newValue, nowX, offsetY, offsetI; var i, j, c, y, x; for(i = height; i--;){ offsetI = i * width; for(j = width; j--;){ for(c = 3; c--;){ newValue = 0; for(y = size2; y--;){ offsetY = (y + i) * mWidth * 4; for(x = size1; x--;){ nowX = (x + j) * 4 + c; newValue += mData[offsetY + nowX]; } } dstData[(j + offsetI) * 4 + c] = newValue / size; } dstData[(j + offsetI) * 4 + 3] = mData[offsetY + startY * mWidth * 4 + (j + startX) * 4 + 3]; } } }else{ console.error("不支持類型。"); } return dst; }
其中size1和size2分別是核的橫向和縱向大小,並且必須是正奇數。
高斯平滑
最有用的濾波器 (盡管不是最快的)。 高斯濾波是將輸入數組的每一個像素點與高斯內核卷積將卷積和當作輸出像素值。
參考一維高斯函數,我們可以看見,他是個中間大兩邊小的函數。
所以高斯濾波器其加權數是中間大,四周小的。
其二維高斯函數為:
其中 為均值 (峰值對應位置),
代表標准差 (變量
和 變量
各有一個均值,也各有一個標准差)。
這里參考OpenCV的實現,不過應該還有優化空間,因為還沒用到分離濾波器。
首先我們做一個getGaussianKernel來返回高斯濾波器的一維數組。
function getGaussianKernel(__n, __sigma){ var SMALL_GAUSSIAN_SIZE = 7, smallGaussianTab = [[1], [0.25, 0.5, 0.25], [0.0625, 0.25, 0.375, 0.25, 0.0625], [0.03125, 0.109375, 0.21875, 0.28125, 0.21875, 0.109375, 0.03125] ]; var fixedKernel = __n & 2 == 1 && __n <= SMALL_GAUSSIAN_SIZE && __sigma <= 0 ? smallGaussianTab[__n >> 1] : 0; var sigmaX = __sigma > 0 ? __sigma : ((__n - 1) * 0.5 - 1) * 0.3 + 0.8, scale2X = -0.5 / (sigmaX * sigmaX), sum = 0; var i, x, t, kernel = []; for(i = 0; i < __n; i++){ x = i - (__n - 1) * 0.5; t = fixedKernel ? fixedKernel[i] : Math.exp(scale2X * x * x); kernel[i] = t; sum += t; } sum = 1 / sum; for(i = __n; i--;){ kernel[i] *= sum; } return kernel; };
然后通過兩個這個一維數組,便可以計算出一個完整的高斯內核,再利用blur里面用到的循環方法,就可以算出高斯平滑后的矩陣了。
function GaussianBlur(__src, __size1, __size2, __sigma1, __sigma2, __borderType, __dst){ if(__src.type && __src.type == "CV_RGBA"){ var height = __src.row, width = __src.col, dst = __dst || new Mat(height, width, CV_RGBA), dstData = dst.data; var sigma1 = __sigma1 || 0, sigma2 = __sigma2 || __sigma1; var size1 = __size1 || Math.round(sigma1 * 6 + 1) | 1, size2 = __size2 || Math.round(sigma2 * 6 + 1) | 1, size = size1 * size2; if(size1 % 2 !== 1 || size2 % 2 !== 1){ console.error("size必須是奇數。"); return __src; } var startX = Math.floor(size1 / 2), startY = Math.floor(size2 / 2); var withBorderMat = copyMakeBorder(__src, startY, startX, 0, 0, __borderType), mData = withBorderMat.data, mWidth = withBorderMat.col; var kernel1 = getGaussianKernel(size1, sigma1), kernel2, kernel = new Array(size1 * size2); if(size1 === size2 && sigma1 === sigma2) kernel2 = kernel1; else kernel2 = getGaussianKernel(size2, sigma2); var i, j, c, y, x; for(i = kernel2.length; i--;){ for(j = kernel1.length; j--;){ kernel[i * size1 + j] = kernel2[i] * kernel1[j]; } } var newValue, nowX, offsetY, offsetI; for(i = height; i--;){ offsetI = i * width; for(j = width; j--;){ for(c = 3; c--;){ newValue = 0; for(y = size2; y--;){ offsetY = (y + i) * mWidth * 4; for(x = size1; x--;){ nowX = (x + j) * 4 + c; newValue += (mData[offsetY + nowX] * kernel[y * size1 + x]); } } dstData[(j + offsetI) * 4 + c] = newValue; } dstData[(j + offsetI) * 4 + 3] = mData[offsetY + startY * mWidth * 4 + (j + startX) * 4 + 3]; } } }else{ console.error("不支持的類型"); } return dst; }
中值平滑
中值濾波將圖像的每個像素用鄰域 (以當前像素為中心的正方形區域)像素的中值代替 。
依然使用blur里面用到的循環,只要得到核中的所有值,再通過sort排序便可以得到中值,然后錨點由該值替代。
function medianBlur(__src, __size1, __size2, __borderType, __dst){ if(__src.type && __src.type == "CV_RGBA"){ var height = __src.row, width = __src.col, dst = __dst || new Mat(height, width, CV_RGBA), dstData = dst.data; var size1 = __size1 || 3, size2 = __size2 || size1, size = size1 * size2; if(size1 % 2 !== 1 || size2 % 2 !== 1){ console.error("size必須是奇數"); return __src; } var startX = Math.floor(size1 / 2), startY = Math.floor(size2 / 2); var withBorderMat = copyMakeBorder(__src, startY, startX, 0, 0, __borderType), mData = withBorderMat.data, mWidth = withBorderMat.col; var newValue = [], nowX, offsetY, offsetI; var i, j, c, y, x; for(i = height; i--;){ offsetI = i * width; for(j = width; j--;){ for(c = 3; c--;){ for(y = size2; y--;){ offsetY = (y + i) * mWidth * 4; for(x = size1; x--;){ nowX = (x + j) * 4 + c; newValue[y * size1 + x] = mData[offsetY + nowX]; } } newValue.sort(function(a, b){return a - b;}); dstData[(j + offsetI) * 4 + c] = newValue[Math.round(size / 2)]; } dstData[(j + offsetI) * 4 + 3] = mData[offsetY + startY * mWidth * 4 + (j + startX) * 4 + 3]; } } }else{ console.error("類型不支持"); } return dst; };
更多例子
系列目錄
參考資料
opencv源碼解析之(4):GaussianBlur() . tornadomeet . 2012-03-10