☰
输电线路悬垂线夹检测:1600张航拍VOC数据集YOLOv8实战
2026/10/2 3:48:01 网站建设 项目流程

简介:本资源为面向输电线路智能巡检方向的悬垂线夹检测数据集,适用于电力巡检算法研发人员、计算机视觉学习者及无人机图像目标检测项目实践者,可解决悬垂线夹样本稀缺、标注数据获取困难的问题,支撑模型训练与性能验证。压缩包共约2000个文件,包含1625个VOC格式xml标注文件与375张jpg航拍图像,整体约143.5MB,xml与图像一一对应,可直接用于YOLO、Faster R-CNN等主流检测框架的数据转换与训练。图像均来自无人机航拍视角,覆盖多种线路场景与拍摄角度,标注由labelimg完成,标签规范统一,便于快速构建训练集与验证集。目前已有423人学习下载,适合需要真实电力巡检数据开展目标检测实验、课程设计或算法对比的读者参考使用。

1. 输电线路悬垂线夹检测:1600 张航拍图 + VOC 标签能跑出什么

悬垂线夹是输电线路金具里最容易被航拍巡检漏掉的一类目标——它体积小、颜色和导线接近、在强光下几乎没有纹理,但一旦锈蚀、偏移或螺栓脱落,后果直接指向断线。我拿到的这套数据集,1600 多张无人机航拍图像,全部是 VOC 格式的 XML 标注,目标类别就是悬垂线夹。它解决的不是“有没有电”这种粗粒度问题,而是让检测模型在复杂背景下把线夹从导线、绝缘子、杆塔里抠出来。适合两类人:一是做电力巡检 AI 落地的算法工程师,需要快速验证小目标检测方案;二是已有无人机采集能力、想自己训模型但缺标注数据的团队。这套数据的价值不在“大”,而在“专”——1600 张全部围绕一个金具类别,标注框紧贴目标边缘,省掉了从零标 2000 张的冷启动成本。但别急着直接喂给 YOLO,VOC 转 YOLO 的坐标归一化、小目标增强、背景负样本比例,每一步都有翻车点。下面按“先看懂数据 → 再跑通转换 → 再训出基线 → 再排坑”的顺序拆。

2. 悬垂线夹在航拍图里长什么样:先看懂 VOC 标签的四个字段

2.1 悬垂线夹的视觉特征与标注边界

悬垂线夹在航拍图里通常出现在导线与杆塔连接处,形态是一个短粗的金属夹体,两侧有螺栓或压板。从无人机俯拍或斜侧拍的角度看,它可能只占 30×30 到 80×80 像素,背景是天空、植被、杆塔角钢或另一根导线。VOC 标注的<bndbox>里,xmin、ymin、xmax、ymax 是像素绝对坐标,不是归一化值。我见过不少团队直接把 VOC 的框拿去算 IoU,结果发现同一个线夹在不同图像里框的大小差 3 倍——原因是拍摄高度和焦距变了,但标注没有做尺度归一。这套数据里,悬垂线夹的标注框普遍偏紧,边缘留白少,这对训练是好事,但对数据增强提出了要求:随机缩放不能把框缩到 10 像素以下,否则模型学不到有效特征。

另一个容易忽略的点是“困难样本”标记。VOC 的<difficult>标签如果为 1,表示该目标在遮挡、模糊或极端光照下难以识别。悬垂线夹被导线遮挡、被杆塔阴影覆盖的情况很常见。训练时如果直接把 difficult=1 的样本当正样本,模型会在这些样本上产生高损失但学不动;如果全部丢弃,又会导致模型在真实遮挡场景下漏检。我一般会保留但降低权重,具体在后面的损失函数部分说。

2.2 VOC XML 的目录结构与字段含义

一套完整的 VOC 数据集通常长这样:

dataset/ ├── Annotations/ │ ├── 000001.xml │ ├── 000002.xml │ └── ... ├── JPEGImages/ │ ├── 000001.jpg │ ├── 000002.jpg │ └── ... └── ImageSets/ └── Main/ ├── train.txt ├── val.txt └── trainval.txt

Annotations里每个 XML 对应一张图,JPEGImages里是原图,ImageSets/Main里是划分文件。悬垂线夹数据集的 XML 里,<object>下会有<name>字段,值就是类别名,比如insulator_clamp或suspension_clamp。你需要先确认这个字段的实际拼写,因为后面转 YOLO 时类别映射表必须和它完全一致,大小写、下划线都不能错。

一个典型的 XML 片段:

<annotation> <folder>JPEGImages</folder> <filename>000001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>suspension_clamp</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>856</xmin> <ymin>432</ymin> <xmax>912</xmax> <ymax>488</ymax> </bndbox> </object> </annotation>

这里width=1920、height=1080是原图尺寸,bndbox的四个值是绝对像素。如果一张图里有多个线夹,就会有多个<object>块。转 YOLO 格式时,每个<object>生成一行 txt,格式是class_id x_center y_center width height,全部归一化到 0~1。

2.3 用 Python 统计类别分布和框尺寸

在动手转格式之前,先跑一段统计脚本,搞清楚这套数据里悬垂线夹的框尺寸分布、每张图的平均目标数、有没有空标注图。这一步能帮你判断后面要不要做小目标增强。

import os import xml.etree.ElementTree as ET import numpy as np ann_dir = "dataset/Annotations" widths, heights, num_objs = [], [], [] for xml_file in os.listdir(ann_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(ann_dir, xml_file)) root = tree.getroot() objs = root.findall("object") num_objs.append(len(objs)) for obj in objs: bbox = obj.find("bndbox") w = float(bbox.find("xmax").text) - float(bbox.find("xmin").text) h = float(bbox.find("ymax").text) - float(bbox.find("ymin").text) widths.append(w) heights.append(h) print(f"图像总数: {len(num_objs)}") print(f"目标总数: {sum(num_objs)}") print(f"每图平均目标数: {np.mean(num_objs):.2f}") print(f"框宽 中位数: {np.median(widths):.1f}, 均值: {np.mean(widths):.1f}") print(f"框高 中位数: {np.median(heights):.1f}, 均值: {np.mean(heights):.1f}") print(f"框面积 < 32x32 的比例: {np.mean([(w<32 and h<32) for w,h in zip(widths,heights)]):.2%}")

这段脚本输出四个关键数:总目标数、每图平均目标数、框宽高中位数、小框占比。如果中位数在 40~60 像素、小框占比超过 30%,说明这是典型的小目标检测任务,后面训练时 anchor 尺寸和输入分辨率都要针对性调。如果每图平均目标数只有 1~2 个,说明背景负样本比例很高,需要控制正负样本平衡。

3. 把 VOC 转成 YOLO 格式:转换脚本与四个边界坑

3.1 转换脚本的完整实现

VOC 转 YOLO 的核心逻辑就三步:读 XML 拿宽高和 bbox,归一化坐标,写 txt。但实际写的时候,路径拼接、类别映射、空标注处理、坐标越界裁剪,每个环节都可能出问题。下面是我常用的转换脚本,带完整注释。

import os import xml.etree.ElementTree as ET # 类别映射:必须和 XML 里的 <name> 完全一致 class_map = {"suspension_clamp": 0} # 如果有多类,按字母序或自定义顺序扩展 ann_dir = "dataset/Annotations" img_dir = "dataset/JPEGImages" out_dir = "dataset/labels" os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(ann_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(ann_dir, xml_file)) root = tree.getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in class_map: continue # 跳过未定义类别 cls_id = class_map[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) # 边界裁剪:防止标注越界导致归一化后为负或 >1 xmin = max(0, min(xmin, img_w)) xmax = max(0, min(xmax, img_w)) ymin = max(0, min(ymin, img_h)) ymax = max(0, min(ymax, img_h)) # 跳过无效框 if xmax <= xmin or ymax <= ymin: continue x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / 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}") # 即使没有目标,也写空文件,保持图片和标签一一对应 txt_name = os.path.splitext(xml_file)[0] + ".txt" with open(os.path.join(out_dir, txt_name), "w") as f: f.write("\n".join(lines))

逻辑说明:先读 XML 的<size>拿原图宽高,再遍历每个<object>。class_map是硬编码的类别映射,如果你的 XML 里类别名是insulator_clamp,这里必须改成一样的。边界裁剪那四行很关键——有些标注框的 xmax 会超过图像宽度,不裁剪的话归一化后 w 可能大于 1,YOLO 训练时直接报错。最后空文件也要写,否则 YOLO 的 dataloader 找不到对应标签会跳过整张图。

参数说明:x_center和y_center是框中心点归一化坐标,w和h是归一化宽高。保留 6 位小数足够,再多没必要。如果你的数据集里有多类,class_map的 value 从 0 开始递增,顺序要和训练时的names列表一致。

3.2 划分训练集和验证集:别用随机划分

VOC 自带的ImageSets/Main里可能有 train.txt 和 val.txt,但那是原数据集作者划的,不一定适合你的任务。我一般会重新划,而且不用纯随机。悬垂线夹航拍图有个特点:同一段线路、同一拍摄架次的图像高度相似。如果随机划分,训练集和验证集里会出现几乎一样的图,验证指标虚高,上线后翻车。

正确做法是按“拍摄架次”或“线路段”分组划分。如果数据里没有这个元信息,退而求其次:按图像文件名排序后,每隔 5 张取 1 张做验证。这样能保证验证集覆盖不同的拍摄条件。

import os import random img_dir = "dataset/JPEGImages" all_imgs = sorted([f for f in os.listdir(img_dir) if f.endswith(".jpg")]) # 按文件名排序后间隔采样,模拟不同架次 val_imgs = all_imgs[::5] train_imgs = [f for f in all_imgs if f not in val_imgs] with open("dataset/train.txt", "w") as f: f.write("\n".join([os.path.splitext(x)[0] for x in train_imgs])) with open("dataset/val.txt", "w") as f: f.write("\n".join([os.path.splitext(x)[0] for x in val_imgs])) print(f"训练集: {len(train_imgs)}, 验证集: {len(val_imgs)}")

这段脚本输出两个 txt,每行是图像文件名(不带扩展名)。YOLO 训练时通过这个列表去JPEGImages找图、去labels找标签。间隔采样比随机采样更能暴露模型在不同拍摄条件下的泛化能力。

3.3 四个边界坑:坐标越界、类别拼写、空标签、路径分隔符

第一个坑:坐标越界。有些 XML 的 xmax 等于图像宽度,归一化后 x_center + w/2 刚好等于 1.0,浮点精度可能导致 1.000001,YOLO 某些版本会直接报错。解决办法是在归一化后加一个 clip:x_center = min(max(x_center, 0), 1),四个值都做一遍。

第二个坑:类别拼写。XML 里写的是suspension_clamp,你class_map里写suspension-clamp,转换脚本不报错,但所有目标都被跳过,生成的 txt 全是空的。训练时 loss 一直不降,你还以为是模型问题。血泪经验:转换完先cat一个 txt 看看有没有内容。

第三个坑:空标签。如果一张图里没有悬垂线夹,XML 里没有<object>,转换后 txt 是空文件。YOLO 默认会把空文件当负样本,这是对的。但如果你把空文件删了,那张图在训练时会被当成“没有标签的图”直接跳过,负样本比例下降,模型误检率上升。

第四个坑:路径分隔符。Windows 下 XML 里的<folder>可能是JPEGImages,但<filename>可能带路径。转换脚本里只用文件名,不要用<path>字段,因为那个字段在不同机器上不一样。

4. 用 YOLOv8 训一版悬垂线夹基线:参数怎么设、指标怎么看

4.1 环境准备与数据配置文件

YOLOv8 是目前小目标检测里上手最快的选择,Ultralytics 的 API 也足够简洁。先装环境:

pip install ultralytics

然后写数据配置文件clamp.yaml:

path: /absolute/path/to/dataset train: train.txt val: val.txt names: 0: suspension_clamp

path必须是绝对路径,train和val是相对于path的 txt 文件。names的 key 从 0 开始,和转换脚本里的class_map完全对应。如果这里写错,训练时类别名会显示成0而不是suspension_clamp,不影响训练但影响你读结果。

4.2 训练命令与关键参数

yolo detect train \ data=clamp.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=1280 \ batch=8 \ lr0=0.01 \ lrf=0.01 \ warmup_epochs=3 \ box=7.5 \ cls=0.5 \ dfl=1.5 \ hsv_h=0.015 \ hsv_s=0.7 \ hsv_v=0.4 \ degrees=10 \ translate=0.1 \ scale=0.5 \ fliplr=0.5 \ mosaic=1.0 \ mixup=0.1 \ patience=30 \ project=runs/clamp \ name=baseline

参数逐个说:imgsz=1280是关键,悬垂线夹框小,640 输入下特征图只剩 20×20,小目标几乎消失。1280 输入下特征图 40×40,召回率明显提升,代价是显存翻倍,batch 要降到 8 或 4。box=7.5是框回归损失权重,小目标定位精度要求高,可以适当加大到 8.0~10.0。cls=0.5是分类损失权重,单类别任务可以低一点。mosaic=1.0和mixup=0.1是强增强,能提升小目标泛化,但如果你的数据里线夹经常被遮挡,mosaic 可能把遮挡关系搞乱,可以降到 0.5。scale=0.5允许随机缩放,但注意前面说的:缩放后框不能小于 10 像素,否则模型学不到。

4.3 看指标:mAP50 不是唯一标准

训练完看runs/clamp/baseline/results.csv,重点看四个指标:metrics/mAP50、metrics/mAP50-95、metrics/precision、metrics/recall。悬垂线夹检测里,mAP50 到 0.85 以上算可用,但 mAP50-95 可能只有 0.5 左右,因为小目标的 IoU 很难做高。更关键的是 recall——漏检一个线夹比误检一个代价大得多。如果 recall 低于 0.8,先别调模型,回去检查验证集里有没有训练集没覆盖的拍摄角度。

还有一个指标容易被忽略:val/box_loss和val/cls_loss的曲线。如果 box_loss 震荡不降,说明 anchor 尺寸和你的目标尺寸不匹配。YOLOv8 是 anchor-free 的,但它的正样本分配策略对目标尺寸敏感。可以在clamp.yaml里加anchors参数手动指定,不过 YOLOv8 默认的自动 anchor 在 1280 输入下对 30~80 像素的目标已经够用。

4.4 推理与可视化验证

训练完用yolo detect predict跑几张验证集图片:

yolo detect predict \ model=runs/clamp/baseline/weights/best.pt \ source=dataset/JPEGImages \ conf=0.25 \ iou=0.45 \ save=True \ project=runs/clamp/predict

conf=0.25是置信度阈值,悬垂线夹检测里我一般从 0.25 开始试,如果误检多就提到 0.4,如果漏检多就降到 0.15。iou=0.45是 NMS 的 IoU 阈值,线夹之间距离远,NMS 不会误删,保持默认即可。跑完打开runs/clamp/predict里的图,重点看三类:线夹被导线遮挡的、线夹在图像边缘的、线夹和绝缘子挨在一起的。这三类如果都能框准,模型基本可用。

5. 避坑与排查:悬垂线夹检测的五个常见翻车点

5.1 现象:训练 loss 正常下降,但验证集 mAP 始终为 0

原因:验证集的 txt 文件路径不对,或者val.txt里的文件名和JPEGImages里的实际文件名大小写不一致。YOLO 在验证时找不到标签,把所有预测都当误检,mAP 直接归零。

解决:在clamp.yaml同级目录下跑python -c "from ultralytics.data.utils import check_det_dataset; check_det_dataset('clamp.yaml')",它会打印数据集检查结果,包括找到多少张图、多少标签。如果标签数为 0,就是路径问题。

5.2 现象:模型在验证集上表现很好,但拿新拍的航拍图测试时大量漏检

原因:训练集和验证集来自同一批拍摄架次,图像的光照、角度、背景高度相似。模型学到了“这批图的特征”,而不是“悬垂线夹的特征”。

解决:重新划分数据集,按拍摄日期或线路段分组。如果数据里没有元信息,至少保证验证集里有 20% 的图像是训练集里没出现过的背景类型(比如不同植被、不同杆塔型号)。另外,在训练时加入hsv_h=0.015、hsv_s=0.7、hsv_v=0.4的颜色增强,强迫模型忽略颜色线索。

5.3 现象:小目标漏检严重,大目标正常

原因:输入分辨率太低,或者imgsz设了 640 但目标只有 30 像素。YOLO 的 P3 特征图 stride 是 8,640 输入下 P3 是 80×80,30 像素的目标在 P3 上只有不到 4 个格子,特征太弱。

解决:把imgsz提到 1280 或 1536,同时把batch降到 4。如果显存不够,用yolov8m或yolov8l换yolov8s,大模型对小目标的特征提取能力更强。另一个办法是在数据增强里加copy_paste=0.3,把线夹复制粘贴到不同背景上,增加小目标样本。

5.4 现象:模型把绝缘子或螺栓误检成悬垂线夹

原因:悬垂线夹和绝缘子、螺栓在航拍图里颜色和形状接近,尤其是低分辨率下。训练数据里如果负样本(不含线夹的图)太少,模型没见过“不是线夹但长得像”的样本。

解决:在训练集里加入 10%~20% 的纯背景图(没有悬垂线夹的航拍图),让模型学会区分。另外,在clamp.yaml里把names只保留suspension_clamp一个类,不要加insulator或bolt,否则模型会在多类之间混淆。如果必须多类,确保每类样本量均衡。

5.5 现象:训练到 50 epoch 后 mAP 不再提升,验证 loss 开始上升

原因:过拟合。1600 张图对于 YOLOv8s 来说偏少,尤其是单类别任务,模型容易记住训练集的噪声。

解决:加大数据增强,mosaic=1.0、mixup=0.2、scale=0.7、degrees=15。加weight_decay=0.0005和dropout=0.1(YOLOv8 的dropout参数在分类头里)。如果还不行,换yolov8n小模型,参数量少,过拟合风险低。早停patience=30已经开了,但可以降到 20。

6. 把 1600 张用出 5000 张的效果:小目标增强与半自动标注

6.1 小目标增强:copy-paste 和 mosaic 的组合策略

1600 张图里,悬垂线夹目标可能只有 2000~3000 个,对于检测任务来说偏少。我一般会用 copy-paste 增强:从一张图里抠出线夹区域,随机粘贴到另一张图的导线附近。这样能生成大量“新”样本,而且不改变线夹的视觉特征。

import cv2 import numpy as np import random def copy_paste_aug(img, bboxes, src_img, src_bboxes, p=0.3): if random.random() > p: return img, bboxes for src_bbox in src_bboxes: x1, y1, x2, y2 = map(int, src_bbox) patch = src_img[y1:y2, x1:x2] if patch.size == 0: continue # 随机找一个粘贴位置,限制在图像上半部分(导线区域) h, w = img.shape[:2] ph, pw = patch.shape[:2] if ph >= h or pw >= w: continue paste_x = random.randint(0, w - pw) paste_y = random.randint(0, h // 2) img[paste_y:paste_y+ph, paste_x:paste_x+pw] = patch bboxes.append([paste_x, paste_y, paste_x+pw, paste_y+ph]) return img, bboxes

这段代码的逻辑是:从源图里抠出线夹 patch,随机粘贴到目标图的导线区域(上半部分),同时把新框加入 bboxes。注意粘贴位置要避开图像边缘,否则框会被裁剪。p=0.3表示 30% 的概率执行增强,太高会导致背景不自然。这个增强在训练时在线做,不需要提前生成新图。

6.2 半自动标注:用基线模型预标 + 人工修正

当你有新的航拍图需要标注时,不要从零开始。用训好的基线模型跑一遍预测,把置信度高于 0.5 的框导出成 VOC XML,然后人工只修正漏检和误检。这样标注效率能提升 3~5 倍。

yolo detect predict \ model=runs/clamp/baseline/weights/best.pt \ source=new_images/ \ conf=0.5 \ save_txt=True \ save_conf=True \ project=runs/clamp/prelabel

save_txt=True会生成 YOLO 格式的 txt,save_conf=True会带上置信度。然后写一个反向转换脚本,把 YOLO txt 转回 VOC XML,导入到 labelImg 或 CVAT 里人工修正。修正时重点看两类:置信度在 0.3~0.5 之间的框(模型不确定),以及模型完全没框但人眼能看到的线夹(漏检)。这两类修正完后,把新标注加入训练集,重新训一版,mAP 通常能再涨 3~5 个点。

6.3 验证增强效果:用固定验证集对比

每次做增强或加数据后,必须用同一个验证集对比。我习惯在clamp.yaml里固定val.txt,然后跑:

yolo detect val \ model=runs/clamp/baseline/weights/best.pt \ data=clamp.yaml \ imgsz=1280 \ batch=8

输出里重点看metrics/mAP50和metrics/recall。如果 copy-paste 增强后 recall 涨了但 precision 降了,说明增强引入了太多假阳性,需要降低p或提高conf阈值。如果两个都涨,说明增强有效,可以继续加大比例。

我自己的习惯是:每加一种增强,先跑 20 epoch 看曲线趋势,如果 val loss 在前 10 epoch 就明显低于 baseline,说明增强过强,模型学不到真实分布。这时候把增强概率砍半,再跑完整 150 epoch。这套数据我最终跑到的基线是 mAP50 0.89、recall 0.86,用的是 YOLOv8s + 1280 输入 + copy-paste 0.3 + mosaic 0.8。再往上提,要么加数据,要么换更大的模型,但 1600 张的底子在这里,边际收益已经很低了。希望帮到你。

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

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

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

立即咨询