ARTICLE DETAIL

资讯详情

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

基于YOLOv8与面部特征的疲劳驾驶检测实战:从模型训练到时序决策

基于YOLOv8与面部特征的疲劳驾驶检测实战:从模型训练到时序决策 简介这份资源面向计算机视觉学习者、交通安全方向研究者及需要实战项目的开发者提供一套基于YOLOv8与面部特征检测的驾驶员疲劳瞌睡检测完整方案可用于长途与夜间驾驶场景下的实时疲劳监控研究。压缩包共16个文件约24.81MB包含8个Python源码文件、2个模型权重文件、2张效果图、1个音频文件及README、yaml、xml等配置说明覆盖数据预处理、模型训练、推理引擎与结果可视化等模块。目前已有616人学习下载。通过源码可深入理解YOLOv8目标检测流程、面部关键点提取与眼睛开闭状态判断逻辑并借助音频提醒与界面管理模块搭建可运行的检测系统适合作为课程设计、毕业设计或算法改进的实践基础。1. 疲劳驾驶检测到底在检测什么从 YOLOv8 到面部特征的那条链路跑长途的司机都有体会困意上来那几秒眼睛一闭一睁车已经偏出去半个车道。疲劳驾驶检测要抓的就是这个瞬间——不是等事故发生了再报警而是在闭眼、打哈欠、低头这些动作刚出现时就给出预警。市面上不少方案只做单一维度要么只数眨眼要么只做头部姿态误报率高得让人想直接拔电源。基于 YOLOv8 加面部特征检测的思路是把目标检测和关键点回归拼成一条链路YOLOv8 负责在画面里框出人脸和眼睛、嘴巴区域面部特征模块再对这些区域做状态判断最后用时间窗口统计闭眼时长和打哈欠频率。这套组合的好处是鲁棒性比纯关键点方案强戴眼镜、侧脸、夜间红外补光下都能撑住。适合谁做车载 DMS 的嵌入式工程师、拿这个方向做毕业设计的学生、以及想给现有行车记录仪加一层疲劳预警的开发者。源码包里通常包含训练好的权重和推理脚本但真正落地时参数怎么调、阈值怎么定才是决定这套东西能不能用的关键。2. YOLOv8 检测人脸与五官模型选型、数据标注和训练参数2.1 为什么选 YOLOv8n 而不是更大的模型疲劳驾驶检测的部署环境通常是车机或边缘盒子算力有限。YOLOv8n 在 COCO 上 mAP 大概 37 左右参数量 3.2M推理一张 640×640 的图在 CPU 上也能跑到 10 FPS 以上换成 GPU 更是轻松过百。如果直接上 YOLOv8x精度提升有限但延迟和功耗会拖垮整个系统。我一般会先用 YOLOv8n 跑通全流程确认误报率可接受后再考虑换 YOLOv8s 微调。这里有个容易翻车的点很多人拿 COCO 预训练权重直接检测人脸结果发现人脸框根本出不来因为 COCO 里没有专门的人脸类别。正确做法是找一个人脸检测数据集或者自己标注把类别设成 face、eye、mouth 三类重新训练。2.2 用 Labelme 标注后转 YOLO 格式的完整脚本标注工具用 Labelme 还是 LabelImg 都行关键是导出格式要转成 YOLO 需要的 txt。Labelme 默认输出 JSON每个 JSON 里有多边形点集。下面这个脚本把 Labelme 的 JSON 批量转成 YOLO 的归一化坐标格式同时生成训练集和验证集的划分。import json import os import random import shutil # 类别映射顺序要和训练时的 data.yaml 一致 class_map {face: 0, eye: 1, mouth: 2} def labelme_to_yolo(json_path, output_txt_path, img_w, img_h): with open(json_path, r, encodingutf-8) as f: data json.load(f) lines [] for shape in data[shapes]: label shape[label] if label not in class_map: continue points shape[points] # 计算多边形外接矩形 xs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) # 归一化中心点和宽高 x_center (x_min x_max) / 2.0 / img_w y_center (y_min y_max) / 2.0 / img_h width (x_max - x_min) / img_w height (y_max - y_min) / img_h lines.append(f{class_map[label]} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) with open(output_txt_path, w) as f: f.write(\n.join(lines)) # 假设图片和 JSON 在同一目录图片尺寸已知或从文件读取 # 这里演示批量处理逻辑 json_dir labelme_jsons img_dir images out_img_dir dataset/images out_lbl_dir dataset/labels os.makedirs(out_img_dir, exist_okTrue) os.makedirs(out_lbl_dir, exist_okTrue) all_files [f for f in os.listdir(json_dir) if f.endswith(.json)] random.shuffle(all_files) split int(len(all_files) * 0.8) for idx, json_file in enumerate(all_files): base os.path.splitext(json_file)[0] img_file base .jpg img_path os.path.join(img_dir, img_file) if not os.path.exists(img_path): continue # 实际使用时用 PIL 或 cv2 读取图片宽高 import cv2 img cv2.imread(img_path) h, w img.shape[:2] txt_path os.path.join(out_lbl_dir, base .txt) labelme_to_yolo(os.path.join(json_dir, json_file), txt_path, w, h) # 按划分复制到对应目录 subset train if idx split else val shutil.copy(img_path, os.path.join(out_img_dir, subset, img_file)) shutil.copy(txt_path, os.path.join(out_lbl_dir, subset, base .txt))这段代码的逻辑是遍历 Labelme 的 JSON 文件把每个多边形的外接矩形算出来再归一化成 YOLO 需要的格式。参数上要注意class_map的顺序必须和data.yaml里的names完全一致否则训练出来的模型会把眼睛认成嘴巴。另外图片宽高最好从实际文件读取不要硬编码否则换一批分辨率不同的图就全乱了。划分比例 8:2 是常规做法如果数据量少于 500 张建议用 9:1验证集太小会导致评估指标波动大。2.3 训练参数怎么设从 epochs 到学习率的实操建议YOLOv8 的训练命令很简洁但参数背后的含义决定了模型能不能收敛。下面这条命令是我在 2000 张人脸数据上跑通的配置yolo detect train \ dataface_data.yaml \ modelyolov8n.pt \ epochs150 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ patience30 \ augmentTrue \ mosaic1.0 \ mixup0.1 \ device0epochs150是经验值人脸检测任务通常 100 到 200 轮就能收敛。lr00.01是初始学习率如果 loss 震荡厉害就降到 0.001。patience30表示 30 轮没有提升就早停避免过拟合。mosaic1.0开启马赛克增强对小目标检测帮助很大但如果你的人脸数据里有很多大脸可以降到 0.5。mixup0.1是混合增强能提升泛化但太高会让训练变慢。训练过程中要盯着val/box_loss和metrics/mAP50如果 mAP 卡在 0.5 上不去大概率是标注框不准或者类别不平衡。3. 面部特征检测眼睛闭合与嘴巴张开的判定逻辑3.1 EAR 和 MAR 的计算方式与阈值设定YOLOv8 框出眼睛和嘴巴后下一步是判断状态。眼睛闭合用 EAREye Aspect Ratio嘴巴张开用 MARMouth Aspect Ratio。EAR 的计算依赖眼睛的 6 个关键点水平方向两组点距离之和除以垂直方向两组点距离之和的 2 倍。公式看起来简单但关键点从哪来如果 YOLOv8 只输出检测框没有关键点就需要额外接一个面部关键点模型比如 PFLD 或 MediaPipe Face Mesh。我一般用 MediaPipe 拿 468 个关键点再取眼部索引算 EAR。下面是一个计算 EAR 和 MAR 的示例import numpy as np def calculate_ear(eye_points): # eye_points: 6 个关键点的 (x, y) 数组 # 水平距离 horizontal np.linalg.norm(eye_points[0] - eye_points[3]) # 垂直距离两组 vertical1 np.linalg.norm(eye_points[1] - eye_points[5]) vertical2 np.linalg.norm(eye_points[2] - eye_points[4]) ear (vertical1 vertical2) / (2.0 * horizontal 1e-6) return ear def calculate_mar(mouth_points): # mouth_points: 上唇、下唇、左嘴角、右嘴角 horizontal np.linalg.norm(mouth_points[0] - mouth_points[1]) vertical np.linalg.norm(mouth_points[2] - mouth_points[3]) mar vertical / (horizontal 1e-6) return mar # 阈值设定EAR 低于 0.2 视为闭眼MAR 高于 0.6 视为打哈欠 EAR_THRESHOLD 0.2 MAR_THRESHOLD 0.6参数说明EAR 阈值不是固定的不同人眼型差异很大亚洲人普遍眼睛偏细长EAR 基线可能在 0.25 左右闭眼时降到 0.15。所以更稳的做法是动态基线——取前 30 帧的 EAR 均值作为个人基线低于基线的 70% 判定为闭眼。MAR 同理有人打哈欠嘴巴张得大有人只是微张固定 0.6 容易漏报。我一般会同时记录闭眼持续帧数和打哈欠次数闭眼超过 1.5 秒或 30 秒内打哈欠超过 3 次才触发报警这样误报率能压到可接受范围。3.2 把 YOLOv8 检测框和关键点对齐的工程细节YOLOv8 输出的框和 MediaPipe 的关键点不一定完全对齐因为两个模型的输入尺寸和预处理不同。常见做法是先把 YOLOv8 的框裁剪出来再送进关键点模型这样关键点更准。但裁剪会丢失上下文侧脸时容易失败。我的经验是如果算力允许直接对整帧跑 MediaPipe然后用 YOLOv8 的框做区域筛选只保留框内的关键点。这样既利用了 YOLOv8 的定位能力又避免了裁剪带来的信息损失。代码上就是先跑 YOLOv8 得到 face 框再跑 MediaPipe 得到所有关键点最后用框的坐标过滤关键点。注意坐标系要统一YOLOv8 输出的是归一化坐标MediaPipe 输出的是像素坐标转换时别搞反。4. 避坑与排查疲劳检测落地时最容易翻车的五个地方4.1 夜间红外画面下模型直接失效现象白天测试好好的一到晚上或者进隧道YOLOv8 的人脸框就飘了眼睛嘴巴全检测不到。原因训练数据全是可见光图像红外成像的灰度分布和可见光差异巨大模型没见过这种域。解决在训练集里混入至少 20% 的红外或低照度图像用灰度化加直方图均衡做增强。如果实在没有红外数据推理前先做一次 CLAHE 对比度受限自适应直方图均衡能救回一部分。4.2 戴眼镜时 EAR 计算偏差大现象戴眼镜的测试者闭眼时 EAR 还在 0.25 以上系统不报警。原因镜框反光干扰了关键点定位尤其是鼻托和镜片边缘MediaPipe 会把反光点误判成眼角。解决在关键点模型前加一个眼镜检测分支如果检测到眼镜就切换到备用关键点索引或者直接用 YOLOv8 的眼睛框宽高比来辅助判断——闭眼时框的高度会明显压缩。4.3 报警阈值在不同人身上差异巨大现象同一个阈值有人频繁误报有人从来不报。原因每个人的眼型、嘴型、驾驶习惯不同固定阈值不可能通用。解决加一个 30 秒的校准阶段让驾驶员正常睁眼注视前方记录 EAR 和 MAR 的基线值之后用基线的百分比作为动态阈值。这个改动能把误报率降低一半以上。4.4 视频流延迟导致报警滞后现象驾驶员已经闭眼两秒了系统才响。原因推理流水线没有做帧丢弃每帧都跑完整模型GPU 排队导致延迟累积。解决用多线程把取帧和推理分开推理线程只处理最新帧旧帧直接丢弃。如果用的是 OpenCV 的 VideoCapture设置CAP_PROP_BUFFERSIZE1能减少缓冲。另外 YOLOv8 可以用 TensorRT 或 ONNX Runtime 加速推理时间能压到 10ms 以内。4.5 模型在侧脸和低头时漏检现象驾驶员转头看后视镜或者低头调空调人脸框消失系统以为驾驶员闭眼了。原因训练数据里正脸占绝大多数侧脸样本太少。解决标注时特意采集侧脸、低头、抬头、手遮脸等负样本训练时开启 YOLOv8 的旋转增强。另外可以加一个头部姿态估计分支如果俯仰角或偏航角超过阈值就暂时挂起疲劳判断避免误报。5. 从单帧检测到时序决策用滑动窗口把误报压下去单帧判断永远不可靠因为一帧闭眼可能只是正常眨眼。真正让这套系统能用的是时序决策。我一般用一个长度为 30 帧的滑动窗口记录每一帧的 EAR 和 MAR 状态然后算三个指标闭眼帧占比、最长连续闭眼帧数、打哈欠次数。闭眼帧占比超过 0.3 或者最长连续闭眼超过 15 帧按 30FPS 算就是 0.5 秒才触发一级预警如果连续闭眼超过 45 帧1.5 秒触发二级报警。打哈欠次数在 60 秒窗口内超过 3 次也触发一级预警。这个逻辑用 Python 实现就是一个双端队列from collections import deque class FatigueDetector: def __init__(self, window_size30, fps30): self.window deque(maxlenwindow_size) self.fps fps self.yawn_count 0 self.yawn_window deque(maxlen60 * fps) # 60 秒窗口 def update(self, ear, mar): # 判断当前帧状态 eye_closed ear 0.2 yawning mar 0.6 self.window.append(eye_closed) self.yawn_window.append(yawning) if yawning: self.yawn_count 1 # 计算闭眼帧占比 closed_ratio sum(self.window) / len(self.window) # 计算最长连续闭眼 max_consecutive 0 current 0 for closed in self.window: if closed: current 1 max_consecutive max(max_consecutive, current) else: current 0 # 决策 if max_consecutive 1.5 * self.fps: return LEVEL2_ALARM if closed_ratio 0.3 or max_consecutive 0.5 * self.fps: return LEVEL1_WARNING if sum(self.yawn_window) 3: return LEVEL1_WARNING return NORMAL这段代码的关键参数是window_size和fps。窗口太小决策抖动大窗口太大反应迟钝。30 帧在 30FPS 下就是 1 秒兼顾了灵敏度和稳定性。yawn_window用 60 秒是因为打哈欠频率本身就不高窗口太短会把正常深呼吸误判成哈欠。另外注意yawn_count其实没用到直接用sum(self.yawn_window)更准因为队列会自动淘汰旧数据。验证这套逻辑是否靠谱我一般会录一段包含正常驾驶、轻度疲劳、重度疲劳的测试视频跑完之后看报警时间点和人工标注的疲劳事件是否对齐。如果报警总是晚 2 秒以上就把窗口缩短到 20 帧如果误报多就把闭眼占比阈值从 0.3 提到 0.4。这个调参过程没有捷径只能拿真实数据反复试。我自己的习惯是每次改完参数都保存一份配置文件和对应的测试结果不然过两天就忘了哪个参数对应哪个效果。希望这套链路能帮你在疲劳驾驶检测这个方向上少走点弯路。本文还有配套的精品资源点击获取
返回列表