简介:YOLO11-DeepSORT驾驶员疲劳检测与跟踪系统资源包,面向智能驾驶安全、车联网及计算机视觉方向的研究者和工程师,解决驾驶途中疲劳状态实时监测与预警问题。方案融合YOLO11目标检测与DeepSORT多目标跟踪算法,可对面部特征、眼睛闭合程度及头部姿态进行持续检测与追踪,并形成连续轨迹,提升预警稳定性。压缩包内共93个文件,以Python脚本(.py/.pyc)、预训练模型(.pt、.t7)、YAML配置、JPG/PNG示例图、MP4演示视频及PDF运行步骤说明为主,体积181.01MB。其中含训练好的yolo11n.pt与DeepSORT权重ckpt.t7,无需重新训练即可直接调用;附带带标注的驾驶员疲劳数据集,覆盖不同光照、角度与表情场景,便于二次训练与泛化验证。资源包还提供完整运行步骤文档和演示视频,已吸引69人学习查看,适合具备一定深度学习基础、希望快速搭建驾驶疲劳预警原型的开发者使用。
1. YOLO11-DeepSORT驾驶员疲劳检测:一个开箱即用的驾驶预警系统,不止是毕设
如果你手头正好有一个叫“YOLO11-DeepSORT驾驶员疲劳检测和跟踪”的项目包,里面带数据集、训练好的检测模型,那你大概率已经在接触当前最主流的一条驾驶员监测技术路线:用YOLO11检测人脸、眼睛和嘴部,用DeepSORT把检测结果跨帧关联成轨迹,再结合PERCLOS等规则判断疲劳状态。它能解决的问题很具体——驾驶员闭眼、打哈欠、低头这三个危险动作的实时识别和预警。适合三类人:做毕设的学生、要快速验证方案的工程师、想把疲劳监测模块塞进现有ADAS系统的开发团队。这篇文章把这条链路从头拆到尾,给出能直接复现的命令和参数,也告诉你哪里容易翻车。
2. 先把原理立住:YOLO11负责“看见”,DeepSORT负责“不跟丢”,疲劳判定靠规则
2.1 检测和跟踪为什么要拆成两步:单帧检测在“眨眼”时最容易丢目标
很多人第一次看到这个项目标题会问:YOLO11不是已经把眼睛和嘴检测出来了吗,为什么还要接一个DeepSORT?直接让YOLO11逐帧输出框不就行了?
问题恰恰出在“逐帧”上。疲劳判定需要的是连续时序,而不是孤立的几帧。摄像头在车内的安装位置通常偏低、偏斜,光线变化大,驾驶员戴眼镜、低头、转头都会让单帧检测的置信度突然掉到0.3以下。如果你只依赖单帧检测,闭眼那一瞬间人脸框往往会丢失,连续闭眼帧数就统计不上来,而PERCLOS恰恰需要的是“闭眼帧数/总帧数”。更麻烦的是,没有跟踪就没法稳定绑定ID,前一秒这个人脸还是A,下一秒检测框跳到旁边就变成B,疲劳数据全乱。
DeepSORT做的事就是跨帧关联。它对你每个检测框做外观特征提取和运动预测:上一帧驾驶员在位置(100, 200),这一帧摄像头震了一下,检测框质量变差,但卡尔曼滤波可以根据历史速度预测出当前位置,仍然把ID绑住。等到下一帧检测正常了,再重新修正轨迹。这就是“检测负责看见,跟踪负责记住”。
所以这条链路里的核心不是单个模型有多强,而是检测和跟踪的配合。YOLO11把每一帧里“人脸、睁眼、闭眼、张嘴、闭嘴”这些类别找出来,DeepSORT把同一个人跨帧串成一条轨迹,最后在轨迹层面统计疲劳指标。这样即使某几帧检测被遮挡、模糊,疲劳判断也不会断。
2.2 疲劳指标怎么算:PERCLOS、打哈欠频率和头部下坠三者配合
疲劳监测系统不是“看到闭眼就报警”,那样误报率会高到没法用。行业内最通用的做法是用多个指标做规则融合,我这里讲最常见的三个。
第一个是PERCLOS,即眼睛闭合时间比例。工程实现上用的是P80标准:眼睑遮住瞳孔超过80%就算一帧闭眼,然后统计滑动窗口内闭眼帧数占比。窗口一般取60秒,阈值设为0.4,也就是一分钟里24秒以上是闭眼状态,就判定为疲劳。但连续闭眼更危险,所以多数系统会叠加一个“短时间内连续闭眼帧数”计数器,比如10秒内连续闭眼超过30帧(按30帧/秒算,即1秒),直接触发预警。
第二个是打哈欠频率。哈欠表现为嘴部张开且持续一段时间,可以用嘴部纵横比(MAR)来衡量,原理和眼睑纵横比(EAR)类似。计算嘴部关键点的纵向距离除以横向距离,连续15帧以上MAR大于0.6,记作一次哈欠;5分钟内超过3次,判定为疲倦倾向。
第三个是头部下坠。驾驶员疲劳时的典型动作是点头、低头。工程上可以用人脸检测框中心的垂直坐标变化来判断:连续若干帧人脸中心y坐标下降超过一定像素,且后续没有恢复,则认为是低头。有些方案会用头部姿态估计模型输出俯仰角,但在这个项目里,用YOLO11的人脸框位置做近似就够用了。
这三类指标在代码里会被组合成一个优先级:连续闭眼 > 低头 > 哈欠频率。只要其中一个触发,就写一条预警日志并调用声音提示。注意这些阈值一定要暴露成配置文件,因为不同摄像头的帧率和安装角度对数值影响很大,写死的话换个环境就翻车。
2.3 数据集与训练好的检测模型:直接用还是重新训练,取决于你的摄像头角度
项目包里的“数据集”通常是把驾驶场景视频按帧抽出来,用LabelImg或X-AnyLabeling标注成YOLO格式。类别一般是五个或六个:face、eye_open、eye_close、mouth_open、mouth_close,有的会加上phone。训练好的检测模型则是拿这个数据集跑YOLO11得到的权重文件,常见的是PyTorch的.pt格式,推理端可能还有TensorRT的.engine格式。
拿到手的第一步不是急着搭环境,而是先检查数据集的类别分布。常见做法是写一个脚本统计每个类别的框数量,顺便看看标注框尺寸分布,因为我见过不少项目包里“eye_close”这类样本只有几百张,其他类别上万张,类别不均衡直接导致闭眼漏检。
import os from collections import Counter label_dir = "dataset/labels" # YOLO格式的txt标注目录 cls_names = {0: "face", 1: "eye_open", 2: "eye_close", 3: "mouth_open", 4: "mouth_close"} cls_counter = Counter() obj_counter = 0 for f in os.listdir(label_dir): if not f.endswith(".txt"): continue with open(os.path.join(label_dir, f), "r", encoding="utf-8") as fp: for line in fp: parts = line.strip().split() if not parts: continue cls_id = int(parts[0]) cls_counter[cls_id] += 1 obj_counter += 1 print(f"总标注框数: {obj_counter}") for cls_id, cnt in sorted(cls_counter.items()): print(f"{cls_names.get(cls_id, cls_id)}: {cnt} 框")这个脚本的核心是用Counter统计每个类别的出现次数,因为YOLO格式每行是“class cx cy w h”,第一个字段就是类别id。逻辑说明:类别id和名称的对应关系一般写在data.yaml里,别靠猜。参数说明:如果你发现eye_close数量远少于其他类别,那这个“训练好的检测模型”在闭眼状态下的泛化能力就要打折扣,最稳妥的办法是把视频里闭眼片段再补抽几帧,用半自动标注工具补一批数据重新训练。
至于训练好的模型能不能直接用,要看你的摄像头视角和训练集视角差多少。同一个模型,装在挡风玻璃正上方和装在仪表盘右侧,人脸的角度、遮挡、光照完全不同。我一般会先用训练好的模型跑一段我自己的摄像头视频,如果检测框在多数帧上稳定,就继续用;如果漏检率超过两成,就不要硬扛,重新训练一个YOLO11模型。别迷信“训练好的”这三个字,它只在和你场景相似的前提下才成立。
3. 跑起来:从解压到实时预警的完整落地过程
3.1 环境搭建:Python、CUDA和两个核心依赖的版本搭配
这个项目的环境依赖说简单也简单,说坑也坑。最核心的是两套东西:跑YOLO11的ultralytics包,和跑DeepSORT的deep_sort_realtime或自己编译的deepsort。我建议直接使用deep_sort_realtime这个开源包,因为它不需要编译caffe或torchreid,少踩很多坑。
环境版本上,Python请用3.9到3.11之间的版本,CUDA对应11.8或12.1,PyTorch装2.x。不要装最新的Python 3.12,ultralytics和deep_sort_realtime的某些依赖索引在3.12上容易翻车。如果你只有CPU机器,也可以跑,只是帧率会掉到个位数,调试代码够用,实时预警不现实。
conda create -n fatigue python=3.10 -y conda activate fatigue pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics deep-sort-realtime opencv-python numpy命令逻辑:第一行创建隔离环境,避免和你其他项目打架。第二行指定安装CUDA 11.8编译的PyTorch,这里不要装CPU版,否则后面DeepSORT的矩阵运算慢得让你怀疑人生。第三行一次性装齐YOLO11、DeepSORT库和其他基础依赖。参数说明:deep-sort-realtime这个名字在PyPI里是带短横线还是下划线,安装时用短横线,import时用下划线,别记混了。
装完之后验证一下:打开Python,输入import torch; print(torch.cuda.is_available()),输出True就说明GPU可用。我见过一半以上的环境问题都出在PyTorch装成了CPU版但自己不知道,所以这步检查别省。
3.2 拿到训练好的检测模型:先跑通单张图片检测
环境就绪后,先用一张图片验证训练好的检测模型能不能正常加载。这个项目里的模型文件一般是best.pt或last.pt,放在weights目录下。单张图片推理是排查问题最快的路径,如果这一步都出错,就别急着接摄像头。
import cv2 from ultralytics import YOLO model_path = "weights/best.pt" img_path = "sample.jpg" model = YOLO(model_path) results = model.predict( source=img_path, conf=0.3, imgsz=640, verbose=False, ) for r in results: boxes = r.boxes.xyxy.cpu().numpy() # 像素坐标的检测框 classes = r.boxes.cls.cpu().numpy().astype(int) confs = r.boxes.conf.cpu().numpy() for box, cls, conf in zip(boxes, classes, confs): x1, y1, x2, y2 = [int(v) for v in box] print(f"类别{cls} 置信度{conf:.2f} 坐标({x1},{y1},{x2},{y2})") cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite("sample_out.jpg", img)这个脚本的逻辑分三段:第一段创建YOLO对象并加载权重;第二段传图片进去推理,得到的结果是一个Results对象,里面的boxes.xyxy是左上右下角的像素坐标,cls是类别id,conf是置信度;第三段把框画到图上并保存。参数说明:conf=0.3是检测置信度阈值,调低能减少漏检,但会增加误检;imgsz=640是输入分辨率,如果原图很大,改成800能提升对小眼睛、嘴部的检测,代价是推理变慢。
跑通之后把sample_out.jpg打开,用肉眼看框的位置对不对。常见问题是:框只框住了人脸,但眼睛和嘴的框没出来——这说明训练的类别可能只有face,或者眼睛/嘴的置信度低于阈值。这时候把conf降到0.1再跑一次,如果出来了,说明模型能检测,只是置信度偏低;如果还是什么都没有,就得检查模型本身了。
3.3 接上DeepSORT:逐帧检测到轨迹跟踪的代码骨架
这是整个项目最核心的一段代码。你要做的就是把YOLO11的检测结果转换成一个DeepSORT能吃的格式,然后逐帧更新。deep_sort_realtime的update接口要求传入一个列表,每个元素是[left, top, right, bottom, confidence, class_id],坐标是像素值且不能是归一化的,否则卡尔曼滤波的位置预测会完全错乱。
import cv2 from ultralytics import YOLO from deep_sort_realtime.deepsort_tracker import DeepSort model = YOLO("weights/best.pt") tracker = DeepSort(max_age=30, n_init=3, nn_budget=100, override_track_class=None) video_path = "driver_01.mp4" cap = cv2.VideoCapture(video_path) out = cv2.VideoWriter("driver_01_out.mp4", cv2.VideoWriter_fourcc(*"mp4v"), 30, (int(cap.get(3)), int(cap.get(4)))) detections = [] prev_positions = {} while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model.predict(source=frame, conf=0.3, imgsz=640, verbose=False)[0] boxes = results.boxes.xyxy.cpu().numpy() confs = results.boxes.conf.cpu().numpy() class_ids = results.boxes.cls.cpu().numpy().astype(int) detections = [] for box, conf, cls_id in zip(boxes, confs, class_ids): x1, y1, x2, y2 = [float(v) for v in box] detections.append([x1, y1, x2, y2, conf, cls_id]) tracks = tracker.update_tracks(detections, frame=frame) for track in tracks: if not track.is_confirmed(): continue track_id = track.track_id ltrb = track.to_ltrb() # [left, top, right, bottom] x1, y1, x2, y2 = [int(v) for v in ltrb] # 记录每个人脸的持续信息,用于后续疲劳统计 prev_positions[track_id] = [(x1 + x2) // 2, (y1 + y2) // 2] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f"ID:{track_id}", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) out.write(frame) cap.release() out.release()这段代码的逻辑很直白:每一帧先用YOLO11检测,然后把所有框连同置信度和类别id一起传给tracker.update_tracks。DeepSORT内部会先做级联匹配,再对未匹配上的检测框初始化新轨迹,最后返回维护中的轨迹列表。参数说明:max_age=30表示一个轨迹最多能连续丢失30帧不被删除,这个值越大,ID越稳定,但可能导致消失的人脸迟迟不释放;n_init=3表示轨迹至少要连续确认3帧才会被标记为confirmed,太低的n_init会让短暂噪声框变成假轨迹;nn_budget=100是外观特征队列的大小,数值越小越省内存,但ID Switch概率会上升。
这里有个容易踩的坑:detections里如果传了class_id,DeepSORT会按类别做外观特征区分。如果你的YOLO11模型同时检测face和eye,两类框会在同一帧里出现,建议把detections列表里只放face类的框,或者把类别id映射到统一值,否则DeepSORT会把同一张脸上的眼睛和嘴巴也当成独立目标跟踪,产生一堆幽灵ID。
3.4 预警触发与记录:是弹窗、声音还是写日志?配置优先级
跟踪拿到stable轨迹后,下一步就是从轨迹上提取疲劳状态。这个项目的“预警”不是随便写个if判断,而是要设计成可配置的模块。我一般会用一个疲劳判断类,输入每个track近60帧的检测置信度序列,输出是否预警。
class FatigueDetector: def __init__(self, perclos_threshold=0.4, close_frames_alert=30): self.perclos_threshold = perclos_threshold self.close_frames_alert = close_frames_alert self.eye_close_frames = {} def update(self, track_id, is_eye_close): if track_id not in self.eye_close_frames: self.eye_close_frames[track_id] = [] self.eye_close_frames[track_id].append(int(is_eye_close)) frames = self.eye_close_frames[track_id][-60:] # 滑动窗口60帧 close_ratio = sum(frames) / len(frames) if close_ratio >= self.perclos_threshold: return "perclos_alarm" consecutive = max(self._consecutive_ones(frames) or [0]) if consecutive >= self.close_frames_alert: return "continuous_close_alarm" return "normal" @staticmethod def _consecutive_ones(arr): counts, cur = [], 0 for v in arr: if v: cur += 1 else: if cur: counts.append(cur) cur = 0 if cur: counts.append(cur) return counts逻辑说明:这个类用滑动窗口保存每帧眼睛是否闭合的状态,闭眼状态怎么来?在上一节DeepSORT跟踪到的每个人脸框内部,再运行YOLO11的eye_close检测,或者直接用模型输出的eye_close类别置信度作为输入。参数说明:perclos_threshold=0.4就是前面讲的PERCLOS阈值,close_frames_alert=30表示连续闭眼30帧(约1秒)直接触发。这两个参数要放在配置文件里,因为摄像头帧率不同,30帧的实际时长完全不同。
预警输出优先级我个人建议:写日志 > 声音提示 > 弹窗。弹窗在真实驾驶场景里反而最危险,因为司机低头看弹窗本身就是分心。代码里预警时调用logging.warning("track %s: perclos alarm", track_id),同时用winsound.Beep(1000, 500)发出提示音,这样事后复盘也有据可查。
4. 避坑笔记:驾驶员疲劳检测项目最常见的5个翻车现场
4.1 现象:模型检测正常,但跟踪框乱跳,ID频繁切换
这是接DeepSORT后最常见的翻车。明明是同一个驾驶员,几秒内ID从2跳到7,人脸上同时叠了好几个框。
原因有两个:一是输入DeepSORT的检测框坐标和原图分辨率不一致,比如YOLO11推理时imgsz=640,但原图是1920x1080,你直接把640尺寸下的坐标传给DeepSORT,导致卡尔曼滤波的位置预测错乱。二是检测框质量不稳定,conf阈值设太低,比如0.15,导致同一个眼睛位置产生多个噪音框。
解决:确认传给DeepSORT的坐标一定是原图坐标。YOLO11在predict时如果不传原图,返回的坐标是相对于模型的输入尺寸的,需要乘以缩放系数还原。我在上节代码里直接用原始frame推理,所以results的坐标就是原图坐标,这一步不能省。另外conf阈值建议不低于0.25,宁可漏几帧,也比一堆假轨迹强。
4.2 现象:加载训练好的检测模型后,每个目标都漏检
你拿的是项目包里“训练好的检测模型”,但一跑,连人脸都检测不到。最常见的原因是类别id没对上。那个模型是用数据集的类别训练出来的,它的类别0可能是face,而你的脚本里类别0是eye_open,而且你只画了eye_open的框,当然什么都画不出来。
解决:用前面那个统计脚本把数据集的类别映射找出来,然后加载模型后打印model.names,看看当前模型实际识别的类别名。如果和你的预期不一致,直接把model.names的值对照着改,不要试图去改模型权重。另一个原因是模型是TensorRT的engine格式,必须在同样的GPU型号和TensorRT版本上加载,换成电脑可能直接报错,这种情况就去项目包里找有没有.pt原版权重。
4.3 现象:GPU占用率上不去,只有20%,帧率还不到15
很多人以为用了GPU就万事大吉,结果跑起来GPU占有率还不如CPU高。原因大概率是视频读取和预处理变成了瓶颈,进程在等CPU解码视频帧。疲劳检测这种场景一般用摄像头或视频文件,Opencv的VideoCapture默认把解码放在主线程,一帧一帧阻塞,GPU自然闲得慌。
解决:最简单的办法是生产者消费者模式,一个线程用VideoCapture读帧,放进队列,另一个线程从队列取帧交给YOLO11推理。队列长度控制在一定范围内,比如32,避免内存暴涨。另外把YOLO11的imgsz从640改成480,推理时间差不多能降三分之一,对眼睛这种小目标影响不大,但对帧率改善非常明显。
import queue, threading, cv2 q = queue.Queue(maxsize=32) def reader(cap): while cap.isOpened(): ret, frame = cap.read() if not ret: q.put(None) break q.put(frame) cap = cv2.VideoCapture("driver_01.mp4") t = threading.Thread(target=reader, args=(cap,), daemon=True) t.start() while True: frame = q.get() if frame is None: break # 推理和跟踪逻辑照常这段代码的逻辑是创建了一个读帧线程,把视频帧预读进队列,主线程拿帧的速度不再被解码速度拖累。参数说明:队列maxsize=32是缓冲区上限,太大内存会涨,太小线程切换频繁,32在大多数机器上是平衡点。
4.4 现象:闭眼状态明明有,疲劳预警却死活不触发
我调试过一个项目,视频里驾驶员明显闭眼两三秒,系统毫无反应。把frame里的检测结果打印出来,发现闭眼帧检测到了,但类别id是2,而疲劳判断代码里写死了判断“eye_close”,没看label映射,于是逻辑永远进不去。
另一个更隐蔽的原因是眼睛检测框和闭眼置信度是分离的,眼睛检测框还在,但内部闭眼类别的置信度可能被嘴巴类别挤掉了。解决是在触发预警时做多条件保障:只要眼睛区域检测框存在,且eye_close的置信度大于0.5,就计一帧闭眼;如果eye_close没检测到,但eye_open置信度也远低于阈值,也可视为疑似闭眼。疲劳判定不能只依赖单一类别的结果。
4.5 现象:打包给别人运行,报“No module named torch”等一堆错
项目跑通了你想打包成exe或放在另一台电脑上,结果对方双击运行,直接黑屏或报ModuleNotFoundError。这基本都是因为conda环境没有完全打包,PyTorch这种大包依赖特别容易丢。
解决:不要太依赖PyInstaller自动收集依赖,我建议在requirements.txt里把所有依赖写死版本,然后给对方一个一键安装脚本,让他自己装conda环境。如果你真想打包exe,不要用conda环境直接打包,新建一个干净虚拟环境,只安装运行所需依赖,然后用PyInstaller加--collect-all ultralytics和--collect-all deep_sort_realtime。就算这样,打包出来的exe体积也会超过2GB,而且启动慢,所以最靠谱的交付方式是源码加脚本,这也是这类项目包最常见的形态。
5. 进阶调优:从“能跑”到“敢用”的关键改动
5.1 yolo11改进的方向:眼睛和嘴部这类小目标为什么容易漏检
原始YOLO11的neck层对80x80以上的目标效果很好,但眼睛和嘴部在640分辨率下往往只有20x20甚至更小。如果你发现数据集里大量目标的宽度小于32像素,就要考虑给YOLO11加一个更浅的检测头,把输入分辨率从640提升到800,或者把SPPF模块改成SPPCSPC以增强小目标特征聚合。常见做法是在yaml配置里增加一个P3级别的输出层,并把anchor或动态anchor的目标尺度适当调小。这一改动会牺牲约20%的帧率,但对闭眼、哈欠的召回率提升显著。另外,把Face和eye_open合并成统一人脸检测,再单独检测eye_close和mouth_open,可以减少漏检的传递。
5.2 DeepSORT参数调整:max_age、n_init和iou_threshold怎么配合
DeepSORT并不是安装后就能用好的黑盒。我见过很多人在真实场景里把max_age调到100,认为ID会稳定,结果是驾驶员转头回来后重影ID一大堆。原因是max_age过大,旧的轨迹状态一直没有消亡,重新出现的人脸和旧轨迹竞争,导致ID漂移。合适的一组起点值是max_age=30、n_init=3、iou_threshold=0.3,如果你发现ID切换频繁,优先调高max_age到40,而不是调低;如果你发现轨迹延迟太长,则调低max_age到20。iou_threshold是检测框和预测框匹配时IoU的最低要求,太高会导致同一目标被两次匹配,太低会导致漏匹配,建议维持在0.3附近。这些参数每次只动一个,改完跑同一段视频统计ID切换次数,否则你根本不知道是哪项改动起了作用。
5.3 验证方法:用一段带标准答案的视频统计误报率和漏报率
调优之前一定要有验证基准。我建议从原始视频里截取3段,每段5分钟,覆盖白天、逆光、戴眼镜三种场景,然后人工标注每一秒属于“正常、闭眼、哈欠、低头”。运行系统后对比输出,统计误报率和漏报率。一个能实际部署的系统至少要达到漏报率低于10%,误报率低于5%。如果你的误报率高,先把PERCLOS阈值从0.4提高到0.45;如果漏报率高,优先把眼睛检测分辨率加大,而不是盲目调跟踪参数。疲劳监测这东西,宁可少报也不能乱报,因为误报会让驾驶员在十分钟内就关掉系统,这是我在调过几次现场后总结的一条血泪经验。
验证的时候还有一个技巧:对同一段视频跑三次,取每次的疲劳事件数量平均值。因为DeepSORT的随机性会让结果有轻微波动,如果三次结果相差超过两倍,说明跟踪没调好,先去解决ID切换问题,再回来谈疲劳判断。希望这套调优路径能帮到你,少走我当年走过的弯路。
本文还有配套的精品资源,点击获取