ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

244张遥感电塔数据集:VOC转YOLO并用YOLOv8训练完整实践

244张遥感电塔数据集:VOC转YOLO并用YOLOv8训练完整实践 简介遥感电塔检测数据集面向目标检测学习者、算法工程师以及电力巡检相关研究人员可直接用于模型训练与算法验证。资源包共734个文件以244张JPG原图、Pascal VOC格式XML标注和YOLO格式TXT标注为主体压缩包整体78.4MB另含少量说明性文本文件。数据集聚焦单一类别“dianta”由labelImg工具统一绘制矩形框累计504个标注框标注规则明确支持直接接入YOLO、Faster R-CNN等主流检测框架省去自行整理数据和转换格式的时间成本。目前已有281人学习适用于遥感地物识别、电力设施智能巡检等场景既适合初学者熟悉检测流程也可作为中高级研究者验证算法的可靠数据基础。1. 244 张电塔遥感图标注成 VOCYOLO 双格式这个小数据集到底能干什么做电力巡检或遥感目标检测容易被一个不痛不痒的问题卡住手上连一张带标注的电塔影像都没有。这个标题给的是一个很典型的“小而专”数据包——244 张遥感图像只标了电塔一个类别同时给了 VOC 的 XML 标注和 YOLO 的 TXT 标注.7z 打包。价值不在量大而在于把「电塔检测」的最小闭环备齐了数据、格式、训练入口都有。适合验证算法思路、练手目标检测流程更适合刚入门 YOLO 的人当一个不会卡爆显存的起步数据集。想直接上生产精度会失望想跑通流程、看清坑244 张刚刚好。2. VOC 和 YOLO 两种标注格式并存不是冗余XML 负责可读TXT 负责直给2.1 打开 XML 看一类标注bndbox 像素坐标和 object 结构VOC 格式诞生于 PASCAL VOC 竞赛它的标注以“一张图一个 XML 文件”的方式存在文件夹通常叫Annotations图片放在JPEGImages。打开一个电塔标注的 XML结构大致是这样annotation filenameIMG_0001.jpg/filename size width1280/width height720/height depth3/depth /size object nametower/name bndbox xmin312/xmin ymin188/ymin xmax396/xmax ymax272/ymax /bndbox /object /annotation这个 XML 里最有用的三块信息是filename告诉模型图片文件名size里的宽高是做坐标归一化时必需的bndbox里的xmin/ymin/xmax/ymax是电塔外接框的绝对像素坐标。注意这里的坐标是“左上角 右下角”的表示法一个人读起来很直观我从第 312 个像素开始框到第 396 个像素。手工检查、修改、写脚本过滤都很方便这也是很多标注工具默认导出 VOC 的原因。2.2 打开 TXT 看另一类归一化中心点和宽高训练直接吃YOLO 格式则是把同样的标注压成了一行五个数字。拿上面那个 XML 对应的目标来说转换后写到labels/IMG_0001.txt里的一行是0 0.2766 0.3194 0.0656 0.1167五个数字依次是类别 id、归一化后的框中心点 x、归一化后的框中心点 y、归一化后的框宽、归一化后的框高。312/1280≈0.2438和396/1280≈0.3094这两个 x 方向的边界值不是直接写进去的而是换算成中心点(0.24380.3094)/2≈0.2766宽度是(0.3094-0.2438)/1280≈0.0656。这种表示的优点是尺度无关图是 1280 宽还是 640 宽标注都落在 0 到 1 之间。训练时 YOLO 的加载器会直接读这些归一化数值叠加到输入图上不需要再关心原图的绝对尺寸。VOC 和 YOLO 两种格式并存对使用者来说真正的好处是分工不同VOC 适合人工核对和二次修改TXT 适合直接喂给训练脚本。很多公开数据集只给一种格式要么得自己找转换脚本要么得忍受训练框架读不进去。这个数据包把两边都给齐省掉的第一件事就是格式适配。下面这张表可以快速看出两者的差别对比项VOC XMLYOLO TXT坐标类型绝对像素坐标xmin, ymin, xmax, ymax归一化相对坐标中心点 x, 中心点 y, 宽, 高每张图对应一个 .xml 文件一个 .txt 文件人可读性好标签名和坐标都是明文一般五个数字不直观训练加载需要转换器或自定义 Dataset原生支持几乎所有 YOLO 版本直接读类别表达object/name 字符串如 tower每行首位的整数 class_id2.3 电塔在遥感图里是什么样小目标、俯视视角、阴影干扰知道格式只是第一步真正决定模型效果的是数据本身的特点。遥感图像里的电塔和自然场景照片里的电塔是完全两种目标。自然场景里电塔通常是侧视角塔身线条完整、和背景分离度高遥感图像是俯视视角电塔在图上往往只占几十到一两百个像素是一个带交叉结构的“小点”。塔身的投影、阴影会拉出一条长影和道路、田埂、树木的影子混在一起这就是电塔检测里最典型的背景干扰。另一个容易被低估的现象是形态重复度高。同一条输电线路上的电塔结构相似、朝向相近模型容易记住“这一种塔的样子”而不是“电塔这一类目标的共性”。这在小数据集上尤其危险244 张图如果全部来自同一段线路训练出来的模型换一个区域基本就废。横向对比一下车牌检测的 CCPD、遥感旋转框检测的 HRSC2016 这类任务专用数据集都存在同样的“场景集中、目标模式单一”的问题业内对它们的处理套路也一致不追求样本量堆出来的通用性而是用迁移学习把预训练模型的泛化能力搬过来再用小样本微调适配这个具体场景。这正是一份 244 张的电塔数据集应该走的路线。3. 把 VOC 转成 YOLO 格式坐标归一化脚本与目录结构核对3.1 转换前先摸清三件事目录、类别名、图片尺寸拿到压缩包第一步是解压。7z 格式在 Windows 上用 7-Zip在 Linux 上直接命令行解压7z x power_tower.7z -opower_tower解压后常见做法是看到两个顶层目录一个按 VOC 习惯组织Annotations/、JPEGImages/、ImageSets/Main/另一个按 YOLO 习惯组织images/、labels/。如果压缩包里只给了 VOC 一侧YOLO 侧目录是空的那就走一遍转换流程如果两侧都有也要抽查几个文件确认内容对应因为不少数据集在制作时先标注成 VOC再批量转 YOLO中间偶尔会漏掉个别文件。转换前我一般先做三件事。第一统计 XML 里到底有哪些name写一句命令就能看全grep -h name Annotations/*.xml | sort | uniq -c第二确认图片后缀是.jpg还是.png这影响后面脚本匹配filename字段。第三随机打开几张原图看分辨率。遥感影像有大有小如果混着 6000×4000 的大图和 800×600 的截图转换脚本没问题但后续训练时要小心这一点留到第 5 章展开。3.2 完整转换脚本XML 里的 bndbox 变成一行五个数VOC 转 YOLO 的核心逻辑不复杂读 XML、取 bndbox、除以图片宽高做归一化、按class_id cx cy w h写入 txt。真正容易出错的是边界条件处理。下面是我常用的转换脚本# voc_to_yolo.py # 用法: python voc_to_yolo.py --voc_dir ./VOC --out_dir ./YOLO import os import argparse import xml.etree.ElementTree as ET from pathlib import Path import shutil def convert_one(xml_path, img_dir, out_img_dir, out_label_dir, class_map): 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) img_name root.find(filename).text stem Path(img_name).stem src_img os.path.join(img_dir, img_name) dst_img os.path.join(out_img_dir, img_name) shutil.copy(src_img, dst_img) out_lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_map: print(f[skip] {img_name} 里出现未定义类别: {name}) continue class_id class_map[name] box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 归一化先换成中心点和宽高再除以图片宽高 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 # 手工标注偶尔会出界clip 到 [0, 1] x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) # 宽或高为 0 的退化框直接跳过否则训练时会被判定为非法标签 if w 0 or h 0: print(f[warn] {img_name} 里存在空框已跳过) continue out_lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) # 一张图对应一个同名 txt写完后一行一个目标 label_path os.path.join(out_label_dir, stem .txt) with open(label_path, w) as f: f.write(\n.join(out_lines)) if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--voc_dir, requiredTrue, helpVOC 根目录内含 Annotations 和 JPEGImages) parser.add_argument(--out_dir, requiredTrue, helpYOLO 输出目录) args parser.parse_args() xml_dir Path(args.voc_dir) / Annotations img_dir Path(args.voc_dir) / JPEGImages out_img_dir Path(args.out_dir) / images out_label_dir Path(args.out_dir) / labels out_img_dir.mkdir(parentsTrue, exist_okTrue) out_label_dir.mkdir(parentsTrue, exist_okTrue) # 单类别数据集key 必须和 XML 里的 name 完全一致 class_map {tower: 0, power_tower: 0, 电塔: 0} xml_files sorted(xml_dir.glob(*.xml)) for xml_path in xml_files: convert_one(str(xml_path), str(img_dir), str(out_img_dir), str(out_label_dir), class_map) print(f转换完成共处理 {len(xml_files)} 个标注文件)脚本逻辑分四段。第一段从 XML 里取图片尺寸和文件名这是归一化的分母漏了或者类型转错会直接抛异常。第二段遍历object把bndbox的四个值从“左上角右下角”换算成“中心点加宽高”再各自除以图片宽高得到 0 到 1 之间的浮点数。第三段是保护逻辑坐标出界的做 clip宽高为 0 的跳过未定义的类别名打日志但不中断整个转换。最后按原图同名写入 txt 文件一行一个目标。几个参数要按实际情况改class_map的 key 必须和 XML 里的name字符串完全一致差一个大小写都会导致这个目标被静默跳过输出目录里的images/和labels/是后续训练时 YOLO 默认约定的目录名别改成别的名字。如果你发现转换后某张图缺 txt第一反应就是去看它在 XML 里的name是不是没进class_map。3.3 转换后的自检用一条命令扫出坐标越界转换完成不代表数据干净。我会写一个几行的检查脚本扫一遍所有 txt验证三件事每行是不是 5 个数、class_id 是不是在nc范围内、四个浮点数是不是都在 [0, 1] 区间。# check_labels.py # 检查 labels 目录下所有 txt 的格式与数值范围 from pathlib import Path label_dir Path(YOLO/labels) bad_count 0 for txt in label_dir.glob(*.txt): for line in txt.read_text().strip().splitlines(): parts line.split() if len(parts) ! 5: print(f[err] {txt.name} 行数不对: {line}) bad_count 1 continue cid, cx, cy, w, h parts if int(cid) ! 0: print(f[err] {txt.name} 类别 id 越界: {cid}) bad_count 1 for v in (cx, cy, w, h): if not (0.0 float(v) 1.0): print(f[err] {txt.name} 数值越界: {line}) bad_count 1 break print(f检查完成异常数量: {bad_count})这个脚本会打印所有异常行的文件位置和内容。一个比较隐蔽的问题是坐标越界有些标注工具的自动标注结果会把框的边界推出图像边缘YOLO 训练时虽然能容忍轻微越界但严重出界会导致 loss 异常跳变。这里顺手再把 class_id 限定在 0因为标题写的是 1 个类别训练配置里nc1时只有 id 0 是合法的。4. 用 YOLOv8 跑通第一批电塔检测模型数据集 YAML 与训练参数调整4.1 先把数据按 train/val 分开固定随机种子再动手244 张图不能全量丢去训练至少留出 20% 左右当验证集。这是最容易随手做错的一步很多人会先把图片分成两份再把 labels 也手动挪结果两边文件对不上。我的做法是写一个脚本同时处理 images 和 labels# split_data.py # 按固定随机种子划分 train/valimages 和 labels 同步移动 import random from pathlib import Path random.seed(42) # 固定种子保证每次划分结果一致 img_root Path(YOLO/images) label_root Path(YOLO/labels) train_img img_root / train val_img img_root / val train_label label_root / train val_label label_root / val for d in (train_img, val_img, train_label, val_label): d.mkdir(exist_okTrue) # 此时 images/ 下还是平铺的图片还没建子目录 imgs sorted(list(img_root.glob(*.jpg)) list(img_root.glob(*.png))) random.shuffle(imgs) val_ratio 0.2 val_num int(len(imgs) * val_ratio) for img in imgs[:val_num]: img.rename(val_img / img.name) label_file label_root / (img.stem .txt) if label_file.exists(): label_file.rename(val_label / label_file.name) for img in imgs[val_num:]: img.rename(train_img / img.name) label_file label_root / (img.stem .txt) if label_file.exists(): label_file.rename(train_label / label_file.name) print(ftrain: {len(imgs) - val_num}, val: {val_num})这个脚本的关键点是用stem不含后缀的文件名去关联 images 和 labels保证图片挪走的同时标注跟着走随机种子固定成 42后续你想复现训练结果划分结构才不会变。244 张按 0.2 的比例分出约 49 张验证集训练集约 195 张这个量级对单类目标检测来说是偏小的但用来判断流程通不通、观察 loss 走势已经够了。4.2 写 data.yaml 和最小训练命令244 张图用什么预训练权重YOLOv8 系列用data.yaml描述数据集路径和类别信息。单类别数据的 yaml 内容非常简洁# power_tower_data.yaml path: /data/power_tower/YOLO # 改成你自己的数据集根目录 train: images/train val: images/val nc: 1 names: 0: power_towerpath是根目录train和val相对于根目录nc必须是 1names里 0 对应的字符串只影响日志显示和输出标签名。这里最容易翻车的点是路径写法如果把path留空train和val就要写绝对路径或相对当前命令运行目录的相对路径写错一半的报错信息是“Dataset not found”并不提示是哪个字段错了。训练命令本身不长我把常用参数全部显式写出来yolo detect train \ datapower_tower_data.yaml \ modelyolov8n.pt \ epochs150 \ imgsz640 \ batch16 \ patience30 \ project./runs \ namepower_tower_exp244 张图配yolov8n.pt是稳妥选择。n 是 nano 版本参数量最小不容易在少量数据上过拟合直接用yolov8x.pt这种大模型195 张训练图根本喂不满验证集 loss 会一路走高。imgsz640是 YOLOv8 的默认输入尺寸但如果原图分辨率特别大这里要斟酌具体见第 5 章。patience30表示验证集指标连续 30 轮不提升就早停小数据集上这个值可以设得大一点因为 loss 曲线在 150 轮里经常是台阶式下降设太小会在还没降到谷底时提前停掉。4.3 训练输出怎么读loss 曲线、mAP50 和过拟合信号训练启动后Ultralytics 会在终端实时打印每个 epoch 的指标同时在runs/power_tower_exp/下画出results.png。小样本训练时我主要盯三个信号。第一个是train/box_loss和val/box_loss的走势两者都下降说明模型在正常学习如果 train 降但 val 不降过拟合已经开始了。第二个是metrics/mAP50这是交并比阈值 0.5 下的平均精度单类数据集里它比 mAP50-95 更直观也更稳定mAP50-95 在少量样本下会被少数几个难例拉得很低参考价值有限。第三个是验证集上的预测结果图训练到一半把save_period设成 10隔几个 epoch 就能翻出一批预测图肉眼检查框有没有套住塔身、有没有把阴影或者树冠识别成电塔。YOLO 的损失函数由分类损失、边界框回归损失和置信度损失三部分组成终端日志里会拆开显示。如果某个分量出现 NaN优先怀疑数据问题而不是网络结构问题比如标注里出现了无穷大的归一化坐标。这个排查顺序能省掉很多不必要的大改。首轮训练我只求“跑通 看走势”不会为了刷一个好看的 mAP 去反复调参。5. 少样本电塔检测的 5 个翻车现场标注、划分与过拟合排查5.1 现象mAP 很高但新图完全检测不到目标原因出在训练集和验证集重叠第一次跑完这个数据集终端里 mAP50 刷到 0.9 以上我高高兴兴拿一张没见过的电塔图去测结果一个框都没出。回头查数据才发现划分脚本用的随机种子不固定跑一次变一次某次划分后验证集里混进了和训练集完全一样的图。模型的“高分”是在背答案不是真学会了。原因有两个层面。一是划分脚本没固定随机种子每次执行结果不同二是 244 张图里如果有连续帧比如同一段航线拍出的多张相邻图人和模型看着像不同图但画面内容高度重叠随机划分很容易把同一场景拆到两边。解决方法是固定随机种子并检查验证集和训练集里有没有特征重复的图。最简单的查法是计算验证集图片的感知哈希perceptual hash相似度超过阈值的疑似重复直接剔除。小数据集上这一步比调任何训练参数都重要。5.2 现象训练刚启动就报 class number is out of range原因是类别 id 对不上训练到第一个 batch 时终端打出一行警告WARNING labels are found in image ... class number is out of range。去看那个 txt发现第一列写着 1而data.yaml里nc1合法类别只有 0。这种现象常见于有人手动改过 txt或者转换脚本的class_map里写了两类但数据分布不均比如把tower映射成 0、power_tower映射成 1但漏改了nc。解决方式很直接回到第 3 章的检查脚本把 label 里出现过的所有第一列数值打印出来确保最大值等于nc - 1。如果用的是我前面给出的转换脚本检查 XML 里的name是不是在class_map里被写进了同一个 id 即可。5.3 现象训练日志出现 zero width/height原因是手工标注的退化框训练时偶尔能看到类似WARNING ignoring zero width/height的提示出现频率不高容易被忽略。这些退化框来自标注环节目标太小时标注员双击鼠标框的左上角和右下角落在同一个点上宽或高变成了 0。模型遇到这种标注不会崩但 loss 会异常抖动因为回归目标里混进了无意义的 0 值。解决方法是转换阶段就拦截第 3 章脚本里专门判断了w 0 or h 0就跳过并且打印 warn 日志。如果你是拿到别人已经转好的 YOLO 格式就写一条命令把所有 txt 里的第 4、5 列检查一遍出现 0 就把对应行删掉。这个小动作能消掉训练日志里大部分莫名其妙的不稳定因素。5.4 现象loss 正常下降但验证集永远没有检测框原因是整幅遥感图被 resize 压没了这个翻车现场最隐蔽。loss 曲线漂亮地下降模型权重正常保存但跑验证集时所有图片的检测框数量都是 0。问题不在于训练在于输入尺寸。假设原图是 6000×4000 的大幅遥感影像电塔在图上只有 40×40 像素imgsz640会把整幅图直接缩放到 640×640此时电塔缩成了约 4×4 像素特征完全消失模型当然找不到目标。解决思路有两个按代价从低到高排列。一是把imgsz提到 1280 甚至 1536YOLOv8 训练和推理都可以用大分辨率输入代价是显存占用上涨、训练变慢二是先做滑窗裁剪把大图切成 640×640 的瓦片再训练让电塔在裁剪后的图中占更大的比例。244 张图的数据集如果原图都是大幅影像我会优先选瓦片方案因为这会顺便把样本数增多用数据增强的思路缓解小样本问题。5.5 现象mosaic 开太猛小目标在拼接中被裁掉增强开成了负作用YOLOv8 默认开启 mosaic 数据增强训练时每张输入图由 4 张图随机裁剪拼接而成。这对大目标、自然图像很有效但用在遥感小目标上容易出问题一块拼接区域里电塔可能被随机裁剪切成两半标注框只剩 1/4 在画面里模型看到的是大量残缺目标。244 张小样本数据再叠上这种噪声验证集 mAP 反而会掉到 0.1 以下。解决办法是保守地调整增强参数。在训练命令里追加mosaic0.3、scale0.3把拼接概率降下来同时把hsv_h0.01、hsv_s0.5调小避免颜色扰动过度。每改一次增强参数跑完一轮后对比验证集 mAP50 和 loss 曲线别迷信默认值也別一次改到底。遥感图还有一个先天优势俯视视角下翻转不改变语义fliplr0.5、flipud0.5可以放心开这是少样本电塔数据集里为数不多安全且有效的增强手段。6. 244 张图练到能用的收尾技巧两阶段迁移训练、增强配比与验证取舍6.1 先冻结骨干再解冻少样本迁移的标准两步走244 张图不足以让模型从头学到通用的遥感特征但预训练权重里已经包含大量边缘、纹理、形状知识。我常用的做法是两阶段训练第一阶段冻结骨干网络只训练检测头让模型先记住“电塔这个类别的输出长什么样”第二阶段解冻全部用小学习率微调整个网络。第一阶段命令在原有基础上加一个freeze10yolo detect train \ datapower_tower_data.yaml \ modelyolov8n.pt \ epochs80 \ imgsz640 \ batch16 \ freeze10 \ lr00.0180 轮后用第一阶段生成的runs/power_tower_exp/weights/best.pt作为第二阶段起点去掉freeze把lr0降到 0.001再训练 50 轮。这套流程对少样本遥感目标检测的效果很稳定因为它把“适应新数据”拆成了两个阶段先解决头的收敛问题再慢慢解冻骨干防止灾难性遗忘。如果你只跑单阶段大概率会遇到验证 metrics 在某个 epoch 后突然跳水那就是骨干在大量新样本下被破坏的典型表现。6.2 验证取舍mAP50 为主、漏检率为辅244 张数据上追求 mAP50-95 没有太大意义它会被少量难例明显拉低而且这个数据集的定位本来就是预研和流程验证。我建议把 mAP50 当作主指标同时特别关注漏检率电塔检测的实际应用场景里漏掉一个塔比多框一个目标严重得多。方法是在验证集上跑一次yolo predict保存结果图肉眼统计“有塔但没框”的图片比例。模型即便 mAP50 只有 0.7只要漏检率低于 5%对这个体量的数据集来说就是可以接受的成果。反过来说如果 mAP50 很高但漏检率也高说明模型只学会了它见过的那几座塔的形态泛化能力并不真实。我现在的习惯是接手任何小样本数据集先做三件事查验证集重叠、查空框、查类别 id 范围。这套检查花不了十分钟却能把后面训练里的玄学问题砍掉大半。244 张电塔图做不出一个全能的生产模型但它能帮你在一个下午之内把这个方向的路走通把格式、训练、评估的坑都踩平。等你拿到几百上千张同场景数据时今天的这些流程积累会比那 244 张图本身更值钱。希望帮到你。本文还有配套的精品资源点击获取
返回列表