☰
基于Python的人脸关键点疲劳检测:EAR与PERCLOS实战
2026/10/7 13:22:11 网站建设 项目流程

简介:这是一份基于Python的驾驶员面部特征疲劳检测系统毕业设计源码包,面向计算机视觉与嵌入式硬件方向的初学者和毕设学生,用于解决驾驶场景下实时判断疲劳状态并预警的问题。整套方案以OpenCV和Dlib为人脸检测与关键点定位基础,完成视频流采集、眼睛闭合度计算、头部姿态估计,并结合单片机串口通信驱动报警,形成从软件到硬件的完整闭环。压缩包共45个文件,约135.75MB,含10个Python源码、20个XML配置与模型、4个iml工程配置,以及mp3提示音频、dat数据文件和说明文档,目录结构清晰,便于按模块阅读和二次开发。目前已有61人学习下载。对准备毕业设计或入门视觉识别的读者,可据此获得可运行的完整工程、配置文件与语音素材,理解实时检测系统的代码组织、模型调用和串口交互,也便于继续优化算法或调整硬件联动逻辑。

1. 疲劳检测系统到底在检测什么:从68个关键点到一份能用的判定输出

我在帮一家物流企业做ADAS预警方案时,第一版疲劳检测被试用司机吐槽“戴墨镜就狂报警”,第二版又被吐槽“开了四个小时一次都没响”。后来才明白,这类“基于Python的驾驶员面部特征的疲劳检测系统”成不成熟,根本不看你模型多新,而是看你有没有把“眼睛闭合程度、眨眼频率、哈欠、点头”这几路信号放进一个时间窗口里,合出一个稳定到敢报警的结论。对有毕设需求、车载预警需求、考勤防代打需求的人来说,这个标题指向的正是一套可离线运行、不依赖云端的方案:Python负责粘合逻辑,人脸关键点负责信号采集,阈值状态机负责出结论。这篇笔记按我实际落地的路线展开,从Python环境怎么装、关键点怎么取、参数怎么设,一路写到最容易翻车的位置和排查办法。

2. 疲劳判定的核心算法选型:为什么用EAR和PERCLOS,而不是端到端深度模型

车载或边缘设备上做疲劳检测,第一约束不是精度,是延迟和算力。端到端的疲劳分类模型在人脸清晰时效果不差,但它在不同芯片、不同摄像头角度下的迁移成本很高,而且你很难向用户解释“为什么这帧被判疲劳了”。常见做法是走人脸关键点加几何特征这条路:先做人脸检测,再取68点或468点关键点,然后用眼纵横比(EAR)、嘴纵横比(MAR)、眼睑闭合时长这几个几何特征做判定。这条路的好处是每步都可观测、可调参、可复盘,出问题时你能精确说清是哪路信号误判。

2.1 眼纵横比(EAR):六个关键点算出来的一个稳定标量

EAR是这套系统里最核心的单帧特征。它不直接测量“眼睛有多大”,而是测量“眼睛的垂直开度相比水平宽度有多大”。当人闭眼时,垂直距离显著变小,水平距离基本不变,所以EAR会掉到很低。用dlib的68点模型时,左眼取第36到41点,右眼取第42到47点,计算方式见下面的函数:

import numpy as np def eye_aspect_ratio(eye_points): """ 计算眼纵横比。eye_points 是6个关键点坐标列表,顺序按dlib的68点索引排列。 垂直边取两组点:P2-P6 和 P3-P5,水平边取 P1-P4。 """ p1, p2, p3, p4, p5, p6 = eye_points vertical_1 = np.linalg.norm(p2 - p6) vertical_2 = np.linalg.norm(p3 - p5) horizontal = np.linalg.norm(p1 - p4) ear = (vertical_1 + vertical_2) / (2.0 * horizontal) return ear

逻辑说明:EAR是“两次垂直距离的平均 / 水平距离”,这个比值天然对缩放不敏感,所以人脸靠近摄像头或远离摄像头时,EAR的基准值不会剧烈漂移。上面代码里用np.linalg.norm算欧氏距离,比手动开平方写法更清晰,也避免单点抖动对结果造成过大的影响。

参数说明:两个垂直距离取平均,是为了抵消眼皮单侧检测抖动带来的误差。水平距离用p1-p4,也就是内眼角到外眼角的宽度,正常睁眼时这个值相对稳定。闭眼时EAR通常掉到0.2以下,正常睁眼在0.25到0.35之间;不同人脸有差异,所以阈值不能拍脑袋定死,后面会专门讲标定方法。

2.2 嘴部纵横比MAR与头部姿态:哈欠和点头是第二信号通道

只靠眼睛判疲劳,误报率会高到没法商用:有人天生眼睛小,有人习惯性眯眼,有人戴墨镜。所以成熟方案会加两路辅助信号:打哈欠和点头。嘴部纵横比MAR的计算逻辑与EAR完全平行,只是取的嘴部关键点不同。用68点模型时,我一般取第61、62、63、64、65、66、67、68这8个嘴角和上下唇沿线点,或者更简单地取左右嘴角48和54、上唇外侧中点51、下唇外侧中点57:

def mouth_aspect_ratio(mouth_points): """ mouth_points 顺序为 [left_mouth_corner, right_mouth_corner, upper_lip_outer, lower_lip_outer] 对应dlib 68点中的 48, 54, 51, 57。 """ left_corner, right_corner, upper_lip, lower_lip = mouth_points vertical = np.linalg.norm(upper_lip - lower_lip) horizontal = np.linalg.norm(left_corner - right_corner) mar = vertical / horizontal return mar

逻辑说明:正常说话时MAR会有起伏,但打哈欠时嘴部开度会持续超过基线较大比例,且持续1到3秒。所以不能拿单帧MAR做判定,要看它在一个连续时间段内的峰值和持有时长。头部姿态则用来识别“点头”动作,常见做法是调用OpenCV的solvePnP配合人脸关键点做3D到2D的位姿解算,输出俯仰角pitch;连续快速点头时pitch会呈现“低-高-低”的抖动模式。

参数说明:我自己在用时的经验值——MAR告警阈值设为正常说话平均MAR的1.8到2.2倍,且持续超过0.8秒才记为一次哈欠。点头识别的俯仰角阈值按摄像头安装高度调整,装在仪表盘上方平视司机时,pitch角抖动超过6度就值得关注。这里不需要一个完美精确的头部姿态解算,很多商用方案甚至直接用关键点中心点的y坐标变化代替,反而更稳。

2.3 PERCLOS:单帧判定不够,把“闭合时间占比”变成疲劳主指标

EAR和MAR是单帧信号,但疲劳行为发生在几十秒到几分钟的时间尺度上。眼睛闭合时间占比(PERCLOS)正是把帧级信号累积成分钟级结论的标准做法。学术界常用P80准则:当眼睑遮住瞳孔面积超过80%时算闭眼,统计单位时间内闭眼时间占比。工程实现更简单:统计EAR低于闭眼阈值的帧数占比。

from collections import deque # 用环形队列保存最近N帧的EAR,N = 采样fps * 窗口秒数 ear_history = deque(maxlen=30) # 假设60fps,则30帧=0.5秒 def compute_perclos(ear_history, closed_threshold=0.2): if len(ear_history) < 10: return 0.0 closed_frames = sum(1 for ear in ear_history if ear < closed_threshold) return closed_frames / len(ear_history)

逻辑说明:上面的compute_perclos输出的是一个0到1之间的小数,代表最近一个窗口内眼睛处于闭合状态的时间比例。这种“窗口内统计”比单帧低于阈值就报警稳定的多——不会因为某一帧关键点抖动而误报,也不会漏掉持续几秒的微闭眼。deque的maxlen参数实现了滑动窗口,窗口长度决定了判定延迟:窗口越长越稳,但响应越慢。

参数说明:closed_threshold=0.2是个起点参考值,实际项目里我用“正常眨眼谷值”和“刻意闭眼稳定值”的中位数。窗口长度我一般设为4到6秒,因为PERCLOS判定的国际标准采集窗口是这个量级。表1整理了这三路特征的定位差异。

特征输出性质时间尺度主要误报来源计算成本
EAR单帧连续值实时大角度侧脸、遮挡极低
MAR单帧连续值实时说话、吃东西极低
PERCLOS窗口统计值4-6秒睫毛长、小眼睛低(需缓存队列)

3. 搭建可复现的检测环境:从Python安装到人脸关键点模型落地

很多新手拿到源码包后卡在第一步:装不上依赖。这个方向最常见的运行环境是Python 3.8或3.9,配合OpenCV和一个人脸关键点模型。Python安装本身不难,真正让人头大的是dlib的编译问题——如果你直接用pip install dlib在Windows上跑,大概率会撞上漫长的CMake编译过程。这里我给两条路线,你按自己的目标选。

3.1 dlib与MediaPipe两条路线,我为什么建议你先选MediaPipe

dlib是传统疲劳检测项目中使用最广的库,原因是它的68点关键点检测非常稳定,模型文件shape_predictor_68_face_landmarks.dat约95MB左右,社区资料多,论文复现几乎都用它。但坑也很明显:在新版Python上安装dlib需要Visual Studio Build Tools或Xcode命令行工具,编译时间随机器性能波动,快的五分钟,慢的半小时以上,而且报错信息对新手极不友好。如果你做的是课程设计或早期验证,我认为直接绕开dlib更务实。

MediaPipe的FaceMesh模型在Python里安装只需要一条pip install mediapipe,关键点数量更多(468点),对眼睛和嘴部的采样更细。它的缺点是关键点索引和dlib完全不同,网上代码不能直接抄,需要重新查表。以我所见,MediaPipe路线踩坑少,出活快。下面两个安装方案你根据环境任选其一:

# 方案A:MediaPipe路线(推荐,安装快,纯pip) pip install opencv-python mediapipe numpy # 方案B:dlib路线(经典但容易踩编译坑) pip install opencv-python numpy pip install dlib # 如果这里报错,先装CMake并配好VS Build Tools

命令说明:方案A的四个库已经覆盖全部需求:opencv负责摄像头读取和画框,mediapipe提供人脸检测与关键点,numpy负责向量运算。方案B里如果你决定用dlib,除了上面的依赖,还需要额外去官方源下载预训练的关键点模型文件,并将路径写进代码。两条路线都可以跑通这套系统,但后续代码的关键点索引表不一样,别混用。

3.2 最小可跑通的人脸关键点检测代码

装完环境后,第一件事不是写疲劳判定,而是先确认摄像头画面里能实时画出人脸关键点。下面是MediaPipe路线的最小闭环代码:

import cv2 import mediapipe as mp mp_face_mesh = mp.solutions.face_mesh face_mesh = mp_face_mesh.FaceMesh( static_image_mode=False, max_num_faces=1, refine_landmarks=True, min_detection_confidence=0.5, min_tracking_confidence=0.5 ) cap = cv2.VideoCapture(0) # 0是摄像头索引,笔记本一般从0开始 while cap.isOpened(): success, frame = cap.read() if not success: break rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results = face_mesh.process(rgb_frame) if results.multi_face_landmarks: for face_landmarks in results.multi_face_landmarks: # 打印左眼区域的关键点编号,用于后续查表 for idx in [33, 160, 158, 133, 153, 144]: x = int(face_landmarks.landmark[idx].x * frame.shape[1]) y = int(face_landmarks.landmark[idx].y * frame.shape[0]) cv2.circle(frame, (x, y), 2, (0, 255, 0), -1) cv2.imshow("Face Landmarks", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

逻辑说明:MediaPipe处理的是RGB图像,所以先用cvtColor把OpenCV读到的BGR帧转成RGB。face_mesh.process(rgb_frame)返回人脸关键点,每帧468个点,坐标是归一化的0到1之间的浮点数,必须乘以画面宽高才能映射回像素坐标。上面代码先画出了左眼周边的6个点,用来验证关键点检测是否正常工作。

参数说明:refine_landmarks=True很关键,它让MediaPipe额外输出带瞳孔和眼睑细节的468点全集,疲劳检测需要用到眼睑上的密集点。min_detection_confidence和min_tracking_confidence默认0.5,如果检测不稳定可降到0.4,但代价是可能出现关键点跳变。max_num_faces=1是因为司机位只有一个目标,降低计算量。

3.3 用录像文件代替摄像头,先进入代码调试模式

如果你手边没有摄像头,或者不想每次调试都对着屏幕眨眼,用短视频文件做输入是更高效的起步方式。把上一节代码里的cv2.VideoCapture(0)换成文件路径就能跑:

cap = cv2.VideoCapture("test_driver.mp4") fps = cap.get(cv2.CAP_PROP_FPS) total_frames = cap.get(cv2.CAP_PROP_FRAME_COUNT) print(f"视频FPS: {fps}, 总帧数: {total_frames}")

这段代码的价值不是“换了个输入源”,而是引入了FPS和总帧数两个量。疲劳判定的所有时间窗口都依赖FPS:队列长度要用int(fps * window_seconds)来算,眨眼持续时长的帧数也要除以FPS才是秒数。很多人在实时摄像头下调参调不准,就是因为帧率不稳定,而在固定帧率的视频上可以精确复现每一次眨眼和哈欠。所以我调试时总是先跑视频,确认逻辑正确后再接摄像头。

4. 从关键点变成疲劳判定:EAR计算、眨眼状态机与疲劳累积逻辑

现在我们已经能拿到关键点了。接下来的问题是:怎么把“眼睛开度小”变成“闭眼”,把“闭眼”变成“眨眼”,把“眨眼频率下降+闭眼时间变长”变成“疲劳报警”。这一步是整套系统的商业价值所在,也是大多数人代码写得最乱的地方。

4.1 计算EAR的MediaPipe版本与滑窗设计

前面EAR函数用的dlib索引,切到MediaPipe后要换成它的468点索引。我整理过一组稳定可用的点:左眼用 [33, 160, 158, 133, 153, 144] 六个点;右眼用 [263, 387, 385, 362, 380, 373] 六个点。每组的第1和第4个点是内外眼角,第2/3与第5/6点的平均值对应上下眼睑。

import cv2 import mediapipe as mp import numpy as np from collections import deque LEFT_EYE_IDX = [33, 160, 158, 133, 153, 144] RIGHT_EYE_IDX = [263, 387, 385, 362, 380, 373] def landmarks_to_np(face_landmarks, idx_list, w, h): pts = [] for i in idx_list: lm = face_landmarks.landmark[i] pts.append([lm.x * w, lm.y * h]) return np.array(pts, dtype=np.float64) def eye_aspect_ratio(eye_pts): vertical_1 = np.linalg.norm(eye_pts[1] - eye_pts[5]) vertical_2 = np.linalg.norm(eye_pts[2] - eye_pts[4]) horizontal = np.linalg.norm(eye_pts[0] - eye_pts[3]) return (vertical_1 + vertical_2) / (2.0 * horizontal) # 滑窗保存左右眼EAR均值,窗口长度 = 6秒 fps = 30 ear_history = deque(maxlen=int(fps * 6))

逻辑说明:MediaPipe模型对眼睑的采样更细,但上下眼睑点在某些角度下会轻微抖动,所以把两次垂直距离取平均还是必要的。右眼的索引和左眼是镜像关系,顺序不能搞反,否则算出来的EAR是错的但不报错——这是最容易被忽略的隐藏问题。

参数说明:deque(maxlen=int(fps * 6))是滑动窗口,存最近6秒的EAR。窗口太长会导致报警延迟,太短又会把一次短暂眯眼当成闭眼。FPS用实际摄像头帧率填,不要写死30,因为很多工业摄像头的输出是25或60帧。

4.2 眨眼计数:为什么不能靠单帧低于阈值就计数

新手最容易犯的错误是“EAR低于阈值就记一次眨眼”。这样做的结果是:一次正常眨眼因为关键点抖动被记成两三次,而一次持续1秒的微闭眼反而只算一次。我用的方法是有限状态机:从睁眼状态进入闭眼状态记录“闭眼开始”,从闭眼状态回到睁眼状态才记一次完整眨眼,同时要求闭眼持续至少2帧才算数。

class BlinkDetector: def __init__(self, ear_threshold=0.22, min_blink_frames=2, cooldown_ms=300): self.ear_threshold = ear_threshold self.min_blink_frames = min_blink_frames self.cooldown_ms = cooldown_ms self.state = "OPEN" self.closed_frames = 0 self.blink_count = 0 self.last_blink_time = 0.0 def update(self, ear, timestamp_ms): if ear < self.ear_threshold: self.closed_frames += 1 if self.state == "OPEN" and self.closed_frames >= self.min_blink_frames: self.state = "CLOSED" else: if self.state == "CLOSED": # 从闭眼恢复睁眼,且超过冷却时间,才算一次有效眨眼 if timestamp_ms - self.last_blink_time > self.cooldown_ms: self.blink_count += 1 self.last_blink_time = timestamp_ms self.state = "OPEN" self.closed_frames = 0 return self.blink_count

逻辑说明:状态机把“眨眼”定义成完整的“睁-闭-睁”序列,而不是单个低值帧。closed_frames负责过滤掉因关键点跳变产生的单帧低值,cooldown_ms负责防止连续眨眼或眯眼被重复计数。注意这里用毫秒时间戳而不是帧数,因为摄像头帧率可能有波动,帧数在时间上不均匀。

参数说明:ear_threshold=0.22是参考值,实际标定方法放在第6章。min_blink_frames=2针对30fps设计,如果是60fps,建议改为4帧才等效于同一时间长度。cooldown_ms=300模拟了人眼正常眨眼的生理周期,小于这个间隔的闭眼抖动不会被记为第二次眨眼。

4.3 疲劳判定与分级报警:两分钟窗口内做多信号融合

单次眨眼和单次哈欠都不能直接报警,否则司机打个哈欠就被提醒一次,体验会很差。我实际采用的策略是两级报警:一级预警对应“PERCLOS升高但尚未持续很久”,二级报警对应“多路信号同时指向疲劳”。核心逻辑是在两分钟滑窗里同时统计PERCLOS、眨眼频率、哈欠次数三个指标,然后按规则表输出报警等级。

class FatigueAssessor: def __init__(self, window_seconds=120): self.window_seconds = window_seconds self.perclos_series = deque(maxlen=window_seconds * 2) # 每0.5秒一个采样点 self.blink_in_window = deque(maxlen=window_seconds) self.yawn_in_window = deque(maxlen=window_seconds) def add_sample(self, perclos, is_blink, is_yawn): self.perclos_series.append(perclos) if is_blink: self.blink_in_window.append(1) if is_yawn: self.yawn_in_window.append(1) def assess(self): p80 = sum(1 for p in self.perclos_series if p > 0.4) / max(len(self.perclos_series), 1) blink_rate = sum(self.blink_in_window) yawn_count = sum(self.yawn_in_window) if p80 > 0.15 and blink_rate < 5 and yawn_count >= 2: return 2 # 二级报警:疲劳特征明显 if p80 > 0.10 or yawn_count >= 3: return 1 # 一级预警 return 0

逻辑说明:add_sample每0.5秒调用一次,把PERCLOS瞬时值、是否有眨眼、是否有哈欠写进三个对应窗口。assess计算的是窗口内超过0.4的高PERCLOS采样占比,如果同时出现眨眼频率低于每分钟5次且哈欠两次以上,说明司机的眼睑控制力和清醒度都在下降,直接触发二级报警。

参数说明:这里的p80 > 0.15意思是两分钟内超过15%的时间处于眼睑高位覆盖状态,这个数值比单帧阈值严格得多。需要注意,报警阈值在不同人群上差异很大,我后面会讲怎么用真实数据标定,而不是靠经验值硬跑。下面给出的参数组合可以作为第一版首发值,但绝不能永远不变。

5. 避坑专章:这套系统最容易翻车的5个点及对应的排查方案

这一章我按自己实际踩过的坑来写。每个都是“现象-原因-解决”三段式,你在复现时遇到类似问题可以直接照方抓药。

5.1 dlib安装失败或编译极慢

现象:pip install dlib卡在Building wheel长达十几分钟,最后报CMake must be installed to build the following extensions: dlib;或者更隐蔽的,编译成功但import时直接段错误崩溃。

原因:dlib的Python包默认走源码编译,你的机器上没有完整C++构建工具链,或者Python版本过新,dlib的预编译索引里没有对应wheel。在黑匣子一样的编译日志里,新手很难定位到底是缺CMake还是缺Visual Studio组件。

解决:最省事的方案是放弃dlib,改用pip install mediapipe;如果项目必须用dlib,先装好CMake和VS Build Tools,然后打开Visual Studio Installer勾选“使用C++的桌面开发”工作负载,再重试安装。另一个备选是降低Python版本到3.8或3.9,老版本匹配度高,报错概率小很多。

5.2 模型文件路径带中文导致运行时崩溃

现象:程序启动后立即抛出找不到文件的异常,或者中途异常退出,代码逻辑看不出任何问题。

原因:OpenCV和dlib对人脸关键点模型文件的加载底层走C/C++接口,对非ASCII字符路径支持很差。Windows下项目放在“桌面\新建文件夹”这类路径里时,模型加载函数拿到的宽字符路径被截断或转码错误。

解决:把整个项目目录迁到英文路径下,比如D:\fatigue_detection\或~/workspace/fatigue_detection/。我自己的习惯是模型文件单独放在models/子目录,所有相对路径不写盘符,这样换机器也能跑。排查时优先打印model_path的绝对路径,确认没有中文或全角空格。

5.3 摄像头索引不对或打不开

现象:cap.read()一直返回False,程序卡死在读取循环里不报错,或者弹出的窗口是黑屏。

原因:笔记本自带摄像头、外接USB摄像头、视频虚拟摄像头各有自己的设备索引,Windows下自带摄像头是0、外接可能是1或2,如果你先开了OBS或虚拟会议软件,索引会被占用。写死VideoCapture(0)是最大的坑。

解决:写一个小脚本枚举0到4的索引,逐个尝试读取一帧并显示画面,找到实际可用的索引号再写回主程序。另外在打开失败时加一个失败处理分支而不是让程序无提示地空转。

import cv2 def find_camera_index(): for idx in range(5): cap = cv2.VideoCapture(idx) if cap.isOpened(): ret, frame = cap.read() if ret and frame is not None: print(f"可用摄像头索引: {idx}") cap.release() return idx cap.release() raise RuntimeError("未找到可用摄像头")

5.4 逆光与夜间场景EAR抖动导致频繁误报

现象:白天背光或者夜间车内有强光时,眼周关键点在相邻帧间大幅跳变,EAR突然掉到0.1以下,系统开始连环误报。

原因:人脸检测框在逆光下不稳定,导致关键点回归的输入图像出现细微裁切变化;眼睑点本身在低对比度区域的置信度降低。这是几何特征方案的天然软肋,不是代码bug。

解决:我试过有效的手段有三个。第一,对EAR原始值做滑动均值滤波,用5帧或7帧的移动平均压制随机抖动;第二,提高画面亮度或对比度,让眼睑轮廓更明显;第三,把闭眼判定阈值从固定值改成自适应值——用过去60秒的EAR中位数乘以一个系数作为动态阈值,避免光线突变时基准值失效。

5.5 眨眼计数过频或漏计

现象:正常状态下每分钟眨眼频次高达40次,或者闭眼2秒都没被记成眨眼。

原因:前者是状态机缺少冷却时间,眨眼还没恢复就又被触发;后者是min_blink_frames太严格,在低帧率摄像头下,一次正常眨眼只有2帧画面,min_blink_frames=4直接把真眨眼全部过滤掉了。

解决:把眨眼冷却时间设为250到350毫秒,与人类正常眨眼周期对齐;min_blink_frames按实际帧率折算,并调低到1到2帧。验证方法是录制10秒自己眨眼的视频,人工数出眨眼次数,再对比程序输出,误差超过10%就要回查参数。这一步看起来很笨,但却是整条链路里性价比最高的校准动作。

6. 进阶:从“实验能跑”到“现场敢用”的验证与标定方法

6.1 用录制视频回放代替实时摄像头,先把参数调稳

我刚做这个方向时喜欢直接对着摄像头边看边调,后来发现这种做法效率极低——因为你的状态和程序判定之间存在一秒以上的延迟,你很难判断到底是谁错了。现在的做法是先录三段视频:一段正常开车、一段打哈欠、一段模拟疲劳闭眼。然后离线回放,把每帧的EAR、MAR、PERCLOS画成时间序列图。绘图时你会发现横轴帧数动辄上千,坐标刻度挤成一团,也就是“python画图横坐标太密集”的老问题。

import matplotlib.pyplot as plt import numpy as np frames = np.arange(len(ear_history)) xticks_positions = np.arange(0, len(ear_history), max(1, len(ear_history) // 10)) plt.plot(frames, ear_history, label="EAR") plt.xticks(xticks_positions, [str(i) for i in xticks_positions], rotation=45) plt.xlabel("Frame Index") plt.ylabel("EAR") plt.legend() plt.show()

逻辑说明:时间序列图能一眼看出眨眼位置、眨眼深度、闭眼持续时间、基线漂移。横坐标抽稀显示是为了避免每帧一个刻度造成重叠,抽稀步长由总帧数决定。这一步在处理疲劳判定时就像量化交易复盘K线一样——你手里没图,就谈不上调参。

参数说明:抽稀逻辑里max(1, len(ear_history) // 10)保证最多显示10个刻度。如果你习惯在回放时同时看视频,可以把EAR时序图和OpenCV画面放在同一窗口的左右两侧,一边看脸一边看波形,定位速度会更快。

6.2 用真实疲劳数据标定三个阈值:我的血泪经验

最后说标定。疲劳检测的参数不是靠查表查出来的,是用你自己的数据跑出来的。我的做法分三步:第一步,录10分钟正常清醒驾驶视频,统计EAR的5%、50%、95%分位数,把闭眼阈值设在5%分位数附近;第二步,录5分钟频繁打哈欠和闭眼的模拟疲劳视频,调整PERCLOS和哈欠判定阈值,让程序在疲劳段报警、在正常段不报警;第三步,找三个人做盲测,每人正常驾驶和疲劳模拟各10分钟,统计误报率和漏报率。这一步很痛苦但必须做,因为不同人眼皮厚度、眨眼习惯差异极大,用一套固定参数跑到现场就是翻车现场。

我的教训是:永远不要迷信别人代码里的默认阈值,那些数字只代表作者在自己的摄像头和自己的人脸上跑出来的结果。你现在拿到的这套系统,真正的价值不在于开箱即用,而在于它给了你一整套可以调整的信号管道——花半天时间标定,换回来的是现场不会被人戳穿的可信度。希望我的这些踩坑记录能帮你把这段路走得快一点,也欢迎你按这套方法先跑一版自己的参数再上真车。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询