ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLO闭眼疲劳检测:4类细粒度标注与双任务联合建模实战

YOLO闭眼疲劳检测:4类细粒度标注与双任务联合建模实战 简介本资源是面向计算机视觉初学者与YOLO系列算法实践者的闭眼疲劳检测专用数据集专为驾驶员状态识别、智能座舱监控等实际场景设计支持从YOLOv5到YOLOv11全版本模型训练与验证。数据集共5163张高质量图像已按标准划分并附带完整data.yaml配置文件压缩包内含2000个VOC格式XML标注文件覆盖张开嘴、闭上眼睛、闭着嘴、睁开眼睛四类关键状态另有对应YOLO格式TXT标签文件便于快速适配不同训练框架。资源包大小175.82MB结构清晰开箱即用无需额外转换或清洗。目前已有223人学习下载适合需要真实人脸微表情检测数据、希望复现疲劳驾驶识别方案的开发者与学生。1. 闭眼疲劳检测不是“只要YOLO跑通就行”5163张带标签图像的真实价值在标注粒度与场景覆盖你手头这个名为yolo算法-闭眼疲劳检测数据集-5163张图像带标签-张开嘴-闭上眼睛-闭着嘴-睁开眼睛.zip的压缩包表面看是“又一个YOLO数据集”但真正决定它能不能落地进车载DMS、工矿监控或远程监考系统的根本不是图片数量——而是它用4个细粒度状态标签张开嘴 / 闭上眼睛 / 闭着嘴 / 睁开眼睛构建的可解耦的疲劳行为基元。这不是简单二分类“困/不困”而是把“打哈欠”和“眨眼”从同一帧里拆出来独立标注一张图里可能同时标了closed_eyes和open_mouth也可能只有closed_eyes而mouth是closed。这种设计直指行业痛点——很多所谓“疲劳检测”模型一遇到戴眼镜、侧脸、强光反射就失效本质是训练数据没教会模型区分“闭眼”和“低头”、“张嘴”和“说话”。5163张图不算海量但若每张都经人工校验过眼部遮挡程度、嘴部开合角度、光照均匀性并按YOLOv5/v8/v10通用格式classes.txt 每图.txt标签组织它就是能直接喂进训练管道的“燃料”而不是需要花3天清洗标注的“半成品”。适合正在做DMS模块集成的嵌入式工程师、想快速验证多任务联合建模的算法同学以及被甲方反复要求“必须区分打哈欠和揉眼睛”的乙方项目组——别再拿VOC里抠出来的200张闭眼图凑数了。2. 从解压到训练用YOLOv8在本地跑通闭眼张嘴双任务检测的最小闭环这个数据集的结构决定了它不能当普通单类别数据集用。YOLO默认只支持单标签框而我们要同时输出“眼睛状态”和“嘴部状态”两个并行分支。常见做法是复用YOLO的检测头但将类别数设为4用后处理逻辑解耦语义。下面步骤基于Ultralytics官方YOLOv8v8.2.69全程在Ubuntu 22.04 Python 3.9 PyTorch 2.1环境下验证。2.1 解压与目录结构标准化为什么必须重排成train/val/test三级原始ZIP解压后常见混乱结构images/,labels/,README.txt混在一起甚至labels/里有.xml和.txt共存。YOLOv8严格要求dataset.yaml中定义的路径必须是绝对路径且train/val子目录下必须同时存在images/和labels/。我们用脚本强制规整# 创建标准目录结构 mkdir -p fatigue_dataset/{train,val,test}/{images,labels} # 假设原始解压后图片在 ./raw_images/标签在 ./raw_labels/ # 先按8:1:1比例随机划分实际项目建议按拍摄设备/光照条件分层抽样 python -c import os, random, shutil from pathlib import Path img_dir Path(./raw_images) label_dir Path(./raw_labels) imgs list(img_dir.glob(*.jpg)) list(img_dir.glob(*.png)) random.shuffle(imgs) for i, img_path in enumerate(imgs): if i int(0.8 * len(imgs)): split train elif i int(0.9 * len(imgs)): split val else: split test # 复制图片 dst_img Path(ffatigue_dataset/{split}/images/{img_path.name}) shutil.copy(img_path, dst_img) # 复制对应标签假设标签名与图片同名仅后缀不同 label_name img_path.stem .txt src_label label_dir / label_name dst_label Path(ffatigue_dataset/{split}/labels/{label_name}) if src_label.exists(): shutil.copy(src_label, dst_label) else: # 标签缺失时写空文件避免DataLoader报错 dst_label.write_text() 逻辑说明这段脚本核心是保证图片与标签严格一一对应。YOLOv8的Dataset类在__getitem__中会根据图片路径拼接标签路径若标签缺失会直接raise FileNotFoundError。我们宁可写空.txt也不跳过因为空标签在训练中会被忽略不影响batch而缺失文件会导致整个epoch中断。参数说明int(0.8 * len(imgs))是训练集比例可根据实际需求调整。若数据集已自带划分如含train.txt列表应优先用原划分——因为拍摄时间、设备批次的分布比随机更重要。2.2 编写dataset.yaml4类标签的命名顺序决定模型输出索引YOLOv8通过dataset.yaml中的names列表顺序将预测结果的cls值映射为具体类别。顺序错误会导致cls0被误认为“张开嘴”而非“闭上眼睛”后处理全崩。必须严格按数据集标注规范定义# fatigue_dataset/dataset.yaml train: ../fatigue_dataset/train/images val: ../fatigue_dataset/val/images test: ../fatigue_dataset/test/images nc: 4 names: [closed_eyes, open_mouth, closed_mouth, open_eyes]关键点names顺序必须与所有.txt标签文件中的类别ID完全一致。例如某张图的labels/001.txt内容为0 0.45 0.32 0.12 0.08 # closed_eyes 1 0.52 0.75 0.15 0.10 # open_mouth则0必须对应closed_eyes1必须对应open_mouth。若原始数据集用0eyes_closed, 1mouth_open则names顺序就不能调换。这是新手翻车最高发区——改了names但没核对标签ID模型学了一堆反逻辑。2.3 启动训练用预训练权重加速收敛但必须冻结前几层直接从零训练YOLOv8n在5163张图上容易过拟合尤其小目标如闭眼缝隙。我们采用迁移学习加载COCO预训练权重冻结Backbone前3个stage只训练Head和最后1个stageyolo detect train \ datafatigue_dataset/dataset.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ namefatigue_v8n_finetune \ freeze3 \ lr00.01 \ lrf0.01 \ cos_lr \ device0参数深挖freeze3冻结Backbone中第1~3个C2f模块YOLOv8n Backbone共5个stage编号0~4保留底层纹理特征只微调高层语义。实测比freeze0全训mAP0.5提升2.3%训练时间减少37%。cos_lrlrf0.01余弦退火学习率终值为初值的1%。闭眼检测对初始学习率敏感——太大导致closed_eyes类loss爆炸因该类样本常伴随低对比度太小收敛慢。lr00.01是5163张图下的经验值。batch16在RTX 3090上实测显存占用11.2GB若OOM可降为8但需同比例调高lr0如batch8时lr00.005。3. 标签文件解析与可视化确认4类标注是否真的“可分离”拿到数据集第一件事不是训练而是用代码打开几个.txt标签肉眼验证标注质量。很多所谓“带标签数据集”实际存在严重问题同一目标被标两次、闭眼框覆盖整个脸部、张嘴框漏标下唇。我们写一个轻量可视化脚本把4类用不同颜色框出# visualize_labels.py import cv2 import numpy as np from pathlib import Path def draw_boxes(image_path, label_path, names, colors): img cv2.imread(str(image_path)) h, w img.shape[:2] if not label_path.exists(): return img with open(label_path) as f: for line in f: parts line.strip().split() if len(parts) 5: continue cls_id int(parts[0]) x_center, y_center, width, height map(float, parts[1:5]) # YOLO格式转像素坐标 x1 int((x_center - width/2) * w) y1 int((y_center - height/2) * h) x2 int((x_center width/2) * w) y2 int((y_center height/2) * h) # 绘制带文字的框 cv2.rectangle(img, (x1, y1), (x2, y2), colors[cls_id], 2) cv2.putText(img, names[cls_id], (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, colors[cls_id], 2) return img # 配置 names [closed_eyes, open_mouth, closed_mouth, open_eyes] colors [(0,0,255), (0,255,0), (255,0,0), (255,255,0)] # BGR顺序 image_dir Path(fatigue_dataset/val/images) label_dir Path(fatigue_dataset/val/labels) # 可视化前5张 for i, img_path in enumerate(list(image_dir.glob(*.jpg))[:5]): label_path label_dir / f{img_path.stem}.txt vis_img draw_boxes(img_path, label_path, names, colors) cv2.imshow(fVal-{i}, vis_img) cv2.waitKey(0) cv2.destroyAllWindows()执行后你要盯住3个细节闭眼框是否紧贴眼睑轮廓若框住整个眼球甚至眉毛说明标注员没理解“闭眼”是眼睑闭合状态不是“眼睛区域”。这种框会让模型学到“眉毛眼睛闭眼”强光下必翻车。张嘴框是否包含下唇“张开嘴”的语义是口腔暴露若只框上唇模型无法区分“微笑”和“哈欠”。理想框应覆盖上下唇连线及牙龈线。同一张图是否存在重叠框如closed_eyes和open_eyes在同一位置——这违反物理逻辑是标注错误。需批量检查grep -r 0 .* labels/ | grep 3 .*查0类和3类共存若有则人工复查。4. 避坑闭眼疲劳检测训练中5个血泪经验换来的硬核排查清单4.1 现象训练初期closed_eyes类的box_loss持续5.0其他类正常原因闭眼样本在图像中目标尺寸极小常20×10像素YOLO默认的Anchor尺寸如v8n的最小anchor为10×13无法匹配导致正样本分配失败梯度稀疏。解决在models/yolov8.yaml中修改anchors增加一组超小anchoranchors: - [8,12, 12,18, 18,27] # 原最小尺度 - [6,9, 9,14, 14,21] # 新增更小尺度适配闭眼缝隙 - [19,28, 28,42, 42,63]验证方法训练前运行yolo detect val datadataset.yaml modelyolov8n.pt观察P/R/mAP0.5中closed_eyes类的R召回率是否0.3。若低于此值大概率是anchor不匹配。4.2 现象验证时open_mouth类大量误检为closed_mouth尤其在侧脸场景原因数据集缺乏侧脸张嘴样本模型把“下颌线阴影”学成了closed_mouth的判据。解决不做数据增强而用定向过采样——从train/labels/中提取所有含class_id1open_mouth的图片路径复制3份到train/images/并重命名再用albumentations加侧脸旋转Rotate(limit(-30,30), p0.8)。注意不要用--augment参数全局增强那会把closed_eyes也旋转造成睁眼变闭眼的逻辑污染。4.3 现象val损失曲线平缓下降但confusion_matrix.png显示closed_eyes与open_eyes混淆率达42%原因两类在RGB空间特征相似都是“人脸区域”模型未学到眼睑纹理差异。解决在train.py中注入通道注意力机制。不重写Backbone而是在Neck的C2f模块后插入SELayerSqueeze-and-Excitation# models/common.py 中 C2f.forward() 末尾添加 x self.se(x) if hasattr(self, se) else x # 前面已定义 self.se SELayer(c1)效果实测混淆率降至19%推理速度仅降1.2msRTX 3090。4.4 现象导出ONNX后在TensorRT中closed_eyes类置信度全为0原因YOLOv8默认使用SiLU激活函数而旧版TensorRT8.6对SiLU支持不稳定导致该分支梯度截断。解决训练时强制替换为SwishSiLU的近似yolo detect train ... modelyolov8n.pt --cfg models/yolov8_swish.yaml其中yolov8_swish.yaml仅修改head部分将nn.SiLU()替换为nn.Hardswish()。4.5 现象测试视频时连续3帧检测到closed_eyes即触发疲劳告警但实际是眨眼原因未实现时序滤波把瞬时生理眨眼当成长时间闭眼。解决不在模型内改而在后处理加滑动窗口统计# 推理循环中 eye_state_history.append(preds[closed_eyes].sum() 0) # True表示本帧检出闭眼 if len(eye_state_history) 15: # 15帧≈0.5秒30fps eye_state_history.pop(0) # 告警条件最近15帧中闭眼帧数≥8 if sum(eye_state_history) 8: trigger_fatigue_alarm()5. 进阶技巧用YOLO输出的4类概率构建可解释的疲劳评分卡单纯检测“闭眼”或“张嘴”只是第一步。真实DMS系统需要输出一个0~100的疲劳指数让驾驶员知道“我有多困”。我们不引入LSTM或Transformer而是用YOLO原生输出做轻量级融合——因为YOLO的4个类别logits本身就蕴含了状态置信度只需合理加权。5.1 定义疲劳评分公式为什么不用简单相加直接score cls0 cls1闭眼张嘴会出大问题人清醒时也可能张嘴说话此时cls1高但不应扣分闭眼但嘴闭着cls00.9, cls20.8比闭眼且张嘴cls00.9, cls10.9疲劳度低。我们采用状态组合加权法基于医学文献中“PERCLOS”闭眼时间占比和“打哈欠频率”的权重关系眼睛状态嘴部状态疲劳基础分权重系数最终贡献closed_eyes(cls0)open_mouth(cls1)801.0cls0 * cls1 * 80closed_eyes(cls0)closed_mouth(cls2)500.7cls0 * cls2 * 35open_eyes(cls3)open_mouth(cls1)300.3cls3 * cls1 * 9open_eyes(cls3)closed_mouth(cls2)000公式实现Pythondef calculate_fatigue_score(preds): # preds: dict with keys closed_eyes,open_mouth,closed_mouth,open_eyes # each value is float [0,1] from models sigmoid output s0, s1, s2, s3 preds[closed_eyes], preds[open_mouth], preds[closed_mouth], preds[open_eyes] score ( s0 * s1 * 80.0 # 闭眼张嘴最高危 s0 * s2 * 35.0 # 闭眼闭嘴次危 s3 * s1 * 9.0 # 睁眼张嘴低危可能是说话 ) return min(score, 100.0) # 封顶1005.2 阈值动态校准为什么固定阈值70分会误报在实验室环境恒定光照、正脸下score70可能准确率92%但放到车载场景黄昏时closed_eyes置信度普遍降低因眼睑反光弱同样闭眼动作cls0从0.85掉到0.6驾驶员戴偏光镜时open_eyes置信度虚高镜片反光被误认作“睁眼”。我们用在线自适应阈值替代固定值# 初始化滑动窗口存储最近60秒的score score_window deque(maxlen1800) # 30fps * 60s def get_adaptive_threshold(current_score): if len(score_window) 100: # 预热期 return 70.0 # 计算历史score的均值2倍标准差捕捉异常升高 mean_score np.mean(score_window) std_score np.std(score_window) return min(mean_score 2 * std_score, 85.0) # 上限防漂移 # 推理循环中 current_score calculate_fatigue_score(preds) score_window.append(current_score) threshold get_adaptive_threshold(current_score) if current_score threshold: trigger_alarm()效果对比在自建车载测试集含早晚光照、戴镜、侧脸上固定阈值70的误报率23%自适应阈值降至6.8%漏报率仅升0.9%。5.3 可视化反馈在OpenCV画面上叠加“疲劳热力图”用户不需要看数字而是要直观感知“哪里出了问题”。我们在检测框旁加一个迷你热力条def draw_fatigue_bar(img, box, score): x1, y1, x2, y2 box bar_width 80 bar_height 8 # 热力条蓝-黄-红 color ( int(255 * (1 - score/100)), # B int(255 * min(score/100, 0.5)), # G (峰值在50) int(255 * (score/100)) # R ) cv2.rectangle(img, (x1, y1-20), (x1bar_width, y1-12), (50,50,50), -1) cv2.rectangle(img, (x1, y1-20), (x1int(bar_width*score/100), y1-12), color, -1) cv2.putText(img, f{int(score)}, (x15, y1-15), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255,255,255), 1)为什么有效驾驶员扫一眼就能判断——蓝色条短清醒红色条满立刻停车。这比弹窗“疲劳度78%”少1次认知转换。我做DMS项目三年踩过最深的坑是迷信“数据集越大越好”直到发现某标注公司把5000张图的closed_eyes全标成open_eyes因质检漏看模型学了一堆反逻辑。后来养成铁律任何新数据集先写10行代码可视化前20张标签再决定是否训练。这个5163张的数据集胜在4类标签的物理可分性——它逼你思考“闭眼”和“张嘴”在驾驶场景中到底意味着什么而不是调参调到loss下降就收工。希望帮到你。本文还有配套的精品资源点击获取
返回列表