首页 / 资讯中心 / 文章详情

dlib 68点疲劳驾驶检测:EAR/MAR/PERCLOS与阈值标定避坑

dlib 68点疲劳驾驶检测:EAR/MAR/PERCLOS与阈值标定避坑 ★ FEATURED ARTICLE
简介面向计算机视觉课程设计、毕业设计与疲劳检测入门实践的文档围绕基于 dlib 的疲劳驾驶检测系统展开重点解决面部特征点提取、眨眼与打哈欠统计、点头行为识别及疲劳状态判定等设计问题。压缩包内为 1 个 docx 文件约 1.13MB内容按摘要、导论、相关技术、系统分析、系统设计等章节组织呈现从研究背景、PERCLOS 原理到需求分析、整体框架与可行性论证的写作脉络。已有 1958 人学习适合需要参考论文结构、算法选型与系统设计表达的学生和开发者。文档对 dlib 的 68 个面部特征点、长宽比计算、人体姿态估计及面部采集流程均有展开可用于梳理疲劳检测方案的技术路线、对照章节模板查漏补缺并辅助完成课程设计或毕业设计中的系统分析与设计部分。1. 一段夜路视频跑出来的报警靠的到底是什么深夜的驾驶模拟舱里摄像头正对着一张脸。屏幕上同时叠着 68 个绿色小点眼睛处的两个椭圆随着眨眼一次次压扁嘴巴那条线在第三个哈欠时冲到 0.62右上角计数器跳到 3警示音响起。这就是一套典型的基于 dlib 模型的疲劳驾驶检测系统在做的事用计算机视觉把困了这个模糊感受翻译成 EAR、MAR、头部俯仰角三组能比较大小的数字再交给 PERCLOS 判据下结论。它适合谁做疲劳检测方向毕业设计的同学、想给车载 Demo 加一层非接触式监控的开发者、以及刚入门人脸关键点、想把程序设计从课设做成能完整演示的系统的程序员。硬件门槛极低——一个普通 USB 摄像头加一台能跑 Python 的笔记本不需要红外相机也不需要贴电极。2. 68 个点怎么变成三组数字EAR、MAR、HPE 的计算口径整套系统的地基就一句话dlib 的 68 点模型给出面部关键点的像素坐标剩下的全是几何。听起来简单但真正决定成败的不是算法本身而是你对哪几个点、怎么连、除以谁这几个问题的取舍。同一条 68 点结果索引取错一个EAR 曲线立刻失真分子分母定义不一致换个人种、换副眼镜阈值就得推倒重来。这一章把三个指标的计算逻辑钉死后面所有调参才有参照物。2.1 环境与模型文件先让单帧跑通再谈实时dlib 在 Windows 上编译是新手第一道坎常见做法是避开源码编译直接用预编译轮子或者走 conda 渠道。Python 版本别追新3.8 到 3.10 之间兼容性最稳太新的解释器经常找不到对应轮子报一堆找不到编译器的错。# 先装基础图像与数值依赖 pip install opencv-python numpy scipy # dlib 单独装能拿到轮子就别编译 pip install dlib # 如果上面这条开始疯狂编译并最终失败退回 conda 渠道 # conda install -c conda-forge dlib三段命令里前两行是纯 Python 包没有编译风险dlib那行才是关键。它一旦进入源码编译机器上必须同时具备 CMake 和 C 构建工具链缺一个就会在中途中断。判断是否踩坑很简单看安装日志里有没有出现Building wheel for dlib之后长时间无输出出现了就说明在本地编译此时切 conda 渠道通常几十秒解决。模型文件只需要一个dlib 公开的 68 点人脸关键点预测器解压后放在工程根目录路径写进代码。建议目录结构按下面这样切后面调试阈值时不至于满项目找文件fatigue_demo/ ├── models/ │ └── shape_predictor_68_face_landmarks.dat ├── core/ │ ├── metrics.py # EAR / MAR / HPE 三个计算函数 │ └── detector.py # 人脸检测与关键点封装 ├── ui/ │ └── app.py # 可视化界面与报警 ├── tools/ │ └── calibrate.py # 阈值标定脚本 └── main.py把计算函数单独放一个文件是整个工程最值钱的一个决定。阈值标定、回归测试、界面刷新都要反复调用这几个函数混在主循环里写后面想做批量回放几乎要重写。2.2 EAR 与 MAR索引错一位曲线全废dlib 的 68 点有固定分区0 到 16 是下颌轮廓17 到 21 是右眉22 到 26 是左眉27 到 35 是鼻梁与鼻尖36 到 41 是图像左侧眼睛42 到 47 是图像右侧眼睛48 到 59 是嘴部外轮廓60 到 67 是内轮廓。注意图像左侧是被检测者本人的右眼写注释时最好标清楚否则调参时自己都会绕晕。EAR 的思路是把眼睛看成一个六边形用上下眼睑的垂直距离除以内外眼角的水平距离眼睛睁大比值升高闭眼时垂直距离趋近于零比值塌下来。import numpy as np def eye_aspect_ratio(eye_pts): eye_pts: 长度为 6 的数组顺序必须是 [p1, p2, p3, p4, p5, p6] 对应 dlib 左眼 36-41 或右眼 42-47 的自然顺序 # 垂直方向两组距离上眼睑中点到下眼睑中点 v1 np.linalg.norm(eye_pts[1] - eye_pts[5]) v2 np.linalg.norm(eye_pts[2] - eye_pts[4]) # 水平方向内外眼角距离 h np.linalg.norm(eye_pts[0] - eye_pts[3]) if h 1e-6: # 关键点重合时兜底避免除零炸掉整帧 return 0.0 return (v1 v2) / (2.0 * h) def mouth_aspect_ratio(mouth_pts): mouth_pts: dlib 48-59 共 12 个外唇轮廓点 # 上下唇中点距离 / 左右嘴角距离 v np.linalg.norm(mouth_pts[3] - mouth_pts[9]) # 51 与 57 h np.linalg.norm(mouth_pts[0] - mouth_pts[6]) # 48 与 54 if h 1e-6: return 0.0 return v / heye_aspect_ratio里取了两组垂直距离再平均是为了抗单侧眼睑被眼镜框遮挡导致的跳变分母乘 2 只是归一化不影响阈值之间的相对关系。mouth_aspect_ratio用了最简版本只用外唇的上下中点与两个嘴角。想让打哈欠的判别更稳可以把内唇 60 到 67 也算一条 MAR取两者的较大值张嘴时内唇比值通常涨得更早但闭嘴时噪声也更大是否叠加取决于你的摄像头分辨率。一个必须提前想清楚的问题眼睛比例对头部俯仰角极其敏感。低头看仪表盘时即使眼睛完全睁开EAR 也可能掉到 0.15 以下直接触发误报。所以 EAR 判定不能只看数值必须和头部姿态耦合这一点在第 4 章的阈值设计里会展开。2.3 HPE 头部姿态solvePnP 的四个输入一个都不能想当然头部姿态估计不走机器学习是标准的 PnP 解算已知若干面部点在真实世界里的三维坐标又知道它们在图像里的二维像素坐标求解相机的旋转和平移旋转矩阵再转成欧拉角就得到俯仰、偏航、滚转三个角度。点头对应俯仰角左右张望对应偏航角歪头对应滚转角。三维模型点用一套通用的人脸刚体近似坐标单位毫米原点是鼻尖import cv2 import numpy as np # 6 个稳定点的三维近似坐标鼻尖、下巴、两个眼角、两个嘴角 MODEL_POINTS np.array([ (0.0, 0.0, 0.0), # 30 鼻尖 (0.0, -63.6, -12.5), # 8 下巴 (-43.3, 32.7, -26.0), # 36 图像左眼角 (43.3, 32.7, -26.0), # 45 图像右眼角 (-28.9, -28.9, -24.1), # 48 图像左嘴角 (28.9, -28.9, -24.1), # 54 图像右嘴角 ], dtypenp.float64) def head_pose_angles(landmarks, frame_w, frame_h): landmarks: dlib 68 点列表每个元素是 (x, y) idx [30, 8, 36, 45, 48, 54] image_points np.array( [(landmarks[i][0], landmarks[i][1]) for i in idx], dtypenp.float64 ) # 简易相机内参焦距按图像宽度估主点取画面中心 focal frame_w camera_matrix np.array([ [focal, 0, frame_w / 2.0], [0, focal, frame_h / 2.0], [0, 0, 1.0], ], dtypenp.float64) dist_coeffs np.zeros((4, 1)) # 不标定畸变时置零别传 None ok, rvec, tvec cv2.solvePnP( MODEL_POINTS, image_points, camera_matrix, dist_coeffs, flagscv2.SOLVEPNP_ITERATIVE ) if not ok: return None rmat, _ cv2.Rodrigues(rvec) proj np.hstack((rmat, tvec)) # 第 7 个返回值是欧拉角顺序为俯仰、偏航、滚转 euler cv2.decomposeProjectionMatrix(proj)[6].flatten() return euler[0], euler[1], euler[2]四类输入逐条说清楚。相机内参用图像宽度当焦距是工程上最常见的近似误差在 10% 量级用来判断头低没低完全够但如果要精确到度数还是老老实实做一次棋盘格标定。畸变系数必须给一个形状正确的零数组传None在某些 OpenCV 版本里会直接抛异常。三维模型点的尺度不必是真实毫米只要六个点之间的相对比例对解出来的角度方向就可用。姿态解算标志位选迭代法对 6 组点这种小规模输入收敛稳定速度快。欧拉角的符号是这套代码里最容易翻车的地方。不同版本的返回值可能会有整体正负翻转判断标准只有一个真人低一次头看俯仰角是变大还是变小然后决定阈值写还是。我一般会在第一次跑通时打一行日志把这六个点画在画面上肉眼确认点的位置和编号对得上再去做符号校准。2.4 PERCLOS 的统计窗口逐帧判定只会得到满屏报警PERCLOS 是单位时间内眼睛闭合时长占比的意思它天生是一个滑动窗口统计量不是逐帧布尔值。常见做法是维护一个长度为 N 帧的队列每帧记录该帧是否闭眼窗口内的闭眼帧数除以 N 就是当前 PERCLOS。N 取多少取决于帧率30 帧每秒的摄像头一分钟窗口对应 1800 帧内存能吃但响应会滞后好几秒实用区间通常在 1 到 3 秒也就是 30 到 90 帧之间。统计量物理含义常用窗口观察到的现象EAR 均值眼睛开合瞬时程度单帧眨眼时快速下探后回升PERCLOS窗口内闭眼帧占比30 到 90 帧疲劳时持续抬升眨眼次数窗口内 EAR 穿越阈值的次数约 60 帧疲劳时变密或变稀MAR 峰值单次张嘴最大开合度单帧打哈欠时明显高于说话俯仰角越界帧数低头且持续约 30 帧点头时成段出现用窗口统计而不是瞬时值带来一个直接好处偶发的一次眨眼、一次说话不会污染最终判断误报率能压下去一大截。代价是响应延迟这就要在窗口长度和报警及时性之间做权衡而权衡的依据是帧率不是拍脑袋。3. 从摄像头到报警把主循环拆成五个可独立验证的阶段很多人写这类系统喜欢把所有逻辑塞进一个while True跑起来能演示一旦误报或者卡顿就完全无从下手。更稳的做法是把链路切成采集、预处理、检测、指标、判定五段每段有明确的输入输出任何一段出问题都能单独拎出来验证。3.1 采集与预处理三个操作随手加一个就是一倍开销采集用 OpenCV 的VideoCapture它在不同平台上的后端差异很大。Windows 下指定 DirectShow 后端能显著降低首帧延迟Linux 下用默认 V4L2 就行。分辨率不要一上来就 1080P人脸检测的开销和像素数基本线性相关640×480 对看清眼睑开合已经足够。import cv2 import dlib detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(models/shape_predictor_68_face_landmarks.dat) # 0 表示默认摄像头Windows 下加 CAP_DSHOW 能避免首帧卡顿 cap cv2.VideoCapture(0, cv2.CAP_DSHOW) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30) while True: ok, frame cap.read() if not ok: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 直方图均衡对抗弱光务必在灰度图上做 gray cv2.equalizeHist(gray) # 中值滤波去椒盐噪声核必须是奇数 gray cv2.medianBlur(gray, 3) # 上采样一次远处小脸能检出代价是耗时翻倍 faces detector(gray, 1) for face in faces: shape predictor(gray, face) pts [(p.x, p.y) for p in shape.parts()] # 后续交给指标计算模块equalizeHist在强光或弱光下的收益很明显但在均匀光照下会把噪声一起放大所以它不是必选项而是按摄像头环境决定。medianBlur的核大小必须为正奇数写 4 会直接抛错这是新手高频错误。detector的第二个参数是上采样次数取 1 时会把整张图放大一倍再检测小脸检出率提升明显但单帧耗时会接近翻倍如果驾驶员离摄像头只有半米直接给 0 就行。灰度图必须同时喂给检测器和预测器两者都只接受单通道输入。预处理里最容易忽略的一条人脸检测和关键点预测应该复用同一张灰度图。有人顺手在预测器那里再转一次灰度看似无害实际上多了一次全图遍历30 帧每秒下就是白扔掉的 5% 到 8% CPU。3.2 检测与关键点提取把封装做对后面少写一大半代码把检测和预测封成一个类好处是后续想换检测器比如换成 CNN 版本或者 MediaPipe时只改一处。class FaceLandmarkDetector: def __init__(self, model_path, upsample1): self.detector dlib.get_frontal_face_detector() self.predictor dlib.shape_predictor(model_path) self.upsample upsample def detect(self, gray): 返回 [(68点列表, 人脸矩形), ...]无脸时返回空列表 results [] faces self.detector(gray, self.upsample) for face in faces: shape self.predictor(gray, face) pts [(p.x, p.y) for p in shape.parts()] results.append((pts, face)) return resultsupsample做成构造参数而不是写死是因为在标定阶段你可能想让远处的人脸也检出来实测阶段又要压帧率两个阶段取值不同。返回空列表而不是None或抛异常是为了让主循环能平静地处理驾驶员扭头看后视镜这种正常情况此时应该保留上一次的统计状态继续累加而不是清零重来。这一点在第 5 章的丢失人脸场景里会重点讲。关键点坐标转成 Python 列表而不是 numpy 数组是权衡后的结果dlib 返回的parts()是生成器逐点访问比一次性转数组快而 68 个点的规模也完全不需要向量化。真正需要数组的是后面的距离计算那一步再转。3.3 状态机与计数连续帧阈值才是抑制误报的核心单帧 EAR 低于阈值不能算眨眼闭眼可能只是正常眨眼的一部分也可能是一帧噪声。稳妥的做法是引入状态机连续 K 帧低于阈值才认定进入闭眼连续 M 帧高于阈值才认定睁眼恢复一次完整的下降再回升记一次眨眼。class BlinkCounter: def __init__(self, ear_thresh0.22, close_frames3, open_frames2): self.ear_thresh ear_thresh self.close_frames close_frames self.open_frames open_frames self.closed_count 0 # 连续闭眼帧数 self.open_count 0 # 连续睁眼帧数 self.is_closed False self.blink_total 0 self.closed_frame_total 0 # 用于 PERCLOS 累加 def update(self, ear): if ear self.ear_thresh: self.closed_count 1 self.open_count 0 self.closed_frame_total 1 if self.closed_count self.close_frames and not self.is_closed: self.is_closed True # 状态切换只发生一次 else: self.open_count 1 self.closed_count 0 if self.open_count self.open_frames and self.is_closed: self.is_closed False self.blink_total 1 # 一次完整闭合记一次眨眼 return self.is_closed, self.blink_total三个参数各有分工。ear_thresh是判定线被眼镜、光照、人种影响最大必须标定。close_frames控制灵敏度取 2 到 3 帧太小噪声就会误触发太大会漏掉快速眨眼。open_frames是防抖回差它的存在让闭眼—睁眼的边界不会在阈值附近来回震荡等于给状态机加了迟滞。closed_frame_total单独累加是为了算 PERCLOS 时不用再扫一遍历史。打哈欠的判定同理但阈值和帧数不一样嘴巴张大是一个持续过程取MAR 0.5且持续超过 15 帧约半秒才算一次哈欠短促的说话张嘴不应该计数。点头判定用俯仰角取绝对值超过某个角度且持续 20 帧以上作为一次点头。3.4 报警与可视化界面刷新和报警音不能堵在主循环里界面有两个常见落法。一是纯 OpenCV 的imshow叠加文字和矩形框代码量最少适合调参阶段二是 Tkinter 或 PyQt 做独立窗口把视频帧转成图像塞进控件适合答辩演示。前者写起来快后者看起来正式取舍看场合。import winsound import threading def fire_alarm_async(): 报警音放子线程避免阻塞视频循环造成卡顿 def _beep(): for _ in range(3): winsound.Beep(1000, 200) threading.Thread(target_beep, daemonTrue).start()报警音一定要异步。winsound.Beep是阻塞调用直接在循环里连响三次画面会明显停顿帧率掉下来之后 EAR 的时序统计全乱。daemonTrue保证主线程退出时不会被这个线程拖住。跨平台的话用pygame.mixer播一段短音频思路一样预加载、单次触发、不等待。界面文字部分除了显示实时 EAR、MAR、俯仰角和各自的阈值强烈建议把最近 5 秒的 EAR 曲线画在一个小画布上。调试阈值时这条曲线比任何日志都直观——你能一眼看到是阈值太高把正常眨眼算成了闭眼还是太低导致闭眼没触发。4. 阈值不是拍脑袋三组参数怎么在自己的摄像头上标定网上流传的 EAR 阈值 0.2、MAR 阈值 0.6 这类数字来自特定数据集和特定设备直接抄过来大概率水土不服。摄像头焦距、安装角度、镜片反光、环境光每一项都能把绝对值推移 20% 以上。真正可靠的做法是在自己的硬件上跑一遍标定用数据说话。4.1 采集基线正常态和疲劳态各要多少帧标定脚本要做的事很朴素连续录 60 秒正常睁眼状态再录 60 秒刻意闭眼、张嘴、低头的状态把每帧的三个指标写进 CSV。30 帧每秒下各 1800 帧统计上足够稳定再多就是浪费时间。import csv import time def collect_baseline(cap, landmark_detector, seconds60, labelnormal, out_pathbaseline.csv): 按标签采集基线数据label 取 normal 或 fatigue writer_rows [] start time.time() while time.time() - start seconds: ok, frame cap.read() if not ok: continue gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) for pts, _ in landmark_detector.detect(gray): ear (eye_aspect_ratio(pts[36:42]) eye_aspect_ratio(pts[42:48])) / 2.0 mar mouth_aspect_ratio(pts[48:60]) angles head_pose_angles(pts, frame.shape[1], frame.shape[0]) if angles is None: continue writer_rows.append([label, round(ear, 4), round(mar, 4), round(angles[0], 2), round(angles[1], 2), round(angles[2], 2)]) with open(out_path, w, newline, encodingutf-8) as f: w csv.writer(f) w.writerow([label, ear, mar, pitch, yaw, roll]) w.writerows(writer_rows)左右眼的 EAR 取平均比只算一只眼稳定得多。头部姿态返回None时必须跳过这一帧而不是写零写零会污染后面的分位数统计。标签列是必须的因为下一步要算的是两种情况的分位数没有标签就只能靠肉眼看分布效率低很多。4.2 EAR 阈值分位数比均值靠谱拿到基线 CSv 之后不要看均值看分位数。正常睁眼状态的 EAR 应该取第 5 百分位附近作为阈值的上界参考——意思是 95% 的正常睁眼帧都高于这个值。刻意闭眼状态取第 95 百分位附近作为下界参考。两个分位数之间如果有 0.05 以上的间隔说明区分度良好阈值取中间偏下的位置如果两个区间大面积重叠说明当前摄像头或者安装角度根本分不开睁眼闭眼此时调阈值是徒劳的得先去改硬件。常见的翻车情形是戴反光镜片镜片把屏幕光反射进镜头眼睑边缘在灰度图上糊成一团关键点抖动剧烈EAR 在 0.15 到 0.35 之间乱跳。这种情况下降采样加中值滤波能救一点但根本上还是建议把摄像头挪到不逆光的位置。4.3 MAR 与 HPE阈值必须和持续时间绑定张嘴这个动作和闭眼有个本质区别说话、咀嚼都会让 MAR 升高单看幅值区分不开。所以 MAR 的判定必须写成幅值 持续帧数的组合缺一不可。参数含义建议初值实测调整方向EAR_THRESH闭眼判定线0.22戴眼镜下调 0.02 到 0.03EAR_CLOSE_FRAMES连续闭眼帧数3帧率低于 20 时减到 2MAR_THRESH张嘴判定线0.55嘴型偏小的人下调MAR_HOLD_FRAMES打哈欠持续帧数15说话频繁的场景上调PITCH_THRESH低头角度阈值15 度摄像头仰角不同需重标PITCH_HOLD_FRAMES点头持续帧数20结合 PERCLOS 窗口调整PERCLOS_WINDOW闭眼统计窗口90 帧与帧率挂钩约 3 秒PERCLOS_THRESH疲劳判定线0.35灵敏度与误报的平衡点俯仰角的初值不能照抄度数因为摄像头安装俯仰直接影响零点。正确做法是让人坐正、平视前方记录 30 秒的俯仰角均值记为零点之后所有判定都改成相对于零点的偏移量。这一步不做换个摄像头位置整套参数就得重来。4.4 三路信号怎么融合加权不如或来得实在初学者容易想到给三个指标各赋一个权重再求和实际上权重怎么定完全没有依据调起来是个无底洞。更实用的方案是分级判定任意单一指标严重越界比如 PERCLOS 超过 0.5或连续低头超过 2 秒直接报疲劳两个以上指标轻度越界PERCLOS 超过 0.3 且哈欠计数在一分钟内超过 3 次也报疲劳。这种或结构的好处是每一条规则的物理意义都清楚排查误报时能直接定位是哪一路触发的。5. 避坑与排查跑不通、误报、卡顿的五个真实成因这一节的东西基本都是踩过之后才愿意写下来的按现象、原因、解决三段来过一遍对照着查能省掉大半天。5.1 画面卡成幻灯片帧率从 30 掉到 5现象是程序刚启动还流畅几十秒后画面明显迟滞EAR 曲线开始出现大段平台。原因通常有两个叠加报警音阻塞主循环以及上采样次数设成了 1 甚至 2。winsound.Beep每次调用阻塞 200 毫秒连响三次就是 0.6 秒这期间一帧都没采集窗口统计的帧数基准就失真了。上采样则让检测耗时直接翻倍。解决方式是报警放子线程并且在启动时把上采样设为 0跑一遍看帧率如果人脸检出率不够再逐级往上加。另外在循环里加一个帧率打印每 30 帧输出一次实际 FPS这个数字比任何主观感受都准。5.2 明明睁着眼系统一直报疲劳现象是报警频繁但回放录像看人精神得很。第一个原因是 EAR 阈值照抄了别人的数字而你的摄像头俯角更大眼睑在图像上本来就显窄正常睁眼的 EAR 只有 0.2 左右一抄 0.22 就全线低于阈值。第二个原因是关键点抖动两侧眼角点在某些帧里跳到眉毛上导致分母突然变大一倍EAR 掉到 0.1 以下。解决方式是先跑标定脚本看自己数据的分布阈值按分位数定再在计算前对关键点做一次三点滑动平均用前后帧的坐标把抖动压掉。滑动平均会引入一帧延迟在人脸追踪这种低频信号上完全可以接受。5.3 一低头就报警看仪表盘也报警现象是每次视线短暂离开正前方报警就响。根本原因是 EAR 判定和头部姿态没有耦合。低头时眼睛在图像上被压缩EAR 自然下降这属于几何效应而不是真的闭眼。解决方式是加一个门控当俯仰角偏离零点超过一定角度时暂停 EAR 的闭眼累加但仍保留姿态本身的判定。换句话说低头这件事交给姿态模块管眼睛模块只在头基本正对摄像头时才发言。这个门控逻辑不做误报率基本不可能降到 10% 以下。5.4 换了台电脑模型加载直接报错现象是代码在开发机上正常拷到另一台机器上启动就抛异常提示找不到预测器文件。原因是路径写成了绝对路径或者模型文件名在传输过程中被改成了带括号的副本名。另一种可能是文件没有完整拷贝解压中断导致体积不对。解决办法是统一用相对于工程根目录的路径并在启动时做一次存在性检查文件缺失时打印明确提示而不是让底层库抛出难懂的异常。路径拼接用pathlib避免 Windows 和 Linux 的斜杠差异。5.5 扭头看后视镜人脸丢失后计数被清零现象是驾驶员扭头两秒再转回来之前累积的哈欠次数和 PERCLOS 全部归零。原因是代码里检测不到人脸就continue顺手把计数器重置了。这个行为在演示时看不出来实测时会严重低估疲劳程度。正确的处理是人脸丢失只冻结统计不清零。用一个lost_frames计数超过 30 帧约 1 秒仍然没人脸才把状态标记为未知并暂停判定重新检测到人脸后接着之前的累加继续走但把丢失期间的帧从 PERCLOS 分母里剔除否则会人为拉低闭眼占比。6. 让它从能跑到可信用固定视频做回放回归答辩前一晚最怕的事是白天还好的系统突然开始乱报。阈值调过、代码改过、摄像头挪过位置任何一个变动都可能让之前的表现失效。我一般的做法是把一段包含正常驾驶、眨眼、打哈欠、低头、扭头五种片段的录像固化成测试素材每次改完参数就跑一遍回放用同一份数据比对结果。回放不需要摄像头把VideoCapture的参数从 0 换成文件路径即可帧率和时序完全一致这也是把指标计算单独抽成模块的最大回报。import csv def evaluate(csv_path, pred_fn): csv_path 含 label 列0 正常 / 1 疲劳pred_fn 输入一行返回 0/1 tp fp tn fn 0 with open(csv_path, encodingutf-8) as f: for row in csv.DictReader(f): truth int(row[label]) pred pred_fn(row) if pred 1 and truth 1: tp 1 elif pred 1 and truth 0: fp 1 elif pred 0 and truth 0: tn 1 else: fn 1 acc (tp tn) / max(tp tn fp fn, 1) fpr fp / max(fp tn, 1) # 误报率正常被误判为疲劳 fnr fn / max(fn tp, 1) # 漏报率疲劳被漏掉 return {accuracy: round(acc, 3), false_positive: round(fpr, 3), false_negative: round(fnr, 3)}这段代码的价值在于把感觉还行变成三个能对比的数字。false_positive对应需求里说的误报率false_negative对应漏报两个指标是此消彼长的关系把阈值调低更灵敏漏报降下去但误报升上来。工程上通常优先压误报因为报得太勤驾驶员会直接关掉系统那就等于零。一套可以参考的回归用例设计是这样的用例内容期望输出关注指标C1正常睁眼 60 秒不报警误报率C2每秒眨眼一次持续 30 秒不报警误报率C3刻意闭眼 3 秒报警检出延迟C4连续打哈欠 3 次报警漏报率C5低头 5 秒报警检出延迟C6扭头离开画面 2 秒再回状态延续不清零状态连续性跑完这六条基本能覆盖答辩时老师会问的绝大多数场景。检出延迟也值得单独记一笔从动作发生到报警响起的帧数超过 45 帧约 1.5 秒就说明窗口设得太长响应会显得迟钝。顺手提一个容易被忽略的细节回归用的 CSV 里label是按片段整体标注的而 PERCLOS 是窗口统计量两者时间粒度不一致。我吃过这个亏——用逐帧标签去对窗口判定算出来的准确率完全没意义。后来改成按片段中后段取样跳过窗口填充期数字才稳定下来。从那以后我每次动阈值都强制走一遍这六条回放一条不过就不往下调。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站