做密集行人检测这个项目,我前后折腾了将近两个月,踩了不少坑,也积累了不少经验。最近把代码和模型整理归档,想了想还是把整个实现过程完整写出来,给想用YOLOv12做目标检测的朋友一条可参考的路径。这套系统本质上是一个完整的软硬件工程:底层是YOLOv12检测模型,上层是PySide6做的图形界面,再加上登录注册模块、数据集标注流程、训练脚本和推理引擎,属于把“模型训练”和“软件工程”缝合到一起的综合项目。如果你正打算做类似的毕业设计、竞赛项目,或者要落地一个真实场景的监控检测系统,这篇内容应该能帮你少走很多弯路。
先聊聊这套系统能做什么。你打开程序,首先看到的是登录注册界面,注册完账号之后进入主界面,主界面里可以加载本地图片、视频、调取摄像头实时画面,模型会自动检测画面里的行人,画出检测框、显示置信度、统计人数,还能把检测结果导出成文件或者直接保存图片。听着不复杂,但把这一整套流程跑通并且做到稳定,涉及的点其实相当多:数据集标注和质量、模型训练参数、UI线程调度、账号数据存储,每一个环节都可能出问题。这篇文章我会按项目推进的顺序,把设计和实操细节完整拆开来讲。
1. 系统整体设计与思路拆解
1.1 为什么选YOLOv12做密集行人检测
选模型这件事,我是认真对比过的。目标检测模型现在主流的就那几类:两阶段的Faster R-CNN系列、单阶段的YOLO系列、基于Transformer的DETR系列。两阶段检测精度高但速度慢,适合离线高精度场景;DETR系列对小样本和重叠目标处理不错,但模型体量大、部署麻烦;YOLO系列在“速度+精度”的平衡上一直做得最好。YOLOv12在v8基础上又引入了注意力机制层面的改进,对特征提取和融合的细节做了优化,小目标和密集重叠目标的检测能力比前代有肉眼可见的提升。
密集行人场景和普通目标检测不一样,它有几个特别致命的问题。
第一个问题是尺度差异。监控摄像头往往架在高处,远处行人只有二三十像素高,近处行人可能占满半个画面。同一个画面里目标尺度跨度太大,很多模型会把小目标漏掉。
第二个问题是遮挡。行人是非刚性目标,姿态多变,人群密集的时候互相遮挡严重,人跟人之间、人跟物体之间的边界很模糊。检测模型输出的框一旦重叠度较高,后处理阶段的NMS很容易把两个行人的框误判成同一个,直接吞掉一个检测结果。
第三个问题是密度。像商场、地铁站出口、演唱会现场这种地方,单张图可以挤下几百个人头,光靠人脸检测肯定不现实,必须靠全身或半身检测去做。
YOLOv12针对这些痛点做了不少优化。它的Backbone和Neck部分在特征融合上更精细,高分辨率特征层保留得更好,加上注意力机制的权重分配,让模型在复杂背景下能更准地把“前景行人”和“背景干扰”分开。我实际训练下来的感受是:同样的数据集和训练轮数,v12在密集小目标场景下的mAP比v8高了差不多2到3个点,尤其是在遮挡严重的样本上,效果差别一眼就能看出来。
1.2 系统架构与功能模块划分
整个系统我按“数据层—模型层—应用层”三层来组织。
数据层负责数据集的管理:原始图片、标注文件、训练集验证集划分、数据增强策略。这一层是基础,做不好后面模型效果一定拉胯。很多人一上来就急着跑模型,数据随便找几张就开训,结果自然是模型根本学不到东西。
模型层负责YOLOv12的训练和推理,包括模型文件、训练脚本、推理引擎封装、结果后处理(NMS、置信度过滤、绘制检测框)。这一层要求代码结构清晰,检测逻辑要能独立运行,不能跟界面代码强耦合。
应用层就是用户能直接操作的部分:登录注册界面、主界面、检测逻辑的调度、检测结果的展示和导出。应用层的核心要求是响应快、不卡顿、操作直观。很多做AI的同学把模型训练做得很好,但界面交互一塌糊涂,用户点一下按钮要卡三秒,这在落地场景里是完全不可接受的。
技术选型上,界面框架我用了PySide6,也就是Qt的Python绑定。之所以不用Tkinter,是因为它做复杂布局和现代化界面太吃力;不用Web前端的原因是本地部署场景下,桌面应用更轻便、不依赖浏览器环境,打开即用。数据库用SQLite,轻量且不需要单独装服务端,用户信息存在本地文件里,对单机应用来说完全够用。
后端肯定是用PyTorch,YOLOv12的原生训练推理生态都在PyTorch上,社区资源也最丰富,出了问题搜解决方案也好搜。
1.3 方案选型背后的取舍逻辑
有几个取舍我想单独说一下,因为很多人会在这几个点上纠结很久。
第一个是“直接拿预训练权重用,还是要自己训练”。如果你检测的目标不是标准COCO 80类里的类别,比如你只想检“安全帽”“车辆品牌”“施工人员”,那预训练权重大概率不满足需求,必须用自己标注的数据微调。行人检测有一点好:公开数据集的person类本身就覆盖得非常充分,预训练模型对行人的效果已经不错。但如果你的监控场景很特殊,比如高空俯瞰摄像头、鱼眼镜头、特定角度下的行人,直接拿官方权重去跑是hold不住的,必须用场景数据重新微调,效果差别很明显。
第二个是界面端处理检测结果是“调用外部脚本”还是“封装成类”。很多人喜欢在UI里用subprocess直接弹一个detect.py命令去执行检测,这样做开发省事,但用户体验很差:界面卡死、结果返回不及时、不能实时显示中间状态。我是花了时间把检测功能封装成独立的Detector类,在UI主线程外开工作线程去跑推理,再用信号把结果传回主线程刷新画面,这才算是一个真正可用的桌面应用。
第三个是Web还是桌面。有些朋友想做成网页版,用Flask或FastAPI做后端,浏览器做前端。这个方向适合远程访问场景,但你需要额外处理并发请求、模型常驻内存、网关部署等一系列问题。纯本地单机场景,桌面端反而更省心。我这套系统的目标场景就是一台终端机或者一台本地服务器直接跑,所以选了桌面端。
2. 密集行人场景的数据集准备与处理
2.1 数据来源与标注格式
数据集的质量直接决定模型性能上限,这个怎么强调都不过分。算法圈里有句话:“模型决定下限,数据决定上限。”花时间把数据做成高质量,比盲目堆模型参数量更有性价比。整个项目最耗时间的不是写代码调参数,而是整理数据集和复查标注。
我的数据来源分三块:第一块是从开源行人检测数据集中筛选的图片,大概占七成;第二块是自己用手机和摄像机在不同场景拍的,再按帧截取,占两成;第三块是从监控录像片段里截取的关键帧,用来补足特殊角度和光线条件。开源数据的优点是量大、场景多样,缺点是风格和我们的目标场景有偏差,所以一定要混入自己的场景数据来微调,纯靠开源数据模型很容易在真实场景上“水土不服”。
标注格式用YOLO的txt格式,每一行对应一个目标框,格式是:类别编号 x_center y_center width height。注意这四项坐标全部要归一化到0到1之间,而且是相对于整张图片宽高的比例,不是像素坐标。很多新手在这一步容易写错,比如直接拿像素坐标除以2就填进去,训出来的模型框全是偏的。
标注工具我用的是LabelImg,它可以直接导出YOLO格式,省去自己写格式转换脚本的麻烦。标注密集行人最耗时的就是“不漏标”,人一多就容易漏,尤其是被遮挡了一半的行人。我的经验是:宁可多标一个模糊框也不要漏标,因为漏标对训练是负反馈,会让模型学着忽略这些目标。
2.2 关键数据预处理策略
数据预处理这块,密集场景和普通场景有个显著差异:必须把“小目标增强”放在第一优先级。常规目标检测可以把精力放在颜色扰动、模糊模拟这些通用策略上,但密集行人场景里,提升小目标检测能力才是决定模型能不能用的核心。
我用到的增强策略包括Mosaic、MixUp、HSV扰动、随机翻转、平移和缩放。Mosaic是YOLOv5提出来的策略,把四张图拼成一张去训练,相当于变相扩大了batch size,还让模型学会在不同尺度下检测目标,对小目标场景尤其有效。MixUp则是把两张图按透明度叠加,模拟遮挡场景,这个思路和行人互相遮挡的现实情况很贴近,相当于预先让模型在训练阶段就见过了“目标有一部分被盖住”的情况。
另外我重点做了“高分辨率训练”,这个非常关键。YOLO系列默认的输入尺寸通常是640×640,但对密集小目标场景,我强烈建议把输入分辨率提到1280。代价是显存占用和训练时间明显增加,但对小目标的检测效果提升非常直接。我用一张包含大量远处行人的测试图实测过,640输入下远处行人漏检率很高,1280输入下基本能框出大部分远处行人。如果你的显存不够,可以先用640把baseline跑出来,再用1280微调最后几十个epoch,效果会比全程用640好很多。
还有一点很多人会忽略:检测框的标注质量在密集场景里尤其致命。如果标注框的边缘歪了,模型学到的就是一堆歪框的先验,最后输出结果也是歪的。我建议标注完一批数据后专门做一次“框质量复查”,尤其是互相遮挡的行人附近,标注框必须尽量贴合目标真实边缘,不要为了赶进度随手拉一个框就完事。
2.3 数据集划分与类别设定细节
数据集按7:2:1划分训练集、验证集、测试集。划分必须在“图片级别”进行,不能把同一段视频连续帧的图片同时分到训练集和验证集里,否则验证集会严重高估模型的实际表现。正确做法是先把同一个视频的所有帧归为一组,再按视频维度打散划分,或者按场景文件夹分组,保证同一个场景只出现在某一个集合里面。这个细节很多人不注意,训练完看验证集指标很好,一部署到真实场景就原形毕露。
类别就一个:person。这有一个小技巧,如果你后续还想对行人做细分,比如区分“正常行人”“倒地行人”“工作人员”等特殊状态,就要在标注阶段一次性把所有需要的类别都标出来。不要等训练完再补标,因为模型训练好后,你要加新类别就得全量重新训练,代价很大。我当时一度想加“骑行者”这个类别,后来发现标注数据里骑行者样本太少,硬加只会拉低整体精度,果断放弃,先把person类做到极致。
训练集图片的尺寸也要统一处理。图片太大占用显存多,太小又丢失细节。我统一做了两步:首先按最长边缩放到接近模型输入尺寸,同时保证宽高比不变;多余部分用灰色填充到正方形,避免直接拉伸导致目标变形。实际测试下来,这种带padding的方式比直接resize的检测精度要稳定一些。
3. 模型训练与参数调优实战
3.1 环境搭建与依赖安装
环境这关就能卡住一半的人,我把完整流程整理出来,照着做基本不会出大问题。
操作系统建议用Ubuntu 20.04或22.04,Windows 11也可以。Python版本建议3.9到3.11,不建议用最新的3.12,因为PyTorch和CUDA生态对3.12的支持还比较慢,容易在装依赖的时候报错。
GPU是训练的前提条件。个人训练建议显存至少8GB起步。我用的是某型号12GB显存的显卡,训练1280分辨率的模型,batch_size大概能开到8到16之间。如果你只有6GB显存,训练1280会非常吃力,建议退回640分辨率,或者用云GPU。
安装步骤很固定:先装CUDA Toolkit和cuDNN,再安装对应版本的PyTorch。CUDA版本和PyTorch的对应关系一定要去查官方兼容表,版本不匹配会出现“CUDA unavailable”这种让人极度怀疑人生的报错。装完之后用下面这段代码验证环境:
import torch print(torch.__version__) print(torch.cuda.is_available()) if torch.cuda.is_available(): print(torch.cuda.get_device_name(0))如果CUDA可用性打印为True,环境基本就通了。这一步能筛掉80%的环境问题,不要跳过这个验证,直接去跑训练脚本,到时候报错都不知道从哪儿查起。
3.2 训练关键参数解析
训练参数直接影响模型质量,我把自己调出来的稳定参数贴出来,并解释每个参数为什么这么设。
- epochs:训练了200个epoch。YOLO训练在100个epoch之后还在持续涨点,不要过早停。我见过有人只训练了30个epoch就拿来部署,结果漏检率高得离谱,模型根本没有充分收敛。
- batch_size:根据显存尽量开大,建议8到16。
- imgsz:640先训练50个epoch做baseline,再用imgsz=1280接着微调30到50个epoch。这种“低分辨率预训练+高分辨率微调”的策略在很多实际项目里都有效。
- lr0:初始学习率0.01,配合余弦退火调整。
- weight_decay:0.0005,防止过拟合。
- 优化器:SGD足够,AdamW不一定更好,YOLO系列默认的SGD表现很稳定。
训练命令大致是这样的:
python train.py --data data/person.yaml --weights yolov12.pt --img 640 --batch 16 --epochs 100 --device 0我把关键参数整理成速查表,方便你排查问题的时候快速对照:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| epochs | 150-200 | 前100个epoch出baseline,后面用更高分辨率微调 |
| batch_size | 8-16 | 显存越大越好,OOM就先降这个 |
| imgsz | 640→1280 | 低频预训练、高频微调是密集小目标的制胜点 |
| lr0 | 0.01 | 配合余弦退火,最后阶段学习率降到很低 |
| weight_decay | 0.0005 | 防止过拟合,数据集小的时候尤其重要 |
| nms_iou | 0.5 | 密集场景不要用默认0.45,容易误杀重叠目标 |
特别要说一个密集行人场景的独有参数:NMS的IoU阈值。训练结束推理的时候,默认的NMS阈值在密集场景往往会误杀大量互相遮挡的框。我把置信度阈值固定为0.25,NMS的IoU阈值从默认的0.45调整到了0.5甚至0.55。同时可以考虑用soft-NMS或者DIoU-NMS替代普通NMS,对密集目标有明显的改善,这个在第五部分还会细说。
3.3 训练过程监控与模型评估
训练过程中我重点关注几个指标曲线:loss、mAP50、mAP50-95、precision、recall。不要只看mAP50这一项指标,因为密集场景下漏检多的时候,precision和recall的平衡问题会被mAP掩盖。
一个实用的经验:当precision和recall差距较大时,要根据业务场景做取舍。如果系统用在地铁站这种人流监控上,我宁愿recall高一些、误检多一些,也不愿意漏检,因为漏一个行人可能意味着监管盲区。如果是做商场客流统计分析,过多误检会导致统计人数虚高,就需要适当提高置信度阈值,牺牲一点recall保住precision。
训练完要导出指标报告,重点看验证集上的PR曲线和混淆矩阵。PR曲线越靠近右上角越好;混淆矩阵能看出模型容易把什么背景误判成行人。比如看到大量把“栏杆”“柱子”这类竖条形物体误检成行人的样本,说明训练集中行人周围的背景样本还不够多样,需要专门收集这类负样本加入训练。
模型导出这一步也很关键。最终交付时我建议同时导出PyTorch的.pt格式和ONNX格式,ONNX格式用onnxruntime推理,部署时不用纠结CUDA版本,兼容性好很多。导出命令只需要调YOLOv12自带的export脚本,导出后可以用onnxruntime的Python API写一个极简推理脚本,验证导出的模型输出结果和PyTorch推理结果是否一致。这一步能提前发现很多模型转换带来的精度损失问题。
4. 核心功能实现:UI界面与登录注册
4.1 界面布局与交互逻辑
UI这块我用PySide6来做。主界面的布局分成三个区域:左侧是控制面板,中间是检测画面显示区,右侧是结果信息栏。
左侧控制面板放三个输入按钮:打开图片、打开视频、打开摄像头。摄像头调用用的是OpenCV的VideoCapture,视频流实时传入推理线程做检测,每处理完一帧就把结果信号发到主线程刷新画面。中间显示区用QLabel配合QPixmap绘制。检测结果帧是把模型输出的boxes画到原图上的结果,绘制用OpenCV的rectangle和putText。
在这里有一个性能关键点:推理线程和UI线程之间传递图像数据,千万不要直接把整个大尺寸图像作为信号参数去发,否则主线程会被大量图像数据的拷贝操作拖垮。我的做法是只发送检测结果结构体(框坐标列表、类别、置信度),由主线程根据结果在原图上自行绘制。实测这样处理视频流时UI帧率稳定不少。
右侧结果栏显示当前帧的累计检测人数、平均置信度、当前模式,以及“保存当前帧”“导出检测结果”两个按钮。保存当前帧很好理解,导出功能是把当前画面的检测结果导出成CSV,包含行人ID、坐标、置信度这些信息,方便后续做数据分析。
4.2 登录注册模块的实现细节
登录注册模块是很多同学做系统最容易糊弄过去的部分,但仔细做的话,这里也有很多值得深挖的细节。
数据库我用SQLite,建了一张users表,字段包括id、username、password_hash、salt、create_time。密码绝对不允许明文存储,这是最基础的安全底线。我这里用sha256配合盐值做了迭代哈希。注册流程大致如下:用户提交用户名和密码后,后端生成一个随机盐值,把密码和盐拼在一起做哈希运算,把哈希结果和盐一起存库。用户登录时,用同样的盐值重算哈希,和库里的哈希比对,一致则登录成功。
代码核心部分大概是这样的:
import hashlib import os def hash_password(password: str, salt: str) -> str: hashed = hashlib.sha256((salt + password).encode()).hexdigest() for _ in range(1000): # 增加迭代轮数,提高暴力破解成本 hashed = hashlib.sha256((hashed + salt).encode()).hexdigest() return hashed def register(username, password): salt = os.urandom(16).hex() password_hash = hash_password(password, salt) # 写入SQLite # INSERT INTO users (username, password_hash, salt)登录成功后,主界面里保存一个登录状态,用全局变量或者配置文件记住当前用户,后续可以按用户记录操作日志。
还有一个容易被忽略的细节:注册时要做“用户名唯一性校验”和“密码强度校验”。密码至少8位,且包含数字和字母,否则注册直接反馈错误。别小看这个细节,很多系统的安全问题就是从弱密码开始的。测试的时候我试过用纯数字“123456”注册,系统直接拒绝,虽然一时觉得麻烦,但这是该有的底线。
4.3 检测引擎与UI的集成方式
检测引擎和UI的集成是整个项目中最容易出现“卡死”问题的环节。核心原则是:任何耗时操作都不能放在UI主线程里。
YOLOv12单帧推理在GPU上大概需要20到60毫秒,加上前后处理,一轮耗时可能在100到200毫秒之间。如果你直接在按钮回调里同步执行推理,点击按钮后界面会无响应,看起来就像死机了一样。
我的做法是利用QThread开一个工作线程,把视频帧队列传给工作线程,工作线程处理完毕之后通过pyqtSignal发信号回UI线程更新画面。核心结构如下:
class DetectWorker(QThread): result_ready = pyqtSignal(dict) def __init__(self): super().__init__() self.frame_queue = queue.Queue() self.running = True def run(self): while self.running: frame = self.frame_queue.get() result = detector.predict(frame) self.result_ready.emit(result)UI里只需要在槽函数里接收结构化的检测结果再刷新画面即可,主线程永远不做推理计算,界面响应就会非常顺畅。这个多线程模型是整个桌面应用流畅度的核心保障。如果不这么做,即使模型效果很好,体验也是糟糕的。
5. 常见问题与排查技巧实录
5.1 漏检误检频繁怎么办
这类问题排在所有问题出现频率第一位。排查思路要按顺序来:先看输入图像质量,再看模型推理参数,最后考虑训练数据。
输入图像质量问题,比如摄像头画面模糊、逆光过曝、运动模糊,这类情况任何模型都很难做得好,要先把输入帧的预处理做干净。如果画面本身不行,模型再强也没有意义。
推理参数层面,置信度阈值调低一点能缓解漏检,但代价是误检增加。我自己用的是0.25这个折中值,NMS IoU阈值调到0.5。如果漏检还是严重,检查一下是否用了1280的高分辨率输入,这个对远处小行人的检测影响极大。
如果这些都不行,问题大概率出在训练数据上:要么数据里小目标样本、遮挡样本太少,要么数据分布和实际测试场景差异太大。这时候不要盲目调参或者加大模型,老老实实补数据永远比调参有效。我遇到过最典型的案例是:某个监控角度特殊,所有测试样本都是俯视角,训练数据全是平视角,模型效果自然差,补了几百张俯视角标注图片之后,检测效果立刻上一个台阶。
我把排查顺序整理成速查表:
| 症状 | 首选排查方向 | 备注 |
|---|---|---|
| 漏检严重 | 置信度阈值、输入分辨率 | 先确认是否在用1280输入训练/推理 |
| 误检多 | 训练数据负样本、置信度阈值 | 补背景负样本,调高置信度阈值 |
| 推理卡UI | 是否在UI主线程做推理 | 必须用QThread工作线程 |
| 显存不足OOM | batch_size、imgsz | 开启amp混合精度 |
| 模型部署后精度下降 | 导出格式 | 优先用ONNX统一部署环境 |
5.2 训练和推理的速度与显存问题
显存不足是新手最常见的“劝退点”。解决顺序是这样的:先把batch_size调小,再把输入分辨率从1280降到960或640,最后考虑开启混合精度训练,也就是amp=True。混合精度训练可以在几乎不损失精度的情况下把显存占用降低将近一半,而且能提速,我建议不管显存够不够都开。
推理速度太慢的话,优先看是不是把推理放在了UI主线程,这会导致视频流整体卡顿。排除之后,可以用TensorRT或者ONNX Runtime做加速,量化和图优化之后推理速度比PyTorch原生推理快不少。如果目标设备有GPU,TensorRT是最优选;纯CPU环境走ONNX Runtime加int8量化,也能做到勉强可用的帧率。
5.3 打包部署阶段的坑
项目做完之后,很多朋友想打包成exe或者独立可执行文件,这个阶段简直是踩坑重灾区。
第一个坑是PySide6资源文件打不进去,空图标、白屏界面都是这个原因。要在打包配置里把qml和资源文件显式包含进去,并且用相对路径引用资源。
第二个坑是模型文件的路径冲突。打包后程序工作目录和开发时不一样,写死绝对路径必然会崩。最好把模型路径设计成可配置项,运行前自动检测同级目录下是否存在权重文件,找不到就提示用户。第三个坑是CUDA运行库。目标机器如果和开发机器的CUDA版本不一致,GPU推理可能直接报错或者退化到CPU模式。稳妥的做法是打包携带ONNX模型而不是PyTorch的pt权重,运行时用ONNX Runtime的GPU版本,兼容性会好很多。
6. 几个容易被忽略但影响很大的细节
这一节集中聊聊前面散落在各个章节里没有展开、但实战中体验非常明显的几个点。
一个是模型导出的格式选择。训练好的模型是.pt格式,在项目目录里用自然没问题,但跨环境部署时.pt格式很依赖PyTorch环境,兼容性问题多。我最终的交付形态是同时提供.pt和ONNX两种格式,ONNX格式用onnxruntime推理,部署时不用关心CUDA版本,省掉很多麻烦。
另一个是检测框的可视化样式。画框的时候我特意做了两种模式:全身框和半身框。密集场景下半身框在很多情况下比全身框更稳定,因为遮挡往往发生在腿部,全身框会被遮挡物切断,导致框的尺寸失真。我在界面里加了一个“检测框模式”的开关,让使用者可以在全身框和半身框之间切换。实测极端拥挤场景下,半身框模式的效果更稳。做产品的时候,这种可视化细节就是体验差异。
日志系统也是容易被忽略但非常实用的模块。我把每次登录、每次检测统计、异常信息都写进本地日志文件,方便后续回溯问题。实际部署之后,日志帮了大忙,用户反馈说某个时段检测不准,一翻日志就能定位是模型置信度设置问题还是视频流中断问题。
另外,训练过程中要定期保存best.pt和last.pt两个权重文件。best是验证集上效果最好的,last是最后一个epoch的结果。很多人习惯只留最后一份,结果训练中途效果最好的可能是第150个epoch,后面那些epoch反而过拟合了,最后拿了一个次优模型去部署,白白浪费了前面的训练成果。
结尾
行,这套基于YOLOv12的密集行人识别检测系统,从数据集、训练、UI到登录注册的完整实现就讲到这里。希望对准备做类似系统的朋友有帮助。最后分享一个我踩过的很典型的坑给大家提个醒:我第一次尝试用1280分辨率训练之前,没有确认显存足够,直接把batch_size设成了16,结果GPU显存爆掉直接崩溃,白跑了两个小时。后来学乖了,任何新参数组合都先跑一个1个epoch的冒烟测试,确认显存和速度都正常再跑完整训练。这个习惯帮我省下的时间,比任何调参技巧都多。如果你在数据集标注、参数调优或者界面集成上遇到了具体问题,欢迎一起交流,我尽量把自己的实操经验讲明白。