简介:这是一份面向智能驾驶与车载行为识别方向的图像数据集,主要用于训练模型判断驾驶员在行车过程中是否存在接打电话、玩手机等分心行为,适合计算机视觉入门者、算法工程师及交通安全相关课题研究者使用。压缩包共包含2000个文件,其中1997张jpg图像构成训练主体,另有3个json标注文件,采用coco json格式,可直接对接主流目标检测框架进行训练与评估,整体包体约82.14MB,体积轻便便于快速下载与本地部署。目前已有226人学习下载,具备一定的参考热度。数据集覆盖打电话、玩手机等典型驾驶分心场景,图像命名与标注结构清晰,便于按类别划分训练集与验证集,读者可据此搭建分心驾驶检测模型,完成从数据加载、标注解析到模型训练与效果验证的完整流程,也可用于迁移学习或数据增强实验,为驾驶安全监测类项目提供可直接复用的数据基础。
1. 1708 张图能干什么:驾驶接打电话识别的数据底座
1708 张训练图,放在动辄几十万张的自动驾驶数据集里,确实不算多。但如果你要做的不是通用障碍物检测,而是「驾驶员有没有在接打电话、玩手机」这个具体动作,1708 张精标图反而是个能跑通的起点。这个数据集的核心价值在于:它把「打电话」「玩手机」这两个高频危险驾驶行为单独拎出来做了标注,并且直接给出 COCO JSON 格式,省掉了从 VOC、YOLO 格式来回转换的折腾。COCO JSON 是目标检测里最通用的交换格式之一,主流框架基本都能直接吃。
它适合谁?一是做车载 DMS(驾驶员监控系统)原型的团队,想快速验证「接打电话识别」这条链路能不能跑通;二是学生或独立开发者,手头没有大规模采集条件,需要一个能直接训练、能出指标的小数据集;三是做边缘部署的工程师,想拿一个小模型在车机或 Jetson 上试推理速度。不适合谁?想直接上量产、要求极高召回和复杂光照鲁棒性的场景,1708 张的覆盖度肯定不够,得靠后续增采和合成补。
这篇文章不讲空话,从数据集的目录结构、COCO JSON 字段含义,一路讲到用 YOLOv8 训练、评估、导出 ONNX,再到实际部署时那几个容易翻车的参数。中间会穿插我踩过的坑,比如类别不平衡、小目标漏检、JSON 里 bbox 越界这些血泪经验。读完你至少能判断:这个方向值不值得投入,以及怎么用最小成本跑出第一版可用模型。
2. 拆开 COCO JSON:1708 张图里到底标了什么
2.1 COCO JSON 的四个核心字段与驾驶场景的对应关系
COCO JSON 不是随便一个 JSON,它有固定结构。拿到数据集先别急着训练,用几行代码把结构摸清楚,能省掉后面一半的报错。核心就四个字段:images、annotations、categories、info。images里每条记录对应一张图,包含id、file_name、width、height;annotations里每条对应一个标注框,包含image_id、category_id、bbox、area、iscrowd;categories定义类别名和 id 的映射;info是元信息,通常不重要。
驾驶接打电话场景里,categories一般就两类:phone_call(接打电话)和playing_phone(玩手机),有的数据集会合并成phone一类。bbox是[x, y, width, height],注意是左上角坐标加宽高,不是[x1, y1, x2, y2]。这个区别在转换 YOLO 格式时如果搞反,框会全部错位,模型直接学废。
import json with open("annotations/instances_train.json", "r", encoding="utf-8") as f: data = json.load(f) print("图片数:", len(data["images"])) print("标注数:", len(data["annotations"])) print("类别:", [(c["id"], c["name"]) for c in data["categories"]]) # 统计每个类别的框数量,看是否严重不平衡 from collections import Counter cat_counter = Counter(ann["category_id"] for ann in data["annotations"]) print("各类别框数:", cat_counter) # 检查 bbox 是否越界 for ann in data["annotations"]: img = next(i for i in data["images"] if i["id"] == ann["image_id"]) x, y, w, h = ann["bbox"] if x < 0 or y < 0 or x + w > img["width"] or y + h > img["height"]: print("越界框:", ann["id"], ann["bbox"], img["file_name"])这段代码做了三件事:确认数据量、统计类别分布、排查越界框。参数上,encoding="utf-8"必须加,否则中文路径或类别名可能报错。cat_counter如果发现两类数量差三倍以上,训练时就要考虑加权或过采样。越界框哪怕只有几个,也会让某些框架在计算 loss 时直接 NaN,提前修掉比训练中途崩掉划算。
2.2 从 COCO 到 YOLO 格式:转换脚本与四个边界坑
YOLOv8 虽然支持直接读 COCO JSON,但实际训练时转成 YOLO txt 格式更稳,加载快、排查方便。转换逻辑不复杂:把[x, y, w, h]归一化成[cx, cy, w, h],再除以图片宽高。但边界坑不少,我列四个最常见的。
第一个坑:iscrowd为 1 的框。COCO 里iscrowd=1表示这是密集区域,不是单个实例。驾驶场景里如果标注员把挡风玻璃反光误标成 crowd,转 YOLO 时应该跳过,否则模型会学到一个巨大的模糊框。第二个坑:类别 id 不连续。COCO 的category_id可能是 1 和 5,但 YOLO 要求从 0 开始连续。必须建映射表,不能直接用原 id。第三个坑:图片文件名带空格或中文。YOLO 训练时按 txt 里的路径找图,空格会被截断,建议统一重命名成000001.jpg这种。第四个坑:空标注图。有些图没有目标,COCO 里不会出现在annotations,但 YOLO 需要一张对应的空 txt,否则会被当成负样本漏掉。
import os, json from pathlib import Path def coco_to_yolo(json_path, img_dir, out_dir): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) # 建立 image_id -> image_info 映射 img_map = {img["id"]: img for img in data["images"]} # 类别 id 重映射,从 0 开始 cat_ids = sorted([c["id"] for c in data["categories"]]) cat_map = {old: new for new, old in enumerate(cat_ids)} out_dir = Path(out_dir) out_dir.mkdir(parents=True, exist_ok=True) # 先给每张图建空 txt,保证负样本也有文件 for img in data["images"]: stem = Path(img["file_name"]).stem (out_dir / f"{stem}.txt").touch() for ann in data["annotations"]: if ann.get("iscrowd", 0) == 1: continue # 跳过 crowd 区域 img = img_map[ann["image_id"]] w_img, h_img = img["width"], img["height"] x, y, w, h = ann["bbox"] # 裁剪到图像范围内,防止越界 x = max(0, min(x, w_img - 1)) y = max(0, min(y, h_img - 1)) w = min(w, w_img - x) h = min(h, h_img - y) cx = (x + w / 2) / w_img cy = (y + h / 2) / h_img nw = w / w_img nh = h / h_img cls = cat_map[ann["category_id"]] stem = Path(img["file_name"]).stem with open(out_dir / f"{stem}.txt", "a") as f: f.write(f"{cls} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n") coco_to_yolo("annotations/instances_train.json", "images/train", "labels/train")逻辑说明:先建空 txt 解决负样本问题;iscrowd跳过;坐标裁剪防止越界;归一化保留 6 位小数,精度足够。参数上,cat_map用sorted保证映射稳定,不要用字典遍历顺序。如果数据集里phone_call和playing_phone要合并,在cat_map里把两个 old id 映射到同一个 new id 即可。转换完抽查几张图的 txt,用matplotlib画框叠在原图上,确认没偏。
2.3 数据划分与类别不平衡的处理策略
1708 张图不能全拿去训练,得划训练集、验证集,比例常见 8:2 或 7:3。划分时要注意:同一个驾驶员、同一段视频抽出来的帧不能同时出现在训练和验证集,否则验证指标虚高,实际部署翻车。如果数据集没给视频来源信息,至少按文件名前缀或时间戳做分组划分。
类别不平衡在这个数据集里很常见:打电话的样本往往比玩手机多,因为打电话动作更明显、更容易采集。处理策略有三档:轻度不平衡(比例 2:1 以内)直接训,YOLOv8 自带的数据增强能缓解;中度(3:1 到 5:1)用copy_paste增强少数类,或者训练时给少数类更高cls权重;重度(10:1 以上)建议先补采,硬训出来的模型对少数类召回会很难看。
import random from pathlib import Path imgs = sorted(Path("images/all").glob("*.jpg")) random.seed(42) random.shuffle(imgs) split = int(len(imgs) * 0.8) train_imgs, val_imgs = imgs[:split], imgs[split:] for name, subset in [("train", train_imgs), ("val", val_imgs)]: with open(f"{name}.txt", "w") as f: for p in subset: f.write(str(p.resolve()) + "\n")这段生成 YOLOv8 需要的 txt 列表,每行一个绝对路径。seed=42保证可复现。如果要做分组划分,把shuffle换成按前缀分组后再分配。验证集至少留 200 张,否则指标波动大,今天 mAP 0.7 明天 0.6,没法判断改动是否有效。
3. 用 YOLOv8 把 1708 张图训到能用的水平
3.1 环境配置与最小训练命令
YOLOv8 的训练链路是目前小数据集最省心的选择,ultralytics包把数据加载、增强、评估都封好了。环境上,Python 3.9 到 3.11 都行,PyTorch 装对应 CUDA 版本。如果只有 CPU,1708 张图训 100 epoch 大概要几小时,能忍;有张 8G 显存的卡,batch 16 跑起来很舒服。
先建数据配置文件phone.yaml,告诉 YOLO 去哪找图和标签:
path: /data/phone_dataset train: train.txt val: val.txt names: 0: phone_call 1: playing_phonepath是数据集根目录,train和val是相对路径的 txt 列表。names必须和转换时的类别顺序一致,错一个顺序整个模型就废了。然后一行命令开训:
yolo detect train \ data=phone.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/phone \ name=exp1参数说明:model=yolov8n.pt用 nano 版,1708 张图从零训容易过拟合,用预训练权重迁移学习更稳。imgsz=640是默认输入尺寸,如果原图分辨率高、手机目标小,可以提到 960,但显存翻倍。lr0=0.01是初始学习率,小数据集建议降到 0.005 到 0.01 之间。patience=20表示 20 epoch 没提升就早停,省时间。batch=16根据显存调,爆显存就减到 8。
3.2 三个必调参数:imgsz、batch、学习率
imgsz是影响小目标召回最直接的参数。驾驶场景里,手机在画面中可能只占几十个像素,640 输入下经过下采样后特征几乎消失。我一般会先跑一版 640 看指标,如果playing_phone的召回明显低于phone_call,就把imgsz提到 960 或 1280 再训一版对比。代价是显存和推理时间增加,部署时要权衡。
batch不只是显存问题,还影响 BatchNorm 的统计稳定性。小数据集用太小的 batch(比如 4)会让 BN 层统计量抖动,训练 loss 震荡。8G 显存下 640 输入,batch=16通常能跑;如果开了mosaic增强,显存占用会再高一点,可以降到 12。batch和lr0要联动:batch 翻倍,学习率可以适当乘 1.5,但小数据集别激进,稳一点。
学习率策略上,YOLOv8 默认用余弦退火,lr0是峰值。小数据集容易过拟合,我一般把lr0设成 0.005 到 0.01,lrf(最终学习率比例)保持默认 0.01。如果训练 loss 前几个 epoch 就掉到很低、验证 mAP 不涨,说明学习率太大,模型直接记住了训练集。这时候降lr0到 0.001 重训,往往验证指标更好。
3.3 训练过程看什么:loss 曲线与 mAP 的读法
训练启动后,runs/phone/exp1/下会有results.csv和 TensorBoard 日志。重点看三个量:train/box_loss、train/cls_loss、metrics/mAP50。box_loss下降说明框回归在收敛;cls_loss下降说明分类在学;mAP50是 IoU 0.5 下的平均精度,驾驶场景里这个指标到 0.85 以上算能用,0.9 以上算不错。
如果box_loss降但mAP50不涨,通常是过拟合,看验证 loss 是不是在涨。如果cls_loss震荡不降,检查类别是否标错,或者两类样本外观太像。我遇到过一次playing_phone和phone_call混淆严重,最后发现是标注时把「手持手机看屏幕」和「手机贴耳」标反了,改完标注重训,mAP 直接涨了 8 个点。所以指标不涨先别怪模型,回去查标注。
import pandas as pd df = pd.read_csv("runs/phone/exp1/results.csv") df.columns = df.columns.str.strip() print(df[["epoch", "train/box_loss", "train/cls_loss", "metrics/mAP50", "metrics/mAP50-95"]].tail(10))读results.csv时注意列名可能带空格,先strip。看最后 10 个 epoch 的指标,如果mAP50还在缓慢涨,可以多训 50 epoch;如果已经平了甚至降,早停是对的。mAP50-95更严格,驾驶场景里能到 0.6 以上就算框得比较准了。
4. 推理、导出与部署:从 PyTorch 到 ONNX 的落地链路
4.1 单图推理与置信度阈值的选择
训练完先别急着导出,用 PyTorch 权重跑几张验证图,肉眼看看框得准不准。YOLOv8 推理接口很简单:
from ultralytics import YOLO model = YOLO("runs/phone/exp1/weights/best.pt") results = model.predict( source="test_images/", conf=0.25, iou=0.45, imgsz=640, save=True, project="runs/predict", name="test1" )conf=0.25是置信度阈值,低于这个的框不输出。驾驶场景里,漏检比误检更危险,所以conf可以降到 0.15 到 0.2,宁可多报几个让后处理过滤。iou=0.45是 NMS 的 IoU 阈值,两个框重叠超过这个值就合并。如果发现同一个手机被框了两次,把iou降到 0.3 到 0.4。imgsz要和训练时一致,训练用 960 推理用 640 会导致小目标漏检。
4.2 导出 ONNX 与推理速度实测
部署到车机或边缘盒子,PyTorch 太重,导出 ONNX 是常见做法。YOLOv8 一行命令导出:
yolo export model=runs/phone/exp1/weights/best.pt format=onnx imgsz=640 opset=12 simplify=Trueopset=12兼容性最好,simplify=True会做图优化,去掉冗余算子。导出后用onnxruntime测速:
import onnxruntime as ort import numpy as np import time sess = ort.InferenceSession("best.onnx", providers=["CPUExecutionProvider"]) input_name = sess.get_inputs()[0].name dummy = np.random.randn(1, 3, 640, 640).astype(np.float32) # 预热 for _ in range(5): sess.run(None, {input_name: dummy}) start = time.time() for _ in range(50): sess.run(None, {input_name: dummy}) print("平均推理耗时: %.2f ms" % ((time.time() - start) / 50 * 1000))CPU 上 yolov8n 640 输入大概 30 到 60 ms,GPU 上 5 到 10 ms。如果部署目标有 NPU 或 DSP,还得转成对应格式,但 ONNX 是中间站,先确保 ONNX 推理结果和 PyTorch 一致。对比方法:同一张图分别用 PyTorch 和 ONNX 推理,看框的坐标和置信度差异,一般小数点后两位内算正常。
4.3 部署时的预处理对齐:别让 letterbox 坑了你
训练时 YOLOv8 会对图片做 letterbox:保持长宽比缩放,短边补灰边到 640。部署时如果预处理不一致,比如直接 resize 到 640x640 拉伸变形,框会系统性偏移。这个坑很隐蔽,因为模型还是能出框,只是位置不准,肉眼看单张图不容易发现。
对齐方法:部署代码里复现 letterbox。计算缩放比例r = min(640/w, 640/h),新尺寸new_w = round(w*r)、new_h = round(h*r),然后 padding 到 640。推理完把框坐标按r和 padding 偏移还原回原图。如果用的是 ONNX 且导出时带了dynamic轴,输入尺寸可以变,但 letterbox 逻辑还是要自己写。我一般会在部署前用同一张图跑 PyTorch 和部署端,把框画出来叠一起,偏差超过 5 个像素就回去查预处理。
5. 避坑与排查:1708 张图训练时最容易翻车的五件事
5.1 现象:mAP 卡在 0.5 上不去,loss 也不降
原因:最常见的是标签格式错。COCO 转 YOLO 时如果忘了归一化,或者bbox当成[x1,y1,x2,y2]处理,框会全部错位。另一种可能是phone.yaml里names顺序和 txt 里的类别 id 对不上,模型学的是反的。
解决:抽 5 张图,把 txt 里的框画回原图,肉眼确认。再检查names映射。如果都对,看cls_loss是否异常高,高的话多半是类别标错,回去抽查标注。
5.2 现象:验证集 mAP 很高,实际视频推理漏检严重
原因:训练验证集划分时,同一段视频的帧被分到了两边,验证集等于见过。实际视频里角度、光照一变,模型就懵。
解决:按视频来源或时间戳分组划分,确保验证集是「没见过」的场景。如果数据集没给来源信息,至少按文件名前缀分组。另外,验证时用conf=0.25,实际部署可以降到 0.15 再测召回。
5.3 现象:训练到一半 loss 变 NaN
原因:越界框或宽高为 0 的框。COCO 里偶尔有w=0或h=0的标注,归一化后除零。或者图片文件损坏,解码出来是空数组。
解决:转换脚本里加裁剪和过滤,w或h小于 1 像素的直接跳过。训练前用PIL遍历一遍图片,能打开的才留下。NaN 一旦出现,只能从上一个 checkpoint 重启,所以提前过滤比事后救火强。
5.4 现象:playing_phone召回率明显低于phone_call
原因:两类样本数量不平衡,或者playing_phone的目标更小、更模糊。驾驶场景里玩手机往往是低头看,手机被手遮挡,特征不如打电话明显。
解决:先统计两类框数,差 3 倍以上就过采样少数类。增强上开copy_paste=0.3,把少数类目标贴到其他图上。如果还不行,把imgsz提到 960,小目标特征保留更多。最后手段是合并两类成一类phone,先保证检出,再在业务层用姿态或位置区分。
5.5 现象:ONNX 推理结果和 PyTorch 不一致
原因:预处理没对齐,最常见的是 letterbox 的 padding 值不对。PyTorch 用 114 灰度填充,部署时如果用了 0 填充,边缘区域特征会变。另一个是归一化,PyTorch 里是/255,部署时如果忘了,输入值域差 255 倍。
解决:把预处理步骤逐行对照 ultralytics 的LetterBox实现,padding 值设 114,归一化除以 255。导出 ONNX 时加simplify=True,有时能消掉一些算子差异。最后用同一张图对比输出,框坐标差在 1 到 2 像素内可接受。
6. 把 1708 张图用出 17000 张的效果:增采与合成的小技巧
1708 张图训出来的模型,边界很清楚:光照单一、角度固定、驾驶员衣着类似的场景下能用,换个车型或夜间红外就掉点。想往上走,不一定非要重新采几千张,几个技巧能把现有数据榨出更多价值。
第一,用训练好的模型去跑未标注的视频,把高置信度的框导出来,人工修正后加入训练集。这叫半自动标注,我一般用conf=0.5筛一遍,能省 60% 以上的标注时间。第二,用copy_paste把手机目标抠出来,随机贴到其他驾驶场景图上,注意贴的时候要匹配光照和透视,否则模型学到的是「贴纸」而不是「手机」。第三,如果手头有红外或夜间数据,哪怕只有几百张,也单独训一版,然后用模型融合或分场景切换,比硬混在一起训效果好。
验证增采有没有用,别只看 mAP。建一个「困难集」:夜间、逆光、戴帽子、手机被遮挡各挑 20 张,每次改动后跑这个集,看召回变化。困难集召回涨了,才是真涨。我自己的习惯是,每次准备加数据前,先跑一遍困难集,记下基线,加完再跑,涨不到 3 个点就不值得扩。这个习惯帮我省了很多无效标注。
最后说一句,1708 张图做接打电话识别,够跑通原型,不够直接量产。但原型跑通的价值在于:你能拿到真实的失败案例,知道该补什么数据、该调什么参数。这比一上来就堆几万张图、训一个月不知道问题在哪,要划算得多。希望帮到你。
本文还有配套的精品资源,点击获取