跳轉到

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)。

演算法步驟#

  1. 取得 4 組對應點:來源影像上的四角 src,目標矩形四角 dst。
  2. 建立 8×9 線性系統 A,用 SVD 解出 h(單應矩陣,正規化 h33 = 1)。
  3. 計算逆矩陣 H⁻¹ 供向後映射。
  4. 對目標影像每個像素:逆映射到來源座標(含齊次除法),插值取色。

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| > ε。

練習#

  1. 用 4 組對應點解單應後,加入第 5 組有雜訊的對應點,改用最小平方法重解,量化誤差對 H 的影響。
  2. 對一張手拍文件的縮圖(或用程式模擬的梯形文字影像)做完整校正流程,輸出校正前後的「角點內角」以驗證 90°。
  3. 證明兩個單應矩陣的乘積仍是單應矩陣,並用程式驗證「先轉梯形再校正回來」的複合矩陣近似單位矩陣(誤差來自哪裡?)。