5.16. 自訂卷積核¶
到目前為止介紹的鄰域濾波器,每一種都有一個內建的統計量,在每個位置套用於視窗——平均值、高斯加權平均、中位數。morph() 是唯一讓應用程式自行提供統計量的濾波器,其形式為一個核(kernel):一個描述濾波器應如何將鄰域像素結合為單一輸出值的小型權重矩陣。
其機制就是經典的卷積運算。在每個輸出位置上,每個鄰域像素都會乘上核中對應的權重,將這些乘積加總起來,再選擇性地進行縮放與偏移,最後將該值寫入輸出像素。不同的核會從相同的輸入產生不同的結果。權重全部相等且為正的核可重現 mean() 濾波器;鐘形分佈的核則可重現 gaussian()。除此之外的各種模式還能產生邊緣響應、浮雕、梯度、銳化、運動模糊,以及一長串其他效果——凡是經典影像處理想用單一線性處理達成的事,幾乎都能辦到。
5.16.1. morph 方法¶
其簽章看起來與其他鄰域濾波器相似,只是多了一個參數:
img.morph(size, kernel, mul=1.0, add=0.0)
size 與其他地方一樣代表半徑,因此核必須剛好是 (2 * size + 1) 列乘以 (2 * size + 1) 行。核本身是一個包含那麼多數字的扁平 Python 串列,依列優先(row-major)順序排列——前 (2 * size + 1) 個項目為最上面一列,接下來 (2 * size + 1) 個為第二列,依此類推,直到最下面一列。mul 在乘積總和寫入輸出像素前先進行縮放,add 則加上一個常數。預設的 mul=1.0 與 add=0.0 會讓卷積輸出維持不變。
有一個細節值得明確說明:此方法在寫入輸出前,會自動將乘積總和除以核各項目的總和。這種自動除法意謂著,一個各項目總和為九的平均化核——例如一個 3 乘 3 的盒狀模糊——無需任何額外處理即可得到九分之一的縮放結果,而一個總和為十六的高斯近似核則得到十六分之一的縮放結果,兩者都不需要應用程式自行計算除法。只有當應用程式想在自動正規化之上再進行額外縮放時,才會設定 mul——或者更常見的情況是,當核的總和為零(邊緣響應核)時,自動除法等於除以零。在這種情況下框架會將總和視為一,而 mul 便成為唯一可用來將未縮放的乘積總和保持在範圍內的調節旋鈕。
來自自適應閾值章節的 threshold=True / offset=N 這組參數同樣適用於 morph(),因此這套自訂核框架也能產生一個其切割點由自訂統計量計算得出的二值閾值。
5.16.2. 核的版面配置¶
一個 3 乘 3 的核(size=1)是由九個數字組成的扁平串列,依照由左到右、由上到下的順序排列。若將該串列拆成三行 Python 程式碼來寫,這個慣例會讀起來相當自然:
sobel_x = [-1, 0, 1,
-2, 0, 2,
-1, 0, 1]
這是 Sobel-x 梯度運算子——任何應用程式都會最先用到的標準核,也是值得從頭到尾走過一遍的範例。其模式很直觀:左行為負權重,右行為正權重,中間行為零。各列的權重 -1, -2, -1(右側則為 1, 2, 1)在中間比在角落大,使得中間列對結果的影響力高於角落列。
當核掃過一條垂直邊緣時——也就是一列由左側暗轉為右側亮的像素——負權重會擷取暗的一側,正權重會擷取亮的一側。乘積總和會是一個很大的正數,濾波器便將其寫成一個明亮的輸出像素。一塊水平且亮度均勻的區域會產生零,因為每個正權重都會被一個位於相同數值像素上、大小相等的負權重所抵消。
執行此核:
img.morph(1, sobel_x, mul=0.25)
Sobel 核的總和為零——左側每個負權重都被右側一個相等的正權重抵消——因此自動除法不會除以任何東西,mul 是乘積總和上唯一的縮放。mul=0.25 能讓響應保持在範圍內:Sobel-x 從一塊 3 乘 3 區域能產生的最大絕對總和大約是 4 * 255 = 1020(八個亮像素,權重最高達 2),將其除以四後可讓極端情況落在 255,正好可被格式乾淨地裁切。
與之對應的 Sobel-y 核則藉由將同樣的權重模式旋轉 90 度來偵測水平邊緣:
sobel_y = [-1, -2, -1,
0, 0, 0,
1, 2, 1]
想偵測任意方向邊緣的應用程式,通常會同時執行兩個 Sobel 並結合其響應。
5.16.3. 對輸出進行偏移¶
add 是縮放故事的另一半。零總和核的響應是帶正負號的——在邊緣的一側為正,另一側為負——而當寫入無號像素時,負的那一半會被裁切為零。add=128 會將響應平移至以中灰為中心,因此負響應會以低於 128 的數值存留下來,正響應則落在其上方:邊緣響應或浮雕效果在兩個方向上都變得可見,代價是兩個方向各損失一半的範圍。
一個核預期搭配哪種 mul 與 add 的組合,是該核設計的一部分;標準核目錄 列出了每個常用核的正確設定。
5.16.4. 較大的核¶
本頁的所有內容都是以 3 乘 3 的核(size=1)來描述的,因為那是標準目錄所使用的尺寸,也因為在那個尺寸下,列優先的版面配置很容易用手寫出來。不過機制本身並未限制核必須是 3 乘 3。size=2 會執行一個 5 乘 5 的核,扁平串列中有二十五個項目;size=3 會執行一個 7 乘 7 的核,有四十九個項目;依此類推,直到應用程式願意付出代價的任何半徑為止。框架在任何奇數尺寸下都能處理扁平串列或巢狀列的版面配置。
選用較大核的理由,與在任何內建濾波器上選用較大鄰域的理由相同:更多的平均化、更廣的特徵偵測、對單一像素雜訊較不敏感。其成本隨半徑的平方成長——5 乘 5 的每像素工作量約為 3 乘 3 的 2.8 倍,7 乘 7 約為 5.4 倍——而這個倍數會直接從影格率中扣除。
實務上的做法是,使用標準目錄時維持在 size=1,只有在演算法需要較大鄰域時才選用較大的尺寸。邊緣偵測器很少能從超過 3 乘 3 的尺寸中獲益;平滑化濾波器有時可以;正確的尺寸取決於應用程式試圖強調或抑制的特徵的尺度。
5.16.5. 何時該選用 morph¶
對於日常的平滑化,mean()、gaussian() 與 bilateral() 更快也更乾淨。對於邊緣偵測,laplacian() 與 find_edges() 則是專門設計的工具。直接動用 morph() 的理由,是當應用程式需要一個內建濾波器未提供的特定卷積時——例如一個有方向性的 Sobel、一個自訂的邊緣樣板、一個針對管線後續會尋找的特定紋理而調校的核,或是經典影像處理數十年來累積下來的標準目錄中任何一個有用的核。任意核的完整彈性皆可使用;代價是應用程式必須自行負責選擇能產生其所想結果的核數值。