简介:基于yolov5s与PyQt5搭建的矿用智能视频监控系统完整源码,面向矿山安全管理人员、计算机视觉开发者及工业现场项目爱好者,重点解决井下人员越位报警、车辆停放识别、视频清晰度与稳定性检测等实际问题,可直接作为相关课题或工程项目的参考基线。压缩包共30个文件,涵盖16个Python源码文件、8张测试图片、2个模型权重文件(含yolov5s.pt)、1个UI设计文件以及依赖列表和说明文档,整体约25.9MB,代码按检测、界面、控制等模块拆分,便于快速定位与修改。目前已有291人学习下载。资源提供了目标检测、PyQt5交互界面、PLC联动控制、异常行为分析等完整实现,视频智能分析与越位报警功能均由Python代码驱动,可从模型加载、图像处理到报警输出全流程跑通。对于希望快速掌握YOLOv5工程化封装、PyQt5上位机开发或工业安全监控方案的读者,这份源码能有效缩短开发周期,也可作为算法研究与功能扩展的起点。
1. 矿用视频监控为什么把 yolov5s 和 pyqt5 组合在一起
井下变电所、皮带巷、卸载站这些区域的摄像头,绝大多数时间都只是把画面送到调度室,既没人盯着看,也没人手动翻录像。真要出问题——比如有人在皮带运行时穿越警戒线,或者矿用卡车停在消防通道上——等值班员发现,事故和视频都已经过去了。这类系统的价值不在“录”,而在“测”和“报”:画面模糊了要提醒,人员跑到禁入区要报警,车辆占道要识别。用 yolo 系列做目标检测、用 PyQt 搭桌面控制端是这类项目里最常见的组合,其中 yolov5s 的体量对矿用边缘设备比较友好,pyqt5 可以把摄像头预览、报警列表、区域标定放在同一个窗口里。下面按我做这类项目时的技术决策顺序,把选型、线程模型、质量检测、报警规则和排错顺序逐个讲清楚。
2. 接入 yolov5s 与 pyqt5 的最小工程骨架:从安装到线程模型
要搭这类系统,第一件事不是写界面,而是把“yolo 负责分析、pyqt5 负责呈现”这两条线想清楚。很多人上手就写界面,回头发现推理一跑,窗口就卡死,问题恰恰出在线程模型上。
2.1 yolov5s 为什么是井下边缘设备的均衡点
yolov5s 的参数量约 7.2M,权重文件约 14MB,640x640 输入在 i5 级别 CPU 上单帧推理大约 80~150ms,放到带入门级 GPU 的工控机上能压到 20~40ms。对比 yolov5m,m 的精度提升有限但推理耗时增加三成左右,工业现场往往要同时拉 4~8 路 IPC 流,路数一多,算力就被摊薄。所以我一般先拿 yolov5s 把整条链路跑通,漏检多了再升级模型,而不是一上来就用大模型。
配套的数据集要注意:COCO 预训练权重里的类别是 person、car 这类通用目标,井下场景常有防爆车、梭车、皮带异物等特殊目标,直接用预训练权重很容易把“人戴安全帽”和“人头”混在一起。最稳妥的做法是拿现场视频抽 2000~5000 帧,标注后做增量微调,类别控制在 3~6 个以内。
2.2 pyqt5 安装与 pycharm 配置:两个常见的坑
先确认 Python 版本,PyQt5 对 3.9~3.11 支持比较好,太新的 Python 小版本容易遇到 pyqt5-qt5 依赖解析异常。安装命令如下:
# 创建独立虚拟环境,避免污染系统 Python conda create -n mine_monitor python=3.9 -y conda activate mine_monitor # 安装 pyqt5 和常用工具包 pip install pyqt5 pyqt5-tools numpy opencv-python如果装的是 torch 版 yolov5,再加pip install torch torchvision;如果设备是纯 CPU 或希望降低部署体积,可以晚点换 onnxruntime。pycharm 里配置解释器时选 mine_monitor 这个环境就行,不需要额外处理 webview 组件。Windows 上常见的坑是 pip 在解析 pyqt5-qt5 时因为自定义源路径失败,表现为安装到一半报错退出,解决办法是临时切换默认源再装,或先升级 pip 再重试。
.ui 文件转 .py 也是必会操作。pycharm 里把 pyuic5 配成 External Tool,命令行对应的操作是:
pyuic5 -o mainwindow.py mainwindow.ui这样每次改界面布局后,一键重新生成 Python 代码,比手写 layout 快得多。要提醒的是,生成的 .py 文件不要手动改,界面逻辑写在新文件里,重新生成时不会覆盖业务代码。
2.3 推理线程与界面线程分离:QThread 和信号槽
把 yolov5 推理直接放进 pyqt5 窗口的主线程,窗口就会每帧卡死一次。标准做法是把推理放在 QThread 里跑,通过 signal/slot 把结果发回界面线程。下面给出一个最简的推理线程骨架:
import cv2 import torch from PyQt5.QtCore import QThread, pyqtSignal class VideoAnalyzer(QThread): # 第一个参数传解析结果dict,第二个参数传预览用QImage frame_ready = pyqtSignal(dict, object) def __init__(self, model_path, video_source, parent=None): super().__init__(parent) self.model = torch.hub.load('.', 'custom', path=model_path, source='local') self.cap = cv2.VideoCapture(video_source) self.running = True def run(self): while self.running and self.cap.isOpened(): ret, frame = self.cap.read() if not ret: break # 推理统一走640x640,预览可以缩到1280以内 results = self.model(frame[:, :, ::-1], size=640) detections = results.xyxy[0].cpu().numpy() # Nx6: x1,y1,x2,y2,conf,cls self.frame_ready.emit({'dets': detections}, frame) self.msleep(30) def stop(self): self.running = False self.wait()这里的关键是frame_ready信号把检测结果和原帧发到主界面,主界面的槽函数只负责绘制,不参与计算。模型加载放在__init__里是为了只加载一次,避免每次启动视频都重新读权重。self.msleep(30)控制分析频率约为 30ms 一帧,实际项目里我通常把推理控制在 10~15 FPS,因为越位和停车检测对这种时间粒度完全不敏感,省下的 CPU 可以留多路视频。results.xyxy[0]返回的六列分别是目标框左上角 x、y、右下角 x、y、置信度和类别 id,后面的区域判断都要基于这个格式。
3. 视频质量检测在矿用监控里的落地:模糊、黑屏与画面冻结
矿用摄像头最容易出现的故障不在算法,而在成像质量。粉尘附着镜片、补光失效、对焦漂移、夜视切换异常,都会让画面变成“能录不能看”。质量检测的目标,就是提前把这种异常变成调度台上的告警。
3.1 选哪些图像指标来判断画面质量
常见的质量检测算法有 BRISQUE 这类无参考评价模型,也有基于梯度、亮度统计的轻量方法。井下设备的算力要匀给目标检测,所以我不用太重的方法,常规做法是检测四类异常:模糊、过暗、过曝、画面冻结。
模糊用拉普拉斯方差(Laplacian Variance)判断,值越低说明边缘越弱、画面越糊。亮度直接看灰度均值,均值落在正常区间之外说明补光或曝光异常。冻结用帧间差判断,连续若干帧的像素变化都很小时,说明编码流可能停了或摄像头死机。这三个指标组合起来,基本能覆盖现场常见的画面问题。
3.2 质量检测函数的实现与阈值标定
import cv2 import numpy as np def check_frame_quality(frame, prev_frame, fps=25): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 1. 清晰度:拉普拉斯方差 lap_var = cv2.Laplacian(gray, cv2.CV_64F).var() blurred = lap_var < 15 # 2. 亮度异常:灰度均值统计 mean_brightness = gray.mean() too_dark = mean_brightness < 50 too_bright = mean_brightness > 200 # 3. 画面冻结:帧间平均绝对差 frozen = False freeze_frames = 0 if prev_frame is not None: diff = cv2.absdiff(gray, cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)) mean_diff = diff.mean() # 连续25帧(1秒)变化值都很低,才判定为冻结 if mean_diff < 1.0: freeze_frames += 1 frozen = freeze_frames > fps return { 'blur': blurred, 'dark': too_dark, 'bright': too_bright, 'frozen': frozen, 'lap_var': lap_var, 'brightness': mean_brightness, }井下低照度场景里,正常画面的拉普拉斯方差往往会比地面场景低一截,所以我不会把<15当成固定标准。实际操作是在每个摄像头装好后,录 10 分钟正常画面的视频,计算这段视频的平均拉普拉斯方差,再乘以 0.4~0.6 作为阈值。比如正常值是 25,阈值就设在 10~15 之间。亮度的正常区间也按白天、夜班分别配置,因为井下照度在交接班时段变化很大。
3.3 把质量检测结果接入 pyqt5 状态栏
在 pyqt5 里做一个 QTimer,每隔 2 秒从当前帧做一次质量检查,把结果通过信号发到界面状态栏。
from PyQt5.QtCore import QTimer quality_timer = QTimer() quality_timer.timeout.connect(on_quality_check) quality_timer.start(2000) def on_quality_check(self): # 从最新帧里取一帧做质量检测 latest_frame = self.analyzers[0].latest_frame if latest_frame is None: return result = check_frame_quality(latest_frame, self.prev_frame) self.prev_frame = latest_frame.copy() # 把四个异常项目显示在状态栏,颜色区分 if result['blur']: self.status_label.setText('摄像头画面模糊,请安排现场擦镜') self.status_label.setStyleSheet('color: red;')质量状态的展示不一定要用复杂组件。有人喜欢把告警做成 HTML 日志面板,用 QTextBrowser 的 setHtml 完全够用,不需要引入 QWebEngineView 那种重量级依赖。真正的坑在self.prev_frame = latest_frame.copy()——如果不 copy,下一次循环里 frame 被覆盖,absdiff 计算的就是同一帧的变化,冻结检测永久不触发。这个细节我在现场调试时踩过,冻结检测迟迟不报,最后发现是引用问题。各种阈值先记在 config 文件里,现场标定后回填,不要写死在函数里。
4. 视频智能分析的两条业务规则:越位报警与停车检测
质量检测解决“画面能不能看”,这一章解决“画面里的情况要不要管”。矿用监控最常见的两条规则:人员进入危险区域要报警,车辆在禁停区域停留过久要告警。这两条规则的判定逻辑不同,但都跑在同一条检测链路上。
4.1 目标检测到业务规则之间的必要一跳:跟踪
yolov5s 输出的每一帧目标框都是独立的,下一帧同一个人的框换了位置,算法并不知道它们是同一个人。越位和停车都需要跨帧判断“谁在哪持续了多久”,所以中间必须加跟踪层。最简单的是基于检测框的 IOU 匹配,复杂一点用 DeepSORT 做外观特征匹配。井下环境光线不稳、粉尘多,外观特征容易被干扰,我一般用轻量的中心点最近邻匹配就够用了。
实现上维护一个字典,key 是 track_id,value 是上一帧的中心点坐标。每帧检测完毕后,对每个新检测框计算与上一帧已有目标的欧氏距离,距离最小的且低于阈值的就认为是同一个目标。这样后面区域判定就有连续的时间上下文了。
4.2 越位报警:区域设定与状态机触发
越位报警的业务逻辑可以分为三步:在界面上画出危险区域多边形、判断目标中心点是否在多边形内、只在状态变化时上报。
区域多边形可以预先写在配置文件里,格式是一组归一化坐标点。程序启动时读取并还原成图像坐标。判断点是否在多边形内,OpenCV 提供了现成的函数。
import cv2 import numpy as np from collections import defaultdict class Zone: def __init__(self, name, points): self.name = name self.points = np.array(points, dtype=np.float32) # [[x1,y1],[x2,y2],...] self.state = 'CLEAR' # CLEAR 或 CROSS self.enter_time = None def check_crossing(dets, zones, now_ts): alerts = [] for zone in zones: in_zone = False for x1, y1, x2, y2, conf, cls in dets: # 只对“人”做越位判断,类别id按自己的数据集调整 if int(cls) not in (0, 1): continue cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 # 返回值为正:点在多边形内; 0:在边上; 负:在外部 if cv2.pointPolygonTest(zone.points, (cx, cy), False) >= 0: in_zone = True break old_state = zone.state zone.state = 'CROSS' if in_zone else 'CLEAR' if zone.state != old_state: if zone.state == 'CROSS': zone.enter_time = now_ts alerts.append({'type': 'cross', 'zone': zone.name, 'time': now_ts}) return alerts这段代码里最关键的是“状态变化才触发”:区域从 CLEAR 变 CROSS 只报警一次,CROSS 持续期间不重复发。如果每帧都发,报警中心会被刷爆。工程上还要加一个冷却窗口,比如某区域 3 秒内只允许产生一条越位告警,避免人员站在区域内边缘抖动导致反复触发。中心点判定的优点是计算快、稳定,缺点是目标被遮挡时中心点可能暂时移出区域造成漏报;如果漏报明显,可以改成计算目标框与区域多边形的 IOU,超过 0.3 就视为越位。
4.3 停车检测:中心点位移与停留时长
停车检测要区分两类语义:一类是“车辆应该停在这里但没停”,常见于车场、装载点;另一类是“车辆不该停在这里但停了”,常见于消防通道、皮带下方通道。两者的判定逻辑一致,只是一条取反、一条取正。
核心做法是对每个 track_id 追踪中心点位移。位移持续小于阈值且达到设定时长,就判定为停车。
import math # track_history 保存每个目标最近一帧的中心点 # stop_record 记录目标开始静止的时间戳 MOVE_THRESHOLD = 10 # 中心点位移小于10像素视为静止 PARK_SECONDS = 15 # 持续15秒触发停车报警 def check_parking(track_hist, dets, now_ts, stop_record): alerts = [] current_ids = set() for x1, y1, x2, y2, conf, cls, track_id in dets: current_ids.add(track_id) cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 if track_id in track_hist: prev_x, prev_y = track_hist[track_id] dist = math.hypot(cx - prev_x, cy - prev_y) if dist < MOVE_THRESHOLD: if track_id not in stop_record: stop_record[track_id] = now_ts elif now_ts - stop_record[track_id] > PARK_SECONDS: alerts.append({'type': 'parking', 'track_id': track_id, 'duration': now_ts - stop_record[track_id]}) else: # 车辆移动了,删除静止计时 stop_record.pop(track_id, None) track_hist[track_id] = (cx, cy) # 清理已经消失的目标 for tid in list(track_hist.keys()): if tid not in current_ids: track_hist.pop(tid, None) stop_record.pop(tid, None) return alertsMOVE_THRESHOLD 的取值跟摄像头安装高度和画面分辨率强相关。1080p 画面里,车辆停在原地时中心点抖动通常不超过 3~5 像素,我把阈值放宽到 10 像素可以容忍轻度抖动。但如果摄像头装在铲运机上跟着画面移动,这个阈值就必须放大或者改用相对坐标。PARK_SECONDS 在现场常设为 10~30 秒,太短容易被装料、卸料过程误触发。停车告警最好附带一张抓拍的原始帧,调度员看照片就能判断是车坏了还是司机离岗,不用反复回放视频。
5. 源码实战的排错顺序与验证技巧
拿到一套这类系统的源码,不要急着跑,先看目录结构。常见组织方式不会偏离这几个模块:config 放摄像头地址、区域多边形、报警阈值;detector 放 yolo 模型加载和推理封装,可能同时有 torch 版本和 onnx 版本;ui 放 pyqt5 界面文件、qss 样式和主窗口代码;core 放质量检测、越位/停车规则引擎。先改 config 里的配置跑一遍,再动代码,是排查问题最快的路径。
5.1 模型推理层先确认跑在哪个后端
很多源码默认走torch.hub.load加载权重。torch 版本和 pyqt5 版本在同一 Python 进程里共存,最容易翻车的是 Python 版本不匹配。如果系统要求免安装部署,常见做法是把模型转成 onnx 后改用 onnxruntime 推理,一来体积小,二来 CPU 推理速度通常更好,三来不依赖 torch 版本。转换命令是:
python export.py --weights best.pt --include onnx --opset 11转完后推理线程里的模型加载部分替换成 onnxruntime 的 InferenceSession,输入输出名要看导出后的模型的元信息。在矿用系统里,onnx 分支几乎是标配,因为现场工控机往往不能联网装大型依赖。
5.2 显示与绘制别反复转换 QImage
pyqt5 显示视频帧的常见性能瓶颈,是把 BGR 转 RGB 后直接 setPixmap 导致内存拷贝太多。正确姿势是 QImage 构造时直接引用合适的数据格式:
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) pixmap = QPixmap.fromImage(qimg.copy()) self.video_label.setPixmap(pixmap.scaled(self.video_label.size(), Qt.KeepAspectRatio))注意qimg.copy()这一步不能省——QImage 默认引用的是 numpy 数组的内存,下一次循环 frame 被覆盖后,界面画出来的就是坏帧。绘制检测框时直接在这个 RGB 副本上用 cv2.rectangle 画完再转 QImage,比先转 QImage 再调 QPainter 画框省事,性能差别不大。
5.3 验证整条报警链路的最小脚本
调试报警规则时,我不想每次都打开图形界面,命令行验证反而更快。下面这个脚本把摄像头视频流跑一遍,把报警记录输出为 JSON lines,方便用 grep 筛选。
import cv2 import json import datetime cap = cv2.VideoCapture(0) # 0为本地摄像头,也可传RTSP地址 track_hist, stop_record = {}, {} while cap.isOpened(): ret, frame = cap.read() if not ret: break dets = run_inference(frame) # 替换成自己的推理函数 alerts = check_crossing(dets, zones, datetime.datetime.now()) alerts += check_parking(track_hist, dets, datetime.datetime.now(), stop_record) for a in alerts: print(json.dumps({'ts': str(datetime.datetime.now()), 'camera': 'cam01', 'event': a}, ensure_ascii=False))验证越位报警,可以找一个有人的视频,把危险区域画在行人必然经过的路径上,循环播放。脚本应当在人迈入区域的时刻输出一条'event': {'type': 'cross'}的记录。验证停车报警,则把禁停区域画在车辆静止位置附近,保证中心点位移小于阈值,等待超过 PARK_SECONDS 后查看有没有告警。这种验证方式还能顺手做回归测试——现场换了一个摄像头视角后,直接拿新录像跑一遍脚本,就能确认规则和阈值需不需要重新标定。
本文还有配套的精品资源,点击获取