☰
YOLO车辆检测数据集实战:9767张六类图像从标注到训练部署全指南
2026/9/28 5:23:49 网站建设 项目流程

简介:目标检测是计算机视觉的基石任务,而高质量标注数据与成熟训练流程是落地效果的关键。YOLO作为工业界最流行的实时检测框架,其训练效果高度依赖数据集的格式规范与类别定义。面对一套包含公交车、卡车、摩托车、行人、自行车、小型车等六类道路目标的车辆检测数据集,从解压检查、标签校验、目录整理到超参数调优,每一步都影响最终模型的表现。工程实践中,格式错位、类别混淆、数据泄漏、样本失衡等问题常导致精度虚高或部署失效。本文以9767张标注图像为例,系统梳理YOLO数据准备的完整链路,并给出训练调参、避坑排查与ONNX部署的落地方法,帮助开发者快速构建可复现的车辆检测基线,为智慧交通、车流统计等场景提供可靠技术支撑。

1. 这个zip装的不只是9767张图,而是一套能直接开训的六类车辆检测基线

收到一份「YOLO算法-车辆检测数据集-9767张图像带标签-公交车-卡车-摩托车-行人-自行车-小型车.zip」,绝大多数人的第一反应是解压、开训、跑通、完事。但Wen件里装的其实不只是9767张图和一堆txt,而是一套可以直接作为车辆检测基线的六类标注数据。适用人群很明确:一是要做城市交通目标检测,又不想从零标数据快速起步的人;二是想在YOLOv5、YOLOv8、YOLOv11之间做对比选型,需要一份干净数据集的从业者。这个压缩包的真正价值不在「能跑通」,而在「跑完之后你怎么确认它没跑偏」,本文按解压、摸清标注、规整目录、训练调参、避坑、验证部署的顺序,把每一步的落地方法和参数选择讲清楚。

2. 先摸清数据集的底细:目录结构、标注格式与六类标签分布

2.1 打开zip先看这三样:images、labels、classes.txt

别急着unzip到当前目录就开干。我拿到这个zip的第一件事是先用unzip -l列出压缩包内部结构,不解压也能看清它是不是YOLO的标准布局:

unzip -l YOLO算法-车辆检测数据集-9767张图像带标签-公交车-卡车-摩托车-行人-自行车-小型车.zip | head -40

如果输出里能直接看到images和labels两个一级目录,并且labels下是txt而不是嵌套的Annotations文件夹,基本可以判定它是Darknet系的原生标注格式。正常的数据集会排布成下面这样:

数据集根目录/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── classes.txt └── data.yaml # 有的自带,没有就自己写

其中classes.txt的每一行对应一个类别名,行号从0开始。第一行是class 0,第二行是class 1,这个顺序决定了后面所有标注文件里第一列整数到底是什么含义。9767张图像对六类目标来说规模不算大,但每张图通常不止一个目标,所以压缩包里标注框的总数会远大于9767,这是正常的,别拿图片数去对标框数。

2.2 标注格式是YOLO的归一化txt,还是VOC的XML?这一步决定你后续动不动力气

拿到数据集第一件事不是训练,而是确认标注格式。常见做法是随便挑一个txt文件看前两行:

head -3 labels/train/000001.txt

一条标准YOLO标注长这样:

5 0.5234375 0.6347656 0.08203125 0.15625 0 0.1298828 0.4843750 0.0458984 0.1171875

第一列是类别id,整数,范围0到5;后四列是归一化坐标:中心x、中心y、目标宽、目标高,全部除以原图宽高,取值在0到1之间。如果打开发现第一列是字符串(car、bus这种),说明它是从VOC或LabelImg的XML转换时没转干净,训练前需要把类名映射成id。如果打开是XML,那是VOC格式,得走一遍「读取对象名和bndbox → 除以图片宽高 → 写出txt」的转换流程。转换本身用Python标准库就能完成,不需要额外装包,但要注意一个坑:每个框的坐标除以的必须是它所属那张图的宽高,而不是固定尺寸,换图尺寸就错位。

顺带说一句,这个数据集是纯2D水平框,不像SemanticKITTI那样带点云语义分割标注,也不像DOTA那样用旋转框。它就是你做通用车辆检测最常用的那种水平矩形框,处理起来比旋转框省心得多。

2.3 六类标签的边界:公交车和卡车怎么划,行人算不算车辆

这个数据集叫「车辆检测」,但类别里混着行人、自行车、摩托车,所以它的语义实际上是「道路参与者检测」,而不只是机动车。你在做车辆计数或者车道占用检测时,不要粗暴地把person类别过滤掉,直接把它当通用道路目标检测器去用,反而更贴合真实道路场景。

六类里最容易混淆的是bus和truck。标注经验是:车身带成排车窗、车门布局按公交排列的算bus;驾驶室和货厢分离、带栏板或集装箱结构的算truck。厢式货车是重灾区,很多标注员会把它标成truck,但它的外形其实介于两者之间。如果你下游要做高速收费站车型分类,这个边界模糊会直接影响精度。

还有一个隐藏问题:骑摩托车和自行车的人,有的标注员会把骑手单独再标一个person,有的只标车不标人。判断办法是做一个类别重叠统计,看person的框和bicycle、motorcycle的框是否大面积重叠。如果重叠比例超过80%,说明标注策略是「人车分离」,训练完之后你需要在后处理里加一个同框融合规则,把「一辆摩托车+一个人」合并输出成「一辆载人摩托车」,而不是当作两个独立目标。

2.4 用脚本统计类别分布和bbox尺寸,提前发现样本失衡

不要相信压缩包描述里「带标签」三个字,标签格式对不对、类别分布均不均匀,必须自己跑出来。我一般会先写一个统计脚本,把每类目标数量和空标签文件数都列出来:

import os import glob from collections import Counter label_dir = "labels/train" cls_counter = Counter() total_boxes = 0 empty_txt = 0 class_names = ["bus", "truck", "motorcycle", "person", "bicycle", "car"] for txt_path in glob.glob(os.path.join(label_dir, "*.txt")): lines = [line for line in open(txt_path) if line.strip()] if not lines: empty_txt += 1 continue for line in lines: parts = line.strip().split() if len(parts) != 5: continue cls_id = int(parts[0]) cls_counter[cls_id] += 1 total_boxes += 1 print(f"总目标数: {total_boxes}, 空标签文件: {empty_txt}") for i, name in enumerate(class_names): print(f"class {i} ({name}): {cls_counter.get(i, 0)}")

这个脚本的逻辑是遍历每个txt,按行读取标注,统计每个类别id出现的次数,同时记录空文件数量。如果输出显示person有三万多个框、bus只有两千出头,那就要警惕样本失衡。9767张图的数据集里,car和person通常是大头,bus和motorcycle是小头,直接默认参数训练,小类大概率学不好,后面要考虑对小类做复制增强或调整cls_loss的权重。

bbox的尺度分布同样值得关心。把每行标注的w和h收集起来看一眼,如果绝大多数框的长边小于32像素,而你训练时imgsz设成640,这些目标经过下采样基本就丢了。遇到这种情况,要么把输入分辨率提到1280,要么用SAHI之类的切片推理方案,而不是盲目加大模型容量。

3. 把数据集整理成YOLOv8能直接吃的格式:目录规整与train/val划分

3.1 标准布局:dataset根目录下images和labels一一对应

YOLOv8对数据集目录结构要求很死板,不满足就报错或识别不到标注。以这个车辆数据集为例,建议统一整理成下面的布局:

datasets/vehicle/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml

两个关键点。第一,images和labels必须在同一个父目录下,YOLO默认按「把images替换成labels」的规则寻找标注文件,你把标注单独扔到别处,就必须在data.yaml里写绝对路径,麻烦且容易错。第二,train和val必须同时出现在images和labels下,哪怕压缩包里只有一份全量数据,也要手动划一刀,否则训练时没有验证集可评估,所有指标都是黑匣子。

压缩包如果已经分好了train和val,直接重命名成上面的结构就行。但多数情况下数据集是把所有图片堆在images下、标签堆在labels下,这时候划分逻辑就很重要了,下面给出的做法能避免一个很隐蔽的泄漏问题。

3.2 按前缀分组划train/val,别让连续帧泄漏验证集

车辆检测数据集很多是从监控视频抽帧生成的,前后帧场景高度相似。如果直接随机划分,同一个视频片段的连续帧会一边进训练集一边进验证集,验证结果好看到虚高,一旦部署到新路段精度立刻掉下来。正确做法是按文件名前缀聚合,再对「组」做划分:

import os import random import shutil from collections import defaultdict from pathlib import Path random.seed(42) train_ratio = 0.85 src_images = Path("images") src_labels = Path("labels") out_root = Path("datasets/vehicle") for split in ["train", "val"]: (out_root / "images" / split).mkdir(parents=True, exist_ok=True) (out_root / "labels" / split).mkdir(parents=True, exist_ok=True) # 按文件名前缀分组,例如 clip01_0001.jpg 归入 clip01 组 groups = defaultdict(list) for img_path in src_images.glob("*.jpg"): prefix = img_path.stem.split("_")[0] groups[prefix].append(img_path.stem) group_names = list(groups.keys()) random.shuffle(group_names) split_idx = int(len(group_names) * train_ratio) train_groups = set(group_names[:split_idx]) def copy_by_name(name, split): img_src = src_images / f"{name}.jpg" lbl_src = src_labels / f"{name}.txt" if not lbl_src.exists() or lbl_src.stat().st_size == 0: return # 缺失或空标签直接跳过,不污染训练集 shutil.copy(img_src, out_root / "images" / split / f"{name}.jpg") shutil.copy(lbl_src, out_root / "labels" / split / f"{name}.txt") for prefix, names in groups.items(): split = "train" if prefix in train_groups else "val" for name in names: copy_by_name(name, split)

脚本核心是按下划线前的字符串把图片分成组,再对组做85/15划分。这样同一个视频片段的所有帧要么全部进入训练集,要么全部进入验证集,从根源上避免数据泄漏。copy_by_name里的空标签判断很重要:如果一个图片有对应的txt但txt是0字节,说明这一帧没有目标,这种文件直接跳过比留着更干净。

划完以后数一遍图片数量:

find datasets/vehicle/images/train -name "*.jpg" | wc -l find datasets/vehicle/images/val -name "*.jpg" | wc -l

两个数字加起来应该等于9767减去被跳过的空标签数量。如果差距过大,说明压缩包里存在缺标注的坏图,可以对比两个目录的文件名来定位问题:

comm -3 <(ls images | sed 's/.jpg//' | sort) <(ls labels | sed 's/.txt//' | sort)

comm输出里只属于images而labels没有的那些名字,就是缺标注的坏图。我建议把它们直接移出训练目录,别留在里面。几十张坏图不至于让模型崩,但会让训练日志每隔几轮冒出一个警告,干扰你判断真正的报错。

提示:划分脚本里random.seed(42)是固定随机种子,目的是让每次划分结果可复现。如果你要多组实验对比,建议每组实验都固定同一个seed,否则数据集变了,模型指标差异就没法归因到模型本身。

3.3 写一个data.yaml,指定路径、类别数和类名

数据整理好后,训练前还要一个data.yaml,它是YOLOv8读取数据集的唯一入口。路径写错、类别数对不上、names顺序和classes.txt不一致,都会引发连锁错误。最基本的写法:

# datasets/vehicle/data.yaml path: /home/user/datasets/vehicle # 数据集根目录绝对路径 train: images/train # 相对path而言的训练集目录 val: images/val # 相对path而言的验证集目录 nc: 6 # 类别数,必须和names长度一致 names: 0: bus 1: truck 2: motorcycle 3: person 4: bicycle 5: car

三个容易翻车的点。第一,path建议写绝对路径,不要写../vehicle这种相对路径,训练脚本的工作目录一变,相对路径就失效,报错还是那种很模糊的AssertionError: train dataset not found。第二,names的索引顺序必须和classes.txt的行号一一对应。classes.txt第一行是bus,names[0]就必须是bus,否则模型训练完全正常、loss也下降,但检测结果类别全错位。第三,nc是类别id最大值加1,如果压缩包里恰好有类别id为6的标注,训练时会直接报CUDA下标越界错,十有八九就是nc写小了。

写完data.yaml,可以先用一个不加载真实权重的模型做快速自检:

python -c " from ultralytics import YOLO m = YOLO('yolov8n.yaml') res = m.val(data='datasets/vehicle/data.yaml', imgsz=640) print(res.results_dict) "

这里用yolov8n.yaml跑验证模式,目的不是测精度,而是看数据集路径和标注能不能被正确加载。如果这一步能跑通且mAP不为0,说明目录、yaml、标注全部对齐,可以进入正式训练。

4. 训练与调参:从零开始训还是加载预训练权重,关键参数怎么设

4.1 预训练权重不是玄学,是省时间的后悔药

9767张图对深度学习训练来说不算大,但也不是小到要从零初始化。常见做法是加载COCO预训练权重作为起点,比如yolov8s.pt或yolov8n.pt。COCO本身就有car、truck、person、bicycle、motorcycle这些类别,模型已经具备很强的底层特征提取能力,在这个车辆数据集上只需要微调最后几层,收敛快得多,最终mAP通常也更高。

第一次运行训练命令时,YOLOv8会自动下载对应权重。如果网络不可用,可以手动把权重文件放进当前目录,训练代码检测到同名文件后就直接加载,不会再请求网络。从零训练不是不行,但epochs得翻到300以上,学习率策略也要重新调,对大多数场景纯粹是浪费时间。我的习惯是先用yolov8s.pt跑一轮,确认能收敛,再决定要不要换更大或更小的模型。

4.2 关键训练参数:epochs、batch、imgsz、patience、device

训练命令写出来只有一行,但这一行里藏着至少五个需要按数据集体量调整的参数:

yolo detect train \ data=datasets/vehicle/data.yaml \ model=yolov8s.pt \ epochs=100 \ batch=16 \ imgsz=640 \ patience=20 \ device=0 \ project=runs/vehicle \ name=exp1

逐项说明我的设定逻辑:

  • epochs=100:9767张图、六类、目标数量中等,100轮基本够。如果第80轮val loss还在明显下降,说明数据复杂度超出初始预期,可以加到150,但不要一上来就填300,纯耗机器。
  • batch=16:由显存决定。16GB显存跑yolov8s加imgsz=640,batch=16是稳的。显存小就降到8,大就加到32。batch太小的坏处是BN统计不稳定,loss曲线抖得厉害,收敛也不踏实。
  • imgsz=640:YOLOv8训练时会把输入统一resize到640×640。这个数据集如果小目标多(远距离的行人、自行车),640会丢细节。建议先跑一版640做基线,再跑一版1280对比小类的AP涨幅,超过5个点就换1280。
  • patience=20:验证集指标连续20轮不提升就提前停止,省时间。调试时可以关掉早停,但正式训练建议开着。
  • device=0:指定第一张GPU。多卡可以写device=0,1,前提是显存够。纯CPU训练这个量级的六类数据集,100轮可能要跑十几个小时,不推荐。

如果你机器上装了多个版本的Ultralytics,训练前先确认用的是哪个:

pip show ultralytics | grep Version

不同版本在参数名上有差异,比如yolov5的hyp超参数文件在v8里改成了cfg配置节,平滑标签和Anchor策略的默认值也不同。项目里同时有v5和v8的老手最容易在这翻车。

4.3 训练日志的读法:损失曲线和混淆矩阵怎么看

训练结束后的runs/vehicle/exp1目录里会生成一堆png和results.csv,不要只扫一眼loss降到多少就收工。四个指标要分开看:

  • box_loss:边界框回归损失。下降慢说明定位学不好,优先调高imgsz或换更大模型。
  • cls_loss:分类损失。如果这个收敛了但mAP不高,问题多半在类别失衡或标签错位。
  • dfl_loss:分布聚焦损失,负责边框质量的分布。抖动大一般不用太在意,它受batch大小影响明显。
  • mAP50和mAP50-95:mAP50是IoU=0.5的宽松匹配,mAP50-95是对多个IoU阈值取平均,更严格。两者差距过大说明边框精度不够,小目标框偏了但还能勉强匹配上IoU=0.5。

confusion_matrix.png是诊断标签错位的第一步工具。对角线越亮越好,如果某一行在错误的列上也很亮,比如bus的行在truck列上很亮,说明这两个类在特征空间太近,要么是数据标注边界不一致,要么是这两个类对当前模型容量来说确实难分,这时候考虑合并类别比加数据更实际。

4.4 不同YOLO版本在这个数据集上怎么选:v5、v8、v11

这个zip标题只写了「YOLO算法」,没锁版本,所以你需要自己选。我的经验是这样:

  • YOLOv8s:默认选择。速度快、部署资料多、训练脚本和数据结构最标准,新手无脑用它跑通全流程。
  • YOLOv5s:如果你已经有一套很熟的v5烧录流程,用v5也没有坏处。Anchor-based机制对车辆这种中尺度目标依然稳定。
  • YOLOv11s:如果对最新结构好奇可以试,但没必要为了指标牺牲生态。v11在推理速度上有优势,但相关算子在嵌入式部署时可能遇到兼容问题,优先保证能部署再谈性能。

同一份数据、预处理完全一样,v5和v8的mAP差距通常在一个点以内,这个差距远小于标注质量带来的误差。所以别迷信版本,把训练流程跑通,然后多试两个版本做交叉验证,看哪个在你自己的验证视频上表现更稳,那才是最终答案。

5. 避坑:标签错位、样本失衡、模型不收敛的排查清单

5.1 现象:训练正常跑完,validation mAP却是0

原因:最可能是把空标签文件或缺失标签的图片划进了验证集。YOLO训练时遇到没有目标的图会跳过该图的目标损失,但验证时会把模型输出和空目标列表做匹配,算出来recall就是0。另一种可能是data.yaml里的val路径指向了没有标注的目录。

解决:训练前用统计脚本把所有空标签文件找出来,剔除或单独放一个目录。如果已经训练完了,就用验证命令重新跑一次:

yolo detect val data=datasets/vehicle/data.yaml model=runs/vehicle/exp1/weights/best.pt

输出里如果per-class的AP有一类算出来是0,基本就是那一类的验证样本几乎为空或全部被遮挡,不是模型崩了,而是验证集划分出了问题。

5.2 现象:loss正常下降,检测结果却张冠李戴

原因:类别id的语义和现实不对应。最常见的是data.yaml的names索引写错了,比如压缩包classes.txt的顺序是bus、truck、motorcycle、person、bicycle、car,你却在names里写成了car、bus、truck、motorcycle、person、bicycle,一个错位全盘错位。模型学的是「第1类」的特征,它并不知道你给第1类起的名字叫car还是叫bus。

解决:训练前跑标签自检脚本,打印每个class_id实际出现的次数,和names一一对应。更稳的方式是直接读classes.txt生成data.yaml,完全不手写:

with open("classes.txt") as f: names = [line.strip() for line in f if line.strip()] nc = len(names) yaml_content = f"path: /abs/path/datasets/vehicle\ntrain: images/train\nval: images/val\nnc: {nc}\nnames:\n" for i, name in enumerate(names): yaml_content += f" {i}: {name}\n" with open("data.yaml", "w") as f: f.write(yaml_content)

这段代码用classes.txt的行号当作names的索引,从源头避免手写错位。注意classes.txt如果带BOM或空行,要先strip再按行处理,否则第一个类名会变成带不可见字符的字符串,训练时匹配不上任何一个类别。

5.3 现象:训练正常,对手机竖拍图片检测不准

原因:jpg的EXIF方向信息。手机竖拍的照片在文件里存的是横向原始像素,加了一个「旋转90度」的EXIF标记,图片查看器会按标记转正显示。但YOLO读取像素时不解析EXIF,直接按原始像素输入模型,相当于模型训练时见过的大部分是正着的图,推理时却给了横着的车。

解决:做一次批量转正,用PIL读取并用exif_transpose把像素真正转正:

from PIL import Image, ImageOps from pathlib import Path for img_path in Path("images").glob("*.jpg"): img = Image.open(img_path) img = ImageOps.exif_transpose(img) img.save(img_path)

转正后最好再做一次统一resize并覆盖原图,能减少训练时随机resize的额外开销。这个坑在补充外部数据时尤其常见,压缩包自带的9767张图一般没问题,但一旦你加入网络爬取或手机拍摄的补充数据,一定要先过一遍exif_transpose。

5.4 现象:zip解压后文件名乱码,或提示需要密码

原因:这个zip很可能是在Windows下用中文系统压缩的,文件名是GBK编码,Linux的unzip按UTF-8解码后中文变成乱码,解出来的目录名一堆问号。另外有些数据集的zip带伪加密标志位,文件实际没有加密,但压缩工具看到标志位就要求密码。

解决:用unzip指定编码解压:

unzip -O GBK 数据集.zip -d dataset

如果unzip提示-O参数不支持,改用Python的zipfile模块配合手动文件名重映射。伪加密的情况可以用Python测试是否能无密码读取:

import zipfile with zipfile.ZipFile("数据集.zip") as zf: zf.extractall("dataset", pwd=None)

如果这里不抛错,说明标识位只是吓唬人。解压后别急着删压缩包,数据集的作者偶尔会在压缩包注释里写类别定义说明,删了就没法回头查。

5.5 现象:混淆矩阵每一行的比例加起来不是100%

原因:混淆矩阵的行代表真实类别,列代表预测结果,统计的是框级匹配数量,不是百分比。YOLO生成的混淆矩阵最后一列还有background,它不会对行做归一化。看到bus这一行0.84在bus列、0.13在truck列、0.07在background列,加起来超过1,就怀疑代码有bug。

解决:这通常不是bug,对角线越接近1越好。如果非要看归一化比例,用sklearn的confusion_matrix配合normalize='true'重画一张。日常迭代中我更关心的是某个类在垂直方向泄漏到哪个类,例如car大量泄漏到bus,说明这两个类特征空间太近,模型学出来的边界和你期望的业务边界不一致。

6. 验证与导出:用best.pt跑视频,再导出ONNX给边缘设备

6.1 先用val跑一遍,确认best.pt不是靠运气

训练完的runs/vehicle/exp1/weights里有两个权重:best.pt和last.pt。best.pt是按验证集mAP挑出来的,last.pt是最后一轮的状态。不要直接拿best.pt去部署,先用它对验证集跑一次完整评测,记住mAP50和mAP50-95,再跑一段测试视频:

yolo detect predict \ model=runs/vehicle/exp1/weights/best.pt \ source=test_video.mp4 \ conf=0.4 iou=0.5 \ save=True

conf和iou是一对搭档。conf=0.4过滤低置信度预测,iou=0.5控制NMS去重强度。车辆检测场景里我一般conf设0.35到0.45,太高会把远距离行人滤光,太低会输出大量重复框。

6.2 导出ONNX与量化,把权重从PyTorch手里解放出来

验证通过后,下一步是部署。最常见的做法是把best.pt导出成ONNX:

yolo export model=runs/vehicle/exp1/weights/best.pt format=onnx imgsz=640 opset=12

导出的onnx可以直接用onnxruntime做CPU推理,也可以用TensorRT或NCNN做GPU和边缘端推理。交通场景里我习惯先跑ONNX再决定要不要量化。高速收费站车流计数这类场景,FP16量化就够;如果要跑到嵌入式设备上,NCNN的INT8量化是常见选项。导出的ONNX输入是NCHW格式,预处理需要多做一步:把图像从BGR转RGB、除以255归一化、resize到640×640。这一步最容易和纯PyTorch推理结果对不上,排查方式是逐像素对比预处理输出。

6.3 权重文件命名与实验管理:我踩过的最后一个坑

我以前干过一件蠢事:把best.pt复制成best_final.pt,然后用旧权重导出ONNX跑测试图,结果精度低了一截,排查了半天才确认是文件名的缓存问题。现在的习惯是每轮实验只保留best.pt和data.yaml的拷贝,实验记录全部写在results.csv里,模型文件用实验名加序号命名,不在名字里写「final」这类含糊词。做目标检测项目,数据格式、标签顺序、验证集划分这三件事确认无误后,模型效果基本不会太差,最怕的就是数据和权重管理上翻车。这份9767张图的车辆检测数据集本身底子不错,按上面的流程走一遍,你得到的不仅是一个能跑的模型,还是一套可复现的车辆检测基线方案。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询