ARTICLE DETAIL

资讯详情

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

交通目标检测数据集:VOC格式转YOLO与训练避坑实战指南

交通目标检测数据集:VOC格式转YOLO与训练避坑实战指南 简介一套面向交通道路场景的目标检测标注数据集覆盖车辆、行人、自行车与摩托车等常见目标类别可帮助人工智能开发者和研究人员有效降低数据准备时间。资源采用Pascal VOC标准格式压缩包内共包含2000个XML标注文件整体大小约129.73MB每个XML文件均详细记录目标类别与边界框坐标兼容YOLO、Faster R-CNN、SSD等主流检测框架。该数据集适用于智慧交通、自动驾驶感知、安防监控等方向的模型训练与效果评测同时也适合高校学生用于计算机视觉课程设计和毕业设计。目前已有590人学习下载实践参考价值较高。需要留意的是压缩包内为纯标注文件使用时将XML与对应的原始图片一一对应即可快速构建完整训练集开展迁移学习、数据增强或场景适配实验。1. 交通道路上的车辆、行人、自行车与摩托车自动识别数据集VOC格式的2998张标注图能直接开跑做交通场景目标检测最烦的不是模型选型而是数据从哪来。这套数据集定位很实在2998张真实道路场景原始图片用VOCPASCAL VOC格式把车辆、行人、自行车、摩托车四类目标逐框标好解压就能喂进目标检测训练流程。对想快速验证YOLOv5/YOLOv8、Faster R-CNN这类检测模型的从业者来说它省掉了最耗时的采集和人工标注环节。适合三类人做智慧交通、车道监控项目需要预训练数据验证的工程师拿公开数据学习目标检测完整流程的新手以及需要补充道路场景小类别样本做调优的算法同学。2. 拆解VOC标注格式从文件命名到XML标签结构2.1 数据集文件清单与命名规律解压后先看文件结构别急着训练。这套资源的文件命名非常有规律图片和标注XML成对出现例如3249_jpg.rf.ceb90c116aa819c33038ff32065306ee.xml 3249_jpg.rf.ceb90c116aa819c33038ff32065306ee.jpgXML和JPG共享同一个主文件名只是扩展名不同。文件名分三段数字编号、jpg标识、一长串哈希。中间的rf标记和末尾哈希串是标注平台在导出数据集时自动生成的关联ID作用是保证XML与图片一一对应多源数据合并时不会因为重名互相覆盖。这种命名风格在很多平台导出的数据集里都能见到不影响标注内容本身。我拿到这类压缩包后的第一个动作是按VOC标准目录约定重新整理。原始包大概率是图片和标注XML平铺而YOLO、MMDetection这些训练框架读取数据时习惯按固定目录结构找文件。常见做法是建一个这样的目录traffic_dataset/ ├── JPEGImages/ # 全部jpg图片 ├── Annotations/ # 全部xml标注 └── ImageSets/ └── Main/ # train.txt / val.txt用一条shell命令批量移动文件避免手工拖拽浪费时间mkdir -p traffic_dataset/JPEGImages traffic_dataset/Annotations traffic_dataset/ImageSets/Main mv *.jpg traffic_dataset/JPEGImages/ mv *.xml traffic_dataset/Annotations/ ls traffic_dataset/JPEGImages | wc -l # 核对图片数量 ls traffic_dataset/Annotations | wc -l # 核对标注数量这段逻辑先建目录再按扩展名批量归类。wc -l用来核对两边数量正常情况下两个数字应该完全一致都是2998如果对不上说明存在缺失或多余文件趁早排查不要拖到训练时报错。提示如果压缩包里图片和XML已经各占一个文件夹直接把原有文件夹改名为JPEGImages和Annotations就行不用真的copy一遍浪费磁盘。2.2 VOC标注的XML结构字段含义与读取重点打开一个XML以3249_jpg.rf.ceb90c116aa819c33038ff32065306ee.xml为例标准VOC目标检测标注长这样annotation folderJPEGImages/folder filename3249_jpg.rf.ceb90c116aa819c33038ff32065306ee.jpg/filename source databaseUnknown/database /source size width1280/width height720/height depth3/depth /size segmented0/segmented object namecar/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin352/xmin ymin284/ymin xmax631/xmax ymax432/ymax /bndbox /object /annotation关键字段的含义和使用优先级如下字段含义说明folder图片所在目录通常为JPEGImages部分训练框架会忽略filename图片文件名转换格式时用来定位图片注意不要改动扩展名size图片宽度/高度/通道数转YOLO归一化坐标的除数缺失或不对会直接算错object/name类别名必须是person、car、bicycle、motorcycle等约定内名称truncated目标是否被截断1表示目标超出画面边缘训练时可选择性保留difficult目标难易程度1表示难以辨识的样本对模型训练有干扰bndbox真实框坐标xmin/ymin/xmax/ymax整数像素坐标读取标注时最该盯紧的是size和bndbox。所有的后续转换都依赖这两个信息size提供分母bndbox提供分子。如果某张图的size和实际图片分辨率对不上归一化后的坐标就是错的模型学到的框位置会系统性偏移。这个坑在后面的避坑章节会展开说。另外该数据集的类别用name标识没有显式的类别映射文件。转格式时类别顺序完全由我们自己定义我习惯按person, car, bicycle, motorcycle这个顺序建索引0: person 1: car 2: bicycle 3: motorcycle这个顺序一旦定下来训练配置和标注txt就都绑定这个索引中途改顺序等于重新标注一遍所以第一步就要想好。从标题来看这套数据的类目就是这四类“车辆、人、自行车、摩托车”做车辆检测竞品对比或车流量统计时这个粒度刚好够用。2.3 用脚本统计类别与初筛标注质量训练前先做一次“体检”看每个类别的样本量、框数量分布判断数据均衡度。写一个简单的Python脚本扫一遍所有XMLimport xml.etree.ElementTree as ET import glob xml_files glob.glob(Annotations/*.xml) stats {} total_boxes 0 for xml_file in xml_files: tree ET.parse(xml_file) root tree.getroot() for obj in root.findall(object): name obj.find(name).text stats[name] stats.get(name, 0) 1 total_boxes 1 print(total images:, len(xml_files)) print(total boxes:, total_boxes) for cls, cnt in sorted(stats.items(), keylambda x: -x[1]): print(f{cls}: {cnt})这段脚本的核心是用glob拿到所有XML逐文件解析object/name按类别计数。输出里比较关键的是两个数字XML文件数是否等于2998各类别框数是否均匀。正常情况下car和person会明显多于bicycle和motorcycle这是真实道路场景的常态不一定是质量问题。但如果某个类别完全没有或数量只有几十个就要考虑这个类是否训练得动后面要不要做过采样或数据增强。同时我还会顺手检查空标注文件的情况。图片存在但XML里没有object的文件会导致训练时该图片loss为0白白占用一个iterationempty [f for f in xml_files if not ET.parse(f).getroot().findall(object)] print(empty annotation files:, len(empty))空标注文件数量占比超过5%的话建议直接剔掉不要带进训练集。3. 把VOC转成YOLO格式转换脚本与数据划分实践3.1 为什么YOLO训练要归一化txt标注VOC格式是给“人看”的YOLO系列训练则要“txt归一化坐标”。原因很简单YOLO的标签格式要求每个目标占一行格式为类别ID 中心点x 中心点y 宽度w 高度h所有坐标都除以图片宽高归一化到0~1之间。这样做的好处是不受图片分辨率影响同一目标在不同尺寸输入下标签值不变训练时无论输进去的特征图分辨率怎么变损失计算都不需要重新缩放坐标。VOC格式直接喂给YOLO训练会报错因为YOLO的DataLoader只认.txt标注和.jpg/.png图片XML文件不在读取范围内。所以转换是必做的步骤。转换前先把类别索引定好我按上一章的顺序来person0car1bicycle2motorcycle3。如果你后面要加载COCO预训练权重注意COCO里的类别ID和这个不完全一致需要额外做类别映射否则模型会以为cat叫person。3.2 VOC转YOLO的Python脚本转换脚本一次性处理全部XML输出对应的txt文件。以下是我在一个道路场景数据集上跑过的版本直接可用import os import xml.etree.ElementTree as ET # 类别顺序必须全局统一 CLASSES [person, car, bicycle, motorcycle] def convert_xml2yolo(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) if img_w 0 or img_h 0: print(f[skip] bad size in {xml_path}) return False lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASSES: print(f[warn] unknown class {name} in {xml_path}) continue cls_id CLASSES.index(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) # 防御异常框坐标反向或宽高为0 if xmax xmin or ymax ymin: print(f[skip] invalid bbox in {xml_path}: {name}) continue x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 归一化后超出[0,1]时做截断YOLO训练不接受越界值 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if not lines: return False base os.path.basename(xml_path).replace(.xml, .txt) with open(os.path.join(out_dir, base), w) as f: f.write(\n.join(lines)) return True # 批量转换 xml_dir Annotations out_dir labels os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if xml_file.endswith(.xml): convert_xml2yolo(os.path.join(xml_dir, xml_file), out_dir)几个值得展开的参数和逻辑CLASSES列表的顺序决定txt第一列的ID务必和训练配置里的names对齐。脚本里的防御逻辑不是可有可无xmax xmin这种反向框在人工标注时偶尔会出现不拦截的话坐标会算成负数训练loss直接飞size为0的图跳过避免除零。归一化后做min/max截断是为了防止某个框越出画面边界一点点导致YOLO在预处理时报“标签越界”错误这类报错很常见而且通常会直接中断训练。转换完成后随机抽一个txt检查cat labels/3249_jpg.rf.ceb90c116aa819c33038ff32065306ee.txt输出大概长这样1 0.4734 0.5132 0.2180 0.2056 0 0.3148 0.6875 0.1139 0.2364第一列是类别ID后面依次是中心点x、中心点y、宽、高全部在0~1之间。如果看到某个值等于1.0或0.0说明有框正好贴边这种框多半是截断目标truncated1可保留可剔除看你的训练策略。黄色车牌、夜间灯光这类特殊样本也会在转换时暴露出一些坐标异常多抽查几张没坏处。3.3 训练集/验证集划分与dataset.yaml配置转换完标注后划分训练集和验证集。我一般按8:1:1分成train/val/test或者8:2只分train和val。这里用random和shutil快速实现import os import random import shutil random.seed(42) dataset_root traffic_dataset img_src os.path.join(dataset_root, JPEGImages) label_src labels train_img os.path.join(dataset_root, images, train) val_img os.path.join(dataset_root, images, val) train_lbl os.path.join(dataset_root, labels, train) val_lbl os.path.join(dataset_root, labels, val) for d in [train_img, val_img, train_lbl, val_lbl]: os.makedirs(d, exist_okTrue) imgs [f for f in os.listdir(img_src) if f.endswith(.jpg)] random.shuffle(imgs) val_cnt int(len(imgs) * 0.2) val_imgs, train_imgs imgs[:val_cnt], imgs[val_cnt:] def move(img_list, img_dst_dir, lbl_dst_dir): for img in img_list: base os.path.splitext(img)[0] lbl base .txt shutil.move(os.path.join(img_src, img), os.path.join(img_dst_dir, img)) shutil.move(os.path.join(label_src, lbl), os.path.join(lbl_dst_dir, lbl)) move(train_imgs, train_img, train_lbl) move(val_imgs, val_img, val_lbl) print(ftrain images: {len(train_imgs)}, val images: {len(val_imgs)})逻辑说明先用random.seed(42)固定随机种子保证每次划分结果一致复现训练结果时不会被数据划分干扰。然后按20%比例切出验证集shuffle之后再切片而不是直接取前20%避免原始文件名排序带来的分布偏差。最后图片和对应的txt一起移动保证两边目录结构同步。这里有个细节shutil.move是移动而非复制原目录清空后不要再从原目录读取数据。YOLOv5/YOLOv8训练时还需要一个描述数据集的yaml文件path: ./traffic_dataset train: images/train val: images/val names: 0: person 1: car 2: bicycle 3: motorcycle注意yaml里的path是相对当前工作目录还是绝对路径取决于你的训练命令。我习惯用绝对路径因为不同机器跑训练时当前目录经常不一样相对路径最容易翻车。names的索引顺序要和转换脚本里的CLASSES完全一致哪怕是名称相同索引错位训练出来的模型预测类别也会全部对不上。4. 实战避坑VOC标注数据训练中的常见问题与排查4.1 图片尺寸与XML记录不一致训练时坐标漂移现象模型训练正常但验证集上所有预测框都偏小或偏左看起来像是“标注不准”。同一批数据在别的框架上跑却正常。原因XML里size记录的分辨率和实际jpg分辨率不一致。常见于标注平台导出时对图片做过压缩或Resize但XML里的size没有同步更新。归一化转换时用XML里的宽度做分母但YOLO训练时用的是实际图片尺寸于是坐标整体偏移。这也是很多平台导出数据集的通病属于标注导出的历史包袱。解决转换前做一个尺寸校验用OpenCV或PIL读真实图片尺寸和XML里的size逐张比对from PIL import Image import xml.etree.ElementTree as ET xml_file Annotations/3249_jpg.rf.ceb90c116aa819c33038ff32065306ee.xml img_file JPEGImages/3249_jpg.rf.ceb90c116aa819c33038ff32065306ee.jpg with Image.open(img_file) as im: real_w, real_h im.size tree ET.parse(xml_file) root tree.getroot() xml_w int(root.find(size/width).text) xml_h int(root.find(size/height).text) print(real_w, real_h, xml_w, xml_h) print(match if (real_w, real_h) (xml_w, xml_h) else mismatch)如果mismatch以图片真实尺寸为准重新转换标注。从那以后我每拿到一批带标注的数据第一件事就是跑这个校验脚本没有例外。4.2 带哈希的长文件名导致路径匹配失败现象训练时YOLO报“image X has no labels”但明明转换脚本生成了对应txt。手动打开labels目录txt也在。原因文件名里的_jpg.rf.和一长串哈希让文件主名相当长。某些工具或脚本在拼接路径时截断或替换了字符导致图片文件名和txt文件名对不上。另外在Windows上超长路径也会引发文件读取失败路径超过260字符时系统直接拒绝访问。解决第一步用脚本核对每个txt是否和jpg同名同目录存在第二步干脆把文件批量重命名为统一的短编号count0 for f in JPEGImages/*.jpg; do count$((count1)) newname$(printf img_%04d $count) mv $f JPEGImages/${newname}.jpg base$(basename $f .jpg) [ -f labels/${base}.txt ] mv labels/${base}.txt labels/${newname}.txt done这段脚本把jpg按顺序重命名为img_0001.jpg这种格式同时把对应txt同步改名。代价是原始文件名信息丢失但换来的是路径短、匹配稳后续不管换训练框架都省心。哈希文件名是平台导出留下的痕迹不影响标注内容重命名不影响训练结果。4.3 difficult1的样本被直接吞掉现象训练完看指标mAP不低但模型对遮挡严重的行人、远处的小车基本不检。翻开XML统计发现这类样本大量标注了difficult1/difficult。原因VOC标准里difficult1表示目标难以辨识PASCAL VOC评测时不计入AP计算。不少转换脚本会直接跳过difficult1的框等于这些样本从训练数据里消失了。对检测框架来说这就是“没标过”模型自然学不到遮挡场景。真实道路场景里被树挡一半的行人、被前车挡住的摩托车恰恰是高频出现的目标把它们全扔了等于放弃最难也最有价值的样本。解决转换时不要默认丢弃而是把difficult1的框也转进txt但单独统计数量方便后续分析。如果数据里difficult占比高建议保留同时配合图像增强来提升遮挡场景的泛化。只有当你确定这些框标注质量差、会污染训练时才考虑剔除。4.4 类别严重不均衡摩托车样本被淹没现象训练loss正常下降但验证集里motorcycle这一类AP只有个位数其他三类都在60以上。查看类别统计发现摩托车框数只有几百而car有上万。原因数据分布本身就不均衡损失函数里所有类别权重相同多数类梯度主导了训练方向少数类学不到足够特征。道路场景天然如此motorcycle和bicycle出现频率远低于car和person这不算标注问题是数据分布的客观现实。解决先做类别计数确定不均衡程度然后对样本少的类别做复制增强每张图重复进训练几轮或Mosaic增强让模型每轮都见到它们。如果还是不理想给损失函数加类别权重YOLOv5的配置文件里有现成的类别权重参数可以调。单纯靠复制增强能解决一部分核心还是增加该类别的多样性模糊化复制对泛化帮助有限。4.5 小目标标注框在下采样后消失现象训练输入分辨率是640x640但数据里很多目标框宽度小于20像素。训练后模型对远处的车、小个头的行人漏检严重。原因VOC框坐标是原始分辨率下的像素值YOLO训练会把图片缩放到输入尺寸。一个1920x1080的图缩放到640后小于缩放比例的目标在特征图上只占几个像素甚至小于下采样倍数直接被网络忽略。真实道路场景里远处车辆、人行道尽头的行人都是这类小目标是业务刚需不能直接放弃。解决转换时设置小目标过滤阈值把归一化后宽度或高度小于0.02的框剔除或单独处理。更推荐的做法是训练时用更高输入分辨率如1280、或者用Tiling切图的方式把大图切成小块训练。我在实际项目里用Tiling方案把这类数据集的mAP提了大概8个点但代价是训练时间拉长显存占用翻倍需要自己权衡。5. 验证标注质量与进阶用法从可视化检查到训练闭环5.1 画框可视化转换后先看再训转完格式、划分完数据后别急着敲训练命令。先用OpenCV把随机几张图连同预测框画出来肉眼确认标注没跑偏。脚本很短import cv2 import os import random img_dir traffic_dataset/images/val lbl_dir traffic_dataset/labels/val names [person, car, bicycle, motorcycle] for img_name in random.sample(os.listdir(img_dir), 10): img_path os.path.join(img_dir, img_name) lbl_path os.path.join(lbl_dir, os.path.splitext(img_name)[0] .txt) img cv2.imread(img_path) h, w img.shape[:2] with open(lbl_path) as f: for line in f: parts line.strip().split() cls_id, cx, cy, bw, bh map(float, parts) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, names[int(cls_id)], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(check_ img_name, img)这段脚本把归一化坐标还原成像素坐标再画框。肉眼检查的重点有两个一是框是否贴合目标轮廓二是标签和框内容是否匹配。我通常抽样10到20张尽量覆盖白天、夜间、雨天各类场景。这个检查动作跑一次只要几分钟但能拦住大量的标注错位问题比训练完再回头查数据高效太多。5.2 训练后按类别看指标找短板再喂数据模型训完一轮后不要只看整体mAP要看每个类别的AP。YOLOv5的val模式会输出每个类别的AP表格。我拿到这类多类别道路数据集时的操作路径是先看person和car的AP能不能到70以上再单独看bicycle和motorcycle。后者如果明显偏低就回去做4.4的类别均衡处理或者给这两个类单独切一批图做增强后再补训。数据增强方面这套道路场景数据集用Mosaic和HSV扰动效果最直接。Mosaic把四张图拼成一张变相增加了每张训练图的目标数量对小目标密集场景帮助明显HSV扰动模拟不同光照对白天黑夜混杂的道路图很友好。这两个增强参数不建议拉满Mosaic概率0.5到0.7就够HSV的饱和度扰动控制在0.1附近拉太高会让颜色失真反而掉点。跑完一轮迭代后我会把增强后的样本可视化一遍确认增强没有把标注框拉扯到错误位置。数据增强是把双刃剑参数调猛了模型学到的可能是“变形”的目标而非真实的交通目标。一个习惯从那以后我每拿到一份外部标注数据集都是先跑尺寸校验再转格式再可视化抽查最后才开训练这套流程走完基本不会再被标注问题坑到半夜。希望帮到你。本文还有配套的精品资源点击获取
返回列表