简介:本数据集面向光伏电站智能巡检与航拍缺陷检测方向,提供367张光伏板航拍图像,聚焦鸟粪这一单一缺陷类别,适合目标检测入门与算法验证使用。包内共1103个文件,含367张jpg原图、367个VOC格式xml标注与367个YOLO格式txt标注,另有少量说明文件,压缩包约23.04MB,可直接用于Pascal VOC与YOLO双格式训练流程。标注由labelImg完成,采用矩形框方式,类别名为niaofen,累计标注框数达1421个,平均每张图约3.9个目标,样本密度较高。已有375人学习下载,读者可据此快速搭建检测基线、验证数据增强与模型调参效果,也可用于对比不同检测框架在真实航拍场景下的表现。需注意,数据集仅提供准确合理的标注,不对训练所得模型或权重文件的精度作任何保证。
1. 367 张航拍图、1421 个鸟粪框:这份光伏缺陷数据集到底能干什么
光伏电站的运维巡检里,鸟粪是个绕不开的麻烦。它不像热斑那样需要红外相机才能看见,也不像组件碎裂那样一眼就能判断,但它造成的局部遮挡会直接拉低单串发电量,严重时还会诱发热斑效应,把一块好板子烧出永久性损伤。问题是,航拍图里鸟粪的形态太随机了——有的像一坨白色涂料,有的和云影、灰尘、反光混在一起,靠人眼在几百张图里逐个框选,一天下来眼睛基本报废。
这份数据集就是冲着这个场景来的。367 张 jpg 航拍图,每张都配了 Pascal VOC 格式的 xml 标注和 YOLO 格式的 txt 标注,标注类别只有一个:niaofen,总框数 1421 个。标注工具用的是 labelImg,标注规则就是画矩形框,没有分割路径的 txt 文件,纯粹的目标检测数据。它适合谁?做光伏巡检算法落地的工程师、想拿真实航拍数据跑 YOLO 训练的学生、以及需要快速验证缺陷检测 pipeline 的从业者。你不需要自己从零标数据,拿到就能直接进训练流程。
2. VOC 与 YOLO 双格式拆解:367 张图怎么对应 367 个 xml 和 367 个 txt
2.1 两种格式的文件结构差异
这份数据集最省心的地方是同时给了 VOC 和 YOLO 两套标注。VOC 格式的 xml 文件里,每个目标用<bndbox>记录xmin、ymin、xmax、ymax四个绝对坐标值,单位是像素。YOLO 格式的 txt 文件则把同样的框转成了归一化的x_center、y_center、width、height,数值范围在 0 到 1 之间,每行一个目标,行首是类别索引。
367 张 jpg 对应 367 个 xml 和 367 个 txt,说明每张图都有完整的双份标注,不存在漏标或只给一种格式的情况。类别只有一个niaofen,在 YOLO 的 txt 里对应的类别索引就是 0。1421 个总框数平均下来每张图大约 3.87 个鸟粪目标,这个密度在航拍缺陷数据里算中等偏上,意味着单张图里可能有多个大小不一的鸟粪区域,训练时模型需要同时处理多尺度目标。
常见做法是直接用 YOLO 格式的 txt 进训练,VOC 的 xml 留着做数据校验或者转其他框架用。我一般会先跑一遍脚本确认 xml 和 txt 的框能对上,避免标注工具导出时出现坐标偏移。
2.2 用 Python 校验 xml 与 txt 的一致性
拿到数据集第一件事不是急着训练,而是验证两套标注是否严格对应。下面这段脚本遍历所有 xml 和 txt,把 VOC 的绝对坐标转成 YOLO 的归一化坐标,再和 txt 里的值做对比,误差超过 0.001 就报出来。
import os import xml.etree.ElementTree as ET # 数据集根目录,里面放 jpg、xml、txt 三种文件 data_dir = "./firc_gfb_dataset" img_dir = os.path.join(data_dir, "images") xml_dir = os.path.join(data_dir, "annotations_xml") txt_dir = os.path.join(data_dir, "labels_yolo") # 从图片文件读取宽高,用于归一化 from PIL import Image mismatch = [] for xml_file in os.listdir(xml_dir): if not xml_file.endswith(".xml"): continue base = xml_file.replace(".xml", "") txt_file = base + ".txt" txt_path = os.path.join(txt_dir, txt_file) if not os.path.exists(txt_path): mismatch.append((base, "txt missing")) continue # 解析 VOC xml tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) voc_boxes = [] for obj in root.findall("object"): cls_name = obj.find("name").text bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) # 转成 YOLO 归一化格式 xc = (xmin + xmax) / 2.0 / img_w yc = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h voc_boxes.append((cls_name, xc, yc, w, h)) # 读 YOLO txt with open(txt_path, "r") as f: lines = [l.strip() for l in f.readlines() if l.strip()] if len(lines) != len(voc_boxes): mismatch.append((base, f"count diff: voc={len(voc_boxes)} txt={len(lines)}")) continue for i, line in enumerate(lines): parts = line.split() cls_id = int(parts[0]) xc_t, yc_t, w_t, h_t = map(float, parts[1:5]) _, xc_v, yc_v, w_v, h_v = voc_boxes[i] if abs(xc_t - xc_v) > 0.001 or abs(yc_t - yc_v) > 0.001: mismatch.append((base, f"box {i} center mismatch")) break print(f"总检查 {len(os.listdir(xml_dir))} 个 xml,发现 {len(mismatch)} 处不一致") for m in mismatch[:10]: print(m)这段脚本的逻辑很直接:先读 xml 里的图片宽高和绝对坐标,手动转成 YOLO 的归一化值,再逐行对比 txt 里的数值。参数上唯一需要注意的是data_dir的路径结构,如果你的文件是平铺在一个目录里,把img_dir、xml_dir、txt_dir都改成同一个路径就行。误差阈值 0.001 是我常用的经验值,labelImg 导出时一般不会超过这个精度,如果报出大量不一致,大概率是 xml 和 txt 不是同一轮标注产生的。
2.3 类别索引与文件名对应关系
YOLO 训练时依赖一个classes.txt或者数据集配置文件里的names列表。这份数据集只有一个类别niaofen,所以 YOLO 的类别索引就是 0。但要注意,有些标注工具在导出时会按字母顺序重排类别,如果后续你往这个数据集里混入其他类别,索引顺序可能会变。我一般会在数据集根目录放一个classes.txt,内容就一行niaofen,训练前用脚本检查所有 txt 第一列是否都是 0。
文件名方面,从项目正文里能看到firc_gfb_175.jpg、firc_gfb_174.jpg这种命名,前缀统一是firc_gfb_,后面跟数字编号。这种命名方式在批量处理时很方便,用glob或者os.listdir排序后能稳定复现。但要注意编号不一定连续,367 张图里可能有跳号,写数据划分脚本时不要假设编号范围。
3. 从 7z 到 YOLO 训练:环境配置、数据划分与首轮跑通
3.1 解压后的目录整理与路径规范
7z 包解压后,常见的情况是 jpg、xml、txt 混在一个文件夹里,或者分在三个子目录。不管原始结构怎样,进训练前我建议统一成 YOLO 官方推荐的结构:
dataset/ images/ train/ val/ labels/ train/ val/ classes.txt dataset.yaml把 jpg 全部放进images,txt 全部放进labels,xml 单独找个地方备份。注意 YOLO 的 txt 文件名必须和 jpg 文件名一一对应,只是扩展名不同。如果原始数据里 xml 和 txt 的文件名有大小写差异,在 Linux 下会直接导致找不到标签,这个坑后面会细说。
3.2 按 8:2 划分训练集与验证集
367 张图不算多,划分时既要保证验证集有足够的样本覆盖不同场景,又不能把训练集压得太小。我一般用 8:2,训练集 293 张,验证集 74 张。下面这个脚本按文件名排序后均匀采样,避免随机种子导致每次划分不一致。
import os import shutil import random random.seed(42) # 固定种子,保证可复现 src_img_dir = "./dataset/images_all" src_lbl_dir = "./dataset/labels_all" dst_root = "./dataset" # 收集所有 jpg 文件名(不含扩展名) all_files = sorted([f.replace(".jpg", "") for f in os.listdir(src_img_dir) if f.endswith(".jpg")]) random.shuffle(all_files) split_idx = int(len(all_files) * 0.8) train_files = all_files[:split_idx] val_files = all_files[split_idx:] for subset, files in [("train", train_files), ("val", val_files)]: img_out = os.path.join(dst_root, "images", subset) lbl_out = os.path.join(dst_root, "labels", subset) os.makedirs(img_out, exist_ok=True) os.makedirs(lbl_out, exist_ok=True) for name in files: shutil.copy(os.path.join(src_img_dir, name + ".jpg"), os.path.join(img_out, name + ".jpg")) shutil.copy(os.path.join(src_lbl_dir, name + ".txt"), os.path.join(lbl_out, name + ".txt")) print(f"训练集 {len(train_files)} 张,验证集 {len(val_files)} 张")random.seed(42)是为了让每次运行得到相同的划分结果,方便复现实验。如果你要做交叉验证,可以把shuffle换成KFold,但 367 张的规模下,单次 8:2 已经够用。复制而不是移动,是为了保留原始文件,万一划分有问题还能重来。
3.3 dataset.yaml 的写法与类别名映射
YOLOv5/v8 系列都需要一个 yaml 文件来描述数据集路径和类别。这份数据集只有一个类别,yaml 写起来很简单:
path: ./dataset train: images/train val: images/val names: 0: niaofenpath是数据集根目录,train和val是相对路径。names里的键 0 对应 txt 第一列的类别索引。如果你用的是 YOLOv8,这个文件直接传给model.train(data="dataset.yaml")就行。注意names的缩进必须是两个空格,用 tab 会报解析错误,这是 YAML 的老毛病。
3.4 首轮训练命令与关键参数
环境配好后,用 YOLOv8n 这种小模型先跑一轮,确认数据管道没问题。命令如下:
yolo detect train \ data=dataset.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/gfb \ name=baselineimgsz=640是 YOLO 的默认输入尺寸,367 张图里如果鸟粪目标偏小,可以提到 1280,但显存占用会翻倍。batch=16在 8G 显存下比较稳,如果报 OOM 就降到 8。patience=20表示 20 轮验证指标不提升就早停,小数据集上能省不少时间。lr0=0.01是初始学习率,YOLOv8 默认用 SGD 时这个值比较合适,如果你换成 AdamW,建议降到 0.001。
跑完第一轮后重点看runs/gfb/baseline/results.csv里的metrics/mAP50和train/box_loss。如果 box_loss 一直不降,大概率是标签路径没对上;如果 mAP50 在 0.5 以下,可能是学习率太大或者数据量太少导致过拟合。
4. 标注质量与格式转换的避坑清单:从坐标越界到类别索引错位
4.1 现象:训练时报 “Label class xxx is not in the dataset” → 原因:类别索引与 names 不匹配 → 解决:统一用脚本检查
这个报错通常出现在你往数据集里加了新类别,或者用了别人提供的预训练权重但 names 列表不一致。这份数据集只有niaofen一个类,txt 第一列应该全是 0。但如果你从 VOC 转 YOLO 时用了某些在线转换工具,它们可能按字母顺序把类别重排,导致索引对不上。解决办法是跑一段脚本统计所有 txt 第一列的唯一值,确认只有 0。
4.2 现象:验证集 mAP 正常但推理时框全偏 → 原因:xml 和 txt 的坐标基准不一致 → 解决:用 2.2 的校验脚本逐张比对
有些标注工具在导出 VOC 时用的是 1-based 坐标,而 YOLO 要求 0-based 归一化。如果转换时没减 1,框会整体偏移一个像素,单看 mAP 可能不明显,但推理时叠加到原图上就能看出偏移。2.2 的脚本能直接抓出这种问题,误差超过 0.001 就会报出来。
4.3 现象:Linux 下训练报 “No labels found” → 原因:文件名大小写不一致 → 解决:批量重命名统一小写
Windows 下文件名不区分大小写,FIRC_GFB_175.jpg和firc_gfb_175.txt能配上,但到了 Linux 就找不到。这份数据集的文件名从正文看是小写前缀,但如果你自己混入了其他来源的图,建议用rename 'y/A-Z/a-z/' *统一转小写,再检查 jpg 和 txt 的 basename 是否完全一致。
4.4 现象:某些图训练时 loss 突然变 NaN → 原因:xml 里存在宽高为 0 的退化框 → 解决:过滤掉 xmax<=xmin 或 ymax<=ymin 的标注
labelImg 在极端情况下会导出宽或高为 0 的框,这种框转成 YOLO 后 width 或 height 为 0,计算 loss 时会出现除零。用下面这段脚本扫一遍 xml,把退化框所在的图找出来,要么重新标,要么直接从训练集里剔除。
import xml.etree.ElementTree as ET import os xml_dir = "./dataset/annotations_xml" bad = [] for f in os.listdir(xml_dir): if not f.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, f)) for obj in tree.getroot().findall("object"): b = obj.find("bndbox") xmin = float(b.find("xmin").text) xmax = float(b.find("xmax").text) ymin = float(b.find("ymin").text) ymax = float(b.find("ymax").text) if xmax <= xmin or ymax <= ymin: bad.append(f) break print(f"发现 {len(bad)} 张图存在退化框:{bad[:5]}")4.5 现象:验证集指标波动大,每次跑结果差很多 → 原因:367 张图太少,单次划分随机性大 → 解决:固定随机种子并做 3 折交叉验证
小数据集上这是常态。367 张图里如果某些场景的鸟粪只出现在少数几张里,随机划分时可能全被分到训练集或验证集,导致指标忽高忽低。除了固定random.seed,更稳妥的做法是做 3 折交叉验证,把每折的 mAP 取平均。YOLO 本身不直接支持交叉验证,但你可以手动生成 3 份不同的dataset.yaml,分别训练后对比。
5. 小样本下的增强策略与推理验证:让 1421 个框发挥最大价值
367 张图、1421 个框,这个量级直接训 YOLOv8m 以上的模型很容易过拟合。我的习惯是先用 YOLOv8n 或 YOLOv8s 跑基线,再根据验证集表现决定要不要上增强。YOLOv8 默认开启了 mosaic、mixup 和 HSV 抖动,但小数据集上 mosaic 的概率可以适当调高,让模型见到更多拼接组合。在dataset.yaml同级目录建一个hyp.yaml,写入以下内容,训练时用hyp=hyp.yaml加载:
mosaic: 1.0 mixup: 0.15 copy_paste: 0.1 hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.4 degrees: 10.0 translate: 0.1 scale: 0.5mosaic: 1.0表示每张图都参与马赛克增强,小数据集上能显著增加场景多样性。mixup: 0.15和copy_paste: 0.1是额外的混合策略,但 copy_paste 需要分割掩码,这份数据集只有检测框,实际不会生效,留着不影响。degrees: 10.0是随机旋转角度,航拍图本身有方向性,旋转太大会让鸟粪的形态失真,10 度以内比较安全。scale: 0.5允许缩放,模拟不同飞行高度下的目标大小变化。
训练完成后,推理验证不能只看 mAP 数字,要实际把预测框画到原图上肉眼检查。下面这段脚本用训练好的权重跑单张图,输出带框的可视化结果:
from ultralytics import YOLO import cv2 model = YOLO("runs/gfb/baseline/weights/best.pt") img_path = "./dataset/images/val/firc_gfb_175.jpg" results = model(img_path, conf=0.25, iou=0.45) for r in results: img = r.plot() # 自动画框和类别 cv2.imwrite("pred_175.jpg", img) print(f"检测到 {len(r.boxes)} 个目标") for box in r.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) xyxy = box.xyxy[0].tolist() print(f"类别 {cls_id} 置信度 {conf:.3f} 坐标 {xyxy}")conf=0.25是置信度阈值,低于这个值的框不输出。iou=0.45是 NMS 的 IoU 阈值,鸟粪目标之间如果有重叠,这个值可以适当调高到 0.5 避免漏检。跑完后打开pred_175.jpg,重点看三种情况:漏检的鸟粪是不是因为太小或太暗、误检的框是不是把云影或反光当成了目标、框的位置有没有明显偏移。如果漏检集中在某几类形态上,可以考虑在训练时针对性地增加那几类的过采样。
还有一个容易被忽略的点:这份数据集的标注规则是画矩形框,但鸟粪的实际形状往往是不规则的。矩形框会包含一些背景像素,训练时模型学到的是“框内大概率有鸟粪”,而不是精确的像素级分割。如果你的下游任务需要精确面积计算,这份数据集只能做检测定位,不能直接用于分割。我一般会在推理后加一步颜色阈值过滤,把框内明显不是鸟粪的像素剔除,再估算面积。
从那以后我每次拿到新的缺陷检测数据集,都强制先跑一遍 xml 和 txt 的一致性校验,再固定随机种子做一次 8:2 划分,最后用 YOLOv8n 跑 10 个 epoch 看 loss 曲线。这三步走完,数据能不能用、有没有暗坑,基本就清楚了。希望帮到你。
本文还有配套的精品资源,点击获取