十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

行人轨迹搜索技术解析:从目标检测到ReID的完整方案

行人轨迹搜索技术解析:从目标检测到ReID的完整方案 简介围绕监控视频的智能处理需求这个项目以机器学习为核心提供一种基于图像的行人轨迹搜索方案。使用者只需提供一张行人目标图像便能在大量视频片段中自动检索出包含该目标的画面并完成轨迹标记适用于行人重识别、跨镜头追踪、安防检索等实际场景。资源包共365个文件大小约30.72MB内含可直接调用的Python源码、预训练模型caffemodel、pb、SQLite数据库和PDF说明文档其中JSON文件用于参数存储与状态保存模型文件可支撑开箱即用的推理演示整体目录结构清晰便于按需查阅。目前已有144人学习下载适合具备一定Python与深度学习基础的开发者用于参考实现或二次开发。附带详细的运行环境说明Python 3.6.2、TensorFlow-GPU、Keras等可有效避开环境配置方面的常见问题提升上手效率。1. 行人轨迹搜索从一张查询图像到整段时空轨迹给定一张行人照片要求在一段长时间视频中找出该行人出现过的所有片段并按时间顺序还原运动轨迹这显然不是单纯的目标检测或单目标跟踪就能解决的问题。目标检测只回答“当前帧里人在哪”单目标跟踪又需要视频首帧手工给框而实际业务里常见的输入只有一张截图我们要在无标注的存量视频里去翻找目标。机器学习在这里承担的是识别与匹配的核心检测器先框出每一帧中的行人重识别模型将框裁剪后映射为外观特征向量再由轨迹关联把跨帧检测结果连成带有ID的轨迹片段。整个过程在工程上可以抽象成一次特征检索把查询图像编码成特征在预先构建的行人特征库中找最相似的Top-K再把命中结果聚合成轨迹输出。这套方案适合做安防视频回溯、商超客流动线分析以及自动驾驶决策数据标注的工程师它把“人肉盯监控”简化成了毫秒级的向量搜索。2. 检测、ReID、关联三层机器学习流水线的原理与选型行人轨迹搜索的完整链路由检测、重识别、轨迹关联三个子问题串联而成。检测解决“人和位置”问题重识别解决“同一人”问题轨迹关联则把前两个模块的结果按时间轴连通。三者都可以独立训练和评估但串联之后会产生级联误差——检测漏框会直接导致轨迹中断ReID特征质量差则会让跨镜头的身份拼接出现跳变。这一节先讲明白每个阶段的原理再给出我常用的选型组合。2.1 目标检测从图像里先框出每个行人检测是整个链路的入口。多数监控视频分辨率不会太高背光、运动模糊、远距离小目标都很常见。我一般优先选用单阶段检测器它们把分类和回归合并到一次前向计算单帧推理速度快在连续帧上做扫描时的整体吞吐量远高于两阶段模型。以YOLOv8为例输出会直接产生一组(x1, y1, x2, y2, score)的包围盒如果项目里要换成其他检测器通常只需改模型加载的一两行代码后续的特征提取逻辑不用动。检测置信度阈值是最先要调的参数。设到0.45误检减少但漏检增多设到0.3小目标召回率上升但大量背景框会被送进ReID模型浪费计算资源。我的定法很简单拿一小段有代表性的视频切片遍历多组阈值观察输出框数量和肉眼误检率按“单帧不超过10个有效框”的目标来反推阈值。实际项目里我也见过把阈值压到0.25强行保召回的做法代价是特征库里全是虚检特征搜索阶段噪声会明显变大。2.2 行人重识别让机器学习分类器退化成特征嵌入重识别ReID的目的是判断两张不同时间、不同摄像机拍到的行人图像是否属于同一个人。训练阶段常把问题建模成分类任务假设训练集中有751个不同身份的行人网络通过全连接层输出这751个ID的概率并用交叉熵损失做监督。但真正有意义的东西在分类头的上一层也就是嵌入空间里的特征向量。模型收敛后移除分类层用这个特征向量表示“一个人长什么样”。如果对跨数据集泛化能力要求更高可以叠加三元组损失让同一ID的样本对特征距离拉近、不同ID的样本对特征距离拉远。生成特征向量的过程走的是典型的机器学习梯度下降路线从随机初始化开始在分类损失和三元组损失上反向传播逐渐把外观信息压缩到嵌入向量里。常用的输出向量是512或1024维推理时做L2归一化再以余弦相似度作为后续配对依据。这里不用刻意追求太深的网络ResNet50配合PCBPart-based Convolutional Baseline结构已经能应付大部分监控场景。PCB把最后的特征图水平切成多段分别池化再拼接让模型对上衣、下装、背包等局部特征分工记忆对遮挡的鲁棒性明显高于直接全局池化。2.3 轨迹关联卡尔曼滤波与匈牙利匹配把检测框连成带ID的轨迹片段逐帧做完检测得到的是互不关联的检测框。要变成轨迹需要做两件事预测与匹配。卡尔曼滤波负责预测已确认轨迹在下一帧的位置和尺寸得到预测量然后把预测框与当前帧真实检测框对比。对比指标有两个运动距离和外观距离。运动距离用IoU或马氏距离计算外观距离用ReID特征算余弦距离。把两个距离加权成综合代价矩阵再用匈牙利算法求解最小代价配对就得到每一帧的匹配结果。两个指标的权重是轨迹关联阶段第一个要调的参数。固定机位的商业场景运动权重取0.6、外观权重取0.4就够机位抖动明显时运动预测误差大我会把运动权重降到0.4以下更多依赖外观特征。另外注意未匹配轨迹的处理一条轨迹连续多帧没有匹配到检测框时不能立刻销毁要维持一段时间比如30帧用于处理遮挡后的轨迹续接。下面是我在真实项目中常用的一组选型组合。流水线模块常用方案输出选型注重点目标检测YOLOv8n / Faster R-CNN包围盒与置信度速度优先选YOLO远距离小目标考虑两阶段行人重识别ResNet50PCB / TransReID512~1024维特征向量跨摄像头或遮挡严重时升级TransReID轨迹关联卡尔曼滤波匈牙利匹配轨迹ID与帧序列固定机位按0.6/0.4权重起步3. 从视频到特征库构建可检索图像特征的工程步骤特征库是这个项目从“验证思路”走向“能用起来”的关键一步。这类项目打包成 zip 分发时打开后通常能看到模型权重、推理脚本和配置文件三部分。配置管理是所有机器学习的落地环节里最容易被忽略的一块路径、视频源、抽帧间隔、检测阈值、ReID输出维度这些参数一旦写死在代码里后面换数据换场景都要改源码。我一般要求配置和数据分离推理脚本只读一个 YAML 或 JSON 文件。3.1 视频采样策略全量逐帧还是抽帧视频按25fps计算一小时素材就有九万帧逐帧检测的GPU开销很大而且相邻帧的检测框高度重叠特征重复度高。实践中我会固定间隔抽帧普通场景每秒取2到3帧步行或奔跑速度快的路段回到全量帧。抽帧间隔直接影响轨迹的连续性间隔过长时两次采样之间行人可能移动好几米卡尔曼滤波的预测误差会被放大最终导致轨迹断裂成多段。反过来间隔太短又会导致大量重复特征被写入索引检索时同一段轨迹会占据多个返回位置影响排序质量。3.2 特征提取主流程检测与ReID串联的最小脚本下面这段代码把检测和ReID串成一个完整的特征提取流程可以直接对着项目里的模型路径改import cv2, torch, numpy as np from ultralytics import YOLO from torchvision import transforms # 检测模型负责从图像中定位行人 detector YOLO(yolov8n.pt) # 假设项目里给出的是能直接推理的ReID模型 reid torch.load(reid_resnet50.pth, map_locationcuda) reid.eval().cuda() transform transforms.Compose([ transforms.ToPILImage(), transforms.Resize((256, 128)), transforms.ToTensor(), transforms.Normalize(mean[.485, .456, .406], std[.229, .224, .225]) ]) cap cv2.VideoCapture(scene_01.mp4) frame_idx 0 samples [] while cap.isOpened(): ret, frame cap.read() if not ret: break # 每5帧抽一帧125fps演示时常可以做得更密 if frame_idx % 5 0: dets detector(frame, conf0.45, classes[0])[0] for box in dets.boxes.xyxy.cpu().numpy(): x1, y1, x2, y2 box.astype(int) crop frame[y1:y2, x1:x2] if crop.size 0: continue inp transform(crop).unsqueeze(0).cuda() with torch.no_grad(): feat reid(inp) # (1, 1024) samples.append({ frame: frame_idx, bbox: box, feature: feat.squeeze(0).cpu().numpy() }) frame_idx 1参数说明conf0.45是检测置信度阈值类别0对应COCO数据集中的person类Resize((256, 128))是ReID模型常见的输入尺寸宽高比接近行人直立形态frame_idx % 5控制了抽帧密度。这里所有检测框都会被直接送入ReID模型没有做非极大值抑制之外的后处理。特征shape是(1, 1024)squeeze后得到1024维向量后面入库和检索都基于这个向量。建议把samples里的框和帧号一并保存后续定位行人位置时不用重新跑检测。3.3 特征索引构建用 Faiss 把向量变成可搜索库特征全部提取出来后下一步是建索引。数据量小可以直接用numpy逐条计算余弦距离但真实监控视频的特征量通常是几十万甚至百万级别必须交给向量检索库。Faiss的IndexFlatIP是精确检索的内积索引配合L2归一化内积值等价于余弦相似度适合第一版使用import faiss feat_matrix np.vstack([s[feature] for s in samples]) # 精确检索要求所有向量连续存储并做归一化 faiss.normalize_L2(feat_matrix) index faiss.IndexFlatIP(feat_matrix.shape[1]) index.add(feat_matrix) # 建索引后顺便保留帧号和框的映射表 frame_map np.array([[s[frame], *s[bbox]] for s in samples])IndexFlatIP虽然精确但内存开销随向量数量线性增长。百万级特征时单精度需要4GB内存勉强可接受到了千万级就必须换IndexIVFFlat或IndexIVFPQ这类近似索引。IVF有两个关键参数nlist控制聚类中心数量nprobe控制查询时搜索的聚类个数。nlist4096, nprobe64是我常用的起点召回率能保持在精确检索的95%以上查询耗时会从毫秒级降到百微秒级。4. 查询与匹配图像查询如何命中整段行迹特征库建好之后搜索环节就变成标准向量检索。这里容易犯的一个错误是直接用查询图像的全部区域做编码冲到库里找最相似的检测框。但实际业务里一张查询图可能裁剪不干净背景占了大半Retrieval返回的结果就会混乱。查询阶段同样要先对图像做人脸外延的裁剪、尺寸归一化和亮度平衡。4.1 查询图像的特征编码查询图像与库特征必须经过完全相同的预处理否则特征空间不对齐搜索精度会明显下降。换句话说3.2里用的transform必须原封不动地用在查询图上from PIL import Image query_img Image.open(query.jpg).convert(RGB) q_tensor transform(np.array(query_img)).unsqueeze(0).cuda() with torch.no_grad(): q_feat reid(q_tensor) q_feat q_feat.squeeze(0).cpu().numpy().reshape(1, -1) # 查询特征同样需要L2归一化再进相似度计算 faiss.normalize_L2(q_feat)如果查询图像不是单人需要先用检测器把目标单独裁剪出来。裁剪时建议把原始框向外扩10%到20%的像素把衣领、背包边缘等判别性区域包含进去但不要扩得太大否则背景干扰会压过行人本身的外观信息。4.2 用余弦相似度检索 Top-K检索过程就是一次index.search调用D, I index.search(q_feat, k20) # D: 相似度分数数值越接近1越好 # I: 命中向量的行号对应 frame_map 里的帧号和检测框 hits frame_map[I[0]]IndexFlatIP返回的内积分数已经等同于余弦相似度范围在[-1, 1]。实际场景里同类行人的相似度一般在0.6到0.9之间低于0.5时就该怀疑查询图本身的质量而不是盲目相信结果。打印出D的前20个值可以快速判断阈值下限如果前20名里出现明显的跳变比如第1名是0.91、第2名变成0.70说明特征库中真正匹配的目标数量很少。4.3 轨迹聚合与排序从命中框到完整轨迹单框检索结果只是散点必须聚合到轨迹级别。做法是把3.3保存的frame_map与轨迹ID对应起来每个检测框附带所属的轨迹ID然后按轨迹ID对Top-K结果分组def rank_trajectories(D, I, frame_map_traj, topk5): # 每个命中向量都有对应的帧号和轨迹ID hit_traj frame_map_traj[I[0]] score_map {} for traj_id, score in zip(hit_traj, D[0]): # 取轨迹内所有命中向量的最高分作为轨迹得分 if traj_id not in score_map or score score_map[traj_id]: score_map[traj_id] score ranked sorted(score_map.items(), keylambda x: x[1], reverseTrue) return ranked[:topk]这里用的是“最高分优先”策略即轨迹评分取该轨迹内命中的检测框最高相似度。还有一种策略是平均分优先适合那些轨迹跨度长、外观变化大的情况。最高分容易受单张误检影响平均分则会拉低长轨迹的得分。两个都试一下观察返回结果的排序合理性再固定方案。5. 搜索质量验证与三个关键参数调节轨迹搜索做得好不好不能只靠肉眼看结果。第一步要先建立一个带标注的验证集抽一定数量的查询图像手动标注它对应的轨迹ID集合然后用脚本计算 Top-K 命中率。这里的指标用mAP或者简化版的Top-K Accuracy都行后者更直观也更容易在项目早期发现问题。提供一个简化验证脚本的骨架def eval_topk(queries, search_func, k10): hits 0 for q in queries: ranked search_func(q[feature], k) if q[gt_traj_id] in ranked: hits 1 return hits / len(queries)search_func是对4.3的封装输入查询特征返回Top-K轨迹ID。验证集至少准备50条查询覆盖正常、遮挡、逆光和跨摄像头四类情况分别统计Top-1和Top-10的命中率。正常场景下Top-10达标线在0.8左右跨摄像头场景能到0.6就算合格。有三个参数对搜索质量影响最大参数推荐范围作用位置调节信号检测置信度阈值0.35~0.50检测输出过滤误检多时调高轨迹断裂多时调低轨迹保留帧数30~60帧轨迹关联遮挡频繁时调高避免轨迹过早消失外观权重0.4~0.8代价矩阵加权相机抖动大时调高机位固定时保持低值检测阈值和轨迹保留帧数是一对相互制约的参数。阈值太高会漏检导致轨迹缺帧保留帧数太少则会让短暂遮挡变成永久断轨。我的调参顺序是先固定外观权重为0.5调检测阈值检测结果稳定后再调保留帧数最后才动外观权重避免参数之间互相干扰。轨迹断裂修复还有一个小技巧当一条轨迹分裂成两段时检查两段轨迹的时间间隔和空间距离。如果间隔小于30帧、且两段头部和尾部的ReID相似度大于0.75可以做轨迹合并把后段的ID改成前段。这类启发式规则在离线回溯场景里非常安全因为不涉及实时决策误合并可以靠人工抽验纠正。本文还有配套的精品资源点击获取
返回列表