简介面向电力行业智能巡检与计算机视觉研究者的杆塔塔材锈损检测航拍数据集包含1700多张带VOC标签的塔材航拍图像可支撑铁塔锈损识别、定位等目标检测任务帮助解决人工巡检效率低、漏检率高等问题。资源共2000个文件主要由1968张jpg图像和1968份xml标签文件组成总大小22.03MB下载后可直接配合YOLO、Faster R-CNN等框架搭建训练流程。数据集通过高斯噪声扩充来模拟拍摄环境中的传感器噪声与光照不均增强模型在真实复杂场景下的鲁棒性xml标签遵循PASCAL VOC格式逐图标注锈损塔材的边界框便于快速完成数据集划分、模型训练与不同算法对比。航拍视角覆盖范围广适合对成片杆塔进行区域级锈损筛查与状态评估。目前已有469人学习下载是电力设施监测、目标检测方向工程师和研究者可直接使用的建模与评测基础数据。1. 杆塔塔材锈损检测航拍图像1700张带VOC标签的可用数据长什么样拿到一批无人机巡检拍回来的塔材照片最头疼的不是算法选型而是标注数据。输电杆塔的塔脚、连接板、斜材在航拍视角下又小又密锈蚀区域往往只有几十个像素宽肉眼判读都费劲更别说标注一致性。这套杆塔塔材锈损检测航拍图像数据集总量1700多张自带VOC格式的XML标注并且用高斯噪声做了扩充解决的就是「想做杆塔锈损检测却没有干净数据起步」的问题。它最大的价值在于缺陷类别清晰、标注框足够细、扩充手段可以在训练阶段直接复用无论是拿来训YOLO系检测器还是做验证集评估都不用再从零标图。适合输电线路缺陷检测的算法工程师、做相关毕业设计的学生以及想快速跑通「VOC→训练→验证」全流程的从业者。2. 数据与标注的底子VOC目录结构、XML标注和图像规格怎么核对拿到这份资源第一步不是急着训练而是先把目录结构和标注格式摸清楚。航拍图像数据集和普通自然图像数据集最大的区别在于整理逻辑飞一趟可能拍几千张最后能用的是挑过的、去过冗余的所以「目录是否规范、文件名是否一一对应」直接决定后面脚本能不能跑通。2.1 目录结构VOC风格的JPEGImages与Annotations怎么组织以VOC标注体系来说规范目录一般长成这样dataset/ ├── JPEGImages/ │ ├── IMG_00001.jpg │ ├── IMG_00002.jpg │ └── ... ├── Annotations/ │ ├── IMG_00001.xml │ ├── IMG_00002.xml │ └── ... ├── ImageSets/ │ └── Main/ │ ├── train.txt │ ├── val.txt │ └── trainval.txt └── label_map.txt拿到后我一般先做三件事第一比对JPEGImages和Annotations里的文件名数量是否完全一致第二抽查几个XML看filename字段是不是和实际图像名对得上第三确认ImageSets里的txt是相对路径还是绝对路径。很多坑都出在这些看起来无伤大雅的地方。这套数据既然是航拍图图像尺寸、焦距、飞行高度都不会完全一致。不同批次的图像很可能来自不同巡检任务意味着同样的锈损区域在不同图里占的像素比例差异很大。例如靠近杆塔底部的锈斑在画面里可能占一百多个像素而在塔身中段的锈蚀可能只占二三十个像素。这样的数据特性决定后续训练时输入尺寸不能设太小anchors也需要根据实际标注框重新聚类。2.2 图像与标注规格锈损类别分布怎么看目标检测数据集的标注质量先看类别设计是否清晰。杆塔锈损常见的问题是「生锈」和「锈蚀穿孔」在视觉上边界模糊如果标签里既有rust又有corrosion人工标注时很容易混。这份数据集的类别设计需要自己打开XML确认但常见的合理做法是只保留一到两个缺陷类别例如rust表面锈蚀和severe_rust严重锈蚀/剥落而不是把每个锈点都拆成单独类别。判断类别分布是否均衡也很关键。如果rust占绝大多数而severe_rust只有几十个框训练出来的模型对严重锈蚀的召回率就会很难看。可以用一个简单脚本统计每类目标的框数量import os import xml.etree.ElementTree as ET annotations_dir Annotations class_count {} for xml_file in os.listdir(annotations_dir): tree ET.parse(os.path.join(annotations_dir, xml_file)) root tree.getroot() for obj in root.iter(object): name obj.find(name).text class_count[name] class_count.get(name, 0) 1 print(class_count)脚本的逻辑就是遍历每个XML把object节点下的name文本提取出来计数。如果发现某个类别数量特别少后续训练要么做类别加权要么先合并相似类别不要硬着头皮训一个极度不均衡的模型。2.3 XML标注内容bndbox和关键字段的坑打开一个典型的VOC XML文件核心结构如下annotation folderJPEGImages/folder filenameIMG_00001.jpg/filename size width1920/width height1080/height depth3/depth /size object namerust/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin412/xmin ymin589/ymin xmax537/xmax ymax698/ymax /bndbox /object /annotation注意看size节点它存的是图像原始宽高。这个信息在转YOLO格式时要用但很多人在写转换脚本时忽略了它直接用bndbox里的坐标除以一个自己猜的尺寸结果就是坐标全乱。实际标注坐标是按原始图像尺寸来的如果训练时resize到640x640需要在数据加载阶段完成坐标归一化而不是提前把XML里的坐标改掉。另外一个随时可能踩的坑是truncated、difficult这类标记。有些标注工具会把模糊、被遮挡的目标标为difficult1。如果转换脚本直接忽略这个字段这些质量存疑的样本也会跟着进训练集等于给模型投喂了噪声。我一般会在转换时过滤掉difficult1的框或者单独建一个文件夹存放并人工复核。3. 高斯噪声扩充为什么选它扩数据参数怎么调不会伤标签标题里明写了「高斯噪声扩充」这其实透露出一个重要信息数据集生成者用加噪这种手段把原始样本量提到了1700多张。加噪声不是随便往图上撒雪花它背后的逻辑和参数设置直接关系到扩充后的数据有没有训练价值。3.1 为什么选高斯噪声而不是翻转和调色航拍塔材锈损检测有个特点缺陷是固定结构上的表面退化不是物体形态变化。翻转、缩放这类几何增广确实能扩数量但塔材的锈蚀区域多数出现在节点板、螺栓连接处翻转之后模型学到的其实是「方向特征」而不是「锈蚀本身的纹理」。高斯噪声则更贴近无人机航拍的真实退化弱光环境下传感器感光度升高、JPEG压缩产生的块状噪声、雾天颗粒感这些在真实巡检数据里都存在。下表是几种常见增广方式在这个场景下的对比增广方式对锈损检测的效果风险点高斯噪声模拟传感器和压缩噪声提升纹理鲁棒性噪声太强会遮住小锈斑水平翻转增加方向多样性适合螺栓等局部件对锈蚀纹理无帮助HSV调色模拟不同光照下的颜色偏移锈蚀本身是颜色特征过度调色会误导随机裁剪增强小目标尺度多样性裁剪比例不对会把目标切掉所以高斯噪声扩充的价值不在于「让图像变花」而是让模型在推理时不被真实噪声干扰。现实巡检图里不可能每一张都干净无噪训练阶段提前见一见噪声推理阶段才不会被低质量的航拍帧搞崩。3.2 一套可复用的高斯噪声扩充脚本我通常用NumPy直接对图像加高斯噪声不引入额外依赖。核心代码如下import cv2 import numpy as np import os def add_gaussian_noise(img, sigma15): mean 0.0 noise np.random.normal(mean, sigma, img.shape) noisy_img np.clip(img noise, 0, 255).astype(np.uint8) return noisy_img src_dir JPEGImages dst_dir JPEGImages_GN os.makedirs(dst_dir, exist_okTrue) for img_name in os.listdir(src_dir): img_path os.path.join(src_dir, img_name) img cv2.imread(img_path) if img is None: continue noisy add_gaussian_noise(img, sigma20) cv2.imwrite(os.path.join(dst_dir, img_name.replace(.jpg, _gn.jpg)), noisy)参数含义sigma是标准差数值越大噪声越重。在锈损检测里我一般取15到25之间。小于10几乎看不出区别扩了等于没扩大于40噪声会直接把锈蚀纹理埋掉人眼都无法辨别。生成之后把对应的XML复制一份文件名改成_gn.xml坐标完全不用动——高斯噪声是逐像素叠加不改变目标位置和框尺寸这是它相比几何增广最大的优势因为标签工作量为零。3.3 扩充后的文件怎么管理训练集与验证集别混扩充图的文件名加后缀是一种好习惯比如IMG_00001.jpg扩成IMG_00001_gn.jpg对应XML也改成IMG_00001_gn.xml。这样在生成训练列表的时候一个if _gn in filename就能区分原始图和扩充图。关键在于扩充图只进训练集不进验证集。验证集的意义是评估模型在近似真实分布上的表现如果验证集里混入大量加噪图mAP会虚高或者虚低完全失去参考意义。常见做法是先把原始图按8:2分成train和val再把扩充图全部塞进train部分这样验证集始终是干净的原始航拍图。4. 从VOC到YOLO坐标换算、转换脚本和训练集切分拿到VOC标注的数据集几乎所有人都会面临同一件事转成YOLO要用的txt格式。YOLO系列训练用的标签是每张图一个txt文件每行一个目标格式是class_id x_center y_center width height坐标全部归一化到0~1。4.1 坐标换算VOC框到YOLO框的公式与矩阵VOC的坐标是像素绝对值YOLO需要的是相对于图像宽高的比例。换算公式并不复杂x_center ((xmin xmax) / 2) / widthy_center ((ymin ymax) / 2) / heightbox_width (xmax - xmin) / widthbox_height (ymax - ymin) / height这里的width和height必须来自XML的size节点而不是读图后重新获取。虽然大部分情况下两者一致但有些标注工具会保存resize后的预览图导致XML里存的尺寸和正图不一致。遇到这种情况一律以XML里的size为准并把实际图像尺寸打印出来做交叉验证。4.2 用Python脚本批量转换VOC到YOLO转换脚本可以写成这样import os import xml.etree.ElementTree as ET classes [rust, severe_rust] def convert_annotation(xml_path, out_dir, image_width, image_height): tree ET.parse(xml_path) root tree.getroot() # 优先从XML读取尺寸 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in classes: continue cls_id classes.index(cls_name) 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) # 归一化 x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) out_name os.path.basename(xml_path).replace(.xml, .txt) with open(os.path.join(out_dir, out_name), w) as f: f.write(\n.join(lines))脚本里的classes列表顺序就是最终模型的类别ID顺序这个顺序必须和训练时的data.yaml保持一致。我写这个脚本时踩过一个坑类的顺序在不同脚本里不一致训练集用[rust, severe_rust]验证的时候用[severe_rust, rust]结果类别错位验证损失乱跳模型输出完全不可信。4.3 训练集与验证集切分固定种子是原则切分时建议固定随机种子保证每一次实验可以在完全相同的子集上对比。写一个简单的划分脚本import os import random random.seed(42) all_images [f for f in os.listdir(JPEGImages) if f.endswith(.jpg) and _gn not in f] random.shuffle(all_images) val_ratio 0.2 val_count int(len(all_images) * val_ratio) val_set set(all_images[:val_count]) train_set all_images[val_count:] # 再把扩充图全部加入训练集 augmented_images [f for f in os.listdir(JPEGImages_GN) if f.endswith(.jpg)] train_set.extend(augmented_images) with open(train.txt, w) as f: for name in train_set: f.write(fdata/images/{name}\n) with open(val.txt, w) as f: for name in val_set: f.write(fdata/images/{name}\n)seed取42只是习惯重点是固定下来不随意改。val先单独划分扩充图后加保证验证集纯净。做这一步时还要注意原始图和扩充图共享相同的XML坐标转换后的txt也要放在对应目录并保持相同的去除后缀命名规则这样训练时image和label才能一一对应。5. 训练与标注的避坑记录五个我踩过的坑含一条数据泄漏读数据、转格式、开训练每一步都埋着雷。这些坑单独看都不算大但任何一个都会让训练结果失真甚至直接报错。都是一线跑出来的真实问题。5.1 航拍远距离小目标漏检严重mAP看着高召回却惨不忍睹现象训练完在验证集上跑mAP0.5数值不错但单独看recall曲线锈蚀面积小的目标几乎全漏。原因航拍图里塔材锈损天然是小目标。输入分辨率不够模型下采样到最后一层特征图时小目标只剩几个像素。加上高斯噪声扩充后噪声纹理反而和锈蚀纹理竞争模型倾向选择更容易区分的特征也就是漏了。解决开启mosaic增强让模型在训练阶段看到更多小尺度上下文输入尺寸从640提高到768或960如果能承受推理速度损失直接上原生分辨率。另外对YOLO系模型重新聚类anchors不要用默认值因为自然场景的COCO锚框分布和航拍结构件完全不一样。5.2 XML里size和真实图像尺寸不一致转换后框全偏现象训练后loss能降但推理画出来的框要么偏左上要么比例明显不对。原因标注工具保存XML时使用了缩略图的尺寸但正图是1920x1080两者比例不同按XML的width归一化后坐标就偏移了。我排查时打印了XML里的尺寸和实际读取的图尺寸发现根节点有错。解决转换脚本里加一个校验如果XML的width和height与cv2读取结果不一致跳过该文件并输出警告。不要自动按XML尺寸换算也不要按实际尺寸换算先人工确定哪个是对的再统一处理。5.3 扩充图混入验证集精度虚高现象训练完验证mAP飙到0.9以上换成纯原始图测试精度掉到0.6。第一反应是模型过拟合后来才发现是验证集里混了大量加噪图模型对噪声纹理产生了记忆。原因生成扩充文件时没有加区分后缀训练列表生成脚本把原始图、扩充图一锅端进train.txt之后又手动往val.txt里也塞了一部分。数据泄漏导致验证结果失真。解决文件命名强制带_gn后缀划分脚本里用_gn not in filename过滤验证集只用原始图。从那以后我每次跑完训练都会单独检查val.txt确认没有一条带扩充标记。5.4 坐标越界标注框超出图像边界现象转YOLO格式后有些txt里出现了大于1或小于0的坐标。YOLO训练不报错但损失曲线周期性抖动模型输出框不稳定。原因标注人员在标注时手滑或者标注界面放大后没有精确闭合目标导致xmax超出widthymin出现负值。VOC格式里这类数据不会被标记为错误YOLO读取时也不拦但归一化坐标越界会直接污染anchors匹配。解决转换时加钳制逻辑xmin max(0, min(xmin, img_w - 1)) xmax max(0, min(xmax, img_w - 1))对越界框先钳制再归一化最后统计一下哪些框是修正过的单独导出文件人工核对。5.5 类别名大小写不一致成了Rust和rust两个类现象训练时data.yaml里设置了一个类但loss始终降不到预期验证集所有目标都预测成同一类。原因不同标注批次里有人用Rust有人用rust还有人用rust spot三个名字对应三种类别。XML处理器按字符串匹配Rust和rust被当成了不同类别导致类别数比预期多。解决转换前统计所有XML里的name字段打印去重结果先用脚本统一替换import os import xml.etree.ElementTree as ET for xml_file in os.listdir(Annotations): path os.path.join(Annotations, xml_file) tree ET.parse(path) root tree.getroot() changed False for obj in root.iter(object): name obj.find(name) if name.text in [Rust, RUST, rust spot]: name.text rust changed True if changed: tree.write(path, encodingutf-8)处理完再跑一次类别统计确认三类并成两类才继续往下走。6. 用「按目标尺寸分组」验证召回一个能快速看出数据短板的小技巧很多人在验证集上只看总mAP觉得差不多就收工。但对航拍塔材锈损这种小目标数据集总指标容易掩盖问题——模型可能对大锈斑检测得很好小锈斑几乎全漏均值一拉数字还行。我习惯加一步按目标尺寸分组评估。思路是把验证集的每个标注框按面积分成三组小目标面积小于32x32像素、中目标32x32到96x96、大目标大于96x96。然后分别计算每组的召回率。做法不复杂基于YOLO预测结果和标注框做一个匹配统计def group_recall(pred_boxes, gt_boxes, img_size, iou_thres0.5): small_gts [] medium_gts [] large_gts [] for gt in gt_boxes: area (gt[2] - gt[0]) * (gt[3] - gt[1]) if area 32 * 32: small_gts.append(gt) elif area 96 * 96: medium_gts.append(gt) else: large_gts.append(gt) small_hit sum([1 for gt in small_gts if any(iou(gt, p) iou_thres for p in pred_boxes)]) medium_hit sum([1 for gt in medium_gts if any(iou(gt, p) iou_thres for p in pred_boxes)]) large_hit sum([1 for gt in large_gts if any(iou(gt, p) iou_thres for p in pred_boxes)]) return { small_recall: small_hit / len(small_gts), medium_recall: medium_hit / len(medium_gts), large_recall: large_hit / len(large_gts), }如果small_recall明显低于large_recall说明模型对小目标不敏感优先去调输入尺寸和anchor。如果medium和large都还行说明标注数据里小目标数量本身不足训练时每个batch里小目标出现的频率太低模型没见过足够多小样本这时就应该回训练集人工确认一下小目标标注框数量。我会搭配另一个指标看预测小目标置信度的分布。把模型在验证集上给出的所有小目标框置信度打印出来如果大量框集中在0.15到0.3之间说明模型不是没见过而是对这类目标的特征响应太弱。这时的修正方向就该转向特征融合层或多尺度训练而不是单纯增加噪声扩充。从那以后我每次拿到一批新数据都先跑一遍分组统计再谈训练甚至会在训练前就把这些小目标和中等目标的数量比打印出来发给标注团队让他们补一批小目标样本。这个方法虽然花不了几分钟但能省下后面好几天调试时间。希望帮到你。本文还有配套的精品资源点击获取
