基于Python与CNN的低算力实时疲劳检测系统实现

基于Python与CNN的低算力实时疲劳检测系统实现 简介一套基于CNN的疲劳驾驶检测Python项目专注眨眼与打哈欠两种疲劳特征识别。项目结合openCV完成人脸、眼睛及嘴部区域定位利用卷积神经网络对眼部开合与嘴部状态进行分类适用于初学者研究图像分类、目标检测与实时监控场景也便于后续接入车载摄像头做安全预警。压缩包共15个文件、约3.94MB包含6个Python脚本涵盖预处理、人脸与眼睛检测、CNN训练与推理、3个pickle模型文件、依赖清单及示例图片结构清晰可直接对照流程二次开发。目前已有3765人浏览学习。借助该项目能拿到完整的疲劳检测实验链路从图片灰度化、直方图均衡化到Haar级联人脸检测、眼睛/打哈欠识别再到模型训练与实时评分代码模块化程度高可快速迁移到其他面部状态识别任务。 做疲劳检测这个项目其实最初是被一个做车载安全的客户需求推着走的。他需要一套能跑在低算力设备上的实时疲劳预警系统不能依赖云端API数据隐私也要握在自己手里。市面上现成方案不是太贵就是太过笨重调研一圈下来我决定用Python结合CNN卷积神经网络自己写一套源码。这套方案的核心思路不算复杂先用传统算法定位人脸关键点把眼睛区域裁出来再用一个轻量级CNN判断眼睛是睁开还是闭合最后按时间窗口统计闭眼时长和眨眼频率综合判定疲劳状态。如果你准备入门深度学习图像分类或者想做一个能实际运行的计算机视觉小项目这套基于CNN的疲劳检测源码会是一个很合适的练手对象。它涵盖了数据准备、模型搭建、训练调优、实时推理的完整链路而且整个工程对硬件要求不高CPU都能跑起来。下面我会把项目的整体设计思路、核心原理、关键代码实现以及我在实战中踩过的坑一并拆开来讲。1. 项目整体设计与思路拆解1.1 检测流程与框架选型先看整体流程设计这个项目的检测Pipeline一共有四步人脸检测、眼部区域提取、眼睛状态分类、疲劳判定。人脸检测用的是dlib的HOG或CNN检测器它输出的人脸关键点包含了左右眼各6个关键点的坐标利用这12个点可以精确框出眼部ROI。接着把裁剪后的眼部图像送入CNN模型输出睁眼或闭眼的置信度。最后在连续帧序列上进行统计如果满足疲劳条件就触发警报。这个方案的巧妙之处在于把传统图像处理和深度学习做了合理分工。dlib负责的定位任务对实时性要求高用传统算法反而比深度学习更快更稳而眼睛睁开还是闭合这种细粒度纹理分类问题正是CNN的强项。两者结合既规避了端到端模型对大规模标注数据的依赖又保持了对光照变化、头部姿态的鲁棒性。1.2 为什么选择CNN而不是其他架构有人问过我既然Transformer在视觉领域这么火为什么不直接用ViT原因有三个参数量、推理速度、数据需求。ViT在ImageNet这种千万级数据上才能发挥优势而你用来训练眼睛分类的数据集可能只有几万张硬上ViT极易过拟合。相比之下CNN的归纳偏置——局部连接和权值共享——天然适合小数据集上的图像分类。另外一个现实因素是部署环境。车载设备往往是一块树莓派或者Jetson NanoCNN模型可以轻松量化到几MB一个卷积层加池化层的组合在CPU上跑一次前向传播只需几毫秒。Transformer的自注意力机制在处理高分辨率特征图时计算复杂度是平方级增长的在边缘设备上往往要吃大亏。所以在这个场景下CNN依然是最务实的方案。2. 核心细节解析与实操要点2.1 数据集构建与预处理疲劳检测的核心是眼睛状态分类所以数据集质量直接决定模型上限。我实验了三种数据来源公开数据集CEWClosed Eyes in the Wild、自己采集的视频帧、以及通过数据增强扩充的样本。CEW大约有1192张闭眼照和1231张睁眼照数量偏少所以我又录了20段不同光照条件下的面部视频每隔几帧截取一次眼部ROI。预处理阶段有几个细节值得注意。眼部图像先转灰度再缩放到24x24这个尺寸是LeNet时代验证过的经验值既能保留眼睑纹理细节又能减少计算量。归一化时将像素值除以255再减去均值可以加速模型收敛。关键一步是数据增强随机旋转正负10度、水平翻转、亮度调整以及加少量高斯噪声实测能把验证集准确率从94%拉到98%左右。提示裁剪眼部ROI时不要裁得太紧。我一开始按关键点坐标精准裁剪模型在测试集上表现不错一上实时视频就频繁误判——因为头部轻微晃动时眼睛边缘会被裁掉一块。后来我在ROI四周各扩展10像素问题立刻缓解。2.2 CNN模型架构设计模型我设计了一个精简版LeNet结构在保证准确率的前提下尽可能减少参数量。输入是24x24的灰度图第一层卷积用32个3x3卷积核输出20x20的特征图接2x2最大池化第二层卷积用64个3x3卷积核输出8x8特征图再接池化得到4x4展平后接128个神经元的全连接层Dropout设为0.5最后用Softmax输出两个类别概率。为什么第一层用32个卷积核而不是16个我做过对比实验16个卷积核在训练初期loss下降得差不多但最终验证集准确率低了约1.5个百分点。卷积核数量直接决定特征提取的容量32个是一个性价比比较好的平衡点。3x3卷积核的另一个好处是叠加两层等效于一个5x5的感受野但参数量只有后者的18/25这就是VGG系列被验证过的“小卷积核堆叠优于大卷积核”原理。Dropout层放在全连接层之前非常关键。我的全连接层有128x128个参数加上之前的卷积层总参数量不到20万如果没有Dropout训练到第15轮左右就会在验证集上出现明显的过拟合信号——训练loss持续下降验证loss开始回升。加上Dropout之后模型在40轮训练内都保持了稳定的泛化能力。2.3 疲劳判定阈值与逻辑模型输出的只是一帧画面中眼睛的状态要把单帧结果转化为疲劳状态需要一套时间窗口统计逻辑。这里我用的是经典的PERCLOS标准加上眨眼频率辅助判断。PERCLOS是指单位时间内眼睛闭合时间所占的比例心理学研究表明它和疲劳程度高度正相关。具体实现上我维护了一个长度为60的环形缓冲区按30fps的帧率算刚好对应2秒的统计窗口。系统计算三个指标闭眼帧占比、每分钟眨眼次数、单次闭眼最大时长。判定逻辑采用加权评分闭眼帧占比超过40%记2分平均每分钟闭眼超过8次记1分单次闭眼超过0.5秒记1分总分超过3分触发疲劳警报。实测这个规则在驾驶场景下比较可靠不会因为偶尔一次眨眼误报。3. 实操过程与核心环节实现3.1 环境配置与依赖安装建议直接用Anaconda创建独立环境避免把系统Python搞乱。我推荐Python 3.8TensorFlow 2.6dlib 19.22OpenCV 4.5。为什么不推荐PyTorch两个框架在模型训练上的差异不大但TensorFlow的TensorFlow Lite工具链在部署到移动端或嵌入式设备时更省事如果你的目标平台是树莓派TFLite的转换工具会更友好。conda create -n fatigue_det python3.8 conda activate fatigue_det pip install tensorflow2.6.0 pip install dlib19.22.0 pip install opencv-python4.5.5.64 pip install imutilsdlib的安装是唯一可能出问题的环节Windows下没有预编译wheel包的话直接从源码编译需要先装好CMake和Visual Studio Build Tools。另一个省力办法是用pip install dlib-bin实测在Windows上能直接装上编译好的版本。3.2 数据加载与预处理代码实现数据加载部分我写了一个自定义DataFrame生成器从文件夹结构自动读取正负样本。这里的关键点是用ImageDataGenerator做实时数据增强而不是在内存中先扩充数据集。实时增强的好处是可以让模型每个epoch看到略微不同的样本相当于隐式扩大了数据集规模。import cv2 import numpy as np from tensorflow.keras.preprocessing.image import ImageDataGenerator def load_eye_data(data_dir, img_size(24, 24)): datagen ImageDataGenerator( rescale1./255, rotation_range10, width_shift_range0.1, height_shift_range0.1, brightness_range[0.8, 1.2], zoom_range0.1, horizontal_flipTrue, validation_split0.2 ) train_generator datagen.flow_from_directory( data_dir, target_sizeimg_size, color_modegrayscale, batch_size32, class_modecategorical, subsettraining, shuffleTrue ) val_generator datagen.flow_from_directory( data_dir, target_sizeimg_size, color_modegrayscale, batch_size32, class_modecategorical, subsetvalidation, shuffleFalse ) return train_generator, val_generator目录结构要求是data_dir/open_eye和data_dir/closed_eye两个子文件夹。validation_split0.2会自动把每个类别的20%样本划为验证集不用手动切分。数据增强参数里brightness_range在光照变化强烈的场景特别有用我测试过把这个参数去掉后模型在逆光条件下的误判率提高了近一倍。3.3 模型构建与训练参数调优模型搭建用的是Keras Sequential API看起来清晰也方便保存。训练时的优化器选择没有用默认的Adam而是换成了AdamW加上了权重衰减。原因在于小数据集上Adam容易出现训练集准确率100%但验证集停滞在96%左右的“虚高”问题AdamW的权重衰减能抑制这种情况。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout def build_cnn_model(input_shape(24, 24, 1), num_classes2): model Sequential([ Conv2D(32, (3, 3), activationrelu, input_shapeinput_shape), MaxPooling2D(pool_size(2, 2)), Conv2D(64, (3, 3), activationrelu), MaxPooling2D(pool_size(2, 2)), Flatten(), Dropout(0.5), Dense(128, activationrelu), Dropout(0.3), Dense(num_classes, activationsoftmax) ]) model.compile( optimizertf.keras.optimizers.AdamW(learning_rate1e-3, weight_decay1e-4), losscategorical_crossentropy, metrics[accuracy] ) return model训练轮数我设为40轮加上早停机制监控验证集loss连续5轮不下降就停止。学习率调度采用的是余弦退火初始1e-3训练过程中逐步衰减到1e-5。训练完成后模型的测试集准确率稳定在98.7%左右模型文件大小仅890KB已经具备部署到嵌入式设备的条件。3.4 实时检测与疲劳判定完整逻辑推理环节是整个工程的核心它把dlib关键点检测、CNN前向推理和疲劳统计逻辑串在一条流水线上。关键点检测比较耗时这里做了一个优化用dlib.get_frontal_face_detector()检测人脸后再取关键点但如果画面中只有一个人脸且上一帧已经检测到了就直接用correlate_tracker跟踪人脸区域这样能把单帧处理时间从约50ms压缩到约20ms。import cv2 import dlib import numpy as np from collections import deque from tensorflow.keras.models import load_model # 初始化 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) eye_model load_model(eye_state_model.h5) # 疲劳统计参数 EAR_THRESH 0.25 CLOSE_RATIO_THRESH 0.4 BLINK_RATE_THRESH 8 CLOSE_DURATION_THRESH 0.5 # 环形缓冲区 fps 30.0 buffer_size int(fps * 2) eye_state_buffer deque(maxlenbuffer_size) blink_times deque(maxlen30) def compute_eye_region(shape, eye_points): # 根据68点关键点计算眼部ROI xs [shape.part(i).x for i in eye_points] ys [shape.part(i).y for i in eye_points] min_x, max_x min(xs), max(xs) min_y, max_y min(ys), max(ys) # 向四周扩展10像素 expand 10 min_x max(0, min_x - expand) min_y max(0, min_y - expand) max_x min(frame_width, max_x expand) max_y min(frame_height, max_y expand) return min_x, min_y, max_x, max_y # 主循环核心逻辑 while True: ret, frame cap.read() gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 人脸检测或跟踪 faces detector(gray, 0) if len(faces) 0: face faces[0] shape predictor(gray, face) # 提取左右眼区域 left_eye compute_eye_region(shape, range(36, 42)) right_eye compute_eye_region(shape, range(42, 48)) # 分别预测并取平均置信度 left_pred predict_eye_state(gray[left_eye[1]:left_eye[3], left_eye[0]:left_eye[2]]) right_pred predict_eye_state(gray[right_eye[1]:right_eye[3], right_eye[0]:right_eye[2]]) eye_closed 0.5 * (left_pred right_pred) 0.5 # 更新缓冲区 eye_state_buffer.append(eye_closed) update_blink_tracking(eye_closed) # 计算疲劳指标 metrics assess_fatigue(eye_state_buffer, blink_times) if metrics[fatigue_score] 3: alarm_trigger()predict_eye_state函数内部会先对ROI做灰度转换、resize到24x24、归一化然后调用model.predict()。这里要提醒你两帧之间如果人脸消失了一瞬间缓冲区的数据会出现空洞最简单的处理是丢弃该帧不更新任何状态不要用上一次的结果填充否则会引入偏差。4. 常见问题与排查技巧实录4.1 训练准确率高但实时检测不准这是我遇到最多的问题也是最典型的小数据集陷阱。训练集准确率能到99%但一接摄像头就频繁误判。排查下来原因有三类训练数据和真实场景分布不一致比如训练集全是室内灯光而测试场景有强光直射二是预处理不一致训练时用rescale1./255归一化推理时却忘了做同样处理三是ROI裁剪尺寸不一致训练时是24x24推理时直接resize到别的尺寸。解决方案也很直接录制10到20段不同环境下的真实视频把眼部ROI的截图补充到训练集中。这套“数据闭环”的思路非常有效——每发现一个误判案例就把对应的眼部图像加入数据集重新训练。迭代三轮之后系统的鲁棒性会有一个肉眼可见的提升。4.2 人脸检测漏检和误检dlib的HOG人脸检测器在正面人脸表现很好但侧脸和低头场景下容易漏检。疲劳驾驶场景中司机经常会有低头、转头动作这会导致检测器暂时失效。我在实际项目中做了两个优化一是降低检测器的检测阈值从默认的0.6降到0.4让更多人脸候选框被保留二是加入追踪器人脸跟丢后不是立刻重新检测而是用dlib.correlation_tracker继续追踪最多30帧。还有一个容易忽略的细节dlib检测到多张人脸时默认取第一张不一定可靠。我选择了面积最大的那张人脸因为疲劳检测场景中目标人脸通常是离摄像头最近的也就是面积最大的。这个小小的改动让误检率下降了很多。4.3 实时性优化与帧率提升如果你在树莓派上跑这套系统帧率可能会掉到个位数。我实测了一套优化组合拳把摄像头分辨率从1080p降到640x480人脸检测耗时直接从80ms降到20ms推理时把model.predict()换成model.predict_on_batch()省去每次调用的Session开销用模型量化把float32权重转为int8模型体积从890KB缩到240KB推理速度提升约2.3倍。帧率提上去之后还要注意一点统计窗口的时长是依赖帧率标定的如果实际帧率和代码里设定的fps不一致疲劳判定会失真。稳妥的做法是用cv2.getTickCount()实时测量每帧间隔而不是硬编码一个fps常量。4.4 常见问题速查表问题现象可能原因解决方案训练loss不收敛学习率过大或数据未归一化降到1e-3以下检查输入是否在[0,1]区间验证集准确率远低于训练集过拟合增大Dropout比例增加数据增强强度闭眼检测总比睁眼敏感类别不平衡用class_weight给闭眼类别更高权重多人场景识别混乱取错人脸框按人脸面积取最大目标报警频繁触发阈值设置过低调大闭眼占比阈值增加确认帧数CPU推理耗时过长模型过大改用深度可分离卷积或进行int8量化5. 一些经验与扩展思考这套系统的检测精度在实验室环境下达到了98%以上但真正部署到实际场景之后我意识到单靠眼睛状态还不够可靠。有人戴墨镜、有人天生眼睛小、有人习惯性眯眼看路这些都会干扰判定。所以后来我在疲劳判定模块中加入了头部姿态估计用dlib关键点计算头部俯仰角和偏转角当司机频繁点头或头部偏转角度过大时也会触发预警。多模态信号融合才是疲劳检测落地到实际场景的最终解法。如果你准备复现这个项目我建议你从数据集质量抓起不要急着调模型结构。先跑通端到端流程再根据你实际使用场景的反馈逐步迭代。这种“先做出来再调好”的思路本身就是做视觉项目最实用的方法论。本文还有配套的精品资源点击获取