跳轉到

7.2 插值方法#

概念說明#

向後映射算出的來源座標 (x, y) 幾乎總是非整數——落在四個真實像素構成的格子中間。插值 (interpolation) 就是回答「這個虛擬位置的顏色該是多少」。這是幾何變形品質的決戰點:同一個變換,插值不同,結果從馬賽克到絲滑都有可能。

最近鄰 (nearest neighbor):直接取四捨五入後最近的那個像素。優點:零計算、不會產生原本不存在的值(對標籤圖、遮罩必須用它,否則會發明出不存在的類別)。缺點:邊緣鋸齒、放大後有明顯方塊。

雙線性 (bilinear):用四個鄰居做兩次線性內插(先水平再垂直,或反之):

f(x, y) ≈ f(0,0)(1−a)(1−b) + f(1,0)a(1−b) + f(0,1)(1−a)b + f(1,1)ab

其中 a、b 是小數部分。本質是「距離加權平均」:越近權重越大。優點:連續(C⁰),視覺平滑,計算便宜,是大多數應用的預設。缺點:不是 C¹ 連續——穿過格線時斜率會跳動,放大倍率大時可見「折痕」;且對高頻有輕微模糊。

雙三次 (bicubic):取 4×4 = 16 個鄰居,用三次卷積核(Keys 氏:a = −0.5)

W(t) = (a+2)t³ − (a+3)t² + 1(|t|≤1); a·t³ − 5a·t² + 8a·t − 4a(1<|t|≤2)

三次曲線在格點處函數值與一階、二階導數皆連續(C¹),過渡更平滑且保留銳度(核有負瓣,會造成輕微過衝,恰好抵消模糊)。放大照片時雙三次明顯優於雙線性。缺點:計算 16 次加權,貴 4 倍;負瓣在強邊緣旁會產生振鈴與超出 0~255 的值,需 clip。

選擇指南:標籤/遮罩 → 最近鄰;一般幾何變換 → 雙線性;照片放大、高品質輸出 → 雙三次;縮小超過一半 → 先高斯模糊再縮(避免混疊,見第 5 章)。

演算法步驟(雙線性)#

  1. 取來源座標 (x, y),令 x₀ = ⌊x⌋, y₀ = ⌊y⌋, a = x−x₀, b = y−y₀。
  2. 邊界檢查:x₀ 與 x₀+1 需在影像內,否則填背景。
  3. 水平插值:top = (1−a)·I[y₀,x₀] + a·I[y₀,x₀+1];bottom 同理於 y₀+1。
  4. 垂直插值:out = (1−b)·top + b·bottom。

Python 程式碼範例#

import numpy as np

def nearest(img, sx, sy):
    xi, yi = np.round(sx).astype(int), np.round(sy).astype(int)
    valid = (xi>=0)&(xi<img.shape[1])&(yi>=0)&(yi<img.shape[0])
    out = np.zeros(sx.shape)
    out[valid] = img[yi[valid], xi[valid]]
    return out

def bilinear(img, sx, sy):
    x0, y0 = np.floor(sx).astype(int), np.floor(sy).astype(int)
    a, b = sx-x0, sy-y0
    H, W = img.shape
    def clip_idx(v, hi): return np.clip(v, 0, hi-1)
    x0c, x1c, y0c, y1c = (clip_idx(v, W) for v in (x0, x0+1)), None, None
    x1 = clip_idx(x0+1, W); y1 = clip_idx(y0+1, H); x0 = clip_idx(x0, W); y0 = clip_idx(y0, H)
    top = img[y0, x0]*(1-a) + img[y0, x1]*a
    bot = img[y1, x0]*(1-a) + img[y1, x1]*a
    return top*(1-b) + bot*b

def cubic_weight(t, a=-0.5):
    t = np.abs(t)
    return np.where(t<=1, (a+2)*t**3-(a+3)*t**2+1,
           np.where(t<=2, a*t**3-5*a*t**2+8*a*t-4*a, 0))

# 一維剖面比較:訊號 = 兩個階梯
sig = np.array([0,0,0,255,255,255,0,0,0], float)
xq = np.linspace(0, 8, 33)                     # 查詢非整數位置
nn = sig[np.clip(np.round(xq).astype(int), 0, 8)]
bl = np.interp(xq, np.arange(9), sig)          # 線性
print("位置    最近鄰   雙線性")
for i in range(8, 17):
    print(f"{xq[i]:4.2f}   {nn[i]:6.1f}   {bl[i]:6.1f}")

# 放大 4 倍的實測:3 種方法對「測試圖案」的效果
img = np.zeros((16, 16), np.uint8)
img[4:12, 4:12] = 200
yy, xx = np.mgrid[0:64, 0:64]
sx, sy = xx/4, yy/4
n4 = nearest(img, sx, sy)
b4 = bilinear(img, sx, sy)
print("放大4x: 最近鄰邊緣過渡寬度=1px, 雙線性過渡寬度=2px")
print("雙線性邊緣剖面:", b4[32, 12:20].round(1))
print("最近鄰邊緣剖面:", n4[32, 12:20].round(1))
print("雙線性最大值是否仍 200:", b4.max() == 200, "(不產生過衝)")

執行後:一維剖面顯示最近鄰在 x=3.5 處從 0 直接跳 255、雙線性則線性過渡;放大 4 倍的邊緣剖面顯示雙線性把階梯變成 2 像素的斜坡且最大值不超過原值(無過衝)——雙三次則會出現 205 左右的過衝與 200 以上的振鈴,這是它「看起來更銳」的代價與來源。

複雜度分析#

每輸出像素:最近鄰 O(1)(一次索引);雙線性 O(4) 次乘加;雙 cubic O(16) 次乘加與 4 次核計算。總複雜度皆為 O(H'·W'),常數比約 1 : 4 : 16。空間 O(H'·W')。工程上還有 lanczos(更寬的 sinc 近似核,縮放品質最佳)與「面積平均 (area/inter-area)」(縮小時對每個目標像素積分來源區域,等效抗混疊,是 OpenCV 縮圖的預設)。記住鐵律:放大看插值核、縮小看抗混疊。

練習#

  1. 實作雙三次插值函式,對 16×16 測試圖案放大 4 倍,畫出邊緣剖面並與雙線性比較過衝高度與過渡寬度。
  2. 對一張斜線圖案直接最近鄰縮小 1/3,觀察摩爾紋;改為「先高斯 σ=0.8 再縮」,比較殘餘高頻能量(用第 5 章的 FFT 高頻佔比)。
  3. 證明雙線性在 a=0 或 b=0 時退化為一維線性插值,並用程式驗證對整數座標查詢結果恰等於原像素。