简介:面向驾驶安全预警与疲劳监测场景,这套基于YOLO11和DeepSORT的驾驶员检测跟踪系统,适合安全驾驶研究人员、算法工程师及高校相关专业学生。系统通过目标检测快速定位驾驶员面部区域,再借助多目标跟踪持续识别闭眼、打哈欠、低头等疲劳状态,并及时发出预警。资源包共包含九十三个文件,主要有程序源码及编译文件、训练好的检测模型、目标跟踪模型、示例图像与演示视频、配置文件和操作说明文档,压缩后大小约一百八十一兆字节,目录清晰便于按模块查阅。目前已有六十九人学习下载。资源提供可直接部署的检测模型与配套数据集,涵盖不同光照、角度和表情的驾驶员面部样本,有助提升模型在真实驾驶环境中的泛化能力。完整源码、演示视频和运行说明可让使用者快速复现流程,也便于二次开发或算法改进,对工程项目、课程设计和毕业设计具有实用价值。
1. 这套 YOLO11+DeepSORT 驾驶员疲劳检测项目,解决的不只是“有没有闭眼”
把摄像头对准驾驶员,实时检测眼睛和哈欠,再判断这人是不是疲劳了——这是驾驶安全预警系统里最典型的一类需求。标题里压着的 YOLO11、DeepSORT、数据集和训练好的检测模型,组合起来就是一个可以直接验证的驾驶疲劳监测方案:YOLO11 负责逐帧把驾驶员的眼睛、嘴巴状态检测出来,DeepSORT 负责在连续帧之间稳住同一个人的身份编号,疲劳判定逻辑再根据眼睛闭合时长、打哈欠频次等指标去触发预警。适合谁?刚入手驾驶员监控项目又想少走弯路的工程师,做车辆安全终端想快速验证 DMS 算法可行性的团队,以及在读学生拿现成数据与权重做复现研究的人。拿到这个压缩包,你最该关心的是四件事:数据集能不能直接训、训练好的模型跑起来准不准、DeepSORT 接进去会不会乱跳 ID、疲劳阈值设置得合不合理。这篇文章就把这四条路一步步趟开。
2. 原理拆解:YOLO11 检测器与 DeepSORT 跟踪器为什么这样配合
2.1 YOLO11 在驾驶舱小目标场景的选型理由
YOLO11 是 Ultralytics 继 YOLOv8 之后推出的检测系列,和之前几代比,骨干网络和颈部结构重新做了设计,参数量更省,推理速度更快。在驾驶舱这种固定机位、目标尺度波动剧烈(人脸忽远忽近、侧脸转过去之后框变小)、光照变化极强的场景里,YOLO11n 或 YOLO11s 这种小体量模型通常比大模型更合适——不是大模型不准,而是 DMS 要求 30 帧以上的实时性,还得把算力预算留给后面的 DeepSORT 特征提取。
这个项目里检测器输出的目标一般不只是“人脸”,常见的类别设计是 face、open_eye、closed_eye、yawn,也可能是 mouth_open 这类细粒度状态。类别划分直接决定了疲劳判定怎么往下写:如果模型已经把闭眼单独训成一个类,那疲劳逻辑就简单很多,只需要关心 closed_eye 的置信度和连续帧数;如果模型只检测人脸,那你还得再加一个关键点模型或者眼部分类器。所以拿到打包好的训练好的检测模型之后,第一件事永远是打开它的类别定义文件确认类别顺序,这比急着跑推理重要得多。
2.2 DeepSORT 跟踪器到底解决什么问题(以及代价)
YOLO11 做的是单帧检测,每一帧的检测结果互相之间没有关联。问题就来了:驾驶员眨一下眼,检测框轻微抖动,甚至某一帧闭眼目标没检出来,下一帧又出现,这时候按帧去统计疲劳状态,数据是碎的。
DeepSORT 把检测框串成轨迹。它的核心机制有三个:卡尔曼滤波预测每个轨迹在下一帧的位置,外观特征用一个小型 CNN 提取并做余弦距离匹配,级联匹配优先处理最近确认的轨迹再处理老轨迹。简单说,YOLO11 负责回答“这一帧里哪里有目标”,DeepSORT 负责回答“这个框是刚才那个人的,不是另一个人”。在多乘客场景下这种人脸跟得稳不稳直接决定了后面按 track_id 统计疲劳数据的可信度。
代价也很明确:DeepSORT 不是免费的,外观特征提取器如果在 CPU 上跑,每帧要多花几十毫秒,一旦视频分辨率高或者帧率要求高,CPU 就成瓶颈。这也是为什么真正落地的时候很多人会把 DeepSORT 的轻量化改造提上日程,关于这点后面避坑章再详细说。
2.3 检测器和跟踪器的协作链路
整套系统的数据流是这样走的:
摄像头或视频文件逐帧读入,YOLO11 先做一次前向推理,得到这一帧所有目标的边界框、类别和置信度;把这些框按类别过滤之后转成 DeepSORT 需要的 Detection 对象,交给 tracker.update();tracker 内部维护着若干条轨迹,每一条轨迹有一个稳定的 track_id;下游的疲劳判定模块把每条轨迹的 closed_eye 置信度、嘴巴张开幅度这些状态积累起来,滑动窗口统计之后决定要不要报警。
记住这个链路里有一个关键取舍:跟踪器需要的是稳定的输入,所以给 DeepSORT 的检测框不能照单全收。置信度低于 0.3 的框、不属于关键类别的框,都应该在送入 tracker 之前过滤掉,否则会出现大量短暂存在的假轨迹,疲劳统计就被噪声毁掉了。
3. 数据集和训练好的检测模型:把推理先跑通
3.1 拿到压缩包先做的三层检查
这类项目打包一般都有固定结构:datasets 目录下放 images 和 labels 的 train/val 划分,外加一个 data.yaml;runs/weights 或 weights 目录下放着 best.pt 权重文件;deep_sort 目录放跟踪器源码;根目录有一个主入口脚本。
我拿到手一定会按这个顺序检查,缺一不可:
第一层是检查 data.yaml 里的类别列表和数量。YOLO 训练用的标签是索引数字,类别顺序直接决定推理时 cls 索引含义。比如 data.yaml 写的是0: face, 1: open_eye, 2: closed_eye,那么推理结果里 cls=2 的就是闭眼。很多翻车事故就是类别定义和后续判定逻辑里写死的索引对不上。
第二层是抽查标签。随便取几张训练图片,把对应的 txt 标签和图片重叠画出来看看框准不准。这一步能快速发现标签坐标归一化错没、有没有错标漏标。驾驶员疲劳场景常见的问题是闭眼标签框得过大,把眉毛和前额都框进去,这会导致模型学到的闭眼特征不纯。
第三层是确认权重文件对应的是哪个模型尺寸。YOLO11n 还是 YOLO11s,参数量差好几倍。推理脚本里的 imgsz 也要对应调,通常 DMS 场景用 640×640 或者缩到 416,太大没意义。
3.2 用 YOLO11 加载训练好的检测模型:最小推理代码
确认完结构后,先别急着接 DeepSORT,用下面这段代码把模型和数据的准确性验证一遍。这里用的是 Ultralytics 的标准接口:
from ultralytics import YOLO model = YOLO("weights/best.pt") results = model.predict( source="test_video.mp4", imgsz=640, conf=0.4, iou=0.5, device="0", stream=False, ) for frame_idx, r in enumerate(results): boxes = r.boxes.xyxy.cpu().numpy() confs = r.boxes.conf.cpu().numpy() clss = r.boxes.cls.cpu().numpy().astype(int) print(frame_idx, boxes.shape, clss)这段代码的作用是跑一条完整视频,输出每一帧所有检测框的坐标、置信度和类别索引。conf=0.4是个值得注意的参数:疲劳检测场景里闭眼目标很小,置信度天然偏低,如果设成 0.5 会漏掉很多真实闭眼帧;但如果低于 0.3,又是后面的 DeepSORT 假轨迹的重灾区。常见调法是先设 0.4 跑一遍,观察漏检和误检比例再微调。
imgsz=640是输入分辨率。过高没有意义,过低会把本来就小的眼部目标压没。stream=True和stream=False的区别留到 3.4 讲,但这里先记住:对视频文件做验证用stream=False拿全量结果,对摄像头实时流一定要用stream=True。
3.3 把检测结果转成结构化数据,为接 DeepSORT 做准备
上一段代码输出的只是推理结果对象,DeepSORT 需要的是标准的[x1, y1, x2, y2]边界框、置信度分数、以及对应的类别索引。更重要的,跟踪器需要按帧组织这些检测,因为它的update()一次接收一个帧的所有检测结果。
import numpy as np def yolo_results_to_detections(r, cls_ids=(0, 1, 2)): """把 YOLO11 单帧结果转成元组列表,供 DeepSORT 的 Detection 构造使用""" detections_info = [] if r.boxes is None: return detections_info boxes = r.boxes.xyxy.cpu().numpy() confs = r.boxes.conf.cpu().numpy() clss = r.boxes.cls.cpu().numpy().astype(int) for box, conf, cls in zip(boxes, confs, clss): if cls not in cls_ids: continue # 过滤掉过小的框,避免把噪声喂给跟踪器 if box[2] - box[0] < 10 or box[3] - box[1] < 10: continue detections_info.append((box, conf, cls)) return detections_info这里做两件事:按类别过滤,只保留项目关心的 face 和眼部类别;过滤掉面积太小的框。眼部检测框在图像里可能只有二三十像素,但小于 10 像素的框基本是虚检,留着只会让 DeepSORT 建一堆假的临时轨迹。这一步本身不参与跟踪,但它是连接检测器和跟踪器之间质量的第一道闸门。
3.4 视频流推理的一个常见误用:整段视频一次性加载进内存
不少人在第一次写疲劳检测脚本时,会直接对摄像头或长视频调用model.predict(),然后发现内存飙升、延迟极高,甚至直接卡死。因为摄像头源是无限长的流,如果stream=False,ultralytics 会把整个源读进内存再推理。正确做法是实时源一律stream=True,让推理以生成器方式逐帧吐出结果:
from ultralytics import YOLO model = YOLO("weights/best.pt") results = model.predict(source="0", imgsz=640, conf=0.4, stream=True) for r in results: frame = r.orig_img detections = yolo_results_to_detections(r) # 接下游处理,不能在这里 break 或堆积stream=True的另一个行为差异是它不会把整段结果保存在内存里,你处理完一帧就可以释放。疲劳检测系统的核心逻辑每帧都在跑,这个逐帧循环就是后续所有跟踪和疲劳判定代码的宿主框架。
4. 接入 DeepSORT 跟踪器:从逐帧检测到稳定的轨迹 ID
4.1 DeepSORT 初始化参数到底怎么设
DeepSORT 的对外接口在很多开源实现里基本一致:先构造距离度量器,再构造跟踪器。下面是常见写法,参数含义我逐个解释:
from deep_sort.deep_sort import nn_matching from deep_sort.deep_sort.detection import Detection from deep_sort.deep_sort.tracker import Tracker max_cosine_distance = 0.2 nn_budget = 100 metric = nn_matching.NearestNeighborDistanceMetric( "cosine", max_cosine_distance, nn_budget ) tracker = Tracker( metric, max_iou_distance=0.7, max_age=30, n_init=3, )max_cosine_distance=0.2是外观特征匹配的阈值。驾驶舱场景里目标外观变化不大,阈值设太大容易把不同人匹配到同一条轨迹上,设太小又会让同一个人因为转头、遮挡而被误判为新目标。0.2 是一个保守起点,如果发现 ID 频繁切换可以适当放宽到 0.25。
nn_budget=100是保存历史外观特征的数量上限,防止特征库无限膨胀。单驾驶员场景 50 就够,多乘客场景建议 150 以上,这个参数会直接影响内存占用。
max_age=30表示一条轨迹最多能忍受 30 帧没有匹配到检测框。疲劳检测场景里闭眼状态检测不稳定,某人低头或侧脸丢失几帧是常事,如果 max_age 太短,回来之后会变成新 ID,疲劳统计就断了。30 帧在 30fps 下就是一秒钟,比较合理。
n_init=3是一条轨迹需要连续被匹配 3 帧才算确认。确认前的轨迹用于抑制假目标,不参与疲劳统计。
4.2 逐帧更新循环:把 YOLO11 检测框喂给 DeepSORT
这是整套代码里承上启下的一段,把 YOLO11 的推理结果、DeepSORT 的轨迹更新、以及按 ID 维护的疲劳状态串在一起。
def process_frame(frame, model, tracker, result_processor): # 1. YOLO11 推理单帧 r = model.predict(frame, imgsz=640, conf=0.4, verbose=False)[0] detections_info = result_processor(r) # 2. 转成 DeepSORT 的 Detection 列表 detections = [] for box, conf, cls in detections_info: bbox_tlwh = xyxy_to_tlwh(box) # [x1,y1,x2,y2] -> [cx,cy,w,h] detections.append(Detection(bbox_tlwh, conf, None)) # 3. 跟踪器预测 + 更新 tracker.predict() tracker.update(detections) # 4. 取出确认的轨迹,按 track_id 返回 outputs = [] for track in tracker.tracks: if not track.is_confirmed(): continue bbox = track.to_tlwh() track_id = track.track_id outputs.append((track_id, bbox)) return outputsDetection的第三个参数是外观特征向量。在标准 DeepSORT 里这里会调用一个 ReID 网络提取 128 维特征,但很多打包项目为了轻量会把这个特征置空,退化成 IOU 匹配模式。我建议至少用一个简单的颜色直方图或小 CNN 特征填进去,不然遮挡后恢复目标的能力会明显下降,这点在第 4.3 节展开。
tracker.predict()必须在update()之前调用,因为 predict 用卡尔曼滤波把每条轨迹预测到当前帧,update 再做匹配和修正,顺序反了会出现轨迹位置滞后一帧的现象。
xyxy_to_tlwh是 DeepSORT 默认的锚框格式约定,它的内部 tracker 和可视化代码都基于 tlwh 计算,转错格式会导致跟踪框位置偏一截,下面这个转换函数是标准的:
def xyxy_to_tlwh(bbox_xyxy): x1, y1, x2, y2 = bbox_xyxy w = x2 - x1 h = y2 - y1 return [x1, y1, w, h]4.3 track_id 在疲劳统计里的关键用法:按 ID 维护状态
检测是每帧独立的,跟踪的价值就是让下游逻辑有一个恒定的维度去累积数据。疲劳统计必须以 track_id 为单位,而不是以“这一帧检测到的闭眼框”为单位。
from collections import defaultdict fatigue_state = defaultdict(lambda: {"closed_frames": 0, "total_frames": 0}) for track_id, bbox in process_frame(frame, model, tracker, result_processor): state = fatigue_state[track_id] state["total_frames"] += 1 # 这里假设闭眼判定来自检测结果里的 closed_eye 置信度 closed_conf = get_closed_eye_conf(frame, bbox, model) if closed_conf > 0.5: state["closed_frames"] += 1这里有一个很容易踩的坑:max_age 超时后轨迹会从追踪器里移除,再出现时拿到的是全新 track_id,旧 ID 的累计状态就断了。所以在疲劳判断逻辑里,不建议对同一个 track_id 做超过 30 帧的长期累计,而应该用滑动窗口,窗口长度小于 max_age 的三分之一,比如 10 秒窗口。这样即使 ID 切换,窗口内的统计也不会被上一次旧轨迹污染得太厉害。
5. 疲劳判定阈值和预警触发:EAR、闭眼时长与打哈欠的坑
5.1 眼睛状态判定:EAR 公式 vs 模型直接分类
疲劳判定的第一层是判断某一帧眼睛是否闭合。有两种主流做法。
第一种:如果 YOLO11 训练集里已经有 open_eye / closed_eye 两个类别,那直接用分类置信度做时间平滑即可。优点是不需要额外模型,速度最快;缺点是对标签质量依赖极高,闭眼类别框得不准,模型就学不到纯正的闭合特征。
第二种:用 EAR 公式计算。如果项目里带的是人脸关键点检测器,或者检测框内能稳定输出 68 点,就用眼睛的 6 个关键点算纵横比:
def eye_aspect_ratio(eye_points): """eye_points: 6 个关键点坐标,顺序按 dlib 68 点索引""" p1, p2, p3, p4, p5, p6 = eye_points vertical_1 = dist(p2, p6) vertical_2 = dist(p3, p5) horizontal = dist(p1, p4) return (vertical_1 + vertical_2) / (2.0 * horizontal)EAR 的好处是跨个体、跨相机距离的稳定性比直接分类置信度好,因为它是归一化的几何比例,和人脸尺度无关。常见阈值是 0.2 到 0.25,低于阈值视为闭眼。但驾驶舱相机离人脸近,脸的大幅晃动会导致 EAR 抖动很厉害,直接低于阈值会误触发。我见过不少项目在 EAR 低于阈值后还要求持续 3 到 5 帧才判定一次闭眼事件,这个缓冲在时序逻辑里几乎是必须的。
5.2 预警触发组合规则:连续帧计数、PERCLOS 滑动窗口、打哈欠频率
疲劳不是“某一帧闭眼”,而是状态在一段时间里持续恶化。通用的做法是把三个特征做加权组合:
第一个是连续闭眼帧数。人在正常眨眼时眼睛闭合时间通常不超过 0.2 秒,在 30fps 下也就是 6 帧左右。如果连续闭眼超过 10 帧,就要进入疑似疲劳状态。
第二个是 PERCLOS 指标,也就是在滑动时间窗口内闭眼帧占总帧数的比例。常见窗口是 60 秒,阈值可以设在 0.2 到 0.4 之间。疲劳初期闭眼比例会明显上升,但还没到长时间闭眼的程度,PERCLOS 能更早捕捉到这个趋势。注意这个指标对 ID 连续性要求很高,如果跟踪器频繁换 ID,窗口内混进多条轨迹的数据,数值就失去意义了。
第三个是打哈欠频次。如果模型里有 yawn 类别,每分钟打哈欠超过 4 到 6 次可以作为一个强提醒信号。打哈欠的持续时间相对固定,用连续 15 到 30 帧检测到 yawn 类别且高置信度来判定一次打哈欠,再乘以每分钟计数。
5.3 预警触发代码和参数化
下面这段代码把上述规则落成可调参的模块,所有阈值都通过参数传入,不要在代码里写死。
class FatigueAlert: def __init__(self, ear_threshold=0.22, closed_min_frames=8, perclos_window=60, perclos_threshold=0.25, yawn_min_frames=20, yawn_per_minute=4): self.ear_threshold = ear_threshold self.closed_min_frames = closed_min_frames self.perclos_window = perclos_window self.perclos_threshold = perclos_threshold self.yawn_min_frames = yawn_min_frames self.yawn_per_minute = yawn_per_minute self.eye_buffer = [] self.yawn_buffer = [] self.yawn_count = 0 self.start_time = None def update(self, ear_value, yawn_flag): # 闭眼判定加帧数缓冲 self.eye_buffer.append(ear_value < self.ear_threshold) if len(self.eye_buffer) > self.perclos_window * 30: self.eye_buffer.pop(0) # PERCLOS 计算 if len(self.eye_buffer) >= 30 * 30: perclos = sum(self.eye_buffer) / len(self.eye_buffer) if perclos > self.perclos_threshold: return "perclos_alert" # 连续闭眼帧数 closed_streak = 0 for closed in reversed(self.eye_buffer[-30:]): if not closed: break closed_streak += 1 if closed_streak >= self.closed_min_frames: return "closed_alert" return "normal"这里有几个参数值得反复调。perclos_window=60太短会导致正常眨眼也被拉高比例,太长又让响应变得迟钝。closed_min_frames=8在 30fps 下约 0.27 秒,能过滤正常眨眼,又不至于漏掉真正的微睡眠。预警还要做防抖,同一状态触发后至少 5 秒内不重复报警,否则连续蜂鸣会让驾驶员更烦躁。实际运行中我习惯把每天的视频片段和预警日志一起记录,回放时看报警时刻是不是真的对应疲劳状态,这是最靠谱的调参依据。
6. 避坑:这套系统最常见的 5 个问题,现象、原因和处理
6.1 现象:一开摄像头就冒出几十个假轨迹,track_id 疯狂跳动
原因:检测器把大量低置信度框和背景噪声框也送进了 DeepSORT,跟踪器把这些都当成潜在目标,n_init=3还没走完又发现新的框,轨迹数量立刻膨胀。
处理:在送入 tracker 之前,把置信度下限提到 0.4,同时按类别过滤,只保留 face 和眼部类别。另外把max_iou_distance从 0.7 调到 0.6,减少进入 IOU 关联的候选框数量。假轨迹少了,疲劳统计的噪声自然就跟着降下来。
6.2 现象:驾驶员戴上墨镜之后,系统几乎不再输出闭眼状态
原因:训练数据里大概率没有或者极少有墨镜样本。闭眼模型依赖眼睛周围皮肤特征,墨镜直接把这些特征盖住了,模型只能靠上下文猜测。
处理:如果条件的允许,优先使用带红外补光的相机,红外能穿透墨镜。如果只能用可见光相机,则需要做数据增强:把训练集的眼部样本做亮度降低、遮挡模拟,人为增加闭眼样本多样性。这时候再配合 EAR 做兜底,即模型输出置信度低时切换到面部关键点的几何判定,能救回一部分场景。
6.3 现象:同一段视频离线跑效果很好,接上摄像头画面翻转过来就崩
原因:摄像头安装角度的镜像翻转导致数据分布变化。模型训练时看到的左右眼位置是固定的,镜像之后眼睛左右关系颠倒,模型的特征激活位置对不上。
处理:在训练阶段就加入水平翻转增强,并固定摄像头的安装位置和镜像设置。实际部署时,在代码入口处加一个标准化的画面方向预处理,把摄像头画面校正到和训练集一致的朝向。
6.4 现象:实时推理帧率低于 15fps,报警延迟严重
原因:YOLO11 和 DeepSORT 的特征提取都压在同一处理器上,CPU 成了瓶颈。视频源分辨率太高时,模型输入做 resize 也在浪费大量时间。
处理:常见做法是把 YOLO11 换成 n 型号并开启 TensorRT 导出。DeepSORT 的特征提取器改成每 5 帧才提取一次,中间帧只做卡尔曼预测。实测这类组合通常能从 10fps 提到 25fps 以上,代价是高频率 ID 切换时匹配准确率略降。
6.5 现象:闭眼类别检测召回很低,疲劳状态经常漏报
原因:数据集中闭眼样本太少,尤其是驾驶场景下大多是正常睁眼,闭眼只占标注数据的百分之几。模型对这个类别的学习不充分。
处理:除了加样本,更切实际的做法是把 loss 里的类别权重调高,比如把 closed_eye 的权重设为 open_eye 的 3 到 5 倍。另一种做法是把判定逻辑的先验阈值调宽松,用更高的误报换更低的漏报,疲劳预警宁可多报也不能不报,这和通用目标检测的调参优先级正好相反。
7. 进阶:离线视频回放验证和 TensorRT 加速部署
系统能跑起来之后,最值得投入的一件事是建立离线验证习惯。我建议录制三段典型视频:一段正常驾驶、一段模拟疲劳驾驶、一段包含低头和侧脸的复杂动作。跑完后把每一帧的 track_id、眼睛闭合状态、PERCLOS 值、预警状态写进 CSV,再逐帧回放比对。这样能精准定位是检测器漏了,还是跟踪器换了 ID,还是阈值设得不对,而不是靠肉眼反复看视频找问题。统计误报率要控制在每 30 分钟不超过 2 次,漏报率要压到零,这是 DMS 系统最基本的底线。
部署层面,先把训练好的模型导出成 ONNX,再转 TensorRT,这一步几乎不损失精度,但帧率收益非常直观:
from ultralytics import YOLO model = YOLO("weights/best.pt") model.export(format="onnx", imgsz=640, dynamic=False, simplify=True) # 再在 Jetson 或带 TensorRT 的设备上转成 engine导出时注意三个参数:dynamic=False固定输入尺寸,TensorRT 会做更多图优化;simplify=True会清洗掉部分冗余算子;导出后一定重新评估一下精度,ONNX 转换偶尔会把某些层的运算顺序打乱,尤其在检测头部分。
对 DeepSORT 的轻量化,常见做法有两种,一是把外观特征提取网络从较大的 ReID 模型换成 MobileNet 底座的小网络,二是降低特征提取频率,5 帧提取一次,中间帧靠卡尔曼预测和 IOU 匹配。前面避坑章提过这个方案,实际落地时收益通常比替换检测器更明显,因为疲劳场景特征变化慢,特征更新的频率不需要那么高。还可以尝试把级联匹配顺序调整,优先匹配最近确认的轨迹而不是最新出现的轨迹,这在低帧率场景下能减少不少 ID 切换。
最后说一个我的习惯:每次改完阈值或模型,我都会把预警日志里的时间戳和真实驾驶录像对上,回看报警前 10 秒到底发生了什么。这套方法救了我很多次,因为阈值这东西真的像玄学,不拿真实片段反复验证,永远不知道它在上班路上会怎么误报。希望这套 YOLO11 加 DeepSORT 的疲劳监测思路能帮你在自己的项目里少踩一遍这些坑。
本文还有配套的精品资源,点击获取