下水道缺陷目标检测:VOC转YOLO格式与YOLOv8训练增强实战

发布时间:2026/9/13 14:58:46
下水道缺陷目标检测:VOC转YOLO格式与YOLOv8训练增强实战 简介面向目标检测任务的下水管道缺陷数据集适合市政管网缺陷识别、AI巡检模型训练等场景。压缩包内图片清晰含1717张jpg原图并提供VOC格式xml与YOLO格式txt两套标注共7类缺陷穿入、错口、堆积、垃圾、裂缝、泥土、树根标签框总数3401个可直接用于YOLO、Faster R-CNN等模型训练和评估。资源包共2000个文件以xml标注和txt标签为主另含数据集说明与类别配置文件整体61.64MB目录按JPEGImages、Annotations、labels分文件夹存放结构清晰易用。已有154人学习适合需要工业视觉训练数据的工程师下载省去自行采集与标注的时间快速开展算法验证与模型调优。1. 1717张下水道缺陷图够不够训练一个7类检测模型在市政管网养护的场景里CCTV机器人下水拍回来的视频连续看两个小时眼睛就分不清裂纹和接缝了这就是“下水道缺陷检测”要做的事情。这份“目标检测-7类下水管道缺陷检测数据集1717张YOLOVOC格式.zip”名字虽然长本质上就是给你一包已经标好框的数据让你直接越过数据采集和标注环节进入检测模型训练。1717张对YOLO来说不算大但下水道场景高度固定缺陷类型就那么几种配上预训练权重做微调完全够喂出一个能落地的模型。这个资源包最常见的使用方式是先以VOC格式的xml看原始标注再转成YOLO格式去做训练整个过程会让你把“目标检测为什么需要两种标注格式”“YOLO坐标为什么是归一化的”“训练集和验证集为什么不能有图像重叠”这几件事一次性全踩明白。无论你是做智慧水务的算法工程师还是用目标检测做毕设的学生这篇文章都能按数据转换、训练、评估、增强这条线给你一套能直接复现的做法。2. 先看懂VOC和YOLO两套标注再做转换2.1 为什么一个数据集要塞进两种格式VOC格式源自Pascal VOC竞赛每一张图对应一个同名xml文件标注信息用标签树存XML里object节点下面记录了类名name以及bndbox的xmin, ymin, xmax, ymax四个绝对像素坐标。这种格式和人的阅读习惯一致打开XML就能看到“这一框是CLIMA像素范围358, 622, 401, 654”方便调试与再标注。YOLO格式则是每个txt文件对应一张图每一行表示一个目标五个数字依次是class_id, x_center_norm, y_center_norm, width_norm, height_norm其中中心坐标和宽高都是除以图像宽高之后的0到1之间的小数。YOLO格式牺牲了可读性换来了极高的训练样本读取效率——反归一化、坐标偏移都在训练代码内部完成不需要像R-CNN系列那样每次从XML里解析box这使得数据装载更简单也成了YOLO系列训练和推理的事实标准。所以这个zip同时给出两种格式意思是让你自己选入口。你当然可以直接读VOC做检测但市面几乎所有YOLO版本的中文教程默认都要求你准备images/和labels/两个目录里面分别是jpg和txt。常见做法是保留VOC为原始标注把YOLO当作中间产物这样即使YOLO格式转换出错也能回到XML重新校验。格式文件后缀坐标含义适合做什么VOC.xml绝对像素坐标风格为左上角右下角人工复核、可视化调试、数据编辑YOLO.txt归一化中心坐标和宽高0到1之间喂给YOLOv5/v8/v11训练加载快2.2 写一个最小转换脚本处理坐标归一化我在处理这类数据集时不会去用LabelImg自带的转换功能因为批量转换、重命名、剔除空白xml都需要自己控制。给你一个可以直接跑的Python脚本注意代码里对xml的width和height做了一次float转换分母为零就直接跳过避免某些标注软件写出高度0的空目标。import xml.etree.ElementTree as ET import os from pathlib import Path def convert_voc2yolo(xml_file, class_names, out_txt): tree ET.parse(xml_file) root tree.getroot() img_w float(root.findtext(.//width)) img_h float(root.findtext(.//height)) if img_w 0 or img_h 0: print(f跳过 {xml_file}图像宽高为0) return 0 lines [] for obj in root.findall(.//object): name obj.findtext(name) if name not in class_names: continue cls_id class_names.index(name) box obj.find(bndbox) x_min, y_min float(box.findtext(xmin)), float(box.findtext(ymin)) x_max, y_max float(box.findtext(xmax)), float(box.findtext(ymax)) # 限制在图像内防止越界生成负值 x_min max(0, min(x_min, img_w)) x_max max(0, min(x_max, img_w)) y_min max(0, min(y_min, img_h)) y_max max(0, min(y_max, img_h)) x_center ((x_min x_max) / 2) / img_w y_center ((y_min y_max) / 2) / img_h width (x_max - x_min) / img_w height (y_max - y_min) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) if lines: with open(out_txt, w, encodingutf-8) as f: f.write(\n.join(lines)) return len(lines) class_names [CLIMA, DEBRIS, ROOT, CORROSION, SAND, JOINT, HOLE] convert_voc2yolo(Annotations/defect_0001.xml, class_names, labels/defect_0001.txt)这段脚本的意义不只是生成txt它把三件容易出错的事一次做完一是检查xml里是否声明了图像宽高如果xml是标注工具默认保存的width和height往往就是真值二是坐标求中心后做了6f浮点截断这会让YOLO的geometry精度控制在约等于千分之一像素正常够用三是对越界框做了clip不然训练时某些框宽高为负数会导致loss直接计算出NaN。换成你自己的7类数据集只需要改class_names这一行列表保持顺序和之后要写的data.yaml一致。2.3 转换后的3项校验别直接开训练转换代码跑完不代表标注就是对的。我一般会先做三个检查发现异常马上回到上一节改脚本而不是等到loss乱跳再去排查模型。第一项校验是类别号连续性。打开任意一个生成的txt看第一列数字是否在0 ~ 6之间。如果xml里出现你没在class_names里定义的类名换算出来的id会越界YOLO训练时会把越界id当作背景忽略等于这一批目标直接蒸发了。可以跑一遍统计命令awk {print $1} labels/*.txt | sort | uniq -c正常输出应该是0到6每个数字都有出现。如果有某个类别只有个位数后面训练时会明显表现差此时要考虑做类别重采样而不是改损失权重。第二项校验是宽高两边保持合理比例。观察第四、五列正常缺陷占比不会太小但也不会超过整图。如果发现大量width大于0.9说明标注人员画框时把整个管道横截面都圈进去了这种框对YOLO学习缺陷边界没有帮助。你要么返工数据要么在损失函数里把红光谱关注小框的权重调高。第三项校验是图像与txt文件名的对应关系。拿jpg文件列表直接去比txt文件列表建议在linux下用一句命令看差异for f in images/*.jpg; do n$(basename $f .jpg); [ ! -f labels/$n.txt ] echo 缺标注: $f; done这一步很重要因为很多zip数据集在打包时会因为中文名或特殊字符把.txt改名训练时不报错但某个类别永远学不出来让你误以为是模型问题。提示如果xml里的坐标已经超出图像边界不要直接用clip修复先检查原始标注是否有错位clip只能解决数值越界解决不了语义错位。3. 用YOLOv8训练7类管道缺陷按这套参数直接跑3.1 数据划分固定随机种子避免同源图像进入不同集合下水道CCTV采集视频有一个特点连续几帧背景几乎相同如果随机拆分同一段管道的两帧会同时出现在train和val里验证集mAP虚高实际部署时效果断崖下跌。常见做法是按视频片段切分但这份数据集只给了图片没有给出视频组信息退而求其次可以在随机划分时使用固定seed并且做一次查重。我这里更推荐先按哈希去重再做8:1:1划分。import os import random import hashlib from collections import deque def img_hash(path): with open(path, rb) as f: return hashlib.md5(f.read()).hexdigest()[:8] # 建立像素层面去重 seen set() unique_files [] for f in sorted(os.listdir(images)): h img_hash(fimages/{f}) if h not in seen: seen.add(h) unique_files.append(f) random.Random(42).shuffle(unique_files) n len(unique_files) train unique_files[:int(0.8*n)] val unique_files[int(0.8*n):int(0.9*n)] test unique_files[int(0.9*n):]去重这一步对水下机器人画面很有用管道缺陷视频里往往几帧完全一样不做去重会让验证集和训练集出现重复样本模型记住而不是学到规律。random.Random(42)固定了种子无论别人怎么复现划分出来的文件集合都一样这对提交实验报告和横向对比模型很关键。最后把train和val里的jpg文件名分别写入train.txt和val.txtYOLOv8直接读这个文件列表。3.2 写对7个类别的data.yaml类名顺序必须一致新建defect.yaml注意names列表里0号对应第二章里class_names的第0个不然所有框的语义都会错位。path: ./cwd_doot # 数据集的根路径 train: train.txt val: val.txt test: test.txt names: 0: CLIMA 1: DEBRIS 2: ROOT 3: CORROSION 4: SAND 5: JOINT 6: HOLE这里有个容易踩的坑如果第二章的脚本里class_names顺序是[CLIMA, DEBRIS]而yaml里写1: DEBRIS, 0: CLIMA实际不会显式报错但是CLIMA和DEBRIS会互换。你验证时看precision发现“腐蚀”总是变成“碎石”第一反应是模型不行其实是标注id映射错了。所以这两处列表的顺序必须完全一致没有任何其他商量余地。3.3 训练命令与缺陷场景的超参数调整如果你是第一次拿这份数据练手直接用YOLOv8s做预训练权重显存压力小且对小目标缺陷比n版本好。管道裂缝往往只有40×20像素s模型的浅层特征表达能力够用。yolo detect train \ modelyolov8s.pt \ datadefect.yaml \ imgsz800 \ batch16 \ epochs100 \ optimizerAdamW \ lr00.005 \ mosaic1.0 \ close_mosaic10 \ projectruns/train \ namepipe_defect跑起来之前先对照下面这张表确认你改对了参数值对这个数据集的影响imgsz640~800640训练速度快但裂纹目标太小容易丢800能提高小目标召回显存够就开800batch8/16/32显存受限时降到8但batch小会让mAP抖动变明显。这里16是单卡平衡点mosaic1.0前期开启可以增加多样性close_mosaic10在最后10轮关闭mosaic让模型适应真实尺寸分布lr00.005从预训练模型来的lr不用太大0.01会出现loss震荡0.001收敛太慢optimizerAdamW这类规模的数据集AdamW比SGD收敛更稳不容易在缺陷这类强不平衡场景卡住YOLO的损失函数由三个部分加权组成box损失负责回归框位置cls损失负责分类dfl损失负责框分布。这份数据集里“锈蚀”和“污物”样本量差异可能大到好几倍但我不建议调这三个权重常见做法是先观察各类ap差异如果少数类ap特别低用离线增强重采样比改loss权重更可控。训练完看曲线时在runs/train/pipe_defect/results.csv里关注val_box_loss是否持续下降如果到了80轮还在下降就不要因为想早停而过早截断下水道缺陷边界模糊loss收敛本身会比普通物体慢不少。4. 评估模型时别被mAP骗了下水道缺陷的真实场景4.1 用验证集导出混淆矩阵和PR曲线训练完以后用下面命令在测试集上跑一次评估yolo detect val modelruns/train/pipe_defect/weights/best.pt datadefect.yaml imgsz800这条命令会生成两个比较关键的产物confusion_matrix.png和F1_curve.png都在runs/detect/val目录。对7类缺陷来说你至少要在混淆矩阵里确认两件事有没有哪个类被大面积错认成背景以及有没有两个可分辨性差的类始终互相混淆。比如SAND和DEBRIS在管线CCTV里视觉上都是散落颗粒混淆矩阵会把它们的成对误判表现得明明白白。如果你希望看到模型对某张图的预测框和置信度可以直接跑yolo detect predict modelruns/train/pipe_defect/weights/best.pt sourcetest/defect_0102.jpg conf0.25这里conf参数值得单独说训练时和预测时使用的置信度阈值完全是两码事。训练时的loss计算会把很大的负样本预测概率当背景损失而推理时只显示conf大于阈值的目标。对下水道这种低对比度场景0.25往往已经漏掉大量细裂缝。4.2 从小目标缺陷看recall和precision的取舍下水道缺陷最典型的难点是小目标。裂纹宽度经常只有2~3个像素训练时mosaic把这些小目标缩得更小导致模型对小目标的特征学习不充分。评估时看mAP50-95相比mAP50掉得厉害几乎可以断定是框定位不精确而不是类别判别出问题。针对这种情况最直接的误区是调低conf-thres而正确解法是调整imgsz。我们来看一个原则由于YOLO输入图像长度必须可以被stride整除推理时若设imgsz1280模型对这个数据集里小裂缝的感知会明显加强但显存开销会变成之前的2.5倍。如果你用V100或4090可以尝试用1280推理但保持训练640这种方式通常能让小目标recall提高8个点。配合以下方法验证recall是否真的提升yolo detect val modelruns/train/pipe_defect/weights/best.pt datadefect.yaml imgsz1280 conf0.05conf0.05配合F1曲线能让你看到在当前模型能力下最大能召回多少目标。如果conf0.05时mAP50比conf0.25时高很多说明确实有真值框被低置信度压制此时不该再调阈值而是要提高分辨率或做数据增强。4.3 检查VOC原始标注里的漏标用一致性脚本找出脏数据很多从旧设备导出的数据集VOC标注是当年人肉标的存在大量漏标特别是不规则的裂纹只画了中间一截。这种标注噪声很难靠视觉检查但可以通过一份简单的一致性脚本来发现统计txt里的目标数量和对应xml里的object节点数量数量对不上的文件就是疑似脏数据。import glob import xml.etree.ElementTree as ET for txt in glob.glob(labels/*.txt): xml Annotations/ txt.split(/)[-1].replace(.txt, .xml) try: tree ET.parse(xml) obj_num len(tree.findall(.//object)) except Exception: obj_num 0 with open(txt) as f: txt_num len(f.readlines()) if obj_num ! txt_num: print(f{txt} 不一致: xml{obj_num}, txt{txt_num})有没有发现这个脚本不检查内容而只检查数量这是因为在瑕疵标注场景里漏标往往发生在整段连续视频里某一帧从头到尾没标对应的txt可能是空文件数量对比会非常醒目。这一招并不能排除一切标注错误但至少能找出格式层面的脏文件你可以在2.3节的转换校验之后立刻执行。把这些脏图片从训练集移除比让模型强行拟合错误标注要划算得多。5. 用切图增强和类别重平衡把1717张再榨出三倍效果到了这个阶段模型已经能跑通剩下主要工作是从有限数据里挖更多价值。417张测试集不动训练集里1717张里约1373张图对7类别的完整检测不太够但可以用离线增强把有效训练样本放大。我在这个数据集上通常用的第一招是切图。下水道单帧图分辨率一般在1180×800左右裂缝只占画面很小面积与其让YOLO在长边缩放下丢失细节不如把训练图切成四块重叠patch同时保住原始坐标。切图后原来漏检的小目标就变成了相对大目标模型更容易学到纹理特征。import cv2 import os def crop_with_labels(img_path, txt_path, out_dir, patch_size640): img cv2.imread(img_path) h, w img.shape[:2] # 按步长切出带重叠的patch for row in range(0, h - patch_size 1, patch_size // 2): for col in range(0, w - patch_size 1, patch_size // 2): patch img[row:rowpatch_size, col:colpatch_size] # 将YOLO格式坐标映射到patch with open(txt_path) as f: boxes [list(map(float, l.split())) for l in f if l.strip()] new_lines [] for cls, cx, cy, bw, bh in boxes: x1 (cx - bw / 2) * w y1 (cy - bh / 2) * h x2 (cx bw / 2) * w y2 (cy bh / 2) * h if x2 col or x1 col patch_size or y2 row or y1 row patch_size: continue nx1 max(col, x1) - col ny1 max(row, y1) - row nx2 min(col patch_size, x2) - col ny2 min(row patch_size, y2) - row new_lines.append(f{int(cls)} {((nx1nx2)/2/patch_size):.6f} {((ny1ny2)/2/patch_size):.6f} {((nx2-nx1)/patch_size):.6f} {((ny2-ny1)/patch_size):.6f})放大后的patch还带有一个隐性好处可以在训练时配合更大的imgsz因为patch本身已经变小了不需要牺牲太多的FLOPs。运行结束后你需要再次执行第二章输出txt的校验步骤因为切图会让某些框变成完全无关的残片残片位于patch边缘的边框目标往往缺少上下文可以考虑把占据patch面积0.3%以下的框直接过滤掉这比留着让模型混乱更好。最后把重平衡落到“复制粘贴”这个思想上来对样本量只有几十张的类别可以在每张原图上将某个缺陷区域在原图内复制一份平移到附近背景区域同时添加一个对应的新标注。下水道的背景相对单纯复制出来的缺陷和真实缺陷天然比较像比装个对抗生成网络生成假样本更稳妥。我用这句命令训练增强后的数据python crop_with_labels.py yolo detect train datadefect.yaml \ modelyolov8s.pt epochs80 imgsz640 batch16 mosaic0.5增强后的训练集达到三四千张左右模型对CLIMA这类少样本缺陷的AP提升明显而大家担心的过拟合没有出现。这就是把数据集从1717张榨干价值的关键离线切图保住上下文坐标重映射别写错重采样后重新校验格式然后再继续跑二次训练。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询