资讯动态

轨迹预测工程实践:目标检测跟踪与LSTM预测全流程解析

发布时间:2026/10/14 16:17:39 来源:尧图企业网站定制
简介压缩包提供了一套基于深度学习的轨迹预测项目源码适合有一定Python基础、希望入门或对比序列预测模型的开发者使用。资源共11个文件以7个Python脚本为核心涵盖LSTM、GRU、逻辑回归三种模型实现并配套数据读取、预处理及轨迹分析模块同时包含模型checkpoint文件和CSV格式的轨迹数据集压缩包整体约723KB便于快速下载研读。目前已有230人学习浏览。代码中不仅提供了LSTM与GRU的训练与保存流程还设置了对比实验脚本可直接运行逻辑回归作为基线帮助理解不同模型在轨迹预测任务上的效果差异。数据读取模块和轨迹相关脚本则为完整复现实验提供了清晰路径对学习时间序列预测、模型对比和工程组织方式都有实用参考价值。1. 轨迹预测的完整代码包不只是检测和跟踪关键是下一步去哪拿到的这份“完整代码.zip”解压之后是一整套可以端到端跑通的轨迹预测工程不是那种只给两个模型文件就完事的半成品。它的主线很清晰目标检测拿到框跟踪器把框串成轨迹然后一个时序模型吃进过去六帧位置输出未来一到两秒的预测轨迹。这套链路在自动驾驶、车路协同监控、园区安防里都是刚需你问“这个车接下来要往哪走”本质上就是在问一条时空序列的下几个点在哪里。适合的人群也很明确已经在跑目标检测和跟踪、想做轨迹预测但不想从零搭模型的人以及写论文需要一个完整基线工程的在校学生。下面按我实际拆包的顺序把环境、数据结构、模型训练、推理可视化一条线讲透最后集中说踩过的坑。2. 拆包看结构拿到 zip 之后先搞清楚这五块再动手2.1 目录规划一个能跑的轨迹预测工程至少要有这五个模块把 zip 解压之后我第一件事不是看模型代码而是先列目录树。一个真正能复现的轨迹预测工程结构上一般可以分成五块数据接入层读标注、读视频帧、检测与跟踪层负责把视频帧变成轨迹段、预测模型层时序模型本体、训练与评估层跑 batch、算 loss、出指标、可视化与导出层把预测结果画回帧里。这套代码包在这五块上基本都有落位但顺序上有个细节值得说它把可视化工具放在了工程根目录下独立的一个文件夹里而不是塞在某个子模块中这个安排会让你在调试的时候省很多事——因为轨迹预测的中间结果如果不画出来看基本等于黑匣子。我一般会把这类工程按“输入-感知-预测-评估”的流水线去读而不是按目录名去读因为很多代码包为了组织方便会把预处理的函数放在训练脚本里把模型结构和推理脚本分开放在不同的包下单看目录名会被带偏。建议拆包后第一件事是先跑通一份 README 或者 demo 脚本确认输入输出格式再回头精读每个模块。完整代码.zip ├── configs/ # 训练与推理的 yaml 配置 ├── data/ # 标注数据、视频样例、预处理脚本 ├── detector/ # 目标检测封装加载权重、做推理 ├── tracker/ # 多目标跟踪封装轨迹片段生成 ├── predictor/ # 轨迹预测模型LSTM/Transformer 类结构 ├── train.py # 训练入口 ├── val.py # 验证入口 ├── visualize.py # 轨迹回放与可视化 └── utils/ # 坐标变换、评估指标、日志工具目录确认之后还需要检查一件事有没有提供预训练权重权重文件放哪里。这份代码包里在 configs 下的配置中引用了相对路径去加载权重比较常见的做法是把权重放在一个名为 weights 的目录下但 zip 解压后有时该目录为空就需要你根据配置里的路径手动从训练产物中拷贝。建议路径写绝对不行用相对路径的写法在换机器跑的时候最容易翻车后面避坑章节会展开。2.2 配置项逐条解读检测阈值、帧率、预测时域全是压在一条链路里的参数这个代码包把关键参数统一收敛到了 configs 下的 yaml 文件里这个设计方向是对的但它的默认值不一定适合你的场景读配置时要特别留意几个参数因为它们决定的不只是预测模型本身而是整个链路的输入质量。第一处是检测置信度阈值代码包里默认设成了 0.45这个值在路侧监控这种机位固定但目标小的场景偏保守第二处是跟踪器允许的最大丢失帧数默认 30 帧意味着目标被遮挡 30 帧后会开一条新轨迹对短遮挡场景可以接受但路口有路灯杆反复遮挡的场景建议降到 15 帧左右第三处是轨迹预测的输入窗口和输出时域这份代码包默认输入最近 6 帧每帧间隔按视频帧率算输出未来 12 帧对应 0.4 秒输入、0.8 秒输出。这组默认值不是随便写的。0.4 秒的输入窗口大约能覆盖目标车辆在 30 FPS * 0.4 12 个视频帧中的运动变化对一辆以 10 米/秒行驶的车辆来说0.4 秒内大约运动 4 米这个尺度足够让 LSTM 类模型提取到明显的速度与转向特征而 0.8 秒的预测时域又短到不至于让纯数据驱动模型发散到离谱的程度。真要用在实车或者路侧设备上建议大家把预测时域先调到 2 秒看看模型在长外推下的表现再决定要不要上更复杂的模型结构。# configs/trajectory_pred.yaml 节选 model: name: TrajLSTM input_len: 6 # 输入轨迹点数量对应 6 帧 pred_len: 12 # 预测轨迹点数量对应未来 12 帧 hidden_dim: 128 # LSTM 隐藏层维度 num_layers: 2 # LSTM 层数 dropout: 0.1 data: sample_interval: 1 # 每隔多少帧取一个轨迹点 normalize: true # 是否对坐标做归一化 max_speed_filter: 40 # 超过该速度m/s的轨迹视为异常并剔除 tracker: max_age: 30 # 目标丢失后保留轨迹的最大帧数 min_hits: 3 # 至少连续命中 3 帧才输出轨迹 conf_thres: 0.45 # 检测置信度阈值这份配置里带 max_speed_filter 这个参数是很多人第一次跑这个代码包时会忽视的。它的作用是剔除跳变轨迹——也就是同一目标在两帧之间位置突变超过物理可达范围的情况。如果这一项不设或者设得过大跳变轨迹会被当成高质量样本送进训练集直接污染时序标签。我的经验是普通城市路口场景车速上限按 25 米/秒约 90 公里/小时来过滤就已经足够了设到 40 意味着容忍货车、个别超速车等极端情况但也会把一些跟踪跳变点放进来。3. 把视频帧变成轨迹检测、跟踪与坐标归一化的完整链路3.1 检测与跟踪的串接方式为什么轨迹预测前要先过一遍跟踪器轨迹预测的输入不是原始视频帧而是“同一目标 ID 连续出现的坐标序列”。目标检测只能告诉你这一帧有哪些地方可能有目标但没法告诉你两个时刻的框是不是同一个目标这个对应关系要靠跟踪器来维护。所以代码包的做法是先用检测器逐帧推理再把这些框交给跟踪器做数据关联输出形如(frame_id, target_id, cx, cy, w, h)的轨迹片段最后才轮到预测模型消费这些数据。# detector_tracker_pipeline.py 简化的检测-跟踪串接过程 import cv2 from detector import YOLODetector from tracker import ByteTrackWrapper detector YOLODetector(weightsweights/yolov8m.pt, conf_thres0.45) tracker ByteTrackWrapper(max_age30, min_hits3) cap cv2.VideoCapture(data/sample_intersection.mp4) fps cap.get(cv2.CAP_PROP_FPS) while True: ret, frame cap.read() if not ret: break dets detector.infer(frame) # 每帧检测结果: [x1, y1, x2, y2, score, class] tracks tracker.update(dets, frame) # 更新轨迹: 每个目标一个 ID for track in tracks: cx (track.x1 track.x2) / 2 cy (track.y1 track.y2) / 2 print(fframe{cap.get(cv2.CAP_PROP_POS_FRAMES):.0f}, id{track.track_id}, cx{cx:.1f}, cy{cy:.1f})这里要说明两个关键设计。第一取框的几何中心点作为轨迹点这个做法在大多数路侧监控场景够用因为检测框一般能稳定框住车辆主体中心点抖动在归一化后会被模型容忍但如果你的场景是行人密集的俯视视角建议换用框底边中点作为轨迹点因为俯视视角下检测框的高度会因行人姿态变化大幅抖动中心点的噪声反而最大。第二跟踪器这里用的是 ByteTrack 的封装方式它相比 DeepSORT 少了一个外观特征提取分支换来了更高的帧率对轨迹预测这种需要高刷新率轨迹点的场景其实是更合适的选型因为预测模型本身不关心 ReID 特征它只关心坐标序列的连续性。3.2 坐标归一化与数据集构建训练轨迹预测模型前必须做的三件事轨迹预测模型的输入是坐标序列但如果你直接把像素坐标喂给 LSTM会发现两个问题一是不同视频分辨率不同像素坐标数值范围差异大模型泛化能力会变得很差二是像素坐标是二维的模型很难直接从中感知“速度”和“加速度”这些运动学量得靠序列差分隐式学习。代码包的处理方式分三步等间隔采样、坐标归一化、差分特征拼接。# build_training_set.py 从轨迹片段构建 LSTM 训练样本 import numpy as np def normalize_track(track_xy, img_w1920, img_h1080): arr np.array(track_xy, dtypenp.float32) arr[:, 0] / img_w # x 除以图像宽映射到 [0, 1] arr[:, 1] / img_h # y 除以图像高映射到 [0, 1] return arr def make_sample(track_norm, input_len6, pred_len12): samples, labels [], [] for i in range(len(track_norm) - input_len - pred_len): x track_norm[i:i input_len] # 过去 6 点 y track_norm[i input_len:i input_len pred_len] # 未来 12 点 samples.append(x.reshape(-1)) # 拉平成 1D 向量 labels.append(y.reshape(-1)) return np.array(samples, dtypenp.float32), np.array(labels, dtypenp.float32)这里第 4 行到第 6 行是归一化把宽高坐标压到 0 到 1 之间损失函数计算时的数值稳定性会好很多第 9 到第 15 行是滑窗采样每次从一条长轨迹中截取输入长度 预测长度的片段前段做输入、后段做标签。滑窗步长默认为 1意味着相邻样本之间共享大部分历史帧这会让训练集样本之间高度相关容易造成验证指标虚高。如果你的训练时间不紧建议把步长改成 2 或 3让样本去重叠验证集指标会更有参考价值。还需要补充一个很多人忽略的点轨迹样本的均衡性。在真实路口数据中直行轨迹数量通常远远多于转弯轨迹如果你直接全量送进训练集模型会学成一个“只会直行”的预测器。这份代码包在数据构建环节没有做采样均衡所以我在实际使用时会对转弯轨迹做过采样让对方差比较大的样本在 batch 里出现的概率不低于 30%。4. 训练轨迹预测模型从损失函数到评估指标把黑匣子打开看4.1 LSTM 编解码结构为什么这里不用单层 LSTM 直接输出未来十二帧代码包里的预测模型采用的是 Encoder-Decoder LSTM 结构而不是一个简单的单层 LSTM 直接输出未来 12 个点。这两者的差异非常关键直接输出的做法是把未来 12 个点当成 12 个独立输出目标模型必须在一个时间步内同时生成所有未来坐标这等于要求它做一步到位的长程外推实际训练时会发现中后段预测点基本退化成匀速直线转弯场景完全失效。Encoder-Decoder 结构则把预测过程变成了逐点迭代Encoder 读完历史序列压缩成一个上下文向量Decoder 拿着这个向量逐步吐出后续点每一步的输入加上一步的输出这更接近轨迹的时序生成本质。# predictor/traj_lstm.py 轨迹预测 LSTM 核心结构节选 import torch import torch.nn as nn class TrajLSTM(nn.Module): def __init__(self, input_dim2, hidden_dim128, num_layers2, pred_len12): super().__init__() self.encoder nn.LSTM(input_dim, hidden_dim, num_layers, batch_firstTrue) self.decoder nn.LSTM(input_dim, hidden_dim, num_layers, batch_firstTrue) self.fc_out nn.Linear(hidden_dim, input_dim) # 输出 (cx, cy) def forward(self, hist): # hist: [batch, input_len, 2] 归一化后的历史轨迹 enc_out, (h, c) self.encoder(hist) dec_input hist[:, -1:, :] # 用最后一个历史点作为解码器首步输入 outputs [] for _ in range(self.pred_len): dec_out, (h, c) self.decoder(dec_input, (h, c)) pred self.fc_out(dec_out) outputs.append(pred) dec_input pred # 自回归把上一步输出作为下一步输入 return torch.cat(outputs, dim1) # [batch, pred_len, 2]这个结构里第 10 行是 Encoder 前向第 13 到第 20 行是自回归解码生成未来点。注意第 15 行解码器初始输入直接取了最后一个历史点坐标这在速度较快的目标上问题不大但如果目标在历史窗口最后一帧与未来第一帧之间有明显的机动比如急转弯解码第一步就会带一个偏置误差。一个常见的改进是让解码器也接收一个速度向量作为初始输入这样第一步输出会带有当前运动趋势信息。代码包里没有做这个改动但如果你在弯道场景测试发现预测轨迹总是往直行方向飘优先检查这一行。训练时的关键在于损失函数的选择。代码包默认用的是均方误差这对离群点非常敏感——跟踪器偶尔产生一个跳变点就会把梯度拉向错误方向。我更建议换成 Huber 损失它对小误差用均方、对大误差用绝对值在轨迹预测这种标签本身含噪声的任务里表现会稳一个档次。4.2 训练、验证与指标解读ADE 和 FDE 到底在衡量什么训练入口是 train.py它做的事情可以概括为读配置、建模型、加载 tracker 输出的轨迹数据、按滑窗切成样本、进入训练循环每个 epoch 结束跑一次验证计算 ADE平均位移误差和 FDE最终位移误差。这两个指标是轨迹预测的行业标准口径ADE 是预测的全部点与真实轨迹全部点之间的平均欧氏距离衡量整体形状拟合程度FDE 只看预测终点与真实终点的距离衡量最终落点的精准度。python train.py --config configs/trajectory_pred.yaml --gpu 0 # 期望看到的训练输出形式节选 Epoch [10/100] train_loss0.00231 lr1e-3 Epoch [10/100] val_ade0.056 val_fde0.089 val_ade_m0.87 val_fde_m1.34在这个工程里val_ade 和 val_fde 是归一化坐标下的数值val_ade_m 和 val_fde_m 是乘以图像尺寸换算出的物理距离单位是米。你判断模型是否可用的核心经验值当预测时域是 0.8 秒时FDE 在 1 到 3 米之间、ADE 在 0.5 到 1.5 米之间属于可接受范围如果 FDE 超过 4 米说明模型实际上大概率是在复读历史运动趋势而不是在“预测”转弯或者减速。一个常用的检查手段是把 validation 样本的历史轨迹和预测轨迹画在同一张图上如果预测轨迹是历史轨迹的线性延长而且每条的延长方向都一样那就要去查训练数据里是否转弯样本占比过低或者隐藏维度太小导致模型欠拟合。还有一个容易被忽视的训练细节坐标归一化要和推理时完全一致。训练时你用的是 1920x1080 作为分母如果推理时输入视频分辨率变成了 1280x720模型输出的预测坐标会被错误地按 1920x1080 去还原导致整条预测轨迹偏移。建议在配置里固定一个标准推理分辨率检测前先把视频缩放或填充到这个分辨率。5. 推理与可视化把预测轨迹画到视频帧里这一步能暴露 80% 的问题5.1 多目标预测的循环结构不同目标共用同一模型但要注意状态隔离推理时模型是不需要再训练的你只需要对视频逐帧跑检测和跟踪每跟踪出一个目标就把它的历史坐标序列收集起来构建成模型输入格式前向一次得到未来轨迹点再换算回像素坐标画在帧上。这个过程要实现多目标并行预测常见做法是维护一个字典键是目标 ID值是最近 N 帧的轨迹点列表每帧更新一次轨迹点不足 N 帧时就不预测等待历史窗口被填满后再开始推理。# visualize.py 多目标轨迹预测与可视化核心逻辑 import cv2, numpy as np, torch history {} # target_id - list of (cx, cy) 归一化坐标 while True: ret, frame cap.read() if not ret: break dets detector.infer(frame) tracks tracker.update(dets, frame) for t in tracks: cx, cy (t.x1 t.x2) / 2 / frame_w, (t.y1 t.y2) / 2 / frame_h if t.track_id not in history: history[t.track_id] [] history[t.track_id].append((cx, cy)) history[t.track_id] history[t.track_id][-input_len:] # 只保留最近的 T 帧 if len(history[t.track_id]) input_len: inp torch.tensor([history[t.track_id]], dtypetorch.float32) with torch.no_grad(): pred model(inp)[0].numpy() # [pred_len, 2] for px, py in pred: # 还原像素坐标并画点 x, y int(px * frame_w), int(py * frame_h) cv2.circle(frame, (x, y), 3, (0, 0, 255), -1) cv2.imshow(traj_pred, frame) if cv2.waitKey(1) 0xFF ord(q): break这里第 7 行到第 11 行是历史窗口维护。一个常见的低级错误是忘记做长度截断导致 history 中某个目标的轨迹点越攒越多模型输入维度越来越大直接报错或者被迫每次重新计算全量序列浪费算力。第 13 行的with torch.no_grad()是推理时必写的否则你会看到显存被缓慢吃满因为默认会构建计算图。在可视化阶段最容易暴露的问题是可视化与推理坐标系不一致。如果检测和跟踪在缩放后的帧上完成而画图用的是原始帧预测轨迹会整体偏移一段距离。代码包的处理方式是把画图也统一在缩放后的帧上完成最后写视频时统一分辨率这样调试时最不容易出偏差。5.2 三种典型失败形态的图形特征轨迹预测模型的效果从数值指标上也许看不出问题但可视化往往一眼就能暴露问题。第一种是“直行复读机”所有目标的预测轨迹都接近历史方向的直线延长转弯场景预测弧度过大这通常是转弯样本在训练集里占比不足或者隐藏层维度太小。第二种是“抖动”预测轨迹呈现高频左右摆动看上去很不平滑这通常是输入轨迹本身噪声偏大建议检查跟踪器输出的中心点是否稳定或者归一化时坐标精度是否够。第三种是“漂移加速”预测轨迹的速度明显快于真实目标在长预测时域下尤其明显这意味着模型没有学到减速行为可以考虑在损失函数里加入一个速度一致性惩罚项或多收集一些减速场景的样本。排错时可以按这条路径走先看训练集的真实轨迹是否平顺再看单样本的输入输出是否对齐最后看模型预测是系统性偏差还是随机噪声。系统性偏差往往指向数据层面随机噪声则指向模型容量或训练收敛问题。6. 轨迹预测的避坑实录五条值得写进笔记的踩坑记录6.1 预测轨迹整体偏移且偏移量与目标在画面中的位置强相关现象预测轨迹大致形态合理但整体位置偏向图像边缘方向越靠近画面边缘偏移越大。原因排查后发现是画面镜头存在明显畸变检测框中心点按像素坐标直接参与训练边缘区域的像素距离在真实物理空间中会被放大或缩小模型实际上在拟合带畸变的坐标空间。解决先对检测框中心点做镜头畸变校正再送入归一化与训练流程。这份代码包没有内置校正模块需要你自己接一个基于标定参数的映射函数。如果只是做实验对比畸变影响不大时可以忽略但要上真实路口场景就必须处理。6.2 模型训练 loss 降不下去卡在 0.004 附近不动现象训练到大约第 30 个 epoch 后train_loss 稳定在 0.004 附近不再下降val_ade 也不再改善。原因检查训练集后发现有相当比例的轨迹片段里包含目标静止帧——比如红灯停车场景。静止帧之间坐标几乎完全相同模型需要同时拟合“静止”和“运动”两种模式单一一组参数很难同时满意损失函数因此停在一个骑墙的位置。解决最直接的办法是在滑窗采样时剔除掉历史段内目标位移小于一个像素阈值的片段把纯静止样本单独筛出来或者把它们单独做成一个二分类任务先学会判断运动/静止再回归轨迹。6.3 可视化时预测轨迹闪烁不稳定同一个目标相邻两帧预测结果跳跃很大现象目标 ID 不变但它的预测轨迹在相邻两帧之间出现明显的位置跳变数值上表现为前后两帧的预测起点接近、但预测终点相差很大。原因查看代码后发现当跟踪器在某一帧没有更新到目标 ID 时比如短暂遮挡history 序列里依然保留旧坐标下一帧目标重新出现时历史窗口会同时包含遮挡前的旧点和重新出现后的新点两个点在时间上并不连续模型被这段拼接后的伪轨迹带偏。解决在维护 history 时需要记录每个点的帧号。一旦发现相邻两个轨迹点的帧号间隔大于 1就把这段轨迹从该断裂处截断宁可推迟预测开始时间也不要把断裂轨迹拼接起来送进模型。6.4 训练集精度很高换一段视频效果骤降现象在自采的第一段路口视频上 ADE 到了 0.5 米但换到另一个路口的视频预测轨迹明显变差形态甚至开始混乱。原因两个路口的机位高度和俯仰角不同。低机位视频中车辆检测框的侧视角占比大中心点随车身尺寸变化明显高机位俯视视频中检测框几乎贴地。坐标归一化只处理了分辨率并没有处理视角差异模型在低机位数据上学的运动模式直接套到高机位视角上自然失真。解决严格来说想要一个模型覆盖多种机位视角需要做坐标视角归一化——比如把图像坐标映射到地面平面坐标系后再做预测或者干脆按机位分组分别训练模型。代码包默认只支持单一机位这一点在使用前就要想清楚。6.5 批量推理时 GPU 显存缓慢上升直到溢出现象程序可以正常跑但大约跑 2000 帧后显存溢出报错。原因推理循环里没有把每一帧的检测输出张量释放掉同时前向推理时没有包裹torch.no_grad()每次前向都往计算图里累积梯度信息。两处叠加导致显存随帧数线性增长。解决推理前把所有模型切换为model.eval()模式并在推理调用外套with torch.no_grad()每帧处理结束时把 batch 显存变量手动置空。另外建议每处理 500 帧调用一次torch.cuda.empty_cache()平滑显存占用曲线。7. 把评估指标换算成真实物理量从像素误差到路口可用的判断标准训练完模型只是第一步真正要回答的是“这套预测结果放到真实路口能不能用”。像素坐标下的 ADE 和 FDE 会因为机位高度、镜头焦距不同产生巨大差异所以在工程落地环节我一般会先做一个单应性变换标定把像素坐标映射到地面平面坐标以米为单位重新计算 ADE 和 FDE。这个变换只需要在画面中选四个地面点然后解一个 3x3 的单应矩阵。# calibrate_homography.py 用四个地面点做像素到地面坐标的单应性映射 import cv2 import numpy as np src_pts np.array([[100, 800], [1200, 800], [400, 400], [1400, 400]], dtypenp.float32) dst_pts np.array([[0, 0], [10, 0], [2, 8], [11, 8]], dtypenp.float32) # 单位: 米 H, _ cv2.findHomography(src_pts, dst_pts) pred_pix np.array([[500, 600], [510, 610]], dtypenp.float32).reshape(-1, 1, 2) pred_ground cv2.perspectiveTransform(pred_pix, H).reshape(-1, 2) print(pred_ground) # 输出地面坐标系下的预测点米用地面坐标重新评估之后什么时候这套模型能直接用于预警就比较好判断了。ADEr 在 1.5 米以内、FDEr 在 3 米以内时可以用于低速园区场景的碰撞提醒城市快速路场景则要求 FDEr 在 4 米以内且置信度超过 80%因为这一米两米的误差直接决定了安全距离的冗余设计。这组数值是我个人实际使用时的经验边界测试时可以作为基准但不应该当成标准——不同设备标定精度、不同场景因素阈值会有浮动。最后一个建议是把模型的预测结果和跟踪轨迹一起存成 JSON 或 CSV 格式再单独做回放分析。因为实测中你会发现光看可视化很难去统计“这个弯道的预测误差到底是多少”只有把每个目标的预测轨迹和真实后续轨迹落成带 ID 和时间戳的数据你才有机会定量复盘模型在哪些场景下持续失效。从那以后我每次拿到新的轨迹预测工程都会先构建这样的回放分析脚本再决定要不要调整模型结构或训练数据分布。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑