尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

VOC/YOLO/JSON三格式车辆行人检测数据集转换实战与训练避坑

VOC/YOLO/JSON三格式车辆行人检测数据集转换实战与训练避坑 简介面向道路车辆与行人目标检测任务这份数据集覆盖行人、轿车、大巴车、卡车四类目标可用于车辆不礼让行人检测、交通违章识别、自动驾驶视觉感知等实际项目也适配课程作业、算法比赛与毕业设计。数据共5930张jpg图片配套5931个txt和5930个xml标签并提供json格式便于跨框架使用txt适配YOLO、xml适配VOC主流检测算法可直接训练评估压缩包总计约1.7万个文件大小约922.74MB目录结构清晰。所有标注经博主多轮训练迭代比对筛选类别分布均匀、质量可靠。目前已有501人学习下载适合需要真实道路目标数据来提升模型泛化能力的中级及以上算法开发者。1. 5930张车辆行人检测数据集为什么建议你从三格式直接起步做车辆行人检测最磨人的往往不是模型结构而是数据本身。你从网上找来的车辆行人检测数据集5930张看起来数量不小真正动手才发现一张图背后要处理三套东西VOC的xml标签、YOLO的txt标签、还有一份或多份json标注。最核心的车辆与行人两类目标平摊到每张图上大约两三个标注框量不算大但格式之间的换算坑很深。这个压缩包的可贵之处在于它把三种格式一次性给你了省去了拿脚本到处凑数据的时间。很多人拿到之后的第一反应是“我只要YOLO的txt”直接删掉xml和json这恰恰是浪费。我把三种格式从字段到转换脚本完整拆一遍你照着跑完就知道这套数据值不值得投入时间清洗、增强然后送进模型训练。2. VOC(xml)、YOLO(txt)、JSON三格式的本质同一条标注的三副面孔2.1 VOC的xml从annotation到bndbox的字段结构VOC格式源自Pascal VOC竞赛它把每张图片的标注写成一个独立xml文件根节点是annotation里面依次记录图片路径、尺寸、目标列表。一个典型的车辆行人xml片段长这样annotation folderJPEGImages/folder filename000001.jpg/filename size width1920/width height1080/height depth3/depth /size object namevehicle/name bndbox xmin264/xmin ymin198/ymin xmax816/xmax ymax602/ymax /bndbox /object /annotation这个结构里最重要的引用关系是filename对应图片文件名size是图片真实尺寸object允许多个每个object里有一个name和一组bndbox坐标。坐标为像素的绝对左上角(xmin, ymin)与右下角(xmax, ymax)是闭区间有些工具算宽高时做xmax - xmin有些做xmax - xmin 1虽然只差1个像素对小目标检测的影响能到几个百分点。VOC格式的优势是自解释性强人眼直接能读懂缺点也很明显一个目标一个对象节点XML序列化和反序列化有额外开销训练框架不会直接吃这种格式。但它是校准其它格式的基准我一般先把一份数据用VOC洗一遍确认类别名和坐标范围无误再转成YOLO的txt喂给模型。2.2 YOLO的txt类别、中心点与归一化宽高YOLO系列从v5到v11使用的txt标注与VOC完全不同每张图对应一个同名txt文件txt里每一行代表一个目标格式是class cx cy w h。class是从0开始计数的整数类别索引cx、cy是目标中心点的归一化坐标除以图片宽度/高度w、h是归一化宽度和高度。比如上节VOC里的那个车辆目标在1920乘1080的图上对应txt行是0 0.28125 0.37037 0.2875 0.37407计算过程为cx (264 816) / 2 / 1920 0.28125cy (198 602) / 2 / 1080 0.37037w (816 - 264) / 1920 0.2875h (602 - 198) / 1080 0.37407。VOC用像素绝对值描述左上角和右下角YOLO用百分比描述中心点和宽高两者换算时最容易出错的就是除以高还是除以宽。中心点的横坐标一定除以图宽纵坐标除以图高宽除以图宽高除以图高四个值全部落在0到1之间才算正常。训练框架如Ultralytics YOLO读取txt后会在内部把归一化坐标还原成像素级坐标用于计算IoU所以txt里数值的精度直接决定标注框的还原精度我一般保留六位小数。2.3 JSON格式COCO风格的images与annotations压缩包里的json通常指COCO格式也可能是指某平台自定义的标注导出格式。大多数情况下它长这样{ images: [ { id: 1, file_name: 000001.jpg, width: 1920, height: 1080 } ], annotations: [ { id: 1, image_id: 1, category_id: 1, bbox: [264, 198, 552, 404], area: 223008, iscrowd: 0 } ], categories: [ {id: 1, name: vehicle}, {id: 2, name: person} ] }COCO的bbox不是左上角和右下角而是[x, y, width, height]即左上角坐标加宽高这是它与VOC的xmin/ymin/xmax/ymax最容易混淆的地方。category_id与id都是整数类别名只在categories字段里出现一次annotations里的每条标注通过image_id关联到对应图片。许多目标检测框架如MMDetection、Detectron2原生支持COCO格式看到json标签你可以直接用它来训练而不必非得转成YOLO的txt。3. 用脚本把三格式互相转换跑通5930张的完整流程3.1 VOC转YOLO一个不依赖第三方库的Python脚本把VOC的xml转成YOLO的txt是拿到5930张数据集后要做的第一件事。完整转换脚本我一般这么写import xml.etree.ElementTree as ET from pathlib import Path voc_dir Path(VOCdevkit) yolo_dir Path(yolo_labels) class_names [vehicle, person] # 按类别在names文件里的顺序排列 yolo_dir.mkdir(parentsTrue, exist_okTrue) for xml_path in voc_dir.glob(*.xml): tree ET.parse(xml_path) root tree.getroot() size root.find(size) width int(size.find(width).text) height int(size.find(height).text) txt_path yolo_dir / (xml_path.stem .txt) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: continue class_id class_names.index(name) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) cx (xmin xmax) / 2 / width cy (ymin ymax) / 2 / height w (xmax - xmin) / width h (ymax - ymin) / height lines.append(f{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) txt_path.write_text(\n.join(lines) if lines else )这段脚本用Python标准库xml.etree.ElementTree解析xml无需安装任何额外依赖。class_names列表的顺序必须与模型配置里的data.yaml中names一致否则类别索引会错位。脚本最后处理了空标注的情况一张图没有任何目标时生成一个空txt文件而不是跳过这能避免训练时图片与标注对不上。你可以在终端里运行python voc_to_yolo.py如果解析出错多半是xml里出现了不在class_names中的类别名脚本会直接跳过它。3.2 YOLO转VOC反向还原xml文件反过来如果你拿到的txt要回灌到某个只认VOC的标注工具或模型里就得逆向转换。YOLO的txt里没有图片尺寸信息你必须自己从图片文件里读宽高import cv2 from pathlib import Path yolo_dir Path(yolo_labels) image_dir Path(images) voc_dir Path(voc_xml_output) class_names [vehicle, person] voc_dir.mkdir(parentsTrue, exist_okTrue) for txt_path in yolo_dir.glob(*.txt): image_path image_dir / (txt_path.stem .jpg) img cv2.imread(str(image_path)) height, width img.shape[:2] lines [] for line in txt_path.read_text().strip().splitlines(): parts line.split() class_id, cx, cy, w, h int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) xmin int((cx - w / 2) * width) ymin int((cy - h / 2) * height) xmax int((cx w / 2) * width) ymax int((cy h / 2) * height) class_name class_names[class_id] object_xml f object name{class_name}/name bndbox xmin{xmin}/xmin ymin{ymin}/ymin xmax{xmax}/xmax ymax{ymax}/ymax /bndbox /object lines.append(object_xml) xml_content fannotation filename{txt_path.stem}.jpg/filename size width{width}/width height{height}/height /size {.join(lines)} /annotation (voc_dir / (txt_path.stem .xml)).write_text(xml_content)这段代码的关键是cv2.imread读取图片后用shape拿高和宽顺序是先行后列别写反。cx - w / 2是还原左上角xcy - h / 2是还原左上角y如果txt里某些框贴边算出来的xmin可能是负数这时需要截断到0否则标注工具会报越界。脚本里我把坐标int()取整了会丢失亚像素精度对检测影响不大但如果你要拿去训练分割模型就应该保留浮点。3.3 JSON转VOCCOCO结构的解析与类别对齐坑JSON转VOC比前两个转换多一层间接性因为COCO把类别名从标注对象里抽离到categories表中。一个常见的翻车点是json里的category_id是1和2但categories数组的索引是0和1直接拿索引当类别ID会整体错位import json from pathlib import Path json_path Path(annotations.json) image_dir Path(images) voc_dir Path(voc_from_json) voc_dir.mkdir(exist_okTrue) with open(json_path, encodingutf-8) as f: data json.load(f) # 先建类别ID到名字的映射 id_to_name {cat[id]: cat[name] for cat in data[categories]} image_id_to_info {img[id]: img for img in data[images]} annotations_by_image {} for ann in data[annotations]: image_id ann[image_id] annotations_by_image.setdefault(image_id, []).append(ann) for image_id, anns in annotations_by_image.items(): img_info image_id_to_info[image_id] width, height img_info[width], img_info[height] file_name img_info[file_name] object_list [] for ann in anns: x, y, w, h ann[bbox] object_list.append(fobject name{id_to_name[ann[category_id]]}/name bndbox xmin{int(x)}/xmin ymin{int(y)}/ymin xmax{int(x w)}/xmax ymax{int(y h)}/ymax /bndbox /object.strip()) xml_content fannotation filename{file_name}/filename size width{width}/width height{height}/height /size {.join(object_list)} /annotation (voc_dir / (Path(file_name).stem .xml)).write_text(xml_content)这段脚本先把categories里的ID到名字映射建好再按image_id把标注归到一起最后把COCO的bbox的x, y, w, h转成VOC的xminx, yminy, xmaxxw, ymaxyh。注意COCO的宽高可以是浮点数转VOC前取整是常规做法。如果json里某些图片在images里有信息但annotations里没有任何标注这段脚本不会为它生成xml这是设计上的取舍——空标注在COCO里合法在VOC里也可以有object为空的xml你需要根据训练框架的要求决定。4. 训练前必做的数据体检类别分布、小目标比例与train/val划分4.1 类别分布检查车辆和行人的比例决定模型偏好5930张数据听起来不少但车辆和行人两个类别的数量往往不是均衡的。用YOLO的txt统计类别分布只需一条命令cat yolo_labels/*.txt | awk {print $1} | sort | uniq -c | sort -rnawk {print $1}取出每行首个字段即类别IDsort | uniq -c按类别聚合计数。看到运行结果后你会立刻知道这张数据集的实际情况如果车辆类标注数量是行人的两三倍那么训练出来的模型对行人的漏检率会偏高这属于正常的数据偏差。解决方式不是删车辆样本而是给行人类别更高的权重在损失函数中调整类别权重或者对行人做更多的数据增强。另一个必查项是空标注文件的比例find yolo_labels -name *.txt -empty | wc -l如果空txt文件数量超过总数的5%说明数据集中存在大量没有车辆和行人的背景图这些图在训练时会被当作负样本如果比例太高模型会倾向于“什么都不检测”。我自己习惯的做法是统计出空文件后手动抽查一部分确认是标注遗漏还是背景本就无目标再决定保留还是删除。4.2 小目标检测从标注框面积分布看数据难度5930张车辆行人数据里远距离拍摄的行人框可能只有几十乘几十像素。在YOLO训练中这类小目标非常容易因为正样本匹配策略而沦为背景。你需要先知道小目标占比再决定是否要做切图或增强。用脚本统计所有标注框面积from pathlib import Path area_list [] for txt_path in Path(yolo_labels).glob(*.txt): for line in txt_path.read_text().strip().splitlines(): parts line.split() if len(parts) ! 5: continue _, cx, cy, w, h map(float, parts) # 归一化面积假设图片统一为640x640重新缩放 area_list.append(w * h * 640 * 640) small sum(1 for a in area_list if a 32 * 32) medium sum(1 for a in area_list if 32 * 32 a 96 * 96) large sum(1 for a in area_list if a 96 * 96) total len(area_list) print(fsmall: {small / total:.2%}, medium: {medium / total:.2%}, large: {large / total:.2%})这段脚本假设推理尺寸为640乘640来估算目标的大小区间符合COCO对小、中、大目标的定义。实际使用时你应该把640替换成自己训练时的imgsz参数否则分析结论会失真。如果小目标占比超过三分之一训练时建议开启YOLO的close_mosaic关闭马赛克增强并在最后几个epoch微调或者直接使用更高分辨率的imgsz如960或1280训练代价是训练时间增加和显存占用变大。4.3 train/val划分按图片目录划分比按文件随机切更稳三格式数据集的划分必须保持图片、xml、txt三者的文件名一一对应最常见的坑是用随机数脚本按文件切分结果训练集里有图没标注验证集里有标注没图。我推荐的划分方式是基于文件名列表import random from pathlib import Path all_images sorted(Path(images).glob(*.jpg)) random.seed(42) random.shuffle(all_images) split_idx int(len(all_images) * 0.9) train_files all_images[:split_idx] val_files all_images[split_idx:] for split, files in [(train, train_files), (val, val_files)]: Path(f{split}_images).mkdir(exist_okTrue) Path(f{split}_labels).mkdir(exist_okTrue) for img in files: # 复制图片 (Path(f{split}_images) / img.name).symlink_to(img.resolve()) # 复制对应txt src_txt Path(yolo_labels) / (img.stem .txt) if src_txt.exists(): (Path(f{split}_labels) / src_txt.name).symlink_to(src_txt.resolve())这个脚本的核心是用同一条文件名列表去关联图片与txt保证两边一定同步。symlink_to创建软链接而不是复制文件5930张图如果全部复制会占好几GB磁盘空间软链接方案对SSD空间紧张的人更友好。划分完后再跑一次上面的类别统计命令对比train和val的类别比例如果偏差超过20%说明随机划分把某个类别集中到了验证集需要重新设定随机种子或改用按类别分层划分。这一步看起来朴素却是整个数据集基建里最值得花时间的一段流程。5. 三格式数据集避坑5个检测训练常见翻车现场与排查记录5.1 xml中filename与实际图片文件名不一致现象训练时提示找不到xxx.jpg或者图片匹配上了但训练出来的模型mAP异常低。原因数据集经过多次拷贝、重命名后xml里的filename字段还保留着原始名字而实际图片名已经改成img_0001.jpg这类新编号。另一种可能是xml里只写文件名不带路径而你的代码期望它带完整路径。解决第一优先以实际图片名称为准遍历xml目录用Path.stem取每个xml的文件名去图片目录找对应文件找不到就报错。宁可让脚本在转换阶段直接失败也不要跳过等训练到一半爆发。我习惯在VOC转YOLO的脚本里加一个校验函数把xml的stem和图片目录做一次全量交叉比对输出缺失清单。5.2 YOLO的txt里出现cx0或w1的边界值现象训练loss正常但验证时某些目标的框永远偏到图片边缘或者一张图上出现巨大的、几乎覆盖全图的预测框。原因txt里的归一化坐标应当严格大于0且小于1但转换脚本在计算时用(xmin xmax) / 2可能会出现中心点等于0的情况比如目标完全贴边且标注框被裁切过。目标框宽度等于整张图宽度时w为1在YOLO的锚框匹配机制里不会触发严重问题但会显著拉低该样本的置信度。解决在转换脚本里过滤边界样本对w或h小于等于0的框直接丢弃。对cx或cy为0或1的框做max(1e-6, min(1 - 1e-6, value))的钳制保证数值严格落在开区间内。这是大多数主流训练框架内部也会做的一步只不过你提前做了能省掉后面排查时间。5.3 JSON的segmentation与bbox不一致现象按COCO json训练Mask R-CNN或YOLO-seg时损失函数里分割分支的loss剧烈震荡不收敛。原因COCO数据集允许annotations里同时出现bbox和segmentation部分标注工具的segmentation是从别的数据集合并来的多边形轮廓和bbox描述的矩形并不对应或者segmentation是空数组但bbox有值。这种做法在纯检测任务里不影响一旦用作分割就炸。解决如果只用检测忽略segmentation字段即可。如果要用分割先校验每个annotation的segmentation多边形是否完整且能计算出合理的mask最简单的方法是逐条调用pycocotools.maskUtils或cv2.fillPoly填充mask统计mask面积与area字段的偏差偏差过大就弃用该条标注。5.4 图片与标注名字不匹配但脚本却正常跑完现象数据集结构看起来完整YOLO训练时labels目录与images目录文件数一致但训练出的模型在验证集上mAP接近0。原因图片文件是000001.jpgtxt是000001.txt但txt内容其实是另一张图的标注。这类情况通常发生在二次打包时有人把A目录的图片和B目录的txt混在一起重新压缩文件名相同而内容错位。脚本只检查了文件名没有核对内容正确性。解决这个坑只能靠抽样可视化排查选10到20张图用脚本读txt画框人工看框是否贴合目标。把这段逻辑写成一个visualize_labels.py在每次拿到新数据集后先跑一遍输出带框的图花15分钟肉眼扫一遍后面能省下数小时无效训练时间。5.5 类别索引错位voc的name映射到yolo索引时整体偏移现象同一张图在VOC里标的是person转成YOLO后的类别ID是1但实际data.yaml里names: [person, vehicle]把ID 1当成了vehicle。原因第3.1节的脚本里class_names列表顺序是你自己写的如果你写成[vehicle, person]而别人的data.yaml是[person, vehicle]所有框的类别全部互换。VOC用字符串name天然可读YOLO用整数索引转换时刻的顺序就是唯一真值。解决在每个数据集的根目录放一个classes.txt内容顺序与data.yaml的names完全一致转换脚本只读这个文件不硬编码。我在实际项目中曾被这个坑折腾过一次后就把class_names从代码常量改成了外部文件输入半年内再没有出现类别张冠李戴。6. 把5930张扩成上万张小目标增强与难例自举的实战技巧6.1 copy-paste增强小目标场景涨点最快的土办法车辆行人数据集里小目标比例偏高时简单缩放全图会导致目标更小反而拉低性能。一个被验证有效的方案是copy-paste增强从训练集里裁剪出车辆、行人的前景块直接贴到背景图上同时把新生成的标注框写入txt。我一般用Python脚本配合OpenCV的cv2.findContours从已有的分割标注里提取前景或者干脆从训练图片里按目标框裁剪。贴的时候要注意目标尺寸的多样性和摆放位置避免遮挡关键区域贴完的样本用翻转、亮度抖动做二次扩展。这个操作能把5930张撑到12000张以上关键是保留好图片与标注文件的同步命名每张增强图生成对应的txt和图片。6.2 难例自举把漏检样本变成新训练数据训练完一版模型后把验证集里置信度低、且与真实框IoU低于0.3的预测结果挑出来这类样本往往是行人背对镜头、光线昏暗或遮挡严重。把这些图片单独收集成一个hard set混合到训练集里重训。为什么这个做法有效因为模型已经在大量常规样本上拟合得差不多了再喂常规样本收益很低而难例正好能矫正边界处的决策面。迭代两轮后你会发现车辆行人检测的mAP涨幅明显放缓说明这个数据集已经快到天花板了继续加难例不如调整输入分辨率或换成更强的backbone。6.3 用混淆矩阵验证模型是否真的在工作扩完数据、训完模型不要只看验证集mAP打开混淆矩阵看看。YOLO训练输出的混淆矩阵能清楚展示vehicle被误判成person的数量、背景被误检为目标的数量。有个血泪经验是当数据扩了一倍后mAP涨了4个点但混淆矩阵里车辆与行人的互相误判也涨了接近一倍说明复制的那些样本在放大模型的类别混淆倾向。这时候该做的不是继续扩数据而是回头检查标注质量看看有没有车辆框里混进行人的旧标注。我把这一步作为每次训练后的固定检查项就像写完代码要跑一次lint一样成本极低收益却很实在。数据集的整理从来不是一次性的体力活它贯穿整个模型迭代周期。我在第一次拿到这类三格式车辆行人数据时也走过先删xml再重新找脚本的弯路后来才体会到三种格式同时保留的最大价值不是给你选择权而是让你有一份可校验、可追溯的标注基准。每次训练前花20分钟跑一遍格式校验和分布统计比训练完再回头找原因要省力得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表