简介:面向机器学习与计算机视觉学习者的疲劳驾驶检测系统源码,整套资源共29个文件、压缩包约152MB,涵盖核心Python程序、可执行字节码文件、驾驶场景视频样本、XML配置文件、图片素材、人脸关键点模型文件及项目说明,能够支撑从数据准备、算法实现到界面部署的完整流程。已有227人学习下载。源码中包含主要算法与交互界面,视频与图片提供典型驾驶场景,配置和工程文件保留了可复现的环境设置,便于直接导入开发工具运行。通过学习该项目,可以掌握基于面部特征判断疲劳状态的方法,包括人脸关键点定位、眼睛开闭状态识别与头部姿态分析等,同时获得一套结构完整、可改造的真实项目案例,对理解机器学习检测任务的数据处理与工程落地有具体帮助。
1. 疲劳驾驶检测为什么必须用机器学习:从系统设计说起
疲劳驾驶是货运和网约车场景里最难防范的事故诱因之一,靠驾驶员自觉不现实,靠人眼盯监控更不现实。基于Python机器学习的疲劳驾驶检测系统,核心思路是用摄像头采集驾驶员的实时面部状态,通过机器学习模型判断当前是否处于疲劳区间,一旦触发条件就本地告警。这个方向真正有价值的点在于:它不依赖昂贵的硬件,一个普通USB摄像头加一台能跑Python的工控机就能落地。
之所以说「必须用机器学习」,是因为疲劳本身没有一个稳定的物理阈值。心率、方向盘转角、车道偏移都可以作为辅助信号,但最直接的信号是人的面部——眼睛闭合时长、眨眼频率、打哈欠的张口幅度。这些特征用手工规则也能写,但光照变化、戴眼镜、口罩遮挡、不同人脸型会让规则迅速失效。机器学习模型擅长从标注数据里学会「什么状态算疲劳」,而不是靠人硬编码规则。这套系统适合两类人:一类是做车载安全产品落地的工程师,另一类是准备用Python机器学习完成一个完整系统设计的开发者——前者关心误报率和实时性,后者关心从数据到模型到推理的完整链路怎么跑通。
2. 方案选型与检测原理:基于Python机器学习的疲劳驾驶检测系统怎么搭
2.1 基于机器学习的疲劳驾驶检测系统:三种主流方案对比
把一个疲劳驾驶检测系统拆开来看,本质上就是「感知 → 特征 → 判断 → 告警」四个环节。感知环节负责从视频帧里找到人脸并定位关键点,特征环节负责把面部状态量化成可计算的指标,判断环节决定当前状态是否疲劳,告警环节触发声光提醒。在Python生态里,这三个环节有非常成熟的组件可以组合。
最常见的做法是用dlib或MediaPipe做人脸关键点检测,然后用计算出的特征喂给机器学习分类器。dlib的68点人脸关键点检测是经典方案,它输出的人脸关键点编号是公开且稳定的——编号37到42是左眼轮廓,43到48是右眼轮廓,49到68覆盖嘴巴区域。这套编号规则是后续所有特征计算的基础,建议先把68点分布图打印出来贴在手边。
早期很多商用系统直接用规则判断:计算眼睛纵横比EAR(Eye Aspect Ratio),连续多帧低于阈值就判定闭眼。但真实场景里,戴墨镜、逆光、侧脸都会导致关键点检测抖动,纯规则的误报率很难压下去。用机器学习模型替代规则判断,实际上是让分类器学习「什么样的EAR时序模式算疲劳」,而不是死记一个阈值。
方案对比上,目前主流有三条技术路线。第一条是人脸关键点特征 + 传统机器学习分类器(如随机森林、梯度提升树),特征是EAR、PERCLOS(单位时间内眼睛闭合时间占比)、打哈欠频率,优点是训练快、可解释性强、在低算力设备上能实时跑;第二条是CNN端到端方案,直接把面部图像输入卷积网络,输出疲劳/清醒二分类,优点是准确率高,但需要较多标注数据和GPU训练成本;第三条是CNN + 时序模型(如LSTM)捕捉疲劳在时间维度上的累积效应,效果最好,但工程复杂度也最高。
从实际落地角度看,第一条路线是性价比最高的起点。车载环境对延迟和算力有硬约束,传统机器学习分类器在树莓派或Jetson Nano这类设备上都能跑到实时。而且特征工程阶段产出的EAR曲线、PERCLOS统计本身就是可解释的——就算模型误判了,你也能回头查是哪几个特征异常,这在真车调试阶段是巨大的时间节省。
2.2 特征提取与机器学习模型选型:为什么二分类够用
疲劳检测的模型选型,核心要回答一个问题:输出几分类。很多第一次做这个方向的人会把它设计成「清醒 / 轻微疲劳 / 中度疲劳 / 重度疲劳」四分类,看起来很专业,实际一训练就发现数据标注成本翻倍、类别边界模糊、模型在小样本下根本学不进去。
我一般建议从二分类开始:正常 vs 疲劳。原因有两个。第一,驾驶安全场景的决策本质上就是「要不要告警」的二元选择,分级可以留到告警策略层去处理,而不是让模型承担它不擅长的精细区分。第二,二分类的标注一致性更容易保证,两个人标注同一段视频,对「是否疲劳」的判定一致性远高于「疲劳到几级」。
特征方面,最值得优先实现的三个特征是PERCLOS、EAR均值和MAR(嘴巴纵横比)。PERCLOS的计算逻辑是统计一段时间窗口内眼睛闭合帧数占总帧数的比例,这个指标在学术界的疲劳研究中已经被反复验证,是SOTA级别的经典特征。EAR是单帧眼睛开合程度的量化值,反映的是瞬时状态;MAR对应嘴巴开合程度,用于捕捉打哈欠。
除了这三个,还可以加入眨眼频率特征。正常驾驶时人的眨眼频率在每分钟15到20次,疲劳时会出现两种极端——眨眼变慢且单次闭合时间变长,或者出现微睡眠前的高频眨眼。把单位时间内的眨眼次数和平均闭眼时长一起算进去,可以让模型更好地区分「正常眨眼」和「疲劳闭眼」。
模型层面,我常用的是随机森林或LightGBM。随机森林的好处是训练快、对特征尺度不敏感、不容易过拟合,适合特征维度只有十几个的情况;LightGBM的精度通常更高,但对参数调优的依赖更强。如果你用的是scikit-learn,一个包含100棵树、最大深度不超过8的随机森林,在几千条样本上几十秒就能训完,完全够用。
2.3 系统整体架构:从摄像头读到告警输出的数据流
在写代码之前先画清楚数据流,能省掉后面大量返工时间。完整系统的数据流是:视频帧采集 → 人脸检测 → 关键点定位 → 特征计算 → 滑动窗口状态判断 → 疲劳告警。每一步都有延迟预算,加起来不能超过单帧处理周期。
一个容易被忽略的设计点是:人脸检测和关键点检测不要每帧都跑。dlib的人脸检测器比较慢,在CPU上单帧可能要花几十毫秒,而关键点检测相对快。常见做法是每3到5帧做一次全图人脸检测,检测到人脸后把面部区域裁出来,后续帧在上一帧的人脸位置附近做小范围搜索,这样能显著降低平均处理耗时。
告警模块也要提前设计。疲劳告警不能只做一个简单的弹窗,需要分级策略:一旦模型连续N帧判疲劳,第一级是语音提醒,第二级是持续蜂鸣加记录截图。同时要把检测结果和告警日志写入本地文件,这个日志在事后事故分析里至关重要。
3. 数据准备与特征工程:从原始帧到训练样本的完整链路
3.1 用OpenCV与dlib提取人脸关键点和EAR特征
数据准备阶段的目标是:把一段段视频变成一条条带标签的训练样本。样本不是原始图像,而是从每帧图像里算出来的特征向量。这里我用一个实际可运行的脚本来说明,用dlib提取68点关键点并计算左右眼的EAR值。
import cv2 import dlib import numpy as np # 初始化dlib人脸检测器和关键点预测器 detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") def compute_ear(eye_points): # 计算眼睛纵横比:分子是上下眼睑的欧氏距离,分母是眼角的水平距离 p2_p6 = np.linalg.norm(eye_points[1] - eye_points[5]) p3_p5 = np.linalg.norm(eye_points[2] - eye_points[4]) p1_p4 = np.linalg.norm(eye_points[0] - eye_points[3]) return (p2_p6 + p3_p5) / (2.0 * p1_p4) def extract_frame_features(frame): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 用detector返回的人脸矩形区域做关键点定位 faces = detector(gray, 0) if len(faces) == 0: return None landmarks = predictor(gray, faces[0]) # dlib的68点模型中,左眼为42-47,右眼为36-41 left_eye = np.array([(landmarks.part(i).x, landmarks.part(i).y) for i in range(42, 48)]) right_eye = np.array([(landmarks.part(i).x, landmarks.part(i).y) for i in range(36, 42)]) left_ear = compute_ear(left_eye) right_ear = compute_ear(right_eye) return left_ear, right_ear这段代码有三个关键参数值得说明。detector(gray, 0)的第二个参数0是上采样次数,设置为0意味着直接用原分辨率检测,速度更快但小尺寸人脸可能漏检;如果摄像头距离驾驶员较远,可以改成1。compute_ear的公式里分母取两个眼角之间的欧氏距离,这个距离在头部转动时会变化,所以单帧EAR值本身就有噪声,不能只看一帧就下结论。
实际运行时你会发现,EAR在0.25到0.35之间表示睁眼,低于0.2基本就是闭眼。但这个阈值因人而异——眼睛大的人EAR基线高,小眼睛的人基线本来就低。所以特征层面不能只记录原始EAR,还要计算相对值:当前EAR相对这个人历史基线的下降比例,这个相对指标比绝对值更稳定。
3.2 构建训练数据集:噪声数据清洗与标签规范
训练数据的来源决定了模型的天花板。最理想的场景是采集真实驾驶数据,但大多数团队没有这个条件,第一阶段用公开数据集或自己录制的模拟驾驶视频是常见做法。录制时注意覆盖白天、夜晚、戴眼镜、不戴眼镜、微微低头、转头说话等变体,这些变体直接决定模型的泛化能力。
标签规范是整个数据准备里最容易被忽视的环节。你要给每条样本打上0(正常)或1(疲劳),但「疲劳」没有明确边界。我用的标注规则是:眼部闭合时间超过2秒,或连续10秒内总闭眼时长超过50%,记为疲劳。这个规则不需要标注者做主观判断,只做客观计时统计,一致性高很多。
机器学习的噪声数据在这里会出现。dlib关键点检测在强逆光和侧脸时会输出跳变点,导致EAR值突然变成极值,这类噪声样本会直接干扰模型训练。我一般会在标注过程中同步记录关键点检测置信度,把置信度低的帧自动剔除。另一个噪声来源是标签错误——标注疲劳状态时,人眼判断会有延迟,真实疲劳从第3秒开始,但标注者可能从第5秒才开始标。处理办法是给标签两端的过渡区域做模糊处理,也就是把类别边界附近的样本权重降低。
3.3 从特征到训练样本:滑动窗口与时序特征拼接
单帧特征无法判断疲劳,因为疲劳是一个时间维度上的累积状态。我通常的做法是取30帧(约1秒)作为一个时间窗口,把窗口内的所有EAR值、PERCLOS、眨眼次数拼成一个固定长度的特征向量,打一个标签。
import pandas as pd from collections import deque def build_window_samples(ear_stream, window_size=30, step_size=10): """ 将连续的EAR流切分为固定长度窗口,每个窗口聚合为一条样本 ear_stream: 每帧计算得到的(left_ear, right_ear)序列 返回: list of (feature_vector, timestamp_index) """ samples = [] buffer = deque(maxlen=window_size) for i, (left_ear, right_ear) in enumerate(ear_stream): buffer.append((left_ear, right_ear)) if len(buffer) == window_size: arr = np.array(buffer) left_mean = np.mean(arr[:, 0]) # 左眼平均EAR right_mean = np.mean(arr[:, 1]) # 右眼平均EAR left_min = np.min(arr[:, 0]) # 窗口内左眼最小EAR # PERCLOS: EAR低于0.2的帧数占比 closed_ratio = np.mean((arr[:, 0] < 0.2) | (arr[:, 1] < 0.2)) feature = [left_mean, right_mean, left_min, right_min, closed_ratio] samples.append((feature, i)) # 每10帧滑动一次窗口,避免重复帧过多 for _ in range(step_size - 1): if ear_stream: next(ear_stream, None) return samples这个滑动窗口的设计有讲究。窗口取30帧是因为疲劳闭眼通常持续1秒以上,这个窗口能捕获完整的闭合事件;步长取10帧是为了让相邻样本有重叠,增加训练数据的平滑性,同时也保证推理时不会每帧都做重复计算。
特征向量里我放了五个量:左右眼EAR均值、左右眼EAR最小值、闭眼帧占比。最小值比均值更能捕捉短暂闭眼事件,占比对应PERCLOS的直接定义。实际使用时,你还可以加上「窗口内EAR方差」来刻画眨眼频率——疲劳时眨眼频率会下降,方差会明显变小。特征不是越多越好,先上五个核心的,等模型跑通再逐步加。
到这里,data preparation部分的产物就是一条条特征向量和对应的标签。把几百条样本整理成csv文件,feature_0, feature_1, feature_2, feature_3, feature_4, label的格式,后面训练脚本直接读就行。
4. 模型训练与系统设计源码:从训练脚本到检测主循环
4.1 训练一个轻量级疲劳检测模型:核心训练代码
特征准备好后,模型的训练代码其实很简洁。scikit-learn的随机森林分类器是零调参也能出结果的选择。下面这段代码可以直接跑,前提是你已经有上面生成的csv样本文件。
import pandas as pd from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, confusion_matrix # 读取样本数据 df = pd.read_csv("fatigue_samples.csv") # 假设csv最后一列是label,前面是特征 X = df.iloc[:, :-1].values y = df.iloc[:, -1].values # 划分训练集和验证集,stratify保证正负样本比例一致 X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 随机森林:100棵树,最大深度8,限制叶子节点数防止过拟合 clf = RandomForestClassifier( n_estimators=100, max_depth=8, min_samples_leaf=5, n_jobs=-1, random_state=42 ) clf.fit(X_train, y_train) # 验证集评估 y_pred = clf.predict(X_val) print(classification_report(y_val, y_pred, target_names=["normal", "fatigue"])) print(confusion_matrix(y_val, y_pred)) # 训练完成后保存模型,供推理模块加载 import joblib joblib.dump(clf, "fatigue_model.pkl")模型参数这里解释一下几个关键项。max_depth=8这个限制是我调过多次的经验值——疲劳检测的特征维度不高,树太深会记住训练集里的噪声;min_samples_leaf=5强制每个叶子节点至少包含5个样本,这相当于一个平滑约束,能显著降低标签噪声的影响。n_jobs=-1让所有CPU核心并行训练,数据量不大时可以忽略。
训练阶段特别值得关注的是classification_report的输出。你最需要看的是recall这一列——疲劳样本的召回率必须高。在安全场景里,漏报一次疲劳可能意味着事故,而误报一次只是打扰驾驶员。如果召回率低于0.9,优先检查样本是否平衡,其次考虑增加特征或调低分类阈值。随机森林的predict_proba输出可以配合阈值调整使用,后面章节会讲到。
4.2 基于Python的疲劳驾驶检测系统源码结构:推理主循环与告警触发
模型训练完成后的系统设计源码,核心是一个实时推理主循环。它负责持续读帧、提取特征、喂给模型、根据输出触发告警。下面给出一个完整的框架,代码可直接作为系统骨架。
import cv2 import time import joblib import numpy as np from collections import deque # 加载训练好的模型和人脸关键点检测器 clf = joblib.load("fatigue_model.pkl") detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") # 状态管理:特征缓冲区和告警计数器 feature_buffer = deque(maxlen=30) fatigue_counter = 0 ALARM_THRESHOLD = 5 # 连续5次模型判疲劳才触发告警 FRAME_SKIP = 3 # 每3帧做一次全图人脸检测 cap = cv2.VideoCapture(0) frame_idx = 0 face_rect = None # 缓存上一帧的人脸位置 while cap.isOpened(): ret, frame = cap.read() if not ret: break frame_idx += 1 # 每隔FRAME_SKIP帧做一次全图检测,中间帧在上一帧位置附近搜索 if frame_idx % FRAME_SKIP == 0: gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray, 1) if len(faces) > 0: face_rect = faces[0] else: # 只在上一帧人脸区域周围微调,减少计算量 if face_rect is not None: gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # dlib的get_faces_in_region可限定区域检测 face_rect = get_face_in_region(gray, predictor, face_rect) if face_rect is None: continue # 提取特征并填充缓冲区 features = extract_frame_features(frame, face_rect) feature_buffer.append(features) if len(feature_buffer) < 30: continue # 当前窗口的特征聚合 window_feat = aggregate_window(feature_buffer) prob = clf.predict_proba([window_feat])[0][1] # 疲劳类概率 # 连续告警计数,防止偶发误判 if prob > 0.7: fatigue_counter += 1 else: fatigue_counter = max(0, fatigue_counter - 1) if fatigue_counter >= ALARM_THRESHOLD: trigger_alarm(frame) fatigue_counter = 0 # 实时可视化,便于调试 cv2.imshow("fatigue_detection", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这段代码里最核心的设计是fatigue_counter计数器和FRAME_SKIP的配合。fatigue_counter不是简单的连续帧计数——当模型输出疲劳概率低于阈值时,计数器只减1而不是归零,这个机制的工程意义很大:它能容忍偶尔一两帧的关键点抖动或角度变化,不会让一次瞬时误判直接触发告警,同时也不会因为一帧正常就把之前的疲劳累积清零。
FRAME_SKIP=3是平衡检测频率与延迟的经验值。全图人脸检测是主要性能瓶颈,每3帧做一次全图检测能减少三分之二的人脸检测调用;中间两帧用上一帧的人脸位置做局部搜索。extract_frame_features和aggregate_window是复用第3章的函数,这里不再展开。
prob > 0.7这个阈值是可调的。模型输出的原始概率并不是理想的0.5分界点,因为训练数据里正负样本比例可能不均衡。把阈值抬高到0.7意味着模型要更「确信」才判定疲劳,这会降低误报率但可能提高漏报率。具体调到多少,取决于你是在做商用车(宁误报不漏报)还是乘用车(误报体验差),建议后续用实际路测数据做阈值扫描。
4.3 告警机制与日志记录落地
告警模块的源码设计需要注意和主循环解耦。直接在推理循环里写print或beep虽然简单,但会让后续扩展受限。我会把告警逻辑封装成一个独立的类,用事件回调的方式触发。
import time import json import threading from datetime import datetime class AlarmManager: def __init__(self, log_path="alarm_log.jsonl"): self.log_path = log_path self.alarm_cooldown = 30 # 两次告警的最小间隔秒数 self.last_alarm_time = 0 def trigger(self, frame, probability): now = time.time() if now - self.last_alarm_time < self.alarm_cooldown: return self.last_alarm_time = now # 记录告警时间、概率和图像路径 record = { "time": datetime.now().isoformat(), "probability": round(probability, 3), "frame_path": f"alarm_{int(now)}.jpg" } cv2.imwrite(record["frame_path"], frame) with open(self.log_path, "a", encoding="utf-8") as f: f.write(json.dumps(record, ensure_ascii=False) + "\n") # 告警动作:蜂鸣或语音,按平台选择 threading.Thread(target=self._beep, daemon=True).start() def _beep(self): # Windows下可用winsound,Linux下用subprocess调aplay import subprocess subprocess.run(["aplay", "alarm.wav"], capture_output=True)日志用JSONL格式(每行一条JSON记录)是这套系统里容易被忽略但很重要的设计。它直接为后续的模型评估和误报分析提供了原始数据——哪天用户抱怨误报太多,你可以打开日志定位具体是哪些时间段的哪些帧触发了误报,把对应截图翻出来分析。没有日志的疲劳检测系统,在真车上调试时基本是黑匣子。
alarm_cooldown这个参数也值得说:它防止系统在驾驶员已经疲劳但还没停车的状态下每秒钟重复报警。30秒的冷却窗口既保证了告警的持续性,也避免了声音对驾驶员的过度干扰。窗口内再次检测到疲劳会跳过本次告警,但日志仍然记录。
5. 疲劳驾驶检测系统避坑指南:5条实打实的踩坑记录
5.1 现象一:车载环境误报率飙升
测试阶段用办公室环境开发,误报率控制得很好,一搬上真车就疯狂误报。办公室的光线稳定、人脸正对摄像头、背景干净;真车上阳光从侧窗打入、驾驶员微微转头看后视镜、背景有树影晃动,每一个变化都会让人脸关键点抖动,进而把EAR曲线拉出异常值。误报多的直接原因是特征抖动,而特征抖动的根因是关键点检测不够稳定,尤其在下巴边缘和眼角位置。
解决思路分两层。第一层是特征层面,把EAR的原始值改成滑动平均,用过去5帧的均值替代当前帧的瞬时值;第二层是判断层面,把单帧闭眼判定改成「连续3帧内至少2帧闭眼才算闭眼事件」,这样偶发的单帧跳变就不会被计入PERCLOS统计。这两层都在不增加计算量的前提下把误报降下来。
5.2 现象二:夜间场景关键点检测失效
夜间的误检率反而比白天低,但漏检率很高。原因不是光线暗导致摄像头拍不到人,而是dlib的检测器在低照度下人脸检测框不稳定,经常丢帧。解决办法不是换模型,而是加一个红外补光或使用支持弱光增强的摄像头,把输入图像的暗部细节提上来。软件层面还可以在进入检测前做一次直方图均衡化,这个操作对夜间人脸检测的提升很明显。
5.3 现象三:眨眼判断把正常眯眼当成疲劳
戴眼镜的驾驶员在反光时,关键点会把镜框边缘误判为上眼睑,导致EAR值一直偏低,系统把正常的眯眼视作疲劳闭眼。解决方法是加入人脸关键点置信度过滤——dlib没有直接输出置信度,但可以通过检测框尺寸和上一帧的位置偏移做间接判断。当关键点位置在相邻帧发生超过阈值的大跳变时,丢弃该帧特征而不参与窗口统计。同时把ALARM_THRESHOLD提到5帧以上,给瞬时误判留出缓冲。
5.4 现象四:模型在真车上延迟飙到500ms
实验室里跑的是单帧处理,没人注意累计延迟。到了真车上一统计,端到端延迟居然到了500毫秒多。定位后发现瓶颈不在模型推理,而在人脸检测的全图扫描——每帧都调detector(gray, 0),CPU负载一上来,视频帧排队,总延迟就被拖垮了。解决办法就是前面代码里的FRAME_SKIP策略,再配合只在上一帧人脸位置周边扩张一定像素范围做局部检测。优化后单帧处理时间从80ms降到了15ms,效果非常明显。
5.5 现象五:训练集和测试集分布不一致
模型在验证集上准确率很高,上路后表现断崖式下降。查了一下问题出在数据泄漏和分布不一致上——训练数据全是在同一时间、同一背景、同一摄像头角度下采集的,模型学到的是「这个环境下的状态」,而不是通用的疲劳特征。验证集由于來自同一段视频的不同帧,和训练集高度相似,准确率失真了。解决方法是按视频文件划分训练集和验证集,而不是按帧随机划分——同一段视频的所有帧只能进一个集合,这样才能评估模型对新场景的真实泛化能力。疲劳检测样本的真实分布远比想象中复杂,训练集里欠采样夜间和戴眼镜场景的话,模型基本等于没学过。
6. 让检测更可靠:阈值自适应与离线验证技巧
模型上线后不等于工作结束。疲劳检测系统最大的特点是:个体差异远大于模型训练时覆盖的样本差异。有人天生眼睛小,EAR基线就是0.18,一个按照标准数据训练的模型会把他的正常状态误判为疲劳。解决这一问题的思路是「阈值自适应」,也就是在系统启动后的前30秒采集该驾驶员的面部特征基线,再根据基线动态调整分类阈值或对EAR特征做归一化。
阈值自适应的实现有两种做法。简单做法是计算启动阶段EAR均值作为基线,后续把当前EAR除以基线得到相对EAR,用相对值替代绝对值送入模型。这种做法适合特征维度低、模型简单的情况,改动也最小。复杂做法是保留模型的原始输入,但把predict_proba的判定阈值从固定0.7改成「基线超出均值一个标准差时降低到0.5」——本质是让模型在不同敏感度之间切换。两种做法都不需要重新训练模型,我一般先用简单做法验证效果,不够再上复杂方案。
离线验证这件事也要养成习惯。每次改完特征或调完参数,不要只盯着验证集准确率,把测试视频整段跑一遍推理,看告警时间点和手工标注的时间点有多大的时间偏移。具体做法是把告警日志和视频时间轴对齐,逐秒比对。我做过一次分析,发现模型对疲劳的判定平均滞后了3.2秒——原因是滑动窗口取30帧本身就引入了约1秒延迟,再加上fatigue_counter连续5帧判定又增加了延迟。这个滞后值不算致命,但对告警体验有影响,后来把窗口从30帧减到20帧,滞后压缩到了1.8秒,误报率没上升。
如果你正在做的项目对告警延迟有硬指标要求,比如必须在闭眼后2秒内发出告警,那滑动窗口长度和连续判定帧数需要一起调。这里有一个血泪教训:窗口太长,滞后大;窗口太短,单帧噪声直接导致误判。比较好的平衡点是窗口20帧、计数器阈值4帧,配合特征层面的滑动平均,在实测中既保持了低误报,又把延迟压在了1.5秒左右。
另外想提醒一点:疲劳检测模型一定要做「连续长时间运行」的稳定性测试。我遇到过运行4小时后内存持续增长的问题,排查后发现是OpenCV的imshow窗口和每次告警保存的截图没有释放干净。视频帧循环本身不会泄漏,但告警截图和log文件句柄如果没关,长时间跑就会把内存占满。解决方法是给告警保存逻辑加上文件句柄的with open管理,并限制截图保留条数。
这套系统的完整落地路径是:先用dlib提取关键点特征,训练一个随机森林模型,再把它塞进实时推理主循环,最后用阈值自适应和离线验证把误报和延迟调到一个可接受区间。每一步都有独立的验证标准,不需要一次到位。从零开始到跑通第一版,数据量不需要太大,几百条标注样本就能得到一个有实用价值的模型——关键在于特征是不是真能代表疲劳状态,以及你的告警逻辑是否能容忍真实场景的噪声。希望这些基于真实踩坑经验的方案对你的系统设计有帮助。
本文还有配套的精品资源,点击获取