简介:本资源面向电力巡检与计算机视觉方向的学习者与开发者,提供一套真实场景下的绝缘子缺陷检测数据集,可直接用于YOLO系列目标检测模型的训练与验证。数据经labelimg精细标注,标注框质量较高,覆盖多种拍摄场景,适合课程设计、科研实验与工程原型验证。压缩包共2000个文件,约57.16MB,包含1000张图片及对应的xml、txt标签文件,并附有html教程、py划分脚本与yaml配置文件,voc、coco、yolo三种格式标签分目录存放,开箱即用。资源另附YOLO环境搭建与训练案例教程,以及训练集、验证集、测试集划分脚本,可按需自行切分数据。目前已有853人学习下载,能帮助读者快速跑通数据准备到模型训练的完整流程,减少标注与格式转换的重复劳动。
1. 电力绝缘子缺陷检测:1000 张图的数据集到底能不能撑起一个 YOLO 项目
输电线路巡检航拍回来的图里,绝缘子破损、自爆、掉串这些缺陷,肉眼在几百张图里翻一遍能看瞎。很多人第一反应是找个开源数据集,直接上 YOLO 训练,跑出一个能自动框缺陷的模型。这个标题给的东西正好卡在这个需求上:1000 张电力绝缘子图片,配 VOC、COCO、YOLO 三种格式标签,外加划分脚本和训练教程。听起来是一条龙,但真正动手前得先想清楚一件事——1000 张图在目标检测里属于小样本,绝缘子缺陷又存在类间不平衡和背景干扰,能不能训出可用模型,取决于你怎么划分、怎么转格式、怎么设参数。这篇笔记就按我实际做这类项目的顺序,把数据集结构、格式转换、划分脚本、训练配置和踩过的坑讲一遍,适合刚拿到这批数据准备跑 YOLOv8 或 YOLOv11 的人,也适合想评估这个方向值不值得投入的同行。
2. 先看清数据集的底子:1000 张图、三种标签格式意味着什么
2.1 绝缘子缺陷检测的任务定义与类别分布
电力绝缘子缺陷检测在目标检测里属于细粒度工业缺陷检测。常见缺陷类型包括绝缘子自爆(玻璃绝缘子伞裙破裂)、破损、掉串、异物悬挂。和通用 COCO 数据集里猫狗车人不同,绝缘子缺陷的类间差异小,自爆和正常伞裙在低分辨率下几乎一样,模型很容易把正常绝缘子误报成缺陷。1000 张图如果按 8:1:1 划分,训练集只有 800 张,验证集和测试集各 100 张。这个量级下,类别分布比总数更关键。假设自爆样本 300 张、破损 200 张、正常绝缘子 500 张,那训练时正常类会主导损失,缺陷类召回率上不去。
我一般拿到数据集先做一件事:统计每个类别的实例数和图片数。不是看文件夹里有多少张图,而是解析标签文件,数每个类别的框数量。这一步能直接决定后面要不要做重采样或加权重。
import os from collections import Counter # 假设 YOLO 格式标签目录,每行格式:class_id x_center y_center w h label_dir = "labels/train" class_names = ["insulator", "broken", "flashover", "normal"] # 按你的 data.yaml 顺序 counter = Counter() for txt in os.listdir(label_dir): if not txt.endswith(".txt"): continue with open(os.path.join(label_dir, txt)) as f: for line in f: parts = line.strip().split() if len(parts) >= 5: counter[int(parts[0])] += 1 for cid, cnt in sorted(counter.items()): print(f"{class_names[cid]}: {cnt} instances")这段代码遍历 YOLO 格式标签,统计每个类别的实例总数。参数上注意class_names的顺序必须和data.yaml里的names完全一致,否则统计结果会错位。如果发现某个类别实例数不到总数的 5%,训练时就要考虑用copy_paste增强或者设置类别权重。1000 张图里如果缺陷实例只有一两百个,直接训 YOLOv8n 大概率会出现漏检,这时候要么换更小的模型加更强增强,要么接受一个偏保守的置信度阈值。
2.2 VOC、COCO、YOLO 三种格式的差异与选用场景
VOC 格式用 XML 存框,坐标是左上角和右下角的绝对像素值,适合用 LabelImg 标注和做数据交换。COCO 格式用 JSON 存,坐标是 [x, y, width, height] 绝对像素,还带iscrowd和area字段,适合跑 MMDetection 或做 COCO 预训练权重迁移。YOLO 格式用 TXT 存,坐标是归一化的中心点和宽高,直接喂给 Ultralytics 的 YOLO 训练脚本。
三种格式同时给,好处是你不用自己写转换,坏处是容易搞混。我见过有人把 COCO 的 JSON 直接改后缀当 YOLO 标签用,训练 loss 不降,排查半天才发现坐标没归一化。正确做法是:训练 YOLO 就用 YOLO 格式,做格式转换验证时用 VOC 或 COCO 交叉检查。比如从 VOC 转 YOLO 后,随机抽 10 张图用脚本把框画出来,和原图对比,确认没有偏移或镜像。
| 格式 | 坐标类型 | 单文件组织 | 典型工具 | 适用场景 |
|---|---|---|---|---|
| VOC | 绝对像素 xmin ymin xmax ymax | 每图一个 XML | LabelImg | 标注交换、转其他格式 |
| COCO | 绝对像素 x y w h | 全量一个 JSON | CVAT、MMDetection | 预训练权重迁移、多任务 |
| YOLO | 归一化 cx cy w h | 每图一个 TXT | Ultralytics | 直接训练 YOLOv5/v8/v11 |
选型上,如果你只是训一个 YOLO 模型做绝缘子缺陷检测,直接用 YOLO 格式最省事。但如果你打算对比不同框架的效果,比如用 MMDetection 跑 RTMDet 或 DINO,那就需要 COCO 格式。VOC 格式在这批数据里更多是作为中间态,方便你用 LabelImg 复查标注质量。
2.3 划分脚本怎么用:别让随机种子毁掉你的验证集
标题里提到划分脚本,这是最容易被忽视但影响最大的环节。很多人直接random.shuffle然后按 8:1:1 切,结果验证集里全是正常绝缘子,缺陷样本一个没有,训出来的模型在测试集上缺陷召回率惨不忍睹。正确做法是按类别分层抽样,保证每个子集里缺陷和正常的比例接近。
import os import random import shutil from collections import defaultdict random.seed(42) # 固定种子,保证可复现 img_dir = "images" label_dir = "labels" out_dir = "dataset_split" train_ratio, val_ratio = 0.8, 0.1 # 按图片是否包含缺陷分层 def has_defect(label_path): with open(label_path) as f: for line in f: if int(line.split()[0]) != 0: # 假设 0 是正常绝缘子 return True return False files = [f for f in os.listdir(img_dir) if f.endswith((".jpg", ".png"))] defect_files = [f for f in files if has_defect(os.path.join(label_dir, f.replace(".jpg", ".txt").replace(".png", ".txt")))] normal_files = [f for f in files if f not in defect_files] for group in [defect_files, normal_files]: random.shuffle(group) n_train = int(len(group) * train_ratio) n_val = int(len(group) * val_ratio) splits = { "train": group[:n_train], "val": group[n_train:n_train + n_val], "test": group[n_train + n_val:] } for split, items in splits.items(): os.makedirs(os.path.join(out_dir, split, "images"), exist_ok=True) os.makedirs(os.path.join(out_dir, split, "labels"), exist_ok=True) for item in items: shutil.copy(os.path.join(img_dir, item), os.path.join(out_dir, split, "images", item)) lbl = item.replace(".jpg", ".txt").replace(".png", ".txt") shutil.copy(os.path.join(label_dir, lbl), os.path.join(out_dir, split, "labels", lbl))这段脚本的核心是has_defect函数和分层逻辑。random.seed(42)保证每次运行划分结果一致,方便复现实验。train_ratio和val_ratio按需调整,1000 张图建议 8:1:1,如果缺陷样本特别少,可以改成 7:1.5:1.5,让验证集和测试集多分一点缺陷图。运行后检查dataset_split下每个子集的图片数和标签数是否一一对应,缺标签的图要单独处理,不能直接丢,否则训练时 Ultralytics 会报No labels found。
3. 从 VOC 和 COCO 转到 YOLO:转换脚本与四个边界坑
3.1 VOC 转 YOLO 的完整脚本与坐标归一化
VOC 的 XML 里size字段给出图片宽高,object里bndbox给绝对坐标。转 YOLO 需要做三件事:读 XML、算归一化中心点和宽高、按类别映射写 TXT。下面这个脚本处理了图片和标签不在同一目录的情况,也处理了difficult字段。
import os import xml.etree.ElementTree as ET voc_dir = "VOC/Annotations" img_dir = "VOC/JPEGImages" out_label_dir = "labels_voc2yolo" class_map = {"insulator": 0, "broken": 1, "flashover": 2, "normal": 3} os.makedirs(out_label_dir, exist_ok=True) for xml_file in os.listdir(voc_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(voc_dir, xml_file)) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_map: continue difficult = obj.find("difficult") if difficult is not None and int(difficult.text) == 1: continue # 跳过难以识别的目标 bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 裁剪到图片边界,防止标注越界 xmin, ymin = max(0, xmin), max(0, ymin) xmax, ymax = min(w, xmax), min(h, ymax) cx = (xmin + xmax) / 2.0 / w cy = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{class_map[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") if lines: with open(os.path.join(out_label_dir, xml_file.replace(".xml", ".txt")), "w") as f: f.write("\n".join(lines))关键点在xmin, ymin = max(0, xmin), max(0, ymin)和xmax, ymax = min(w, xmax), min(h, ymax)。绝缘子标注时经常有人把框拉到图片外面,不裁剪的话归一化后坐标会小于 0 或大于 1,YOLO 训练时虽然不报错,但实际框会偏移。difficult字段为 1 的目标建议跳过,这些通常是遮挡严重或模糊的样本,留着会拉低模型精度。class_map必须和data.yaml一致,顺序错了类别就全乱。
3.2 COCO 转 YOLO 时容易忽略的 iscrowd 和面积过滤
COCO 的 JSON 里annotations数组每个元素有bbox、category_id、iscrowd、area。转 YOLO 时iscrowd=1的框要跳过,因为 YOLO 不支持 crowd 区域。另外area太小的框(比如小于 10 像素)也要过滤,绝缘子缺陷检测里这种小框多半是标注噪声。
import json import os coco_json = "annotations/instances_train.json" img_dir = "train2017" out_label_dir = "labels_coco2yolo" min_area = 10 # 最小面积阈值 with open(coco_json) as f: data = json.load(f) # 建立 image_id 到文件名的映射 img_id2name = {img["id"]: img["file_name"] for img in data["images"]} # category_id 从 1 开始,YOLO 从 0 开始 cat_id2yolo = {cat["id"]: i for i, cat in enumerate(data["categories"])} os.makedirs(out_label_dir, exist_ok=True) img2lines = {} for ann in data["annotations"]: if ann.get("iscrowd", 0) == 1: continue if ann["area"] < min_area: continue img_info = next(img for img in data["images"] if img["id"] == ann["image_id"]) w, h = img_info["width"], img_info["height"] x, y, bw, bh = ann["bbox"] cx = (x + bw / 2) / w cy = (y + bh / 2) / h nw = bw / w nh = bh / h yolo_id = cat_id2yolo[ann["category_id"]] img2lines.setdefault(ann["image_id"], []).append(f"{yolo_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") for img_id, lines in img2lines.items(): name = img_id2name[img_id].replace(".jpg", ".txt") with open(os.path.join(out_label_dir, name), "w") as f: f.write("\n".join(lines))cat_id2yolo用enumerate把 COCO 的 category_id 映射成从 0 开始的连续整数,这一步不能省,因为 COCO 的 category_id 可能不连续。min_area设 10 是经验值,绝缘子缺陷如果特别小,可以降到 5,但再低就容易把噪声框进来。转换完随机抽几张图用cv2.rectangle画框验证,确认坐标没反。
3.3 转换后必须做的三项校验
转换脚本跑完不代表数据就能用。我一般做三项校验:第一,统计转换前后每个类别的实例数,差异超过 5% 就要查;第二,随机抽 20 张图,用 OpenCV 把 YOLO 标签画出来,和原图对比;第三,检查有没有空标签文件,空文件在训练时会被 Ultralytics 当成负样本,如果图片里确实没目标可以留,如果是转换丢了就要补。
import cv2 import os import random img_dir = "images/train" label_dir = "labels/train" sample_imgs = random.sample(os.listdir(img_dir), 20) for img_name in sample_imgs: img = cv2.imread(os.path.join(img_dir, img_name)) h, w = img.shape[:2] lbl_path = os.path.join(label_dir, img_name.replace(".jpg", ".txt")) if not os.path.exists(lbl_path): print(f"missing label: {img_name}") continue with open(lbl_path) as f: for line in f: cid, cx, cy, bw, bh = map(float, line.split()) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(f"check_{img_name}", img)画出来的框如果明显偏左上或偏右下,多半是归一化时用了错误的宽高,比如把 COCO 的width和height搞反。这个校验步骤花不了几分钟,但能省掉后面几小时的排查。
4. 用这批数据训 YOLO:配置、参数和收敛判断
4.1 data.yaml 与训练命令的最小配置
Ultralytics 的 YOLO 训练入口是yolo detect train,核心配置文件是data.yaml。1000 张图的数据集,data.yaml里train、val、test指向划分后的目录,names按类别顺序写。
path: ./dataset_split train: train/images val: val/images test: test/images nc: 4 names: 0: insulator 1: broken 2: flashover 3: normal训练命令用 YOLOv8n 或 YOLOv11n 起步,因为数据量小,大模型容易过拟合。
yolo detect train \ data=dataset_split/data.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=30 \ augment=True \ mosaic=1.0 \ mixup=0.1 \ project=runs/insulator \ name=exp1epochs=150配合patience=30,如果 30 轮验证集 mAP 不升就早停。imgsz=640是 YOLO 默认,绝缘子缺陷如果很小可以提到 1280,但显存要够。batch=16在 8G 显存上跑 YOLOv8n 没问题,显存小就降到 8。mosaic=1.0和mixup=0.1对小数据集很关键,能显著提升泛化,但如果缺陷样本特别少,mixup可能把缺陷特征混没,可以降到 0.05 或关掉。
4.2 小样本下的学习率与增强参数怎么调
1000 张图训练时,lr0=0.01是 SGD 的常见起点,但如果用 AdamW,建议降到 0.001。判断学习率是否合适看训练日志里的box_loss和cls_loss:如果前 10 轮 loss 震荡不降,学习率偏大;如果 loss 降得极慢,学习率偏小。我一般先跑 20 轮看趋势,再决定要不要调。
增强参数里mosaic对小数据集帮助最大,它把 4 张图拼成一张,等效增加了样本多样性。但mosaic会让目标变小,如果绝缘子缺陷本身就不大,mosaic后可能小到模型学不到,这时候把mosaic从 1.0 降到 0.5,或者用close_mosaic=10在最后 10 轮关掉。hsv_h、hsv_s、hsv_v控制颜色抖动,航拍图光照变化大,可以适当调高hsv_v到 0.5。degrees旋转增强对绝缘子有用,因为航拍角度多变,但degrees=180会把绝缘子转成倒的,可能引入噪声,一般设 10 到 15。
4.3 看什么指标判断模型能不能用
训练完看results.csv里的metrics/mAP50和metrics/mAP50-95。绝缘子缺陷检测里,mAP50到 0.85 以上算可用,mAP50-95到 0.5 以上算不错。但别只看总体 mAP,要分看每个类别的metrics/mAP50。如果broken类 mAP 只有 0.3,而normal类 0.95,说明模型偏向正常类,需要检查缺陷样本数量或加类别权重。
验证集上的val/box_loss如果持续上升而train/box_loss下降,是过拟合信号,这时候要么加数据增强,要么减模型复杂度,要么早停。测试集跑一次yolo detect val,看混淆矩阵,如果正常绝缘子被大量误报成缺陷,调高置信度阈值到 0.5 或 0.6,牺牲一点召回换精度。
5. 避坑与排查:绝缘子数据集训练中最容易翻车的 5 个点
5.1 标签类别顺序和 data.yaml 不一致
现象:训练 loss 正常下降,但验证时框的位置对,类别全错,比如自爆被标成正常。原因:YOLO 标签里的class_id是整数,data.yaml的names顺序决定这个整数对应哪个类。如果转换脚本里class_map和data.yaml不一致,模型学到的就是错位映射。解决:转换前先定死data.yaml的names顺序,所有转换脚本的class_map按这个顺序写,转换后抽一张图确认类别 ID 和名称对应。
5.2 图片和标签文件名不匹配
现象:训练时报No labels found in ...,或者某些图被跳过。原因:图片是.jpg,标签是.txt,但文件名大小写不一致,或者图片有.JPG和.jpg混用。解决:统一转成小写后缀,用脚本批量重命名。另外注意 Ultralytics 默认按文件名匹配,图片和标签必须在对应目录下同名。
5.3 验证集里没有缺陷样本
现象:训练时val/box_loss很低,但测试集上缺陷召回率极低。原因:随机划分时缺陷样本全进了训练集,验证集只有正常绝缘子,模型在验证集上表现好是假象。解决:用分层抽样,按是否包含缺陷划分,保证验证集和测试集里缺陷样本占比和总体一致。划分后统计每个子集的类别实例数,差异大就重新划。
5.4 图片尺寸不一致导致训练报错
现象:训练启动时报RuntimeError: shape mismatch或Caught Exception: Image size ...。原因:数据集里混了不同分辨率的图,YOLO 默认会 resize 到imgsz,但如果某张图特别小,resize 后目标几乎消失。解决:训练前用脚本统计图片宽高,把小于 320 像素的图筛出来单独处理,或者统一 resize 到 640 再训。Ultralytics 的imgsz参数会做 letterbox,但极端长宽比仍可能出问题。
5.5 显存不足导致 batch 被迫调小
现象:batch=16时报CUDA out of memory。原因:YOLOv8n 在 640 分辨率下 batch=16 大约占 4G 显存,但如果开了mosaic和mixup,显存会涨。解决:先降batch到 8,再不行降到 4,同时开amp=True混合精度。如果还不行,把imgsz从 640 降到 512,但绝缘子缺陷小的话不建议降太多。
6. 把 1000 张图用出 5000 张的效果:离线增强与交叉验证的实操技巧
数据量小的时候,在线增强不够用,我一般会做离线增强扩充训练集。具体做法是对训练集里的缺陷图片做旋转、翻转、亮度调整,生成额外样本,但验证集和测试集绝对不动。旋转角度选 90、180、270 和 ±15 度,翻转用水平和垂直,亮度调整 ±30%。每张缺陷图生成 3 到 4 个变体,1000 张里如果有 300 张缺陷图,能扩到 1200 张左右,加上正常图,训练集能到 2000 张上下。
import cv2 import os import numpy as np src_img_dir = "dataset_split/train/images" src_lbl_dir = "dataset_split/train/labels" aug_img_dir = "dataset_split/train/images_aug" aug_lbl_dir = "dataset_split/train/labels_aug" os.makedirs(aug_img_dir, exist_ok=True) os.makedirs(aug_lbl_dir, exist_ok=True) def augment(img, labels, angle, flip): h, w = img.shape[:2] if flip == "h": img = cv2.flip(img, 1) labels = [(c, 1 - cx, cy, bw, bh) for c, cx, cy, bw, bh in labels] elif flip == "v": img = cv2.flip(img, 0) labels = [(c, cx, 1 - cy, bw, bh) for c, cx, cy, bw, bh in labels] if angle != 0: M = cv2.getRotationMatrix2D((w / 2, h / 2), angle, 1.0) img = cv2.warpAffine(img, M, (w, h)) # 旋转后标签需要重新计算,这里简化处理,只保留中心点变换 new_labels = [] for c, cx, cy, bw, bh in labels: x, y = cx * w, cy * h nx = M[0, 0] * x + M[0, 1] * y + M[0, 2] ny = M[1, 0] * x + M[1, 1] * y + M[1, 2] if 0 < nx < w and 0 < ny < h: new_labels.append((c, nx / w, ny / h, bw, bh)) labels = new_labels return img, labels for img_name in os.listdir(src_img_dir): if not img_name.endswith(".jpg"): continue lbl_path = os.path.join(src_lbl_dir, img_name.replace(".jpg", ".txt")) if not os.path.exists(lbl_path): continue with open(lbl_path) as f: labels = [tuple(map(float, line.split())) for line in f if line.strip()] # 只增强包含缺陷的图 if all(int(c) == 0 for c, *_ in labels): continue img = cv2.imread(os.path.join(src_img_dir, img_name)) for angle in [90, 180, 270]: for flip in ["h", "v"]: aug_img, aug_labels = augment(img.copy(), labels.copy(), angle, flip) if not aug_labels: continue base = img_name.replace(".jpg", f"_a{angle}_{flip}") cv2.imwrite(os.path.join(aug_img_dir, base + ".jpg"), aug_img) with open(os.path.join(aug_lbl_dir, base + ".txt"), "w") as f: for c, cx, cy, bw, bh in aug_labels: f.write(f"{int(c)} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n")这段脚本只对包含缺陷的图做增强,正常绝缘子不扩,避免正常类过度主导。旋转后标签用旋转矩阵重新算中心点,宽高保持不变,这是简化处理,严格来说旋转后宽高会变,但小角度下误差可接受。增强完把images_aug和labels_aug合并进训练集,重新跑训练。注意增强后的图要和原图一起参与划分,不能只增强训练集后再划验证集,否则验证集里会有增强样本,指标虚高。
另一个技巧是交叉验证。1000 张图做 5 折交叉验证,每折 800 张训练、200 张验证,跑 5 次取平均 mAP。这样能更稳地评估模型性能,也能在数据量少时充分利用每张图。Ultralytics 不直接支持 K 折,需要自己写循环改data.yaml的train和val路径。我一般跑 3 折,因为 5 折训练时间太长,3 折已经能看出模型稳不稳定。
最后说一个我自己的习惯:每次训完模型,不管指标多好,都会拿测试集里最难的 10 张图单独跑一遍,人眼看框。指标是给论文和汇报用的,实际能不能用,得看那些模糊、遮挡、小目标的图有没有漏。绝缘子缺陷检测里,漏检一个自爆可能比误报十个正常更严重,所以召回率优先于精度。希望帮到你。
本文还有配套的精品资源,点击获取