简介:面向汽车安全与智能座舱领域研究者和开发者,这套驾驶员疲劳检测与跟踪方案基于YOLO11目标检测与DeepSORT跟踪算法,可实时识别驾驶员面部特征、眼睛闭合和头部姿态,判断疲劳状态并触发预警,适用于疲劳驾驶监测、辅助驾驶系统开发及相关的毕设实战项目。压缩包共93个文件,包含Python源码及pyc编译文件、预训练模型权重(pt、t7)、数据集图片(jpg、png)、演示视频(mp4)、模型配置(yaml)和运行说明文档(pdf、md)等,整体大小181.01MB,目录结构清晰,便于按模块使用。目前已有69人学习下载。除可直接运行的检测与跟踪模块外,还附带了训练好的yolo11n.pt和ckpt.t7模型,以及带标注的驾驶员面部数据集、示例视频和deep_sort配置文件,使用者无需从零训练即可快速部署体验,也可基于已有数据和代码进一步调优,显著降低开发门槛。
1. 为什么是 YOLO11 + DeepSORT:疲劳检测不是「看脸」而是「盯人」
YOLO11-DeepSORT驾驶员疲劳检测和跟踪这套资源,解决的不是「这一帧脸上有没有闭眼」,而是「同一张脸能不能在连续几十帧里被稳定记住」。单帧检测只能告诉你此刻眼睛闭着、嘴张着,但疲劳是时间过程,必须靠跟踪把各帧状态串起来。DeepSORT 的职责就是给每个检测框分配持久 id,让后续疲劳判定有据可依。
压缩包里包含微调好的 YOLO11 检测模型(区分清醒/打哈欠)、DeepSORT 的 ReID 权重 ckpt.t7、训练数据集、track.py 主程序、测试视频和运行步骤 PDF。装好环境直接输出带跟踪 id 的视频,不用从零训练。
适合三类人:做毕业设计的学生、做 ADAS 疲劳预警预研的工程师、想拿现成工程练手目标跟踪的新手。下面按「资源结构 → 运行参数 → 跟踪原理 → 踩坑 → 落地技巧」拆一遍。
2. 资源包里有什么:一份能直接跑的检测+跟踪闭环
拿到压缩包别急着 pip install,先把目录结构摸清楚。这套包不是散装代码,目录层级对应着「数据 → 模型 → 推理 → 跟踪」整条链路,很多参数都写死在相对路径里,少了任何一个文件都可能跑不起来。
2.1 顶层文件拆解:谁负责检测、谁负责跟踪、谁负责输出
先看整体分工,我用一张表说清楚:
| 路径 | 角色 | 说明 |
|---|---|---|
| track.py | 主程序入口 | 读视频、调检测器、调跟踪器、写输出 |
| utils.py | 工具函数 | 类名映射、画框、视频读写辅助 |
| weights/ | 检测权重 | yolo11n.pt 基座 + yolo11n-dms_awake_yawn_data 微调版 |
| deep_sort_pytorch/ | 跟踪模块 | 含 configs、utils、deep_sort 子目录,ReID 权重 ckpt.t7 通常在其中 |
| vid-1.mp4 / car-1.mp4 | 测试视频 | 驾驶舱视角素材,用于验证检测+跟踪链路 |
| YOLO11-DeepSORT...运行步骤.pdf | 操作手册 | 环境安装、运行命令的图文说明 |
这个结构和常见的 yolov5-deepsort 系工程一脉相承:主程序只做调度,检测器和跟踪器各自独立成目录。好处是替换检测权重或换 ReID 模型都不用动主逻辑,坏处是路径写死,目录一动就报 FileNotFoundError。我拿到手的第一件事就是确认 weights 和 deep_sort_pytorch 的相对路径跟 track.py 里写的一致,这是最容易碰到的第一个坑。
至于 inference 和 output 两个目录,不同版本里输出目录名不一样,有的跑完写 inference,有的写 output,看 track.py 里 save_dir 变量的取值就行,不用纠结哪个是标准。顺带提一句,压缩包里还有pycache和 utils.cpython-38.pyc 这类编译缓存,说明作者是在 Python 3.8 环境跑的,复现时优先用 Python 3.8 能少踩很多兼容性坑。
2.2 检测模型:yolo11n 微调版和官方基座怎么分工
weights 目录下有两个关键文件。yolo11n.pt 是官方基座,COCO 预训练权重;yolo11n-dms_awake_yawn_data 是在驾驶员监控数据集上微调出来的检测权重,从命名能看出类别只有两个:awake(清醒)和 yawn(打哈欠)。两个文件都别删:微调版用于推理,基座留着做热启动训练或对比实验。
为什么选 yolo11n 而不是更大的 s/m/l 版本?疲劳检测的输入是驾驶舱内单人场景,人脸在 640 分辨率下通常只有几十到一百多像素,这个目标尺度下 nano 模型的精度损失可以接受,但速度优势非常明显。实时性是疲劳预警的硬约束,宁可用小模型保证 30fps,也不能拿大模型跑到每秒十帧。如果后续觉得精度不够,常见做法是直接继承这套代码结构,把权重换成 yolo11s 再跑一遍,track.py 不用改。
这里多说一句 DMS 数据。驾驶室摄像头是座舱内顶装或仪表盘视角,拍到的脸是侧脸、低头、戴墨镜、光照不均的各种状态,和普通行人检测数据集差异很大。直接用 COCO 官方权重去推理,漏检会非常严重,所以微调是必须的一步,不是可选项。如果手上的 DMS 样本量不够,常见做法是先用公开的驾驶场景目标检测数据集做一轮预训练,再拿座舱数据微调,收敛速度和最终精度都会好不少。
2.3 数据集:YOLO 标注格式和训练前必查的三件事
yolo11n-dms_awake_yawn_data 这个目录名同时暗示了数据集的标注格式,YOLO 系列数据集的标准组织方式如下:
train/ images/xxx.jpg labels/xxx.txt valid/ images/xxx.jpg labels/xxx.txtlabels 里的每行对应一个目标框,格式是:
0 0.512 0.463 0.221 0.184 1 0.410 0.382 0.195 0.172每行五个数:类别 id、归一化中心 x、归一化中心 y、归一化宽、归一化高。0 通常对应 awake,1 对应 yawn,具体以数据集里的 classes.txt 为准。
训练前我习惯先做三件事,能省掉后面很多玄学问题。第一查类别平衡:疲劳数据天然稀缺,yawn 的样本量经常只有 awake 的五分之一,不看分布直接训练很可能得到一个「永远输出 awake」的模型,loss 还降得下去,但实际毫无用处。第二查光照和角度覆盖:纯白天数据训练出来的模型晚上一测就崩,就是典型的泛化失败。第三查标签文件有没有空文件或非法坐标,用脚本扫一遍比人工省事:
import os for split in ("train", "valid"): label_dir = os.path.join(split, "labels") if not os.path.isdir(label_dir): continue for f in os.listdir(label_dir): path = os.path.join(label_dir, f) if os.path.getsize(path) == 0: print("empty label:", f) continue with open(path) as fp: for line in fp: p = [float(x) for x in line.split()] if p[2] <= 0 or p[3] <= 0: print("bad bbox:", f, line.strip())这段脚本只做最基础的合法性检查:空文件直接报出来,宽高小于等于零的框打标记。实际标注工具很少产生越界坐标,反倒是空 label 文件更常见,它会让训练时该样本变成纯背景,白白浪费一张图。如果数据集里混入了这类脏样本,训练出来的模型会在对应场景上莫名漏检,排查起来非常痛苦。
3. 把流程跑起来:track.py 的调用方式与六个核心参数
环境装好之后进入正题。track.py 是整个工程的调度中枢,理解了它的一帧循环,后面调参就有方向了。
3.1 主循环:检测 → 裁剪提特征 → 跟踪更新 → 画框
整个推理过程按帧推进,核心节奏可以用下面这段伪代码概括:
# track.py 主循环的逻辑骨架 while cap.isOpened(): ret, frame = cap.read() # 读一帧 dets = detector(frame) # YOLO11 单帧检测,返回框、类别、置信度 crops = [crop(frame, b) for b in dets] # 按检测框裁剪人脸区域 feats = reid_model(crops) # ckpt.t7 提取 128 维外观特征 tracks = tracker.update(dets, feats) # DeepSORT 卡尔曼预测 + 级联匹配 draw(frame, tracks) # 画检测框、画轨迹 id、写类别名第一行是视频源,可以是文件也可以是摄像头设备号。detector 拿到的是原始检测结果,DeepSORT 并不直接消费检测框,它需要两样东西:框坐标和对应的外观特征。crops 这一步很多人忽略,实际非常关键——ReID 模型输入的是裁剪后的人脸图,裁剪尺寸不对或没做 padding,特征质量直接下降。tracker.update 返回的是确认过的轨迹,包含稳定的 id,这才是画框和后续疲劳统计的数据源。
3.2 检测器参数:conf-thres、iou-thres、imgsz 的推荐值
track.py 通常支持命令行参数覆盖默认值,典型调用方式如下:
python track.py \ --source vid-1.mp4 \ --yolo-weights weights/yolo11n-dms_awake_yawn_data.pt \ --deep-sort-weights deep_sort_pytorch/deep_sort/deep/checkpoint/ckpt.t7 \ --conf-thres 0.35 \ --iou-thres 0.45 \ --imgsz 640 \ --save-vid--source 指定视频或摄像头编号,--yolo-weights 指向微调权重,--deep-sort-weights 指向 ReID 权重。如果解压后 ckpt.t7 不在这个位置,按实际路径改即可。重点说后面三个参数,它们的组合直接决定检测质量:
| 参数 | 推荐值 | 影响 |
|---|---|---|
| --conf-thres | 0.35~0.45 | 设太低会把方向盘、手误检成人脸,id 乱跳;设太高会漏掉低头或侧脸 |
| --iou-thres | 0.45~0.5 | NMS 去重阈值,两个框 IoU 超过该值就合并 |
| --imgsz | 320~640 | 输入分辨率,速度不够就降到 320,精度优先保持 640 |
conf-thres 是最需要反复试的参数。疲劳场景里人脸经常处于半遮挡、大角度状态,模型输出的置信度普遍偏低,推荐值 0.35 是「宁可多一点误检、不能漏检」的思路,因为 DeepSORT 的级联匹配本身会过滤一部分噪声框。如果发现跟踪 id 频繁跳变,优先怀疑 conf-thres 而不是跟踪器参数。
3.3 DeepSORT 配置:deep_sort.yaml 里的六个阈值
跟踪器配置集中在 deep_sort_pytorch/configs/deep_sort.yaml,结构一般是这样的:
DEEP_SORT: REID_CKPT: deep_sort_pytorch/deep_sort/deep/checkpoint/ckpt.t7 MAX_DIST: 0.2 MIN_CONFIDENCE: 0.3 NMS_MAX_OVERLAP: 0.5 MAX_IOU_DISTANCE: 0.7 MAX_AGE: 70 N_INIT: 3 NN_BUDGET: 100逐条解释。MAX_DIST 是外观特征匹配的余弦距离门控,超过 0.2 就认为不是同一个目标,调大允许的外观差异更大,但对遮挡后的误匹配也更宽容。MIN_CONFIDENCE 是进入跟踪器的置信度下限,它和 track.py 的 --conf-thres 是两道关卡,先过检测阈值再过这道。NMS_MAX_OVERLAP 是输入跟踪器前的检测框去重。MAX_IOU_DISTANCE 是仅靠位置匹配时的 IoU 距离上限,注意这里用的是 1 减去 IoU。
MAX_AGE 和 N_INIT 对疲劳场景影响最大。MAX_AGE 控制轨迹失配后最多保留多少帧:驾驶员闭眼、转头时目标短暂消失,如果 MAX_AGE 只有三四十帧,轨迹很快被删,睁眼后再出现的脸会被分配全新 id,前面的疲劳统计全部作废。疲劳检测场景我一般调到 60~90。N_INIT 是轨迹从 tentative 转 confirmed 需要连续匹配的帧数,默认 3 就够了。
NN_BUDGET 是每条轨迹最多缓存多少条外观特征,超出就淘汰最旧的。这个值决定内存占用和匹配稳定性,100 是默认值,不用动。
提示:改完 yaml 不用重装环境,track.py 每次启动都会重新读配置。但确认路径用的是相对路径,一定在工程根目录下运行命令。
4. DeepSORT 原理:同一张脸为什么能被记住几十帧
这章把跟踪原理讲透。只会上手跑、不懂原理,遇到 id 跳变和轨迹丢失时,你根本不知道往哪个方向调。
4.1 卡尔曼滤波:八维状态向量和匀速假设
DeepSORT 对每条轨迹维护一个八维状态向量 (u, v, γ, h, u̇, v̇, γ̇, ḣ)。u、v 是目标框中心点坐标,γ 是宽高比,h 是框高,后四项是对应的速度。它假设目标在两帧之间做匀速运动,用上一帧的状态预测当前帧的框位置,再用当前帧的检测结果做修正。
这里有个容易被忽略的设计细节:为什么不直接预测框宽 w,而是预测宽高比 γ?因为目标的宽随姿态和距离变化很大,噪声多,而宽高比相对稳定。驾驶员转头时框宽变化剧烈,但 γ 变化小,预测误差可控,这也是 DeepSORT 在目标形变场景下依然能稳住 id 的原因之一。
卡尔曼输出的预测框和真实检测框之间会算一个马氏距离,这个距离只看位置和尺度是否吻合,用来做第一轮门控,把明显八竿子打不着的匹配对直接排除掉。
4.2 级联匹配:为什么先匹配「新鲜」轨迹
光靠位置不够。人脸闭眼和睁眼时外观差异很大,但位置几乎没动,这时候马氏距离会认为两个状态一致。所以 DeepSORT 还要求检测框裁剪图经过 ReID 网络生成外观特征,用余弦距离度量两张脸是不是同一个人。ckpt.t7 就是这个 ReID 网络导出的权重。
两个距离各有各的毛病,DeepSORT 的解法是级联匹配:按轨迹上次匹配成功的帧数排序,优先给「最近还持续出现」的轨迹分配检测框,老轨迹排后面。这么做的原因是,长时间没匹配的轨迹,它的卡尔曼预测不确定性会累积放大,位置已经不可信,如果让它和新轨迹抢检测框,很容易把刚出现的目标错配给一个早已丢失的老轨迹。级联顺序一乱,id 就开始互相抢。
这也是「deepsort改进」类工作最爱动刀的地方:有人把余弦距离换成更轻量的特征比对,有人改匈牙利匹配的实现方式,但核心框架没变过。理解级联匹配,你就能看懂任何一种改进版本在改什么,也就能判断某个改进对你这个场景到底有没有用。
4.3 轨迹生命周期:tentative、confirmed、deleted
每条轨迹有三个状态。新检测框触发的新轨迹先进入 tentative,连续 N_INIT 帧都被匹配上才转 confirmed,confirmed 轨迹才会输出稳定的跟踪 id。反过来,confirmed 轨迹如果连续多帧没有检测框能匹配它,会进入等待删除状态,超过 MAX_AGE 帧就直接销毁。
放到疲劳检测场景里看这个机制:驾驶员打了个哈欠,脸因为张大嘴而变形,检测框还在,但外观特征偏移,匹配距离超标;紧接着低头几秒,框直接消失。如果 MAX_AGE 不够长,轨迹在低头期间就被删了,抬头后系统会把同一张脸当成新人,id 从 5 变成 42。用这样的输出去统计「过去一分钟打了几个哈欠」,数据全是碎的。把 MAX_AGE 调到 90,轨迹就能撑过这十几帧的遮挡窗口,id 保持连续。
注意:MAX_AGE 不是越大越好。调太大时,目标真正离开画面很久后旧轨迹还滞留,新出现的其他目标可能被错配给它,反而制造新的 id 错误。以驾驶员场景的遮挡时长为准,60~90 帧是比较稳的区间。
5. 避坑指南:五个踩过的坑和对应的解法
这套工程我在 Win10 + CUDA、Ubuntu 18.04、纯 CPU 笔记本三种环境各跑过一遍,遇到的坑不少,挑最劝退的五条写在这里,每条都是「现象 → 原因 → 解决」的结构。
5.1 一跑 track.py 就报 ImportError: lap 或 scipy 不存在
现象:环境装完 torch 和 opencv 之后,运行 track.py 立刻报 no module named 'lap',或者 scipy 版本冲突。
原因:deep_sort_pytorch 的线性分配依赖 lap 库,它是个 C 扩展,在 Windows 上 pip 直接装经常编译失败;scipy 则常见于系统里已有旧版本,和 numpy 新版本不兼容。压缩包里的 pycache 是 Python 3.8 留下的,如果本机是 3.10 以上,torch 和 ReID 代码的兼容性也要一并确认。
解决:Windows 上优先装预编译 wheel,直接pip install lap,失败就换conda install -c conda-forge lap。scipy 建议先升级 numpy 再pip install scipy --upgrade。装完后在 Python 里import lap验证一下,过得去再跑主程序。
5.2 检测框乱跳,同一张脸同时出现两个 id
现象:输出视频里一张脸被两个框来回抢,id 一会儿是 3 一会儿是 8,跟踪完全没意义。
原因:这个现象九成是 --conf-thres 设太低。检测器把方向盘、手臂、座椅边缘误检成人脸,这些假框和真脸框交替进入跟踪器,导致轨迹分裂。
解决:先把 conf-thres 提到 0.4 重跑一遍,如果误检消失就停在 0.4;如果还有,检查 yolo 权重是不是真的微调版。有人图省事用 yolo11n.pt 基座直接做推理,COCO 模型在 DMS 场景下误检率极高,必须换 yolo11n-dms_awake_yawn_data。这一步验证比重装任何依赖都重要。
5.3 显存溢出:CUDA out of memory
现象:跑了几百帧之后程序崩掉,报 RuntimeError: CUDA out of memory。
原因:imgsz 设成 640 时,YOLO11n 单帧显存占用不大,但 track.py 里如果开了批量推理或者同时缓存 ReID 的特征张量,时间一长显存就被积压占满。
解决:先把 --imgsz 降到 320,检测速度翻倍,显存占用减半,代价是对远处小脸的召回率下降,疲劳检测场景可以接受。如果还溢出,在推理循环里定期调用torch.cuda.empty_cache(),或者限制特征队列长度。纯 CPU 笔记本跑的话,imgsz 320 加上小模型,大概能到 8~12fps,做验证够用。
5.4 ckpt.t7 加载报错,或跟踪距离全部异常
现象:torch.load 读 ckpt.t7 时报警告甚至报错,或者跟踪器能跑但 id 随机切换,MAX_DIST 怎么调都没用。
原因:ckpt.t7 很可能是用旧版 PyTorch 保存的,新版 torch.load 默认安全校验会拦下来。更隐蔽的问题是 ReID 权重和代码里的网络结构对不上,常见于把别人项目的 ckpt.t7 挪到自己的工程里。
解决:加载时显式指定 map_location 和关闭安全校验:
ckpt = torch.load("ckpt.t7", map_location="cpu", weights_only=False)如果确认结构对不上,就别硬用了。常见做法是换一个公开的 ReID 预训练权重,结构要匹配 deep_sort_pytorch 里的模型定义,然后重新导出。整套 yolo11-deepsort 的代码结构很成熟,ReID 部分单独替换不影响检测链路。
5.5 视频路径带中文,cv2.VideoCapture 读不出帧
现象:cap.isOpened() 返回 True,但 read() 一直拿到空帧,输出视频里全是黑屏或直接没输出。
原因:OpenCV 在 Windows 上用 VideoCapture 读文件时对中文路径支持很差,路径里带「测试」「视频」这类中文会静默失败,不报错只出空帧,特别难排查。
解决:把 vid-1.mp4、car-1.mp4 和输出目录全部改成纯英文路径,工程根目录也不要放中文。如果项目文件夹名本身带中文,整个挪到英文目录下再跑。这个坑一次踩过之后,我所有视频工程的目录命名都强制要求 ASCII。
6. 疲劳预警落地技巧:时间窗口投票与阈值标定
检测和跟踪跑通只是第一步,真正让系统变成「可用的疲劳预警」,要把每帧的分类结果变成稳定报警信号。模型单帧输出的 awake/yawn 是有噪声的:同一段打哈欠,可能连续 20 帧是 yawn,中间突然跳 5 帧 awake。直接拿单帧判断,误报会多到没法用。
我一般用时间窗口投票:维护一个固定长度的分类结果队列,只有当窗口内 yawn 占比超过阈值才触发预警。实现很简单:
from collections import deque WINDOW = 150 # 5 秒 @ 30fps,可随视频帧率调整 YAWN_RATIO = 0.2 # 窗口内打哈欠帧占比阈值 def push_result(cls_queue: deque, cls_id: int) -> bool: cls_queue.append(cls_id) if len(cls_queue) > WINDOW: cls_queue.popleft() if len(cls_queue) < WINDOW: return False yawn_cnt = sum(1 for c in cls_queue if c == 1) # 1 = yawn return yawn_cnt / WINDOW > YAWN_RATIO队列满之前不触发报警,避免启动瞬间误报;窗口长度按视频帧率换算,30fps 用 150,25fps 用 125。YAWN_RATIO 这个阈值必须用数据标定,不能拍脑袋:把 vid-1.mp4 按每 2 秒一段人工标注一遍哪些片段在打哈欠,然后跑推理统计真实 yawn 段里窗口占比的中位数,再往下降 20% 作为报警阈值,既覆盖真阳性又不过分敏感。
标定完再验证一遍:把输出视频和预警时间戳对齐,逐段看有没有漏报和延迟。如果漏报集中在低头场景,说明检测阶段漏检比阈值问题更严重,回去调 conf-thres 而不是动窗口。从那以后我每次拿到这类检测+跟踪工程,都强制自己走完整流程:先跑自带视频确认 id 稳定,再调检测阈值,最后才碰窗口和报警参数。这套顺序帮我少走了很多弯路,希望帮到你。
本文还有配套的精品资源,点击获取