☰
Python+卷积神经网络的人脸识别疲劳检测系统:从YOLOv5到PERCLOS预警
2026/10/2 2:42:02 网站建设 项目流程

简介:一款基于Python卷积神经网络的人脸识别驾驶员疲劳检测与预警系统完整项目,面向毕业设计、课程设计及项目开发场景,适合具备Python基础的深度学习学习者直接参考与二次扩展。项目从人脸朝向、眼睛开合度、眨眼频率、瞳孔收缩率等特征入手,实现打哈欠、眨眼、点头三类疲劳行为的实时检测与安全提示,代码结构覆盖数据处理、模型训练、界面交互等环节。压缩包共包含20个文件,以11个Python脚本为核心,辅以TXT运行说明、XML人脸特征分类器、HDF5预训练模型及可直接运行的EXE程序,整体大小78.33MB,目录清晰易上手。目前已有723人学习下载,资源内附完整源码、文档及模型文件,便于理解卷积神经网络在疲劳检测中的实际应用流程,也支持在此基础上进一步延伸与优化。

1. 这套基于Python卷积神经网络的人脸识别疲劳检测系统,到底解决什么问题

疲劳驾驶是路上最隐蔽的事故源头,司机自己往往意识不到眼睛已经闭上。把卷积神经网络(CNN)用在驾驶员疲劳检测上,等于给车内装了一双不眨眼的眼睛:摄像头对着人脸,模型判断眼睛是睁开还是闭合,再结合时间窗口判断“这人是不是困了”,困了就触发预警。这套方案要解决的,不是“识别这是谁”,而是“识别这个人现在的状态”,这也是它和人脸识别门禁的最大区别。适合毕业设计、课程设计拿来落地,也适合做产品原型——你不需要从零发明算法,但要能把模型、摄像头、预警逻辑串成一套能跑的完整系统。这篇文章会把从原理到踩坑的整个路径讲清楚,按着做就能复现。

2. 原理先行:为什么疲劳检测要用卷积神经网络

2.1 传统方案卡在哪,CNN补上了什么

在CNN方案之前,最常见的疲劳检测做法是dlib人脸关键点 + 眼睛纵横比阈值判断。dlib能给出人脸68个关键点,取眼睛周围6个点算EAR值,睁眼时EAR大约0.25,闭眼时降到0.1以下,设个阈值就能判断。这套逻辑本身没错,但真实驾驶场景会把它逼到墙角:逆光时关键点定位偏移、戴墨镜时眼睛区域特征丢失、侧脸时关键点直接飞掉、车内昏暗时整张脸都检测不到。

CNN的本质是学习“眼睛区域长什么样”的视觉特征,而不是依赖手工设计的几何规则。它直接在像素上做卷积,把图像里“睁眼”和“闭眼”的分布差异学出来,对光照变化、姿态变化、遮挡的容忍度都明显高于手工特征方案。这也是为什么这一类系统普遍把卷积神经网络作为检测前端的原因。

2.2 把问题拆成两步:人脸检测 + 眼睛状态识别

很多人第一次做会想:直接用CNN端到端判断“困了没”。这个想法听着省事,实际做起来非常糟糕。疲劳是一个时间维度上的状态,单帧图像只能判断“眼睛开没开”,要判断“疲劳”需要连续几十帧的上下文。所以工程上必须拆成两步:

第一步是人脸检测,定位驾驶员脸部在画面里的位置。这里用CNN检测器,比如YOLOv5的人脸检测模型,输出人脸边框。第二步才是眼睛状态识别,把面部区域或眼部区域裁剪出来,送入一个二分类CNN网络,输出“open”或“closed”的置信度。这个两步链路的好处是可控:人脸检测不准时可以单独调检测模型,眼睛状态误判时可以单独调分类模型,不会牵一发动全身。

2.3 三套常见方案对比:自制小CNN、YOLOv5两分类、MediaPipe

方案精度表现部署成本适合场景
自制小CNN(几层卷积+全连接)数据集质量决定上限最低,CPU也能跑课程设计、快速原型
YOLOv5两分类(直接检测睁开/闭合眼睛)稳定,兼顾检测与分类需要GPU训练,推理可CPU生产可用、毕业设计完整项目
MediaPipe人脸网格 + 关键点规则对光照敏感最轻,实时性最好前期验证、算法对比

我一般推荐YOLOv5这个路线。它本身是CNN目标检测网络,标题里的“卷积神经网络”落在它身上最有说服力,同时它能在检测人脸的同时输出闭眼/睁眼两个类别的边界框,训练脚本、预训练权重、数据集格式都是现成的,学生项目做到“完整可交付”的难度最低。自制小CNN适合讲解原理,但要从人脸区域里再裁眼睛、再训练,链路长;MediaPipe则更偏工程摆拍,和CNN的关联弱一些。

2.4 为什么YOLOv5两分类能同时承担“检测”和“状态识别”

YOLOv5是anchor-based的目标检测网络,它在Backbone里用卷积提取特征,Neck层做多尺度融合,Head层输出类别和边框。我们用它训练“open”和“closed”两个类别的眼睛检测:输入一张驾驶画面,模型会框出驾驶员的左眼和右眼,并对每个框给出“打开的概率”和“闭合的概率”。

和“先裁眼睛再分类”的老路相比,这个方案省掉了眼睛区域裁剪的像素坐标换算——那一环节在真实摄像头画面中经常因为人脸偏转而出错。YOLOv5直接把“眼睛在哪里”和“眼睛什么状态”合并成一个卷积网络的前向推理问题,逻辑简单,端到端可训练,推理时一次forward就拿到全部结果,帧率也好看。

3. 从零搭建可复现环境:Python版本、依赖安装与数据集准备

3.1 Python版本与依赖安装:一套踩过坑的版本组合

环境搭错是这类项目最常见的第一道坎。YOLOv5官方要求Python 3.8以上,我试过Python 3.11配合旧版torch的坑,也遇到过pip默认装到conda环境外面的问题。最稳的组合是:Python 3.8或3.10 + PyTorch 1.13 / 2.0 + CUDA 11.7/11.8。如果你只是课程设计且电脑没有NVIDIA显卡,纯CPU版torch也能跑,只是训练时间会从几十分钟拉长到几个小时。

创建虚拟环境后,依赖安装顺序这样做:

conda create -n fatigue python=3.10 conda activate fatigue pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics opencv-python numpy pillow pandas

逻辑说明:先把torch和torchvision单独装,避免和ultralytics的依赖冲突。--index-url指定CUDA 11.8对应的wheel源,如果你没有NVIDIA GPU,去掉这行改用pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu即可。ultralytics包已经内置了YOLOv5的接口,opencv-python负责视频流读取,pandas后面用来统计疲劳指标。

参数说明:Python版本不要选3.12,部分老版本依赖会编译失败;CUDA版本要和显卡驱动匹配,训练前用python -c "import torch; print(torch.cuda.is_available())"验证GPU可用,返回False时别急着换显卡,先查驱动和CUDA版本是不是对不上。

3.2 数据集来源与取舍:公开闭眼数据集和自采数据怎么组合

眼睛开闭检测在学术界有相对成熟的开源数据集,比如CEW(Closed Eyes in the Wild)和MRL Eye Dataset,两类样本加起来都够训练一个基础模型。但公开数据集有个共同问题:以欧美人脸为主,光照条件偏实验室环境,放在真实行车记录仪画面上效果会打折扣。

我一般建议“公开数据打底 + 自采数据纠偏”。先下载公开数据集训练出一版能用的模型,再用摄像头对着自己录10分钟视频:正常睁眼、故意闭眼、戴眼镜、戴墨镜、侧脸、低头,各录一段,把画面按帧截取出来,筛选后补进训练集。这样模型才能在交付演示时扛住你的真实环境,而不是只在别人的数据集上自嗨。

3.3 VOC转YOLO标注格式:转换脚本与四个边界坑

如果你用的数据集是VOC格式的XML标注,需要转成YOLO的txt格式,否则ultralytics读不了。转换脚本的核心逻辑如下:

import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, out_path, class_map): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text.strip().lower() if name not in class_map: continue box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) x_center = (x1 + x2) / 2 / img_w y_center = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{class_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") if lines: out_path.write_text("\n".join(lines), encoding="utf-8")

逻辑说明:VOC标注里的坐标是像素值,YOLO格式需要归一化到0~1。脚本先取图片宽高,再把xmin/ymin/xmax/ymax换算成中心点坐标和宽高,最后写出类别id cx cy w h的文本行。class_map是{"open": 0, "closed": 1}这样的字典。

参数说明:把坐标从int转成float后,要检查w和h是否接近0,如果原标注里存在退化的框,直接跳过而不是带病训练。另外YOLO要求图片和txt同名且在同一目录,图片用.jpg、标注用.txt,路径对了训练才能找得到。

VOC转YOLO有四个高频坑:一是XML里size标签缺失,脚本直接抛异常,处理老数据集时要给size加容错;二是类别名大小写不一致,Open和open会变成两个类;三是坐标超出图片边界,比如xmax大于图片宽度,归一化后超过1,训练时框会跑到画面外;四是白底黑字的眼睛图片转灰度后对比度剧烈变化,这类样本要么提前做直方图均衡化,要么直接过滤。

4. 训练与推理:把卷积神经网络接入摄像头画面

4.1 训练眼睛状态检测模型:命令与关键参数说明

数据集目录准备好后,开始训练。用ultralytics提供的YOLOv5训练接口,训练脚本如下:

python train.py \ --img 640 \ --batch 16 \ --epochs 80 \ --data ./datasets/eye.yaml \ --weights yolov5s.pt \ --project runs/eye_train \ --name exp1

逻辑说明:train.py会自动从eye.yaml读取训练集路径、验证集路径和类别数量。--weights指定预训练权重,用yolov5s作为起点可以让模型更快收敛,尤其是你的数据集不大的时候,从头训练容易欠拟合。

参数说明:

  • --img 640:输入图像分辨率。眼睛是小目标,分辨率太低会漏检,但640在CPU推理时会明显掉帧;如果需要兼顾速度,训练用640,推理时降到320或480。
  • --batch 16:单卡显存8G以下建议改成8,否则会出现CUDA out of memory。批大小影响BatchNorm统计,改小后要适当降低学习率。
  • --epochs 80:眼睛二分类任务属于简单任务,一般40~50轮就收敛,80轮是为了让曲线彻底稳下来。跑完看results.png里验证集mAP是否还在上升,上升就再加20轮。
  • --weights yolov5s.pt:网络规模最小的预训练权重。项目演示用s足够,如果检测框频繁漏掉闭眼状态,再换yolov5m,但推理速度会下降。

训练完成后,模型保存在runs/eye_train/exp1/weights/best.pt。记得用验证集里的闭眼图片单独测一次,确认模型真的学到了“闭眼”,而不是靠背景颜色偷懒。

4.2 数据增强与类别不均衡:让模型在夜间逆光下不翻车

训练时最容易忽视的是数据增强配置。YOLOv5默认开启hsv变换和随机翻转,但驾驶场景还需要额外关注两点:

在eye.yaml同级的hyp.yaml里,把hsv_h、hsv_s、hsv_v分别调整到0.02、0.6、0.5,让模型在训练时看到更多亮度偏移的样本,这相当于免费扩充夜间样本。同时degrees设为5,允许轻微旋转——司机头部会自然晃动,完全不旋转会让模型对姿态变化过于敏感。类别不均衡是另一个坑:如果你的数据集里睁眼样本是闭眼的两倍以上,模型会倾向把所有框都判成open,因为这样整体loss最小。解决办法是在eye.yaml里给闭眼类别加权重,或者在采样阶段对闭眼图片做复制增强。

4.3 单帧推理:从YOLOv5输出到摄像头画面

训练完模型,写一段最简推理脚本,验证模型能对摄像头画面实时输出眼睛状态:

import cv2 import torch model = torch.hub.load("ultralytics/yolov5", "custom", path="runs/eye_train/exp1/weights/best.pt") cap = cv2.VideoCapture(0) if not cap.isOpened(): raise RuntimeError("摄像头打开失败,检查驱动或换个USB口") while True: ret, frame = cap.read() if not ret: continue results = model(frame, size=320, conf_thres=0.35) for det in results.xyxy[0].tolist(): x1, y1, x2, y2, conf, cls_id = det label = model.names[int(cls_id)] cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255) if label == "closed" else (0, 255, 0), 2) cv2.putText(frame, f"{label} {conf:.2f}", (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) cv2.imshow("fatigue", frame) if cv2.waitKey(1) == ord("q"): break cap.release() cv2.destroyAllWindows()

逻辑说明:torch.hub.load直接加载训练好的权重,首次运行会自动下载YOLOv5依赖组件。model(frame, size=320)把每一帧缩放到320分辨率送进网络,这里为了推理速度故意降低分辨率;conf_thres=0.35表示置信度低于0.35的检测结果会被过滤掉。results.xyxy[0]返回一个二维张量,每行是x1, y1, x2, y2, 置信度, 类别id。

参数说明:conf_thres的高低直接影响误报率。设成0.2会看到很多置信度低的抖动框,设成0.6又会漏掉一些闭眼状态。可以根据摄像头画面实测效果调整,我一般会在0.3~0.4区间试一圈再定。推理时如果CPU占用过高,把size再降到256,代价是远距离眼睛检测会失准。

4.4 从单帧检测到连续帧状态追踪

摄像头每秒产生大约30帧画面,逐帧独立检测的问题是:某一帧检测框抖动会导致状态跳变,闭眼检测偶尔漏一帧中间夹一个“睁眼”。疲劳预警必须基于连续帧的状态序列,最简单的状态机做法是维护一个闭眼帧计数:

closed_frames = 0 CLOSE_FRAME_THRESHOLD = 12 # 约0.4秒@30fps while True: ret, frame = cap.read() results = model(frame, size=320) labels = [model.names[int(d[5])] for d in results.xyxy[0].tolist()] if "closed" in labels and "open" not in labels: closed_frames += 1 else: closed_frames = 0 if closed_frames >= CLOSE_FRAME_THRESHOLD: trigger_alarm()

逻辑说明:这个状态机只做两件事——检测到闭眼就累加计数,检测到睁眼就把计数清零。CLOSE_FRAME_THRESHOLD设为12,表示连续12帧闭眼才报警,目的是滤掉单帧的检测噪声。

参数说明:阈值12是经验值,对应30fps下约0.4秒。实际调试时,你可以在车内记录一段“驾驶员打瞌睡”的视频,数一下闭眼到彻底入睡的帧数,再反推阈值。闭眼持续时间短于0.3秒可能是正常眨眼,不该报警;超过2秒必须报警。中间的阈值看你想要系统更灵敏还是更保守。

5. 疲劳预警逻辑:EAR、PERCLOS与报警阈值怎么定

5.1 EAR眼部纵横比:把闭眼程度变成一个可计算数值

虽然YOLOv5直接输出开闭类别,但为了做到更精细的预警分档(比如“微困”“嗜睡”),需要引入EAR(Eye Aspect Ratio)作为连续指标。EAR的计算依赖眼睛6个关键点:外眼角1个、内眼角1个、上下眼睑各2个。公式是垂直距离的平均值除以水平距离:

def eye_aspect_ratio(landmarks, eye_idxs): # eye_idxs 是dlib 68关键点中左右眼的索引集合 p1 = landmarks[eye_idxs[0]] p2 = landmarks[eye_idxs[1]] p3 = landmarks[eye_idxs[2]] p4 = landmarks[eye_idxs[3]] p5 = landmarks[eye_idxs[4]] p6 = landmarks[eye_idxs[5]] vertical = abs(p2.y - p6.y) + abs(p3.y - p5.y) horizontal = abs(p1.x - p4.x) return vertical / (2.0 * horizontal)

逻辑说明:EAR的原理很直观,眼睛睁开时上下眼睑距离远、内外眼角距离相对固定,比值在0.25~0.35;闭眼时上下眼睑几乎重合,垂直距离趋近于0,EAR会跌到0.1以下。把每组眼睛的EAR按时间轴画出来,能清晰看到“眨眼”对应的V型低谷和“闭眼”对应的持续低谷。

参数说明:左右眼的eye_idxs不同——右眼是[36, 37, 38, 39, 40, 41],左眼是[42, 43, 44, 45, 46, 47]。这个编号对应dlib 68关键点标准排序,搞反了算出来的EAR是乱值。EAR阈值一般取0.2,但不同人眼睛大小差异明显,正式使用前需要做一次10秒标定——睁眼正常看前方,记录睁眼EAR平均值,乘以0.7作为个人闭眼判定线。

5.2 PERCLOS标准与连续闭眼帧:报警触发条件该怎么设计

行业内衡量疲劳有一个经典指标叫PERCLOS(Perclos of Eye Closure),核心定义是:单位时间内眼睛闭合时间所占的百分比。实际落地时,通常有两种触发模式:

第一种是“窗口比例模式”:统计最近60秒内闭眼帧数占总帧数的比例,超过15%触发一级预警。这种模式适合做长期疲劳趋势判断。第二种是“连续闭眼模式”:统计连续闭眼帧数,超过1.2秒触发报警。这是更紧迫的嗜睡信号,适合做紧急预警。

触发模式时间窗口判定阈值实际用途
眨眼频率异常60秒每分钟眨眼次数低于5次疲劳潜伏期提示
PERCLOS一级预警60秒闭眼时间占比≥15%建议休息
连续闭眼紧急报警实时连续闭眼≥1.2秒立即提醒

这套组合比单纯看某几帧更有说服力:短暂闭眼可能是正常眨眼,连续闭眼才是危险信号。把两种模式合并成一个综合判定状态机,就能覆盖从“轻度疲劳”到“嗜睡”的全过程。

5.3 预警触发与响应:声音报警、界面提示与防抖

预警响应端要做三件事:声音报警、界面状态切换、报警日志记录。声音报警用playsound库或系统提示音,界面画面上把状态文字从“正常”切换到“疲劳”并变成红色。日志记录每个报警时间点和当时的EAR均值,方便答辩或验收时展示“系统确实在准确的时间点报警了”。

import threading from playsound import playsound alarm_active = False def play_alarm(): while True: if alarm_active: playsound("alarm.wav", False) time.sleep(1)

逻辑说明:报警播放放到独立线程里,避免阻塞摄像头主循环导致帧率骤降。alarm_active是全局状态位,由疲劳判定状态机置位和复位。

参数说明:这里避开了playsound的阻塞参数True,因为它在Windows上偶尔会卡住主线程;设为False后用睡眠循环控制重复频率。报警后要设置一个至少10秒的冷却窗口,否则司机刚被提醒就恢复正常状态,系统会反复报警造成烦扰。

6. 避坑与验收:光照、眼镜和低算力环境下的踩坑记录

6.1 戴眼镜时闭眼被模型判成睁眼,误报率翻倍

现象:戴眼镜的测试者闭眼时,系统无响应,日志显示闭眼帧占比很低。原因是镜片反光把眼睑轮廓照亮,模型看到的是镜片上的环境光影,误认为眼睑还有缝隙。解决:在数据增强里加入高斯模糊和随机亮度抖动的闭眼样本,让模型学会忽略镜片反光;更直接的办法是在录制训练数据时就戴上眼镜录一遍闭眼视频,强制模型见到“闭眼+眼镜”的组合。

6.2 夜间逆光下检测框抖动,闭眼帧断续

现象:车载摄像头在夜间隧道出口或对向车灯照射下,检测框每几帧跳一次坐标,状态机里的闭眼计数被“伪睁眼”帧打断。原因是高光区域导致YOLOv5的IoU不稳定。解决:推理前对帧做一次自适应直方图均衡化,再用OpenCV的createCLAHE限制对比度;同时在状态机里把“闭眼计数”改成允许在10帧中出现至多1帧睁眼,避免被孤立噪声打断报警。

6.3 误报频繁还是漏报频繁:两类阈值谁先调

现象:系统在家里测试一切正常,上车实测后每分钟误报一次。原因是眼部区域光照不均,模型在低置信度区间摇摆。解决:先调conf_thres,从0.25提到0.4,把低置信度的检测框过滤掉;再看EAR阈值,闭眼判定从0.2降到0.18。调参顺序很重要——永远先过滤检测噪声,再调状态判定阈值,反过来容易越调越乱。

6.4 低端CPU跑不动:降分辨率还是换轻量网络

现象:没有NVIDIA显卡的笔记本跑640分辨率推理只有5帧,画面像是幻灯片。解决:推理时把model(frame, size=320)改成224分辨率,帧率能翻倍;代价是远距离人脸漏检增加。如果还需要更快,把YOLOv5s替换成更轻量的方案,比如用YOLOv5的nano版本,或者把眼睛区域单独裁出来送一个极小的二分类CNN。做课程设计用降分辨率就够了,做产品原型再考虑nano版。

6.5 验收方法:用标注视频量化系统的疲劳检出率

最后验收不能靠“感觉好像能检测到”。我会录制三段60秒的测试视频:一段正常驾驶、一段频繁眨眼、一段持续闭眼嗜睡,人工标注闭眼的起止帧,跑完系统后用Python脚本对比报警时间段和标注时间段的重合度。重合时间除以标注闭眼总时长就是检出率,至少90%才算合格,误报率控制在每5分钟一次以内。这套量化流程比一百句“系统运行稳定”都有说服力,答辩时直接展示数据和曲线。做这套系统的过程中我踩过最多的坑就是拿主观感受代替量化指标,后来习惯了每次调参都跑同一段标注视频,前后对比清晰了,问题也定位得快。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询