资讯动态

车辆检测数据集car-dataset实操指南:从格式检查到YOLO训练前的体检

发布时间:2026/10/27 16:13:58 来源:尧图企业网站定制
简介一套用于YOLO车辆检测的标注数据集面向目标检测学习者和算法工程师解决车辆检测训练数据不足的问题。数据集中包含白天、夜间以及俯视场景的车辆图像共两千余张图片全部完成标注类别统一为车辆car。每张图片均提供txt和xml两种标签格式分别适用于不同训练框架无需额外格式转换。整个压缩包共七千二百一十个文件其中图片两千四百零三个txt标签两千四百零四个xml标签两千四百零三个包体约八百零三点五八MB文件与标签一一对应结构清晰易用。该数据集省去了手动标注的繁琐工作白天和夜间样本有助于增强模型在不同光照下的鲁棒性俯视视角则可服务于停车场监控、无人机检测等场景。目前已有六千一百五十二人学习下载是训练车辆检测模型时值得参考的现成数据集。1. car-dataset 是什么一个能直接喂给 YOLO 的车辆检测数据集想做车辆检测大部分人卡住的第一关不是模型选型而是没有一张带着正确标注的图。car-dataset 这类车辆检测数据集就是把道路场景图片按 YOLO 习惯整理成 images 和 labels 两个目录每张图片对应一个同名的 txt 标签文件里面写的是每辆车的类别编号和归一化坐标。YOLO 训练时直接读这个目录就能跑不需要再做格式转换这是它比零散爬虫图更有价值的地方。它适合两类人一是在 YOLOv5/YOLOv8 上快速拉一条 baseline 做方案验证的开发者二是刚入门车辆检测、想省掉几千张手工标注时间的学习者。但前提是得先搞清楚它里面装的是什么、边界在哪否则训到一半才发现标签错位返工成本比重新标注还高。2. 选数据集先做对比公开车辆数据集与 car-dataset 的边界差异2.1 主流的车辆检测数据集长什么样做车辆检测能选的数据集其实不少但各自的标注格式和适用场景差别很大。我一般会在动手前把 KITTI、UA-DETRAC、BDD100K、COCO 这几类列出来对比一遍再决定是自己采集还是直接复用社区整理好的 car-dataset 包。数据集标注格式典型场景车辆目标特点上手成本KITTIPascal VOCXML城市道路、高速公路大目标为主遮挡少需要转 YOLO 格式UA-DETRACXML类 VOC城市路口、交通监控小目标密集遮挡严重需要按自己类别重映射BDD100KJSON多城市道路含夜间雨天场景覆盖最广格式复杂抽取麻烦COCOJSON日常生活照片car/truck/bus 只是其中几类类别 id 与 YOLO 不对齐car-dataset 社区包YOLO txt常见形态取决于原作者采集来源通常只含车辆相关类拿来即用但需要体检这里要特别提醒一句car-dataset 并没有一个统一的官方规范。社区里流传的车辆检测数据集很多是从 KITTI 或 UA-DETRAC 抽取图片后转换打包的也有人是完全自采自标。所以拿到手第一件事不是训练是先确认它的来源和组成。2.2 拿到手先判断这份 car-dataset 是不是原生 YOLO 格式YOLO 的标签格式极其简单每一行五个数字依次是类别编号class id、归一化中心点 x、归一化中心点 y、归一化宽、归一化高。比如一个 txt 里写着0 0.5143 0.5278 0.1832 0.2254意思是第 0 类车辆中心点在图片横向 51.43% 和纵向 52.78% 的位置宽高分别占整图的 18.32% 和 22.54%。很多人在这一步会纠结“coco80 怎么读”的问题。如果你之前用过 COCO 数据集它的 80 个类别里 car 排在索引 2、truck 排在 7这是 COCO 官方定义的顺序。但 YOLO 训练时不认类别名字只认数字索引所以一个只有 car、truck、bus 三个类的数据集的 class id 是从 0 开始自己排的。如果把 COCO 的索引直接搬过来用car 的 id 是 2而这个数据集里 0 才是 car训练出来的模型自然是乱的。正确的检查方式是先找这个数据集有没有带 data.yaml 或 names 文件。完整的 YOLO 数据目录里一定会有类似这样的内容train: dataset/images/train val: dataset/images/val nc: 3 names: [car, truck, bus]如果手头的 car-dataset 只给了图片和 txt没有 names那就要小心了txt 里的 0 到底代表什么只能靠抽图人工确认。我的做法是随机抽十张图把标签画上去看框对应的是什么车再反推 class id 的含义。注意class id 与 names 的对应关系是这个数据集里最容易出错、也最不容易被发现的坑训练过程可能完全不报错但推理结果全错。2.3 按应用场景选数据集别只看数量“数据集越大越好”在车辆检测这里不成立。一个做高速卡口车牌抓拍的项目如果喂给它一堆城市路口拥堵场景的数据模型训练出来后误检率会高到没法上线。我的建议是先把自己的部署场景拆成三个维度道路类型高速/城市/乡村、光照条件白天/夜间/逆光、拍摄视角平视/俯视/斜视再去匹配数据。如果做的是城市路口交通监控UA-DETRAC 的密集车辆和遮挡情况是最接近的如果做的是自动驾驶前视感知KITTI 和 BDD100K 的视角更合适如果只是做一个 demo 验证 YOLO 的流程能不能走通那直接从 car-dataset 社区包开始就好。在这个阶段不用纠结数据量是几千还是几万先把场景对齐了训练出来的东西才有参考价值。3. 从零备好 car-dataset图片清洗、去重与目录规范化3.1 第一步先做图片完整性检查别把损坏文件喂给训练器从网上下载的车辆图片数据集最常见的翻车点是里面混着损坏的 JPEG 文件。YOLO 训练时如果碰到无法解码的图片有的框架直接崩有的框架会跳过后默默少算一张导致训练集和标签对不上。所以拿到数据后的第一个脚本一定是图片完整性检查。import os from PIL import Image bad_list [] for root, _, files in os.walk(raw_images): for f in files: p os.path.join(root, f) if not f.lower().endswith((.jpg, .jpeg, .png)): continue try: with Image.open(p) as im: im.verify() # 只校验文件结构不加载像素速度快 except Exception: bad_list.append(p) print(损坏图片数量:, len(bad_list)) for p in bad_list: print(p)这段脚本的逻辑是遍历整个原始图片目录对每张图片调用im.verify()去校验文件头和解码结构。这里有个关键参数选择用的是verify()而不是load()因为verify()只检查文件格式和完整性不会把整张图读进内存几千张图跑完也就是几十秒的事。如果后续要做更严格的检查比如检测图像是否全黑、全灰再改成load()并统计像素分布。3.2 去重相同帧和近似帧会让验证集指标虚高车辆数据集的图片高度自相似特别是从视频里抽帧得到的数据同一辆车在相邻两帧里几乎一模一样。如果这些重复帧同时进了训练集和验证集训练出来的模型在验证集上的 mAP 会高得离谱一到真实场景就打回原形。这个去重步骤经常被人跳过但恰恰是让指标失真最严重的因素之一。我用的是感知哈希dHash做去重它比 MD5 更实用MD5 只能找出完全相同的文件而车辆图片之间往往只是裁剪、调色上的小差异感知哈希可以把这些“看起来差不多”的图也找出来。from PIL import Image def dhash(img, size8): gray img.convert(L).resize((size 1, size), Image.LANCZOS) bits [] for row in range(size): for col in range(size): left gray.getpixel((col, row)) right gray.getpixel((col 1, row)) bits.append(1 if left right else 0) return bytes( int(.join(str(b) for b in bits[i:i8]), 2) for i in range(0, len(bits), 8) ) def hamming(a, b): return sum(bin(x ^ y).count(1) for x, y in zip(a, b)) # 使用示例遍历目录计算每张图的 dhash # 两两比较汉明距离小于 5 判定为重复图像dHash 的核心思路是把图片缩小成 9×8 的灰度图比较相邻像素的亮度变化得到一个 64 位的哈希值。两张图越相似哈希值的汉明距离越小。size8是精度参数数值越大对细节敏感度越高但也会放过一些轻微变形的重复帧我一般在车辆数据集上把阈值定在 5 左右这个值能识别出同一段视频里抽出的相近帧又不会把不同车道的相似场景误判成重复。3.3 目录结构规范化images 与 labels 必须严格同名配对YOLO 系列框架对数据目录的约定是统一的图片放 images标签放 labels两个目录下再分 train 和 val 子目录。图片和标签是靠文件名关联的images/train/000001.jpg必须对应labels/train/000001.txt后缀不同没关系但主文件名必须一模一样。mkdir -p dataset/images/train dataset/images/val mkdir -p dataset/labels/train dataset/labels/val目录建好后把清洗和去重后的图片按比例复制进 train 和 val。这里我一般用脚本处理而不是手动拖拽原因是在图片数量上千以后手动操作很容易漏掉某张图的标签文件而 YOLO 训练时如果一张图没有对应标签默认会把它当背景图处理——这个“背景图”会直接影响 loss 的计算但你看日志基本发现不了。import os import random import shutil random.seed(42) img_list sorted(os.listdir(images_clean)) random.shuffle(img_list) val_num int(len(img_list) * 0.2) for i, name in enumerate(img_list): stem os.path.splitext(name)[0] src_img os.path.join(images_clean, name) src_txt os.path.join(labels_clean, stem .txt) if not os.path.exists(src_txt): continue # 没有标签的图直接丢弃不混入数据集 split val if i val_num else train shutil.copy(src_img, os.path.join(dataset/images, split, name)) shutil.copy(src_txt, os.path.join(dataset/labels, split, stem .txt)) print(处理完成train/val 已拆分)代码里的两个关键点一是random.seed(42)固定随机种子保证每次拆分的集合一致方便复现训练结果二是遇到没有对应标签的图片直接跳过而不是复制进数据集当背景。这一步和很多人的习惯不同——有人会把没标签的图当 negative sample 用这在特定场景是有效策略但对一个刚拿到的 car-dataset 来说我建议先保持干净只保留有标签的样本。4. 把标注转成 YOLO 格式VOC 转 txt 的脚本与三个关键参数4.1 从手工标注开始LabelImg 保存成 VOC XML如果拿到的数据需要自己标注最常见的工作流是用 LabelImg 画框。把图片目录打开后选择 PascalVOC 格式保存每张图会生成一个同名 XML 文件里面记录了size/width、size/height以及每个object的bndbox坐标。这个格式和 KITTI 的标注规格相近所以从 KITTI 抽取的车辆数据集也常常以 XML 形式散落在网上。这个步骤本身没什么技术含量但有一个操作细节值得注意标注时框要贴合车辆边缘尤其是车头和车尾这类轮廓分明的部位。如果框松了每辆车都多出一圈背景模型学习到的特征里就会混入路面信息如果框紧了车灯、车牌这些关键特征被切掉检测精度同样上不去。一两张图看不出差别几百张图累积起来就是几个点的 mAP 差距。4.2 VOC 转 YOLO 的转换脚本归一化、类别映射、坐标校验缺一不可拿到 XML 标注后接下来是最容易出错的一步把xmin, ymin, xmax, ymax这种绝对像素坐标转换成 YOLO 需要的归一化中心点坐标。转换公式本身不复杂但实际写脚本时有两个经常会踩的坑类别 id 怎么从标签名字映射成数字以及 XML 里的宽高和图片实际尺寸不一致时怎么办。import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_txt, map_id): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in map_id: continue # 不在目标类别里的目标直接忽略 cls_id map_id[name] bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h bbox_w (xmax - xmin) / img_w bbox_h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {bbox_w:.6f} {bbox_h:.6f}) with open(out_txt, w) as f: f.write(\n.join(lines)) # 使用示例只保留 car/truck/bus 三个类别 label_map {car: 0, truck: 1, bus: 2} voc_to_yolo(000001.xml, 000001.txt, label_map)这个脚本需要重点理解三个参数。第一是归一化的基准img_w和img_h必须来自 XML 里记录的尺寸而不是脚本运行时重新用 PIL 读取的尺寸。如果 XML 是从原始分辨率标注的而图片后又被压缩过两者不一致时转换出的坐标全部偏移这类错误在可视化阶段才能发现。第二是坐标精度.6f保留六位小数在 1920×1080 的分辨率下足够精确到像素级再多的位数没有意义。第三是类别过滤策略map_id里不存在的类别直接跳过这保证转换出来的 txt 里不会混入行人、骑车人这类无关目标。4.3 训练集验证集划分视频帧数据不要随机切分第 3 章里的随机划分脚本适用于零散图片但如果你的 car-dataset 是从视频抽帧得到的随机划分会出现严重问题。同一个视频片段里的相邻帧背景几乎一样模型在训练集里见过这一帧验证集里出现它的下一帧时相当于直接把答案泄露给了模型。验证集的 mAP 看着高部署到新场景立刻崩。正确的做法是按视频片段分组进行划分。假设文件名格式是highway_01_0001.jpg其中highway_01是视频片段编号就用这个前缀作为分组单位import os import random import shutil from collections import defaultdict random.seed(42) video_frames defaultdict(list) for name in os.listdir(images_clean): prefix name.rsplit(_, 1)[0] # 例如 highway_01 video_frames[prefix].append(name) video_ids list(video_frames.keys()) random.shuffle(video_ids) val_num int(len(video_ids) * 0.2) for i, vid in enumerate(video_ids): split val if i val_num else train for name in video_frames[vid]: stem os.path.splitext(name)[0] shutil.copy( os.path.join(images_clean, name), os.path.join(dataset/images, split, name), ) shutil.copy( os.path.join(labels_clean, stem .txt), os.path.join(dataset/labels, split, stem .txt), )按片段分组后同一个视频的帧只会出现在 train 或 val 其中一个集合里。这个处理会损失一部分训练样本量但换来的是验证指标的可靠性。我见过太多项目为了省事随机切分最后花了两周调参才发现是验证集泄漏导致的虚假高分。5. 训练前的数据集体检五类常见标注坑与排查手段5.1 坑一class id 与 names 顺序对不上训练不报错但推理全错这是 car-dataset 最容易踩的坑也是最难发现的一个。现象是训练过程完全正常loss 正常下降验证集的 mAP 看起来也不错但实际推理时一辆小轿车被识别成卡车或者被识别成公交车。原因是数据集的 txt 里 class id0 代表 car而训练配置的 data.yaml 里names: [truck, car, bus]框架不关心名字只按索引对齐。检查方法很简单在训练前统计一下标签里出现了哪些类别 idawk {print $1} dataset/labels/train/*.txt | sort -u这个命令会打印出训练集标签里所有的 class id。然后随机挑几张图片用可视化脚本画框确认 id0 对应的到底是什么车型。我一般会把这个检查结果直接贴在 data.yaml 旁边做注释防止过两周自己回来都忘了当时是怎么对齐的。5.2 坑二坐标越界或宽高为负导致 loss 突然变 NaN训练到一半 loss 飞掉这是最让人头疼的情况。现象是前几百轮 loss 正常下降突然某一步变成 NaN之后再也回不来。很多人以为是学习率太大把学习率调低重新跑结果还是在同一个位置崩掉。真正原因往往是标签数据里混进了非法坐标比如转换脚本没处理xmax xmin的情况或者某些框超出了图片边界导致归一化后的坐标大于 1。这类数据在数据集较大的时候只有零星几条但 PyTorch 在计算 loss 时只要碰到一个 NaN 就会把梯度全部污染。写一个全量扫描脚本很有必要import os def validate_labels(label_dir): bad_records [] for txt_name in sorted(os.listdir(label_dir)): txt_path os.path.join(label_dir, txt_name) with open(txt_path) as f: for line_no, line in enumerate(f, 1): parts line.strip().split() if len(parts) ! 5: bad_records.append((txt_name, line_no, 列数不是5)) continue try: cls, xc, yc, w, h map(float, parts) except ValueError: bad_records.append((txt_name, line_no, 存在非数字)) continue if xc 0 or yc 0 or w 0 or h 0: bad_records.append((txt_name, line_no, 坐标或宽高非正数)) if xc 1 or yc 1 or w 1 or h 1: bad_records.append((txt_name, line_no, 归一化值超过1)) return bad_records bad validate_labels(dataset/labels/train) for rec in bad[:20]: print(rec) print(非法记录总数:, len(bad))这段脚本逐行解析每个 txt检查三个条件行内是不是正好五个数字、坐标和宽高是不是正数、归一化后的值是不是在合理范围内。发现非法记录后直接打开对应图片确认是标注错了还是转换脚本的 bug千万别图省事把整份标签都删掉重来。5.3 坑三漏标和误标让验证指标彻底失真手动标注的数据集里漏标几乎不可避免但漏标比例超过一定阈值后mAP 就会变成完全不可信的指标。现象是验证集 mAP 很高但在实际场景里测试时全是误报。这里的机制很多人没想清楚模型把一辆没有标注的车检出来在验证集里会被判成 false positive拉低 precision。但模型在训练时又确实学到了“这就是车”的特征。所以漏标严重的数据集上模型会陷入一种矛盾——它对车的召回能力很强但验证集不承认这些检测结果。这时 mAP 低不代表模型差mAP 高也不代表模型好整个指标沦为摆设。解决漏标没有捷径只能靠人工抽查加可视化。随机抽一百张验证集图片把标签画上去看框是不是覆盖了所有明显的车辆目标。如果发现某张图里明显有三辆车但只标了一辆说明这批标注质量有问题要么补标要么把这张图踢出验证集。这个工作很枯燥但这是数据集上线前的必要体检。5.4 坑四EXIF 旋转信息导致标注框偏离实际位置手机或行车记录仪拍的照片经常带着 EXIF 旋转信息。图片存储时像素矩阵没转但查看器会根据 EXIF 自动转过来显示。PIL 默认读图时不会应用 EXIF 旋转直接用标注软件打开时标注软件转了而训练时框架读图没转两边的坐标系就差了 90 度或 180 度。现象是训练 loss 下不去可视化时发现框的位置和车辆完全对不上。处理方式是在标注和转换之前先把所有图片统一转正from PIL import Image, ImageOps import os for root, _, files in os.walk(raw_images): for f in files: if not f.lower().endswith(.jpg): continue p os.path.join(root, f) img Image.open(p) img ImageOps.exif_transpose(img) # 把旋转信息写入像素 img.save(p, quality95)ImageOps.exif_transpose会把 EXIF 里的旋转角度真正应用到像素数据上再保存时旋转信息就固化到像素里了。这个处理必须在任何转换脚本之前做否则后面所有坐标计算都是基于错误的图像方向。5.5 坑五样本不平衡白天多夜间少导致夜间检测崩溃这是一个场景分布问题而不是标注问题。车辆数据集里白天晴天的高质量图片最容易采集夜间、雨天、逆光场景稀缺。现象是白天测试效果很好稍微到晚上就完全失效。如果目标是做全天候部署的车辆检测光靠数据增强里的亮度抖动是不够的。亮度增强只能模拟暗环境但夜间车辆检测的特征和白天完全不同——车灯是主要视觉锚点车身的颜色和轮廓都看不清。我建议先统计一下数据集中图片的时段分布如果夜间占比低于 20%考虑从 BDD100K 这类全时段数据集里专门抽取夜间样本补进来。这一步会直接决定模型能否在真实场景落地。6. 用可视化和损失曲线验证数据集能不能直接开训6.1 最直接的验证把标签画到图上人工扫一遍数据集的验证工作里可视化永远比任何统计指标都直观。把 labels 里的 txt 转换成 OpenCV 能画的矩形框批量生成拼图检查。这里我写了一个小脚本会把验证集里每个 txt 对应的图片和框输出成一张张检查图import cv2 import os def draw_labels(img_path, txt_path, names, out_path): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls, xc, yc, bw, bh map(float, parts) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) # y1/y2 越界时截断到图像边界避免绘制报错 x1, y1 max(0, x1), max(0, y1) x2, y2 min(w, x2), min(h, y2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, names[int(cls)], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) cv2.imwrite(out_path, img) names [car, truck, bus] draw_labels( dataset/images/val/000001.jpg, dataset/labels/val/000001.txt, names, check_000001.jpg, )上面代码里的画框坐标是从归一化坐标反算出来的和训练时 YOLO 内部的计算方式一致。如果这个脚本画出来的框和车辆边缘明显不贴合说明转换脚本或 EXIF 处理存在问题。检查时重点关注三件事框是不是贴住车辆边缘、有没有明显漏掉大目标、class id 对应的类别名是不是对的。6.2 用损失曲线反查标注质量可视化确认过后我会先跑一个短训练然后看训练日志里的损失曲线。这里有几个经验性的判断train loss 下降但 val loss 降不动排除模型容量问题后多半是标注噪声太大——框的位置不准确模型在训练时反复学习错误的回归目标val loss 和 train loss 都降得很好但 val mAP 的召回率明显偏低则要怀疑验证集漏标。YOLO 的损失函数由分类损失、置信度损失和框回归损失三部分组成如果框回归损失在训练后期持续震荡不收敛回到数据集里检查框的宽高分布。车辆目标的框宽高比通常在 0.5 到 2.0 之间如果出现大量异常细长的框大概率是转换时坐标算错了。6.3 一个一直沿用的习惯先跑 200 张的快速训练再上全量我现在的流程是拿到任何一个 car-dataset先不急着全量训练。从训练集里随机抽 200 张图分辨率压到 640batch size 设 8跑 20 个 epoch。这个量级的训练只要十几分钟但能暴露绝大多数数据集问题loss 是否稳定下降、有没有 NaN、可视化输出是否正常。全量训练的时间成本高如果在跑了十个小时之后才发现标签错了整个项目节奏都被打乱。我之前就跳过可视化直接全量训练过一个卡口车辆检测模型三天后才发现转换脚本把宽和高写反了——损失曲线看起来一切正常因为模型学会了把错误的数据也“拟合”得很好。那个模型最后只能全部推倒重来这种后悔药是没法吃的。先用小样本验证流程再投入全量训练看起来多了一步实际是效率最高的路径。希望这个流程能帮你在车辆检测数据集上少走一些弯路至少从拿到数据的第一天起就先做一次体检再开训。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑