资讯动态

基于dlib的疲劳检测:EAR/MAR/HPE阈值与PERCLOS实践

发布时间:2026/10/25 13:56:06 来源:尧图企业网站定制
简介围绕基于 dlib 模型的疲劳驾驶检测系统展开的毕业设计文档面向计算机视觉、程序设计与 Web 设计方向的学生及开发者聚焦驾驶员眨眼、打哈欠、点头等疲劳行为的识别与预警。文档以单份 docx 交付压缩包约 1.13MB按导论、相关技术、系统分析、系统设计等章节展开涉及 OpenCV 图像处理、dlib 68 个面部特征点、PERCLOS 指标、人体姿态估计以及需求分析、整体框架、可行性与模块划分等内容。读者可从中梳理面部采集、图像预处理、人脸检测、疲劳行为识别和报警提示的实现路径理解从算法选型到系统集成的设计逻辑适合作为课程设计、毕业设计选题参考或相关项目的前期资料。目前已有 1958 人学习便于对照同类方案查漏补缺。1. 阈值比模型更决定成败这套 dlib 疲劳检测系统能干什么先说个反直觉的结论。这套基于 dlib 的疲劳驾驶检测系统真正决定它好不好用的不是 68 个特征点准不准而是 EAR、MAR、头部俯仰角这几个阈值在你自己的脸、你的摄像头、你的光照条件下能不能站得住。项目本体是一份完整的计算机视觉方向的毕业设计工程OpenCV 抓帧做预处理dlib 出人脸框和 68 个关键点再用 EAR、MAR、HPE 三个几何指标把眨眼、打哈欠、点头量化成数字最后用 PERCLOS 时间窗做疲劳判决触发界面提示和声音报警。它适合两类人一类是要交毕业设计、需要一套有界面能跑起来的完整工程的人另一类是想在车载设备或工位监控上做个低成本视觉原型的开发者。接下来按能干什么、怎么跑、怎么改的顺序往下拆。2. 选型与骨架OpenCV 抓帧、dlib 出点、三个几何指标拼成一条流水线2.1 三条技术路线为什么最后只剩面部特征疲劳检测在这个领域里有三条成熟路线选型阶段必须先把它们摆平否则后面所有代码都是白写。第一条是基于生理信息。心电、脑电、肌电、心率变异性这些信号确实能真实反映疲劳程度精度在所有方案里最高。问题在于接触式采集电极要贴在身上驾驶员动作一大就掉视线和操作也会被干扰实际装车时几乎没人愿意配合。第二条是基于车辆信息靠方向盘转角频率、车道偏离、速度波动来反推。这条路非接触、成本低但采集到的数据受车型、路况、驾驶习惯影响极大权重怎么分配至今没有统一说法阈值没法固定出了误判也很难解释清楚是哪一步错了。第三条是基于面部信息这几年成了主流。一个普通摄像头对着驾驶员靠眨眼频率、哈欠、点头这些外显行为判断状态非接触、硬件便宜、结果直观可解释——你甚至能截一帧图给人看这一刻他闭眼超过两秒。这套系统选的就是它。选它不代表它没短板。面部方案对光照极其敏感强逆光下 dlib 的 HOG 检测器会直接丢脸戴粗框眼镜、口罩、墨镜会破坏特征点定位夜间只有仪表盘微光时不补红外基本没法用。所以在需求分析里我把适用范围划得很清楚白天强光、夜间弱光、带透明镜片眼镜这三种场景要能跑人脸丢失时不做判定但保留历史累计值。这条边界写进代码里比写在文档里有用得多。2.2 六个模块的职责边界整个系统拆成六个模块每个模块的输入输出必须明确否则调试时会陷入到底是检测错了还是判定错了的黑匣子。下面这张表是我从设计文档里整理出来的实际划分模块输入输出关键依赖视频采集摄像头索引BGR 帧cv2.VideoCapture图像预处理BGR 帧灰度图、缩放图cvtColor、equalizeHist、medianBlur人脸检测与定位灰度图人脸矩形、68 点坐标dlib.get_frontal_face_detector、shape_predictor特征计算68 点坐标EAR、MAR、pitch/yaw/rollnumpy、cv2.solvePnP疲劳判决三个指标序列疲劳等级 0/1/2collections.deque 滑窗报警与界面疲劳等级、原始帧提示文字、蜂鸣Tkinter、winsound这里有个容易被忽略的设计点特征计算和疲劳判决必须分开。前者是纯函数输入坐标输出浮点数可以单独拿一段视频离线跑后者是有状态的维护着时间窗口和计数器。混在一起写的话你永远没法回答到底是 EAR 算错了还是窗口太小这个问题。我当时就是把这两块硬拆开后面调阈值的时候省了至少两天。2.3 预处理链路的三个动作和它们的实际收益原始帧直接丢给 dlib 检测器是能跑的但检出率会明显偏低尤其是驾驶员坐得稍远、车内光线不均的时候。预处理链路加三个动作每个都有明确目的不是为了凑代码行数。import cv2 import numpy as np def preprocess(frame, det_scale0.5, blur_ksize3): frame: 原始 BGR 帧 det_scale: 检测用缩放比例, 0.5 表示缩小一半, 提升速度 blur_ksize: 中值滤波核大小, 必须是奇数 返回: (gray_full, gray_small) 全尺寸灰度图用于取点, 缩放图用于检测 # 1. 转灰度: dlib 的 HOG 检测器只吃单通道图 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 2. 直方图均衡: 夜间仪表盘微光、隧道进出口明暗突变时拉升对比度 gray cv2.equalizeHist(gray) # 3. 中值滤波: 车载摄像头噪点以椒盐噪声为主, 中值比高斯更对症 gray cv2.medianBlur(gray, blur_ksize) # 4. 缩放一份给检测器用, 人脸较大时能显著降耗时 h, w gray.shape[:2] gray_small cv2.resize(gray, (int(w * det_scale), int(h * det_scale))) return gray, gray_small这段代码里三个参数的含义值得展开。det_scale控制检测分辨率人脸在画面里占比大时取 0.5 甚至 0.4速度能翻倍但如果你把摄像头装在方向盘后方一米开外人脸只占画面十分之一就要改成 1.0 甚至用上采样否则 dlib 直接检不到。blur_ksize一般固定 3调到 5 会把眼睑边缘也糊掉EAR 计算会跟着漂。直方图均衡这一步有个血泪经验cv2.equalizeHist只接受单通道八位图直接对彩色帧调用会抛异常。有人想保留彩色画面做均衡正确做法是转到 YCrCb 空间只处理 Y 通道再转回来但这套系统显示层用的是原始彩色帧均衡只服务于检测所以先转灰度就够了没必要多绕一圈。注意缩放后的坐标不能直接拿去取 68 点。dlib 的shape_predictor接收的是原图 矩形如果你在缩小图上检测到人脸框必须把矩形按1/det_scale的倍数还原回原图坐标系再送进去否则取出来的关键点位置会整体偏移。这个坑我第一次写的时候踩得结结实实画面上眼睛标在了下巴上。3. 核心算法拆解EAR、MAR、HPE 如何从 68 个坐标点算出疲劳证据3.1 68 个特征点的分组与索引约定dlib 的shape_predictor_68_face_landmarks.dat输出的是一个 68 行 2 列的数组每行是一个点的 (x, y)。这 68 个点的编号是固定的必须记牢因为后面所有计算都靠索引取值区域索引范围点数用途下颌轮廓0–1617一般不用可用于人脸对齐右眉17–215辅助判断左眉22–265辅助判断鼻梁与鼻尖27–359鼻尖 30 用于头部姿态右眼36–416EAR 右眼左眼42–476EAR 左眼外唇48–5912MAR 外唇内唇60–678MAR 内唇更灵敏左右眼的 6 个点排列顺序是固定的第 1 个和第 4 个是内外眼角第 2、3 个在上眼睑第 5、6 个在下眼睑。这个顺序决定了 EAR 公式怎么写写反了算出来的值会变成倒数。# 索引常量, 全局只定义一次, 避免各处魔法数字 RIGHT_EYE list(range(36, 42)) LEFT_EYE list(range(42, 48)) OUTER_MOUTH list(range(48, 60)) INNER_MOUTH list(range(60, 68)) def get_points(shape, idx_list): 把 dlib 的 shape 对象转成 numpy 数组, 方便后面做向量运算 return np.array([[shape.part(i).x, shape.part(i).y] for i in idx_list], dtypenp.float64)统一转成 numpy 是必要的。dlib 的shape.part(i)每次调用都有开销逐帧算三个指标要取几十次点累积起来在低端 CPU 上能吃掉两三毫秒转成数组后一次取完就没事了。3.2 EAR 眼睛长宽比眨眼判定的核心公式EAR 的思路很朴素眼睛睁开时是一个接近椭圆的形状高度和宽度有稳定比例闭眼时高度趋近于零比例跟着塌下去。公式用两组垂直距离之和除以两倍的水平距离做归一化这样人坐得远近都不影响判断。def eye_aspect_ratio(pts): pts: 6x2 数组, 顺序为 [左眼角, 上眼睑1, 上眼睑2, 右眼角, 下眼睑1, 下眼睑2] 返回: 浮点数, 睁眼正常值约 0.28~0.35, 闭眼低于 0.15 # 两组垂直距离: 上眼睑到对应下眼睑 v1 np.linalg.norm(pts[1] - pts[5]) v2 np.linalg.norm(pts[2] - pts[4]) # 水平距离: 内眼角到外眼角 h np.linalg.norm(pts[0] - pts[3]) # 分母兜底, 防止侧脸时水平距离退化为 0 导致除零 if h 1e-6: return 0.0 return (v1 v2) / (2.0 * h)代码里pts[0]和pts[3]对应索引 36 和 39右眼42 和 45左眼。垂直方向用 37-41 和 38-40 两组平均一下能抵消单组点在侧脸时的抖动。实际取值上正常人睁眼 EAR 在 0.28–0.35 之间浮动闭眼会掉到 0.10 以下中间有一段过渡区。阈值定在 0.22 左右比较稳但这不是绝对值——不同人的眼型差异很大单眼皮和双眼皮能差出 0.05。所以这套系统在正式判定前会做一段基线标定取前若干帧睁眼状态的均值作为个人基线阈值按基线的一个比例下浮。这一步不做换个人用就误报。还有一个细节左右眼要分别算再取平均不能只算一只。驾驶员侧头看后视镜时靠近摄像头的那只眼可能被鼻梁挡住一半单眼判定会直接崩取平均能把这个影响压下去。3.3 MAR 嘴部长宽比把打哈欠和说话区分开MAR 的公式结构和 EAR 完全一样换个部位而已。区别在于嘴巴的点位更复杂外唇 12 个点、内唇 8 个点用哪一组会直接影响灵敏度。def mouth_aspect_ratio(pts): pts: 外唇 12 个点 (索引 48~59) 返回: 浮点数, 闭嘴约 0.05~0.15, 打哈欠张嘴可达 0.6 以上 # 上下唇垂直距离, 取两组取平均 v1 np.linalg.norm(pts[3] - pts[9]) # 51 - 57 v2 np.linalg.norm(pts[2] - pts[10]) # 50 - 58 # 嘴角水平距离 h np.linalg.norm(pts[0] - pts[6]) # 48 - 54 if h 1e-6: return 0.0 return (v1 v2) / (2.0 * h)选外唇而不是内唇原因是内唇在说话时开合幅度太大正常交流就能冲到 0.4误报率极高。外唇因为要考虑嘴唇本身的厚度同样的张嘴幅度算出来的值更保守打哈欠这种大幅度张嘴才能突破 0.6。但光靠 MAR 一个阈值区分不了打哈欠和说话这两件事在单帧上看起来太像了。所以判定必须加时间维度打哈欠是一个持续一到三秒、幅度由小到大再回落的过程说话是快速抖动。工程上的做法是记录 MAR 连续超过阈值的帧数帧数落在 20 到 90 帧之间30fps 下约 0.7 到 3 秒才计一次哈欠超出的当成长时间张嘴忽略掉。满足这个条件才算一次有效哈欠单次不报警累计到一定次数或者和闭眼指标叠加才升级疲劳等级。这一层时间过滤是把误报率从没法看压到能接受的关键比调阈值有用得多。3.4 HPE 头部姿态估计pitch 角判点头yaw 角防作弊头部姿态这一块是整套系统里最容易翻车的部分因为它引入了三维运算一步错处处错。思路是dlib 给的是图像上的二维像素坐标我们手里还有一组人脸的通用三维模型坐标知道至少 4 组三维点 ↔ 二维点的对应关系就能用cv2.solvePnP解出相机的旋转向量和平移向量旋转向量转成旋转矩阵再转欧拉角就得到头部的俯仰、偏航、翻滚。# 通用人脸三维模型坐标(单位 mm), 顺序与下面取的二维点一一对应 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) LANDMARK_IDX [30, 8, 36, 45, 48, 54] def head_pose(shape, frame_size): 返回 (pitch, yaw, roll), 单位度 pitch 为负表示低头, 这套系统里 |pitch| 15 视为低头 h, w frame_size img_pts np.array([[shape.part(i).x, shape.part(i).y] for i in LANDMARK_IDX], dtypenp.float64) # 简化相机内参: 焦距取画面宽度, 主点取画面中心 # 真实标定过的内参更准, 但原型阶段这样够用 focal w cam_matrix np.array([[focal, 0, w / 2.0], [0, focal, h / 2.0], [0, 0, 1.0]], dtypenp.float64) dist np.zeros((4, 1)) ok, rvec, tvec cv2.solvePnP(MODEL_POINTS, img_pts, cam_matrix, dist, flagscv2.SOLVEPNP_ITERATIVE) if not ok: return 0.0, 0.0, 0.0 rmat, _ cv2.Rodrigues(rvec) proj np.hstack((rmat, tvec)) _, _, _, _, _, _, euler cv2.decomposeProjectionMatrix(proj) pitch, yaw, roll euler.flatten() # decomposeProjectionMatrix 返回的角度可能落在 0~180, 转成 -90~90 更方便判断 pitch pitch - 180 if pitch 90 else pitch yaw yaw - 180 if yaw 90 else yaw roll roll - 180 if roll 90 else roll return pitch, yaw, roll参数上有几处必须说清楚。MODEL_POINTS是一套通用的人脸尺寸假设单位毫米来自 dlib 官方示例别自己瞎编改错一个坐标整个角度就歪了。focal w是拿画面宽度当焦距的近似对普通广角摄像头误差在 10% 以内判点头足够了如果你要用作精细测量得拿棋盘格老老实实标一次内参。SOLVEPNP_ITERATIVE适合点数少的场景用 EPNP 也行但 ITERATIVE 在正脸情况下更稳。俯仰角判定的坑在于阈值符号。低头时 pitch 可能是负的也可能是正的取决于模型点顺序和相机坐标系的定义方向不同人抄同一份代码结果可能相反。我一般做法是让测试者故意低头打印出实际角度值看到底往哪个方向走再定符号。这一步不实测光看公式十个有八个搞反。另一个必须加的保护是 yaw 角。驾驶员转头看后视镜、低头看手机这两种动作都会让 pitch 出现大幅变化但只有真正的疲劳点头是慢速下沉、猛地抬起的节奏。所以判定逻辑里加了 yaw 约束|yaw| 超过 25 度时暂停点头计数避免把正常观察路况算成瞌睡。4. 判决与报警PERCLOS 滑窗、阈值标定实验和四页面可视化界面4.1 PERCLOS 滑窗把单帧指标变成时间序列判决PERCLOS 的定义是单位时间内眼睛闭合时间所占的比例原始公式就是闭眼时长除以总时长再乘百分之百。它比眨眼次数更可靠的地方在于疲劳的典型表现不是眨得快而是闭眼时间变长——微睡眠状态下一次闭眼可能持续两秒以上而正常眨眼只有一百多毫秒。工程实现上不能真的去记时间戳太麻烦。用固定长度的帧队列更简单维持一个长度为 N 的 deque每帧往里塞一个布尔值这一帧是否闭眼队列满了就自动弹出最老的然后算队列里 True 的比例。from collections import deque class FatigueDetector: def __init__(self, fps30, window_sec30, ear_th0.22, mar_th0.62, pitch_th15.0, perclos_mild0.15, perclos_severe0.30): # 滑窗长度按秒换算成帧数, 30fps 下 30 秒 900 帧 self.window int(fps * window_sec) self.eye_buf deque(maxlenself.window) self.mar_th mar_th self.pitch_th pitch_th self.perclos_mild perclos_mild self.perclos_severe perclos_severe self.ear_th ear_th self.closed_frames 0 # 连续闭眼帧 self.blink_count 0 # 累计眨眼次数 self.yawn_frames 0 # 连续张嘴帧 self.yawn_count 0 # 累计哈欠次数 self.nod_frames 0 # 连续低头帧 self.nod_count 0 # 累计点头次数 def update(self, ear, mar, pitch, yaw): 每帧调用一次, 返回 (疲劳等级, 各项统计) # --- 眼部: 同时维护实时计数和 PERCLOS 滑窗 --- closed ear self.ear_th self.eye_buf.append(closed) if closed: self.closed_frames 1 else: # 闭眼后重新睁眼, 且持续了 2~15 帧, 计一次有效眨眼 if 2 self.closed_frames 15: self.blink_count 1 self.closed_frames 0 # --- 嘴部: 连续 20~90 帧才算一次哈欠, 过滤说话抖动 --- if mar self.mar_th: self.yawn_frames 1 else: if 20 self.yawn_frames 90: self.yawn_count 1 self.yawn_frames 0 # --- 头部: yaw 过大时暂停点头计数, 避免转头误判 --- if abs(yaw) 25.0 and pitch -self.pitch_th: self.nod_frames 1 else: if self.nod_frames 10: # 低头持续 0.3 秒以上才算 self.nod_count 1 self.nod_frames 0 # --- PERCLOS 计算 --- perclos sum(self.eye_buf) / len(self.eye_buf) if self.eye_buf else 0.0 # --- 综合判决: 单指标不报警, 多指标叠加才升级 --- level 0 if perclos self.perclos_severe or self.closed_frames 20: level 2 # 重度: 立即声音报警 elif perclos self.perclos_mild or self.yawn_count 1 and self.nod_count 1: level 1 # 轻度: 屏幕提示 return level, { ear: round(ear, 3), mar: round(mar, 3), pitch: round(pitch, 1), perclos: round(perclos, 3), blink: self.blink_count, yawn: self.yawn_count, nod: self.nod_count, }这段代码里每个数字都有来历。2 closed_frames 15是为了过滤掉瞬时的检测抖动单帧闭眼很可能是特征点跳了不是真眨。超过 15 帧半秒就不算眨眼了那是闭眼或微睡眠单独由closed_frames 20这条规则升级到重度报警。20 yawn_frames 90同理低于 20 帧是说话高于 90 帧是长时间张嘴打哈欠或者干脆在嚼东西。perclos_mild0.15和perclos_severe0.30是经验值。正常清醒状态下 PERCLOS 在 0.05 以下开始犯困会爬到 0.15 左右到 0.30 基本就是打瞌睡了。窗口长度用 30 秒太短噪声大太长反应迟钝——三分钟窗口的 PERCLOS 很稳但你得等三分钟才能等到报警实际早就睡着了。4.2 阈值标定实验EAR、MAR、HPE 各调了几轮这套系统的设计文档里专门有一节叫阈值优化写的就是这三个指标怎么定。做法很土但有效录一段自己正常驾驶状态的视频一段故意打哈欠的视频一段故意点头的视频然后离线跑指标、打印曲线、看分布。EAR 的结论是睁眼时稳定在 0.30 上下自然眨眼时最低到 0.14–0.18用力闭眼能到 0.08。阈值定在 0.22落在这两段之间的空档里误判空间最大。后来发现戴眼镜的人睁眼基线会低 0.02 左右就把标定环节加上了前 100 帧取中位数作为个人基线阈值取基线的 0.7 倍。MAR 的结论是闭嘴 0.06正常说话 0.25–0.40打哈欠峰值 0.60–0.75。阈值定 0.62 能避开说话但偶有漏检——张嘴幅度小的人可能只到 0.55。补偿办法是靠时间过滤而不是降阈值因为降阈值会把说话全放进来。HPE 的结论最麻烦。低头 20 度时算出来的 pitch 是 -18 到 -22点头过程中峰值能到 -35 左右。阈值定 -15 度同时要求持续 10 帧以上。这里有个翻车点戴眼镜的人鼻梁点位的检测会因为镜框反光偏移导致 pitch 整体偏个三五度。所以判定用的不是绝对值而是相对于标定阶段记录的正脸基准角的偏移量。4.3 四页面可视化界面与报警模块界面用的是 Tkinter轻、不需要额外装运行时、打包成 exe 也小。设计文档里分了四个页面行车小贴士页启动时的安全提示、开始页选择摄像头索引和参数、介绍页系统说明、检测页实时画面加指标面板。四个页面用tkinter.ttk.Notebook或者一个简单的Frame.raise()切换都行后者更可控。真正麻烦的是线程。视频采集必须放在独立线程里跑Tkinter 的界面循环在主线程两者直接交叉调用会卡死甚至崩溃。标准做法是用queue.Queue做单向传递import threading, queue, tkinter as tk class DetectApp: def __init__(self): self.root tk.Tk() self.frame_q queue.Queue(maxsize2) # 只留最新帧, 满了丢弃旧帧 self.stat_q queue.Queue(maxsize8) self.running False self._build_ui() def _build_ui(self): self.canvas tk.Canvas(self.root, width640, height480) self.canvas.pack() self.label tk.Label(self.root, text等待开始, font(微软雅黑, 14)) self.label.pack() self.root.after(30, self._refresh) # 每 30ms 刷新一次界面 def _refresh(self): 界面刷新只从队列取数据, 绝不在这里跑检测 try: img self.frame_q.get_nowait() # PIL 转 ImageTk 后画到 canvas, 略 except queue.Empty: pass try: stats self.stat_q.get_nowait() self.label.config(textfEAR {stats[ear]} PERCLOS {stats[perclos]}) except queue.Empty: pass self.root.after(30, self._refresh) def start(self): self.running True threading.Thread(targetself._worker, daemonTrue).start() self.root.mainloop()maxsize2这个细节很关键。如果队列不设上限视频线程产出帧的速度快过界面消费速度队列会越积越长画面延迟越来越高最后变成人已经抬头了屏幕上还在显示五秒前的低头。设成 2 配合put_nowait加异常吞噬就变成了自动丢帧永远是新的覆盖旧的。报警模块分两级。轻度疲劳只在界面上把文字变黄并弹提示条重度疲劳要出声音。Windows 上用winsound.Beep(1000, 500)最简单跨平台就用pygame.mixer播一段提示音或者用simpleaudio。注意声音播放不能阻塞视频线程否则响一下卡一下帧率直接掉下来得丢进单独的线程或者用异步播放。5. 跑通与排查依赖安装、最小可运行脚本和五条踩坑记录5.1 环境搭建与最小可运行脚本这套工程的依赖不多但有两个是硬骨头。Python 版本建议 3.8–3.10太新的版本 dlib 预编译包常常还没跟上。# 建议先建虚拟环境, 避免全局污染 python -m venv venv venv\Scripts\activate # Windows source venv/bin/activate # Linux / macOS # 顺序别乱: 先装 numpy, 再装其余 pip install numpy1.24.3 pip install opencv-python4.8.0.76 pip install dlib19.24.1 pip install pillow # dlib 装不上的话走源码编译, 前提是先装好 CMake 和 C 编译工具链 # Windows 需要 Visual Studio Build Tools 的使用 C 的桌面开发组件 pip install cmake pip install dlib --no-binary dlib模型文件shape_predictor_68_face_landmarks.dat有六十多兆得单独下载后放到工程目录下。路径别用中文别用相对路径靠运行目录猜用os.path.dirname(__file__)拼绝对路径省得换台机器就报文件找不到。跑通的最小验证脚本不用界面先确认摄像头、检测器、关键点三件事都对import cv2, dlib, os detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ok, frame cap.read() if not ok: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 第二个参数是上采样次数, 1 表示放大一倍后检测, 小脸更易检出 faces detector(gray, 1) for rect in faces: shape predictor(gray, rect) # 画左右眼轮廓 for i in list(range(36, 42)) list(range(42, 48)): cv2.circle(frame, (shape.part(i).x, shape.part(i).y), 2, (0, 255, 0), -1) cv2.imshow(landmarks, frame) if cv2.waitKey(1) 0xFF 27: # ESC 退出 break cap.release() cv2.destroyAllWindows()detector(gray, 1)里的 1 是上采样次数每加一次图像放大一倍检测耗时大约翻四倍。人脸大就传 0人脸小传 1别传 2——除非你想看幻灯片。cv2.waitKey(1)必须调用否则 imshow 窗口画不出来这是 OpenCV 的经典坑。5.2 五条踩坑记录现象pip install dlib报错提示找不到 CMake 或者编译一堆 C 报错。原因没有预编译 wheel 匹配当前 Python 版本和系统时pip 会退回源码编译这时依赖本地的 C 工具链。解决优先降 Python 版本到 3.9 或 3.10 找现成 wheel不行就装 CMake 加 Visual Studio Build Tools确保cl.exe在 PATH 里。用 conda 装conda install -c conda-forge dlib成功率更高能省掉一整个下午。现象cap.read()一直返回 False或者画面全黑。原因摄像头索引不对、被其他程序占用、或者笔记本的外接摄像头和内置摄像头索引不同。解决把VideoCapture(0)依次试到 4看哪个能出画面。另外先关掉微信、腾讯会议、浏览器里所有可能占摄像头的页面。Windows 上还要注意隐私设置里的摄像头权限开关这个开关关着的话 OpenCV 打不开摄像头但不报错属于玄学级问题。现象画面里人明明在但一帧都检测不到人脸。原因光照过暗或逆光、人脸在画面里占比太小、传进检测器的不是灰度图。解决先确认传的是单通道gray彩色图传进去 dlib 会静默返回空列表。再确认尺寸人脸宽度小于 80 像素时把上采样次数改成 1。夜间场景基本无解除非换红外摄像头这个在需求阶段就该写清楚。现象界面越跑越卡五六分钟后完全跟不上。原因帧队列没有长度上限视频线程产出的帧越积越多或者把耗时的人脸检测放在了界面刷新回调里。解决队列设maxsize并用put_nowait配合 try 吞掉queue.Full异常检测逻辑一律放工作线程界面线程只负责取最新结果渲染。现象误报严重人正常说话被判成打哈欠低头看手机被判成瞌睡。原因只在单帧上做阈值比较没有时间维度的持续帧过滤头部姿态只用了 pitch 没看 yaw。解决给每个动作都加最小持续帧和最大持续帧的下上界说话 3 到 10 帧就结束打哈欠会持续 20 帧以上点头判定加abs(yaw) 25约束。这两个过滤加完误报率下降非常明显。6. 二次开发从帧级判定到状态机与自适应基线的改造思路如果只是交作业上面这套跑通就够了。但真想拿去做原型验证有两处必须动。第一处是把帧级判定升级成状态机。现在的update是每帧独立算等级问题在于等级会来回抖——闭一下眼跳到重度睁开又回轻度报警器变成节拍器。改成显式的三状态机清醒 / 轻度疲劳 / 重度疲劳加迟滞进入重度需要连续 N 帧满足条件退出重度需要连续 M 帧不满足M 大于 N。这一改报警的稳会立刻上一个档次。第二处是 EAR 自适应基线。前面提过不同人眼型差异能到 0.05硬编码阈值换个人就得重调。做法是启动后前 100 帧进入标定态这段时间只采集不判定取 EAR 的中位数作为睁眼基线ear_base实际阈值用ear_base * 0.7。同时每隔一段时间比如每五分钟在检测到连续睁眼时缓慢更新基线防止疲劳加深后基线漂移导致的漏检。改造点清单大致是这样改造点涉及文件改动量收益三级状态机加迟滞detector.py约 60 行报警稳定性显著提升EAR 个人基线标定detector.py 界面约 40 行换人不用改代码多线程队列限长app.py约 20 行延迟不再累积检测结果落盘新增 logger.py约 30 行便于事后复盘误报指标推送到 web 前端新增 Flask 服务约 80 行可做远程监控面板最后一条值得单独说。这套系统的界面是 Tkinter 桌面端如果需求变成车队管理端要远程看司机状态桌面界面就没用了。比较省事的做法是保留采集和计算逻辑把每帧的指标打成 JSON用 Flask 起一个服务一帧图像走 MJPEG 推流接口指标走 WebSocket 长连接推给浏览器。前后端分离后界面那部分用网页重写反而更灵活也不用担心跨平台打包的问题。这就是这类视觉原型项目往 web 方向演进的常规路子图像和数值分开走两条通道别把帧塞进 WebSocket 里。还有个小技巧把每帧的 EAR、MAR、pitch、perclos 连同时间戳写进 CSV。调阈值的时候直接拉出来画散点图比盯着实时画面猜快十倍。我当时是靠自己反复装作打哈欠才把 MAR 阈值定下来的后来有了日志才发现我自然说话时的 MAR 峰值有 0.48离原来的阈值只差 0.14纯属运气好没误报。从那以后我每次调视觉类阈值都强制先落一份离线日志再下手改数字不再靠盯着屏幕感觉。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑