在工地安全管理场景里,人员是否按规定佩戴安全帽,是每天都要检查的高频事项。人工巡检只能抽查,无法覆盖全部监控画面;虽然摄像头已经普及,但普通摄像头本身没有判断能力。要让监控系统自动识别未佩戴安全帽的人员,并把结果推送给管理人员,就需要一个目标检测识别系统。这里选择 YOLOv8 作为检测算法,PyQt5 作为桌面客户端开发框架,两者组合可以快速搭出一个可运行的工地安全帽佩戴检测识别系统。
读者看完正文后,能完成四件事:搭建 YOLOv8 训练环境;准备安全帽标注数据集并训练检测模型;把训练好的模型集成到 PyQt5 桌面程序里,实现图片和摄像头实时检测;理解浮点精度、模型导出、线程设计和常见问题排查。整个流程以可复现为目标,文中的目录和代码是工程示例,实际项目要结合自己的数据集、显卡型号和部署环境调整。
1. 先理解检测系统的整体架构与检测流程
1.1 为什么这类项目适合用 YOLOv8
安全帽佩戴检测本质上是目标检测任务:在图像或视频帧中找出“安全帽”“人头”等目标,并输出边界框、类别和置信度。YOLOv8 是目前落地频率较高的检测框架,原因是它的使用链路完整,训练、验证、预测、导出都有统一命令行和 Python API。
相比早期的 YOLOv5,YOLOv8 在结构上做了几处调整:主干网络继续使用 C2f 模块,颈部采用 FPN-PAN 结构,检测头将分类和回归分支解耦,并使用 Anchor-Free 方式预测目标。对于安全帽这类大小相对固定、背景复杂的工地场景,这些调整带来的收益是训练更稳定、后处理更简单。
需要说明的是,YOLOv8 并不是在所有场景都绝对最优。比如极端小目标场景,可能还需要配合 SAHI 切片推理或自定义检测头。但作为通用基准,YOLOv8 适合中小团队快速落地,而且后续可以切换更高版本或改进模型。工程项目的关键不是追最新结构,而是建立一条可迭代、可评估、可回滚的流程。
1.2 系统的功能模块划分
一个完整的安全帽佩戴检测系统至少包含三个层面,不能只写一个模型文件就算完成。
| 模块 | 职责 | 常见技术选择 |
|---|---|---|
| 数据层 | 采集工地图片、标注安全帽和未佩戴状态、划分训练集和验证集 | LabelImg、LabelMe、Python 脚本 |
| 模型层 | 训练 YOLOv8 模型、评估指标、导出部署格式 | Ultralytics YOLOv8、ONNX、TensorRT |
| 应用层 | 打开图片或摄像头、调用模型推理、显示结果、保存日志 | PyQt5、OpenCV、QThread |
实际项目里,应用层比算法层更容易被低估。摄像头流不稳定、界面卡顿、模型路径找不到、日志没有落盘,这些都会让算法表现大打折扣。因此本文将算法链路和客户端工程放在同等重要的位置。
1.3 从视频帧到检测结果的完整链路
检测链路可以概括为:
采集图像或视频帧 -> 图像预处理 -> 模型推理 -> 后处理 -> 结果绘制与展示。
预处理阶段主要做尺寸调整、归一化和 letterbox。letterbox 的作用是把原始图像等比缩放后填充到模型输入尺寸,比如 640x640,避免因为目标被拉伸变形而降低检测精度。模型推理输出的是原始特征图上的预测信息,必须经过解码、置信度过滤、非极大值抑制,才能得到最终的边界框。非极大值抑制用来解决同一个目标被多个框重复预测的问题。
很多初学者直接调用model.predict(),看到结果正常就不关心链路,这会导致遇到精度下降或推理变慢时无从排查。把链路拆清楚后,至少知道问题出在预处理、模型还是后处理。
1.4 本文要实现的最小闭环
本文的最小闭环包含两部分:先训练一个安全帽检测模型,再用 PyQt5 做一个带界面的小程序。小程序支持选择本地图片检测,也支持打开摄像头实时检测。训练输出模型格式先用best.pt,方便快速验证;后续再根据性能需求导出 ONNX 或 TensorRT 格式。
整个流程跑通后,你可以在此基础上增加告警、数据库记录、多路摄像头切换等功能,但核心的“数据->训练->推理->展示”路径不会变化。
2. 环境准备与依赖安装
2.1 硬件与软件版本建议
训练阶段建议使用 NVIDIA GPU,因为 YOLOv8 的卷积操作在 GPU 上能明显提速。推理阶段如果只做图片演示,CPU 也可以接受,但实时视频检测会有明显卡顿。
| 环境项 | 建议配置 | 说明 |
|---|---|---|
| 操作系统 | Windows 10/11 或 Ubuntu 20.04 | 训练和部署尽量使用同一种环境,避免路径和编码差异 |
| Python | 3.9 或 3.10 | Ultralytics 对 Python 版本有依赖要求,3.10 兼容性较好 |
| GPU | NVIDIA 显卡,显存 8GB 以上 | 训练精度可选;显存不足时降低 batch 或 imgsz |
| CUDA 版本 | 11.8 或 12.1 | 要先确认显卡驱动支持,再选择 PyTorch 版本 |
| PyTorch | 2.0 以上 | 不同版本对 CUDA 的支持不同,需要搭配安装 |
| Ultralytics | 8.0 以上 | 训练和推理 API 在 8.x 版本中一致 |
| PyQt5 | 5.15 系列 | 用来开发桌面客户端 |
| OpenCV | 4.x | 图像读取和视频帧处理 |
这里给出的版本只是示例,实际安装前要查看当时最新的 PyTorch 和 Ultralytics 支持矩阵。不要直接复制网上过时的命令。
2.2 创建虚拟环境并安装依赖
推荐使用 conda 创建独立环境,防止不同项目之间的依赖冲突。下面的命令假设你已经安装并配置好 conda,并且显卡驱动版本可以支持 CUDA 11.8。
conda create -n helmet python=3.10 -y conda activate helmet pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics pip install pyqt5 pip install opencv-python如果只有 CPU 环境,torch的安装命令可以改成:
pip install torch torchvision torchaudio这里要注意:训练安全帽模型时,如果 GPU 显存较小,先不要安装 CPU 版 PyTorch,因为二者不可混用。先确认需要训练还是只做推理,再选择对应版本。
2.3 验证 YOLOv8 和 PyQt5 是否可用
安装完成后,用一段简单的 Python 脚本验证环境是否正常:
import torch print("torch:", torch.__version__) print("cuda available:", torch.cuda.is_available()) from ultralytics import YOLO model = YOLO("yolov8n.pt") print("yolo ok") import PyQt5.QtCore print("pyqt ok")第一次执行YOLO("yolov8n.pt")时,Ultralytics 会下载预训练权重,需要保持网络通畅。如果torch.cuda.is_available()返回False,即使装了 GPU 版 PyTorch,也说明 CUDA 驱动或版本匹配有问题。
2.4 环境安装最常见的坑
安装阶段最常见的问题有三个。
第一个是 PyTorch 与 CUDA 版本不匹配。现象是安装后torch.cuda.is_available()为False,或者运行时提示找不到 CUDA 动态库。解决思路是先通过nvidia-smi查看驱动支持的 CUDA 版本,再选择对应的 PyTorch 安装命令。
第二个是pip与conda混装导致包版本被覆盖。推荐在虚拟环境中统一使用pip,不要先conda install torch再用pip install ultralytics,否则容易出现动态链接库冲突。
第三个是没有安装pyqt5-sip,导致导入PyQt5报错。出现这种情况时,单独安装pyqt5-sip可以解决:
pip install pyqt5-sip注意:环境验证不只是看 import 是否成功,还要确认 PyTorch 能否正常使用 GPU。训练前先跑一个最小模型,能节省大量排错时间。
3. 准备安全帽数据集并转换为 YOLO 格式
3.1 类别定义与标注约定
安全帽佩戴识别的类别定义有多种方式。最简单的方案是只标注一个类别helmet,但这样无法区分“未佩戴安全帽”的状态。实际项目中更常见的方案是定义两个类别:
| 类别 ID | 名称 | 含义 |
|---|---|---|
| 0 | helmet | 戴在头上的安全帽,或独立的安全帽目标 |
| 1 | head | 没有佩戴安全帽的人头 |
在应用层,可以结合head和helmet的位置做判断:当helmet框覆盖了head框的大部分区域时,判定为已佩戴;否则为未佩戴。如果业务需要同时识别整个人体,可以增加person类,但类别越多,标注成本和训练难度越高。
标注时要统一两个原则:第一,一张图片中所有可见的相关目标都要标注,漏标会导致模型把未标注目标当成背景,产生误检;第二,遮挡严重的目标也尽量标注,安全帽在工地画面中经常被遮挡,完全不标注会让模型在推理阶段漏掉真实目标。
3.2 数据集目录结构
YOLO 格式对目录结构有严格要求。推荐使用下面的结构:
dataset/ ├── images/ │ ├── train/ │ │ ├── 0001.jpg │ │ ├── 0002.jpg │ └── val/ │ ├── 1001.jpg │ └── 1002.jpg ├── labels/ │ ├── train/ │ │ ├── 0001.txt │ │ ├── 0002.txt │ └── val/ │ ├── 1001.txt │ └── 1002.txt └── data.yaml图片和标签必须同名,且位于对应的 train 或 val 目录下。比如images/train/0001.jpg对应labels/train/0001.txt。
3.3 标注与格式转换
LabelImg 支持直接保存为 YOLO 格式,LabelMe 默认输出 JSON 文件,需要转换。YOLO 标签每一行表示一个目标,格式如下:
class_id x_center y_center width height需要注意,x_center、y_center、width、height都是相对图片宽高的归一化值,范围在 0 到 1 之间。例如一张图片宽 1920、高 1080,某个安全帽边界框的中心点坐标是(960, 540),宽高是(192, 216),对应的坐标应该写成:
0 0.5 0.5 0.1 0.2如果标注工具输出的是像素坐标,需要先转换再写入 txt 文件。转换脚本的核心逻辑如下:
import os # 像素坐标 x_min, y_min, x_max, y_max = 100, 120, 220, 280 img_w, img_h = 1920, 1080 # 转换为 YOLO 格式 box_w = x_max - x_min box_h = y_max - y_min x_center = x_min + box_w / 2 y_center = y_min + box_h / 2 x_center_norm = x_center / img_w y_center_norm = y_center / img_h box_w_norm = box_w / img_w box_h_norm = box_h / img_h line = f"0 {x_center_norm:.6f} {y_center_norm:.6f} {box_w_norm:.6f} {box_h_norm:.6f}\n"这段代码展示了转换思路,实际项目中建议使用批量脚本处理整个数据集,避免手工计算。
3.4 划分训练集和验证集
训练集和验证集的划分要保证类别分布一致,最简单的做法是随机划分,但需要设置随机种子,保证结果可复现。
import os import random import shutil random.seed(42) image_dir = "dataset/images" train_dir = "dataset/images/train" val_dir = "dataset/images/val" label_dir = "dataset/labels" train_label_dir = "dataset/labels/train" val_label_dir = "dataset/labels/val" all_images = [f for f in os.listdir(image_dir) if f.endswith(".jpg")] random.shuffle(all_images) val_ratio = 0.2 val_count = int(len(all_images) * val_ratio) for i, img_name in enumerate(all_images): base = os.path.splitext(img_name)[0] label_name = base + ".txt" if i < val_count: shutil.move(os.path.join(image_dir, img_name), os.path.join(val_dir, img_name)) shutil.move(os.path.join(label_dir, label_name), os.path.join(val_label_dir, label_name)) else: shutil.move(os.path.join(image_dir, img_name), os.path.join(train_dir, img_name)) shutil.move(os.path.join(label_dir, label_name), os.path.join(train_label_dir, label_name))实际项目中要注意:划分前先检查标签文件是否存在,避免只移动图片没有移动标签。还要检查训练集和验证集中每个类别的目标数量,防止某一类在验证集里完全没有样本,导致评估指标失真。
3.5 编写 data.yaml 配置文件
Ultralytics 使用data.yaml描述数据集路径和类别信息:
path: dataset train: images/train val: images/val nc: 2 names: 0: helmet 1: headpath可以写绝对路径,也可以写相对于当前工作目录的路径。训练时如果提示找不到图片,优先检查path是否正确。names的类别顺序必须和标注文件里的class_id一致,否则模型会把安全帽当成头,训练结果会非常混乱。
3.6 数据集阶段的常见坑
数据集阶段最致命的坑是图片和标签不同名,训练时大量图片没有标签。Ultralytics 会跳过没有标签的图片,但训练日志中不会明显报错,只有到验证阶段才会发现 mAP 异常低。
第二个常见坑是标注框坐标越界。某些标注工具生成的坐标可能超出图片范围,模型训练时会出现警告。可以在训练前写脚本统一裁剪到图片范围内。
第三个坑是类别不平衡。比如helmet样本有 8000 个,head样本只有 2000 个,模型会偏向预测helmet。处理方式不是简单增加训练轮数,而是补充更多head未戴帽样本,或者使用类别权重。
4. 训练 YOLOv8 安全帽检测模型
4.1 选择预训练模型与设置训练参数
YOLOv8 提供 n、s、m、l、x 几个不同规模的预训练模型。n 最小,适合快速验证和边缘设备;s 和 m 是安全帽检测项目的常见选择;l 和 x 精度更高,但对显存和推理时间要求也更高。
训练命令示例:
yolo detect train \ data=dataset/data.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0yolov8s.pt是预训练权重,以它为初始权重可以明显加快收敛,也能在小数据集上得到更稳定的效果。epochs表示最大训练轮数;imgsz是输入图片尺寸;batch是每次迭代处理的图片数量;device=0表示使用第一张 GPU。
4.2 关键训练参数说明
| 参数 | 默认值 | 含义 | 调整建议 |
|---|---|---|---|
| epochs | 100 | 最大训练轮数 | 数据量少时可缩短到 50;数据量大时适当增加 |
| imgsz | 640 | 训练输入尺寸 | 小目标多时提高到 960,但会显著增加显存占用 |
| batch | 16 | 批大小 | 显存不足时降低到 8 或 4 |
| lr0 | 0.01 | 初始学习率 | 使用预训练权重时可降低到 0.001 附近 |
| patience | 50 | 早停等待轮数 | 验证指标不再提升时停止训练,防止过拟合 |
| optimizer | auto | 优化器选择 | 默认自动选择,一般不需要改 |
| amp | True | 是否使用混合精度训练 | 显存不足时开启,可节省显存并提高速度 |
参数之间是联动的。imgsz=640和batch=16组成的训练显存占用,与imgsz=960、batch=16差别很大。如果训练时报 CUDA out of memory,先降低batch,再考虑降低imgsz。
4.3 启动训练并理解日志中的指标
启动训练后,终端会输出类似下面的信息:
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 1/100 6.2G 1.352 1.421 1.211 50 640 2/100 6.2G 1.102 1.203 1.052 45 640训练日志中,box_loss是边界框回归损失,cls_loss是分类损失,dfl_loss是分布式焦点损失。它们都不是越小越一定是好事,要结合验证集指标一起看。验证阶段更关注:
| 指标 | 含义 |
|---|---|
| precision | 预测为正样本中真正正确的比例 |
| recall | 真实目标中被正确检出的比例 |
| mAP50 | IoU 阈值为 0.5 时的平均精度 |
| mAP50-95 | IoU 阈值从 0.5 到 0.95 的平均精度,更严格 |
假设训练 100 轮后,mAP50达到 0.85,说明在 IoU 阈值为 0.5 的情况下,模型整体识别效果较好;如果mAP50-95明显偏低,说明模型的定位精度还不够精细。
训练完成后,结果保存在runs/detect/train/目录下,其中weights/best.pt是验证集上表现最好的权重,weights/last.pt是最后一轮的权重,results.png包含损失和指标曲线。
4.4 训练阶段常见坑与解决思路
第一个坑是不加区分地使用last.pt部署。如果训练后期发生过拟合,last.pt反而比best.pt更差。部署时默认选择best.pt。
第二个坑是显存不足。可以降低batch,但不要降到 1 后仍然不收敛。此时应优先考虑降低imgsz,或者使用yolov8n.pt做基线实验。
第三个坑是训练不收敛。原因可能是学习率设置不合适、数据标注明显错误、类别排列混乱。排查方法是先用少量样本训练 20 轮,看是否能过拟合。如果不能,优先检查数据和标注。
如果要在已有模型基础上继续训练,比如新增了一批困难样本,可以采用增量训练方式:
yolo detect train \ data=dataset/data.yaml \ model=runs/detect/train/weights/best.pt \ epochs=50 \ lr0=0.0005增量训练使用小学习率,让模型在新数据上微调,不要直接用默认学习率重新训练,否则容易破坏已经学到的特征。
4.5 导出模型用于部署
训练好的best.pt可以直接在 PyTorch 中使用,但如果要部署到更轻量的环境,可以导出为 ONNX 或 TensorRT 格式。
导出 ONNX:
yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640导出 TensorRT:
yolo export model=runs/detect/train/weights/best.pt format=engine imgsz=640 half=Truehalf=True会让模型使用半精度推理,降低显存占用和计算量,但前提是部署环境支持 GPU。导出前要确认使用场景,如果只在 CPU 上运行,ONNX 和 PyTorch 的.pt格式是更稳妥的选择。
注意:
best.pt只是训练产物,不是最终交付物。部署前一定要在真实测试集上重新验证导出后的模型,因为不同推理引擎对算子的支持程度不同,输出可能会有微小差异。
5. 用 PyQt5 搭建桌面识别客户端
5.1 客户端功能设计与线程模型
PyQt5 客户端的功能可以拆成几个按钮和区域:选择图片按钮、打开摄像头按钮、检测画面显示区域、状态信息栏。功能逻辑不复杂,但线程模型需要提前设计。
检测模型推理是一个耗时操作,如果直接在 PyQt5 的主线程里执行,界面会在推理期间无响应。尤其摄像头实时检测时,每一帧都要推理,UI 线程会被彻底阻塞。正确做法是把检测任务放到QThread子线程中,通过信号把结果传回主线程。
5.2 使用 QThread 封装推理任务
下面是一个简单的检测线程类:
import cv2 from PyQt5.QtCore import QThread, pyqtSignal from ultralytics import YOLO class DetectWorker(QThread): result_ready = pyqtSignal(object, object, float) error = pyqtSignal(str) def __init__(self, model_path, source, parent=None): super().__init__(parent) self.model = YOLO(model_path) self.source = source self.running = True def run(self): if isinstance(self.source, int): self.process_camera() else: self.process_image() def process_image(self): frame = cv2.imread(self.source) if frame is None: self.error.emit("无法读取图片") return img, results, spend = self.detect_frame(frame) self.result_ready.emit(img, results, spend) def process_camera(self): cap = cv2.VideoCapture(self.source, cv2.CAP_DSHOW) if not cap.isOpened(): self.error.emit("摄像头无法打开") return while self.running: ret, frame = cap.read() if not ret: break img, results, spend = self.detect_frame(frame) self.result_ready.emit(img, results, spend) cap.release() def detect_frame(self, frame): start = cv2.getTickCount() results = self.model(frame, conf=0.5, iou=0.45)[0] spend = (cv2.getTickCount() - start) / cv2.getTickFrequency() * 1000 annotated = results.plot() return annotated, results, spend def stop(self): self.running = False self.wait()这个类同时支持图片路径和摄像头设备号。source为整数时使用摄像头,为字符串路径时读取图片。running标志用于退出摄像头循环,关闭窗口时调用stop()可以避免线程崩溃。
5.3 图片检测与结果展示
在 PyQt5 主窗口中,选择图片后创建检测线程:
from PyQt5.QtWidgets import QFileDialog, QLabel, QPushButton, QVBoxLayout, QWidget from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import Qt import cv2 import numpy as np class MainWindow(QWidget): def __init__(self): super().__init__() self.layout = QVBoxLayout(self) self.label = QLabel("检测画面") self.label.setAlignment(Qt.AlignCenter) self.layout.addWidget(self.label) self.btn_image = QPushButton("选择图片") self.btn_camera = QPushButton("打开摄像头") self.layout.addWidget(self.btn_image) self.layout.addWidget(self.btn_camera) self.worker = None self.btn_image.clicked.connect(self.open_image) self.btn_camera.clicked.connect(self.open_camera) def open_image(self): path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "Images (*.jpg *.png *.bmp)") if not path: return self.worker = DetectWorker("best.pt", path) self.worker.result_ready.connect(self.show_result) self.worker.error.connect(self.show_error) self.worker.start() def show_result(self, frame, results, spend): rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) rgb = np.ascontiguousarray(rgb) h, w, ch = rgb.shape qimage = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) pixmap = QPixmap.fromImage(qimage) self.label.setPixmap(pixmap.scaled(self.label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) self.setWindowTitle(f"检测耗时: {spend:.1f} ms") def show_error(self, msg): self.label.setText(msg)这里展示的是核心逻辑。QImage构造时传入rgb.data,由于numpy数组在传递时可能会被回收,使用np.ascontiguousarray并转为 RGB 后再传给QPixmap,能降低内存访问异常的概率。
5.4 摄像头实时检测
摄像头实时检测和图片检测的差异主要在循环读取帧。DetectWorker中已经包含process_camera方法。在窗口中打开摄像头时,需要先停止之前的检测线程,防止并发冲突:
def open_camera(self): if self.worker is not None: self.worker.stop() self.worker = DetectWorker("best.pt", 0) self.worker.result_ready.connect(self.show_result) self.worker.error.connect(self.show_error) self.worker.start()在 Windows 上使用cv2.VideoCapture(0, cv2.CAP_DSHOW)比默认方式更容易打开摄像头,因为CAP_DSHOW会使用 DirectShow 后端,减少摄像头被占用时的等待时间。
5.5 界面布局与启动入口
完整的界面还可以增加保存检测结果、显示实时帧率、设置置信度阈值等控件。启动入口很简单:
import sys from PyQt5.QtWidgets import QApplication if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.resize(960, 640) window.show() sys.exit(app.exec_())最小界面已经能完成“打开图片 -> 检测 -> 显示结果”,但要注意:每次创建DetectWorker都会重新加载模型,图片检测场景可以接受,摄像头实时检测场景建议把模型加载放到初始化阶段,避免重复加载耗时。
6. 推理性能与浮点精度选型
6.1 浮点格式的基本概念
YOLOv8 训练时默认使用 32 位浮点数,也就是 FP32。FP32 精度高,但占用内存多、计算慢。推理时如果显卡支持低精度计算,把模型切换到 FP16、BF16 或 TF32,可以减少显存占用并提高速度,代价是可能带来轻微精度损失。
理解和选型浮点格式,是深度学习模型部署的常见考点,也是 YOLOv8 导出部署时最容易出问题的环节。
6.2 四种推理精度格式对比
| 格式 | 位宽 | 指数位 | 尾数位 | 特点 | 适用场景 |
|---|---|---|---|---|---|
| FP32 | 32 | 8 | 23 | 通用精度基准,兼容性最好 | 训练默认、CPU 推理、精度优先 |
| FP16 | 16 | 5 | 10 | 显存占用减半,速度更快,但动态范围小 | NVIDIA GPU 推理,支持 TensorCore 的环境 |
| BF16 | 16 | 8 | 7 | 动态范围与 FP32 相同,精度降低主要来自尾数减少 | 训练和推理均可,尤其适合大模型 |
| TF32 | 19 位有效运算 | 8 | 10 | Ampere 架构 GPU 的 TensorCore 加速模式,不是完整存储格式 | 在支持 TF32 的 GPU 上快速训练 |
FP16 的尾数位只有 10 位,数值范围比 FP32 小。当激活值过大或过小时,FP16 可能出现溢出,需要配合混合精度技术。BF16 保留了 8 位指数位,因此动态范围更广,但尾数位只有 7 位,数值精度比 FP16 还低,适用于对精度要求不高的任务。
6.3 在 YOLOv8 推理中切换精度
在 Ultralytics 中,推理时可以直接启用半精度:
from ultralytics import YOLO model = YOLO("best.pt") results = model.predict("test.jpg", half=True)half=True会尝试把模型和输入数据转为 FP16。这个操作只在 GPU 环境有意义,CPU 上 FP16 通常不会加速,反而可能因为转换开销变慢。
导出 ONNX 时也可以使用半精度:
yolo export model=best.pt format=onnx half=True不过 ONNX Runtime 在不同的执行提供程序上对 FP16 支持不同,导出后要实际运行一次,不能只凭导出成功判断可用。
6.4 结合部署场景选择合适的精度
| 部署场景 | 推荐精度 | 原因 |
|---|---|---|
| 训练阶段 | FP32 + AMP | 混合精度默认开启,节省显存,训练速度更快 |
| 服务端 GPU 推理 | FP16 或 TensorRT FP16 | NVIDIA GPU 上加速明显,显存占用低 |
| Windows 桌面端 CPU 推理 | FP32 | CPU 对 FP16 支持有限,FP32 更稳定 |
| 低算力嵌入式设备 | INT8 量化 | 需要校准数据,并验证 mAP 下降幅度 |
在安全帽检测项目中,如果客户端是普通办公电脑,CPU 推理使用 FP32 往往比 FP16 更稳定。如果客户端有 NVIDIA 显卡,再用 FP16 优化,不要盲从网上“半精度一定更快”的说法。
6.5 精度切换的常见坑
第一个坑是在 CPU 上执行half=True,结果检测慢或报错。发现环境只支持 CPU 时,直接使用默认 FP32。
第二个坑是 FP16 导出后精度下降很多。安全帽检测中,小目标对框回归精度比较敏感。如果mAP50-95下降超过可接受范围,可以用 INT8 量化加校准来优化,或者退回 FP32。
第三个坑是混淆 TF32 和 FP16。TF32 是 NVIDIA Ampere 架构上 TensorCore 对 FP32 运算的加速模式,它并不改变存储格式,而是在矩阵乘算时截断输入。PyTorch 中可以通过torch.backends.cuda.matmul.allow_tf32 = True控制,不是简单的模型精度转换。
7. 常见问题排查链路
7.1 训练时 loss 不下降
现象是训练日志中 loss 在很大范围内波动,或者一直居高不下。常见原因包括:数据集标注错误、学习率设置不当、训练集和验证集划分混乱。
排查顺序:
- 可视化一批训练图片,把标注框绘制出来,检查类别 ID 和坐标是否正确。
- 使用小数据集训练 20 轮,看模型能否过拟合。
- 把
lr0调低到0.0001,排除学习率过大的问题。 - 查看
results.png中损失曲线是否有下降趋势。
如果小数据集能过拟合,通常说明数据量或数据多样性不足,而不是模型结构问题。
7.2 检测准确率不足
现象是安全帽被漏检,或者把其他物品误检成安全帽。
常见原因和检查方式如下表:
| 问题现象 | 常见原因 | 检查方向 | 处理建议 |
|---|---|---|---|
| 小目标漏检 | 目标像素面积小 | 统计训练集中目标宽高分布 | 提高 imgsz,使用切片推理 |
| 远距离漏检 | 验证集与训练集分布差异大 | 观察错误样本的拍摄距离 | 增加对应距离的训练数据 |
| 误检率偏高 | 背景与安全帽颜色纹理相似 | 查看误检样本 | 增加负样本或使用更高置信度阈值 |
| 同一目标多个框 | NMS 阈值过低 | 查看后处理参数 | 适当提高 iou 阈值 |
排查时不要一上来就换大模型,先看错误样本集中在哪一类。常见的情况是某类样本太少,模型对该类别的 recall 很低,此时扩充样本比换模型更有效。
7.3 PyQt5 界面卡顿
现象是移动窗口、点按钮响应很慢。原因通常是推理操作放到了 UI 线程。排查时看 CPU 或 GPU 占用情况,如果在点击检测按钮后程序无响应,说明主线程被阻塞。
解决办法是把推理放到QThread中,同时控制摄像头处理帧率,不需要每帧都检测,可以设置为 10 FPS 或 15 FPS,避免连续推理导致画面延迟不断累积。还可以把检测结果显示优化为只