前面幾天把幾場全馬的資料攤開來看:分段配速、心率漂移、後半掉速。
那些都是二維的圖—橫軸時間、縱軸某個數值,而 FIT 檔裡其實有經緯度,所以路線可以直接畫成 3D。
第三個維度用什麼?直覺答案是海拔。
而問題是:FIT 檔裡有兩條不一樣的海拔。
解析東京馬那支 FIT,record 訊息有 28 個欄位。挑幾個看:
enhanced_altitude, enhanced_speed, position_lat, position_long,
heart_rate, cadence, power, vertical_oscillation, step_length, ...
enhanced_altitude 就是我要的高度,12,128 筆,一秒一筆。
但翻到 gps_metadata 這個訊息的時候,發現裡面也有一個 enhanced_altitude。
record.enhanced_altitude 12128 筆 起點 34.4 m
gps_metadata.enhanced_altitude 12146 筆 起點 42.6 m
同一場比賽、同一支錶,兩條高度資料相差 8.2 公尺。
光看起點差 8 公尺還不夠說明什麼,真正的差別在逐秒的變化量:
d = [abs(v[i+1] - v[i]) for i in range(len(v)-1)]
| 資料流 | 逐秒平均變化 |
|---|---|
record |
0.066 m |
gps_metadata |
0.325 m |
4.9 倍。
一個人跑步的時候,一秒之內海拔會有 0.3 公尺的差距嗎嗎?除非在爬樓梯,不然不會,所以 gps_metadata 那條的抖動大部分是雜訊。
而 record 那條平滑很多—平滑到不像是原始感測器輸出。
所以推論是:平常大家讀的 record.enhanced_altitude,不是原始 GPS 高度,是處理過的。 可能是平滑、可能是多感測器融合,Garmin 沒有公開演算法。
(我試著在檔案裡找氣壓或溫度欄位來佐證,record 那 28 個欄位裡一個都沒有,所以怎麼處理的,從檔案內證實不了。)
抖動是隨機的,可以靠平滑處理掉,但海拔還有另一個問題:它會隨時間走掉。
東京馬的路線有幾段會重複經過同一個地點,所以可以這樣量:同一個位置、不同時間經過,高度差多少?
配對條件:
跑出來:
| 兩次經過的時間間隔 | 高度差 | 配對數 |
|---|---|---|
| ~0 分 | +0.09 m | 35 |
| ~15 分 | +4.60 m | 90 |
| ~75 分 | −4.77 m | 14 |
間隔越久,差越大。 而間隔 ~0 分的那組幾乎沒有誤差—這排除了「是隨機雜訊」的可能,因為隨機雜訊不會挑時間。
這是漂移,不是雜訊,同一個地方,開跑 15 分鐘經過時比剛起跑高 4.6 公尺,75 分鐘時又低了 4.8 公尺。
上面那張表少了一格。原始筆記裡還有「~45 分」那組,我把它拿掉了,因為三次量測跑出三個不同的答案:
| 間隔 | 8/16 | 8/20 | 今天 |
|---|---|---|---|
| ~0 分 | +0.34 | −0.14 | +0.09 |
| ~15 分 | +4.34 | +4.16 | +4.60 |
| ~45 分 | −3.65 | −2.16 | −1.51 |
| ~75 分 | −5.08 | −5.03 | −4.77 |
其他三格三次都在同一個範圍內,只有 ~45 分那格一路從 −3.65 漂到 −1.51。而且更早的測試裡,把配對門檻從 20 公尺改成 25、30 公尺,那一格會變成 −1.30 和 +0.35—連正負號都翻掉。
所以那格不能用,它跟著門檻在動,不是跟著資料在動。
去查了兩件事,都有官方文件。
一、Garmin 的錶用的是氣壓式高度計。
accuracy of +/-10 feet at any given point while weather conditions remain stable
(i.e. ambient pressure is not changing)
精度 ±10 英尺(約 ±3 公尺),但明文以「氣壓不變」為前提,而一場全馬三個多小時,氣壓不可能不變。
二、天氣造成高度誤差,官方直接寫明。
Significant Weather Changes: Recording an activity during a strong incoming or
departing low or high-pressure weather system can skew your elevation readings.
While the overall elevation gain and loss are often unaffected, your starting and
ending elevations may be recorded as too high or too low.
同一篇還提到強風會影響氣壓感測器,以及感測孔阻塞。
所以我量到的漂移,跟官方描述的現象對得上。 但我沒辦法證明就是氣壓造成的。
理論上有個辦法:如果 gps_metadata 那條是純 GPS 高度、不受氣壓影響,而它沒有跟著漂,那就能把氣壓指認出來。
但那個比對做不了。
因為 gps_metadata 在這支檔案裡只寫了兩個欄位:
enhanced_altitude: 42.6
enhanced_speed: 2.524
沒有時間戳,也沒有經緯度 ,FIT 規格裡這個訊息(global msg 160)定義了九個欄位,包含 timestamp、position_lat、position_long—但錶只寫了高度和速度。
所以那條流是一串沒有座標的數字,它存在、它的值跟 record 不一樣、它抖得多 4.9 倍—這些都量得到,但「它在第幾公里漂了多少」沒辦法算,因為不知道每個點在哪裡。
而兩條的筆數還差了 18 筆(12,128 vs 12,146),所以連「按順序硬對」都不安全。
結論就停在這裡:我知道漂移存在、量得出多少、也知道官方說氣壓會造成這件事,但「這次的漂移是氣壓造成的」無法證明出來。
寫到這裡就卡住了:既然 record 那條比較好用,為什麼還要留一條抖成那樣的?
先講規格層面查得到的:gps_metadata(global msg 160)在 FIT 規格裡定義了九個欄位—
253 timestamp 1 position_lat 3 enhanced_altitude 5 heading
0 timestamp_ms 2 position_long 4 enhanced_speed 6 utc_timestamp
7 velocity
所以「GPS 層自己報高度」是規格裡就有的位置,不是誤植,而我的錶只寫了第 3、4 兩個,其他七個都空著。
至於用途—我去 Garmin 官方的開發者論壇找,結果是:
有人問過同樣的問題,而且沒人回答。
一串標題叫〈FIT enhanced_speed Record or GPS Metadata?〉,狀態是 Not Answered,另一串〈Anyone knows what gps_metadata.enhanced_speed in the .FIT files are used for?〉,沒有 Garmin 員工回覆。
那兩串裡,使用者觀察到的現象跟我一樣:兩條數值不同,高度差「約 10 公尺」(我的是 8.2)。有人猜是 DEM 校正造成的,但那是猜測,沒有證據。
而官方的 FIT Protocol 文件完全沒有說明 gps_metadata 的用途,只有 SDK 裡的欄位定義。
所以答案是:規格允許,用途沒公開。
record 那條是錶面和 Garmin Connect 用的 — 論壇使用者的說法,合理但非官方這件事本身值得記一下:一個每天被幾百萬人戴著的裝置,它寫進檔案裡的東西有一部分是沒有文件的。 而我們要決定 3D 圖上那條垂直軸用哪一條資料—這邊能依據的只有自己量出來的抖動量。
整場比賽的每 5K 平均海拔長這樣:
0- 5K 32.1 m ← 最高
5-10K 9.2 m
20-25K 3.2 m ← 最低
40-45K 16.2 m
這不是漂移,是真的下坡。 而這次查得到官方數字—東京馬的官方課程說明寫著:最低標高 0 公尺、最高 39 公尺、高低差 39 公尺,而且前 6 到 7 公里(飯田橋附近)下降 35 公尺,之後幾乎沒有起伏。
而我的 GPS 資料:
| 官方 | 我的錶 | |
|---|---|---|
| 最高 | 39 m | 40.8 m |
| 最低 | 0 m | −1.0 m |
| 前 6–7K 下降 | 35 m | 27.2 m |
最高最低都只差 1 到 2 公尺—那正好是前面量到的漂移量級。
但「前 7K 下降了多少」差了 7.8 公尺。 因為那是兩個時間點相減,而兩個時間點各自帶著自己的漂移。
這就是為什麼不能拿「起跑高度 vs 某處高度」去談誤差—那個減法會把地形和漂移加在一起。必須用「同一個地點、不同時間」比對,才分得開。那就是前面那套配對條件存在的原因。
回到最初的問題。
畫 3D 路線的時候,我用的是 record.enhanced_altitude—因為它抖得少,畫出來的線比較好看。
但「比較好看」是因為它已經被處理過了,而處理的細節我不知道。所以那條 3D 曲線的垂直起伏,準確度大概是這樣:
而一場東京馬的實際起伏是 39 公尺—漂移量差不多是它的八分之一到十分之一。
所以誠實的做法是:3D 圖上的高度軸不標數值。 它表達的是「這裡有起伏」,不是「這裡比那裡高幾公尺」。
Day 23 講手機版的時候留了一件事沒處理,因為那時候還沒介紹 3D 這一頁。
OrbitControls 是 three.js 內建的相機控制—滑鼠拖曳轉視角、滾輪縮放。而它會把 canvas 的 touch-action 設成 none:
touch-action: none
意思是那塊區域吃掉所有觸控手勢。
桌機沒問題:滑鼠拖曳轉視角、滾輪捲頁面,兩件事分開。但手機只有一根手指—而 3D 卡片在窄螢幕佔掉大半個畫面,手指一放上去,頁面就再也捲不動了。
這是 three.js 的合理預設。 它假設 3D 是主角,一個全螢幕的檢視器。而我把它嵌在一個要往下捲的頁面裡,那個假設就不成立了。
改法跟 Day 23 的色帶圖一樣:觸控裝置預設不吃手勢,點一下才進入互動。
// 觸控裝置預設不吃手勢 — 手指一放上去頁面就捲不動,使用者會以為卡住
controls.enabled = !touchOnly.value;
點了之後右上角出現「完成」交還捲動。判斷用 matchMedia('(hover: none)') 不是螢幕寬度—有觸控筆的桌機、有滑鼠的平板都存在。
而這跟今天講的高度軸是同一類問題:工具的預設值帶著它自己的使用情境。OrbitControls 預設全螢幕,record.enhanced_altitude 預設你要一條好看的線—兩個都是合理的預設—只是它們預設的使用方式跟這邊不一樣。
今天大半在講「海拔這條資料有多不可靠」。
但還有另一個問題:就算它完全準確,海拔真的是那個垂直軸最好的選擇嗎?
明天用田徑場的間歇資料回答這件事—那是一筆軌跡完全重疊的田徑場資料。