简介围绕疲劳驾驶检测这一计算机视觉落地场景这份毕业设计文档给出了基于 dlib 模型的系统设计与实现方案适合计算机视觉、Web 设计方向的学生以及需要完成课程设计、毕业设计的学习者参考。内容从研究背景与意义切入梳理生理信息、车辆信息与面部信息三类检测思路进而展开 OpenCV 与 dlib 的 68 个面部特征点定位、长宽比计算、PERCLOS 原理、眨眼与哈欠次数统计、人体姿态估计点头计数以及系统需求、整体框架、可行性与面部采集等章节形成从技术选型到模块设计的完整论证链条。压缩包仅含 1 个 docx 文件大小约 1.13MB轻量便于查阅与二次整理。目前已有 1958 人学习下载。读者可从中获取论文目录结构、技术选型依据、系统模块划分与疲劳判定思路作为开题、撰写和答辩准备的参考样本。1. 摄像头夹在 A 柱旁的那半张脸摄像头夹在 A 柱旁边画面里只有半张脸光线还忽明忽暗——大多数「基于 dlib 模型的疲劳驾驶检测系统」第一次在真机上跑起来都是这个样子。方案本身不算复杂dlib 从人脸框里回归出 68 个特征点算 EAR 判眨眼、算 MAR 判打哈欠、算头部欧拉角判点头再用 PERCLOS 准则把三个指标合成一个疲劳分。真正麻烦的是把它做成一份能交、能复现、能答辩的完整设计与实现。这篇写给两类人正在做疲劳检测毕业设计、需要一套完整结构的学生以及要交计算机视觉课程设计或带界面演示的从业者。后面按需求、选型、架构、实现、避坑、验收的顺序把整套东西摊开讲。2. 需求分析与技术选型三条路线怎么选为什么落在 OpenCV dlib2.1 生理、车辆、面部三条路线横向对比做疲劳检测先要回答一个问题疲劳从哪里读出来。学术界和工业界摸过的路线基本就三条每条都有明确的代价选型不是选先进是选代价能接受的那条。基于生理信息的路线读的是脑电、肌电、心电、眼电这些信号。脑电随疲劳程度会出现频带能量迁移肌电在疲劳时频率和幅值曲线会有明显变化心电则靠心率变异性做相关性分析。这条路线准确度最高因为它测的就是生理本体不依赖行为外显。但它的代价是接触式电极要贴在头皮、胸口、手腕上长时间驾驶会痒、会移位驾驶员主观上非常抵触实际车里根本铺不开。它适合实验室和模拟驾驶舱不适合量产。基于车辆信息的路线读的是方向盘转角与转动频率、车速与加速度波动、车道偏离程度。这条路线的优势是非接触传感器本来就在车上不用额外加装。问题在于干扰源太多车型不同转向手感不同路面状况不同方向盘抖动不同驾驶习惯不同操作风格不同同一套参数在 A 车上标定完换到 B 车上就飘了。更麻烦的是几个指标的权重至今没有统一定论判错了也没法给出可解释的归因。它能做辅助信号不适合做单一判据。基于面部信息的路线读的是眨眼、打哈欠、点头这些外显行为。一个普通摄像头就能采非接触硬件成本压到最低而且判定逻辑对人是透明的——「你刚才连续闭眼超过两秒」这种解释任何人都听得懂。它的短板是受光照和遮挡影响夜间、逆光、戴口罩都会掉精度。但对一套要做完整设计与实现、要能演示、要能讲清原理的系统来说这条路线是唯一在成本和可解释性上都过关的。维度生理信息车辆信息面部信息接触方式接触式需佩戴电极非接触读总线信号非接触单摄像头硬件成本高中依赖车型低抗环境干扰强中受路况车况影响弱受光照遮挡影响判定可解释性强弱权重无定论强指标直观落地与演示难度高中低提示选型章节在文档里不要只写「面部信息更好」要把三条路线的代价写出来。答辩时导师最常追问的就是「你为什么不选另外两条」这段对比表就是答案。2.2 OpenCV 与 dlib 各自负责什么工具链分工必须清楚否则代码会写成谁调谁都说不清。OpenCV 负责「取和画」VideoCapture 拉摄像头流、cvtColor 转灰度、equalizeHist 拉对比度、medianBlur 去噪、rectangle 和 putText 把结果画回画面。dlib 负责「找和定位」get_frontal_face_detector 用 HOG 特征加 SVM 分类器做人脸框检测shape_predictor 在框内回归 68 个特征点的坐标。numpy 负责算距离imutils 负责缩放。为什么不用端到端 CNN 一步到位三个现实原因。第一是标注成本端到端要几千张带疲劳标签的人脸图一个人做毕设标不完。第二是可解释性卷积网络输出一个概率值答辩时说不清它凭什么判定疲劳而 EAR 是一条曲线每一帧都能指着数字讲。第三是算力HOG SVM 在纯 CPU 上跑 640×480 的画面能到 20 帧以上笔记本直接演示不用赌实验室有没有显卡。# 基础依赖。dlib 在 Windows 上需要 CMake 和 VS Build Tools缺一个就会编译失败 pip install opencv-python dlib imutils numpyimport cv2 import dlib import imutils import numpy as np # 两个模型文件人脸检测器无需下载68 点预测器需要单独放到位 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) def preprocess(frame, width640): # 统一宽度分辨率越高 dlib 越慢640 是速度与精度的折中点 frame imutils.resize(frame, widthwidth) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) # 直方图均衡弱光下把脸部对比度拉回来 gray cv2.medianBlur(gray, 3) # 中值滤波抑制摄像头噪点核必须取奇数 return frame, graypreprocess里三步的顺序不能随意调换。灰度化必须最先做因为后面两个算子都只吃单通道直方图均衡放在去噪之前否则噪声也会被一起放大均衡完再滤就滤不干净了medianBlur的核大小取 3 是保守值取 5 在弱光下会连睫毛一起糊掉EAR 会失真。imutils.resize只改宽度、按比例算高度比直接cv2.resize少一次手动算比例的出错机会。2.3 功能需求与性能需求怎么写才不空需求章节最容易写成废话比如「系统应具有良好的实时性」。合格的需求必须可测。下面这张表是按系统实际能覆盖的场景列的每一条都对应一个能现场演示的动作。功能项说明白天强光检测车窗逆光、侧光条件下仍能稳定框住人脸夜晚弱光检测车内顶灯或仪表盘微光下完成检测佩戴透明镜片眼镜检测镜片反光不导致关键点崩坏疲劳判定行驶过程中综合眼部、嘴部、头部三路信号给出等级丢脸处理检测不到人脸时不误报保留算法状态等待恢复疲劳报警达到阈值后屏幕显示预警信号并发声性能指标同样要落到公式。准确率定义为真实疲劳帧数除以总帧数目标 90% 及以上误报率定义为误判疲劳帧数除以总帧数目标 10% 及以下。这两个定义写进文档的意义在于它把「准确率」从形容词变成了可复现的计算过程——你需要人工标一段视频作为基准逐帧比对系统输出。标 500 帧大概要花一个下午但这一下午是整份文档里最有说服力的部分。3. 总体架构设计两路特征子系统、五个模块与一条帧循环3.1 模块划分与职责边界整个系统按「采集 → 预处理 → 检测定位 → 特征判定 → 报警呈现」切成五个模块同时按特征来源分成两路子系统的观察视角面部特征子系统管眼睛和嘴巴头部特征子系统管姿态角。两路并行算最后在判定层汇合。模块核心职责关键依赖视频采集打开本地摄像头按固定分辨率持续读帧cv2.VideoCapture图像预处理缩放、灰度化、直方图均衡、中值滤波imutils、cv2人脸检测与定位输出人脸框、68 点坐标、人脸区域dlib HOG 检测器、shape_predictor疲劳检测由 68 点分组算出 EAR、MAR、pitch做融合判定numpy报警与界面显示实时指标、疲劳等级、触发声音与提示Tkinter 或 PyQt5、winsound这个划分的好处是每个模块都能单独替换。比如换摄像头就从采集模块改换判定规则只动疲劳检测模块界面的同学和算法的同学可以并行开工。毕设常见的翻车是所有人挤在一个main.py里改最后谁也说不清哪行是谁写的。3.2 主帧循环从 VideoCapture 到一次判定主循环是整个系统的主干写法决定了后面所有优化有没有空间。cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) frame_idx 0 while True: ok, frame cap.read() if not ok: break frame_idx 1 frame, gray preprocess(frame) # 第二个参数 0 表示不放大改 1 会做一次上采样小脸更准但帧率减半 faces detector(gray, 0) if len(faces) 0: # 丢脸不等于疲劳不清空窗口只标记本帧无效 state.on_face_lost() cv2.imshow(Fatigue Detection, frame) if cv2.waitKey(1) 0xFF 27: break continue shape predictor(gray, faces[0]) pts np.array([(shape.part(i).x, shape.part(i).y) for i in range(68)]) left_eye pts[36:42] right_eye pts[42:48] mouth pts[48:60] ear (eye_aspect_ratio(left_eye) eye_aspect_ratio(right_eye)) / 2.0 mar mouth_aspect_ratio(mouth) angles head_pose_angles(pts, frame.shape[1], frame.shape[0]) state.push(ear, mar, angles, frame_idx) cap.release() cv2.destroyAllWindows()几个参数值得单独说。faces[0]只取第一张脸因为驾驶位上只可能有一个驾驶员取多人脸反而会引入副驾干扰。detector(gray, 0)的上采样参数是性能开关0 表示原图检测1 表示先放大一倍再检测——放大对小脸和远距离有效但耗时直接翻倍笔记本上开 1 通常就掉到 10 帧以下。cv2.waitKey(1)的 1 是毫秒等待不写这句画面根本刷不出来这是新手最常踩的一个坑。ESC 键退出ASCII 27比关窗口按钮可靠因为 OpenCV 的窗口关闭事件在部分系统上不触发。3.3 68 个特征点的分组与索引约定dlib 的 68 点是有固定编号的不记住编号后面全是玄学。索引区间部位本系统用途0–16下颌轮廓不参与判定可作人脸区域参考17–21一侧眉毛不参与判定22–26另一侧眉毛不参与判定27–35鼻梁与鼻翼取 30 号作姿态估计的鼻尖参考点36–41图像左侧眼睛6 点EAR 计算42–47图像右侧眼睛6 点EAR 计算48–59嘴部外轮廓12 点MAR 计算60–67嘴部内轮廓8 点可选用于加强哈欠判定注意dlib 的「左右」是按图像坐标说的跟人脸解剖学上的左右眼正好相反。写文档时要么统一按图像方向描述要么在图上标清楚否则读者对着代码看会以为你写反了。3.4 时间窗口与状态缓存为什么不能逐帧判疲劳单帧数据毫无意义。一帧里眼睛闭着可能只是正常眨眼一帧里嘴巴张着可能只是在说话。疲劳是时间维度上的统计现象所以必须有一个滑动窗口把最近若干帧的指标攒起来。窗口长度的选择有个换算原则按时间定不按帧数定。30 帧每秒时 3 秒是 90 帧但笔记本掉帧到 15 帧每秒时90 帧就变成 6 秒了判定会明显迟钝。稳妥的做法是记录每帧的时间戳窗口按秒裁剪。from collections import deque import time class FatigueState: def __init__(self, window_sec3.0): self.window window_sec self.ear_buf deque() # 存 (时间戳, EAR) self.mar_buf deque() self.pitch_buf deque() def _trim(self, buf, now): # 按时间戳裁剪掉帧时窗口的真实时长仍然稳定 while buf and now - buf[0][0] self.window: buf.popleft() def push(self, ear, mar, angles, _frame_idx): now time.time() self.ear_buf.append((now, ear)) self.mar_buf.append((now, mar)) if angles is not None: self.pitch_buf.append((now, angles[0])) self._trim(self.ear_buf, now) self._trim(self.mar_buf, now) self._trim(self.pitch_buf, now)deque比 list 合适因为窗口滑动时要频繁从左侧弹出list 的pop(0)是 O(n)跑久了会拖慢主循环。_trim用时间戳而不是长度做裁剪条件这是整套代码里唯一一处主动对抗帧率波动的设计改掉它判定就会在低配机器上失准。4. 三路特征实现与判定融合EAR、MAR、HPE 和 PERCLOS4.1 眼部 EAR用长宽比把「睁眼」变成一个数EAR 的思路很朴素眼睛睁开时是个偏圆的形状闭合时趋于一条线。用垂直方向的两组距离之和除以水平方向的距离再取平均就得到一个跟人脸尺度无关的比值。def eye_aspect_ratio(eye): # eye 是 6 个点的数组顺序左角、上左、上右、右角、下右、下左 a np.linalg.norm(eye[1] - eye[5]) # 上左到 下左 b np.linalg.norm(eye[2] - eye[4]) # 上右到 下右 c np.linalg.norm(eye[0] - eye[3]) # 左角到 右角作归一化基准 return (a b) / (2.0 * c)除以2.0 * c这一步是整套算法能跨人使用的原因。不同人在画面里的大小不同脸大的像素距离大、脸小的像素距离小但比值基本一致。除以 2 是取两组垂直距离的平均避免单侧关键点抖动主导结果。左右眼各算一次再平均是为了压制单眼被头发或镜框遮住时的异常值。EAR 的典型取值范围完全睁开大约 0.28 到 0.35正常眨眼瞬间会掉到 0.15 以下。判定闭眼用的阈值通常落在 0.22 到 0.25 之间具体取多少要看你的摄像头高度和俯仰角后面 4.5 节讲怎么标。4.2 嘴部 MAR哈欠和张嘴的区别在持续时间MAR 的构造跟 EAR 同源只是垂直方向取三组距离因为嘴的张开是上下颌分离中间段最容易测到位移。def mouth_aspect_ratio(mouth): # mouth 取 48~59 共 12 点局部下标 0~11 a np.linalg.norm(mouth[2] - mouth[10]) # 上内唇 到 下内唇 b np.linalg.norm(mouth[4] - mouth[8]) # 另一组上下唇 c np.linalg.norm(mouth[0] - mouth[6]) # 左右嘴角归一化基准 return (a b) / (2.0 * c)只靠 MAR 超阈值判哈欠误报会高得离谱——说话、唱歌、打嗝都会触发。真正区分哈欠的是持续时长正常说话嘴部开合是高频小幅哈欠是一次持续 1.5 秒以上的大开合。所以 MAR 判定必须配一个持续时间条件通常是连续超过阈值 45 帧约 1.5 秒才计一次哈欠计完还要锁一段时间防重复计数。4.3 头部姿态 HPEsolvePnP 把 2D 点还原成三维角点头判定用人体姿态估计的思路拿 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(pts, w, h): image_points np.array( [pts[30], pts[8], pts[36], pts[45], pts[48], pts[54]], dtypenp.float64 ) # 没有标定板就用近似内参焦距取画面宽度主点取画布中心 focal w camera_matrix np.array([[focal, 0, w / 2.0], [0, focal, h / 2.0], [0, 0, 1.0]], dtypenp.float64) dist_coeffs np.zeros((4, 1)) ok, rvec, _ cv2.solvePnP(MODEL_POINTS, image_points, camera_matrix, dist_coeffs, flagscv2.SOLVEPNP_ITERATIVE) if not ok: return None rmat, _ cv2.Rodrigues(rvec) sy np.sqrt(rmat[0, 0] ** 2 rmat[1, 0] ** 2) pitch np.degrees(np.arctan2(-rmat[2, 0], sy)) # 俯仰点头的主指标 yaw np.degrees(np.arctan2(rmat[1, 0], rmat[0, 0])) # 偏航左右转头 roll np.degrees(np.arctan2(rmat[2, 1], rmat[2, 2])) # 翻滚歪头 return pitch, yaw, rollMODEL_POINTS里的坐标是一套通用成人人脸的平均尺寸单位毫米原点在鼻尖。它不需要精确到某个真人因为 solvePnP 只关心相对结构。camera_matrix用近似内参是这套方案能落地的关键妥协——没有棋盘格标定就用画面宽度当焦距误差在可接受范围内但这也意味着 pitch 的绝对值不准只有相对变化可用。所以点头阈值不能写死成「pitch 小于 -15 度」得先让系统采集一段正常驾驶的基线再按基线的偏移量判定。欧拉角的符号方向跟坐标系定义有关有人算出来点头是负的有人是正的。别照抄符号跑一遍抬头低头的动作看数值往哪个方向走再定阈值。4.4 PERCLOS 与三指标融合判定PERCLOS 的核心是一个比例在一段时间内眼睛闭合的时间占比。工程上落成帧占比因为逐帧比较便宜。传统的 P80 定义是眼睑覆盖瞳孔面积超过 80% 的时间比例用关键点做的话等价于 EAR 低于阈值的帧占窗口总帧的比例。EAR_TH 0.23 # 闭眼阈值需按 4.5 节标定 MAR_TH 0.60 # 哈欠阈值 PITCH_TH -14.0 # 低头阈值需按基线偏移修正 W_PERCLOS, W_YAWN, W_NOD 0.55, 0.25, 0.20 # 权重来自标定实验 def judge_fatigue(ear_buf, mar_buf, pitch_buf): n max(len(ear_buf), 1) # 1) PERCLOS窗口内闭眼帧占比 perclos sum(1 for _, v in ear_buf if v EAR_TH) / n # 2) 哈欠帧占比 yawn sum(1 for _, v in mar_buf if v MAR_TH) / max(len(mar_buf), 1) # 3) 低头帧占比 nod sum(1 for _, v in pitch_buf if v PITCH_TH) / max(len(pitch_buf), 1) score W_PERCLOS * perclos W_YAWN * yawn W_NOD * nod if score 0.62: return 2, score # 重度立即报警 if score 0.40: return 1, score # 轻度界面提示 小贴士 return 0, score三个权重不是拍脑袋来的是标定阶段用正常和模拟疲劳两组数据跑出来的哪个指标在疲劳组和正常组之间的区分度大权重就给得高。PERCLOS 权重最高因为它是三者中跟疲劳相关性最稳的MAR 次之因为哈欠的个体差异大pitch 权重最低因为低头看仪表盘会污染这个信号。判定阈值 0.40 和 0.62 同样是标定结果不是普适常数。4.5 阈值标定实验把三个参数从玄学变成数据阈值是这个项目里最容易被质疑的地方。导师一定会问你这个 0.23 是哪来的答案必须是「我标出来的」不能是「参考别人论文的」。标定流程通常这样做。先采两段数据各 60 秒一段是正常状态下的静坐和自然眨眼一段是模拟疲劳状态下的缓慢闭眼、打哈欠、点头。摄像头位置、光照、佩戴眼镜情况必须跟实际演示时一致。然后把逐帧 EAR、MAR、pitch 全部存成 CSV画出分布直方图。闭眼阈值取正常状态 EAR 的均值减去两倍标准差这样正常眨眼有 95% 以上的概率不会误触。参数参考区间取值依据EAR_TH0.22 – 0.25正常睁眼均值减 2 倍标准差MAR_TH0.55 – 0.65哈欠峰值与正常说话峰值的中间偏上位置PITCH_TH基线偏移 -10° 至 -15°相对正常驾驶姿态的偏移量不是绝对角窗口长度2.5 – 3.5 秒覆盖至少两个完整眨眼周期哈欠持续40 – 50 帧明显长于正常说话的嘴部动作标定的意义不只是数值它还是性能指标的分母。90% 准确率怎么算出来的就靠标定时留下的人工标注结果。这份 CSV 建议在文档附录里贴一段答辩时直接打开给人看比任何形容词都有力。4.6 界面与报警联动的实现界面部分通常用 Tkinter 或 PyQt5四个页面分工明确。界面主要元素作用开始界面标题、开始检测、使用说明、退出进入前的功能说明与适用范围提示介绍界面算法流程、三项指标含义答辩时讲原理的落地页行车小贴士休息建议、疲劳征兆清单轻度疲劳时弹出不打断检测检测界面视频画面、EAR/MAR/pitch 实时数值、疲劳等级条、报警区主工作区检测界面必须把三个原始数值实时显示出来。这不只是为了好看它是调试期的黑匣子——判定异常时你能一眼看出是 EAR 飘了还是 pitch 崩了。报警用独立的线程发声音不要在主循环里直接Beep否则声音播放期间画面会卡住。import threading import winsound def fire_alarm(level): # 二级用长鸣一级用短提示两者都不能阻塞主循环 if level 2: threading.Thread(targetlambda: winsound.Beep(1000, 800), daemonTrue).start() elif level 1: threading.Thread(targetlambda: winsound.Beep(600, 200), daemonTrue).start()5. 避坑与排查五个最容易翻车的点5.1 白天准得离谱一到夜里就丢脸现象实验室白天演示一切正常晚上把车开到地库或者关掉顶灯人脸框直接消失系统一直走「丢脸」分支。原因dlib 的 HOG SVM 检测器是在正常光照的人脸上训的对低对比度极度敏感。灰度化之后如果整体像素集中在一个很窄的区间梯度特征就提不出来检测器直接判无人脸。解决把直方图均衡提到灰度化之后的第一位并且加一个亮度判断——整帧平均灰度低于阈值时先做一次cv2.convertScaleAbs拉亮再送入检测。同时在采集端加一盏朝向面部的补光这是最有效的一招比任何算法补偿都稳定。夜里还要把detector的上采样参数从 0 改成 1牺牲帧率换检出率。5.2 报警器响成交响乐现象进入疲劳状态后报警声每帧触发一次一秒响三十下演示现场直接失控。原因判定函数是无状态的每帧独立算一次 score只要 score 达标就调一次fire_alarm。缺一个状态机。解决加状态保持和冷却。连续 N 帧判定为疲劳才升级到报警报警后锁住 M 帧不再触发直到状态回落到正常才解锁。class AlarmGate: def __init__(self, hold45, cooldown300): self.hold hold # 连续多少帧才确认疲劳 self.cooldown cooldown # 报警后的静默帧数 self.count 0 self.rest 0 self.fired False def update(self, level): if self.rest 0: self.rest - 1 return False if level 2: self.count 1 else: self.count 0 self.fired False if self.count self.hold and not self.fired: self.fired True self.rest self.cooldown return True return Falsehold取 45 帧约等于 1.5 秒刚好跨过正常眨眼的时长cooldown取 300 帧约等于 10 秒给驾驶员一个反应窗口。5.3 帧率掉到个位数画面像幻灯片现象跑起来 CPU 单核占满帧率只有 8 到 10EAR 曲线抖得不成样子。原因detector(gray, 1)的上采样让检测耗时翻倍另外每帧都在全图跑检测画面里绝大部分像素是车内饰纯属浪费。解决三个动作按性价比排序。第一把上采样参数改回 0只在连续多帧检测失败时才临时切到 1。第二缩到 640 宽再检测1080p 直接送进去等于自找麻烦。第三加跟踪检测成功后的下一帧用dlib.correlation_tracker跟踪人脸框每 5 帧才做一次真正的检测关键点预测仍然每帧做。这一条通常能把帧率拉回 20 以上。5.4 低头看仪表盘就被判疲劳现象驾驶员正常看导航、看仪表、调空调系统一律判重度疲劳报警不停。原因pitch 只看俯仰角而低头看仪表跟疲劳点头在数值上是同一个方向单靠角度分不开。加上 yaw 大幅偏转转头看后视镜时 solvePnP 的解会失真pitch 也跟着乱跳。解决给点头判定加两个前置条件。一是 yaw 必须在正负 20 度以内转头时直接跳过点头判定二是点头必须「快速下坠再缓慢抬起」用 pitch 的一阶差分判断下降速度疲劳点头是失控的快速下坠主动低头是匀速的。把这两条加上误报能降一大截。5.5 论文里的阈值抄过来误报率飙到三成现象按参考文献里的 EAR 阈值 0.25 设好别人的数据集上跑得挺好自己摄像头一装误报率超过 30%。原因EAR 的绝对值跟摄像头高度、俯仰角、镜头焦距强相关。摄像头装在仪表台上往上拍眼睑的投影形状跟平视完全不一样同一个人的 EAR 能差 0.05。直接搬阈值必然翻车。解决重新标定而且是必须在最终安装位置标定。标定流程按 4.5 节走一遍采两段各 60 秒的数据算出自己的阈值。这一步没有任何捷径但它花的时间大概两小时远少于后期调参耗掉的时间。6. 验收与答辩用一段脚本化录像把方案讲圆到了验收环节多数人栽在同一个地方功能都能跑但说不清「怎么证明它对」。我的建议是提前准备一段脚本化测试录像把要演示的行为按固定顺序演一遍正常驾驶 20 秒、连续眨眼 10 秒、打两次哈欠、点三次头、转头看后视镜 5 秒、摘下眼镜 5 秒。全片录下来存成文件演示时用cv2.VideoCapture(test.mp4)读取而不是开摄像头现场演。这样做的最大好处是可复现——导师让你再放一遍你放的就是同一段不会因为现场光线或坐姿变化出现「刚才明明报警了」的尴尬。配合这段录像准备三张表表名内容用途功能测试用例表用例编号、操作步骤、预期结果、实际结果、是否通过逐条对应需求表证明每条需求都有验证性能指标表准确率、误报率、平均帧率、丢脸恢复时间用标定阶段的人工标注做分母阈值标定表三个阈值的取值、标定方法、参考区间回答「这个数哪来的」答辩时最容易被追问的三个问题提前把答案写进讲稿。第一个是「为什么不用深度学习」答案在 2.2 节的成本、可解释性、算力三条。第二个是「夜间怎么办」答案在 5.1 节的补光加均衡加降采样策略。第三个是「误报怎么控制」答案在 5.4 节的 yaw 过滤加下降速度判据以及 5.2 节的状态机。还有一个细节值得单独提醒录演示视频时一定要用跟最终答辩同一个摄像头、同一个分辨率。我曾见过一套系统在实验室的 720p 摄像头上跑得稳稳的换到答辩教室的 1080p 设备上因为预处理没做统一缩放帧率掉到 5整个演示卡成 PPT。从那以后我每次交付前都强制走一遍「目标机器冷启动 脚本录像回放」的流程这一步花不了十分钟但能挡掉绝大部分现场事故。希望这些能帮到你少踩两个坑多睡两小时。本文还有配套的精品资源点击获取