YOLOv5+PyQt5实战:构建疲劳驾驶检测系统的完整指南
简介基于YOLOv5与PyQt5的疲劳危险驾驶行为检测完整工程主要面向正在做毕业设计或课程项目的计算机专业学生以及需要实战练习的开发者可用于实时检测打哈欠、闭眼等疲劳状态。压缩包共58个文件涵盖20个Python源码、18个YAML配置、13个编译缓存pyc、1个pt模型权重等程序结构按检测逻辑、界面、工具模块分层89MB的包体便于快速部署。该项目包含训练好的最佳模型best.pt与人脸关键点检测数据、PyQt5图形界面mainwindow.ui及对应py文件关键代码附详细注释配合使用说明即可直接启动。目前已有918人学习下载项目经过严格调试不仅能作为毕业设计完整方案也适合在此基础上扩展驾驶行为分析功能。1. 基于 yolov5PyQt5 的疲劳危险驾驶检测这套工程到底在解决什么问题疲劳驾驶导致的交通事故占比常年居高不下车内摄像头又恰好能拍到驾驶员正脸用视觉判断疲劳状态就成了 CV 落地的高频场景。标题里的压缩包拆开看是一条完整检测闭环yolov5 负责找出眼睛、嘴巴这类小目标训练好的 best.pt 提供开箱即用的检测能力PyQt5 把推理结果落成带视频画面、状态面板和分级报警的 GUI。对刚入门目标检测的人这是从训练到界面最短的完整路径对做毕设或车载小系统的人它又是一套能换数据集重训、改判定规则再打包分发的工程基座。下面按训练、界面、检测链路、疲劳判定、打包排错五个环节讲每段给出可复现的命令、代码和参数并说明改什么、为什么。2. yolov5 环境配置与训练自己的疲劳检测数据集2.1 yolov5 环境配置CPU 与 GPU 的差异入门最常见的卡点就是环境配置。yolov5 的核心依赖是 PyTorch、opencv-python、numpy其余由 requirements.txt 统一安装。PyTorch 建议 1.8 以上2.x 可用关键是按机器选轮子有 N 卡装对应 CUDA 版本没有独立显卡就装 CPU 版。用 GPU 版 torch 跑纯 CPU 机器import 阶段就会报 DLL load failed。推荐先装 PyTorch 再装 yolov5 依赖避免 torch 被不带 CUDA 的版本覆盖。# 无独显机器先装 CPU 版 PyTorch pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 再补齐 yolov5 仓库依赖 cd yolov5 pip install -r requirements.txt装完在仓库根目录执行python train.py --help不报错环境配置就完成了。之后是模型档位选择。yolov5 提供 n/s/m/l/x 五档疲劳驾驶检测里用得最多的是 yolov5s权重约 14MB显存 4GB 以上就能训精度与速度的平衡点刚好落在监控场景。yolov5n 更轻适合 Jetson 这类边缘设备但眼睛和嘴巴在 640×640 画面里只占几十个像素属于典型小目标模型过轻会让召回率明显下滑。档位参数量权重体积CPU 推理耗时(640×640)适用场景yolov5n1.9M约 4MB30~60ms边缘设备实时检测yolov5s7.2M约 14MB60~120ms本工程默认选型yolov5m21.2M约 40MB120~200ms离线视频分析耗时是相对值取决于 CPU 型号与是否开半精度表格的意义在于确认多数人应从 yolov5s 起步。2.2 yolov5 网络结构Backbone、Neck、Head 与多尺度输出很多人在训练命令里改参数却说不清改的是网络哪一段。yolov5 网络结构分三段Backbone 用 CSPDarknet 提取特征C3 模块把特征通道拆成两条路径再拼接保留梯度信息的同时压低计算量Neck 用 FPNPAN 做特征融合FPN 把深层语义自顶向下传给浅层PAN 再把浅层位置自底向上送回深层Head 是 8 倍、16 倍、32 倍三个步长的检测头大特征图负责小目标小特征图负责大目标。这套结构和疲劳检测的关系很直接眼睛在 640×640 输入下大约只有 10×20 像素经 32 倍下采样后剩下不到一个网格基本不可检小目标完全依赖 stride 8 的检测头。训练时如果发现 eye_close 的召回率明显低于嘴巴先别急着加数据把--img从 640 提到 704 或 768眼睛能多分到几个像素召回往往立刻改善代价是每个 batch 显存占用变大batch 要相应调小。这也是 yolov5 训练自己的数据集时最值得先试的超参数之一。2.3 标注与数据集组织训练自己的疲劳数据集疲劳检测的类别定义和普通目标检测不同要识别的是眼睛和嘴巴的状态。常见做法定四类eye_open、eye_close、mouth_open、mouth_close要判断目标归属时再加一个 face 类。标注工具用 LabelImg导出 YOLO 格式每张图生成同名 txt每行是 class 加归一化的中心坐标和宽高。标注有一条纪律睁眼框和闭眼框的尺寸和位置要对齐只框眼裂区域把眉毛框进去会让分类模型学歪半闭眼状态单独归到 eye_close它是疲劳报警最关键的样本。单人场景每类 500 张起步一图多人时所有个体都要标全漏标等于给模型喂错误样本。数据集目录固定为 images 与 labels 两个分支下的 train/val 子目录fatigue_data/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/对应的数据配置 fatigue.yaml 如下# 训练集与验证集路径建议写绝对路径 train: /home/user/fatigue_data/images/train val: /home/user/fatigue_data/images/val # 类别数量与名称顺序必须和标注 txt 的类别编号一致 nc: 4 names: [eye_open, eye_close, mouth_open, mouth_close]yaml 是 train.py 读取数据集的唯一入口。nc 填错会直接中断训练names 的顺序一旦和标注不一致模型会把睁眼当闭眼训。拿到陌生数据集先打印几条 txt 看类别 id 再填 yaml能省一整轮重训。2.4 训练命令与关键超参数在 yolov5 仓库根目录执行python train.py \ --img 640 \ # 训练输入分辨率 --batch 16 \ # 单卡每轮样本数8GB 显存可用 16 --epochs 100 \ # 总训练轮数 --data fatigue.yaml \ # 数据集配置 --weights yolov5s.pt \ # 预训练权重迁移学习起点 --cache # 图片缓存进内存加快小数据集训练参数逐个说--img 640 是精度和显存的平衡点显存 6GB 以下降到 512 或把 batch 减半--batch 16 在 8GB 卡上合适batch 太小 BatchNorm 统计量不稳太大直接 OOM--weights 必须给预训练权重做迁移学习空串从头训练收敛极慢小数据集很难训出可用模型--cache 适合几千张的数据集能省一半训练时间。100 轮配合--patience 30做早停验证集连续 30 轮不提升就提前结束。超参数里值得单独说的还有 --mosaic。Mosaic 数据增强把四张图拼一张对小目标的提升明显但它属于拼图变换训练后期如果 val 指标停滞可以把 mosaic 关掉再续训几十轮让模型适应真实图片的分布。学习率 lr0 保持默认 0.01迁移学习下动它收益很小。2.4.1 训练产物的检查训练结束看 runs/train/exp/ 下的 results.png 和 confusion_matrix.png不要只盯准确率。results.png 里 val/cls_loss 训到后段还在明显下降说明 epoch 给少了或早停设太早混淆矩阵里 eye_close 被误分成 eye_open 的比例超过 10%优先检查半闭眼标注是否归错类而不是急着堆数据。weights 目录下 best.pt 是验证集指标最好的权重后续 GUI 只加载它last.pt 是最后一轮权重best 过拟合时可回退对比。3. pyqt5 安装、界面设计与视频显示3.1 pyqt5 安装与 PyCharm 配置GUI 部分第一步是 pyqt5 安装pip install pyqt5 pyqt5-tools。pyqt5 是运行库pyqt5-tools 提供 Qt Designer 可视化设计器。Python 3.10 以上建议装新版 PyQt55.15.7 之后老版本在新解释器上容易遇到模块缺失。装完把环境 Scripts 目录下的 designer.exe 在 PyCharm 里配成外部工具就能直接拖控件搭界面保存成 .ui 后用pyuic5 mainwindow.ui -o mainwindow_ui.py转成 Python 代码。这是 pyqt5 界面设计效率最高的工作流布局用 Designer业务逻辑手写。验证环境是否就绪# pip install pyqt5 之后先跑这个小脚本 from PyQt5.QtWidgets import QApplication, QMainWindow import sys app QApplication(sys.argv) win QMainWindow() win.setWindowTitle(test) win.show() sys.exit(app.exec_())弹出窗口即为 pyqt5 安装成功后续界面都从 QMainWindow 继承扩展。3.2 pyqt5 界面设计驾驶监测面板的排版疲劳检测 GUI 的布局基本是固定三段式左侧占三分之二的实时视频区右侧状态信息区底部操作按钮区。视频区用 QLabel 承载状态区实时刷新睁眼/闭眼统计、疲劳等级、FPS按钮区放打开摄像头、打开视频、开始检测、停止。手写布局比 Designer 更可控核心代码# main_window.py 界面结构 from PyQt5.QtWidgets import (QMainWindow, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QWidget) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(疲劳驾驶检测系统) # 左侧视频显示区域 self.video_label QLabel(等待视频输入...) self.video_label.setMinimumSize(640, 480) self.video_label.setStyleSheet(background:#202020;color:white;) # 右侧状态面板 self.status_label QLabel(状态正常) self.fps_label QLabel(FPS0) right_panel QVBoxLayout() right_panel.addWidget(self.status_label) right_panel.addWidget(self.fps_label) right_panel.addStretch(1) # 状态控件推到顶部底部留白 # 底部按钮 self.btn_camera QPushButton(打开摄像头) self.btn_video QPushButton(打开视频) self.btn_stop QPushButton(停止检测) bottom QHBoxLayout() bottom.addWidget(self.btn_camera) bottom.addWidget(self.btn_video) bottom.addWidget(self.btn_stop) # 组合左列放视频和按钮右侧状态独立 left_col QVBoxLayout() left_col.addWidget(self.video_label) left_col.addLayout(bottom) main_layout QHBoxLayout() main_layout.addLayout(left_col, stretch3) main_layout.addLayout(right_panel, stretch1) root QWidget() root.setLayout(main_layout) self.setCentralWidget(root)两个参数值得说明addLayout 里的 stretch 是伸缩系数3:1 让视频区横向宽度是状态区的三倍addStretch(1) 占满剩余垂直空间。右侧标签之后只调 setText不要动态增删控件动态增删是界面卡顿和内存泄漏的高发区。3.3 视频流采集为什么不能在 UI 线程里读摄像头新手最常见的写法是 while 循环里直接 cv2.VideoCapture(0).read()结果窗口一打开就假死。原因在于 cap.read() 是阻塞调用USB 摄像头单帧要 30~50msQt 主线程的事件循环被 while 卡住后重绘、点击全部失效。三种方案的取舍方案优点缺点QTimer 轮询全局帧变量代码最少采集阻塞仍影响刷新threading.Thread 信号轻量灵活线程退出要自己管理标志位QThread 子类采集生命周期清晰配合 pyqtSignal 最顺手需处理好退出标志本工程采用第三种采集线程实现如下# video_worker.py 摄像头/视频采集线程 import cv2, time from PyQt5.QtCore import QThread, pyqtSignal class VideoWorker(QThread): frame_ready pyqtSignal(object) # 每帧采集完成后发信号 def __init__(self, src0): super().__init__() self.src src # 摄像头索引或视频文件路径 self.running True # 退出标志主线程可随时置 False def run(self): cap cv2.VideoCapture(self.src) if not cap.isOpened(): self.frame_ready.emit(None) # 通知主线程摄像头不可用 return while self.running: ret, frame cap.read() if not ret: break # 视频播放完或摄像头断连 self.frame_ready.emit(frame) # 信号跨线程传数组是安全的 time.sleep(0.01) # 限速防止采集线程占满 CPU cap.release()设计要点pyqtSignal(object) 默认走队列连接跨线程传 numpy 数组没有线程安全问题退出用 running 标志加 break不要调用 terminate()强制终止会让摄像头资源来不及释放下次打开大概率失败。3.4 从 cv2.Mat 到 QPixmap 的类型转换OpenCV 读出的帧是 BGR 顺序的 numpy 数组QLabel 只接受 QPixmap中间要过一道 QImagedef cv2_to_qpixmap(frame): rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # BGR 转 RGB否则偏色 h, w, ch rgb.shape bytes_per_line ch * w # 每行字节数必须显式给出 qt_img QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888) return QPixmap.fromImage(qt_img)最容易踩的坑是 bytes_per_lineQImage 默认按 4 字节对齐解析数据行宽高不满足对齐条件时画面会斜切或花屏显式传 ch*w 就能消除。Format_RGB888 对应三通道彩色图灰度图要换成 Format_Grayscale8。每次转换拷贝一次内存640×480 的帧开销约 0.5ms实时显示可接受。颜色顺序错了会看到红蓝互换的怪画面这是 GUI 显示项目里出现频率最高的低级错误。4. 加载本地 yolov5 模型与实时疲劳检测链路4.1 加载本地模型 best.pt两种离线推理方式训练完的权重要脱离训练脚本单独用最省事的是 torch.hub 本地仓库模式。只要当前工作目录在 yolov5 仓库根目录内sourcelocal 就能完全离线加载import torch # 从本地仓库加载自定义权重path 指向训练产物 model torch.hub.load(., custom, pathruns/train/exp/weights/best.pt, sourcelocal) model.conf 0.35 # 置信度阈值疲劳检测建议放宽到 0.35 model.iou 0.45 # NMS 的 IoU 阈值torch.hub.load 的四个关键参数第一个 . 表示从当前目录的仓库加载custom 表示加载自定义 .pt 而不是官方预训练权重path 指向权重文件source 固定 local 才是离线模式。加载完成后 model.conf 和 model.iou 直接控制推理阶段的过滤和后处理比在业务代码里手动调 NMS 简单得多。另一种方式是导出 ONNXpython export.py --weights best.pt --include onnx运行时用 onnxruntime 推理不再依赖 PyTorch打包体积能小一半以上但需要对网络输出层做解码一般留到打包阶段再切换。4.2 前处理、推理与结果解析模型输入要求 RGB 和 640×640 尺寸摄像头给的是 BGR 且宽高比不固定直接 resize 会让目标变形。torch.hub 返回的 AutoShape 模型内部封装了 letterbox 填充、色彩转换和 NMS业务侧只需要两行def detect_frame(model, frame): results model(frame) # 内部完成 resize、填充、NMS # xyxy[0] 是当前帧的所有检测框转 numpy 方便遍历 return results.xyxy[0].cpu().numpy()results.xyxy[0] 是形状为 N×6 的数组每行是 [x1, y1, x2, y2, confidence, class_id]坐标已经是原图尺度不需要手工缩放。class_id 对应 fatigue.yaml 里 names 的下标0 是 eye_open、1 是 eye_close、2 是 mouth_open、3 是 mouth_close。画框直接 cv2.rectangle按类别给不同颜色闭眼和张嘴用红色、正常状态用绿色视觉上第一时间能看出异常目标。注意 model() 不会修改传入的 frame画框时确保操作的是自己的数据。4.3 采集与推理合一的工作线程采集线程只传原始帧、主线程做推理会导致界面刷新被推理时间拖住。更干净的划分是把采集和推理都放进工作线程主线程只接收已经画好框的结果帧和结构化统计。这也是此类工程中代码注释最密集的部分# detect_worker.py 采集 推理 画框 class DetectWorker(QThread): frame_ready pyqtSignal(object) # 画框完成的帧 result_ready pyqtSignal(dict) # 该帧的类别统计 def __init__(self, model_path, src0): super().__init__() # 模型只加载一次放在 __init__ 而不是 run 里否则每开一次检测卡几秒 self.model torch.hub.load(., custom, pathmodel_path, sourcelocal) self.src src self.running True def run(self): cap cv2.VideoCapture(self.src) while self.running: ret, frame cap.read() if not ret: break frame, counts self.detect_and_draw(frame) self.frame_ready.emit(frame) self.result_ready.emit(counts) cap.release() def detect_and_draw(self, frame): # 类别统计初始化必须包含全部四个类别 counts {eye_open: 0, eye_close: 0, mouth_open: 0, mouth_close: 0} boxes self.model(frame).xyxy[0].cpu().numpy() for x1, y1, x2, y2, conf, cls in boxes: name self.model.names[int(cls)] # 从模型取类别名避免硬编码 counts[name] 1 color (0, 0, 255) if name in (eye_close, mouth_open) else (0, 255, 0) cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) cv2.putText(frame, f{name} {conf:.2f}, (int(x1), int(y1) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) return frame, counts多线程下的三个细节模型加载放init是为了避免每次开始检测都重新读权重self.model.names 存的是权重自带的类别名映射不要硬编码类别表换模型时容易错位两个信号分别传画面和统计结果主线程各自刷新控件互不阻塞。4.4 置信度、IOU 与 FPS 的联动调参检测效果不对先查阈值再怀疑模型。实时视频里运动模糊和暗光普遍置信度阈值开默认的 0.45 会漏掉大量半闭眼状态建议降到 0.35IoU 保持 0.45NMS 掐太紧会出现同一只眼睛两个框闭眼计数虚高。FPS 优化按优先级调优化手段修改方式效果与代价降低推理分辨率输入切到 512 或 320提速 2~4 倍小目标召回下降GPU 半精度model(halfTrue)吞吐提升约 30%仅 GPU 生效换轻量权重换 yolov5n.pt速度提升明显精度略有回落调低 conf0.45 → 0.35召回提升误报相应增加参数说明halfTrue 在 CPU 上反而更慢不要盲目开降分辨率是性价比最高的提速手段但眼睛目标本来就只有十几个像素降到 320 后几乎不可检视频场景慎用。FPS 统计用 deque 保存最近 30 帧的时间戳取平均比单帧 1/dt 平滑得多界面数字不会乱跳。5. 疲劳危险驾驶判定逻辑闭眼比例、哈欠频率与分级报警5.1 目标关联把眼睛和嘴巴挂到对应的人脸上模型输出的只是独立目标框判定这个人困不困之前先得知道哪些眼睛、嘴巴属于同一个人。常用方案有两种A 在数据集里增加 face 类检测后按框的包含关系把器官归到最近的人脸B 先检测人脸再对裁剪区域单独跑眼部模型。方案 B 精度更高但要跑两遍网络实时性差这套工程普遍用方案 A一次推理完成所有关联def associate(boxes, names): persons [] # 每个元素存一个人的人脸框和他的器官框 for x1, y1, x2, y2, conf, cls in boxes: if names[int(cls)] face: persons.append({face: (x1, y1, x2, y2), parts: []}) for x1, y1, x2, y2, conf, cls in boxes: if names[int(cls)] face: continue cx, cy (x1 x2) / 2, (y1 y2) / 2 # 器官框中心点 for p in persons: fx1, fy1, fx2, fy2 p[face] if fx1 cx fx2 and fy1 cy fy2: p[parts].append((x1, y1, x2, y2, conf, int(cls))) break # 只归到第一个包含它的人脸 return persons关联用的是中心点包含规则眼部或嘴部框的中心落在某个人脸框内部就归给这个人。单人驾驶场景逻辑简单多人同框时用最小面积包含规则避免器官归属混乱。副驾或后座人脸会被摄像头拍到需要在业务层加一条过滤人脸框下沿低于画面高度一半以上的才参与疲劳判定否则等红灯时看一眼副驾就误报。5.2 PERCLOS 指标与滑动窗口统计PERCLOS 是疲劳检测领域的经典指标指单位时间内眼睛闭合帧数占总帧数的比例。工程实现用简化版维护 60 帧的滑动窗口统计 eye_close 占比超过 40% 且累积若干帧后判定疲劳。deque 定长队列天然适合这个场景# judge.py 疲劳判定器 from collections import deque class FatigueJudge: def __init__(self, window60, close_ratio0.4, alarm_frames90): self.window deque(maxlenwindow) # 定长窗口满后自动丢最旧帧 self.close_ratio close_ratio # 闭眼帧占比阈值 self.alarm_frames alarm_frames # 连续触发帧数30fps 下约 3 秒 self.triggered 0 def update(self, person): # 双眼同时闭眼记 1否则记 0单眼闭可能只是眨眼瞬间 closed 1 if (person[eye_close] 2 and person[eye_open] 0) else 0 self.window.append(closed) ratio sum(self.window) / self.window.maxlen # 窗口内闭眼占比 self.triggered self.triggered 1 if ratio self.close_ratio else 0 return self.triggered self.alarm_frames逻辑说明deque(maxlen60) 每帧 append 一次超过 60 帧自动弹出最旧数据省去手写环形索引ratio 是窗口内闭眼帧占比triggered 记录连续满足比例阈值的帧数达到 90约 3 秒才报警。窗口长度直接影响灵敏度窗口过短一次低头就误报过长疲劳状态要攒很久才响应。60 帧约 2 秒是 demo 里最常用的值接入车规场景需要按摄像头实际帧率换算不能写死。5.3 哈欠检测与复合判定单靠闭眼比例会有漏判打哈欠是疲劳的另一个强信号。哈欠判定同样用连续帧mouth_open 连续出现超过 15 帧约 0.5 秒记为一次哈欠1 分钟内哈欠达到 3 次则抬高疲劳等级。短促张嘴不计数说话、唱歌都会张嘴但持续时间达不到阈值。完整判定规则条件疲劳等级报警方式闭眼比例 40% 持续约 3 秒一级语音提示界面警示1 分钟哈欠 ≥ 3 次二级蜂鸣红色边框闪烁闭眼与哈欠同时触发三级持续蜂鸣需人工解除规则表的工程含义一级只提示不打断二级开始强制提醒三级必须有我清醒按钮才能解除报警防止驾驶员靠反复按键蒙混。哈欠计数在每分钟窗口用同样的 deque 实现统计量独立于闭眼窗口互不干扰。实际项目要接入车辆信号做交叉验证时在这层留一个外部事件接口即可GUI 和 yolov5 检测链路都不需要改动。5.4 阈值参数与现场调优疲劳判定调参容易陷入盲目试。先分清三类阈值各管什么置信度阈值管检测层的漏检和误检闭眼比例管时间维度的疲劳程度连续帧数管瞬时动作的持续时间。调整顺序也按这个来先用单张图片把检测层调到不漏检再播一段视频调时间维度参数。参数默认值偏低后果偏高后果conf0.35误报多眨眼被算成闭眼半闭眼漏检PERCLOS 比例0.40低头看手机就报警真疲劳时才触发哈欠连续帧数15说话被当哈欠哈欠漏判人脸高度过滤0.5副驾误入画面俯身取物检测不到所有阈值都要能调而不是写死在代码里主界面提供下拉框或输入框一边看视频实时改调好后再存配置文件。这是工具型 GUI 和论文 demo 的区别参数可见、可调、可持久化。夜间场景还要考虑红外补光对分类的影响闭眼类样本如果都是白天采集的夜间误报率会偏高补充夜间数据重训往往比调阈值更有效。6. 打包发布与三个高频踩坑点6.1 PyInstaller 打包成独立程序交付的最后一步是打包。PyInstaller 对 PyQt5 支持较好yolov5 的依赖也能收集进去# -F 单文件-w 不弹控制台窗口 pyinstaller -F -w main.py \ --add-data runs/train/exp/weights/best.pt:models \ --collect-all torch--add-data 将模型打进包内Windows 用分号分隔源和目标Linux 与 macOS 用冒号--collect-all torch 会把 torch 动态库全部收进来CPU 版 torch 打包出来 600MB 左右属正常现象不要用 UPX 压缩torch 解压启动反而更慢。打包后的 exe 读取模型要用 resource_path 函数处理运行时 sys._MEIPASS 指向临时解压目录开发环境的相对路径在打包后失效这是打包后黑屏最常见的原因。想瘦身就把推理切到 ONNX运行时只依赖 onnxruntime体积能砍掉一半以上。6.2 三个高频踩坑模型加载、摄像头索引、跨线程控件第一个坑是 torch.hub.load 卡在下载。工作目录不在 yolov5 仓库内、或漏了 sourcelocaltorch 会尝试联网拉取仓库失败时程序长时间无响应。保持工作目录在仓库根目录并固定写 sourcelocal 可解决或者用 torch.jit.trace 导出 jit.pt加载时不依赖仓库目录。第二个坑是摄像头索引漂移。笔记本自带摄像头是 0外接 USB 摄像头可能是 1 或 2插拔后索引会变被其他程序占用时 VideoCapture 不报错只是 isOpened() 返回 FalseGUI 会一直黑屏。调试阶段写循环逐个试 0 到 4 的索引确认后再写死。停止检测按顺序 cap.release()、worker 置 runningFalse、wait() 等线程退出顺序颠倒会导致下次打不开摄像头。第三个坑是跨线程操作 Qt 控件。子线程里直接 setText、setPixmap 会偶发崩溃或界面不刷新这是 Qt 线程模型的红线。铁律是子线程只发信号控件更新全部放在主线程的槽函数里数据量大时可在主线程用 QTimer 定时从线程安全队列取结果避免信号频率过高挤爆事件循环。提示验证打包产物时把 best.pt、一段包含闭眼和哈欠的录制视频、打包后的 exe 放同一目录做冒烟测试比直接接摄像头可复现性好得多。能正常报警、退出后资源全释放这套基于 yolov5PyQt5 的疲劳危险驾驶检测才算真正落地交付。本文还有配套的精品资源点击获取