光伏电池缺陷检测:xml标注解析与YOLO格式转换实战

发布时间:2026/10/1 16:08:51
光伏电池缺陷检测:xml标注解析与YOLO格式转换实战 简介这份资源面向计算机视觉开发者与深度学习学习者提供光伏电池缺陷检测的目标检测数据集用于训练模型识别并定位电池表面的“损坏”与“无效”两类缺陷可服务于太阳能行业的质量控制场景。压缩包共433个文件包含216个png图像、216个xml标注文件及1个json类别索引文件整体约8.35MBxml中记录每个目标的边界框坐标与类别信息json则便于映射类别标签。目前已有917人学习下载适合作为Faster R-CNN、YOLO等检测模型的训练与验证素材。读者可据此完成标注解析、训练集与验证集划分、模型训练及mAP、召回率等指标评估快速搭建从数据预处理到缺陷定位的完整流程是入门目标检测与工业质检实践的实用起点。1. 光伏电池缺陷检测数据集xml 标注文件到底怎么用光伏电池片产线上的 EL 和 PL 成像设备每天产出几万张图真正卡住算法团队的往往不是模型结构而是手里那堆 xml 标注文件——它们来自 LabelImg、CVAT 或产线自研标注工具格式看着差不多字段却各写各的。目标检测数据在光伏电池缺陷检测这个场景里核心诉求就三件事把 xml 里的缺陷框准确读出来、把类别体系对齐到产线关心的几类缺陷隐裂、断栅、黑斑、混档、边缘崩边、再把标注转成 YOLO 或 COCO 能直接吃的格式。这套流程做扎实了后面换 yolov8、yolov11 还是更新的检测头数据侧都不用返工。这篇写给正在处理光伏 EL 缺陷标注、被 xml 字段和坐标转换折腾过的工程师从解析、校验到转换、训练前检查一步步走通。2. 先看懂 xml 标注文件字段、坐标系与光伏缺陷类别2.1 一份典型光伏缺陷 xml 里有什么Pascal VOC 风格的 xml 是光伏缺陷标注里最常见的一种结构大致长这样annotation folderPV_EL_2024/folder filenamecell_000123.jpg/filename size width1024/width height1024/height depth3/depth /size object namecrack/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin312/xmin ymin488/ymin xmax401/xmax ymax560/ymax /bndbox /object /annotationsize里的宽高是原图尺寸bndbox用的是左上角加右下角的绝对像素坐标原点在图片左上角x 向右、y 向下。光伏 EL 图通常是 1024×1024 或 2048×2048 的灰度图但标注工具存出来的 xml 里depth可能写 3实际图是单通道这个不一致后面会咬人。name字段是类别名光伏场景里常见的有 crack隐裂、finger_break断栅、black_spot黑斑、edge_damage崩边、mixed混档。不同标注团队命名习惯不一样有的用中文有的用拼音缩写有的把隐裂细分成微裂和贯穿裂。类别体系不统一是后面训练翻车的头号原因。2.2 坐标系与坐标越界光伏图最容易出的问题光伏电池片是方形缺陷经常贴着边缘标注时手一抖xmax就超过width或者xmin写成负数。这类框在转 YOLO 归一化坐标时会算出大于 1 或小于 0 的值训练时不一定报错但会让模型学到错误的边界信息。判断一个框是否越界逻辑很简单def is_valid_box(box, w, h): xmin, ymin, xmax, ymax box # 坐标必须落在图像范围内 if xmin 0 or ymin 0 or xmax w or ymax h: return False # 宽高必须为正排除标注时点反了的情况 if xmax xmin or ymax ymin: return False return True参数说明box是(xmin, ymin, xmax, ymax)四元组w、h来自 xml 的size字段。返回 False 的框要么修正要么丢弃别直接塞进训练集。我一般会把越界框单独写到一个bad_boxes.txt里回头找标注团队核对而不是默默裁掉——裁掉会改变缺陷的真实形态。2.3 类别体系对齐别让 crack 和 Crack 变成两类光伏缺陷的类别名大小写、空格、中英文混用极其常见。crack、Crack、crack带尾空格、隐裂在字符串层面是四个不同类别直接转 YOLO 会生成四个类模型学得稀里糊涂。对齐做法是维护一张映射表LABEL_MAP { crack: crack, Crack: crack, 隐裂: crack, finger_break: finger_break, 断栅: finger_break, black_spot: black_spot, 黑斑: black_spot, edge_damage: edge_damage, 崩边: edge_damage, } def normalize_label(raw_name): key raw_name.strip() if key not in LABEL_MAP: # 遇到没见过的类别名记下来人工确认不要静默丢弃 print(f[WARN] unknown label: {raw_name}) return None return LABEL_MAP[key]参数说明LABEL_MAP的 key 是 xml 里可能出现的原始写法value 是最终统一的英文类别名。normalize_label返回 None 表示这个类别没登记过需要人工介入。光伏项目里类别通常就 5 到 8 类映射表维护成本很低但能省掉后面大量排查时间。3. 用 Python 批量解析 xml 并转成 YOLO 格式3.1 解析 xml 的最小可用脚本批量处理几千个 xml用xml.etree.ElementTree就够不必上 lxml。下面这个脚本读一个目录下所有 xml输出 YOLO 格式的 txtimport os import xml.etree.ElementTree as ET CLASSES [crack, finger_break, black_spot, edge_damage, mixed] CLASS_TO_ID {c: i for i, c in enumerate(CLASSES)} def convert_one(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) lines [] for obj in root.findall(object): raw_name obj.find(name).text cls_name normalize_label(raw_name) if cls_name is None or cls_name not in CLASS_TO_ID: continue bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) if not is_valid_box((xmin, ymin, xmax, ymax), w, h): print(f[SKIP] invalid box in {xml_path}) continue # YOLO 用归一化的中心点加宽高 cx (xmin xmax) / 2.0 / w cy (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h cls_id CLASS_TO_ID[cls_name] lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) base os.path.splitext(os.path.basename(xml_path))[0] out_path os.path.join(out_dir, base .txt) with open(out_path, w) as f: f.write(\n.join(lines))逻辑说明先读size拿到原图宽高再遍历每个object做类别归一化和坐标校验最后把绝对坐标转成 YOLO 的归一化中心点格式。cx、cy、bw、bh都除以了w或h所以值域在 0 到 1 之间。保留 6 位小数是为了避免小目标精度损失——光伏隐裂有时候只有十几个像素宽小数位太少会引入明显误差。参数说明CLASSES的顺序决定了类别 id一旦定下来就不要改改了要重新生成所有标签。out_dir要和图片目录分开管理后面做数据集划分时按文件名配对。3.2 批量跑目录并统计类别分布单文件转换跑通后套一层目录遍历import glob def convert_all(xml_dir, out_dir): os.makedirs(out_dir, exist_okTrue) xml_files glob.glob(os.path.join(xml_dir, *.xml)) print(ffound {len(xml_files)} xml files) for xp in xml_files: try: convert_one(xp, out_dir) except Exception as e: # 单个文件出错不要中断整批记下来单独处理 print(f[ERROR] {xp}: {e}) # 统计每个类别的框数量判断类别是否失衡 counter {c: 0 for c in CLASSES} for txt in glob.glob(os.path.join(out_dir, *.txt)): with open(txt) as f: for line in f: if line.strip(): cid int(line.split()[0]) counter[CLASSES[cid]] 1 print(class distribution:, counter)逻辑说明glob拿到所有 xml逐个转换用 try/except 保证一个坏文件不会让整批停掉。转换完统计每个类别的框数这一步很关键——光伏数据里隐裂可能占 70%崩边可能只有 2%类别极度失衡时直接训练会让模型偏向多数类。参数说明xml_dir是标注目录out_dir是输出 txt 目录。统计结果里如果某一类少于总框数的 1%要么补标注要么在训练时用重采样或 focal loss 处理别指望模型自己学会。3.3 划分训练验证集按电池片而不是按图光伏产线一个电池片可能拍多张图不同角度或不同曝光如果按图随机划分同一个电池片的图可能同时出现在训练集和验证集验证指标会虚高。正确做法是按电池片 id 分组划分。import random from collections import defaultdict def split_by_cell(txt_dir, img_dir, val_ratio0.2): # 假设文件名格式为 cellid_xxx.txt取 cellid 作为分组键 groups defaultdict(list) for txt in glob.glob(os.path.join(txt_dir, *.txt)): base os.path.splitext(os.path.basename(txt))[0] cell_id base.split(_)[0] groups[cell_id].append(base) cell_ids list(groups.keys()) random.seed(42) random.shuffle(cell_ids) n_val int(len(cell_ids) * val_ratio) val_cells set(cell_ids[:n_val]) train_list, val_list [], [] for cid, bases in groups.items(): target val_list if cid in val_cells else train_list for b in bases: target.append(b) return train_list, val_list逻辑说明先按文件名前缀聚合成电池片分组再对分组打乱后切分保证同一电池片的所有图只进一个集合。random.seed(42)固定随机种子方便复现。参数说明val_ratio一般取 0.15 到 0.2光伏数据量小时可以取 0.25。如果产线有明确的时间划分比如按天优先按时间切避免数据泄漏。4. 避坑与排查xml 转 YOLO 最常见的 5 个翻车点4.1 图片和标签对不上号现象训练启动时报No labels found或大量图片被跳过。原因图片是.jpgxml 里filename写的是.png或者转换后 txt 的文件名和图片名差了一个前缀。光伏数据经常从不同设备导出命名规则不统一。解决转换后做一次配对检查遍历图片目录确认每个图片都有同名 txtdef check_pairing(img_dir, txt_dir): imgs glob.glob(os.path.join(img_dir, *.jpg)) missing [] for img in imgs: base os.path.splitext(os.path.basename(img))[0] if not os.path.exists(os.path.join(txt_dir, base .txt)): missing.append(base) print(fmissing labels: {len(missing)}) return missing4.2 坐标没归一化就喂给 YOLO现象训练 loss 一开始就很大或者模型完全学不动。原因把 xml 里的绝对像素坐标直接写进 txt没有除以宽高。YOLO 期望的是 0 到 1 的归一化值绝对坐标会被当成超大框。解决转换脚本里强制检查任何cx、cy、bw、bh超过 1.0 就报警if max(cx, cy, bw, bh) 1.0: print(f[WARN] unnormalized box in {xml_path})4.3 空标签文件被当成负样本现象验证集 mAP 正常但实际推理时误检特别多。原因有些图片确实没有缺陷转换后生成了空 txt。YOLO 会把空 txt 当成负样本这本身没错但如果空 txt 是因为解析失败产生的就会把有缺陷的图当成无缺陷训练。解决区分「真负样本」和「解析失败」。解析失败时不要写空文件而是跳过并记录真负样本才写空 txt。在转换脚本里如果 xml 里有object但一个都没转成功要单独报警。4.4 类别 id 和 data.yaml 不一致现象训练能跑但预测出来的类别名全是错的。原因转换脚本里CLASSES的顺序和data.yaml里的names顺序不一致。比如脚本里 crack 是 0yaml 里 crack 是 2。解决把类别定义抽到一个公共文件转换和训练都从同一个地方读。光伏项目类别少但一旦错位排查起来很费时间。4.5 小目标框在缩放时丢失现象隐裂这类细长缺陷在训练时召回率很低。原因YOLO 默认输入 640×640光伏原图 1024×1024 缩下来十几像素宽的隐裂只剩几个像素特征几乎没了。解决训练时把imgsz提到 1024 或 1280或者用切片推理SAHI 那套思路。另外在转换阶段确认小框没有被误判为无效框丢掉——is_valid_box只检查越界和宽高为正不检查框的大小这点要守住。5. 从标注到训练数据质量检查与进阶技巧5.1 训练前必做的三项数据体检转完格式别急着开训先做三项检查。第一项是类别分布用 3.2 里的统计脚本跑一遍看有没有类别少于 1%。第二项是框的尺寸分布把bw、bh乘回原图尺寸统计宽高直方图光伏隐裂的宽高比通常很极端细长如果发现大量接近正方形的框可能是标注时框错了。第三项是可视化抽查随机抽 20 张图把框画出来看import cv2 def visualize(img_path, txt_path, classes): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path) as f: for line in f: if not line.strip(): continue cid, cx, cy, bw, bh line.split() cid int(cid) cx, cy, bw, bh map(float, (cx, cy, bw, bh)) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, classes[cid], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(check_vis.jpg, img)参数说明classes是类别名列表顺序必须和训练配置一致。可视化抽查是性价比最高的一步很多标注错误肉眼一看就出来了比看 loss 曲线快得多。5.2 用 xml 里的 difficult 字段做难例挖掘Pascal VOC 的 xml 里有个difficult字段很多标注工具默认写 0但光伏场景里标注员有时会把模糊的隐裂标成difficult1。这个字段别浪费转换时可以单独记录训练时对这些样本加权或者在验证时单独看这一子集的指标。def collect_difficult(xml_dir): hard_samples [] for xp in glob.glob(os.path.join(xml_dir, *.xml)): root ET.parse(xp).getroot() for obj in root.findall(object): diff obj.find(difficult) if diff is not None and diff.text 1: hard_samples.append(os.path.basename(xp)) break return hard_samples逻辑说明遍历所有 xml只要有一个difficult1的框就把这张图记为难例。返回的列表可以拿去单独评估模型表现也可以作为主动学习的候选集优先补标。5.3 增量标注时怎么合并新旧 xml产线跑起来后标注是持续增加的。新一批 xml 和老数据合并时最容易出的问题是同一张图被标了两次或者类别名用了新的写法。我的习惯是维护一个processed_files.txt记录已经转过的图片名新数据进来先过滤掉已处理的再跑转换。类别映射表也要同步更新遇到新类别名先停下来确认别让脚本自动放行。这套流程我在几个光伏 EL 项目里反复用过最深的教训是数据侧的检查脚本要写得比模型代码还细。模型换一版可能涨一个点但一个类别映射错误能让指标直接掉十个点而且排查起来毫无头绪。把 xml 解析、坐标校验、类别对齐、配对检查这几步固化成脚本每次新数据进来跑一遍比事后救火省心得多。希望帮到你。本文还有配套的精品资源点击获取