
简介面向驾驶安全监控与行为规范执行场景提供一套基于YOLO11与PyQt5的完整系统方案适合智能交通、车辆安全监控及计算机视觉方向的学生与工程师实战参考。资源覆盖从模型训练到界面展示的完整链路包含用于训练安全带识别模型的标注数据集与训练好的模型权重可直接用于摄像头或视频流的实时安全带状态检测PyQt5可视化界面支持实时画面展示与检测结果标注便于二次开发或快速部署验证。压缩包共2000个文件其中1999个XML为数据集标注信息1个PDF为具体使用步骤说明整体约328.4MB。已有190人浏览学习。借助该资源包可快速理解YOLO11目标检测模型在安全监控场景的落地流程掌握深度学习模型与PyQt5桌面界面集成的工程思路对驾驶行为规范管理等实际项目开发有直接帮助。1. 怎么把 yolo11 安全带检测从「一条命令出结果」做成「驾驶监控里能用的 PyQt5 界面」看到一个带「数据集 训练好的模型 PyQt5 可视化界面」三件套的 yolo11 安全带检测项目第一反应不应该是问精度多少而是问这套东西拆开能不能直接装到驾驶监控场景里用。yolo11 负责出检测框PyQt5 负责把检测结果变成操作者能看、能管、能回溯的界面数据集和训练好的模型则是中间那座桥。三块合起来才算是一个完整的驾驶安全监控与行为规范执行系统摄像头画面进来、界面实时显示、驾驶员没系安全带时触发报警并留存事件记录。这篇笔记按数据准备、模型训练、界面集成的顺序把每一步的命令、参数和常见坑写清楚。适合两种人一是拿到这类「检测 界面 数据」项目包想做二次开发的车载监控从业者二是想把 yolo11 检测模型真正做成可交付产品的算法工程师——后者最容易忽视界面和事件留存而这两块恰恰是项目能不能验收的关键。2. 数据集准备安全带检测样本从哪来、怎么整理成 yolo11 能直接吃的格式标题里「数据集」排在模型前面但很多人的实际操作是倒过来的——先急着训模型训完发现误报没法看才回头补数据。安全带检测的数据工作有它的特殊性目标是一条斜跨胸前的带子在 1080p 画面里只占三四十个像素而且正样本系了安全带远多于负样本没系。如果只拿现成驾驶数据集里随手抽的帧来训多半会在遮挡、夜间、深色衣服这些场景上翻车。2.1 数据来源与类别设计person 和 seatbelt 一起标别只标安全带常见的数据来源有两类公开的驾驶行为或司乘检测数据集里带安全带标签的图片以及自己从行车记录仪、车内监控视频里抽帧标注。公开数据集的优势是量大、场景多样但摄像头安装角度和你实际部署的位置往往对不上——你是从后视镜上方斜向下拍数据集里可能是正面视角这会导致迁移效果打折。我一般建议公开数据打底自采数据做补充自采视频按 1030 帧抽一帧避免相邻帧几乎一样让训练集「看起来很多、信息量很低」。类别设计是这里最关键的决策。很多人只标安全带一个类推理时靠「画面里有没有检测到安全带」来判断是否系了这在单人固定摄像头的场景下勉强能用但遇到空车、后排有人、多人同框就直接误报。正确的做法是把 person 也标进去。类别就两个person和seatbelt推理逻辑变成「每一个 person 框内有没有 seatbelt 框」没有就是未系。这样模型只学「安全带长什么样」和「人长什么样」不学「系没系」这种需要推理的语义误报率可控得多。2.2 用 labelme 标注安装参数和标注规范标注工具用 labelme注意 Python 版本。遇到过 labelme 装不上 PyQt5 的情况基本都是 Python 3.12 以上没有对应 wheel 导致用 3.10 建虚拟环境最省事。启动命令conda create -n label python3.10 -y conda activate label pip install labelme labelme --labels person,seatbelt --nodata images_raw--labels参数让标注下拉框只有 person 和 seatbelt 两个选项避免手输标签拼错--nodata让生成的 JSON 里不嵌入 base64 图像文件体积小很多转换脚本读起来也快。标注规范上person 框包住人的可见主体即可seatbelt 框要紧贴安全带斜带画四个点围成的矩形不要包住整个人。深色安全带在夜间画面里容易漏标宁可少标也不要标错位置错误标签比缺标签更伤模型。2.3 把 labelme 的 JSON 转成 YOLO txt转换脚本与坐标归一化labelme 输出的是 polygon 点集而 yolo11 要的是归一化矩形框中间必须写转换脚本。这一步是数据链路上最容易出 bug 的地方坐标归一化分母用错、类别 ID 对不上都会让训练 loss 直接起飞。这个脚本可以直接抄import json, os import cv2 from pathlib import Path LABEL_MAP {person: 0, seatbelt: 1} def labelme_to_yolo(json_path, img_dir, out_dir): with open(json_path, encodingutf-8) as f: data json.load(f) img_path os.path.join(img_dir, data[imagePath]) img cv2.imread(img_path) if img is None: print(f图片读不到跳过: {img_path}) return h, w img.shape[:2] lines [] for shape in data[shapes]: cls_id LABEL_MAP.get(shape[label]) if cls_id is None: continue pts shape[points] x_min min(p[0] for p in pts) y_min min(p[1] for p in pts) x_max max(p[0] for p in pts) y_max max(p[1] for p in pts) bw, bh x_max - x_min, y_max - y_min if bw 0 or bh 0: continue x_c (x_min bw / 2) / w y_c (y_min bh / 2) / h bw_n bw / w bh_n bh / h lines.append(f{cls_id} {x_c:.6f} {y_c:.6f} {bw_n:.6f} {bh_n:.6f}) if lines: txt_path os.path.join(out_dir, Path(data[imagePath]).stem .txt) with open(txt_path, w) as f: f.write(\n.join(lines) \n) print(f已生成: {txt_path})代码逻辑遍历 JSON 里的 shapes把 polygon 顶点取最小最大值围成矩形再用图像真实宽高做归一化。关键点是归一化分母必须是这张图自己的宽高不是 640 也不是数据集统一尺寸输出 txt 文件名必须和图像文件名一致只换后缀ultralytics 是靠这个对应关系找标签的。类别 ID 直接查LABEL_MAP遇到没定义的标签跳过防止标注时手滑多打了一个类名导致整份标签作废。跑完抽查几个 txt坐标都在 01 之间基本就没问题。2.4 训练集/验证集划分与标签分布检查转换完成后按 9:1 划分训练集和验证集注意用固定随机种子保证每次复现结果一致。划分脚本import glob, os, random, shutil random.seed(2025) imgs glob.glob(images_raw/*.jpg) glob.glob(images_raw/*.png) random.shuffle(imgs) split int(len(imgs) * 0.9) for i, img_path in enumerate(imgs): tag train if i split else val label_path img_path.replace(images_raw, labels_raw).replace(.jpg, .txt).replace(.png, .txt) if not os.path.exists(label_path): continue shutil.copy(img_path, fdataset/images/{tag}/) shutil.copy(label_path, fdataset/labels/{tag}/)划分后务必做一次分布检查统计每个类的框数写个脚本遍历 labels 目录数一数类别 0 和类别 1 的框数量。如果 seatbelt 框只有 person 框的零头说明正样本严重不足训练时模型会把「没系」当默认答案。这时候不是调模型而是回头补标数据或者对 seatbelt 样本做复制增强但复制的图片不能同时出现在 train 和 val 里否则验证集虚高。提示训练前跑一遍ls dataset/images/train | wc -l和ls dataset/labels/train | wc -l两个数字必须一致。这个三分钟检查能省掉后面排查「模型为什么啥也没学到」的几小时。3. 用 yolo11 训安全带检测Windows 装环境的三步和三组必调参数数据准备妥当后进入训练环节。网上搜 yolo11 环境配置会看到一大堆帖子但核心其实就一句话先把 CUDA 版 torch 装对再装 ultralytics顺序颠倒就会在 Windows 上翻车。你如果看过 yolo11 网络结构图会发现它的 backbone 和 neck 相比 v8 做了轻量化调整但训练命令、数据集格式和接口跟 v8 完全一致所以 v8 时代的经验可以直接搬过来。3.1 Windows 安装 yolo11先 torch 后 ultralytics顺序不能反直接pip install ultralytics不是不行但这样装上来的是 CPU 版 torch训练速度慢到没法接受。正确顺序是先装 CUDA 版 torch再装 ultralytics最后验证 CUDA 是否真的可用conda create -n seatbelt python3.10 -y conda activate seatbelt pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install ultralytics python -c import torch; print(torch.cuda.is_available())最后一条命令打印True才是真的用上显卡打印False就先去查显卡驱动支不支持对应 CUDA 版本别急着重装。cu121 这个版本对应 CUDA 12.1在 30 系、40 系显卡上兼容性都不错如果是 20 系以下老卡换成 cu118 更稳。默认 pip 源下载慢的话换清华镜像源加-i https://pypi.tuna.tsinghua.edu.cn/simple即可。Windows 安装 yolo11 的另一个坑是 conda 环境名和项目路径里别带中文和空格ultralytics 在 Windows 下对中文路径的支持一直不省心。3.2 训练命令与 data.yamlyolo11s 起步先跑通再谈精度数据目录按 ultralytics 约定的结构放好dataset/images/train、dataset/images/val、dataset/labels/train、dataset/labels/val然后写 data.yaml# dataset/data.yaml path: dataset train: images/train val: images/val nc: 2 names: 0: person 1: seatbeltpath是相对当前运行目录的数据集根路径train和val是相对path的图片目录ultralytics 会自动在同级找到 labels 目录。训练命令yolo train datadataset/data.yaml \ modelyolo11s.pt \ epochs100 imgsz640 batch16 \ patience20 projectruns/detect nameseatbelt_v1modelyolo11s.pt是官方预训练权重第一次运行会自动下载epochs100配patience20表示验证集指标连续 20 个 epoch 不提升就早停imgsz640是输入分辨率batch16按显存来定。训练完成后标题里说的「训练好的模型」就在runs/detect/seatbelt_v1/weights/best.pt这个文件后面 PyQt5 界面要直接加载。3.3 四个必调参数imgsz、mosaic、patience、batch 在安全带场景怎么取值安全带检测的目标尺寸偏小参数不能照搬 COCO 的默认配置。下面这张表是我在类似项目里的经验取值参数建议取值说明imgsz640 起步960 精调分辨率翻倍显存占用约 2.25 倍小目标召回明显提升batch16OOM 先降 8/4显存不够优先降 batch别降 imgszepochs100 配 patience20安全带类别单一收敛快设太大容易过拟合mosaic0.5close_mosaic10拼接增强对小目标有效但最后 10 个 epoch 要关闭imgsz是最值得调的参数。安全带在 1080p 画面里宽度只有三四十像素640 分辨率下会被压缩到十几像素漏检是必然的先跑 640 拿 baseline再用 960 精调通常 mAP 能涨一截。代价是显存和训练时间batch 16 在 12G 显存下 960 分辨率就跑不动了需要降到 8。mosaic是另一个安全带场景特有的坑拼接增强会把目标切到图块边缘安全带这种细长条被切断的概率很高设 0.5 并配合close_mosaic10让最后 10 个 epoch 用真实完整图稳定收敛。如果训完 mAP 还是上不去优先回数据补夜间和遮挡样本而不是换 yolo11x——安全带检测翻车大多翻在数据分布不在模型容量。4. 用 PyQt5 把训练好的模型装进驾驶监控界面线程、报警与事件留存PyQt5 界面设计本身不复杂网上模板一抓一大把但把摄像头帧接进去不卡、把报警做成交付级功能的人不多。核心在于数据流模型推理和界面刷新必须分线程否则点一下「开始监控」界面立刻假死给你看。4.1 界面搭什么四个区域和对应控件驾驶安全监控界面按功能拆四个区域每个区域职责单一区域核心控件职责视频显示区QLabel实时显示检测帧画好框再转 QImage状态区QLabel QLCDNumber显示当前是否系带、检测置信度报警控制区QPushButton QLabel触发报警后的停止/复位操作事件日志区QTableWidget展示时间、结果、置信度并导出 CSV界面控件是死的数据流是活的。报警状态和日志不能写在 UI 刷新函数里而是由推理线程通过信号发出来主线程只负责收信号更新界面。这条规矩守住界面卡死的问题就解决了一半。4.2 QThread 跑推理模型只加载一次信号回传结果推理线程用 QThread 实现模型在__init__里加载一次run()里循环读帧、预测、发信号# inference_thread.py import time import cv2 from PyQt5.QtCore import QThread, pyqtSignal from ultralytics import YOLO class InferenceThread(QThread): frame_ready pyqtSignal(object, list) # 帧 检测结果列表 alarm_triggered pyqtSignal(dict) # 报警事件 def __init__(self, model_path, source0, conf0.25, parentNone): super().__init__(parent) self.model YOLO(model_path) # 模型只加载一次 self.cap cv2.VideoCapture(source, cv2.CAP_DSHOW if source 0 else 0) self.conf conf self.running True self.no_belt_count 0 def run(self): while self.running and self.cap.isOpened(): ok, frame self.cap.read() if not ok: break results self.model.predict(frame, imgsz640, confself.conf, verboseFalse)[0] dets [] for b in results.boxes: x1, y1, x2, y2 map(int, b.xyxy[0].tolist()) conf float(b.conf[0]) cls int(b.cls[0]) dets.append((x1, y1, x2, y2, conf, cls)) persons [d for d in dets if d[5] 0] belts [d for d in dets if d[5] 1] has_no_belt_person False for px1, py1, px2, py2, _, _ in persons: has_belt False for bx1, by1, bx2, by2, _, _ in belts: bcx, bcy (bx1 bx2) / 2, (by1 by2) / 2 if px1 bcx px2 and py1 bcy py2: has_belt True break if not has_belt: has_no_belt_person True self.frame_ready.emit(frame, dets) if has_no_belt_person: self.no_belt_count 1 else: self.no_belt_count 0 if self.no_belt_count 5: # 连续5帧未系才报警 self.alarm_triggered.emit({ time: time.strftime(%Y-%m-%d %H:%M:%S), person_count: len(persons) }) self.no_belt_count 0 time.sleep(3) # 报警后冷却防止刷屏 time.sleep(0.03)代码逻辑先把预测结果拆成 person 和 belt 两组然后遍历每个人判断有没有安全带框的中心点落在这个人的框内。注意这里用「中心点落在框内」而不是 IoU因为安全带是细长条IoU 计算容易受框大小影响中心点判断更稳定。no_belt_count是连续帧计数器连续 5 帧出现「有人无带」才触发报警单帧误检会被过滤掉。报警后sleep(3)做冷却避免同一违规行为几秒钟报一次。frame_ready信号把原始帧和检测结果一起发出去画框放在主线程做不在推理线程里画减少线程里的工作量。主窗口这边接收信号更新界面重点在关闭窗口时的清理class MainWindow(QMainWindow): def __init__(self): super().__init__() self.thread None self.last_frame None def start_monitor(self): self.thread InferenceThread(runs/detect/seatbelt_v1/weights/best.pt, source0) self.thread.frame_ready.connect(self.update_view) self.thread.alarm_triggered.connect(self.handle_alarm) self.thread.start() def update_view(self, frame, dets): self.last_frame frame.copy() for x1, y1, x2, y2, conf, cls in dets: color (0, 255, 0) if cls 1 else (255, 0, 0) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) qt_img QImage(rgb.data, rgb.shape[1], rgb.shape[0], QImage.Format_RGB888).copy() self.video_label.setPixmap(QPixmap.fromImage(qt_img)) def closeEvent(self, event): if self.thread: self.thread.running False self.thread.wait(2000) event.accept()QImage(...).copy()必须加不加的话rgb的缓冲区一旦被回收界面显示就会花屏。closeEvent里先置running False再wait(2000)等线程退出——这个顺序反过来窗口关闭时会直接报QThread: Destroyed while thread is still running程序崩溃。如果你在 PyCharm 里跑这个程序发现 print 看不到输出记得勾选运行配置里的 Emulate terminal in output console这是 PyQt5 程序在 PyCharm 下的老问题。4.3 行为规范执行怎么落地报警阈值、事件日志与截屏留存报警触发后光在界面上闪红灯是不够的「行为规范执行」需要可追溯的证据链。每次报警要落一条事件日志同时存一张当时的截图。日志用 CSV 最稳后续要出报表也方便def handle_alarm(self, info): self.status_label.setText(未系安全带) snapshot falarm_{info[time].replace(:, ).replace( , _)}.jpg cv2.imwrite(snapshot, self.last_frame) with open(events.csv, a, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([info[time], info[person_count], snapshot]) row self.log_table.rowCount() self.log_table.insertRow(0) self.log_table.setItem(0, 0, QTableWidgetItem(info[time])) self.log_table.setItem(0, 1, QTableWidgetItem(snapshot))日志字段至少包含时间、人员数量、截图路径这样回查时能还原当时的画面。有人会把事件报表用 PyQt5 的 QTextBrowser 渲染成 HTML 表格展示周报也是一种可行做法但 CSV 落盘是底线界面只是展示层数据必须独立存在。报警后的复位逻辑也要想清楚我一般做成「报警保持 10 秒操作员确认后才复位」而不是自动恢复避免违规事件被快速刷过去。提示连续帧计数和报警冷却时长应该开放成界面可配置的参数不同车队对「持续多久算违规」的容忍度不一样写死在代码里后面改需求很被动。5. 安全带检测项目的常见坑环境、数据、界面五条排查记录这个项目从环境到推理链路长每段都有各自的坑。下面是五条血泪经验按现象、原因、解决的顺序写遇到问题直接对号入座。5.1 labelme 装不上PyQt5 报错先查 Python 版本别跟 pip 死磕现象pip install labelme执行到一半PyQt5 编译报错或者直接超时装完后运行 labelme 闪退。原因Python 3.12 以上版本对老版本 PyQt5 没有预编译 wheelpip 会尝试源码编译在 Windows 上大概率失败另一个常见原因是默认源下载太慢文件损坏。解决换 Python 3.10 建独立环境再装pip 换清华镜像源后重装pyqt5和labelme。如果这两招还不行直接用 LabelStudio 网页版替代本地 labelme标注效率和稳定性都比本地工具好这个坑不值得耗时间。5.2 训练 loss 不降、mAP 恒为 0先查 labels 目录和 txt 坐标现象训练进度条正常走但每个 epoch 的检测指标全是 0loss 纹丝不动。原因最常见的是 labels 目录结构不对——图片在images/train标签却被转换脚本写到了别处或者 txt 文件名和图片名对不上再就是坐标转换时除错了分母坐标值超过 1ultralytics 直接当无效标签丢弃。解决训练前跑一个对照检查先数文件数ls dataset/images/train | wc -l ls dataset/labels/train | wc -l两个数字必须一致。再抽查一个 txt 内容确认每行是类别 x_center y_center width height五个数值且后四个都在 01 之间。这个检查只要三分钟我每次换数据集都先跑一遍从没落空。5.3 关窗口报 QThread: Destroyed while thread is still running现象点击窗口关闭按钮界面关了控制台报错进程不退出或者直接崩溃。原因窗口销毁时推理线程还在跑循环Qt 的 C 线程对象先被释放线程本体却还在访问它。解决重写closeEvent先置running False让循环退出再thread.wait(2000)等线程真正停掉最后才event.accept()。这个坑几乎是 PyQt5 多线程程序的必考题第一次做界面的人十有八九会踩。5.4 换机加载 best.pt 报 Ultralytics 版本不匹配现象在训练机上跑得好好的把 best.pt 拷到另一台电脑加载时报错提示训练时的 ultralytics 版本与当前环境不一致。原因.pt文件里存了训练环境的 ultralytics 版本和模型配置部署机和训练机版本差太多时会拒绝加载。解决在部署机上固定版本安装pip install ultralytics训练机版本号或者把版本号写进 requirements.txt 一起交付。更好的做法是直接yolo export modelbest.pt formatonnx导出 ONNX用 ONNX Runtime 部署彻底绕开.pt的版本兼容问题代价是 ONNX 输出后处理要自己写一点代码。5.5 摄像头打不开或画面全黑Windows 下 OpenCV 的采集后端问题现象cap.isOpened()返回 False或者打开后预览画面是黑的、只有声音没有画面。原因OpenCV 在 Windows 默认用的 Media Foundation 采集后端和很多 USB 摄像头兼容性差初始化失败但没抛异常。解决创建 VideoCapture 时强制指定 DirectShow 后端cv2.VideoCapture(0, cv2.CAP_DSHOW)再不行手动设分辨率cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)和帧率cap.set(cv2.CAP_PROP_FPS, 25)有些摄像头默认分辨率下带宽不足降低到 720p 就正常了。这个参数建议在界面上做成下拉选项不同现场换摄像头是常事。6. 让模型更耐用把精度焦虑换成回归测试模型训完、界面跑通很多人就开始纠结要不要换更大的模型、加改进模块。我的建议是先别动结构把报警逻辑里的双阈值策略定下来然后用一段固定视频做回归测试这比任何调参都管用。双阈值的思路是界面画框的检测阈值放低报警阈值放高。画框用 0.25保证不遗漏可疑目标触发「未系安全带」报警用 0.6过滤掉低置信度的误报。很多人只调一个 conf 参数结果两个目标打架——调低误报满天飞调高漏检没人敢用。判断代码很简单DET_CONF 0.25 # 画框阈值低一点不漏 ALARM_CONF 0.6 # 报警阈值高一点少误报 belts [d for d in dets if d[5] 1 and d[4] ALARM_CONF]回归测试的具体做法录三段固定视频白天、夜间、逆光各三分钟每段视频从头到尾跑一遍推理输出报警事件表。改任何参数、换任何模型都拿这三段视频重跑对比报警事件的数量和时间点。不要用肉眼看几帧就下结论眼睛会被当前画面带偏觉得「效果还行」结果第二天换段视频全是误报。这个回归脚本值得花半天时间写好它是整个项目最便宜的后悔药。我自己的教训是曾经花了两周调 yolo11 的改进模块最后发现夜间漏检的根本原因不是模型结构是训练数据里几乎没有夜间样本。从那以后每次项目都是先把数据分布和回归测试跑透再谈模型改动。安全带的检测精度从来不是单靠模型结构撑起来的是数据、阈值、测试方法一起撑起来的。希望帮到你。本文还有配套的精品资源点击获取