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

资讯详情

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

路面信息数据集解析:VOC XML转YOLO格式实战

路面信息数据集解析:VOC XML转YOLO格式实战 简介这是一套面向自动驾驶与智能交通场景的路面信息数据集覆盖汽车、卡车、自行车、行人、路牙以及多种道路标线、压线状态等18类路面要素。数据按JPEGImages和Annotations两个文件夹组织分别存放2684张jpg原图和2684个xml标注文件图像与标注一一对应压缩包整体约209.49MB。包内文件类型以jpg和xml为主目录结构直观可直接用于目标检测、实例分割、车道线识别等主流模型的训练与验证。标注体系划分细致包含白实线、双白线、黄实线、双黄线、黄虚线、白虚线以及实线/虚线头尾等细粒度类别并额外提供压线、未压线状态适合道路元素精细感知任务。数据集目前已有1195人学习下载对从事自动驾驶感知、路面识别研究的学生和工程师来说是一份可直接上手的真实场景数据。1. 路面信息数据集长这样先看JPEGImages和Annotations对不对得上拿到一份路面信息数据集解压后看到JPEGImages和Annotations两个目录各自2684项这个结构一眼就能认出是PASCAL VOC体系。它不负责告诉你类别有多少种、图像分辨率多大只负责把图片和图片里每个目标的标签用同名文件摆在一起。做路面裂纹检测、坑槽识别或道路健康评分的人拿到这类数据的第一天通常不是急着训练而是先确认两份清单的对应关系是否完整。很多采集任务在打包时会出现中间丢失xml或图片的情况如果直接喂给训练脚本后期排查代价远比先用几行脚本核对要大。这条路子也适合做路面状态评估、自动化巡检标注质量抽检的从业者下文的解析思路不依赖具体类别名换一套标签同样适用。2. 拆解Annotations的XML结构路面信息如何存进标注文件2.1 为什么是JPEGImages和Annotations而不是一张图片一个JSONJSON紧凑、易读CV任务里却很少成为首选原因是标注文件要跟图片生命周期分开管理。JPEGImages只放原图Annotations放同名XML任何标注协议的升级比如从单框变成带多边形分割都只改动Annotations目录图片字节不变。采集过程通常是用LabelImg这类工具逐张画框保存时自动生成与图片同名的XML。路面信息数据集的XML里每个object代表一个路面目标常见的name可能是crack、pothole、repair、lane_line这类自定义标签也可能是damage和normal两类粗粒度标签。目录结构通用类别语义不同所以下面所有解析与转换代码不需要关心你手上具体的类别名。2.2 XML里与训练强相关的5个字段标注软件保存的XML不是给人看的是给训练框架读的值得关心的字段只有下面几个。XML路径作用训练中的用法filename对应JPEGImages文件名按stem快速关联图片与标注size/width 与 size/height原图宽高判断bbox是否越界、换算归一化坐标object/name目标类别名映射成class_idobject/difficult是否难例设为1时多数框架默认跳过object/bndbox(xmin,ymin,xmax,ymax)目标边界框训练的回归目标重点说一下difficult。路面目标经常被路边护栏、行人遮挡或因为拍摄距离太远而模糊标注员为了不让这些框污染训练会把它们标成difficult。但注意VOC官方的评估和多数检测框架的默认行为是忽略difficult样本。如果你希望模型在遮挡场景下仍有召回就不能简单跳过可以在解析时保留一个开关。另一个容易踩的是坐标起点。部分标注工具生成的XML坐标为1-based像素索引而cv2、PIL画图以及一些训练框架的anchor计算按0-based处理两者差1像素。单像素偏移在普通检测里无感但在路面裂缝这种窄目标上如果再叠加下采样1像素偏差会让框偏移出裂缝带。解析时把xmin、ymin减去1还是保持不变取决于你后续框架的约定关键不是用哪套而是全流程必须统一。2.3 用命令和Python快速核对2684项文件先看最基础的完整性与命名规则。如果文件名不是连续编号而是带采集时间或路段ID那更好直接从命名上确认来源。cd 数据集的根目录 ls JPEGImages | sed s/\.jpg$// | sort /tmp/jpeg.txt ls Annotations | sed s/\.xml$// | sort /tmp/anno.txt diff /tmp/jpeg.txt /tmp/anno.txt echo 一一对应这段命令把两个目录的文件名去掉扩展名排序后交给diff。diff无输出说明两侧完全一致正常语义就是一一对应。sed把图片扩展名去掉是为了让两者可比因为XML文件是.xml扩展名。如果diff输出两边差异再用comm -3 /tmp/jpeg.txt /tmp/anno.txt查看差集定位是缺图片还是缺标注。用Python检查则能同时把命名规则、损坏图片、空标注一起查掉from pathlib import Path import re jpg_dir Path(JPEGImages) xml_dir Path(Annotations) jpg_ids {p.stem for p in jpg_dir.glob(*.jpg)} xml_ids {p.stem for p in xml_dir.glob(*.xml)} print(f图片 {len(jpg_ids)} 项标注 {len(xml_ids)} 项) print(只有图片没有标注:, jpg_ids - xml_ids) print(只有标注没有图片:, xml_ids - jpg_ids) pattern re.compile(r^\d{6}$) bad_names [p.name for p in jpg_dir.glob(*.jpg) if not pattern.match(p.stem)] print(不符合6位数字命名的文件:, bad_names[:10])参数说明Path.glob(*.jpg)只匹配JPG后缀如果数据里有png需要改后缀p.stem取文件名主体部分也就是不带扩展名的那段。正则^\d{6}$匹配正好六位数字这种命名是VOC工具生成的默认风格。如果数据来自路面巡检车文件名往往是路名加里程桩可以把匹配串改成你需要的规则后复用。检查完再开始下一步解析基本能挡住一半的返工。3. 用Python把2684项JPEGImages读成可训练的路面样本3.1 最小XML解析器一行一个框这里用标准库写一个解析器不引入任何第三方依赖。xml.etree.ElementTree是Python自带的XML解析模块用来读VOC这种简单结构足够快不必为这一步上lxml。import xml.etree.ElementTree as ET from pathlib import Path def parse_voc(xml_path: Path) - dict: tree ET.parse(str(xml_path)) root tree.getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) objects [] for obj in root.iter(object): name obj.find(name).text difficult int(obj.find(difficult).text or 0) truncated int(obj.find(truncated).text or 0) 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) objects.append({ name: name, difficult: difficult, truncated: truncated, bbox: [xmin, ymin, xmax, ymax], }) return {filename: root.find(filename).text, width: w, height: h, objects: objects}代码逻辑说明root.find(size/width)用相对路径取size节点下的width子节点会比先拿size再拿width少两行root.iter(object)遍历当前XML里所有object节点而不是find只取第一个。一张路面图上有多处病害靠iter而不是find否则只会读到第一个框。所有坐标都转成float是为了防止后续做归一化时因为整除丢精度检测框坐标最终还是要作为连续数值送入回归头。参数说明int(obj.find(difficult).text or 0)是对空节点做兜底。部分标注工具在不勾选时不会写出difficult字段直接.text会得到None导致int报错truncated同理。bbox读出来的坐标顺序是(xmin, ymin, xmax, ymax)左上右下后文所有代码都遵循这个顺序。3.2 可视化把框画到原图上做直观校验解析出来的数据不要先想相对大小直接画到原图上看能发现很多结构化检查发现不了的问题。import cv2 def draw_boxes(img_path: Path, anno: dict, colors: dict None) - None: if colors is None: colors {crack: (0, 0, 255), pothole: (0, 255, 255), lane_line: (0, 255, 0)} img cv2.imread(str(img_path)) if img is None: print(f图片读取失败: {img_path}) return for obj in anno[objects]: xmin, ymin, xmax, ymax [int(v) for v in obj[bbox]] color colors.get(obj[name], (0, 255, 0)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), color, 2) label obj[name] cv2.putText(img, label, (xmin, max(ymin - 4, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) out f{anno[filename]}.checked.jpg cv2.imwrite(out, img)这段代码把每个bbox按类别着不同颜色画到原始图上。int(v)这一步很关键因为cv2.rectangle的坐标参数要求整数float坐标直接传进去会报类型错误max(ymin - 4, 0)防止文字画到图像上边界之外否则左上角目标的标签会贴在半截。如果发现某张图上裂缝的框是横跨全图的粗框而不是沿裂缝走向的细长矩形基本可以判断是标注员把裂缝整条外接矩形给框了这种框会让模型学到大量空白背景训练前就要重新标注或做切分。可视化校验建议不要只抽查一两张而是抽20到50张均匀分布覆盖白天、阴影、桥梁接缝等不同场景重点看细裂缝框是否贴住病害边缘。3.3 生成训练清单划分train和val要防相邻帧泄漏训练检测模型前需要把2684项拆成训练集和验证集。路面巡检通常是连拍相邻帧的病害位置几乎重叠如果随机划分模型会在验证集上看到几乎同样的目标val分数会虚高。import random from pathlib import Path random.seed(2024) all_images sorted(Path(JPEGImages).glob(*.jpg)) val_count int(len(all_images) * 0.15) random.shuffle(all_images) val_set set(all_images[:val_count]) with open(train.txt, w) as ft, open(val.txt, w) as fv: for img_path in all_images: img_str str(img_path.resolve()) if img_path in val_set: fv.write(img_str \n) else: ft.write(img_str \n)参数说明val_count按总量的15%取整2684项的15%约402张按经验这个比例对中等规模数据集够用。random.seed(2024)固定随机种子保证同一次划分可以复现。写出的train.txt和val.txt里是图片的绝对路径训练脚本读取清单后按路径找图片再通过图片stem找对应XML这是大多数检测框架能直接吃的格式。如果数据是巡检车连拍更稳妥的做法是先按文件名前缀分组再把整个组划到同一侧避免轨迹泄漏。4. 路面数据集的高发问题坐标越界、类别失衡与细缝丢失4.1 四类脏数据及检查方式由于上文的parse_voc已经提供width和height可以顺手写一个巡检脚本bad_cnt 0 for xml_path in sorted(Path(Annotations).glob(*.xml)): anno parse_voc(xml_path) for i, obj in enumerate(anno[objects]): xmin, ymin, xmax, ymax obj[bbox] if xmin 0 or ymin 0 or xmax anno[width] or ymax anno[height]: print(f{xml_path.name} 第{i}个框越界: {obj[bbox]}) bad_cnt 1 print(f越界框总数: {bad_cnt})逻辑说明把解析出的坐标直接与XML里的原图宽高比较任何一边超出即判定越界。越界框的来源主要是工具导出bug和人工手滑另外如果之前有脚本对图像做过缩放却没有同步更新XML这里也会暴露。检查结果若越界框占比超过1%建议退货或重新标注而不是在代码里硬剪裁。脏数据类型典型特征处理方式图片与标注缺失diff有差集删除多余项或回退采集端核对图片损坏cv2.imread返回None直接移除对应样本对bbox越界xmax大于width等裁剪到边界或丢弃该框空标注objects为空保留作负样本但确认框架支持4.2 类别分布统计与路面任务的不均衡路面数据集有一个很现实的规律裂缝出现的数量级远高于坑槽或修补带。用parse_voc统计一下from collections import Counter counter Counter() for xml_path in sorted(Path(Annotations).glob(*.xml)): anno parse_voc(xml_path) counter.update(obj[name] for obj in anno[objects]) print(counter.most_common())如果crack一类的样本数达到其他类别的数十倍训练出的模型会对稀疏类别几乎不产生梯度。常见处理是设定类别采样权重在采样时让稀有类别有更高概率被抽到。具体到参数层面多数框架支持按类别设置权重数组例如给pothole权重2.0、crack权重0.8让稀有类别在损失中占比更大另一个思路是在数据加载时按类别数做过采样而不是简单复制图片复制图片会让模型记住重复样本的噪声。4.3 2684项不够用滑窗切patch是路面数据的常规操作路面病害有个特点细裂缝往往只在原图的高分辨率下可见。整图输入会被缩放裂缝只有几个像素宽缩放后基本消失。常见做法是在训练前把大图按固定步长切成小块每个patch内保留原始分辨率同时把XML里对应patch的坐标平移。patch_w, patch_h 640, 640 step 320 # 重叠一半避免裂缝恰好落在切缝上 img cv2.imread(str(img_path)) for ty in range(0, img.shape[0] - patch_h 1, step): for tx in range(0, img.shape[1] - patch_w 1, step): patch img[ty:ty patch_h, tx:tx patch_w] # 对该patch内的bbox做偏移: x - tx, y - ty # 保留与patch有交集的框去掉完全在patch外的框参数说明640是训练常用的输入分辨率320步长产生50%重叠这样一条横跨两个patch的裂缝在相邻patch里都至少保留一半可见区域不会因为目标只有一小段在patch内而丢标签。滑窗会让同一张路面图产生几十个patch有效缓解了2684张原始图片数量不足的问题。注意切patch后要做一次边界裁剪因为缩放到640时落在patch边缘的框会超出patch范围。切完的patch建议单独存成目录并把对应的XML也切出来再走一遍第3章的解析流程。5. 把JPEGImages和Annotations一键转成YOLO txt并做一致性自检5.1 三步完成VOC到YOLO txt的转换YOLO系框架的标签不是XML而是每个图片对应一个txt每行格式为class_id x_center y_center width height四个坐标值都归一化到0到1之间。转换代码可以直接复用第3章的parse_vocdef voc2yolo(xml_path: Path, class_map: dict, out_dir: Path) - None: anno parse_voc(xml_path) lines [] for obj in anno[objects]: if obj[difficult]: continue # 多数YOLO训练默认不加载难例 if obj[name] not in class_map: continue xmin, ymin, xmax, ymax obj[bbox] box_w (xmax - xmin) / anno[width] box_h (ymax - ymin) / anno[height] x_center (xmin xmax) / 2 / anno[width] y_center (ymin ymax) / 2 / anno[height] lines.append(f{class_map[obj[name]]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) out_dir.mkdir(parentsTrue, exist_okTrue) out_dir.joinpath(xml_path.stem .txt).write_text(\n.join(lines))参数说明class_map是类别名到整数id的映射比如{crack: 0, pothole: 1, lane_line: 2}id从0开始是YOLO系框架的硬性要求从1开始会让首个类别空置。difficult框先跳过因为绝大多数训练配置不加载难例标签但这会带来一个问题如果某个patch里只有difficult框转换后txt文件是空的空标签文件在部分框架里会触发警告需要保留为0字节文件还是整个移除取决于你的训练配置。坐标保留6位小数对像素级框来说精度足够再多有效位只会增加文件体积。5.2 转完格式先做一致性校验转换完不要急着开训先验证txt行数与XML的object数对得上这一步能拦住坐标换算做错导致的静默丢框。for txt_path in sorted(out_dir.glob(*.txt)): xml_path xml_dir / (txt_path.stem .xml) anno parse_voc(xml_path) expect sum(1 for obj in anno[objects] if int(obj[difficult]) 0) actual len(txt_path.read_text().splitlines()) if actual ! expect: print(f不一致: {txt_path.stem} 期望{expect}行 实际{actual}行)这个检查会在每个txt上做一次XML解析2684个文件几秒内跑完。如果出现大量不一致优先检查parse_voc里是否对difficult做了过滤其次检查bbox坐标是否在归一化前除以了正确的width和height。通过校验后再把txt目录按train和val清单拆开连同JPEGImages一起交给训练脚本这份路面信息数据集才算真正进入了可用状态。本文还有配套的精品资源点击获取
返回列表