☰
基于YOLOv11的疲劳驾驶检测系统:从数据集训练到UI界面与登录注册完整实现
2026/10/19 3:16:18 网站建设 项目流程

去年有个跑长途的师傅跟我说,他在高速上开夜车,闭了下眼的瞬间车子压到了实线,幸好旁边没车。这种场景不是个例,疲劳驾驶一直是大货车、网约车、自驾游人群绕不开的安全隐患,而市面上真正好用的主动预警设备要么贵、要么安装麻烦。所以当时我决定自己动手攒一套完整的方案——用YOLOv11做核心检测模型,加上YOLO格式数据集训练、疲劳行为判定、检测UI界面、登录注册模块,整体用Python串起来,最终得到一个开箱即用的项目源码和模型包。

这篇文章就把我搭建这套系统的完整过程写出来,包括模型怎么选、疲劳到底怎么“算”、数据集和训练怎么做、UI和登录注册怎么打通,以及部署到真机后遇到的各种幺蛾子。无论你是刚入门的深度学习爱好者,还是想快速落地一套安全预警Demo的开发者,都能从这里找到可以直接复用和二次开发的东西。我不写理论堆砌,只讲实际跑过的流程和踩过的坑。

1. 这套系统到底在做什么:疲劳驾驶不只是“检测到人”

很多人一听“疲劳驾驶检测”,第一反应就是跑个目标检测模型去识别人脸,然后就没然后了。实际上这远远不够。疲劳驾驶检测是一个完整的闭环,它至少包括三件事:先从视频帧里把眼睛、嘴巴、人脸这类目标找出来,再根据时序上的状态变化判断驾驶员是否处于疲劳状态,最后在界面和硬件上给出提醒,整个过程还要兼顾实时性和误报率。

我在这套系统里定义的核心检测目标很简单:人脸、睁眼、闭眼、打哈欠。模型每帧输出这些目标的位置和类别,之后由疲劳判定模块去统计闭眼时长、哈欠频率。为什么不全做“疲劳/正常”二分类直接端到端识别?因为真实驾驶场景里没有那么多现成的“疲劳标签”数据,而眼睛和嘴巴的状态特征是客观、可标注的,模型更容易学,判定逻辑也更加透明可控。比如我可以直接看到某段时间闭眼占比是多少、哈欠出现多少次,而不是接受一个黑盒输出结果。

系统整体上分为五个模块:检测模型、疲劳判定引擎、视频采集与推流、桌面UI界面、用户注册登录。其中UI界面不只是拿来展示检测框,它还要承载实时视频流、疲劳等级指示灯、报警信息、历史记录、用户切换等功能。登录注册模块则是为了区分不同驾驶员,每个人的疲劳阈值和报警偏好可以独立保存,这样一套系统可以给车队里不同司机轮流使用,互不干扰。

这也解释了为什么标题里会出现“UI界面+登录注册界面+Python项目源码+模型”这一整串东西——算法模型只占一半工作量,另一半工作量是工程化。你在网上能下载到很多只有Python推理脚本的YOLO检测项目,但一部署就给不了人用,因为少了交互层。这套系统从一开始就按“可交付产品”的标准来做,而不是按“学习Demo”的标准。

2. 模型选型的底层逻辑:为什么我锁定了YOLOv11

YOLO系列我已经从v5一路用到v11,每次新版本出来都会在项目里拿真实数据跑一遍。选择YOLOv11不是因为它“版本号最新”,而是几个维度综合下来它在这个场景里最合适。先看它相比前代做了哪些改进:

  • 主干网络中引入了新的C3k2模块,替换掉了旧版常用的C3/C2f结构。C3k2在保证轻量的同时,改进了跨阶段特征融合的梯度流,简单说就是用更少的计算量拿到了更丰富的特征,这对小目标——比如驾驶舱里被方向盘挡住的眼睛、侧面偏转的嘴巴——是有实际帮助的。
  • 在较大版本的模型里加入了C2PSA模块,把自注意力机制和跨阶段部分连接结合到了一起。它能在空间金字塔池化之后进一步捕捉全局上下文,让模型对光照不均、面部遮挡的鲁棒性更高。疲劳驾驶场景里最烦人的就是强逆光和反光镜干扰,全局特征理解能明显降低误检。
  • 检测头部分继续采用解耦头结构,分类和回归分支分开处理,收敛更快,后处理时的目标框定位也更干脆。另外它在训练阶段对损失函数和标签分配策略做了优化,对小目标的回归精度提升尤其明显。

我在同一批自制数据集上对比过YOLOv8s和YOLOv11s,在保证相同输入尺寸和训练轮数的前提下,YOLOv11s的mAP@0.5大概能高出1到1.5个百分点,推理帧率基本持平甚至略有提升。这个提升幅度听起来不算夸张,但在疲劳检测这种对漏报零容忍的场景里,多挽回几个关键帧的检测结果就意味着能多触发一次有效报警。

选型时还考虑过更大的YOLOv11m甚至x,但实测下来GPU加速后的延迟增长明显,而识别精度的提高已经进入瓶颈区间。驾驶场景是典型的实时任务,我给自己定下的硬指标是端到端延迟控制在200毫秒以内,否则报警就失去了意义。所以最终我把模型锁定在YOLOv11s,并在earlier版本里保留YOLOv11n作为低算力设备的备选。

模型版本输入尺寸单帧GPU耗时(实测)mAP@0.5(自制测试集)场景适配
YOLOv11n640x640约12ms0.912低配电脑/CPU推理
YOLOv11s640x640约18ms0.934本系统主力模型
YOLOv11m640x640约32ms0.941对精度苛求且算力充裕

3. 疲劳怎么被“算”出来:指标设计、判定逻辑与阈值标定

模型把眼睛和嘴巴的状态识别出来只是第一步,真正决定这套系统好不好用的,是后面的疲劳判定引擎。我常跟人说,疲劳检测算法的本质不是深度学习,而是一套有依据的生理指标工程。这里先记住一个关键概念:PERCLOS,即单位时间内眼睛闭合时间所占的比例,它是目前学术界和工业界公认的、与疲劳程度相关性最强的指标之一。

PERCLOS的典型计算标准是P80,意思是眼睑遮住瞳孔面积超过80%视为一次有效闭合。落到我这种基于目标检测的实现里,我会把模型输出的“闭眼”这一类别当作有效闭合帧,在一个滑动时间窗口内统计闭眼帧数占总帧数的比例。窗口一般取60秒,也可以缩短到30秒以加快响应速度。闭眼占比超过设定阈值就判定为疲劳。这个阈值不是拍脑袋定的,我参考了相关研究的建议区间(0.15到0.25),然后结合自己的测试视频标定到0.2。

除了PERCLOS,我还引入了两个辅助指标:哈欠频率和点头特征。哈欠检测看的是“嘴巴张开持续帧数”和“单位时间内出现次数”,一次有效哈欠通常要持续0.5秒以上,比如25帧左右,如果连续张嘴但每帧都是转瞬即逝,那更可能是说话而不是打哈欠,必须过滤掉。点头特征则需要人脸关键点配合,计算头部俯仰角的变化率,疲劳时驾驶员会不自觉出现逐渐低头再猛然抬起的动作,这种周期性点头模式也是有效信号。

判定逻辑我用一个简单的状态机来管理:正常、疑似、疲劳三级。连续30帧内闭眼比例在10%到20%之间,先触发“疑似”,界面亮黄灯;超过20%或在一个60秒窗口内检出3次及以上哈欠,直接进入“疲劳”状态,亮红灯并发出声音和弹窗报警。这里有很重要的一点:单帧检测结果不能直接触发报警,必须经过时间窗口平滑,否则驾驶员只是眨了一下眼系统就乱叫,用不了几天就会被关掉。

class FatigueJudge: def __init__(self, window_size=30, threshold_closed=0.2, yawn_limit=3): self.window_size = window_size self.threshold_closed = threshold_closed self.yawn_limit = yawn_limit self.closed_history = [] self.yawn_history = [] self.state = "normal" # normal / suspect / fatigue def update(self, eye_closed: bool, yawning: bool): # 维护滑动窗口 self.closed_history.append(1 if eye_closed else 0) self.yawn_history.append(1 if yawning else 0) if len(self.closed_history) > self.window_size: self.closed_history.pop(0) self.yawn_history.pop(0) closed_ratio = sum(self.closed_history) / max(len(self.closed_history), 1) yawn_count = sum(self.yawn_history) if closed_ratio >= self.threshold_closed or yawn_count >= self.yawn_limit: self.state = "fatigue" elif closed_ratio >= self.threshold_closed * 0.5 or yawn_count >= 2: self.state = "suspect" else: self.state = "normal" return self.state

阈值标定这步特别容易被忽略。很多人拿一个开源模型换上就开始用,结果白天正常、晚上狂报。我的做法是:先录制三段真实驾驶场景的视频——一段清醒状态正常驾驶、一段刻意模拟疲劳驾驶、一段包含各种正常眨眼说话的日常操作,然后离线跑完所有数据,画出闭眼比例的分布曲线,再根据分布去选择能把三种场景尽量分开的阈值。不要只盯着训练集上的精度,疲劳检测的阈值必须在真实光照和摄像头角度下重新标定。

4. 数据集与训练实录:从样本收集到模型收敛的每一步

这套系统的检测模型要识别人脸、睁眼、闭眼、哈欠这几个类别,数据和标注的质量直接决定了最终效果。我的数据来源分成两块:一部分来自公开的驾驶行为数据集(主要是高校和竞赛释出的驾驶舱视角视频),另一部分是自己在不同光线条件下补采的样本。公开数据的长处是场景广、数量大,短处是在国内驾驶环境和不同人脸特征上覆盖不足,所以自采数据主要用来“对症下药”——比如口罩遮挡、墨镜、夜晚仪表盘反光这些真实问题。

所有视频抽帧后统一用目标检测标注工具打成YOLO格式的txt文件,一个类别一行,格式是“类别id 中心点x 中心点y 框宽 框高”,坐标全部归一化到0到1。标注规范上我踩过个坑:一开始把“眼睛睁开”和“眼睛闭着”混在一个目标框里,表现为对同一只眼睛有时标睁有时标闭,但框大小完全一样,模型学起来非常纠结。后来我固定了规则:眼睛框以眼裂区域为基准,不管睁闭都标注同一个物理区域,只通过类别id区分状态。这样模型能把“位置”和“状态”解耦,收敛速度快很多。

训练前的数据增强按真实场景来配:Mosaic拼接和随机仿射变换用来提升模型对位置偏移的容忍度;HSV颜色抖动模拟白天黑夜的光照变化;随机遮挡模拟方向盘、遮阳板造成的局部遮挡。另外我还专门做了一个小技巧——把驾驶舱内饰背景图作为负样本混进训练集,数量约占5%,用来压制那些把座椅纹路、方向盘标误检成人脸的假阳性,效果立竿见影。

训练这块我直接用YOLO官方开源库提供的Python训练入口,显存不够就开混合精度。数据集结构按照官方约定组织:images和labels两个目录,各自下面分train和val子目录。训练配置大致如下:

yolo detect train data=fatigue.yaml model=yolov11s.pt epochs=150 imgsz=640 batch=16 lr0=0.01 patience=20 device=0 amp=True

fatigue.yaml里我按照类别顺序填了四个类:face、eye_open、eye_closed、mouth_open。训练过程中我发现两个问题需要特别处理。一个是类别不平衡,打哈欠这类样本天然稀少,虽然不至于让模型完全学不到,但哈欠的召回率明显低于眼睛状态。解法不是粗暴地复制样本,而是对哈欠类做轻度过采样加targeted增强——把哈欠样本进行小幅旋转、亮度变化后再放入训练集。另一个问题是张嘴说话和打哈欠容易混淆,它们在单帧图像上确实很像,模型区分不出来。这个靠模型本身很难彻底解决,我在标注时额外要求打哈欠样本必须包含明显的嘴部拉伸形态,边缘帧不标,同时靠后面的时序判定逻辑去兜底。

最终模型在自制测试集上的指标如下:所有类别的mAP@0.5达到0.934,其中人脸0.982、睁眼0.951、闭眼0.918、哈欠0.886。哈欠类精度略低是预期内的,毕竟它的形态变化大、边界模糊,但满足系统使用需求。训练过程里我设置了每10轮保存一次checkpoint,并且保留best.pt,方便回滚到精度最高的版本,这个习惯建议每个做训练的人都保留。

5. 从模型到产品:登录注册、视频流与检测界面完整打通

模型跑通之后,最花时间的其实是把整套系统包装成“有界面、有账户、能长期用”的产品级应用。我选择用PyQt5做桌面UI,Python写业务逻辑,SQLite做用户存储。之所以不选Web页面,是因为疲劳检测需要实时视频流和本地摄像头,桌面应用在调用硬件、控制延迟上更直接,部署时也不用搭服务环境。

先看登录注册模块。SQLite存用户名和密码哈希,我坚决不在数据库里存明文密码,而是用“随机盐+哈希算法”做存储。每个用户注册时生成一段独立的盐,密码拼接盐后再做SHA-256哈希存储。登录时取出盐重新计算哈希比对,这样即使数据库文件泄露,也不会直接暴露用户的密码习惯。

import sqlite3, hashlib, os def register(username, password): salt = os.urandom(16).hex() hashed = hashlib.sha256((salt + password).encode()).hexdigest() conn = sqlite3.connect("users.db") conn.execute("INSERT INTO users(username, salt, pwd_hash) VALUES (?,?,?)", (username, salt, hashed)) conn.commit() conn.close() def check_login(username, password): conn = sqlite3.connect("users.db") row = conn.execute("SELECT salt, pwd_hash FROM users WHERE username=?", (username,)).fetchone() conn.close() if not row: return False salt, pwd_hash = row return hashlib.sha256((salt + password).encode()).hexdigest() == pwd_hash

登录成功后才进入主检测界面。主界面的实时视频流部分,我没有直接在UI线程里跑OpenCV的读取循环,而是单独起一个检测线程,通过Qt的信号机制把视频帧和检测结果传回UI线程刷新,避免界面卡死。这是PyQt5实时视频最常见的性能陷阱——帧处理一旦阻塞,整个窗口会变成“未响应”。

class DetectThread(QThread): frame_ready = pyqtSignal(QImage) state_ready = pyqtSignal(str) def __init__(self, model_path): super().__init__() self.model = load_model(model_path) self.running = True def run(self): cap = cv2.VideoCapture(0) judge = FatigueJudge() while self.running: ok, frame = cap.read() if not ok: continue dets = self.model(frame, conf=0.35) frame, state, has_yawn = draw_and_judge(frame, dets, judge) img = QImage(frame.data, frame.shape[1], frame.shape[0], frame.strides[0], QImage.Format_BGR888) self.frame_ready.emit(img.copy()) self.state_ready.emit(state) cap.release()

主界面左侧是实时视频区域,右侧是结果面板。结果面板里我放了三块内容:当前检测出的目标列表、疲劳状态指示灯(绿/黄/红三色切换)、报警日志列表。考虑到实际驾驶场景里司机不可能盯着屏幕看,报警必须同时具备视觉、听觉两种通道:进入疲劳状态时主窗口弹出半透明全屏警告,同时调用系统蜂鸣声循环播放。状态从“疲劳”恢复到“正常”之前,报警不会自动停止,这是故意设计的——提醒必须持续到驾驶员真正休息。

做这套UI时还有一个细节:检测框的可视化不是简单画个框,不同类别用不同颜色,脸是蓝色、睁眼是绿色、闭眼是红色、哈欠是橙色。颜色一定要和人直觉一致,闭眼用红色、哈欠用橙色,司机或者测试人员在看回放时能一眼看懂当前报警原因,很多初版UI因为所有框都用一种颜色,回放排错时非常痛苦。

用户管理上也做了联动:登录账号不同,历史报警记录分开存储;登录后的“驾驶员信息”表里可以额外记录该用户的疲劳阈值偏好。多人共用的场景下,A司机习惯闭眼占比到0.18就会明显疲劳,B司机可能0.25才需要提醒,这样避免了对所有用户一刀切导致的误报或漏报。

6. 实测中的意外情况和优化手段:延迟、误报、光线与姿态问题

模型和UI都跑通之后,我把整套系统装到一台带摄像头的笔记本上,又接了一个外接USB广角摄像头模拟驾驶舱视角,开始真刀真枪地测。

第一个遭遇的问题是深度学习的延迟不是唯一的延迟。图像采集、预处理、模型推理、UI绘制、报警响应,每个环节都会累加延迟。我的方案是给视频帧处理做流水线:采集线程只管读帧并放入先进先出队列,检测线程从队列取帧并推理,UI只负责显示队列里的最新一帧。队列一定要有上限,超过上限就丢旧帧,宁肯跳帧也不要让延迟一路积压。这套流水线改造之后,端到端延迟从接近400毫秒降到了180毫秒以内。

第二个问题是黄昏逆光。坐在车里向西开的时候,阳光从挡风玻璃直射人脸,整张脸过曝,眼睛区域几乎看不清。我的应对手段是在预处理阶段加入自适应直方图均衡化,对亮度分布不匀的帧做对比度拉伸,测试下来闭眼检测的召回率提高了不少。更稳妥的方案是用带红外功能的摄像头补光,但普通USB摄像头场景下,预处理已经能解决大部分情况。

第三个问题是眼镜反光造成的误检。有些镜片反射角度会让模型把反光亮点检测成眼睛,而且时睁时闭,状态跳变得很厉害。我用状态机加了一个“最小持续时间”约束:单帧检测到的睁/闭状态至少连续3帧才能改变判定状态,眨眼这种正常生理行为才不会被当成疲劳信号。

第四个大坑是口罩。现在很多人开车也戴口罩,模型在口罩遮挡下巴的情况下一度完全丢失打哈欠特征。这个问题的解法要从训练端下手:收集一批戴口罩开车的样本重新训练,这样哈欠检测不再依赖下巴轮廓,而是依赖颧骨和眼袋区域的肌肉变化。实测下来,戴口罩场景下哈欠检测虽然精度不如不戴口罩,但已经能做到不误报、偶尔漏报的状态。

为了把模型部署到更低成本的设备上,我也做了ONNX导出和半精度量化。导出命令非常简单:

yolo export model=best.pt format=onnx imgsz=640 onnxruntime-gpu engine = ORTInferenceSession("best.onnx", providers=["CUDAExecutionProvider"])

导出后用ONNX Runtime在GPU上实测,推理延迟比PyTorch原生部署降低了大约15%,显存占用也降了一些。如果你要在嵌入式设备上跑,还可以进一步用TensorRT导出engine格式并开启FP16,但要注意量化后闭眼这种精细类别可能掉点,导出后一定要再用测试集回归一遍指标。

最后分享一个我实测时觉得特别高效的验证方法:不要只在实时摄像头上测,先录一段包含各种状态的驾驶行为视频,然后让系统离线回放这段视频,同时记录每一帧的判定结果和触发报警的时间线。这样你可以反复回放同一个场景进行回归测试,改一次阈值、调一次逻辑,立刻就能看到对整条时间线的影响。我靠这个办法把一次偶发的误报场景定位到单独一帧,最后发现是驾驶员低头看手机的动作被误判成了点头——通过限制俯仰角变化速率才把这个误报彻底消掉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询