yolov5+PyQt5区域入侵检测:多边形判定与事件去重统计实战
简介基于YOLOv5与PyQt5构建的自定义区域入侵检测事件统计系统是一份完整可运行的Python项目适合用于毕业设计、期末大作业与课程设计也适合刚接触目标检测与桌面应用开发的学习者参考。压缩包共149个文件约17.8MB以41个Python源码文件为核心辅以YAML配置、UI界面文件、PNG图标、使用说明文档及JSON配置并附带运行录屏与Dockerfile方便快速部署与环境复现。目前已有120人下载学习。项目代码含有清晰注释讲解文档与教程notebook配套能帮助使用者理解区域入侵检测的判定逻辑与事件信息统计流程同时系统功能完善、界面美观、操作简单。从模型配置、界面布局到数据导出均有完整覆盖下载后按要求部署即可直接运行既能支撑高分答辩也为进一步二次开发提供坚实基础。1. 区域入侵检测真正的难点其实不在 yolov5很多人拿到这个自定义区域入侵检测项目第一反应是“yolov5 检测目标嘛无非是框出来画个矩形”。但实际拆过代码就会发现最花心思的部分根本不在目标检测而在两件事第一检测框落在你画的不规则多边形区域内怎么判定第二人进区域、离开区域、一直在区域里这三类事件怎么去重统计而不是一帧一个结果把统计表刷爆。这个基于 yolov5PyQt5 的项目就是把这两个问题完整解决掉的毕业设计级源码——它不只是调了个检测模型而是从界面画区域、模型推理、状态跟踪到事件落库都给了可运行的实现还带教程文档和 Dockerfile。适合三类人拿它做毕设/课程设计要交代码和说明书的想学 PyQt5 怎么嵌深度学习推理管线的以及确实需要布一个本地摄像头区域报警原型的。2. yolov5 推理管线与 PyQt5 的线程模型2.1 为什么不能把模型加载直接丢进 UI 线程PyQt5 的 GUI 线程跑的是事件循环而 yolov5 推理一张 640x640 的图在 CPU 上大约需要 200-500msGPU 上 20-50ms。如果直接在按钮的 clicked 信号里同步调 detect界面会直接卡死表现为窗口无响应、画布不刷新。这个项目在架构上做了一个常见的正确处理用QThread跑推理循环用信号把检测结果传回主线程刷新界面。class DetectThread(QThread): result_ready pyqtSignal(dict) def __init__(self, model_path, conf_thres0.25, iou_thres0.45): super().__init__() self.model torch.hub.load(ultralytics/yolov5, custom, pathmodel_path, force_reloadFalse) self.model.conf conf_thres self.model.iou iou_thres self.running True def run(self): while self.running: frame self.get_frame() if frame is None: continue results self.model(frame) boxes results.xyxy[0].cpu().numpy() self.result_ready.emit({frame: frame, boxes: boxes})逻辑说明DetectThread继承QThread在run()里不断从视频源取帧、推理、发信号。result_ready是pyqtSignal携带的 dict 里放了原始帧和检测框数组。主线程的槽函数收到这个 dict 后做绘制和统计不参与推理。参数说明conf_thres0.25是置信度阈值低于 0.25 的检测框会被丢弃毕设演示场景建议调低到 0.2 以增加召回率但误报也会增多。iou_thres0.45是 NMS 的 IoU 阈值两个框重叠超过 0.45 时会合并目标挨得近的场景可调到 0.5。torch.hub.load的customTrue表示加载本地权重不是从官方仓库拉预训练模型。2.2 摄像头与视频文件双输入的处理项目支持两种输入源这在代码里是分开处理的但对外暴露的接口一致——都返回一帧 BGR 图像。摄像头走 OpenCV 的VideoCapture(0)文件走VideoCapture(path)。一个容易被忽略的点是摄像头分辨率默认可能是 640x480而 yolov5 会把输入缩放到 640x640这会导致宽高比变化检测框的坐标需要等比映射回原图。def get_frame(self): ret, frame self.cap.read() if not ret: return None return frame def map_coords(self, x1, y1, x2, y2, orig_w, orig_h): scale 640 / max(orig_w, orig_h) pad_x (640 - orig_w * scale) / 2 pad_y (640 - orig_h * scale) / 2 return (x1 - pad_x) / scale, (y1 - pad_y) / scale, (x2 - pad_x) / scale, (y2 - pad_y) / scale逻辑说明yolov5 的 letterbox 预处理会先等比缩放再填充灰边所以检测框坐标是相对 640x640 的必须逆运算回原图坐标系否则画框位置会偏移。pad_x和pad_y就是填充的灰边尺寸。参数说明orig_w和orig_h是原始帧宽高必须从frame.shape[1]和frame.shape[0]取。如果你换用imgsz320推理把 640 替换为 320 即可。这个映射函数建议做成工具方法因为后续画区域、算入侵都依赖它。3. 自定义多边形的区域判定与事件状态机3.1 射线法判断目标中心点是否在区域内这里的核心算法是射线法Ray Casting也叫奇偶规则从目标点发一条水平射线计算与多边形边的交点数量奇数则在内部偶数则在外部。OpenCV 的pointPolygonTest能直接做这件事但项目里自己实现了好处是不依赖 OpenCV 的数据结构纯 Python 列表就能算方便导出为独立模块。def point_in_polygon(point, polygon): x, y point n len(polygon) inside False j n - 1 for i in range(n): xi, yi polygon[i] xj, yj polygon[j] if (yi y) ! (yj y): x_intersect (xj - xi) * (y - yi) / (yj - yi) xi if x_intersect x: inside not inside j i return inside逻辑说明遍历多边形的每条边(yi y) ! (yj y)判断目标点的 y 坐标是否落在边的两端之间x_intersect是射线与该边的交点 x 坐标如果交点位于目标点右侧则翻转一次 inside 状态。最终 inside 为 True 表示点在多边形内。参数说明polygon是[(x1,y1), (x2,y2), ...]形式的顶点列表顶点顺序必须一致顺时针或逆时针都可以。这个算法处理凹多边形和凸多边形都正确但不处理自交多边形。检测框的“中心点”通常取((x1x2)/2, (y1y2)/2)对行人、车辆这类目标用底部中点比中心点更准——因为一个站着的人脚在区域内但中心点可能在区域外实际语义应该是人进来了。注意默认用中心点判定简单但容易误判我一般建议改成框底部中点(cx, y2)尤其是监控摄像头俯视场景下这个改动对准确率的影响非常大。3.2 去重统计从一帧结果到事件记录每个目标在连续帧里都会被检测到如果每帧都记录一次入侵统计表会爆炸。这个项目的处理方式是维护一个目标状态字典以track_id为键记录目标当前处于什么状态inside在区域内、outside在区域外、pending刚进入边界。只有当状态从不入侵变为入侵时才产生一条新事件。class EventTracker: def __init__(self): self.track_boxes {} self.events [] def update(self, detections, polygon): current_ids set() for det in detections: tid int(det[track_id]) x1, y1, x2, y2 det[box] cx, cy (x1 x2) / 2, y2 status inside if point_in_polygon((cx, cy), polygon) else outside prev self.track_boxes.get(tid, {status: outside}) if prev[status] outside and status inside: self.events.append({ track_id: tid, class: det[class], time: time.time(), type: enter }) self.track_boxes[tid] {status: status} current_ids.add(tid) for tid in list(self.track_boxes.keys()): if tid not in current_ids: del self.track_boxes[tid]逻辑说明update每帧调用一次传入检测结果和当前生效的多边形。对每个检测框取底部中点(cx, y2)做射线法判定状态机只有两个状态outside→inside时记录一次enter事件。目标消失超过一帧就从track_boxes里移除避免 ID 累积。这样统计的就是“有意义的入侵次数”而不是“帧数”。参数说明track_id来自跟踪模块yolov5 原生的detect.py不带跟踪这里用的是 ByteTrack 或 DeepSORT 的输出。class字段是类别 ID比如人0、车2COCO 数据集按类别分组统计时直接用它做 SQL 的 GROUP BY。事件时间用的是time.time()的 Unix 时间戳展示层再格式化。3.3 事件统计维度的设计统计表建议这样设计同类的展示界面直接用表格控件渲染这个结构字段类型说明track_idint目标跟踪 ID去重依据class_namestr目标类别如 person / carevent_typestrenter进入或 leave离开ts_enterfloat进入区域的时间戳duration_msint在区域内停留时长毫秒frame_countint持续在区域内的帧数累计每次记录 enter 事件时把duration_ms置 0后续每帧如果在区域内则累加frame_count离开时补记duration_ms。这个表能做到两个维度的查询按时间段统计入侵次数比如每小时的入侵频次按类别统计占比人比车多还是车比人多。项目里对这些统计结果的展示用的是 QTableWidget 直接填充数据量超过 1000 行时建议换 QTableView Model否则界面会卡。4. PyQt5 界面集成与实时绘制4.1 画布分层原图、多边形、检测框、状态文本界面上要把四层内容叠在同一块控件上底层是视频帧上面是用户画的多边形再上面是实时检测框最后是角落的统计信息文本。直接用QLabel.setPixmap每次全量重绘会闪烁这个项目用的是QWidget.paintEvent里重绘每次拿到新帧后更新成员变量再update()触发重绘。class DetectionCanvas(QWidget): def __init__(self): super().__init__() self.frame None self.polygon [] self.boxes [] def paintEvent(self, event): painter QPainter(self) if self.frame is not None: rgb cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) painter.drawImage(self.rect(), qimg) if self.polygon: painter.setPen(QPen(QColor(255, 0, 0), 2)) painter.setBrush(QColor(255, 0, 0, 50)) points [QPoint(int(x), int(y)) for x, y in self.polygon] painter.drawPolygon(QPolygon(points)) for box in self.boxes: x1, y1, x2, y2 [int(v) for v in box[:4]] conf, cls box[4], int(box[5]) painter.setPen(QPen(QColor(0, 255, 0), 2)) painter.drawRect(x1, y1, x2 - x1, y2 - y1) painter.drawText(x1, y1 - 5, f{self.names[cls]} {conf:.2f}) painter.end()逻辑说明paintEvent里先画视频帧再画半透明的多边形填充区域最后叠加检测框。QImage.Format_RGB888要求数据是 RGB 顺序所以先用cv2.cvtColor把 OpenCV 的 BGR 转成 RGB。半透明填充用的是QColor的第四个参数 alpha50这样能看到被区域盖住的画面细节。参数说明painter.drawPolygon接受QPolygon顶点按顺序连线。检测框置信度显示保留两位小数类别名从self.names取这是在加载模型时model.names字典直接拿到的。这里有个细节paintEvent里的所有计算都不应该做耗时操作否则会阻塞绘制线程。4.2 鼠标交互画多边形与区域管理画区域的操作逻辑是用户鼠标左键单击依次添加顶点右键双击闭合多边形。这块交互代码是整个界面最容易被忽略但是评分老师一定会点的部分——画不出区域后面全是空谈。def mousePressEvent(self, event): if event.button() Qt.LeftButton: self.polygon.append((event.x(), event.y())) self.update() elif event.button() Qt.RightButton: if len(self.polygon) 3: self.polygon_closed.emit(self.polygon) self.drawing False def mouseMoveEvent(self, event): if self.drawing: self.current_pos (event.x(), event.y()) self.update()逻辑说明鼠标左键每次点击添加一个顶点右键闭合多边形并触发polygon_closed信号。mouseMoveEvent里更新鼠标当前位置绘制阶段把“最后一个顶点到鼠标”的线段画出来作为预览方便用户看到多边形将如何封闭。这是交互上很小的一个细节但直接决定这个系统好不好用。参数说明event.x()和event.y()是相对当前控件的坐标。如果你的视频帧按等比缩放显示QLabel里用setScaledContents那鼠标坐标和图像坐标可能不对应需要在resizeEvent里计算缩放比并反向映射坐标。这个项目直接用了控件铺满没有处理缩放映射你在二次开发时要留意这一点。4.3 界面刷新与多线程衔接主线程收到DetectThread发来的信号后更新画布数据和统计表格。这里要避免的是在槽函数里做耗时的统计写入或文件 IO否则即便推理在线程里界面还是会卡。pyqtSlot(dict) def on_result(self, data): frame data[frame] boxes data[boxes] tracked self.tracker.update(boxes, self.current_polygon) self.canvas.frame frame self.canvas.boxes tracked self.canvas.update() self.update_stats_table(self.tracker.events)逻辑说明pyqtSlot(dict)装饰器显式声明槽函数的参数类型可以略微提升信号分发效率。tracker.update完成状态机更新canvas.update()只是把重绘事件加入事件队列实际绘制由 Qt 调度。统计表格更新放在最后如果表格行数超过 500 行建议用setRowCount限制只显示最近 500 条。5. 部署、Docker 化与三个验证技巧5.1 一分钟快速部署检查清单项目带了 Dockerfile 和使用教程文档部署顺序是先装依赖再验证模型。依赖安装的核心是 PyQt5 和 torch 的版本兼容PyQt5 5.15.x 要求 Python 3.8-3.10torch 1.12 以上建议配 CUDA 11.x。Dockerfile 里如果把 PyQt5 装进纯 Python 镜像运行时会报could not load the Qt platform plugin xcb因为缺 X11 库。常见做法是装libgl1-mesa-glx和libxcb-xinerama0并且加上-e DISPLAY$DISPLAY和挂载/tmp/.X11-unix才能把 GUI 显示出来。FROM pytorch/pytorch:1.13.1-cuda11.6-cudnn8-devel RUN apt-get update apt-get install -y libgl1-mesa-glx libxcb-xinerama0 RUN pip install pyqt55.15.7 numpy1.23.5 opencv-python4.8.0.74 COPY . /app WORKDIR /app ENTRYPOINT [python, main.py]参数说明pytorch/pytorch:1.13.1-cuda11.6-cudnn8-devel是带 CUDA 的开发镜像体积较大但是推理必需如果你只用 CPU换成pytorch/pytorch:1.13.1-cpu即可但libgl1-mesa-glx和libxcb-xinerama0这两个包必须保留。PyQt5 5.15.7 与 Python 3.10、torch 1.13 组合是验证过的稳定组合。numpy必须锁定版本OpenCV 4.8 和 numpy 2.x 有二进制不兼容问题会报_ARRAY_API not found。启动容器时用xhost 和docker run -e DISPLAY才看得到界面。5.2 用 bus.jpg 做静态侵入验证项目自带的bus.jpg可以用来做断路验证而不用开摄像头先用model(bus.jpg)跑一次基础检测确认模型能出框再手动构造一个覆盖公交车的多边形调用point_in_polygon验证区域判定逻辑是否正确。常见坑是模型输出的坐标是经过 letterbox 映射的直接画到原图上位置对不上——要验证map_coords函数的逆变换是否写对。5.3 事件去重的三个边界测试事件统计有没有写对用同一段视频跑三遍看结果是否一致就行但要手动验证下面三种情况目标贴着区域边缘反复横跳时是否会重复计次、两个目标在区域内交错时是否会产生串号、目标站在区域内静止不动时事件数是否不增长。这三个问题对应状态机的三个边界边缘抖动需要加一个“连续 M 帧进入才算进入”的缓冲计数串号需要检查跟踪器输出的 ID 稳定性静止目标事件不增长说明状态机只在状态变化时记录事件而不是每帧记录。项目源码里对边缘抖动没有做缓冲这是你二次开发加分的切入点。提示入侵检测的“事件数”和“出现的帧数”是两个完全不同的指标。很多答辩老师会直接问“为什么人在里面站着不动统计次数不变”把这个状态机的逻辑讲清楚比展示检测框的准确度更能说明你真正理解了系统。本文还有配套的精品资源点击获取