
简介面向智能监控开发者与计算机视觉学习者这是一套基于YOLOv5与Qt5的行人范围超界报警系统完整项目。系统以YOLOv5做实时行人检测结合Qt5图形界面完成目标区域划定、越界距离计算与报警触发同时将检测结果和报警信息实时反馈到界面适合学校、商场等公共场所安防场景。压缩包共30个文件大小约13.45MB包含14个Python脚本、11个pyc编译文件、1个YOLOv5s模型权重、1个Qt界面ui文件另有Dockerfile与yaml配置覆盖模型推理、工具函数、界面交互和部署环境。资源内已列出main.py程序入口、models/yolo.py模型定义、utils通用模块、ui.ui界面布局等关键文件各模块职责清晰便于直接运行与二次开发也有助于学习YOLOv5目标检测与Qt5桌面端应用的整合流程。已有2207人学习下载适用于希望快速落地智能越界报警方案的中高级开发者可作为实际项目或课程设计的参考。1. 这个YOLOv5行人超界报警包到底在解决什么第一次从压缩包文件名看以为只是个YOLOv5检测加界面拆开以后才明白它真正要解决的其实是“检测结果怎么变成可用的报警事件”。在监控大屏上画面里同时出现十几个人是常态模型把人都框出来只完成了一半工作另一半是判断“谁越过了我画的那条线、那个区域”。这个包把YOLOv5的检测结果和Qt5的交互画布连了起来你可以在视频上直接圈出目标区域系统实时计算行人与区域边界的距离一旦超过设定的阈值就触发报警。适合两类人一类是刚接触YOLOv5想看看检测怎么和界面工程结合的开发新手另一类是手里有摄像头急着搭一个“区域闯入提醒”原型的验证者。我会从文件结构开始一路拆到报警状态机和Qt的线程坑。2. 拆包看结构QT5界面与YOLOv5推理是两条线拿到压缩包先看文件清单里面同时出现了qt目录、ui.ui、main.py以及models、utils、weights。这个布局已经告诉你项目是两条线合成一条YOLOv5模型推理是一层PyQt5界面是另一层。两者通过信号和数据队列通信而不是搅在同一个函数里。2.1 文件与模块职责对应关系models/yolo.py、models/common.py是YOLOv5的核心模型定义utils/general.py、datasets.py、loss.py、metrics.py、plots.py这些是官方训练和验证工具。要认清这一点作者并没有从零写检测模型而是把YOLOv5官方仓库的代码整体搬进了包里。这是一种非常务实的做法检测这件事直接复用成熟实现自己的精力全放在“超界判断”和“界面交互”上。文件职责使用时机main.py程序入口加载ui并启动检测线程每次运行ui.uiQt Designer界面布局包含视频显示区、控制按钮被uic加载models/yolo.pyYOLOv5检测模型结构推理与训练models/common.pyDetect、Conv、SPPF等组件推理utils/general.py非极大值抑制、常用工具推理后处理utils/datasets.py数据加载与增强训练时utils/loss.py损失函数训练时weights/yolov5s.pt预训练权重推理文件列表里还有google_app_engine、google_utils.py、activations.py这些它们是YOLOv5官方仓库的遗留工具或网络组件报警系统不会直接调用。如果看到__pycache__目录那是Python运行时自动生成的缓存不用管。真正动手时你会经常回到这个表格定位报错训练相关报错去utils/datasets.py找数据增强逻辑推理结果异常先怀疑utils/general.py里的nms参数。我一般会把表格贴在项目笔记里出问题先对号入座。2.2 为什么选YOLOv5s作为默认权重weights目录下只放了yolov5s.pt。s是small的意思参数量约7.2M是YOLOv5系列里最小的模型。选它做报警系统的默认权重是合理的这套系统要跑在普通监控主机的CPU上m和l模型在CPU上的耗时可能是s模型的2到4倍实时性撑不住。s模型的精度对行人这类大目标已经足够尤其是监控画面中行人占比通常比较大不需要高分辨率特征。模型参数量CPU耗时参考适用场景yolov5s7.2M约120ms本项目默认yolov5m21.2M约250ms有GPU或高精度需求yolov5l46.5M约450ms离线分析不适合实时如果你手头有GPU想提高检测质量可以把路径换成yolov5m.pt或yolov5l.pt但要注意几点一是CPU推理会明显变慢二是显存小于4GB的显卡跑l会很吃力三是报警系统的响应延迟一般要求小于500ms模型推理时间不能占用太多。我的习惯是先跑一遍yolov5s.pt统计一帧的推理耗时如果已经占了CPU时间片一半以上就不再上调模型规模。2.3 检测线程必须和UI线程分离很多新手拿到这个包第一反应是直接在main.py里while True读帧检测然后更新界面。这个写法在本地视频上可能只是卡顿换成实时摄像头之后窗口拖动会像幻灯片严重的会直接卡死。原因是YOLOv5推理是CPU计算密集任务跑一帧要一两百毫秒而Qt主线程需要及时处理鼠标事件和重绘两者挤在同一个线程里必然互相拖累。常见做法是开一个QThread子线程做检测检测结果通过信号发回主线程更新界面。代码骨架如下class DetectThread(QThread): result_ready pyqtSignal(dict) def __init__(self): super().__init__() self.model torch.hub.load( ultralytics/yolov5, custom, pathweights/yolov5s.pt ) self.running True def run(self): while self.running: frame self.get_latest_frame() with torch.no_grad(): results self.model(frame) boxes results.xyxy[0].cpu().numpy() self.result_ready.emit({frame: frame, boxes: boxes}) def get_latest_frame(self): if not self.frame_queue.empty(): return self.frame_queue.get() return self.last_frame这里要解释两个关键点。第一QThread重写run()之后run方法里不能直接操作界面控件所有界面刷新必须走pyqtSignal因为跨线程改控件在Qt里是不安全的。第二get_latest_frame是为了防止视频读取线程阻塞在检测上检测处理不过来的帧可以直接丢掉保证显示的永远是屏幕时间最近的画面。这就是所谓的“丢帧保实时”。加上torch.no_grad()后推理模式不再保存梯度显存占用下降速度也能提一点这个细节实战中很值得养成习惯。推理参数也要注意。YOLOv5的model()默认conf_thres是0.25iou_thres是0.45。对行人检测来说0.25置信度阈值会有少量漏检尤其是背光、小尺寸行人。我一般会降到0.15漏检会改善很多但代价是会出现一些误检框需要在后处理里用类别过滤和区域过滤兜底。iou_thres影响的是同一个行人会不会被重复框住如果发现重框就把它升到0.7。imgsz保持640即可这是速度与精度的平衡点改成1280会让检测框精细一些但CPU耗时几乎翻倍报警系统完全没必要。3. 把“超界”变成代码从检测框到距离与报警这一章是包的核心逻辑。YOLOv5输出的检测框是xyxy格式也就是[x1, y1, x2, y2]分别代表左上角和右下角坐标。但“超界”不能只看框本身还要结合你预先画定的区域。系统首先要支持用户在界面上画一个多边形然后实时判断框与多边形之间的位置关系。这里的难点不是几何计算而是怎么定义“超界”以及怎么避免报警乱跳。3.1 区域划定的数据表示用户用鼠标在视频画布上点击得到的点不能直接存成像素坐标。原因有两个一是视频画面在QLabel里会做缩放显示界面坐标和原始帧像素坐标之间有比例差异二是如果换了摄像头分辨率原先的像素区域就全废了。正确做法是把每个点归一化到0到1之间x除以画面宽度y除以画面高度。需要显示时再乘回当前分辨率。假设你画了一个四边形的目标区域从ui传过来的point_list是归一化坐标region_points [ (0.3, 0.2), (0.7, 0.2), (0.8, 0.6), (0.25, 0.65) ]四个点按顺时针方向存储这个顺序影响后续射线法和线段距离计算的正确性。多数情况下这个换算在代码里是同步完成的不需要单独脚本。为了避免每次判断都在坐标系之间来回切换我会在检测线程里维护一份像素坐标的region_list只在界面重绘时用归一化坐标。这样报警判断不依赖界面即使界面卡顿检测逻辑也不会停。3.2 距离计算与阈值判断项目摘要里明确提到“当行人与目标区域的距离超过预设阈值时触发报警”。这里的关键词是“距离”。最简单粗暴的实现是看检测框的中心点是否在多边形内不在就算超界。但这个实现有个问题行人站在区域边界线内侧但身体有一半已经探出区域这种情况中心点还在区域内系统认为正常实际已经构成侵犯。更合理的做法是计算中心点到多边形最近边的距离当这个距离小于阈值就认为处于“临界状态”小于0表示已经出界。几何计算方法如下对多边形的每一条边计算点到线段的距离取最小值。点到线段的距离要考虑点在边上的投影是否落在端点范围内否则直接取到端点距离。点是否在多边形内用射线法判断。def dist_point_to_segment(px, py, x1, y1, x2, y2): dx, dy x2 - x1, y2 - y1 if dx 0 and dy 0: return ((px - x1) ** 2 (py - y1) ** 2) ** 0.5 t ((px - x1) * dx (py - y1) * dy) / (dx * dx dy * dy) t max(0, min(1, t)) cx, cy x1 t * dx, y1 t * dy return ((px - cx) ** 2 (py - cy) ** 2) ** 0.5这段代码是点到线段的距离t表示投影点在线段上的比例被限制在0到1之间避免计算到延长线上的距离。这个函数会被每个检测框循环调用几十次Python处理几千次计算在一帧时间内可以忽略。如果画面里同时有多个行人先按置信度排序取置信度最高的框作为该人的代表框同一行人出现多个重叠框时选面积最大的避免中心点来回跳。超界判定参数有两处要重点说。第一是报警距离阈值通常用归一化单位表示我习惯设0.05也就是画面宽高的5%。在1080p下大约50像素对应一个中等行人的身位既能提前预警又不会因为行人正常走动频繁误报。第二是持续帧数连续3帧都满足超界条件才报警这是最便宜的防抖方案。3.3 报警状态机避免报警刷屏如果每帧都判断并弹窗报警画面里一旦有人长时间站在边界外报警窗口会被打开几十次用户体验是灾难级的。必须引入状态机只在状态发生变化时触发一次事件。状态分两种NORMAL和TRIGGERED。从NORMAL跳到TRIGGERED是“进入报警”从TRIGGERED回到NORMAL是“恢复”中间持续超界不重复提醒。class AlarmState: def __init__(self): self.state NORMAL self.trigger_count 0 def check(self, is_outside): if is_outside: self.trigger_count 1 if self.state NORMAL and self.trigger_count 3: self.state TRIGGERED return enter else: self.trigger_count 0 if self.state TRIGGERED: self.state NORMAL return leave return Nonecheck方法返回None表示不报警进入报警返回enter恢复正常返回leave。这里用trigger_count做了3帧确认而不是is_outside一为True就报警。状态机还有一个隐藏好处报警事件可以带着当前帧的截图一起发出去便于值班员确认。要注意的是这个状态机是给单个目标用的。画面里同时有多个行人时需要对每个检测框都维护一个AlarmState实例。YOLOv5默认没有目标跟踪框ID会逐帧变化如果直接拿坐标当key不稳定。常见做法是先不做跟踪对每一个行人独立判断只要有任意一个框触发报警就让主界面亮红灯要精确定位是哪个行人就得再集成ByteTrack之类的跟踪模块成本会高不少。4. 让界面动起来Qt5里挂视频流和信号槽检测逻辑再完整最终也要投到Qt5界面上。这个环节最大的坑不是控件不会用而是视频帧的数据格式转换和线程安全问题。我把拆包时踩过的三个关键点拆开讲。4.1 把OpenCV的BGR帧转成QImage显示YOLOv5模型训练时用的图像是RGB顺序而OpenCV默认读入的是BGR。转到Qt界面显示时如果直接把BGR数据塞进QImage画面里红蓝会颠倒。转一次颜色顺序是必须的。def cv_to_qimage(frame): rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape bytes_per_line ch * w return QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888).copy()注意最后加了.copy()。因为QImage只是保存了指针没有接管内存。如果rgb是函数里的临时变量函数返回后内存被释放QImage就成了悬空指针界面会出现花屏甚至崩溃。加copy()等于把图像数据复制一份一劳永逸。这条规则同样适用于QPixmap.fromImage的转换尽量用副本。4.2 信号槽更新界面的写法在2.3节已经定义了DetectThread它通过result_ready信号发数据。主线程要做的就是在初始化时连接信号然后在槽函数里把检测结果画到界面上。thread DetectThread() thread.result_ready.connect(on_result) thread.start() def on_result(data): image cv_to_qimage(data[frame]) pixmap QPixmap.fromImage(image) video_label.setPixmap(pixmap) draw_boxes(pixmap, data[boxes])一个容易犯的低级错误是connect写在thread.start()后面。如果start()先执行线程可能在connect建立之前就发出了第一个信号结果那一帧就永远丢了。虽然丢一帧不影响大局但排查起来很玄学代码里还是按“先connect再start”的固定顺序写。另外不要在on_result里面调用QMessageBox弹窗因为on_result是主线程事件循环里的槽弹窗会阻塞事件循环导致界面卡死。4.3 鼠标画区域事件覆盖与坐标换算用户要能在界面上画出目标区域这需要在显示视频的控件上捕获鼠标事件。直接扩展QLabel并不合适常见做法是新建一个QWidget子类重写mousePressEvent、mouseMoveEvent和paintEvent。class VideoCanvas(QWidget): left_clicked pyqtSignal(float, float) def __init__(self): super().__init__() self.points [] def mousePressEvent(self, event): if event.button() Qt.LeftButton: pos event.pos() norm_x pos.x() / self.width() norm_y pos.y() / self.height() self.points.append((norm_x, norm_y)) self.update() elif event.button() Qt.RightButton: self.points.clear() self.update()坐标换算的关键就在这两个除法上。如果用QLabel且开启了setScaledContents(True)图像会被拉伸显示鼠标点击位置是控件坐标而绘制检测框用的是原始图像像素坐标两者之间必须换算。界面上显示的是缩放后的pixmap画出的区域点也要先转换成控件坐标再画不然线会画错位置。右键双击清空所有点重新圈定这个交互可以让调试效率高不少。5. 踩坑排查报警乱跳、卡顿、模型不加载的五类问题这一章写的是拆包复现过程中必然会遇到的几个坑。每条都按现象、原因、解决的顺序整理可以直接照做。5.1 报警频繁误触发现象行人站在画面角落没怎么动报警却一两秒触发一次然后自动恢复再触发循环反复。原因有两层一是检测框本身有抖动YOLOv5在连续帧里对同一个行人输出的坐标会有几个像素的波动当行人正好站在区域边线附近时中心点在多边形内外反复横跳二是报警状态机没确认机制只要is_outside为True下一帧就直接发出进入报警。解决先给检测框坐标加一阶低通滤波x_new alpha * x (1 - alpha) * x_oldalpha取0.4左右再把状态机的确认帧数从默认值提高到3到5帧。我实际测试过对1080p画面、行人以正常步速走动时5帧确认能滤掉绝大多数抖动同时不会漏掉快速跑过的行人。另外置信度阈值低于0.25的检测框建议直接丢弃这类框往往是背景误检位置随机跳动最容易干扰状态机。5.2 视频帧卡顿界面拖拽不跟手现象视频画面跳动拖拽窗口时就像在用十几年前的远程桌面CPU占用率100%。原因读帧、检测、显示全在同一个线程里跑。检测模型推理耗时600毫秒主线程就被卡死600毫秒期间任何鼠标事件都无响应。甚至有些写法的同步锁还包住模型推理导致读帧也被阻塞。解决彻底按照第2章和第4章的线程模型来改视频读取线程只负责把最新帧放进队列检测线程从队列拿帧并跑模型发送信号给主线程显示。队列使用queue.Queue设置maxsize2放不下就直接丢弃旧帧保证取到的永远是新鲜帧。改完之后即便推理耗时还在600毫秒左右界面刷新也不会低于10帧拖拽恢复跟手。5.3 模型文件加载失败torch.hub.load一直报错现象代码写的是torch.hub.load(ultralytics/yolov5, custom, pathweights/yolov5s.pt)weights路径没问题但一直报错说找不到模型。原因torch.hub.load的第一个参数是仓库名默认会去GitHub下载ultralytics/yolov5的源码到缓存目录。如果运行环境无法访问外网或者缓存目录里已经有损坏的代码加载就会失败。很多人以为问题出在pt文件实际出在仓库下载环节。解决把YOLOv5官方仓库代码整个放到本地比如解压到项目里的yolov5_src目录然后改成torch.hub.load(yolov5_src, custom, pathweights/yolov5s.pt, sourcelocal)。source指定为local后不再触发网络下载。另一个做法是不走torch.hub直接在本地导入models.experimental的attempt_load函数加载权重代码量只多几行但完全不依赖网络部署环境一致性更好。5.4 画好的区域和实际报警区域错位现象在界面上画的区域看起来把门框完全包住了但实际只有右侧半边报警左侧有人走过不报警。原因QLabel开启了setScaledContents图像被拉伸到控件大小而鼠标坐标和原始图像坐标没有换算。画的时候把控件坐标直接当成了图像坐标报警判断却用原始分辨率两边对不上。如果窗口比例和视频比例不一致这种错位会非常明显。解决在存储区域点时统一保存归一化坐标。屏幕上的点存储为(norm_x, norm_y)报警逻辑把归一化坐标乘以当前帧宽度和高度。绘制时反过来把归一化坐标乘以控件宽度和高度。这套换算在界面缩放、摄像头换分辨率后依然健壮。我自己的项目里所有和区域相关的坐标全走归一化只有显示的那一层做换算。5.5 报警事件在界面没有任何显示现象后台控制台打印triggered但是界面没弹窗、没闪烁、没反应。原因报警信号是在检测线程里直接调QMessageBox或者直接改控件属性忽略了Qt线程安全限制。子线程改控件轻则不生效重则进程崩溃。另外控制台打印能看到不等于UI收到了信号可能connect写在了start之后信号已经发出。解决把报警信号改成pyqtSignal统一emit到主线程。在主线程槽函数里再做UI操作弹窗、闪烁某个指示灯控件、写入日志。connect务必在start之前两者顺序固定。还有一个工程细节报警弹窗不要用模态QMessageBox否则弹窗期间视频画面冻结。建议改用非模态QMessageBox或者只在界面上亮红灯加蜂鸣声。6. 把检测权重换掉训练自己的行人模型再验证如果你只是做功能验证weights/yolov5s.pt已经能检测行人直接跑。如果你想让系统在你的真实场景里更准就需要用自己的数据集训练一个模型再把best.pt换进来。这一章讲清楚替换路径和验证方法。6.1 用YOLOv5训练自己的行人数据集训练用的代码在YOLOv5官方仓库的train.py数据集按YOLO格式准备。图片放images目录标注txt放labels目录每一行是class_id x_center y_center width height数值都归一化到0到1。data.yaml里写训练集和验证集路径类别可以是单个person也可以增加你要检测的类别。train: datasets/mydata/images/train val: datasets/mydata/images/val nc: 1 names: [person]训练完之后把权重文件改名成best.pt放到weights目录然后修改main.py里DetectThread中path的指向。这一步几乎是零门槛但要小心类别索引变化带来的误检。如果只训练person一个类names只有person就可以在代码里过滤掉非person类别的检测结果如果保留了多个类别就要在xyxy结果里检查第5列类别ID别把汽车当成行人触发报警。6.2 离线回放验证报警事件验证方面我养成的一个习惯是把检测框、状态机事件、报警截图全部记录到CSV再用离线视频回放。这样能分清到底是模型漏检还是报警逻辑误判。具体做法是用一段十分钟的监控视频跑完以后统计两件事一是行人出现的帧数里模型有没有框住二是报警事件和实际超界行为的时间差。如果报警领先超界太多说明阈值太敏感如果延后太多说明确认帧数设得过于保守需要逐一调整。这个包最值得借鉴的其实是那个报警状态机它让检测结果变成了可以被人信任的事件流。我从复现里学到的教训是模型精度只能决定检测质量报警可靠性完全靠后处理和状态管理。从那以后我每次跑这类项目都强制先走一遍“记录CSV、离线回放报警事件”的验证流程能定位大多数玄学问题。希望这次拆包笔记也能帮到你。本文还有配套的精品资源点击获取