ARTICLE DETAIL

资讯详情

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

道路车辆识别数据集YOLO训练实战与踩坑指南

道路车辆识别数据集YOLO训练实战与踩坑指南 简介面向道路交通监控与车辆识别检测任务提供覆盖自行车、电动自行车、摩托车、三轮车、面包车等9类常见道路车辆的YOLO格式标注数据。完整数据集共2534张图片按两部分拆分当前为第1部分适合YOLO全系列算法直接训练与试验并已划分训练/验证/测试集。压缩包内共2000个文件其中1999个txt标签文件对应各图像的检测框标注另有1个yaml文件定义类别与数据路径整体约634.3MB结构清晰无需额外转换即可加载使用。目前已有574人浏览学习可见其适配科研、课设与落地项目的实用价值。标注精准类别覆盖城市交通常见车型可直接用于交通违规检测、车流量统计等场景按YOLOv9训练可达到92.3%的准确率帮助用户快速获得有效的道路车辆识别基线结果也可作为算法改进与模型评估的基准数据。1. 9类道路车辆识别数据集2534张带yolo txt标签的图够不够用做道路车辆检测的人第一次卡住的通常不是模型结构而是没有带标注的数据。9类道路车辆识别检测数据集这个资源提供了2534张已经标注好的真实场景图像覆盖自行车、电动自行车、摩托车、三轮车、面包车等常见路面车辆类型并且每张图都配套yolo格式的txt标签解压之后可以直接进入YOLO训练流程不用自己去画框。适合正在做交通流量统计、路口监控分析或智慧城市试点手头缺标注样本的开发者。需要注意的是标题里写着“第1部分”说明这个数据集可能是分批发出的先拿到第一部分的人可以先把目录结构、训练流程和评价指标跑通盘顺等后续部分到位再合并训练降低试错成本。2. 拿到压缩包先别急着训练目录结构、标签格式与类别映射做目标检测最忌讳的是拿到数据包后直接双击一张图看一眼然后立刻去敲训练命令。我踩过这个大坑当年拿到一个行车记录仪数据集没检查标签就开训结果损失函数第一个epoch直接NaN整整排查了一下午最后发现是一个标签文件里某一行坐标写成负数。所以解压之后的前十分钟一定用来摸清楚数据的底细。2.1 解压后的目录结构怎么看先把压缩包解开再把目录结构打印出来unzip 9类道路车辆识别检测数据集2534张含yolo格式txt标签*第1部分.zip -d vehicle_dataset cd vehicle_dataset find . -maxdepth 2 -type d | sort第一行命令把zip包解压到 vehicle_dataset 目录。注意这里的-d参数它指定了解压目标目录避免把一堆文件直接散落在当前文件夹里。第二行的find -maxdepth 2 -type d只显示两层目录因为大多数数据集的嵌套层级不会超过这个深度。如果输出结果很乱就重点确认三件事images 和 labels 是不是平级、有没有自带 train/val 子目录、README 或类别说明文件是否存在。常见的布局有三种images/和labels/平级、图像和标签混在同一目录、已经按train/val划分好。第一种最常见后两种都需要在第3章做目录重排处理。这里多花五分钟第4章的训练配置就不用反复改路径。如果发现解压后文件特别多可以用ls -l | wc -l快速统计文件数量跟标题声称的2534张对一下。我见过有人拿到的所谓数据集实际只有2000张图其余都是重复文件这一步能帮你提前发现货不对板。2.2 YOLO txt标签的真实格式长什么样YOLO标签不是XML也不是JSON就是纯文本每一行代表图片里的一个目标框共5个字段类别id 归一化中心x 归一化中心y 归一化宽度 归一化高度读一个标签文件看看实际内容from glob import glob import os label_dir vehicle_dataset/labels label glob(os.path.join(label_dir, *.txt))[0] with open(label, r) as f: lines f.readlines() print(标签文件:, label) print(目标数量:, len(lines)) for line in lines[:5]: parts line.strip().split() if len(parts) 5: cls, x, y, w, h parts print(f类别:{cls} 中心x:{x} 中心y:{y} 宽:{w} 高:{h}) else: print(异常行:, line.strip())这段代码顺手把列数不是5的行也标出来了因为这是YOLO标签最容易被动手脚的位置。坐标全部是归一化后的浮点数范围应该在0到1之间。如果看到1.0532这种值说明目标框已经超出图像边界得标记出来准备清洗。类别id从0开始计数0就是类别说明文件里定义的第1个类别。2.3 用一条命令统计每个类别的样本分布了解类别分布对后续训练策略极其重要直接统计所有标签文件的第一列for f in vehicle_dataset/labels/*.txt; do awk {print $1} $f; done | sort | uniq -c | sort -rn解释一下外层遍历每个标签文件awk {print $1}取每行第一个字段也就是类别id合并排序后用uniq -c统计次数最后的sort -rn按次数从大到小排。输出像这样520 1 413 0 270 4第二列是类别id第一列是出现次数。如果看到某个类只有几十个样本、另一个类有上千个那就是典型的类别不平衡数据集后面第3章会专门处理。这里有个细节容易被忽略如果类别id不连续比如跳过2直接出现3要么重新映射为连续的0到N-1要么保证训练yaml里的names跟原始id对齐。实际项目中不连续id最容易让预测阶段类别编号对不上号。2.4 数据质量检查图像和标签能否一一对上我每次拿到新数据集都会全量检查一次2534张图规模下这种检查成本很低但能省掉你后面几天的排查时间import os from glob import glob from PIL import Image image_dir vehicle_dataset/images label_dir vehicle_dataset/labels images glob(os.path.join(image_dir, *)) label_stems {os.path.splitext(os.path.basename(p))[0] for p in glob(os.path.join(label_dir, *.txt))} image_stems {os.path.splitext(os.path.basename(p))[0] for p in images} # 检查一配对情况 only_img image_stems - label_stems only_lbl label_stems - image_stems print(f有图无标签: {len(only_img)}, 有标签无图: {len(only_lbl)}) # 检查二每张图能否正常打开 bad_imgs [] for img_path in images: try: with Image.open(img_path) as im: im.load() except Exception as e: bad_imgs.append((img_path, str(e))) if bad_imgs: print(损坏图像:, bad_imgs[:5]) else: print(所有图像可以正常打开) # 检查三标签坐标合法性 bad_labels [] for lbl_path in glob(os.path.join(label_dir, *.txt)): for line_num, line in enumerate(open(lbl_path), 1): parts line.strip().split() if len(parts) ! 5: bad_labels.append((lbl_path, line_num, 字段数不为5)) continue try: x, y, w, h map(float, parts[1:5]) except ValueError: bad_labels.append((lbl_path, line_num, 坐标包含非数字)) continue if not (0 x 1 and 0 y 1 and 0 w 1 and 0 h 1): bad_labels.append((lbl_path, line_num, f坐标越界: {line.strip()})) if bad_labels: print(f发现问题标签 {len(bad_labels)} 处前5条:) for item in bad_labels[:5]: print(item) else: print(标签坐标全部合法)这个脚本做了三件事配对检查、图像完整性检查、标签坐标合法检查。配对检查解决的是训练过程中那句Found no labels报错图像完整性检查过滤掉无法解码的文件坐标合法性检查直接避免损失函数NaN。2534张图全量跑一遍不会超过十几秒不要抽样。如果你在标签里发现少量越界框不要整张图删掉做一个边界裁剪把它修正过来好的目标框不影响使用。做完这一步数据集的底就算彻底摸清了。3. 把2534张图改成YOLO训练标准结构目录重排、随机划分与data.yaml生成YOLOv5和YOLOv8的训练器默认从 data.yaml 里获取数据集路径和类别信息。第2章解压出来的原始目录大概率不能直接拿来训练你得先把数据结构标准化。YOLO标准的数据集结构大概是yolo_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml3.1 为什么不直接改yaml路径而要重排目录很多新手会想既然yaml里能指定路径直接让 train 指到vehicle_dataset/images不就行了问题在于YOLO会默认在图像同级目录找标签或者根据给的两个路径去推算。当 images/labels 分开放、文件名还带不同扩展名时只要这样写了训练器就会报 label not found最后还是得回去改目录。与其在yaml上做各种绕弯不如一次性重排成标准结构后面所有脚本都能按同一个约定来写。3.2 目录重排与随机划分脚本写一个Python脚本完成三件事创建目标目录、随机打乱、按比例拷贝到train和valimport os import shutil import random from glob import glob random.seed(42) src_img vehicle_dataset/images src_lbl vehicle_dataset/labels dst yolo_dataset # 创建标准目录结构 for split in [train, val]: os.makedirs(os.path.join(dst, fimages/{split}), exist_okTrue) os.makedirs(os.path.join(dst, flabels/{split}), exist_okTrue) # 找出所有图像文件 images [p for p in glob(os.path.join(src_img, *)) if p.lower().endswith((.jpg, .jpeg, .png, .bmp))] random.shuffle(images) # 20%作为验证集其余训练 val_count int(len(images) * 0.2) val_images, train_images images[:val_count], images[val_count:] val_count max(val_count, 1) # 防止数据集过小时验证集为0 print(f训练集 {len(train_images)} 张验证集 {len(val_images)} 张) for name, img_list in [(train, train_images), (val, val_images)]: for img_path in img_list: stem os.path.splitext(os.path.basename(img_path))[0] ext os.path.splitext(img_path)[1].lower() # 图像和标签保持相同stem这是YOLO系列的文件匹配约定 shutil.copy(img_path, os.path.join(dst, fimages/{name}/{stem}{ext})) lbl_path os.path.join(src_lbl, f{stem}.txt) if os.path.exists(lbl_path): shutil.copy(lbl_path, os.path.join(dst, flabels/{name}/{stem}.txt)) else: print(f警告: {stem} 缺少标签)random.seed(42)固定随机种子保证每次划分结果一致这对后续复现实验非常重要。val_count取图像总数的20%2534张图大概会分到500张验证集。用shutil.copy而不是os.rename保留原始数据不动万一划分错了还能重新来。图像文件名保留了原始扩展名但建议你后续统一改成.jpg因为opencv对部分.bmp的兼容性有些诡异的问题。划分比例上20%是经验值。如果验证集太小置信区间就太宽评估结果不稳定可以考虑降到10%或直接做交叉验证。不过2534张图用20%问题不大。3.3 根据标签内容确认类别数量类别名单必须和标签里的id严格对应。如果数据集提供了 classes.txt 就直接读cat vehicle_dataset/classes.txt如果没提供就扫描所有标签文件里的最大类别idimport glob max_cls -1 for lbl in glob.glob(vehicle_dataset/labels/*.txt): with open(lbl) as f: for line in f: cid int(line.strip().split()[0]) max_cls max(cid, max_cls) print(类别总数:, max_cls 1) print(最大类别id:, max_cls)扫描结果直接决定你在yaml里要写多少类。有时候标签文件里某个类别一条都没有但标题说了有9类那你要看是不是这个类样本量刚好为0还是原始数据压根缺了这个类。如果是后者可以先按实际存在的类别训练等后续分卷补齐。3.4 写入data.yaml并验证用yaml库生成配置文件import os import yaml # 注意这里只是示例实际类别名以你解压出来的classes.txt为准 class_names [bicycle, electric_bicycle, motorcycle, tricycle, van, car, bus, truck, other_vehicle] data_cfg { path: os.path.abspath(yolo_dataset), train: images/train, val: images/val, names: {i: name for i, name in enumerate(class_names)}, } yaml_path os.path.join(yolo_dataset, data.yaml) with open(yaml_path, w, encodingutf-8) as f: yaml.dump(data_cfg, f, allow_unicodeTrue, sort_keysFalse) print(已写入:, yaml_path)三个参数需要较真path必须是绝对路径否则YOLO会基于当前工作目录拼接换个目录运行就找不到数据names必须是{0: xxx, 1: xxx}这种字典格式键从0开始连续allow_unicodeTrue是为了类别名如果带中文不报编码问题但类别名强烈建议用英文避免不同环境下字符集不一致。3.5 类别不平衡先跑baseline再决定处理方案用第2章的统计命令看分布如果自行车类有800多个框、三轮车只有60多个差距超过10倍就得认真考虑不平衡问题。我的处理思路分三步第一步不采样直接训练一版baseline。用验证脚本输出每个类别的AP看清楚哪个类最差。第二步如果小类确实差优先检查是不是标签错标比如电动自行车被标成摩托车这种系统性错误比采样更致命。第三步确认标签无误后再对少数类做复制增强或者调整mosaic增强的参与率但要注意过采样会导致过拟合。还要强调一点验证集划分在类别不平衡时也要保证各类别都有代表。2534张图的规模下数据多的大类随机划分基本能均匀分布但样本量只有50个的小类有可能全落到训练集或验证集。划分完用第2章的统计命令再跑一次如果某个类在验证集里一条都没有就换个随机种子重新分或者手动分几张进去。4. 用YOLOv8在本地跑通车辆检测训练最小命令、关键参数与训练监控第3章的目录结构和data.yaml都准备好了下面直接进入训练。现在YOLO系列做目标检测的主流选择是YOLOv8命令行工具很简洁适合快速验证数据集质量。4.1 环境准备ultralytics安装与CUDA确认YOLOv8的官方实现封装在 ultralytics 这个Python包里pip install ultralytics这个命令会自动带上pytorch、torchvision、opencv-python等依赖。但如果你的机器是老显卡pip默认装的pytorch可能是CPU版需要先确认import torch print(torch.cuda.is_available()) print(torch.__version__)输出第一行是 True 说明能用GPUFalse 就只能在CPU上练。2534张图的规模CPU训练也不是跑不动一个epoch可能5到10分钟100轮就是十几个小时能接受也行。但我的建议是能搞到GPU就用GPU别在CPU上耗时间。4.2 训练命令先小模型拿baseline激活Python环境后直接执行yolo train modelyolov8n.pt datayolo_dataset/data.yaml epochs100 imgsz640 batch16 device0 namevehicle_v1逐个拆解modelyolov8n.pt是YOLOv8的nano版本文件最小、速度最快适合首轮跑通流程。第一次运行会自动下载 yolo预训练模型到缓存目录如果网络不好下不动就手动把 yolov8n.pt 放到项目目录再运行一次命令行会优先使用本地权重。data指向第3章生成的yamlepochs100是这个量级数据集的合理上限实际收敛会更早imgsz640是速度和精度的平衡点后面会讲怎么用960提小目标召回batch16对应单卡16张图显存不够就改成8device0指定用第0块GPUnamevehicle_v1是实验任务名跑完结果全在runs/detect/vehicle_v1下面。4.3 训练过程看什么损失曲线和异常告警训练启动后终端会实时打印每轮的box_loss、cls_loss、dfl_loss和学习率。新手最容易犯的错是不看这些数值直接挂机睡觉。我建议至少盯住前10个epoch正常情况三个loss都应该是稳步下降的如果第一个epoch就出现nan停下来回到第2章把标签坐标再检查一遍不要浪费一整个晚上跑一个注定失败的实验。训练结束后runs/detect/vehicle_v1目录下会生成results.png里面画了损失曲线和mAP曲线。把这个图保存好后面每次调参都要跟它对比。4.4 两个最容易遇到的训练异常第一个是 yolo 训练中 bn 崩溃现象是loss突然从0.5跳到几万甚至更大。这本质上是批归一化层的统计量被极端输入破坏通常是大学习率和小batch配合不当造成的。解决方法是把初始学习率调小比如在命令行加上lr00.005或者把batch调大再试。2534张图这个量级、batch16的情况下bn崩溃不常见但一旦遇到不要怀疑模型先降学习率。第二个是训练结束后验证集mAP高、实际视频上效果差。这类问题多半不在训练环节而在类别不平衡和验证集划分上前面章节已经讲了这里不再重复。4.5 训练多久算够早停与技术指标判断YOLOv8命令行支持patience参数意思是验证集mAP连续多少个epoch没有提升就提前终止yolo train modelyolov8n.pt datayolo_dataset/data.yaml epochs100 imgsz640 batch16 patience20 device0 namevehicle_v1patience20表示连续20轮mAP没提升就自动停会把验证集指标最优的那轮权重保存为best.pt。2534张图的小数据集通常40到60轮已经收敛设patience能省掉后面四十轮的机器时间。训练结束后runs/detect/vehicle_v1/weights/下有两个文件last.pt是最后一轮权重best.pt是验证集表现最好的权重。做推理和部署时只用best.pt这是最容易踩错的地方——有人为了省事选了last结果精度比best低好几个点才发现。5. 避坑手册从标签越界到类别不平衡的5条踩坑记录第2章和第4章该讲的流程走完了下面集中把我在道路车辆数据集上亲身踩过的坑列出来方便你对照排查。每个坑都按「现象 → 原因 → 解决」三段式记录。5.1 标签越界导致box_loss出现NaN现象训练第一个epoch就报了NaN或者loss在前几个epoch正常、到第20个epoch突然变成NaN整个实验只能重来。原因标签txt里某个目标的x或w超过归一化范围比如1.2。YOLO做正负样本匹配时这种框会算出异常的正样本数数学上直接让损失变成无穷或NaN。解决用第2.4节的检查脚本扫一遍把所有越界行打出来。但注意不要直接把越界目标删掉因为那个目标可能是一辆真实存在的车。正确做法是反算回像素坐标裁剪到图像边界再重新归一化写回txtfrom PIL import Image import os def clip_label(img_path, lbl_path): with Image.open(img_path) as im: w_img, h_img im.size new_lines [] with open(lbl_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue # 跳过格式错误的行 cid, x, y, w, h int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) # 归一化坐标转像素坐标 px (x - w / 2) * w_img py (y - h / 2) * h_img pw w * w_img ph h * h_img # 裁剪到图像边界 left max(0, px) top max(0, py) right min(w_img, px pw) bottom min(h_img, py ph) if right - left 2 or bottom - top 2: continue # 裁剪后太小直接忽略 # 重新归一化 new_x (left right) / 2 / w_img new_y (top bottom) / 2 / h_img new_w (right - left) / w_img new_h (bottom - top) / h_img new_lines.append(f{cid} {new_x:.6f} {new_y:.6f} {new_w:.6f} {new_h:.6f}\n) with open(lbl_path, w) as f: f.writelines(new_lines)逻辑说明把归一化坐标换算成像素框用max/min把越界部分裁掉最后重新归一化写回。宽或高裁剪后小于2像素的目标说明它本身已经严重脱离视野留着反而干扰训练直接删除这一行即可。5.2 小目标车辆漏检三轮车在远处基本检不出来现象验证集整体跑分看着还行把模型放到路口视频上远处一个只有20像素高的三轮车完全没有框。原因2534张图像里的远距离小目标样本占比太低加上YOLOv8在stride32的大特征图上对小目标本身就弱。它是多尺度输出但小目标主要靠高分辨率特征层如果训练样本里的目标普遍偏大小目标这一支就训练不充分。解决尝试更大的推理尺寸imgsz960或1280目标在输入图像里的相对尺寸变大特征更有效。显存不够时把batch降到8。实测中小目标车辆的召回率往往能提升几个点。代价是训练时间变长显存占用变高。5.3 类别不平衡造成的mAP虚高现象训练结束打印mAP有0.78看起来不错细看per-class AP发现面包车只有0.31自行车的AP高达0.93。整体数字是被大类抬上去的。原因9类车辆样本量相差悬殊少数类只贡献了极少的正样本模型学到的特征不完整AP自然上不去。解决不要只看总mAP。用yolo val modelbest.pt datadata.yaml跑验证看它输出的per-class AP和混淆矩阵。某个类太差时先确认是不是标签系统性误标再考虑对该类复制样本或把该类单独提出来做增强。注意增强后重新训练要保持验证集和训练集的数据不重叠不要让同一个目标既进训练集又出现在验证集。5.4 图像与标签文件名不一致训练报找不到标签现象训练启动后日志里频繁出现no labels found或者验证时检测结果为0。原因拷贝图像时有人把image_001.JPG改成了image_001.jpg但标签文件名仍然是image_001.JPG.txt或大小写不一致。YOLO在Linux下严格区分大小写一个字符对不上就匹配失败。解决全量统一命名。推荐做法是在第3章重排阶段就把所有文件统一成小写扩展名、同名stem。命名规则只有一条图像的stem和标签的stem必须严格一致包括大小写和中间的下划线。脚本里加一个断言收尾assert label_stems image_stems不通过就停下来检查别带着问题往下走。5.5 训练正常但验证集mAP不稳定现象每次重新划分数据重新训练mAP波动超过2个点验证时还会看到混淆矩阵各类别行总和每次都不一样。原因数据量小的时候随机划分带来的验证集差异会被放大。如果某个类只有60个目标验证集分到20个还是40个对该类AP的影响极大。混淆矩阵行总和不固定正是因为验证集每次随机采样量不同不是bug是计数波动。解决固定随机种子并且每次实验用同一个划分文件。如果要对外报数字对同一个数据划分跑3次训练取平均更可靠。另一个办法是交叉验证把训练集切5份轮流做验证报告平均mAP和方差。这个做法对2534张图的小数据集特别合适代价是训练5次时间成本要提前评估。6. 模型落地用best.pt跑真实视频验证车辆计数与类别分布训练完拿到 best.pt下一步不是盯着mAP数字自我感动而是把模型接到实际视频上看看它在真实场景里的行为。我一般先用一段30秒的路口视频做冒烟测试统计9类车辆的出现次数再和人工数数对比偏差超过15%就回去调数据。6.1 用官方接口做视频车辆计数from ultralytics import YOLO import cv2 model YOLO(runs/detect/vehicle_v1/weights/best.pt) cap cv2.VideoCapture(street.mp4) class_totals {} # 类别名 - 计数 seen_ids set() # 已统计过的track id total 0 while cap.isOpened(): ret, frame cap.read() if not ret: break # track模式跨帧关联同一个目标稳定性优于反复predict results model.track(frame, persistTrue, imgsz640, conf0.35, iou0.5) if results[0].boxes.id is not None: boxes results[0].boxes ids boxes.id.cpu().numpy() cls_ids boxes.cls.cpu().numpy() names model.names for obj_id, c in zip(ids, cls_ids): if obj_id not in seen_ids: seen_ids.add(obj_id) class_name names[int(c)] class_totals[class_name] class_totals.get(class_name, 0) 1 total 1 annotated results[0].plot() cv2.imshow(vehicle_count, annotated) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows() print(检测到的车辆总数:, total) for name, count in sorted(class_totals.items()): print(f{name}: {count})这段脚本的关键在model.track它内部会调用ByteTrack做跨帧关联返回的目标id在连续帧之间保持一致。用track id去重同一辆车只在第一次出现时计数避免每帧重复累加。conf0.35是经验阈值漏检多就调到0.25误检多就调到0.5。iou0.5是NMS参数目标密集时可以升到0.7减少重叠框被误删的情况。6.2 重点验证电动自行车和摩托车这两个易混淆类9类里外观差异明显的自行车、三轮车、面包车通常不会出大问题最容易翻车的是电动自行车和摩托车二者轮廓高度相似。跑完视频后我会单独把这两个类的检测框截图抽出来看如果混淆矩阵里这两个类互串大概率是标签本身有系统性误标。这时最有效的操作不是调模型而是抽20到30张出错样本回看原图确认是不是标注阶段把背着小货箱的电瓶车标成了摩托车。6.3 模型上线前把推理尺寸和阈值钉死进配置最后提醒一个生产环境习惯部署时的imgsz必须和验证时一致否则同一模型在不同输入尺寸下的mAP会漂移。我见过不少人训练用960、验证用640然后在线上骂模型不稳定这种问题全是自己造成的。写配置时把imgsz、conf、iou三个参数钉死放进项目的config文件里部署代码只读配置不写死数字后续换模型就不用改代码。2534张图的第1部分入门阶段完全够用。先把这套流程跑熟等后续分卷的数据到位合并训练时只需要把第3章的源路径更新一下第4章的data.yaml如果类别没变可以直接复用。到那时候你已经有一套固定的数据处理脚本和训练策略扩数据、扩类别的成本会低很多。希望今天的分享对你有帮助卡数据格式的坑早点跳过去把精力放在模型调优和真实场景落地这些更重要的事情上。本文还有配套的精品资源点击获取
返回列表