SSIM vs ResNet 餘弦距離:兩種跨年變化偵測方法的取捨
系列 B 第二篇:量化「兩個年份之間城市變了多少」有兩條主流路徑——SSIM 著重結構相似度,ResNet 著重語義變化。本篇拆解兩者各自的擅長領域,並以實際數據示範何時應同時採用兩個指標。
一、為何需要「跨年變化偵測」
前一篇介紹的三個指標(邊緣密度、建築覆蓋率、紋理熵)屬於單幀指標——每張影像獨立計算一個數值。
然而研究城市發展時,真正想回答的問題是「這個地點從 2018 到 2025 發生了多大的變化」。最直覺的做法是差分(metric_2025 - metric_2018),但此方式有一個根本限制:
兩張影像的 edge_density 同樣是 0.18,差值為零——但實際上整片區域可能已完全重建,只是新舊建築的邊緣密度恰好相同。
差分丟失了「像素級的對應關係」。需要的是直接比對兩張影像本身的方法。
主流有兩條路徑:
| 方法 | 偵測對象 | 計算成本 | 可解釋性 |
|---|---|---|---|
| SSIM | 結構相似度(像素對應) | 低(CPU 即可) | 高 |
| ResNet 餘弦距離 | 語義相似度(特徵對應) | 中(一次 forward pass) | 中 |
兩者並非替代關係,而是互補。
二、SSIM:結構相似度
直覺
SSIM(Structural Similarity Index)衡量兩張影像在亮度、對比度、結構三個維度的相似程度,輸出值域為 −1 至 1,值為 1 代表完全相同。
其設計初衷是取代 MSE / PSNR 評估影像壓縮品質——因為 MSE 過於機械,輕微平移即導致數值暴漲,但人眼幾乎察覺不出差異。SSIM 改採滑窗加結構統計,更貼近人類視覺感知。
公式
對兩張影像 x、y,以逐像素滑窗(通常為 11×11 高斯窗)計算:
SSIM(x, y) = [l(x,y)]^α · [c(x,y)]^β · [s(x,y)]^γ
l(x,y) = (2μ_x μ_y + C_1) / (μ_x² + μ_y² + C_1) # 亮度
c(x,y) = (2σ_x σ_y + C_2) / (σ_x² + σ_y² + C_2) # 對比度
s(x,y) = (σ_xy + C_3) / (σ_x σ_y + C_3) # 結構
C_1, C_2, C_3 為避免除零的穩定化常數,最終取整張影像的均值。
程式碼
# cv/cv_metrics.py:45-53
def calc_ssim(img1_gray: np.ndarray, img2_gray: np.ndarray) -> float:
"""結構相似度 SSIM,值越低代表兩年份變化越大。"""
h = min(img1_gray.shape[0], img2_gray.shape[0])
w = min(img1_gray.shape[1], img2_gray.shape[1])
a = cv2.resize(img1_gray, (w, h))
b = cv2.resize(img2_gray, (w, h))
score, _ = ssim(a, b, full=True)
return float(score)
兩個設計細節值得注意:
- 採用
skimage.metrics.structural_similarity,無需自行實作 - 比對前須統一影像尺寸。不同年份的 Mapbox 切片尺寸可能存在差異,省略此步驟將導致直接報錯
- 以灰階圖輸入——不關注顏色變化,僅比對結構

SSIM 的偵測範圍與局限
適合偵測:
- 建築物位移(同一建築在影像中的位置略有偏移)
- 大範圍空地轉變為建築(亮度、對比度、紋理全面改變)
- 道路重新鋪設(亮度顯著變化)
難以偵測:
- 季節差異(草地枯黃或翠綠 → SSIM 下降,但城市並未改變)
- 雲影、陰影等短期光照變化
- 語義變化但結構相似:舊社區拆除、新社區重建,若整體紋理未顯著改變,SSIM 可能仍維持在 0.7 以上
台中 8 年 SSIM 統計
| 統計項目 | 數值 |
|---|---|
| 平均值 | 0.6300 |
| 標準差 | 0.1202 |
| 最小值 | 0.3417 |
| 最大值 | 0.8516 |
平均 0.63 表示相鄰年份影像存在中等程度的結構差異。最小值 0.34 暗示特定地點在單一年度內發生顯著變化,對應重建或大規模工程事件。
三、ResNet 餘弦距離:語義相似度
直覺
將兩張影像分別輸入 ImageNet 預訓練的 ResNet,取倒數第二層(global average pooling 之後)的 2048 維特徵向量。兩個向量的餘弦相似度,反映兩張影像在 ResNet 視角下「是否屬於同一類場景」。
ResNet 在 ImageNet 上習得的特徵屬於語義層級——能夠區辨「都市」「郊區」「農田」「水體」等高層次概念,而非僅僅統計邊緣數量這類底層特徵。
公式
cos_sim = (f_1 · f_2) / (|f_1| · |f_2|)
cos_dist = 1 − cos_sim
距離為 0 代表語義相同;距離愈大代表語義變化愈顯著。
程式碼
# cv/cv_metrics.py:56-59
def calc_resnet_cosine_distance(feat1: np.ndarray, feat2: np.ndarray) -> float:
"""ResNet 特徵餘弦距離,值越大代表語義變化越大。"""
cos_sim = np.dot(feat1, feat2) / (np.linalg.norm(feat1) * np.linalg.norm(feat2) + 1e-8)
return float(1.0 - cos_sim)
特徵抽取流程(不進行訓練):
# src/aiutils.py:102-147(節錄)
class CityResNet:
def __init__(self):
self.model = models.resnet50(pretrained=True)
# 移除最後一層分類頭,保留 GAP 後的 2048 維特徵
self.feature_extractor = nn.Sequential(*list(self.model.children())[:-1])
self.model.eval()
self.preprocess = transforms.Compose([
transforms.Resize((256, 256)),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
def get_vector(self, img_path):
img = Image.open(img_path).convert('RGB')
tensor = self.preprocess(img).unsqueeze(0)
with torch.no_grad():
vector = self.feature_extractor(tensor).flatten().numpy()
return vector
三個工程細節
1. 採用 pretrained=True,不進行微調
ImageNet 預訓練特徵已具備相當的通用性。微調需要標註資料(例如:哪些座標屬於「繁榮」?此類標籤難以取得)。直接使用 frozen 特徵加餘弦距離,可跳過全部訓練流程。
2. 採用 ResNet50 而非 ResNet18
50 層的特徵維度(2048 維)是 18 層(512 維)的四倍,描述能力更強。城市衛星影像細節豐富,多花一點推理時間是值得的取捨。8 年 × 44 採樣點 = 352 次 forward pass,CPU 執行僅需數分鐘。
3. [:-1] 保留 GAP,去除 FC 層
ResNet 最後是「2048 維 → 1000 類分類」的全連接層。此處需要的是 2048 維的中間表示,而非分類結果,因此移除最後一層。

ResNet 的偵測範圍與局限
適合偵測:
- 語義巨變:空地轉變為高層建築群、農田轉變為商業區
- 場景類別更替:低密度住宅區轉變為高密度住宅區
- 跨季節穩定:草地枯黃或翠綠在 ResNet 視角下同屬「植被」,不易誤判
難以偵測:
- 同類細微變化:5 棟低層建築增至 6 棟,整體場景類別未變,距離值偏小
- 個別建築替換:拆除一棟、重建一棟,整體場景類別不變,難以捕捉
台中 8 年 ResNet 餘弦距離統計
| 統計項目 | 數值 |
|---|---|
| 平均值 | 0.0860 |
| 標準差 | 0.0619 |
| 最小值 | 0.0126 |
| 最大值 | 0.3524 |
平均 0.086 表示整體語義變化溫和——大多數採樣點的場景類別未發生改變。最大值 0.3524 對應某個從空地轉變為密集建築的地點。
四、互補性:兩個指標合併解讀
將 SSIM 與 ResNet 餘弦距離繪製為散佈圖,每個資料點代表某一座標的某對比較年份:
↑ ResNet 距離大(語義變化顯著)
│
類別 1 │ 類別 2
「重大改建」│ 「整片重建」
│
───────────┼─────────── → SSIM 小(結構變化顯著)
│
類別 3 │ 類別 4
「光照差異」│ 「微小調整」
│
↓ ResNet 距離小(語義穩定)
四個象限的解讀邏輯:
| 象限 | SSIM | ResNet 距離 | 解讀 |
|---|---|---|---|
| 類別 1 | 高(結構穩定) | 大(語義變化) | 重大改建——個別建築功能更替,整體佈局未動(如商辦轉住宅) |
| 類別 2 | 低(結構變化) | 大(語義變化) | 整片重建——拆遷重建、大規模開發 |
| 類別 3 | 低(結構變化) | 小(語義穩定) | 光照或季節差異——城市無實質變化,拍攝條件不同 |
| 類別 4 | 高(結構穩定) | 小(語義穩定) | 微小調整——無明顯變化 |
類別 2 是最值得關注的「真實重大變化」。單看 SSIM 易受類別 3 干擾(誤報);單看 ResNet 距離則可能漏掉類別 1(漏報)。兩者同時異常,才能以高精確率、高召回率識別真實大事件。

五、實作:兩個指標整合於同一批次流程
# cv/cv_metrics.py:114-167(節錄)
def build_change_metrics(raw_dir, year_pairs):
"""計算跨年份變化指標(SSIM + ResNet 餘弦距離)。"""
from src.aiutils import CityResNet
resnet = CityResNet()
rows = []
for y1, y2 in year_pairs:
dir1 = raw_dir / str(y1)
dir2 = raw_dir / str(y2)
files1 = {f.name: f for f in dir1.glob("*.png")}
files2 = {f.name: f for f in dir2.glob("*.png")}
common = sorted(set(files1) & set(files2))
for fname in common:
p1, p2 = files1[fname], files2[fname]
img1 = cv2.imread(str(p1), cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread(str(p2), cv2.IMREAD_GRAYSCALE)
if img1 is None or img2 is None:
continue
ssim_val = calc_ssim(img1, img2)
feat1 = resnet.get_vector(str(p1))
feat2 = resnet.get_vector(str(p2))
cos_dist = calc_resnet_cosine_distance(feat1, feat2)
# 從檔名解析座標(格式:<lon>_<lat>_*.png)
parts = fname.replace(".png", "").split("_")
lon, lat = float(parts[0]), float(parts[1])
rows.append({
"year_from": y1, "year_to": y2,
"lon": lon, "lat": lat,
"ssim": ssim_val,
"resnet_cosine_dist": cos_dist,
})
return pd.DataFrame(rows)
兩個指標共用同一輪檔案遍歷,僅額外增加 ResNet 一次 forward pass 的計算成本。
六、選用指引
| 使用場景 | 建議做法 |
|---|---|
| 資源有限、僅需單一指標 | SSIM——無需 GPU、無需安裝 PyTorch、可解釋性高 |
| 偵測場景類型變化(如農地都市化) | ResNet 餘弦距離——SSIM 對類別變化不敏感 |
| 進行大事件異常偵測 | 兩者皆計算,取兩者同時異常的採樣點 |
| 影像存在嚴重季節或光照差異 | 以 ResNet 為主,SSIM 雜訊過大 |
| 學術發表 | 兩者皆呈現,可增強審稿說服力 |
七、未來方向:城市場景專用 backbone
ResNet50 以 ImageNet 預訓練,主要習得「物體」特徵(狗、汽車、椅子)。對於城市俯瞰場景,理論上以 Places365 預訓練的 backbone 更為適配。
Places365 是 MIT 發布的 365 類場景分類資料集,類別涵蓋「downtown」「residential」「highway」「farm」等,非常契合城市衛星分析的需求。
替換方式相當簡單:
# 僅需修改一行
self.model = models.resnet50(weights=None)
self.model.load_state_dict(torch.hub.load_state_dict_from_url(
"http://places2.csail.mit.edu/models_places365/resnet50_places365.pth.tar"
))
此替換留待後續實驗驗證。就目前資料而言,ImageNet ResNet 已足以識別「空地轉變為建築群」這類顯著的語義變化。
八、本篇小結
| 指標 | 一句話描述 | 適合偵測的場景 |
|---|---|---|
| SSIM | 像素級結構相似度 | 結構變化、低運算成本 |
| ResNet 餘弦距離 | 語義級場景相似度 | 場景類型變化、對光照差異穩健 |
兩個指標合併使用方為最佳實踐。台中數據顯示:SSIM 平均 0.63、ResNet 距離平均 0.086,兩者 Pearson 相關係數約 −0.4——存在相關但不重疊,正是互補性的量化體現。
下一篇預告:跳至系列 C 學術深度系列,Moran's I 入門——城市發展是隨機分佈還是空間聚集?
程式碼:cv/cv_metrics.py:45-59、src/aiutils.py:102-147
依賴套件:scikit-image、torch、torchvision
運算成本:352 對影像約 5 分鐘(CPU),不足 1 分鐘(GPU)