简介:面向深度学习与计算机视觉开发者的驾驶行为检测系统项目源码,针对闭眼、抽烟、打电话等危险驾驶行为识别而设计,可作为图像分类、目标检测入门及智能交通应用开发的完整参考实现。代码基于Python 3.9与Ultralytics框架编写,完整覆盖环境配置、数据集结构设计、模型训练、推理评估与界面部署环节,并给出学习率、批大小、损失函数等关键训练参数的设置方式,适合希望从零搭建检测项目的初学开发者,也便于进阶者快速比对工程实现细节。压缩包共34个文件、大小约34KB,以Python脚本、YAML配置、TXT说明及JPG示例图片为主,目录内部按入口、训练、检测、评估、界面和测试等模块划分,并附带数据下载脚本与CPU演示脚本,结构清晰、便于定位与复用。设计上使用5215条训练样本与1303条验证样本组织数据集,明确标注规范与数据组织方式;推理端支持图片、视频和摄像头三种输入,同时提供PyQt5图形界面,以及准确率、召回率等性能评估方法;附带依赖清单、使用说明与开发者备注,便于将模型落地到实际应用场景。目前已有125人学习浏览,是一份轻量但链路完整、具有实用借鉴价值的目标检测参考代码包。
1. 驾驶行为检测项目,为什么我建议直接选 YOLOv8 起步
在我经手的几个车载视觉 Demo 里,驾驶行为检测是需求最集中、也最容易让新人卡住的方向——摄像头装在驾驶舱内,要实时识别打电话、看手机、抽烟、喝水这些动作,训练数据标注得花一周,跑起来帧率还得跟上。很多人一开始在 Faster R-CNN、YOLOv5、YOLOv8 之间反复横跳,最后浪费在框架选型和数据格式上。我的建议很直接:如果你要做的是 YOLOv8 驾驶行为检测,就用 YOLOv8 官方那套训练工具起步,它把数据接口、训练参数、评估指标和导出格式全部统一了,从零到跑通第一次训练通常只要半天。这篇文章会沿着「为什么选它 → 数据怎么做 → 参数怎么调 → 坑在哪 → 怎么进阶」往下写,新手能一步步复现,熟手也能找到小目标和夜间场景的解法。
2. 选型逻辑:为什么驾驶行为检测最终都绕不开 YOLOv8
2.1 行为检测对模型提出的三项硬指标
驾驶行为检测的输入是驾驶舱内的广角摄像头画面,检测目标是手机、烟、水杯这类小物件,以及手部、头部这些身体部位。这个任务和通用目标检测最大的区别在于,它同时被三个硬约束卡着,缺一个都落不了地。
第一个约束是实时性。完整链路是「摄像头采集 → 目标检测 → 行为判定 → 报警」,检测模块至少要跑到 20 FPS 以上,才能在边缘设备上留出余量给后续的逻辑判断。两阶段检测器在这类场景里很吃亏,除非配高性能显卡,否则很难满帧运行。单阶段检测器天然占优,这也是行为检测方案普遍压在 YOLO 系列上的原因。
第二个约束是目标尺度偏小。一部手机在 1280×720 的驾驶舱画面里往往只占几十个像素,一支烟可能只有十几个像素。检测器对小目标的召回能力,直接决定了这类行为能不能被及时发现。很多模型在公开数据集上 mAP 很好看,一换到车内视角就漏检,问题基本都出在这。
第三个约束是光照和视角变化剧烈。白天逆光、夜间仪表盘反光、隧道内忽明忽暗,图像的亮度分布和色彩偏移变化非常大。模型如果只在明亮正光的数据上训,换到夜间场景精度会断崖式下跌。所以选型时不能只看精度榜单,还得看训练生态能不能方便地做数据增强、多尺度训练和模型导出,这三件事在行为检测里比模型本身的涨点更重要。
把这三条约束摆在一起,YOLOv8 的定位就很清楚了:它不是为了在某个榜单上争第一,而是为了让「训练-调参-部署」这条链路尽量少出幺蛾子。对要做驾驶行为检测的人来说,稳定可复现比一两分 mAP 的提升更值钱。
2.2 YOLOv8 针对痛点做的三个结构设计
YOLOv8 相比 YOLOv5 的改动集中在三处,这三处正好都踩在行为检测的痛点上。
第一处是骨干网络里的 C2f 模块。C2f 在 C3 的基础上增加了多条梯度流分支,让特征在层间传递时不容易衰减。对手机、烟这类纹理弱、像素少的目标来说,信息流动越充分,小目标特征就越能保留到深层。实际训练中最直观的感受是:同样的数据量和训练轮数,YOLOv8 比 YOLOv5 收敛更快,验证集上的精度波动也更小。我在行为检测项目里做过对比,YOLOv8n 在 100 个 epoch 内能达到 YOLOv5s 跑 150 个 epoch 的效果,省下的时间都花在调数据上更划算。
第二处是检测头从 anchor-based 改成了 anchor-free 的解耦头。解耦的意思是分类和回归各走一条分支,避免两类损失互相干扰。行为检测的类别之间差异很小,「打电话」和「看手机」经常只差一个手部姿态,解耦头能让分类分支专注学这些细微差别,而不是被框回归的损失带偏。这在类间混淆比较严重的数据集上提升很明显,后面讲避坑时还会细说。
第三处是特征金字塔的输出尺度。YOLOv8 默认用 P3、P4、P5 三个尺度的特征图分别负责小、中、大目标,P3 层在 640 输入下对应 80×80 的特征图。手机这类小目标主要靠 P3 召回,这也是为什么之后把 imgsz 从 640 提到 960 会带来明显涨点——输入分辨率直接决定了 P3 层给每个小目标分配多少有效像素。
2.3 与 Faster R-CNN、YOLOv5、RT-DETR 的横向对比
| 模型 | 推理速度 | 小目标能力 | 训练生态 | 部署难度 | 行为检测适配度 |
|---|---|---|---|---|---|
| Faster R-CNN | 慢,难实时 | 中 | 需要自己拼训练流程 | 高 | 低 |
| EfficientDet | 中 | 中 | 一般 | 高 | 一般 |
| YOLOv5 | 快 | 中 | 好 | 低 | 适合 |
| YOLOv8 | 快 | 较好 | 好 | 低 | 首选 |
| RT-DETR | 中 | 中 | 一般 | 中 | 可选 |
这张表是我做选型评审时常用的对比维度。Faster R-CNN 在精度上有上限吗?没有,但它的训练流程长、部署链路复杂,换一个分辨率就要重新调一堆参数,在行为检测这种数据集经常要迭代的场景里非常拖节奏。YOLOv5 和 YOLOv8 的差距主要体现在 C2f、解耦头这些结构改动上,外加官方工具链对数据格式、模型导出支持得更完整。RT-DETR 是端到端方案,省掉了 NMS,精度也不错,但对边缘设备的算子兼容性要求高,部署踩坑成本大。
我一直强调一个观点:选型不是选精度最高的,而是选「失败成本最低」的。YOLOv8 的数据格式、训练命令、导出方式都高度统一,即使某个环节翻车了,网上可验证的案例也多,这是行为检测入门阶段最需要的安全网。
3. 把原始图片变成 YOLO 能用的数据集:类别、目录与转换脚本
3.1 行为类别定义:互斥比数量更重要
很多人在标注驾驶行为数据集时,上来就分十几类:左手打电话、右手打电话、左手看手机、右手看手机、抽烟、喝水、回头、捡东西、打哈欠……类别分得越细,标注工作量越大,模型混淆的概率也越高。我自己踩过这个坑,第一次做行为检测分了 12 类,训练完一看混淆矩阵,左手打电话和右手打电话互相串,看手机和打电话也串,最后只能合并类别重新训练。
我的建议是:第一版控制在 6 类以内,并且保证类间互斥。「打电话」和「看手机」在实际画面里很难肉眼区分,干脆合并成一个phone_use类,或者严格规定「手机靠近耳边才算打电话,手机在手里看屏幕才算看手机」,否则标注员和模型都会迷茫。类别的定义应该写进标注规范里,每个类配参考图,这比事后清洗数据省力得多。
行为检测里比较稳的类别划分是:正常驾驶、使用手机、抽烟、喝水、伸手够物、疲劳状态。疲劳状态可以进一步拆成闭眼和打哈欠,但这两者在行为检测里更多是依赖人脸关键点而不是检测框,所以我一般只在检测层级保留一个疲劳大类,细节交给后续时序模块去判断。
3.2 目录结构与 data.yaml 配置
YOLOv8 官方工具的数据接口很死板,但也因为死板而省心。你只需要按固定目录放图片和标签,再写一个 data.yaml。标准目录结构如下:
driving_behavior_dataset/ ├── images/ │ ├── train/ # 训练集图片 │ └── val/ # 验证集图片 └── labels/ ├── train/ # 训练集标注 txt └── val/ # 验证集标注 txt对应的 data.yaml 配置:
# data.yaml - 驾驶行为检测数据集配置 path: ./driving_behavior_dataset # 数据集根目录 train: images/train # 训练集图片目录 val: images/val # 验证集图片目录 # 类别清单:索引顺序必须与标注 txt 中的类别 id 完全一致 names: 0: normal # 正常驾驶 1: phone_use # 使用手机(打电话/看手机) 2: smoking # 抽烟 3: drinking # 喝水 4: reaching # 伸手够物 5: fatigue # 疲劳(闭眼/打哈欠)这里最容易翻车的是类别顺序。如果你用脚本从 VOC 或 COCO 格式转过来,脚本里的类别列表必须和 data.yaml 里的 names 顺序一致,否则训练出来的模型输出的类别全是错位的。我有一个习惯:转换完成后随机抽三张训练图片,直接打开对应的 txt 文件对照一下标注框的位置和类别,确认无误再开始训练。
3.3 把 VOC 标注批量转成 YOLO txt 格式的脚本
很多公开数据集和标注工具导出的都是 VOC 格式的 XML,YOLOv8 不认这个格式,需要先转成纯文本的 YOLO 格式。YOLO 格式每行表示一个目标:类别 id + 归一化后的中心点 x、y + 归一化后的宽、高。我一般会写一个这样的转换脚本:
# voc2yolo.py - 将 VOC XML 标注批量转换为 YOLO txt 格式 import os import xml.etree.ElementTree as ET from pathlib import Path # 类别列表,顺序必须与 data.yaml 中 names 完全一致 CLASSES = ['normal', 'phone_use', 'smoking', 'drinking', 'reaching', 'fatigue'] def convert_xml_to_yolo(xml_path: str, output_dir: str) -> None: """把单个 VOC XML 转成 YOLO 格式的 txt 文件""" tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) txt_name = Path(xml_path).stem + '.txt' with open(Path(output_dir) / txt_name, 'w', encoding='utf-8') as f: for obj in root.findall('object'): cls_name = obj.find('name').text if cls_name not in CLASSES: continue # 跳过不在类别列表中的标注 cls_id = CLASSES.index(cls_name) bbox = obj.find('bndbox') x_min = float(bbox.find('xmin').text) y_min = float(bbox.find('ymin').text) x_max = float(bbox.find('xmax').text) y_max = float(bbox.find('ymax').text) # VOC 存的是绝对像素坐标,YOLO 需要归一化中心点 + 宽高 x_center = (x_min + x_max) / 2 / img_w y_center = (y_min + y_max) / 2 / img_h width = (x_max - x_min) / img_w height = (y_max - y_min) / img_h # 标注偶尔越界,clip 到 [0,1] 避免训练时报错 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) width = min(max(width, 0.0), 1.0) height = min(max(height, 0.0), 1.0) f.write(f'{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n') def batch_convert(xml_dir: str, output_dir: str) -> None: """批量转换目录下所有 XML 标注""" os.makedirs(output_dir, exist_ok=True) for xml_file in Path(xml_dir).glob('*.xml'): convert_xml_to_yolo(str(xml_file), output_dir) print(f'converted: {xml_file.name}') if __name__ == '__main__': batch_convert('annotations/xml', 'labels/train')脚本逻辑分三步:读 XML 里的图片宽高和每个目标的原始坐标,把左上角右下角坐标换算成中心点加宽高,最后归一化到 0 到 1 之间。参数说明:img_w和img_h来自 XML 的size节点,这是归一化的分母,一旦读错宽高,后面所有坐标全部错位。CLASSES.index(cls_name)负责把类别名字映射成数字 id,顺序错一个,整个模型的输出语义就乱套了。
转换完成后,我强烈建议做一个校验:统计一下 labels 目录下的 txt 文件数量和 XML 数量是否一致,再随机打开几个 txt 看坐标值是否都在 0 到 1 之间。这个步骤能挡掉八成数据格式问题。
3.4 数据增强的取舍:水平翻转可能是陷阱
YOLOv8 默认开启一批数据增强:Mosaic、随机透视、HSV 扰动、水平翻转等。大部分增强在通用检测里是安全的,但水平翻转对驾驶行为检测是一个隐患。如果你的类别里区分了左手和右手,比如「左手持手机」和「右手持手机」,水平翻转会把左右手互换,模型会被互相矛盾的学习目标搞晕,训练损失下不去,精度也上不来。
解决办法有两个:一是把左右手类别合并,这样水平翻转反而成了免费的数据扩充;二是关闭水平翻转,在训练参数里设置flip_lr=0.0。我个人的习惯是合并类别,因为驾驶行为检测的最终目标是报警提示,不需要精确到左手还是右手,合并之后数据量翻倍,对小目标的泛化更友好。
另外一个需要留意的增强是 Mosaic。Mosaic 把四张图拼在一起训练,对提高鲁棒性很有帮助,但如果你的数据集里每张图的目标本来就不多,Mosaic 之后单个目标可能被切掉大半,导致标注框里大量空白区域。我一般在训练后期把 Mosaic 的概率调低,比如在 30 个 epoch 之后只开一半,让模型在接近真实分布的样本上精调。
4. 训练与调参:一条命令跑通,再到精度提升
4.1 第一次训练:最小命令跑通全流程
数据集准备好之后,第一次训练的目标不是最高精度,而是把整条链路跑通。我通常会先用最小的模型和默认参数做一次快速验证,确认数据加载、类别映射、损失下降都正常。最小命令长这样:
# 第一次训练:从 COCO 预训练权重开始迁移学习 yolo detect train \ data=data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ patience=20参数说明:model=yolov8n.pt表示加载 COCO 预训练权重,YOLOv8n 是最小的 n 版本,参数量约 3.2M,适合先验证流程。imgsz=640是输入分辨率,训练时图片会被缩放到 640×640。batch=16按显存调整,12G 显存跑 n 版本没问题。patience=20是早停轮数,验证集 mAP 连续 20 个 epoch 不涨就自动停止,避免空跑。
第一次训练时应该盯四个东西:训练损失是否稳步下降、验证集 mAP50 是否在 30 到 50 个 epoch 内开始爬升、类别分布是否均衡地出现在验证集预测结果里、训练日志是否报数据相关的 warning。如果损失在前 10 个 epoch 里不降,优先检查数据格式和类别映射,而不是急着调学习率。
跑完之后,结果会输出到runs/detect/train/目录里,里面包含weights/best.pt和weights/last.pt。best.pt 是按验证集 mAP 保存的最优权重,后面部署和继续训练都用它。
4.2 精度遇到瓶颈,按这张参数表逐个调
第一次跑通之后,你会发现精度一般不会直接达标,尤其是小目标和夜间场景。这时不要随机调参,我习惯按下面这张表的顺序逐个来,每个参数改了只训一轮对比,不要一次改多个,否则根本不知道是哪个参数起了作用。
| 调整顺序 | 参数 | 默认值 | 行为检测里的典型改动 | 作用 |
|---|---|---|---|---|
| 1 | imgsz | 640 | 960 | 提高小目标的有效像素 |
| 2 | model | yolov8n | yolov8s / yolov8m | 增大模型容量 |
| 3 | lr0 | 0.01 | 0.005 | 数据量小时降低初始学习率 |
| 4 | lrf | 0.01 | 0.001 | 让末期精调更细 |
| 5 | batch | 16 | 8 或 32 | 影响收敛稳定性 |
| 6 | mosaic | 1.0 | 0.5 | 后期减弱拼接增强干扰 |
| 7 | weight_decay | 0.0005 | 0.0003 | 防止小数据过拟合 |
这里最值得说透的是imgsz。行为检测里手机、烟这些目标很小,640 分辨率下 P3 层给一个小目标可能只有 4×4 个特征点,信息量不够。提到 960 之后,同样一个目标能分到更多特征点,小目标召回率往往涨 3~5 个点。代价是训练和推理速度变慢,显存占用升高。如果设备跑不动 960,另一个折中做法是保持 640 训练、推理时用更大的 imgsz,但效果会打折扣,因为训练和推理分辨率不一致会影响框的置信度。
学习率这块,行为检测的数据集通常只有几千张图,属于典型的小数据场景。默认 0.01 的初始学习率偶尔会出现前几个 epoch 损失不降反升的情况,这时候降到 0.005 往往就好了。lrf控制学习率衰减到最终值的比例,调到 0.001 能让模型在最后几十个 epoch 里做更细的收敛。
4.3 用混淆矩阵和坏例反推数据问题
训练结束后,runs/detect/train/目录里会自动生成confusion_matrix.png、results.png、以及一批验证集预测坏例的保存图。我每次训练完的第一件事不是看 mAP,而是打开混淆矩阵看行列分布。
混淆矩阵的对角线是正确分类的占比,非对角线上的亮点就是混淆点。在驾驶行为检测里,最常见的混淆组合是phone_use和smoking,因为烟和手机的位置、手部姿态都很接近。如果看到这两个类别互相串,优先检查标注规范里的边界描述是否清晰,必要时把这个类别的样本单独拉出来重新标注,而不是去调模型。
另一个实用的验证方法是看labels.jpg,它会展示训练集里所有标注框的分布热图。如果发现手机、烟这类目标在画面中心区域特别集中,说明标注时的采样视角偏窄,模型对画面边缘的目标会先天弱势。这时应该补充边缘视角的数据,或者把区域采样做得更均衡。
我还会手动跑一次坏例可视化,命令是yolo detect predict在验证集上预测,把漏检和误检的图挑出来归类,一般逃不出四类:目标太小、严重遮挡、光照异常、类间相似。每一类对应不同的解法,比闷头调参高效得多。
5. 避坑指南:驾驶行为检测最常见的 5 个翻车现场
5.1 手机在画面里太小导致漏检
现象:训练完模型在车内实拍视频上跑,正常驾驶和抽烟都能检出,唯独手机经常漏掉,尤其是手垂在方向盘下方时。
原因:手机在 1280×720 的画面里可能只有 40×60 像素,属于严格意义上的小目标。640 输入分辨率下,P3 层分配给它的特征点太少,模型学不到稳定的表观特征,置信度普遍偏低,NMS 之后很容易被滤掉。
解决:把训练和推理的imgsz提到 960,这是立竿见影的做法,我的实测里小目标召回率能涨 3~5 个点。如果设备跑不动 960,就把画面按区域切成两块独立推理,比如左右各一半,变相提高目标占比,速度损失不大。
5.2 夜间与逆光场景误检率飙升
现象:白天测试集上 mAP50 有 88%,夜间视频一跑,检测框开始乱跳,频繁把方向盘反光、车窗投影误判成手机或烟。
原因:夜间和逆光场景的亮度分布、色彩偏移和白天差异极大,训练集里如果没有足够多的低光样本,模型学到的特征分布覆盖不到这些情况,就会去找亮度高、形状近似的小目标来凑数。
解决:最靠谱的办法是在训练集里混入 20%~30% 的夜间和逆光数据。如果没有现成样本,用图像增强模拟也行——降低亮度、加高斯噪声、提高对比度后再标注。另一个辅助做法是推理前加一层简单的自适应亮度校正,把暗部提亮后再送进模型。
5.3 手部遮挡导致检测框反复横跳
现象:驾驶员打电话时手偶尔挡住手机,检测结果一会儿检出、一会儿消失,单帧看起来时好时坏,报警逻辑也跟着抖。
原因:单帧检测天然只能看到当前画面,遮挡发生时目标特征不完整,模型输出置信度跌到阈值以下,下一帧遮挡消失又恢复。这在行为检测里不是模型缺陷,而是单帧检测的固有局限。
解决:不要用单帧判定行为,改成时序判定。对同一目标的检测结果做滑动窗口统计,连续 3 秒内命中帧数超过一半才触发报警。这个思路能过滤掉绝大多数遮挡导致的抖动,后面进阶章有具体代码。
5.4 打电话与看手机分不清:类间混淆
现象:confusion_matrix.png 里phone_use和smoking之间有一块明显的非对角亮点,看视频时模型把烟当成手机,或者把手机当成烟。
原因:烟和手机的形状接近、位置接近,都出现在手部附近,如果标注标准里没有明确区分「烟在指间、手机在手掌」,模型学到的是高度相似的特征,分错是必然的。
解决:两种路子,一是重新定义类别边界并回炉清洗标注,二是干脆合并这两个类别,把问题从「区分烟和手机」降级成「检测异常手持动作」。如果业务要求区分,就需要补充大量「手部特写」视角的样本,让模型有机会看到更细节的差异。
5.5 训练精度高、部署精度低:量化掉点
现象:训练时验证集 mAP50 90%,导成边缘设备使用的模型后掉到 80%,误报明显变多。
原因:FP32 权重转成 FP16 或 INT8 时,数值精度损失对置信度分布有影响。行为检测场景里小目标本身的置信度就低,量化后容易越过阈值边界,表现为漏检和误检同时增加。
解决:如果设备支持 FP16,优先用 FP16,掉点通常控制在 1 个点以内。INT8 需要准备一批有代表性的校准图片做量化校准,校准集最好包含夜间和逆光样本。还有一个经验:量化后别卡 0.25 的默认置信度阈值,重新在验证集上扫一遍阈值,往往能找回不少精度。
6. 进阶用法:把单帧检测升级成时序行为判别的三个技巧
驾驶行为检测到能稳定输出检测框只是第一步,上真实场景时你会发现,单帧结果没法直接用——遮挡、抖动、瞬时误检都会让报警逻辑过敏。我落地时通常会加三个时序手段。
第一个技巧是滑动窗口投票。对连续帧里同一行为类别的出现次数做统计,超过阈值才判定为真。按 5 FPS 抽帧、窗口 15 帧、命中 10 帧以上触发报警,能滤掉大量单帧噪声。
# 滑动窗口投票:连续 N 帧出现同一行为才触发报警 from collections import deque WINDOW_SIZE = 15 # 窗口大小,对应 3 秒(按 5 FPS 抽帧) TRIGGER_NUM = 10 # 窗口内至少 10 帧命中才报警 window = deque(maxlen=WINDOW_SIZE) def judge_behavior(frame_detections, target_cls): """frame_detections: 当前帧检测到的类别 id 列表""" hit = 1 if target_cls in frame_detections else 0 window.append(hit) if sum(window) >= TRIGGER_NUM: return True # 触发一次行为报警 return False第二个技巧是给报警加冷却时间和持续校验。触发报警后进入 10 秒冷却,冷却期内不再重复报警,避免一条违规弹五次提醒。同时,行为一旦持续存在,报警状态要保持到行为消失后再延迟 2 秒复位,防止驾驶员放下手机瞬间误收报警。
第三个技巧是把检测结果和触发帧的图一起缓存下来。报警时不只存当前帧,还要存触发前 3 秒的截图。这样人工复核时有完整的上下文,也能收集回来做后续模型的增量训练。这是我用得最多的闭环——报警系统本身就是一个持续的数据采集器。
导出模型时也有一点经验:训练完先用yolo export导出 ONNX,确认推理结果和 PyTorch 输出一致后再转成推理引擎格式,这个中间层能挡住八成部署问题。
# 导出 ONNX,确认中间格式无误 yolo export model=runs/detect/train/weights/best.pt format=onnx opset=12 # 再导出推理引擎格式(需要设备支持),half=True 开启 FP16 yolo export model=best.pt format=engine half=True device=0走到这一步,YOLOv8 驾驶行为检测就不只是「能跑」而是「能扛」了。我每次接新的行为检测需求,都会先问三件事:数据里有没有夜间样本、类别定义是否互斥、部署设备能不能跑 FP16。这三件事确认了,后面基本不会出大岔子。这个习惯帮我避过好几次返工,希望也能帮到你。
本文还有配套的精品资源,点击获取