资讯动态

YOLO驾驶员疲劳检测实战:从数据集构建到训练避坑指南

发布时间:2026/9/28 17:04:15 来源:尧图企业网站定制
简介面向计算机视觉与深度学习开发者的YOLO驾驶员疲劳检测数据集聚焦闭眼、打哈欠等疲劳行为的实时识别适用于驾驶辅助安全、智能座舱监控等场景可直接用于目标检测模型的训练与验证。资源共包含8744个文件以2915张JPG图像为核心配套2915个TXT标注文件和2914个XML标注文件TXT记录边界框坐标与类别XML保留结构化元数据整体压缩包约256.88MB标注格式覆盖YOLO系列常用训练需求。目前已有2924人学习下载。数据集涵盖驾驶员不同疲劳程度的面部状态与多类行为样本可直接划分训练集投入YOLOv5、YOLOv8等模型微调也可结合XML信息开展多任务扩展图像与标注一一对应省去自行清洗与格式转换成本适合需要真实驾驶场景数据验证疲劳检测算法的研究人员与学习者。1. yolo算法驾驶员疲劳检测先解决数据集再谈模型做驾驶员疲劳检测很多人一上来就挑模型、调参结果训练完一测闭眼漏检、打哈欠误报问题根本不在网络结构上而在你喂给 yolo 的数据本身。这个方向的核心矛盾很直接疲劳状态是强时序、弱外观的目标单帧里闭眼和正常眨眼几乎没有区别需要大量贴近真实驾驶环境的样本才能让模型学会区分。整个任务里模型训练只占三成工作量剩下七成都在和数据打交道。本文会按从业者实际落地的顺序把数据集怎么选、怎么转成 yolo 格式、训练参数怎么配、以及最容易翻车的几个坑讲清楚适合正在做车载 DMS、司机行为分析或相关毕业设计的同学直接照着做。2. 疲劳检测数据集的选择逻辑公开数据集与自采方案的取舍2.1 三种公开数据集的指标与适用边界常见的做法是先找公开数据集验证流程再决定要不要自己补数据。驾驶员疲劳检测方向有几个经常被用到的公开数据集各自侧重点不同。一类是打哈欠检测数据集样本集中在嘴部区域适合单独训练 yawn 类别一类是疲劳驾驶模拟数据集包含头部姿态、眼睛状态和打哈欠的连续帧标注适合做完整的驾驶员状态识别还有一类是驾驶行为视频数据集在真实车内环境采集含有遮挡、光照变化和不同人种样本但标注质量参差不齐。选型时看的核心指标不是总量多少而是三个东西标注的类别是否覆盖你需要的状态、图像分辨率是否足够看清眼睛区域、以及场景是否包含夜间和逆光。很多数据集用实验室环境拍摄背景干净、光照均匀模型在这种数据上训练完一到真实车里就明显掉精度。我的建议是公开数据集只用来做预训练和流程验证最终模型必须混入自采或真实场景数据微调。另外需要注意数据集的标注格式。公开数据集大多是 VOC 格式的 XML 标注或者 COCO 格式的 JSON而 yolo 训练要的是 txt 文件每个框一行格式是类别编号加归一化后的中心点坐标和宽高。这一步格式转换没有技术难度但很耗时间而且容易出错下一节会给出完整的转换脚本。2.2 自采数据的最小方案摄像头安装位置与光照控制自采数据是让模型在真实场景下可用的关键一步。采集方案不需要多复杂关键是模拟实际部署条件。摄像头要装在被测者斜前方 30 到 45 度、距离人脸 50 到 80 厘米的位置这个角度能同时拍到眼睛和嘴部而且不容易被方向盘遮挡。分辨率至少 720p实际经验是 1080p 更好因为闭眼和打哈欠都是小目标眼睛区域可能只有几十个像素分辨率不够直接导致模型学不到特征。采集时要覆盖三种光照白天顺光、白天逆光、夜间仪表盘照明。夜间是最容易翻车的场景因为人眼在暗光下瞳孔放大眨眼频率和白天不一样模型如果只在白天数据上训练夜间基本不可用。建议在采集时故意让被测者戴墨镜、用手遮挡部分脸部这样模型不会过度依赖某一个局部特征。标注环节有一个值得注意的策略不要只标两种状态正常、疲劳而是拆成三个类别——eye_open、eye_closed、yawn。原因在于疲劳判定需要统计闭眼持续时间和打哈欠频率如果模型只输出一个“疲劳”标签你就拿不到中间状态的数据后续做时序判定会非常被动。提示自采数据规模建议不少于 5000 张其中闭眼和打哈欠样本各占 20% 以上。如果实际采集很难凑够疲劳样本可以用合成数据补充后面会讲。3. 把原始数据转成 yolo 训练格式标注清洗与脚本落地3.1 VOC XML 转 yolo txt坐标归一化与类别映射公开数据集和标注工具导出的文件一般是 VOC 格式XML 里存的是xmin、ymin、xmax、ymax绝对坐标。yolo 训练需要的是归一化后的中心点坐标和宽高转换逻辑如下x_center (xmin xmax) / 2 / widthy_center (ymin ymax) / 2 / height、w (xmax - xmin) / width、h (ymax - ymin) / height。下面是一段可直接用的转换脚本import os import xml.etree.ElementTree as ET # 类别映射按你自己的类别顺序调整训练时 data.yaml 里 class 顺序必须和这里一致 CLASS_MAP {eye_open: 0, eye_closed: 1, yawn: 2} def convert_voc_to_yolo(xml_path, out_dir, class_map): tree ET.parse(xml_path) root tree.getroot() img_width int(root.find(size/width).text) img_height int(root.find(size/height).text) img_name os.path.splitext(os.path.basename(xml_path))[0] out_path os.path.join(out_dir, img_name .txt) lines [] for obj in root.findall(object): cls_name obj.find(name).text.strip() if cls_name not in class_map: continue # 跳过不在类别映射里的标注比如“person”“car” cls_id class_map[cls_name] box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 防止标注越界clip 到图像范围内 xmin max(0, min(xmin, img_width)) xmax max(0, min(xmax, img_width)) ymin max(0, min(ymin, img_height)) ymax max(0, min(ymax, img_height)) x_center (xmin xmax) / 2 / img_width y_center (ymin ymax) / 2 / img_height w (xmax - xmin) / img_width h (ymax - ymin) / img_height # 过滤掉转换后变成 0 的无效框 if w 0 or h 0: continue lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if lines: with open(out_path, w) as f: f.write(\n.join(lines)) # 批量处理一个目录下的所有 xml 文件 xml_dir VOC_annotations output_dir YOLO_labels os.makedirs(output_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if xml_file.endswith(.xml): convert_voc_to_yolo(os.path.join(xml_dir, xml_file), output_dir, CLASS_MAP)这段脚本里有三个参数需要你按实际改CLASS_MAP的类别编号顺序必须和训练用的data.yaml保持一致xml_dir路径要换成你自己的标注目录输出的 txt 文件和图像要放在同一个文件名下只是扩展名不同。转换完成后对比一下生成结果用文本编辑器打开一个 txt确认坐标值都在 0 到 1 之间如果没有问题再继续。这里有一个容易忽略的点有些公开数据集的类别名不统一比如用closed_eye而不是eye_closed或者把打哈欠标成mouth_open。转换前先统计一下 XML 里所有的name标签分布避免类别映射漏掉数据导致样本量直接缩水。3.2 标注清洗重复框、无效框与类别平衡的预处理格式转换做完后还有一个清洗环节。我在项目里跑过统计公开数据集里通常有 3% 到 5% 的问题样本最常见的三种问题同一目标被标了两个重叠度很高的框、极端小的框比如整张人脸只有 20 像素宽、以及类别标错的框。这些问题如果不去掉训练时会直接表现为验证集的 mAP 忽高忽低或者损失函数曲线震荡得厉害。import os def clean_labels(label_dir, min_w0.01, min_h0.01, iou_thresh0.8): 清洗 yolo 格式的 txt 标注文件 min_w, min_h: 小于该比例的框会被过滤 iou_thresh: 两个框 IoU 大于该值时保留面积更大的框 for txt_file in os.listdir(label_dir): if not txt_file.endswith(.txt): continue path os.path.join(label_dir, txt_file) with open(path, r) as f: lines f.readlines() boxes [] for line in lines: parts line.strip().split() if len(parts) ! 5: continue # 跳过格式异常的行 cls_id int(parts[0]) x_center, y_center, w, h map(float, parts[1:]) # 过滤过小目标和越界框 if w min_w or h min_h: continue if x_center 0 or y_center 0 or w 0 or h 0: continue boxes.append([cls_id, x_center, y_center, w, h]) # 去除重复框按面积排序逐个比较 IoU boxes.sort(keylambda b: b[3] * b[4], reverseTrue) kept [] for box in boxes: duplicate False for k in kept: iou calc_iou(box[1:], k[1:]) if iou iou_thresh: duplicate True break if not duplicate: kept.append(box) with open(path, w) as f: for box in kept: f.write(f{box[0]} {box[1]:.6f} {box[2]:.6f} {box[3]:.6f} {box[4]:.6f}\n) def calc_iou(box_a, box_b): # 计算 yolo 格式中心点框的 IoU ax1, ay1 box_a[0] - box_a[2] / 2, box_a[1] - box_a[3] / 2 ax2, ay2 box_a[0] box_a[2] / 2, box_a[1] box_a[3] / 2 bx1, by1 box_b[0] - box_b[2] / 2, box_b[1] - box_b[3] / 2 bx2, by2 box_b[0] box_b[2] / 2, box_b[1] box_b[3] / 2 inter_w max(0, min(ax2, bx2) - max(ax1, bx1)) inter_h max(0, min(ay2, by2) - max(ay1, by1)) inter_area inter_w * inter_h union_area box_a[2] * box_a[3] box_b[2] * box_b[3] - inter_area return inter_area / union_area if union_area 0 else 0 clean_labels(YOLO_labels, min_w0.02, min_h0.02, iou_thresh0.7)清洗参数min_w和min_h的取值要参考你的实际任务。对驾驶员疲劳检测来说眼睛框在整图中的占比经常只有 2% 到 3%所以阈值不要设太高我一般用 0.01 到 0.02 之间。iou_thresh设 0.7 到 0.8过低会误删相邻标注比如同时标了闭眼和打哈欠的两个框。清洗完还要做类别平衡检查。统计每个类别的样本数如果eye_closed的数量只有eye_open的十分之一训练出来的模型会倾向于把闭眼也预测成睁眼。解决方法是复制少数类样本过采样并做轻微的数据增强或者对多数类做下采样。注意过采样时不能简单复制要配合缩放、平移、亮度调整否则模型会过拟合到特定的图片。初学者最容易在这个环节偷懒结果训练到一半发现 loss 降不下去调参半天没有效果最后回头查才知道是数据没处理好。4. 用 yolo 在本地跑通疲劳检测训练配置文件与核心参数4.1 data.yaml 与预训练权重准备训练前的准备工作包含两个文件data.yaml和预训练权重。data.yaml告诉 yolo 去哪里找图片和标签、类别有几类、类别名是什么。这个文件内容很简单但路径写错会导致训练直接报错path: ./driver_fatigue # 数据集根目录相对路径或绝对路径 train: images/train # 训练图片目录 val: images/val # 验证图片目录 names: 0: eye_open 1: eye_closed 2: yawn注意names下的类别顺序必须和上一节转换脚本里的CLASS_MAP完全一致。如果你用公开数据集自带的类别名也要先统一再训练。常见的一种误用是改了data.yaml但没有改动 txt 文件里的类别编号导致模型学到的类别标签是乱的。另外train和val的路径是相对于path的所以目录结构通常是driver_fatigue/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/图片和标签要严格同名images/train/001.jpg对应的标签是labels/train/001.txt。如果标签文件缺失或名字对不上yolo 在训练时会跳过该图片不会报错但你的样本量会悄悄变少。预训练权重方面我一般用yolov8n.pt或yolov8s.pt作为起点而不是从零训练。这样做的好处是两个收敛更快、在小数据集上效果更稳。从零训练需要几倍的 epoch 才能达到同样的精度而且很容易在训练初期就发散。下载预训练权重时要确认版本和你安装的 ultralytics 包版本匹配版本差距过大可能报结构不兼容的错误。4.2 核心训练参数imgsz、batch、epochs 与训练策略训练命令本身不复杂参数才是真正需要花时间调的部分。以下是一份可以直接跑的 yolo 训练命令yolo detect train \ modelyolov8n.pt \ datadriver_fatigue.yaml \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/fatigue \ nameexp1关键参数含义和选取理由如下imgsz是输入分辨率。640 是速度和精度的平衡点。如果你的是夜间场景可以试试 960小目标眼睛会更容易被检测到但训练时间会增加约一倍。注意推理时最好用和训练一致的分辨率否则精度会掉。batch的大小取决于显存在 12G 显存的卡上imgsz640时batch16是安全的。batch 太小会导致 BN 层统计不稳定训练过程会出现 loss 震荡严重时直接 NaN。lr0是初始学习率迁移训练一般用 0.01如果训练曲线发散就降到 0.005。patience是早停参数验证集 mAP 连续 20 个 epoch 没有提升就自动停止这个参数能节省很多时间我习惯设 20 到 30。训练开始后最需要关注的是cls_loss和dfl_loss这两条曲线的下降趋势不要总盯着box_loss。在驾驶员疲劳检测任务里闭眼框和打哈欠框都比较小box_loss早期下降是正常的但如果cls_loss在 30 个 epoch 后还在震荡大概率是类别不平衡或者标注噪声的问题不是学习率的问题。4.3 验证结果怎么看混淆矩阵与 mAP 的判定标准训练结束后ultralytics 会在runs/fatigue/exp1/下生成confusion_matrix.png和results.csv。看混淆矩阵时要关注行方向的召回率也就是每个真实类别有多少比例被正确预测出来。对疲劳检测来说eye_closed的召回率要比整体 mAP 更值得关注。漏检一个闭眼动作可能导致后续疲劳状态判定直接失效所以eye_closed召回率低于 90% 的模型我一般不会部署。mAP0.5 达到多少算合格没有统一标准要看你的实际场景。我在夜间车载场景的实测经验是mAP0.5 达到 0.85 以上眼闭检测的召回率在 0.9 以上这个模型才有基本的可用性。如果只是白天测试mAP 会虚高因为光照均匀、遮挡少模型在这个条件下表现好不代表真实环境里靠谱。一个值得做的验证方法是把 test 视频按时间段切分分别统计白天、夜间、逆光三个片段的检测指标这样能看出模型在哪个场景掉链子。5. 疲劳检测避坑指南数据问题与训练陷阱的排查5.1 闭眼样本太少导致漏检现象训练完成后的模型对闭眼状态几乎不输出检测框或者输出的置信度很低只有 0.3 左右。原因数据集中eye_open的样本占 90% 以上模型学到了“大多数情况下是睁眼”的先验优化方向偏向于多数的正常类。闭眼类别的梯度贡献太小边界框回归学不好。解决过采样eye_closed和yawn类别将其占比提升到 20% 以上。同时对该类别的图像做随机亮度调整和水平翻转增加样本多样性。如果还是不够就合成数据做法是把闭眼的人眼区域复制到背景图上并微调边缘色差。这是数据层面最直接的手段调损失函数权重是备选方案效果不如直接加样本可靠。5.2 训练中 BN 崩溃loss 突然变成 NaN现象训练进行到 20 到 40 个 epoch 时loss 突然跳变为nan之后所有指标都变成nan训练只能重来。原因batch太小导致 BN 层统计量不稳定学习率过高导致梯度爆炸。在疲劳检测这种小目标多、背景复杂的任务里batch8 以下很容易复现这个问题。解决把batch提高到 16 以上显存不够就开cacheTrue显存缓存或者减小imgsz到 416。同时把lr0从 0.01 降到 0.005patience设小一点让早停保住已经训好的权重。另外可以加weight_decay0.0005稍微增加正则约束。如果 NaN 发生在训练一开始优先查输入数据很可能有标注越界的框没过滤干净。注意训练到一半 NaN 时不要直接降低学习率继续跑先清除旧的输出目录再重训否则 optimizer 状态里可能已经积累了异常统计量。5.3 混淆矩阵总和不为 1 的困惑现象手动把混淆矩阵里的所有数值加起来发现不是 1怀疑是验证集标签和预测框数量对不上导致统计错乱。原因yolo 的混淆矩阵按“预测框和真实框的匹配关系”统计不是按“像素面积占比”统计。每个真实框最多匹配一个预测框未匹配的预测框会被计入背景类同时置信度阈值改变时低置信度的预测框会被分到背景列所以矩阵各行各列的总和会随机改变这是正常的。解决看混淆矩阵时不要关注总和只看对角线上的值。另外要固定一个置信度阈值再对比不同模型的混淆矩阵一般来说用 0.25 置信度和默认 IoU 阈值 0.5。如果你发现eye_closed有不少预测被分到了eye_open说明两者的特征区分度不够需要回去检查标注时是否把半闭眼状态标错了类别。5.4 视频推理闪烁单帧检测在眨眼瞬间误判现象在视频流上做实时检测时同一个人的闭眼框和睁眼框按帧交替出现导致疲劳状态判断在正常和疲劳之间来回跳。原因单帧检测天然存在抖动眨眼瞬间眼睛区域介于睁眼和闭眼之间模型给两者的置信度都接近 0.5阈值微小的变化就会让输出类别翻转。解决不要只用单帧结果下结论在检测后面加一个时序平滑模块。最简单的做法是维护一个长度为 5 到 10 帧的滑动窗口取窗口内出现次数最多的类别作为当前状态。更可靠的做法是计算每帧的 EAR 值用连续帧的变化趋势判断这个方案在下一章展开。5.5 不同版本 yolo 之间迁移权重导致类别错位现象用yolov5训练的权重加载到yolov8继续微调训练曲线正常但推理结果完全没有输出。原因两个版本的类别编号、head 结构和 anchor 配置都有差异。yolov5 的类别从 0 开始计数yolov8 的 decoupled head 也不一样直接迁移时参数名匹配不上实际上等于随机初始化了检测头。解决只迁移 backbone 层的权重或者直接找同版本的预训练权重重新开始。如果你一定要用旧权重做起点手动把类别映射改一遍再转权重非常麻烦实际项目里不值得省这个时间。正规路径是下载和当前 ultralytics 版本匹配的.pt文件一步到位。6. 从检测到判定用帧序列状态机替代单帧硬判疲劳检测的最终输出不应该是一堆检测框而是一个稳定的状态判定。我常用的做法是把 yolo 的检测结果转化为眼睛宽高比 EAR 值再做时序状态机判定。EAR 定义是眼睛的垂直方向距离和水平方向距离的比值正常睁眼时这个值在 0.25 左右闭眼时接近 0。yolo 输出的eye_open框宽高比可以作为 EAR 的近似替代。def get_ear(box): # box 是 [x_center, y_center, w, h]用宽高比近似真实的 EAR 特征 return box[3] / box[2] # h / w睁眼时较大闭眼时趋近于 0 def judge_fatigue(ear_values, ear_thresh0.18, frames_thresh10): # ear_values: 最近 10 帧的 EAR 值列表 # 如果连续超过 frames_thresh 帧 EAR 低于阈值判定为疲劳 closed_count sum(1 for e in ear_values if e ear_thresh) return FATIGUE if closed_count frames_thresh - 2 else NORMAL这个方案的思路是用连续帧的 EAR 序列替代单帧的类别输出frames_thresh取 10 意味着需要约 0.3 秒的持续闭眼才触发疲劳状态可以过滤掉自然眨眼的干扰。实际使用时阈值要根据摄像头安装距离微调眼睛离镜头越远EAR 值整体越低阈值需要下调。配合 yolo 的概率输出做一个简单的加权投票检测稳定性会好很多。这套方案我用了很长时间相对翻车次数少一点的经验是先跑通数据管道再回头调模型顺序不要反。很多人一上来就研究网络结构改进但疲劳检测这种任务真正的瓶颈在于小目标特征和时序信息靠改 yolo 结构收益非常有限。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑