
简介面向机器学习与计算机视觉方向学习者的疲劳驾驶检测系统完整源码包基于Python语言开发实现融合人脸关键点检测与机器学习算法通过分析驾驶员面部视频中的眼睑开合、头部姿态等特征为疲劳驾驶预警提供可落地的技术方案。包内共29个文件包含8个MP4测试视频、5个XML工程配置、3个Python核心源码、2个编译后的Python文件、1个DAT人脸关键点模型、4个PNG界面图片以及LICENSE、GITIGNORE、IML等辅助文件压缩包约151.9MB。其中视频与图片构成训练/测试数据集XML与IML记录开发环境和模块配置Python与pyc实现算法逻辑和桌面UIDAT为预训练模型TXT与MP3提供说明和提示音频。目前已有227人学习适合需要完整课设/赛题方案或进行二次开发的读者可对照源码、示例视频与说明文档理解数据采集、特征提取、模型调用及UI交互的完整链路。1. 疲劳驾驶检测为什么先死在数据上而不是模型疲劳驾驶检测系统真正落地时大家默认的难点是“模型不够准”但做过实车测试的人几乎都会撞上同一个结论模型是在哪里训练、用什么特征进去的比模型本身更决定成败。拿公开数据集训练时准确率能到95%以上换到自己的摄像头画面立刻降到七成这不是玄学是特征和数据分布没对齐。我打算用一套基于Python机器学习特征工程的方案把这件事讲清楚人脸关键点提取眼睛纵横比、嘴巴纵横比和头部姿态再交给随机森林或SVM去分类清醒与疲劳状态最后用OpenCV接实时摄像头触发报警。这套系统的价值在于每步都可复现适合做课程设计、毕业设计或产品原型验证的人也适合想从零学会计算机视觉和机器学习区别的读者——前者输出特征后者负责分类边界比端到端深度学习清晰得多。2. 系统框架与选型传统机器学习怎么打赢深度学习2.1 特征驱动方案与端到端深度学习的边界疲劳检测的主流路径有两条一条是用卷积神经网络直接从视频帧预测疲劳状态一条是先用人脸关键点提取手工特征再用经典机器学习算法分类。很多初看这个题目的人第一反应是“都202X年了还用机器学习不上CNN是不是过时了”但实际做工程时特征驱动方案有它不可替代的位置。CNN方案需要大量标注好的疲劳视频帧疲劳样本的标注比物体检测难得多——什么是“疲劳”的画面在摄像头下很模糊连续打哈欠算不算眼睛闭多久算疲劳这些判定标准在不同人脸上差异巨大。公开数据集不够自己标又贵这是大多数深度方案在半路失败的根本原因。而特征方案把问题拆成了两步第一步只做“眼睛开了多大、嘴张了多大”的几何测量不直接回答疲劳第二步才用机器学习把几何数据映射到疲劳状态。中间多出的这一步让人能插进去规则、阈值、时序统计这是深度模型难以做到的。特征方案另一个优势是性能。在树莓派或老旧笔记本上跑dlib人脸关键点检测能到15到25帧每秒再加一个随机森林分类器计算开销几乎可以忽略而MobileNetV3这类轻量CNN虽然也能跑但实时视频流里每帧都要做卷积CPU占用和发热会让长时间运行变得很难受。如果你做的系统还要并发检测多路摄像头整体帧率会进一步拉开差距。所以我的建议很直接做疲劳驾驶检测系统第一版基本不要碰深度学习用既有的人脸关键点库加上机器学习分类器先把准确率、误报率、延迟这三个指标跑出来再去考虑是否用深度网络替换。只有当你需要识别“揉眼睛”“打哈欠”“点头”这种复合动作而不仅仅是眼睛开合度时深度模型才值得重新评估。简单几何特征在高分辨率正面人脸场景下已经足够解决大部分问题。2.2 人脸关键点提取与开发环境搭建整个系统最底层的依赖是人脸关键点检测。我常用的是dlib的68点模型它在标准正面和侧面30度以内的人脸上表现稳定输出的是左右眼、眉毛、鼻梁、嘴巴、下颌轮廓共68个坐标点。疲劳特征主要用到36到48这几个点——左眼6个点、右眼6个点、嘴巴外轮廓8个点它们在索引表里有固定编号不需要自己训练模型。搭建环境这一步有很多人栽过跟头。dlib在Windows上pip直接安装往往会触发源码编译如果没有配好CMake和VS Build Tools会报一堆C错误这是最常见的环境踩坑点。我一般会用conda创建虚拟环境从conda-forge通道安装预编译的dlib省掉编译环节。conda create -n fatigue python3.9 -y conda activate fatigue conda install -c conda-forge dlib opencv -y pip install scikit-learn numpy scipy imutils joblib这里把Python锁在3.9是为了兼容性dlib和OpenCV在3.10以上版本偶尔会出现扩展库加载失败。scikit-learn负责训练随机森林和SVMscipy的distance模块用来算欧氏距离joblib负责模型持久化imutils是OpenCV的辅助工具库处理截图缩放和旋转时少写很多循环。关键点模型文件需要单独下载文件大约60到100MB在dlib官方模型页面能找到文件名是shape_predictor_68_face_landmarks.dat。下载后放到项目根目录路径写在配置里不要硬编码到每个脚本内部。模型下载和安装环境是两件容易被混为一谈的事环境装好了模型文件却没有运行时会直接在加载那行报错FileNotFoundError这种报错信息最典型也最容易排查看到就能定位到文件缺失。2.3 一个最小可运行的图像采集与特征计算脚本环境就绪之后我建议先不急着训练分类器而是写一个几十行的调试脚本把摄像头画面里左右眼的6个关键点画出来确认dlib加载正常、关键点索引正确。这一步能帮你分成两个变量摄像头问题还是模型问题。import cv2 import dlib # 初始化人脸检测器与人脸关键点检测器 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) cap cv2.VideoCapture(0) if not cap.isOpened(): raise RuntimeError(摄像头无法打开检查设备索引或权限) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 参数1表示对图像做一次上采样小人脸也能检测到速度会变慢 faces detector(gray, 1) for face in faces: landmarks predictor(gray, face) # 左眼关键点为索引36到41右眼为42到47 for i in range(36, 42): point landmarks.part(i) cv2.circle(frame, (point.x, point.y), 2, (0, 255, 0), -1) for i in range(42, 48): point landmarks.part(i) cv2.circle(frame, (point.x, point.y), 2, (0, 255, 0), -1) cv2.imshow(face landmarks, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()detector的第二个参数1代表对输入图像做一次金字塔上采样这样能检测到更小的人脸但是会把速度拖慢近一倍。如果摄像头距离人脸较近、画面里人脸足够大我一般会把参数改成0换取更高的实时帧率。cam.read()返回ret和frame两个值ret为False时直接break这是防止摄像头被拔出时脚本卡死的标准写法。这个脚本跑起来后你应该能看到左右眼周围各贴着6个绿点且能跟随人脸移动。如果点画出来了但在错误的位置说明模型加载正常只是坐标系理解有问题如果点完全不动或程序卡死问题多半在摄像头帧率和waitKey的刷新逻辑上。把这一步当成整个项目的体检比直接跳到训练要稳妥得多。3. 特征工程把疲劳写进数字里3.1 EAR、MAR、PERCLOS 三件套怎么算才算对特征工程是这个项目里信息密度最高的部分。在疲劳检测文献里眼睛纵横比EAR是使用最广泛的指标它把6个眼部关键点压缩成一个标量眼睛睁得越大EAR值越高正常平视时通常在0.25到0.35之间闭眼时骤降到0.1以下。嘴巴纵横比MAR的原理相同用来检测打哈欠正常闭嘴时接近0.2打哈欠时会显著超过0.5。from scipy.spatial import distance as dist def eye_aspect_ratio(eye): # 垂直方向两组关键点的欧氏距离 vertical_a dist.euclidean(eye[1], eye[5]) vertical_b dist.euclidean(eye[2], eye[4]) # 水平方向一组关键点的欧氏距离 horizontal dist.euclidean(eye[0], eye[3]) ear (vertical_a vertical_b) / (2.0 * horizontal) return ear def mouth_aspect_ratio(mouth): # 嘴巴外轮廓的垂直距离与水平距离之比 vertical_a dist.euclidean(mouth[2], mouth[10]) vertical_b dist.euclidean(mouth[4], mouth[8]) horizontal dist.euclidean(mouth[0], mouth[6]) mar (vertical_a vertical_b) / (2.0 * horizontal) return mar我这里eye参数传入的不是原始坐标点而是按dlib的索引切出来的6个点列表。左眼是landmarks的36到41右眼是42到47顺序不能搞混否则垂直距离会变成对角距离EAR值会整体失真。mouth是用48到59里的关键点按照外轮廓顺序取8个自己在调试时可以打印出来核对坐标值。EAR计算公式本身不复杂但“算得对”和“算得稳”是两回事。常见的错误是直接拿单帧EAR做疲劳判定导致眼睛一个个眨就触发报警。正确的做法是把EAR放到时间窗口里做统计——计算PERCLOS也就是单位时间内眼睛闭合时间所占比例。具体实现是记住过去60秒内EAR低于闭眼阈值的帧数再除以总帧数。这个统计指标比瞬时EAR稳定得多因为驾驶疲劳本身是一个缓慢累积的过程眨眼这种生理动作单次只有100到200毫秒不应该被算成疲劳。闭眼阈值怎么定我的经验是先采集一段自己正常睁眼和闭眼的视频分别算出EAR的中位数取两者的中间值作为初值一般在0.18到0.25之间。不要直接抄文献里的0.2就万事大吉戴眼镜、单眼皮、眼睛小的人EAR基线差异巨大有人睁眼EAR只有0.22阈值设0.2就会持续误报。3.2 数据集选择与自建样本的注意点训练分类器需要带标签的数据。公开数据集方面NTHU-DDD和YawDD是疲劳驾驶领域常用的两个视频数据集包含正常驾驶、打哈欠、闭眼等多个场景视频帧里有已经标注好的事件标签。这类数据集适合做预训练和验证算法流程但它们的人脸角度、车内光照与你的实际摄像头安装位置往往不一致拿它们训练的模型换到实车上掉点很正常。我一般会做两件事来缓解数据偏移第一用公开数据集预训练一个基础分类器第二用自己摄像头录制一个几十秒的短样本分为正常状态和疲劳状态两段再在基础分类器上做调整。自建样本不需要逐帧手动标注录制时人为控制状态即可——正常驾驶时保持睁眼平视疲劳状态模拟频繁眨眼和低头闭眼按时间段切分标签。这段自采数据虽然粗糙但它是整个测试闭环里最重要的部分。它代表了真实使用环境的光照、摄像头角度、人脸距离而这些因素在模型训练里往往比算法本身影响更大。我见过有人花一周时间调模型参数准确率纹丝不动最后一查原因是摄像头装在方向盘下方人脸只露出了下半张脸眼睛关键点全被遮挡。数据集不对后面再怎么调参都是白费功夫。3.3 特征向量构造与训练数据矩阵的组装单帧的EAR和MAR只有两个数值作为分类器输入太单薄我习惯把单帧特征扩成滑动窗口统计量包括当前帧的EAR和MAR、过去30帧EAR的平均值和标准差、过去5秒MAR超过阈值的帧数占比以及头部俯仰角变化量。import numpy as np def build_feature_vector(ear_now, mar_now, ear_history, mar_history, head_angle): ear_arr np.array(ear_history) mar_arr np.array(mar_history) # 统计历史窗口内的分布特征压缩成8维向量 features [ ear_now, # 当前帧眼睛纵横比 mar_now, # 当前帧嘴巴纵横比 float(np.mean(ear_arr)), # 近30帧EAR均值 float(np.std(ear_arr)), # 近30帧EAR标准差 float(np.mean(mar_arr)), # 近30帧MAR均值 float(np.sum(mar_arr 0.5)) / len(mar_arr), # 最近5秒打哈欠帧占比 abs(head_angle), # 头部点头幅度绝对值 1 if ear_now 0.2 else 0 # 当前帧是否闭眼标志 ] return np.array(features).reshape(1, -1)Windows下有一个隐蔽的头文件依赖如果你使用scipy.optimize或某些数值计算路径需要Microsoft Visual C Redistributable环境缺失时表现为ImportError而不是编译错误建议在conda环境里直接安装conda install -c conda-forge scipy来连带补齐运行库。save函数负责持久化最近30帧的列表我在sys.stdout.buffer.write前加了一层buffer刷新确保print输出在标准输出被重定向到文件时不会卡在缓冲区里这在后续无人值守跑数据采集时能减少因输出缓冲导致的信息丢失。最后的4个长度校验参数和中心裁剪策略都是防止模型输入尺寸不匹配的保险。我还在debugTrue的分支里用cv2.imwrite保存了每一帧的预览图方便回查某段数据丢失的具体原因这个习惯帮我避免了不少“莫名奇妙的帧率跳动”现场。4. 模型训练与参数调优随机森林打底SVM做对比4.1 为什么先上随机森林疲劳驾驶样本是典型的类别不平衡数据清醒状态占多数疲劳状态短暂且稀少。逻辑回归在这类场景下往往欠拟合而随机森林作为集成树模型天然自带特征选择能力对于8维到十几维的输入特征不需要做标准化也不用担心特征尺度差异造成的权重倾斜。随机森林的工作原理是训练多棵决策树每棵树在抽样出的样本和子特征上学习最终用投票决定结果。它的好处体现在两个实际工程场景中一是特征的离散非线性关系能被树结构自动捕捉比如EAR小但头部角度正常可能是短暂眨眼EAR小且头部角度偏转大才更像瞌睡这种联合关系在树模型里会分裂成不同路径不需要手工设计交互特征二是它能输出特征重要性训练结束后可以直接看到在最终模型中EAR均值、MAR占比、头部角度哪一项权重更大这对后续向导师或同事汇报系统设计时非常有说服力。SVM在疲劳检测文献中出场率也很高它擅长在小样本高维空间里找分类超平面但需要做特征标准化并且核函数和惩罚系数的选择比较敏感调参周期长。我的策略是把随机森林作为默认方案SVM作为对照组用交叉验证分数来验证随机森林是否真的在当前数据上更优而不是凭感觉选模型。4.2 训练、验证、保存模型的完整代码把上一章攒出来的特征矩阵和标签送入模型训练这步的代码量其实不大重点在于数据切分和模型验证的方式要规范。import numpy as np from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, confusion_matrix import joblib # 加载特征工程阶段保存的数据 X np.load(features.npy) y np.load(labels.npy) # 按类别比例分层切分随机种子固定保证结果可复现 X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) model RandomForestClassifier( n_estimators200, max_depth12, min_samples_leaf4, class_weightbalanced, random_state42 ) model.fit(X_train, y_train) # 在验证集上输出混淆矩阵和逐类别精确率 y_pred model.predict(X_val) print(classification_report(y_val, y_pred, target_names[awake, fatigue])) print(confusion_matrix(y_val, y_pred)) # 保存模型文件部署时直接load joblib.dump(model, fatigue_model.pkl)train_test_split里的stratifyy是最关键的参数它保证切分后训练集和验证集中疲劳样本的比例与原始数据一致避免随机切分把一小段连续的疲劳视频帧全部落进验证集导致验证指标虚高。random_state固定为42是为了同一份数据每次切分结果一致否则你无法分辨准确率的变化是模型改进带来的还是切分运气造成的。class_weightbalanced用来缓解类别不平衡问题它的含义是让少数类在计算分裂增益时获得更高权重而这正是疲劳样本数量少时最容易踩的坑。min_samples_leaf4限制了叶子节点的最少样本数防止树对少数类样本进行无意义的精确记忆也就是常说的过拟合。模型保存用joblib而不是pickle因为joblib对大数组对象和嵌套对象的序列化效率高很多且加载出来就是可以直接调predict的完整对象。4.3 三个必调参数与调参顺序调参这件事在所有机器学习项目里都容易被夸大但对随机森林来说真正决定上限的通常只有三个参数max_depth、min_samples_leaf和n_estimators其余参数在疲劳检测这个低维特征场景下影响很小。我的调参顺序是先调max_depth从8到16逐步试观察验证集准确率的变化。max_depth过小会让树无法捕获特征之间的交互关系过大则会让树在训练集上无限分裂疲劳样本被逐帧精确记忆验证集一测就翻车。然后是min_samples_leaf从1到8逐个试这个参数的作用是强制叶子节点拥有足够多样本相当于对树做平滑正则数值越大模型越保守。最后才是n_estimators从100试到500超过300之后准确率提升通常不到1个百分点但预测时间会线性增加得不偿失。这三个参数之间有相互作用用网格搜索同时调三个参数在计算上是浪费的因为它们对模型的影响方向可以解耦。我一般会先用默认参数跑一遍拿到基线准确率再调max_depth看趋势确定深度后固定再调min_samples_leaf这样每一步的变化都可以归属到单一参数上。如果你把三个参数同时丢进GridSearchCV得到的是一组在验证集上最优但缺乏可解释性的组合换一批数据往往立刻失效这就是过拟合验证集本身。调参的另一个重要指标是看混淆矩阵而不是只看准确率。疲劳检测系统里最关键的错误类型是“把疲劳判为清醒”这会直接导致漏报事故而“把清醒判为疲劳”虽然烦人但最多是误报。如果分类报告里fatigue类的召回率低于0.85说明模型在漏报此时优先调class_weight和阈值不要盲目加深树。5. 实时检测与系统集成从离线分类器走到摄像头画面5.1 实时流水线的代码骨架离线阶段训练好的模型在实时系统里要重新设计数据流。每一帧画面被采集后要依次经过人脸检测、关键点提取、特征计算、分类预测四个步骤这四步在单线程里做会直接拉低帧率。我的做法是分层处理人脸检测和最耗时的关键点提取放在主循环里特征计算和分类则是纯数值运算几乎不占时间。import cv2 import dlib import numpy as np import joblib from collections import deque detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) model joblib.load(fatigue_model.pkl) # 保存最近30帧的EAR和MAR值用于构造时序特征 ear_history deque(maxlen30) mar_history deque(maxlen30) ear_threshold 0.2 # 闭眼判定阈值按实际采集数据动态调整 perclos_frames 0 def extract_eye_and_mouth(landmarks): # 按dlib索引切出左右眼和嘴巴外轮廓的坐标点 left_eye [(landmarks.part(i).x, landmarks.part(i).y) for i in range(36, 42)] right_eye [(landmarks.part(i).x, landmarks.part(i).y) for i in range(42, 48)] mouth [(landmarks.part(i).x, landmarks.part(i).y) for i in range(48, 60)] return left_eye, right_eye, mouth cap cv2.VideoCapture(0) frame_count 0 while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) if len(faces) 0: face faces[0] landmarks predictor(gray, face) left_eye, right_eye, mouth extract_eye_and_mouth(landmarks) ear (eye_aspect_ratio(left_eye) eye_aspect_ratio(right_eye)) / 2.0 mar mouth_aspect_ratio(mouth) ear_history.append(ear) mar_history.append(mar) # 用滑动窗口构造特征向量交给随机森林分类 feature build_feature_vector(ear, mar, ear_history, mar_history, 0.0) state model.predict(feature)[0] if ear ear_threshold: perclos_frames 1 # 每秒计算一次PERCLOS并判断是否触发报警 if frame_count % 30 0: perclos perclos_frames / 30.0 perclos_frames 0 if perclos 0.4 or state 1: cv2.putText(frame, FATIGUE - WAKE UP!, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2) frame_count 1 cv2.imshow(fatigue detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()detector(gray, 0)这里参数改成了0就是为了保证摄像头实时预览的帧率因为detector上采样一次要额外消耗几十毫秒。deque(maxlen30)的设计很巧妙窗口满后新数据进来会自动挤掉最旧的数据不需要手动维护FIFO队列历史EAR的均值和标准差计算直接拿这个队列算就行。perclos_frames的统计窗口是30帧在30帧每秒的摄像头下正好是1秒这样可以评估每秒内闭眼帧的比例是否超过0.4。state 1是随机森林模型输出的疲劳倾向预测两者取“或”关系触发报警这样即使某帧特征不够典型只要长时间闭眼比例超限同样能报警。实际工程里报警条件往往比这更严格需要连续多个窗口都触发才真正拉起蜂鸣器否则一两秒的信号抖动就会让系统变成狼来了。5.2 防误报逻辑眨眼与闭眼的区分单帧EAR低于阈值就报警是疲劳检测系统最典型的错误设计因为它混淆了眨眼和闭眼的区别。眨眼是生理现象正常人每分钟眨眼15到20次每次持续时间约100到150毫秒换成30帧每秒的摄像头就是3到5帧。如果用单帧阈值判定相当于把眨眼当成疲劳信号误报频率会高到让人直接关掉系统。正确的方法是做持续闭眼判定只有EAR连续低于阈值持续超过比如20帧也就是约0.67秒才认为进入微睡眠状态。实现方式是用一个计数器在每一帧判断EAR是否低于阈值是则加1否则清零当计数超过20时才置疲劳标志。这个逻辑比直接看单帧状态更贴近生理学中微睡眠的定义同时也能滤掉眨眼造成的短暂丢失。另一个动作干扰源是低头看仪表盘或按中控按钮此时摄像头画面里的人脸在移动关键点仍然存在但角度变化明显EAR可能因为透视原因短暂变小触发误判。此时需要引入头部姿态估计通过人脸平面上的多个关键点求解头部俯仰角俯仰角绝对值超过30度时应该优先判定为“低头”而不是疲劳等待头部回正后再恢复检测。这需要一个简单的solvePnP求解器可以用OpenCV的相机模型估算姿态也可以简化成只用鼻尖和两侧眼睛中心点的相对位移做近似角度计算。5.3 常见坑与排查现象、原因、解决坑一摄像头画面卡顿但CPU不高。现象是画面显示延迟严重帧率只有个位数但任务管理器里Python进程CPU占用并不高。原因是cv2.VideoCapture默认开启了摄像头缓冲区缓冲区里堆积了大量旧帧程序处理的速度赶不上摄像头产生帧的速度于是反复读到老画面。解决办法是设置像素格式和缓冲大小并手动跳帧。标准做法是cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)同时把读取放进单独线程尽量保证主循环拿到的是最新帧。坑二光照变化后关键点检测崩溃。现象是上午跑得好好的下午逆光或进入隧道时人脸关键点飘忽不定甚至检测不到人脸。原因是dlib的正面人脸检测器对光照变化敏感画面过亮或过暗都会导致特征丢失。我的处理是先对灰度图做直方图均衡化再送入检测器也就是cv2.equalizeHist能把对比度拉开。如果夜间场景多建议直接换红外摄像头因为普通摄像头在暗光下噪点太多关键点模型几乎没有抵抗能力。坑三戴眼镜的人频繁误报疲劳。现象是同一套参数下戴眼镜的测试者误报率明显高于不戴眼镜的人。原因是镜框反光和镜片畸变干扰了眼睛关键点的定位EAR值被压低到阈值以下。解决思路不是调高阈值而是对关键点坐标做平滑滤波。我在工程里会对连续几帧的眼睛关键点坐标做移动平均把单帧的定位噪声过滤掉让EAR曲线变得平滑后再判断。这个方法对普通镜框有效但对重度反光的太阳镜还是会失效那种情况只能换专门的近红外疲劳检测方案。坑四验证集准确率很高实车一测就大幅下降。现象是离线阶段分类报告显示准确率96%装到车上跑了十分钟就频繁漏报。原因是训练数据里的脸部位置和测试现场不一致比如数据采集中人脸通常居于画面中央而实际驾驶时司机会转头看后视镜。这说明模型学到的特征里掺杂了人脸位置信息。解决办法是在数据收集阶段刻意让人脸位置在画面中变化加入左右转头、低头、抬头动作让训练分布覆盖实际使用分布。坑五长时间运行后内存缓慢上涨。现象是系统跑一两个小时CPU正常但内存占用不断增长。原因是每个检测循环都在创建新的list和numpy数组历史队列虽然限制了长度但显示用图像和临时变量没有被及时释放。对Python来说最直接的办法是在循环体里避免使用会持续增长的全局容器所有临时结果用完即丢同时把视频帧的副本控制在需要显示的路径上。还有一个隐藏来源是cv2.imshow在窗口模式下会保存内部状态无头服务器上记得去掉这行。6. 验证方法进阶用回放实验测出真实准确率离线指标只能证明模型在历史数据上的拟合程度不能证明系统在真实连续性驾驶场景里的价值。我在完成模型部署后的第一件事是录制一段回放视频设计一个“模拟驾驶”实验来逼近真实指标。录制时找一名测试者坐进驾驶位按3分钟正常驾驶、1分钟频繁眨眼、1分钟长时间闭眼、1分钟打哈欠加低头这样的顺序依次进行全程不中断录制。录制结束后把这段视频重新喂给检测系统系统只输出判定结果不显示视频画面用另一份脚本逐秒对齐真实状态和预测状态。这个流程能同时产出一张混淆矩阵和一组逐秒错误曲线比凭记忆判断系统好坏可靠得多。实验得到的三个指标要分开看疲劳样本的召回率是最优先的低于0.8基本不能上路测试清醒样本的误报率要控制在每小时一次以下否则司机很快就会对报警声脱敏平均单帧处理时间是系统能否实时的硬指标超过50毫秒就要考虑裁剪检测区域或降低分辨率。在这套系统上走了完整流程之后我养成了一个习惯每次改动特征或模型参数都保留同一段回放视频作为回归测试基准确保改动只在局部改善而不是到处拆东墙补西墙。这个习惯帮我避免了很多次“验证集分数升高但实际体验变差”的假象。疲劳驾驶检测本质上是一个闭环系统模型只是其中一环摄像头安装角度、报警交互逻辑、阈值校准流程共同决定它是否值得被使用。希望这些实践细节能帮你在自己落地时少走几段弯路。本文还有配套的精品资源点击获取