
简介变电站及输电线路异物检测图像数据集面向目标检测研究与电力智能巡检应用开发者聚焦变电站与输电线路场景下的异物如鸟类、树叶、塑料袋等识别与定位问题。资源共包含336个文件其中168张JPG原始图像配168个VOC格式XML标注文件包体大小18.29MB图像与标注一一对应可直接用于训练和验证Faster R-CNN、YOLO、EfficientDet等主流检测模型也可作为数据增强与模型评估的练习素材。数据面向真实变电站与输电线路场景标注内容包含目标类别和边界框坐标有助于使用者理解VOC标注规范并通过调整预处理与超参数来优化检测精度。VOC标签可直接转换为YOLO、COCO等检测框架所需格式方便在统一流程中对比不同模型效果。已有3448人学习适合计算机视觉初学者快速上手异物检测项目也可供电力行业工程师开展巡检算法验证与实时监测系统集成测试。1. 变电站及输电线路异物检测图像数据集168张VOC标注图能解决什么问题夏季雷雨前的大风天风筝线缠上高压输电线塑料布兜上绝缘子串鸟巢搭在铁塔横担拐角。这些异物不会等你有上万张标注样本才出现电力巡检的真实情况恰恰相反——故障库里正常巡检照片堆成山真正带异物的标注图像往往只有一两百张。这份变电站及输电线路异物检测图像数据集就是给这种“样本极少但场景真实”的尴尬局面准备的168张真实场景图像覆盖变电站设备与架空输电线路标注文件统一采用VOC格式每个目标对象都写好了类别名与边界框坐标。适合两类人做电力视觉巡检落地的工程师拿它做迁移学习基线、盲测集或标注规范参考刚入门目标检测的学生用这一份小数据集完整走通“训练-评估-可视化”流程比在公开大模型上泛泛调参更能看清问题在哪儿。2. 看懂VOC标注再谈训练XML字段、目录结构与质量盘点拿到这份数据集的第一步不是急着训练而是先把VOC标签吃透。VOCPascal VOC格式是最经典的检测标注格式每张图像对应一个同名XML文件里面记录图像尺寸和所有目标框的类别与坐标。别把标注文件当黑匣子直接拖进编辑器看一眼字段结构、类别命名习惯、坐标是否规整全都写在明面上。真实场景的小样本数据集标注质量直接决定模型上限数据里有一张错框、一个类别名手滑后面都会在训练指标上暴露出来。2.1 一张VOC XML能读到什么以数据集里常见的标注结构为例一个XML文件的核心内容大致长这样我用注释标出了每个字段的作用annotation folderJPEGImages/folder filenameIMG_0042.jpg/filename size width1280/width height720/height depth3/depth /size object namekite/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin312/xmin ymin98/ymin xmax564/xmax ymax286/ymax /bndbox /object !-- 同一张图里有多个异物就重复出现多个 object 块 -- /annotationPascal VOC 官方那套格式原本定义了20个竞赛类别但这份异物检测数据集的类别名是标注人员根据电力巡检场景自定的常见可能包括风筝、漂浮物、塑料布、鸟巢、施工机械等具体以你解压后XML里实际出现的name为准不要套用VOC的20类。bndbox里四个坐标是像素绝对值左上角为原点xmin/ymin是框左上角xmax/ymax是右下角。训练框架读取时直接拿这四个值做后续归一化但要注意坐标越界、宽高为0的退化框在真实标注里并不罕见。2.2 目录结构与自查清单先对照再动手VOC标准数据集通常按下面这个目录组织这份资源如果按常规打包结构应该类似├── JPEGImages/ # 168张原始图像 │ ├── IMG_0042.jpg │ └── ... ├── Annotations/ # 每张图对应的同名XML标注 │ ├── IMG_0042.xml │ └── ... └── ImageSets/ └── Main/ # 可选train.txt / val.txt 文件名清单动手前先跑一遍自查确认三件事图像和XML是否一一对应、类别名集合是什么、是否已经自带划分文件。用一段bash就能快速摸底# 统计图像和XML数量 ls JPEGImages | wc -l ls Annotations | wc -l # 提取所有XML里出现的类别名去重后打印 grep -h name Annotations/*.xml | sort | uniq -c # 查看是否已经带了数据划分文件 ls -R ImageSets 2/dev/null || echo 没有自带 train/val 划分需要自己拆分第一个命令对比两个数量正常情况应该一致。第二个命令特别重要它能直接告诉你这份数据集标注了哪些异物类别以及每个类别有多少目标实例。如果有某个类别只出现两三次后面拆分数据集时就要重点关照否则随机一拆这个类可能全部掉进验证集。第三个命令决定你是否要跳过下一步的拆分工作。2.3 用脚本盘点标注质量缺漏、越界与退化框我一般会在训练前跑一个标注质量盘点脚本遍历所有XML统计每类目标数量同时检查坐标越界和宽高为零的退化框。这个脚本不挑框架Python标准库就能跑import os import xml.etree.ElementTree as ET ann_dir Annotations cls_count {} # 类别 - 目标实例数 bad_xmls [] # 有问题的XML文件名 for xml_file in sorted(os.listdir(ann_dir)): if not xml_file.endswith(.xml): continue path os.path.join(ann_dir, xml_file) tree ET.parse(path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) has_bad False for obj in root.iter(object): name obj.find(name).text cls_count[name] cls_count.get(name, 0) 1 box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) w x2 - x1 h y2 - y1 # 坐标越界标注时手滑超出图像边界 if x1 0 or y1 0 or x2 img_w or y2 img_h: print(f[越界] {xml_file}: {name} 框超出图像范围) has_bad True # 退化框宽或高为0说明标注了一半没标完 if w 0 or h 0: print(f[退化框] {xml_file}: {name} 宽高异常 w{w} h{h}) has_bad True if has_bad: bad_xmls.append(xml_file) print(类别统计:, cls_count) print(问题XML数量:, len(bad_xmls))这段脚本的逻辑是逐个解析XML读取图像尺寸和目标框坐标然后做两类检查。越界框会直接干扰归一化原本应该落在0到1之间的中心点坐标可能超出范围退化框更危险归一化后宽高变成0或负值训练时损失函数直接报NaN。168张图不算多跑一次全量检查几秒钟就出结果。看到类别统计之后记得把类别名原样复制存到一个文本里后面写YOLO的data.yaml要用手打很容易把下划线和缩写打错。3. 从VOC到训练集固定种子拆分与YOLO格式转换一次跑通VOC格式最大的麻烦是主流训练框架不能直接吃YOLO系、MMDetection系都要求把标签转成自己的格式。这一步不复杂但细节坑很多。本章按“先拆分、再转换、后校验”的顺序走每一步都附脚本和参数说明跟着跑就能把这份数据集变成可训练的YOLO格式。3.1 固定随机种子做train/val拆分数据集没有自带ImageSets/Main划分时需要自己拆。小数据集最怕随机拆分把某个稀有类别全分到测试集里比如全部168张里只有2张含鸟巢随机一切两张都进了val训练时模型从来没见过鸟巢验证时又要求它检出鸟巢——这等于白训。所以我的做法是按类别保底分配稀有类别优先保证至少一张进val其余样本按比例随机分。import os import random import xml.etree.ElementTree as ET from collections import defaultdict # 固定随机种子同一份数据每次拆分结果可复现 random.seed(2025) ann_dir Annotations cls_to_imgs defaultdict(set) # 类别 - 包含该类的图像文件名集合 for xml_file in os.listdir(ann_dir): if not xml_file.endswith(.xml): continue root ET.parse(os.path.join(ann_dir, xml_file)).getroot() img_name root.find(filename).text for obj in root.iter(object): cls_to_imgs[obj.find(name).text].add(img_name) val_imgs set() for cls, img_set in cls_to_imgs.items(): candidates sorted(img_set) # 排序保证顺序稳定 random.shuffle(candidates) keep max(1, len(candidates) // 5) # 每个类别至少保底1张进val val_imgs.update(candidates[:keep]) all_imgs {f.replace(.xml, ) for f in os.listdir(ann_dir) if f.endswith(.xml)} train_imgs all_imgs - val_imgs # 写入ImageSets/Main下的txt每行一个不带扩展名的文件名 os.makedirs(ImageSets/Main, exist_okTrue) with open(ImageSets/Main/train.txt, w) as f: f.write(\n.join(sorted(train_imgs))) with open(ImageSets/Main/val.txt, w) as f: f.write(\n.join(sorted(val_imgs)))这里有几个关键选择。random.seed(2025)保证了每次跑脚本得到完全一样的划分后续调模型参数时可以排除“数据划分不同”这个变量。按类别保底的逻辑是先找稀有类别让它优先进入val保证验证集能覆盖所有类别其他样本再随机分配val总量大约占两成。最后写文件时只写不带扩展名的文件名这是YOLO和大多数检测框架的惯例读取时再拼接路径。3.2 VOC转YOLO转换脚本与四个边界坑YOLO格式的标签是每张图一个txt文件文件名与图像同名每一行内容为类别ID 中心点x_norm 中心点y_norm 宽度_norm 高度_norm所有坐标都归一化到0到1之间。转换脚本本身不难难的是真实数据里各种脏数据。以下脚本是我处理这份数据集时实际用的方案关键坑点都写在注释里import os import xml.etree.ElementTree as ET from PIL import Image ann_dir Annotations img_dir JPEGImages label_dir labels os.makedirs(label_dir, exist_okTrue) # 类别清单先跑2.3节的统计脚本从结果里原样复制过来 cls_to_id {kite: 0, float: 1, bird_nest: 2, trash_bag: 3} for xml_file in os.listdir(ann_dir): if not xml_file.endswith(.xml): continue stem xml_file.replace(.xml, ) xml_path os.path.join(ann_dir, xml_file) img_path os.path.join(img_dir, stem .jpg) root ET.parse(xml_path).getroot() # 坑1XML里size字段可能缺失必须用真实图像尺寸兜底 try: with Image.open(img_path) as im: img_w, img_h im.size # 注意size返回的是(宽, 高) except FileNotFoundError: print(f[警告] 图片缺失: {img_path}) continue lines [] for obj in root.iter(object): name obj.find(name).text # 坑2类别名对不上时不要静默跳过要打印出来 if name not in cls_to_id: print(f[警告] 未映射类别 {name} {xml_file}) continue b obj.find(bndbox) x1 float(b.find(xmin).text) y1 float(b.find(ymin).text) x2 float(b.find(xmax).text) y2 float(b.find(ymax).text) # 坑3坐标越界框直接裁剪到边界避免归一化后超过1 x1 max(0, min(x1, img_w)) x2 max(0, min(x2, img_w)) y1 max(0, min(y1, img_h)) y2 max(0, min(y2, img_h)) bw x2 - x1 bh y2 - y1 if bw 0 or bh 0: continue # 转中心点坐标再分别除以宽/高做归一化 xc (x1 x2) / 2.0 / img_w yc (y1 y2) / 2.0 / img_h nw bw / img_w nh bh / img_h cls_id cls_to_id[name] lines.append(f{cls_id} {xc:.6f} {yc:.6f} {nw:.6f} {nh:.6f}) if lines: with open(os.path.join(label_dir, stem .txt), w) as f: f.write(\n.join(lines))四个边界坑逐个说清楚。第一个坑是size字段不可靠。真实标注过程中偶尔会有XML写完对象但忘了填全图像尺寸或者填的尺寸与图像实际尺寸不一致。我的兜底方案是直接用PIL读图像真实宽高以实际为准。第二个坑是类别名映射。VOC里的name是字符串而YOLO需要整数ID转换脚本里写死一份cls_to_id字典。这个字典的key必须严格复制自2.3节的统计结果比如XML里写的是trash_bag你写成trashbag模型就会把这一整类目标当背景。第三个坑是越界坐标标注软件手滑很容易让xmax超出图像宽度除以img_w后可能出现1.03这种越界值YOLO训练时会报坐标越界的警告所以先裁剪再归一化。第四个坑是浮点精度坐标保留6位小数足够im.size返回的是(宽, 高)顺序取数时不要颠倒。转换完成后还需要写一个data.yaml给YOLO用path: ./ train: ImageSets/Main/train.txt val: ImageSets/Main/val.txt names: 0: kite 1: float 2: bird_nest 3: trash_bag注意names下标的顺序必须和转换脚本里cls_to_id的赋值顺序一致YOLO读取标签时只认整数IDID对应错了整个类别全部错位。到这里这份数据集就已经变成YOLO可以直接训练的样子了。4. 168张图怎么训练复制粘贴增强与迁移学习并用168张原始图像直接扔进模型训练几乎必过拟合。训练集太小模型很快就能把像素背下来验证集一换场景就原形毕露。这一章讲两条经验路线一条是数据侧的复制粘贴增强另一条是训练侧的迁移学习与参数调优两条路必须同时走单靠其中一条都不够。4.1 复制粘贴增强把异物从原图抠出来贴到新背景最有效的小样本增强手段之一是把标注目标从原图抠出来贴到另一张巡检背景图上同时生成新的标注坐标。因为这份数据集只有矩形框标注没有像素级mask所以抠图直接按矩形区域抠。用一段脚本说明核心逻辑import cv2 import numpy as np import random random.seed(7) # 以 从A图抠风筝贴到B图 为例 src_img cv2.imread(JPEGImages/IMG_0042.jpg) bg_img cv2.imread(JPEGImages/IMG_0113.jpg) # 背景图尽量选无异物的 # 矩形抠图范围来自XML标注框 x1, y1, x2, y2 312, 98, 564, 286 patch src_img[y1:y2, x1:x2].copy() ph, pw patch.shape[:2] bh, bw bg_img.shape[:2] # 随机缩放0.8~1.2倍模拟不同拍摄距离 scale random.uniform(0.8, 1.2) pw_new, ph_new int(pw * scale), int(ph * scale) patch_resized cv2.resize(patch, (pw_new, ph_new)) # 随机找一个可放下的位置避免贴出边界 ox random.randint(0, bw - pw_new) oy random.randint(0, bh - ph_new) bg_img[oy:oy ph_new, ox:ox pw_new] patch_resized # 新标注矩形框直接按贴图位置换算 new_x1, new_y1 ox, oy new_x2, new_y2 ox pw_new, oy ph_new print(新标注框:, new_x1, new_y1, new_x2, new_y2)这段脚本演示了一次完整的复制粘贴流程实际批量生成时循环遍历所有标注目标每张背景图贴两三个异物生成新图并写入对应txt或XML即可。矩形抠图会连带着抠走一块背景对风筝、塑料袋这类目标占框面积大的物体影响不大但对鸟巢这种树枝外露的目标要慎重鸟巢框边缘混着导线和铁塔贴到新图上会形成一个“半个巢加半截导线”的错误训练框。边缘融合处理是这个做法里最玄学的部分我一般不做精细融合——对检测任务来说贴图带来的位置多样性和尺度多样性比融合质量重要得多。另外必须强调一点复制粘贴生成的增强图只能进训练集不能进验证集。正确流程是先按3.1节拆好train/val再对train部分做增强。否则同一张原始图生成的增强图会同时出现在训练集和验证集里验证指标虚高。4.2 训练参数用预训练权重、冻结backbone、小学习率168张图从零训练一个检测网络完全不现实正确做法是加载官方预训练权重做迁移学习。以Ultralytics YOLO系为例训练命令和参数选择如下yolo detect train \ modelyolov8n.pt \ datadata.yaml \ epochs120 \ imgsz640 \ batch16 \ lr00.002 \ freeze10 \ mosaic0.5每个参数的调整理由解释一下。modelyolov8n.pt加载COCO预训练权重模型虽然没学过“风筝”和“鸟巢”但对边缘、纹理、形状的底层特征已经有很强的提取能力这是小数据集能训出结果的前提。epochs取120小数据集往往四五十轮就收敛再多轮次模型开始在训练集上死记硬背。lr0从默认的0.01降到0.002低学习率微调预训练特征防止前几轮就把预训练权重冲坏。freeze10冻结backbone前10层保留底层通用特征只训练高层特征和检测头。mosaic0.5是数据增强里必须调的一项mosaic1.0会把目标切碎小目标在拼接后被缩放得更小标注框大量被裁剪反而加重过拟合。显存吃紧时把batch降到8同时imgsz也可以考虑从640提到960。这个数据集里的异物以中小目标为主640下风筝可能只占十几个像素提升输入分辨率对小目标检测的帮助立竿见影代价是训练时间增加约一倍。5. 避坑与常见问题小样本异物检测翻过车才懂的五件事这一章是血泪经验六条坑按数据侧和训练侧分两组写。每条都按“现象、原因、解决”展开都是我拿这类小样本数据集实际跑过之后踩出来的。5.1 数据侧翻车验证集污染、类别名错位、退化框漏网现象一val mAP虚高到0.9换一批现场图片直接崩。第一次拿增强图训练时图省事先扩增全量数据再拆分结果val里混进了训练集的近亲样本。原因很明确复制粘贴增强生成的图在文件层面是独立文件随机拆分时同一张原始图的多张增强版本被同时分到train和val验证集失去独立性。解决方法是严格按3.1节先拆后增强增强图文件名加前缀固定进train目录val目录永远只用原始图。从那以后我拿到任何小样本数据集第一件事就是确认train和val在图像来源上完全不相交。现象二训练日志里某个类别的loss忽高忽低预测时这个类永远不出现。排查后发现XML里标的类别叫plastic_floatdata.yaml里写的是float中间差了一个前缀YOLO把这个类当背景处理了。原因是转换脚本里cls_to_id字典手打时漏了字符。解决方法是把2.3节统计脚本打印出的类别名原样复制粘贴到字典和yaml里绝不手动重输。手工敲击造成的字符串不一致是VOC转YOLO场景最容易犯且最难查的错误。现象三训练到一半loss变成NaN检查发现标注里有一张退化框。某张图像的XML里xmax等于xmin归一化后目标宽度为0损失函数计算时除零。2.3节的质量盘点脚本应该跑在数据预处理之前但实际项目里总有人跳过这一步。解决方法是把盘点脚本集成进转换流程发现问题框直接打印文件名和坐标宁可删掉这张图也不能让它进训练集。5.2 训练侧翻车过拟合背景、小目标漏检、异物与背景同色现象四loss一路降到0.2val mAP却是0.05。这是典型的小数据集过拟合信号——模型把绝缘子串、导线纹理当成异物特征训练集上表现完美验证集上完全不通。原因在于原图里异物占比太小模型很容易走捷径学背景。解决方法是按4.2节冻结更多backbone层、调低学习率、开启mosaic之外的HSV色彩抖动让模型被迫关注目标本身的形状特征而不是背景纹理。现象五图像里风筝只有100×100像素模型始终漏检。小目标检测的本质问题是高倍下采样之后目标在特征图上只剩几个像素跟背景特征混在一起。最直接的解决方法是把imgsz从640提到960输入分辨率提高后风筝在640×640特征图上的尺寸变大匹配到的anchor数量明显增加另外可以用YOLO自带的anchor自适应工具重新计算anchor尺寸让先验框更贴合这份数据集的真实目标分布。现象六鸟巢检测几乎为零标注框都在但预测就是不出来。巡检图像里鸟巢搭在横担上树枝颜色和铁塔锈色非常接近模型学到的区分特征几乎是零。单独调参数无效最后用数据手段解决对鸟巢类目标做复制粘贴增强时配合HSV的亮度和对比度随机抖动把“不同光照下的鸟巢”这一多样性喂给模型才算把这类检测精度拉起来。6. 可视化验证把预测框与标注框叠回原图逐个bad case看漏检误检训练完成后别只盯着mAP数字拿模型对验证集逐张预测把预测框和标注框同时画回原图看每一张图的漏检和误检这一遍下来对数据集问题的理解比看十次指标都深。脚本如下import cv2 import xml.etree.ElementTree as ET from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) img_path JPEGImages/IMG_0042.jpg xml_path Annotations/IMG_0042.xml img cv2.imread(img_path) # 画ground truth绿色框类别名在框上方 root ET.parse(xml_path).getroot() for obj in root.iter(object): b obj.find(bndbox) x1, y1 int(float(b.find(xmin).text)), int(float(b.find(ymin).text)) x2, y2 int(float(b.find(xmax).text)), int(float(b.find(ymax).text)) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, obj.find(name).text, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 1) # 画预测框红色框带置信度 results model.predict(img_path, conf0.25, iou0.45)[0] for box in results.boxes: x1, y1, x2, y2 [int(v) for v in box.xyxy[0].tolist()] cls_name model.names[int(box.cls)] conf float(box.conf) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, f{cls_name} {conf:.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 1) cv2.imwrite(visual_check.jpg, img)颜色约定很直观绿色是人工标注的ground truth红色是模型预测。绿色框上没有红色框覆盖就是漏检红色框没有对应的绿色框就是误检。conf0.25和iou0.45是验证集默认阈值想看得严一点把conf提到0.5想知道模型在低置信度下还有什么潜力就降到0.1。逐张看完验证集之后把漏检案例按类别归类你会清楚地看到哪一类样本最少、哪一类和背景最像——这些结论直接决定下一步是补数据还是调增强策略。从那以后我每次拿到小样本标注数据集都强制走一遍“统计类别-质量盘点-先拆后增强-转换-可视化回看”的全流程至少能筛掉一半标注和配置层面的低级错误。这个流程本身不复杂但每一步都能省下后面排错的大把时间。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。