简介:一套基于卷积神经网络、PyQt5与OpenCV的人员离岗检测告警系统完整工程,面向毕业设计、课程设计及深度学习视觉应用开发者。系统支持加载本地视频或网络视频流,通过自定义危险区域坐标,对固定视角下的人员离岗行为进行实时识别与告警,并具备完整PyQt交互界面,便于适配不同监控场景。压缩包共221个文件,约150.88MB,涵盖Python源码、yaml配置文件、模型权重(pt)、界面ui/qrc资源、mp4运行演示视频及md使用说明等,覆盖模型训练到界面部署全链路。使用说明整理了环境创建、依赖安装与危险区域坐标配置等关键信息,另附训练图片和多段测试视频便于比对效果。已有342人学习下载,适合需要快速搭建离岗检测系统、研究卷积神经网络应用落地的开发者二次开发与调试。
1. 为什么人员离岗检测的关键不在检测,而在区域判定
固定视角的监控画面里,判断一个人是不是真的离开工位,难点不在“有没有人”这个分类,而在两个容易被忽略的细节:一是人形检测框和岗位区域之间到底怎么算“在里面”,二是无人的持续时间如何做防抖。这套基于 YOLOv5 v6.1 的卷积神经网络 + OpenCV + PyQt5 人员离岗检测告警系统,核心并不是把目标检测跑通,而是在固定背景下去定义危险区域,再用检测框中心点判断该区域内是否持续无人。项目结构直白,适合毕业设计、课程设计以及安全监管类的演示场景,调优空间也都集中在坐标和阈值上。
2. 环境搭建与依赖管理:Python3.8、PyQt5、OpenCV 的版本约束
拿到压缩包之后,内部包含main.py、提取背景.py、requirements.txt、模型权重文件、运行视频,以及test1.jpg、test2.jpg、train_batch0.jpg等用于快速验证的图片。这种项目通常不是单文件脚本,而是 PyQt5 界面 + OpenCV 视频流 + YOLOv5 推理三层结构。如果直接在系统 Python 环境中安装依赖,很容易遇到 opencv-whell 和 PyQt5 的 Qt 插件互相抢占,或者 torch 版本过新导致 YOLOv5 v6.1 不兼容。因此第一步不是打开 main.py,而是先搭一个干净的 Python 3.8 虚拟环境。
2.1 Anaconda 创建虚拟环境与镜像加速
摘要里要求的流程是先在 Anaconda 中创建 Python 3.8 环境,再在 PyCharm 中导入该环境。这个顺序不能反过来,否则 PyCharm 会默认使用基础环境,把 site-packages 搞得一团乱。推荐命令如下:
conda create -n duty python=3.8 -y conda activate duty pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple选择 Python 3.8 的原因是 YOLOv5 v6.1 在自动化测试时覆盖了 3.8,而 PyQt5 5.15.x 和 torch 1.10 到 1.13 这一区间内,对 Python 3.8 的 ABI 兼容性最稳定。如果直接用 Python 3.10 或 3.11,安装 opencv-python 时可能拿到新版预编译包,底层依赖的 Qt 版本会和 PyQt5 冲突,出现 QThread 崩溃或者cv2.imshow被强行关闭。
这里有个常见的安装错误:MODULE_NOT_FOUND_ERROR: No module named 'opencv'。这是因为 pip 包名不叫 opencv,而是 opencv-python。项目使用import cv2,所以 requirements.txt 中必须写opencv-python。如果只跑后台检测不显示窗口,可以换成opencv-python-headless,但本项目的告警界面需要实时绘制危险区域,建议保留完整版 OpenCV。
2.2 PyQt5 与 OpenCV 的共存验证
安装完成后不要急着跑 main.py,先用一条命令验证两个库能否同时导入:
import cv2 from PyQt5.QtCore import PYQT_VERSION_STR print(cv2.__version__) print(PYQT_VERSION_STR) cap = cv2.VideoCapture(0) print(cap.isOpened()) cap.release()这段代码同时加载了 OpenCV 的 VideoCapture 和 PyQt5 的 QtCore。如果版本冲突,通常会在这个阶段输出Failed to create Qt platform plugin或直接 segment fault。项目推荐的依赖版本如下,实测时可以对照自查:
| 依赖包 | 推荐版本区间 | 用途 |
|---|---|---|
| python | 3.8.x | 解释器 |
| torch | 1.10.0 ~ 1.13.0 | YOLOv5 卷积神经网络推理 |
| torchvision | 与 torch 匹配 | 模型前处理 |
| opencv-python | 4.5.5 ~ 4.8.0 | 视频读取、绘制、坐标转换 |
| PyQt5 | 5.15.2 ~ 5.15.10 | 主界面 |
| numpy | 1.21 ~ 1.23 | 图像数组运算 |
提示:不要在
import PyQt5之后再调用cv2.imshow(),这两个库的事件循环会互相抢占。正式运行时,OpenCV 只负责读帧和绘制,界面显示全部交给 PyQt5 的 QLabel。
2.3 requirements.txt 与 Dockerfile 的存在意义
压缩包里有 Dockerfile,但这个方法在国内网络环境下并不总是顺畅。Docker 里跑 PyQt5 需要额外配置 X11 转发,摄像头设备也要映射,对调试不友好。Dockerfile 更适合把 YOLOv5 推理单独做成无界面的后台服务。日常开发还是用 Anaconda + PyCharm 更直接。
requirements.txt 中常见依赖包括 torch、torchvision、PyYAML、matplotlib、tqdm、PyQt5、opencv-python。matplotlib 和 tqdm 仅用于 YOLOv5 训练可视化,如果只做推理,可以去掉,但为避免 main.py 里隐式引用,建议原样保留。路径不要出现中文,否则 Windows 下 OpenCV 读取模型和视频都可能返回空对象。
3. YOLOv5 v6.1 模型加载与 OpenCV 实时帧处理
系统始终在做的是一件事:从摄像头或视频文件中取出一帧图像,把这一帧交给卷积神经网络推理,得到人形检测框,再根据检测框和危险区域的相对位置决定是否告警。这一章先把“取帧 + 推理”这条链路写清楚。
3.1 从本地权重加载 YOLOv5 模型
YOLOv5 v6.1 的主干网络是 CSPDarknet53,它的特征提取过程全部由卷积层完成,属于典型的 CNN 目标检测模型。项目根目录的PyQt5-YOLOv5-yolov5_v6.1.iml说明这是一个 PyCharm 工程,模型代码大概率以本地目录形式存放在工程内。加载方式建议使用 torch.hub:
import torch device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = torch.hub.load('.', 'custom', path='weights/best.pt', source='local') model.conf = 0.35 model.iou = 0.45 model.classes = [0] model.to(device)这里的source='local'表示从当前项目目录加载 YOLOv5 源码,而不是从 GitHub 下载。path='weights/best.pt'指向训练好或预训练好的权重文件,如果使用官方预训练权重则替换为yolov5s.pt。model.classes = [0]是过滤出 COCO 类别中的 person,这一步能让检测结果只留下人员框,避免桌子、椅子、显示器造成的干扰。
如果运行后发现测试图test1.jpg没有任何输出,优先检查权重路径和model.classes。model.conf = 0.35表示置信度阈值,低于 0.35 的检测框都会被丢弃。对远处小目标可以降到 0.25,但误检会增加;近景工位可以提高到 0.4。
3.2 对视频帧进行检测并提取人员框
模型接收的是 BGR 数组,YOLOv5 内部会自动处理 letterbox 和颜色空间。推理代码只需要把 OpenCV 读到的帧直接传入:
results = model(frame, size=640) det = results.pandas().xyxy[0] person_boxes = det[det['class'] == 0]size=640是输入到网络的图像尺寸。YOLOv5 会先把帧缩放填充到 640 x 640,再交给卷积网络。这里有一个容易忽略的点:results.pandas().xyxy[0]的每一行包含xmin, ymin, xmax, ymax, confidence, class, name。过滤条件应该使用class而不是name,因为name可能随模型数据集变化。
person_boxes里的坐标是相对于原始图像尺寸的,不是 640 缩放宽高。这一点很重要,绘制矩形时直接用cv2.rectangle(frame, (int(row.xmin), int(row.ymin)), ...)即可,不需要再做坐标缩放。
3.3 OpenCV 视频读取与跳帧控制
系统支持本地视频和网络视频流。OpenCV 读取的核心是cv2.VideoCapture,但实时推理时不能每一帧都跑一次 YOLOv5,否则帧率会降到个位数。常见做法是做跳帧推理:
cap = cv2.VideoCapture(video_path) cap.set(cv2.CAP_PROP_BUFFERSIZE, 3) frame_skip = 2 frame_count = 0 while cap.isOpened(): ret, frame = cap.read() if not ret: break frame_count += 1 if frame_count % frame_skip != 0: continue person_boxes = detect_person(frame, model)frame_skip = 2表示每 2 帧推理一次,其余帧沿用上一次的检测结果。30fps 的视频经过跳帧后只需要处理 15fps,能大幅降低 GPU 占用。CAP_PROP_BUFFERSIZE对 RTSP 流有效,减小缓冲可以降低延迟,但对本地视频文件没有作用。
3.4 OpenCV 帧转换为 PyQt5 QImage
OpenCV 读出的颜色空间是 BGR,而 Qt 的 QImage 使用 RGB,不转换直接显示会让画面偏蓝红。转换函数如下:
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape bytes_per_line = ch * w qt_img = QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888) label.setPixmap(QPixmap.fromImage(qt_img).scaled(label.size(), Qt.KeepAspectRatio))bytes_per_line必须等于ch * w,否则图像会出现斜向撕裂。QImage只保存了rgb.data的指针,不持有数据所有权,所以rgb这个变量必须在整个界面刷新过程中存活。最简单的做法是把rgb保存成类成员变量,避免被垃圾回收导致花屏。
4. 危险区域标定与离岗判定:background.png 和 main.py 第 161 行
这一部分是项目最容易踩坑的地方。很多人把背景图功能理解为随便截一帧,然后画个框,结果人还没离开就触发告警。实际项目中,提取背景.py生成background.png,再用画图工具读取坐标,修改 main.py 第 161 行左右,这个流程每一步都有物理意义。
4.1 提取背景.py 的原理
固定视角场景下,视频里每一帧都包含当前时刻的人和物。如果直接取某一帧作为背景,画出来的危险区域可能贴着人的轮廓,视觉上没问题,但运行时人员检测框的中心点很容易落在区域外。因此项目在开始标定前先做背景提取。常见实现是多帧取中值或均值:
import cv2 import numpy as np cap = cv2.VideoCapture("duty_video.mp4") frames = [] for _ in range(50): ret, frame = cap.read() if not ret: break frame = cv2.resize(frame, (1280, 720)) frames.append(frame) bg = np.median(np.stack(frames), axis=0).astype(np.uint8) cv2.imwrite("background.png", bg)这里取 50 帧画面进行中值合成。中值比均值更抗稀疏前景干扰,因为人只在部分帧中出现,中值能把这部分像素过滤掉。需要说明的是,如果检测场景里有长时间停留的物体,比如有人靠在工位上不动,它会成为背景的一部分。这样生成的背景图在运行时可能让人形框和背景融为一体,出现检测不到人的问题。所以提取背景时要确保画面中人员离开。
4.2 从画图工具读取坐标并写入 main.py
用 Windows 自带“画图”打开background.png。鼠标移到目标区域时,左下角会显示该点的 x 和 y 像素坐标。危险区域用矩形的左上顶点和右下顶点表示,例如鼠标在工位左上角读到(420, 260),在右下角读到(760, 530)。修改 main.py 第 161 行附近的代码:
zone = ((420, 260), (760, 530))左上顶点对应xmin, ymin,右下顶点对应xmax, ymax。写反后 OpenCV 绘制矩形虽然不报错,但判定逻辑中的大小比较会失效。这里还要注意,如果运行提取背景.py时对视频做了 resize,那么画图读到的坐标也必须对应同一个分辨率。比如背景图是 1280x720,输入视频是 640x480,需要手动把坐标乘以系数,或者在背景提取脚本中不改变尺寸。
提示:改完坐标后重新运行 main.py,应先在测试图上绘制半透明危险区域确认。OpenCV 的
cv2.rectangle没有透明度参数,可以用cv2.addWeighted把填充层和原图叠加,避免绘制后盖住人员。
4.3 人员是否在岗的判定逻辑
模型检测出所有人形框后,系统需要判断这些框是否落在zone内。项目推荐使用中心点而不是检测框与危险区域的 IOU。关键代码逻辑如下:
def center_in_zone(xyxy, zone): x1, y1, x2, y2 = xyxy cx = (x1 + x2) / 2 cy = (y1 + y2) / 2 zx1, zy1, zx2, zy2 = zone[0][0], zone[0][1], zone[1][0], zone[1][1] return zx1 <= cx <= zx2 and zy1 <= cy <= zy2 occupied = False for _, row in person_boxes.iterrows(): if center_in_zone([row.xmin, row.ymin, row.xmax, row.ymax], zone): occupied = True break使用中心点的原因是目标检测框的宽高会随着人的站姿、坐姿、手臂伸展幅度变化。比如一个人坐在工位上把手臂抬起来,IOU 可能瞬间降到危险区域之外,实际上人并没有离岗。中心点具备更强的稳健性,只要躯干位置仍在区域内,判定就保持有人。缺点是弯腰严重时中心点可能偏移到区域外,此时可以改用检测框底部中心点,因为这个位置更接近人脚部,适用于有固定工位的场景。
4.4 无人持续时长与告警复位
离岗不能一秒钟判定,否则只要检测框抖动一次就误报。常见的实现是维护两个时间变量:
empty_start = None if occupied: empty_start = None else: if empty_start is None: empty_start = time.time() elif time.time() - empty_start > absence_sec: trigger_alarm(zone_id)absence_sec是无人持续阈值,项目里通常设为 5 秒或 10 秒。这里需要处理告警复位的时间差:人员回到工位后,occupied变为 True,状态恢复,告警关闭。如果希望告警维持一段时间,可以加入冷却时间cooldown_sec,允许在人员离开又回来时至少保留 3 秒告警。状态机逻辑放到 PyQt5 线程里按检测帧轮询即可。
4.5 多区域扩展的坐标设计
有些方案需要同时监管多个座位,这时可以把zone改成列表:
zones = [((420, 260), (760, 530)), ((800, 260), (1100, 540))]每个区域单独维护empty_start和告警状态。判定逻辑复用center_in_zone,对每个区域分别遍历person_boxes。界面显示时用不同颜色的矩形标识区域,红色表示告警,绿色表示有人。多区域坐标读取仍然用画图工具逐个记录两个顶点坐标,不需要修改背景提取逻辑。
5. 告警联动与 PyQt5 界面交互:QThread 与状态机
如果直接在 PyQt5 主线程里跑 YOLOv5 推理,视频画面会卡顿,窗口拖动无响应,甚至弹出 “QThread: Destroyed while thread is running” 错误。告警系统的可靠性取决于线程分离和状态机设计。
5.1 使用 QThread 隔离视频检测循环
视频循环应放在 QThread 的run()方法中,通过信号将处理后的帧和告警状态传回主线程。一个精简的线程类如下:
class DetectThread(QThread): frame_signal = pyqtSignal(QImage) alarm_signal = pyqtSignal(bool, int) def run(self): cap = cv2.VideoCapture(self.video_source) while not self.isInterruptionRequested(): ret, frame = cap.read() if not ret: break person_boxes = detect_person(frame, self.model) occupied, empty_seconds = check_zones(person_boxes, self.zones) qt_img = convert_to_qimage(frame) self.frame_signal.emit(qt_img) self.alarm_signal.emit(occupied, empty_seconds) cap.release()pyqtSignal是线程安全的,Qt 主线程会通过事件队列接收信号并更新界面。不要在子线程里直接调用label.setPixmap(),这会绕过 Qt 的事件循环,轻则刷新异常,重则崩溃。线程退出时一定要释放摄像头,否则再次点击开始按钮时摄像头仍被占用,报[ WARN:0] Failed to open camera。
5.2 告警方式的三级联动
项目支持界面告警和声音告警,使用方可以根据现场需求扩展为平台通知。简单的 HTTP 告警在内部网络环境中非常实用:
import requests def send_alarm(zone_id, empty_seconds): payload = {"zone_id": zone_id, "empty_seconds": empty_seconds} try: requests.post("http://192.168.1.100:8080/alert", json=payload, timeout=2) except requests.exceptions.ConnectionError: passtimeout=2是必须的。如果告警服务器不可达,默认请求会阻塞几十秒,导致检测线程积压大量视频帧。这里应该把网络异常吞掉,只记录日志,不影响检测主流程。声音告警可以使用 PyQt5 的QSound播放 WAV 文件,或者使用winsound.Beep,后者在 Windows 上不需要额外资源文件。
5.3 检测参数与模型选择
YOLOv5 的不同权重对离岗检测的影响非常直接:
| 场景 | 推荐权重 | 输入尺寸 | conf | 说明 |
|---|---|---|---|---|
| 单工位近景 | yolov5s.pt | 640 | 0.35 | 速度快,检测稳定 |
| 多工位远景 | yolov5m.pt | 960 | 0.25 | 小目标召回率更高 |
| 低性能机器 | yolov5n.pt | 416 | 0.4 | 帧率高,远距离易漏检 |
conf的调整要依赖现场验证。如果人员形框频繁闪烁,表现为同一人在连续帧中被识别和丢失,可以把iou阈值提高到 0.6,或者在多帧结果之间做检测框平滑。需要注意的是,跳帧检测后平滑的意义不大,因为两次推理之间帧号并不连续。
6. 部署排错与离岗判定参数验证
系统进入现场部署后,最常遇到的是视频流打开失败和离岗误报。下面给出一套可以直接复用的排查与验证方法。
6.1 OpenCV 读取 RTSP 失败的处理顺序
当cap.isOpened()返回 False,先不要怀疑 YOLOv5,问题基本出在 OpenCV 的 ffmpeg 后端。设置超时参数可以减少卡死时间:
cap = cv2.VideoCapture(rtsp_url, cv2.CAP_FFMPEG) cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000) cap.set(cv2.CAP_PROP_READ_TIMEOUT_MSEC, 5000) cap.set(cv2.CAP_PROP_BUFFERSIZE, 3)CAP_PROP_BUFFERSIZE=3能限制 OpenCV 内部缓冲,避免读取到延迟十几秒的旧帧。如果 RTSP 拉流画面正常但仍然无法打开,先用 VLC 验证地址。对于海康、大华等设备,需要配置 RTSP 路径中的 stream 编码,部分设备默认输出 H.265,OpenCV 的 ffmpeg 版本可能不支持,需要在设备端改成 H.264。
6.2 离岗判定阈值的量化验证
调整absence_sec时,不要靠肉眼观察。从项目运行视频里截取一段十分钟的片段,人工记录每一秒是否有人,再和算法输出对比。简单的验证脚本可以按时间窗口统计:
def verify(labels, detections, threshold=5): tp = fp = fn = 0 for label, det in zip(labels, detections): if label == 1 and det == 1: tp += 1 elif label == 0 and det == 0: continue elif label == 0 and det == 1: fp += 1 else: fn += 1 print("误报:", fp, "漏报:", fn)运行环境里建议以 1 秒为时间片做统计。当漏报多时,降低model.conf或增大检测框中心点判定范围;误报多时,提高absence_sec并检查是否有路过的外人进入危险区域。模型 conf 和 absence_sec 的作用面不同,conf 影响检出能力,absence_sec 影响时间稳定性,修改参数后要分别验证。启动系统后,用手机拍摄工位画面,在检测区域内进出来回走动,观察告警触发和复位时间是否符合预期;只有连续无人超过absence_sec时才进入告警状态,这个判定逻辑需要从日志中看到明确的触发和恢复时间戳。
本文还有配套的精品资源,点击获取