驾驶视频检索如何落地?轨迹引导的视频嵌入学习解析
2026/9/3 6:39:39 网站建设 项目流程

自动驾驶、智能座舱、路况记录仪这几类场景每天都在产生大量视频数据,但真正要用的时候,会发现一个问题:视频不是文本,搜不到“那个路口左转后被加塞的片段”。尤其在自动驾驶模型训练、场景挖掘、事故溯源这样的任务里,视频检索往往成为数据闭环中最消耗人工的瓶颈。

最近读到 TraVEL 这个研究工作,标题是TraVEL: Trajectory-Guided Video Embedding Learning for Driving-Video Retrieval。它把注意力放在一个很有意思的思路上:不做通用的视频检索,而是面向驾驶视频,用轨迹信息来引导视频嵌入的学习。这个“轨迹引导”和“驾驶视频特有模态”的组合,恰好切中了自动驾驶数据闭环里一个非常实际的问题。

这篇博客我想从问题出发,拆解 TraVEL 为什么值得关注,驾驶视频检索和通用视频检索到底差在哪里,“轨迹引导”到底引导了什么,最后给出一个大致的复现思路和工程落地建议。没有堆砌无损指标,重点整理清楚原理、流程和适用于真实项目的框架设计。

1. 为什么要单独研究“驾驶视频检索”

先看一个真实场景。某个自动驾驶团队积累了几十万条路测视频,一天算法工程师想找一批“前方车辆突然切入”的片段用于模型训练。如果用人工方式翻视频,至少要经历“回放、确认、打标签、切片、清洗”五个环节,一个熟练标注员一天能处理的片段数非常有限。而如果直接拿现成的视频检索模型去搜,结果通常也很不稳定,原因很简单:通用视频检索模型是为互联网视频设计的,它理解的“精彩片段”可能是“烟花”“无人机航拍”“跑步”,但自动驾驶需要的是“目标切入”“变道压线”“行人横穿”这类具有明确几何和运动语义的片段。

TraVEL 的研究动机正在于此。它提出专门针对驾驶视频的检索方案,让用户通过自然语言描述视频里的“场景内容”或者“车辆运动状态”,就能从大规模驾驶视频库中召回相关片段。

这个方向的意义不只是少招几个标注员。在自动驾驶数据闭环中,检索能力直接影响以下三个环节:

  • 场景挖掘:从路测数据里找到典型 corner case,用于补充训练集。
  • 模型评测:评测某个感知模型在“雨夜近距离切出”场景下的表现,需要按条件检索测试集。
  • 问题追溯:发生误判后,需要回看相同道路结构下历史视频的表现。

这三个环节的共同点是:查询条件是多层次的。既包括天气、光照、道路类型这样的全局属性,也包括“目标车是否在减速”“本车是否在左转”这样的局部运动属性。而一辆车在某个时间窗口内的运动轨迹,恰恰把这些属性串成了一条时空上下文链。

一句话判断是:驾驶视频检索不是通用视频检索的子集,它是一个有独立模态、有独特对齐逻辑的新检索问题。TraVEL 的目标,就是为这个问题设计一套更合理的嵌入学习方法。

2. 视频嵌入检索的基础逻辑

在展开 TraVEL 之前,有必要先对齐一下视频嵌入检索的基础概念。

视频嵌入检索做的事情可以概括成一句话:把视频和文本都映射到同一个向量空间,然后用向量距离度量相关性。

通常流程如下:

  1. 将视频按均匀间隔采样若干帧,送入视频编码器提取时空特征,得到多个帧级特征向量。
  2. 对帧级特征做平均池化或注意力池化,得到整个视频的嵌入向量v
  3. 将文本查询送入文本编码器,得到查询嵌入向量t
  4. 训练阶段通过对比学习,拉近匹配对(v, t)的距离,推开不匹配对。

模型训练完成后,检索阶段先离线把视频库中每个视频编码成向量并建立索引,在线查询时只需计算文本向量与库中视频向量的相似度,取 top-K 返回。

对比学习的常见做法是 InfoNCE 损失。给定一个 batch 内的第 i 个视频-文本对,视频到文本的损失大致如下:

import torch import torch.nn.functional as F def info_nce_loss(video_embeds, text_embeds, temperature=0.07): """ video_embeds: [batch_size, dim] text_embeds: [batch_size, dim] """ # 归一化 video_embeds = F.normalize(video_embeds, dim=-1) text_embeds = F.normalize(text_embeds, dim=-1) # 相似度矩阵 [batch_size, batch_size] logits = video_embeds @ text_embeds.t() / temperature batch_size = video_embeds.size(0) labels = torch.arange(batch_size, device=video_embeds.device) loss_v2t = F.cross_entropy(logits, labels) loss_t2v = F.cross_entropy(logits.t(), labels) return (loss_v2t + loss_t2v) / 2

这套范式在通用视频文本检索中被广泛验证。但如果直接把同一套方法应用到驾驶视频上,会出现几个明显的错位。

2.1 通用视频模型忽略了“可行驶区域”这一几何信息

通用视频文本模型主要依赖 RGB 帧内容和对应的文本描述,它不会显式区分路面、车道线、护栏、交通标志。可是在驾驶视频描述里,用户很可能说“车辆在最右侧车道被公交车挡住”,如果模型不理解车道结构,只靠像素特征匹配,就很难把这种描述和视频对应起来。

2.2 文本描述对应的时间跨度不一致

通用视频检索数据集的文本一般是完整描述整段视频,而驾驶场景中一个自然语言查询可能对应视频中的某几秒。比如“前方出租车急刹车”在 10 秒视频中可能只出现在第 4 到第 6 秒。如果直接把整段视频嵌入和整句文本做全局对齐,模型会被大量无关帧干扰。

2.3 单靠“视觉外观”无法支持意图级查询

“车辆正在向左变道”和“车辆已经完成变道”在某一帧上看起来可能很像,但在运动轨迹上差异明显。只有把连续帧上的位置变化聚合成轨迹,才能对这类查询有效建模。

TraVEL 切入角度就是用轨迹信息来缓解以上错位。轨迹在这里的作用,可以理解为给模型提供了一条时间轴上的几何锚点,而不是简单地再增加一个输入通道。

3. “轨迹引导”到底是什么,又为什么不直接拼特征

如果只看论文标题,容易产生一个直觉:把车辆检测框的中心点连成轨迹曲线,再把这个曲线编码成一个特征向量,拼到视频特征后面,任务就完成了。

但在工程上,这种“直接把轨迹编码进融合特征”的做法会带来几个问题。

3.1 轨迹特征和文本特征不是同一个抽象层级

文本描述“车辆向左变道”是一个语义概念,包含意图、运动变化、道路关系。而原始轨迹只是一串坐标序列[(x1, y1, t1), (x2, y2, t2), ...],它更接近传感器信号,远没有达到语义层面。轨迹特征提供的是时间连续性和几何约束,而不是可直接对齐文本的语义向量。需要一层转化,才能进入文本可对齐的空间。

3.2 轨迹是稀疏的,视频是稠密的

一段 10 秒 30fps 的视频有 300 帧,但能稳定跟踪到的目标轨迹可能只有几十个点,而且目标检测一旦漏检,轨迹就会出现断点。如果把原始轨迹直接拼进嵌入模型,断点噪声会被当成真实运动信号放大。

3.3 驾驶场景涉及多个目标,轨迹无法单条使用

查询文本可能会说“本车在路口等待,对向车辆左转通过”,这时候需要本车轨迹、对向车辆轨迹、道路拓扑三层信息同时参与。只拿某一条轨迹做引导,信息维度不够。

所以,从方法设计上看,“轨迹引导”不是把轨迹向量简单拼接到视觉向量后面,而更可能是一种结构化引导机制。结合同类工作常用的做法,可以合理推测 TraVEL 的框架会包含以下几个部件:

  1. 视频编码器,用于提取帧级视觉特征,并建模时间上下文。
  2. 轨迹编码器,用于把目标轨迹、本车轨迹转换为时序特征序列。
  3. 几何引导模块,用于把轨迹特征作为约束或注意力偏置,影响视频帧特征的聚合。
  4. 文本编码器,用于把自然语言查询编码到共同嵌入空间。
  5. 对齐损失,在文本、视频、轨迹三种模态之间做联合学习。

注意这里“轨迹”不等于“GPS 路径”。在驾驶视频检索场景里,轨迹既可以指车辆在车道坐标系下的运动轨迹,也可以指图像平面中目标检测框中心的运动轨迹,也可以融合惯导信息得到更平滑的真实运动曲线。TraVEL 的细粒度设计需要看原文的表征定义,但核心创新点大概率不在“有没有轨迹特征”,而在“如何让轨迹特征去引导视频嵌入的学习过程”。

4. 从任务逻辑拆解 TraVEL 的可能路线

我没有逐行复现原文,所以下文是对方法设计空间的推演,而不是对论文内部模块的逐字复述。这个推演对工程落地有实际帮助,因为我们可以先把任务拆成“视频侧”“轨迹侧”“文本侧”和“对齐侧”四部分来思考。

4.1 视频侧:不只是抽帧,要按语义片段建模

驾驶视频检索不适合把一整段路测视频直接编码成一个全局向量。更合理的做法是先用场景切分算法把长视频切成候选片段,再对候选片段做嵌入检索。片段切分可以依据地图拓扑、红绿灯状态、车辆启停状态或场景检测器输出的场景标签。

工程实现上,即使先不引入复杂模型,也可以在编码前用一个简单的运动幅度指标辅助切分。例如计算相邻帧光流模长的变化,突变点往往意味着镜头场景变化或车辆急停。

4.2 轨迹侧:本车轨迹和目标轨迹要分开建模

从数据获取角度看,本车轨迹通常可以从 CAN 总线或组合导航系统拿到,是连续且相对干净的;目标轨迹则需要感知模块输出,依赖检测和跟踪,噪声较大。两类轨迹的置信度不同,不能简单合并建模。

一个可行的轨迹特征提取思路是用 1D 卷积或 Transformer 编码时间序列。输入可以是目标在图像坐标或 BEV 坐标下的位置序列、速度序列和加速度序列。为了消除不同视频分辨率带来的尺度差异,建议在模型输入端统一归一化到 BEV 坐标系,或者使用车道坐标系投影。

import torch import torch.nn as nn class TrajectoryEncoder(nn.Module): """ 输入: [batch_size, seq_len, feat_dim] 初步编码轨迹序列,输出一个轨迹级特征向量。 实际项目中可以考虑加入速度、朝向、类别信息。 """ def __init__(self, input_dim=4, hidden_dim=128, output_dim=256, num_layers=2): super().__init__() self.input_proj = nn.Linear(input_dim, hidden_dim) encoder_layer = nn.TransformerEncoderLayer( d_model=hidden_dim, nhead=4, batch_first=True ) self.transformer = nn.TransformerEncoder( encoder_layer, num_layers=num_layers ) self.output_proj = nn.Sequential( nn.Linear(hidden_dim, output_dim), nn.ReLU(), nn.Linear(output_dim, output_dim), ) def forward(self, traj): # traj: [B, T, input_dim] x = self.input_proj(traj) # [B, T, hidden_dim] x = self.transformer(x) # [B, T, hidden_dim] x = x.mean(dim=1) # [B, hidden_dim] x = self.output_proj(x) # [B, output_dim] return x

这段代码只是一个基础壳子,重点说明轨迹需要先经过 Transformer 层捕捉运动上下文,再投影到向量空间,而不是直接把坐标连成扁平向量。这样即使轨迹长度变化,模型也能接受变长输入。

4.3 文本侧:驾驶场景查询词要结构化

通用检索里文本是一整句话,模型全交给文本编码器理解。但在驾驶场景里,查询文本往往包含空间关系、道路结构、目标类别和目标动作。比如“直行通过路口时右侧有摩托车抢行”就是一个复合查询,解析难度比“一个人在跑步”高一个量级。

比较稳妥的做法是把原始文本交给一个大模型生成结构化查询表达式,例如分解为:

  • 场景:路口 / 路段 / 匝道
  • 本车动作:直行 / 左转 / 右转 / 变道
  • 目标类型:轿车 / 卡车 / 行人 / 摩托车 / 自行车
  • 目标动作:切出 / 抢行 / 急刹 / 慢速横穿
  • 天气光照:白天 / 夜晚 / 雨天 / 逆光

当然,这只是工程侧的一种补救手段。TraVEL 作为学术方法,更关注的应该是如何在嵌入空间里自然实现这种结构化对齐,而不是显式做规则解析。

4.4 对齐侧:轨迹作为中间锚点的三种常见设计

在缺少原文精确结构的前提下,这里有三种常见的轨迹引导设计思路:

思路一:交叉注意力引导。把轨迹特征作为 query,视频帧特征作为 key/value。模型通过注意力计算出每个视频帧与轨迹状态的相关程度,再加权聚合视频特征。这样轨迹就可以“挑选”与运动过程相关的帧,抑制静态背景的干扰。

import torch import torch.nn as nn import torch.nn.functional as F class TrajectoryGuidedAttention(nn.Module): def __init__(self, hidden_dim=256): super().__init__() self.hidden_dim = hidden_dim self.q_proj = nn.Linear(hidden_dim, hidden_dim) self.k_proj = nn.Linear(hidden_dim, hidden_dim) self.v_proj = nn.Linear(hidden_dim, hidden_dim) self.scale = hidden_dim ** 0.5 def forward(self, traj_embed, frame_embeds): """ traj_embed: [B, hidden_dim] frame_embeds: [B, T, hidden_dim] """ q = self.q_proj(traj_embed).unsqueeze(1) # [B, 1, hidden_dim] k = self.k_proj(frame_embeds) # [B, T, hidden_dim] v = self.v_proj(frame_embeds) # [B, T, hidden_dim] attn = torch.bmm(q, k.transpose(1, 2)) / self.scale # [B, 1, T] attn = F.softmax(attn, dim=-1) out = torch.bmm(attn, v) # [B, 1, hidden_dim] return out.squeeze(1)

思路二:状态级时间对齐。驾驶视频检索里文本可能只描述了部分时间片段,全局求平均会稀释信息。可以把轨迹分成多个状态段,例如“减速接近”“停止等待”“加速通过”,每个状态段生成一个状态向量,文本描述与对应状态段做细粒度对比。这和视频级对比是两种粒度,同时训练能让模型在小片段检索上受益。

思路三:几何预训练约束。用轨迹信息构造辅助任务,例如让模型预测两个视频片段的相对车道位置、预测目标下一时刻的位置或判断本车是否正在变道。把这些辅助损失和对比损失联合训练,模型会在主干网络中被迫保留对车道几何和运动趋势的敏感度。

5. 最小可用工程链路:先跑通再优化

如果要在这个方向做工程实践,我建议先不要追求复现完整论文,而是用一个最小链路验证“轨迹引导对检索有没有帮助”。下面给出一个本地可运行的工程思路。

5.1 准备数据

初次验证不需要大规模数据集。可以先自采或复用一小批驾驶记录视频。对每个视频片段准备三类信息:

  • 视频片段,长度 5 到 10 秒。
  • 对应的自然语言描述,例如“雨天路口左转,行人等待通过”。
  • 轨迹信息,可以是本车 GPS 轨迹,也可以简单用光流估计的车辆运动趋势近似。

合成轨迹这一说法需要谨慎,但如果在没有感知模块的情况下起步,可以用一个简化替代:对视频做帧间光流统计,得到全局运动向量序列,当作粗粒度轨迹信号。

5.2 提取初步特征

用预训练视频模型提取视频帧特征,先冻结主干,只训练后面的对齐和融合模块。这样能减少显存占用和训练时间。

import torch import torchvision.models as models # 使用预训练模型作为帧特征提取器,这里以 ResNet50 为例 # 实践中也可以换成 Video Swin、TimeSformer 等视频模型 backbone = models.resnet50(weights=models.ResNet50_Weights.DEFAULT) backbone.fc = torch.nn.Identity() # 去掉分类头,只保留特征 backbone.eval() def extract_video_frames(video_path, sample_fps=5): """简化演示:从视频中采样若干帧并返回特征列表。""" import cv2 cap = cv2.VideoCapture(video_path) frames = [] total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) for idx in range(0, total, max(1, int(cap.get(cv2.CAP_PROP_FPS)) // sample_fps)): cap.set(cv2.CAP_PROP_POS_FRAMES, idx) ok, frame = cap.read() if ok: frames.append(frame) cap.release() return frames # 真实项目中建议预先缓存所有视频特征,避免重复抽取 def encode_frames(frames, model, device="cuda"): model = model.to(device) feats = [] for frame in frames: img = cv2.resize(frame, (224, 224)) img = torch.from_numpy(img).permute(2, 0, 1).float().unsqueeze(0).to(device) img = img / 255.0 # 注意:ResNet 预训练通常使用 ImageNet 归一化,这里仅演示结构 with torch.no_grad(): feat = model(img) feats.append(feat) return torch.cat(feats, dim=0) # [T, feature_dim]

以上代码用于说明特征抽取的一般写法,实际使用需要加上 ImageNet 均值方差归一化等细节。

5.3 定义轨迹引导对比训练

建议在视频嵌入层和文本嵌入层之间加入轨迹引导模块。训练时同时计算视频-文本对比损失和轨迹-文本对比损失。后者的意义在于强制文本描述与运动状态对上号。

一个简化的训练循环如下:

import torch import torch.nn.functional as F from torch.utils.data import DataLoader def train_step(video_embeds, text_embeds, traj_embeds, temperature=0.07): """ video_embeds: [B, dim] text_embeds: [B, dim] traj_embeds: [B, dim] """ video_embeds = F.normalize(video_embeds, dim=-1) text_embeds = F.normalize(text_embeds, dim=-1) traj_embeds = F.normalize(traj_embeds, dim=-1) # 视频到文本 logits_vt = video_embeds @ text_embeds.t() / temperature # 轨迹到文本 logits_tt = traj_embeds @ text_embeds.t() / temperature # 视频到轨迹,这里假设同一个片段的视频和轨迹有对齐关系 logits_vtr = video_embeds @ traj_embeds.t() / temperature batch_size = video_embeds.size(0) labels = torch.arange(batch_size, device=video_embeds.device) loss_vt = F.cross_entropy(logits_vt, labels) loss_tt = F.cross_entropy(logits_tt, labels) loss_vtr = F.cross_entropy(logits_vtr, labels) loss = loss_vt + 0.5 * loss_tt + 0.5 * loss_vtr return loss

这个设计想要解决的问题是:如果只有视频-文本对比损失,模型可能学会了“看到雨夜路面反光就输出夜晚特征”,但它不一定学会“本车正在减速”。加上轨迹-文本对比损失后,模型必须保证轨迹运动状态和文本动作描述在向量空间里一致。于是文本就必须学会与运动时序关联,而不只是与静态外观关联。

5.4 建立检索索引并查询

训练完成后,对视频库中所有片段抽取特征,生成向量索引。查询阶段只需要编码文本,然后计算其与库中向量之间的距离。

推荐使用 FAISS 作为向量检索工具,支持上百万级别向量的 ANN 搜索。

import faiss import numpy as np def build_faiss_index(all_video_embeds: np.ndarray): """ all_video_embeds: [num_videos, dim] """ dim = all_video_embeds.shape[1] index = faiss.IndexFlatIP(dim) # 内积检索,配合归一化向量相当于余弦相似度 faiss.normalize_L2(all_video_embeds) index.add(all_video_embeds) return index def search(index, query_embed, top_k=5): """ query_embed: [1, dim],需要归一化 """ query_embed = np.asarray(query_embed, dtype=np.float32).reshape(1, -1) faiss.normalize_L2(query_embed) scores, idx = index.search(query_embed, top_k) return scores, idx

这一步做完,最小工程链路就跑通了。它会返回一批候选视频片段。后续再按候选片段做精排,例如轨迹相似度重排、文本重排,效果会进一步提升。

6. 驾驶视频检索的数据组织建议

数据是驾驶视频检索项目里最容易被低估的部分。很多团队跑完模型后效果不理想,回头一看,发现问题根本不出在模型结构,而是出在数据组织方式。

6.1 一个片段只对应一个主事件

训练数据里的一条视频片段应该表达一个相对完整的事件,比如“本车从匝道汇入主路”。如果一个片段同时包含了汇入、加速、变更两条车道,模型就难以定位文本对应的具体时间段。

6.2 文本描述要覆盖静态与动态两层

只写“十字路口”是静态层,缺少动作描述;只写“加速通过”是动态层,缺少道路背景。最好统一模板为“在什么场景下,谁做了什么,本车做了什么”。这样写出来的文本在嵌入学习中更容易被模型解耦。

6.3 轨迹质量要有单独标注

目标轨迹噪声会影响训练,建议在数据构建时同时输出轨迹置信度。后续可以用该置信度作为 loss 权重,让模型不要被不确定的轨迹带偏。

7. 评估指标的常见坑

视频检索的标准指标一般是 Recall@K 和 Median Rank。

  • Recall@K:在前 K 个结果中命中的比例。
  • Median Rank:正确结果在排序中的中位数排名。

但驾驶视频检索的特殊性在于,用户往往是按条件检索一批数据,而不是只找一个正确答案。例如“找 200 段夜间逆光下目标切出的视频”,一次检索往往需要返回一批合格片段。因此单纯看 Recall@1 不够,还要关注 Precision@K 和分类条件下的覆盖率。

评估时建议至少拆成四组:

查询类型示例挑战
场景级查询雨天 / 路口 / 夜晚全局属性识别
行为级查询车辆变道 / 急刹时间定位与运动识别
关系级查询左侧车辆切出空间关系与目标交互
组合查询雨天路口左转遇到行人多条件联合约束

只有分组评估,才能看出轨迹引导到底在哪些类型的查询上带来了提升。如果只报一个总指标,很容易被场景级查询的平均效果掩盖行为级查询上的失败。

8. 常见问题与解决思路

问题现象可能原因排查方式解决方案
视频向量和文本向量相似度整体偏低视频特征与文本特征没有对齐初始化检查训练 loss 是否下降,单跑一次 retrieval 小验证集先冻结视频主干,只训练文本侧和融合侧,稳定后再解冻
轨迹特征对结果没有贡献轨迹特征只是被平均池化,丢失时序结构对比去掉轨迹模块后的检索结果使用注意力池化或状态级池化,而不是简单 mean pooling
检索结果命中大量静态背景相似片段模型主要靠外观特征分类检查难负样本中的轨迹运动差异增加难负样本挖掘,把轨迹相近但意图不同的片段加入负样本
长视频检索速度太慢每个片段都走完整编码器看延迟瓶颈在帧采样还是模型推理先抽帧缓存特征,再做轻量级 candidate retrieval,最后精排
轨迹有断帧导致序列长度不一致跟踪丢失或目标遮挡统计轨迹长度分布和断点率在轨迹编码器中使用 mask 机制,或按状态补全轨迹

9. 实践中的四点建议

第一,不要把“轨迹引导”理解成锦上添花的多模态特征,要把它当成时间对齐的锚。驾驶视频里真正困难的是查询语句与视频局部时间段的对应关系,轨迹提供了“本车在哪个时间段做了什么”的天然骨架。

第二,起步阶段用合成数据和公开数据验证 pipeline,再进真实路测数据。路测数据治理成本极高,先在小规模干净数据上调通训练、评估、索引全链路,能节省大量时间。

第三,一定要做分组评价。轨迹引导对“本车运动类描述”的增益通常大于“场景外观类描述”。如果总指标没有提升,先看分组指标,判断是模块没起作用还是评测方式掩盖了细分能力。

第四,大规模部署时,建议把流程拆成“召回 + 精排”两段。粗召回用轻量视频向量和 FAISS 完成,精排阶段再叠加轨迹相似度、时间范围约束、文本重排序模型。这样既保证检索速度,也保留轨迹引导带来的精度收益。

10. 总结

TraVEL 这个研究方向的价值不在提出一个新的视频编码器,而在于它把检索问题重新放回“驾驶场景”这一具体语境中,揭示了通用的视频-文本嵌入模型在运动时序、道路几何和意图级查询上的不足。轨迹引导把原本只停留在视觉像素层的视频嵌入学习,拉回到车辆运动本质上来。

如果你正在做自动驾驶数据闭环、场景检索或视频数据治理,这个方向值得跟踪。它提示我们:当通用模型无法解决垂直问题时,回到垂直场景里寻找“领域特有的稳定信号”,往往比继续堆通用数据更有效。对于视频检索来说,视觉特征让模型“看得见”,文本特征让模型“听得懂”,而轨迹引导要解决的是让模型“记得住运动过程”。这三件事对齐了,视频检索才能真正在驾驶场景里落地。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询