Day 27 的成績單:召回 5/8、框的精確率 12.5%、5 張沒有機場的圖全部誤判。結論是「模型沒有學會什麼是機場」,但「沒學會」的原因有很多種可能。今天把它拆成三個問題,每個問題做一個實驗:
門檻設太低?把信心分數調高能不能把誤判濾掉?色彩不一樣?EOX 是真彩色,訓練圖是 B1/B2/B3,把色彩對齊能不能救回來?是什麼?最直覺的想法:誤判的框應該分數比較低吧?把門檻從 0.25 拉高就好了。
把 Day 27 的 80 個框依照命中與否分開看分數:
命中框 10 個,分數 0.262 ~ 0.674,中位數 0.350
誤判框 70 個,分數 0.257 ~ 0.684,中位數 0.421
誤判框的分數中位數比命中框還高。分數最高的那個框(0.684)是誤判。
掃一次門檻:
| 門檻 | 剩幾框 | 命中 | 精確率 |
|---|---|---|---|
| 0.25 | 80 | 10 | 12.5% |
| 0.30 | 66 | 7 | 10.6% |
| 0.40 | 42 | 3 | 7.1% |
| 0.50 | 26 | 1 | 3.8% |
| 0.60 | 7 | 1 | 14.3% |
門檻越高,精確率反而越低(0.6 那格只剩 7 個框,樣本太少不能算數)。
這代表模型對「港口碼頭是機場」的把握,比「機場是機場」還大。門檻不是解藥,信心分數跟正確與否根本沒有關係。
Day 12 講評估指標的時候說過,mAP 是在不同信心門檻下積分 precision-recall 曲線。這個前提是分數越高越可能是對的,今天這個前提不成立,所以 Day 13 那些 mAP 數字在新場景上沒有任何意義。
Day 23 換成物理正確的真彩色時,偵測從 1 個框崩成 13 個框。Day 27 的 EOX 影像也是真彩色,會不會只是色彩分布不同造成的?
這叫 domain shift(領域偏移):訓練資料跟測試資料的分布不一樣。最簡單的對策是直方圖對齊(histogram matching),讓測試影像每個通道的亮度分布跟訓練影像一模一樣。
概念是對齊累積分布函數(CDF):
def match_histogram(src, ref):
out = np.empty_like(src)
for c in range(3):
s_hist = np.bincount(src[..., c].ravel(), minlength=256).cumsum()
r_hist = np.bincount(ref[..., c].ravel(), minlength=256).cumsum()
s_cdf, r_cdf = s_hist / s_hist[-1], r_hist / r_hist[-1]
lut = np.searchsorted(r_cdf, s_cdf).clip(0, 255).astype(np.uint8)
out[..., c] = lut[src[..., c]]
return out
假設 src 的灰階 120 是全圖第 60 百分位,就去 ref 找第 60 百分位是多少,比如 45,那 src 所有的 120 都換成 45。三個通道各做一次,做成 256 格的查表。
這個操作不會改變影像的結構(邊緣、形狀都還在),只改變亮度與色彩的分布。
參考影像用原始 tif 的 B1/B2/B3 + max 拉伸,也就是訓練圖的長相。

左邊是 EOX 原圖,中間是對齊後,右邊是訓練圖的長相。訓練圖非常暗,因為 max 拉伸讓雲決定了整張圖的曝光(Day 23 講過),而且那層藍綠色的霧是 B1 海岸氣膠波段造成的。對齊之後的 EOX 影像跟訓練圖的色調很接近了。
scene 機場 raw boxes hit fp best_inside 結果
tpe TPE 2 2 2 0 100.0% 找到
tsa TSA 2 1 0 1 37.2% 漏判
rmq RMQ 2 2 1 1 100.0% 找到
cyi CYI 0 0 0 0 0.0% 漏判
tnn TNN 0 0 0 0 0.0% 漏判
khh KHH 1 1 1 0 57.4% 找到
hun HUN 31 4 0 4 5.6% 漏判
ttt TTT 4 3 0 3 0.0% 漏判
neg_banqiao - 0 0 0 0 0.0% 乾淨
neg_taipeiport - 5 2 0 2 0.0% 誤判
neg_taichungport - 6 4 0 4 0.0% 誤判
neg_mailiao - 4 4 0 4 0.0% 誤判
neg_changhua - 5 4 0 4 0.0% 誤判
機場召回率 3/8
框的精確率 4/27 = 14.8%
負樣本誤判 4/5 張有框,共 14 個

跟 Day 27 並排比:
| 原始 RGB | 色彩對齊 | |
|---|---|---|
| 框的總數 | 80 | 27 |
| 命中 | 10 | 4 |
| 精確率 | 12.5% | 14.8% |
| 機場召回 | 5/8 | 3/8 |
| 負樣本有框 | 5/5 | 4/5 |
色彩對齊讓模型安靜很多,框少了三分之二,但命中也少了一半多。精確率只從 12.5% 動到 14.8%,召回反而掉了。
桃園(TPE)變成 2 框 2 中、零誤判,看起來很漂亮,但這是它在訓練時看過的機場,換上它熟悉的色調當然表現好。真正的新機場裡,嘉義和台南一個框都沒有,花蓮則冒出 4 個大到離譜的誤判框。
結論:色彩有影響,但不是主因。把顏色調回訓練時的樣子,模型也沒有變成一個會找機場的模型,只是變得比較不敢開火。
前兩個實驗都在看模型的輸出,這一個要回頭看模型的輸入:訓練標註。
Day 24 量過標註的長寬比,但那是在像素空間量的。現在有 OSM 的機場圍籬,可以問一個更直接的問題:標註框的東西,有多少真的是機場?
問題是訓練圖是 Roboflow 裁切、旋轉過的 640x640 JPG,沒有地理座標。要把標註換成經緯度,得先知道每張訓練圖是從原始 tif 的哪裡裁下來的。
這是影像對位(image registration)的經典問題。用 SIFT 在訓練圖和原始 tif 上各找特徵點,配對之後算出變換矩陣:
kp, desc = sift.detectAndCompute(train_gray, None)
good = [a for a, b in matcher.knnMatch(desc, ref_desc, k=2)
if a.distance < 0.75 * b.distance] # Lowe 的比值檢定
m, inliers = cv2.estimateAffinePartial2D(src, dst, method=cv2.RANSAC,
ransacReprojThreshold=3.0)
estimateAffinePartial2D 算的是相似變換:平移 + 旋轉 + 等比縮放,4 個自由度。Roboflow 的增強只有裁切、旋轉、翻轉,用相似變換就夠了,比 8 個自由度的 homography 穩定。
驗收的方法是看縮放比:如果訓練圖真的是從這張 tif 裁出來的,縮放比應該剛好是 1。
split source inl scale
test 461127c6 17 1.000
train 58c5f2b1 18 1.002
train 886495b2 40 1.000
train 95f2ba92 84 1.000
train c4ac24fa 110 1.000
valid ed7c4282 42 1.001
...
對位成功 8/10 張唯一來源圖
縮放比 1.000。Day 23 是用眼睛看出「10 張來源圖是同一個場景」,現在有數學上的證據了:每張訓練圖都是從 som_san_air.tif 原尺寸直接裁下來的,連縮放都沒有。
(有 2 張對位失敗,它們的三個增強版本都被旋轉得太厲害,配對點不夠。)
有了變換矩陣,標註多邊形就能一路換到經緯度:
訓練圖像素 --(SIFT 相似變換)--> tif 像素 --(仿射矩陣)--> UTM --(Day 24 逆投影)--> 經緯度
tif_px = vals @ m[:, :2].T + m[:, 2]
lon, lat = pixel_to_lonlat(georef, tif_px[:, 0], tif_px[:, 1])
然後用 Day 25 的 score_feature 對 OSM 桃園機場圍籬打分數:
split source inl scale km² 在機場內 蓋到機場
test 461127c6 17 1.000 0.80 100.0% 7.2%
train 58c5f2b1 18 1.002 0.43 100.0% 3.8%
train 62356747 9 0.999 1.12 92.8% 9.3%
train 886495b2 40 1.000 0.14 100.0% 1.2%
train 886495b2 40 1.000 0.52 99.7% 4.6%
train 95f2ba92 84 1.000 1.72 49.6% 7.6%
train c4117192 9 1.000 0.60 100.0% 5.3%
train c4ac24fa 110 1.000 0.32 100.0% 2.8%
valid ed7c4282 42 1.001 0.45 100.0% 4.0%
9 個標註多邊形,平均 0.68 km²;OSM 桃園機場圍籬 11.20 km²

這張圖是今天最重要的一張。藍色是 OSM 機場圍籬,綠色是過半落在機場內的標註,紅色是沒有的。
標註大部分是對的,但每一個都只框了機場的一小塊。9 個標註裡 8 個有 90% 以上落在圍籬內,這部分沒問題。但平均面積只有 0.68 km²,整座機場有 11.2 km²,每個標註只蓋到機場的 1% ~ 9%。
每張圖框的部位都不一樣。有的框北跑道的一段,有的框航廈東北角,有的框貨運區,有的框南側一塊空地。
對模型來說,這 9 個標註傳達的「機場」概念是:一塊大約 0.7 km² 的、邊界不規則的、在這座機場某處的區域。它從來沒看過一個框住整座機場的標註。
紅色那一個,就是 Day 24 那個框的來源。95f2ba92 是面積最大的標註(1.72 km²),而且只有 49.6% 在圍籬內,另一半畫到了機場西北側的農地上。
回頭看 Day 25:模型在原始 tif 上唯一的那個框,在圍籬內的比例是 45.8%,位置也在機場西北側。把 Day 24 的框跟每個標註算重疊:
source 標註有多少在框內
95f2ba92 99.5%
886495b2 98.0%
58c5f2b1 90.9%
c4117192 50.5%
62356747 41.6%
其餘 4 個 0.0%
Day 24 的框幾乎完整包住了 95f2ba92,同時也包住了北側另外兩個標註。模型畫出來的,就像是把機場北半邊那幾個標註聯集起來的外框,連 95f2ba92 畫出圍籬的那一半一起包了進去。南側的 4 個標註它完全沒碰。
模型在推論時看到的就是它訓練時看過的那塊像素,它交出的答案也就是那幾個標註的拼貼。
| 假設 | 實驗 | 結果 |
|---|---|---|
| 門檻太低 | 掃門檻 | ✗ 誤判框分數比命中框還高 |
| 色彩不同 | 直方圖對齊 | △ 框變少,但精確率幾乎沒變、召回下降 |
| 標註教錯 | 標註換成經緯度對 OSM | ✓ 標註只框機場碎片、位置不一致、有一個畫出圍籬外 |
把這幾天的發現串起來,Day 27 那張爛成績單的原因是:
airport,但每個標註只框機場的一個碎片,而且每張框的部位不同(今天)前三點都是資料問題,第四點是前處理問題。Day 17 到 Day 22 我花了六天在模型這一側(OBB、SAHI、切片訓練、碎框合併、混合資料集),但真正的瓶頸在更前面。
今天的工具其實已經指出了修法:
lonlat_to_pixel 就能投影到任何有地理座標的影像上,變成 YOLO 的標註。不用再手動畫fetch_s2_scenes.py 可以抓任何地方的 Sentinel-2 cloudless而且全世界的機場都在 OSM 上,用台灣以外的機場訓練、用台灣的機場測試,資料洩漏的問題就徹底解決了。
這是一個完整的重新訓練計畫,不是三天做得完的事。這個系列剩下的時間,我決定不做。理由是:Day 22 就是在沒搞清楚資料之前就急著改模型,結果連續兩天往錯的方向優化(Day 24)。這次寧可把問題記錄清楚、把工具留好,也不要在最後幾天倉促訓練一個新模型,然後再看到一組不知道在量什麼的數字。
今天用三個實驗拆解 Day 27 的誤判:
信心分數跟正確與否無關
每個標註只框機場的 1% ~ 9%,而面積最大的那個有一半畫在圍籬外,Day 24 那個偏移的框包住了它的 99.5%,正是北半邊幾個標註的拼貼Day 24 我說「一個座標系統就把三天的自我感覺良好戳破了」。今天更進一步:同一個座標系統,也能用來稽核訓練資料。只要標註能換成經緯度,就能跟外部的地面真相比對,這是純像素空間做不到的。
明天要把 Day 23 到今天散在各個腳本裡的東西,包成一個命令列工具。這樣就算模型要重訓,整條 抓圖 → 偵測 → 評分 → 地圖 → 資料庫 的流程也不用再重寫一次,那我們明天見。