ARTICLE DETAIL

资讯详情

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

YOLO车辆检测数据集:VOC/COCO/YOLO格式转换与训练全流程

YOLO车辆检测数据集:VOC/COCO/YOLO格式转换与训练全流程 简介本资源面向计算机视觉入门与进阶开发者提供一套真实场景下的YOLO车辆分类检测数据集可用于目标检测模型的训练、验证与调优适合课程设计、毕业设计及算法练习等场景。压缩包共2000个文件约51.91MB包含1250个xml标注文件、741个txt标签文件以及少量html教程、py脚本与yaml配置文件分别对应VOC、COCO和YOLO三种标签格式可直接接入YOLO系列检测框架。资源附赠数据集划分脚本支持按需生成训练集、验证集与测试集并配有环境搭建与训练教程帮助读者快速跑通从数据准备到模型训练的全流程。目前已有621人学习下载标注质量较高场景覆盖丰富能有效减少数据清洗与格式转换的重复工作为车辆检测任务提供开箱即用的数据基础。1. 从一份车辆数据集说起1000 张图、三种标签格式到底解决了什么做车辆检测项目的人十有八九卡在同一个地方模型代码抄好了环境也配通了结果手里没有一份能直接喂进去的数据。公开数据集要么类别太粗只有 car 一个类要么标注格式跟手头框架对不上要么图片数量少到训不出东西。这份「YOLO 车辆分类检测数据集」把三件事一次性打包了1000 张车辆图片、voc/coco/yolo 三种格式标签、划分脚本和训练教程。它解决的不是算法问题而是从「有图」到「能训」之间那段最磨人的脏活。这份数据适合谁刚入门目标检测、想跑通第一个自定义数据集的新手需要快速验证某个 YOLO 改进点、不想在数据准备上耗时间的熟手以及做车辆计数、违章抓拍、停车场管理等落地场景、需要一份可复现基线数据的工程师。1000 张的规模不算大但足够跑通全流程、验证 pipeline 是否正确。真正值钱的是那套格式转换和划分脚本——它把 VOC 的 XML、COCO 的 JSON、YOLO 的 TXT 之间的映射关系摊开给你看理解了这套映射以后换任何数据集都是同一套逻辑。2. 三种标签格式的底层差异为什么同一批图要存三份2.1 VOC、COCO、YOLO 的坐标表示到底差在哪很多人以为三种格式只是文件后缀不同其实坐标系的定义完全不一样搞混了就是框全歪。VOC 用绝对像素坐标存的是左上角和右下角两个点(xmin, ymin, xmax, ymax)写在 XML 里一个目标一个object节点。COCO 用绝对像素坐标但存的是左上角加宽高(x, y, width, height)全部塞进一个 JSON 的annotations数组靠image_id和category_id关联。YOLO 用归一化坐标存的是中心点加宽高(x_center, y_center, width, height)四个值都除以图片宽高范围落在 0 到 1 之间一个目标一行 TXT。差异的根源在于训练时框架怎么读数据。YOLO 系列在数据加载阶段直接读 TXT归一化坐标省去了运行时除以图片尺寸的开销还能天然适配不同分辨率的输入。VOC 和 COCO 更多是标注工具和历史数据集的默认输出转换到 YOLO 是必经一步。理解这一点你就明白为什么数据集要同时给三份——VOC 方便用 LabelImg 继续改标COCO 方便接 mmdetection 这类框架YOLO 直接开训。格式坐标类型存储结构典型用途VOC绝对像素 (xmin,ymin,xmax,ymax)每图一个 XMLLabelImg 标注、历史数据集COCO绝对像素 (x,y,w,h)单个 JSON 汇总mmdetection、COCO 预训练权重YOLO归一化 (xc,yc,w,h)每图一个 TXTYOLOv5/v8 直接训练2.2 从 VOC 转 YOLO转换脚本的四个关键步骤转换的核心就一件事把绝对坐标读出来除以图片宽高再算中心点。但实操里有几个地方容易翻车下面这段脚本把关键逻辑都标出来了。import os import xml.etree.ElementTree as ET from PIL import Image # 类别映射必须和 data.yaml 里的 names 顺序完全一致 CLASS_MAP {car: 0, bus: 1, truck: 2, van: 3} def voc_to_yolo(xml_path, img_dir, out_dir): tree ET.parse(xml_path) root tree.getroot() # 从 XML 里拿图片尺寸不要用 PIL 再读一遍省 IO size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) img_name root.find(filename).text lines [] for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in CLASS_MAP: continue # 未定义类别直接跳过避免训练时报 index 越界 cls_id CLASS_MAP[cls_name] bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 关键先裁剪到图片边界内再归一化 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) xc (xmin xmax) / 2.0 / img_w yc (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}) # 写 TXT文件名和图片名保持一致只换后缀 txt_name os.path.splitext(img_name)[0] .txt with open(os.path.join(out_dir, txt_name), w) as f: f.write(\n.join(lines)) # 批量处理 for xml_file in os.listdir(annotations): if xml_file.endswith(.xml): voc_to_yolo(os.path.join(annotations, xml_file), images, labels)逻辑说明CLASS_MAP是整条链路最容易出错的地方它决定了类别 id 的分配必须和训练时data.yaml里的names列表顺序严格对应否则模型学出来的类别全是错的。坐标裁剪那两行是后悔药——标注时手抖把框拖出图片边界是常事不裁剪的话归一化后会出现大于 1 或小于 0 的值YOLO 训练时虽然不一定报错但会引入噪声框。保留六位小数是精度和文件体积的折中四位在小目标上会有可见偏差。参数说明img_w、img_h从 XML 的size节点读取这是 VOC 标注时自动写入的比用 PIL 重新打开图片快得多。如果你的 XML 里没有 size 节点某些标注工具会省略就得改成Image.open()读取记得加异常处理。out_dir要和图片目录分开YOLO 训练时通过路径替换找标签混在一起容易出问题。2.3 COCO 转 YOLO 的坑category_id 不连续怎么办COCO 的category_id经常不是从 0 开始的连续整数比如只有 1、3、7 三个类别。直接拿它当 YOLO 的类别 id 会出大问题——YOLO 要求类别 id 从 0 连续编号中间断了会导致训练时类别数对不上。正确做法是建一个映射表把原始category_id重映射到 0 开始的连续整数。import json with open(annotations.json) as f: coco json.load(f) # 建立原始 id 到连续 id 的映射 cat_ids sorted([c[id] for c in coco[categories]]) id_map {old: new for new, old in enumerate(cat_ids)} # 同时保留类别名方便写 data.yaml names [c[name] for c in sorted(coco[categories], keylambda x: x[id])] # 按 image_id 聚合标注 from collections import defaultdict img_anns defaultdict(list) for ann in coco[annotations]: img_anns[ann[image_id]].append(ann) # 建立 image_id 到文件名的映射 img_info {img[id]: img for img in coco[images]} for img_id, anns in img_anns.items(): info img_info[img_id] w, h info[width], info[height] lines [] for ann in anns: cls_id id_map[ann[category_id]] x, y, bw, bh ann[bbox] # COCO 的 bbox 是左上角宽高 xc (x bw / 2) / w yc (y bh / 2) / h lines.append(f{cls_id} {xc:.6f} {yc:.6f} {bw/w:.6f} {bh/h:.6f}) txt_name os.path.splitext(info[file_name])[0] .txt with open(flabels/{txt_name}, w) as f: f.write(\n.join(lines))逻辑说明id_map是这段脚本的灵魂。COCO 的category_id是全局唯一标识可能因为数据集合并、类别删除等原因变得不连续而 YOLO 的类别 id 必须是 0 到nc-1的连续整数。names列表的顺序也要跟着id_map走写data.yaml时直接用它。COCO 的 bbox 格式是[x, y, width, height]注意不是[xmin, ymin, xmax, ymax]转换时别搞反。参数说明ann[bbox]里的宽高是绝对像素值除以图片宽高得到归一化值。COCO 的images数组里每张图都有width和height字段直接用不要另外读图。如果数据集里存在iscrowd1的标注建议跳过这类是密集人群之类的区域标注不适合当普通检测目标训。3. 数据集划分脚本为什么随机划分会害了你3.1 训练集验证集测试集的正确切分逻辑拿到 1000 张图很多人第一反应是random.shuffle然后按 8:1:1 切。如果这 1000 张图是从视频里抽帧来的相邻帧几乎一模一样随机切分会导致训练集和验证集里出现高度相似的图片验证集精度虚高上线就翻车。正确做法是先按场景或视频来源分组再在组级别做划分保证同一组的图片只出现在一个集合里。import os import random import shutil from collections import defaultdict random.seed(42) # 固定种子保证每次划分结果一致方便复现 img_dir images label_dir labels out_dir dataset # 按文件名前缀分组假设命名规则是 场景_编号.jpg groups defaultdict(list) for f in os.listdir(img_dir): if f.endswith((.jpg, .png)): group_key f.split(_)[0] # 按场景前缀分组 groups[group_key].append(f) group_keys list(groups.keys()) random.shuffle(group_keys) # 按组划分8:1:1 n len(group_keys) train_g group_keys[:int(n * 0.8)] val_g group_keys[int(n * 0.8):int(n * 0.9)] test_g group_keys[int(n * 0.9):] def copy_files(group_list, split): for g in group_list: for f in groups[g]: # 复制图片 shutil.copy(os.path.join(img_dir, f), os.path.join(out_dir, images, split, f)) # 复制对应标签 txt os.path.splitext(f)[0] .txt src_label os.path.join(label_dir, txt) if os.path.exists(src_label): shutil.copy(src_label, os.path.join(out_dir, labels, split, txt)) copy_files(train_g, train) copy_files(val_g, val) copy_files(test_g, test)逻辑说明groups字典按文件名前缀把图片分组这是最朴素也最有效的去泄漏手段。如果你的图片命名没有规律退而求其次可以用图片的感知哈希pHash做聚类把相似图片分到同一组。random.seed(42)是血泪经验——不固定种子的话每次跑划分脚本结果都不一样实验没法复现调参调到最后自己都糊涂。参数说明group_key f.split(_)[0]假设文件名格式是场景_编号.jpg如果你的命名规则不同改这一行就行。划分比例 8:1:1 是常规做法数据量小于 500 张时建议改成 7:2:1验证集太小的话评估指标波动会很大。out_dir下的目录结构要符合 YOLO 的默认约定images/train、images/val、labels/train、labels/valYOLOv8 会自动按这个结构找标签。3.2 划分后必须做的三项校验划分完不是就完事了直接开训大概率会遇到标签找不到、类别对不上、空标签文件这些问题。下面三项校验每次划分后都跑一遍能省下大量排查时间。第一项图片和标签一一对应。遍历images下每个文件检查labels下是否有同名 TXT反过来也要查。缺失标签的图片在训练时会被跳过或报错提前发现比训到一半崩了强。第二项类别 id 范围检查。读所有 TXT 文件的第一列确认最大值小于nc类别数最小值大于等于 0。出现负数或超范围的值说明转换脚本有 bug 或者类别映射表写错了。第三项空标签文件统计。有些图片可能确实没有目标对应的 TXT 是空的这是正常的。但如果空文件比例超过 20%要么是标注漏了要么是转换时类别过滤太狠需要回头查。import os def validate_dataset(img_dir, label_dir, nc): img_files {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith((.jpg, .png))} label_files {os.path.splitext(f)[0] for f in os.listdir(label_dir) if f.endswith(.txt)} # 检查一一对应 missing_label img_files - label_files missing_img label_files - img_files print(f缺标签的图片: {len(missing_label)}) print(f缺图片的标签: {len(missing_img)}) # 检查类别 id 和空文件 empty_count 0 bad_cls [] for lf in label_files: path os.path.join(label_dir, lf .txt) with open(path) as f: lines [l.strip() for l in f if l.strip()] if not lines: empty_count 1 continue for line in lines: cls_id int(line.split()[0]) if cls_id 0 or cls_id nc: bad_cls.append((lf, cls_id)) print(f空标签文件: {empty_count}) print(f越界类别 id: {len(bad_cls)}) if bad_cls[:5]: print(示例:, bad_cls[:5]) validate_dataset(dataset/images/train, dataset/labels/train, nc4)逻辑说明用集合运算做一一对应检查是最快的方式img_files - label_files直接给出缺标签的图片名。类别 id 检查遍历每个 TXT 的每一行虽然慢一点但值得。空标签文件单独统计因为空文件在 YOLO 训练里是合法的负样本但比例过高就有问题。参数说明nc传你的实际类别数比如 4 类就传 4。bad_cls只打印前 5 个示例避免刷屏。这个校验脚本建议存成check_dataset.py每次重新划分后跑一次养成习惯。4. 训练教程落地从 data.yaml 到第一个收敛的模型4.1 data.yaml 的五个字段怎么写才不出错YOLOv8 训练读的是data.yaml这个文件写错一个字段训练直接起不来。五个字段分别是path、train、val、test、names。path是数据集根目录train、val、test是相对于path的图片路径names是类别名列表顺序必须和标签里的类别 id 对应。path: /home/user/dataset train: images/train val: images/val test: images/test names: 0: car 1: bus 2: truck 3: van逻辑说明path用绝对路径最稳相对路径在不同工作目录下跑容易找不到。train等字段指向的是图片目录YOLO 会自动把images替换成labels去找标签所以目录结构必须严格是images/xxx和labels/xxx平行。names写成字典形式键是类别 id值是类别名这样比列表更不容易搞错顺序。参数说明如果你的数据集没有独立测试集test字段可以删掉YOLO 不会强制要求。names里的类别名不要用中文虽然 YAML 支持但训练日志和评估输出里中文容易乱码用英文或拼音。4.2 第一次训练的命令与关键参数环境装好之后一条命令就能开训。但默认参数不一定适合你的数据下面这条命令把几个关键参数都显式指定了。yolo detect train \ data/home/user/dataset/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/train \ namevehicle_exp1逻辑说明modelyolov8n.pt用的是 nano 版本预训练权重1000 张图的小数据集从预训练权重微调比从头训收敛快得多。epochs100配合patience20意思是 20 轮验证指标不提升就早停避免过拟合。imgsz640是 YOLOv8 的默认输入尺寸如果你的图片里车辆目标很小可以提到 1280但显存占用会翻倍。参数说明batch16在 8GB 显存上跑 640 尺寸基本够用显存不够就降到 8 或 4。lr00.01是初始学习率微调场景下这个值偏大如果 loss 震荡厉害可以降到 0.001。project和name决定训练结果存哪建议每次实验换个name方便对比。4.3 训练过程中该盯哪几个指标训练日志刷屏但真正需要盯的就几个。box_loss和cls_loss是训练损失正常情况应该稳步下降如果震荡或者不降先查学习率和数据标签。mAP50是验证集上 IoU 阈值 0.5 的平均精度这是最直观的指标1000 张图 4 个类别的车辆检测正常能到 0.85 以上。mAP50-95更严格一般比mAP50低 10 到 20 个点。如果mAP50在训练早期就冲到很高然后不动了大概率是验证集和训练集有泄漏回头查划分脚本。如果cls_loss一直很高检查类别是否不平衡比如 truck 只有几十张而 car 有几百张这种情况需要加类别权重或者补充数据。5. 避坑与排查车辆数据集训练最常见的五个翻车现场5.1 现象训练启动就报 No labels found原因YOLO 按images替换成labels找标签如果你的目录结构是train/images和train/labels但data.yaml里写的是train: train/images替换后变成train/labels路径是对的。但如果标签目录名不是labels而是Annotations之类就找不到了。解决统一目录命名为images和labelsdata.yaml里train字段只写到images这一层比如train: images/train。改完跑一遍第 3 章的校验脚本确认。5.2 现象训练 loss 正常下降但 mAP 一直是 0原因类别 id 映射错了。标签里的类别 id 和data.yaml里names的顺序对不上模型学出来的预测和评估时的 ground truth 完全错位mAP 自然为 0。解决打开任意一个 TXT 标签看第一列的数字对照data.yaml里names的键。比如标签里是0 1 2 3names也必须是0: xxx到3: xxx。如果标签里出现了4但names只到3说明转换时类别映射表漏了类别。5.3 现象验证集 mAP 很高但实际推理效果差原因数据泄漏。同一段视频抽的帧被随机分到了训练集和验证集验证集里的图片和训练集高度相似模型相当于在背答案。解决按第 3 章的分组划分逻辑重新切分确保同一场景或同一视频的图片只出现在一个集合里。如果图片没有可用的分组标识用感知哈希做相似度聚类把相似图片分到同一组。5.4 现象训练到一半 loss 突然变成 nan原因学习率太大或者标签里有非法值。归一化坐标出现大于 1 或小于 0 的值反向传播时梯度爆炸。解决先跑校验脚本查标签里的坐标范围把所有值限制在 0 到 1 之间。然后把lr0降到 0.001 重训。如果还不行加梯度裁剪YOLOv8 默认没开可以在训练参数里加clip10。5.5 现象小目标车辆检测不出来原因输入尺寸太小。640 的输入下如果原图里车辆只占几十个像素缩放到 640 后目标更小特征提取网络根本抓不到。解决把imgsz提到 1280同时batch减半避免显存溢出。如果显存不够用切片推理把大图切成小块分别检测再合并。另一个方向是换更大的模型yolov8s或yolov8m对小目标的特征提取能力比 nano 强。6. 把 1000 张图用出 10000 张的效果三个进阶技巧第一个技巧是离线数据增强。YOLOv8 训练时自带在线增强马赛克、翻转、色彩抖动但每次 epoch 增强是随机的小数据集下模型看到的多样性有限。我一般会在训练前用 albumentations 做一轮离线增强把 1000 张扩到 3000 到 4000 张重点加随机裁剪、亮度对比度扰动、运动模糊。车辆检测场景里运动模糊和夜间低照度是实际部署时最常见的退化因素提前在训练数据里覆盖这些情况模型鲁棒性会好很多。注意增强后的标签要同步变换albumentations 支持 bbox 参数直接传 YOLO 格式的归一化坐标就行。第二个技巧是用预训练权重做分层微调。yolov8n.pt是在 COCO 上训的COCO 里本来就有 car、bus、truck 这些类backbone 已经学到了车辆的基础特征。微调时把 backbone 的学习率设成 head 的十分之一让 head 快速适配你的类别定义backbone 只做轻微调整。YOLOv8 的命令行没直接暴露这个参数需要改训练配置或者用 Python API 手动设置参数组。第三个技巧是难例挖掘。第一轮训完用模型在验证集上推理把漏检和误检的图片挑出来人工检查标注是否有问题然后把这些图复制多份加入训练集。这个循环做两到三轮mAP 通常能再涨 3 到 5 个点。1000 张的规模下每一张难例的价值都很高比盲目加数据有效得多。# 用训练好的模型找难例 from ultralytics import YOLO model YOLO(runs/train/vehicle_exp1/weights/best.pt) results model.predict(dataset/images/val, save_txtTrue, conf0.25) # 对比预测结果和 ground truth找出漏检 import os for r in results: img_name os.path.splitext(os.path.basename(r.path))[0] pred_count len(r.boxes) if r.boxes else 0 gt_path fdataset/labels/val/{img_name}.txt gt_count 0 if os.path.exists(gt_path): with open(gt_path) as f: gt_count len([l for l in f if l.strip()]) if pred_count gt_count: print(f漏检: {img_name}, 预测 {pred_count}, 实际 {gt_count})逻辑说明conf0.25是置信度阈值低于这个值的预测不输出。漏检判断用预测框数量小于真实框数量来粗略筛选实际还要看 IoU 匹配但作为第一轮筛选够用了。挑出来的图片人工确认后复制到训练集目录重新训一轮。参数说明save_txtTrue会把预测结果存成 YOLO 格式的 TXT方便后续对比。conf阈值不要设太高0.25 能覆盖大部分漏检情况设 0.5 会漏掉很多低置信度的正确预测。这套流程我跑过好几个车辆检测项目1000 张起步的数据集按上面的划分、校验、训练、难例挖掘走一遍两周内拿到一个能用的基线模型是稳的。真正花时间的从来不是调参而是数据准备阶段那些不起眼的格式转换和校验。把第 2 章和第 3 章的脚本吃透以后换任何数据集都是同一套动作。希望帮到你。本文还有配套的精品资源点击获取
返回列表