☰
无人机航拍杆塔锈损检测:VOC标注数据与高斯噪声扩充实践
2026/9/26 16:36:20 网站建设 项目流程

简介:面向电力行业智能巡检与计算机视觉研究者的杆塔塔材锈损检测航拍数据集,包含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> <folder>JPEGImages</folder> <filename>IMG_00001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>rust</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>412</xmin> <ymin>589</ymin> <xmax>537</xmax> <ymax>698</ymax> </bndbox> </object> </annotation>

注意看<size>节点,它存的是图像原始宽高。这个信息在转YOLO格式时要用,但很多人在写转换脚本时忽略了它,直接用<bndbox>里的坐标除以一个自己猜的尺寸,结果就是坐标全乱。实际标注坐标是按原始图像尺寸来的,如果训练时resize到640x640,需要在数据加载阶段完成坐标归一化,而不是提前把XML里的坐标改掉。

另外一个随时可能踩的坑是<truncated>、<difficult>这类标记。有些标注工具会把模糊、被遮挡的目标标为difficult=1。如果转换脚本直接忽略这个字段,这些质量存疑的样本也会跟着进训练集,等于给模型投喂了噪声。我一般会在转换时过滤掉difficult=1的框,或者单独建一个文件夹存放并人工复核。

3. 高斯噪声扩充:为什么选它扩数据,参数怎么调不会伤标签

标题里明写了「高斯噪声扩充」,这其实透露出一个重要信息:数据集生成者用加噪这种手段把原始样本量提到了1700多张。加噪声不是随便往图上撒雪花,它背后的逻辑和参数设置直接关系到扩充后的数据有没有训练价值。

3.1 为什么选高斯噪声而不是翻转和调色

航拍塔材锈损检测有个特点:缺陷是固定结构上的表面退化,不是物体形态变化。翻转、缩放这类几何增广确实能扩数量,但塔材的锈蚀区域多数出现在节点板、螺栓连接处,翻转之后模型学到的其实是「方向特征」,而不是「锈蚀本身的纹理」。高斯噪声则更贴近无人机航拍的真实退化:弱光环境下传感器感光度升高、JPEG压缩产生的块状噪声、雾天颗粒感,这些在真实巡检数据里都存在。

下表是几种常见增广方式在这个场景下的对比:

增广方式对锈损检测的效果风险点
高斯噪声模拟传感器和压缩噪声,提升纹理鲁棒性噪声太强会遮住小锈斑
水平翻转增加方向多样性,适合螺栓等局部件对锈蚀纹理无帮助
HSV调色模拟不同光照下的颜色偏移锈蚀本身是颜色特征,过度调色会误导
随机裁剪增强小目标尺度多样性裁剪比例不对会把目标切掉

所以高斯噪声扩充的价值不在于「让图像变花」,而是让模型在推理时不被真实噪声干扰。现实巡检图里不可能每一张都干净无噪,训练阶段提前见一见噪声,推理阶段才不会被低质量的航拍帧搞崩。

3.2 一套可复用的高斯噪声扩充脚本

我通常用NumPy直接对图像加高斯噪声,不引入额外依赖。核心代码如下:

import cv2 import numpy as np import os def add_gaussian_noise(img, sigma=15): 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_ok=True) 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, sigma=20) 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) / width
  • y_center = ((ymin + ymax) / 2) / height
  • box_width = (xmax - xmin) / width
  • box_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(f"data/images/{name}\n") with open("val.txt", "w") as f: for name in val_set: f.write(f"data/images/{name}\n")

seed取42只是习惯,重点是固定下来不随意改。val先单独划分,扩充图后加,保证验证集纯净。做这一步时还要注意,原始图和扩充图共享相同的XML坐标,转换后的txt也要放在对应目录,并保持相同的去除后缀命名规则,这样训练时image和label才能一一对应。

5. 训练与标注的避坑记录:五个我踩过的坑,含一条数据泄漏

读数据、转格式、开训练,每一步都埋着雷。这些坑单独看都不算大,但任何一个都会让训练结果失真甚至直接报错。都是一线跑出来的真实问题。

5.1 航拍远距离小目标漏检严重:mAP看着高,召回却惨不忍睹

现象:训练完在验证集上跑,mAP@0.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超出width,ymin出现负值。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, encoding="utf-8")

处理完再跑一次类别统计,确认三类并成两类才继续往下走。

6. 用「按目标尺寸分组」验证召回:一个能快速看出数据短板的小技巧

很多人在验证集上只看总mAP,觉得差不多就收工。但对航拍塔材锈损这种小目标数据集,总指标容易掩盖问题——模型可能对大锈斑检测得很好,小锈斑几乎全漏,均值一拉,数字还行。我习惯加一步按目标尺寸分组评估。

思路是把验证集的每个标注框按面积分成三组:小目标(面积小于32x32像素)、中目标(32x32到96x96)、大目标(大于96x96)。然后分别计算每组的召回率。做法不复杂,基于YOLO预测结果和标注框做一个匹配统计:

def group_recall(pred_boxes, gt_boxes, img_size, iou_thres=0.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之间,说明模型不是没见过,而是对这类目标的特征响应太弱。这时的修正方向就该转向特征融合层或多尺度训练,而不是单纯增加噪声扩充。

从那以后我每次拿到一批新数据,都先跑一遍分组统计再谈训练,甚至会在训练前就把这些小目标和中等目标的数量比打印出来发给标注团队,让他们补一批小目标样本。这个方法虽然花不了几分钟,但能省下后面好几天调试时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询