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 章)。
演算法步驟(雙線性)#
- 取來源座標 (x, y),令 x₀ = ⌊x⌋, y₀ = ⌊y⌋, a = x−x₀, b = y−y₀。
- 邊界檢查:x₀ 與 x₀+1 需在影像內,否則填背景。
- 水平插值:top = (1−a)·I[y₀,x₀] + a·I[y₀,x₀+1];bottom 同理於 y₀+1。
- 垂直插值: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 縮圖的預設)。記住鐵律:放大看插值核、縮小看抗混疊。
練習#
- 實作雙三次插值函式,對 16×16 測試圖案放大 4 倍,畫出邊緣剖面並與雙線性比較過衝高度與過渡寬度。
- 對一張斜線圖案直接最近鄰縮小 1/3,觀察摩爾紋;改為「先高斯 σ=0.8 再縮」,比較殘餘高頻能量(用第 5 章的 FFT 高頻佔比)。
- 證明雙線性在 a=0 或 b=0 時退化為一維線性插值,並用程式驗證對整數座標查詢結果恰等於原像素。