
最开始让我下决心折腾YOLOv12的是在一个商场客流统计项目里被旧模型按在地上摩擦的经历。镜头从二楼俯拍下去扶梯口密密麻麻全是人检测框一个叠一个漏检率直接飙到我不想看统计报表。后来把整体方案换成“YOLOv12 密集行人数据集 PyQt5界面 登录注册”的完整系统效果才真正稳定下来。这篇就把整个《基于深度学习YOLOv12的密集行人识别检测系统》从数据准备、模型训练、界面联调到现场排错的过程完整翻出来项目源码、模型、UI、登录注册这套东西怎么拼到一起以及我踩过的坑一次讲清楚。不管你是做安防监控、客流统计还是正在写毕设/课设只要离不开“密集人群检测”这几个字这篇都适用。1. 密集人群检测这个任务到底在考验什么1.1 为什么普通模型在密集人群里“翻车”很多人觉得行人检测就是“把行人框出来”听起来比车辆检测还简单——大家都是直立行走的特征稳定又没有轮子。但真实项目里密集人群检测恰恰是目标检测里最难受的场景之一。先说遮挡。商场扶梯口、景区检票处、演唱会散场通道那真是人贴人。目标被挡住40%到60%是常态头部可能还在躯干已经被前面的人盖得严严实实。普通模型学的是“完整行人”的外观特征一旦外观残缺置信度就哗哗往下掉。这时候不是模型笨是训练时压根没见过这么多“半个人”。再说尺度。同样的一个人在1080p画面里距离镜头5米和15米像素尺寸能差三四倍。密集人群里必然混着大量小目标而小目标在特征图下层池化几次以后可能只剩下几个像素的信息很容易被模型当成背景抹掉。第三是“群体上下文”的干扰。人一多背景就不干净了到处都是胳膊、腿、背包、行李箱这些局部外观和行人本身的局部很相似。模型如果把注意力放在这些重复纹理上就容易输出一堆低质量框。所以密集行人检测真正要解决的问题不是“能不能检出人”而是“在互相遮挡、尺度变化大、背景杂乱的情况下能不能稳定地把每个人框出来”。1.2 YOLOv12带来的关键变化YOLOv12是2025年推出的新版本核心变化是把注意力机制比较系统地放进了YOLO架构里。传统YOLO系列尤其是大家熟悉的v5、v8骨干网络基本是CNN堆叠靠卷积感受野一点一点往上扩。CNN的优点是快、稳、部署方便缺点是对“全局关系”的建模偏弱。一个人被挡住半边CNN看到的还是局部纹理拼凑而带注意力机制的模型会在特征层面去建模“这个区域是不是一个完整目标的局部”相当于把散落的拼图块关联起来。YOLOv12并不是简单粗暴地整个Transformer搬进来而是做了工程化折中在不太牺牲推理速度的前提下把注意力模块嵌在骨干和特征融合的关键位置。实际跑下来的感觉是普通街景场景下它跟v8、v11的差距没有想象中大但一旦进入拥挤场景漏检率和误检率确实能拉开一个身位。对大目标、整体轮廓被破坏的目标注意力带来的收益尤其明显。另外YOLOv12训练时可以原生使用Ultralytics系列的训练管线数据标注格式、评估指标、命令行入口基本可以无缝切换。我这次没有用魔改分支直接基于官方代码仓库来跑数据格式还是熟悉的YOLO txt格式省去了很多适配成本。1.3 这套系统围绕什么场景设计这套系统从一开始就不是给人脸检测或者自动驾驶做的它的核心目标场景有三个第一是出入口客流统计比如商场出入口、地铁闸机、展览馆门禁需要在多人同时通过的瞬间快速计数并且保留每个人的轨迹特征。第二是人群聚集预警比如景区广场、车站候车厅当某个区域瞬时人数超过阈值时系统要给出提示这时候除了检测框还需要一种简单的区域人数统计算法。第三是安防检索辅助比如事后翻查监控录像想快速找到“某个时间段某个人是否出现在画面里”需要把检测结果连同时间戳、画面帧一起落盘。这三个场景决定了系统不能只是“跑个模型出个框”它必须有视频源接入能力、实时画面展示、检测结果统计和存储还要有一个让人操作的门面。这就是为什么这套系统除了模型还会配套UI界面和登录注册。后面我会把每一块都拆开讲。2. 系统拆解技术栈选型与模块边界2.1 为什么用Python PyTorch OpenCV而不是纯C部署很多做嵌入式或者C部署的朋友会问识别系统不是应该用TensorRT C吗精度高、延迟低、好落地。但你要看项目阶段的诉求。这套系统要用于算法验证、效果演示、现场参数快速调整迭代速度远比极致性能重要。Python生态的优点在算法验证阶段太明显了Ultralytics的检测接口可以直接调用OpenCV负责摄像头读取和画框PyQt5写界面SQLite存账号数据整个链路不需要编译改一个阈值参数马上能看到效果。等到算法彻底定型再考虑用ONNX或TensorRT导出到C部署也不迟。我在这个系统里保留了ONNX导出端口后面实测部分会细说。PyTorch的选择没有悬念。模型训练、推理、迁移学习都靠它特别是torch的DataLoader和amp混合精度在训练阶段能省下大量时间。OpenCV承担的任务比较杂读取本地视频、连接RTSP摄像头、BGR与RGB转换、图像缩放、在画面上叠加检测框和文字。它虽然不是最高效的但兼容性和社区资料是最稳的。2.2 UI层选PyQt5还是WebUI有两条路桌面端用PyQt5/PySide6Web端用Flask/FastAPI 前端框架。我这次选择的是PyQt5。原因并不复杂。当下的需求是单机运行、操作直观、低延迟显示视频流。PyQt5的QLabel可以直接刷OpenCV的帧实时性好鼠标拖动缩放、窗口布局都很好控制不依赖浏览器和网络服务。现场项目里给安保人员一台装了界面的电脑打开就能用比开浏览器输地址直观得多。如果是做远程访问或者多终端同时看那Web方案更合理但系统复杂度会上一个层级要处理并发问题。这套系统的定位是“本地一体机”PyQt5够用了。如果你打算改成Web检测引擎部分可以完全复用只把UI替换成Flask WebSocket流推送。2.3 一个干净的目录结构怎么设计工程最怕写到后面文件乱飞。这个系统我用的目录结构是这样的pedestrian_detection_system/ ├── app.py # 程序入口 ├── config.yaml # 全局配置 ├── requirements.txt # 依赖包清单 ├── models/ # 训练好的模型权重 ├── dataset/ # 原始数据集与标签 ├── ui/ │ ├── login_window.py # 登录注册界面 │ ├── main_window.py # 主界面 │ └── resources/ # 图标、样式表 ├── core/ │ ├── detector.py # YOLOv12推理封装 │ ├── video_source.py # 本地/RTSP视频源管理 │ ├── stats.py # 检测统计 │ └── database.py # SQLite用户与日志 └── utils/ ├── draw.py # 画框、画统计信息 └── logger.py # 日志记录这样拆的好处是检测逻辑和界面完全解耦。你想换成别的数据集重训一个模型只需要替换models目录下的权重和config里的模型配置你想换个UI框架core目录完全不需要动。3. 数据准备从开源数据集到YOLO格式的真实细节3.1 哪些公开数据集值得用模型效果的上限一半是由数据决定的。我这次训练数据主要来自三块第一块是COCO数据集的person类。COCO覆盖面广场景杂但很多图是日常街景密集程度不算高。它适合做底料让模型学到一个比较通用的“行人”概念。第二块是VisDrone的部分行人类别。VisDrone是无人机视角人群密度高目标小遮挡严重正好补COCO的不足。它的标注是MS COCO格式需要转一次。刚开始模型在密集场景下无力就是缺少这类“高压数据”的毒打。第三块是从MOT Challenge等行人跟踪数据集中筛出来的帧。跟踪数据集天然包含连续帧人挨着人的画面特别多拿来补充遮挡样本很合适。我自己的经验是不要只拖一个数据集用到底。把上面三个按比例混合效果最好我这里大概是COCO 6成、VisDrone 3成、MOT帧1成。混合之前先统一类别全部只保留person这一类。3.2 YOLO标注格式和转换脚本的坑YOLO格式的标注是每张图一个同名txt文件每行代表一个目标格式为“类别id x_center y_center width height”全部坐标都是相对图片宽高的归一化数值。这个格式看起来简单转换时却有三个容易出错的点。第一个是类别id要对齐。COCO的person类是0但有些数据集person是别的id。转换脚本里最容易翻车的就是这里训练时发现模型什么都不检先看是不是标签标签里混着499和500这种非法id我踩过。第二个是坐标归一化。有些开源数据给的是左上角坐标和宽高要自己算成中心点坐标。四舍五入保留几位也影响不大但别用int取整会损失小目标的框精度。第三个是无效标签。有些json标注里会出现空标签文件也就是一张图没有任何目标。训练时如果用了多尺度、Mosaic增强空标签会报错。转换脚本里统一过滤掉这种图片就好。下面是我当年写的数据转换核心片段注释标出了最容易错的地方import json import os from PIL import Image def coco_to_yolo(coco_json_path, image_dir, output_dir, valid_category_ids(0,)): with open(coco_json_path, r, encodingutf-8) as f: coco_data json.load(f) image_map {img[id]: img for img in coco_data[images]} category_map {cat[id]: cat[name] for cat in coco_data[categories]} annotations_by_image {} for ann in coco_data[annotations]: if ann[category_id] not in valid_category_ids: continue annotations_by_image.setdefault(ann[image_id], []).append(ann) for image_id, ann_list in annotations_by_image.items(): img_info image_map[image_id] img_path os.path.join(image_dir, img_info[file_name]) # 注意读取宽高时优先从图片本身获取不要完全依赖json里的宽高字段 with Image.open(img_path) as img: img_w, img_h img.size txt_path os.path.join(output_dir, os.path.splitext(img_info[file_name])[0] .txt) with open(txt_path, w, encodingutf-8) as f: for ann in ann_list: x, y, w, h ann[bbox] # coco bbox给的是左上角坐标和宽高 center_x x w / 2.0 center_y y h / 2.0 # 关键所有坐标除以图片宽高得到0~1之间的归一化值 center_x_norm center_x / img_w center_y_norm center_y / img_h w_norm w / img_w h_norm h / img_h # category_id这里要映射成你自己的类别id不能直接抄写原数据集的id f.write(f0 {center_x_norm:.6f} {center_y_norm:.6f} {w_norm:.6f} {h_norm:.6f}\n)转换完以后我会随机抽20张图把标注框画回去人工检查一遍。这一步不能省因为脚本写错时数据比例往往看着正常一眼根本看不出来。3.3 针对密集场景的数据增强策略YOLO系列自带Mosaic增强也就是把四张图拼成一张训练图。这在常规场景里很好用但密集人群场景下我还额外加了两个操作随机贴片模拟遮挡随机从别的图里扣一块行人区域贴到当前图上盖住部分目标。这能强迫模型学着用可见局部识别目标。随机裁剪缩放对高密度区域按0.5到0.8的随机比例裁剪后再缩回训练尺寸。这能让模型适应不同尺度的拥挤画面也相当于做了尺度增强。增强不是越多越好。训练后期你会发现增强太重会让模型很难收敛loss降不下去。我的习惯是前一半epoch用完整增强后一半逐步降低增强强度让模型在干净数据上“收一收”。Ultralytics里可以用超参配置调控例如mosaic: 0.8 mixup: 0.2 fliplr: 0.5 scale: 0.5这个配置是根据我的场景定的你不用照抄但思路可以借用密集场景优先保mosaic和scaleflip保持0.5mixup不要调太高否则动态模糊反而让目标边界更乱。3.4 数据划分按场景切而不是随机切很多新手训练模型时会随机把图片分成train和val。这样做出来的指标好看但实际部署就露馅。原因很简单随机划分会让同一视频里的连续帧同时出现在训练集和验证集模型等于变相“看过答案”。我这套系统的做法是按采集场景划分。同一个摄像头、同一个地点、同一段时间的素材全部归到同一个集合里绝不让它们跨集。比如A商场的素材全部进训练B商场的素材全部进验证C项目现场素材最后拿来压测。这样验证结果才能代表真实场景。另外密集人群验证集的比例我故意调高到30%。这类任务如果验证集里没有大量拥挤画面mAP再高都说明不了问题。4. 模型训练参数、监控与评估4.1 训练环境配置训练时我用的是单卡环境显存24G跑YOLOv12的中型模型比较宽裕。如果你是学生或者在普通机器上跑建议直接选nano或small版本效果差不了太多训练时间能省一半以上。PyTorch版本建议2.xCUDA对应装好就行Ultralytics对版本兼容性做得比较好一般不会卡在环境上。依赖清单长这样python3.10 torch2.1.0 torchvision0.16.0 ultralytics8.3.x opencv-python4.8.* PyQt55.15.* pyyaml6.*这里有个细节不要贪最新版本。Ultralytics家更新很快有时候新版会改默认行为你照着教程跑结果不一样不一定是代码错可能是框架行为变了。我在项目里会把requirements.txt锁死版本方便以后重建环境。4.2 训练参数怎么定训练命令直接用Ultralytics的CLI就行yolo detect train \ --model yolo12s.yaml \ --data dataset.yaml \ --epochs 200 \ --batch 16 \ --imgsz 1280 \ --device 0 \ --optimizer AdamW \ --lr0 0.001 \ --weight_decay 0.0005 \ --amp几个参数我说下选择的理由。imgsz对密集小目标影响最大。1080p画面里的人群目标普遍偏小如果缩到640再送进网络很多目标只有十几个像素神仙模型也难救。我把训练尺寸调到1280之后小目标的召回率提升非常明显。代价是训练时间翻倍、显存压力变大。如果机器跑不动先640跑通流程再用1280精调。batch受显存约束不是越大越好。batch太大会导致每个epoch的迭代次数少模型反而欠拟合。16在24G显存下配合1280输入刚刚好。epochs我设了200但实际在140左右就用早停收工了。密集人群训练后期很容易过拟合表现为训练loss继续降、验证mAP不再涨甚至掉。建议开着早停patience设30。4.3 训练过程的观察要点训练时不要只盯屏幕等进度条一定要看训练曲线的三个信号第一是box_loss有没有相对平滑地下降。如果前10个epoch loss纹丝不动先查学习率、查标签有没有问题不要头铁继续跑。第二是验证集mAP是否稳步上升。如果训练集已经涨得很高、验证集迟迟不涨基本就是过拟合该减epoch或者加数据增强。第三是看特定类别的AP这里只有person类所以就是整体AP。如果mAP50能到85%以上但mAP50-95偏低说明模型框的位置还不够准可以朝调高输入尺寸、优化锚框/标签分配方向想。密集行人场景我个人的手感是mAP50能到88%以上密集画面基本可用mAP50-95到60%以上就是很好的水平了不必死磕。4.4 密集人群下的评估指标别只用mAPmAP是一个综合指标但密集人群场景里它不够直观。我评估模型好不好用还会加两个指标漏检率也就是标注里“人”被完全漏掉的比例。统计一下画面里有100个真目标模型只给出80个框漏检率就是20%。这个数直接影响客流统计的准确性。误检率也就是模型把拉杆箱、垃圾桶、广告牌上的假人认成人的比例。这个数直接影响预警系统会不会没事乱响。某个模型mAP看起来很高但漏检和误检可能同时很高——它可能输出一堆重复框、覆盖率高但位置漂移的框mAP计算时重复框会被NMS处理掉所以看不出来。我之前就吃过这个亏模型在COCO上mAP50有90%拿到密集现场一看一个人可能被框了三遍客流数直接爆表。所以代码里除了调用evaluate得到mAP我还单独写了一个脚本统计漏检率和误检率专门用来判断“这个模型能不能上线”。5. 登录注册与UI一个“能用”的产品该有的样子5.1 登录注册模块要不要做有人会问一个本机的检测工具搞登录注册是不是多此一举我的答案是看项目交期和使用对象。如果只是自己跑着玩登录确实可以砍掉。但如果有多个操作人员交替使用或者要把这个系统交给甲方验收登录注册就是必要功能。它至少解决了三件事账号权限隔离不是每个人都能改模型参数和检测阈值。操作留痕谁在几点登录、运行了哪些检测都有记录。给人“这是一个完整系统”的观感甲方看到登录界面对项目的完成度评价会高一个档次。5.2 本地账号体系SQLite就够了这个系统无需联网账号体系本地化最合适。我用SQLite建了一张用户表密码不存明文而是用哈希加盐方式存储。下面这段示例展示了哈希校验的核心思路import hashlib import sqlite3 import os def hash_password(password: str, salt: str None) - str: if salt is None: salt os.urandom(16).hex() digest hashlib.sha256((salt password).encode(utf-8)).hexdigest() return f{salt}${digest} def verify_password(stored: str, password: str) - bool: salt, digest stored.split($, 1) return hash_password(password, salt) stored这里没有使用bcrypt这类重量级库是因为本机系统被攻破的风险面本来很小用SHA256加盐已经足够防止“看一眼数据库就知道密码”的情况。系统越简单越容易维护。同时我用数据库记录登录日志登录时间、登录结果、检测时长。运行几次以后这套日志对排查问题很有帮助。5.3 主界面该放什么控件主界面我用PyQt5实现布局分成四块区域左侧视频显示区用来展示原始画面和检测结果画框。视频源可以切换本地文件、USB摄像头、RTSP协议流地址。右侧实时统计区显示当前帧检测人数、峰值人数、每秒帧数以及总检测数。还可以用一个折线图展示最近N秒人数变化趋势。底部控制区放开始/停止按钮、置信度阈值滑块、IOU阈值滑块、截图按钮、导出报表按钮。菜单栏放模型选择、日志查看、用户管理入口。置信度阈值和IOU阈值这两个滑块一定要放在界面上不要写死在代码里。原因很简单不同摄像头角度、不同密度的画面最优阈值差别很大。比如俯拍场景置信度阈值0.3就够平视场景可能要调到0.55才能压住误检。现场人员可以在不重启程序的情况下实时调整这才是“能用”的产品。5.4 检测引擎和UI解耦很多人写PyQt界面时习惯把检测循环直接塞进界面的槽函数里结果界面一卡一卡的拖一下窗口画面就卡死。原因很直接检测是耗时操作放在UI主线程里界面必然等它。正确做法是单独开一个工作线程跑检测通过信号把处理好的帧传回界面。PyQt5里可以用QThread或者更轻量的QThreadPool。我把检测循环封装在了core/detector.py里它只依赖opencv和模型自己不知道UI的存在。UI线程只负责接收检测结果并刷新画面。class DetectionThread(QThread): frame_ready pyqtSignal(object, object) def __init__(self, video_source, detector, conf_thresh, iou_thresh): super().__init__() self.video_source video_source self.detector detector self.conf_thresh conf_thresh self.iou_thresh iou_thresh self.running True def run(self): for frame in self.video_source.read_frames(): if not self.running: break results self.detector.predict(frame, self.conf_thresh, self.iou_thresh) annotated self.detector.draw_boxes(frame, results) self.frame_ready.emit(annotated, results)这个线程把“读一帧、推理、画框”整套流程局限在工作线程里界面主线程收到frame_ready信号后只做一次label图片更新流畅度和稳定性都有保证。6. 实测与排错记录6.1 漏检严重时先别急着换模型刚把YOLOv12配置跑通时我在一个商场扶梯的测试视频上发现问题上半段人群稀疏检测很好但扶梯出口处的密集区域漏检一片。当时第一反应是“模型不行”后来一步步排查才发现问题出在输入尺寸上。测试代码里我沿用了自己习惯的imgsz640这个尺寸对普通场景够用但对密集场景就是致命伤。扶梯出口处一个人可能只有20像素高640输入下特征图上的响应极弱。把推理尺寸改成1280后漏检率肉眼可见地降下来。所以如果你也遇到密集场景漏检先检查输入尺寸再检查置信度阈值最后才轮到模型架构。6.2 帮你少走弯路的一些调参笔记置信度阈值密集人群场景建议从0.25开始调如果误检多就往上加到0.45左右如果漏检多就往下减。IOU阈值建议保持在0.45到0.5之间过高比如0.7会导致同一个人的多个重叠框都保留计数虚高过低又会让两个人的框被合并成一个计数漏掉。推理尺寸训练和推理尺寸要尽量一致不要训练1280、推理640效果损失非常大。数据增强如果场景光照变化大可以在优化阶段把光照扰动、模糊扰动加上这些对行人检测的鲁棒性很有帮助。密度极高场景如果人群密集到只露出一片人头可以考虑单独训练一个detection head或使用人头检测模型做二次过滤整体效果会明显好过单个全身模型。6.3 GPU推理速度与界面流畅度系统在GTX级别显卡上推理1280分辨率单帧大约20到40毫秒。但界面刷新帧率不高原因在画框和图像格式转换。OpenCV默认是BGRPyQt显示需要转成RGB再转QImage这个开销比想象中大。优化方式是只在结果返回时才做那个帧的格式转换不要把视频流每一帧都转减少无谓开销。另外如果推理帧率和显示帧率不一致不要强行同步。检测线程按自己的节奏跑界面按固定的刷新间隔显示最新帧就能让画面看起来流畅。没有必要让UI去等待检测速度。6.4 视频源兼容问题本地视频文件基本没问题USB摄像头要看驱动型号Windows下容易遇到权限或占用问题。RTSP流是最容易踩坑的不同厂家的摄像头对OpenCV的兼容性不一样有些流用H.265编码OpenCV默认支持不好画面根本打不开。解决办法是先用VLC确认这个流能正常播放再在OpenCV里尝试拉流。如果实在不行用FFmpeg转封装再读也是个不错的备选。7. 一点后续可以顺手做的小扩展系统主体跑通以后我强烈建议你加一个“检测结果落盘”的功能每一帧的检测框、置信度、人数统计、时间戳都写进CSV同时对超过人数阈值的关键帧自动截图。这个扩展能极大提升系统的实用性用来做事后统计、轨迹分析都是现成的数据基础。落盘代码用一个简单的格式就够with open(detect_log.csv, a, newline, encodingutf-8) as f: writer csv.writer(f) for box in results.boxes: writer.writerow([ frame_id, datetime.now().isoformat(), box.cls.item(), box.conf.item(), box.xyxy[0, 0].item(), box.xyxy[0, 1].item(), box.xyxy[0, 2].item(), box.xyxy[0, 3].item() ])这条日志会帮你省很多事。现场出了问题回看日志基本能判断是模型漏检、阈值设置不合适还是视频源本身的问题。8. 项目实战中最重要的几点总结这套系统做下来最值得记住的经验有三条。第一密集人群检测的数据比模型更重要把时间花在数据筛选、清洗和场景划分上比反复换模型变体划算得多。第二推理尺寸、置信度阈值、IOU阈值这些参数要根据真实场景反复试不要期望一组参数通吃所有摄像头。第三界面和检测逻辑一定要分层解耦用线程和信号组织数据流否则系统永远卡顿也就谈不上实用。如果你正在复现这套系统我建议按这个顺序推进先把YOLOv12模型在公开数据上跑通推理再把数据转换和训练流程搭好模型效果稳定之后再去做UI和登录注册。千万不要一上来就铺开做界面后面发现模型效果不行所有的界面代码都要返工。先把“模型能不能在密集场景里站稳”这个问题解决掉其他都是水到渠成的事。