ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Class 47机车运行数据分析:Python解析GPS轨迹与速度曲线

Class 47机车运行数据分析:Python解析GPS轨迹与速度曲线 铁路迷之间讨论“Class 47 D1994 在 Churnet Valley Railway 上从 Kingsley 和 Froghall 运行到 Ipstones”这件事第一反应往往是机车的历史、涂装和鸣笛声。但从技术角度看这组运行记录真正让人感兴趣的是它能被还原成一组可分析的时空数据GPS 轨迹、速度曲线、坡度变化、停车点位置以及机车在不同区段表现出的牵引特征。这篇文章不打算只讲情怀而是把这台 Classic 47 在 CVR 上的运行当作一个“数据采集与分析”案例来处理。我们会把这段线路从 Kingsley/Froghall 到 Ipstones 的移动过程拆解为传感器数据然后用 Python 完成轨迹解析、速度计算、坡度估算并把结果叠加到视频或图表中。对于喜欢铁路摄影、机车数据建模或者单纯想用真实 GPS 数据练手的朋友这套流程可以直接复用。1. 这篇文章真正要解决的问题如果你只是在网上刷到一段“Class 47 冒烟爬坡”的视频那确实不需要技术分析。但当你开始自己拍摄、记录或者研究一列真实机车的运行过程时会遇到几个很现实的问题第一单纯拍视频很难量化运行状态。视频里能看出机车快慢但看不出它在哪一段线路加速、在哪一个弯道降速、坡度到底有多大。第二手机 GPS 记录的轨迹噪声很大。尤其是在山谷里穿行时卫星信号会被地形和树木遮挡直接画出来的速度曲线非常难看。第三铁路数据涉及多个数据源。GPS 轨迹、视频时间戳、列车时刻表、线路坡度信息这些数据如果没有按统一时间轴对齐后续分析几乎无法进行。所以这篇文章要解决的核心问题不是“怎么拍火车”而是“怎么把一次机车运行变成一份可以反复分析的数据档案”。我会以 Class 47 D1994 在 Churnet Valley RailwayCVR上从 Kingsley/Froghall 到 Ipstones 方向运行作为背景给出一个完整的轻量级数据工作流。即使你没有任何铁路专业知识也能跟着步骤做出一张带速度曲线的轨迹图。2. 机车与线路背景为什么选这条线作技术案例2.1 Class 47一台老式电传动内燃机车Class 47 是英国铁路历史上非常经典的内燃机车由 Brush Traction 在 1960 年代制造。它属于电传动内燃机车意思是柴油机不直接驱动车轮而是带动主发电机发电再由牵引电动机驱动轮对。这种设计的好处是动力传递更平顺低速时扭矩大柴油机可以稳定在高效转速区间运行牵引电动机的并联/串联组合能获得不同的速度档位。Class 47 搭载的是一台 Sulzer 12 缸柴油机输出功率按资料普遍在 2500 到 2750 马力之间。它的轴式为 Co-Co也就是两台转向架每个转向架三根动轴全车六根动轴。这样的轴式安排让机车在湿滑轨道上有更好的粘着性能。D1994 是这台机车的编号前缀。英国铁路早期使用 D 开头的五位编号体系后来转入 TOPS 体系后变成 47xxx。在遗产铁路上很多机车会恢复早期编号或保持受保护时期的涂装所以 D1994 这个编号本身带有强烈的历史时期感。2.2 Churnet Valley Railway 的线路特点CVR 是位于英国斯塔福德郡的一条标准轨距遗产铁路利用的是过去北斯塔福德郡铁路的旧线。和很多遗产铁路一样它不是一条平坦的展示轨道而是真实的山谷铁路包含车站、道口、桥梁、弯道和一定坡度的区间。本次关注的重点是从 Kingsley 和 Froghall 到 Ipstones 方向。Froghall 通常被看作一个重要的端点站或换向点而 Ipstones 方向属于线路中更有挑战性的区段。这条线路对数据记录特别有意思的地方正好在于山谷地形会明显影响 GPS 精度便于演示轨迹校正线路存在坡度可以用来计算牵引力变化站点之间距离不长停车、启动频繁速度曲线有清晰特征。所以这不是一条让人“看个热闹”的线而是一条能用来讲清楚运行数据处理的线。3. 环境准备数据记录需要的硬件与软件在真正开始分析之前我们先明确需要准备什么。这里我不想把方案设计得很复杂只讲一个普通技术爱好者就能完成的组合。3.1 硬件清单设备作用推荐级别手机或运动相机记录视频画面必备手机内置 GPS 或外接蓝牙 GPS记录经纬度、速度、时间必备外接 GNSS 模块提升轨迹精度降低漂移建议录音笔或手机录音记录环境音与报站声辅助时间同步可选如果你只是想体验流程直接用手机安装 GPS Logger 类 App 就够用。手机 App 会输出 GPX、NMEA 或 CSV 文件这些格式都能被 Python 读取。如果要做更严谨的数据档案建议外接 u-blox 或同类支持北斗/GPS/GLONASS 的多模模块。外接模块的好处是采样频率可以做到 5Hz 甚至 10Hz而手机 GPS 通常只有 1Hz。3.2 软件环境本文所有代码基于 Python 3.9 以上版本。建议创建一个独立的虚拟环境python -m venv railway-env source railway-env/bin/activate随后安装以下依赖pip install pandas numpy matplotlib gpxpy这几个库的作用pandas处理表格化的 GPS 数据numpy做距离和速度计算matplotlib绘制速度曲线和轨迹图gpxpy读取标准 GPX 轨迹文件。如果你的 GPS 设备输出 CSV可以跳过gpxpy直接用pandas.read_csv读取。4. 数据采集与轨迹数据格式4.1 采集时最容易犯的错误很多人拿起手机就记录结果跑完全程回来数据根本不能用。最常见的问题有三个时间戳不统一手机 GPS 记录器和相机使用各自的时间没有提前对表。视频里机车启动的时刻和 GPS 轨迹上的时间点对不上。采样频率太低1Hz 的 GPS 在车站停车、启动缓行时也能凑合但如果想分析通过道口前的制动过程相邻两个点之间已经跳过了重要细节。把 GPS 放在金属外壳里登山包、保温杯、铁盒都会削弱卫星信号。在机车这种金属结构附近GPS 天线位置尤其重要。4.2 推荐的文件组织方式一次完整的运行记录建议至少保存三类文件20250101_d1994_run/ ├── raw/ │ ├── gps.csv │ ├── camera_video.mp4 │ └── memo.txt ├── processed/ │ ├── track_clean.csv │ └── speed_curve.csv └── output/ └── analysis.pngmemo.txt是一个容易被忽略但很有用的文件。在上面手动记录几个关键时间点例如“10:23:15 从 Froghall 开出”“10:31:40 到达某停车点”。这些备注会成为后期对齐视频和 GPS 的锚点。5. 轨迹数据处理从原始 GPS 到速度曲线现在我们开始进入核心代码部分。这段流程不依赖任何私有 API只使用公开的 Python 数据处理库。5.1 读取 GPS CSV 数据假设 GPS 记录器导出的 CSV 包含以下列timestamp,lat,lon,altitude,speed。有的设备会直接给速度值但这个速度值往往来自 GPS 多普勒频移短时间波动很大我更推荐用经纬度自己计算速度。import pandas as pd df pd.read_csv(raw/gps.csv, parse_dates[timestamp]) print(df.head()) print(df.info())如果导出的是 GPX 文件则用gpxpy解析import gpxpy with open(raw/gps.gpx, r) as f: gpx gpxpy.parse(f) points [] for track in gpx.tracks: for segment in track.segments: for point in segment.points: points.append({ timestamp: point.time, lat: point.latitude, lon: point.longitude, altitude: point.elevation, speed: point.speed }) df pd.DataFrame(points) print(df.head())这两段代码的目标是一样的把 GPS 轨迹变成 DataFrame后面的计算才能统一。5.2 用 Haversine 公式计算里程GPS 坐标是经纬度不能直接用欧几里得距离。计算地球表面两点距离的常用方法是 Haversine 公式。import numpy as np def haversine_distance(lat1, lon1, lat2, lon2): R 6371000.0 # 地球平均半径单位米 phi1 np.radians(lat1) phi2 np.radians(lat2) dphi np.radians(lat2 - lat1) dlambda np.radians(lon2 - lon1) a np.sin(dphi / 2) ** 2 np.cos(phi1) * np.cos(phi2) * np.sin(dlambda / 2) ** 2 c 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a)) return R * c # 计算相邻点之间的距离 lat df[lat].to_numpy() lon df[lon].to_numpy() distances [0.0] for i in range(1, len(df)): dist haversine_distance(lat[i-1], lon[i-1], lat[i], lon[i]) distances.append(dist) df[distance_m] distances df[cumulative_km] df[distance_m].cumsum() / 1000.0这一段和纯数据表最大的区别是我们增加了累计里程。之后画图时横轴可以直接用公里数而不是一条条散乱的点位。5.3 计算速度并平滑相邻两点距离除以时间差就是这段区间的平均速度。但 GPS 原始距离有抖动直接求出来的速度曲线会非常毛躁。所以我会先计算原始速度再做滚动平均。df[time_delta] df[timestamp].diff().dt.total_seconds() df[speed_raw_ms] df[distance_m] / df[time_delta] # 过滤掉时间差为零或负数的异常行 df df[df[time_delta] 0].copy() # 将米每秒转换为公里每小时 df[speed_raw_kmh] df[speed_raw_ms] * 3.6 # 5 点滚动平均减少 GPS 噪声 df[speed_smooth_kmh] df[speed_raw_kmh].rolling(window5, centerTrue, min_periods1).mean()为什么不用原始speed字段因为不同 GPS 模块对速度的计算方式不同有的会直接输出瞬时速度但瞬时速度容易受信号反射影响。自己计算的好处是逻辑统一后续如果要换成 10Hz 的定位数据代码完全不用改。5.4 绘制速度和里程图当 DataFrame 里有了累计里程和速度曲线之后可以直接用matplotlib输出图表。import matplotlib.pyplot as plt plt.figure(figsize(12, 5)) plt.plot(df[cumulative_km], df[speed_smooth_kmh], linewidth2, color#C0392B) plt.xlabel(累计里程 (km)) plt.ylabel(速度 (km/h)) plt.title(Class 47 D1994 CVR Kingsley/Froghall 区间速度曲线) plt.grid(alpha0.3) plt.tight_layout() plt.savefig(output/speed_curve.png, dpi180) plt.show()这张图就是整个数据分析流程的第一个可交付物。从图里你能直接看出出站后加速段的位置哪些区段速度明显下降对应弯道或坡道到达停车点前减速过程是否平滑最快速度出现在哪一段。6. 更进一步坡度与牵引力估算6.1 从高程数据计算坡度很多 GPS 设备会输出海拔。虽然海拔误差比平面坐标大但做宏观坡度分析仍然有参考价值。计算方式是相邻两点的高程差除以水平距离。df[elevation_delta] df[altitude].diff().fillna(0) df[grade_percent] (df[elevation_delta] / df[distance_m].replace(0, np.nan)) * 100 # 对坡度也做平滑降低高程噪声 df[grade_smooth] df[grade_percent].rolling(window10, centerTrue, min_periods1).mean()坡度单位为百分比比如 2% 表示每 100 米水平距离上升 2 米。铁路行业中坡度跟牵引力计算直接相关。6.2 估算维持速度所需要的牵引力这里不追求精确工程计算只做一个教学估算。列车在坡道上匀速运行时需要克服的阻力主要有三项坡道阻力与坡度成正比基本运行阻力与速度有关曲线阻力与弯道半径有关此处可以先忽略。假设整列列车的总质量约为 250 吨重力加速度取 9.81 m/s²。如果当前坡度为 2%那么单纯克服坡道需要的力为mass_ton 250 # 列车总质量单位吨 grade_percent 2.0 # 坡度百分比 g 9.81 mass_kg mass_ton * 1000 grade_fraction grade_percent / 100.0 force_grade_newton mass_kg * g * grade_fraction print(f克服 2% 坡度需要的牵引力约为 {force_grade_newton / 1000:.1f} kN)把这段代码扩展成一个函数就能针对每个 GPS 点计算理论牵引力需求。def estimate_tractive_force(speed_kmh, grade_percent, mass_ton250): 速度单位km/h 坡度单位% 返回值需要施加在轮周上的牵引力单位 kN mass_kg mass_ton * 1000 v speed_kmh / 3.6 # 简化的基本运行阻力单位 N rolling_resistance mass_kg * 0.002 aero_resistance 6.3 * v grade_resistance mass_kg * 9.81 * (grade_percent / 100.0) total_force rolling_resistance aero_resistance grade_resistance return total_force / 1000.0 df[tractive_force_kN] [ estimate_tractive_force(spd, grd) for spd, grd in zip(df[speed_smooth_kmh], df[grade_smooth]) ]这个结果完全可以作为一次运行记录的附加维度。你会看到列车在启动时需要的牵引力最大因为还需要额外克服加速阻力而到了坡道中段牵引力需求会随坡度变化明显波动。7. 视频与数据对齐把速度叠到画面上数据分析完成后最常见的需求是把速度表、里程、坡度叠加到原始视频里形成类似行车记录仪的画面。很多人以为一定要用复杂软件其实用 Python 配合 ffmpeg 就能做一个基础版本。基本思路是从视频中按固定时间间隔抽帧根据帧时间戳从 DataFrame 里找出对应的速度、里程、坡度用 PIL 或 OpenCV 在帧上绘制信息把带文字的帧重新拼接成视频。一个核心工作是统一时间轴。建议在采集时就把手机 GPS 记录器的时钟和相机的时钟对到秒级并且拍一段时钟 App 画面作为参考。这样后期对齐会轻松很多。下面是一段抽帧并叠加文字的示意逻辑import cv2 import pandas as pd cap cv2.VideoCapture(raw/camera_video.mp4) fps cap.get(cv2.CAP_PROP_FPS) frame_count int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) video_start_time pd.Timestamp(2025-01-01 10:23:00) # 根据记录时的时间戳填写 output_writer cv2.VideoWriter( output/d1994_with_hud.mp4, cv2.VideoWriter_fourcc(*mp4v), fps, (int(cap.get(3)), int(cap.get(4))) ) frame_idx 0 while True: ret, frame cap.read() if not ret: break current_time video_start_time pd.Timedelta(secondsframe_idx / fps) nearest_row df.iloc[(df[timestamp] - current_time).abs().argsort()[:1]] speed_text f{nearest_row[speed_smooth_kmh].values[0]:.1f} km/h dist_text f{nearest_row[cumulative_km].values[0]:.3f} km grade_text f{nearest_row[grade_smooth].values[0]:.2f}% cv2.putText(frame, speed_text, (50, 100), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3) cv2.putText(frame, dist_text, (50, 160), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), 2) cv2.putText(frame, grade_text, (50, 220), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 255), 2) output_writer.write(frame) frame_idx 1 cap.release() output_writer.release()这段代码的逻辑非常简单但已经足够生成一个带 HUD 的铁路运行视频。实际使用时你需要根据机型和分辨率调整文字位置与字体大小。8. 常见问题与排查方法问题现象可能原因排查方式解决方案速度曲线严重向 0 方向抖动GPS 漂移导致相邻点距离偏小查看原始经纬度轨迹是否有跳变点对经纬度做中值滤波或提高采样率轨迹上出现“飞点”卫星信号丢失后重新定位检查 altitude 或 lat/lon 是否突变删除超出最大速度阈值的点例如超过 200 km/h停车时速度不为 0静止状态下 GPS 噪声累积查看 time_delta 和 distance_m 分布设置静止阈值如 0.5 km/h 以下归零视频与数据错位相机和 GPS 时钟未对齐对比视频中道口或站点时间与 GPS 时间重新记录锚点时间或用人工标记时间戳偏移坡度忽正忽负海拔数据噪声大画出 altitude 曲线观察使用更大幅度的高程平滑窗口累计里程比实际线路短采样点被错误过滤检查是否有连续时间缺失调整异常过滤阈值保留有效点9. 铁路数据工程的一些通用建议这类分析工作最大的坑并不是算法而是数据采集阶段的粗糙。根据我处理类似数据的经验有几点值得反复强调。第一采样前先固定坐标系。不要拿到 GPS 直接就按经纬度画图。如果要做里程计算必须明确使用 WGS84 坐标系并在代码中统一转换。虽然 Haversine 公式不需要投影但如果后续要计算曲线半径或与地图叠加最好使用 UTM 或 Web Mercator 投影。第二给每次运行记录建立元数据文档。哪怕只是几行备忘录也要写清楚记录日期、运行车次、机车编号、车厢数量、GPS 设备型号、相机型号、采样频率。没有元数据的数据三个月后就跟没用差不多。第三数据要分层处理。原始数据永远保留一份不要在原始文件上反复修改。清洗后的数据单独存放在processed目录输出结果放到output目录。这个习惯能避免很多混乱。第四不要为了炫技引入复杂模型。对大多数铁路运行分析来说Haversine 距离、滚动平均、线性插值已经足够。真正复杂的车辆动力学模型需要完整的列车编组参数、线路纵断面图、信号距离这些在非专业场景下既获取困难也难以验证。10. 总结与后续可以深入的方向这篇文章从 Class 47 D1994 运行在 CVR 上这个场景出发实际上完成了一套完整的“铁路运行数据记录与可视化”流程用手机或外接 GPS 采集轨迹用 Python 读取 CSV/GPX 数据计算里程、速度和坡度绘制速度曲线估算牵引力需求将数据叠加到视频画面。不管你是想分析某台机车在特定区段的运行表现还是想给自己的铁路摄影作品加一个数据化 HUD这套流程都能直接落地。接下来如果你还想继续深入可以尝试这些方向用地图瓦片绘制运行轨迹叠加图收集多趟列车通过同一区段的数据比较不同机车的加速能力结合历史时刻表分析实际运行时间与计划时间的偏差学习folium或keplergl输出可交互的轨迹地图。需要注意的是任何现场拍摄和观察活动都必须遵守遗产铁路的运营规定保持在允许的观察区域之外操作不得进入轨道区域也不得干扰铁路运营。D1994 只是一个起点。真正有意思的是你开始用数据去看一辆机车的运行方式。当速度和坡度两条曲线同时出现在屏幕上的时候铁路就不再只是“一列火车开过去”而是一套可以被量化、被复现、被分析的工程系统。
返回列表