從 Day 10 第一次訓練到現在,我們的模型只看過一張圖。Day 23 發現訓練集的 10 張來源圖全是桃園同一個場景,Day 26 寫好的資料庫查詢也只有一張圖可以問。今天要做的事情很簡單,也是這個系列早就該做的事:拿模型沒看過的地方去測它。
原本的計畫是用 Copernicus Data Space 下載 Sentinel-2 原始影像。但實際評估了一下流程:
為了測 8 座機場,要下載好幾 GB 的資料。
Day 25 做地圖的時候,我放了一個底圖叫 Sentinel-2 cloudless,是 EOX 這家公司把一整年的 Sentinel-2 影像去雲之後拼成的全球鑲嵌圖:
| Copernicus 原檔 | EOX Sentinel-2 cloudless | |
|---|---|---|
| 感測器 | Sentinel-2 | Sentinel-2 |
| 解析度 | 10 m | 10 m |
| 波段 | 13 個 | RGB(B4/B3/B2)3 個 |
| 雲 | 看運氣 | 已經去掉了 |
| 帳號 | 要 | 不用 |
| 格式 | 整個 tile | 圖磚,要多少抓多少 |
感測器一樣、解析度一樣,而且 Day 25 寫好的 fetch_mosaic 本來就能抓它的圖磚。代價是只有 RGB 三個波段,沒有我們訓練時用的 B1。這件事等一下會直接撞上。
授權部分,s2cloudless 2020 版是 CC BY-NC-SA 4.0,非商業使用要標註來源:「Sentinel-2 cloudless by EOX IT Services GmbH (Contains modified Copernicus Sentinel data 2020)」。個人部落格文章沒問題。
測試集要能回答兩個問題:
找不找得到?(召回)會不會亂框?(誤判)所以分兩類:
正樣本(8 張):中心對準 OSM 機場圍籬的形心
POSITIVE = ["TPE", "TSA", "RMQ", "CYI", "TNN", "KHH", "HUN", "TTT"]
桃園(TPE)一定要放,它是對照組:同一座機場、不同的渲染方式,模型還認不認得?
負樣本(5 張):沒有機場,但有長條狀的人造物,專門用來騙模型
NEGATIVE = {
"neg_banqiao": (121.455, 25.012, "板橋/新莊市區"),
"neg_taipeiport": (121.375, 25.150, "台北港"),
"neg_taichungport": (120.505, 24.275, "台中港"),
"neg_mailiao": (120.195, 23.785, "麥寮六輕工業區"),
"neg_changhua": (120.450, 23.950, "彰化農田"),
}
港口有長長的碼頭,工業區有大片矩形廠房,河流有筆直的堤防,這些在 10 m 解析度下都很像跑道。農田放進來是當簡單題,如果連農田都會框那問題就大了。
每張大小 12 km x 8 km,跟原本的 som_san_air.tif(13.5 x 9.2 km)差不多。
Day 25 算過,第 14 層在台灣的實地解析度是:
第 14 層實地解析度 8.73 m/px
模型是在 10 m/px 的影像上訓練的。直接拿 8.73 m 的圖去測,同一座機場會大 15%。YOLO 對尺度有一定的容忍度,但能對齊就對齊,不要讓變因混在一起。
縮小影像很簡單,麻煩的是仿射矩陣要跟著改:
def resample_to_ground(rgb, georef, lat, target=10.0):
factor = georef.transform[0, 0] * np.cos(np.radians(lat)) / target
h, w = rgb.shape[:2]
new_w, new_h = round(w * factor), round(h * factor)
out = cv2.resize(rgb, (new_w, new_h), interpolation=cv2.INTER_AREA)
t = georef.transform.copy()
t[0, 0] *= w / new_w # 像素變大了,每個像素代表的距離跟著變大
t[1, 1] *= h / new_h
return out, GeoRef(transform=t, epsg=georef.epsg)
左上角的原點不變,只有像素大小要乘上縮放比。縮小用 INTER_AREA,它會把多個像素平均起來,比 INTER_LINEAR 不容易產生鋸齒。
抓下來的影像要存成 GeoTIFF,這樣 Day 23 的管線不用改就能吃。
Day 23 只寫了讀取,今天補上寫入。tifffile 寫圖沒問題,但 GeoTIFF 的地理資訊要自己用 extratags 塞進去:
keys = [1, 1, 0, 3, # 版本 1.1.0,後面接 3 把鑰匙
1024, 0, 1, 1, # GTModelType = 投影座標系
1025, 0, 1, 1, # GTRasterType = PixelIsArea
3072, 0, 1, 3857] # ProjectedCSType = EPSG:3857
extratags = [
(33550, "d", 3, (res_x, res_y, 0.0), False), # ModelPixelScale
(33922, "d", 6, (0, 0, 0, x0, y0, 0), False), # ModelTiepoint
(34735, "H", len(keys), keys, False), # GeoKeyDirectory
]
tifffile.imwrite(path, rgb, photometric="rgb", extratags=extratags)
GeoKeyDirectory 是一個扁平的整數陣列,每 4 個一組:(鑰匙代碼, 存放位置, 個數, 值)。第一組是標頭,最後一個 3 表示後面有 3 把鑰匙。
Day 23 提過 GeoTIFF 有兩種存仿射矩陣的方式,這裡用 PixelScale + Tiepoint 那種,也是 GDAL 預設的寫法。
寫完讀回來驗證:
(801, 1201, 3) uint8 epsg=3857
影像中心 -> (121.2327, 25.0790)
跟 OSM 桃園機場圍籬的形心 (121.2327, 25.0790) 完全一致。
13 個場景全部抓下來:
tpe 1201x801 機場 ['TPE'] Taiwan Taoyuan International Airport
tsa 1200x801 機場 ['TSA'] Taipei Songshan Airport
rmq 1201x801 機場 ['RMQ'] Taichung Airport
...
neg_mailiao 1201x801 機場 無 麥寮六輕工業區
neg_changhua 1201x801 機場 無 彰化農田
13 個場景 -> data/s2cloudless
管線完全沒改,只有一個參數要想清楚:波段。
detector = AirportDetector(bands=(0, 1, 2), stretch="max")
Day 23 講過,(0, 1, 2) 在原始的 158 波段堆疊裡是 B1/B2/B3,物理上不正確,但模型是在這個組合上訓練的。EOX 的 tif 只有三個波段 R/G/B,所以 (0, 1, 2) 在這裡就是物理正確的真彩色。
也就是說,今天模型看到的是它從來沒看過的色彩。這是我們能拿到的最接近「真實世界」的測試。
每張圖跑完,用 Day 25 的 OSM 圍籬打分數、寫進 Day 26 的 PostGIS。
scene 機場 raw boxes hit fp best_inside 結果
tpe TPE 13 8 3 5 100.0% 找到
tsa TSA 6 4 0 4 0.0% 漏判
rmq RMQ 23 9 4 5 100.0% 找到
cyi CYI 13 7 1 6 59.2% 找到
tnn TNN 18 9 1 8 100.0% 找到
khh KHH 14 9 0 9 40.1% 漏判
hun HUN 10 7 1 6 100.0% 找到
ttt TTT 15 6 0 6 16.0% 漏判
neg_banqiao - 5 4 0 4 0.0% 誤判
neg_taipeiport - 5 5 0 5 0.0% 誤判
neg_taichungport - 14 4 0 4 0.0% 誤判
neg_mailiao - 7 5 0 5 0.0% 誤判
neg_changhua - 9 3 0 3 0.0% 誤判
機場召回率 5/8
框的精確率 10/80 = 12.5%
負樣本誤判 5/5 張有框,共 21 個

藍色是 OSM 機場圍籬,綠色是命中的框(過半面積在機場內),紅色是誤判。
召回 5/8 看起來還行,但很寬鬆。「找到」的定義是至少有一個框過半落在機場內。台中(RMQ)9 個框裡有 4 個命中,但另外 5 個框在周圍的農田和河床上。
精確率 12.5% 才是真相。模型畫了 80 個框,只有 10 個在機場上。每 8 個框只有 1 個是對的。
負樣本全軍覆沒。5 張沒有機場的圖,每一張都有框,連最簡單的彰化農田都框了 3 個。
把紅框一個個看過去,有一個很明顯的共同點:長條形、邊界筆直的東西。
淡水河和基隆河的河岸上模型學到的不是「機場」,比較像是「有直線邊界的大塊區域」。
TPE 是模型在原始 tif 上看過的那座機場。換成 EOX 渲染之後:
同一座機場換了色彩,框的數量跟位置全部不一樣。這不太像一個「學會辨識機場」的模型會有的行為。
這次每張圖都寫進了 PostGIS,Day 26 寫好的漏判查詢終於有用了:
SELECT r.source, a.iata
FROM runs r JOIN airports a ON ST_Within(a.geom, r.footprint)
WHERE r.source LIKE '%#rgb'
AND NOT EXISTS (
SELECT 1 FROM detections d
WHERE d.run_id = r.id AND ST_Intersects(d.geom, a.geom))
tsa.tif#rgb TSA
松山機場是唯一一座連碰都沒碰到的機場。KHH 和 TTT 都有框擦到圍籬,只是沒有過半,所以 SQL 層面不算完全漏判。這就是 ST_Intersects(有碰到)跟 50% 面積門檻(命中)的差別,兩個問題都有用。
今天是這個系列第一次真正的泛化測試:
Sentinel-2 cloudless 代替 Copernicus,同感測器、同解析度、不用帳號重採樣到 10 m 對齊訓練尺度,仿射矩陣的像素大小要跟著改write_geotiff,自己組 GeoKeyDirectory,讓圖磚影像能直接餵進 Day 23 的管線12.5%、負樣本 5/5 全部誤判
這個結果不意外。Day 23 就已經知道訓練集只有一個場景,今天只是用數字把它說清楚:模型沒有學會什麼是機場。
但「沒學會」有很多種可能。是門檻設太低?是色彩不一樣?還是訓練資料本身就教錯了?明天要做誤判分析,把這三個可能一個一個拆開來驗,那我們明天見。