基于CNN与OpenCV的人员离岗检测系统实战
2026/9/20 9:53:07 网站建设 项目流程

简介:基于卷积神经网络与PyQt5、OpenCV的人员离岗检测告警系统,面向深度学习、毕业设计、课程设计场景,适合需要完成实时监测、越界预警等功能的开发者。资源包共221个文件,包含49个Python脚本、49个模型配置yaml、49个网络结构示意图png、5个训练权重pt、4个演示mp4以及UI设计文件和说明文档,压缩包约150.88MB,覆盖从模型训练到界面部署的完整链路。已有342人学习下载。内容包含可直接运行的main.py、独立的环境依赖清单、背景提取工具与坐标标注方法,并附有使用说明和运行视频,能够帮助读者快速搭建环境、加载模型并实现固定视角下的人员离岗检测。作者还对危险区域绘制、实时视频流接入等关键环节做了特别说明,便于二次开发与功能扩展,也适合作为课程设计与毕业设计的项目原型。

1. 人员离岗检测系统的基本思路与技术选型

值班室、监控机房、生产工位这些场景里,“人还在不在岗位上”是个高频需求,但监控画面往往几十路同时挂着,靠人盯屏幕不现实。用卷积神经网络做这件事,本质上是把“岗位是否有人”变成一个图像分类问题:对每个工位区域截图,让 CNN 判断画面里是“在岗”还是“离岗”。OpenCV 负责取流、裁剪、画框这些图像处理,PyQt5 则把检测结果、告警状态和视频画面做成一个能直接操作的桌面界面。这个组合的优势在于每一层都有明确分工,出问题时可以单独定位是模型不准、视频流没读到,还是界面线程卡死。适合正在做安防、考勤或工位管理项目的开发者,也适合想用 CNN 做实际落地而不是只跑 MNIST 的初学者。

2. 基于卷积神经网络的岗位状态分类模型

2.1 数据集的构造方式与标注规范

离岗检测的模型可以走两条路线:一条是目标检测,直接用 YOLO 之类检测画面里的人;另一条是图像分类,只对固定工位区域做“有人/无人”二分类。标题里写的是卷积神经网络,没有限定具体模型结构,一般项目里更常见、也更容易训练的是第二条路线,因为分类模型对数据量和标注要求都低很多,单张图只有一个标签,不需要画边界框。

数据集构造的常见做法是:固定摄像头角度,截取工位区域的 ROI,然后按时间段采集画面。比如每天上午、下午、夜间各采一批,包含人员坐着、站着、弯腰、短暂离开、完全没人等状态。每张图缩放到统一尺寸,比如 224×224,标签按目录组织,on_dutyoff_duty两个文件夹各放对应的图片。

dataset/ ├── train/ │ ├── on_duty/ │ │ ├── img_001.jpg │ │ └── ... │ └── off_duty/ │ ├── img_001.jpg │ └── ... └── val/ ├── on_duty/ └── off_duty/

数据量上,每类 500 到 1000 张基本够用。如果只有几十张,需要做数据增强,常用的有随机水平翻转、小角度旋转、亮度扰动。这里特别提醒:离岗空画面往往和有人在时的背景极其相似,所以增强时不要做随机裁剪,否则会把“无人区域的一部分”这种中间态裁出来,反而把模型搞糊涂。

2.2 模型定义与训练参数设置

模型结构不需要很复杂。预训练 ResNet18、MobileNetV3 都是常见选择,二分类任务在工位这种场景下,MobileNetV3-Small 的精度和速度平衡最好,CPU 上单帧推理可以做到 30ms 以内。这里给出一个用 PyTorch 定义 ResNet18 二分类模型的例子:

import torch.nn as nn from torchvision import models class DutyClassifier(nn.Module): def __init__(self, num_classes=2): super().__init__() self.backbone = models.resnet18(pretrained=True) in_features = self.backbone.fc.in_features self.backbone.fc = nn.Linear(in_features, num_classes) def forward(self, x): return self.backbone(x)

训练时几个关键参数:输入尺寸 224×224,batch size 32,优化器用 Adam,初始学习率 1e-4。预训练模型的前几层特征已经能识别边缘、纹理这些通用特征,所以只训练最后一层全连接也可以,但工位画面比较特殊,全部微调效果更好,训练 20 到 30 个 epoch。损失函数用 CrossEntropyLoss,同时监控验证集准确率,防止过拟合。

import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_data = datasets.ImageFolder('dataset/train', transform=transform) train_loader = DataLoader(train_data, batch_size=32, shuffle=True)

ImageFolder 会自动按子目录名生成标签,所以目录结构里 on_duty 在前,off_duty 在后,标签 0 和 1 就对应这两个类别。训练结束后保存权重文件,后面 PyQt5 界面加载它做推理。

2.3 模型导出与推理验证

训练完的模型要转成适合部署的格式。PyTorch 里直接保存 state_dict 是最简单的方式,但加载时还要重新定义一次模型结构。更稳妥的做法是用 TorchScript 导出完整模型,这样 PyQt5 那边不需要再引入训练用的模型类定义:

model.eval() example_input = torch.randn(1, 3, 224, 224) traced_model = torch.jit.trace(model, example_input) traced_model.save('duty_model.pt')

推理验证时,要把读到的图像先做同样的预处理,不要直接往模型里丢原始帧。特别注意 PIL 读图是 RGB,OpenCV 读出来是 BGR,顺序不对会导致颜色通道错乱,准确率明显下降。验证脚本里可以输出每一类的置信度,看看模型是大致猜的还是真的学到了“有人/无人”的差异。如果准确率不到 95%,优先检查数据里有没有大量相似画面,以及采集时间段是否覆盖了不同光线条件。

3. PyQt5 + OpenCV 的界面与视频流实现

3.1 开发环境准备与依赖安装

PyQt5 和 OpenCV 的安装是这个项目里最容易卡住的环节。网上搜“pyqt5安装”“opencv安装教程”能找到很多资料,但真正操作时最需要注意的是 Python 版本匹配和镜像源。用清华源安装可以有效避免下载超时的问题:

pip install pyqt5 -i https://pypi.tuna.tsinghua.edu.cn/simple pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple pip install torch torchvision -i https://pypi.tuna.tsinghua.edu.cn/simple

PyQt5 装完后如果出现ModuleNotFoundError: No module named 'PyQt5',先执行pip list确认包名。有些环境里 pip 对应的是 Python 2,需要换成pip3。另外 PyQt5 自带的 Qt Designer 在Lib\site-packages\pyqt5_tools目录下,如果没装到,可以单独执行pip install pyqt5-tools。OpenCV 那边需要注意opencv-python是带 GUI 支持的版本,读取摄像头和显示画面都靠它;opencv-python-headless不带 GUI 功能,服务器环境才用它。

3.2 视频流读取与画面显示的线程隔离

PyQt5 界面里直接写一个 while 循环读取摄像头帧,会立刻卡死界面,因为视频读取是阻塞操作,而 Qt 的界面刷新需要事件循环。常见做法是用 QThread 单独跑视频读取,通过 signal 把帧传回主线程更新显示。下面是一个最小可用的实现:

import cv2 from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage class VideoThread(QThread): frame_ready = pyqtSignal(QImage) def __init__(self, source=0): super().__init__() self.cap = cv2.VideoCapture(source) self.running = True def run(self): while self.running: ret, frame = self.cap.read() if not ret: continue rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.frame_ready.emit(qimg) self.cap.release()

cv2.cvtColor把 BGR 转成 RGB,是为了让 Qt 显示的颜色正常。ch * w作为 bytesPerLine 参数,告诉 QImage 每一行占多少字节,少了会导致图像错位。如果用的是摄像头,source 传 0 或 1;如果是 RTSP 流,传rtsp://...字符串就行,OpenCV 的 VideoCapture 可以直接解析。

3.3 界面布局与状态显示

界面布局一般分成左侧视频显示区、右侧信息面板和底部控制按钮。核心控件就三个:QLabel 用来显示视频帧、QPushButton 用来控制开始/停止检测、QTextBrowser 或 QTableWidget 用来展示告警记录。

告警记录的实时更新要注意线程安全。视频线程里做模型推理得到离岗结果后,不能直接操作界面控件,要通过 signal 把状态和数据发出去。界面上显示检测状态可以用不同颜色区分:正常显示绿色,离开状态变红色,并附带离开时长。

class DutyWidget: def __init__(self): self.video_thread = VideoThread() self.video_thread.frame_ready.connect(self.update_frame) self.video_thread.alert_triggered.connect(self.show_alert) def show_alert(self, name, duration): self.status_label.setText(f"离岗告警: {name}") self.status_label.setStyleSheet("color: red; font-size: 16px;")

这里把离岗时长也一并展示出来,比单纯显示“离岗”更有用,用户可以知道这个人已经离开多久,决定要不要去现场查看。界面的刷新频率不用太高,25 到 30 FPS 足够,过高的刷新率反而让 CPU 占用率飙升,影响模型推理速度。

4. 离岗判定逻辑与关键参数调优

4.1 基于状态的连续帧判定算法

模型单帧输出一个分类结果,但如果拿单帧结果直接触发告警,画面里有一瞬间的遮挡、转身、弯腰都会误报。实际系统里要用一个连续判定机制:连续 N 帧判为离岗,才开始计时告警;中途出现一帧在岗,计时清零。这个逻辑用状态机实现最清晰:

class DutyStateMachine: def __init__(self, miss_threshold=10, alert_threshold=30): self.miss_threshold = miss_threshold self.alert_threshold = alert_threshold self.miss_count = 0 self.alert_count = 0 self.is_alerting = False def update(self, is_present): if is_present: self.miss_count = 0 self.alert_count = 0 self.is_alerting = False else: self.miss_count += 1 if self.miss_count >= self.miss_threshold: self.is_alerting = True self.alert_count += 1 return self.is_alerting, self.alert_count

这里的miss_thresholdalert_threshold分别对应两个时间窗:miss_threshold 是容忍的帧数,低于这个帧数的离岗不算数;alert_count 用于记录已经离岗多久,方便后续换算成秒数。假设视频是 25 FPS,miss_threshold=10 表示一个人离开 0.4 秒内回来不会触发告警,适合对快速起身活动频繁的岗位;如果岗位要求人必须长时间在凳子上,可以把阈值再调大。

4.2 告警联动与截图留证

告警触发后,常见处理方式有三个:界面弹窗提醒、蜂鸣器发声、同时保存当前帧截图作为留证。截图保存这个功能虽然简单,但部署时非常有用,事后查看告警记录时可以直接打开图片确认现场情况。实现上要注意保存路径按日期分目录,避免文件越攒越多。

import os from datetime import datetime def save_alert_image(frame, save_dir="alerts"): now = datetime.now() day_dir = os.path.join(save_dir, now.strftime("%Y%m%d")) os.makedirs(day_dir, exist_ok=True) filename = os.path.join(day_dir, now.strftime("%H%M%S") + ".jpg") cv2.imwrite(filename, frame) return filename

文件名用年月日时分秒生成,基本可以做到不重名。这里的 frame 是 OpenCV 的 BGR 格式原始帧,如果存的是前面转成 RGB 的 QImage,颜色就会反过来。所以截图动作要放在图像转换之前做,或者单独把原始帧存一份。

4.3 关键参数表与实际调整建议

离岗检测系统的可调整参数不多,但每个参数的取值对实际效果影响很大。整理成一张表方便对照:

参数默认值作用调整方向
输入尺寸224×224模型输入分辨率画面清晰度不够时降到 160
miss_threshold10 帧离岗容忍帧数频繁短暂起身就调大
检测间隔每帧推理频率CPU 不够时改为每 3 帧一次
置信度阈值0.5二分类阈值宁缺毋滥就调到 0.7
ROI 区域全画面检测范围多人场景必须框出单人区域

置信度阈值这里多说一句:模型输出通常是 softmax 后的概率,比如 in_duty=0.35,off_duty=0.65。默认取最大值就判离岗。但如果你更在意别漏报,可以把阈值调低;更在意别误报就调高。这个参数比调整模型本身快得多,遇到准确率不够的问题,先改阈值而不是重新训练。

多人同框是离岗检测最常见的误报来源。值班室两个人坐在相邻工位,模型会把整个画面当成一个整体来判断。解决办法是把每个工位单独裁剪成一张图,分别送进模型。OpenCV 里透视变换可以矫正摄像头角度造成的倾斜,把 ROI 从四边形拉正成矩形,识别效果会稳定很多。如果画面本身是正对工位的俯视角度,直接frame[y1:y2, x1:x2]裁剪就行,不一定要做透视矫正。

5. 运行验证与异常排查的几个实用技巧

5.1 用命令行快速验证环境完整性

界面启动报错时,先不用打开完整的 PyQt5 程序,在命令行分三层验证:摄像头是否能被 OpenCV 打开、模型是否能被加载、PyQt5 的 QImage 转换是否正常。这样能快速缩小问题范围。

python -c "import cv2; cap = cv2.VideoCapture(0); print(cap.isOpened())" python -c "import torch; model = torch.jit.load('duty_model.pt'); print('model ok')" python -c "from PyQt5.QtWidgets import QApplication; print('qt ok')"

如果第一行输出False,说明 opencv 打开摄像头失败,最常见原因是摄像头被其他程序占用,或者 Windows 下分辨率过高导致的兼容性问题。可以先想办法释放摄像头,或者换一个视频源测试。第三行报错的话,检查 PyQt5 和 PyQt5-Qt5 这个运行时包是否都装了。

5.2 关键帧丢失与画面异常的处理

视频流偶尔丢一帧是正常的,但丢帧频繁会导致检测结果时断时续。观察丢帧的方法是在读取循环里统计连续读取失败的次数,如果连续 30 帧都失败,主动重建 VideoCapture 对象,而不是继续读一个坏掉的流。

画面显示偏绿或偏蓝,绝大多数是 RGB 和 BGR 转换问题。记住一条规则:cv2 读出来和存进去的都是 BGR,Qt 显示和 PIL 处理都是 RGB。转换位置放错,颜色就会错。如果画面颜色不对,先查代码里有几处 cvtColor,是不是重复转换了。

5.3 让模型更适应现场环境的两个技巧

部署后发现模型在白天准确率高、晚上误报多,这个问题的根源是训练数据的场景覆盖不够。一个不重新训练就能缓解的技巧是:在推理前做一次简单的亮度归一化,比如先计算灰度图均值,再调整到接近训练集的标准亮度。训练集如果大部分是白天采的,画面均值在 120 左右,那晚上画面均值只有 70,先做对比度拉伸再送进模型,能明显减少误报。

第二个技巧是给模型推理加一个时间上的平滑窗口。不用每次单帧都重新判定,把最近 5 帧的预测结果做投票,输出出现次数多的类别。这个操作和之前的状态机判定逻辑叠加后,误报率能再降一些。代价是检测响应会慢 5 帧左右,换算成时间大概 0.2 秒,对离岗检测来说完全可以接受。推理代码里加上collections.Counter对最近几帧结果计数即可,注意别用列表无限往里面 append,用deque(maxlen=5)自动丢弃旧帧。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询