尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Dlib与OpenCV的驾驶员疲劳检测:关键点、EAR/MAR与点头识别

基于Dlib与OpenCV的驾驶员疲劳检测:关键点、EAR/MAR与点头识别 简介这是一套基于图像检测的Python驾驶员疲劳检测系统面向计算机视觉和人工智能方向的学生、研究者及开发者用于识别打哈欠、眨眼、瞌睡点头等疲劳行为并附带可直接部署的安装程序。压缩包共包含21个文件大小约85.77MB其中有Python核心源码、Jupyter Notebook演示、界面工程文件、人脸68关键点模型、测试视频、附赠内容包及UI运行程序等可覆盖模型加载、特征提取、界面交互完整链路。已有55人学习下载。资源内置三类行为的判定阈值连续3帧内眼睛长宽比0.2视为眨眼嘴部长宽比0.5视为打哈欠头部pitch旋转角0.3视为瞌睡点头方便对照算法原理进行调参和复现。配套的图形界面程序可快速查看检测效果测试视频便于直接验证并含README与许可说明帮助理清工程结构。整体适合作为图像处理、深度学习或Python编程的实战练习项目可体验从视频流到预警输出的全过程。来源为网络分享仅供学习交流使用。1. 疲劳检测为什么必须落到图像上打哈欠、眨眼与点头的视觉信号深夜开长途眼皮打架方向盘上的人往往自己都意识不到“我已经睡着了”。摄像头对着驾驶员脸用 Python 做实时人脸关键点检测通过眼睛开合比例、嘴巴张开持续时间和头部下沉角度就能在驾驶员连续眨眼、哈欠连天、点头瞌睡时给出报警。这套方案不依赖脑电或方向盘传感器成本就在一个普通 RGB 摄像头加一台工控机。适合三类人做驾驶安全课设毕设的学生、给车队或网约车做疲劳提醒的小团队、想在智能座舱里预研视觉方案的开发者。先说结论别一上来就追深度学习目标检测Dlib 的 68 点关键点在普通 CPU 上就能跑 25 帧以上完全够用。2. 搭出检测主框架OpenCV dlib 面部关键点2.1 选型为什么用 dlib 的 68 点关键点而不是 OpenCV 自带人脸检测做疲劳检测第一件事不是写检测代码而是选人脸关键点模型。OpenCV 自带的 Haar 级联检测器只能给出人脸矩形框给不出眼睛、嘴巴、鼻子的坐标所以没法往下算眼睛闭合比例。OpenCV 也有 Facemark 关键点模型但训练数据比较老对戴眼镜、逆光场景容易飘而且关键点只有 35 个嘴巴和眼睛的采样点不够密。MediaPipe 的 Face Mesh 能给出 468 点精度更高但它是基于 TensorFlow Lite 的打包成 exe 时体积会大一圈还可能引入一堆依赖。我一般用 dlib 的 HOG 人脸检测器加 shape_predictor_68_face_landmarks.dat 模型。它输出 68 个关键点索引固定左眼 36 到 41右眼 42 到 47嘴巴外圈 48 到 59鼻尖 30下巴 8。这套索引几乎成了行业约定网上大部分开源代码都按这个顺序写遇到问题好查。更重要的是dlib 的检测器是传统特征点不依赖 GPU在 i5 级别的 CPU 上640 x 480 分辨率下每秒能跑 20 到 30 帧放到驾驶室这种固定机位绰绰有余。这里提醒一句不管你是用 PyCharm 还是 VSCode 配的 Python 环境先确认 Python 是 64 位的。dlib 的预编译 wheel 基本只给 64 位 Python32 位环境下经常要自己编译编译要装 Visual Studio Build Tools不是不能装而是容易把 Python 环境搞乱。我自己就踩过这个坑后来统一用 64 位 Python 3.8 或 3.9省了很多后续打包的麻烦。2.2 最小可运行的实时取帧与关键点标注代码先写一个最简版本把摄像头画面读出来跑人脸检测和关键点预测把 68 个点画在画面上。这样你能直观看到模型有没有跑通也为后面计算 EAR、MAR 打基础。import cv2 import dlib # 初始化人脸检测器与关键点预测器 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) cap cv2.VideoCapture(0) # 0 表示默认摄像头 while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # dlib 检测器吃灰度图 faces detector(gray, 1) # 第二个参数是上采样次数 for face in faces: landmarks predictor(gray, face) for i in range(68): x landmarks.part(i).x y landmarks.part(i).y cv2.circle(frame, (x, y), 1, (0, 255, 0), -1) cv2.imshow(face_landmarks, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码里有两个地方值得解释。detector(gray, 1)的第二个参数是图像金字塔上采样次数设为 1 相当于把图像放大一倍后再检测能提高远距离小脸的召回率代价是每帧耗时变长。如果你摄像头离人很近改成 0 反而更稳。cv2.waitKey(1)的参数是等待键盘输入的毫秒数1 表示每帧最多阻塞 1 毫秒保证循环以摄像头帧率运行如果设成 0画面会卡住只有按键才继续这在实时检测里是个容易手滑的坑。还要说一个很多人忽略的点灰度图。dlib 的 HOG 检测器输入是单通道灰度图因为原始特征依赖梯度方向对色彩不敏感直接输入 BGR 三通道图会报维度错误。转换用cv2.cvtColor就好不要用cv2.imread的灰度模式读摄像头摄像头根本没有灰度模式这个说法。2.3 三个必调参数上采样次数、摄像头编号、模型文件路径这节把刚才提到的参数单独拉出来讲它们直接影响检测速度和稳定性。第一个是检测器上采样次数第二个是摄像头编号第三个是模型文件路径。参数常见取值影响detector(gray, n)的 n0 或 1越大越能检测小脸但每帧多花 10~20mscv2.VideoCapture(device)0、1、2 或视频文件路径笔记本多摄像头时要逐个试shape_predictor(...)路径绝对路径或相对路径路径错了直接抛异常或找不到模型摄像头编号是翻车最多的。笔记本一般有内置摄像头和外接 USB 摄像头内置可能是 0也可能是 1。而且系统会把虚拟摄像头、OBS 的虚拟设备也算进去。遇到黑屏时不要怀疑代码先把编号从 0 到 3 全部试一遍通常就能解决。模型文件路径也值得注意。shape_predictor_68_face_landmarks.dat是 dlib 官方发布的预训练模型体积大约 90 多 MB不要放到代码目录里就以为万事大吉。如果你用 PyInstaller 打包后面会发现它不会自动被收进 exe需要在打包配置里显式指定这在第 5 章会详细说。3. 打哈欠和眨眼的判定EAR/MAR 指标与连续帧决策3.1 EAR 眼睛纵横比为什么用比例而不是绝对像素眼睛闭合程度可以用一个叫 EAREye Aspect Ratio眼睛纵横比的指标来衡量。它不是用眼睛的绝对宽度或高度而是用两个垂直距离除以水平距离。最早是 Soukupová 和 Cech 在 2016 年发表的论文里提出的后来几乎成了疲劳检测的标准特征。左眼的六个关键点编号是 36、37、38、39、40、41右眼是 42、43、44、45、46、47。以左眼为例EAR 的计算公式是EAR (||P36 - P41|| ||P37 - P40||) / (2 * ||P38 - P39||)也就是上眼皮两个点到下眼皮两个点的垂直距离之和除以眼角水平距离的两倍。人眼睁开时EAR 大概在 0.25 到 0.35 之间闭上眼睛时上下眼皮靠近垂直距离趋近于零EAR 会掉到 0.1 以下。我们可以在 Python 里实现这个计算注意 dlib 的索引是从 0 开始所以左眼编号是 36 到 41和论文中的 1-based 编号差一个偏移。import math def eye_aspect_ratio(eye_points): # eye_points 是一个长度为 6 的列表每个元素为 (x, y) vertical_1 math.dist(eye_points[1], eye_points[5]) vertical_2 math.dist(eye_points[2], eye_points[4]) horizontal math.dist(eye_points[0], eye_points[3]) return (vertical_1 vertical_2) / (2.0 * horizontal)math.dist是 Python 3.8 自带的两点欧氏距离函数不用再手写平方根。这个 EAR 值的妙处在于它是个比例和摄像头到人脸的距离无关。你靠近摄像头眼睛像素变大水平和垂直距离同时变大比值基本不变你往后靠像素变小比值还是接近原来的值。这就避免了“换了个坐姿阈值就得重调”的尴尬。3.2 MAR 嘴巴纵横比与打哈欠阈值打哈欠用嘴巴纵横比MARMouth Aspect Ratio来识别。嘴部关键点很多外圈是 48 到 59一共 12 个点。计算时取上下嘴唇之间的垂直距离和嘴角水平距离的比例。和 EAR 类似公式是MAR (||P50 - P58|| ||P51 - P57||) / (2 * ||P48 - P54||)这里 P48 和 P54 是左右嘴角P50 到 P58、P51 到 P57 是上下嘴唇唇线的点。人在正常说话时 MAR 会短时间波动但打哈欠时嘴巴张开的持续时间长、幅度大MAR 会持续超过 0.5甚至到 0.8。如果不做连续帧计数把短暂张嘴误判成哈欠的概率会很高。def mouth_aspect_ratio(mouth_points): # mouth_points 是外圈 12 个点的列表 vertical_1 math.dist(mouth_points[2], mouth_points[10]) vertical_2 math.dist(mouth_points[4], mouth_points[8]) horizontal math.dist(mouth_points[0], mouth_points[6]) return (vertical_1 vertical_2) / (2.0 * horizontal)注意这里 mouth_points 的体积顺序要按照关键点 48 到 59 的索引0 位对应 P486 位对应 P54。如果你手滑把顺序写反MAR 会忽大忽小最后阈值不管怎么调都不对。3.3 状态机连续帧计数怎么把一次眨眼和一次哈欠记准单独看每一帧的 EAR 和 MAR 是不够的因为噪声和轻微表情变化会造成抖动。我一般用一个简单的状态机低于阈值的状态连续保持 N 帧才真正记一次事件。眨眼的特点是“快速闭上再睁开”正常一次眨眼大约 100 到 150 毫秒在 25 帧每秒的摄像头下就是 3 到 4 帧。所以判断眨眼时EAR 低于阈值连续 2 到 3 帧就可以记一次超过 5 帧还没睁开就说明可能是病理性闭眼或已经睡着需要升级报警。打哈欠则相反持续时间长成年人一次哈欠大约 3 到 5 秒对应 75 到 150 帧。所以 MAR 高于阈值连续 20 帧以上才计一次哈欠。下面这个类把两个计数器放在一起方便后面接入疲劳评分。class FatigueCounter: def __init__(self, ear_threshold0.2, mar_threshold0.5, blink_frames3, yawn_frames20): self.ear_threshold ear_threshold self.mar_threshold mar_threshold self.blink_frames blink_frames self.yawn_frames yawn_frames self.blink_counter 0 self.yawn_counter 0 self.blink_total 0 self.yawn_total 0 def update(self, ear, mar): # 眨眼计数 if ear self.ear_threshold: self.blink_counter 1 else: if self.blink_counter self.blink_frames: self.blink_total 1 self.blink_counter 0 # 打哈欠计数 if mar self.mar_threshold: self.yawn_counter 1 else: if self.yawn_counter self.yawn_frames: self.yawn_total 1 self.yawn_counter 0 return self.blink_total, self.yawn_total四个参数里EAR 阈值是关键。不同人的眼睛大小、眼眶深度会差很多我之前在暗光环境下测同一个人的 EAR 能从 0.28 掉到 0.22但还没眨眼。这种情况下固定阈值容易误报。我的做法是先拍 30 秒正常驾驶画面算出 EAR 的均值再用均值减去 0.08 作为阈值。MAR 阈值相对稳定0.5 到 0.6 之间都可以因为我只需要区分“张嘴”和“哈欠”真正起区分作用的是持续帧数。另外要注意判断眨眼用了“连续低阈值帧数”这个计数器在 EAR 恢复正常时要清零。如果你不清零一个人眼皮半睁半闭持续很久计数会累加成一个贴别大的数眨眼总数直接爆炸。这也是上面代码里else分支的作用。4. 瞌睡点头检测头部姿态估计的轻量实现4.1 为什么需要第三个信号只靠眼睛和嘴巴会漏掉微睡眠在真实驾驶场景里有些司机进入微睡眠时眼睛并不完全闭合眼皮会保持在半睁状态EAR 可能一直高于 0.15眨眼计数也正常。打哈欠也不是时刻都有尤其是刚上高速的前半小时疲劳还不明显。但点头这个动作几乎和瞌睡同步出现因为颈部肌肉放松头部会不受控制地往下沉然后因为本能反应猛地抬起来。这个“快速低头再抬头”的过程就是点头。只做眼睛和嘴巴检测等于只有两个输入信号漏报率会很高。所以第三个通道必须上。但这里不需要特别精确的三维姿态估计因为我们要抓的是“头部相对正常姿态持续下沉”而不是精确的角度。越简单越不容易被摄像头安装位置和驾驶震动影响。4.2 用 2D 关键点比例做点头检测最小代码与标定常见做法是拿 dlib 输出的关键点算一个“低头比”眼睛中心到鼻尖的距离除以鼻尖到下巴的距离。人脸正对摄像头时这个比值相对固定低头时鼻尖和下巴在图像里的距离被压缩比值会明显变大。注意这个指标和摄像头安装高度有关系换一个安装位置就必须重新标定阈值。下面是核心代码配合前面的 landmarks 直接用。import numpy as np def head_down_ratio(landmarks): nose np.array([landmarks.part(30).x, landmarks.part(30).y]) chin np.array([landmarks.part(8).x, landmarks.part(8).y]) left_eye np.array([landmarks.part(36).x, landmarks.part(36).y]) right_eye np.array([landmarks.part(45).x, landmarks.part(45).y]) eye_center (left_eye right_eye) / 2.0 eye_nose np.linalg.norm(eye_center - nose) nose_chin np.linalg.norm(nose - chin) return eye_nose / (nose_chin 1e-6) def is_nod(landmarks, threshold1.15): ratio head_down_ratio(landmarks) # 排除左右摇头左右眼到鼻尖的距离差过大时不算点头 left_eye np.array([landmarks.part(36).x, landmarks.part(36).y]) right_eye np.array([landmarks.part(45).x, landmarks.part(45).y]) nose np.array([landmarks.part(30).x, landmarks.part(30).y]) d_left np.linalg.norm(left_eye - nose) d_right np.linalg.norm(right_eye - nose) if abs(d_left - d_right) 0.3 * (d_left d_right): return False, ratio return ratio threshold, ratione 加1e-6是为了防止鼻尖和下巴重合时除零。阈值 1.15 是我在驾驶室场景里的经验值平视时eye_nose / nose_chin大约是 0.9 到 1.0低头 20 度以上会超过 1.15。但不同人的脸型会改变这个基数圆脸和长脸的差异不小所以上线前一定要做一次标定让司机正常坐好记录 10 秒再低头 30 度记录 10 秒取两个状态的中间值作为阈值。这个方法的局限性是只能感应”点头“不能区分“低头看仪表盘”和“瞌睡点头”。解决方法是加一个持续时间约束低头状态要持续超过 2 秒并且之后快速回升才记一次点头。短暂低头看仪表盘通常不到 1 秒不会触发。4.3 疲劳评分把眨眼频率、哈欠次数、点头次数加权有了眨眼、哈欠、点头三个计数后下一步是合成一个疲劳分数。最简单的办法是统计固定时间窗内的累计值再按系数加权。下面这个类用时间戳记录眨眼时间算最近一分钟的眨眼频率同时维护哈欠和点头的累计窗口。from collections import deque import time class FatigueScorer: def __init__(self, window_seconds120): self.window window_seconds self.blink_timestamps deque() self.yawn_timestamps deque() self.nod_timestamps deque() def add_event(self, kind): ts time.time() if kind blink: self.blink_timestamps.append(ts) elif kind yawn: self.yawn_timestamps.append(ts) elif kind nod: self.nod_timestamps.append(ts) def score(self): now time.time() for buf in (self.blink_timestamps, self.yawn_timestamps, self.nod_timestamps): while buf and buf[0] now - self.window: buf.popleft() blink_rate len(self.blink_timestamps) * (60.0 / self.window) yawn_count len(self.yawn_timestamps) nod_count len(self.nod_timestamps) # 正常每分钟眨眼 15 到 20 次低于 8 次或高于 25 次都算疲劳征兆 blink_score 0.5 if (blink_rate 8 or blink_rate 25) else 0.0 yawn_score 0.3 if yawn_count 2 else 0.0 nod_score 0.8 if nod_count 2 else 0.0 return min(1.0, blink_score yawn_score nod_score)加权逻辑很简单点头是强信号一次就记 0.8打哈欠两次记 0.3眨眼频率异常记 0.5。这些权重没有绝对标准你可以按业务调整。需要注意的是评分窗口不要拉太长120 秒足够覆盖一次打哈欠和一次点头又不至于把十分钟前的疲劳信号一直挂在脸上。实际部署时我还会在评分超过 0.9 且持续 10 秒后触发声音报警避免瞬时误报。5. 安装程序制作与常见问题排查把模型和运行库一起交付5.1 打包前先理清环境dlib 与 Python、OpenCV 的版本绑定代码跑通只是第一步把检测系统变成安装程序才是真正的坑。Python 的库在哪个目录下、dlib 的二进制文件绑定了哪个 Python 版本这些问题在开发机上不明显一旦打包就全冒出来了。先说 Python 版本。dlib 的预编译 wheel 对 Python 版本很挑剔我用的是 Python 3.8搭配 dlib 19.24 和 OpenCV 4.x这个组合在 x64 Windows 上最稳。PyInstaller 不支持跨 Python 版本打包所以在 VSCode 配置 Python 环境时一定要让虚拟环境和你后来打包用的 Python 解释器是同一个。如果你开发用的是 Anaconda 里的 Python打包也必须在同一个 conda 环境下执行否则会报ModuleNotFoundError: dlib但你已经明明装了 dlib。另外dlib 自带一个dlib.dllOpenCV 有一堆插件 DLL默认打包工具扫不到全部依赖。我的经验是不要用-F打单文件单文件虽然分发方便但启动时要解压几十 MB 的数据到临时目录杀毒软件经常拦。用目录模式dist 下是一个完整文件夹模型文件放在同目录安装程序直接把这个文件夹塞进 Program Files用户启动更快也容易排查缺 DLL 的问题。5.2 用 PyInstaller 生成可执行文件spec 文件与隐藏导入PyInstaller 基本命令是这样pip install pyinstaller pyinstaller -D --hidden-import dlib --collect-all cv2 .\fatigue.py-D表示生成目录模式不是单文件。--hidden-import dlib强制 PyInstaller 追踪dlib模块因为 dlib 的动态库是在运行时加载的如果不加打出来的 exe 会卡在初始化阶段。--collect-all cv2会把 OpenCV 的.pyd文件、DLL、数据文件全部拷到输出目录少了这个参数exe 常见会报ImportError: DLL load failed。但命令行的参数一多就乱我更推荐用 spec 文件。PyInstaller 第一次运行会生成一个.spec文件手动把数据文件加进去之后每次打包都复用# fatigue.spec from PyInstaller.utils.hooks import collect_all datas [(shape_predictor_68_face_landmarks.dat, .)] hiddenimports [dlib] binaries [] for package in [cv2, dlib]: b, d, h collect_all(package) binaries b datas d hiddenimports h a Analysis( [fatigue.py], pathex[], binariesbinaries, datasdatas, hiddenimportshiddenimports, noarchiveFalse, ) ...这段 spec 文件里的核心是collect_all。它帮我们把cv2和dlib的全部动态库、子模块、配置文件一次性收集进来省去自己逐个找 DLL 的时间。模型文件通过datas显式放在 exe 同目录下这样用户以后想换模型直接替换.dat文件即可不用重新打包。打包完成后先不要急着做安装程序在开发机上直接双击dist\fatigue\fatigue.exe验证一次。如果这个 exe 能正常识别摄像头和模型再进入安装程序制作。5.3 用 Inno Setup 制作安装程序模型文件、运行库与桌面快捷方式安装程序我习惯用 Inno Setup脚本语言简单支持多语言生成的安装包就是标准 Windows 向导。下面是一个可用的最小脚本[Setup] AppName驾驶员疲劳检测系统 AppVersion1.0 DefaultDirName{pf}\FatigueDetector OutputBaseFilenameFatigueDetectorSetup Compressionlzma2 SolidCompressionyes ArchitecturesInstallIn64BitModex64compatible [Files] Source: dist\FatigueDetector\*; DestDir: {app}; Flags: recursesubdirs Source: vc_redist.x64.exe; DestDir: {tmp}; Flags: deleteafterinstall [Run] Filename: {tmp}\vc_redist.x64.exe; Parameters: /quiet /norestart; \ StatusMsg: 正在安装 VC 运行库...; Flags: waituntilterminated; \ OnlyBelowVersion: 6.0 Filename: {app}\FatigueDetector.exe; Description: 启动疲劳检测系统; \ Flags: nowait postinstall skipifsilent脚本里有两个关键点。第一DefaultDirName默认是{pf}\FatigueDetector也就是 Program Files 下的英文目录千万不要改成中文目录名OpenCV 的 DLL 在非 ASCII 路径下可能加载失败。第二[Run]段静默安装 VC 运行库这是解决目标机器缺少concrt140.dll这类报错的最直接办法。模型文件在安装脚本里不用单独写因为 5.2 里已经把.dat放进了 dist 文件夹Source: dist\FatigueDetector\*会把它一并拷过去。安装后所有文件都在{app}目录下exe 和模型文件同级。5.4 常见问题排查5 个安装与启动阶段的坑这里整理了几条我在交付安装包时反复遇到的坑每一条都是真实场景。现象 1双击 exe 没有窗口任务管理器里有进程但一闪而过。原因杀毒软件拦截了 PyInstaller 的临时目录释放或者 dlib 的 DLL 没被正确收集。PyInstaller 的目录模式会在运行时才加载 DLL杀毒软件容易把这种“释放后加载”的行为判定为恶意。解决先把整个 dist 目录加入杀毒软件白名单再运行 exe。如果正常了说明是拦截问题如果仍然闪退在 dist 目录下打开 cmd手动运行fatigue.exe看控制台输出的异常信息。现象 2提示“由于找不到 concrt140.dll无法继续执行代码”。原因目标机器缺少 VC 2015-2022 运行库。dlib 和 OpenCV 的二进制都依赖这个运行库。解决把vc_redist.x64.exe随安装包分发在 5.3 的[Run]段里已经写了静默安装。注意要让安装程序先装运行库再装 exe否则用户点了“下一步”之后直接崩溃。现象 3安装到中文或俄文字母路径下exe 能启动但摄像头画面全黑或者识别率骤降。原因OpenCV 的某些插件在非 ASCII 路径下加载失败但不是每次都会报错属于那种“玄学”问题。解决安装脚本里锁定默认安装路径同时把安装路径校验写进去用户只能手动输入英文或数字路径。不要在安装程序里做中文路径的“友好提示”技术债会很难还。现象 4模型加载失败提示找不到 shape_predictor_68_face_landmarks.dat。原因PyInstaller 默认不收集.dat文件程序运行时用的是开发机里的绝对路径换成 exe 所在目录就找不到了。解决在 spec 文件的datas里显式加入模型文件并设置相对路径。打包前再检查 dist 目录里有没有那个.dat文件没有就先别做安装包。现象 5摄像头被占用检测窗口是灰的但程序不报错。原因笔记本摄像头被视频会议软件占用或者设备编号不是 0。解决把摄像头编号从 0 改成 1、2 再试。代码里最好加一个-c命令行参数让值班人员不用改代码就能切换设备。6. 进阶用日志和回放曲线校准阈值而不是靠肉眼调参阈值这个东西最容易翻车。EAR 阈值设大了会一直误报眨眼设小了人真睡着都检测不到。与其对着屏幕一遍遍试不如给检测系统加一条日志通道用离线数据校准。我在FatigueCounter和head_down_ratio跑通的每个循环里会额外写一行 CSV。字段包括当前时间戳、帧序号、EAR、MAR、低头比、综合评分。代码只有几行import csv from datetime import datetime def log_frame(frame_id, ear, mar, nod_ratio, score): with open(fatigue_log.csv, a, newline) as f: writer csv.writer(f) writer.writerow([ datetime.now().isoformat(), frame_id, round(ear, 3), round(mar, 3), round(nod_ratio, 3), score ])跑完一段 10 分钟的视频后用 Python 的 matplotlib 把 EAR 和低头比画在同一张图上。我通常会标出人工观察到的三次打哈欠和两次点头的区间看这些区间的曲线是不是和日志里的峰值对得上。对不上就说明阈值或关键点索引有问题而不是算法本身不行。我自己最常用的一套校准流程是先录一段司机正常驾驶 5 分钟再录一段他模拟打哈欠和低头瞌睡的 2 分钟视频把日志跑一遍取正常区间的 EAR 均值和低头比均值再分别加一个 0.08 和 0.15 的裕量作为阈值。这样比网上抄来的 0.2 和 1.15 更贴合实际摄像头位置。记住别人的阈值是别人的副驾驶位置你的摄像头装在遮阳板上还是挡风玻璃上效果完全不一样。最后再说一个习惯每次交付安装包之前我会把fatigue_log.csv清空一次然后在目标机器上跑 30 秒确认日志文件能正常写入。这个细节能同时验证三个东西exe 有没有权限写当前目录、摄像头有没有权限访问、模型有没有正确加载。哪怕只是给熟人装一个测试版这个 30 秒也不算白费。希望帮到你。本文还有配套的精品资源点击获取
返回列表