近年来,随着城市非机动车出行数量持续上升,骑乘电动自行车不佩戴头盔的现象仍然比较常见。传统的人工检查和监控抽查方式效率有限,很难做到全天候、全路口的实时提醒与记录。借助计算机视觉和深度学习目标检测技术,我们可以训练一个能够自动识别“是否佩戴头盔”的检测模型,再结合桌面端 GUI 工具打包成一套完整的小型应用系统,用于路口监控、小区出入口、校园周边等场景的辅助管理。
本文将围绕“基于深度学习 YOLOv8 + PyQt5 的非机动车头盔佩戴检测系统”展开,从技术选型、环境配置、数据集准备、模型训练、推理封装到 PyQt5 桌面界面开发,完整拆解一套可运行的最小系统。无论你是正在做毕业设计、课程项目,还是希望将目标检测落地到实际业务中,都可以参考这篇文章的思路和代码。
1. 背景与核心概念
1.1 为什么要做头盔佩戴检测
非机动车骑行中的头盔佩戴问题,直接关系到骑乘人员的生命安全。传统的管理方式主要依赖交警现场执法或人工查看监控视频,面对大量视频数据时效率较低,且无法做到实时预警。如果能把“头盔检测”交给算法自动完成,系统就可以在识别到未佩戴头盔时自动抓拍、记录时间地点,甚至在现场通过语音或弹窗提醒管理人员,极大减轻人工压力。
这类系统本质上是一个目标检测任务(Object Detection),即在一张图片或一帧视频中定位出目标对象的位置,并给出对应的类别。在头盔检测场景中,目标通常分为两类:
- 佩戴头盔的骑行人(with_helmet)
- 未佩戴头盔的骑行人(without_helmet)
当然,更细化的项目还会增加“二轮车”类别,先检测车辆再判断骑行人,但这种方案会提高标注和训练的复杂度。作为入门级系统,本文采用直接检测骑行人的二分类方案。
1.2 YOLOv8 在目标检测中的优势
YOLO(You Only Look Once)系列是目标检测领域非常经典的算法,它的核心思想是将目标检测看作一个回归问题,通过单次前向传播直接预测目标的类别和边界框,因此速度非常快。YOLOv8 是 Ultralytics 公司推出的版本,在 YOLOv5 的基础上继续优化了网络结构、训练策略和数据增强方式,同时提供了统一的 Python SDK,非常适合快速训练和部署。
YOLOv8 的主要特点:
- 支持目标检测、实例分割、姿态估计、图像分类等多种任务。
- 提供简洁的 Python API,只需几行代码就能完成训练和推理。
- 模型文件体积相对可控,在 GPU 或 CPU 环境下均可运行。
- 支持导出为 ONNX、TensorRT 等格式,便于后续部署。
对于头盔检测这种类间距较大的二分类任务,YOLOv8s 或 YOLOv8n 这类轻量级模型就基本够用。如果对精度要求更高,可以换用 YOLOv8m 或 YOLOv8l,但推理耗时也会相应增加。
1.3 PyQt5 在桌面应用开发中的角色
PyQt5 是 Qt 框架的 Python 绑定,可以用来开发跨平台的桌面 GUI 程序。在深度学习项目中,模型推理代码本身只是命令行脚本,普通用户很难直接使用。通过 PyQt5 封装一个窗口程序,可以把“选择图片检测”“实时摄像头检测”“视频文件检测”等功能做成按钮和界面,用户不需要了解任何模型细节,只需点击按钮即可完成操作。
本系统中的 PyQt5 主要承担以下工作:
- 展示图片或视频画面。
- 触发模型推理并显示检测结果。
- 展示检测统计信息,如检测帧率、未佩戴数量等。
- 提供简单的文件选择和摄像头开关功能。
1.4 系统整体技术架构
下面用一个简洁的流程梳理整个系统的数据走向:
视频帧/图片输入 ↓ OpenCV 读取画面 ↓ YOLOv8 模型推理 ↓ 输出检测框 + 类别 + 置信度 ↓ PyQt5 界面绘制并显示 ↓ 统计结果 / 抓拍保存整个系统可以拆成两个独立模块:
- 模型侧:PyTorch + YOLOv8,负责训练和推理。
- 界面侧:PyQt5 + OpenCV,负责交互和画面显示。
两者通过一个核心检测函数进行衔接,该函数接收一帧图像,返回标注后的图像和检测结果列表。
2. 环境准备与版本说明
2.1 开发环境概览
本文示例以 Windows 10/11 系统为主,Python 使用 3.9 或 3.10 版本。深度学习框架采用 PyTorch,YOLOv8 依赖 Ultralytics 库。界面部分使用 PyQt5,图像处理使用 OpenCV。
| 依赖项 | 推荐版本 | 说明 |
|---|---|---|
| Python | 3.9 / 3.10 | 过低版本可能无法安装最新 PyTorch |
| PyTorch | 2.x | 根据 CUDA 版本选择对应安装命令 |
| Ultralytics | 8.x | YOLOv8 官方 Python SDK |
| PyQt5 | 5.15.x | 稳定版本,兼容性较好 |
| OpenCV-Python | 4.8.x 或更新 | 用于图像和视频处理 |
| NumPy | 1.24 左右即可 | PyTorch 和 OpenCV 的公共依赖 |
需要注意的是,如果你的电脑没有 NVIDIA GPU,可以使用 CPU 版本的 PyTorch,模型仍然可以运行,只是训练和推理速度较慢。对于头盔检测这种小模型,CPU 推理速度大约在每秒几帧到十几帧之间,对实时摄像头检测会有一定压力。
2.2 Python 虚拟环境创建
建议为项目单独创建虚拟环境,避免和系统 Python 环境产生包冲突。打开终端,执行以下命令:
python -m venv helmet_env helmet_env\Scripts\activate激活成功后,命令行前缀会显示(helmet_env),说明已经进入虚拟环境。
2.3 安装 PyTorch
PyTorch 的安装命令取决于你是否使用 GPU。如果使用 GPU,需要先去 NCNN 官网或者 PyTorch 官网查看对应 CUDA 版本的安装命令。以 CUDA 11.8 为例:
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118如果没有 GPU,直接安装 CPU 版本即可:
pip install torch torchvision torchaudio安装完成后,可以用下面的 Python 命令验证 PyTorch 是否可以正常导入:
import torch print(torch.__version__) print(torch.cuda.is_available())如果torch.cuda.is_available()返回True,说明 GPU 环境配置成功;如果返回False,则是 CPU 环境,不影响后续代码运行。
2.4 安装其他依赖库
pip install ultralytics pip install pyqt5 pip install opencv-python pip install numpy安装完成后,为了确保 YOLOv8 可以正常下载预训练权重,建议保持网络畅通。后面运行时,Ultralytics 会自动下载yolov8n.pt等权重文件。
2.5 验证 YOLOv8 是否安装成功
在终端执行:
yolo predict source=https://ultralytics.com/images/bus.jpg这个命令会下载一张示例图片并进行检测。如果你能看到终端输出检测结果,并且本地生成了标注后的图片,说明 YOLOv8 环境已经准备好了。
3. 数据集准备与模型训练
3.1 数据集来源与标注格式
训练自己的头盔检测模型,需要准备带标注的数据集。常用的标注格式是 YOLO 格式,每个图片对应一个同名的.txt文件,文件中每行代表一个目标,格式如下:
类别ID 中心点X 中心点Y 宽度 高度其中坐标值都是归一化到 0~1 之间的小数。例如:
0 0.5 0.4 0.3 0.5表示图片中心位置有一个类别 ID 为 0 的目标,边界框宽度占整张图片的 30%,高度占 50%。
数据集的目录结构通常如下:
dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/你也可以使用 LabelImg 或 LabelStudio 等标注工具完成标注工作。对于头盔检测,建议至少准备 2000~5000 张图片,类别分布尽量均衡。
3.2 数据集配置文件的编写
Ultralytics 训练时需要读取一个 YAML 格式的数据集配置文件,内容示例如下:
# dataset.yaml path: D:/helmet_dataset # 数据集根目录 train: images/train # 训练图片目录 val: images/val # 验证图片目录 nc: 2 names: 0: with_helmet 1: without_helmet配置完成后,将文件保存为dataset.yaml。训练时只需要把这个 YAML 文件路径传给 YOLOv8 即可。
3.3 编写训练脚本
YOLOv8 的训练脚本非常简洁,核心代码如下:
from ultralytics import YOLO # 加载预训练模型,这里使用 YOLOv8s 作为基础模型 model = YOLOL("yolov8s.pt") # 开始训练 model.train( data="dataset.yaml", epochs=100, imgsz=640, batch=16, device="0", # 使用 GPU 训练,CPU 可改为 "cpu" workers=4, project="runs/train", name="helmet_detect", exist_ok=True, )训练过程会在终端实时打印每个 epoch 的损失值、精度、召回率、mAP 等指标。训练完成后,最优权重文件会保存在runs/train/helmet_detect/weights/best.pt。
3.4 训练结果评估
训练结束后,可以执行以下代码查看模型在验证集上的表现:
from ultralytics import YOLO model = YOLO("runs/train/helmet_detect/weights/best.pt") metrics = model.val(data="dataset.yaml") print(f"mAP50: {metrics.box.map50:.4f}") print(f"mAP50-95: {metrics.box.map:.4f}")对于头盔检测任务,mAP50 一般建议达到 0.85 以上才算比较理想。如果精度偏低,可以尝试以下方法:
- 增加训练图片数量,尤其是未佩戴头盔的样本。
- 增加训练轮数,但要注意过拟合。
- 更换更大的模型,例如
yolov8m.pt或yolov8l.pt。 - 调整数据增强参数,如翻转、旋转、亮度变化。
3.5 训练轮数与精度的关系
很多初学者会问,训练轮数设置多少合适。这个没有固定答案,主要看损失曲线是否收敛。一般来说:
- 数据集较小(几千张)时,50~100 轮足够。
- 数据集较大(几万张)时,150~300 轮比较常见。
- 如果训练后期 loss 持续下降但验证集 mAP 开始波动,可能出现过拟合,可以用早停机制或适当增大数据增强。
在 Ultralytics 库中,也可以通过设置patience参数启用早停:
model.train( data="dataset.yaml", epochs=300, patience=20, )patience=20表示如果连续 20 轮验证集指标没有提升,训练会自动提前结束。
4. 基于 YOLOv8 的模型推理封装
4.1 核心推理函数设计
为了让 PyQt5 界面方便调用,我们需要把模型推理逻辑封装成一个独立的类或函数。输入是一帧 BGR 格式的图像(OpenCV 默认格式),输出是标注后的图像以及检测信息列表。
# detector.py import cv2 from ultralytics import YOLO class HelmetDetector: def __init__(self, weights_path="runs/train/helmet_detect/weights/best.pt", conf_thres=0.35, iou_thres=0.5): self.model = YOLO(weights_path) self.conf_thres = conf_thres self.iou_thres = iou_thres def detect_frame(self, frame): """ 输入 BGR 图像,返回标注后的图像和检测结果列表。 结果列表中的每个元素为 (class_name, confidence, x1, y1, x2, y2) """ results = self.model.predict( source=frame, conf=self.conf_thres, iou=self.iou_thres, verbose=False, ) annotated_frame = frame.copy() detections = [] for result in results: boxes = result.boxes if boxes is None: continue for box in boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = float(box.conf[0]) cls_id = int(box.cls[0]) class_name = result.names[cls_id] detections.append((class_name, conf, x1, y1, x2, y2)) # 绘制检测框 color = (0, 255, 0) if class_name == "with_helmet" else (0, 0, 255) cv2.rectangle(annotated_frame, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) label = f"{class_name} {conf:.2f}" cv2.putText(annotated_frame, label, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) return annotated_frame, detections4.2 图片检测测试
在命令行下可以先用一张测试图片验证推理封装是否正确:
import cv2 from detector import HelmetDetector detector = HelmetDetector("runs/train/helmet_detect/weights/best.pt") img = cv2.imread("test.jpg") result_img, detections = detector.detect_frame(img) print(f"检测到 {len(detections)} 个目标") for det in detections: print(det) cv2.imwrite("test_result.jpg", result_img)4.3 视频文件检测测试
处理视频时,需要循环读取每一帧,并统计检测结果。下面是一个简单的视频检测脚本:
import cv2 from detector import HelmetDetector detector = HelmetDetector("runs/train/helmet_detect/weights/best.pt") cap = cv2.VideoCapture("test_video.mp4") fps = cap.get(cv2.CAP_PROP_FPS) while cap.isOpened(): ret, frame = cap.read() if not ret: break result_frame, detections = detector.detect_frame(frame) cv2.imshow("helmet detection", result_frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()这段代码可以直接运行,只是界面比较简陋。接下来我们把它整合到 PyQt5 中。
5. PyQt5 桌面界面开发
5.1 界面布局设计
本系统的 PyQt5 界面主要包含以下区域:
- 顶部按钮区:打开图片、打开视频、打开摄像头、退出。
- 中央画面区:显示原始画面和检测结果。
- 右侧信息区:显示实时统计信息,如总检测人数、未佩戴人数、帧率等。
我们使用QMainWindow作为主窗口,通过QLabel显示图像,通过QPushButton创建按钮,通过QGridLayout或QVBoxLayout布局。
5.2 主窗口代码
下面是一个精简但完整的 PyQt5 主窗口示例:
# main_window.py import cv2 import sys from PyQt5.QtWidgets import QMainWindow, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QApplication, QFileDialog, QTextEdit from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import Qt from detector import HelmetDetector class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("非机动车头盔佩戴检测系统") self.setMinimumSize(1000, 600) self.detector = HelmetDetector("runs/train/helmet_detect/weights/best.pt") self.cap = None self.timer = None self.init_ui() def init_ui(self): # 中央布局 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QHBoxLayout() # 左侧按钮布局 left_layout = QVBoxLayout() self.btn_open_image = QPushButton("打开图片") self.btn_open_video = QPushButton("打开视频") self.btn_open_camera = QPushButton("打开摄像头") self.btn_exit = QPushButton("退出") left_layout.addWidget(self.btn_open_image) left_layout.addWidget(self.btn_open_video) left_layout.addWidget(self.btn_open_camera) left_layout.addWidget(self.btn_exit) left_layout.addStretch() # 中间图像显示 self.label_image = QLabel("请打开图片或视频") self.label_image.setAlignment(Qt.AlignCenter) self.label_image.setStyleSheet("background-color: #000; color: #fff;") # 右侧信息栏 right_layout = QVBoxLayout() self.text_info = QTextEdit() self.text_info.setReadOnly(True) right_layout.addWidget(self.text_info) main_layout.addLayout(left_layout, 1) main_layout.addWidget(self.label_image, 4) main_layout.addLayout(right_layout, 1) central_widget.setLayout(main_layout) # 绑定事件 self.btn_open_image.clicked.connect(self.open_image) self.btn_open_video.clicked.connect(self.open_video) self.btn_open_camera.clicked.connect(self.open_camera) self.btn_exit.clicked.connect(self.close) def open_image(self): file_path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "Image Files (*.jpg *.png *.jpeg)" ) if not file_path: return frame = cv2.imread(file_path) result_frame, detections = self.detector.detect_frame(frame) self.show_image(result_frame) self.text_info.clear() self.text_info.append(f"图片检测完成,共 {len(detections)} 个目标") for det in detections: self.text_info.append(f"{det[0]} 置信度: {det[1]:.2f}") def open_video(self): file_path, _ = QFileDialog.getOpenFileName( self, "选择视频", "", "Video Files (*.mp4 *.avi *.mov)" ) if not file_path: return self.cap = cv2.VideoCapture(file_path) self.text_info.clear() self.text_info.append("视频检测开始") while self.cap.isOpened(): ret, frame = self.cap.read() if not ret: break result_frame, detections = self.detector.detect_frame(frame) self.show_image(result_frame) self.update_info(detections) cv2.waitKey(1) self.cap.release() def open_camera(self): self.cap = cv2.VideoCapture(0) self.text_info.clear() self.text_info.append("摄像头检测开始") while self.cap.isOpened(): ret, frame = self.cap.read() if not ret: break result_frame, detections = self.detector.detect_frame(frame) self.show_image(result_frame) self.update_info(detections) cv2.waitKey(1) self.cap.release() def update_info(self, detections): total = len(detections) without = sum(1 for d in detections if d[0] == "without_helmet") self.text_info.clear() self.text_info.append(f"当前帧检测到 {total} 个目标") self.text_info.append(f"未佩戴头盔人数: {without}") def show_image(self, frame): frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = frame.shape bytes_per_line = ch * w q_image = QImage(frame.data, w, h, bytes_per_line, QImage.Format_RGB888) pixmap = QPixmap.fromImage(q_image) scaled_pixmap = pixmap.scaled( self.label_image.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation, ) self.label_image.setPixmap(scaled_pixmap) if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_())5.3 界面卡顿问题说明
上面的简化代码中,视频检测是在主线程中循环读取的,实际运行时容易出现界面卡顿。更合理的设计是把视频读取和模型推理放到子线程中,主线程只负责刷新界面。但由于本文是入门教程,先以功能可用为主,生产级代码需要进一步优化线程模型。
6. 完整系统运行与验证
6.1 项目目录结构
建议按下面的目录组织工程:
helmet_detection_system/ ├── main_window.py # PyQt5 主界面 ├── detector.py # YOLOv8 推理封装 ├── dataset.yaml # 数据集配置 ├── runs/ │ └── train/ │ └── helmet_detect/ │ └── weights/ │ ├── best.pt │ └── last.pt ├── test_images/ ├── test_videos/ └── requirements.txt6.2 requirements.txt
ultralytics==8.2.0 PyQt5==5.15.9 opencv-python>=4.8.0 numpy>=1.24.0安装依赖时可直接执行:
pip install -r requirements.txt6.3 启动系统
在项目根目录执行:
python main_window.py启动后,点击“打开图片”选择一个包含骑行人的图片,系统会自动标注检测框。点击“打开视频”可以处理视频文件,点击“打开摄像头”可以调用本地摄像头进行实时检测。
6.4 预期效果
当模型检测到佩戴头盔的骑行人时,绘制绿色检测框,类别为with_helmet;当检测到未佩戴头盔的骑行人时,绘制红色检测框,类别为without_helmet。右侧信息栏会显示当前帧的检测统计结果。
7. 常见问题与排查思路
7.1 PyQt5 安装失败
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| pip 安装 pyqt5 报错 | Python 版本过高或网络问题 | 使用 Python 3.9/3.10,换用国内镜像源 |
| 导入 PyQt5 时提示 DLL 加载失败 | 缺少 Visual C++ 运行库 | 安装官方 Visual C++ Redistributable |
国内镜像源安装命令示例:
pip install pyqt5 -i https://pypi.tuna.tsinghua.edu.cn/simple7.2 CUDA 不可用
如果已经安装了 GPU 版 PyTorch,但训练时提示 CUDA 不可用,可以按下面步骤排查:
- 使用
nvidia-smi查看显卡驱动是否正常。 - 确认 PyTorch 版本与 CUDA 版本匹配。
- 使用
python -c "import torch; print(torch.cuda.is_available())"验证。
如果显卡驱动版本太低,需要去显卡官网更新驱动;如果 PyTorch 安装成了 CPU 版本,需要重新安装对应 CUDA 版本的 PyTorch。
7.3 检测效果不佳
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 漏检较多 | 训练数据不足或不均衡 | 增加训练图片,尤其是困难样本 |
| 误检较多 | 置信度阈值设置太低 | 调高 conf_thres 参数 |
| 小目标检测不到 | 图片分辨率不足 | 提高 imgsz 或使用更高分辨率模型 |
7.4 PyQt5 界面显示黑屏
在调用 OpenCV 读取视频帧时,如果摄像头没有被正确释放,或者视频路径不存在,画面会停留在默认黑屏状态。建议在处理前先判断:
if self.cap is None or not self.cap.isOpened(): self.text_info.append("无法打开视频或摄像头") return7.5 模型加载慢
YOLOv8 模型首次加载时,如果有 GPU 环境,会进行 CUDA 初始化,耗时较长属于正常现象。可以将模型加载放在程序启动时完成,避免每次检测都重复加载。
8. 最佳实践与工程建议
8.1 线程模型优化
在实际项目中,视频检测流程不应该直接写在 UI 事件回调里,否则界面在检测时会卡住无法点击。推荐使用QThread或QTimer,将读取视频帧、执行模型推理放到子线程中,主线程通过信号槽机制接收结果并刷新界面。
核心思路如下:
- 子线程循环读取摄像头/视频帧。
- 调用模型推理得到结果。
- 发送
QImage和检测结果到主线程。 - 主线程只负责显示。
8.2 模型选型建议
对于 CPU 部署场景,优先选择yolov8n或yolov8s。对于 GPU 实时检测场景,yolov8s和yolov8m是性能和精度的折中选择。如果项目要求更高的精度,且在服务器端运行,可以尝试yolov8l。
8.3 数据标注注意事项
- 标注框要尽量贴合目标,不要包含大量背景。
- 遮挡严重的骑行人,如果人眼都无法判断是否佩戴头盔,建议直接不标注。
- 训练集和验证集要保证分布一致,最好按场景或时间段划分,避免同一摄像头画面同时出现在训练集和验证集。
8.4 安全与合规建议
如果系统要部署到真实路口或公共区域,需要注意以下几点:
- 确保监控设备安装和图像采集符合当地法律法规要求。
- 系统只用于辅助管理和事后统计,不直接用于行政执法处罚。
- 对采集到的图像数据做好访问权限控制,避免个人隐私泄露。
- 涉及大规模部署时,建议在边缘设备或本地服务器完成推理,减少原始视频外传风险。
8.5 模型导出与部署
训练好的best.pt可以导出为 ONNX 格式,方便在 OpenCV DNN、ONNX Runtime 或其他推理框架中运行:
from ultralytics import YOLO model = YOLO("runs/train/helmet_detect/weights/best.pt") model.export(format="onnx", imgsz=640)导出后的best.onnx文件体积更小,适合嵌入到其他应用中。
9. 项目扩展方向
9.1 增加抓拍保存功能
在检测到未佩戴头盔时,自动截取当前帧并保存到本地目录。这个功能实现比较简单,只需要在检测到without_helmet时执行:
cv2.imwrite(f"capture/{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg", frame)9.2 接入数据库记录
将检测时间、地点、未佩戴数量等信息写入 MySQL 或 SQLite,方便事后查询统计。
9.3 使用告警提示音
在 PyQt5 中可以通过QSound或winsound模块播放提示音,当检测到未佩戴头盔时触发语音提醒。
9.4 多线程实时视频流
如果使用 IP 摄像头,只需将cv2.VideoCapture(0)中的 0 替换为摄像头 RTSP 地址即可:
cap = cv2.VideoCapture("rtsp://user:password@ip:port/stream")但 RTSP 流的延迟和断线重连问题在工程中需要额外处理。
10. 总结
本文完整实现了基于 YOLOv8 和 PyQt5 的非机动车头盔佩戴检测系统,涵盖数据集准备、模型训练、推理封装、桌面界面开发和常见问题排查。你可以在这个基础上继续完善线程设计、抓拍存储、日志统计等功能,把它扩展成真正可落地的辅助管理工具。
如果这篇文章对你有帮助,欢迎收藏备用。后续也可以多做测试样本,观察模型在不同光线、角度、场景下的表现,逐步优化你的数据集和训练策略。