2026/10/11 0:53:45

光伏板缺陷检测实战:YOLOv8数据集制作与训练避坑指南

光伏板缺陷检测实战:YOLOv8数据集制作与训练避坑指南 简介面向光伏板表面缺陷检测任务该数据集涵盖裂纹、栅线与斑点三类典型缺陷并同时提供Pascal VOC和YOLO两种标注格式适合目标检测模型训练、算法对比以及毕业设计实践。压缩包共2000个文件以VOC格式的xml标注文件为主体另含YOLO格式的txt标签及说明文档其中xml记录目标的类别与位置信息txt以归一化坐标表达两种格式可灵活转换或直接用于主流框架整体约310.4MB下载解压后目录清晰。目前已有362人学习/下载。数据集中crack框数892、grid框数884、spot框数852总计2628个矩形标注框均由labelImg工具按类别人工绘制定位准确、边界贴合目标三类缺陷样本数量接近有利于减少训练时的类别不平衡问题。配合明确的标签体系可直接输给YOLOv5/YOLOv8、Faster R-CNN等常见框架支持从数据准备、模型训练到缺陷检测效果评估的完整流程操作。1. 光伏板缺陷检测数据集为什么 2400 张就能让一个工程新手出活做光伏电站巡检或者组件生产质检的工程师迟早要和「光伏板缺陷检测数据集 YOLO」这一套组合打交道。光伏板也就是太阳能板表面常见的隐裂、污渍、热斑在照片里往往是几个像素宽的长条或者一小块色差人工看图费眼睛产线又要求逐片检查于是目标检测成了最直接的自动化路径。2400 张图、3 个缺陷类别、同时打包 VOC 和 YOLO 两种标注格式——这个规模对想跑通完整流程的人很合适比只有几百张的玩具数据大又不至于像几十万张的公开大规模数据集那样下载费时间、迭代跑不动。很多第一次接触缺陷检测的人会卡在同一个地方不是不会训练而是不知道手里这套数据该怎么整理、怎么转格式、怎么判断训练结果到底行不行。VOC 转 YOLO、目录划分、yaml 配置、小目标漏检每一步都有能让你白白烧掉两周时间的坑。下面这几章就是我按实际项目经验把这条路重新走一遍的记录。2. VOC 转 YOLO 格式目录结构、坐标归一化与批量转换脚本2.1 VOC 标注其实是一份 XMLobject、size 和 bndbox 的读法Pascal VOC 是目标检测领域最经典的标注格式之一很多标注工具导出时默认就是它。打开任意一张图的标注文件你能看到一整棵 XML 树root 节点下面有 folder、filename、source、size然后是一串 object 节点每个 object 对应一个目标框。annotation folderPV2024/folder filenamepv_0187.jpg/filename source databaseUnknown/database /source size width2560/width height1920/height depth3/depth /size object namecrack/name bndbox xmin1040/xmin ymin721/ymin xmax1142/xmax ymax731/ymax /bndbox /object /annotation注意 object 里有两个容易被忽略的字段truncated 和 difficult。truncated 表示目标被图像边缘截断difficult 表示这个目标本身很难辨认。VOC 数据集当年用这两个字段来控制评测难度但在实际工业缺陷检测里我一般建议把它们都视为普通样本不要过滤——光伏板缺陷往往就出现在板子边缘或被遮挡处滤掉它们会让模型在真实场景里退化。size 节点则记录图像原始宽高这是后面做坐标系换算的基准千万不能拿缩放后的图片尺寸去套标注。2.2 YOLO 标注是归一化坐标5 个字段的数学关系YOLO 格式每个目标占一行五个字段类别 id、目标中心点 x、目标中心点 y、目标宽度 w、目标高度 h。后四个值都归一化到 0 到 1 之间不需要也不允许出现像素绝对值。换算公式是从 VOC 的 bndbox 推导的x_center (xmin xmax) / 2 / image_width y_center (ymin ymax) / 2 / image_height bbox_width (xmax - xmin) / image_width bbox_height (ymax - ymin) / image_height举个例子上面 XML 里的框在 2560×1920 的图上会变成0 0.4258 0.3763 0.0398 0.0052我第一次看到这种格式时觉得它很反直觉因为它不存左上角和右下角而是存中心点和宽高。但正是这种归一化表示让模型在不同分辨率输入下不需要重新处理标注。训练时你设 imgsz1280 也好推理时输入变成 1024 也好标注的语义保持不变。类别 id 则严格按 classes 列表的顺序来比如crack0、stain1、hotspot2这个顺序决定训练时类别名怎么显示也决定混淆矩阵的行列含义。2.3 一个脚本完成 VOC 与 YOLO 互转关键函数与批量处理虽然这份数据集已经同时打包了两种格式但工程上你迟早要自己写转换换一批标注工具、加一个类别、或者把别的 VOC 数据集并进来。下面这个脚本是我常用的双向转换器跑一次就能把整个 Annotations 目录批量转成 YOLO labels或者反着转回去。import os import xml.etree.ElementTree as ET CLASSES [crack, stain, hotspot] # 示例类别清单按你自己数据集的顺序写 def voc_to_yolo(xml_path, out_txt_path): 将单张 VOC XML 转为 YOLO txt tree ET.parse(xml_path) root tree.getroot() img_w int(root.findtext(.//size/width)) img_h int(root.findtext(.//size/height)) lines [] for obj in root.iter(object): name obj.findtext(name) if name not in CLASSES: continue # 遇到未知类别直接跳过避免类别 id 错位 class_id CLASSES.index(name) xmin int(float(obj.findtext(.//bndbox/xmin))) ymin int(float(obj.findtext(.//bndbox/ymin))) xmax int(float(obj.findtext(.//bndbox/xmax))) ymax int(float(obj.findtext(.//bndbox/ymax))) if xmax xmin or ymax ymin: continue # 宽高为负的脏框直接丢弃 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 lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_txt_path, w, encodingutf-8) as f: f.write(\n.join(lines) \n if lines else ) def batch_voc2yolo(anno_dir, out_dir): 批量转换整个标注目录 os.makedirs(out_dir, exist_okTrue) for fname in os.listdir(anno_dir): if not fname.lower().endswith(.xml): continue stem os.path.splitext(fname)[0] voc_to_yolo(os.path.join(anno_dir, fname), os.path.join(out_dir, stem .txt)) if __name__ __main__: batch_voc2yolo(Annotations, labels)逻辑说明脚本核心是两次解析第一次从 size 取图宽高第二次遍历每个 object 节点取类别和框坐标。归一化结果保留 6 位小数对 2400 张这种规模的数据集足够不会因为位数不足产生相邻目标混淆。遇到 xmax 小于 xmin 这种脏数据直接跳过而不是报错是为了批量处理时不至于因为单张坏标注中断整个目录。参数说明CLASSES列表必须与数据集实际的类别清单一致顺序就是 YOLO 的类别 id。img_w和img_h必须来自原始 XML 的 size 节点不能用os.path.getsize之类的文件大小代替。反向转换yolo_to_voc本质上就是把这个公式倒回去乘回图像宽高得到整数值再写进新 XML 的 bndbox 节点我这里就不重复贴了。2.4 拿到数据集先做三件事验数量、验类别、验越界网上下载的数据集不管来源多正规第一个动作永远是核对而不是直接丢进训练脚本。我习惯在终端跑三组命令花五分钟把数据集的“户口”查清楚# 1. 图片数和标注文件数是否一致缺一张都会在训练时报错或漏样本 ls images/*.jpg | wc -l ls labels/*.txt | wc -l # 2. 每个类别出现的框数量提前知道类别是否均衡 grep -h ^0 labels/*.txt | wc -l grep -h ^1 labels/*.txt | wc -l grep -h ^2 labels/*.txt | wc -l # 3. 抽查归一化坐标是否越界YOLO 要求所有值在 [0, 1] 区间 awk $20 || $21 || $30 || $31 {print FILENAME, $0} labels/*.txt第三组命令尤其重要。很多数据包在导出时如果改了图像尺寸但没同步更新标注就会出现坐标大于 1 的情况训练时某些框架会直接报 label out of bounds有些框架则是静默把越界框裁掉导致你完全不知道模型少学了哪些目标。越界的框能修就修修不了就直接删不然它就像一颗定时炸弹训练到一半报错让你白白重跑几十个 epoch。3. 用 YOLOv8 训练自己的光伏缺陷数据集目录布局、data.yaml 与划分脚本3.1 训练集目录images 和 labels 必须一一对应拿到一份同时带 VOC 和 YOLO 格式的数据集建议直接用 YOLO 目录结构组织训练集因为 Ultralytics YOLOv8 默认就是按 images 和 labels 两个平级目录来寻找标注的。标准布局如下data/ ├── images/ │ ├── train/ │ │ ├── pv_0001.jpg │ │ └── ... │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ │ ├── pv_0001.txt │ │ └── ... │ ├── val/ │ └── test/ └── data.yaml要求很简单images/train 里的 pv_0001.jpg 必须能在 labels/train 里找到同名 pv_0001.txt扩展名不同没关系但文件名主体必须完全一致。YOLOv8 会自动把同目录名的 images 路径替换成 labels 来找标注不需要你在 yaml 里写两遍标注路径。我自己习惯把 test 目录也留出来虽然训练时用不到但最后验证模型泛化能力时有个独立测试集会让你少挨很多骂。3.2 数据划分别只靠随机按批次分组的划分逻辑大部分人拿到 2400 张图第一反应就是random.shuffle然后按 7:2:1 切。这个做法在普通分类任务里行得通但在光伏缺陷检测里很容易埋雷同一个光伏板、同一个拍摄批次、同一个电站的图片在画面结构上高度相似如果随机打乱训练集里出现过的大量相似背景会在验证集里也出现val mAP 虚高到 0.95一到现场就现原形。我一般先看文件名有没有分组信息。很多巡检数据集的文件名会带前缀或拍摄编号比如pv001_0001.jpg、pv001_0002.jpg这种就可以按前缀分组再划分保证同一个组只进一个集合import os import random from collections import defaultdict IMAGES_DIR images random.seed(42) all_imgs sorted(os.listdir(IMAGES_DIR)) groups defaultdict(list) for img in all_imgs: group_key img.split(_)[0] # 假设文件名格式是 组号_序号.jpg groups[group_key].append(img) group_names sorted(groups.keys()) random.shuffle(group_names) train_groups group_names[: int(len(group_names) * 0.7)] val_groups group_names[int(len(group_names) * 0.7): int(len(group_names) * 0.9)] test_groups group_names[int(len(group_names) * 0.9):] def move_files(group_list, subset): os.makedirs(fimages/{subset}, exist_okTrue) os.makedirs(flabels/{subset}, exist_okTrue) for g in group_list: for img in groups[g]: os.rename(os.path.join(images, img), os.path.join(fimages/{subset}, img)) stem os.path.splitext(img)[0] txt stem .txt if os.path.exists(os.path.join(labels, txt)): os.rename(os.path.join(labels, txt), os.path.join(flabels/{subset}, txt)) move_files(train_groups, train) move_files(val_groups, val) move_files(test_groups, test)参数说明random.seed(42)不是玄学它保证你每次跑脚本划分结果一致否则下一次划分不同之前的训练结果就没法复现。按组划分比按图划分更保守代价是验证集会跟训练集的分布差异更大但这才是真实部署时遇到的情况——现场来的图不会和你训练集来自同一批拍摄。3.3 data.yaml 就这么写nc、names、路径三个要素yaml 是 YOLOv8 找数据和类别的唯一入口写错一个字段就全盘报错。最简单的配置长这样path: /abs/path/to/data train: images/train val: images/val test: images/test nc: 3 names: [crack, stain, hotspot]说明一下几个字段的含义path是数据集的绝对路径最好写绝对路径而不是相对路径因为训练脚本的工作目录常常和数据集不在同一层相对路径会解析错。train、val、test写的是相对于path的子目录路径不要加前导斜杠。nc是类别数量必须和names列表长度严格一致多一个少一个都会在你训练时给你一份完全没法看的类别混淆。然后可以直接开始第一次训练。这里我推荐从预训练权重起步而不是随机初始化yolo train modelyolov8s.pt datadata.yaml epochs100 imgsz1280 batch16这条命令会加载 COCO 预训练的 yolov8s 权重并自动冻结前几层做迁移学习。imgsz1280 是关键——如果你用默认的 640裂纹这种几个像素宽的目标会在降采样时直接消失后面避坑章节我会展开说。batch16 是在消费级显卡上的稳妥值24GB 显存的卡可以试着提到 32。3.4 类别不平衡先量化统计标签分布再决定要不要调2400 张图、3 个类别听起来数量还行但真实数据集中三个类别的框数往往差距巨大。隐裂可能每一块板上都有热斑却只有零星几张。如果直接训练模型会把大多数训练精力花在样本最多的类别上对少量类别学不充分。先跑一段统计脚本把每个类别在训练集中的框数量打印出来from collections import Counter def count_labels(label_dir): counter Counter() total_files 0 empty_files 0 for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue total_files 1 with open(os.path.join(label_dir, fname)) as f: lines [line.strip() for line in f if line.strip()] if not lines: empty_files 1 for line in lines: cls int(line.split()[0]) counter[cls] 1 return counter, total_files, empty_files counter, total, empty count_labels(labels/train) print(f标注文件数: {total}, 空文件数: {empty}) for cls_id in range(3): print(f类别 {cls_id}: {counter[cls_id]} 框)逻辑说明统计结果能直接告诉你三件事。第一某个类别框数只有几十个的话后面数据增强要重点往这个类别上倾斜。第二空文件数量多说明不少图片没有缺陷标注这类图在训练里是纯背景样本占比过高会让模型学会“什么都不输出”。第三如果某个类别框数明显异常比如 0说明你的类别 id 映射表写错了VOC 转 YOLO 时类别顺序对不上。如果发现类别严重不平衡常见做法是对少类别做在线复制增强或者简单地对多类别图做欠采样。YOLOv8 没有直接暴露 class weight 参数所以我一般先把少类别的图在数据增强里加大翻转和裁剪概率再不行就复制少类别样本进训练集让它每轮多出现几次。4. 光伏板缺陷检测训练避坑5 个翻车现场与修复方法4.1 图片 2400 张但标签只有 2300 个文件缺失与空标注排查现象训练跑起来没几分钟就报错提示某个图片文件找不到对应标注或者 loss 直接变成 NaN。检查后发现 images 目录有 2400 张 jpglabels 目录却只有 2300 个 txt少了 100 个文件。原因下载的数据集经过网盘压缩再解压部分小文件可能在传输或解压时被跳过也有些标注文件本来就是空的导出时只生成了 0 字节 txt统计时容易忽略。解决在训练前跑一遍上一章的比对逻辑用文件名集合做差集把缺标注和空标注的图片单独列出来。对空标注文件如果确定图片上确实没有缺陷就保留空 txt 作为背景样本如果只是标注丢了就从 VOC 的 XML 重新转换一次别偷懒否则模型会在缺失标签的图上把无缺陷区域当成前景去学训练指标直接乱掉。4.2 裂纹被降采样吞掉imgsz640 时模型为什么学不到现象第一次训练用默认 imgsz640跑了 100 个 epochval mAP50 只有 0.2 左右预测可视化时发现所有缺陷都没检测出来模型输出几乎全是背景类。原因光伏板缺陷里的隐裂是几像素宽、几十像素长的细线。原图如果是一张 2560×1920 的航拍图缩放到 640×480 时裂纹宽度会降到 1 像素以下卷积核根本采不到足够的纹理信息模型只能学到“图上有没有深色的东西”这种模糊表征。解决把 imgsz 提高到 1280 或 1536代价是显存和训练时间上涨。如果显存不够另一个常见做法是把大图切成 640×640 的 patch 再训练但切图时注意标注框跨边界的问题需要加 overlap 或按中心归属来分配 patch。我一般更喜欢直接开 1280因为切 patch 会引入额外的预处理复杂度而且有些现场照片本身分辨率就不高切了反而没有帮助。4.3 val mAP 虚高而现场漏检同源数据泄漏现象训练时 val mAP50 能到 0.93看起来效果极好。模型一部署到现场新拍的照片上漏检率陡然升高特别是裂纹类几乎全部漏掉。原因这是数据划分策略的问题。如果随机洗牌时把同一个光伏板、同一批拍摄条件的照片同时分到训练集和验证集验证集里的目标在纹理、光线、角度上都和训练集高度相似模型相当于“见过”验证集的答案mAP 自然虚高。真实现场的照片来自完全不同的拍摄批次分布差异立刻暴露。解决严格按照拍摄批次、电站、时间分组划分训练和验证集。宁可验证集数量少一些、难一些也不要让它和训练集同源。另外建议留出一部分完全没参与训练的数据做最终 smoke test只跑推理不看训练指标才能测出真实泛化能力。4.4 缺陷占太少模型只会输出背景正负样本失衡现象训练 loss 下降正常但预测时所有图片都没有框或者只有零星几个框置信度也普遍低于 0.1。原因3 类缺陷目标在 2400 张图里可能只占几十个框而每张图上的背景区域占了绝对多数。锚点匹配时每个位置都在学习“这里没有目标”正样本的梯度被负样本淹没。模型不是没能力检测而是它发现“全输出背景”这一招就能把整体 loss 降得很低。解决先做两类操作。第一把置信度阈值调低验证时看原始输出而不是只看 conf_thres0.25 以上的框。第二数据层面把少类别样本复制几份进训练集或者适当提高 mosaic 和透视增强的概率让同一张缺陷图在每轮训练中以不同形态出现多次把正样本的有效占比拉上来。极端情况下可以关掉 mosaic 做一轮因为 mosaic 虽然增加背景多样性但也把目标缩小了对细长缺陷不友好。4.5 导出 ONNX 掉点预处理不一致与固定输入尺寸现象训练时 mAP 很高导出成 ONNX 后在 onnxruntime 里跑同一批验证图mAP 掉了将近 15 个百分点。原因大部分情况是预处理管线不一致。YOLOv8 训练时图像先做 BGR 通道、letterbox、归一化到 0-1然后才进网络。导出 ONNX 后如果外部推理代码忘记做 letterbox或者把 RGB 当成 BGR 输入模型的输入分布就变了精度必然下降。还有个隐蔽问题是动态输入尺寸训练时是 1280推理时喂进去 1024会轻微影响 BatchNorm 统计量的表现。解决导出时用yolo export modelbest.pt formatonnx imgsz1280 opset12固定输入尺寸导出前的预处理代码和训练时保持完全一致。如果要部署到 Orin 这类边缘设备上建议导出后再用 TensorRT 做一轮 int8 或 fp16 校准缺陷检测这种细目标场景优先保留 fp16int8 掉点太明显。5. 从 2400 张扩到可部署模型基座选型、数据增强与训练策略5.1 选 n 还是 s基座模型在 2400 张数据上的取舍YOLOv8 从 n 到 x 模型体积依次变大但在 2400 张的小数据集上大模型并不一定更好。n 模型参数量小、训练快适合快速验证思路s 模型在召回率上比 n 有明显提升尤其是在像素级细目标上m 以上在数据量不足时容易过拟合训练 loss 降得漂亮验证集 mAP 反而回落。我一般用 s 起步。理由很简单缺陷检测对漏检的容忍度很低我们更需要在召回率上多留余量。s 在 1280 输入下显存占用中等大多数 GPU 都能跑。如果训练资源确实紧张n 也能出结果但推理时的误检框会多一些需要你在 conf_thres 或 NMS 参数上花更多时间调。5.2 mosaic 对细裂纹并不友好增强参数怎么给很多人会用默认增强参数直接训练但默认值是按通用检测任务调的不是按光伏板缺陷调的。mosaic 会把四张图拼在一起缩小目标面积变小细长裂纹在这种组合下更难被模型捕捉。水平翻转和上下翻转相对安全但要注意光伏板的纹理方向——如果数据集中裂纹大多是垂直走向翻转后模型学的方向特征会变乱。我自己常用这组增强参数缺陷检测场景够用且不会太过augment_params { mosaic: 0.3, # 降低默认的 1.0避免小目标被拼图再缩小 hsv_h: 0.01, # 光伏板颜色单一色相抖动调小 hsv_s: 0.3, # 饱和度轻微抖动模拟不同光照 hsv_v: 0.3, # 亮度抖动稍大模拟早晚和阴影 fliplr: 0.5, flipud: 0.3, # 上下翻转概率调低保持裂纹方向语义 degrees: 5.0, # 小角度旋转模拟巡检姿态偏差 scale: 0.4, # 缩放范围别太大避免目标缩小 }参数说明mosaic0.3让 30% 的批次做拼图增强其余保持原始尺寸degrees5控制旋转角度太大容易把光伏板的矩形边框旋转成奇怪形状模型学到不真实的方向特征。这套参数不一定对所有场景最优但比默认值更适合细长缺陷。5.3 损失函数与后处理box_loss、conf_thres 的工程含义训练日志里那几项 loss很多人只看总和其实分开才有意义。YOLOv8 的 loss 由三块组成box_loss 负责框的位置精度cls_loss 负责类别判断dfl_loss 负责边缘分布拟合。对光伏缺陷来说box_loss 和 dfl_loss 才是重点——因为裂纹框是长条形宽高比悬殊模型对它的位置稍微偏差一点框就和真实缺陷错开一大截。如果看到 dfl_loss 降得很慢说明模型对边界定位不敏感优先检查 imgsz 和数据增强而不是换模型结构。推理时的两个阈值参数对缺陷检测影响很大。conf_thres是置信度门槛通用任务常用 0.25但缺陷检测我一般降到 0.15 左右宁可多出几个误检框也别让真缺陷从低置信度漏过去。iou_thres用于 NMS默认 0.45长条形目标之间几乎不重叠基本不用动。预测命令如下yolo predict modelbest.pt source./val/imgs imgsz1280 conf0.15 iou0.455.4 两阶段迁移学习先固 backbone 再全量调2400 张数据不足以从零训练但也不建议直接全量微调 COCO 权重。我的习惯是分两个阶段。第一阶段把 backbone 冻结只训练检测头让模型先适应光伏板的颜色纹理和缺陷框的尺度分布一般跑 30 个 epoch 就能看到比较合理的 loss 下降。第二阶段解冻所有层用小学习率全量微调把 backbone 里卷积核的语义慢慢迁移到“隐裂是这种纹理、热斑是这种色块”的知识上。yolo train modelyolov8s.pt datadata.yaml epochs30 imgsz1280 batch16 freeze10 yolo train modelruns/detect/train/weights/last.pt datadata.yaml epochs100 imgsz1280 batch16 lr00.001参数说明freeze10表示冻结模型前 10 层Ultralytics 会自动按层索引冻结 backbone第一阶段结束后的 last.pt 直接作为第二阶段初始权重这时学习率降到 0.001避免在大梯度下把预训练学到的通用特征冲掉。6. 拿结果说话mAP、混淆矩阵与缺陷漏检率的判断口径6.1 不要只报 mAP用混淆矩阵找出“漏掉最多的那一类”训练结束后大家总会先看一眼 val mAP50但单看这个数字不够。我每次都用yolo val modelbest.pt datadata.yaml拿到完整指标然后专门看每个类别的单类 AP 和混淆矩阵。混淆矩阵里威力最大的信息不在对角线而在列和行哪一类的真实缺陷被预测成背景、哪一类的误检最多一目了然。如果裂纹那列的漏检率超过了 20%就算整体 mAP50 有 0.85这个模型也还不适合上场。6.2 工程验收看两个数漏检率与误报率到了和业务方对需求的时候我不太谈 mAP会谈两个更直观的数字漏检率和误报率。漏检率是“有缺陷的板子里模型漏掉了多少”误报率是“模型报出来的框里有多少其实不是缺陷”。具体统计时我会拿着验证集跑一次推理把预测结果和标注做匹配阈值设定为置信度 0.15然后单独算出平均每块光伏板的误报框数。这个习惯是吃过亏换来的。有一次我盯着 val mAP 看了整个下午指标漂亮得接近完美结果客户拿来的真实红外照片里全是模型从来没见过的热斑形态漏检率直接没法看。从那以后我每份数据都先跑一遍 badcase 统计验证集里漏掉最多的缺陷类别才是我接下来真正要优化的对象——标注要不要补增强参数要不要改都是在回答一个问题我们到底能不能容忍这一类的漏。希望这篇能把你在光伏板缺陷检测这条路上该走的弯路提前标出来少踩几个坑多留点时间给真正难啃的漏检问题。本文还有配套的精品资源点击获取