銳利化會對圖像的邊緣輪廓加強,使得像素跟像素之間的邊界越來越明顯,可以讓圖片看起來變得更清晰,也因為我們總會被圖片對比最高的地方吸引,所以也可以用來強調某些特別的區域,吸引別人目光,而在介紹如何實作他之前我們先需要介紹濾波矩陣
濾波矩陣在圖像處理上很多效果都會使用到,例如模糊、銳利化、邊緣偵測等等,使用不同的濾波矩陣就可以產生出不同的效果,而使用上我們會將圖像的每一個點對應到矩陣中心,接著計算他的相鄰像素和對應矩陣位子相乘的結果,最後相加替代為原本的值。
先做出上面的算法,在邊界部分目前採用直接跳過, ( 例如圖片最左上角,當中心點對到中間時左邊跟上面都是超出邊界情況 ),其他常用的作法有取平均值、補零等等。
export const convolve = (imageData, kernel) => {
const pixelData = imageData.data
const imageWidth = imageData.width
const imageHeight = imageData.height
// 這邊需要複製一份新的資料是因為接下來算權重的時候我們需要用原本的值去做計算,
// 不是已經計算過權重的值
const output = new ImageData(
new Uint8ClampedArray(imageData.data),
imageData.width,
imageData.height
)
// 尋找單邊長度,矩陣通常為奇數 3 * 3 , 5 * 5 ...
const side = Math.sqrt(kernel.length)
const half = Math.floor(side / 2)
const outputPixelData = output.data
for (let y = 0; y < imageHeight; y++) {
for (let x = 0; x < imageWidth; x++) {
const dstOff = (y * imageWidth + x) * 4
let totalR = 0
let totalG = 0
let totalB = 0
for (let row = 0; row < side; row++) {
for (let col = 0; col < side; col++) {
// 尋找範圍內座標
const srcY = y + row - half
const srcX = x + col - half
// 如果範圍超出,退出 ex 最左上角之點
if (srcY < 0 || srcY > imageHeight || srcX < 0 || srcX > imageWidth) {
continue
}
const srcOff = (srcY * imageWidth + srcX) * 4
const weight = kernel[row * side + col]
const [r, g, b] = [
pixelData[srcOff],
pixelData[srcOff + 1],
pixelData[srcOff + 2]
]
totalR += r * weight
totalG += g * weight
totalB += b * weight
}
}
outputPixelData[dstOff] = totalR
outputPixelData[dstOff + 1] = totalG
outputPixelData[dstOff + 2] = totalB
}
}
return output
}
先看張原圖
這是第一次看到活生生的氂牛,那時候突然有一種哇終於見到教科書上的東西了的感覺
所以接下來實際操作一下吧,如果用下面這個矩陣
[
1/9, 1/9 , 1/9
1/9, 1/9 , 1/9
1/9, 1/9 , 1/9
]
最後的值會與周圍的像素取平均,也就是原本較突出的部分會變得比較不明顯,也就是模糊化,最後的結果會像這樣子。
接下來使用邊緣偵測的矩陣,
[
-1, -1 , -1
-1, 8 , -1
-1, -1 , -1
]
從矩陣上來看,總和為0,也就是說,當周圍的像素跟本身幾乎是一樣時,得出來的結果會趨近於黑色。也就是我們把背景給過濾掉變成接近黑色( 因為背景顏色幾乎會是一致的,所以處理完之後會趨近於 0),而當這個像素原本就相對於其他周遭元素來的更強烈時( 也就是我們平常看到的邊緣 ),經過這個算法之後,會變得更強烈,也就是可能會接近白色。所以透過這個矩陣我們應該可以得到下面的結果。
接下來使用銳利化的矩陣,
[
-1, -1 , -1
-1, 9 , -1
-1, -1 , -1
]
跟上面的矩陣很像,差別在於總和為一,且因為中心點權重較高,所以邊緣會更顯得突出,而且整體亮度來說應該會稍微增加 ( 因為 RGB
值都被增加了 )
所以簡單更改矩陣就可以做出不同的效果,讚嘆偉大的數學家。
關於更嚴謹的數學定義以及推導方程式有興趣者可以搜尋 Kernel、卷積 以及這篇解釋的文章 如何通俗易懂地解释卷积?、,這邊就直接針對如何使用。
所以我們有了銳利化跟模糊的兩個最終結果之後,我們就可以透過一些算式簡單計算一下比例。
首先先判斷丟進來的值是大於零還是小於零,大於零的話我們就使用銳利化的矩陣,小於則使用模糊化的矩陣,接著再把值對應成 0 ~ 1
export const sharpen = (imageData, amount) => {
if (amount > 0) {
const sharpenKernel = [-1, -1, -1, -1, 9, -1, -1, -1, -1]
const outputRate = [0, 1]
const base = [0, 100]
return convolve(
imageData,
sharpenKernel,
convertRange(amount, outputRate, base)
)
} else if (amount < 0) {
const blurKernel = [
1 / 9,
1 / 9,
1 / 9,
1 / 9,
1 / 9,
1 / 9,
1 / 9,
1 / 9,
1 / 9
]
const outputRate = [0, 1]
const base = [0, 100]
return convolve(
imageData,
blurKernel,
convertRange(Math.abs(amount), outputRate, base)
)
}
}
接著更改上面計算權重的程式,如果有傳入值,我們就計算原本的值跟最終完成值的比率,所以當達到 1 的時候就會是剛剛矩陣呈現的樣子
if (amount) {
outputPixelData[dstOff] =
totalR * amount + outputPixelData[dstOff] * (1 - amount)
outputPixelData[dstOff + 1] =
totalG * amount + outputPixelData[dstOff + 1] * (1 - amount)
outputPixelData[dstOff + 2] =
totalB * amount + outputPixelData[dstOff + 2] * (1 - amount)
} else {
outputPixelData[dstOff] = totalR
outputPixelData[dstOff + 1] = totalG
outputPixelData[dstOff + 2] = totalB
}
今天介紹了銳利化處理,如果有包含毛髮照片的話效果應該會比較明顯,雖然拋開複雜數學過程不講,但其實做到現在來深深發現圖像各種轉換或者處理其實都跟數學息息相關,如果真的要深入研究可是要好好補一下數學。而這次這個處理方式也應該是目前最耗時的計算,因為會一直尋找周圍像素,而矩陣長度目前都使用 3 * 3 的小矩陣,有興趣的讀者可以在去使用更長的矩陣,會有不同的效果。當然相對應的處理時間會耗費更加多,效能上可能要看一下,明天見!