ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

打哈欠检测数据集实操:用YOLOv8训练疲劳驾驶目标检测模型

打哈欠检测数据集实操:用YOLOv8训练疲劳驾驶目标检测模型 简介这是面向YOLO系列算法目标检测任务的数据集聚焦打哈欠与不打哈欠两类状态识别适配YOLOv5、YOLOv8、YOLOv9、YOLOv10、YOLO11等常见框架适合目标检测初学者与进阶开发者直接用于模型训练、验证和测试。压缩包共2000个文件以标注文件为主整体约230.71MB标注格式同时提供YOLO格式txt与VOC格式XML两种规范并包含已划分好的数据集配置文件data.yaml可直接接入训练流程。YOLO格式按类别索引与归一化坐标存储VOC格式便于可视化调试两种文件分别存放便于按需调用。数据集已完成训练、验证、测试划分下载后无需额外整理即可开展实验。目前已有236人学习下载适合需要标准带标签数据集进行打哈欠检测模型训练或算法对比研究的读者。1. 打哈欠数据集能解决什么问题从疲劳驾驶到课堂专注度做疲劳驾驶检测时最容易被误判的不是闭眼而是打哈欠。打哈欠这个动作往往持续2到4秒嘴部变形大、遮挡多、光照一变就容易漏检单靠眨眼检测根本抓不准。专门训练一个“不打呵欠 / 打哈欠”二分类目标检测模型比笼统地做一个疲劳状态分类器更可控误报也能单独排查。这个16300张带标签的打哈欠数据集就是为这类任务准备的规模中等、类别简单负样本足够不会让刚入门的人被海量标注淹没也不会让项目在数据上缺胳膊少腿。适合刚跑通 yolo 入门流程的人用它把数据清洗、格式转换、训练调参这条路完整走一遍也适合已经在做驾驶行为分析、课堂专注度检测的工程师拿来做动作级的前置检测。后面所有内容都围绕一个目标拿到这个 zip 之后怎么在最短时间内训练出一个能用的打哈欠检测模型。2. 拆开压缩包先别急着训练搞清数据结构和标签语义很多人的第一个动作是把 zip 直接解压扔进训练脚本然后跑出来一堆乱框。这个数据集在多数情况下会用 YOLO 格式的 txt 标签但也可能内嵌 VOC 的 XML甚至同时带一份 classes.txt。无论哪种形式我都建议先停下来把目录结构、命名规则、标签的类别顺序和标注区域确认清楚。这一步花不了十分钟却能避免后面“标签全错”的翻车。2.1 压缩包里常见的目录结构与命名规则先解压然后用 tree 看目录。常见的打包方式有两种一种是 images 与 labels 平级另一种是 JPEGImages / Annotations 这种旧 VOC 习惯。命令如下mkdir -p dataset cd dataset unzip ../yolo算法-打哈欠数据集-16300张图像带标签-不打呵欠-打哈欠.zip tree -L 2 -d如果目录层级太深比如解压后还有一个套娃文件夹就先find . -name *.jpg | head -5定位图片的真实位置再想办法把结构拨正。这里有一个隐藏约定YOLO 格式的图片和标签靠文件名关联img_0001.jpg对img_0001.txt多一个字符都不行。我一般会顺手做一个快速统计确认图片数量和标签数量对得上from pathlib import Path img_dir Path(images) lbl_dir Path(labels) imgs sorted(img_dir.glob(*.jpg)) sorted(img_dir.glob(*.png)) txts sorted(lbl_dir.glob(*.txt)) print(fimages: {len(imgs)}, labels: {len(txts)}) img_names {p.stem for p in imgs} lbl_names {p.stem for p in txts} print(missing label:, len(img_names - lbl_names)) print(orphan label:, len(lbl_names - img_names))不要小看 orphan label有标签没图片的问题。很多公开压缩包是有人中途手动删过图的孤儿标签不会让训练报错但会在验证时引入脏数据污染 mAP 统计。我习惯先跑这段脚本把文件名对不齐的统统挑出来再决定是补图还是删标签。2.2 “不打呵欠”和“打哈欠”到底标在哪这两类的含义不是字面那么简单。同样是打哈欠有的数据集把框标在嘴部区域数据集里的“打哈欠”代表嘴巴张开幅度大、脸颊有拉伸有的数据集则干脆标人脸区域把人脸框住然后靠类别区分状态。如果框标在整个人脸上模型学到的是“人脸加张嘴的整体外貌”部署时对距离和角度的要求更低但框的尺寸一致性差两个类别在外观上差异也小训练更难收敛。如果框标在嘴巴上模型学的是嘴部局部纹理对摄像头位置敏感但检测结果更容易结合嘴部宽高比做二次判断也更容易迁移到其他场景。所以拿到这个 zip 后第一件正经事是可视化。我把检查脚本放在下面它按 YOLO txt 的格式画框类别 0 画绿色、类别 1 画红色颜色直观可调import cv2 from pathlib import Path class_names {0: not_yawning, 1: yawning} def show_sample(image_path: Path, label_path: Path): img cv2.imread(str(image_path)) if img is None: print(cannot read, image_path) return h, w img.shape[:2] for line in label_path.read_text().splitlines(): parts line.split() if len(parts) ! 5: continue cid, xc, yc, bw, bh map(float, parts) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) color (0, 255, 0) if int(cid) 0 else (0, 0, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, class_names.get(int(cid), ?), (x1, y1 - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) img cv2.resize(img, (960, 720)) cv2.imshow(sample, img) cv2.waitKey(0) cv2.destroyAllWindows() img_path Path(images/img_0001.jpg) label_path Path(labels/img_0001.txt) show_sample(img_path, label_path)脚本本身没有复杂逻辑但你能从中得出两个关键结论框比例是否符合目标形态以及类别 0 和类别 1 的分配是否合理。若发现框普遍是正方形而且大多落在嘴部位置那这就是一个口部检测任务若框覆盖人头且类别一换再用就要注意误报处理逻辑必须和标注方式对齐。2.3 标签格式与公开数据集的差异数据包里的格式不是唯一标准。YOLO 格式的标签是归一化的class x_center y_center width height而 VOC 的 XML 记录的是绝对像素的 xmin/ymin/xmax/ymaxCCPD 车牌数据集用的是 JSONBDD100K 车辆检测数据集用的是更复杂的 json 标注。同样是检测数据带进来的信息量完全不同。这个 zip 如果本身是 YOLO txt那 labels 目录里的文件很小每张图只有几行纯数字如果打开 txt 发现全是浮点数但第一个数不是 0 或 1说明类别顺序可能和你想的不一样需要回到包内自带的类别清单确认不要凭文件名猜。3. 数据清洗与格式校验训练前必做的三件事目标检测数据集的质量检查与图像分类不同。分类数据集跑得动基本就能训检测数据集的标签哪怕只错一行训练时 loss 会莫名其妙波动推理时又会对着一团空气画框。我每次拿到新数据集都会做三件事扫坏图和空标签、搞清格式并统一转换、分层划分训练验证集。这三步都做完训练才能不受脏数据干扰。3.1 扫描坏图、空标签和越界框YOLO 的 txt 标签是归一化坐标理论上 x_center、y_center、w、h 都应该在 0 到 1 之间。但实际数据集中经常出现两种脏数据一是标注工具导出时坐标越界框的一部分跑到图像外二是某张图片没有对应标签或标签文件是 0 字节。空标签尤其阴险训练时不报错但会造成负样本信息缺失验证时又被当成漏检mAP50 被拉到很低。下面这个脚本一次性处理坏图、空标签、越界框三类问题from PIL import Image from pathlib import Path img_root Path(images) lbl_root Path(labels) broken_imgs [] empty_labels [] bad_boxes [] for img_path in sorted(img_root.glob(*.jpg)) sorted(img_root.glob(*.png)): label_path lbl_root / (img_path.stem .txt) if not label_path.exists() or label_path.stat().st_size 0: empty_labels.append(img_path) continue try: im Image.open(img_path) im.verify() except Exception: broken_imgs.append(img_path) continue w, h im.size for line in label_path.read_text().splitlines(): parts line.split() if len(parts) ! 5: bad_boxes.append((img_path, bad line: line)) continue try: cid, xc, yc, bw, bh map(float, parts) except ValueError: bad_boxes.append((img_path, parse error: line)) continue if not (0.0 xc 1.0 and 0.0 yc 1.0 and 0.0 bw 1.0 and 0.0 bh 1.0): bad_boxes.append((img_path, bbox out of range)) print(broken images:, len(broken_imgs)) print(empty labels:, len(empty_labels)) print(bad boxes:, len(bad_boxes))逻辑说明先检查标签是否存在且非空再用 PIL 的verify()方法快速校验图片完整性最后逐行解析五个字段并判断是否在归一化范围内。越界框的处理我一般不是直接删除而是把 x1、y1、x2、y2 裁剪到图像边界再重新算归一化坐标直接删框会丢失部分有效目标。如果扫描结果里空标签超过总图片数的 5%我就不会直接删图而是考虑重新生成标注。因为大量空标签通常意味着图片是后期从视频抽帧出来的模型会学到“这种场景没目标”推理时误检率会高到没法用。3.2 把 VOC XML 转成 YOLO txt转换脚本与边界坑很多数据集的历史版本会以 VOC XML 形式发布拿到手后还得自己转成 YOLO 格式。常见做法是写一个 XML 解析脚本把 xmin/ymin/xmax/ymax 转成归一化的中心点和宽高。这里最容易踩的坑是XML 文件里filename未必和实际图片同名可能带相对路径size缺失时宽高只能从图片本身读取object里出现你不关心的类别比如“yawning”之外还标了“talking”。处理方法如下import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image class_map {not_yawning: 0, yawning: 1} xml_dir Path(Annotations) txt_dir Path(labels) txt_dir.mkdir(exist_okTrue) for xml_file in sorted(xml_dir.glob(*.xml)): root ET.parse(xml_file).getroot() img_name root.findtext(filename) if not img_name: continue img_path Path(images) / img_name if not img_path.exists(): print(image not found:, img_path) continue w_img, h_img Image.open(img_path).size lines [] for obj in root.findall(object): name obj.findtext(name) if name not in class_map: continue bndbox obj.find(bndbox) x1 float(bndbox.findtext(xmin)) y1 float(bndbox.findtext(ymin)) x2 float(bndbox.findtext(xmax)) y2 float(bndbox.findtext(ymax)) x1 max(0, min(x1, w_img - 1)) y1 max(0, min(y1, h_img - 1)) x2 max(0, min(x2, w_img - 1)) y2 max(0, min(y2, h_img - 1)) if x2 x1 or y2 y1: continue xc (x1 x2) / 2 / w_img yc (y1 y2) / 2 / h_img bw (x2 - x1) / w_img bh (y2 - y1) / h_img lines.append(f{class_map[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) if lines: (txt_dir / (xml_file.stem .txt)).write_text(\n.join(lines))说明几点findtext(size/width)在 XML 里缺项时返回 None所以我直接从原图读取宽高避免除零bndbox的坐标做了越界裁剪防止 xmax 超出图像宽度类别映射用字典class_map只要遇到未注册类别就跳过防止把背景标进训练集。很多人问“xml格式文件没有标签怎么办”最典型的场景就是这个转换脚本跑完后 labels 目录里只有几千个 txt这时要回去检查 XML 里 object 的数量有的框标注了但 class 拼写和 map 不一致被静默跳过了。转换完成后再跑一次 2.1 的统计脚本确认图片与标签一一对应再进入下一步。3.3 划分 train/val 并检查类别分布16300 张图常见做法是 8:2 划分。但直接shuf随机分可能造成打哈欠样本集中到训练集另一个类在验证集上近乎没有。为了避免这种尴尬我一般会按类别做分层划分保证训练集和验证集里“不打呵欠”和“打哈欠”的占比都接近全量占比。下面这个 python 脚本只是按文件名照搬目录不移动原文件比较安全import random from pathlib import Path random.seed(0) img_root Path(images) lbl_root Path(labels) train_ratio 0.8 val_images, train_images [], [] for img_path in img_root.glob(*.jpg): label_path lbl_root / (img_path.stem .txt) if not label_path.exists(): continue if random.random() train_ratio: train_images.append(img_path) else: val_images.append(img_path) train_root Path(train) val_root Path(val) train_root.mkdir(exist_okTrue) val_root.mkdir(exist_okTrue) for split, split_images in [(train, train_images), (val, val_images)]: out_img Path(split) / images out_lbl Path(split) / labels out_img.mkdir(parentsTrue, exist_okTrue) out_lbl.mkdir(parentsTrue, exist_okTrue) for img_path in split_images: out_img.joinpath(img_path.name).symlink_to(img_path.resolve()) lbl_path lbl_root / (img_path.stem .txt) out_lbl.joinpath(lbl_path.name).symlink_to(lbl_path.resolve())我特意用了符号链接而不是复制因为 16300 张图复制一份会占不少磁盘训练时读 link 和读原文件没有性能差异。划分完后要统计一个分类表确认两个 split 的样本是否均衡。如果发现 val 里打哈欠只有几十张说明随机种子选得不好或者原数据集本身不平衡。这时候不要硬调 split而是要在训练时用类别权重缓解。4. 用 YOLOv8 在本地跑通打哈欠检测最小复现配置数据整理完下一步就是训练。我选择 YOLOv8 作为基准模型因为这个数据集的场景相对简单两个类别、目标不会太小YOLOv8 的默认配置基本能直接落地不管是本机显卡还是服务器都能跑。这章所有命令都基于 ultralytics 官方仓库的 CLI不需要改源码。4.1 使用 PyCharm 安装并使用 YOLO环境搭建如果你是在 Windows 上做实验最顺手的方式是用 PyCharm 建一个专用虚拟环境。先创建一个 conda 环境再在 PyCharm 的 Settings 里把它指定为项目解释器后面所有 pip 包都装进这个环境不会污染系统 Pythonconda create -n yolo python3.10 -y conda activate yolo pip install ultralytics pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118CPU 版本的 torch 也能跑但 640 分辨率、batch 32 在 CPU 上会慢到让人怀疑人生。如果你的机器没有 NVIDIA 显卡建议把 batch 降到 8、模型换成 yolov8n用 CPU 也能训练完只是时间要按小时算。PyCharm 安装 YOLO 的常见坑是解释器没切换成功导致终端里yolo命令说找不到。这时去 Settings - Project - Python Interpreter 看路径是不是 conda 环境的 python.exe而不是默认的 python 3.10。4.2 data.yaml 与目录组织让 ultralytics 找到数据ultralytics 的训练入口不关心图片在哪个绝对路径只要求 data.yaml 里写清楚。我习惯把数据集组织成 train/images、train/labels、val/images、val/labels 四个目录这是 YOLOv8 默认支持的布局。data.yaml 放在项目根目录内容如下path: /home/user/datasets/yawning train: train/images val: val/images names: 0: not_yawning 1: yawning注意path必须是绝对路径如果留空或者只写相对路径YOLOv8 在 Windows 上经常因为盘符问题找不到图片。names里的顺序必须和 txt 标注里的第一个数字一致这段顺序错了训练不会报错但推理时打哈欠和正常状态会整个反过来这种错误最坑因为指标还会显示很高。另外不要在 data.yaml 里写nc字段ultralytics 会根据 names 的长度自动推断手动写反而可能因为数字不一致引发启动报错。4.3 训练命令与 yolo 损失函数强相关的三个参数训练命令本身非常短yolo detect train \ modelyolov8n.pt \ datadatanew.yaml \ epochs100 \ imgsz640 \ batch32 \ lr00.01 \ patience15 \ projectruns/yawnmodelyolov8n.pt是官方预训练权重从 COCO 上迁移过来类别数会自动改造最后一层。选 n 版本而不是 s 或 m是因为这个数据集只有两个类别模型容量不需要太大n 版本在本机显卡上能跑到 60fps 以上部署成本低。三个和 yolo 损失函数强相关的参数分别是 imgsz、batch、lr0。imgsz 决定图像缩放尺寸。打哈欠检测的最终目标是嘴部区域如果原始图里人脸只占很小一块imgsz640 可能让嘴部丢细节。这时候可以试试 480训练更快嘴部像素更集中反过来如果摄像头离人很近、嘴部已经占了图像很大比例就维持 640 甚至提高到 768不要盲目加大。batch 受显存限制。12GB 显存跑 yolov8n、imgsz640batch 一般能开到 32 或 64。batch 太小时 BN 统计不稳定loss 曲线会像锯齿batch 太大又容易在数据集不够大时过拟合。我的经验是 batch 选能塞进显存的最大值然后用cos_lrTrue把学习率衰减拉平这样收敛比较顺。lr0 是初始学习率默认 0.01 对 yolov8n 适用。如果你用了较大的 batch要按比例调高 lr0例如 batch 翻倍到 64lr0 可以试 0.02否则收敛偏慢。训练完成后yolo 损失函数对应的三条曲线 box_loss、cls_loss、dfl_loss 都会存在runs/yawn/results.csv里box_loss 起伏大说明定位没学好cls_loss 降不下去说明两个类别区分度不够不要只看总 loss。4.4 训练完先看这几张图别急着看 mAP训练结束后大多数人第一件事是打开 last.pt 测图。但验收集里最重要的文件不是模型而是runs/yawn/val_batch0_pred.jpg。这张图把第一组验证图的预测框直接画出来了你可以肉眼确认打哈欠框是否落在嘴部而不是脸颊不打呵欠时是否出现了大量冗余框漏检主要是小目标还是遮挡目标。再看混淆矩阵confusion_matrix.png如果“打哈欠”那一行的横坐标大量落在“不打呵欠”上说明两个类别在训练集里就长得像单纯换模型结构不一定有用要回数据层面做难例挖掘。权重的选择也有讲究best.pt是验证集 mAP 最高的权重last.pt是最后一个 epoch 的权重。早期训练阶段两者差距很大但如果训练后期发生过拟合best.pt 并不一定比 last.pt 更可靠。我一般用 val 集上 mAP50-95 和混淆矩阵一起判断不单看 mAP50。5. 打哈欠检测最容易翻车的地方与排查这章写训练和部署中容易踩的坑。每一条都是我实际做疲劳检测类项目时真实处理过的问题按“现象 → 原因 → 解决”的顺序写你遇到类似情况可以直接照方抓药。5.1 现象张嘴说话被判成打哈欠模型在测试视频里对着一路说话的人疯狂报警张嘴幅度大一点就触发。原因很直白两个类的差异本来就不在外面轮廓而在嘴部张开的持续时间和变化过程。纯静态检测模型只看到“嘴张开”和“嘴闭合”张嘴说话、唱歌、打哈欠的前半段都会落入正样本。解决思路分两层数据层面把标注里明显是说话、大笑的图片单独挖出来修正或剔除避免模型学会“凡张嘴即哈欠”推理层面不要对单帧结果做最终判决而是连续统计 5 帧中嘴部张开宽高比的变化。打哈欠时嘴部从闭合到张开再到闭合通常是平滑弧线说话时的嘴巴状态在帧间跳变明显用一个长度为 5 的滑动窗口对检测置信度取平均再把连续高置信度的帧数做阈值判断误报能压下去一截。5.2 现象白天测着没问题晚上误报成灾模型在室内光下 mAP 很好看一到夜间或逆光场景就满屏乱框。原因就是训练数据大多数是自然光图像模型学到的是光照充足下的嘴部纹理对低照度不敏感。夜间场景下脸部阴影重很多正常闭嘴的画面被模型误判成张着嘴。解决方法是训练时加大数据增强尤其是亮度饱和度的随机扰动。YOLOv8 的增强参数里hsv_h、hsv_s、hsv_v三个值控制色相、饱和度、明度扰动默认值对低照度场景不够我一般会把hsv_v从默认的 0.4 提到 0.6再加degrees10让模型适应侧脸角度。如果手头有几段夜间视频抽帧出来后用 best.pt 做伪标签人工修正后补进训练集效果比单纯调参数更明显。5.3 现象loss 收敛正常但 mAP50 只有 0.6loss 曲线下降得漂亮验证集 mAP 却上不去。原因最常见的是标注质量不均比如一部分“打哈欠”的框明显偏大把下巴和下嘴唇全包进去了另一部分框又只框到嘴缝。模型在拟合两种不一致的目标导致定位不稳定。处理办法是重新审视 2.2 的可视化结果把框偏大或偏小的图片统计出来用脚本统一缩放。如果整体标注风格不一致但每张图内部自洽可以考虑把box_loss的权重调高让模型更重视定位精度而不是分类概率。另外也要看验证集是否太小如果 val 只有两三百张mAP 波动本身就大这时候应该做 5 折交叉验证而不是单次随机划分。5.4 现象训练中途 loss 突然冲高之后再也降不回来训练到第 20 个 epoch 左右loss 突然从 1.2 跳到 5 以上。原因大概率是数据增强把某些标签“洗”出了原始位置比如开启了degrees45的大角度旋转嘴部框转出图像边界或者mosaic1.0把四张图拼在一起某个标签的归一化坐标计算错位。增强本身没问题但超参太激进就会制造错位标注模型被迫在噪声上拟合。处理方法是先看results.png里的增强样本图YOLOv8 会把增强后的训练图存下来。如果发现异常把 degrees 降到 5mosaic 从 1.0 降到 0.5同时打开close_mosaic10让最后 10 个 epoch 关闭 mosaic保证模型在接近真实分布的数据上结束训练。数据增强翻车和模型结构无关属于纯超参问题不要因此回去重做数据。5.5 现象模型在 Jetson 上只有 10fps没法实时这条在真机部署时最常见。原因有两个一是用了 yolov8l 或 x 版本模型参数量太大二是输入分辨率设置过高边缘设备上 preprocess 和 postprocess 也占算力。解决方法是先换模型规格。打哈欠检测是二分类任务yolov8n 在边缘设备上的推理速度接近 x 版本的十倍。如果目标区域只有嘴部可以先把人脸检测出来把嘴部区域裁剪后单独送进模型输入尺寸从 640 降到 224帧率会显著提升。还有一个容易被忽略的点YOLOv8 的推理结果要自己过滤置信度低于 0.25 的框如果部署代码里没过滤后处理耗时和误检率都会上升。6. 让打哈欠检测真正上线的三个进阶做法如果验证集 mAP 能稳定在 0.85 以上接下来该做的是让模型更贴合真实业务而不是继续刷那零点几个点。我试过最有效的三个方向嘴部关键点辅助、时序判定、轻量化与分割联动。第一个做法是把检测框升级成嘴部关键点。检测模型只能告诉你“这里有个张开的嘴”但嘴部关键点可以告诉你上嘴唇和下嘴唇的距离变化。用 Mediapipe 从人脸框里取左右嘴角和上下嘴唇的 6 个关键点计算嘴巴纵横比 MARMouth Aspect Ratio本质上就是嘴部张开程度的量化指标。检测框负责锁定位置MAR 负责判断程度两者结合后“微张嘴”和“打哈欠”的区分会清晰很多比单纯置信度阈值可靠。第二个做法是加时序窗口。打哈欠是一个持续过程单帧静止图本身不具备足够信息。我通常的做法是在推理端维护一个 3 秒的滑动窗口统计窗口内 MAR 超过阈值的帧占比。连续多帧占比超过 60% 才报警单帧触发则忽略。这个逻辑只有十几行代码但能把说话、吃东西、咳嗽造成的偶发误报降低大半。注意阈值不要按静态图的分布拍脑袋定要对着实际视频的标注数据调。第三个做法是端侧部署与性能兜底。如果要在 Jetson 或嵌入式设备上跑先做模型剪枝和量化。YOLOv8n 本身已经很小再上 TensorRT FP16输入 640 时跑 30fps 以上并不难。另外如果嘴部分割也需要可以用 yolo 实例分割把嘴部区域分割出来用掩码面积变化作为打哈欠判定的辅助特征。这种方法在侧脸、遮挡场景下比矩形框更稳定但标注成本和训练成本都会上升适合摄像头角度固定、对精度有硬性要求的项目。我早期做驾驶疲劳检测时以为把“打哈欠”的检测精度刷上去就能直接交付结果在实车测试中因为说话误报、暗光漏检来回返工最后才发现问题从来不在单帧模型而在数据和后处理。把时序判定和后处理当成系统的一部分去设计比反复调参换模型更能节省时间。这个打哈欠数据集本身就是很好的起点按前三章的流程把数据吃透再用这章的方法做边界优化你离一个可交付的疲劳检测模块就不远了。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进