很早以前,大家都说三峰的累计爬升是 1800 米或者 1750 米。去年我们用 5 小时 20 分跑完三峰的时候,两步路记录的累计爬升只有 1622 米!
该轨迹(越野跑路线,横切了望京塔和北尖)的地址是:https://2bulu.com/track/t-7XsfjWfqGH3p%25252FR2KBg5Tzw%25253D%25253D.htm。
我们下载了轨迹的原始文件,抽取出其中的位置数据。在 5 个小时 20 分钟中,两步路的该轨迹包含 3319 个位置,大部分抽样时间在 4 秒到 12 秒不等。每个位置有四个字段,分别为时间、精度、纬度和海拔,比如前几个点的数据如下:
时间 | 经度 | 纬度 | 海拔 |
---|---|---|---|
8:42:56 | 116.100694 | 40.051520 | 127.25 |
8:43:01 | 116.100661 | 40.051479 | 127.25 |
8:43:07 | 116.100656 | 40.051417 | 128.29 |
8:43:13 | 116.100656 | 40.051355 | 127.39 |
8:43:17 | 116.100661 | 40.051295 | 126.18 |
只看海拔数据如下:
如果直接用这 3319 个海拔数据,两两之间的距离计算海拔变化来计算,该路线的累计爬升将高达 2012 米!远高于两步路给的 1622 米的累计爬升。但 2012 米的爬升肯定是高估的,因为 GPS 获得的数据会有误差。
一个极端的例子是平路跑圈。如果在学校操场上跑圈,每个样本点上 GPS 的海拔也会有微小的几厘米几十厘米的上上下下的误差,如果时间很长,取点量过多,同样会造成很高的累计爬升。比如平地跑步 1 小时取样 1000 个点,每个点就算平均 10 厘米的误差,也会带来 100 米的累计爬升。
在上面三峰路线的轨迹中, 3319 个位置,其相邻点之间的上山和下山转换了 384 次,这显然是不可能的,其中大部分都是因为取样的误差造成的。所以我们需要过滤掉这些误差,一些细小的起伏,更可能是 GPS 轨迹的误差带来的,而不是真正的上山和下山。
我的实验结果表明,两步路对起伏过滤太严格,导致严重低估了路线的累计爬升。忽略指定的起伏幅度后,能计算的累计爬升数据如下:
忽略起伏幅度 | 累计爬升 | 起伏次数 |
---|---|---|
- | 2,012 | 384 |
5 米 | 1,842 | 36 |
10 米 | 1,775 | 18 |
15 米 | 1,752 | 14 |
20 米 | 1,736 | 10 |
25 米 | 1,715 | 10 |
30 米 | 1,715 | 10 |
35 米 | 1,715 | 10 |
40 米 | 1,679 | 8 |
45 米 | 1,679 | 8 |
50 米 | 1,679 | 8 |
55 米 | 1,679 | 8 |
60 米 | 1,621 | 6 |
根据这个实验,两步路事实上忽略了 60 米以下的起伏,才让三峰路线的累计爬升计算跌到 1622 米的幅度!当然由于算法可能的不一致,这个忽略幅度只是估计值,但这足以说明两步路在计算累计爬升时的严格程度。忽略六十米以下的起伏后是什么情况呢?此时三峰路线被简化为 7 个点,只有六段起伏:
7 个点 6 段起伏如下:
时间 | 经度 | 纬度 | 海拔 | 大致位置 |
---|---|---|---|---|
8:42:56 | 116.10069 | 40.05152 | 127 | 大觉寺 |
11:00:52 | 116.04700 | 40.07900 | 1,264 | 阳台山 |
11:22:42 | 116.03184 | 40.08247 | 1,014 | |
11:59:14 | 116.01440 | 40.07257 | 1,270 | 妙峰山 |
12:30:22 | 116.03327 | 40.06262 | 758 | 涧沟村 |
12:56:54 | 116.04141 | 40.05783 | 987 | |
14:03:55 | 116.10050 | 40.05074 | 133 | 大觉寺 |
这就有点太狠了,连北尖都被过滤了,茶棚到阳台山中间的土包也被过滤了,还有一些更小的起伏被过滤了。
这也是为什么很多人总觉得两步路给的爬升过低的真实原因,很多人就喜欢用两步路的海拔纠偏功能。但前文已研究,海拔纠偏是用一个错误来解决另一个错误,会带来更大的不准确性。
那三峰路线真正的爬升是多少呢,我感觉路线上起伏转换次数在 15 次左右,对应的过滤幅度应该是 10 到 15 米。以 15 米为例,路线上起伏 14 次,状况如下图,累计爬升 1750 米。这可能是三峰路线的真实爬升。
附录:路线起伏的算法非常类似于股票走势里的分段算法,过滤小波段,识别大波段。代码开源在:https://gitee.com/zhiqiang.org/outdoor。
核心算法是不断循环下面操作(因为规模不大,未对效率做优化):
- 合并同样走势的波段,即若某个点前后的走势一样,这个点是无效的,可删除。
- 查找最小的波动,若该波段过小,删除该波段(即波段的两边端点)。
详细代码如下,也可直接看源文件。
def calc_cum_up_down(self, ignore):
points = self.points[:]
while True:
merged = merge(points)
removed = remove_minimum(merged, ignore)
if len(removed) == len(points):
alts = [p1[3] - p0[3] for p0, p1 in zip(removed[:-1], removed[1:])]
times = [p[0] for p in removed]
return (sum([alt for alt in alts if alt > 0]),
sum([alt for alt in alts if alt < 0]),
removed,
[ p if p[0] in times else [*p[:3], ""] for p in self.points ])
else:
points = removed
def merge(points):
""" 合并爬升、下降路段 """
current_direction = points[1][3] - points[0][3]
merged = [points[0], ]
for (p1, p2) in zip(points[:-1], points[1:]):
alt = p2[3] - p1[3]
if alt * current_direction >= 0:
current_direction = p2[3] - merged[-1][3]
else:
merged.append(p1)
current_direction = alt
merged.append(points[-1])
return merged
def remove_minimum(points, ignore):
""" 删除海拔变化最短的一段的路径 """
if len(points) <= 2: return points
alts = [abs(p2[3] - p1[3]) for p1, p2 in zip(points[:-1], points[1:])]
min_alt = min(alts)
if min_alt > ignore:
return points
min_i = alts.index(min_alt)
if min_i == 0:
return [points[0], *points[2:]]
elif min_i == len(alts) - 1:
return [*points[:-2], points[1]]
else:
return [*points[:min_i], *points[min_i+2:]]
Q. E. D.