用ffmpeg和Python打造公交POV视频:完整线路记录流程

发布时间:2026/9/3 12:59:47
用ffmpeg和Python打造公交POV视频:完整线路记录流程 一条公交 POV 视频看起来只是把手机或记录仪架在驾驶室前方完整拍下一趟 88 路从石家庄东站开往市中心西部的运行过程。实际上要做出信息准确、画面稳定、站点清晰、里程可查的成片背后是一个典型的“视频素材 线路数据 后期脚本”复合项目。这条线路全程约 16 公里属于中短线站点数量适中、跨区路径不算复杂很适合用来练习公交线路记录类项目的完整流程先整理站点表再采集视频和轨迹最后用 ffmpeg 与 Python 完成裁剪、压制、站点核对、里程统计和发布前检查。下面这套流程会拆成 6 个环节每个环节都会给出可直接使用的命令、代码和检查表。1. 先拆解88路POV项目的交付物与技术主线1.1 公交POV到底在记录什么POV 是 Point of View 的缩写在公交爱好者圈子里通常指把摄像机或手机固定在驾驶室前方以第一视角完整记录一趟公交车从始发站到终点站的运行过程。画面里通常能看到前风挡、仪表台、路况、站台和上下车乘客声音里会有报站器、关门提示和车辆运行噪声。单纯从视频角度理解它是一条“长镜头纪实素材”。但从工程角度理解POV 项目的核心产出物不止是视频本身。一次规范记录至少包含四样东西一条经过剪辑和压制的成片视频。一份结构化站点清单包含站序、站名、到达时刻偏移。一份可与视频对应起来的里程和轨迹数据。一份拍摄日期、方向、车号、素材路径和验证记录的说明文档。真正决定一条公交 POV 是否有长期价值的不是镜头多稳、调色多艳而是成片能不能准确回答“这趟车从哪开到哪、经过哪些站、用了多长时间、全程约多少公里”。88 路这条 16 公里的中短线正好可以把这些问题压缩在一个比较容易验证的范围内。1.2 16公里中短线为什么适合作为线路记录案例公交线路越长后期工作量的增长不是线性的。一条 16 公里左右的线路单程拍摄通常不会产生极端庞大的素材体量站点数量也不会多到人工核对失控。更重要的是整条线路可以在一个连续时间段内完整拍完不需要在中间换乘、分段拼接。长线路一旦超过 30 公里会遇到这些问题同一段路可能因为早高峰、午间、夜间不同时段产生完全不同的运行节奏。车外光线跨度大早出发时逆光到达终点时可能已经黄昏。单程素材过长剪辑软件预览卡顿、压片时间成倍增加。GPS 轨迹如果中途丢星很难判断具体丢在哪一段。所以如果想把一套流程练熟16 公里左右的中短线是最合适的练手对象。88 路连接石家庄东站和中心城区西部跨区意味着路径中会经过快速路、主干道、普通道路和公交专用道等多种路况素材本身有足够的分析价值又不至于把流程拖到失控。这里要强调一点项目资料里提到的“全程16公里”是一个口径值。实际制作时不能简单把这个数值抄进成片说明而应该通过站点坐标计算、GPS 轨迹或车辆里程表逐一验证。这个差异本身就是后期脚本要解决的问题。1.3 技术主线是“素材与数据互相校验”这条 POV 项目的主线不是“怎么把视频拍得好看”而是“视频、站点表、轨迹数据如何互相校验”。举例来说报站器说到了某站但视频里没有站牌可能素材没拍完整。GPS 轨迹显示走了 15.8 公里但站点累计直线距离只有 14 公里这是正常的因为公交要绕行、进站、转弯。视频里出现了站名但 CSV 里漏了这条记录说明站点表需要返工。成片里字幕写的是上行方向但原始文件名和拍摄时间对不上说明素材归档出了问题。后续每一个步骤都围绕这个主线展开。先建立站点数据然后采集视频和轨迹再用脚本把里程算出来最后用检查表把四层产出物对齐。2. 出发前把线路基础信息和站点表固化成CSV2.1 线路口径表不要等到拍摄后才定义88路“哪一趟”公交线路最容易忽略的问题是方向口径。88 路从石家庄东站往中心城区西部是一个方向从中心城区西部返回石家庄东站是另一个方向。两个方向的站序可能不完全对称有些站只在其中一个方向停靠有些路段可能实行单行组织。出发前先用一张基础信息表把这个项目要记录的“哪一趟”锁死字段含义建议记录位置验证方式线路编号88路文件名、视频字幕车头路牌、站牌方向石家庄东站往中心城区西部 / 反向文件名、说明文档车辆侧牌、报站器起点站具体首末站名CSV 首行官方站牌、实时公交终点站具体首末站名CSV 尾行官方站牌、实时公交里程口径约16公里标注为“资料参考值”说明文档实际累计坐标验证拍摄日期年-月-日文件名、视频元数据车内外时钟车号具体车辆自编号说明文档车身、智能调度屏票价规则是否为单一票价说明文档车厢票价表如果原始资料没有给出某个首末站的精确名称不要凭记忆补。正确做法是保留“中心城区西部方向”这类描述到现场拍下站牌后再补精确站名。缺失字段可以留空但不能写成不准确的“事实”。2.2 站点数据字段设计在车上用手机备忘录一条条记站名回到电脑以后再整理会出现很多问题站序乱、字写错、上下行混在一起、漏掉临时站。推荐做法是把站点表设计成一条条 CSV 记录每个字段都有明确作用。最小可用字段如下字段类型说明stop_order整数站序从 1 开始stop_name文本与站牌一致的站名lat浮点数纬度WGS84 坐标系lng浮点数经度WGS84 坐标系arrive_offset_sec整数距起点离站时刻的秒数偏移note文本换乘、折返、临时站等备注lat 和 lng 可以先用空值占位拍摄结束后用地图坐标补齐。arrive_offset_sec 是后期字幕和时间轴对齐的重要依据记录方式可以是在车辆离站瞬间用秒表软件记一个 Unix 时间戳也可以事后观看视频在关键站帧位置读取时间码。2.3 建一个最小CSV模板下面的 CSV 是结构模板站点名均为示例不是 88 路真实站表。实际制作时要把 stop_name、经纬度和到达秒数替换成现场核对结果。stop_order,stop_name,lat,lng,arrive_offset_sec,note 1,起点站A,38.010000,114.500000,0,示例数据非真实线路 2,途经站B,38.012000,114.510000,185,示例数据非真实线路 3,途经站C,38.016000,114.520000,412,示例数据非真实线路 4,终点站D,38.021000,114.535000,650,示例数据非真实线路建好 CSV 后可以先写一个简单的校验脚本检查是否存在空站名、站序是否连续、是否存在重复站名import csv with open(stations.csv, encodingutf-8) as f: rows list(csv.DictReader(f)) names [] for i, row in enumerate(rows, start1): if not row[stop_name].strip(): print(f第{i}行站名为空) names.append(row[stop_name]) if len(set(names)) ! len(names): print(存在重复站名请核对上下行是否混用) orders [int(row[stop_order]) for row in rows] if orders ! list(range(1, len(rows) 1)): print(站序不连续请检查)这段代码的价值在于把“应该没有重复站名、站序应该连续”这类人工检查变成程序检查。哪怕素材再乱CSV 结构没问题后期脚本就不会因为缺字段而中断。3. 拍摄现场视频、报站音频、GPS时间线要同步采集3.1 设备、镜头角度与码率选择POV 拍摄不一定要用昂贵设备。手机、运动相机、行车记录仪都可以但有三点会影响后期质量镜头视野要足够覆盖车道和站台最好使用广角或超广角。固定要牢固避免车辆颠簸时画面晃动。日期时间要准确因为站点字幕依赖时间线对齐。画面设置上优先使用 1080P、30fps 或更高。这个分辨率足够看清楚站牌文字不会产生过大的素材体积。以 20Mbps 码率估算一分钟素材约 150MB20 分钟约 3GB。实际占用量取决于编码器、动态场景和码率设置拍摄前要估算好存储剩余空间。推荐设置参考参数学习/练手环境成片发布环境分辨率1080P1080P 或 4K帧率30fps30fps码率自动/标准较高恒定码率日期时间开启并校准必须校准防抖尽量开启结合云台或后期稳定收音机内麦克风外接或保留车内原声3.2 开始拍摄前要做三组时间对齐公交 POV 最怕的问题不是画面模糊而是“视频时间、站点时间、GPS 时间三条时间线对不上”。第一镜头开机后先拍 3 秒的当前时间画面或者用手机拍一张车辆仪表盘时钟作为参照。第二在起点站离站瞬间用手机秒表或纸质记录记下时刻。第三如果使用 GPS 轨迹记录仪或手机地图轨迹记录要确认采样间隔通常 1 秒一次比较合适并在站点停靠时确认轨迹点没有明显漂移。建议在每一站报站之后用较轻的声音补一句“当前到 XX 站”作为音频标记。这样即使后续没有 GPS 轨迹也可以依据音频波形快速找到每个站点的时间偏移。3.3 用ffprobe检查素材元数据拍摄结束后、剪辑开始前先检查原始素材的基本信息避免压了很久才发现文件损坏或时间不对。ffprobe -v quiet -print_format json \ -show_format -show_streams \ raw/20260602_88_east_to_west_01.mp4 \ docs/video_meta.json这个命令会把时长、编码、分辨率、创建时间等信息导出到 JSON 文件。查看关键字段python -m json.tool docs/video_meta.json | head -n 80重点核对以下信息字段位置检查目的durationformat素材时长是否与行程区间匹配create_timeformat.tags拍摄时间是否与实际一致codec_namestreams确认是否为可编辑的编码width / heightstreams确认分辨率r_frame_ratestreams确认帧率如果 create_time 与出发时间相差几小时说明机内时钟未校准。不要等到字幕合成阶段再发现这个问题那时返工成本非常高。注意ffprobe 输出的时长只是容器或流元数据不等于有效画面时长。有些录制设备在断电异常时会导致元数据时长和真实播放时长不一致压制前最好用播放器拖动到结尾确认。4. 后期压片裁剪、字幕、统一格式的ffmpeg脚本4.1 先做缩小范围的粗剪原始素材从开机到关机往往会有上下车、等候、聊天等片段。建议先用一个粗剪命令把“起点站离站”到“终点站停稳”之间的区间截出来再在这个区间上做字幕和压制。ffmpeg 的-ss参数放在-i之前会启动快速定位速度很快但会落在最近的关键帧附近如果发现切点误差明显可以把-ss放到-i之后进行精确逐帧解码定位代价是解码时间变长。示例ffmpeg -ss 00:02:18 -to 00:47:36 \ -i raw/20260602_88_east_to_west_01.mp4 \ -c:v libx264 -preset slow -crf 20 \ -c:a aac -b:a 128k \ -movflags faststart \ cut/88_full_20260602.mp4参数含义-preset slow提高压缩效率压出来的文件在同等码率下画质更好代价是编码时间更长。-crf 20控制质量数值越小画质越高文件越大。18 到 23 是比较常见的区间。-movflags faststart让视频可以在网页端边下边播适合发布到流媒体平台。Windows 的 cmd 不支持命令行末尾的\换行符实际运行时需要把整条命令写成一整行。4.2 用drawtext压入线路信息和站点定位公交 POV 成片通常会在左上角或顶部压一行说明文字内容类似“石家庄公交88路 石家庄东站方向”。使用 ffmpeg 的 drawtext 滤镜可以实现ffmpeg -i cut/88_full_20260602.mp4 \ -vf drawtextfontfile/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc:text石家庄公交88路 石家庄东站往中心城区西部:fontsize48:fontcolorwhite:borderw3:bordercolorblack:xw-tw-60:y50 \ -c:v libx264 -preset medium -crf 20 \ -c:a copy \ output/88_pov_20260602.mp4这里最容易踩坑的是中文字体。drawtext 默认字体通常不含中文字形即使包含字库路径也必须准确。Linux 常见路径需要根据发行版确认Windows 常见字体路径中的冒号在 filter 语法里要用转义处理。如果不确定字体路径可以先执行一个只输出几帧的测试ffmpeg -i cut/88_full_20260602.mp4 -t 1 \ -vf drawtextfontfile你的字体文件路径:text测试中文:fontsize48 \ test.png查看 test.png 里的文字是不是正常中文再执行完整压制。4.3 素材命名与目录结构POV 项目会积累大量素材不做统一命名三个月后再找“那天拍的那趟车”会非常痛苦。推荐目录结构88路pov/ ├── raw/ # 原始素材 ├── cut/ # 粗剪素材 ├── output/ # 成片 ├── data/ # CSV、GPX、JSON └── docs/ # 拍摄说明、检查表文件命名建议包含日期、线路、方向、序号四类信息20260602_88_east_to_west_01.mp4 20260602_88_west_to_east_01.mp4即使方向在视频画面中一眼能看出来也要写进文件名。文件名本身是一种最基础的数据索引不应该依赖人脑记忆。5. 用Python把站点与里程核到数据表里5.1 为什么里程不建议直接抄导航结果打开任何一个地图 App输入两个首末站会得到“规划距离”。但这个距离和公交实际行驶距离通常不一致。原因包括地图默认规划可能走了社会车辆路线而公交有专用道或特定转弯限制。公交必须在规定站点停靠进出站会产生额外行驶距离。遇到临时施工公交会按交通管理部门发布的绕行方案行驶。所以“全程约16公里”这类信息应该当作待验证的参考值而不是直接写进成片说明的结论。验证方式之一是结合站点经纬度用 Python 逐段计算相邻站点距离并累计。5.2 用哈弗辛公式计算相邻站点直线里程相邻站点之间的道路通常不是直线所以哈弗辛公式算出来的是“大圆球面距离”一定小于等于实际道路里程。它适合用来排查明显错误比如两站坐标反了、GPS 点漂移严重、站点顺序错乱。创建 calc_distance.pyimport csv import math import sys def haversine_km(lat1, lng1, lat2, lng2): 计算两个经纬度坐标之间的球面距离单位为千米。 radius 6371.0088 p1 math.radians(lat1) p2 math.radians(lat2) dp math.radians(lat2 - lat1) dl math.radians(lng2 - lng1) a (math.sin(dp / 2) ** 2 math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2) return 2 * radius * math.asin(math.sqrt(a)) def main(csv_path): with open(csv_path, encodingutf-8) as f: rows list(csv.DictReader(f)) prev None total 0.0 print(站序\t站名\t分段(km)\t累计(km)\t到站偏移(s)) for row in rows: lat float(row[lat]) lng float(row[lng]) seg 0.0 if prev is not None: seg haversine_km(prev[0], prev[1], lat, lng) total seg prev (lat, lng) print(f{row[stop_order]}\t{row[stop_name]}\t f{seg:.3f}\t{total:.3f}\t{row[arrive_offset_sec]}) print(f\n累计里程约: {total:.3f} km) if __name__ __main__: main(sys.argv[1] if len(sys.argv) 1 else stations.csv)运行python calc_distance.py data/stations_88.csv这里一定要明确两个坐标系前提CSV 中的坐标使用纬度在前、经度在后的顺序坐标系统一按 WGS84 处理。如果坐标来自某些地图 App 的 GCJ-02 坐标系和标准 GPS 轨迹混在一起计算出来的距离会偏差几十到几百米而且这种错误极难排查。5.3 输出结果怎么解释假设 CSV 中是上文的示例数据运行结果大概是站序 站名 分段(km) 累计(km) 到站偏移(s) 1 起点站A 0.000 0.000 0 2 途经站B 0.911 0.911 185 3 途经站C 0.945 1.856 412 4 终点站D 1.420 3.276 650 累计里程约: 3.276 km这个结果只有示范意义因为它用的坐标是虚构占位数据。把真实站名和真实经纬度填入后公式计算出的累计结果是“相邻站点直线距离和”不是“公交实际行驶里程”所以数值会低于“约16公里”的资料描述。两者差异主要来自以下方面误差来源说明处理建议道路曲线直线距离不含弯道损耗使用 GPS 轨迹累计差分里程进出站路径站台前后有较长变道距离不要只用站台坐标坐标漂移隧道和高架下 GPS 漂移大与视频时间线交叉核对坐标系混用GCJ-02 与 WGS84 混用统一坐标系记录来源要达到接近实际里程的数值需要把视频时间轴和 GPS 轨迹对齐按秒累计相邻轨迹点。这个过程比站点直线距离复杂但如果素材本身带有稳定 GPS 轨迹是值得做的。6. 发布前检查清单、常见坑和扩展方向6.1 成片发布前核对表公交 POV 的“测试环境”是本地快速验证素材能播放、字幕能显示、音频正常。而“发布环境”要考虑信息准确性、观看体验和长期可管理性。发布前按这张表逐项核对检查项检查方式通过标准线路号与方向对照车头路牌和文件名字幕、文件名、画面三者一致上下行站序对照报站器和站牌CSV 站序与视频一致起止区间完整拖动到视频首尾包含起点离站到终点停稳时间偏移用报站音频和字幕对比字幕站名与实际报站吻合中文字幕单帧截图放大检查无方框、无乱码、不压人脸图像稳定性快速预览整段无长时间剧烈抖动音频监听前、中、后段无爆音、无断续编码ffprobe 查看输出流播放器兼容存在 faststart文件命名查看 output 目录日期、线路、方向、序号齐全6.2 三个容易漏掉的坑第一个坑是把上行和下行的站名混在一起。88 路从石家庄东站往中心城区西部和反向返回的路径不完全一致是正常现象如果只改字幕里的终点站名却沿用另一方向 CSV 的站序就会整段错位。解决方法是在 CSV 文件名里直接带上方向比如 stations_88_east_to_west.csv而不是笼统写成 stations_88.csv。第二个坑是视频时间轴和站点偏移没有对齐。很多人会在拍摄结束几小时后才整理站点时间偏移凭记忆填秒数误差可能达到几十秒。正确做法是拍摄过程中用报站音频做标记或者后期在剪辑软件时间线上逐个定位“报站器刚播完上一站”的时间点。第三个坑是 drawtext 中文字体路径错误。ffmpeg 不会因为找不到字体就报错退出它可能生成文字为空或者整段没有字幕的视频。必须先用单帧测试确认中文渲染成功再执行全片压制。6.3 排错顺序和一个可复用的检查清单如果成片出现了“站名对不上、里程差很多、字幕乱码”这类问题按下面顺序排查先看原始视频能否正常播放排除素材损坏。再确认文件名中的日期、方向与拍摄记录是否一致。然后核对 CSV 的站序、坐标字段是否完整。接着检查 ffprobe 输出的时间信息确认素材时间没有偏移。再验证字幕字体路径和中文渲染。最后对比计算出的累计里程和 GPS 轨迹里程判断是数据问题还是道路走向差异。可以把这段逻辑整理成下面这份最小化清单直接放进 docs 目录[ ] 原始素材能播放 [ ] 日期、方向与文件名一致 [ ] CSV 中无空站名站序连续 [ ] 视频起止区间完整 [ ] 字幕中文渲染正常 [ ] 计算里程与资料口径差异已解释 [ ] 成片命名规范 [ ] 素材和脚本已归档6.4 后续可以扩展的方向88 路这条 16 公里线路完成一次完整记录后技术路线可以继续向这些方向扩展把真实站点经纬度转成 GeoJSON在地图