ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于CNN的疲劳驾驶识别系统:从数据处理到PERCLOS实时报警

基于CNN的疲劳驾驶识别系统:从数据处理到PERCLOS实时报警 简介一份基于卷积神经网络CNN的疲劳驾驶识别检测完整毕业设计项目面向计算机相关专业正在准备毕业设计、课程设计或期末大作业的学生也适合需要项目实战练习的开发者。项目经导师指导并获评98分覆盖从模型训练、评估到摄像头/视频实时检测的完整流程包含SSD网络结构、损失函数、数据增强、配置与工具模块等典型代码模块。压缩包共37个文件以Python源码16个py、预训练权重与模型文件3个pth、数据集zip、测试图片jpg、说明文档txt及训练日志log为主整体约500.41MB结构清晰便于二次开发。目前已有128人学习下载可作为毕业设计答辩、算法复现或项目实战的参考蓝本帮助快速理解卷积神经网络在疲劳驾驶检测场景中的落地实现。1. 基于CNN的疲劳驾驶识别从跑通模型到系统闭环这条路有多长做疲劳驾驶识别检测系统的毕业设计时最容易出现的问题是找一个预训练模型跑通闭眼分类就以为任务完成。但基于卷积神经网络CNN的疲劳驾驶识别检测系统真正卡住人的从来不是那几层卷积而是数据是否干净、疲劳判定阈值是否合理、系统接到摄像头之后能不能在连续视频流里给出可靠报警。这篇文章从我实际完成这类项目的角度出发把数据集划分、模型训练、疲劳判定到常见故障排查的整条链路拆开讲。适合正在做Python毕设、准备把算法从笔记本迁到实时摄像头的同学读完后你能照着复现也能在答辩时讲清楚每个参数背后的取舍。2. 数据集准备与预处理先让样本会说话再让 CNN 学疲劳2.1 疲劳样本从哪来公开数据集与自采补全的取舍拿到完整毕设项目之后第一件事不是运行train.py而是把 dataset 目录拆开看一眼。绝大多数疲劳驾驶识别项目的数据集都会按场景或状态分目录常见的有睁眼、闭眼、打哈欠三类有的还会加入正常驾驶、低头、打电话等干扰类别。先检查每个类别的样本数、图片尺寸和文件格式把模糊帧、重复帧、标签错位的图片清掉这件事比调参更影响最终效果。公开数据集最大的问题是拍摄场景单一。如果整套数据都来自同一位实验者在同一个座位上录制CNN 很容易学到“这个人的脸长什么样”而不是“眼睛是否闭上”。我在实际项目里见过这样的情况训练集准确率超过 97%拿自己手机摄像头一测频繁误报因为肤色、光照、头部角度全部变了。所以建议在已有公开数据集的基础上至少用手机录制十分钟自己的面部状态每隔两帧抽一张补充到对应目录里。自采样本不需要很多每个类别增加 200 到 300 张就能让跨摄像头表现明显改善。类别设计要克制。疲劳驾驶识别最常用的三类是 awake、closed_eye、yawn目录结构如下| 类别标签 | 目录名 | 典型场景 | | awake | open_eye | 正常睁眼、驾驶 | | closed_eye | closed_eye | 闭眼或半闭眼 | | yawn | yawn | 打哈欠、张嘴 |也可以改成四类比如加入“低头看手机”但类别越碎需要的样本量越大。毕设答辩时三类刚好对应三个可解释语义清醒、瞌睡、哈欠。闭眼类别的可靠性优先级最高因为后面计算 PERCLOS 指标依赖的是闭眼帧占比闭眼漏标会直接让疲劳判定失真。2.2 训练集划分两种脚本与数据泄露的边界很多开源项目的训练脚本简单地把所有图片打乱后按比例切分。如果数据集本身就是一堆独立图片这种切法没问题但如果图片是从连续视频帧里抽出来的同一段视频的相邻帧极其相似随机切分会让验证集里出现训练集的“近亲”导致验证指标虚高。换到新视频上准确率立刻下跌。这种问题叫数据泄露它的典型表现是训练和验证准确率都在 97% 以上真实场景却一塌糊涂。先看一个按单帧随机切分的常见脚本理解它的问题在哪import os, random, shutil from glob import glob def split_by_frame(source_root, target_root, train_ratio0.7, val_ratio0.15, seed42): random.seed(seed) for label in os.listdir(source_root): label_path os.path.join(source_root, label) if not os.path.isdir(label_path): continue images glob(os.path.join(label_path, *.jpg)) random.shuffle(images) train_end int(len(images) * train_ratio) val_end int(len(images) * (train_ratio val_ratio)) for split_name, imgs in zip([train, val, test], [images[:train_end], images[train_end:val_end], images[val_end:]]): out_dir os.path.join(target_root, split_name, label) os.makedirs(out_dir, exist_okTrue) for img in imgs: shutil.copy(img, os.path.join(out_dir, os.path.basename(img)))逻辑说明先固定seed让打乱顺序可复现然后按train_ratio和val_ratio切出训练、验证、测试三份。参数0.7/0.15/0.15是常用比例验证集太少时闭眼类别的少量样本可能没有被覆盖到调阈值时看到的曲线会比较毛糙。这个脚本的缺陷在于images列表来自某个目录下的所有图片没有记录视频来源。如果你只有单帧图片目录它能用如果源数据来自视频请换用按视频片段划分的方式。更可靠的做法是用GroupShuffleSplit让同一次连续拍摄的帧全部留在同一边from sklearn.model_selection import GroupShuffleSplit import os all_images [] clip_ids [] for root, _, files in os.walk(source_root): for f in files: if f.endswith(.jpg): all_images.append(os.path.join(root, f)) clip_ids.append(os.path.basename(os.path.dirname(root))) gss GroupShuffleSplit(n_splits1, train_size0.7, random_state42) train_idx, val_idx next(gss.split(all_images, groupsclip_ids))逻辑说明clip_ids里存的是每个样本所属的视频片段或拍摄批次 IDGroupShuffleSplit按组划分保证同一组的帧不会同时出现在训练集和验证集里。答辩时如果能主动说出“我避免了数据泄露”作用比列一堆模型指标更直接因为这体现的是工程思维。2.3 预处理与数据增强分辨率、归一化与闭眼样本过采样CNN 输入尺寸直接决定训练速度和实时推理帧率。疲劳驾驶识别是细粒度图像分类不需要特别大的分辨率。我一般把输入统一到 112x112如果电脑性能一般降到 64x64 也能跑只是闭眼细节的区分度会下降。不要盲目使用 ImageNet 的 224x224除非你有 GPU 且不在乎实时性。预处理代码用 PyTorch 的transforms写from torchvision import transforms PREPROCESS transforms.Compose([ transforms.Resize((112, 112)), transforms.Grayscale(num_output_channels3), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])逻辑说明Resize把任意尺寸的摄像头帧压成统一尺寸Grayscale(num_output_channels3)对灰度摄像头友好同时保留三通道结构兼容预训练模型ToTensor把 HWC 排列的像素数组转成 CHW 张量并归一化到 0 到 1最后的Normalize使用 ImageNet 的统计量。参数说明如果模型是从零训练mean 和 std 可以改成自己数据集上算出的像素均值迁移学习则继续沿用 ImageNet 统计值。数据增强不宜过度。疲劳检测里闭眼是一种局部细节状态翻转和旋转可以增强姿态鲁棒性但旋转角度过大会让模型学习到本不存在的倒立人脸。亮度扰动用来模拟逆光、隧道光照但也可能把闭眼的阴影抹掉。我常驻的三项增强如下AUGMENT transforms.Compose([ transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees8), transforms.ColorJitter(brightness0.2, contrast0.2), ])逻辑说明水平翻转对驾驶场景合理因为左右眼状态语义一致8 度旋动能覆盖头部轻微倾斜不会制造异常姿态亮度 0.2 的扰动足够模拟不同光线又不至于让闭眼特征消失。如果闭眼类样本明显偏少除了在Loss上加权重也可以在增强时对闭眼样本多执行一轮随机模糊和随机噪声让网络看到更多闭眼的变体。复制粘贴闭眼帧没有新增信息这条不算增强。3. CNN 模型搭建与训练用 PyTorch 把分类器做扎实3.1 自建小 CNN 还是迁移学习毕设场景的选型逻辑每次看到“基于 CNN”的课题第一反应是自己写卷积层。但疲劳驾驶识别的样本量普遍在几千到几万张从零训练一个深层网络容易欠拟合训练时间却一点不短。在一线做这个方向最常用的路数是迁移学习加载 ImageNet 预训练权重只替换最后的全连接层。预训练网络已经学会了边缘、纹理、形状这些通用特征迁移到眼睛开合、嘴部状态识别时收敛快精度也更有保障。如果导师明确要求你展示自行搭建 CNN 的能力那至少要做成一个三卷积块以上的结构。这里给出一个轻量级网络能跑但不要指望它超过预训练 ResNetimport torch.nn as nn class DrowsyCNN(nn.Module): def __init__(self, num_classes3): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 14 * 14, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes), ) def forward(self, x): return self.classifier(self.features(x))逻辑说明输入 112x112三次池化后空间尺寸变成 14x14所以全连接层输入维度是128 * 14 * 14。每层卷积后接 BatchNorm2d 和 ReLUBatchNorm 能让训练中各层输入的分布更平稳Dropout 则控制过拟合。如果是毕设主线更推荐直接用 ResNet18。替换最后一层分类头from torchvision import models import torch.nn as nn model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 3)逻辑说明预训练模型的model.fc原本输出 1000 类in_features是最后一个全连接层的输入维度 512替换成输出 3 类的线性层即可。训练时前面的特征层负责提取通用特征fc分类头负责当前任务的类别决策。参数说明ResNet18 在普通 CPU 笔记本上做实时推理已经偏重如果预算有限可以换 MobileNetV2。MobileNetV2 也是 ImageNet 预训练模型结构里大量使用深度可分离卷积参数量只有 ResNet18 的约三分之一摄像头推理帧率更高。3.2 训练脚本与超参数让 loss 稳步下降训练循环不要一次写复杂。先跑通最简版本再逐步加验证、加权重。优化器我一般用 AdamW它相比 SGD 对学习率不那么敏感在分类任务上通常几分钟就能让 loss 走到合理区间。学习率分配是迁移学习的关键分类头用 1e-3backbone 特征层用 1e-4防止预训练权重被大幅破坏。训练循环最小可行版本如下import torch from torch.optim import AdamW device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) optimizer AdamW([ {params: model.layer4.parameters(), lr: 1e-4}, {params: model.fc.parameters(), lr: 1e-3}, ], weight_decay1e-4) criterion nn.CrossEntropyLoss() for epoch in range(30): model.train() for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() # 每个 epoch 结束后的验证逻辑 model.eval() total_correct 0 total_num 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) pred torch.argmax(outputs, dim1) total_correct (pred labels).sum().item() total_num labels.size(0) print(fepoch {epoch1} val acc: {total_correct / total_num:.4f})逻辑说明optimizer里给不同参数组配不同学习率layer4是 ResNet18 最后一个残差层保留更多高层特征学习率压低fc是全新分类头学习率放大。weight_decay1e-4是 L2 正则能减轻过拟合。参数说明train_loader需要设置shuffleTrue否则每个 epoch 的数据顺序完全一样优化过程会来回震荡。验证阶段必须调用model.eval()关闭 Dropout 和 BatchNorm 的统计更新并包在torch.no_grad()里避免累积梯度。训练过程中观察 val acc 的曲线如果训练 loss 持续下降但 val acc 不涨说明过拟合回到数据增强和 Dropout 上找问题如果训练和验证 acc 都不涨先检查数据预处理和标签顺序是否匹配再怀疑学习率过高或过低。3.3 类别不平衡CrossEntropyLoss 的 weight 参数疲劳驾驶数据集中睁眼帧往往远多于闭眼帧。正常开车时眼睛大多数时间睁开采集到的视频帧自然也以睁眼为主。如果睁眼:闭眼 8:1模型只要全输出“清醒”就能拿到 88% 准确率但实际一点用都没有。这时候需要用类别权重修正损失函数。计算权重推荐直接用sklearnfrom sklearn.utils.class_weight import compute_class_weight import numpy as np import torch labels np.array([sample[label] for sample in train_samples]) classes np.array([0, 1, 2]) weights compute_class_weight(class_weightbalanced, classesclasses, ylabels) class_weights torch.tensor(weights, dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)逻辑说明compute_class_weight(balanced, ...)返回的权重遵循反比逻辑样本多的类别权重低样本少的类别权重高。把权重传给CrossEntropyLoss之后闭眼类的误判会产生更大 loss模型被迫更重视少数类。参数说明如果闭眼样本特别极端比如只有睁眼的十分之一单一权重可能过于激进导致训练初期不稳。可以把权重做开方平滑sqrt(class_weights)既保留补偿又不会让模型对闭眼过拟合。在实现疲劳判定时还可以把闭眼判定阈值轻微放低宁可多报一次疲劳也不要让一次真正闭眼被漏过去。4. 疲劳判定逻辑与系统集成从单帧概率变成报警动作4.1 PERCLOS 指标标准化的疲劳阈值从哪里来CNN 分类器输出的是某一帧图像属于闭眼的概率但人本来就会正常眨眼偶尔一帧闭眼不能判定疲劳。工程化做法是把连续若干帧的结果汇总成一个时间窗口指标其中最经典的是 PERCLOS也就是眼睛闭合时间占比。它的计算公式是PERCLOS 闭眼帧数 / 窗口总帧数 × 100%常见的阈值设置在 0.4 左右意思是 30 秒窗口内闭眼帧占比超过 40% 就触发报警。这个值不是死参数。高速公路场景注意力要求高可以调到 0.35城市低速场景正常眨眼更频繁阈值可以放宽到 0.5。关键是阈值要留出调整接口而不是写死在代码里方便你在不同测试视频上做对比。窗口长度直接影响报警延迟和误判率。窗口太短一次几秒的眨眼会被当成疲劳窗口太长真疲劳了要过很久才报警。推荐初始值window_size90配合 30 FPS 摄像头正好三秒窗口后续按实际视频帧率调整。4.2 摄像头实时检测把模型接进 cv2 循环系统集成阶段最核心的代码是一个带滑动窗口状态的检测循环import cv2 import torch import numpy as np from collections import deque from PIL import Image from torchvision import transforms def run_detector(model, device, video_source0, window_size90, alarm_threshold0.4, input_size112): preprocess transforms.Compose([ transforms.Resize((input_size, input_size)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) cap cv2.VideoCapture(video_source) recent deque(maxlenwindow_size) model model.to(device).eval() with torch.no_grad(): while True: ret, frame cap.read() if not ret: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) pil Image.fromarray(rgb) tensor preprocess(pil).unsqueeze(0).to(device) prob torch.softmax(model(tensor), dim1) label int(torch.argmax(prob, dim1)) # 假设类别 0 是闭眼把闭眼帧记作疲劳信号 recent.append(1 if label 0 else 0) perclos float(np.mean(recent)) if perclos alarm_threshold: cv2.putText(frame, DROWSY PERCLOS%.2f % perclos, (30, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.imshow(drowsy detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明deque(maxlenwindow_size)是固定长度队列新帧进来时最旧一帧自动弹出便于计算滑动窗口内的闭眼占比。torch.softmax把网络输出转成概率argmax得到当前帧的类别编号。闭眼帧用1填入队列开眼帧用0队列均值就是 PERCLOS。参数说明video_source0表示默认摄像头如果测试视频文件传视频路径即可。input_size要和训练预处理保持一致否则模型看到的图和训练时差异很大。alarm_threshold建议通过一组真实测试视频录制后标定不要在理想环境下随便写一个数。当前代码直接对整帧做分类没有做人脸检测。实际场景中如果摄像头在方向盘上方画面中包含前挡风玻璃、中控台等大量背景整帧分类会受影响。更可靠的做法是先用 OpenCV 的CascadeClassifier或mediapipe截取人脸区域把裁剪后的 face ROI 送到 CNN 分类器这样背景干扰会小很多。这个改动对精度提升非常明显。4.3 用 EAR 做自检CNN 概率与几何特征双重验证如果希望系统具备可解释性可以在 CNN 之外再加一路几何特征最常见的是眼睑纵横比 EAR。EAR 通过人脸关键点计算眼睛的垂直距离与水平距离比值睁眼时大约在 0.25 到 0.33 之间闭眼时接近 0.1。EAR 的计算公式是EAR (||P2-P6|| ||P3-P5||) / (2 * ||P1-P4||)其中 P1 到 P6 是单只眼睛从外眼角到内眼角按顺序排列的六个关键点。EAR 和 CNN 概率可以做成并联判定只有两个通道都判定闭眼时才把当前帧计为闭眼帧。这样能有效规避阴影遮挡造成的误判也能减少把打哈欠时眯眼当成闭眼的情况。对于毕设EAR 不是必需的但它能帮助你在答辩时讲清楚“为什么不是单模型走天下”。常见做法是把 EAR 作为验证工具在离线数据集上统计 CNN 判断和 EAR 判断的不一致率找出哪些视频帧让两个通道产生分歧再针对性地补充训练样本。这种分析过程本身也是论文里很容易写的一节。5. 排查疲劳识别系统最常见的五个坑现象、原因与解决5.1 现象准确率高但摄像头预览卡成 PPT训练集准确率很高一打开摄像头实时预览画面肉眼可见地卡顿推理延迟超过 300 毫秒。原因绝大多数出问题项目都用了 224x224 输入加上 ResNet34 或更大网络还在每一帧上都完整推理没有跳帧。笔记本电脑 CPU 推理一张 224x224 图片耗时往往超过 100 毫秒加上画面显示开销帧率直接掉到 5 FPS 以下。解决把输入尺寸降到 112x112模型换成 ResNet18 或 MobileNetV2。依然卡就做跳帧每两帧只推理一次另一帧沿用上一次结果。人眼对 15 FPS 以上的画面感知已经接近连续跳帧不会影响最终报警体验。5.2 现象验证集准确率高换一个人就失效在自己录制的视频上表现很好换一个同学坐到摄像头前系统频繁误报。不是模型坏了而是模型过拟合到了特定受试者的外貌特征。原因数据集采集自单一受试者模型学的不是“闭眼”这个语义而是“这个人的眼睛颜色、脸型、眼镜框”等信息。肤色较深或戴眼镜的人一出现闭眼与不闭眼的分界线就错乱。解决按受试者划分训练与测试集确保同一位受试者的所有帧只出现在训练或测试单侧。其次扩充自采样本覆盖不同肤色、不同眼镜佩戴情况。如果没有条件采多人数据就在增强中加入随机遮挡和局部模糊模拟墨镜、口罩对局部特征的遮盖。5.3 现象打哈欠总是被误判成闭眼一段视频里人物打哈欠时眼睛明明是睁开的系统却持续报警疲劳。原因哈欠类样本太少CNN 把嘴部大幅张开、面部肌肉收缩的纹理错误关联到了闭眼类也可能是哈欠帧里本来就伴随半闭眼标签把这类帧归到了 yawn但模型没学到区分两者。解决打开样本目录检查 yawn 和 closed_eye 两类图片是否互相串了。把嘴角变形但眼睛睁开的图片从 closed_eye 里清掉。如果只有两类需要为哈欠单独立类。模型层面可以用 EAR 做校验CNN 判定闭眼时检查 EAR 是否也低于阈值双通道一致才计入疲劳帧。5.4 现象loss 正常但验证指标震荡最后全输出一类训练过程中 loss 每个 epoch 都在下降但验证准确率忽高忽低某次验证后突然绝大部分预测都是同一个类别模型变成“复读机”。原因排查时先看 DataLoader 是不是忘了shuffleTrue数据顺序固定会让优化过程周期性循环。另一个常见原因是学习率过大AdamW 下学习率超过 3e-3 就会在后期震荡权重被推出最优点。解决固定随机种子给训练加载器补上shuffleTrue学习率降到 1e-4 到 1e-3 的合理区间。还要确认验证时调用model.eval()否则 BatchNorm 使用 batch 统计量预测结果会不稳定。5.5 现象无 GPU 电脑部署时模型加载报错项目在自己的 GPU 机器上正常拷到一台没有 NVIDIA 显卡的电脑上加载权重时报错提示 CUDA 张量无法加载到 CPU。原因训练时保存的state_dict里张量绑定在cuda:0设备上部署机没有 GPU直接torch.load就崩了。解决加载权重时强制映射到 CPUstate_dict torch.load(model.pth, map_locationcpu) model.load_state_dict(state_dict)如果你想转成 ONNX 在 OpenVINO 或 TensorRT 上部署需要固定输入尺寸并提前把model.eval()设好再导出。给答辩演示准备一个 CPU 可跑版本永远比临时找 GPU 要安全。6. 最后一步用混淆矩阵和跨受试者验证证明你的系统真的可用6.1 混淆矩阵优先看漏报而不是只看准确率疲劳驾驶系统的可用性不能只看总体准确率。闭眼是少数类总体准确率很容易被大量清醒帧撑高。我最关心的指标是混淆矩阵里的 FN也就是把闭眼判成清醒的帧数。漏掉一次真闭眼意味着报警信号完全缺失这对驾驶安全系统是不可接受的。用 sklearn 输出混淆矩阵from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt y_true [] # 真实标签 y_pred [] # 模型预测标签 cm confusion_matrix(y_true, y_pred) disp ConfusionMatrixDisplay(confusion_matrixcm, display_labels[awake, closed_eye, yawn]) disp.plot() plt.show()逻辑说明混淆矩阵的行是真实类别列是预测类别。对角线是正确判断非对角线是错误。看 closed_eye 这一行落在 awake 列的数量就是漏报看 awake 行落在 closed_eye 列的数量就是误报。疲劳检测系统可以先想办法把漏报压到最低再通过阈值调回可接受的误报范围。6.2 跨受试者验证的简单协议如果项目里有多位受试者的录制数据做一次跨受试者验证会让结论更有说服力。协议很简单训练集中不包含受试者 A 的任何帧用剩余受试者数据训练测试集是受试者 A 的全部视频帧之后轮换让每位受试者都做一次测试对象。最后统计每位受试者的准确率和漏报率如果某位受试者的表现与训练集中其他人差异很大说明模型存在明显的身份泄露。这个协议不需要新写模型只是改一改数据划分逻辑。很多毕设论文只给一个全局准确率答辩老师一问“测试集里有没有训练数据中的人”就接不上了。能把跨受试者验证讲清楚是让项目从“跑通”走向“可信”的分水岭。6.3 一个固定下来的习惯我在做这类项目时有一个固定习惯每次实验前固定随机种子并把数据划分结果存成索引文件记录是第几次划分。曾经吃过一次亏调了两天损失函数精度没变化最后发现是数据增强顺序在每次运行时不一样模型看到的样本分布一直在变。从那以后所有预处理、增强、划分操作都做成可复现流程任何一次实验结果都能回溯到当时的参数组合。做疲劳驾驶识别这项毕设真正的价值不只是“用 CNN 分出了闭眼”而是你愿意把每一个阈值、每一个样本、每一处报错都当成可解释的工程问题去处理。希望这几段经验能帮你少走一些弯路。本文还有配套的精品资源点击获取
返回列表