光伏板积灰检测:从VOC转YOLO到YOLOv8训练实战
简介这份数据集面向光伏运维、目标检测及深度学习入门者用于太阳能光伏板表面灰尘Dirt检测模型的训练与验证。资源共包含1463张jpg图片每张均对应Pascal VOC格式的xml标注和YOLO格式的txt标注总计标注目标框6822个类别仅Dirt一项采用labelImg工具以矩形框方式标注。压缩包内文件总数2000主要由1463个xml、537个txt及图片文件组成整体大小75.98MB目录按图片编号组织可直接对接YOLO等常见训练流程。目前已有537人浏览/学习数据热度适中。数据集标注规范、边界框定位准确适合快速开展光伏积灰检测实验或作为目标检测教学案例作者同时提示图片重复度较高且不对特定模型权重精度作保证因此更适合对样本多样性要求不高、追求低成本获取标注数据的场景正式项目使用前建议抽样核查。1. 光伏板积灰检测数据集1463张单类别能撑起什么场景光伏电站最头疼的运维问题之一就是积灰。板面落灰之后发电量直线掉人工巡检又慢又贵所以很多团队转向“无人机巡检 目标检测”的路线。这个数据集包就是为这条路准备的1463张光伏板图像只标注一个类别——积灰同时提供VOC和YOLO两套标注格式。如果你正在做光伏积灰检测相关的yolo入门项目或者手里有电站图像但不知道怎么组织成能训练的数据集这套数据可以直接作为起点。它适合两类人一是想验证“积灰检测到底能不能用视觉做”的工程师二是已经在跑yolov8训练自己的数据集但被数据格式卡住的新手。不适合谁想做灰尘厚度分级、想区分鸟粪和灰、想判断清洗优先级的人单类别标注撑不起这种需求。2. VOC转YOLO格式归一化坐标是所有转换脚本的核心拿到一个同时带VOC和YOLO格式的数据集第一件事不是急着训练而是搞清楚两种格式的坐标逻辑。Pascal VOC用XML保存绝对像素坐标YOLO用txt保存相对坐标。两套格式在数据集的目录结构、标注内容、类别编号方式上完全不同转换脚本写错一个分母检测框就全偏。2.1 Pascal VOC的XML里到底存了什么VOC格式的目录通常长这样VOCdevkit/ └── VOC2007/ ├── Annotations/ # 每个jpg对应一个xml ├── JPEGImages/ # 原图 └── ImageSets/Main # 训练/验证划分文件一个标注文件内关键节点有三个size原图宽高、object的name类别名、bndboxxmin、ymin、xmax、ymax四个绝对坐标。比如annotation size width1920/width height1080/height /size object namedust/name bndbox xmin300/xmin ymin450/ymin xmax720/xmax ymax610/ymax /bndbox /object /annotation这里的坐标是像素值直接基于原图分辨率不需要任何额外计算。老工具链、标注软件很多都吃这套格式这也是为什么很多数据分发喜欢附带VOC版本——通用性最好。2.2 YOLO的txt标注为什么必须归一化YOLO格式不关心原图是多大每个目标单独一行五个数class_id x_center y_center width height后四个全部归一化到[0,1]。归一化意味着坐标除以原图宽高模型在训练时不用处理不同分辨率带来的尺度问题这是YOLO系算法能批量缩放输入图的基础。注意一个细节YOLO的中心坐标和宽高都是相对值所以转换公式是x_center (xmin xmax) / 2 / image_width y_center (ymin ymax) / 2 / image_height width (xmax - xmin) / image_width height (ymax - ymin) / image_height很多转换翻车就翻在忘记除以宽高或者只归一化了中心点而忘记把框宽高也归一化。另外类别ID必须从0开始单类别数据集就是0不是1。2.3 用Python把VOC转成YOLO的常用写法如果你拿到手的数据只有VOC格式或者想验证数据集里自带YOLO标注是否正确可以自己写一个转换脚本。下面这个是我常用的版本import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path: Path, out_txt_path: Path, class_names: list): 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.iter(object): name obj.find(name).text.strip() if name not in class_names: continue cls_id class_names.index(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 box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h # 限制在[0,1]区间防止标注越界影响训练 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) box_w min(max(box_w, 0.0), 1.0) box_h min(max(box_h, 0.0), 1.0) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) out_txt_path.write_text(\n.join(lines)) class_names [dust] # 单类别ID为0 xml_dir Path(Annotations) txt_dir Path(labels) for xml_file in xml_dir.glob(*.xml): voc_to_yolo(xml_file, txt_dir / f{xml_file.stem}.txt, class_names)逻辑说明脚本先解析XML读原图宽高再遍历每个object取类别和边框最后把绝对坐标转成相对坐标写入txt。class_names.index(name)决定了类别ID如果你把类别名列表写成[dust, dust2]但数据里只有dust一个类训练不会报错只是模型会以为存在两个类导致类别数不匹配问题。参数说明Class_names的顺序就是ID顺序必须和后续训练配置文件保持一致min(max(...), 1.0)是为了防止标注数据越界导致训练时loss异常。如果你发现转换后的框整体偏移先查size节点里宽高和实际图片分辨率是否一致。3. 用YOLOv8训练自己的积灰数据集从解压到出结果的完整命令格式确认没问题之后下一步就是把数据集喂给模型。现阶段用ultralytics的YOLOv8训练自己的数据集是主流做法社区资料多、命令收敛、报错也好搜。整个流程分三步整理目录、写data.yaml、跑训练命令。3.1 数据集目录和data.yaml怎么组织先把压缩包解压成一个固定结构的目录YOLO系列训练器对目录有默认约定。我一般这样组织solar_dust/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/每个jpg在images/train下同名txt必须在labels/train下。图片和标注文件名一一对应缺一个txt就少一个训练样本。如果解压出来的数据集是VOC的ImageSets划分你需要先按划分文件把图片和XML拷到对应目录。然后写data.yamlpath: /data/solar_dust # 数据集绝对路径 train: images/train # 相对于path val: images/val # 相对于path names: 0: dust # 类别ID从0开始参数说明path尽量写绝对路径避免相对路径在不同工作目录下解析错误names的键是类别ID0对应第2章转换脚本里的class_name顺序单类别场景names只有一项模型输出维度会相应收敛推理时只需要一个置信度阈值。3.2 训练、验证一条命令跑通目录和yaml就绪后直接跑yolo detect train datadata.yaml modelyolov8s.pt epochs120 imgsz640 batch16 patience20 projectruns namesolar_dust参数说明modelyolov8s.pt是加载COCO预训练权重对小数据集有帮助不建议从头训练imgsz640是训练输入尺寸光伏板图像通常分辨率很高1920以上但训练时YOLO会做缩放640是速度和精度折中batch16要看显存8G显存可以降到8patience20表示20个epoch没有提升就自动停防止过拟合。如果你用的是ultralytics库而不是CLI等价写法是from ultralytics import YOLO model YOLO(yolov8s.pt) model.train(datadata.yaml, epochs120, imgsz640, batch16, patience20)跑起来之后终端会打印每个epoch的box_loss、cls_loss、dfl_loss和mAP指标。第一次跑完你会在runs/solar_dust下看到权重文件best.pt和last.pt以及一堆可视化输出。3.3 训练输出的几个关键文件怎么看训练完成后重点看四个东西results.png整个训练过程的loss曲线和mAP曲线。如果epoch到中期loss就掉不下去可能是学习率或数据问题如果验证曲线在训练曲线还在降时就开始反弹就是过拟合。confusion_matrix.png单类别下主要看背景误检情况。如果很多干净板面被预测成积灰说明负样本不够。val_batch0_pred.jpg第一张验证集图像的预测可视化。这一步能最直观地看到框有没有偏移、积灰区域是不是被漏检。best.pt和last.ptbest是验证集指标最好的权重部署时用best。提示单类别数据集不建议只盯mAP50-95光伏积灰检测的现实需求是“框得准、别误报”mAP50更有参考价值。4. 单类别检测调参mAP和损失函数不看错方向就是白调1463张小数据集训练本身不难难的是验证集怎么分、loss怎么看、增强怎么开。这几个坑不处理训练日志再漂亮现场一跑就露馅。4.1 训练/验证划分按电站分别按文件随机分这是小数据集最容易犯的错误。随机划分会把同一个电站、同一个组件在不同角度、光照下的照片同时分进训练集和验证集验证mAP会虚高到接近1.0看起来完美部署到新电站直接崩。常见做法是按拍摄来源划分。如果文件名有电站前缀比如site01_0001.jpg、site02_0012.jpg先按前缀分组再划分from pathlib import Path import random img_files sorted(Path(images).glob(*.jpg)) grouped {} for img in img_files: site img.stem.split(_)[0] # 按文件名前缀分组 grouped.setdefault(site, []).append(img) random.seed(42) groups list(grouped.values()) random.shuffle(groups) train_ratio 0.85 train_groups groups[:int(len(groups) * train_ratio)] val_groups groups[int(len(groups) * train_ratio):] # 落盘到目录并在labels目录中同步对应txt如果不能按来源分至少保证同一张图片的变体裁剪、翻转、滤镜版本不跨集合。判断标准很简单验证是衡量“没见过的电站”能不能检测而不是“见过的电站换个角度”能不能检测。4.2 读yolo训练日志box_loss、cls_loss、dfl_loss分别盯什么单类别训练时三个loss的变化意义完全不同cls_loss分类损失。单类别下它很快会降到一个极低值因为模型只需要区分“前景/背景”这是正常的不用觉得诡异。box_loss定位损失决定框准不准。这是单类别数据集最需要盯的指标。如果box_loss收敛慢或震荡优先怀疑标注框质量问题。dfl_lossDistributed Focal Loss影响边界框回归的分布。它和box_loss趋势一致如果两者背离检查学习率是否过大。另外单类别模型的验证集mAP很可能远高于多类别因为只有一个类混淆矩阵里只有正类和背景两个维度。不要因为这个数字高就认为模型可以上线真正决定能不能用的指标是误报率。4.3 小数据集数据增强怎么开到不翻车YOLOv8默认开mosaic等增强但对1463张的单类别积灰数据增强不是越大越好。光伏板方向固定、积灰区域纹理也无明显旋转不变性过强的旋转和色彩增强反而让模型学到错误特征。我常用的调整方向yolo detect train datadata.yaml modelyolov8s.pt \ epochs120 imgsz640 batch16 \ mosaic0.5 hsv_h0.01 hsv_s0.3 hsv_v0.3 degrees5 fliplr0.5参数说明mosaic0.5把默认的1.0降下来小数据集mosaic太猛会过度混合背景degrees5只允许小角度旋转光伏板要么水平要么小倾角不需要90度旋转增强fliplr0.5水平翻转保留对积灰检测基本无副作用。如果你发现验证集mAP波动大可以进一步把mosaic降到0.2甚至关掉用mosaic0.0锁定对比。4.4 置信度阈值和NMS的Iou阈值怎么定训练完成后推理时的默认阈值是conf0.25, iou0.45但这套默认值在单类别积灰检测里通常偏松。积灰检测一旦误检运维人员就得跑一趟现场误报代价高。我一般会先用验证集扫一遍yolo predict modelbest.pt sourceval_images conf0.4 iou0.5然后看预测结果里的误检数量。confidence从0.3到0.5逐档试选择误检数量和召回率平衡点。太阳能板面反光、边框阴影、杂草遮挡都会产生高分误检阈值设到0.4左右通常能压掉大部分但也会牺牲部分小面积积灰区域需要结合现场接受度来定。5. 积灰检测常见问题与避坑标注、转换、训练三类翻车现场这个方向我自己跑的时候踩过不少坑挑五个最典型的写出来。每条都是真实的现场问题按“现象→原因→解决”写方便你遇到时对号入座。5.1 VOC转YOLO后框全偏多半是没按原图宽高做归一化现象训练时loss正常下降但可视化预测框整体向左上或右下偏移框的尺寸比例明显不对。 原因转换脚本没有读size节点或者size里的宽高和实际图片分辨率不一致。还有一种可能是XML里存的是缩放后的标注坐标但图片本身没缩放。 解决先用PIL读图片宽高与XML的size节点逐一比对转换时统一用图片实际宽高做分母。我习惯直接在转换脚本里同时读原图from PIL import Image img_w, img_h Image.open(jpg_path).size用图片真实宽高代替XML里的size能避免源标注文件本身没更新分辨率的问题。5.2 训练loss收敛但验证mAP很低先查数据泄漏和样本不平衡现象训练集mAP能到0.95以上验证集mAP只有0.6甚至更低且波动大。 原因两个可能。第一个是验证集场景和训练集差异过大比如训练集全是地面电站平视角度验证集全是无人机俯拍第二个是积灰面积大的极端样本都集中在训练集验证集只剩小面积积灰。 解决按4.1的分组方式重新划分确保每个划分里都有不同面积积灰的样本。另外检查验证集是否存在难样本比如灰尘颜色和板面底色接近的低对比度图这类样本会严重拉低mAP但恰恰是现场最需要处理的。5.3 预测时把干净板面误检测成积灰负样本和阈值要一起调现象模型在正常光伏板上框出一大片“积灰”尤其反光强烈的时候。 原因这是单类别检测的老毛病——模型只在正样本上学习没见过“干净的板面”也可以作为背景。如果积灰区域和板面清洗后的反光区域纹理相似模型就会混淆。 解决收集负样本。不需要额外标注直接拉一批干净光伏板图片放进训练集不配txt标签模型就会把它们当作背景学习。同时配合提高置信度阈值到0.4以上双管齐下。负样本来源可以是网上的清洗后电站照片也可以是自己拍的未积灰板面。5.4 类别ID不一致导致训练报错单类别也要统一从0开始现象训练启动时报错提示number of classes mismatch或者模型训练正常但推理时把所有检测框都归为0类而0类又是错误的类别。 原因VOC里的类别名是字符串dustYOLO是整数ID。如果你在data.yaml里写成names: {1: dust}或者转换脚本里用class_names.index(name)1把ID变成1就会出现类别数对不上。 解决单类别的类别ID固定是0。检查转换脚本里是否加了1检查data.yaml里names是否从0开始两者确认一致再启动训练。5.5 文件名对不上导致图片被跳过训练前先跑一遍校验脚本现象训练日志里显示图片数量比数据集少几十张或者模型对某几张图完全检测不到。 原因图片是jpg标签txt名字里多了空格、后缀大小写不一致——比如IMG_001.JPG对应img_001.txtWindows和Linux文件名大小写敏感差异会直接漏读。 解决训练前先做一次强制校验from pathlib import Path for split in [train, val]: img_dir Path(fimages/{split}) lbl_dir Path(flabels/{split}) imgs {p.stem.lower(): p for p in img_dir.glob(*.jpg)} for lbl in lbl_dir.glob(*.txt): if lbl.stem.lower() not in imgs: print(f标签无对应图片: {lbl.name})这段脚本把所有文件名统一转小写后比对能快速揪出大小写不一致、多余空格、后缀差异三类问题。跑一遍再训练避免静默丢数据。6. 把检测框变成运维指标积灰面积占比的现场后处理模型输出的是一堆框运维要的是一个可以排期的指标。常见做法是用检测框面积除以整块光伏板面积得到“积灰面积占比”再按占比分清洗优先级低于10%不处理10%到30%列入计划超过30%优先清洗。6.1 用检测框算每块组件的积灰面积占比思路是先检测出组件区域再在组件内聚合所有积灰框计算覆盖比例。如果你的数据是单块板面特写组件区域就是整张图如果是多块板面的巡检图要先拿到组件框。以下代码按“整图是一块板”的假设聚合def dust_ratio(dust_boxes, img_w, img_h): panel_area img_w * img_h dust_area 0.0 for x1, y1, x2, y2 in dust_boxes: # 将归一化坐标转回像素假设输入已归一化 px1, py1 int(x1 * img_w), int(y1 * img_h) px2, py2 int(x2 * img_w), int(y2 * img_h) dust_area (px2 - px1) * (py2 - py1) return min(dust_area / panel_area, 1.0)这段的用途是给出一个可用指标但要注意重复检测问题。同一块积灰区域被模型输出5个重叠框时面积会被算5次。合理做法是先做NMS或者按中心点聚类再把聚类的框面积合并。6.2 导出ONNX并在连续帧上做置信度过滤验证完效果就要考虑部署。把best.pt导出成ONNX格式yolo export modelbest.pt formatonnx imgsz640导出后的ONNX可以在Jetson、工控机、树莓派上跑CPU或GPU推理。现场巡检往往是连续拍摄单帧误检不可避免我的习惯是连续10帧里至少7帧检测到积灰才触发告警用时间维度换掉一半误报。再叠加一个“相邻帧检测框IoU大于0.5才认为是同一目标”的过滤基本能压住偶发的光照突变误检。另一个低成本交叉验证是灰度直方图积灰区域会让光伏板表面灰度分布偏离干净板面的峰值。取检测框内灰度均值和同组件干净区域的灰度均值做对比两者差异低于阈值就认为是一次误检。这个方法不增加模型复杂度只作后处理但在低对比度场景下能救回不少误报。我的实际经验是任何检测模型到了现场都要按电站重新校准阈值别指望一次训练吃遍所有电站。如果你调的模型在A电站很稳换到B电站又翻车先检查训练集里有没有B电站的样本再怀疑模型本身。希望帮到你。本文还有配套的精品资源点击获取