简介面向汽车安全与智能座舱领域研究者和开发者这套驾驶员疲劳检测与跟踪方案基于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 通常对应 awake1 对应 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-thres0.35~0.45设太低会把方向盘、手误检成人脸id 乱跳设太高会漏掉低头或侧脸--iou-thres0.45~0.5NMS 去重阈值两个框 IoU 超过该值就合并--imgsz320~640输入分辨率速度不够就降到 320精度优先保持 640conf-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 帧都被匹配上才转 confirmedconfirmed 轨迹才会输出稳定的跟踪 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_locationcpu, weights_onlyFalse)如果确认结构对不上就别硬用了。常见做法是换一个公开的 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 用 15025fps 用 125。YAWN_RATIO 这个阈值必须用数据标定不能拍脑袋把 vid-1.mp4 按每 2 秒一段人工标注一遍哪些片段在打哈欠然后跑推理统计真实 yawn 段里窗口占比的中位数再往下降 20% 作为报警阈值既覆盖真阳性又不过分敏感。标定完再验证一遍把输出视频和预警时间戳对齐逐段看有没有漏报和延迟。如果漏报集中在低头场景说明检测阶段漏检比阈值问题更严重回去调 conf-thres 而不是动窗口。从那以后我每次拿到这类检测跟踪工程都强制自己走完整流程先跑自带视频确认 id 稳定再调检测阈值最后才碰窗口和报警参数。这套顺序帮我少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取