资讯动态

YOLOv3+行人重识别:跨摄像头行人查找的完整实践

发布时间:2026/9/12 7:29:41 来源:尧图企业网站定制
简介项目整合YOLOv3目标检测与行人重识别技术实现从视频或图像中检测行人并查找特定目标的完整流程适合人工智能、计算机、自动化等专业的毕业设计、课程大作业及入门进阶学习。压缩包共51个文件以27个Python脚本为核心覆盖模型定义、数据加载、配置解析、检测与ReID推理等环节另含模型配置cfg/data/names、19张示例图片及说明文档整体仅5.52MB代码经调试可运行。资源按检测、重识别、查询等模块组织附有query图片与ReID模型配置完整覆盖从目标检测、行人外观特征提取到特定目标匹配的pipeline便于快速理解与二次开发。目前已有109人学习作为答辩98分的毕设项目整体工程具有较高学习与借鉴价值可在此基础上扩展多目标跟踪、跨摄像头检索等功能。1. 为什么是“YOLOv3 行人重识别”而不是一套端到端检测器停车场、写字楼、校园的监控系统常遇到一个问题昨天下午穿蓝色外套、背双肩包的人今天在另一个摄像头下又出现了系统能不能自动认出他来。单靠目标检测模型只能回答“画面里有个人”回答不了“是不是我要找的人”。姿态、光照、拍摄角度一变同一个人的像素差异可能比不同人都大像素级匹配根本不可靠。因此业界把任务拆成两段YOLOv3 在每一帧里框出所有行人行人重识别Person ReID模型把框出的行人图压缩成高维特征向量再与目标库比对排序。这也是大量人工智能大作业、毕业设计和监控项目 POC 里最常见的组合——检测部分直接跑通ReID 部分替换开源权重最后串成一套“查找特定行人”的可演示系统。下面把两段模型的关键原理、最小代码、参数设置和验证方法一次说清。2. YOLOv3 行人检测Darknet-53 结构、Anchor 与最小推理命令2.1 为什么现在仍用 YOLOv3 做行人检测YOLOv8 已经出到很后面但把 YOLOv3 用在“行人检测 ReID”这个组合里依然是高性价比的选择。原因是这个任务里行人只是单一类别不需要模型去学复杂语义边界ReID 真正关心的是“行人框得准不准、漏没漏”对检测头的表达能力要求并不高。YOLOv3 在 416x416 输入下对中等尺寸行人的召回率足够而且 Darknet-53 的卷积结构在 OpenCV DNN、ONNX Runtime、TensorRT 这些推理后端里算子支持很全部署链路干净。另一个现实因素是资料密度。搜索“yolov3 行人检测 源码”能看到大量可改的 cfg、weights 和 python 源码对刚进入这个方向的人来说踩坑成本比新模型低很多。很多人工智能学习路线把 YOLOv3 列为必读 baseline因为 Anchor、多尺度特征融合、NMS 这些概念在 v3 里最直观。先把它跑通再去换更新检测器改动点主要在网络加载和输出解析骨干替换成本并不高。2.2 三尺度输出与 Anchor 的处理逻辑YOLOv3 的骨干是 Darknet-53包含 53 个卷积层经过 5 次步长为 2 的下采样得到 13x13 特征图。网络从这条主干的三个不同深度引出 13x13、26x26、52x52 三个尺度的预测头分别负责大、中、小目标。每个网格单元预测 3 个边界框对应 9 个预设 Anchor在 COCO 数据集上这 9 个 Anchor 的宽高是 (10,13)、(16,30)、(33,23)、(30,61)、(62,45)、(59,119)、(116,90)、(156,198)、(373,326)分配规则是尺寸越大的 Anchor 挂在越小的特征图上。每个 Anchor 输出一个5 num_classes维向量前 4 个是相对网格的坐标偏移和宽高缩放第 5 个是 objectness 置信度后面是类别概率。推理时把三个尺度的输出全部收集起来先按置信度阈值过滤再用 NMS 合并同一个行人身上重叠的框。行人这类目标在画面中往往是竖长的长宽比集中在 0.3 到 0.5 之间公开权重自带的 Anchor 基本不用改这也是直接拿预训练权重做行人检测的前提。2.3 用 OpenCV DNN 跑通 YOLOv3 的最小代码import cv2 import numpy as np # 加载 Darknet 格式权重与配置文件 net cv2.dnn.readNetFromDarknet(yolov3.cfg, yolov3.weights) # 获取三个 YOLO 输出层的名字 layer_names net.getLayerNames() out_layers [layer_names[i - 1] for i in net.getUnconnectedOutLayers()] img cv2.imread(street.jpg) h, w img.shape[:2] # 转成网络输入 blob blob cv2.dnn.blobFromImage( img, 1 / 255.0, (416, 416), (0, 0, 0), swapRBTrue, cropFalse ) net.setInput(blob) outs net.forward(out_layers) boxes, confs, class_ids [], [], [] for out in outs: for detection in out[0]: scores detection[5:] cls_id int(np.argmax(scores)) conf float(scores[cls_id]) if conf 0.5: continue cx, cy, bw, bh detection[:4] # 坐标是相对 416x416 的归一化值映射回原图尺寸 cx, cy, bw, bh cx * w, cy * h, bw * w, bh * h boxes.append([int(cx - bw / 2), int(cy - bh / 2), int(bw), int(bh)]) confs.append(conf) class_ids.append(cls_id) # NMS 抑制重叠框 idx cv2.dnn.NMSBoxes(boxes, confs, 0.5, 0.4) persons [] if len(idx) 0: for i in idx.flatten(): if class_ids[i] 0: # COCO 类别 0 是 person persons.append(boxes[i])代码里需要注意三点。第一blobFromImage的1/255.0是输入归一化(416, 416)是网络输入尺寸cropFalse表示保持原图宽高比并填充而不是强行拉伸否则行人比例会变化。第二getUnconnectedOutLayers()返回的层索引要减 1 才能从layer_names中取到真实名字这个行为在不同 OpenCV 版本里一致。第三输出的detection[:4]是中心点坐标和宽高且归一化到 0 到 1必须乘回原图尺寸再画框很多人漏掉这一步得到一堆叠加在左上角的错误框。模型文件用官方yolov3.weights和yolov3.cfg按“yolov3 免费数据集 源码”去搜通常能找到配套下载。检测置信度先设 0.5 起步跑通后再按场景调。2.4 一张表改完全部检测参数参数名常用初始值调节方向与问题conf_thres0.5漏检多就降到 0.3误检多就升到 0.6这个值决定检测模块的召回上限nms_thres0.4密集人群重影多就降单人场景可以升到 0.5input_size416远处小行人多用 608追求帧率用 320行人高度小于 40 像素时请升到 608blob mean0Darknet 版本不同有 (0,0,0) 和 (0.5,0.5,0.5) 两种需要和权重匹配提示检测置信度和 NMS 阈值是两个作用完全不同的旋钮。置信度管“这个格子是不是真有行人”NMS 管“两个框是不是在描述同一个行人”调参时不要混用。调参时会遇到三种典型情况。一是行人密集但互相遮挡漏检不高但一个行人被框住两三次这时把nms_thres从 0.4 降到 0.3重叠框会更快被合并。二是相机在 15 米外行人只有 30 像素高416 输入下这类小目标基本被跳过把input_size升到 608或者放弃检测改用整帧滑窗。三是夜间监控噪声大conf_thres应提高到 0.6 以上否则车灯反光、树干晃动都会触发检测给后端 ReID 灌入大量没有价值的裁剪图。3. 行人重识别ReID从“检测到”到“是同一个人”3.1 任务定义与评价指标Rank-1 和 mAP行人重识别要回答的问题是给定一张目标行人的 query 图在一批不知道身份的 gallery 图里按“是同一个人的可能性”从高到低排序和以图搜图的逻辑一致。评测时用两个指标Rank-1 表示第一个返回结果就是目标人物的占比衡量“能不能一眼找到人”mAP 衡量整个排序的稳定性考虑目标人物出现在第 5 位、第 10 位这些情况在整个测试集上的平均精度。实际项目里两个一起看Rank-1 高但 mAP 低说明系统大多数时候能蒙对但不稳定换摄像头角度就会崩。为什么不能直接把两张行人图做像素匹配同一个人的两张照片在不同摄像头下往往差在光照、视角、分辨率和遮挡上像素层面的差异可能比不同人还大。CNN 提取的 embedding 经过行人数据训练后会淡化外观干扰保留体态、衣着配色组合、配饰这类身份级特征。核心不是卷积层本身而是让同类距离小、异类距离大的度量学习目标。3.2 特征提取器ResNet50 去掉 FC 层工程里最常见也最稳妥的方案是把 ImageNet 预训练的 ResNet50 去掉最后两层用倒数第二层的 2048 维特征作为行人描述子再接一个 BNNeck 和全连接层在行人数据上微调。输入尺寸固定为 256x128这是 ReID 一直沿用的 2:1 宽高比因为行人整体竖长直接 resize 成正方形会把比例压坏。训练时同时使用 ID 交叉熵损失和 Triplet Loss前者让模型知道“这几张图属于同一个人”后者在特征空间里拉开不同人之间的距离。开源社区里成熟的 ReID 项目训练好后通常会把编码器权重单独导出一个文件推理时输出 512 维或 2048 维的向量。训练数据的目录结构一般是ID/xxxx.jpg一个文件夹一个人每个人至少两个视角以上。数据集规模不需要很大几十个 ID、每个 ID 5 到 10 张图就能让实验跑出可演示效果。3.3 手写 PyTorch 特征提取与相似度计算import torch import torch.nn.functional as F import torchvision.transforms as T from torchvision import models from PIL import Image # 骨干网络ResNet50去掉最后的 avgpool 和 fc base models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) encoder torch.nn.Sequential(*list(base.children())[:-1]) encoder.eval() transform T.Compose([ T.Resize((256, 128)), T.ToTensor(), T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) torch.no_grad() def get_feature(pil_img): x transform(pil_img.convert(RGB)).unsqueeze(0) feat encoder(x).flatten(1) # (1, 2048) return F.normalize(feat, p2, dim1) # L2 归一化后直接用余弦相似度 q get_feature(Image.open(query.jpg)) g get_feature(Image.open(gallery_1.jpg)) score (q g.T).item() # 余弦相似度范围约 [-1, 1]逻辑说明list(base.children())[:-1]拿掉的是 avgpool 和全连接分类层保留卷积部分的全局响应flatten(1)之后得到 2048 维特征。L2 归一化把特征向量长度抹平内积就等于余弦相似度分数只和方向有关和行人图亮度、对比度带来的幅值无关。这个 baseline 直接在 ImageNet 权重上跑没有经过行人数据微调相似度分数会整体偏高或偏低但做验证流程足够等跑通之后再换在 Market1501 上训练过的权重替换 encoder。3.4 3 个影响重识别精度的参数参数常用值作用输入分辨率256x128换 384x192 能再提升 1 到 3 个点 Rank-1特征耗时增加约 40%embedding 维度512 / 2048512 维省内存2048 维精度最高检索耗时接近翻倍归一化方式L2 余弦不归一化时亮度差异会污染相似度欧氏距离在低光照下表现差输入分辨率是第一个参数。256x128 是速度与精度的平衡点如果画面里行人框本身切得很紧分辨率再高收益有限。第二个是 embedding 维度gallery 有十万张图时内存和检索耗时都会上升用一个线性层把 2048 压到 512 维Rank-1 掉 0.5 到 1 个点换来内存大幅改善。第三个是归一化方式特征必须做 L2 归一化再比较度量尽量用余弦相似度特别当 gallery 里行人图宽度差异很大的时候。另外检测框的裁剪质量比模型结构更影响结果——框如果只切到半个头ReID 效果会断崖式下跌。提示如果发现某张 query 和整批 gallery 分数都在 0.9 以上大概率是特征提取模型没有经过 ReID 训练把所有行人混在了一起先检查归一化和模型文件不要急着调相似度阈值。4. 完整查找流程采集图建立 Gallery视频帧里检测并排序4.1 整条链路的顺序与去重策略完整的查找流程分两侧。离线侧把目标行人的几张照片送进 YOLOv3 裁剪成行人图再逐张提取特征存入 gallery每一条记录包含特征向量、来源文件名、采集时间。在线侧视频帧进来以后先检测行人把每个新裁剪图也过一遍同一个特征提取器然后与 gallery 里的每个特征计算余弦相似度分数超过阈值就认为命中目标输出这一帧的时间点和坐标框。这里必须处理一个工程问题同一个行人在视频里会被连续几十帧检测到直接上报会产生几十条重复告警。常见做法是加一个命中缓冲比如记录上次命中时间和框坐标命中后进入 5 秒冷却期或者维护一个轻量的短期跟踪表用 IoU 判断新候选框和上一帧已确认目标框是否延续延续命中只更新时间戳不重复告警。另一个细节是所有人像特征都应该先经过 L2 归一化再进 gallery不然不同照明条件下向量长度不同余弦相似度计算会失真。4.2 端到端代码YOLOv3 输出接入 ReID 特征提取下面的代码假设你已经运行过 2.3 节和 3.3 节的初始化部分即net、out_layers、encoder、transform都已在内存中。import cv2 import numpy as np import torch from PIL import Image import time # 检测函数输入 BGR 图返回裁剪好的行人 RGB 图片列表 def detect_persons(img_bgr): blob cv2.dnn.blobFromImage( img_bgr, 1/255.0, (416, 416), (0, 0, 0), swapRBTrue, cropFalse ) net.setInput(blob) outs net.forward(out_layers) boxes, confs, class_ids [], [], [] for out in outs: for det in out[0]: scores det[5:] cls_id int(np.argmax(scores)) conf float(scores[cls_id]) if cls_id 0 and conf 0.4: cx, cy, bw, bh det[:4] h, w img_bgr.shape[:2] boxes.append([int((cx-bw/2)*w), int((cy-bh/2)*h), int(bw*w), int(bh*h)]) confs.append(conf) idx cv2.dnn.NMSBoxes(boxes, confs, 0.4, 0.4) if len(idx) 0: return [] crops [] for i in idx.flatten(): x, y, w, h boxes[i] crop img_bgr[y:yh, x:xw] crops.append(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB)) return crops # 构建 gallery目标行人的特征和来源 def build_gallery(target_paths): feats, meta [], [] for path in target_paths: img cv2.imread(path) for crop in detect_persons(img): feat get_feature(Image.fromarray(crop)) feats.append(feat) meta.append(path) return torch.cat(feats, dim0), meta gallery_feats, gallery_meta build_gallery([ target_1.jpg, target_2.jpg, target_3.jpg ]) # 在线查询处理视频流的一帧 for frame in video_stream: # video_stream 来自 cv2.VideoCapture frame preprocess_frame(frame) # 缩放到宽 960 等预处理 for crop in detect_persons(frame): q get_feature(Image.fromarray(crop)) sims q gallery_feats.T best_score, best_idx sims.max(dim1) if best_score.item() 0.7: report_hit(frame, crop, gallery_meta[best_idx], best_score.item()) cooldown time.time() 5 # 5 秒冷却这个完整流程有三个可调的系统级参数相似度阈值SIM_THRESHOLD初始 0.7检测置信度MIN_CONF初始 0.4冷却时间COOLDOWN初始 5 秒。检测置信度设 0.4 而不是 0.5是因为后面还有 ReID 判定宁可多给几个候选框让特征模型去判别也不漏掉目标q gallery_feats.T一次算完所有相似度比 for 循环快得多。build_gallery会把多张目标图检出的人全部拼成(N, 2048)特征矩阵在线查询时直接矩阵乘。参数初始值影响与调整SIM_THRESHOLD0.7漏报多调低到 0.6误报多调高到 0.75围绕 0.6~0.8 做验证集扫描MIN_CONF0.4检测召回优先设 0.4性能紧张可提到 0.5COOLDOWN5s太长会漏掉目标第二次出现太短会刷屏gallery 更新学习率0.1每次高置信命中后按0.9*旧特征 0.1*新特征增量更新4.3 视频流场景里的 3 个工程化要点第一点是 ROI 与最小尺寸限制。监控画面里 50 米外的行人可能只有 20 像素高检测几乎必挂ReID 也提不出有效特征。建议在检测前把画面缩放到宽 960并且只统计行人框高度大于 60 像素的检测结果这个限制能消掉大量误报源。第二点是 gallery 的动态更新。同一个目标在下午换了外套晚上再出现时最初采集的特征就有些跟不上。常见做法是每条 gallery 特征维护一个滑窗每次高置信命中后用新特征做指数滑动平均更新系数取 0.1 到 0.2 之间。注意只更新与命中框 IoU 连续的目标避免把相似路人特征混进库里。第三点是多摄像头的时间线整合。摄像头编号加命中时间戳可以拼出目标的行动轨迹这是“查找特定行人”项目真正要交付的东西。实现时给每个镜头维护一个独立的冷却时间用镜头编号和命中时刻做聚合查询即可不必上重量级跟踪算法。5. 自建评测与推理提速——Rank-1/mAP 验证与 TensorRT 部署5.1 用自建数据算 Rank-1自建评测集按query/和gallery/两目录组织文件名带上 ID比如ID_001_shot_01.jpg。划分时同一个 ID 在 query 和 gallery 里都必须出现且不能来自同一帧否则评测结果会虚高。Rank-1 验证代码def eval_rank1(query_feats, query_ids, gallery_feats, gallery_ids): hits 0 for qf, qid in zip(query_feats, query_ids): sims qf gallery_feats.T top_idx sims.argsort(descendingTrue)[0] hits (gallery_ids[top_idx] qid) return hits / len(query_ids)mAP 则每个 query 算一次 AP 再取平均。实际验证时有个典型现象同一 ID 的图在时间上相邻时 Rank-1 接近 100%跨镜头后立刻掉到 60%这说明模型记住的是场景背景而不是行人本身评测数据必须按镜头切分。5.2 推理提速ONNX Runtime 与 TensorRT检测部分换推理后端是最直接的提速手段。OpenCV DNN 之外先把模型导出成 ONNX再用 ONNX Runtime GPU 跑通常能提升 30% 到 50%。导出后可用 TensorRT 优化trtexec --onnxyolov3.onnx --saveEngineyolov3.engine --fp16实时系统里更大的瓶颈反而是 ReID每帧多个行人图每张都过一次 ResNet50 延迟很高。常见做法是攒批次把一帧里的所有行人图拼成一个 batch 一起前向crops [transform(c).unsqueeze(0) for c in person_crops] batch torch.cat(crops, dim0) # (N, 3, 256, 128) feats encoder(batch).flatten(1) # (N, 2048)这一处改动就能把 ReID 部分从逐张推理变成批量推理GPU 利用率明显上升。如果还慢再把输入分辨率从 256x128 降到 192x96Rank-1 会有 2 到 3 个点的损失。5.3 联合阈值把检测置信度和 ReID 相似度绑在一起整个系统里最容易被低估的是阈值组合方式。单独把SIM_THRESHOLD设在 0.7ReID 误判会把很多路人当成目标单独把MIN_CONF设在 0.4一堆模糊行人框涌进 ReID 又会造成误匹配。两级判定效果好于单一阈值第一级检测置信度用 0.4 保持召回第二级 ReID 用 0.72 收严同时要求连续两帧命中同一个框才上报if det_conf 0.4 and reid_sim 0.72 and hit_streak 2: report()在自建验证集上分别扫几组(det_conf, reid_sim)组合画出漏报率和误报率的曲线曲线拐点就是这套数据的最优工作点。把这组数标到告警日志里比盲调固定阈值有用得多。本文还有配套的精品资源点击获取

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

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

免费获取报价