ARTICLE DETAIL

资讯详情

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

道路坑洞目标检测实战:VOC与YOLO格式转换、数据校验及YOLOv8训练部署

道路坑洞目标检测实战:VOC与YOLO格式转换、数据校验及YOLOv8训练部署 简介面向道路病害检测与计算机视觉目标检测模型训练提供一套道路坑洞pothole标注数据集同时给出Pascal VOC与YOLO两种主流标注格式可直接接入YOLOv5、YOLOv8、Faster R-CNN等常见训练框架减少数据格式转换成本。压缩包共2000个文件类型以txt标注、xml标注及jpg图像样本为主整体大小约23.87MB标注文件与图片同名对应方便按需划分训练集、验证集与测试集。数据集包含665张道路场景图像标注框总数达1740个覆盖单一类别pothole由labelImg工具画矩形框完成标注信息准确规范。该数据集既适合目标检测入门者练习完整训练与评估流程也可作为道路坑洞识别、路面健康监测等课题的补充数据来源。目前已有400余人浏览学习整体内容紧凑、格式统一下载后即可开展实验。1. 道路坑洞目标检测数据集为什么值得自己先过一遍标注格式道路坑洞目标检测看起来场景单一但真正动手做时光线变化、沥青裂缝、阴影积水都会让标注边界很难定。这次拿到的VOCYOLO双格式数据集整理得相当干净665张jpg对应665个xml和665个txt唯一类别pothole标注框总数1740平均下来每张图约2.6个框。它没有提供任何训练好的模型权重也没有做train/val划分所以更适合用来练手完整的目标检测流程而不是直接拿来出结果。对于做巡检系统的工程师、刚接触YOLO目标检测的读者或者想研究Pascal VOC和YOLO标签差异的人这份数据都能当作可靠的起点。接下来我会从标签字段、统计校验、训练参数和部署过滤几个层面拆开讲。2. VOC与YOLO两种标注格式的结构差异与互相转换目标检测训练框架对标签要求不是完全一样。Pascal VOC用xml保存绝对像素坐标YOLO保存的是归一化中心坐标与宽高。两者记录的是同一个矩形只是坐标系和精度习惯不同。这个数据集同时保留两种格式正好可以用一个文件去校验另一个文件也可以让你在切换框架时不至于重新画框。2.1 XML与TXT字段怎么对应VOC格式的每个xml文件里外层是annotation内部有folder、filename、size、object等节点。其中size下必须包含width、height、depthobject下必须有name和bndbox而bndbox里的xmin、ymin、xmax、ymax就是矩形框在原始图像中的绝对像素坐标。对于坑洞这种单类别任务name基本都是pothole但格式本身并不限制类别数量。YOLO的txt格式则完全不同的每一行代表一个目标格式是class_id x_center y_center width height后四个值全部除以图像宽高做归一化所以取值范围通常在0到1之间。class_id从0开始编号这个数据集只有pothole一个类别所以txt里的第一列永远是0。为了直观对比把常见字段列成下表信息VOC XMLYOLO txt说明类别名namepothole/name0YOLO只认类别id需要names映射图像宽width640/width不存转YOLO前必须知道原图宽高边界框xmin100/xmin等x_center y_center w h单位分别是像素和归一化比例多目标有多个object每行一个目标同一图片不同框逐行写入上表里用宽度640做例子是因为很多公开数据集默认到这个尺寸你解压后看到的数值要以实际xml为准。转换公式是x_center (xmin xmax) / 2 / widthy_center同理用height计算w (xmax - xmin) / widthh (ymax - ymin) / height。这里有个常见误区不要把xmin、xmax直接当作中心点必须先求中心再除以宽高。2.2 用Python脚本批量转换并控制坐标精度虽然这个数据集已经同时给出了xml和txt但在实际项目里你仍然经常需要自己转换。例如从labelImg标注完只得到xml或者需要把矩形框旋转框转换到OBB检测框架再到后来把yolo格式转回VOC给MMDetection用。我一般会保留一个转换脚本而不是依赖在线工具。下面这段代码可以逐个读取xml并输出txtimport xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w float(size.find(width).text) h float(size.find(height).text) stem os.path.splitext(os.path.basename(xml_path))[0] out_lines [] for obj in root.iter(object): cls_name obj.find(name).text # 如果只保留pothole默认类别id为0 if cls_name ! pothole: continue 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.0 / w cy (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h out_lines.append(f0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_path os.path.join(out_dir, stem .txt) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(out_lines))这段代码先把w和h转成float避免Python在整数除法时丢失小数点。之后遍历object节点读取bndbox里的四个坐标值再按前面提到的公式归一化累积到out_lines最后写文件。保留6位小数是YOLO训练里的常见做法精度已经远超图像像素误差。cls_name ! pothole这一行是保险如果xml里混入其他类别你不会把错误类别写成0而是直接跳过。如果未来加到多类别你需要先定义类别到id的映射字典例如{pothole: 0, crack: 1}。使用时可以按目录批量执行mkdir labels python -c import os; from voc_to_yolo import voc_to_yolo; [voc_to_yolo(os.path.join(annotations, f), labels) for f in os.listdir(annotations) if f.endswith(.xml)]这个命令适合目录不大、临时验证的场景。项目文件较多时我还是建议把脚本写成argparse版本支持--xml_dir、--out_dir参数否则到了下一个数据集还要改路径。2.3 用txt反向还原XML核对一致性转换完成之后不能只看生成了几行txt就认为没问题。我见过不少标签错位就是因为源xml里有个框的xmax写成了小于xmin的数值或者图像宽高读取错误导致归一化坐标超界。下面这段反向还原函数可以直接把yolo格式的某一行还原成像素坐标def yolo_to_voc_line(line, w, h): cid, xc, yc, bw, bh map(float, line.split()) xmin (xc - bw / 2) * w ymin (yc - bh / 2) * h xmax (xc bw / 2) * w ymax (yc bh / 2) * h return int(cid), xmin, ymin, xmax, ymax逻辑并不复杂先读入类别id和归一化中心坐标、宽高然后用中心点减去一半宽高得到左上角加上一半得到右下角。校验时拿这个结果和xml里的bndbox做差值如果差值绝对值在2像素以内通常认为转换一致。数据集的txt和xml是同一个标注工具生成的一般不会差太多但如果你用在线工具转换过这一步仍然值得做。需要提醒的是YOLO txt本身不保存图像尺寸所以反向还原时必须传入和原图一致的w、h。如果原始包是平铺结构建议在转换脚本里同时读取同名jpg的宽高而不是相信固定尺寸。这里项目描述里没有给出图像分辨率我建议你解压后先用Python的PIL.Image读一遍尺寸把异常宽高记录下来再做后续训练。因为不同来源的监控相机可能输出不同分辨率混在一起会带来隐性问题。3. 数据集标注统计和边界框质量分析方法许多目标检测项目失败不是模型结构不对而是标注数据在输入训练脚本之前就已经有残缺。拿到这个数据集后第一步不是着急跑训练而是先复算描述里的关键数字图片665、xml 665、txt 665、pothole框1740。这些数字对得上说明文件完整性没问题后续训练才能安心。3.1 统计每个类别的框数和每图框数用xml遍历是统计最直接的方式因为xml里既有filename又有完整的object列表。写一个几十行的脚本就能把类别框数、平均每图框数都打出来import glob import xml.etree.ElementTree as ET total_xml 0 total_boxes 0 class_box {} per_image_box [] for xml_path in glob.glob(annotations/*.xml): total_xml 1 root ET.parse(xml_path).getroot() objs root.findall(object) total_boxes len(objs) per_image_box.append(len(objs)) for obj in objs: name obj.find(name).text class_box[name] class_box.get(name, 0) 1 print(fxml files: {total_xml}) print(ftotal boxes: {total_boxes}) print(fclass boxes: {class_box}) print(favg boxes/image: {sum(per_image_box) / len(per_image_box):.2f})这段脚本用glob收集所有xmlroot.findall(object)可以直接拿到一张图里所有标注目标不需要遍历整棵树的子节点速度上也比iter更快。跑完应该看到xml files: 665、total boxes: 1740、class boxes: {pothole: 1740}avg boxes/image约2.62。这说明平均每张图里有2到3个坑洞不少图是多个框重叠或贴近排列的这对NMS和后处理会有一定压力。如果统计出的框数不是1740先从标注工具导出设置找原因比如labelImg的自动保存是否覆盖了旧文件。类别统计里只出现pothole是符合预期的。如果某个xml出现第二种类别你就要看是不是画错或者混入了其他数据集。类别的纯净度直接影响后续names配置yaml里写nc: 1没问题但如果混入第二种整个训练验证标准就要重做。3.2 检查边界坐标是否越界和框面积是否合理VOC转YOLO时最常见的错误是xml里的xmax大于图像宽度或者ymax大于图像高度。原因可能是标注时拖出了画布也可能是图像被resize但标签没同步更新。越界标签转出来的YOLO坐标会大于1训练时虽然不会报错但会误导损失函数。下面这个函数专门检查单个xml是否存在越界或非法框def check_annotation(root): size root.find(size) w int(size.find(width).text) h int(size.find(height).text) problems [] for idx, obj in enumerate(root.findall(object)): box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) if xmin 0 or ymin 0 or xmax w or ymax h: problems.append((idx, out_of_bound)) if xmax xmin or ymax ymin: problems.append((idx, invalid_area)) if (xmax - xmin) * (ymax - ymin) 16: problems.append((idx, too_small)) return problems对每个object先检查坐标是否落在图像范围内再判断宽高是否反转最后看框面积。16像素这个阈值只是示例对普通监控画面来说小于16像素的坑洞基本无法通过检测器稳定识别就算标出来也大概率是噪声。如果图像分辨率很高比如1920x1080以上可以把阈值提高到50或100避免把小墨点当作坑洞。运行后把problems统一写进日志文件不要只print否则665张图会刷屏。我一般还会统计一张图里面积最大的框和最小的框的比例。道路坑洞在远景时会很小近景时很大如果同一张图里出现面积相差两个数量级的框训练时YOLO的尺度适应会更有挑战。这时可通过imgsz1280提升小目标的分辨率但代价是显存占用显著上升。3.3 常见标签质量问题和排查顺序整理成一张表方便新手对照现象判断方法对训练的影响xml和txt框位置不一致用2.3的反向还原对比边界框偏移loss曲线震荡txt行数多于xml对象数逐行统计每个txt行数空标签或重复目标mAP失真小目标框过多计算框面积与图像面积比mAP50-95偏低需要更高输入分辨率个别图没有标注对比jpg文件名和xml文件名图片被跳过评估集数据量减少文件后缀大小写不一致检查.JPG和.jpg文件路径匹配失败训练报错这套数据里文件名是firc_pothole_356这种风格既有jpg也有txt和xml命名上相对统一。但要注意不要随手改成中文名YOLO按文件名匹配图片和标签中文或空格容易导致路径解析问题。上一章提到的反向还原脚本在这里就能派上用场把所有txt还原成矩形框和xml对比数值差超过2像素的文件单独列出来。处理完之后再进入训练才能保证后面所有实验都可以复现。4. 用YOLOv8训练坑洞检测模型时的数据划分与训练参数665张图片属于小型数据集直接拿全部数据训练容易过拟合盲目增加数据增强也可能掩盖标签问题。这里推荐用YOLOv8作为讲解对象因为它把标注格式、数据划分和训练命令统一做得比较顺环境安装也比旧版v5简单。数据集的xml在这里可以当作备份真正训练时用的是txt和对应的jpg。4.1 数据目录组织与yaml配置YOLO系列通常要求目录结构是images/train、images/val、labels/train、labels/val。原始zip如果平铺在同一个目录需要先建结构再移动文件。下面这段脚本把图片和txt按比例分到训练集和验证集import random from pathlib import Path root Path(pothole_dataset) for split in [train, val]: (root / images / split).mkdir(parentsTrue, exist_okTrue) (root / labels / split).mkdir(parentsTrue, exist_okTrue) imgs sorted((root / images).glob(*.jpg)) random.seed(42) random.shuffle(imgs) val_num int(len(imgs) * 0.2) for i, img in enumerate(imgs): split val if i val_num else train label root / labels / (img.stem .txt) img.rename(root / images / split / img.name) if label.exists(): label.rename(root / labels / split / label.name)脚本先用Path.mkdir(parentsTrue, exist_okTrue)创建四层目录再按20%比例把图片放入val。random.seed(42)是为了固定随机划分换别的随机数会导致训练集不同实验对比就不公平。移动时用rename而不是复制因为665张图复制一份更快但如果你还要保留xml建议先把整个zip解压后的目录做一次备份再进行划分。如果原始目录里images下没有图片而是在annotations和labels之外单独放着图片先把它们统一归入pothole_dataset/images再执行这段脚本。划分之后写pothole.yamlpath: pothole_dataset train: images/train val: images/val nc: 1 names: [pothole]path是相对当前命令运行目录的根路径也可以写绝对路径。train和val相对于path填写不要写成/images/train。nc: 1因为只有pothole一个类别names里的顺序就是txt类别id的映射所以第一个元素必须是pothole。很多新手在names里加一个background这在YOLOv8里是错误的背景不需要占类别位。4.2 训练超参数选择与显存控制小数据集训练关键是控制模型容量和训练时长。我建议先用yolov8n起步而不是直接yolov8x因为665张图不足以让大模型充分收敛。预训练权重能提供通用特征坑洞在纹理上和普通路面有较强区分性n或s的容量已经够用。命令如下yolo detect train datapothole.yaml modelyolov8n.pt epochs100 imgsz640 batch16 patience20 cacheFalseepochs100对这类数量级的数据是足够的如果早停触发早也可以节省时间。patience20表示连续20轮验证集指标不上升就停止这是小数据集防过拟合的重要手段。batch16是默认值如果GPU显存不到8GB建议降到8或4因为坑洞检测的输入尺寸如果保持640显存占用会比理论数值高。cacheFalse表示不把图片一次性载入内存虽然会慢一些但避免内存不足导致进程被杀。如果原图分辨率明显高于640比如很多道路巡检相机的输出是1920x1080可以先试imgsz960。坑洞在图像里通常只占几十到几百像素放大输入能保留更多细节但训练时间和显存都会上升。我一般先跑一次640确认流程没问题后再试1280而不是一开始就追求最高分辨率。注意yolov8默认开启马赛克增强对小数据集是多阶段训练中比较关键的一项不要为了“看得清”把它关掉。4.3 单类目标的训练输出怎么看训练结束后重点看results.png、weights/best.pt和last.pt。results.png里有train/box_loss、val/box_loss、metrics/mAP50、metrics/mAP50-95。由于是单类别mAP50就是pothole类别本身的检测精度不需要再做平均。坑洞目标小且外形接近椭圆mAP50-95通常低于mAP50很多如果两者差距过大说明模型对边框精度的拟合还不够后期可以微调损失函数的box IoU权重或者提高输入分辨率。训练loss曲线如果还在下降说明epoch不够可以继续用last.pt训练训练loss下降但验证loss上升就是过拟合信号此时可以增加验证集比例或把patience调小。5. 部署时对坑洞检测结果做置信度和尺度过滤的实用技巧模型训练完成之后直接拿默认参数推理往往不够稳。道路环境中井盖、排水格栅、沥青裂缝和阴影都很容易被误判成坑洞所以部署时要做两层过滤。5.1 用conf和iou控制坑洞误检推理命令里最值得调的就是conf和iou。下面这段命令适合检测单张图片yolo detect predict modelruns/detect/train/weights/best.pt sourceroad.jpg conf0.30 iou0.45 imgsz640conf0.30表示只保留置信度高于30%的框能去掉大量低置信度误检。如果发现坑洞漏检把conf调到0.15但你要接受更多阴影误报。iou0.45是NMS的合并阈值道路视频里几个坑洞挨得很近太高会把相邻框合并掉太低又会产生同一个目标多个框0.45是一个比较折中的起点。如果最终要部署在Jetson等边缘设备imgsz可以降到480或320但小坑洞召回率会明显下降需要拿验证集做一次最佳conf和imgsz组合搜索。5.2 根据坑洞宽高比做后处理透视关系下坑洞通常不会出现特别夸张的宽高比。有些裂缝会被模型标成一个高瘦矩形实际坑洞更多是横向或方形。在onnx或tensorrt的推理后处理里加一个简单的宽高比过滤def filter_aspect(boxes, scores, min_ratio0.4, max_ratio2.5): result_box [] result_score [] for box, score in zip(boxes, scores): x1, y1, x2, y2 box w x2 - x1 h y2 - y1 ratio w / h if h 0 else 999 if min_ratio ratio max_ratio: result_box.append(box) result_score.append(score) return result_box, result_score这段函数先计算w/h再判断是否落在0.4到2.5之间。数值范围需要根据你部署摄像头的安装角度调整可以用一批验证集ground truth统计宽高比分布再设上下分位数。过滤掉的长条框大部分是裂缝和栏杆阴影对坑洞检测的误报抑制非常有效。把这段函数放在NMS之后、输出结果之前每条框只多几行Python计算不会影响帧率。后续接入巡检流程时我一般还会把检测结果对齐到图像GPS位置用连续几帧的确认来降低单帧误报。本文还有配套的精品资源点击获取
返回列表