简介:面向遥感图像目标检测的“光伏发电”识别数据集,采用Pascal VOC与YOLO双格式标注,适用于YOLO系列、Faster R-CNN等检测模型训练与验证。内含271张JPEG原图,及一一对应的271个XML、271个TXT标注文件,XML记录Pascal VOC格式的框坐标,TXT提供YOLO格式的归一化坐标,类别统一为guangfu,共标注722个矩形框,可用于光伏电站自动识别、土地利用监测等场景。压缩包共815个文件,除图片与双格式标注外,还含少量辅助文本,整体约132.45MB,目录结构规整,文件名形如firc_guangfu_xxx.jpg,便于直接接入主流检测框架。目前已有212人学习,标注工具为labelImg,人工按统一规则绘制,适合学术实验、课程设计或工程初期的数据准备。需要说明的是,数据集只保证标注合理准确,不附带训练权重或精度承诺,模型效果需使用者自行验证,可结合数据增强或迁移学习提升性能。
1. 遥感光伏检测数据集只有271张:先搞清楚它够不够用
接手光伏发电检测这个方向时,最常见的尴尬不是模型选型,而是手里数据少得可怜。271张遥感图像、1个类别、VOC和YOLO两种标注格式,这套数据集对新手来说像是一块敲门砖:能跑通完整流程,但也容易让人误判自己的模型已经“能用”。真实场景里,光伏板在遥感影像中往往只有几十到几百像素,屋顶分布式电站和地面集中式电站的形态差异很大,271张图只能覆盖其中很小一部分分布。
所以这篇文章不是劝你“有数据就能训练”,而是讲清楚拿到这样一份VOC+YOLO双格式数据集之后,怎么解压、怎么校验、怎么转换、怎么训练、哪些地方会翻车。适合刚入坑目标检测、打算用yolov8或yolov5做遥感地物识别的从业者。271张的体量决定了它更适合做流程验证和基线实验,不太适合直接上生产。
2. 读懂VOC与YOLO两种标注格式:目录结构、xml字段与归一化坐标
2.1 VOC格式的目录约定:JPEGImages、Annotations与xml的五个关键字段
Pascal VOC格式是目标检测领域最经典的数据交换格式,很多数据集发布者选择VOC作为“母版”。常见目录结构是三个文件夹加一个划分文件:
VOCdevkit/ VOC2007/ JPEGImages/ # 原始图片,jpg或png Annotations/ # 每个图片对应的xml标注 ImageSets/ Main/ # train.txt / val.txt 等划分文件JPEGImages里放图片,Annotations里放同名xml。xml的body结构有五个字段必须读懂:
<annotation> <folder>JPEGImages</folder> <filename>pv_001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>solar_panel</name> <bndbox> <xmin>420</xmin> <ymin>315</ymin> <xmax>640</xmax> <ymax>510</ymax> </bndbox> </object> </xml>folder和filename告诉解析器图片放在哪里、叫什么名字;size记录原图宽高和通道数,转换坐标时必须依赖它;object可以出现多次,每个object对应一个目标实例,name是类别名,bndbox里的四个值是像素坐标,左上角原点。多目标场景下,一个xml里会有多个<object>块。
这套格式的优点是树状结构容易解析,缺点也很明显:文件多、冗余字段多、磁盘占用大。271张图的xml全部打开可能不到一兆,但如果是上万张的数据集,xml的读写开销会让人头疼。
2.2 YOLO格式的txt标注:六个小数和一个永远为0的class_id
YOLO格式是Darknet系沿用至今的标注格式,yolov5和yolov8都直接支持。它没有xml,每个图片对应一个同名的txt文件,每行代表一个目标:
0 0.552083 0.381944 0.114583 0.180556 0 0.781250 0.472222 0.083333 0.138889五个字段依次是:class_id x_center y_center width height。前四个坐标值全部做了归一化,除以图片宽或高,取值范围是0到1。类别ID从0开始计数,这套数据集只有1个类别,所以class_id恒为0。
归一化坐标的好处是resize图片后标注依然有效,因为框是相对位置。比如原图宽1920,xmin=420,xmax=640,归一化的x_center就是(420+640)/2/1920≈0.276。yolo训练时会把图片缩放到固定尺寸(如640x640),标注框跟着等比缩放,不会错位。
2.3 双格式交付的实际意义:省掉的不只是转换时间
数据集同时给了VOC和YOLO两种格式,有人觉得多余,其实这背后是生态割裂的现实。老牌检测框架如detectron2、mmdetection常用COCO或VOC格式,而yolov5、yolov8、YOLOX这些新势力统一吃txt标注。拿到双格式,意味着无论你进哪个技术栈都不用先写转换脚本。
之前接过一个燃气管道图像检测的活,对方只给了LabelImg标注的VOC文件,而项目组用的是yolov5。光写转换脚本就花了大半天,还因为坐标取整问题丢了几百个框。所以一套带VOC+YOLO双格式的数据集,省掉的是最枯燥、最容易出错的工程环节。不过省归省,你仍然要了解两种格式的对应关系,否则校验阶段看不出问题。
3. 从7z到yolov8能跑的目录:解压校验与组装的最小步骤
3.1 用7z命令解压数据集:参数说明与权限坑
7z压缩比高于zip,遥感图像动辄几千像素的jpg用7z能压掉不少体积。Linux下解压需要安装p7zip-full:
# Ubuntu/Debian系 sudo apt install p7zip-full # 解压到指定目录,-o后面不能有空格 7z x solar_pv_dataset.7z -o/home/user/datasets/ # 查看压缩包内容,不解压 7z l solar_pv_dataset.7zx表示解压并保留目录结构,-o指定输出路径。注意-o和路径之间没有空格,写反了会提示参数错误。Windows用户装个7-Zip官方版,右键解压就行。
常见问题是解压到一半提示“Cannot open file as archive”——多半是压缩包下载不完整。先比对一下文件MD5或者7z t测试压缩包完整性:
# 测试压缩包是否损坏 7z t solar_pv_dataset.7z如果输出Everything is Ok说明压缩包没问题。这一步不要跳过,网盘下载的压缩包经常在传输中丢了字节。
解压完成后先别急着训练,我有一次直接跑yolov8的train.py,报错一大堆,回头才发现是目录结构不对。先花两分钟看目录长什么样:
# 看目录树,限制两层深度 tree -L 2 /home/user/datasets/solar_pv/3.2 解压后先做三件事:数量核对、图片完整性、标注可解析
拿到解压目录后,第一步不是看图片,而是数文件数量。标题说271张,那JPEGImages里就应该是271张图,Annotations里是271个xml。多一张少一张都要追查:
# 统计图片和标注数量 find /home/user/datasets/solar_pv/JPEGImages -name "*.jpg" | wc -l find /home/user/datasets/solar_pv/Annotations -name "*.xml" | wc -l第二步是检查图片有没有损坏。远程传输过程中jpg头部可能损坏,训练时cv2.imread返回None,DataLoader直接崩。用Python批量验证:
import cv2 import glob img_paths = sorted(glob.glob('/home/user/datasets/solar_pv/JPEGImages/*.jpg')) broken = [] for p in img_paths: img = cv2.imread(p) if img is None: broken.append(p) print(f"共 {len(img_paths)} 张图,损坏 {len(broken)} 张") for p in broken: print("[损坏]", p)cv2.imread读不回来就是文件损坏或格式伪装。有些下载工具会把网页存成jpg,实际内容是HTML文本,imread读到None,这种必须删掉。
第三步是验证xml能否被解析,以及xml里的filename字段与实际的图片文件名一致。用一段脚本抽查即可:
import os import xml.etree.ElementTree as ET ann_dir = '/home/user/datasets/solar_pv/Annotations' img_dir = '/home/user/datasets/solar_pv/JPEGImages' err = [] for xml_name in os.listdir(ann_dir): xml_path = os.path.join(ann_dir, xml_name) tree = ET.parse(xml_path) root = tree.getroot() fn = root.find('filename').text if not os.path.exists(os.path.join(img_dir, fn)): err.append((xml_name, fn)) if err: print("标注文件名与图片不一致:") for xml_name, fn in err: print(f"{xml_name} -> {fn}") else: print("所有xml引用的图片都存在")3.3 按yolov8的数据要求组织image与label目录
yolov8和yolov5一样,期望的目录结构是:
dataset/ images/ train/ val/ labels/ train/ val/不管拿到手的目录是VOC风格还是YOLO风格,最终都要转成上面这个形态。如果解压出来已经是VOC+YOLO双格式,那YOLO侧通常自带images和labels。这时要检查yolo标注的txt文件名是否和图片名一一对应:
# 找出没有txt的jpg,或没有jpg的txt cd /home/user/datasets/solar_pv for img in images/train/*.jpg; do base=$(basename "$img" .jpg) if [ ! -f "labels/train/$base.txt" ]; then echo "缺失标注: $img" fi done这一步是很多人跳过之后付出惨痛代价的地方。yolov8训练时如果图片存在但标注缺失,不会报错,只是那张图会被自动当成背景图,导致正样本数量进一步缩水。271张的训练集本身就很小,再丢几张,mAP能掉两个点。
提示:如果发现缺失标注,别急着手工补框。先看是不是文件名后缀不一致,比如图片是
.png而脚本只用.jpg匹配。处理后缀问题可能就全对齐了。
4. 把VOC转成YOLO:转换脚本与四个边界坑
4.1 写一个最小可用的VOC转YOLO脚本
虽然这套数据集带了YOLO格式,但你仍然需要掌握转换方法。理由很现实:你自己的标注数据大概率是VOC或COCO格式,转YOLO是必经之路。下面这个脚本足够跑通单类别场景:
import os import glob import xml.etree.ElementTree as ET # 配置区 VOC_ANNO_DIR = 'Annotations' VOC_IMG_DIR = 'JPEGImages' YOLO_LABEL_DIR = 'labels' CLASS_NAMES = ['solar_panel'] # 单类别,写实际类别名 os.makedirs(YOLO_LABEL_DIR, exist_ok=True) def convert_annotation(xml_path, out_path, img_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') if size is None: print(f"[跳过] {xml_path} 缺少size字段,无法归一化") return False img_w = int(size.find('width').text) img_h = int(size.find('height').text) if img_w == 0 or img_h == 0: print(f"[跳过] {xml_path} 图片尺寸为0") return False # 先读图片确认实际宽高,以防xml字段与真实图片不一致 import cv2 fn = root.find('filename').text img_path = os.path.join(img_dir, fn) probe = cv2.imread(img_path) if probe is None: print(f"[警告] {img_path} 无法读取") else: real_h, real_w = probe.shape[:2] if real_w != img_w or real_h != img_h: print(f"[警告] {xml_path} 尺寸不匹配 xml:({img_w},{img_h}) real:({real_w},{real_h})") # 以真实图片尺寸为准 img_w, img_h = real_w, real_h lines = [] for obj in root.findall('object'): name = obj.find('name').text.strip() if name not in CLASS_NAMES: print(f"[跳过] 未知类别 {name} in {xml_path}") continue cls_id = CLASS_NAMES.index(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) # 越界修正:标注框超出图片边界很常见 xmin = max(0, min(xmin, img_w - 1)) xmax = max(0, min(xmax, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) ymax = max(0, min(ymax, img_h - 1)) if xmax <= xmin or ymax <= ymin: print(f"[跳过] {xml_path} 中 {name} 框无效") continue x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 夹到 [0,1],极端情况会因浮点误差出现1.000001 x_center = max(0, min(x_center, 1.0)) y_center = max(0, min(y_center, 1.0)) w = max(0, min(w, 1.0)) h = max(0, min(h, 1.0)) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") if lines: with open(out_path, 'w') as f: f.write('\n'.join(lines) + '\n') return True else: print(f"[跳过] {xml_path} 没有有效目标") return False # 主流程 xml_files = sorted(glob.glob(os.path.join(VOC_ANNO_DIR, '*.xml'))) success = 0 for xml_path in xml_files: stem = os.path.splitext(os.path.basename(xml_path))[0] out_path = os.path.join(YOLO_LABEL_DIR, stem + '.txt') if convert_annotation(xml_path, out_path, VOC_IMG_DIR): success += 1 print(f"转换完成:{success}/{len(xml_files)}")脚本的核心逻辑是:先解析xml拿到size和每个object的bndbox,然后按归一化公式计算中心点、宽高。加了几个保护分支——图片实际尺寸与xml字段不一致时以实际尺寸为准,越界框做钳制,无效框直接丢弃。这些保护不是为了炫技,是转换500多行真实标注时踩出来的坑。
参数说明:CLASS_NAMES列表必须和标签定义一致,这套数据集只有1个类别所以列表只有一个元素。输出目录YOLO_LABEL_DIR会自动创建。转换完成后,打开一个txt看一眼:
cat labels/pv_001.txt如果所有框的坐标都在0到1之间且第一个数字是0,基本就没问题。
4.2 边界坑1:xml的size字段与实际图片尺寸不一致
这是转换最容易踩的坑。有些标注工具在修改图片尺寸后没有同步更新xml,xml里写着1920x1080,实际图片是1280x720。如果信了xml的size,归一化坐标整体偏差,到了训练阶段loss奇葩地降不下去。
解决方式就是脚本里那一句“以实际图片尺寸为准”。转换前用cv2.imread探测一遍,既校验了图片可读性,又拿到了真实尺寸。代价是每张图多一次IO,271张图多耗几毫秒,完全值得。
4.3 边界坑2:空xml与无标注图片
有些图片没有任何光伏板,对应的xml里没有object节点。转换时如果直接跳过,就不会生成txt文件。这在yolov8里意味着这张图变成纯背景图,会参与训练但没有任何正样本。
不推荐把所有空标注图塞进训练集。单类别检测任务里,背景图的比例太高,模型会把大量精力花在学习“什么都不检测”上,容易降低召回率。应对策略是:空xml分开存放,只把其中一小部分(比如20%)混入训练集,其余用于测试阶段的假阳性评估。
4.4 边界坑3:坐标越界与零宽零高
标注框的xmax偶尔会等于图片宽度,归一化后宽度算出来是1.0,虽然不报错,但yolov8的loss计算里会对这种框做特殊处理,容易产生NaN清洗。更常见的是某些标注工具生成的框左上角和右下角坐标颠倒,导致宽高为负值。
脚本里的钳制逻辑解决了大部分问题,剩余负值宽高直接丢弃。检查方式很简单:
# 找到标注里有0宽度或0高度的txt awk '{if ($4<=0 || $5<=0) print FILENAME": "$0}' labels/*.txt零宽零高的框混进去,轻则训练指标不准,重则在某些版本的损失函数里触发除零错误,直接崩训练。
4.5 边界坑4:train/val划分时类别分布不均匀
271张图、1个类别,听起来划分就是按比例随机抽。但遥感图像有个特殊性:同一块光伏电站的影像往往连续多张都含有光伏板,随机划分很容易把它们全分到训练集或验证集,导致验证集mAP波动巨大。
稳妥做法是按文件名前缀或地理位置分组后再划分。比如文件名pv_001到pv_050来自同一区域,那就整组划分:
import os import random # 按前缀分组 ann_files = os.listdir('Annotations') groups = {} for f in ann_files: prefix = f.split('_')[0] # 比如 "pv" groups.setdefault(prefix, []).append(f) train_ratio = 0.8 train_files, val_files = [], [] for prefix, files in groups.items(): random.shuffle(files) split = int(len(files) * train_ratio) train_files.extend(files[:split]) val_files.extend(files[split:]) with open('train.txt', 'w') as f: f.write('\n'.join(train_files)) with open('val.txt', 'w') as f: f.write('\n'.join(val_files))如果文件名看不出区域信息,至少用哈希分桶(hash(img_name) % 10 < 8进训练集),保证同一张图的划分结果可复现。
5. 新手最容易踩的5个坑:从解压报错到训练翻车
5.1 7z解压提示密码错误,但密码明明是对的
现象:压缩包设置了解压密码,输入密码后7z报错“Wrong password”,用人眼看密码没敲错。原因多半是特殊字符被Shell转义,或密码文件是GBK编码而终端用UTF-8。另一个隐藏原因是压缩包本身是分卷压缩,只下载了第一个分卷。解决方法是把密码放入单引号避免Shell解释,或者用交互式输入:
7z x dataset.7z -o./out -p'实际密码'如果还不行,检查同目录下有没有.7z.001之类的分卷文件,分卷没下全是报密码错误之外最常见的解压失败原因。
5.2 训练时loss正常下降,但mAP一直是0
现象:yolov8训练10个epoch,box_loss和cls_loss都在降,val/box_map却是0。原因有两个高频场景:一是数据集的类别名和data.yaml里的names不一致,标注里叫solar而配置里写solar_panel;二是标注txt的class_id超出nc-1,比如只有1个类别却出现了class_id=1,yolov8静默忽略这类样本。
解决:训练前用脚本扫描所有txt里的class_id,确认最大值不大于nc-1:
cat labels/*.txt | awk '{print $1}' | sort | uniq输出只有0就是正常。如果出现1,直接批量替换回0。另一个排查点是看验证集的预测结果图,如果框全画出来了但score极低,去查conf_thres是不是设得太高。
5.3 遥感大图直接进网络,显存溢出
现象:GTX 3060 12G显存,batch_size=16,训练到第3步就OOM。原因不用猜,271张遥感图大多是2000x2000以上分辨率,yolov8默认imgsz=640,但不要以为它会自动缩小——它只是resize,可resize之前的解码和图幅预处理照样吃显存。更隐蔽的是如果忘记设imgsz,某些版本会用原图尺寸执行前向,那就必爆。
解决:显式指定小尺寸和更小的batch:
yolo detect train data=data.yaml model=yolov8n.pt imgsz=640 batch=8如果还想更稳,先把图片离线resize到1280再训练,可以省掉DataLoader里每次缩放的CPU开销。遥感光伏板检测这种任务,目标本身往往较小,直接resize到640会丢失细节,一般用1280训练、640只是兜底选项。
5.4 Mosaic增强把光伏板切成两半
现象:训练loss曲线很漂亮,验证mAP也不错,但推理时对狭长型光伏板经常漏检,尤其是跨越图片中缝的目标。原因基本是mosaic增强的锅。yolov8默认开启mosaic=1.0,训练时把4张图拼接,如果标注框横跨拼接边界,目标被切成两半,模型学到的是不完整的语义。
解决:mosaic概率调低或关掉。在data.yaml同目录的配置里,mosaic设为0.3,让它只作为数据多样性补充,不主导训练分布。271张的小数据集本来就稀缺,宁可少一点增强也不要让模型学到错误的形态。
5.5 混淆矩阵里全是背景类,光伏板那一行是空的
现象:训练完成后打印混淆矩阵,发现绝大多数样本被分到背景,solar_panel的召回率几乎为0。这事在遥感单类别检测里特别典型——光伏板在影像中的面积占比太小,正负样本严重不平衡。如果用了cls_loss的权重默认值,模型发现“全部预测为背景”的损失很低,自然倾向这种偷懒方案。
解决:调高正样本权重或使用focal loss。yolov8里可以在训练配置里调整cls损失的系数,常见做法是把它从默认的0.5提到1.0,同时把box损失系数维持原样。另一个动手前的检查项:确认标注框面积没有普遍小于10x10像素,这种极小目标即使训练收敛也推不准,最好先对图片做切片(patch)再训练。
6. 把271张用出千张效果:预训练权重、数据增强与验证策略
数据只有271张,选择预训练权重就是最重要的决策。常见做法是先加载COCO预训练的yolov8n或yolov8s,冻结前10层骨干网络,只训练检测头和后半段骨干。遥感图像和COCO自然图像领域差异大,但底层纹理特征仍然可迁移,尤其光伏板的边缘轮廓与建筑物边缘有共通之处。训练脚本里加一句freeze=10,前几个epoch用较低的学习率0.001,等loss不再下降时解冻全部层,把学习率调回0.0001再跑20个epoch。
数据增强上除了mosaic,强烈建议把旋转增强范围从默认的0度扩到正负30度,光伏板在遥感影像里方向分布很散,既有正南北排列也有斜45度排列。yolov8的degrees参数直接设成30,配合flipud=0.5的上下翻转,等于把有效训练样本量翻了三倍。亮度对比度扰动该开就开,遥感不同时相的光照差异很大,hsv_h、hsv_s各设0.015能提升泛化性,但别开太大,否则光伏板的蓝色特征会被洗掉。
验证策略上,271张图不建议做固定划分,可以用K-Fold交叉验证,折数设5,每折训练后记录mAP。取5折的平均mAP作为这套数据集的真实基线。跑完第一折后,花半小时去看验证集的前20张预测叠加图,比看mAP数字更能暴露问题——漏检长条形光伏板就在这一步发现的。
最后的个人习惯:小数据集训练我会把yolov8的patience设大一点,因为数据少导致验证集波动大,早停容易误伤。一般设到80到100,宁可多花点卡时也不让一个还没收敛的模型停在半路。希望这个思路帮你在271张图上少走点弯路,真上了更大规模的光伏场地,流程也不用推翻重来。
本文还有配套的精品资源,点击获取