遥感图像电塔检测数据集:VOC与YOLO格式转换及YOLOv8训练实践

发布时间:2026/10/1 11:07:34
遥感图像电塔检测数据集:VOC与YOLO格式转换及YOLOv8训练实践
简介目标检测模型的落地效果很大程度上取决于标注数据的质量与格式。在电力巡检场景中电塔作为典型小目标在遥感图像中像素占比少且容易与背景混淆对检测算法的精度和鲁棒性提出较高要求。针对此类任务Pascal VOC与YOLO txt是两种最常用的标注格式前者以绝对坐标描述边界框后者使用归一化中心点与宽高二者转换时需严格依据图像实际尺寸计算避免坐标偏移。合理的数据校验与参数配置是训练可靠模型的基础通过YOLOv8在244张小样本遥感图像上进行迁移学习能够快速验证模型在小目标检测上的可行性。本文围绕遥感图像电塔检测数据集系统梳理双格式结构、转换脚本、边界校验及训练避坑要点为电力巡检算法工程师和计算机视觉学习者提供完整的工程实践路径。1. 遥感图像电塔检测数据集244 张图能做什么为什么值得下做电力巡检的目标检测第一步基本都栽在数据上。电塔在遥感图像里往往只占几十个像素绝缘子、铁塔阴影和电线杆又长得像模型误检率常年下不来。这套遥感图像电塔检测数据集一共 244 张 jpg、504 个 dianta 框同时带了 Pascal VOC 的 xml 和 YOLO 的 txt 两种标注解压就能喂给 YOLOv8 或 YOLOv5 训练。适合三类人一是做输电线路巡检算法的工程师二是目标检测方向做毕业设计的学生三是想彻底搞懂 VOC 与 YOLO 格式对应关系、准备自己标数据的人。我按实际拆解的流程把目录结构、格式转换、训练参数和落地坑全过一遍你照着能少折腾一晚上。2. 先把数据集盘清楚VOC 与 YOLO 双格式的目录结构、XML 字段与统计口径2.1 先看目录一个文件名对应三种文件拿到压缩包第一件事是解压看结构不要急着训练。我一般习惯先把 .7z 解压到本地工作目录然后用 find 命令看一眼整体文件组织。这套数据解压后是典型的 labelImg 导出风格JPEGImages 放 firc_yaogan_*.jpg 原图Annotations 放同名 xmllabels 放同名 txt。labels 目录里只有检测框的 txt 文件没有分割路径文件也就是说这套数据只做目标检测不做实例分割。mkdir -p /data/firc_yaogan cd /data/firc_yaogan 7z x firc_yaogan_dataset.7z -o./dataset find dataset -type f | awk -F. {print $NF} | sort | uniq -c解压命令里-o./dataset指的是输出目录注意-o后面紧跟路径、不能有空格否则 7z 会把空格也算进路径里。最后一行是按扩展名统计文件数量正常应该看到 jpg、xml、txt 三类文件数量一致都是 244 个。如果三类文件数量对不上优先怀疑解压不完整或者下载包损坏。2.2 读 XML坐标是绝对值类别名是 diantaVOC 格式的 xml 里一个object节点对应一个标注框核心字段是name和bndbox里的 xmin、ymin、xmax、ymax。这套数据是用 labelImg 画的矩形框所以坐标轴是平行于图像边界的 axis-aligned 框不涉及旋转框这也意味着直接接 YOLO 默认检测头就行不需要上 MMRotate 那套旋转检测框架。annotation folderJPEGImages/folder filenamefirc_yaogan_22.jpg/filename size width768/width height768/height depth3/depth /size object namedianta/name bndbox xmin301/xmin ymin188/ymin xmax345/xmax ymax242/ymax /bndbox /object /annotation注意filename只存了文件名没有存路径所以代码里拼接图片路径时要自己拼目录。size里的宽高决定了后面转 YOLO 格式时的归一化分母如果 XML 里写的尺寸和 jpg 实际尺寸不一致生成的 YOLO txt 坐标一定是偏的这是第一处容易翻车的地方。该数据集标注类别统一为 dianta一个 XML 里可以出现多个 object对应同一张图里多个电塔。2.3 统计框数自己算一遍 504 才放心摘要里写 504 个框但自己动手算一遍这 504 才靠谱。下面的脚本用 Python 标准库解析全部 XML统计每个类别的框数和总框数不需要安装任何第三方包。import os import xml.etree.ElementTree as ET xml_dir dataset/Annotations stat {} total_boxes 0 for fname in sorted(os.listdir(xml_dir)): if not fname.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, fname)) root tree.getroot() for obj in root.findall(object): name obj.find(name).text.strip() stat[name] stat.get(name, 0) 1 total_boxes 1 print(f类别统计: {stat}) print(f总框数 : {total_boxes})这段逻辑很直接ET.parse解析 XML 文件findall(object)拿到所有标注框节点然后对name字段做计数。这个脚本的通用性在于换到别的 VOC 数据集也能直接跑建议把它存成inspect_voc.py留在项目里。如果统计出来 dianta 不是 504先检查是不是解压后有 xml 缺失或者有人在半途重命名过文件。3. 跑通第一轮训练YOLOv8 接数据集的目录整理、data.yaml 与关键训练参数3.1 按 YOLOv8 的约定摆放目录YOLOv8 期望的数据布局是 images 和 labels 两个同级目录labels 里的 txt 与 images 里的 jpg 逐一同名。数据集原始目录并不满足这个约定所以要先做一次整理。我习惯用复制而不是移动因为训练过程如果要把标注改来改去原始 xml 还在原处等于留了后悔药。cd /data/firc_yaogan mkdir -p yolo/images yolo/labels cp dataset/JPEGImages/*.jpg yolo/images/ cp dataset/labels/*.txt yolo/labels/复制完成后数一下两边文件数确保 244 对 244。这里不把原始 VOC 的 Annotations 复制进 yolo 目录因为 YOLO 训练只需要 txtxml 留着做后续校验用。3.2 看一眼 YOLO 标注内容归一化坐标的换算关系YOLO 的 txt 每行对应一个框格式是class_id x_center y_center width height后面四个值全部是 0 到 1 之间的相对坐标。拿训练脚本之前先随机打开一个 txt 确认内容格式。head -3 dataset/labels/firc_yaogan_22.txt看到的内容长这样0 0.4206 0.2799 0.0573 0.0703这一行的含义是类别 id 为 0对应 dianta框中心点在图片横向 42.06% 的位置纵向 27.99% 的位置框宽为图片宽度的 5.73%框高为图片高度的 7.03%。换算回绝对坐标要乘上图片宽高cx, cy, w, h 0.4206, 0.2799, 0.0573, 0.0703 img_w, img_h 768, 768 x1 int((cx - w / 2) * img_w) y1 int((cy - h / 2) * img_h) x2 int((cx w / 2) * img_w) y2 int((cy h / 2) * img_h)归一化坐标和图片分辨率解耦是 YOLO 格式最大的优点模型训练时无论输入尺寸是 640 还是 1024标注相对位置都不变。遇到那种只有绝对坐标的标注数据时转成 YOLO 格式前必须先拿到图片真实宽高这个换算关系后面会反复用到。3.3 data.yaml 与训练命令小数据集的参数取舍YOLOv8 训练需要一份 yaml 告诉框架数据在哪、类别有几个。这里 class 数量必须写 1类别名建议保持 dianta 和标注里的名称一致方便后面看混淆矩阵时对照。path: /data/firc_yaogan train: yolo/images val: yolo/images names: 0: dianta我的习惯是把 train 和 val 先都指到同一份 images第一轮跑通流程之后再去做数据划分。244 张图硬拆出一个独立验证集会进一步减少训练样本电塔框总共 504 个拆完每个类别的正样本数会更紧张所以先全量训练作为基线。启动训练的命令如下cd /data/firc_yaogan yolo detect train \ datadataset.yaml \ modelyolov8n.pt \ epochs150 \ imgsz1024 \ batch8 \ patience30 \ projectruns说几个实际调参的点。imgsz 我直接给了 1024遥感电塔是大图里的小目标用默认 640 会把塔顶那个小三角直接下采样到几个像素后面再怎么调 loss 都救不回来。batch 给 8 是基于 8GB 显存的经验值如果你的卡显存更大可以加到 16但注意小数据集上大 batch 会加速收敛、也更容易过拟合。patience30 表示连续 30 轮验证集指标不提升就早停因为样本少后面很多轮都是震荡早停能救回不少时间。训练过程中重点看 box_loss 和 cls_loss 两条曲线正常情况应该是稳定下降、val 曲线跟随 train 曲线如果 val 曲线在第 80 轮后开始抬头说明已经在过拟合。3.4 可视化第一轮结果先看漏检再看误检训练结束后的 best.pt 存在 runs/detect/train/weights 下拿它跑一遍预测把结果图直接拖出来看。yolo predict \ modelruns/detect/train/weights/best.pt \ sourcedataset/JPEGImages/firc_yaogan_50.jpg \ conf0.25 \ iou0.45conf0.25 是置信度阈值低于 0.25 的框会被丢掉iou0.45 是 NMS 的交并比阈值两个框重叠超过这个值就合并。初跑阶段我习惯把 conf 调低到 0.2宁可多看误检也别漏检漏检往往说明特征没学到误检还能靠调阈值压一压。看预测图时重点找两类现象塔顶被漏掉以及把远处电线杆框进来。4. VOC 转 YOLO 的脚本化校验四类常见故障与参数边界4.1 转换脚本坐标换算的五个关键点虽然这套数据集已经同时给了 xml 和 txt但实际做项目时经常只拿到 VOC 标注。比如巡检队用 labelImg 标了一批新场景导出的只有 xml这时候用现成脚本把 XML 转成 YOLO txt 是省不了的。转换脚本核心要处理五件事读图片宽高、过滤掉不在类别列表里的 object、把 xmin/ymin/xmax/ymax 换算成中心点加宽高的归一化值、控制小数精度、写入同名 txt。import os import xml.etree.ElementTree as ET CLASSES [dianta] def convert_voc_to_yolo(xml_path, txt_path): tree ET.parse(xml_path) 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 CLASSES: print(f跳过未知类别: {name}) continue cls_id CLASSES.index(name) bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) cx (x1 x2) / 2.0 / img_w cy (y1 y2) / 2.0 / img_h bw (x2 - x1) / img_w bh (y2 - y1) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) xml_dir dataset/Annotations txt_dir dataset/labels os.makedirs(txt_dir, exist_okTrue) for f in os.listdir(xml_dir): if not f.endswith(.xml): continue xml_file os.path.join(xml_dir, f) txt_file os.path.join(txt_dir, f.replace(.xml, .txt)) convert_voc_to_yolo(xml_file, txt_file)这段代码里最容易出问题的是图片宽高的读取位置size节点必须在object循环前取一次不要在循环里重复解析。cx的计算除以了 2对应 xmin 和 xmax 的中点分母用图片真实宽高而不是标注文件里可能出现的某个固定值。f{cls_id} {cx:.6f}保留六位小数是 YOLO 训练的通用精度太短会丢坐标细节太长文件体积变大但精度不会再涨。4.2 边界校验归一化坐标和类别号一个都不能错转换完成不等于转换正确必须写一段独立的校验脚本扫一遍全部 txt。校验的重点是四个边界类别 id 是否在区间内、归一化坐标是否都在 0 到 1 之间、框宽高是否大于 0、图片文件和 txt 文件是否一一对应。import os TXT_DIR dataset/labels IMG_DIR dataset/JPEGImages ok_count 0 for f in sorted(os.listdir(TXT_DIR)): if not f.endswith(.txt): continue img_file os.path.join(IMG_DIR, f.replace(.txt, .jpg)) if not os.path.exists(img_file): print(f缺图: {f}) continue with open(os.path.join(TXT_DIR, f), r) as fh: lines fh.read().strip().splitlines() if not lines: print(f空标注: {f}) continue for line in lines: parts line.split() if len(parts) ! 5: print(f格式异常: {f} - {line}) continue cls_id int(parts[0]) vals [float(v) for v in parts[1:]] if not (0 cls_id 1): print(f类别越界: {f} - {line}) if not all(0 v 1 for v in vals): print(f坐标越界: {f} - {line}) if vals[2] 0 or vals[3] 0: print(f宽高为0: {f} - {line}) ok_count 1 print(f校验通过框数: {ok_count})校验脚本跑完如果没有任何输出说明所有框都通过检查。这里的cls_id 1判断是针对该数据集只有 dianta 一个类如果你的项目有多个类这里要改成cls_id len(CLASSES)。坐标越界这条骂过很多次常见原因是 xml 里有 xmax 小于 xmin 的脏数据转换脚本不加保护直接算出来的归一化宽高就是负数。4.3 故障对照表转换后最常见的四类问题用表格把转换环节的高频故障列一下能省去不少排查时间。故障现象原因解决办法训练报 class index out of rangetxt 里类别 id 不是从 0 开始或者类别数大于 yaml 声明检查 txt 首列最小值确认 yaml 里 names 数量大于类别 idmAP 为 0但 loss 正常下降txt 坐标全部为 0一般是表达式里复制粘贴时写错了变量比如忘除以图片宽高随机抽三个 txt 打印首行对照原图人工检查训练时图片数量与标注数量不匹配jpg 和 txt 文件名不完全同名用diff (ls ...)对比两边文件名重命名对齐框的位置整体偏移xml 里 size 写死为 640但实际图片是 768以 jpg 实际尺寸为准动态读取图片宽高这些故障有一个共同点都不会让训练直接崩溃而是让模型在错误标注上默默收敛等你发现时已经跑了几十个小时。所以我的习惯是每次拿到新数据集先跑一遍 4.2 的校验脚本再随机抽 10 张图叠加标注可视化最后才开训练。5. 电塔检测训练避坑清单类别号、小目标与过拟合5.1 训练报错 class index out of range类别号从 0 还是 1 开始刚上手 YOLO 的人特别容易在这个坑里卡半小时。现象是训练开始没几秒就直接报错日志里出现class index out of range后面跟着一个数字。原因绝大多数是 txt 里的类别 id 从 1 开始写而 YOLO 的类别 id 是从 0 开始的如果 yaml 里写了names: {0: dianta}那 txt 里出现 1 就越界了。labelImg 导出 VOC 时类别名是字符串没有 id转换脚本里CLASSES.index(name)拿到的下标一定从 0 开始但有些人手写的转换脚本直接用了int(类别编号) - 1减完之后又没校验。解决方法是先跑一遍校验脚本统计 txt 所有首列的最大值和最小值只要出现大于等于类别数的 id立刻排查转换逻辑。5.2 loss 正常下降但 mAP 上不去小目标被下采样吃掉了电塔在遥感图里属于典型小目标一个塔在 768x768 的原图里经常只有 40x60 像素。现象是训练时 loss 曲线非常漂亮验证集 mAP0.5 却卡在 0.2 上不去可视化预测结果发现塔顶基本没框出来。原因是 imgsz 太小YOLO 在训练时会先把图 resize 到输入尺寸如果设成 64040 像素的电塔就只剩下 30 像素左右再经过 5 层下采样到检测头时特征图上的响应已经微乎其微。解决思路是把 imgsz 调到 1024 甚至 1280同时考虑用 YOLOv8 的 P2 层小目标检测头。显存不够时优先降 batch 而不是降 imgsz小目标数据上 imgsz 的优先级远高于 batch。5.3 误检一堆把电线杆、铁塔阴影都框成 dianta现象是预测结果里大量框落在非电塔物体上尤其集中在电线杆和铁塔阴影区域。原因分两层正样本太少模型没真正学会电塔的判别特征只能靠颜色和长宽比瞎猜负样本几乎没有244 张图里背景比较单一模型没见过电线杆长什么样。解决方法是先调整 conf 阈值看误检能不能压下去不行就从标注上找补。我的做法是从原图里裁一批完全没有电塔的负样本区域单独建一个 background 类别如果不想加负样本至少把训练图里那些容易误检的区域在增强阶段多做随机裁剪。另外检查一下是不是 imgsz 太大导致模型看到了太多细节背景1024 时容易连远处的杆子一起框上退回 640 对比一下误检数量。5.4 训练到 80 轮 val loss 反升244 张图的过拟合边界小数据集训练最典型的现象是前半段一切正常到第 70 到 90 轮之间train loss 还在降val loss 突然掉头往上走然后早停触发。原因很直接模型的参数量远大于有效样本能支撑的信息量它在反复记忆训练图里电塔旁边的云彩、山地纹理这些背景特征。解决的优先级我一般按这个顺序试先把 epochs 降到 100 配合 patience20让早停更敏感然后把 mosaic 和 mixup 增强的强度调高一档等价于变相扩充样本最后考虑换更小的模型yolov8n 换成更轻的版本或者用预训练权重做迁移学习而不是从头训练。预训练权重在这个场景里帮助很大yolo 官方提供的 yolov8n.pt 是在 COCO 上训过的骨干网络对纹理和边缘的通用特征已经学到微调 244 张图比从头训练稳很多。5.5 txt 里有框但训练时显示 0 张有效图文件名错位和时间戳陷阱这个坑最隐蔽。现象是翻开 labels 目录每个 txt 都有内容但训练启动时日志显示0 images或者训练了但 loss 一直不动。原因大概率是 jpg 和 txt 的文件名有一小部分不严格同名比如从微信或网盘传过一次后文件名被加了时间戳后缀jpg 是 firc_yaogan_22.jpgtxt 却变成了 firc_yaogan_22(1).txt。YOLO 框架按文件名首部对齐对不上的那份标注被静默跳过。解决方法是写一个对齐脚本把 txt 文件名强制改成 jpg 同名后用 diff 对比两边文件名集合多一个少一个都输出到日志。从那以后我每次训练前都会先把文件名对齐跑一遍这条规则已经救了我至少三次。希望帮到你。6. 从 244 张到可用模型增强可视化与验证技巧6.1 增强参数要克制先可视化再开跑小数据集几乎必须依赖数据增强但增强开过头会把电塔变成不像电塔的东西比如旋转 90 度后塔顶和塔基的位置关系完全不成立。我给这个数据集的建议是打开 hsv_h、hsv_s、hsv_v 三个颜色增强和 scale 缩放关掉 fliplr 之外的强几何增强mosaic 和 mixup 根据 5.4 的情况再决定开多大。改完增强参数后先跑一遍可视化把增强后的图逐张看看到有扭曲得不像电塔的样本就减小对应参数。yolo detect train datadataset.yaml modelyolov8n.pt \ epochs100 imgsz1024 batch8 \ hsv_h0.015 hsv_s0.7 hsv_v0.4 \ mosaic0.8 mixup0.2 fliplr0.5增强参数的意义在于它们共同决定了模型见过的所有变体。hsv_h 控制色调变化幅度遥感图像里电塔颜色偏灰给 0.015 就够给太大模型会把颜色当成噪声源scale 默认 0.5 表示每轮训练图会被随机缩放到原尺寸的 50% 到 150%电塔小目标为主建议保留默认不要调更大。6.2 验证不要只看 mAP0.5电塔要看 mAP0.5:0.95 和定位误差电塔检测有一个特殊性框稍微偏一点在视觉上就能看出来但 mAP0.5 对框位置的容忍度太高IoU 到 0.5 就还算命中导致 mAP0.5 高得好看实际部署却发现框总是飘在塔基旁边。我建议验证阶段同时记录 mAP0.5:0.95以及每个类别的 precision 和 recall这个小数据集只有 dianta 一个类直接看单类曲线就行。如果 mAP0.5 有 0.8 而 mAP0.5:0.95 只有 0.3说明框位置精度差这时候优先考虑调高 loss 里 box_loss 的权重而不是继续加增强。电塔这种小目标场景单张图里出现两三个塔很常见验证时还要看一眼 nms 之后的重复框率。重复框高说明模型对塔顶和塔身各自产生了响应目标盖住后 NMS 没有把它们合并干净这种情况调 iou 阈值比调 conf 更有效。从那以后我每次拿到新的检测数据集都会先跑一遍格式校验、增强可视化和双指标评估这三板斧再决定要不要动模型结构。希望帮到你。本文还有配套的精品资源点击获取