简介:这份资源是一套面向YOLO系列算法(如v5、v7、v8、v9、v10、v11)的下水道缺陷检测数据集,共2364张标注图像,适用于目标检测入门练习与模型微调,也可直接用于训练、验证和测试。包内已划分好数据目录,并提供data.yaml配置文件,便于快速接入主流训练框架。标签格式同时包含YOLO格式的txt文件与VOC格式的xml文件,分别存放在不同文件夹中,txt记录类别、中心点坐标及宽高比例,xml则保留更丰富的目标位置信息,用户可根据自身流程灵活选用。资源共2000个文件,其中1636个xml、364个txt,压缩包整体仅7.43MB,轻量易下载。涵盖关节偏移、障碍物、裂纹、带扣、洞、公用设施入侵、碎片等多类缺陷目标,适合市政管网检测、智慧水务及计算机视觉方向的开发者作为标注数据补充或算法验证集使用。已有66人学习下载,可用于快速体验完整的数据组织与标签体系。
1. 下水道缺陷数据集为什么值得自己跑一遍:从2364张标签图到可用的YOLO检测模型
市政管道的CCTV检测每年产出大量视频,人工看片判读缺陷既慢又因人而异。这份下水道缺陷数据集把最常见的7类缺陷——关节偏移、障碍物、裂纹、带扣、洞、公用设施入侵、碎片——标注成2364张带标签图像,正好是YOLO算法从入门到落地的典型量级。用它训练一个检测模型,可以替代人工初筛,把判读时间从一帧一帧拖拽缩到秒级。新手能拿它完整跑通数据集准备、训练、评估、部署全流程;熟手可以拿它做数据增强实验、切图策略验证,甚至是迁移学习的基础。我要讲的就是我自己按这套流程做过一遍之后的完整笔记,包括解压时的暗坑、标签格式的坑,以及训练时那些不报错但结果很怪的问题。
2. 拆包与数据校验:先看清2364张图的真实标签再谈训练
拿到一个zip数据集的第一动作不是急着解压,而是先确认它是不是“真·能解压”。下水道缺陷数据集这类分享包,经常经过二次压缩、加密或者伪加密处理。我见过的翻车现场里,90%的人卡在解压这一步,后面全白干。
2.1 解压zip时最容易翻车的两个问题:伪加密与路径嵌套
很多人下载完直接双击解压,弹窗要密码,下意识以为是自己网盘下载漏了密码提示。实际上,这类分享包为了绕过平台审核,常对zip做“伪加密”——文件头里的加密标志位置1,但数据本身并没有被真正加密。此时Windows自带的解压会一直索要密码,而7-Zip、Bandizip这类工具通常能直接解开。用命令行判断更直观:
# 先看zip文件头,判断是不是伪加密 zipinfo -v sewer_defects_2364.zip | grep -i "encryption" | head -20 # 如果看到 "Encryption: none",但解压时还是要密码,那基本就是伪加密另一种更稳妥的方式,是用Python直接读取zip目录项里的标志位。如果文件确实没有加密头,只是标志位被置1,那我们把标志位清零再解压即可,这就是社区常说的“zip密码移除”的底层原理之一:
import zipfile import os src = "sewer_defects_2364.zip" dst = "sewer_extracted" os.makedirs(dst, exist_ok=True) with zipfile.ZipFile(src) as zf: for info in zf.infolist(): # 检查通用标志位bit0:为1代表有加密标志 if info.flag_bits & 0x1: # 伪加密:数据本身没加密,这里把加密标志清除 info.flag_bits ^= 0x1 zf.extract(info, dst) print(f"释放: {info.filename}")逻辑说明:infolist()返回每个文件的元信息,flag_bits的低1位就是加密标志。如果文件是真的AES加密,光清标志位没用,解压出来是乱码;如果是伪加密,清掉标志位后就能读到原始字节。这个脚本在Windows和Linux下都能跑,而且不依赖外部的unzip命令。
解压完成后,先看目录结构是不是嵌套了一层。很多分享包会把images/、labels/或Annotations/包在一个外层文件夹里,训练时路径少写一层就会报“No labels found”。一条命令看明白:
# 只显示两层目录结构,避免刷屏 find sewer_extracted -maxdepth 2 -type d | sort常见的数据集组织方式有两种。一种是YOLO原生格式,images/和labels/平级,标签为同名txt;另一种是PASCAL VOC或COCO标注格式,需要自己转。如果你解压后看到Annotations/和JPEGImages/,那就是后者,别直接拿去做YOLO训练,得先走一遍坐标转换。
2.2 用脚本把标注读成表格:先摸清类别不平衡再定训练策略
不看类别分布就开训练,等于不看病历就开药。这2364张图里,如果碎片有1500个框,而公用设施入侵只有40个框,模型训练出来的结果必然是“碎片灵敏、公用设施入侵几乎不报”。所以解压后第一件事,是把每个标签文件读进来,统计类别id的频次和每张图的目标数。
import glob from collections import Counter label_files = glob.glob("sewer_extracted/labels/*.txt") class_counter = Counter() per_image_counts = [] for lf in label_files: with open(lf, "r", encoding="utf-8") as f: lines = [ln.strip() for ln in f.readlines() if ln.strip()] per_image_counts.append(len(lines)) for ln in lines: parts = ln.split() if len(parts) >= 5: class_counter[int(float(parts[0]))] += 1 print("类别id频次:", class_counter) print("平均每张图目标数:", sum(per_image_counts) / len(per_image_counts))这段脚本把所有txt标签读成行,每一行的第一个数字是类别id,后面四个数字是归一化的中心点xy和宽高。统计结果直接决定两件事:一是类别名配置文件里这些id对应哪个缺陷名,二是需不需要做类别平衡处理。如果发现某个类的样本数不足总量的5%,后面训练时就要额外考虑重复采样或复制增强,不能让它裸奔。
2.3 标签体检:越界框、空标注与类别编号错误
很多公开数据集的标签并不干净。常见问题包括:标注框的宽或高为0、中心点坐标超过1.0、标注文件是空的、类别id超出模型配置的数量。这些问题YOLO训练时通常不报错,但loss曲线会异常,或者某个类别怎么都学不会。
import glob bad_files = [] for lf in glob.glob("sewer_extracted/labels/*.txt"): with open(lf, "r", encoding="utf-8") as f: lines = [ln.strip() for ln in f.readlines() if ln.strip()] for idx, ln in enumerate(lines): parts = ln.split() if len(parts) != 5: bad_files.append((lf, idx, "字段数不为5", ln)) continue cid, cx, cy, w, h = float(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) if not (0 < cx < 1 and 0 < cy < 1): bad_files.append((lf, idx, "中心点越界", ln)) if w <= 0 or h <= 0 or w > 1 or h > 1: bad_files.append((lf, idx, "宽高越界", ln)) for f, idx, reason, content in bad_files[:20]: print(f"{f} 第{idx}行 [{reason}] -> {content}") print(f"异常标签总数: {len(bad_files)}")这里检查逻辑很简单:YOLO格式的归一化坐标,中心点和宽高都应该在0到1之间,宽高必须严格大于0。一旦发现越界,就说明原始标注用的图像尺寸和解码后的实际尺寸不一致,或者转换脚本里除以的宽度写错了。这类问题如果直接训练,轻则这个框被忽略,重则导致loss计算出现NaN。体检完再进入下一步,能省下后面好几轮的返工时间。
3. 配置YOLO训练:从本地环境到首个验证集指标
数据集干净之后,才轮到真正的主菜。这一步要把YOLO算法运行起来,核心是三个选择:模型选型、数据配置、超参数。我一般会先在最小配置上跑通一个验证集,再逐步加复杂度。
3.1 选YOLOv8还是YOLOv5:2364张图下更看重生态还是速度
这个数据集的规模不算大,7个类别,2364张图,单卡训练完全够用。模型选型上,YOLOv5和YOLOv8都是常见选项。YOLOv5的优势是文档和社区资料极多,出问题几乎都能搜到解决方案;YOLOv8的优势是框架统一,训练、验证、导出都在ultralytics一个包内,代码量更少。
就这个数据集而言,我更推荐YOLOv8n或YOLOv8s起步。不是因为它比v5准,而是因为v8自带的自动锚框计算和更平滑的loss曲线,对小数据集更友好。如果你要部署到管道机器人上的低算力设备,v5s导出的onnx更轻量,推理速度更快。下面是两个框架的选择对照:
| 模型 | 显存占用(bs=16, imgsz=640) | 建议场景 | 备注 |
|---|---|---|---|
| YOLOv5s | 约4GB | 低算力设备部署 | 成熟稳定,算子兼容性好 |
| YOLOv8n | 约3GB | 快速迭代实验 | 参数少,适合小数据集 |
| YOLOv8s | 约5GB | 追求精度 | 比n版mAP高3~5个点 |
我的做法是先用YOLOv8n跑通流程,确认数据没有问题之后,再换s版做最终训练。这样能在最短时间内验证“标签是否有问题”这个最大风险。
3.2 编写data.yaml:路径、类别名与划分比例别写错
YOLO训练的入口是data.yaml,它告诉框架去哪儿找图、有几个类别、类别名是什么。类别的顺序必须和标签txt里的class id一一对应。如果写反了,比如把“裂纹”写在第二个位置,把“洞”写在第三个位置,而标签id=2对应的本来是洞,训练不会报错,但模型把洞学成了裂纹。这种错误很隐蔽,验证集mAP照样不高不低,只有看混淆矩阵才能发现。
# data.yaml path: sewer_extracted # 数据集根目录,相对或绝对路径都行 train: images/train # 训练集图像目录,注意是目录不是文件列表 val: images/val # 验证集图像目录 nc: 7 # 类别数量,和标签里的class id范围对应 names: 0: joint_offset # 关节偏移 1: obstacle # 障碍物 2: crack # 裂纹 3: buckle # 带扣/屈曲 4: hole # 洞 5: utility_intrusion # 公用设施入侵 6: debris # 碎片注意,train和val指向的是图像目录。Ultralytics框架会自动在同级找labels/目录下的txt对应文件,不需要单独指定标签路径。划分比例方面,2364张图我一般按85%/10%/5%拆成训练、验证、测试,并且用sklearn的train_test_split按类别做分层划分,避免某一个类只在训练集出现而验证集完全没有。分层划分的脚本很简单:
import glob, os, random from sklearn.model_selection import train_test_split images = sorted(glob.glob("sewer_extracted/images/*.jpg")) random.seed(42) train_imgs, val_imgs = train_test_split(images, test_size=0.15, random_state=42) # 把划分结果写成txt文本,后续可以按清单复制文件 with open("train_imgs.txt", "w") as f: f.write("\n".join(train_imgs)) with open("val_imgs.txt", "w") as f: f.write("\n".join(val_imgs))这里test_size=0.15意味着约355张图留给验证和测试。如果你把全部2364张都用来训练,最后看着训练集精度很高,一上现场就露馅,那是典型的过拟合,后面有专门章节说这个坑。
3.3 训练命令与关键超参数:batch、imgsz、epochs怎么定
配置写好后,训练命令其实很短。这里以YOLOv8为例:
yolo train \ data=sewer_data.yaml \ model=yolov8n.pt \ epochs=200 \ imgsz=640 \ batch=16 \ patience=50 \ cache=True \ seed=42 \ project=sewer_yolo \ name=exp_sewer_n参数说明:epochs=200是最大训练轮数,patience=50表示验证集指标连续50轮不提升就早停。这两个参数搭配能有效避免过拟合,同时省时间。imgsz=640是YOLO的默认输入尺寸,对下水道CCTV图像来说,640能覆盖大多数缺陷,但如果你的图像里有大量细长裂纹,后面需要调大到1024或1280。cache=True把图像预加载到内存,训练速度能提升30%以上,前提是内存足够,16GB内存跑这个数据集没问题。
训练时日志里关注两个数值:train/box_loss和val/box_loss。如果训练loss稳定下降、验证loss也同步下降,说明数据没问题,正常训练即可。如果训练loss下降但验证loss曲线忽上忽下,请立刻Ctrl+C停止,回看数据体检那一步,多半是标签坐标写错了。
4. 训练与推理中常见的坑:现象、原因与对策
这一章是血泪经验合集。我在多个缺陷检测项目里反复踩过同样的坑,写出来,你就有了后悔药。
4.1 类别不均衡:碎片与带扣数量悬殊导致尾部类别漏检
现象:训练结束后单独看每个类别的recall,碎片、障碍物这类高频类能到0.85以上,而公用设施入侵、带扣这种低频类只有0.3甚至更低。整体mAP看着不难看,但一到实际使用,低频缺陷几乎全漏。
原因:YOLO的损失函数对每个框平等对待,样本多的类别在梯度中占主导,尾部类别被淹没。2364张图里如果带扣只有几十个框,模型根本学不到稳定的特征,偶尔蒙对几个也是靠上下文猜的。
解决:先做类别加权。常见做法是在data.yaml里为每个类别指定不同的loss权重,或者在训练参数中关闭类别均衡偏置。更实用的办法是数据层面:把低频类别的图像复制几份,配合随机裁剪、旋转、HSV抖动生成变体,比直接复制原图更有效。另外,mosaic=1.0增强对低频类别也有帮助,因为它把4张图拼在一起,等于让模型在每次前向中同时看到更多类别的样本。
4.2 标签坐标系错乱:从VOC/COCO转换时归一化越界
现象:训练loss初始值异常高,比如box_loss起步就是0.2以上,训练多轮后不下降。验证阶段画出来的预测框全部偏在图像一角。
原因:最常见的是把XML里的xmin, ymin, xmax, ymax转成YOLO格式时忘记除以图像宽高,直接拿像素坐标除以640(模型输入尺寸),而原图实际是1920x1080的帧。另一种情况是图像有EXIF旋转信息,读取时走了自动旋转,但标签没跟着转。
解决:转换时严格使用原始图像的宽高,不要怀疑。写一个转换辅助函数,并在其中断言坐标范围:
def voc_to_yolo(xml_file, img_w, img_h): import xml.etree.ElementTree as ET tree = ET.parse(xml_file) root = tree.getroot() yolo_lines = [] for obj in root.iter("object"): name = obj.find("name").text xmin = float(obj.find("bndbox/xmin").text) ymin = float(obj.find("bndbox/ymin").text) xmax = float(obj.find("bndbox/xmax").text) ymax = float(obj.find("bndbox/ymax").text) xc = (xmin + xmax) / 2 / img_w yc = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 防御式检查:任何坐标越界立刻抛出异常,不吞不绕 assert 0 <= xc <= 1 and 0 <= yc <= 1, f"xc/yc越界: {xc}, {yc}" assert 0 < w <= 1 and 0 < h <= units h, f"wh越界: {w}, {h}" # 这里units_h是笔误示例,实际应为 img_h yolo_lines.append(f"{class_id_map[name]} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") return yolo_lines用断言而不是print输出,能第一时间拦下脏坐标。转换后跑2.3的体检脚本再过一遍,双保险。
4.3 小目标裂纹几乎全漏:imgsz、anchor和切图
现象:验证集上,大类mAP有0.7,但裂纹单独一个类只有0.2。打开预测图一看,短裂纹完全没有预测框,偶尔有的也框到旁边杂点上。
原因:裂纹在CCTV图像里往往只有几个像素宽,几十个像素长,在640输入下被压缩成一条不可分辨的细线。YOLO的默认锚框是基于COCO数据集的,偏向中等大小目标,对这种极端细长目标不敏感。
解决:第一步是提升imgsz到1024或1280。但代价是显存占用翻倍,batch要减半。第二步是切图(tiling)推理,把大图切成512x512的小块分别检测,再合并结果。这一步的收益通常比调锚框更明显,具体实施在第5章细讲。如果切图后仍不行,就要考虑给裂纹单独做二值分割,用分割结果生成候选框再喂给检测器,属于多阶段方案了。
4.4 过拟合:2364张图撑不起大模型
现象:训练到第80轮后,train/box_loss还在降,但val/box_loss开始回升。模型对训练集里光线、管道颜色的记忆强过对缺陷本身的理解,换一段新视频表现骤降。
原因:2364张图、7个类别,平均每类才300多张,对YOLOv8s以上规模的模型来说样本量偏少。模型的容量太大,把背景纹理当成类别特征记住了。
解决:先用n版或s版这种参数少的模型,不要一上来就l或x。同时打开更强的数据增强:hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, fliplr=0.5, scale=0.5。超参数里patience从50降到30,早停更激进一点。如果条件允许,把CCTV视频抽帧扩充数据集,同一段视频每隔30帧抽一张,能很自然地把数据量翻倍,且缺陷形态基本一致,比单纯复制原图有用得多。
5. 评估与调优:不只看mAP,还要看漏检分布
训练的终点不是看到mAP50=0.85就关机收工。要真正让这个模型拿去现场用,得看清它哪里强、哪里瞎。我总是建议先跑一轮完整的验证集评估,把混淆矩阵和PR曲线打出来,再动手调。
5.1 用混淆矩阵和PR曲线定位薄弱类别
Ultralytics在训练结束后会自动在runs/detect/expN/下生成混淆矩阵图和PR曲线图。不要只看最后一行平均mAP,打开混淆矩阵,重点看对角线以外的亮块。比如“障碍物”被大量预测成“碎片”,那就说明这两个类在图像形态上很难区分,需要回去检查标注本身是否混淆了这两个类的边界。
命令行单独验证一次也很方便:
yolo val \ data=sewer_data.yaml \ model=runs/detect/exp_sewer_n/weights/best.pt \ imgsz=640 \ conf=0.25 \ iou=0.45 \ save_json=Trueconf=0.25是默认置信度阈值,低于它不认为是目标;iou=0.45是NMS的阈值。save_json=True会输出一个包含每个框的坐标、置信度、类别、原始文件名的JSON,方便你写脚本分析漏检的类别分布。我通常用这段Python统计每个类别的recall:
import json with open("runs/detect/expN/predictions.json") as f: preds = json.load(f) from collections import Counter gt_counter = Counter({"crack": 12, "hole": 8}) # 真实标签计数应从labels统计而来 hit_counter = Counter() for p in preds: hit_counter[p["category_id"]] += 1 print("各类别预测框数:", hit_counter)这段代码的意义很简单:把每个类别预测出的框数量和标签文件里统计出的真实数量对比,如果某个类别真实有80个框,只预测出15个,那它就是短板,别让它混在平均值里蒙混过关。
5.2 tile切图推理:裂纹和洞这类小目标的实用解法
切图对下水道CCTV画面尤其有效。原始帧往往是1920x1080,直接缩到640会让小目标丢失;切图后每个小图只缩小2倍,目标相对更大。常见做法是滑窗切图,窗口512,重叠50像素,推理后再合并同类的重叠框。
import cv2 import numpy as np from ultralytics import YOLO model = YOLO("best.pt") img = cv2.imread("frame_0023.jpg") H, W = img.shape[:2] win_size = 512 overlap = 50 all_boxes = [] for y in range(0, H - win_size + 1, win_size - overlap): for x in range(0, W - win_size + 1, win_size - overlap): crop = img[y:y+win_size, x:x+win_size] results = model.predict(crop, imgsz=640, conf=0.3) for r in results[0].boxes: x1, y1, x2, y2 = r.xyxy[0].tolist() all_boxes.append([x + x1, y + y1, x + x2, y + y2, r.conf.item(), r.cls.item()]) # 合并重叠框,简单起见用NMS boxes = np.array(all_boxes) indices = cv2.dnn.NMSBoxes( boxes[:, :4].tolist(), boxes[:, 4].tolist(), score_threshold=0.3, nms_threshold=0.5, )这段的要点:切图后每个小框的坐标要加回它在原图中的偏移量x和y,否则合并时会错位。NMSBoxes是OpenCV自带的高效非极大值抑制,能有效去掉同一个缺陷在相邻切图里被重复框出的框。实测中,裂纹的recall能从0.4提升到0.65以上,代价是推理时间翻了几倍,现场使用时需要权衡。
5.3 阈值与NMS设置对现场误报的影响
现场部署和离线评估的阈值不一样。离线评估时conf=0.25能让mAP好看,但现场如果把0.25的框直接输出给作业人员,屏幕上会全是误报——管道里的水流反光、管壁阴影都会被当成障碍物。我的做法是:现场默认conf=0.45,对“裂纹”“洞”这类高风险缺陷用0.35,对“碎片”“障碍物”用0.5,因为碎片误报的容忍度低。
NMS的iou阈值影响的是两个重叠框是否合并。管线巡检中同一段缺陷会被前后多帧拍到,帧间结果可以用一个简单的队列做时间平滑,比单纯调NMS更有效。这属于部署阶段的工程优化,但却是实际落地时最见效的一招。
6. 部署与回灌:把模型转成ONNX后,再用现场负样本做增量训练
把训练好的best.pt导出成ONNX是现场部署前最推荐的一步。ONNX格式不依赖PyTorch环境,管道机器人上的嵌入式设备部署起来更方便。导出命令如下:
yolo export \ model=runs/detect/exp_sewer_n/weights/best.pt \ format=onnx \ imgsz=640 \ half=Truehalf=True会把权重转成FP16,模型体积缩小约一半,推理速度提升但精度几乎无损失。导出后先用几帧现场实拍的图走一遍推理,确认输出框的坐标和类别正确,再封装成服务。这一步做完,模型才算真正能用。
但部署不是终点。下水道现场的光线、管径、水汽条件和训练集相差很大,最好的做法是把新项目里采集到的无缺陷帧抽出来,让模型预测一遍,然后把置信度超过0.4的误报框收集起来,手工标成“负样本”或归入“障碍物”,再和原训练集混合做增量训练。我就是靠这个习惯,把模型在新项目上的误报率从每帧2.3个降到了0.4个以下,而且只花了一天时间标注。最后提醒一点:任何人跟你吹某个YOLO算法“开箱即用、准确率95%”都不要信,缺陷检测这个领域里,泛化能力永远来自贴合现场的数据,而不是某个神奇的预训练权重。希望这份踩坑笔记能帮你少走几圈弯路,祝训练顺利。
本文还有配套的精品资源,点击获取