简介:这是一份面向驾驶员疲劳检测场景的YOLO算法数据集,适合目标检测学习者、计算机视觉研究者和车载安全系统开发者使用,核心目标是让模型学会识别闭眼、打哈欠等典型疲劳行为。资源共8744个文件,包括2915张jpg图像、2915个txt标注文件和2914个xml标注文件,txt简洁记录边界框坐标与类别,xml提供更完整的结构化元数据,便于按需转换格式;整体压缩包256.88MB,体积适中,下载后即可直接开展训练与验证。图像内容覆盖不同疲劳程度的驾驶员面部状态和驾驶动作,尤其包含面部特写,可用于模型学习眼部闭合、嘴部动作等关键疲劳特征;基于YOLO一次预测全部目标的特性,还能支撑同时检测闭眼与打哈欠等多目标任务,适合用于算法对比和实时监控场景验证。已有2926人学习下载,标注关系一一对应,目录组织清晰,能显著减少数据预处理工作量,适合快速搭建驾驶员疲劳检测原型系统。
1. 驾驶员疲劳检测数据集:YOLO算法好写,数据才是真正的门槛
白天路测一切正常,一到夜间或者逆光,模型就开始把正常眨眼当成闭眼,把打哈欠漏掉——这是用YOLO算法做驾驶员疲劳检测数据集训练时最常见的首发症状。疲劳检测本质是一个小目标、强姿态变化、光照敏感的目标检测任务:眼睛区域在640分辨率下只有几十个像素,标注质量直接决定模型上限。这篇笔记适合打算从零构建疲劳检测数据集、用YOLO做训练和部署的工程师。我会先讲数据集从哪来,再给转换脚本、训练参数和避坑经验,最后落到实车验证。
2. 数据集从哪来:开源选型与自建采集的取舍
2.1 先看开源:YawDD、NTHU、DROZY 能直接用吗
做驾驶员疲劳检测,业内常用的是 YawDD、NTHU-DDD 和 DROZY 这几个公开数据集。YawDD 是车内视角拍摄的打哈欠视频,标注到帧级别,适合做打哈欠检测;NTHU-DDD 覆盖了驾驶中打电话、喝水、转头等分心行为,类别比较全;DROZY 包含闭眼、打哈欠和头部姿态的标注,跟疲劳状态直接相关。
但这几个数据集有一个共同问题:它们大多是实验室环境采集,背景单一、光照固定、摄像头位置理想。拿 YawDD 来说,它用的是车内后视镜附近视角,人物正对镜头,但真实行车场景里驾驶员会转头看后视镜、低头看仪表盘、被方向盘遮挡。从公开数据训练出来的模型,到实车上大概率会掉点。
我的建议是:公开数据集只作为预训练或者数据补充,不要作为唯一数据源。先用公开数据让模型跑通流程,确定 benchmark,再花时间采自己的数据。另一个思路是参考 BDD100K 这种自动驾驶数据集做背景迁移——它不是疲劳数据集,但提供各种光照天气下的车内语义信息,可以用来做风格迁移或数据增强的素材。
2.2 自建采集:摄像头位置、光照覆盖与标注工具
自建数据集的摄像头位置是第一个决策点。常见做法是装在仪表盘上方或后视镜位置,让镜头正对驾驶员面部,这样能同时看到眼睛、嘴巴和头部姿态。如果装得太低,下巴以下会被遮挡;装太高,俯仰角会让眼睛区域的像素进一步压缩。
光照覆盖是第二个决策点。白天自然光、夜间仪表盘微光、隧道里的频闪照明、逆光,这四种条件必须采集到,否则后面做好数据增强也救不回来。夜间数据尤其重要——疲劳检测最大的应用场景就是夜间长途驾驶,而夜间人在困倦时头部姿态和白天差别很大,喜欢歪头、托腮,这些都会干扰检测。
标注工具我用过 LabelImg 和 X-anything(X-AnyLabeling),两者都支持导出 VOC 格式 XML。LabelImg 轻量,适合小团队;X-AnyLabeling 支持 SAM 辅助标注,效率高一些。标注类别就定三种:open_eye(睁眼)、closed_eye(闭眼)、yawn(打哈欠),也有人把 yawn 拆成 mouth_open 和 mouth_closed,但类别越多标注成本和错标率越高,我一般先用三类跑通。
2.3 数据清洗:删除坏样本比标注更重要
很多新手忽略数据清洗这一步,直接把采集的视频按帧抽出来就标。结果模型训练出来,loss 降不下去,一查发现训练集里大量模糊帧、过曝帧、驾驶员低头到完全看不到眼睛的帧,标签没法标,模型就只能硬学。
我一般会在标注前先做一轮自动清洗:用 OpenCV 检测人脸,把检测不到人脸或者人脸置信度低于阈值的帧直接丢掉。再按亮度直方图筛一遍,把全黑和全白过曝的帧剔除。清洗后剩下的帧,至少要保证左右眼可见且嘴巴完整。这一步能砍掉大概 15% 到 20% 的无效数据,省下来的时间远大于清洗脚本的成本。
清洗脚本不复杂,核心就两步:
import cv2 import numpy as np def filter_frames(video_path, min_face_conf=0.8, brightness_range=(20, 235)): cap = cv2.VideoCapture(video_path) # 用OpenCV自带的人脸检测器做粗筛,精度够用 face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) frame_idx = 0 kept = [] while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(40, 40)) brightness = np.mean(gray) # 同时满足“检测到人脸”和“亮度在合理区间”才保留 if len(faces) > 0 and brightness_range[0] <= brightness <= brightness_range[1]: kept.append(frame_idx) frame_idx += 1 cap.release() return kept # 用法示例:只保留检测到人脸的帧 if __name__ == "__main__": kept_frames = filter_frames("night_drive_01.mp4") print(f"原始帧数: {frame_idx}, 保留帧数: {len(kept_frames)}")上面这段代码逻辑很简单:逐帧读视频,转灰度后用人脸检测器找脸,同时计算整帧平均亮度。只有两个条件同时满足才保留。min_szie=(40, 40)是怕误检小区域,brightness_range上下限可以按实际摄像头微调。注意 Haar 级联检测器在侧面人脸时会失效,所以这个脚本只能做粗筛,漏检的侧面帧需要靠标注时人工把关。
3. 把原始标注变成 YOLO 能吃的格式:VOC 转 YOLO 的转换脚本与四个边界坑
3.1 三种标注格式的差异:非搞清楚不可
标注工具默认导出的是 VOC 格式,也就是 XML 文件——每个目标一个<object>节点,里面记录类别名称和边界框的四个绝对值坐标xmin, ymin, xmax, ymax。YOLO 训练需要的是 TXT 格式——每行一个目标,格式是class cx cy w h,而且cx, cy, w, h必须归一化到 0 到 1 之间。
第三类常见格式是 COCO 的 JSON,用bbox: [x, y, w, h]表示,坐标是左上角加宽高。很多做数据增强的库(比如 albumentations)依赖 COCO 格式,所以转换脚本里最好能直接输出三种格式,调试时好相互对照。
转换的坑主要不在格式本身,而在坐标系的换算。从 VOC 转 YOLO 的公式是cx = (xmin + xmax) / 2 / img_width,w = (xmax - xmin) / img_width,高度同理。只要图片尺寸拿错,所有归一化坐标全部偏移。这就是第一个边界坑:别从 XML 里猜图片尺寸,要直接从原图读。
3.2 一个能直接跑的 XML 转 TXT 脚本
下面这个脚本我用了很久,除了转换,还顺带做了类别过滤和坐标越界钳制:
import os import cv2 import xml.etree.ElementTree as ET CLASS_MAP = {"open_eye": 0, "closed_eye": 1, "yawn": 2} def convert_voc_to_yolo(xml_path, img_dir, out_dir): tree = ET.parse(xml_path) root = tree.getroot() # 关键:从原图读取尺寸,而不是依赖XML里的size节点 img_path = os.path.join(img_dir, root.find("filename").text) img = cv2.imread(img_path) if img is None: print(f"图片读取失败: {img_path}") return h, w = img.shape[:2] lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in CLASS_MAP: continue cls_id = CLASS_MAP[name] box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) # 钳制到图片范围内,防止标注出界导致训练异常 xmin = max(0, min(xmin, w - 1)) xmax = max(0, min(xmax, w - 1)) ymin = max(0, min(ymin, h - 1)) ymax = max(0, min(ymax, h - 1)) cx = (xmin + xmax) / 2 / w cy = (ymin + ymax) / 2 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_name = os.path.splitext(os.path.basename(img_path))[0] + ".txt" with open(os.path.join(out_dir, out_name), "w") as f: f.write("\n".join(lines)) # 批量转换目录下的所有XML if __name__ == "__main__": xml_dir = "annotations_voc" img_dir = "images" out_dir = "labels_yolo" os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if xml_file.endswith(".xml"): convert_voc_to_yolo( os.path.join(xml_dir, xml_file), img_dir, out_dir )逻辑说明:先解析 XML,拿到filename字段拼出原图路径,用 OpenCV 读图取真实宽高;接着遍历所有<object>,跳过不在CLASS_MAP里的类别;坐标钳制到[0, w-1]和[0, h-1]是为了防标注工具画框时稍微出界,这个在疲劳检测里很常见,因为眼睛框经常会贴着图片边缘;最后按归一化公式写入 TXT。
参数说明:CLASS_MAP要和你之后的data.yaml类别顺序一致,改一个地方四个文件都得跟着改。钳制范围我用w-1而不是w,这是像素索引的习惯,避免索引越界。注意如果一张图没有任何有效目标,生成的 TXT 是空文件,YOLO 训练会跳过它,但建议还是把这张图从数据集删掉,因为空标签文件在后续做 mosaic 增强时可能被当成背景样本,干扰训练。
3.3 三个边界坑:图片尺寸、类别顺序、空标签
第一个边界坑已经在代码里解决了:不要信 XML 里的<size>节点。有些标注工具会在尺寸变化后不更新 XML,导致 XML 里写 1280x720,实际图片是 1920x1080,坐标全部错位。直接从原图cv2.imread读,一劳永逸。
第二个坑是类别顺序不一致。YOLO 的类别 ID 是按字母序或者你定义的顺序从 0 开始编号的,但 VOC 里类别名是字符串。如果CLASS_MAP里yawn是 2,训练配置里却把yawn排在第一位,那所有打哈欠的框都会被当成睁眼训练。防止办法是训练前用脚本统计每类标签的数量,跟人工标注的数量对一下。
第三个坑是划分数据集时文件匹配丢失。常见做法是train.txt里写图片路径,YOLO 靠把图片路径里的images替换成labels来找标签文件。如果目录结构不规范,比如图片在dataset/images/train/,标签放在dataset/labels/train/,这没问题;但一旦有嵌套目录,或者图片后缀是.jpg而标签后缀是.txt之外的情况,训练时就会提示找不到标签。我一般划分完会跑一段校验脚本:
import os def verify_split(img_dir, label_dir): imgs = set(os.path.splitext(f)[0] for f in os.listdir(img_dir)) labels = set(os.path.splitext(f)[0] for f in os.listdir(label_dir)) missing = imgs - labels extra = labels - imgs print(f"缺标签的图片: {len(missing)} 张") print(f"没有对应图片的标签: {len(extra)} 个") # 列出前5个缺失项,重点排查 for m in list(missing)[:5]: print(f" {m}") verify_split("images/train", "labels/train")这套校验逻辑是拿文件名取主干的集合做差集。缺标签的图片如果直接喂给训练,YOLO 会跳过或者当成背景,数据量少时看不出问题,数据量大了会明显拉低 mAP。extra是那些有标签但图片被清洗掉的残留,直接删掉即可。
4. 模型选型与训练:YOLOv5 还是 YOLOv8,参数怎么调
4.1 选型逻辑:不是越新越好,是部署约束说了算
到训练这一步,手头已经有了清洗后的图片和 YOLO 格式标签。接下来要面对的第一个问题是:用 YOLOv5 还是 YOLOv8?对疲劳检测这种小目标密集的实时任务,我建议优先试 YOLOv8n 或 YOLOv5s。
这两个版本的核心差异在 head 结构和 loss 设计。YOLOv8 用 Anchor-Free 的 Decoupled Head,把分类和回归分支拆开,在睁眼/闭眼这种高相似度的分类任务上,收敛更快、类别混淆更少;YOLOv5 的 Anchor 机制对特定尺寸的目标更敏感,如果你数据里眼睛框的宽高比很稳定,v5 表现也不差。
我的实践经验是:先用 YOLOv8n 跑一遍 baseline,因为它在同等算力下收敛速度快,方便快速验证数据质量。如果模型推理要部署到 Jetson 这类边缘设备,再对比 YOLOv5s 的 TensorRT 加速效果。v8 和 v5 在 ONNX 导出和 TensorRT 部署上都有成熟工具链,不需要担心生态问题。
4.2 训练命令与关键参数:img_size、batch、epochs 怎么设
以 YOLOv8 为例,训练命令本身不复杂:
yolo train \ model=yolov8n.pt \ data=data.yaml \ imgsz=640 \ batch=32 \ epochs=200 \ patience=30 \ device=0 \ workers=8 \ project=driver_fatigue \ name=exp_night \ pretrained=Trueimgsz=640是检测精度和速度的平衡点。疲劳检测里眼睛区域很小,如果显卡显存允许,建议用 640 起步,先看 baseline,再试imgsz=800对比小目标召回率。batch不是越大越好,YOLOv8 默认开启 AutoBatch,但手动设一个能稳定跑满显存的值更好——我一般让 batch 乘以 640 平方再乘以 3 字节,不超过显存的 70%。
patience=30是早停耐心值,如果验证集 mAP 连续 30 个 epoch 不提升就提前结束。疲劳检测数据量通常不大,几百到几千张,训练到第 100 到 150 个 epoch 就会收敛,epochs=200配合早停足够。
data.yaml是数据集配置文件,YOLOv8 要求里面必须有train、val路径和nc(类别数)与names(类别名列表)。常见写法:
train: /home/user/driver_fatigue/images/train val: /home/user/driver_fatigue/images/val nc: 3 names: ["open_eye", "closed_eye", "yawn"]注意训练路径要写到images目录,YOLO 会自动在同级目录找labels。如果你把标签放在别处,可以在配置里用label_map或者直接改目录结构,别省这一步。
4.3 损失函数与训练监控:loss 降了不代表检测准了
YOLOv8 的损失函数由三部分构成:分类损失用 BCE,回归损失用 CIoU 变体,分布损失用 DFL。对疲劳检测来说,最值得关注的是回归损失——因为闭眼和睁眼两个类别的边界框几乎在同一位置,只有高度上有细微差别,回归不准就会把这两种状态混淆。
训练时不要只看box_loss和cls_loss曲线,要重点看验证集上每个类别的单独召回率。YOLO 训练日志里会输出每个类的 AP,我一般要求closed_eye的 AP 达到 90% 以上才算数据达标,yawn可以放宽到 85%——打哈欠的头部姿态变化大,本来就难检。
监控训练的一个实用方法是每 10 个 epoch 手动跑一次推理,把结果图存下来:
yolo predict model=runs/driver_fatigue/exp_night/weights/best.pt \ source=samples/night_test/ \ save=True \ conf=0.25conf=0.25是置信度阈值,疲劳检测任务通常设 0.25 到 0.3 之间。如果低于 0.25,误报会多到没法用——正常开车的驾驶员会被频繁判定为闭眼,这在实车上会引发误报警。我在实车测试时还专门测过一个指标:连续正常驾驶 10 分钟,模型报警次数必须为 0,这是硬指标。
5. 避坑指南:疲劳检测数据集训练五个翻车现场
5.1 训练 loss 正常,验证 mAP 却很低
现象:box_loss 和 cls_loss 都平滑下降,训练集 mAP 很高,但验证集 mAP 始终在 0.5 以下。
原因:这是典型的数据泄漏或划分不随机。最常见的是同一个人的视频帧被同时分进 train 和 val,模型在训练集里"记住"了这个人,验证时遇到的还是同一张脸,结果一到新的人脸上就失效。另一种可能是采集的连续视频帧高度相似,train 和 val 里都是同一场景的相邻帧。
解决:按视频文件划分数据集,而不是按帧划分。一个视频的所有帧要么全进 train,要么全进 val。可以用如下方式按视频名分组:
import os, random from collections import defaultdict # 假设图片命名格式为 videoXX_frameYY.jpg def split_by_video(img_dir, train_ratio=0.8): video_groups = defaultdict(list) for f in os.listdir(img_dir): video_id = f.split("_")[0] # 取视频ID video_groups[video_id].append(f) videos = list(video_groups.keys()) random.seed(42) random.shuffle(videos) split_idx = int(len(videos) * train_ratio) train_videos = set(videos[:split_idx]) train_files = [] val_files = [] for v, files in video_groups.items(): if v in train_videos: train_files.extend(files) else: val_files.extend(files) return train_files, val_files这段脚本按视频 ID 分组后切分,且设了随机种子保证可复现。关键在split_idx的比例设置,疲劳数据一般在 8:2 到 9:1 之间。如果视频数量太少,比如少于 10 个,按视频划分会导致 val 集合太小,这时建议改用 K 折交叉验证。
5.2 白天检测正常,夜间全部失效
现象:白天测试集 mAP 超过 90%,夜间测试集 mAP 直接掉到 60% 以下。
原因:训练数据中白天和夜间的比例失衡,通常白天占了 80% 以上。YOLO 训练时的颜色空间增强(hsv_h、hsv_s、hsv_v)只能模拟小范围的亮度变化,不足以填补夜间数据的分布空缺。
解决:第一优先级是采集更多夜间数据,最低要求夜间数据占比达到 25% 到 30%。第二优先级是训练时加大色彩增强参数。在 YOLOv8 的配置里可以这样调:
hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.6 degrees: 5.0 translate: 0.1 fliplr: 0.5 mosaic: 1.0hsv_v从默认的 0.4 调到 0.6,可以增强亮度扰动,等于变相做了亮度泛化。fliplr是水平翻转,注意闭眼和打哈欠在翻转后仍然是有效的——左眼变成右眼,类别不变。不要用垂直翻转,脸部上下颠倒后模型学到的特征完全没有实际意义。
另一个有效的夜间泛化手段是图像增强时做 gamma 校正模拟夜间对比度,我用过albumentations库在训练前做离线增强,但要注意增强后的图片要重新生成标签。YOLO 自带的增强是 on-the-fly 的,不需要动标签文件,优先用自带的。
5.3 戴眼镜的驾驶员闭眼检测不到
现象:训练时没注意,部署后戴眼镜的驾驶员闭眼时模型完全没有反应,看样本却发现镜片反光把眼睛区域完全遮住了。
原因:眼镜反光或者镜框遮挡导致闭眼特征被破坏。普通眼镜还好,偏光镜和墨镜几乎会让闭眼检测彻底失效,因为眼睛区域一片黑,模型无法区分"闭眼"和"戴墨镜睁眼"。
解决:标注时需要专门增加一个类别sunglasses,把戴墨镜的样本单独标出来,然后在后处理逻辑里做特殊处理——检测到sunglasses时,不再输出closed_eye的判罚,转而依靠头部姿态判断疲劳。这个策略需要数据里真的有对应样本,所以采集时要有意识地覆盖戴眼镜人群。训练时还可以额外用随机遮挡增强:
# 在YOLOv8的augment配置中增加随机遮挡,模拟镜框反光 # 在data.yaml同目录建一个augment.yaml # 核心思路: 训练时随机对眼睛区域做像素扰动实际上 YOLOv8 的增强并没有内置随机遮挡模块,我是在标注阶段追加了一轮人工复核专门处理眼镜样本,宁可样本少而精,不要多而滥。
5.4 闭眼小目标漏检,mAP 高但实际用不起来
现象:整图 mAP 不低,但闭眼类别的 AP 远低于睁眼类别的 AP,把验证集图片放大看,发现小尺寸的闭眼框全部没检出。
原因:眼睛在 640x640 的输入图上通常只占 20x10 像素左右,属于典型的小目标。YOLOv8 的检测头虽然有 P3、P4、P5 三个尺度,但小目标的 anchor-free 回归依然容易漏。
解决:有两条路,一是裁剪训练——把脸部区域先检测出来,裁剪成单独的图片再训练一个眼睛检测模型,两级级联;二是在不裁剪的前提下提高输入分辨率到 800 或者 960。我通常先试imgsz=800,如果显存撑得住,小目标 AP 能提升 3 到 5 个点。显存不够就降低 batch 大小配合梯度累积。
还有一个隐藏参数容易忽略:YOLOv8 的close_mosaic参数,默认最后 10 个 epoch 关闭 mosaic 增强。小目标训练时 mosaic 会进一步压缩目标尺寸,训练初期有效,后期反而有害,保持默认即可,不要手动把mosaic全程关掉。
5.5 类别严重不平衡,打哈欠样本占不到 10%
现象:训练出的模型对yawn类几乎无感,标签统计里yawn框的数量只有open_eye的十分之一。
原因:疲劳数据采集时驾驶员清醒状态下打哈欠本来就少,标注人员也倾向于把嘴巴微张的样本漏标。类别不平衡会让损失函数被大类主导,YOLO 的 BCE 分类损失对小类别的梯度被稀释。
解决:先做标签统计确认失衡程度,然后针对少样本类别做重采样。YOLOv8 没有内置按类别过采样的参数,但可以在数据层面解决——把yawn类的图片在训练集里复制几份。注意是按图片复制,不是按标注复制,复制后的图片要用不同的增强参数(比如水平翻转 + 色彩抖动),否则模型只会背样本。数据量实在不够时还有一种办法:从公开数据集里单独抽出打哈欠的视频帧补充进来,DROZY 和 YawDD 的打哈欠片段质量都还可以。
6. 部署验证:从 ONNX 导出到实车效果判断
6.1 导出 ONNX 并用 TensorRT 加速
训练收敛后,部署到实车设备前,先把模型导出为 ONNX 再转 TensorRT。YOLOv8 导出命令很简洁:
yolo export model=best.pt format=onnx imgsz=640 dynamic=False opset=12dynamic=False表示输入尺寸固定 640x640,TensorRT 优化时能用固定 shape 做更多层融合。如果实车上摄像头分辨率不是标准 640,可以在导出后先用cv2.resize把输入帧缩放到 640,推理完再把框映射回原始分辨率。疲劳检测对延迟敏感,固定分辨率比动态分辨率省下不少毫秒。
TensorRT 转换在 Jetson 上用的是trtexec工具,桌面端可以用torch_tensorrt或者 ONNX Runtime 的 CUDA EP。转完精度会有微小损失,实车测试前必须做一次精度对比:用同一批验证集图片,比较 PyTorch 模型和 TensorRT 模型的 mAP,差值超过 0.5 个点就要检查是不是量化配置出错。
6.2 实车验证:mAP 之外必须盯的三个指标
表:疲劳检测实车验证的核心指标
| 指标 | 含义 | 阈值参考 | 说明 |
|---|---|---|---|
| 漏报率 | 实际闭眼但未报警 | 小于 2% | 漏报是安全红线 |
| 误报率 | 正常驾驶但报警 | 每 10 分钟 0 次 | 误报会让人关掉系统 |
| 单帧延迟 | 从取帧到输出结果 | 小于 50ms | 报警需要及时性 |
实车验证要在不同时段跑,至少覆盖清晨逆光、正午强光、夜间三种场景。我做过一次对比,白天 mAP 95% 的模型,在夜间隧道口逆光场景直接暴跌到 70%,原因是采集数据时没有考虑到摄像头逆光下的自动曝光导致人脸过暗。这类问题在实验室完全测不出来,只能实车迭代。
6.3 一个实用技巧:报警状态的时序滤波
疲劳检测不能只看单帧输出——单帧误检无法避免,但可以靠时序逻辑消除。我常用的办法是引入"连续帧确认"机制:连续 5 帧中至少 4 帧检测到closed_eye,才触发闭眼报警;连续 3 帧检测到yawn才记录打哈欠事件。这个逻辑在代码里只是几行状态机,但能显著压低误报率。
这是我做过好几个疲劳检测项目后最深的教训:模型精度只决定上限,状态机设计决定真实体验。有一次实测时夜间误报率降到零,不是因为我换了更强的模型,而是因为我把确认帧数从 3 改成 5,代价是报警延迟多了约 100ms——对于疲劳提醒场景,这个折中是值得的。数据、模型、状态机,三者缺一不可。希望帮到你。
本文还有配套的精品资源,点击获取