简介这是一份面向光伏板检测与计算机视觉方向研究者的标注数据集适合从事目标检测模型训练、算法对比与新能源监测应用的开发者使用。数据采集完成后借助labelimg逐张绘制边界框每张图片均配有对应的xml标注文件记录光伏板坐标、尺寸与类别信息可直接用于yolov8等目标检测模型的训练与验证。压缩包共377个文件以120张jpg与137张png图片为主另含120个xml标注文件整体约66.43MB图片与标签一一对应目录结构便于按需检索与批量读取。目前已有345人学习下载可作为光伏板检测任务的现成数据基础省去从零采集与标注的时间成本也便于横向评估不同检测算法在该场景下的性能表现为后续模型搭建、训练调优与部署提供可靠支撑。1. 光伏板数据集到手之后labelimg 标注的 xml 到底能不能直接喂给 YOLOv8拿到一份「光伏板数据集使用 labelimg 手动标注包含图片和 xml 文件可进行 yolov8 训练」的资源很多人的第一反应是解压、装环境、敲yolo train然后被一堆路径报错和空标签劝退。问题不在 YOLOv8而在于 labelimg 产出的是 Pascal VOC 风格的 xml而 YOLOv8 认的是每张图对应一个 txt 的归一化坐标。中间这层转换没做对训练要么直接崩要么 mAP 低得离谱。这份数据集的价值在于「人工标注」四个字光伏板在航拍或电站巡检图里存在遮挡、反光、边框贴边、密集排列等情况自动标注工具很容易漏框人工框出来的边界更贴近真实缺陷区域。它适合做电站巡检、无人机光伏板识别、红外图缺陷定位这类落地任务也适合拿来做 YOLOv8 训练自己的数据集的第一份练手数据。下面按「先看懂 xml → 再转成 YOLO 格式 → 配好环境跑通 → 排掉常见的坑」这条线走一遍能直接抄作业。2. 先读懂 labelimg 的 xml字段含义与转换前的自检2.1 xml 里到底存了什么labelimg 保存的 xml 结构固定核心就三块文件信息、图像尺寸、目标框。用文本编辑器或xml解析库打开一个文件长这样annotation folderimages/folder filenamepv_0001.jpg/filename path/data/pv/images/pv_0001.jpg/path size width1920/width height1080/height depth3/depth /size object namepv_panel/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin312/xmin ymin204/ymin xmax880/xmax ymax760/ymax /bndbox /object /annotationfilename必须和图片实际文件名一致size里的宽高是原图尺寸bndbox是左上角和右下角的绝对像素坐标。YOLO 需要的格式是class_id x_center y_center width height全部归一化到 0~1。转换的本质就是拿bndbox和size做一次除法和中心点计算。提示转换前先确认size里的宽高和图片真实分辨率一致。有些标注流程里图片被缩放后再标xml 记的是缩放后尺寸直接转会导致框整体偏移。2.2 转换前必须做的三项自检第一检查有没有空 xml。labelimg 在没框任何目标时也会生成 xmlobject节点为空。这种文件转出来是空 txtYOLOv8 会把它当成「这张图没有目标」的负样本少量可以大量会拉低召回。第二检查类别名是否统一。人工标注最容易出现pv、PV、panel、pv_panel混用转换脚本要先把所有name收集起来做一次映射否则类别数会虚高。第三检查坐标越界。xmax大于width或xmin为负的情况在贴边目标上很常见转换时要 clamp 到[0, width]不然归一化后会出现大于 1 的值训练时被静默截断。import os import xml.etree.ElementTree as ET def scan_dataset(xml_dir): classes set() empty_files [] bad_boxes [] for f in os.listdir(xml_dir): if not f.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, f)) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) objs root.findall(object) if not objs: empty_files.append(f) continue for obj in objs: classes.add(obj.find(name).text) box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) if xmin 0 or ymin 0 or xmax w or ymax h: bad_boxes.append((f, xmin, ymin, xmax, ymax)) print(类别:, sorted(classes)) print(空标注文件数:, len(empty_files)) print(越界框数量:, len(bad_boxes)) return sorted(classes) scan_dataset(./annotations)这段脚本不写任何文件只做体检。classes用来生成data.yaml的namesempty_files决定要不要剔除bad_boxes决定 clamp 策略。跑完再决定下一步比转完发现类别对不上要省事得多。3. 把 xml 转成 YOLOv8 能吃的 txt脚本、目录结构与 data.yaml3.1 转换脚本与参数说明import os import xml.etree.ElementTree as ET # 类别顺序必须和 data.yaml 的 names 完全一致 CLASSES [pv_panel] XML_DIR ./annotations IMG_DIR ./images OUT_LABEL_DIR ./labels os.makedirs(OUT_LABEL_DIR, exist_okTrue) def convert(xml_path, out_path): 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): name obj.find(name).text if name not in CLASSES: continue cls_id CLASSES.index(name) box obj.find(bndbox) xmin max(0, int(box.find(xmin).text)) ymin max(0, int(box.find(ymin).text)) xmax min(w, int(box.find(xmax).text)) ymax min(h, int(box.find(ymax).text)) # 过滤掉宽高为 0 的退化框 if xmax xmin or ymax ymin: continue xc (xmin xmax) / 2.0 / w yc (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cls_id} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) with open(out_path, w) as f: f.write(\n.join(lines)) for f in os.listdir(XML_DIR): if f.endswith(.xml): stem os.path.splitext(f)[0] convert(os.path.join(XML_DIR, f), os.path.join(OUT_LABEL_DIR, stem .txt))CLASSES是唯一需要你手动对齐的地方顺序错了类别就全乱。max(0, ...)和min(w, ...)是坐标 clamp处理贴边框。xmax xmin的过滤是防止标注时手抖拉出零面积框这种框在 YOLOv8 里会触发除零或无效 anchor。保留 6 位小数是 YOLO 官方推荐的精度够用且不冗长。3.2 目录结构要按 YOLOv8 的约定摆YOLOv8 不强制目录名但data.yaml里的路径必须和实际一致。推荐这样组织pv_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml图片和标签按同名 stem 对应images/train/pv_0001.jpg对应labels/train/pv_0001.txt。划分比例常见做法是 8:2 或 9:1光伏板数据如果来自同一电站的连续帧要按「电站/日期」划分而不是随机划分否则相邻帧同时进训练和验证mAP 会虚高。3.3 data.yaml 的写法与路径坑path: /data/pv_dataset train: images/train val: images/val nc: 1 names: 0: pv_panelpath是数据集根目录train和val是相对path的路径。nc必须等于names的条目数names的 key 从 0 开始且和转换脚本里的CLASSES索引一一对应。最常见的翻车是path写成相对路径训练时工作目录一变就找不到图直接写绝对路径最稳。注意如果 xml 里有多类目标nc和names要同步扩展转换脚本的CLASSES列表顺序就是类别 id 顺序三者必须锁死。4. 用 YOLOv8 跑通第一轮训练环境、命令与关键参数4.1 环境配置的最小步骤conda create -n pv python3.10 -y conda activate pv pip install ultralytics yolo checksultralytics会自动带上 torch 依赖yolo checks用来确认 CUDA 是否可用。如果只有 CPU训练能跑但极慢光伏板数据集动辄几千张建议至少有一张 6G 显存以上的卡。GTX1660Ti 这类 6G 卡跑yolov8n在imgsz640下 batch 设 8 是能跑起来的再大就 OOM。4.2 训练命令与参数怎么改yolo train \ modelyolov8n.pt \ data/data/pv_dataset/data.yaml \ epochs100 \ imgsz640 \ batch8 \ lr00.01 \ patience20 \ projectruns/pv \ nameexp1modelyolov8n.pt是 nano 版参数量小、速度快适合先验证数据管线通不通。imgsz640是默认值光伏板目标如果普遍偏小可以提到 960 或 1280但显存占用会明显上升。lr00.01是 SGD 的初始学习率YOLOv8 默认优化器会自动选一般不用动。patience20表示 20 轮没有提升就早停防止过拟合。project和name决定权重和日志落盘位置。4.3 训练过程看什么训练启动后重点看三样box_loss是否稳定下降、mAP50是否在涨、有没有大量corrupt image警告。box_loss震荡不降通常是学习率过大或标注框质量差mAP50卡在低位要回去查类别映射和空标签比例corrupt image说明有图片打不开或格式异常需要单独筛出来。yolo train ... 21 | tee train.log grep -i corrupt\|warning train.log把日志落盘再 grep比盯着滚动输出靠谱。训练结束后runs/pv/exp1/weights/best.pt就是可用权重results.csv里存了每轮的 loss 和 mAP可以直接画损失函数曲线。5. 光伏板数据集训练避坑5 个真实踩坑记录5.1 现象训练启动就报No labels found原因data.yaml里的train路径指向了图片目录但 YOLOv8 会去同级或约定位置找labels目录结构没按images/labels对称摆放时找不到 txt。解决严格按images/train和labels/train对称组织或者用yolo train前先跑一遍scan_dataset确认每张图都有同名 txt。缺标签的图要么补要么从训练集移除。5.2 现象mAP 一直上不去loss 正常下降原因类别名不统一导致nc虚高或者空 xml 转出的空 txt 占比过大模型学到大量「无目标」样本。解决回到 2.2 的自检脚本统计空标注文件比例。超过 10% 就考虑剔除或补充标注。类别名统一映射后再转一次。5.3 现象验证集 mAP 很高实际推理框全偏原因训练集和验证集来自同一段连续视频帧相邻帧高度相似验证集等于变相泄漏。解决按电站、日期或拍摄批次划分数据集不要随机划分。光伏板巡检数据尤其要注意这点同一块板的多帧不能跨集。5.4 现象贴边光伏板检测不到原因标注时贴边框的xmax等于或略大于width转换时没 clamp归一化后大于 1训练时被截断成无效框。解决转换脚本里加min(w, ...)和max(0, ...)并在自检阶段统计越界框数量数量多说明标注规范需要统一。5.5 现象GTX1660Ti 上训练中途 OOM原因batch或imgsz设太大6G 显存扛不住yolov8s以上的模型加高分辨率。解决先用yolov8nimgsz640batch8跑通再逐步升模型或分辨率。也可以开ampTrue默认开混合精度或者用cacheFalse避免把图片全缓存进内存。6. 从能跑到好用验证权重、看损失曲线与部署前的最后一步训练出best.pt只是开始真正决定这个数据集值不值得继续投入的是验证环节能不能复现训练时的指标。我一般会先用验证命令跑一遍独立测试集yolo val modelruns/pv/exp1/weights/best.pt \ data/data/pv_dataset/data.yaml \ imgsz640 \ batch8重点看mAP50和mAP50-95的差距。如果mAP50高但mAP50-95低说明框的位置不够准光伏板边界回归还有优化空间可以考虑提高imgsz或换更大的模型。如果两者都低先回去查数据不要急着调模型。损失曲线直接用results.csv画import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/pv/exp1/results.csv) df.columns df.columns.str.strip() plt.plot(df[epoch], df[train/box_loss], labeltrain box) plt.plot(df[epoch], df[val/box_loss], labelval box) plt.plot(df[epoch], df[metrics/mAP50(B)], labelmAP50) plt.legend() plt.savefig(curve.png)results.csv的列名带空格读进来先strip()否则取列会报 KeyError。三条线放一起看train 和 val 的 box loss 如果分叉就是过拟合早停或加数据增强mAP50 曲线如果后期还在涨说明epochs还能加。最后一步是导出部署格式。如果后续要上 RK3588 这类边缘板不要直接拿.pt去转先用yolo export导出 ONNX再走 RKNN 工具链yolo export modelruns/pv/exp1/weights/best.pt formatonnx imgsz640导出后拿一张真实光伏板图跑一次 ONNX 推理确认输出和 PyTorch 一致再进量化流程。这一步偷懒后面在板子上调精度会花掉几倍时间。我自己踩过最深的坑是拿到数据集先急着训练结果类别名没统一、空标签没清、贴边框没 clamp三轮训练全白跑。后来养成习惯任何数据集到手先跑自检脚本把类别、空标注、越界框三个数看清楚再动手。这份光伏板数据集本身质量取决于标注规范转换和训练只是把它兑现成可用权重的过程前面多花二十分钟体检后面少熬两个通宵。希望帮到你。本文还有配套的精品资源点击获取