无人机光伏巡检缺陷检测:YOLO小目标检测从标注到部署全流程
发布时间:2026/9/28 12:19:17来源:尧图网络
简介这是一份基于无人机场景的光伏面板缺陷检测实战项目面向光伏电站运维人员、计算机视觉开发者及算法学习者。项目围绕空中巡检图像完整覆盖图像预处理、分割、特征提取与缺陷分类流程并给出可运行的Python源码调用了OpenCV、TensorFlow、Keras等库便于二次开发与算法替换。资源包共405个文件以py源码为主289个包含训练权重pth、模型配置、依赖库dll/pyd、运行脚本及说明文档等整体约106.72MB结构清晰。已有285人学习下载。相比传统人工检测该方案可提升巡检效率与缺陷识别准确性还附带操作指南和扩展思路适合用于算法研究、课设实战或电站智能运维改造。1. 无人机拍回来的光伏面板缺陷检测从热斑漏报到可复现的项目闭环光伏面板缺陷检测在无人机巡检里是个标准的小目标检测场景飞机飞一圈几千张航拍图落盘人工盯屏复盘一小时起步还容易漏掉只有三四十像素的热斑。把算法加在这条链路里要回答的核心问题无非三个缺陷在航拍底下长什么样怎么把大图切成模型吃得了的样本实拍时怎么还原坐标、避免一通漏检和误报。这篇笔记按这个闭环讲适合做光伏运维和无人机巡检方案集成的朋友参考新手照着能跑通最小训练流程熟手重点看部署坑和验收方式。2. 缺陷成像与标注热斑、隐裂、脏污在航拍图里怎么区分2.1 无人机视角下的缺陷类型可见光与热红外各看什么无人机挂载的传感器通常分两类可见光 RGB 相机和热红外相机。常见做法是双挂载同一条航线同时取两路数据因为缺陷在两种波段下的表现完全不重叠。热斑是电池片被遮挡或内部损坏后局部温度升高在热红外图里表现为一片高亮的圆形或椭圆形区域可见光下往往看不出明显异常。隐裂在可见光下是沿着电池片晶格走向的细线侧面光或者逆光时更清楚红外下反而不明显。脏污、鸟粪、积灰则是可见光下的色块和遮挡阴影明显但积灰本身不会显著升温热像上很难分辨。所以在做数据采集和标注前先定一个原则最终训练集以可见光为主热红外专门用来找热斑和接线盒故障。不要把两类图混在一个模型里训练除非你用的是多模态输入结构否则可见光和热红外的成像差异会让模型学到“亮的就是缺陷”这种错误特征。标注类别建议控制在 3 到 4 类常见做法是 hot_spot、crack、dust再加一个 diode_failure 给直流接线盒的发热故障。类别太多会让小目标样本更加稀疏模型很容易过拟合到背景。2.2 标注规范框缺陷不是框面板小目标别硬标很多人第一次做光伏缺陷标注习惯把整个面板框出来再标一个 defect 标签这是最快踩坑的方式。光伏面板缺陷检测的价值是定位到缺陷本身框住整块板只会让模型学会“看到板子就输出一个框”对缺陷的召回率几乎为零。正确做法是把热斑、裂纹、污渍各自框出来哪怕缺陷只有十几个像素也要照实框标签名对应缺陷类型而不是面板状态。小目标判定可以参考 COCO 的约定小于 32×32 像素就算小目标。无人机在 40 到 80 米高度巡检热斑在 4K 原图里经常只有 20 到 50 像素属于典型的难样本。标注这类目标时我一般会要求复审一遍第一次粗标第二次专门放大到 200% 检查边缘是否贴合。裂纹这种细长目标更麻烦框稍微偏一点 IoU 就掉得厉害训练时反而会教模型学到错误的位置。宁可少标几个模糊目标也不要标出一堆边缘粗糙的框数据质量直接决定后面的模型效果。2.3 先跑一段标注统计脚本再决定切片尺寸标注完成后不要急着训练先统计一下每个类别的框宽高分布确定目标尺寸范围再决定切片大小和训练分辨率。这里给一段读 VOC XML 的统计脚本可以输出每个类别的框数量、平均宽高和小于 32 像素的目标占比。import xml.etree.ElementTree as ET import glob from collections import defaultdict voc_files glob.glob(annotations/*.xml) stats defaultdict(lambda: {count: 0, small: 0, w: 0, h: 0}) for f in voc_files: tree ET.parse(f) for obj in tree.findall(object): name obj.find(name).text box obj.find(bndbox) w int(float(box.find(xmax).text)) - int(float(box.find(xmin).text)) h int(float(box.find(ymax).text)) - int(float(box.find(ymin).text)) stats[name][count] 1 stats[name][w] w stats[name][h] h if w 32 and h 32: stats[name][small] 1 for name, s in stats.items(): print(f{name}: 框数{s[count]}, 平均宽{s[w]/s[count]:.1f}, f平均高{s[h]/s[count]:.1f}, 小目标占比{s[small]/s[count]*100:.1f}%)这段脚本输出的“小目标占比”是后续所有参数选择的地基。如果小于 32 像素的缺陷超过 40%切片尺寸一定要往小里做训练分辨率至少 1024后面推理时还要用重叠滑窗否则这些小目标在特征图里连一个 anchor 都分不到。如果各类别框数差异超过 5 倍比如 dust 有两千个框而 crack 只有三百个就要考虑类别权重或者对 crack 做针对性增强而不是直接开训。3. 把航拍大图做成数据集切片、标签转换与数据增强3.1 滑窗切片尺寸、重叠率与空白块过滤是三个关键旋钮无人机原图常见尺寸在 4000×3000 到 8000×6000 之间直接整图训练基本不可能显存不够小目标也被压没了。常见做法是滑窗切成 1024×1024 或 640×640 的块。切片尺寸的选择逻辑是切片必须比最大缺陷大两倍以上同时保证切出来的小目标不至于在缩小后完全消失。我的经验是先用 2.3 的统计结果看目标中位尺寸如果中位宽在 40 像素左右1024 是起步值显存紧张就先用 640 跑通再回提分辨率。重叠率同样重要。训练切片设 10% 到 20% 的重叠就够了重叠太多会产生大量高度相似的样本模型反而被“背题”。但推理时的重叠率要提到 30% 以上原因在后面讲这是训练和推理一个容易忽略的不对称点。import cv2 import xml.etree.ElementTree as ET def slide_crop(image_path, xml_path, out_dir, crop_size1024, overlap0.2): img cv2.imread(image_path) h, w img.shape[:2] step int(crop_size * (1 - overlap)) # 边界补齐保证最后一行/一列也能完整切到 y_steps list(range(0, h - crop_size 1, step)) x_steps list(range(0, w - crop_size 1, step)) if y_steps[-1] ! h - crop_size: y_steps.append(h - crop_size) if x_steps[-1] ! w - crop_size: x_steps.append(w - crop_size) tree ET.parse(xml_path) boxes [] for obj in tree.findall(object): b obj.find(bndbox) boxes.append((obj.find(name).text, int(float(b.find(xmin).text)), int(float(b.find(ymin).text)), int(float(b.find(xmax).text)), int(float(b.find(ymax).text)))) idx 0 for y in y_steps: for x in x_steps: crop img[y:y crop_size, x:x crop_size] keep [] for name, x1, y1, x2, y2 in boxes: cx, cy (x1 x2) / 2, (y1 y2) / 2 if x cx x crop_size and y cy y crop_size: keep.append((name, x1 - x, y1 - y, x2 - x, y2 - y)) gray cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) if gray.var() 400 and not keep: continue # 空白面板块过滤掉 # 这里写保存 crop 和对应 xml 的逻辑 idx 1这段代码里三个参数要解释清楚。overlap0.2意味着步长是 816 像素相邻切片有 20% 的内容重叠能降低缺陷正好卡在切片边缘的概率。gray.var() 400是判空白块的阈值纯蓝色天空或平整的水泥地方差很小直接跳过如果巡检现场有大量植被、阴影这个阈值要调低到 200否则会把含背景的样本全过滤掉导致训练集背景单一。保留标注框的规则是“中心点落在切片内才保留”一张缺陷图只会完整出现在一个切片里避免同一个缺陷被反复裁切产生重复样本。3.2 VOC 转 YOLO坐标换算与越界框处理用 LabelImg 标注出来的数据通常是 VOC XML 格式YOLO 系列训练需要的是归一化的 txt 标注。转换这一步容易翻车的点有两个一是切片后的框越界二是碎框没过滤。下面这段转换函数处理了这两个问题。def voc_to_yolo(xml_path, out_txt, img_w, img_h): tree ET.parse(xml_path) with open(out_txt, w) as f: for obj in tree.findall(object): name obj.find(name).text cls_id class_names.index(name) b obj.find(bndbox) x1 int(float(b.find(xmin).text)) y1 int(float(b.find(ymin).text)) x2 int(float(b.find(xmax).text)) y2 int(float(b.find(ymax).text)) # 越界裁剪 x1, y1 max(0, x1), max(0, y1) x2, y2 min(img_w - 1, x2), min(img_h - 1, y2) if x2 - x1 10 or y2 - y1 10: continue # 碎框没有学习价值 x_center (x1 x2) / 2 / img_w y_center (y1 y2) / 2 / img_h width (x2 - x1) / img_w height (y2 - y1) / img_h f.write(f{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n)为什么小于 10 像素的框直接丢弃裂纹和热斑在切片后若只剩下 5 像素宽模型学到的是“一小团噪声等于缺陷”会把背景纹理学进去。这类碎框即使保留在 NMS 阶段也会被大框吞掉对训练没有正收益。注意转换后的 width 和 height 都是相对切片尺寸的归一化值不要写错成绝对像素。3.3 数据增强让模型见过更真实的天气和光照光伏场景最大的干扰是光照多变早晨低角度强反光、中午高照度、多云时阵列上出现云影还有组件表面的镜面反射。增强策略要围绕这些真实干扰设计而不是堆一堆随机旋转。我一般保留水平翻转、垂直翻转这两个操作对缺陷检测没有语义破坏HSV 亮度扰动加一点模拟不同时段的光照mosaic 增强在 YOLO 里默认开启但对纯小目标数据集不一定是好事——四张图拼在一起每张更小热斑更看不清。数据集里小目标占比超过 50% 时我建议把 mosaic 提到的概率调低甚至只在训练前中期开启。离线增强和在线增强的取舍如果你的标注数据有两千到五千张离线做 3 到 5 倍的增强就够了重点增强的是样本量少的类别比如 crack。注意增强后的新样本不要和原样本重复进入同一个 epoch最好独立存成目录混在训练集里做随机采样否则模型会对增强后的变异过拟合。这一步做完回头看一眼类别分布如果 dust 类因为增强变得太多可以少生成一些避免训练失衡。4. 训练与验证YOLO 系列配置、关键参数与漏检定位4.1 训练配置数据集 yaml 与模型选择数据准备好了下一步是训练目标检测模型。项目标题里的“算法”落点常见做法是采用 YOLO 系列先拿 v5 或 v8 的 s/m 权重做迁移学习训练成本低部署生态也成熟。不要一上来就上最大的 x 模型光伏缺陷数据集通常只有几千张大模型在小数据上表现为严重的过拟合训练时间还长。先跑通小模型拿到基线再根据漏检情况决定是否升级。# dataset.yaml path: ./datasets/pv_defect train: images/train val: images/val nc: 3 names: [hot_spot, crack, dust]yolo detect train \ modelyolov8s.pt \ datadataset.yaml \ imgsz1024 \ batch16 \ epochs200 \ patience30 \ ampTrue \ cacheram \ workers4这里要解释一下cacheram。几千张 1024 切片全量读进内存大概要 8 到 16GB如果机器内存够这一步能省掉每次 epoch 的磁盘 IO训练速度快一倍。内存不足就删掉这个参数不要硬开。patience30是早停参数训练集小的时候模型在 50 到 80 轮就容易过拟合不等它跑完 200 轮。4.2 关键参数imgsz、anchor 与类别不平衡imgsz1024是基于第 2 章统计结果定出来的。如果训练图和推理图不一致比如训练用 640 部署时喂 1024模型对目标尺度的响应会乱表现为大批漏检。所以训练分辨率一旦定下来后面数据增强、推理切片的尺寸都要对齐。在这个项目里我建议训练和推理都用 1024除非目标已经大到中位数超过 128 像素才考虑降到 640 提速。类别不平衡方面如果你的数据像大多数光伏项目一样 dust 占一半、crack 只有十分之一需要在配置里给类别损失加权。YOLO 的cls损失权重默认是 0.5可以在训练配置里调高到 4.0 左右让模型在训练时更关注错分代价。同时给稀缺类设定更高的最小置信度保留阈值……不对训练阶段不用管推理阈值这里要做的只是把类别权重加上。另外 batch 不要设太小batch16 是个折中点太小的话 BN 层的统计量不稳定小目标特征的收敛会非常慢。4.3 训练验证mAP、混淆矩阵与漏检定位训练完先跑验证集yolo detect val \ modelruns/detect/train/weights/best.pt \ datadataset.yaml看结果不要只看总 mAP。mAP 高可能只是因为 dust 这类易检类别做得很好真正决定项目能不能交付的是 crack 和 hot_spot 的 recall。打开混淆矩阵重点看这两行是不是大量落到了背景列。如果 hot_spot 的 recall 低于 0.7大概率是下面几个原因之一标注框偏大导致 IoU 计算困难、切片分辨率不够、缺陷被切片截断。这时候先别急调模型回去看几个漏检的样本占多数的是哪种情况。如果是切片截断导致的去调训练切片的 overlap如果全是极小目标把 imgsz 提到 1280 再试一轮。这样定位比盲目调 anchor 有效得多。5. 实拍推理与部署排查坐标还原、边缘设备与 5 个高频坑5.1 整图切片推理与坐标还原别让缺陷飞到旁边那块板上训练完的模型输入是 1024×1024而无人机实拍图是几千像素宽的大图。直接 resize 推理会让热斑缩到 10 像素以下基本等于盲检。正确做法是用和训练时相同尺寸的滑窗去切推理图逐块预测再把检测框坐标加回偏移量最后做整图 NMS 合并重叠框。def infer_full_image(model, image_path, crop_size1024, overlap0.3, conf0.25): img cv2.imread(image_path) h, w img.shape[:2] step int(crop_size * (1 - overlap)) detections [] for y in range(0, h - crop_size 1, step): for x in range(0, w - crop_size 1, step): crop img[y:y crop_size, x:x crop_size] results model.predict(crop, confconf, verboseFalse) for box in results[0].boxes.data.cpu().numpy(): x1, y1, x2, y2, score, cls_id box detections.append([x x1, y y1, x x2, y y2, score, cls_id]) # 整图 NMS合并跨切片的重复检测 keep cv2.dnn.NMSBoxes( [b[:4] for b in detections], [b[4] for b in detections], score_thresholdconf, nms_threshold0.4 ) final [detections[i] for i in keep.flatten()] if len(keep) else [] return final这段代码的关键点是overlap0.3。推理重叠率必须比训练高因为同一个缺陷如果横跨两个切片会被检测两次靠 NMS 合并没问题但如果 overlap 太小缺陷在切片边缘时会因为上下文缺失而漏检。这个现象在训练时不成问题因为训练样本的缺陷会随机落在切片各处推理时却是固定网格切所以训练 overlap 可以低推理 overlap 必须高。坐标还原的坑在代码里看不出来但经常出在索引上切片坐标x x1是整数加法没有陷阱真正的坑是有人把切片的偏移量写成了除以步长后的格子序号结果所有框都偏了半块板的位置。巡检结果最终要落到“哪一排哪一块板”的告警工单上框偏一两个像素可能无所谓偏几十像素就会指到相邻面板上。5.2 边缘设备部署在 Jetson 上跑还是离线跑无人机巡检的算力部署通常有两类场景。一类是飞机落地后把 SD 卡里的图批量跑一遍离线推理这个只要电脑有 NVIDIA 显卡就行模型用.pt权重直接跑。另一类是机场或机巢边缘节点实时处理常见设备是 Jetson Orin Nano 或同级别盒子做法是把模型转成 TensorRT 的 engine 格式FP16 量化后推理。TensorRT 转换后模型精度会掉一点mAP 一般降 1 到 3 个百分点这是量化误差属于正常现象。做法是把 TensorRT engine 拿到至少 100 张未参与训练的实拍图上做 regression test对比转换前后的检测框数量。如果检测框数量少了三分之一说明量化敏感要退回 FP32 或者改用 P 系列的 INT8 校正集重新校准。这里有一个取舍经验实时不是光伏巡检的硬需求单张大图推理两三秒完全可接受所以如果 TensorRT 掉点明显就别强上离线推理更省心。5.3 无人机光伏巡检部署的 5 个高频坑与排查顺序第一个坑训练集 mAP 0.87上现场漏检一半。现象是模型在验证集上表现很好但无人机实拍的图大量漏检。原因是训练切片大多是从标注好的缺陷图里裁的缺陷位置偏向图像中心模型学到的是“中间有亮斑才算热斑”。解决重新做滑窗切片时不裁剪、不居中按网格扫全图让缺陷出现在切片的不同位置训练时再叠加随机平移增强。第二个坑镜面反光引发大量误检。现象是晴天下组件玻璃的反射光被识别成 hot_spot而且是成片出现。原因是训练样本里没有足够多的反光负样本模型把“局部高亮”当成了热斑特征。解决专门采集一批无缺陷但带反射、带云影的切片放进训练集作为背景同时推理时加一条后置规则缺陷框内部像素方差极低且边缘亮度梯度不对称的判为反光丢弃。第三个坑缺陷正好切在切片边缘两个切片都只检测到一半。现象是同一个热斑被输出成两个相邻的框合并后中心点偏移。原因是推理 overlap 太低。解决推理 overlap 提到 0.3 到 0.5NMS 的 IoU 阈值调到 0.4不要用默认的 0.5否则跨切片的重叠框在合并时容易被当成两个目标保留。第四个坑边缘设备推理时显存溢出或者单张大图耗时数秒。现象是 Jetson 上跑 1024 imgsz 直接 OOM。原因是模型太大、batch 开太大、输入图没切片直接喂整图。解决模型从 m 换成 s 或 n 的 TensorRT 版本batch 固定 1输入尺寸保持 1024确认推理链路的预处理缩放、归一化、通道顺序与训练时完全一致。这个“预处理不一致”问题特别隐蔽很多人在 TensorRT 转换后掉精度都是因为 BGR/RGB 顺序和归一化除以 255 的细节不一致。第五个坑检测框坐标和 GIS 地图上的面板编号对不上。现象是算法输出的缺陷框在图上位置正确但落到电站台账上对应到了错误的面板。原因是像素坐标没有做投影变换直接用照片边角估算经纬度。解决用无人机 RTK 记录的相机位置和云台姿态角做单应变换或者在地面选 4 个控制点标定投影矩阵先验证 10 个点的映射误差误差大于 1 米就不能用于自动生成工单。6. 验收技巧用锚定测试集卡每个类的漏检底线项目能不能交付不是看训练集 mAP而是看模型在“没调过的实拍图上”到底漏了多少缺陷。我的习惯是把全部已标数据抽 30% 锁成锚定测试集这个集合只在最终验收时跑一次平时训练和调参永远不碰。验收时按类看 recall而不是只看总 mAP因为不同缺陷的容错率完全不同。热斑和接线盒故障直接关系电站安全漏检不可接受recall 至少要 0.9裂纹和脏污属于待维护项recall 在 0.7 以上即可。在验收表里写明每个类别的底线然后给每个类别单独设一份置信度阈值而不是全图统一用 0.25。常见做法是热斑阈值 0.15、裂纹 0.2、脏污 0.3把阈值调低换召回再用面积下限过滤噪声。这份验收表也要包含误检上限每 1000 块正常面板里误报不超过 10 条超过了说明背景样本不够得回到第 3 章补反光、阴影、绝缘条这类硬负样本。这样整个项目就闭环了标注统计决定切片参数切片决定训练分辨率训练结果回看漏检原因推理 overlap 决定实拍召回锚定测试集决定是否交付。我踩过的最大教训就是跳过验收标准直接调模型结果每调一版都要重新翻一遍实拍图白白耗掉两三周。先定验收标准再动训练参数这个顺序不要颠倒。这个思路对无人机视觉感知类的项目都适用希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网