基于YOLOv10的疲劳驾驶检测系统:从数据标注到UI部署全流程解析
疲劳驾驶检测这几年可以说是计算机视觉领域最热门的实战方向之一GitHub上相关的开源项目一大堆但很多都是“能跑、效果一般、界面简陋”的演示级作品。我这次做了一套相对完整的方案把YOLOv10目标检测、自制标注数据集、实时UI界面和Python推理逻辑串到了一起不光是训练脚本跑通而是从数据准备到最终封装成可交付的项目源码整个过程踩了不少坑也沉淀了不少经验。这篇文章就把整个系统的技术拆解、选型理由、训练过程、界面逻辑和实测避坑完整地记录下来给正在做相似项目或者准备拿这套思路做课程设计、毕业设计的同学一个可以直接参考的路径。很多人一听到“疲劳驾驶检测”就想到各种复杂的生理信号采集比如脑电波、心率、方向盘转角等等。实际上基于视觉的方案现在已经是主流核心思路并不神秘用目标检测模型实时识别驾驶员的面部状态判断是否存在闭眼、打哈欠、头部姿态异常等疲惫信号再结合时间维度上的统计逻辑判定疲劳等级并触发报警。这套系统的优势是硬件成本极低一个普通USB摄像头加一台带GPU的电脑就能跑不需要贴电极、戴手环驾驶员是无感的。对于想入门深度学习实战、特别是想完整走一遍“数据标注-模型训练-应用落地”全流程的人来说这简直是最合适的训练场。下面我把这套系统的设计思路和完整实现逐层拆开讲。1. 从需求到技术方案的推演为什么最终选了YOLOv101.1 疲劳检测的逻辑起点先识别“状态”再判断“疲劳”我们得把问题拆清楚。疲劳驾驶检测本质上包含两个递进层次感知层和决策层。感知层的任务是回答“当前画面里驾驶员的眼睛是睁开还是闭合、嘴巴是张开还是闭合、头部是不是在低垂”决策层的任务是回答“过去若干秒内的这些状态组合在一起能否判定为疲劳”。这一个顺序极其重要初期没想清楚导致我走了不少弯路。很多新手一上来就想端到端地训练“疲劳/清醒”二分类网络让模型直接回答“是否疲劳”。这个方向本质上就走偏了。原因在于疲劳是一个时间尺度上的统计概念单张静态图根本承载不了这个信息。一个人闭眼0.2秒是正常眨眼闭眼1.5秒就是明显的瞌睡信号如果训练数据里把这两种情况都标成“疲劳”模型只会越学越混乱。所以正确的做法是把感知层做成强健的目标检测模型专门识别眼睛闭合、眼睛睁开、打哈欠、正常状态这几个具体视觉状态然后交给决策层去做时序统计。这套思路是当下工程落地里被验证过的成熟路径。1.2 YOLOv8、YOLOv5、YOLOv10横向对比YOLO系列现在的选择非常多选型时我重点考虑了三个因素推理速度、部署便利性、以及训练生态的成熟度。下表是我实测后的直观对比注意YOLOv5时代的老将到现在依然是可靠选择但既然做新项目条件允许时优先用新架构。模型端到端推理NMS后处理检测精度同等体积下工程生态成熟度我的结论YOLOv5否需要中极成熟教程最多备选适合老设备、老部署链路YOLOv8否需要中高极成熟Ultralytics统一接口稳健之选适合求稳项目YOLOv10是不需要高生态相对较新但API直接兼容最终选择省去NMS调参YOLOv10让我最终下定决心的核心理由有两个。第一个是无NMSNon-Maximum Suppression非极大值抑制推理机制它提出了一种双标签分配策略在训练阶段用一对多标签分配让模型更快收敛在推理阶段换成一对一分配直接输出最终预测框这就把推流后处理环节里的NMS步骤删掉了。你可别小看这一个步骤在嵌入式边缘设备上NMS涉及的框排序、IoU计算和迭代筛选逻辑本身就是不小的CPU开销去掉之后不仅延迟降低推理代码也简化了一大截。第二个理由是它的C2fCIB模块设计。CIBCompact Inverted Bottleneck在保证梯度流通顺畅的同时带了一点轻量化味虽然训练显存稍涨但推理时表现更好。实测发现YOLOv10s在兼顾速度和精度的平衡上对小目标比如从驾驶座视角看远处的眼睛区域的表现也比同梯度YOLOv8好一些。1.3 系统整体架构摄像头采集、模型推理到UI反馈整套系统的运行链路简单画出来就是这样的摄像头持续采集驾驶位画面把每一帧图像交给YOLOv10推理引擎做对象检测得到每个人的面部状态类别和置信度然后由疲劳判定引擎把连续帧的检测结果拉成时间窗口计算闭眼比例、哈欠频率等疲劳指标超过阈值就联动UI弹出告警并记录截图日志同时界面需要持续显示实时视频流、检测框、运行帧率和状态统计数据。这里有个容易被忽略的关键点摄像头采集、模型推理和UI刷新这三个环节的速率是不同步的。摄像头可能输出30帧每秒模型推理可能只有15到20帧每秒如果UI直接在推理线程里做刷新画面就会像抽风一样跳。因此我在架构上把推理线程和UI线程解耦通过线程安全队列传递带标注结果的帧数据UI定时器以固定频率拉取新帧来绘制这套生产者消费者模型让界面刷新非常平稳。项目中代码结构也按这个思路组织模型推理模块、疲劳检测模块、UI模块相互独立替换算法或扩展功能都不需大动干戈。2. 数据集构建与标注清洗最容易低估却又最影响精度的一环2.1 数据集来源与类别的确定模型的生命力在于数据。我最初以为训练过程是最难的部分实际做下来才明白数据质量和标注一致性才是精度的天花板。这个项目需要的数据集包含四类目标状态睁开眼睛open_eye、闭上眼睛closed_eye、打哈欠yawn和正常状态normal。其中normal主要作为背景锚点和非疲劳状态的基准实际检测时也用它来兜底那些没有显著状态的画面。公开数据集确实提供了不少基础素材网上能找到一些开源的人眼状态和打哈欠数据集但直接用它们的原始标注会遇到很多隐患。比如很多开源数据集的图片来自摄影棚灯光环境面部角度单一、背景干净和驾驶室复杂光线下的画面分布差异很大又比如有些打哈欠数据里嘴巴张开的程度不够夸张模型学会了却在实际场景里误判。因此我最终的策略是以公开数据为基础加入自采和合成数据扩充再对全部数据做统一的标注规范清洗最终构成自己的训练集。自采数据这一块我的做法比较朴素但有效。用手机摄像头拍摄日常场景下自己闭眼、打哈欠的画面每天积累100到200张连续一周后人工筛选出可用的图片因为手机摄像头和真实驾驶场景的监控摄像头色彩风格其实差异不小加入这些数据后极大地提升了模型的泛化能力。注意自采时避免在敏感场景下进行纯粹作为技术样本使用即可。2.2 标注规范和边界情况的处理标注是决定模型上限的隐形因素。YOLO格式的标注是一行五个数字类别id0到3对应上述四种状态加归一化的中心点x、中心点y、宽度w、高度h所有坐标都是0到1的小数。表面看非常简单但实际标注中会遇到几个典型的边界坑。第一个坑是闭眼和睁眼的边界。眼睛半睁的状态应该标什么我在标注规范里定为若瞳孔可见面积大于一半则标open_eye小于等于一半且眼睑明显下垂则标closed_eye。这个标准必须从第一个标注样本一直贯穿到最后一个样本因为标注不一致是模型困惑感最大的来源。我自己重新审核一遍数据集时发现早期有些样本把微眯眼标成closed_eye后面又把类似样本标成open_eye这就给模型传递了矛盾信号重新统一后才明显感觉训练曲线平稳了。第二个坑是打哈欠的判定标准。打哈欠时嘴巴张开的程度差异很大我设定的标准是嘴巴张开的宽高比要足够明显具体操作时看嘴部外接框的高度是否显著大于正常说话状态同时四周唇部轮廓清晰可见才标yawn。光张嘴不说话和打哈欠之间的灰色地带宁可不标也不乱标避免误导模型去学“张嘴哈欠”。这和疲劳判定逻辑是配套的决策层还会配合闭眼信号加权。2.3 数据增强与平衡性处理数据增强这里Ultralytics框架默认提供了一系列增强策略Mosaic拼接、随机仿射变换、HSV色彩扰动、翻转等等。对疲劳检测这个场景我经验性地关闭了垂直翻转增强因为驾驶场景里人脸必然是正立的垂直翻转会造成物理上不可能出现的目标形态反而让模型学到错误特征。水平翻转保留摄像头左右位置差异正好可以用它来模拟。还有一个关键操作是保持类别平衡。如果训练集里open_eye样本有一万张closed_eye只有八百张模型很容易把闭眼样本全部当作误检漏掉。我当时的策略是给所有类别设定目标数量比如每类不低于2500张低于这个数目就用现有样本做轻度复制加模糊、缩放变换和光照扰动来扩充确保每个类别都有足够且差异化的样本。实际训练之后各类别AP平均精度差异压缩到很小证明这一步非常值得。3. YOLOv10训练过程全记录配置、命令、曲线解读与常见问题3.1 训练环境与资源配置工欲善其事必先利其器。模型训练这块我的环境配置是Ubuntu系统加一块24GB显存的显卡训练YOLOv10s规模的模型完全够用。如果你只有8GB显存甚至更少也不用慌减小batch size或者使用m模型改小输入尺寸都能跑只是训练时间会长一些。依赖安装直接用Ultralytics的官方pip包它会自动把对应版本的PyTorch拉进来。这里提醒一句PyTorch的安装一定要去官网选择适配你机器CUDA版本的命令不要直接无脑pip install torch否则大概率装成CPU版本。我第一次就在这上面浪费了大半天时间后来重新通过官网命令安装了基于CUDA的构建版本训练速度直接提升了几十倍。3.2 数据集配置与模型结构选择Ultralytics框架的标准做法是准备一个data.yaml文件描述训练集、验证集的路径和类别信息。内容只有几行train: datasets/mydata/train val: datasets/mydata/valid nc: 4 names: [open_eye, closed_eye, yawn, normal]nc后面的4要和类别列表一一对应位置错了模型训练时就会张冠李戴。训练命令也十分简洁yolo detect train datadatasets/mydata/data.yaml modelyolo11n.pt epochs150 batch48 imgsz640 device0需要注意的是我初始使用的是YOLOv10n的预训练权重作为起点而不是从零开始训练。迁移学习对于目标检测这种复杂任务价值极大COCO预训练模型已经学会了大量通用的纹理、轮廓和物体概念从这些特征上做微调通常几十个epoch就能比随机初始化训练几百个epoch的效果还好。3.3 训练曲线解读不要只盯着mAP很多人看训练结果只关注最终mAP数字这是自己给自己埋雷。我建议把训练日志里的loss曲线、精确率曲线、召回率曲线都导出来整体分析。loss曲线应该在训练后期平滑下降并趋于平台期如果出现上升拐点说明过拟合需要增加数据增强或提前停止。精确率和召回率是一对矛盾指标对疲劳检测这个场景来说我宁可牺牲一点精确率也要保证召回率也就是“宁可误报不要漏报”。疲劳告警漏一次可能就是一场事故而误报可以通过时间窗口的二次判定来抑制。训练结束后框架会自动输出一批验证集上的推理可视化图。这一步一定要看不要只看数据指标。我当时的验证集中有几张戴墨镜、侧脸的困难样本模型在这类图上表现不稳定。这些画面暴露出的问题往往是标注规范或者数据分布导致的没有经验的同学经常会忽略这个检查环节。3.4 训练阶段踩过的三个典型坑坑一显存溢出。第一个版本的batch size设成128直接把显卡显存占满然后进程被杀。解决方法是逐步降低batch size同时确认输入尺寸imgsz是否为640。把batch size降到48后训练稳定完成牺牲的只是单次更新步数总epoch增加后整体收益不变。坑二模型不收敛。训练20个epoch后loss几乎不降查阅日志后发现是数据集里有一批图片把横放的拍摄照片直接作为训练样本模型被这些畸形样本带偏了。清洗这批横躺图像并统一旋转修正后loss曲线立刻恢复了正常的下降趋势。坑三过拟合导致验证集指标虚高。有一版验证集划分和训练集有重叠部分自拍摄素材在两类中都出现验证集表现非常好但实际推流摄像头画面效果很差。后来我重新按时间维度切分数据集保证同一段视频中的不同帧不会横跨训练集和验证集评估结果才变得有参考价值。3.5 模型评估结果以mAP50-95为标尺的横向对比训练结束后我得到了一组关键指标mAP50约94%mAP50-95约71%推理耗时在单张RTX 30系显卡上约为4到6毫秒每帧。这个指标在实际摄像头推流场景下已经完全够用。作为对比如果换成YOLOv5smAP50-95大概在66%左右推理耗时多出1到2毫秒而如果用轻量级移动端模型推理能到2毫秒但mAP50-95掉到58%左右。这正是我坚持选用YOLOv10这个精度区间的现实原因——疲劳驾驶容错率很低精度是硬指标不能被单纯的速度优势所替代。4. UI界面与实时疲劳判定逻辑让模型真正“用起来”的关键4.1 UI框架选型PyQt5为什么是妥当之选模型训练得再好没有界面就无法交付给真实用户使用。UI框架的选择上PyQt5是Python生态里最常见的桌面GUI方案跨平台、控件丰富、文档齐全。相比于单纯用OpenCV的imshow窗口显示画面PyQt5能提供完整的交互体验多个按钮、状态面板、参数输入框可以自由布局对后期扩展时间线回放、数据统计等功能也极其方便。界面设计上我做了三个区域左侧是视频画面显示区域实时绘制检测框和标签右侧上方是状态监控面板展示帧率、当前检测状态、疲劳得分和告警等级右侧下方是控制区域包含开始检测、停止检测、阈值调整滑杆和事件日志列表。整体布局对驾驶监控场景来说足够直观一眼就能看出当前驾驶员处于什么状态。4.2 从单帧检测到时间窗口统计疲劳判定算法的核心这是整个UI模块里最重要的一段逻辑。单帧检测结果只是“这一刻眼睛是闭合的”但人要眨眼眨眼也是闭合状态且频繁出现如果只看单帧就报警系统会疯掉。所以需要一个时间窗口来平滑这些瞬间变化。我采用的算法思路是这样的维护一个长度为N的循环队列相当于记录最近3秒内的每一帧检测结果。假设摄像头推理帧率是15帧每秒队列长度就设为45每秒计算一次窗口内的统计数据。统计什么呢第一是PERCLOS值即眼睛闭合时间占比具体计算方法是窗口内closed_eye帧数除以open_eye加closed_eye帧数之和。当PERCLOS连续多秒超过设定阈值比如0.35判定为一级疲劳。第二是哈欠频率窗口内yawn类别的帧数超过设定值且平均张嘴持续时间较长可触发二级疲劳。第三是点头低头检测如果模型检测到的人脸区域在连续多帧中垂直位移持续下降、且constrained状态下持续时间较长结合闭眼状态加权。下面这一段是简化后的疲劳判定伪代码真实项目里我还会加一些去抖逻辑class FatigueEvaluator: def __init__(self, window_size45, perc_threshold0.35): self.window_size window_size self.perc_threshold perc_threshold self.state_queue deque(maxlenwindow_size) def add_frame(self, detections): # detections是模型返回的类别列表 frame_state self.analyze_frame(detections) self.state_queue.append(frame_state) def analyze_frame(self, detections): has_closed any(d.cls closed_eye for d in detections) has_open any(d.cls open_eye for d in detections) has_yawn any(d.cls yawn for d in detections) # 返回本帧状态注意睁开与闭合同帧时以闭合优先避免漏报 if has_closed and not has_open: return closed if has_yawn and not has_open: return yawn return normal def evaluate(self): if len(self.state_queue) self.window_size: return 0 # 队列未满不判定避免冷启动误报 closed_ratio self.state_queue.count(closed) / self.window_size yawn_ratio self.state_queue.count(yawn) / self.window_size if closed_ratio self.perc_threshold: return 2 # 二级疲劳 if yawn_ratio 0.2: return 1 # 一级疲劳 return 0这段代码的价值在于“状态去抖”偶尔一两帧闭眼不会触发告警只有连续闭眼占比上升才会进入疲劳状态。实际测试里正常眨眼时PERCLOS通常不会超过0.15而困倦时闭眼状态持续时间变长、比例显著突破0.35。4.3 多线程设计解决视频流卡顿与UI冻结如果直接把模型推理放在UI主线程里做画面会卡到怀疑人生。因为PyQt的UI事件循环和执行推理的while True循环互相抢占用户点击按钮都会没有响应。我采用的方式是推理线程加信号槽机制。推理线程从摄像头读取帧跑模型前处理、推理和后处理然后emit一个携带QImage的signal给主界面主界面在界面上绘制图像。UI只负责显示和接收用户操作完全不阻塞推理线程。有一个关键的线程安全细节。在PyQt里子线程不能直接修改主界面的控件必须通过信号槽把数据安全地传递到主线程。如果子线程代码里强制去设置QLabel的文字或图片轻则界面无响应重则直接崩溃。我当时在这个问题上卡了很久最明显的症状就是点击“停止检测”按钮后界面直接假死几十秒优化成信号槽机制后在界面上进行的所有操作都变得极为流畅。4.4 告警机制与应用扩展告警不仅是界面弹窗这么简单。真实场景里驾驶员可能正盯着前方根本来不及看屏幕边框弹出的提示。所以完整的告警应该具备多模态提示界面弹窗加蜂鸣器报警加语音合成提示。语音播报我用的方式是在告警触发时调用系统语音引擎播放“请勿疲劳驾驶”之类的提示语音效果直接且不依赖额外外设。事件日志模块也有大用。每次告警会把告警时刻的截图、告警等级、近5秒内的PERCLOS统计值记录到一个CSV文件中。这个日志文件对研究驾驶行为、评估疲劳判定算法是否合理、甚至作为交通管理的辅助参考都非常有价值。我实测中发现采集到一天的驾驶数据后把日志导入分析工具做可视化能清晰看到不同时间段的疲劳分布这对后续做疲劳预警的个性化阈值调整帮助极大。5. 模型部署与推理加速从PyTorch到ONNX的工程化过一遍5.1 为什么导出ONNX而不是直接跑PyTorch推理模型的训练态和部署态是两回事。训练态模型为了支持反向传播计算图里包含大量梯度信息和动态控制流直接用它做推理不仅内存占用高而且速度不够理想。ONNXOpen Neural Network Exchange作为开放神经网络交换格式把这些训练附加信息全部剥离掉保留纯推理计算图在特定后端上有不少优化空间。导出ONNX的命令在Ultralytics框架里极其简单from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) model.export(formatonnx, imgsz640)导出后模型的推理代码也从原来的框架变成了ONNX Runtime调用。使用ONNX Runtime的另一个好处在于CPU推理性能提升明显。我实测同一台机器上PyTorch直接跑CPU推理平均每帧200毫秒ONNX Runtime只要120毫秒左右提速将近一倍。如果部署到带TensorRT的NVIDIA环境这个差距还会进一步拉开。5.2 推理代码前的预处理细节BGR、RGB与归一化一个极容易翻车的细节是图像通道顺序。OpenCV读进来的是BGR格式而YOLO训练和推理默认使用的输入是RGB格式中间一旦漏了转换模型的效果就会急剧下降出现各种诡异误检。Ultralytics封装的高级API会自动处理好这一步但如果你直接在ONNX Runtime里手写推理就必须在预处理阶段把BGR转成RGB再做等比缩放和归一化最后把结果reshape成形状为(1,3,640,640)的张量。漏掉这个步骤的结果就是模型明明训练时很准部署后突然什么都检测不出来。尺寸缩放这里同样有讲究。输入尺寸不一定是640原图但需要保持原始宽高比多余部分用灰色填充而不是直接拉伸变形。原先我图省事拉伸填充后模型的IoU和坐标映射出现系统性偏差边框位置漂移改成letterbox填充后整个推理链路才恢复正常。5.3 低功耗设备的优化方向如果你计划把这套系统部署到Jetson Nano或者树莓派这类低功耗设备上那么V10n级别的轻量模型和TensorRT的INT8量化是必须考虑的两个方向。在YOLOv10的无NMS推理架构下INT8量化会比传统模型更容易保持精度因为省去的NMS后处理阶段涉及的浮点比较运算在实际量化时会成为最不确定的误差源。我这里没有来得及做完整的量化流程但这是项目下一步非常明确的优化路径。6. 实测中的意外情况、误报抑制与进阶优化建议6.1 真实场景中的误报链路排查系统搭建完我拿着摄像头坐在工位上模拟驾驶姿态做了几轮完整测试结果发现误报比预想的多得多。最典型的是三种情况。第一种是低头看手机被误判为疲劳。低头时眼睛自然是向下看的如果角度过大模型可能检测不到eyes此时如果只看closed_eye的占比就会触发误报。解决方法是把“检测不到眼睛”和“检测到闭眼”作为两种状态分开处理并且结合头部姿态角度只有头部低垂角度超过特定值才会增强疲劳置信度。第二种是环境光线过暗导致整个人脸区域检测失败。这个问题的解决思路是启用红外摄像头或者做简单的亮度归一化模型层面也可以用光照扰动增强来缓解。第三种是模糊帧问题车辆颠簸导致画面出现运动模糊检测器的置信度波动极大。我的做法是在帧送入检测器前先做灰度方差检测方差低于阈值说明图像太平滑直接跳过该帧不做疲劳判定这相当于一个轻量级的退帧保护。6.2 判定阈值的自适应调整固定阈值是最省事的方案但不同驾驶员、不同时段、不同光线条件下的基准PERCLOS差异其实不小。有人天生眨眼频率高纯睁眼状态PERCLOS就可能逼近0.2有的人开车时长时间盯路闭眼频率本来就低一旦闭眼就是即将进入睡眠的强信号。这个差异靠一个固定阈值无法兼顾。我实测之后做了一个简单的自适应方案系统启动后先运行两分钟进入校准模式统计驾驶员正常状态下的PERCLOS基线值之后的疲劳阈值就以基线值为基准做动态偏移例如阈值为baseline0.2。这个方案在实验中明显降低了固定阈值导致的误报和漏报。6.3 UI界面指标显示与实际处理延迟的平衡界面左侧的视频流显示是有讲究的。如果你把每一帧推理结果都即时绘制再刷到界面高帧率下CPU占用会非常高还有可能导致UI事件循环一直处于高负载状态。我最后采用了隔帧显示的策略模型推理保持全帧率进行但UI视频流每两帧刷新一次既保证了画面的流畅感又显著降低了负载。界面上的帧率显示也分模型推理帧率和UI刷新帧率两种避免用户误以为画面卡顿就是检测卡顿。6.4 后续可以继续优化的几个方向这套系统目前是纯视觉方案它的核心局限在于黑暗环境下的表现和极端角度下的稳定性。后续优化方向我认为可以沿着三条线推进一是接入红外深度摄像头在近红外波段下采集人脸图像彻底解决暗光环境失效的问题二是引入关键点检测分支用面部关键点的相对距离计算眼睛纵横比和嘴巴纵横比这两个比值是业界公认的疲劳量化指标比单纯依赖目标框类别判断要细腻得多三是加入驾驶员识别和个性化模型比如在系统启动时通过一个人脸ID绑定专属阈值让每个驾驶员的判定基线都不一样。这些方向都不需要推翻现有架构在当前的检测结果输出基础上叠加新模块即可。做完这套系统我最大的感受是目标检测项目完整的落地链条不是单点技术决定的。模型效果很好只是其中一个环节数据标注的一致性、线程设计、部署链路、前后端衔接任何一个点做得粗糙最终盘子都会塌陷。特别是疲劳检测这个场景花费了大量的精力去对抗光线、遮挡和模糊变量最后真正决定好用与否的反而是那些看起来不起眼的工程细节。如果你正在做类似项目我的建议是先搭通整套推理和UI链路再回头调数据和训练而不是把所有时间砸在训练阶段反复测精度。链路通了模型怎么换都会很快见效。