简介:一套面向下水管道缺陷检测的YOLO格式数据集,专门为使用YOLOv5/v7/v8/v9/v10/v11等系列算法的目标检测任务设计。数据覆盖关节偏移、障碍物、裂纹、带扣、洞、公用设施入侵、碎片等多类典型缺陷,适用于市政管道巡检、智慧水务与工业视觉场景,帮助研究者与工程师快速启动训练与验证。
包体共2000个文件,压缩后约33.89MB。内含980个xml与980个txt标注文件,分别对应VOC格式和YOLO格式,另附39张图像与1个data.yaml配置文件。图像标注采用标准YOLO格式的类别、中心点、宽高相对坐标,配合预设的数据集划分与配置,可直接对接主流YOLO训练框架,无需额外整理。
目前已有417人学习,适合需要真实场景缺陷样本、希望减少数据标注成本并快速验证模型效果的读者。压缩包目录结构清晰,标注文件命名规范,拿到后即可直接开展训练、测试与结果分析,是进行管道缺陷检测算法实验与工程落地的实用数据基础。
1. 下水道缺陷检测:980张标注图要解决的核心矛盾
管道CCTV检测车跑一圈回来,视频素材动辄几十个G,切帧后的有效画面里真正有缺陷的往往不超过三分之一。我见过太多团队卡在同一个环节:算法没开始调,先被标注拖了两周;好不容易标注完,类别口径又不统一,返工更痛苦。这套下水管道缺陷检测数据集,一共980张带标签图像,覆盖七类最常见的管道缺陷——关节偏移、障碍物、裂纹、带扣、洞、公用设施入侵、碎片。标签按YOLO格式组织,下载解压后可以直接接进YOLOv5或YOLOv8训练,不需要做格式转换。它适合的是已经被数据卡住的人:想快速验证YOLO在下水管道缺陷检测上能不能落地,想给甲方搭一个可演示的算法demo,或者做毕业设计和横向课题时需要一个能复现的基线数据集。
2. 七类缺陷标签体系:类目定义、YOLO格式与质检脚本
这套数据集的价值一半在图像,另一半在标签口径。下水管道缺陷分类在工程上是有讲究的,七个类别对应的是CCTV管道检测里最常见的缺陷类型:关节偏移和带扣属于接口类缺陷,裂纹和洞属于结构破坏类缺陷,障碍物、碎片、公用设施入侵属于管道功能受阻类缺陷。类别定义清楚了,训练出来的模型才有工程意义,否则只是“框了一堆东西”而已。
2.1 七类缺陷长什么样,哪两类最容易混淆
先把七类缺陷的视觉特征和容易混淆点说清楚。我后面标注新数据时,会直接把下面这张表打印出来贴屏幕上。
| 类别 | 视觉特征 | 最容易混的类 |
|---|---|---|
| 关节偏移 | 两节管接口错位,接口处张开或台阶状凸起 | 带扣 |
| 障碍物 | 管道内径被沉积物、树根、大块杂物侵占 | 碎片 |
| 裂纹 | 管壁线状裂缝,可长可短,有时带分支 | 光照阴影 |
| 带扣 | 接口处的钢带、套环松动翘起或脱落 | 关节偏移 |
| 洞 | 管壁局部缺失,边缘清晰,周围常有渗漏泥渍 | 大面积裂纹 |
| 公用设施入侵 | 电力、通信线缆从检查井或破损处穿入管道内部 | 障碍物 |
| 碎片 | 掉落的砖石、混凝土块、管道残片散落在管底 | 障碍物 |
标注时最容易翻车的是障碍物和碎片。我的血泪经验是:障碍物强调“侵占管道的截面空间”,比如树根长进管道、泥沙堆在管底;碎片强调“掉落的离散块体”,两者边界模糊时以矩形框内主目标为准,不要一框多义。另一个容易翻车的是公用设施入侵,有些标注员会把整条穿入的线缆全框进去,矩形框里一半是背景,正确的做法是只框“穿入管道可见段”,框得紧,不要框得松。
裂纹这类线状目标在YOLO的矩形框表达下天生吃亏,标注时宁可让框紧贴裂纹边缘,也不要把周围管壁的阴影包进来。框松了,模型学到的是“裂纹=管壁+阴影”,换一段管道马上误检。
2.2 YOLO格式标签解读与自动化质检脚本
这套数据集的标签是标准YOLO格式,每张jpg对应一个同名txt,txt里每一行代表一个目标:
class_id x_center y_center width height其中x_center、y_center、width、height全部归一化到0到1之间,除以图像宽高得到的。解压后建议先跑一遍质检脚本,别直接开训练,我吃过标签里混进非法坐标的亏。
import os from pathlib import Path import cv2 def check_label(img_dir, label_dir, num_classes=7): """ 检查YOLO标签中的类别越界、坐标越界和文件缺失 img_dir: 图像目录 label_dir: 标签目录 num_classes: 类别总数,本数据集为7 """ img_paths = list(Path(img_dir).glob('*.jpg')) bad_cnt = 0 for img_path in img_paths: label_path = Path(label_dir) / (img_path.stem + '.txt') if not label_path.exists(): print(f'缺标签: {img_path.name}') bad_cnt += 1 continue img = cv2.imread(str(img_path)) img_h, img_w = img.shape[:2] for line in label_path.read_text().strip().splitlines(): parts = line.split() if len(parts) != 5: print(f'字段数不对: {label_path.name}: {line}') bad_cnt += 1 continue cls_id = int(parts[0]) xc, yc, w, h = float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) if not (0 <= cls_id < num_classes): print(f'类别越界: {img_path.name} cls={cls_id}') bad_cnt += 1 if not (0 <= xc <= 1 and 0 <= yc <= 1 and 0 < w <= 1 and 0 < h <= 1): print(f'坐标越界: {img_path.name} {line}') bad_cnt += 1 print(f'检查完成,问题文件数: {bad_cnt}') # 使用示例 check_label('dataset/images', 'dataset/labels', num_classes=7)这段脚本的逻辑很简单但很实用:第一步确认每个图像文件都有同名标签文件;第二步读图像真实宽高,检查归一化坐标是否落在合法区间。归一化坐标越界最常见的来源是标注工具导出的格式不对,或者图像resize后标签没有同步处理,这类问题不查会在训练时被YOLO静默忽略,导致mAP莫名偏低。
还有一个很多人忽略的参数:num_classes。这个值必须和data.yaml里的nc一致,如果标注时类别编号从1开始而不是从0开始,第一个类别会被直接判定为越界,这时候不要急着改脚本,先把标签生成逻辑改对。
2.3 标注框精度对AP的影响:两个一致性习惯
980张图里的标签质量决定了模型上限。我在标注团队里定过两条硬规矩,现在标注出来的数据基本不用返工。
第一,最小目标尺寸要有阈值。碎片和裂纹的小目标,如果短边小于5个像素,不标。这种目标在YOLO下采样后只剩一个点的信息,强行标注只会变成标签噪声,让模型在模糊预测和清晰预测之间摇摆不定。第二,同一张图不同人标完,框与框之间的IoU要大于0.7。低于这个值说明大家对目标边界的理解不一致,需要回头重新对齐标注规则,而不是继续往下标。
3. YOLOv8复现流程:Anaconda环境、数据划分与训练参数实测
3.1 Anaconda环境配置与依赖版本
YOLOv8的复现门槛不高,但环境版本配错会浪费半天。我建议用Anaconda建独立环境,Python版本选3.10,PyTorch用CUDA 12.1对应的版本,Ultralytics包选8.2.x这个稳定版本。
conda create -n defect python=3.10 -y conda activate defect pip install ultralytics==8.2.0 torch==2.1.2 torchvision==0.16.2 --index-url https://download.pytorch.org/whl/cu121这里解释一下为什么固定版本:torch 2.1.2和torchvision 0.16.2是配对的,版本不匹配时训练可能直接崩在DataLoader阶段。Ultralytics选8.2.0而不是最新版本,是因为后续版本对某些参数名做过调整,教程和网上资料大多基于8.x早期版本,踩坑时更容易搜到解决方案。
安装完成后,首次运行训练命令会自动下载yolov8s.pt预训练权重。如果网络受限,可以手动把权重文件下载后放到当前目录,训练命令里的model参数直接指到本地文件即可。
3.2 数据集目录结构与train/val划分
YOLOv8要求数据集按固定目录结构组织:images下分train和val,labels下分train和val,严格一一对应。误差在目录结构上是最亏的,因为YOLO会把找不到标签的图像静默跳过,只在启动时输出一行警告。
dataset/ ├── images/ │ ├── train/ │ ├── val/ ├── labels/ │ ├── train/ │ ├── val/ └── data.yaml目录建好后,用脚本按8:2划分训练集和验证集。这里有一个关键技巧:不要按文件名随机划分,要按管道来源划分。同一段管道连续帧里的缺陷高度相似,随机划分会让验证集里混入训练集的“近亲”,mAP虚高0.1以上,换到真实管道上立刻现原形。
import os import random import shutil from pathlib import Path # 假设原始文件在raw/目录下 src_img = Path('raw/images') src_lbl = Path('raw/labels') dst = Path('dataset') train_ratio = 0.8 random.seed(42) # 建立目标目录 for split in ['train', 'val']: os.makedirs(dst / 'images' / split, exist_ok=True) os.makedirs(dst / 'labels' / split, exist_ok=True) # 收集所有图像文件名 names = [p.stem for p in src_img.glob('*.jpg')] random.shuffle(names) split_idx = int(len(names) * train_ratio) train_names, val_names = names[:split_idx], names[split_idx:] # 复制图像和标签 for name in train_names: shutil.copy(src_img / f'{name}.jpg', dst / 'images' / 'train') shutil.copy(src_lbl / f'{name}.txt', dst / 'labels' / 'train') for name in val_names: shutil.copy(src_img / f'{name}.jpg', dst / 'images' / 'val') shutil.copy(src_lbl / f'{name}.txt', dst / 'labels' / 'val') print(f'train={len(train_names)}, val={len(val_names)}')shutil.copy是逐文件复制,文件量大时稍慢,但980张图完全够用。random.seed(42)保证每次运行划分结果一致,这对后续对比实验很重要——同一个数据集如果每次划分不同,训练结果就没有可比性。
如果原始文件名里带管道编号或里程桩信息,更稳妥的做法是按文件名前缀分组后划分,我这边处理市政数据时基本都这么做。
3.3 data.yaml与训练命令参数
data.yaml是YOLOv8的数据集配置文件,路径、类别数、类别名都在这里。注意names列表的顺序必须和txt标签里的class_id一一对应,顺序错了模型训练出来预测结果全是乱的。
path: dataset train: images/train val: images/val nc: 7 names: - joint_offset - obstacle - crack - buckle - hole - utility_intrusion - debris配置好data.yaml后,训练命令如下:
yolo detect train \ data=dataset/data.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=1280 \ batch=16 \ patience=20 \ device=0 \ cache=True参数是我按管道缺陷场景试出来的经验值,逐个说明:epochs=150是偏保守的设定,配合patience=20做早停,验证集指标连续20轮不提升就自动停止,省时间;imgsz=1280是这套数据的关键,裂纹和碎片是小目标,640分辨率下特征会被下采样抹掉,1280能保留更多细节,代价是显存占用翻倍;batch=16对应16G显存,如果你的显卡只有8G,建议降到8并关闭cache,或者先用imgsz=640跑通流程再调分辨率。
cache=True会把图像预加载到显存里,训练中不重复读磁盘,能提速30%以上,但会额外占用几个G显存。显存紧张时设cache=False即可。
3.4 训练输出:results.csv、混淆矩阵与权重选择
训练结束后,结果保存在runs/detect/train/目录下。里面最值得关注的是best.pt和last.pt这两个权重文件:best.pt是按验证集mAP选出来的最优权重,last.pt是最后一轮权重。推理和部署一律用best.pt,last.pt只用于断点续训。
results.csv记录了每一轮的box_loss、cls_loss、dfl_loss和mAP指标。判断模型是否收敛,不要只看loss,要看验证集mAP50-95是否还有上升趋势。这个指标是测试集中每个类别的AP按IoU阈值0.5到0.95取平均,能更真实反映目标定位精度。
4. 训练与推理避坑:五个踩坑记录从类别不平衡到误检
4.1 类别不平衡:洞和带扣的AP被裂纹压着抬不起来
现象:训练完成后看混淆矩阵,裂纹的AP能到0.9以上,但洞和带扣的Recall不到0.5,几乎一半的真目标没框出来。
原因:这套数据集里裂纹样本占了将近四成,而洞和带扣合计不到15%。YOLOv8默认的类别损失是平均计算的,少数类正样本太少,梯度被多数类淹没。
解决:先统计训练集每类的框数量,再决定策略。
from pathlib import Path from collections import Counter cnt = Counter() for label_path in Path('dataset/labels/train').glob('*.txt'): for line in label_path.read_text().strip().splitlines(): cls_id = int(line.split()[0]) cnt[cls_id] += 1 names = ['joint_offset', 'obstacle', 'crack', 'buckle', 'hole', 'utility_intrusion', 'debris'] for i in range(7): print(f'{names[i]}: {cnt[i]}')如果两类样本数量差三倍以上,常见做法有三种:一是对少数类做过采样,把含洞和带扣的图像复制到训练集,但文件名要加后缀避免覆盖;二是在训练参数里传class_weights,按类别数量倒数给少数类更高的cls_loss权重;三是把mosaic增强比例调大,让每张训练图里能拼进更多小目标。我实测下来,过采样加class_weights双管齐下最稳。
4.2 小目标漏检:imgsz=640直接抹掉了细裂纹
现象:验证集上裂纹召回率低,放大预测图才发现,有些裂纹宽只有10个像素,在640分辨率下经过五次下采样,特征图只剩20×20,细线状特征几乎消失。
原因:这不是模型问题,是输入分辨率问题。YOLOv8默认imgsz=640,对细小缺陷不够友好。
解决:把训练和推理分辨率提到1280。显存不够时,推理阶段可以做切片——把1920×1080的帧切成左右两半,分别推理再合并结果。
import cv2 import numpy as np from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') img = cv2.imread('frame_0012.jpg') h, w = img.shape[:2] mid = w // 2 # 切成左右两半,各自推理 left_res = model.predict(img[:, :mid], imgsz=1280, conf=0.3)[0] right_res = model.predict(img[:, mid:], imgsz=1280, conf=0.3)[0] boxes, scores, class_ids = [], [], [] for res, offset_x in [(left_res, 0), (right_res, mid)]: for box in res.boxes: x1, y1, x2, y2 = box.xyxy[0].cpu().numpy() boxes.append([x1 + offset_x, y1, x2 + offset_x, y2]) scores.append(float(box.conf)) class_ids.append(int(box.cls))切片推理的代价是拼接处可能漏检,切的时候重叠10个像素,合并后还要跑一次NMS把重复框去掉。逻辑上就是“切半→推理→坐标偏移→NMS合并”,代码里offset_x是关键,右侧半图的x坐标必须加回mid。
4.3 标签文件名对不上:训练时N images但M labels的隐患
现象:训练启动时YOLO提示找到了N张图像、M个标签,M远小于N,但训练还能跑完,loss正常下降,mAP却很低。
原因:部分图像的同名标签文件缺失,或者标签目录里混入了其他文件中转残留的txt,文件名对不上就被静默跳过。YOLO不会因为这些文件报错,只会输出一行信息,很容易忽略。
解决:训练前无论如何先跑一遍第2.2节的质检脚本,保证每个jpg都有同名txt,且txt里每行坐标合法。标签文件放在错误的路径是另一个翻车点,YOLO只认images和labels同级目录,放到annotations目录里等于没放。
4.4 置信度0.25压不住潮湿管壁的误检
现象:验证集指标不差,但把模型扔到完整管道视频上,水迹、光照反光、管壁纹理全被当成碎片和裂纹,一帧能出二三十个框。
原因:训练集图像相对干净,现场视频的光照和管壁状态差异大,模型输出的置信度整体偏低且分布偏移,默认的conf=0.25阈值压不住低置信度误检。
解决:不要用默认阈值,在验证集上画F1-confidence曲线找最优阈值。管道缺陷场景我常用的现场阈值在0.35到0.45之间,如果追求不漏检、容忍误检,阈值往左移;如果检测结果要进自动报告,阈值往右移。
4.5 现场光照一变,模型Confidence整体漂移
现象:同一套权重,白天拍的管道检测效果好,雨天或夜间光照不足的视频漏检率升到40%以上。
原因:数据集采集环境相对单一,模型学到的特征是“特定光照下的缺陷”,光照分布一变,特征分布就漂了。这类问题靠调参数解决不了,必须往训练集里补充现场分布的数据。
解决:做法是把现有模型先压到新场景视频上做预标注,人工修正后合并进训练集做第二轮训练。这就是第6章要展开的数据回灌闭环,核心思路是用模型自己帮自己扩充数据。
5. 验证优化:混淆矩阵与置信度阈值把误检压下去
5.1 用val模式单独跑验证,定位易混淆类别
训练结束后不要只盯着训练日志里的mAP,单独跑一次验证,拿到完整的混淆矩阵,才能看清哪些类别在互相打架。
yolo detect val \ data=dataset/data.yaml \ model=runs/detect/train/weights/best.pt \ split=val参数说明:split=val显式指定验证集,避免YOLO在训练集和验证集上重复输出;model参数指向best.pt。
跑完后的confusion_matrix.png里,非对角线上的亮点就是易混淆对。比如关节偏移和带扣互检,碎片被错检成障碍物,都在混淆矩阵上一目了然。这类混淆有两个修正方向:一是回头检查标注,看看是不是标注口径本来就没分开;二是增加对应类别的难例数据,让模型见过足够多的“边界样本”。
5.2 F1-confidence曲线与现场阈值的确定方法
YOLOv8验证结果里自带F1_curve.png,横轴是置信度阈值,纵轴是F1分数,但后台生成的都是默认参数。我更习惯自己扫一遍置信度,确定现场该用多少。
from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') for conf in [0.25, 0.30, 0.35, 0.40, 0.45, 0.50]: metrics = model.val( data='dataset/data.yaml', split='val', conf=conf, iou=0.45, verbose=False ) print(f'conf={conf:.2f} mAP50={metrics.box.map50:.4f}')输出结果里挑mAP50下降不明显但预测框数量明显变少的阈值。比如conf从0.30调到0.40,mAP50只降了0.02,但整段视频的误检框从200个降到80个,那0.40就是当前场景的最优阈值。注意这个阈值是场景相关的,换一段光照完全不同的管道,阈值要重新扫。
5.3 损失函数侧微调:cls_loss权重和box_loss的平衡
YOLOv8的总损失由三部分组成:box_loss负责定位框,cls_loss负责分类置信度,dfl_loss负责边界框回归的分布。管道缺陷场景里,类别不平衡时最值得动的是cls_loss。
YOLOv8的超参数文件里cls默认0.5左右,box默认7.5左右。当少数类(洞、带扣)的AP明显偏低时,我一般把cls调到1.0到1.5,让分类误差在总损失里占更大比重。这个调整是通过训练命令传参实现的:
yolo detect train \ data=dataset/data.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=1280 \ cls=1.2cls从0.5调到1.2后,常见变化是多数类(裂纹)的Recall轻微下降,但少数类的AP能提升3到5个点。这在缺陷检测场景里通常是划算的——裂纹漏掉一条,和洞漏掉一个,后者造成的事故风险高得多。
6. 数据回灌:用难例框把下一轮训练集变厚的技巧
第一轮模型跑通后,最难啃的反而不是模型结构,而是数据本身。我一般不会急着直接标注新视频,而是用现有模型跑“难例回收”,只标那些“模型拿不准”的框,效率能高出一大截。
import cv2 from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') # 假设video_path是一个管道cctv视频 cap = cv2.VideoCapture('new_pipeline_video.mp4') frame_id = 0 while True: ret, frame = cap.read() if not ret: break # 每15帧抽一帧,减少冗余 if frame_id % 15 != 0: frame_id += 1 continue # 用低置信度阈值0.15,把模型的“犹豫区”全部捞出来 results = model.predict(frame, conf=0.15, iou=0.5, verbose=False)[0] for box in results.boxes: conf = float(box.conf) # 只保留置信度在0.15~0.45之间的难例 if conf < 0.45: x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int) cls_name = model.names[int(box.cls)] crop = frame[y1:y2, x1:x2] cv2.imwrite(f'hard_examples/{cls_name}_{conf:.2f}_{frame_id}.jpg', crop) frame_id += 1这段代码的核心逻辑是:把置信度大于0.45的框过滤掉——那些是模型已经很确定的,不需要人工参与;把小于0.15的框也过滤掉——那些大多是噪声,让标注员去看反而浪费时间;只保留0.15到0.45这个“犹豫区间”,它们要么是新的缺陷形态,要么是模型易错样本。
我一般让标注员只看这批裁剪图,一小时能筛几百个难例,筛完的框重新对齐到原图坐标,补齐标签后合并进训练集,跑第二轮训练。这个习惯让模型每迭代一版,就更贴近现场真实分布。
从那以后,我每次拿到新场景的管道视频,第一件事不是打开标注工具从零开始标,而是先把模型压上去跑一遍难例回收,把犹豫区的框抽出来筛。看似绕了一圈,实际比全量标注省下至少一半时间,模型效果还更稳。希望帮到你。
本文还有配套的精品资源,点击获取