
简介基于YOLOv3与行人重识别ReID的实战项目面向图像识别与智能安防方向的开发者和学习者。项目先利用YOLOv3快速检测画面中的行人再通过ReID模型提取特征并跨摄像头匹配最终实现特定行人的定位与检索适合希望掌握目标检测与重识别串联应用的中级AI实践者。压缩包共49个文件大小5.51MB以27个Python脚本为主涵盖模型结构、检测推理、查询匹配等核心逻辑另含1个YOLOv3配置、1个类别文件、1个数据配置及19张示例图片便于直接运行与效果验证。资源目录结构清晰包含检测模块、ReID模型定义、特征查询与工具脚本并附有行人样例图片开箱即用。通过完整代码与样例可快速复现行人检测与检索流程为优化检测速度或提升识别精度提供基线。目前已有1358人学习下载对理解真实场景下的行人搜索系统搭建具有较高参考价值。1. 从“监控里找人”说起这就是YOLOv3加行人重识别在做的事想象一个很普通的实际需求校园活动结束后要在一整天、多个摄像头的视频里找一个背红色书包的人。人眼回放太慢逐帧看更不现实于是你需要一条自动化流水线先让检测模型把所有出现的行人框出来再让一个“认人”的模型判断每个框里的人是不是目标。这个标题讲的就是这条流水线的落地实现。YOLOv3负责“看见人”行人重识别Person Re-ID负责“认出是谁”两个模型各干各的拼成典型的检测加识别两阶段结构。这既是人工智能大作业和毕业设计里的高频选题也是智能安防中“以图搜图”的真实雏形。你不需要从零训练一个超大模型只需要把两个成熟模型正确串起来就能得到一个能用的查找系统。这篇就把这个方案从原理、参数、代码到踩坑记录完整讲清楚适合有一定深度学习基础、想动手做项目的读者。2. YOLOv3在流水线里的位置检测分支的选型与参数校准2.1 为什么选YOLOv3来“找人”两阶段结构的第一个关键决策检测在整条流水线里的职责非常单一从一帧画面中找出所有可能是人的区域给出包围框。它不需要知道这个人是谁只需要确保“人别漏掉”。这个定位决定了检测模型的选择标准——成熟、稳定、预训练权重好找、推理速度快。YOLOv3虽然发布已有年头但它恰好满足这些条件而且结构清晰适合用来理解检测原理。YOLOv3的核心思路是用Darknet-53骨干网络提取特征在三个不同尺度上分别输出预测结果分别负责大、中、小目标。它对行人这种中等尺寸目标比较友好因为行人在画面里通常占据一定面积不像远处的小目标那么依赖高分辨率特征图。另外YOLOv3的anchor框是通过COCO数据集聚类得到的其中就包含适合行人竖长形状的先验框这比从零设计检测头要省事得多。实际项目里几乎没人从随机权重开始训练YOLOv3而是直接加载在COCO上训练好的权重。COCO数据集有80个类别person是其中一类正好符合我们的需求。对做课程设计或毕业设计的人来说这个选择尤其重要——时间应该花在调优检测阈值和搭建下游识别流程上而不是花两周去训练一个检测器。提示如果你不熟悉检测模型第一次跑通建议直接用官方预训练权重做推理先把流程跑通再考虑要不要微调。2.2 跑通检测用YOLOv3把视频里的行人全部框出来这里给出一个最小可用的行人检测脚本。下面这段代码使用PyTorch生态中常见的YOLOv3实现假设你已经安装了torch和opencv-python。import cv2 import torch import numpy as np # 加载 YOLOv3 官方预训练权重 model torch.hub.load(ultralytics/yolov3, yolov3) model.eval() def detect_persons(frame, conf_thres0.4): 输入一帧 BGR 图像返回所有行人框 [x1, y1, x2, y2, score] # 保持宽高比缩放不足部分填充灰色避免目标被拉伸 img, scale, pad letterbox(frame, new_shape(416, 416)) img_tensor torch.from_numpy(img.transpose(2, 0, 1)).float() / 255.0 img_tensor img_tensor.unsqueeze(0) with torch.no_grad(): preds model(img_tensor) boxes [] for det in preds: if det is None or len(det) 0: continue x1, y1, x2, y2, conf, cls det.cpu().numpy() if int(cls) 0 and conf conf_thres: # COCO 中 0 对应 person # 坐标从 416x416 空间映射回原图 x1 (x1 - pad[0]) / scale y1 (y1 - pad[1]) / scale x2 (x2 - pad[0]) / scale y2 (y2 - pad[1]) / scale boxes.append([x1, y1, x2, y2, float(conf)]) return boxes def letterbox(img, new_shape(416, 416)): h, w img.shape[:2] scale min(new_shape[0] / h, new_shape[1] / w) nh, nw int(h * scale), int(w * scale) resized cv2.resize(img, (nw, nh)) canvas np.full((new_shape[0], new_shape[1], 3), 114, dtypenp.uint8) pad_h (new_shape[0] - nh) // 2 pad_w (new_shape[1] - nw) // 2 canvas[pad_h:pad_h nh, pad_w:pad_w nw] resized return canvas, scale, (pad_w, pad_h)这段代码包含三个关键步骤首先是letterbox预处理把任意分辨率的帧缩放成416x416同时保持宽高比多出来的部分用灰色填充。这一步很重要因为直接把图像拉伸成正方形会让行人变形检测框位置也会跟着偏移。其次是模型推理YOLOv3内部已经完成了多尺度特征融合和非极大值抑制NMS所以拿到的是最终的检测结果里面包含检测框坐标、置信度和类别编号。最后是坐标映射模型输出是在416x416空间里的坐标需要按之前letterbox的比例和padding值还原到原图坐标系否则你画出来的框会对不上画面里的人。参数说明参数作用推荐值conf_thres只保留置信度高于该值的框常规场景0.4密集场景0.3new_shape输入模型的分辨率416或608画面小目标多用608cls类别编号COCO中person固定为02.3 行人密集场景下的两个必调参数置信度与NMS阈值很多人在第一个版本里直接套用默认阈值结果在行人密集、互相遮挡的场景里翻车。常见现象是两个人靠得太近被检测成一个框或者一个人只露出一半身体就被过滤掉了。这时候你需要关注的是置信度和NMS阈值。置信度阈值控制“什么算行人”。阈值设得高漏检增多设得低误检增多。在Re-ID流水线里我一般倾向于往低调因为漏掉一个行人意味着这个人在后续所有帧里都不会被识别这是不可逆的损失。而误检框最多只是增加一些特征库里的垃圾数据后面用距离阈值还能滤掉一部分。NMS阈值控制“重叠框去重”的力度。行人密集时两个紧贴的人会产生两个高度重叠的框如果NMS阈值太低其中一个会被当成另一个的重复框被抑制掉导致漏检。我的经验是对行人场景把NMS阈值保持在0.5到0.6之间不要低于0.4。场景conf_thresNMS阈值备注稀疏街道0.50.5追求精确减少误检校园/商场0.40.5均衡配置密集人群0.30.6优先保召回容忍误检注意NMS不是越低越好。把NMS阈值调成0.1会直接让密集人群的检测框大量消失看似画面干净了实际是犯了漏检的隐蔽错误。3. “认人”的环节行人重识别如何把一次出现变成一个身份向量3.1 跨摄像头找同一个人Re-ID到底在解决什么问题检测框把画面切成了一个个行人切片现在的问题是怎么判断这个框里的人和另一个框里的人是同一个行人重识别Person Re-ID就是专门解决这个问题的。它的任务是让计算机“看整体外表认人”而不是看脸。Re-ID模型做的事是输入一张裁剪出来的行人图像输出一个固定维度的特征向量。训练目标是让同一个人的不同图像在特征空间里距离很近不同人的图像距离很远。模型骨干网络通常用ResNet50、OSNet这些在ImageNet上预训练过的CNN再在Market-1501这类行人数据集上微调。训练时使用分类损失加三元组损失前者保证模型能区分不同身份后者保证距离度量合理。一个常被误解的点是Re-ID不是人脸识别。人脸识别关注五官细节而Re-ID看的是全身——衣服颜色、背包、体型、发型这些全局外观。这意味着如果目标人中途换了外套Re-ID的准确率会明显下降。这是这个技术方向的固有边界做项目时需要提前跟需求方说清楚而不是等项目跑完才发现“换件衣服就找不到了”。对做毕设或课程项目来说通常不需要自己训练Re-ID模型直接使用在公开数据集上预训练好的权重即可。把模型加载进来后去掉最后一层分类器只保留骨干网络输出的特征向量。这也是接下来代码要做的核心事情。3.2 从一张裁剪图到特征向量Re-ID特征提取的最小实现下面是一段非常典型的Re-ID特征提取代码。假设你已经有了一个训练好的Re-ID模型权重这里重点展示预处理和推理细节。import cv2 import torch import torchvision.transforms as T import numpy as np # 这里假设 model 是加载好的 Re-ID 模型例如基于 ResNet50 的训练结果 # 关键点推理时要取 backbone 输出的特征而不是分类层的 softmax 结果 model torch.load(reid_model.pth, map_locationcpu) model.eval() # Re-ID 通用的预处理流程输入尺寸 256x128配合 ImageNet 的 mean/std transform T.Compose([ T.ToPILImage(), T.Resize((256, 128)), # 注意顺序高 256宽 128不是正方形 T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def extract_feature(cropped_img): cropped_img: YOLOv3 框出来的行人区域BGR 格式 返回值: L2 归一化后的特征向量维度取决于模型 backbone rgb cv2.cvtColor(cropped_img, cv2.COLOR_BGR2RGB) tensor transform(rgb).unsqueeze(0) with torch.no_grad(): feat model(tensor) # L2 归一化让余弦相似度可以直接用内积计算 feat feat / torch.norm(feat, p2, dim1, keepdimTrue) return feat.numpy().flatten()这段代码看起来短但有两个容易被忽略的细节。第一resize的尺寸是256x128而不是正方形的256x256因为行人图像天然是竖长形状用高大于宽的尺寸能保留更多体型信息同时减少背景干扰。很多新手在这里直接用正方形resize导致行人被横向拉伸特征质量明显下降。第二特征向量做了L2归一化。这一步的深意是归一化之后两个向量的余弦相似度可以直接用点积计算不用再开根号、除模长后面的检索代码可以全部用矩阵乘法完成速度提升非常明显。参数说明参数值说明输入尺寸256x128高256宽128竖长比例归一化均值[0.485, 0.456, 0.406]ImageNet统计值归一化方差[0.229, 0.224, 0.225]ImageNet统计值特征向量L2归一化方便用内积计算余弦相似度3.3 相似度怎么比、阈值怎么定一个函数搞定身份判定有了特征向量下一步就是比较。你把目标行人的一张照片作为查询图query把监控视频里检测到的每个行人作为候选库gallery现在要计算查询图和候选库中每个特征的距离。最常用的度量方式是余弦相似度。由于前面特征向量已经做了L2归一化相似度计算就变成一个矩阵乘法import numpy as np def compute_similarity(query_feat, gallery_feats): query_feat: 目标行人特征向量形状 (dim,) gallery_feats: 库特征矩阵形状 (N, dim) 返回: 相似度分数数组形状 (N,) scores gallery_feats query_feat.reshape(-1, 1) return scores.flatten() def match_top_k(scores, meta, top_k10, thr0.6): scores: 所有候选的相似度 meta: 候选元数据例如帧号、检测框坐标 thr: 相似度阈值低于该值的结果视为不匹配 order np.argsort(scores)[::-1] results [] for idx in order[:top_k]: if scores[idx] thr: break results.append((meta[idx], float(scores[idx]))) return results这段代码里gallery_feats query_feat.reshape(-1, 1)一步就完成了N个候选向量和目标向量的余弦相似度计算。向量化计算比用for循环逐个算余弦快几十倍这是整个流水线能跑起来的基础保障。关于相似度阈值怎么定这是新人最容易问“玄学”问题的地方。实际上不同的Re-ID模型训练数据不同分数分布差异很大。我的做法是先挑几对“同一个人”的图像和几对“不同人”的图像分别计算相似度画出分数分布。通常同一个人的相似度在0.7到0.9之间不同人的相似度在0.2到0.5之间阈值取两者之间的间隙处。如果没有标注数据可以先设0.6然后根据检索结果里的误检情况微调。4. 把两段拼成一条流水线从检测到识别的最小可复现方案4.1 整体数据流视频帧、行人框、特征库、检索结果把前面的检测和识别两个环节串起来整个流程可以描述为视频按一定间隔抽帧每一帧先经过YOLOv3检测得到一组行人框。每个行人框从原图中裁剪下来送入Re-ID模型提取特征向量。所有这些特征向量连同它们对应的帧号和框坐标一起存入特征库。当需要查找目标行人时用目标照片同样提取一个特征向量在库里做一次相似度排序返回最接近的结果及其出现的帧号和时间。这里有一个设计取舍特征库是离线建还是在线实时建。离线建库的逻辑是先完整跑一遍视频把所有帧的行人特征存成文件之后每次查询都只做检索速度很快适合“事后找人”的需求。在线建库则是一边抽帧一边比对适用于实时监控场景但计算压力大得多。对大部分毕设和工程原型来说离线建库是首选先把事情跑通再谈实时性。4.2 离线建库把一整段视频变成特征文件下面这段代码实现完整的建库过程综合了检测、裁剪、特征提取和存储import cv2 import numpy as np def build_gallery(video_path, save_path, sample_interval5): 处理一整段视频抽帧检测并提取行人特征保存为特征库 sample_interval: 每隔多少秒抽一帧 cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) gallery_feats [] gallery_meta [] frame_total 0 while True: ret, frame cap.read() if not ret: break frame_total 1 # 按秒为单位抽帧避免连续帧产生大量重复数据 if frame_total % int(fps * sample_interval) ! 0: continue dets detect_persons(frame, conf_thres0.4) for x1, y1, x2, y2, conf in dets: x1, y1, x2, y2 int(x1), int(y1), int(x2), int(y2) crop frame[y1:y2, x1:x2] if crop.shape[0] 32 or crop.shape[1] 16: continue # 小框很可能是远端行人特征质量太差直接丢弃 feat extract_feature(crop) gallery_feats.append(feat) gallery_meta.append([frame_total, x1, y1, x2, y2]) cap.release() feats np.array(gallery_feats, dtypenp.float32) meta np.array(gallery_meta, dtypenp.int32) np.save(save_path _feats.npy, feats) np.save(save_path _meta.npy, meta) return feats, meta这段代码里有三个参数需要根据实际场景调整。抽帧间隔sample_interval决定了特征库的大小一个人行走速度正常的情况下每5秒抽一帧已经能覆盖多次出现但如果目标快速跑过画面建议改成2秒一帧否则可能漏掉关键出现瞬间。最小框过滤条件crop.shape[0] 32是为了剔除那些太小、细节全丢失的行人框这类框提特征几乎没有区分度反而会拖慢后面的检索。有一点需要注意存储时把特征和元数据分开保存特征存成float32的numpy数组元数据存成int32数组帧号和坐标信息都记在里面。这样后面查询时如果想要“跳转到那一帧截图”直接拿帧号定位即可不用把整个视频重新遍历一遍。4.3 查找目标行人用一张照片把目标从库里捞出来建好库之后查询就简单多了。下面这段代码接收一张目标人照片算出特征向量和库里所有特征做一次相似度排序输出排名最高的几个候选。def search_person(query_img_path, feats_path, video_path, top_k10, thr0.6): # 加载特征库和元数据 feats np.load(feats_path _feats.npy) meta np.load(feats_path _meta.npy) # 提取查询图的特征向量 query_img cv2.imread(query_img_path) query_feat extract_feature(query_img) # 矩阵乘法一次算出所有相似度 scores feats query_feat.reshape(-1, 1) scores scores.flatten() order np.argsort(scores)[::-1] cap cv2.VideoCapture(video_path) for rank, idx in enumerate(order[:top_k]): if scores[idx] thr: break frame_id, x1, y1, x2, y2 meta[idx] # 跳到指定帧并截图保存 cap.set(cv2.CAP_PROP_POS_FRAMES, int(frame_id)) ret, frame cap.read() if ret: cv2.imwrite(fresult_{rank}_{scores[idx]:.3f}.jpg, frame) print(fTop {rank}: frame{frame_id}, score{scores[idx]:.4f}) cap.release() cap.release()逻辑说明查询过程本质上是在特征库矩阵上做一次矩阵乘法然后把结果排序取前K个。这里用cap.set跳转到指定帧再截图比顺序播放视频逐帧寻找要高效得多因为视频的每一帧都是通过时间索引寻址的跳转成本很低。参数方面top_k决定最多返回几个候选结果thr决定相似度低于多少的结果不算匹配。如果查询结果里混入大量不同人的画面说明阈值低了如果目标人出现了但没被返回说明阈值高了或者抽帧间隔太大把目标的关键帧漏掉了。提示第一次跑查询时建议把阈值临时调到0.2先看排序结果里目标人排在第几位。这个排名比绝对分数更能说明模型表现方便你确定合理的阈值范围。4.4 先跑小样本冒烟测试再跑全天视频我在这里有一条血泪经验不要第一次就直接对24小时视频建库。一个小时的视频以每5秒抽一帧计算大约产生720帧每帧可能有几个行人特征库会有数千条数据跑起来不算慢但如果生产环境里是十几个摄像头数据量会直接翻几倍出问题时定位成本很高。正确做法是先截取一段含目标人出现的1分钟视频片段跑一遍建库和查询全流程确认检测阈值、抽帧间隔、Re-ID特征提取、相似度计算都正常再扩展到全量数据。冒烟测试能帮你避开很多后期难以定位的问题比如检测框坐标映射错误、特征维度不匹配、npy文件路径错误这些低级但极其消耗时间的坑。这一步只需要多花十分钟能省下后面几小时甚至一天的排查时间怎么做都划算。5. 实战排雷5个反复出现的坑与排查手段5.1 目标人被漏检置信度阈值挡住了一半行人现象视频中明明有目标人出现但检索结果里完全找不到回看检测输出发现那一帧只有一个很低的置信度框比如0.28被设置的0.5阈值直接滤掉了。原因行人检测置信度在不同场景下差异极大。逆光、遮挡、远距离、行人只露出一部分身体都会让置信度显著下降。统一用一个较高的阈值等于把这些难样本全部放弃。解决把检测置信度下调到0.3同时对整个视频的检测结果做一次抽样检查看看低置信度的框是不是真的都是误检。如果是漏检居多就保持低阈值如果是误检居多再把阈值上调到0.35到0.4之间找平衡。5.2 检测框抖动导致同一人特征突变现象同一个行人连续出现在相邻几帧里分别提取特征后两个特征向量的相似度却只有0.5左右甚至低于与另一个人的相似度。原因YOLOv3的检测框在视频帧之间不是完全稳定的会有几个像素的抖动。裁剪出来的区域会因此带上不同比例的背景或者切掉一部分身体。Re-ID模型对输入图像非常敏感一点点裁剪偏移就可能让特征向量产生明显漂移。解决一是在裁剪时对检测框做适度外扩比如上下左右各扩10%到15%把完整的身体轮廓包含进去减少背景比例变化的影响二是在时间维度上做滑动平均取目标连续出现的5帧特征向量做平均用均值作为最终特征。这个办法看似简单实际效果立竿见影。5.3 特征库太大检索卡到怀疑人生现象特征库有几十万条数据检索一次需要好几秒完全达不到“输入照片立刻出结果”的预期。原因代码里用了for循环逐个计算相似度几十万次循环自然慢。另一个常见原因是numpy数组没有显式指定dtype默认变成了float64内存占用翻倍缓存命中率下降。解决用矩阵乘法替代循环一次算出所有相似度存储时明确用float32如果数据超过百万级可以直接用向量检索库例如faiss它能在毫秒级完成千万级向量的近邻搜索。工程上把特征索引单独部署成服务是全套方案走向可用的关键一步。5.4 换了摄像头同一个人的相似度整体掉到0.4现象A摄像头的目标特征和B摄像头的同一特征做比对相似度只有0.4左右明显低于同摄像头内比对时的0.8。这个现象在晴天和阴天的摄像头画面差异大时尤其明显。原因不同摄像头之间的白平衡、曝光和色彩偏移会让同一个人在画面里的颜色发生显著变化。Re-ID模型对颜色非常敏感因为衣服颜色是判别身份的重要线索。色彩偏移直接破坏了特征一致性。解决换用训练时做过色彩扰动的Re-ID模型比如训练阶段加了随机亮度、对比度、饱和度变化的模型它对跨摄像头色彩偏移的鲁棒性会好很多。另外在预处理环节做一次白平衡归一化也有效但要注意训练和推理时的预处理必须一致。5.5 库图和查询图的尺寸差异导致正向样本分数偏低现象特征库里的图为全身照而查询图是半身照或者只占了画面一小块导致实际是同一个人相似度却只有0.5排在了错误的位置。原因Re-ID模型对行人身体比例很敏感训练时输入基本是标准的全身图像。如果查询图和库图在身体占比上差异过大特征空间里会形成系统性偏移。解决对查询图做统一预处理把人像在画面中的占比控制在一定范围内必要时手动裁剪或补全背景。如果查询图本身就拍得不完整应该启动人工判断先确认输入图中行人是否完整再进入自动查询流程。这一步看起来不起眼但确实是很多项目从“能跑”到“实际能用”的差距所在。6. 验证与进阶用Rank-1和mAP量化效果再谈三个提升技巧6.1 用Rank-1和mAP给你的“找人”方案打分做了这么多工作总得有个量化指标说明效果好还是不好。在行人重识别领域最常用的两个指标是Rank-1和mAP。Rank-1的含义是在检索结果中排名第一的候选是否命中目标。实际操作上选10个目标行人作为查询集在库里标记出每个目标的正确出现帧跑完查询后统计第一条结果正确的比例。这个指标直观反映“系统能不能把人找对”。mAP考虑的是所有正确结果在排序中的位置。把一张查询图的检索结果从高到低排列对所有命中位置计算平均精度再对全部查询图取均值。mAP高说明系统不仅能把目标排在第一位而且能把所有正确出现都排在靠前的位置。对“查找特定行人”这个需求来说mAP比Rank-1更贴合实际因为目标可能在多个时间段多次出现。手工构建一个小验证集很简单选10个人每人一张查询图在刚才建好的特征库里标出他出现的所有框。跑一遍查询统计两个指标就够。这一步不需要额外写复杂代码能把之前的检索流程直接复用。6.2 三个可以直接落地的进阶技巧第一个技巧是时序平滑特征。前面提到过对连续帧的特征取滑动平均这里展开说一下具体做法对同一个检测框ID在连续帧中的特征向量做指数移动平均保留最后的结果作为该次出现的代表特征。这个技巧能明显降低检测框抖动带来的噪声。第二个技巧是检测加跟踪结合。纯检测加Re-ID是一帧一帧独立处理的如果同一人在相邻帧被识别成不同ID结果会非常碎。把YOLOv3的输出接一个轻量级跟踪器比如简单的IOU匹配或卡尔曼滤波给每个行人分配一个临时ID再针对一段轨迹做Re-ID特征聚合能找到更完整的“该行人从画面走进来又走出去”的轨迹片段这对输出结果的可读性提升非常大。第三个技巧是分离式缓存。同样一个出现在多帧里的行人每帧都提取一次特征很浪费算力。做法是按检测框的IOU或跟踪ID做去重同一个人只在首次出现时提取特征后续帧直接沿用。这个优化能把建库时间缩短一半以上。我现在的习惯是每换一个场景先拿一小段视频跑通冒烟测试把检测阈值、抽帧间隔、Re-ID预处理这些参数固定下来再跑全量数据。宁可前面多花十分钟调参数也不要让两小时的建库任务白跑一遍。这套流程陪着我在多个项目里踩坑、填坑希望能帮到你。本文还有配套的精品资源点击获取