YOLOv8驾驶行为检测全流程:数据标注、训练部署与行为判定
简介YOLOv8驾驶行为检测项目代码提供了一套从数据标注到模型部署的完整解决方案适合人工智能相关专业学生及希望落地目标检测应用的开发者。系统聚焦闭眼、抽烟、打电话三类危险行为覆盖环境配置、数据准备、模型训练、性能评估与推理实现等全流程支持图片、视频、实时摄像头三种输入方式其中训练集包含5215张图片、验证集1303张均按YOLO格式标注。资源包共34个文件大小仅34KB9个Python脚本承担训练、检测、GUI展示等核心功能2个yaml文件用于数据与参数配置9张jpg图像作为示例另有txt、markdown文档说明项目结构和环境要求。目前已有125人学习或下载。整体代码结构清晰模块划分明确可直接运行复现流程便于二次扩展自定义行为类别适合毕业设计或工程实践参考。1. YOLOv8驾驶行为检测先定检测目标再谈改网络把YOLOv8驾驶行为检测当成一个普通的YOLO目标检测项目来做大概率会在上线时翻车。原因很简单这套系统的输出不是“框得准不准”而是“短时间内司机到底在做什么”——单帧检测出来的手机、烟、水杯只有被翻译成持续行为才会变成车队管理或驾驶辅助系统里的告警。它的典型应用是实时识别手持电话、吸烟、喝水这类高风险动作主要用在商用车车载摄像头、驾校模拟考试和驾驶安全评分上。做这件事的人通常是算法工程师、嵌入式视觉工程师或者准备用视觉方案做车队安全合规评估的学生团队。好消息是项目的主体并不玄学数据、训练、部署、行为判定四条线都成熟真正决定效果的是数据口径和判定规则而不是网络结构本身。2. 数据与标注驾驶行为样本从哪里来以及转YOLO格式的脚本驾驶行为检测的数据准备要比普通物体检测更讲究。手机、烟、水杯在画面里占比小运动模糊常见遮挡频繁而且不同车型的驾驶舱布局差异大。如果一上来就找通用目标检测数据集来训练换一个摄像头角度就废掉一半指标。我一般把数据工作拆成三件事定类别口径、整理标注、配增强策略。2.1 类别口径是第一个决策点先定类别再采样本先回答一个基础问题检测几类分别是什么常见做法是聚焦三个高风险动作手持电话handheld_phone、吸烟smoking、喝水drinking。为什么不把“打电话”和“拿手机”拆开因为单帧图像很难可靠区分“正在通话”和“只是拿着”拆开会引入大量标注歧义两个类在不同标注者手里的边界完全不统一。我的习惯是把这类动作合到“手持电话”一个类里后续需要区分时再通过手部与耳侧区域的重叠关系做二次判定而不是交给检测模型硬学。类别定了之后样本采集才有依据。公开的驾驶行为视频数据集可以拿来抽帧做初版但很少有数据集能覆盖到你要落地的那个具体车型、摄像头安装位置和光线条件。我一般会留出一部分自采数据作为“泛化集”不动它专门用来衡量模型换场景后的真实水平。采集时要刻意覆盖几个容易翻车的场景夜间、逆光、雨刮器遮挡、车窗贴膜反光以及司机戴墨镜打哈欠这类边缘情况。注意人脸数据涉及肖像权采集和标注前必须走完合规流程。2.2 标注整理把VOC风格JSON转成YOLO格式的脚本标注工具不唯一但产出格式一般是两种VOC风格JSONLabelme导出的典型结构或COCO格式。YOLOv8训练直接吃txt每行一个目标类别ID加归一化的中心点坐标和宽高。这里有个老生常谈但天天有人踩的坑JSON里存的是多边形点集合不是标好的矩形框。你需要先把多边形近似成外接矩形再转成YOLO的cxcywh格式。# voc_json_to_yolo.py # 将Labelme风格的JSON标注转换成YOLO格式的txt文件 import json def convert_voc_json_to_yolo(json_path, out_txt_path, class_names): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w, img_h data[imageWidth], data[imageHeight] lines [] for shape in data[shapes]: label shape[label] if label not in class_names: continue class_id class_names.index(label) points shape[points] xs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) # 多边形顶点可能略微越界这里强制裁剪到图像范围内 x_min max(0.0, x_min) y_min max(0.0, y_min) x_max min(img_w, x_max) y_max min(img_h, y_max) if x_max x_min or y_max y_min: continue # 转成YOLO的归一化cxcywh格式 cx (x_min x_max) / 2.0 / img_w cy (y_min y_max) / 2.0 / img_h bw (x_max - x_min) / img_w bh (y_max - y_min) / img_h lines.append(f{class_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(out_txt_path, w, encodingutf-8) as f: f.write(\n.join(lines) \n)这段脚本的逻辑很直白从JSON里读出图像宽高遍历每个标注形状取多边形所有点的最小和最大坐标构造成矩形做一次边界裁剪后标准化成YOLO格式。裁剪那两行不是多余的多边形角点经常因为手抖画到画面外不裁剪会导致归一化坐标出现负数或大于1的值训练时变成无效框甚至报错。跑完转换后一定要检查图像与txt的同名对应关系以及txt里有没有空文件。我习惯写一个检查脚本逐张图像统计标注框数量少于1个的文件单独列出来看是漏标还是转换丢了。这一步能提前拦住大部分“训练集一堆图但有效目标很少”的隐性坑。标注质检时最好找两个人背靠背标同一批图算一次框间IoU低于0.7的样本重新讨论口径尤其是手机这种小物体框大一点小一点对检测结果影响非常明显。2.3 数据增强mosaic和亮度扰动怎么配YOLOv8默认带一套增强参数但直接默认值跑驾驶行为检测并不是最优解有两个地方我会刻意改。第一是水平翻转默认fliplr一般是0.5但在驾驶舱场景里方向盘和仪表盘布局是有固定方向的翻转会导致样本出现现实中不存在的镜像布局尤其是手机贴近右耳打电话这类动作翻转后语义变化很大。我一般直接设成0.0让模型专注真实布局。第二是亮度扰动驾驶场景的室内光照变化比室外更大隧道、树荫、地库都是亮度突变场景hsv_v可以从默认值往上提。# hyp_driving.yaml # 覆盖默认增强超参只写需要改动的项 mosaic: 0.8 # 4张图拼接概率增强小目标上下文 mixup: 0.2 # 图像混合概率样本少时不要开高 fliplr: 0.0 # 驾驶舱有左右布局约束禁止水平翻转 hsv_h: 0.015 # 色调扰动幅度 hsv_s: 0.6 # 饱和度扰动幅度 hsv_v: 0.5 # 亮度扰动幅度调高应对隧道和地库光照 scale: 0.3 # 尺度扰动保留小目标的尺度多样性mixup参数我建议保守一点。驾驶行为检测的样本量通常不会很大mixup比例开高了会造成边界模糊尤其烟和手部的边缘本来就难标混合后模型更难学到锐利特征。mosaic反而是加分项它能在一张图里塞进多个动作目标让模型在小目标上的上下文更丰富。如果标注框非常密集mosaic之后框容易溢出边界ultralytics会自动处理一部分但输出框的质量还是要在验证集上体现出来。注意增强参数不是在训练集上把损失降得越低越好。合适的标准是验证集mAP不再明显波动且换一个采集角度后的掉点幅度在可接受范围内。增强过头训练集损失很漂亮实拍一样漏检。3. 训练配置与参数用一张消费级显卡跑通最小可用模型数据准备好之后训练阶段的目标是先出一个能用的baseline而不是一上来就追精度。驾驶行为检测的模型选择、输入分辨率和超参配比决定了下游部署是顺滑还是反复返工。我见过太多人把时间花在改网络结构上结果发现瓶颈在训练配置上。3.1 模型选择YOLOv8n起步还是直接上s/mYOLOv8的模型规格从n到x都有驾驶行为检测场景里我强烈建议从n或s起步。先看部署环境如果目标设备是常见边缘板卡或车内工控机n是首选参数如果跑在服务器上做离线分析s更稳妥。n的特点是速度快、显存占用小但小目标检测能力偏弱s的mAP通常比n高3到5个点速度慢一些但还在可接受范围。至于m和l除非你有明确的算力富余否则驾驶行为场景用不上——这类场景对延迟很敏感对几个点的mAP提升没那么敏感。模型规格推理速度定位适用场景我的选择建议YOLOv8n最快适配边缘设备车内实时预警默认起步先跑通链路YOLOv8s中等速度精度更好服务器离线分析或算力充足的边缘设备数据量大、精度不够时升级YOLOv8m/l速度明显下降高精度要求的离线抽帧审核一般不推荐收益偏低训练基线用n还有一个实际原因迭代快。一次训练几十分钟就能看到增强配置、数据质量的变化趋势比跑一轮m再回头改数据高效得多。我的习惯是n跑出val mAP 50以上的baseline再根据实际误报情况决定要不要换s。3.2 数据集yaml和训练命令直接照抄的最小配置YOLOv8的数据配置是一个yaml文件指定类别数和类别名。路径要格外注意ultralytics的路径解析逻辑在不同版本里有微妙差异训练和验证建议用相对路径并固定根目录避免换机器后路径失效。# driving_behavior.yaml path: ../dataset # 数据根目录 train: images/train # 训练图像目录 val: images/val # 验证图像目录 nc: 3 # 类别数量 names: 0: handheld_phone 1: smoking 2: drinking训练命令可以直接用Python脚本调用便于把增强配置、训练参数固化在文件里方便复现。这也是标题里“项目代码”的意义所在——整个训练流程要能一把梭跑完而不是靠人肉记住每次改了哪些参数。# train_driving.py from ultralytics import YOLO # 加载COCO预训练权重迁移学习能让训练快速收敛 model YOLO(yolov8n.pt) model.train( datadriving_behavior.yaml, epochs100, imgsz640, batch16, lr00.01, patience15, augmentTrue, cacheram, hyphyp_driving.yaml, projectruns/driving, nameexp1 )这段训练代码里有几个参数值得说明。imgsz640是速度和精度的平衡点如果画面里手机目标普遍很小可以考虑768但训练和推理时间都会涨。batch16在8G显存左右的卡上可以跑通n模型显存不足先降到8不要硬扛。lr0用默认的0.01即可预训练权重不需要重新调学习率。cacheram把训练图像缓存到内存里能显著减少IO等待但大内存才会比较舒服。patience15表示连续15轮验证集指标不提升就提前终止省时间。训练跑起来之后看两个东西一是每轮输出的box_loss和cls_loss正常应该稳步下降二是验证集mAP50和mAP50-95的趋势。mAP50-95涨得慢是正常的但如果box_loss掉到一定程度后震荡明显多半是标注噪声或增强过猛。第一个epoch的loss比预训练阶段低是正常的因为分类头被随机初始化的部分正在快速适应新类别。3.3 三个值得手动调的参数lr0、mosaic、cls_pw很多人以为YOLOv8加载预训练权重后直接用默认参数就行但驾驶行为检测有个特殊问题类别之间数量极不平衡。手持电话的样本可能几千张吸烟的样本可能只有两三百张。这时候如果不做任何处理模型会倾向于把所有靠近嘴部区域的检测都判成drinking因为那一类样本多。我一般会在hyp配置里增加cls_pw让模型对样本少的类别施加更大的分类权重# 仍是在train_driving.py里通过hyp覆盖文件传入 # 注意实际使用需要在hyp文件里写或者直接改成hyp{cls_pw: [1.0, 3.0, 1.5]}这种加权方式的逻辑是让模型对smoking这类样本少的类别更敏感宁可多给几个候选框也别直接判成背景。mosaic的作用前面提过这里再补一句初始阶段mosaic开0.8没事训练中后期如果发现小目标过拟合可以降回0.5。lr0不是万能的预训练模型用0.01起步问题不大但如果你从零训练不加载预训练权重学习率要降到0.001左右才稳。参数默认值参考训练中的调试方向原因lr00.01从零训练时降到0.001预训练权重已有好特征微调不需要大学习率mosaic0.8过拟合小目标时降到0.5提高目标尺度多样性也防止增强失真cls_pw全部1.0对稀缺类别加权缓解吸烟、喝水这类样本不足带来的漏检这三个参数调整的依据不是训练集loss而是验证集上按类别分开看的召回率。ultralytics的训练日志里会打印每个类别的mAP如果smoking的mAP50明显低于其他类优先查样本量和标注一致性其次才调cls_pw。调参不是玄学你要能说清楚“调这个参数为什么能影响那一类检测”。4. 部署与推理导出ONNX并在车内环境把模型跑起来训练完的模型是pt格式不能直接塞进边缘设备。部署链路一般是pt转ONNX然后再转成目标设备的推理引擎格式。这一步的坑集中在导出参数和预处理还原上尤其是坐标换算错了框全画偏看起来像模型废了其实是推理代码没配合好。4.1 模型导出从pt到ONNX再到边缘推理引擎ultralytics直接把导出逻辑封装好了但参数还是要自己过一遍# export_onnx.py from ultralytics import YOLO model YOLO(runs/driving/exp1/weights/best.pt) model.export( formatonnx, imgsz640, opset12, dynamicFalse, simplifyTrue )opset12是我习惯的起点兼容性好老版本推理框架也支持。simplifyTrue会做一轮计算图精简能减少部分冗余算子导出后留意ONNX大小有没有异常膨胀。dynamicFalse把输入尺寸固定成1x3x640x640这在车载场景下是合理的选择——你不需要在运行时改分辨率固定shape能让推理引擎做更多优化。如果要支持动态尺寸dynamicTrue会带来额外的前处理复杂度通常没必要。ONNX导出后在NVIDIA设备上我一般会再转一遍TensorRT引擎尤其是推理帧率吃紧的场景。用trtexec转就行# 用trtexec把ONNX转成FP16的TensorRT引擎 trtexec --onnxbest.onnx \ --saveEnginebest_fp16.engine \ --fp16 \ --minShapesimages:1x3x640x640 \ --optShapesimages:4x3x640x640 \ --maxShapesimages:8x3x640x640min/opt/max三组shape用来定义动态batch范围实际使用中如果固定batch为1可以省掉这些参数直接转换。FP16在驾驶行为检测上几乎没有精度损失但对帧率提升是实打实的。如果部署设备不支持TensorRTONNX Runtime加CUDA也能达到不错的效果下面这套代码同时适用于ONNX和ONNX Runtime环境。4.2 用Python做一次完整推理letterbox预处理与坐标还原部署环节最容易翻车的地方在前处理和后处理。YOLOv8训练时会把图像resize到正方形并填充灰边推理时如果不做同样的letterbox宽屏画面会被直接拉伸变形检测框全部偏移。推理输出的坐标是相对于640x640输入图的必须反算回原图坐标否则画框位置不对。# inference_onnx.py import cv2 import numpy as np import onnxruntime as ort def letterbox(img, new_shape640, color(114, 114, 114)): h, w img.shape[:2] r min(new_shape / h, new_shape / w) # 缩放比例 nh, nw int(h * r), int(w * r) resized cv2.resize(img, (nw, nh)) canvas np.full((new_shape, new_shape, 3), color, dtypenp.uint8) x_offset (new_shape - nw) // 2 y_offset (new_shape - nh) // 2 canvas[y_offset:y_offset nh, x_offset:x_offset nw] resized return canvas, r, x_offset, y_offset # 原始一帧画面 img cv2.imread(frame.jpg) canvas, r, x_offset, y_offset letterbox(img) # 预处理BGR转RGB、HWC转CHW、归一化到0~1 input_tensor canvas[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) / 255.0 input_tensor np.expand_dims(input_tensor, axis0) # 创建推理会话优先CUDA session ort.InferenceSession( best.onnx, providers[CUDAExecutionProvider, CPUExecutionProvider] ) # 模型输出shape为(1, 4nc, 8400)nc3时是(1, 7, 8400) output session.run(None, {session.get_inputs()[0].name: input_tensor})[0][0] scores output[4:, :] # 类别得分部分 class_ids scores.argmax(0) conf scores.max(0) # 先按置信度过滤低质量框 mask conf 0.3 boxes_cxcywh output[:4, mask].T conf_top conf[mask] class_top class_ids[mask] # 从cxcywh转成xyxy boxes_xyxy np.concatenate([ boxes_cxcywh[:, :2] - boxes_cxcywh[:, 2:] / 2, boxes_cxcywh[:, :2] boxes_cxcywh[:, 2:] / 2 ], axis1) # 用OpenCV的NMS做类别无关去重 keep cv2.dnn.NMSBoxes( boxes_xyxy.tolist(), conf_top.tolist(), 0.3, 0.45 ) # 坐标还原回原图 for idx in keep.flatten(): x1, y1, x2, y2 boxes_xyxy[idx] x1 (x1 - x_offset) / r y1 (y1 - y_offset) / r x2 (x2 - x_offset) / r y2 (y2 - y_offset) / r label class_top[idx] print(fclass{int(class_top[idx])}, conf{conf_top[idx]:.2f}, box({x1:.0f},{y1:.0f},{x2:.0f},{y2:.0f}))这段代码里有两个参数直接影响结果conf阈值0.3和NMS的iou阈值0.45。conf阈值决定保留哪些框阈值越低召回越高但误报也越多iou阈值控制重叠框的合并力度。驾驶行为场景下我建议conf先按0.3起步上线后根据误报率再调。需要强调一下YOLOv8输出的坐标是归一化到0~1的上面代码直接从output数组里取的坐标就是这个尺度除以缩放系数r后才是原图像素坐标这一步不要省。4.3 推理链路的性能观察点部署跑通后还要看几个运行指标帧率、CPU占用、内存占用和抖动。ONNX Runtime在CPU上跑n模型640分辨率帧率不会特别高如果达不到实时要求优先考虑换TensorRT引擎或降低输入分辨率到512。我见过有人把imgsz调到480后帧率上去了但手机小目标几乎全丢得不偿失。注意换推理尺寸必须重新转引擎不能只改代码里的resize。训练尺寸、导出尺寸、推理尺寸三者不一致时精度会明显下降这不是模型问题是部署链路没对齐。5. 避坑与排查数据、训练、部署三条线的高频问题这个方向做久了翻车点其实挺固定。下面按“现象→原因→解决”写几条高频踩坑记录都是我反复遇到过或者看同行踩过的能帮你少走不少弯路。5.1 小目标漏检手机体积小、特征弱现象验证集mAP50有85%以上但实际车载画面里手机贴近耳边时经常检测不出来尤其是距离摄像头远、手型与肤色相近的情况。原因手机目标在640分辨率里可能只占几十个像素特征信息少卷积下采样到8倍或16倍特征图后更弱另外标注框不统一也加剧了这个问题。解决先检查标注框是否贴合手机外轮廓很多框把手指一起包进去了目标变得更大但特征被稀释然后把训练分辨率提到768试试代价是训练和推理变慢最后是引入近景样本让模型见过更大尺度的手机。我实测过把部分远距离画面裁剪放大再训练的路径对召回提升明显但要注意裁剪后与原图标注对齐别引入错误样本。5.2 类别不平衡吸烟样本稀少导致召回率极低现象smoking类目标precision很高每次检出基本是对的但recall只有个位数大量吸烟动作被漏掉。原因烟支目标又小又细标注难度大样本量天然少加上吸烟动作常和喝水动作共用“手到嘴部”的空间特征模型学不到区分性特征干脆倾向输出样本多的drinking。解决第一步是本类过采样把smoking的样本复制进训练集配合cls_pw加权第二步是检查标注手烟应该整体标框还是只标烟口径必须统一第三步是把烟支附近区域裁出来做局部增强让模型看到更多细节。注意不要靠加大mosaic解决类别不平衡mosaic对单类特征增强有限。5.3 夜间和隧道亮度场景集体翻车现象白天效果很好一到夜间或进出隧道时漏检率陡增误报也变多。原因训练数据以白天光线为主模型对低照度、强反光场景没见过车载摄像头自动增益在夜间会拉高ISO画面出现大量噪声和光晕单纯调亮度扰动模拟不出来。解决第一步hsv_v增强要开但只能当辅助第二步收集夜间、隧道、地库的真实视频抽帧补充训练集这部分必须实采合成数据救不回来第三步上线前单独用一段夜间视频做验证不看总体mAP按时间段分拆指标。不要试图用一个gamma变换就糊弄过去夜间翻车往往是曝光和噪声的复合问题不是亮度的简单偏移。5.4 置信度阈值调来调去误报和漏报互相打架现象conf阈值设0.5手机漏检严重降到0.25喝水动作频繁误报成吸烟。原因不同类别的得分分布差异大一个全局阈值无法同时满足三个类别。解决在推理端按类别分开设阈值比如smoking因为样本少、特征弱用0.25drinking用0.4handheld_phone用0.3。这个做法不会改变模型精度但能在真实告警层面减少一半以上的无效报警。我习惯把阈值配置外置成一个config文件方便现场调不用改代码。别小看这个细节阈值策略直接决定用户对系统的信任度——频繁误报的系统最后都会被关掉。5.5 验证集指标高但换场景就崩泛化问题被忽视现象在自建验证集上mAP50有0.85换一台车、换一个摄像头安装高度后精度大幅下滑。原因训练数据来自固定的机位视角车型、座椅位置、玻璃反光都高度相似模型其实记住了特定场景的纹理而不是泛化的“手机”“烟”“水杯”语义。解决从项目一开始就留一个“泛化集”这个数据集不参与训练、不做验证调参只用来每训练一轮结束做一次盲测。泛化集来源越杂越好不同车型、不同视角、不同天气各来一些。如果泛化集指标掉得厉害优先回数据侧加多样性而不是继续调网络参数。很多团队栽在这一步交付后才发现现场画面和训练集差距太大返工成本极高。6. 行为判定规则与离线验证从单帧检测框到有用告警检测模型输出的是单帧结果但驾驶行为告警必须是时间维度上的判断。一个常见的错误是某一帧检测到手持电话就立刻告警结果司机只是短暂拿起手机看一眼导航系统疯狂误报。我一般会在检测层之上加一个轻量级决策层用连续帧计数和冷却时间控制告警节奏。# decision.py # 行为判定连续N帧检出同一动作才触发告警 DECISION_BUFFER 5 # 连续检出多少帧算一次有效行为 COOLDOWN_SEC 30 # 同一动作两次告警之间的冷却时间 frame_buffers {} # 每个动作最近积累的帧号 last_alert_at {} # 每个动作上次触发告警的时间戳 def feed_detections(action_list, frame_id, timestamp): for action in [handheld_phone, smoking, drinking]: if action in action_list: frame_buffers.setdefault(action, []).append(frame_id) frame_buffers[action] frame_buffers[action][-DECISION_BUFFER:] else: frame_buffers[action] [] if (len(frame_buffers[action]) DECISION_BUFFER and timestamp - last_alert_at.get(action, 0) COOLDOWN_SEC): last_alert_at[action] timestamp fire_alert(action, frame_id)这个逻辑看起来简单但它解决的是告警可用性的核心问题不是所有单帧检出都值得上报只有持续存在的动作才是真实风险。DECISION_BUFFER设5意味着按25帧每秒算动作持续0.2秒才触发能过滤绝大多数闪烁误检COOLDOWN_SEC设30则避免同一个动作在短时间内反复告警刷屏。这两个参数要根据实际监控并发量调整实测时先大后小宁可漏一次告警也不要让使用者对系统脱敏。离线验证是上线前最后一道关口。我习惯把一段真实视频按帧标注出“哪些时间段有哪类动作”然后让检测系统跑一遍统计每个动作的检出帧数、触发告警次数、误报次数和延迟。这个验证过程用代码自动化最终产出一张“帧级召回率 vs 告警精确率”的简单报表比只看mAP更能说明系统是否可用。关键帧保存是个实用技巧告警触发时把前后各几秒的视频片段连同检测框截图存下来人工复核只看这些片段一天几千条告警也能在一小时内审完。这个方向的坑我都拿实拍踩过最后收敛下来的经验就一条先跑通端到端的最小链路再回头补精度和泛化。最开始做的时候我在网络结构上花了很多时间后来发现标注口径和判定规则才是决定项目成败的部分。希望帮到你。本文还有配套的精品资源点击获取