☰
光伏板缺陷检测实战:YOLOv8数据集制作与训练避坑指南
2026/10/11 0:53:37 网站建设 项目流程

简介:面向光伏板表面缺陷检测任务,该数据集涵盖裂纹、栅线与斑点三类典型缺陷,并同时提供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 标注其实是一份 XML:object、size 和 bndbox 的读法

Pascal VOC 是目标检测领域最经典的标注格式之一,很多标注工具导出时默认就是它。打开任意一张图的标注文件,你能看到一整棵 XML 树:root 节点下面有 folder、filename、source、size,然后是一串 object 节点,每个 object 对应一个目标框。

<annotation> <folder>PV2024</folder> <filename>pv_0187.jpg</filename> <source> <database>Unknown</database> </source> <size> <width>2560</width> <height>1920</height> <depth>3</depth> </size> <object> <name>crack</name> <bndbox> <xmin>1040</xmin> <ymin>721</ymin> <xmax>1142</xmax> <ymax>731</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

我第一次看到这种格式时觉得它很反直觉,因为它不存左上角和右下角,而是存中心点和宽高。但正是这种归一化表示,让模型在不同分辨率输入下不需要重新处理标注。训练时你设 imgsz=1280 也好,推理时输入变成 1024 也好,标注的语义保持不变。类别 id 则严格按 classes 列表的顺序来,比如crack=0、stain=1、hotspot=2,这个顺序决定训练时类别名怎么显示,也决定混淆矩阵的行列含义。

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", encoding="utf-8") as f: f.write("\n".join(lines) + "\n" if lines else "") def batch_voc2yolo(anno_dir, out_dir): """批量转换整个标注目录""" os.makedirs(out_dir, exist_ok=True) 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 '$2<0 || $2>1 || $3<0 || $3>1 {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(f"images/{subset}", exist_ok=True) os.makedirs(f"labels/{subset}", exist_ok=True) for g in group_list: for img in groups[g]: os.rename(os.path.join("images", img), os.path.join(f"images/{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(f"labels/{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 model=yolov8s.pt data=data.yaml epochs=100 imgsz=1280 batch=16

这条命令会加载 COCO 预训练的 yolov8s 权重,并自动冻结前几层做迁移学习。imgsz=1280 是关键——如果你用默认的 640,裂纹这种几个像素宽的目标会在降采样时直接消失,后面避坑章节我会展开说。batch=16 是在消费级显卡上的稳妥值,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 张 jpg,labels 目录却只有 2300 个 txt,少了 100 个文件。

原因:下载的数据集经过网盘压缩再解压,部分小文件可能在传输或解压时被跳过;也有些标注文件本来就是空的,导出时只生成了 0 字节 txt,统计时容易忽略。

解决:在训练前跑一遍上一章的比对逻辑,用文件名集合做差集,把缺标注和空标注的图片单独列出来。对空标注文件,如果确定图片上确实没有缺陷,就保留空 txt 作为背景样本;如果只是标注丢了,就从 VOC 的 XML 重新转换一次,别偷懒,否则模型会在缺失标签的图上把无缺陷区域当成前景去学,训练指标直接乱掉。

4.2 裂纹被降采样吞掉:imgsz=640 时模型为什么学不到

现象:第一次训练用默认 imgsz=640,跑了 100 个 epoch,val 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_thres=0.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 model=best.pt format=onnx imgsz=1280 opset=12固定输入尺寸,导出前的预处理代码和训练时保持完全一致。如果要部署到 Orin 这类边缘设备上,建议导出后再用 TensorRT 做一轮 int8 或 fp16 校准,缺陷检测这种细目标场景优先保留 fp16,int8 掉点太明显。

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, # 缩放范围别太大,避免目标缩小 }

参数说明:mosaic=0.3让 30% 的批次做拼图增强,其余保持原始尺寸;degrees=5控制旋转角度,太大容易把光伏板的矩形边框旋转成奇怪形状,模型学到不真实的方向特征。这套参数不一定对所有场景最优,但比默认值更适合细长缺陷。

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 model=best.pt source=./val/imgs imgsz=1280 conf=0.15 iou=0.45

5.4 两阶段迁移学习:先固 backbone 再全量调

2400 张数据不足以从零训练,但也不建议直接全量微调 COCO 权重。我的习惯是分两个阶段。第一阶段把 backbone 冻结,只训练检测头,让模型先适应光伏板的颜色纹理和缺陷框的尺度分布,一般跑 30 个 epoch 就能看到比较合理的 loss 下降。第二阶段解冻所有层,用小学习率全量微调,把 backbone 里卷积核的语义慢慢迁移到“隐裂是这种纹理、热斑是这种色块”的知识上。

yolo train model=yolov8s.pt data=data.yaml epochs=30 imgsz=1280 batch=16 freeze=10 yolo train model=runs/detect/train/weights/last.pt data=data.yaml epochs=100 imgsz=1280 batch=16 lr0=0.001

参数说明:freeze=10表示冻结模型前 10 层,Ultralytics 会自动按层索引冻结 backbone;第一阶段结束后的 last.pt 直接作为第二阶段初始权重,这时学习率降到 0.001,避免在大梯度下把预训练学到的通用特征冲掉。

6. 拿结果说话:mAP、混淆矩阵与缺陷漏检率的判断口径

6.1 不要只报 mAP:用混淆矩阵找出“漏掉最多的那一类”

训练结束后大家总会先看一眼 val mAP50,但单看这个数字不够。我每次都用yolo val model=best.pt data=data.yaml拿到完整指标,然后专门看每个类别的单类 AP 和混淆矩阵。混淆矩阵里威力最大的信息不在对角线,而在列和行:哪一类的真实缺陷被预测成背景、哪一类的误检最多,一目了然。如果裂纹那列的漏检率超过了 20%,就算整体 mAP50 有 0.85,这个模型也还不适合上场。

6.2 工程验收看两个数:漏检率与误报率

到了和业务方对需求的时候,我不太谈 mAP,会谈两个更直观的数字:漏检率和误报率。漏检率是“有缺陷的板子里,模型漏掉了多少”,误报率是“模型报出来的框里,有多少其实不是缺陷”。具体统计时,我会拿着验证集跑一次推理,把预测结果和标注做匹配,阈值设定为置信度 0.15,然后单独算出平均每块光伏板的误报框数。

这个习惯是吃过亏换来的。有一次我盯着 val mAP 看了整个下午,指标漂亮得接近完美,结果客户拿来的真实红外照片里全是模型从来没见过的热斑形态,漏检率直接没法看。从那以后,我每份数据都先跑一遍 badcase 统计,验证集里漏掉最多的缺陷类别,才是我接下来真正要优化的对象——标注要不要补,增强参数要不要改,都是在回答一个问题:我们到底能不能容忍这一类的漏。

希望这篇能把你在光伏板缺陷检测这条路上该走的弯路提前标出来,少踩几个坑,多留点时间给真正难啃的漏检问题。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询