跳轉到

6.3 壓縮品質評估PSNR與SSIM#

概念說明#

有損壓縮必須回答:失真了多少? 我們需要客觀、可重複、可比較的品質指標。

MSE (Mean Squared Error):逐像素平方誤差的平均。PSNR (Peak Signal-to-Noise Ratio) 把它轉成對數尺度:

PSNR = 10 · log₁₀(255² / MSE),單位 dB

直觀:訊號(影像)對「雜訊」(誤差)的比值,8-bit 影像理論上限無窮大(MSE=0 時定義為 ∞),實務上 30 dB 起可看、40 dB 算好、>45 dB 幾乎不可分辨。PSNR 的優點:計算簡單、數學性質好(與 L2 範數直接掛鉤)、易於最佳化。缺點:MSE 對所有像素一視同仁,但人眼不是——我們對結構錯位極敏感、對均勻區域的小亮度漂移幾乎無感,對局部對比的改變很敏感。兩張 PSNR 相同的影像,視覺品質可能天差地遠。

SSIM (Structural Similarity, 2004) 從人眼視覺出發,認為「結構」是影像品質的關鍵。它在局部視窗(通常 11×11 高斯加權)比較三個成分:

  • 亮度 l = (2μₓμ_y + C₁)/(μₓ² + μ_y² + C₁)
  • 對比 c = (2σₓσ_y + C₂)/(σₓ² + σ_y² + C₂)
  • 結構 s = (σ_xy + C₂/2)/(σₓσ_y + C₂/2)

SSIM = l · c · s,全域值取所有視窗的平均

μ 是局部平均(亮度)、σ 是局部標準差(對比)、σ_xy 是協方差(結構相似度,即線性相關)。SSIM ∈ [−1, 1],1 表示完全相同;實務上 >0.95 通常肉眼難辨,<0.8 就明顯劣化。C₁、C₂ 是避免除零的小常數。

使用建議:論文與工程報告習慣兩者並報。PSNR 適合監控訓練與快速迴歸測試;SSIM 更貼近主觀感受,適合最終品質判斷。此外還有 MS-SSIM(多尺度版,對縮放穩健)、LPIPS(用深度特徵衡量感知距離,見第 10 章)等後繼者。指標終究是代理——最嚴謹的評估仍需人眼主觀實驗 (MOS)。

演算法步驟#

PSNR:1. 逐像素平方差 → 2. 平均得 MSE → 3. 代公式。

SSIM: 1. 對 x、y 各做高斯濾波得局部 μₓ、μ_y、σₓ²、σ_y²、σ_xy。 2. 逐像素代入 l·c·s 得 SSIM 圖。 3. 平均得總分。SSIM 圖本身很有價值——它標示出失真最嚴重的區域。

Python 程式碼範例#

import numpy as np

def psnr(a, b):
    mse = np.mean((a.astype(np.float64) - b.astype(np.float64))**2)
    return float("inf") if mse == 0 else 10*np.log10(255**2/mse)

def ssim(x, y, k=11, sigma=1.5):
    C1, C2 = (0.01*255)**2, (0.03*255)**2
    def g(img):
        r = k//2; xg = np.arange(k)-r
        w = np.exp(-xg**2/(2*sigma**2)); w /= w.sum()
        t = np.apply_along_axis(lambda row: np.convolve(row, w, "same"), 1, img)
        return np.apply_along_axis(lambda col: np.convolve(col, w, "same"), 0, t)
    x, y = x.astype(np.float64), y.astype(np.float64)
    mx, my = g(x), g(y)
    sxx, syy, sxy = g(x*x)-mx*mx, g(y*y)-my*my, g(x*y)-mx*my
    l = (2*mx*my + C1) / (mx*mx + my*my + C1)
    c = (2*sxx*syy + C2) / (sxx + syy + C2)
    s = (sxy + C2/2) / (np.sqrt(sxx*syy) + C2/2)
    return (l*c*s).mean()

rng = np.random.default_rng(12)
ref = (np.add.outer(np.arange(128), np.arange(128)) * 1.5).clip(0,255).astype(np.uint8)

# 失真 A:亮度漂移(全域 +10)
# 失真 B:高斯雜訊 σ=10
# 失真 C:局部對比反轉的一小塊
A = np.clip(ref.astype(int)+10, 0, 255).astype(np.uint8)
B = np.clip(ref + rng.normal(0, 10, ref.shape), 0, 255).astype(np.uint8)
C = ref.copy(); C[60:70, 60:70] = 255 - C[60:70, 60:70]

for name, t in [("亮度漂移", A), ("雜訊", B), ("局部破壞", C)]:
    print(f"{name}: PSNR={psnr(ref,t):5.1f} dB  SSIM={ssim(ref,t):.4f}")

執行後可看到指標的「性格」差異:亮度漂移的 MSE 小(每點只差 10)但 SSIM 給中等分數,因為結構沒壞;雜訊的 PSNR 與 SSIM 都下降;局部 100 像素的完全破壞對 PSNR 幾乎無感(被 16384 個像素稀釋),但 SSIM 圖會在該處亮起紅燈、平均分也掉得更多——因為 SSIM 是局部窗口計算,天然對「集中的結構破壞」敏感。這正是「PSNR 與 SSIM 並報」的理由。

複雜度分析#

PSNR:O(N),一次掃描。SSIM:主要成本是五次高斯濾波(μₓ、μ_y、σₓ²、σ_y²、σ_xy 各一次),可分離核心下 O(5·N·k),k=11 時約 55 次乘加/像素;用積分圖或 FFT 可再加速。空間 O(N)(SSIM 圖)。實務提醒:SSIM 對參數敏感(視窗大小、σ、是否下取樣),比較數字必須在相同參數下;跨文獻比較時需註明實作版本。PSNR 則無參數問題,適合作為可重複的迴歸指標。

練習#

  1. 產生兩張與參考圖 MSE 完全相同的失真影像(一張全域亮度漂移、一張半幅對比壓縮),證明它們 PSNR 相同但 SSIM 不同。
  2. 修改 ssim() 讓它回傳 SSIM 圖,對 6.2 節 QF=10 的 JPEG 還原結果畫出 SSIM 圖的行剖面,確認最低值出現在 8×8 區塊邊界。
  3. 查閱 MS-SSIM 的定義(多尺度平均),實作兩層版本並說明它為何對「縮放過的失真」更穩健。