超越單一像素:鄰域為何重要
在上一篇指南中,我們做的每一個運算都把每個像素當成一座孤島。要把影像調亮,我們對每個像素獨立地加上一個數值;要做閾值處理,我們一次只看一個像素,問:「這個值有沒有超過 128?」這些叫做點運算,因為一個像素的新值只取決於它自己的舊值,從不參考鄰居。這很強大,但有個硬天花板:單一一個數字,根本無法告訴你眼前是一面牆平滑的中段、一道門口俐落的邊界,還是一粒相機的雜訊顆粒。
這裡有個關鍵直覺。想像我給你看一個完全孤立的手寫字母,它可能是潦草的「c」也可能是潦草的「e」,你真的無從判斷。但把同樣的形狀放進「c_t」對比「m_t」這樣的單字裡,你的大腦立刻就補上了答案。這個字母只有在周圍字母的脈絡中才變得有意義。像素也完全一樣:一個孤零零的灰階值(比如 130)毫無意義,但若它的鄰居全都在 130 附近,你就身處一塊平滑區域;若它旁邊的值從 40 突然跳到 220,你就正坐在一條邊上。
於是我們躍進到鄰域運算。我們不再只看一個像素,而是看以我們關心的像素為中心、由像素組成的一小塊方形區塊——它的鄰域,也叫視窗。3×3 的視窗是該像素加上它周圍的八個直接鄰居;5×5 的視窗再往外延伸一圈。我們要建立的規則一句話就能說清:要算出輸出影像的一個像素,就去看輸入影像對應位置周圍那一窗像素,用某種選定的方式把那些數字結合起來,再把單一結果寫進輸出。對每個位置都這樣做,整張影像就被濾過一遍了。
一個由方格組成的規則網格代表像素,中央一個 3 乘 3 的區塊被加上陰影,標出某中心像素周圍的鄰域視窗。
幾乎每一種傳統影像濾波器——模糊、銳化、邊緣偵測、浮雕——都只是這一個模式,只是換了不同的「如何結合視窗」配方。整套機器叫做空間濾波,本篇指南接下來會一步步把它建起來:先是滑動視窗機制本身,再來是影像邊界該怎麼處理,最後是兩個具體的濾波器(均值模糊與高斯模糊),把這套機制變成看得見的實用工具。
卷積:濾波核心的滑動視窗
現在來談整條學習路徑的核心概念。「如何結合視窗」的配方本身就是一小格數字,叫做核(也叫濾波器或遮罩)。一個 3×3 的核就是九個權重。我們把這個核疊在影像上,讓它蓋住一塊 3×3 的區塊,把每個核權重乘上它底下的像素值,再把這九個乘積全部加起來,這個總和就成為中心像素的新值。然後我們把核往右滑一格再重複一次,像讀書頁一樣掃過整張影像。這種一邊滑動、一邊相乘相加的程序,就叫卷積。
一個好用的心像:這個核是一個會計算加權平均的小型鏤空模板。想像一個透明印章,它的九格各寫著一個數字。你把它按在哪裡,它就讀取透出來的那九個像素,按各自印著的數字加權,再印回一個混合後的單一值。不同的印章造出不同的效果——全是相等權重的印章會模糊,中央大正值、邊緣為負值的印章會銳化——但「蓋章」這個動作永遠相同:重疊、相乘、相加。
一張輸入影像網格,上面疊著一個 3 乘 3 的小核,箭頭顯示逐元素相乘並加總,在輸出網格產生單一數值。
空間濾波的展開式:輸出 = 視窗內 權重 × 像素 的總和。
讓我們把每個符號都拆解一遍,因為這一條式子撐起了整條路徑。f 是輸入影像,所以 f(x+s, y+t) 是位於第 y+t 列、第 x+s 行那個輸入像素的亮度。w 是核,也就是權重的方格,用小小的偏移量 s 與 t 來索引;對 3×3 的核來說,s 和 t 都跑過 −1、0、+1,所以 (s,t)=(0,0) 是中心權重,(s,t)=(−1,−1) 是左上角那個。g 是濾波後的輸出,g(x,y) 就是我們為輸出位置 (x,y) 算出的那一個數字。雙重加總只是在說:走遍核視窗的每一格,把那個權重乘上它此刻蓋住的輸入像素,再把所有乘積加起來。用三個詞概括:重疊、相乘、相加。
現在來親手完整算一個 3×3 的例子——別跳過,這正是卷積「叮」一聲想通的時刻。假設核底下的輸入區塊逐列讀出來是 [10, 10, 10 / 10, 50, 10 / 10, 10, 10]——一塊平坦的灰色區域,中央有一個亮度 50 的尖峰。取一個簡單的平均核,每個權重都是 1/9。每個乘積是 (1/9)×像素,總和為 (1/9)×(10+10+10+10+50+10+10+10+10) = (1/9)×130 ≈ 14.4。於是那個亮的 50 被拉低到約 14——尖峰被它周圍平靜的鄰居平均掉了。這個單一輸出值 14 被寫進 g 的中心位置,接著核就滑到下一個位置。
影像的邊界:填補與步幅
有個我們略過的實務小麻煩。當核坐落在影像最角落的像素上時,它有一部分懸到了邊界外、懸在虛空中——對一個中心落在左上角像素的 3×3 核來說,它的九格裡有五格底下根本沒有像素。我們無法把權重乘上一個不存在的值,所以邊界需要一套處置政策。這就是填補的問題。
用個比喻:想像你在貼壁紙,但你要蓋的圖案需要略微超出牆的邊緣。填補就是你決定在那段懸出處假裝有什麼。三種常見選擇:補零假設邊界外一切都是黑的(值為 0)——最簡單,但可能讓邊界變暗、產生一圈淡淡的暗邊。複製(邊緣)填補把最靠近的真實邊緣像素往外複製,像把邊界顏色塗抹出牆外——做模糊時通常最自然。鏡射填補以邊緣為軸把影像鏡像出去,彷彿牆遇上了一面鏡子,能避免複製有時造成的人工平坦帶。沒有哪一種是放諸四海皆準的;它們只是對未知處所做的不同合理猜測。
與之搭配的概念是步幅:核每次停下之間跳多遠。步幅為 1(幾乎所有平滑或邊緣濾波器的預設值)時,視窗一次移動一個像素,輸出基本上是每個輸入像素對應一個值。步幅為 2 時,它每隔一個位置才停一次,對影像取樣得較粗,產生較小的輸出。本路徑大多停在步幅 1,但現在就先認識步幅是值得的,因為它在後面的 CNN 路徑會變成關鍵概念,那裡用帶步幅的卷積作為縮小特徵圖的標準手法。
一張影像網格外圍多了一圈填補格,圖中顯示核在相隔一個步幅距離的兩個位置上。
輸出寬度,作為輸入寬度、核大小、填補與步幅的函數。
這條小公式純粹是尺寸的記帳,每個符號都很具體。W 是輸入寬度(以像素計);k 是核大小(3×3 的核就是 3);p 是你在每一側加上的填補像素數;s 是步幅;out 是算出的輸出寬度。括號 ⌊ ⌋ 代表取下高斯(向下取整)——往下捨到最接近的整數,因為不可能有分數個像素。直覺從左讀到右:W − k 是核中心在衝出邊界前能走多遠(你損失了相當於核大小的邊距),+2p 把填補替你買回的空間加回來(左邊 p、右邊 p),除以 s 反映出每步跳得更大,最後 +1 算進核起步的那第一個位置。
代入真實數字,讓它不再抽象。取一張寬 W = 100 的影像、k = 3 的核、不填補(p = 0)、步幅 s = 1:out = ⌊(100 − 3 + 0)/1⌋ + 1 = ⌊97⌋ + 1 = 98。所以不填補時,影像會悄悄從 100 縮成 98——你在每一側各損失一像素的邊。現在加上 p = 1 的填補:out = ⌊(100 − 3 + 2)/1⌋ + 1 = ⌊99⌋ + 1 = 100,正好是原來的大小。這就是讓影像通過濾波器後維持原尺寸的標準技巧:填補 p = (k−1)/2,對 3×3 的核就是 1。最後試試在同樣填補下用步幅 s = 2:out = ⌊99/2⌋ + 1 = ⌊49.5⌋ + 1 = 49 + 1 = 50——大約是一半的大小,正如更大的跳幅所預示。
均值濾波器:最簡單的模糊
現在我們終於把核填上真正的數字,看這台機器動起來。最直覺的平滑配方是:把每個像素換成它鄰域的單純平均。做這件事的核就是方框或均值濾波器——對 3×3 視窗來說,九個權重每一個都單純是 1/9。
import numpy as np
# A 3x3 mean (box) kernel: nine equal weights of 1/9.
kernel = np.ones((3, 3), dtype=np.float32) / 9.0
# [[1/9, 1/9, 1/9],
# [1/9, 1/9, 1/9],
# [1/9, 1/9, 1/9]]
def mean_filter_pixel(patch):
# patch is the 3x3 window of input pixels under the kernel.
# 'Overlap, multiply, add' == element-wise product then sum.
return float(np.sum(patch * kernel)) # identical to patch.mean()每個權重都等於 1/N,所以這個濾波器就是視窗上的單純平均。
讀這條公式:它就是第 2 節的一般卷積,只是把權重 w(s,t) 對每一格都釘死在常數 1/N 上。N 是視窗裡的像素數——3×3 是 k·k = 3·3 = 9,5×5 是 25。因為每個鄰居都乘上同樣的 1/N、再把乘積加總,結果就字面上是視窗的算術平均。把它想成一場民主投票:每個鄰居拿到一張相等的票,輸出就是共識。這份平等同時是這個濾波器的長處與短處。
為什麼平均能減少雜訊?雜訊是隨機的——在某個像素上,感測器可能讀得略高,而它的鄰居讀得略低,毫無規律。當你平均九個這樣的像素時,隨機的偏高與偏低傾向互相抵消,而真正底層的亮度(在這九個之間大致相同)則存活下來。回想我們算過的例子:一片 10 之海中孤零零的尖峰 50 塌縮成了約 14。若那個 50 是一次雜訊故障,均值濾波器剛剛把它漂亮地清掉了。
但為什麼同一個濾波器又會模糊?因為它完全不知道自己坐在什麼結構上。在一條真實的邊上——比如九個讀數是 [10, 10, 10, 200, 200, 200, 200, 200, 200],一個從暗到亮的乾淨階躍——均值濾波器照樣只是把它們平均到約 137,把俐落的邊界抹成一道柔和的灰色斜坡。這場民主投票把邊緣像素和平坦區域像素一視同仁:它分不清有意義的跳躍與無意義的抖動,於是把兩者一樣地稀釋掉。這就是平滑的根本張力,而這正是下一個濾波器、以及下一篇指南,要動手修正的缺陷。
高斯模糊:尊重距離的平滑
均值濾波器的缺陷在於,它給視窗遙遠角落的票,和中心像素自己的票一樣重。但直覺上,緊鄰你的像素,應該比三步之外的像素更能說明你的真實值。修正之道是讓權重隨距離衰減——近鄰算得重、遠鄰算得輕。經典的做法是高斯模糊,它的核是一座鐘形的權重小丘:中央高聳,向邊緣平滑地收細。
二維高斯:一座以該像素為中心的平滑權重小丘。
我們仔細拆解。x 與 y 是某個核格相對中心的偏移——中心格是 (0,0),正右方是 (1,0),依此類推,所以 x²+y² 就是該格到中心的距離平方。σ(sigma)是標準差,是控制這座丘有多寬的單一旋鈕——它的「搆得到的範圍」或模糊半徑。exp(−距離²/2σ²) 這一項是核心:在中心,指數為 0,exp(0)=1(權重最大),而當你往外移動,權重迅速縮小,因為距離是以平方進入的。前面的因子 1/(2πσ²) 是個正規化常數,刻意選來讓所有權重加起來等於 1——這讓影像整體亮度保持不變(不會意外變暗或變亮)。
σ 是你真正會去轉的那個旋鈕。小的 σ 造出一座緊窄的丘:權重幾乎全集中在中心像素上,所以模糊極少、細節得以存活。大的 σ 把丘攤得很寬:許多鄰居都拿到有意義的權重,於是影像被強烈而柔和地平滑。再注意兩個性質。第一,因為公式只取決於 x²+y²(距離,而非方向),這個核是旋轉對稱的——它在每個方向上模糊得一樣多,所以高斯模糊沒有偏好方向,不會帶來條紋狀的人工痕跡。第二,它完全對稱,所以(記得第 2 節那則提示)這裡卷積與互相關完全相同——不必擔心翻轉。
這裡有個具體的 3×3 高斯核(小 σ 時常見的近似)。在乘上正規化常數前,它是整數格 [1, 2, 1 / 2, 4, 2 / 1, 2, 1]。這九個數加起來是 16,所以把每一項除以 16,它們的總和就成為 1。注意它的結構:中心權重是 4/16,四個邊鄰各是 2/16,而四個對角角落(離得較遠)各只有 1/16。直接和均值濾波器對比,那裡九格全是齊平的 1/9:高斯仍會平滑,但它讓中心像素保有最大的發言權,所以邊緣的保留程度明顯優於方框濾波器那種粗暴的民主。
平滑:去雜訊的第一口
退一步看看我們真正建起了什麼。平滑在日常中的主要工作是去雜訊——清掉滲進影像裡的隨機斑點。你見過它:在昏暗光線下拍的照片看起來顆粒感很重,散布著一點一點、實際場景裡並不存在的明暗斑塊。那種顆粒就是影像雜訊,是感測器在光線太少、無法對每個像素有把握時所加上的隨機誤差。因為雜訊是隨機的、像素與像素之間互不相關,而真實場景結構在鄰居之間平滑一致,所以一個把鄰居平均在一起的模糊,自然會壓抑雜訊多於它對訊號的影響。
但我們也見過了那個陷阱,值得明白地說出來,因為它推動了這條路徑接下來的整個段落。均值與高斯模糊都是盲目的平滑器:它們不管視窗裡是什麼,都照樣平均。所以它們替你換來更乾淨的平坦區域,代價是更柔的邊緣——每一次模糊都是用銳利度去換低雜訊。把 σ 調高以殺掉更多顆粒,就看著邊緣融化;把 σ 壓低以保護邊緣,就看著顆粒存活。單純的模糊找不到任何一組設定能在去除雜訊的同時讓真正的邊緣維持俐落,因為這種濾波器根本分不出兩者。
本篇的一切都建立在你如今已掌握的一個概念上:卷積的滑動視窗。一小格權重掃過影像;重疊、相乘、相加;填補邊界;選定權重,你就選定了效果——齊平的權重做出均值模糊,鐘形的權重做出高斯模糊。這同一台滑動視窗機器,正是邊緣偵測(再下一篇指南)、乃至後面路徑中卷積神經網路的根基。平滑只是學會它最溫柔、最直觀的入口。接下來,我們要讓這些濾波器變得夠聰明,去尊重它們至今一直在融化掉的那些邊緣。