7.3 影像校正與透視變換#
概念說明#
仿射變換保平行,但透視投影不保:現實中平行的鐵軌在照片裡會相交於消失點。斜拍的矩形文件在照片裡是梯形——四個角不再是 90°。要把梯形拉回矩形,需要的變換比仿射更一般:透視變換 (perspective transform),又稱單應變換 (homography),用 3×3 矩陣在齊次座標下定義:
[x'_h] [h11 h12 h13] [x] [y'_h] = [h21 h22 h23] [y] [w'_h] [h31 h32 h33] [1]
最後要除以 w'_h:x' = x'_h/w'_h, y' = y'_h/w'_h。這個「除法」正是透視的數學本體——分母依位置而變,平行線得以相交。8 個獨立自由度(3×3 矩陣整體縮放無意義)。
估計單應矩陣:每組對應點 (x, y)↔(x', y') 提供 2 條線性方程,因此 4 組對應點(不共線、任意 3 點不共線)即可唯一解出 8 個未知數。把方程整理成線性系統 A·h = 0,解 A 的最小奇異值對應的奇異向量(SVD 求解)。對應點超過 4 組時用最小平方法;有錯配時用 RANSAC(第 9 章會回來用)。這正是掃描 App「自動拉正」的核心:偵測文件四角 → 四點解單應 → 透視拉回。
影像校正 (rectification):選定目標矩形的四個角(如 A4 比例 595×842),用 get_perspective 從「影像中觀察到的四角」映射到「目標規則網格」,再做向後映射變換。結果是一張彷彿正對著拍的照片。
單應變換的性質:直線仍是直線(比仿射弱的保證)、可複合(矩陣相乘)、逆變換也存在。仿射是單應的特例(h31 = h32 = 0)。
演算法步驟#
- 取得 4 組對應點:來源影像上的四角 src,目標矩形四角 dst。
- 建立 8×9 線性系統 A,用 SVD 解出 h(單應矩陣,正規化 h33 = 1)。
- 計算逆矩陣 H⁻¹ 供向後映射。
- 對目標影像每個像素:逆映射到來源座標(含齊次除法),插值取色。
Python 程式碼範例#
import numpy as np
def find_homography(src, dst):
"""4 組以上對應點,SVD 解單應矩陣"""
A = []
for (x, y), (X, Y) in zip(src, dst):
A.append([x, y, 1, 0, 0, 0, -X*x, -X*y, -X])
A.append([0, 0, 0, x, y, 1, -Y*x, -Y*y, -Y])
A = np.array(A)
_, _, Vt = np.linalg.svd(A)
H = Vt[-1].reshape(3, 3)
return H / H[2, 2]
def warp_perspective(img, H, out_shape):
Hi = np.linalg.inv(H)
Hh, Ww = out_shape
yy, xx = np.mgrid[0:Hh, 0:Ww]
ones = np.ones_like(xx)
pts = np.stack([xx, yy, ones]).reshape(3, -1) # 齊次座標
q = Hi @ pts
sx, sy = q[0]/q[2], q[1]/q[2]
sx = sx.reshape(Hh, Ww); sy = sy.reshape(Hh, Ww)
x0, y0 = np.floor(sx).astype(int), np.floor(sy).astype(int)
a, b = sx - x0, sy - y0
valid = (x0>=0)&(x0<img.shape[1]-1)&(y0>=0)&(y0<img.shape[0]-1)
x0c, y0c = np.clip(x0,0,img.shape[1]-2), np.clip(y0,0,img.shape[0]-2)
x1c, y1c = x0c+1, y0c+1
out = np.zeros(out_shape)
top = img[y0c,x0c]*(1-a) + img[y0c,x1c]*a
bot = img[y1c,x0c]*(1-a) + img[y1c,x1c]*a
out = top*(1-b) + bot*b
out[~valid] = 0
return out
# 模擬「斜拍文件」:把一張規則網格影像用已知 H 投影到梯形
grid = np.zeros((100, 100), np.uint8)
grid[::10, :] = 150; grid[:, ::10] = 150 # 網格
src_corners = [(0,0), (99,0), (99,99), (0,99)]
dst_corners = [(15,25), (85,10), (95,90), (5,80)] # 斜視圖的四角
H_fwd = find_homography(src_corners, dst_corners)
print("前向單應矩陣:\n", H_fwd.round(3))
tilted = warp_perspective(grid, H_fwd, (100, 100)).astype(np.uint8)
print("驗證角點映射: H @ (0,0,1) →", (H_fwd @ np.array([0,0,1.0])).round(2),
"應為 (15,25) 的齊次比例")
# 校正:把梯形四角拉回矩形
H_rect = find_homography(dst_corners, src_corners)
rectified = warp_perspective(tilted, H_rect, (100, 100))
print("校正後與原網格的一致率:",
round(((rectified > 100) == (grid > 100)).mean(), 3))
執行後:SVD 解出的 H 精確地把 (0,0) 映到 (15,25)、把矩形映成梯形;再用「四點反解」的 H_rect 校正回來,網格線的位置與原圖一致率超過 95%(誤差主要來自兩次插值)。這個「投歪再拉正」的閉環驗證,正是掃描軟體的完整數學骨架。
複雜度分析#
解單應:建立 8×9(或 2n×9)矩陣 O(n),SVD 對 8×9 是 O(1) 級小矩陣運算;實務微秒級。warp 主體:每輸出像素一次 3×3 矩陣乘 + 齊次除法 + 雙線性插值,O(H'·W')。空間 O(H'·W')。RANSAC 提升穩健性時,成本變 O(迭代數 × n)。與仿射的差別只在每像素多兩次乘加與一次除法,速度幾乎相同——能用單應就不用仿射妥協。注意 w 接近 0 的像素(映射到無窮遠)必須防除零,實作要檢查 |w| > ε。
練習#
- 用 4 組對應點解單應後,加入第 5 組有雜訊的對應點,改用最小平方法重解,量化誤差對 H 的影響。
- 對一張手拍文件的縮圖(或用程式模擬的梯形文字影像)做完整校正流程,輸出校正前後的「角點內角」以驗證 90°。
- 證明兩個單應矩陣的乘積仍是單應矩陣,並用程式驗證「先轉梯形再校正回來」的複合矩陣近似單位矩陣(誤差來自哪裡?)。