简介:面向光伏电站运维与目标检测实践的数据集资源。内容聚焦太阳能光伏板积灰/灰尘检测,基于1463张现场图像构建,统一采用Pascal VOC与YOLO双格式标注,覆盖单一类别“Dirt”,共包含6822个矩形标注框,可支撑YOLO系列模型在积灰检测、清洁度判断等场景下的训练与验证。数据集由labelImg工具完成标注,标注规则明确,适合已有基础深度学习知识的目标检测入门者及需要真实光伏场景数据的研究者。资源共2000个文件,以图像、XML标注文件、TXT标注文件为主,压缩包大小75.98MB,目录结构清晰,便于直接划分训练集与验证集。目前已有536人学习下载。需要注意图片重复度较高,数据集虽保证标注准确,但模型精度需自行验证。
1. 太阳能光伏板积灰检测数据集:VOC+YOLO 双格式 1463 张图,下不下先看三个点
做光伏巡检的人都知道,灰尘板子表面会发灰,机器视觉识别时最难平衡的是“框住整个积灰区”还是“只框灰团”。这个数据集给的是已经被 labelImg 画好的 1463 张积灰检测样本,VOC xml 和 YOLO txt 双格式,单类别 Dirt,共 6822 个框。它解决的是光伏板表面灰尘目标检测的起步问题:不需要从零标注,直接拆训练集、改 data.yaml、上 YOLOv8 就能跑通一条完整检测流程。适合想跑通目标检测全流程的入门者,也适合做光伏运维算法预研的工程师。作者在说明里明确写了“图片重复度很高、低价出售、请谨慎下载”,这不是谦虚,而是下载前必须看清的提示,后面我会展开讲重复样本对训练划分和验证精度的影响。
2. 目录结构与双格式标注:XML 和 TXT 对齐之后,训练框架才不打架
2.1 1463 张图对应 1463 个 XML 和 TXT,数量齐整但别高兴太早
压缩包解开后,图片是.jpg,标注同时存在.xml和.txt,文件名前缀是firc_solarpanel_xxxx。三者的命名一一对应,比如firc_solarpanel_1345.jpg搭配firc_solarpanel_1345.xml和firc_solarpanel_1345.txt。
| 文件类型 | 数量 | 作用 |
|---|---|---|
| jpg 图像 | 1463 | 光伏板表面状态原图 |
| VOC xml | 1463 | 保存类别名和 xmin/ymin/xmax/ymax |
| YOLO txt | 1463 | 归一化后的中心点坐标与宽高 |
这种一份标注双格式的情况,对工程落地很友好:PyTorch 系的检测框架大多认 YOLO txt,而调试可视化时读 XML 更直观。数量齐整说明没有缺漏文件,但图片内容重复度很高,文件名后面那串数字并不代表 1463 个完全不同的组件,很多是同一块板子的不同角度或连续帧。这个点先记住,它直接影响后面的训练集划分和验证集评估。
2.2 用 Python 解析 XML:先看文件名、图像尺寸和 Dirt 框
拿到 XML 后,我习惯写一段小脚本把标注统计出来,而不是直接扔给训练脚本。常见做法是用xml.etree.ElementTree读树结构:
import xml.etree.ElementTree as ET tree = ET.parse("firc_solarpanel_1345.xml") root = tree.getroot() filename = root.find("filename").text size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) print("文件名:", filename, "分辨率:", img_w, img_h) for obj in root.findall("object"): name = obj.find("name").text bnd = obj.find("bndbox") xmin = int(bnd.find("xmin").text) ymin = int(bnd.find("ymin").text) xmax = int(bnd.find("xmax").text) ymax = int(bnd.find("ymax").text) print(name, "框:", (xmin, ymin, xmax, ymax))这段代码做的事很直接:先读<size>拿宽高,再遍历<object>拿类别名和边界框坐标。输出会看到Dirt类别和一堆像素坐标。为什么强调先跑这段?因为数据集里的框并不都紧贴灰尘团,有些框把一片灰斑区域整体包起来,这种宽松标注对检测模型来说,会让预测框偏向大而松,后面评估精确率时影响很大。先统计,心里有数,后面才好决定用多大的置信度阈值。
2.3 YOLO txt 的五行字段:中心点、宽高和归一化
YOLO 格式的 TXT 不像 XML 那么直观,它每行代表一个目标框。用文本方式打开firc_solarpanel_1349.txt,你会看到类似0 0.5231 0.4872 0.1289 0.2031的结构。
head -3 firc_solarpanel_1349.txt解释一下字段:
- 第 1 位
0是类别 id,单类别 Dirt 固定为 0; - 第 2 位
0.5231是框中心点 x 在图像宽度上的比例; - 第 3 位
0.4872是框中心点 y 在图像高度上的比例; - 第 4 位
0.1289是框宽占图像宽的比例; - 第 5 位
0.2031是框高占图像高的比例。
这个坐标系统的好处是无论原图是 640×480 还是 1024×768,训练时都能直接缩放。但要注意,灰尘目标在很多图上非常小,宽高比可能只有 0.01 到 0.03,人眼在 640 像素的图上几乎看不见。如果直接用小分辨率训练,这些小框很容易在特征图下采样时被丢掉。
从 VOC 的 xmin/ymin/xmax/ymax 转成 YOLO 格式,公式也很固定:
def voc2yolo(xmin, ymin, xmax, ymax, img_w, img_h): x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h return f"0 {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}"核心是先把像素坐标换算成相对比例,再输出成六位小数的固定格式。常见翻车点有两个:一是x_center写成了xmin / img_w,忘记加框宽的一半;二是 class id 没有从 0 开始。转换完最好拿一张图回读 txt,把归一化坐标乘回宽高,再在原图上画框做可视化,确认没有偏移。
这一章的结论是:双格式让数据集适配面广,但 Dirt 框偏松、目标偏小这两个特点,决定了后续训练时不能无脑上来就跑。
3. 划分训练集和验证集:按文件名前缀分组,避免重复图串集
3.1 随机 shuffle 是数据泄漏的常见来源
之前我拿到这类数据集,第一反应是random.shuffle然后按 8:2 直接切。后来做真实场景测试时发现,验证集 mAP 很高,换到现场新图掉得厉害。事后分析,根因就是数据重复度高:同一块光伏板的连续帧,一部分进了训练集,另一部分进了验证集,模型已经在训练时“见过”验证图,分数自然虚高。这也是作者在说明里反复提示重复度高的原因。
要解决这个问题,不能只看文件多,而是看这张图与训练集里哪些图是同源的。最简单可靠的方式是拿文件名里的编号做分组,把同一来源的所有图片放到同一个集合里,再按组划分。
3.2 分组划分脚本:保证同一编号的图片只进 train 或 val 其中一边
下面是我的划分脚本,核心是按firc_solarpanel_1345里的1345这组编号聚合。
import os import random from collections import defaultdict img_files = [f for f in os.listdir("images") if f.endswith(".jpg")] grouped = defaultdict(list) for f in img_files: # 例如 firc_solarpanel_1345.jpg -> 1345 seq_id = f.split("_")[2] grouped[seq_id].append(f) groups = list(grouped.keys()) random.seed(42) random.shuffle(groups) split_idx = int(len(groups) * 0.8) train_groups = set(groups[:split_idx]) val_groups = set(groups[split_idx:]) train_files = [] val_files = [] for g in train_groups: train_files.extend(grouped[g]) for g in val_groups: val_files.extend(grouped[g]) print(f"train images: {len(train_files)}, val images: {len(val_files)}")要关注的参数就这么几个:
split_idx = int(len(groups) * 0.8):80% 的编号组进训练集,20% 进验证集。如果样本量小,可以把比例改成 0.7 或多留 5% 做测试。random.seed(42):固定随机种子,复现结果时不会每次跑出来不一样。- 用集合
set去重,避免同一个编号出现在两个集合中。
这段脚本只输出了图片列表,真正使用时要把对应图片和标注一起复制到images/train、labels/train这样的目录结构里,YOLOv8 训练时会自动按data.yaml里的路径找同名的 txt 标注。
import shutil from pathlib import Path base = Path("solar_dataset") for f in train_files: shutil.copy(f"images/{f}", base / "images/train" / f) shutil.copy(f"labels_yolo/{f.replace('.jpg', '.txt')}", base / "labels/train" / f.replace(".jpg", ".txt")) for f in val_files: shutil.copy(f"images/{f}", base / "images/val" / f) shutil.copy(f"labels_yolo/{f.replace('.jpg', '.txt')}", base / "labels/val" / f.replace(".jpg", ".txt"))复制完检查一下目录数量,train 和 val 的图片数相加要等于 1463,labels 对应 txt 也要相等。少了说明文件名对不上,通常是.jpg和.JPG后缀大小写不一致导致的。
3.3 标注分布检查:单类 6822 框在划分后是否均衡
划分完还要确认一件事:灰尘框有没有集中出现在训练集,而验证集一张带框的都没有。这种情况在重复度高的数据集里经常发生,因为同一组图片的框数极度相似。
def count_boxes(file_list): total = 0 for f in file_list: txt_path = os.path.join("labels_yolo", f.replace(".jpg", ".txt")) with open(txt_path) as fp: total += len(fp.readlines()) return total print("train 框数:", count_boxes(train_files)) print("val 框数:", count_boxes(val_files))这段逻辑是逐行读 YOLO txt,每行代表一个框。Dirt 类全数据集总共 6822 个框,平均每张 4.66 个框。如果 val 的框数明显偏低,说明验证集偏向于“干净板子”,模型只需要学会输出零框就能拿到不错的表现,评估没有意义。这时要回到分组划分,按框数重新均衡两个集合。
把验证集质量搞扎实,后续判断模型是否过拟合才有参考价值。这一章值得多花十分钟自查。
4. 用 YOLOv8 训练积灰检测模型:data.yaml、训练命令和五组关键参数
4.1 按 YOLOv8 标准目录整理数据
YOLOv8 的默认数据组织方式是 images 和 labels 两大目录,里面再分 train/val。刚才划分阶段生成的train_files和val_files,已经用shutil.copy复制到solar_dataset下了。结构如下:
solar_dataset/ images/train images/val labels/train labels/val注意 labels 里只放 YOLO 的 txt 文件,XML 不用放进来,因为 Ultralytics 训练时读的是 txt。如果你的项目同时需要 VOC 格式调试,可以把 XML 放在另一个目录,但 data.yaml 一定指向 txt 所在目录。这个布局是整个流程的骨架,后面换数据集、换机器都是这套规矩。
4.2 data.yaml 配置:单类也要把类别名写对
下面是一份可用的data.yaml:
path: solar_dataset train: images/train val: images/val nc: 1 names: 0: Dirt重点解释几个字段:
path写数据集根目录,相对路径和绝对路径都可以,推荐相对路径,方便换机器训练。train和val是在path下的相对路径,YOLOv8 支持单字符串或列表。nc类别数,这个数据集是单类,必须写1,写多会导致类别维度不匹配。names用字典格式,键从 0 开始,类名要和训练输出对应。YOLO txt 里的第一个数字0就对应这里的Dirt。
很多人把names: {Dirt: 0}写反了,虽然 Ultralytics 偶尔能自动修正,但最好按规范写。单类数据集最容易出的问题不是类别名,而是背景样本太多,导致模型学成“什么框都不出”。
4.3 训练命令与关键参数:imgsz、batch、lr0、patience
在终端里执行训练,最省事的方式是用命令行:
yolo detect train \ data=solar_dataset/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ device=0参数怎么选,我的经验如下:
model=yolov8n.pt:用官方预训练权重初始化,会先从本地缓存找权重,没有就自动下载。如果离线环境下载失败,就把权重文件放到固定缓存目录里,再写绝对路径。网络正常时不用管。imgsz=640:默认值。但灰尘目标偏小,如果显卡显存允许,建议改成960或1280。目标越小,输入分辨率越影响召回率。batch=16:取决于显卡显存。8G 显存跑yolov8n可以上 32,16G 可以尝试 batch 64。显存不够先降 batch,不要直接降 imgsz。lr0=0.01:默认。数据量只有 1463 张且重复高时,学习率再低一点,比如0.005,不容易震荡。patience=20:连续 20 个 epoch 验证集没有提升就早停。这个值用来防过拟合非常有效。
训练完成后,runs/detect/train/weights/下会生成best.pt和last.pt。接下来要做的是看曲线,不是急着跑推理。
4.4 从 results.csv 判断过拟合,不要只看 mAP
训练日志里的 mAP50 和 mAP50-95 容易让人产生错觉。重复度高的数据集常出现 mAP50 高但泛化差的情况。我的习惯是训练结束后立刻读runs/detect/train/results.csv,对比训练 loss 和验证 loss 的间距。
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/detect/train/results.csv") plt.plot(df["epoch"], df["train/box_loss"], label="train_box_loss") plt.plot(df["epoch"], df["val/box_loss"], label="val_box_loss") plt.legend() plt.show()正常的趋势是两条线一起下降并靠近,如果 val_box_loss 在某个 epoch 后开始掉头往上,而 train_box_loss 还在降,就是过拟合信号。此时该做的是回第 3 章去重,而不是堆 epoch。另一个检查点是confusion_matrix.png,单类看 background 误报是否居高不下,背景误报高说明图片里灰尘区域太小,模型找不到目标,这时候优先提高 imgsz,而不是调训练轮数。
5. 常见问题与避坑:拿到数据集后最容易踩的五个坑
5.1 重复图让验证分数虚高
现象:训练时 mAP50 冲到 90% 以上,一到现场新图就漏检。
原因:文件名前缀相同的图片在随机划分时被同时分进 train 和 val,模型记住了场景,而不是学会“灰尘长什么样”。作者已在说明中提示图片重复度很高,这个坑几乎是必然踩到。
解决:严格按 3.2 的分组脚本划分,同一个编号的图只允许出现在一侧。如果条件允许,更狠的做法是把重复帧完全去重,只保留每个编号下差异最大的一到两张,再用去重后的子集训练。
5.2 Dirt 标签框太松,框住整个灰斑而非单个灰尘团
现象:模型预测出来的框很大,把干净区域也包进去,视觉上像“圈地”。
原因:labelImg 标注时,对一片密密麻麻的灰尘区,直接画一个外接矩形框住整体,而不是每个灰团一个框。这种宽标注在评估时因为 IoU 较大,mAP 不会被惩罚,但实际定位精度差。
解决:训练时把置信度阈值调高到 0.35 甚至 0.5,滤掉边缘模糊的大框;推理后按宽高比筛掉面积异常的框;如果要做精细检测,需要自己补一批更紧的框数据。
5.3 VOC 转 YOLO 时中心坐标计算错位
现象:训练 loss 不降,打开 txt 一看坐标值大于 1 或者全是 0.5。
原因:转写脚本时把x_center直接写成(xmin + xmax) / 2 / img_w,手一滑就漏了括号;或者把归一化的宽高又做了一次除以 2。
解决:用 2.3 的voc2yolo函数,并做回读校验。取一张图,把 txt 里的值乘回宽高,画框叠加在用 OpenCV 读出的原图上,肉眼确认框的位置和灰尘区域匹配再批量转换。
5.4 验证集框数分布严重不均衡
现象:val 的 loss 很低,但 val 里大多数图根本没有框。
原因:重复度高的数据里,同组图片标注稀疏,分组划分后恰好把“干净组”整组分到了验证集。
解决:每次划分后都要跑一遍 3.3 的框统计。如果 val 框数占比和图片占比相差超过 10%,重新调随机种子或按分组手动均衡,保证验证集至少有一半以上图像带框。
5.5 把数据集当全部生产数据用
现象:训练完拿去做实际光伏电站巡检,遇到倾斜角度、阴天、背板反光就抓瞎。
原因:这只是一个标注好、价格低的起步数据集,不覆盖所有光伏板材质、光照和灰尘形态。作者也特别声明不对模型精度作任何保证。
解决:把这份数据当成流程验证和基准测试,不要当成最终生产数据。真正落地前,至少要补拍本电站的 500 张以上现场图,沿用同样的 VOC/YOLO 双格式做增量微调。
6. 进阶:把 Dirt 框转成积灰覆盖率,用面积占比验证模型效果
6.1 批量推理并输出检测框坐标
模型训练到够用之后,最有价值的输出不是“图上有灰尘”,而是“这块光伏板积灰面积大概占多少”。在运维场景中,这个面积占比直接对应清洗决策。我一般用下面的脚本做推理统计:
import cv2 from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") img = cv2.imread("test_solar.jpg") results = model.predict(img, conf=0.35, iou=0.5)[0] boxes = results.boxes.xyxy.cpu().numpy() img_area = img.shape[0] * img.shape[1] dirt_area = 0 for x1, y1, x2, y2 in boxes: dirt_area += (x2 - x1) * (y2 - y1) print(f"预测框总面积占比: {dirt_area / img_area:.1%}")这里conf=0.35是为了过滤宽松标签造成的大而模糊的框;iou=0.5用于去重叠。如果检测到的框非常多且密集,直接按框面积求和会高估覆盖率,因为灰尘其实是稀疏分布的。遇到这种情况,我会在求和前先按单框面积过滤,把像素面积小于图像总面积 0.2% 的框视为噪声。
6.2 用覆盖率趋势做模型验证
这个方法比单纯看 mAP 更贴近业务:选一块积灰程度中等的板子,人工目测给出一个粗略的积灰等级,再和模型算出的覆盖率对照。如果外观上有明显灰尘但模型输出趋近于零,说明召回不够,需要回去提高 imgsz 或补充小目标样本。如果模型输出覆盖率明显高于目测,说明框太松,应该调高置信度或重新标一批紧框。
从那以后,我每次拿到这种“重复度高、标签偏松”的下载数据集,都会强制走一遍全集去重、分组划分、框分布统计、裸测真实图这四条流程,再决定用不用它。可以这么说:这个数据集本身不完美,但用它能完整演练一遍“VOC+YOLO 双格式转训练、评估、误判排查、面积量化”的工程链路,对刚接触目标检测的人很值得。希望帮到你。
本文还有配套的精品资源,点击获取