简介:面向YOLO系列算法应用的下水道缺陷检测数据集,对应2364张图像中的缺陷标注信息,适用于目标检测模型训练、验证与测试环节。主要缺陷类型包括关节偏移、障碍物、裂纹、带扣、洞、公用设施入侵与碎片,可帮助算法工程师快速启动市政管道缺陷自动识别项目,也适合教学科研场景下的目标检测实验。压缩包内共2000个文件,其中1636份VOC格式xml标签与364份YOLO格式txt标签,已预先按训练、验证需求划分,并附带data.yaml配置文件,可直接适配yolov5、yolov7、yolov8、yolov9、yolov10、yolo11等常见算法版本,省去类别定义和目录调整环节。整体包体仅7.43MB,轻量便捷,便于快速下载与迭代实验。目前已有66人学习使用,两类标签格式可满足不同训练框架的导入需求,无需自行整理数据;针对实际巡检场景,可快速检验各版本YOLO在下水道缺陷检测上的精度差异,辅助算法选型与优化。
1. 下水道缺陷数据集配上 YOLO 算法:2364 张带标签图为什么能直接拿来做检测
管网运维团队每年靠 CCTV 机器人钻进排水管拍出几百小时视频,真正能把缺陷筛出来的工程师却没几个。这套下水道缺陷数据集正好卡在这个节骨眼上:它将关节偏移、障碍物、裂纹、带扣、洞、公用设施入侵、碎片这七类常见缺陷整理成 2364 张带标签图像,并用 YOLO 格式的 txt 标注文件固化下来。也就是说,你不必再从零做起标注工作,拿到 zip 就能解压、核验、划分并直接喂给 YOLO 算法训练一个可用的缺陷检测模型。这篇笔记就按这个顺序写:先教会你看懂标签和类别分布,再完成数据集划分与 YOLOv8 训练命令配置,接着处理损失函数和增强参数,最后把几个最容易翻车的坑列给你看,并附上推理结果转维修报告的小技巧。
2. 先拆包核验标签:2364 张图的类别分布和标注格式长什么样
拿到 zip 第一件事不是解压后直接开训,而是先搞清三件事:图片是不是真的 2364 张、标签文件是不是对得上、七个类别的编号顺序是什么。顺序错了模型不会报错,但训练完预测出来的名字会错位,这种问题排查起来非常费时。
2.1 七个缺陷类别分别对应管道里的哪种病理
| class id | 标签名 | 管道现场对应物 | 检测难点 |
|---|---|---|---|
| 0 | 关节偏移 | 管节接口错位、脱节、橡胶圈外露 | 与背景纹理差异小,常靠接缝阴影判断 |
| 1 | 障碍物 | 管道内遗留的石块、沙袋、工具 | 尺寸跨度极大,近景能占半幅图 |
| 2 | 裂纹 | 管壁纵向或环向开裂 | 细长小目标,标注框容易带上大量背景 |
| 3 | 带扣 | 管壁屈曲、内衬褶皱、局部鼓包 | 边缘模糊,没有清晰矩形轮廓 |
| 4 | 洞 | 管壁穿孔、破损缺口 | 容易被渗水、泥土或阴影遮挡 |
| 5 | 公用设施入侵 | 第三方电缆、支管、拉索穿入管道 | 常与洞、裂纹同时出现在一个画面 |
| 6 | 碎片 | 碎石、砖块、垃圾、沉积物堆积 | 数量多、彼此粘连,小目标密度高 |
这七个类别大概率来自某次 CCTV 检测项目的自定义编码,不一定完全对应 CJJ 181-2012 或 PACP 缺陷编码体系。所以你要把它当成一个私有的类别集合来用,不要想当然地往标准缺陷代码上套。比如 class 3 的“带扣”,现场表现更像管壁屈曲或内衬褶皱,而不是字面上的金属扣件;class 5 的“公用设施入侵”,拍到的多半是电缆或支管穿过检测井的画面。训练前把每个类别的代表图扫一遍,知道它实际长什么样,后面调阈值和写报告时才不会指鹿为马。
2.2 看一眼标注文件:YOLO 标签格式与 class id 校验脚本
先解压,然后数一数图片和标签文件的数量。最常见的目录结构是 images 和 labels 两个平级目录,图片是 jpg,标签是同名 txt。
unzip -q "下水道缺陷数据集-2364张.zip" -d sewer_defect find sewer_defect -type f -name "*.jpg" | wc -l find sewer_defect -type f -name "*.txt" | wc -l find sewer_defect -type f | head -20如果图片数和 txt 数不一致,多数情况是某几张图没有对应缺陷,标注人员没生成空 txt,或者某几张图的 txt 文件名与 jpg 不一致。YOLO 训练时会自动跳过没有标签的 jpg,但如果你以为 2364 张都有框,实际只有 2000 张参与训练,mAP 会低于预期。所以这个数量校验非常值得做。
数量对得上后,写个小脚本检查标签内容。YOLO 格式每行是“class x_center y_center width height”,五个字段,坐标均为 0 到 1 的归一化值。这里我把 class id 合法性、字段数量、边界越界一起查了:
import os from collections import Counter label_dir = "sewer_defect/labels" cls_counter = Counter() bad_lines = [] for f in os.listdir(label_dir): if not f.endswith(".txt"): continue path = os.path.join(label_dir, f) with open(path, encoding="utf-8") as fp: lines = [ln.strip() for ln in fp if ln.strip()] if not lines: print("空标签文件:", f) for ln in lines: parts = ln.split() if len(parts) != 5: bad_lines.append((f, ln, "字段数不为5")) continue cls_id = int(parts[0]) try: x, y, w, h = [float(v) for v in parts[1:5]] except ValueError: bad_lines.append((f, ln, "坐标非数值")) continue if not (0 <= cls_id <= 6): bad_lines.append((f, ln, f"class id 越界: {cls_id}")) if not (0 <= x <= 1 and 0 <= y <= 1 and 0 < w <= 1 and 0 < h <= 1): bad_lines.append((f, ln, "归一化坐标越界")) else: cls_counter[cls_id] += 1 print("类别分布:", dict(sorted(cls_counter.items()))) print("异常行数:", len(bad_lines)) for item in bad_lines[:5]: print(item)脚本逻辑很简单,但有两处要注意。第一,class id 的上限要严格按照 nc=7 来设,也就是 0 到 6;如果数据集里写了个 7,说明标注过程出现过类别错乱,必须揪出来。第二,坐标必须是严格大于 0 且小于等于 1 的浮点数,如果出现负数或宽度为 0,训练时那个框会被 YOLO 直接忽略,既不会报错也不会进 loss,这种隐性脏数据只能靠脚本抓。空标签文件不一定要删,YOLO 会把它们当成纯背景图参与训练,对模型有时反而是好事。
2.3 类别分布不均是常态:训练前先做一次普查
跑完上面的脚本,你会得到一张类别计数表。如果 2364 张图里碎片占了 40% 以上,而公用设施入侵只有几十条,那就要在训练策略上做区分。YOLO 训练默认对所有类别一视同仁,但数据分布倾斜会让模型偏向多数类。最直接的应对不是改损失函数,而是先查清分布,再决定少数类要不要复制增强或降低多数类的采样权重。
我一般会把类别计数画成条形图,同时统计每张图的框数量。一张图里如果有二十个碎片框,训练时 loss 会被这张图主导;如果这种高密度图还集中在验证集,mAP 会被拉低很多。另一个要记录的是“单张图最大框数”和“框面积分布”。下水道缺陷里裂纹和洞都是小目标,框面积可能只有整张图的 0.5% 甚至更低,这类目标对标注质量极其敏感,稍微标偏一点,训练时 IoU 就达不到正样本阈值。分布普查阶段发现的这些小目标图,值得单独建一个子目录,后面调 imgsz 和增强参数时重点观察。
很多同学拿到带标签数据集就直接跑训练,遇到 mAP 低再回头找原因,这时数据集里的结构问题已经被模型“消化”了,你分不清是过拟合还是标注问题。所以这一步核验时间花得越细,后面训练阶段越省心。
3. 数据集划分与 YOLOv8 训练配置:目录结构、data.yaml 和一条命令跑通训练
YOLO 训练有一个非常容易迷惑新手的点:图片和标签必须分别在 train 和 val 两个目录下,而且目录结构是 images 与 labels 同级,不是把 train 和 val 分开放在顶级目录后让标签跟着图片走。很多人在这里栽跟头,导致 YOLO 报 “found no labels”。
3.1 数据集目录结构:images 和 labels 必须严格同级
这套 zip 如果解压出来是 images 和 labels 两个平级目录,那就按下面的方式建立训练集和验证集子目录:
cd sewer_defect mkdir -p images/train images/val labels/train labels/val然后按 8:1:1 或 7:2:1 的比例把图片和同名标签分进 train、val,这里我推荐 8:1:1,因为 2364 张图本身不算多,验证集留 10% 已经能评估出有效 mAP。测试集可以暂时不用建,先把验证集结果跑稳,后面从实际 CCTV 视频里截帧做更真实的测试。
划分时务必保证图片和标签文件成对移动,不要只移图片不移标签。下面这个 bash 循环是我常用的做法,它循环遍历验证集名单,并同步复制图片和标签:
# 假设 val_imgs.txt 里存放验证集图片名,每行一个不含扩展名 while read name; do mv "images/${name}.jpg" "images/val/" mv "labels/${name}.txt" "labels/val/" done < val_imgs.txt移动完成后检查一下 labels/val 的文件数量,应该和 images/val 的 jpg 数量一致。这种“先移动再验证”的做法比用随机脚本一次性搞定更稳妥,因为一旦发现哪张图没有标签,你还能及时把它留在训练集里当背景图,而不是让验证集出现一个无标签样本。
3.2 编写 dataset.yaml:类别顺序必须与 txt 的 class id 一致
在这个数据集的场景里,data.yaml 就是整个训练流程的“黑匣子开关”。YOLOv8 使用一种简单的 YAML 结构,这里我按七类别写一份完整配置:
# sewer_defect/dataset.yaml path: ./sewer_defect train: images/train val: images/val nc: 7 names: 0: joint-offset 1: obstacle 2: crack 3: buckle 4: hole 5: utility-intrusion 6: debris这里最关键的约定是 names 的索引顺序。txt 标签里 class id 写的是 0 到 6,YAML 里 names 的第 0 项必须对应关节偏移,第 1 项对应障碍物,以此类推。如果你把 names 写成了从 1 开始的字典,比如1: crack,那么预测结果里 “裂纹” 会被显示成 “关节偏移”,而且 loss 并不会因此变大。另外,path字段建议写绝对路径或相对 dataset.yaml 所在目录的路径。在 Windows 上训练时,path: ./sewer_defect有时会因为盘符问题读不到,更省事的做法是直接把 path 写成C:/data/sewer_defect这种绝对路径。
类别名称建议用英文或拼音,中文类别名在部分可视化脚本里会出现字体编码问题,推理时画框看不出标签名。你不要在这里花费额外精力,留给最终报告输出时再映射成中文描述。
3.3 用 YOLOv8 训练自己的数据集:一条命令与先看的三个参数
现在把训练跑起来,环境安装一步带过,重点看参数:
pip install ultralytics yolo detect train \ data=sewer_defect/dataset.yaml \ model=yolov8n.pt \ epochs=120 \ imgsz=640 \ batch=16 \ workers=4 \ project=sewer_runs \ name=defect_v1训练开始后不要干等,前十分钟重点看三样东西:第一,日志里 P(精确率)、R(召回率)、mAP50 是否在 epoch 5 之前有了非零值;第二,训练 loss 和验证 loss 的下降曲线是否同步;第三,images/val 里的样本有没有被正确加载。如果验证 loss 从第三个 epoch 开始就不再下降而训练 loss 仍在降,那就是过拟合信号,先停下来调增强参数,而不是跑满 120 个 epoch。
model=yolov8n.pt是 nano 版,适合先从轻量模型起步,因为下水道缺陷数据只有两千多张,用 s 或 m 反而更容易过拟合。batch=16在 8GB 显存上比较安全,如果你有 12GB 以上显存可以加到 32,batch 越大 BN 统计越稳。workers=4在 Windows 上如果报 DataLoader worker 错误,改成 0 或 2 即可,这是最常见的兼容性问题之一。
数据集的 2364 张图看起来不大,但加上 mosaic 增强和随机仿射变换,一个 epoch 的迭代次数仍然够模型在 120 epoch 内充分收敛。我建议先把 epochs 设成 120,同时开启patience=30,这样验证集连续 30 个 epoch 不提升就自动停止,节省时间也防止过拟合。
4. 调参不是玄学:损失函数三个分量、imgsz 与小目标增强策略
很多人在这个数据集上跑出 mAP50 只有 0.3,第一反应是换更大的模型,但我遇到过反过来的情况:用 yolov8n 在 640 分辨率下把 mAP50 从 0.4 提到 0.7,靠的只是改损失函数权重和增强参数。YOLO 的损失函数常被当成不用碰的黑匣子,其实它由三个可调节的分量组成,每个分量对缺陷检测的敏感性完全不同。
4.1 损失函数在管什么:box、cls、dfl 三个权重的调节逻辑
YOLOv8 的损失函数由三部分累加:box loss 负责预测框和真实框的 IoU 回归,cls loss 负责分类是否正确,dfl loss 负责边框分布建模。命令行对应参数是box、cls、dfl,默认值一般是7.5、0.5、1.5。在下水道缺陷场景里,裂纹和洞的框很小,box loss 权重过低会导致框回归不准,mAP50 看着还行,但 mAP50-95 掉得很厉害。
# sewer_runs/defect_v1/hyp.yaml 中修改 loss_obox: 7.5 loss_cls: 0.5 loss_dfl: 1.5我在这套数据上倾向把 box 保持 7.5 不变,把 cls 从 0.5 降到 0.4。原因是碎片和障碍物这类类别特征非常明显,分类压力不大;而关节偏移和带扣的边界模糊,更需要 box 回归把框贴紧。如果你发现模型总是把洞和障碍物混淆,再把 cls 升到 0.7 试一次。每次只改一个参数,不要同时动三个,否则没法定位是哪个改动带来了收益。
4.2 裂纹和洞是小目标:imgsz 从 640 提到 960 之前的权衡
YOLOv8 默认imgsz=640,对大多数场景够用,但下水道图像里裂纹宽度可能只有几个像素,640 下缩放到模型输入后,裂纹区域可能退化到 2×2 像素,特征图上的响应几乎消失。把 imgsz 提到 960 或 1280 能让小目标保留更多细节,代价是显存占用和训练时间同步上升。
yolo detect train \ data=sewer_defect/dataset.yaml \ model=yolov8n.pt \ imgsz=960 \ batch=8 \ epochs=100 \ close_mosaic=10这里batch必须从 16 降到 8,因为 960 分辨率下单张图显存占用约为 640 的 2.25 倍。close_mosaic=10表示最后 10 个 epoch 关闭 mosaic 增强,这个参数非常关键:mosaic 会把四张图拼成一张,小目标被进一步缩小,训练后期如果不关闭,框回归精度会受影响。另外提一句,YOLO 训练时会自动按 imgsz 缩放标注坐标,不需要你手动改 txt 里的归一化值,但如果你之前用脚本把标签都按 640 重新归一化过,换 imgsz 后反而要重新核验一遍。
4.3 从 2364 张图里榨出泛化能力:增强参数要按缺陷场景配
下水道 CCTV 图像有很强的一致性,光照相对固定,但旋转方向、俯仰角度和镜头距离变化很大。数据增强里对这套数据收益最大的是scale和degrees。因为检测机器人镜头可能任意旋转,管道内壁圆形缺陷在画面中会以任意角度出现。
yolo detect train \ data=sewer_defect/dataset.yaml \ model=yolov8n.pt \ scale=0.5 \ degrees=90 \ fliplr=0.5 \ mosaic=1.0 \ mixup=0.1scale=0.5允许图像缩放范围在 0.5 到 1.5 倍之间,对应 CCTV 镜头远近变化。degrees=90是旋转增强角度上限,我见过有人设 180,但那会引入大量颠倒样本,对判断管底沉积物反而有害。mixup=0.1不要设太高,下水道缺陷类别之间语义差异不算大,mixup 过高会让裂纹和碎片互相融合出诡异的训练样本。
增强参数调完务必保留一次原始参数的训练作为对照组。你可以分别记录两个版本的 mAP50-95,而不是只看 mAP50。很多时候增强让 mAP50 下降但 mAP50-95 上升,说明框回归更稳了,这种情况下我选择增强版本上线。
5. 这套数据集最容易翻车的 5 个坑:现象、原因、解决办法
这部分是血泪经验。我见过不止一个人在这套类似的数据集上白跑几十个小时,最后发现是数据本身或参数配置的隐性错误。下面五条按出现频率从高到低排列,每条都按现象、原因、解决写清楚。
5.1 训练一切正常,日志却提示 “found no labels”
现象:yolo 命令正常运行,loss 从 2 左右开始下降,但每轮 epoch 的迭代次数特别少,而且验证集 mAP 一直是 0。训练日志里只有图像信息没有标签统计。
原因:data.yaml 里的 train 和 val 路径写成了图片目录,而 YOLO 默认在图片目录的同级 labels 目录中找标签。比如 path 是sewer_defect,train 是images/train,YOLO 会到sewer_defect/labels/train找标签。如果你的实际目录是sewer_defect/train/images和sewer_defect/train/labels,只写train: images/train就会找不到标签。
解决:把 data.yaml 改为相对 path 下的完整结构,确保 labels 和 images 平级。如果你原来的组织是images和labels分别包含 train 子目录,YAML 里就写train: images/train,不要带../,不要写绝对路径再拼接相对路径。
5.2 图片文件名带空格或中文,导致标签匹配不上
现象:解压后图片名类似“CCTV_01 管道.jpg”,训练中部分图像没有参与 loss 计算,但也不报错。
原因:YOLO 按文件名前缀匹配 jpg 和 txt,如果 jpg 和 txt 的编码或空格不一致,会导致部分样本被静默跳过。中文文件名在部分 Linux 环境下还会因为 locale 编码不同产生匹配失败。
解决:训练前统一重命名,把空格替换为下划线,删除中文。处理完以后重新跑一遍 2.2 节的校验脚本,确保 jpg 名和 txt 名完全一致。这个坑看起来笨,但在下水道数据集这类历史数据里很常见。
5.3 训练完成但七个类别中某个类别 mAP 为 0
现象:训练 loss 正常收敛,验证集打印 per-class 指标时,带扣或公用设施入侵的 precision 全为 0,其他类别正常。
原因:这个类别的标注框数量太少,比如只有 30 个框,训练时正样本不足,模型几乎不可能学出有效特征。更隐蔽的情况是这类别的框全部集中在同一批图片里,而这批图片恰好被划分到了训练集,验证集没有正样本,per-class mAP 自然显示为 0。
解决:回到 2.3 节的类别普查结果,如果某个类别数量少于 50,建议先做类别重平衡:复制该类别样本到训练集三份,或对该类别单独做旋转平移增强。如果数量实在太少,我一般会考虑合并类别,比如把带扣和障碍物合并成“结构异常”,保证模型至少能框出区域,再由人工查看具体类型。
5.4 imgsz 提高后召回率反而下降
现象:从 640 提升到 1280 后,mAP50 下降,尤其裂纹和洞的召回率从 0.6 掉到 0.4。
原因:一方面 batch 从 16 降到 8 后 BN 统计不稳定;另一方面,图像分辨率提高时,如果原图本身模糊,提升 imgsz 只是把噪点放大,没有新增有效纹理信息。CCTV 机器人镜头离目标近时图像清晰,离远时本来就糊,强行放大只会让模型学到噪声。
解决:先用 960 试,不要一步跳到 1280。如果 960 比 640 有明显收益就定在 960。同时检查验证集里模糊图像的比例,这类图在推理时要单独设低置信度阈值,否则会被全部过滤掉。
5.5 验证集 mAP 很高,实际 CCTV 视频里全是误检
现象:模型在数据集验证集上 mAP50 达到 0.85,放到一段新的CCTV 巡检视频上,把管壁接缝全框成裂纹,把检修孔阴影框成洞。
原因:这是典型的数据泄漏。2364 张图来自相同管段的连续截帧,训练集和验证集如果随机划分,两张连续帧可能非常相似,模型相当于记住了这些帧,没有学到跨管段的泛化特征。
解决:用 2.1 的类别分布和文件名做一次聚类,把同一段管道的图像尽量放进同一个集。最稳的做法是按视频片段划分,而不是按单帧随机划分。如果 zip 内的文件名带片段编号,就按编号前缀切分 train 和 val;如果不带,至少保证验证集图片不是训练集相邻帧。这个步骤无法靠修改训练参数弥补,只能在划分阶段做对。
6. 把推理结果变成维修报告:置信度阈值与 CSV 输出技巧
模型训练完,不要只在验证集上看数字,而是要拿真实 CCTV 视频跑一段,把检测结果整理成维修班组能直接使用的表格。这一步的通用做法是写一个推理脚本,输出带框的视频片段和分类统计表。
from ultralytics import YOLO import csv import cv2 model = YOLO("sewer_runs/defect_v1/weights/best.pt") video_path = "field/clip_01.mp4" cap = cv2.VideoCapture(video_path) results = [] while cap.isOpened(): ret, frame = cap.read() if not ret: break preds = model.predict(frame, conf=0.35, imgsz=960, verbose=False) for box in preds[0].boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) results.append([video_path, cls_id, round(conf, 3)]) # 可在这里把每帧画框结果写入视频 cap.release() print("目标数量:", len(results)) with open("sewer_defect_report.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["video", "class_id", "conf"]) writer.writerows(results)这里有个验证技巧值得做:把conf从 0.2 到 0.5 间隔 0.05 跑一遍同一段视频,记录每档的检出数和误检数。误检数量下降最快的那一档,往往就是当前场景的最佳阈值。下水道视频里管壁接缝和阴影很多,0.35 是我的起点,如果你的模型在验证集上 mAP50 很高但实际视频误检多,把 conf 提高到 0.45 是更稳妥的选择。
最后说一个我吃过亏的习惯:模型训练完会顺手把 best.pt 当作最终模型,但我现在一定会拿它跑两段未参与训练的真实 CCTV 片段,一段是管道状况良好的,一段是缺陷密集的。良好管段上如果出现大量框,说明模型对背景过拟合;缺陷密集管段上如果漏检,说明验证集划分有数据泄漏。只有这两段视频表现都合理,我才会进入报告输出环节。这个验证动作只需要二十分钟,却能避免你把一个只在数据集上好看的模型交到维修组手里,然后在现场被质疑。希望你拿到这套数据集时,能从拆包核验这一步开始做扎实,训练和上线都会顺很多。希望帮到你。
本文还有配套的精品资源,点击获取