news 2026/9/10 13:41:02

光伏板积灰检测数据集:VOC转YOLO、去重与YOLOv8训练

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
光伏板积灰检测数据集:VOC转YOLO、去重与YOLOv8训练

简介:面向太阳能光伏板积灰与灰尘检测场景的目标检测数据集,包含1463张JPEG图像及对应的VOC格式XML与YOLO格式TXT标注文件,标注类别为Dirt单一类,共6822个矩形框。压缩包约75.98MB,共2000个文件,以XML与TXT标注文件为主,目录结构清晰,可直接接入YOLO、MMDetection等主流检测框架训练。数据集使用labelImg人工画框标注,类别规则统一,适合光伏运维巡检、新能源视觉检测方向的学习者快速搭建基线模型,也可用于熟悉VOC与YOLO格式转换流程。由于图片重复度较高且价格定位偏低,更适合初学者练手或作为数据增强素材。已有536人学习下载,是一份实用且易上手的入门级光伏积灰检测数据资源。

1. 光伏板积灰检测,先处理数据再谈模型

巡检无人机传回来的光伏板照片里,真正影响发电量的往往不是热斑和隐裂,而是组件表面积灰。这套太阳能光伏板积灰灰尘检测数据集提供了1463张jpg图片,标注类别只有Dirt,总框数6822,同时给出labelImg导出的VOC xml和转换好的YOLO txt,不需要再做格式迁移就能接入yolo目标检测流程。适合两类人:一类是刚跑通yolov5或yolov8、想用真实脏污样本把训练流程走完整的初学者;另一类是准备研究数据增强和重复样本去重策略的算法工程师。需要先说清楚,数据集说明里明确写了图片重复度很高,如果你不管这个特点直接随机划分train/val,mAP会虚高,后面所有指标判断都不可信。所以拿到压缩包之后第一件事不是训练,是清洗和复核标注。

2. 数据集结构与VOC/YOLO标注格式的转换细节

2.1 解压后先核对三类同名文件

压缩包解压后是平铺结构,jpg、xml、txt三个后缀同名共处一个目录,文件名类似firc_solarpanel_1345.jpgfirc_solarpanel_1345.xmlfirc_solarpanel_1345.txt,另外附带一个使用前必读.txt说明文件。这里的txt是YOLO检测格式,每行对应一个目标框,不包含分割掩码路径,和分割任务用的txt不是一回事。

unzip firc_solarpanel_1463.zip -d solar_panel_dirt cd solar_panel_dirt find . -name "*.jpg" | wc -l find . -name "*.xml" | wc -l find . -name "*.txt" ! -name "使用前必读.txt" | wc -l

unzip -d指定解压目录,后面三条find命令分别统计jpg、xml、txt数量,! -name把说明文件排除在统计之外。我这里实际跑完得到三个1463,和摘要描述一致。如果某个数字偏少,优先检查是不是有空的xml文件,或者文件名后缀大小写不一致。

这三种文件的对应关系如下表:

文件类型数量作用
jpg1463原始光伏板图像
xml1463VOC格式标注,包含目标类别与绝对坐标
txt1463YOLO格式标注,包含类别id与归一化坐标

总框数6822除以1463张图,平均每张约4.66个框,说明Dirt目标不是每张只框一个大区域,而是分散在组件表面多个位置。这个分布特点会影响后面训练时anchor和imgsz的选择。

2.2 VOC的xml与YOLO的txt字段差异

VOC格式的核心是object节点,一个目标对应一个<object>,坐标用左上角和右下角的绝对像素值表示:

<annotation> <filename>firc_solarpanel_1345.jpg</filename> <size> <width>1280</width> <height>960</height> <depth>3</depth> </size> <object> <name>Dirt</name> <bndbox> <xmin>100</xmin> <ymin>80</ymin> <xmax>300</xmax> <ymax>240</ymax> </bndbox> </object> </annotation>

上面这段是示意结构,实际坐标值以压缩包内xml为准。YOLO txt里每一行是类别id 中心点x 中心点y 宽度 高度,且全部除以图片宽高做了归一化。两种格式的差异直接决定了转换逻辑:

对比项VOC xmlYOLO txt
类别标识字符串名称,如Dirt整数索引,如0
坐标形式xmin、ymin、xmax、ymaxx_center、y_center、width、height
坐标范围像素绝对值0到1之间的归一化值
是否需要图片尺寸是,xml里自带size节点否,训练时从图像本身获取

转换时容易踩的坑是坐标顺序。VOC里先给左上角再给右下角,YOLO里是先给中心点再给宽高,两个顺序很容易换错。另外类别索引必须从0开始,Dirt对应0,如果后续自己加了一类,这个索引就会整体错位。

2.3 用Python做VOC到YOLO的批量转换

labelImg默认导出VOC xml,而yolov5和yolov8训练需要YOLO txt。转换脚本关键点在于把绝对坐标换算成归一化中心坐标:

import os import xml.etree.ElementTree as ET source_xml_dir = "annotations" target_txt_dir = "labels" os.makedirs(target_txt_dir, exist_ok=True) class_names = ["Dirt"] def convert(size, box): dw = 1.0 / size[0] dh = 1.0 / size[1] x = (box[0] + box[1]) / 2.0 y = (box[2] + box[3]) / 2.0 w = box[1] - box[0] h = box[3] - box[2] return x * dw, y * dh, w * dw, h * dh for xml_name in os.listdir(source_xml_dir): if not xml_name.endswith(".xml"): continue root = ET.parse(os.path.join(source_xml_dir, xml_name)).getroot() size = root.find("size") width = int(size.find("width").text) height = int(size.find("height").text) lines = [] for obj in root.findall("object"): cls_id = class_names.index(obj.find("name").text) bndbox = obj.find("bndbox") box = [ float(bndbox.find("xmin").text), float(bndbox.find("ymin").text), float(bndbox.find("xmax").text), float(bndbox.find("ymax").text), ] cx, cy, bw, bh = convert((width, height), box) lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_path = os.path.join(target_txt_dir, xml_name.replace(".xml", ".txt")) with open(out_path, "w") as f: f.write("\n".join(lines))

脚本读取xml里的width和height,把左上角和右下角的坐标换算成中心点加宽高的归一化形式。class_names.index负责把字符串类别映射成int索引,所以class_names列表的顺序必须和后续训练配置里的names顺序完全一致。{:.6f}保留6位小数,对640或960分辨率的图片足够,不需要担心精度损失。

注意:xml里如果缺少<size>节点,这段脚本会直接报错。处理办法是打开对应图片用PIL读取宽高补进去,而不是跳过该文件。

3. 高重复度图片的清洗与标注复核

3.1 为什么重复图片会让训练指标失真

数据集的重复度高,这是影响使用方式的最关键属性。所谓重复不一定是完全相同的文件,也可能是同一块光伏板在相近角度、相近光照下拍摄的多张图片。这种近似重复如果同时落入训练集和验证集,模型等于提前见过验证集答案,mAP会异常高,但部署到新场景立刻打回原形。

常规做法是先用感知哈希把近似重复图片分到同一组,然后按组为单位划分数据集,保证同组图片要么全在train、要么全在val。另一个容易被忽略的动作是核对标注总数,1463个xml和1463个txt应该都包含6822个框,如果txt转换时漏了某个object,最终训练框数会偏少。

3.2 用感知哈希找出近似重复

感知哈希里最常用的是dhash和phash,dhash对亮度变化敏感,phash对JPEG重压缩更稳。当前这个场景可以用phash配合hash_size=8,把每张图压缩成64位指纹,指纹完全相同的图片视为同一组。

from PIL import Image import imagehash import os img_dir = "images" hash_to_imgs = {} for img_name in sorted(os.listdir(img_dir)): if not img_name.endswith(".jpg"): continue img_path = os.path.join(img_dir, img_name) img_hash = imagehash.phash(Image.open(img_path), hash_size=8) hash_to_imgs.setdefault(img_hash, []).append(img_name) duplicate_groups = [v for v in hash_to_imgs.values() if len(v) > 1] print(f"重复组数: {len(duplicate_groups)}") for group in duplicate_groups[:5]: print(group)

hash_size=8生成8x8的灰度指纹,最终得到一个64位哈希值。setdefault把相同哈希的图片名聚合到同一个列表里。hash_size越大,指纹越敏感,过大的hash_size会把同一场景的光照差异也判定为不同图片。对光伏板这种背景单调的图像,8已经能区分“同一块板子换了个角度”的情况。

几种查重方式的选择可以按下面这个表格判断:

方法敏感度适合场景
md5最高完全相同的文件
dhash亮度变化较小的近似重复
phash中低JPEG重压缩、缩放后的重复图

3.3 按哈希分组重新划分train和val

找到重复组之后,不要直接按单张图片随机划分,要按组划分。单张随机切会让同一场景的相似图同时出现在训练集和验证集,造成信息泄漏。

import os import shutil import random groups = list(hash_to_imgs.values()) random.Random(42).shuffle(groups) split_point = int(len(groups) * 0.85) train_groups = groups[:split_point] val_groups = groups[split_point:] def copy_group(group, split_name): out_img_dir = f"dataset/images/{split_name}" out_txt_dir = f"dataset/labels/{split_name}" os.makedirs(out_img_dir, exist_ok=True) os.makedirs(out_txt_dir, exist_ok=True) for img_name in group: shutil.copy( os.path.join(img_dir, img_name), os.path.join(out_img_dir, img_name), ) txt_name = img_name.replace(".jpg", ".txt") txt_path = os.path.join("labels", txt_name) if os.path.exists(txt_path): shutil.copy(txt_path, os.path.join(out_txt_dir, txt_name)) for group in train_groups: copy_group(group, "train") for group in val_groups: copy_group(group, "val")

random.Random(42)固定随机种子,保证多次运行划分结果一致,便于复现。copy_group内部的os.makedirs会递归创建目录,exist_ok=True避免重复报错。这里有个细节:只有txt存在时才复制,因为个别xml可能没有对应标注,这种情况在labelImg里经常出现。

3.4 统计txt框数并检查越界坐标

划分完成后还要验证一下总框数。YOLO txt格式里,类别id必须是整数,中心点和宽高必须是0到1之间的浮点数,宽高必须大于0。编写一个快速校验脚本:

import os total_boxes = 0 bad_files = [] for txt_name in sorted(os.listdir("labels")): if not txt_name.endswith(".txt"): continue txt_path = os.path.join("labels", txt_name) with open(txt_path) as f: lines = [line.strip() for line in f if line.strip()] for line in lines: parts = line.split() if len(parts) != 5: bad_files.append((txt_name, "字段数异常")) continue cls_id, x, y, w, h = parts if not cls_id.isdigit(): bad_files.append((txt_name, "类别id不是整数")) if not (0 <= float(x) <= 1 and 0 <= float(y) <= 1): bad_files.append((txt_name, "中心点越界")) if float(w) <= 0 or float(h) <= 0: bad_files.append((txt_name, "宽高异常")) total_boxes += len(lines) print(f"总框数: {total_boxes}") print(f"异常记录: {bad_files[:10]}") print(f"异常文件数: {len(bad_files)}")

总框数如果等于6822,说明VOC转YOLO过程没有丢目标。len(parts) != 5用于捕获空行或多余空格导致的问题,cls_id.isdigit()判断类别索引是否符合要求。越界检查对后续yolov8训练尤其重要,因为训练时数据加载器会把越界框裁剪到图像边缘,少量越界不报错但会干扰损失计算。

4. YOLOv8训练自己的Dirt数据集

4.1 准备data.yaml目录配置

yolov8训练过程中使用data.yaml描述数据集路径和类别名称。这里的关键是path、train、val相对于yaml文件的位置,以及names索引必须与txt里的类别id对应。

path: ./solar_panel_dirt train: images/train val: images/val names: 0: Dirt

path是数据集根目录,trainval写相对路径即可,yolov8会自动拼接成./solar_panel_dirt/images/trainnames字典的key从0开始,当前数据集只有一个Dirt类别,所以只有0一项。如果你自己增加了类别,顺序必须和生成txt时使用的class_names列表保持一致。

4.2 训练超参数与命令

积灰目标通常比较小,且光伏板表面反光会导致同一块脏污在不同图片里亮度差异很大。训练前需要把imgsz、batch、数据增强这些参数确定下来。下面是我在这个数据集上常用的参数组合:

参数建议值说明
imgsz640或960960对小目标更友好,但显存占用明显增加
batch168GB显存建议改成8
epochs120单类别任务不需要过大epoch
optimizerAdamW比SGD收敛快,适合小数据集
mosaic0.8保留mosaic增强,但不要全程开启
lr00.001预训练模型沿用这个初始学习率比较稳

对应的训练命令为:

yolo detect train \ data=solar_panel_dirt.yaml \ model=yolov8s.pt \ epochs=120 \ imgsz=640 \ batch=16 \ optimizer=AdamW \ lr0=0.001 \ mosaic=0.8

model=yolov8s.pt表示使用yolov8s的COCO预训练权重作为起点,迁移学习在小数据集上效果远好于随机初始化。mosaic=0.8在ultralytics中会自动按epoch衰减,不需要自己实现mosaic关闭逻辑。如果验证集mAP一直不稳定,先把mosaic关掉再观察。如果你的显卡显存只有6GB,把model换成yolov8n.pt并把batch改成8。

4.3 从训练曲线判断积灰模型是否在泛化

训练完成后,ultralytics会在runs/detect/train/目录下生成results.csv,里面记录了每个epoch的box loss、cls loss、mAP50、mAP50-95等指标。画出box loss和mAP50的变化趋势,能快速判断模型是否过拟合到重复图片上。

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/detect/train/results.csv") fig, ax1 = plt.subplots() ax1.plot(df["train/box_loss"], color="tab:red", label="train box loss") ax2 = ax1.twinx() ax2.plot(df["metrics/mAP50(B)"], color="tab:blue", label="val mAP50") fig.legend() plt.savefig("training_curve.png")

train/box_loss对应YOLOv8的CIoU边界框回归损失,下降说明预测框逐步贴近真实框。metrics/mAP50(B)是验证集IoU阈值0.5下的平均精度。如果box loss持续下降但mAP50在某个epoch后不再上涨,说明模型开始记忆训练集中的重复模式,应当用第3章的去重结果重新划分,或者提前早停。对于积灰这种边缘模糊的目标,mAP50和mAP50-95之间的差距通常比较大,这是正常的,不必因为mAP50-95偏低就怀疑标注质量。

5. 验证集中残留重复图的检查与TTA推理

5.1 训练前检查train和val之间是否存在图片泄漏

数据划分之后、训练开始之前,最好再做一次跨集合重复检查。即便第3章按hash分组划分,phash指纹完全相同的情况也可能漏掉少量“同一块板子不同曝光”的图片。检查逻辑很简单:分别收集train和val目录下所有图片的phash,求交集。

from PIL import Image import imagehash import os def collect_hashes(root): hash_to_name = {} for img_name in os.listdir(root): if not img_name.endswith(".jpg"): continue img_path = os.path.join(root, img_name) h = imagehash.phash(Image.open(img_path), hash_size=8) hash_to_name[h] = img_name return hash_to_name train_hashes = collect_hashes("dataset/images/train") val_hashes = collect_hashes("dataset/images/val") leaks = set(train_hashes.keys()) & set(val_hashes.keys()) print(f"train/val潜在重复数: {len(leaks)}") for h in list(leaks)[:5]: print(train_hashes[h], "->", val_hashes[h])

这段脚本把train和val中phash完全一致的图片视为潜在泄漏。如果potential_leak_count大于0,说明之前的划分还不够严格,需要回到第3章改用更大的hash_size,或者把这些重复组整体挪到训练集一侧。注意这里用的是集合交集,每个hash只保留了一张图片名,所以输出只能用来定位问题,不能完整列出所有重复对。

5.2 用augment推理评估模型鲁棒性

最后一个小技巧是用yolo推理的augment参数做Test-Time Augmentation。训练完的权重直接推理会看到较低或较高的mAP,但TTA能把水平翻转、缩放、颜色抖动后的预测融合起来,对积灰这种边缘模糊目标更稳定。命令如下:

yolo detect predict \ model=runs/detect/train/weights/best.pt \ source=dataset/images/test \ imgsz=640 \ augment=True

augment=True会启用水平翻转、多尺度等推理增强,ultralytics会把多次预测结果融合后再输出最终框。这个参数默认是False,部署时不要打开,因为推理耗时大约会增加到原来的2到3倍。在验证集上比较augment=True和普通模式的mAP,如果TTA明显更高,说明模型对光照变化敏感,后续可以增加亮度扰动数据增强而不是继续堆epoch。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 13:41:02

AD8602ARMZ-REEL,8MHz双通道轨到轨输入输出单电源精密CMOS运算放大器

AD8602ARMZ-REEL是ADI亚德诺DigiTrim工艺双通道CMOS运算放大器&#xff0c;专为电池供电便携设备、高阻抗传感器信号调理、单电源ADC缓冲、音频前置放大场景设计。支持2.7V~5.5V单电源供电&#xff0c;具备8MHz增益带宽积、80μV低输入失调、极低输入偏置电流、轨到轨输入输出&…

作者头像 李华
网站建设 2026/9/10 13:38:49

UI设计中的空间与层次构建技巧解析

1. 项目概述&#xff1a;UI设计中的空间与层次概念解析 在数字产品设计领域&#xff0c;视觉层次的构建能力直接决定了用户体验的质量。最近为一个金融类APP做设计改版时&#xff0c;我通过重构空间层次使关键业务转化率提升了37%。这个案例让我深刻认识到&#xff1a;优秀的UI…

作者头像 李华
网站建设 2026/9/10 13:38:38

STM32+ESP8266对接阿里云飞燕平台实战指南

简介&#xff1a;本资源是一套面向嵌入式物联网开发者的完整实战项目代码&#xff0c;聚焦STM32F103与ESP8266协同接入阿里云飞燕平台的端到端实现&#xff0c;适用于具备C语言基础与Keil开发经验的中级工程师及高校电子/物联网专业学生。项目涵盖设备联网、数据主动上报、云端…

作者头像 李华
网站建设 2026/9/10 13:38:25

CANN/ge SetOutputAttr API

SetOutputAttr 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlow …

作者头像 李华