
简介针对管道运维与安全监测需求面向目标检测与工业视觉应用的一份管道缺陷检测数据集超过900张带标签图片适合研究人员与工程师直接用于YOLO系列模型的训练与验证。数据处理成YOLO格式并做了数据增广内置训练集、验证集和类别文件类别信息集中在defect等标签中。压缩包共1893个文件其中以946个txt标签与945个jpg图片为主体另有1个py脚本和1个png示意图整体约32.47MB目录结构清晰便于直接接入训练流程。show脚本可以把检测框绘制在图像上用于快速核对标注质量、观察目标分布针对裂纹、锈蚀、凹陷等典型工业表面异常可直接基于该数据构建检测模型。目前已有1940人学习下载适合希望快速搭建缺陷检测基线、验证数据增强策略、开展目标检测实验或进行工业质检算法预研的开发者。1. 管道缺陷检测数据集拿到 900 张目标检测标注图先别急着训练管道缺陷检测数据集一共超过 900 张带标签图片是目标检测方向里难得能直接落地的标注资源。第一次拿到这份数据我跟大多数人的想法一样先解压再看图然后满怀期待打开训练脚本。真正跑过一轮以后才发现900 张图能不能撑起一个可用的检测模型取决于你是否先回答三个问题标签坐标是绝对坐标还是归一化坐标缺陷类别分布是否均衡训练集和验证集有没有做同源隔离。这份资源覆盖了燃气、排水、工业管道巡检里常见的表面缺陷类型适合用来训练 YOLO、Faster R-CNN 这一类的目标检测模型。无论你是想快速搭一个管道缺陷识别 Demo还是要为巡检系统补充训练样本都值得先把这套数据和标签完整地跑一遍再决定后面怎么调。2. 先拆数据目录标注格式、类别分布与图像质量三件事拿到数据包以后第一件事不是把图片全部扫一遍而是把目录结构和标注文件过一遍。图片看起来差不多但标注可能是 VOC 的 XML、COCO 的 JSON也可能是 YOLO 的 TXT三者的坐标含义完全不同。如果不对格式做确认就直接写加载器很容易读到一半报越界或者在坐标换算时把框画到图片外面。下面是我通常的处理顺序每一步都是为了后面训练时少出问题。2.1 目录组织一张图对应一个标签的对应逻辑数据解压后先用一条树命令看看整体组织tree -L 2 pipe_defect_dataset/常见的目录结构是这样pipe_defect_dataset/ ├── images/ │ ├── 0001.jpg │ ├── 0002.jpg │ └── ... ├── labels/ │ ├── 0001.xml │ ├── 0002.xml │ └── ... └── classes.txtimages 和 labels 目录下的文件名一一对应只是扩展名不同。加载时用图片的 stem 去找同名标签这种结构最省心也是我接触到的大多数标注数据集的常见组织方式。还有另一种组织方式是标签全部集中在 CSV 或 JSON 里图像文件和标签分开存放这种情况下需要先读索引字段才能建立图片和标注的关联。不管哪种组织我建议先看一套图片和它对应的标签文件内容确认格式后再设计加载逻辑。尤其要注意 labels 目录里如果混着 .txt 和 .xml不要靠猜先跑一遍扩展名统计。from pathlib import Path from collections import Counter label_dir Path(pipe_defect_dataset/labels) ext_counter Counter(p.suffix.lower() for p in label_dir.iterdir()) print(ext_counter)这段脚本统计 labels 目录下每种扩展名的文件数量。输出类似 Counter({.xml: 850, .txt: 80})说明多数图片是 XML 标注少量是新补充的 TXT 标注那么加载器就要同时兼容两种格式。注意 Path.iterdir() 会遍历目录里所有文件如果 labels 目录混入了非标注文件统计结果会被污染需要先人工看一眼输出再决定是否过滤。2.2 标注坐标体系像素坐标与归一化坐标的换算XML 格式通常对应 Pascal VOC坐标是像素绝对值一个对象的标注写成 xmin、ymin、xmax、ymax 这组值。TXT 格式通常对应 YOLO一行五个数类别编号、中心点 x、中心点 y、框宽、框高其中中心坐标和宽高都相对图片尺寸做了归一化。这两种格式之间的换算是高频操作。下面这段代码把 YOLO 一行标注转回像素坐标def yolo_to_pixel(img_w, img_h, line): # line 形如 0 0.5201 0.4812 0.1120 0.0945 parts list(map(float, line.strip().split())) class_id int(parts[0]) xc, yc, bw, bh parts[1:] x1 (xc - bw / 2) * img_w y1 (yc - bh / 2) * img_h x2 (xc bw / 2) * img_w y2 (yc bh / 2) * img_h return class_id, x1, y1, x2, y2注意 YOLO 的归一化是相对图片宽高求比值转回像素坐标必须拿到图片真实的宽高。如果标签生成时用了某个固定尺寸而后期图片被 resize 过归一化值仍然有效所以最好始终使用当前图片的实际宽高。换算结果通常不是整数在保存和绘制时不要立刻 int() 截断等最终画框再做一次类型转换否则框会偏移一到两个像素。2.3 类别分布统计看清哪些类是大头哪些类几乎没样本拿到标注以后我会先统计每个类别出现的次数。这个操作看起来简单却能暴露很多问题如果某个类只出现了十几次训练时大概率学不充分后面切分时需要特殊处理。对于 VOC 格式的 XML直接用标准库解析import xml.etree.ElementTree as ET from collections import Counter def parse_voc_xml(xml_path): # 解析 VOC XML 标注返回 (类别名, x1, y1, x2, y2) 列表 tree ET.parse(xml_path) root tree.getroot() boxes [] for obj in root.iter(object): name obj.findtext(name).strip().lower() bbox obj.find(bndbox) boxes.append(( name, float(bbox.findtext(xmin)), float(bbox.findtext(ymin)), float(bbox.findtext(xmax)), float(bbox.findtext(ymax)), )) return boxes all_boxes [] for xml_file in label_dir.glob(*.xml): all_boxes.extend(parse_voc_xml(xml_file)) name_counter Counter(box[0] for box in all_boxes) print(name_counter)如果标签是 YOLO 格式的 TXT里面只存类别编号没有类别名需要先读 classes.txt 把编号映射成名称。把上面的解析函数改成按行读取 TXT再用 yolo_to_pixel 换算逻辑是一样的。这里最要注意的不是统计结果而是类别名不一致问题有的标注写 Crack有的写 crack还有的写 裂缝。先确认 classes.txt 里有没有统一规范没统一就自己维护一张映射表在训练前把所有标签重写一遍。2.4 图像分辨率与缺陷尺度判断你有没有小目标问题管道缺陷在整幅图像里经常只占很小一块区域焊缝裂纹可能只有几十个像素宽。如果数据里大量标注框面积小于 32 乘 32后续检测模型的下采样倍数会直接影响这类目标的召回率。from PIL import Image small_box_count 0 total_box_count 0 for xml_file in label_dir.glob(*.xml): img_file img_dir / (xml_file.stem .jpg) if not img_file.exists(): img_file img_dir / (xml_file.stem .png) with Image.open(img_file) as im: img_w, img_h im.size for _, x1, y1, x2, y2 in parse_voc_xml(xml_file): total_box_count 1 if (x2 - x1) 32 or (y2 - y1) 32: small_box_count 1 print(small ratio:, small_box_count / max(total_box_count, 1))32 是一个经验阈值不是固定标准。下采样 32 倍的网络小于 32 像素的目标在最后一层特征图上只占一个点左右检测几乎不可能稳定。如果小目标比例高后面做增强时就要控制随机裁剪的范围避免把已经很小的缺陷裁掉一半。注意统计小目标比例时如果 total_box_count 为 0说明加载路径写错了。先回到 2.1 步确认图片和标签文件的对应关系再回来跑脚本。3. 切分训练集与验证集同源隔离与分层抽样数据切分看起来是小事实际是管道缺陷检测训练里最容易翻车的一环。很多项目组直接把图片随机打乱切分训完验证集指标不错部署到新管道上立刻失效问题根源往往不在模型结构而在数据切分阶段。3.1 为什么不能只做随机打乱管道缺陷图像经常来自一段段连续拍摄的视频同一根管道相邻帧的背景、光照和缺陷形态都非常相似。如果直接随机切分相邻帧很可能一个进训练集、一个进验证集。模型在验证集上看到的未知图片背景和缺陷形态与训练样本基本同源验证集失真模型泛化能力被高估。部署到新场景时光照条件、管道内壁材质、拍摄距离一变模型表现就会明显下降。所以切分之前先看文件名结构。如果文件名包含来源编号比如设备编号加帧号优先按设备维度切分保证同一设备或同一管段的数据要么全部在训练集要么全部在验证集而不是两边都有。没有这类元数据时至少把按序号连续的一段段图片看成同一个来源按段切。3.2 固定随机种子的基础划分脚本下面是一个最基础的随机划分脚本强调固定随机种子import random import math from pathlib import Path random.seed(42) # 固定种子保证每次切分结果一致 img_dir Path(pipe_defect_dataset/images) label_dir Path(pipe_defect_dataset/labels) img_files list(img_dir.glob(*.jpg)) list(img_dir.glob(*.png)) random.shuffle(img_files) val_ratio 0.2 val_num int(math.floor(len(img_files) * val_ratio)) val_files img_files[:val_num] train_files img_files[val_num:] def write_list(file_list, output_path): with open(output_path, w, encodingutf-8) as f: for p in file_list: f.write(str(p.resolve()) \n) write_list(train_files, train.txt) write_list(val_files, val.txt) print(ftrain: {len(train_files)}, val: {len(val_files)})随机种子固定成 42保证每次切分结果完全一致。目标检测训练周期长一旦发现数据划分有问题用相同种子重新划分才能和上一轮结果做对比也能让队友复现你的结果。val_ratio 设为 0.2900 张图大约 180 张进验证集够评估一类缺陷模型用如果类别很稀疏我会把验证集比例降到 0.15多留一点训练样本。resolve() 把相对路径转成绝对路径避免训练时因为当前工作目录变化而找不到文件。3.3 稀有类别要做的分层抽样如果统计发现某个缺陷类别只有一个巴掌数量的样本随机划分后这些框可能全部落在训练集或全部落在验证集导致验证指标失真。解决方法是先按类别对图片分组再从每组里分别取验证集。下面是按指定类别实行分层的脚本import xml.etree.ElementTree as ET def find_images_containing(label_dir, cls_name): result [] for label_path in label_dir.glob(*): if label_path.suffix .xml: tree ET.parse(label_path) if any(obj.findtext(name).strip().lower() cls_name for obj in tree.getroot().iter(object)): result.append(label_path) return result rare_class crack # 换成你统计出来的稀有类别名 rare_images find_images_containing(label_dir, rare_class) print(frare class images: {len(rare_images)})class 名要根据 2.3 的统计结果替换。找到包含稀有类别的图片后按一定比例从 rare_images 里单独抽出验证集再把这些图片排除出训练集其他类别仍然按随机划分流程走。这一步解决了常见问题稀有类别的小目标在验证集里一个都找不到评估指标看起来是 0其实不是模型没学好而是划分时把样本全漏了。3.4 划分之后先自查再开训练划分完成后不要急着启动训练。先检查训练集和验证集是否有文件名重复再统计验证集类别分布确认稀有类至少出现在验证集里。val_stems set(p.stem for p in val_files) train_stems set(p.stem for p in train_files) dup val_stems train_stems if dup: print(duplicate stems:, dup) else: print(no duplicate, ready to train)如果输出 duplicate stems 非空先排查原因可能是同一张图被复制到不同子目录也可能是图片重命名后产生了同名文件。出现重复不想重命名的话把重复文件直接从其中一个列表里删掉再做一次分布检查。这个动作虽然花不了两分钟但能避免训练和验证数据重叠带来的一系列隐性评估问题。4. 避坑管道缺陷检测里最常见的五个翻车点管道缺陷检测有自己的特殊性管壁纹理复杂、缺陷尺度小、类别名称容易混乱。这里列五个我实际遇到过的问题每一条都按 现象、原因、解决 三个层面拆开讲供你排查自己数据时对照。4.1 训练集 loss 掉得快验证集 mAP 长时间不动现象训练几百个 epoch训练集的分类和定位损失都明显下降验证集 mAP 几乎不变偶有抖动。原因最常见的并不是模型欠拟合而是训练集和验证集划分时发生了同源泄漏。同一段视频的相邻帧被同时分进训练集和验证集模型在验证集上看到的图像与训练样本极度相似训练损失降低并不代表真实泛化能力提升mAP 自然不随 loss 变化。解决回到第 3 章按视频片段或设备编号做分组切分让验证集里的图像来源和训练集完全不重叠。切分完成后再跑一遍训练通常半个 epoch 就能看到验证指标开始随训练节奏变化。4.2 验证集小目标缺陷几乎全部漏检现象验证时输出结果显示大的缺陷框能检出小于 40 像素的缺陷漏检率接近百分之百。原因模型下采样倍率对标注框尺寸不匹配。以 N 倍下采样为例小于 N 像素的小目标在最后一层特征图只占一个点特征表达严重不足。另一个常见原因是训练时的随机裁剪把部分小目标裁出画面模型见到的有效小目标样本更少了。解决先按 2.4 的脚本统计小目标比例。如果小目标确实多把训练输入分辨率提高或者调整训练阶段只对图片做等比例缩放不做随机裁剪再配合 Mosaic 增强让每个 batch 里都有小目标样本。如果检测锚框机制支持手工指定锚框尺寸把小尺寸锚框补进去。4.3 训练到一半报坐标越界训练进程直接崩溃现象模型加载数据时报错提示某个 bounding box 的 xmax 大于图片宽度或者出现负坐标训练在数据增强阶段直接中断。原因标签框坐标超出图像边界。这种情况在人工标注边缘缺陷时经常出现标注员把框画出画面另一种原因是格式转换时对 yolo_to_pixel 的结果进行 int() 截断导致坐标出现一两个像素偏移在数据增强旋转后越界被放大。解决在数据加载管线里加一道坐标裁剪逻辑。框坐标统一与图像边界做 clamp裁剪后如果框宽或框高小于 8 像素直接丢弃。注意这个滤波过程要放在增强之前避免增强变换把原本在边界的框推到更危险的位置。4.4 同一缺陷类别在不同图片里叫法不一致现象classes.txt 里定义只有三个类别但统计后发现标注里出现了 crack、Crack、裂缝、craze 等多个相似名称模型把同一个缺陷当成多个类别去学习。原因数据集可能是多人协作标注或者来源合并不同标注人员习惯不同有的写英文有的写中文有的加了下划线训练框架按类别名处理时全部当成独立类别。解决先把 2.3 的输出完整列出来人工确认哪些应该是同一类别写一个类别名映射字典重新生成标签文件。以 crack 为例把本质上同一类缺陷的名称统一成一个 ID再更新 classes.txt。这一步必须在训练前做不然后面还要反复调整模型输出层。4.5 模型把管壁纹理误识别成缺陷现象检测结果里频繁出现假阳性尤其是焊缝、管壁阴影、积水反光容易被识别成缺陷精确率上不去。原因训练样本里只包含了缺陷正样本缺乏没有缺陷但外观显著的背景负样本。模型没有见过这类纹理自然把特征接近的背景块当目标输出。解决管壁图像中如果存在大量不适合检测的背景帧比如没有标注任何缺陷的帧不要丢弃把这些无标注样本作为纯背景加入训练数据让模型学习这些负样本的特征。实际场景里我一般会准备一文件夹的背景帧在训练配置里单独指定负样本路径让 loss 在背景块上得到抑制。没有现成负样本时把产生误检的验证集误报块截取出来作为额外负样本加入下一轮训练。5. 增强与标注同步变换让 900 张图发挥更大价值900 张图对深度学习来说不算多但管道缺陷检测有个优势目标形态相对固定缺陷类别有限正样本特征较集中。只要增强策略控制得当900 张图可以撑住一版可用的检测模型。核心原则是增强操作不能改变缺陷类别的本体特征尤其是裂纹的方向和纹理细节。5.1 哪些增强适合管道内壁图适合管道内壁图的增强包括小角度旋转、水平翻转、亮度对比度扰动、轻度模糊和灰度化。水平翻转对管道检测几乎无副作用垂直翻转要谨慎管道内壁的上下方向经常对应真实重力方向积水、淤积类缺陷会因为垂直翻转丢失先验信息。大角度旋转要控制边界比如 ±90 度旋转会让裂纹方向变成随机方向模型学到的是方向无关的特征如果实际场景中裂纹方向与管轴存在固定关系大角度旋转反而损害泛化。随机裁剪也要保守管道图像里缺陷往往只占很小区域过度裁剪容易把目标中心裁掉导致增强后样本标签丢失。5.2 色彩增强脚本与参数边界管道内壁光照变化是实际场景中最大的干扰来源所以色彩增强非常有效。用 HSV 空间做亮度饱和度扰动比较安全import cv2 import numpy as np def hsv_augment(img, hue_shift5, sat_scale1.1, val_scale1.1): # 输入 BGR 图像输出增强后的 BGR 图像 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV).astype(np.float32) hsv[..., 0] (hsv[..., 0] hue_shift) % 180 hsv[..., 1] np.clip(hsv[..., 1] * sat_scale, 0, 255) hsv[..., 2] np.clip(hsv[..., 2] * val_scale, 0, 255) return cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)hue_shift 控制色调偏移管道内壁主色调偏灰黄偏移量超过 10 会让颜色失真所以我一般控制在 5 以内。sat_scale 和 val_scale 分别控制饱和度和亮度系数1.1 表示原始值乘以 1.1超过 1.3 后管壁纹理细节会丢。增强函数只处理图像不与标注框发生交互可以离线批量生成样本也可以做成在线变换。5.3 几何增强旋转时同步修改标注框几何增强最大的坑是只旋转了图片没有同步更新标注框。下面这个函数用 OpenCV 的旋转矩阵同时处理图片和框def rotate_image_and_boxes(img, boxes, angle): # boxes: [(x1, y1, x2, y2), ...]像素坐标 h, w img.shape[:2] matrix cv2.getRotationMatrix2D((w / 2, h / 2), angle, 1.0) rotated cv2.warpAffine(img, matrix, (w, h), borderModecv2.BORDER_REFLECT) new_boxes [] for x1, y1, x2, y2 in boxes: corners np.array( [[x1, y1], [x2, y1], [x2, y2], [x1, y2]], dtypenp.float32, ).reshape(-1, 1, 2) rotated_corners cv2.transform(corners, matrix).reshape(4, 2) rx1 max(0, rotated_corners[:, 0].min()) ry1 max(0, rotated_corners[:, 1].min()) rx2 min(w, rotated_corners[:, 0].max()) ry2 min(h, rotated_corners[:, 1].max()) if rx2 - rx1 8 or ry2 - ry1 8: continue # 旋转后框太小丢弃 new_boxes.append((rx1, ry1, rx2, ry2)) return rotated, new_boxes边框模式用的 BORDER_REFLECT让旋转后边缘区域从相邻像素镜像填充避免纯黑边框影响训练。旋转角度建议在正负 15 度之间。旋转后外接框必然比原来大框尺寸过滤阈值设为 8 像素太小的框在增强后已经丧失标注价值。如果数据是 YOLO 归一化格式旋转完成后再把新框坐标除以新的图像宽高。5.4 增强样本抽检与存档建议增强不是生成完就结束。离线增强时我习惯随机抽 5 到 10 张图片把增强后的图片和标签框叠加绘制出来肉眼确认框是否贴合缺陷。重点看旋转后框是否偏移、裁剪后缺陷是否完整、色相增强是否让缺陷边缘模糊。在线增强则建议在训练配置里固定一组增强参数不要每个 epoch 都改。把增强参数写在配置文件里训练中断重开时保持一致否则前一轮的结果和后一轮完全不可比。不要一次把所有增强手段都拉满管道缺陷检测对纹理敏感增得过猛会把缺陷特征洗掉模型收敛更慢而不是更快。6. 把第一轮预测结果画出来用错误样本反推数据集问题第一轮训练完成后不要只看 mAP 数字把预测框和真实框画到同一张图上逐张看错在哪。这个环节比调参更能帮助你定位问题。6.1 叠加绘制预测框和真实框下面这个函数把预测框和标注框画在同一张图上import cv2 def draw_pred_vs_true(image_path, pred_boxes, true_boxes, save_path): # pred_boxes: [(x1, y1, x2, y2, score, cls_name)] # true_boxes: [(x1, y1, x2, y2, cls_name)] img cv2.imread(image_path) for x1, y1, x2, y2, score, cls_name in pred_boxes: cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2) cv2.putText(img, f{cls_name}:{score:.2f}, (int(x1), max(0, int(y1) - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) for x1, y1, x2, y2, cls_name in true_boxes: cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(img, cls_name, (int(x1), min(img.shape[0], int(y2) 18)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(save_path, img)红色是预测框绿色是真实框。重点看三类错误预测框和真实框完全错位说明目标边界模糊或标注偏移预测框有高置信度但真实框不存在说明背景误检真实框存在但预测框置信度很低说明这类目标特征没有学好。6.2 按缺陷类型归类错误决定下一轮动作把错误样本按缺陷类别归堆以后处理方向就清晰了。如果错误集中在某一类先怀疑这类目标标注质量如果错误集中在小目标提高输入分辨率或增加小目标增强如果误检集中在管壁纹理区域按第 4.5 条补负样本。每轮只改一个变量改完再训练对比同一组验证图片的变化。从那以后我每次拿到新的检测数据集都强制先做一遍格式统计、分布统计、切分自查再训练一轮并可视化错误样本走通这条流程才敢把模型往场景里部署。这个习惯帮我少踩了不知道多少坑也让我对每份资源的真实边界心里有数。希望帮到你。本文还有配套的精品资源点击获取