无人机光伏缺陷检测落地指南:从数据切图到模型训练与巡检报表
简介针对无人机场景的光伏面板缺陷检测需求该资源提供了一套可直接运行与二次开发的完整项目方案。项目以Python语言编写融合OpenCV、TensorFlow、Keras等工具链覆盖图像采集、预处理、分割、特征提取及缺陷分类全流程并利用SVM、随机森林等机器学习模型判断面板缺陷类型适用于光伏电站智能巡检、高校毕设及算法研究人员。资源包共405个文件压缩包大小106.72MB以289个py源码文件为主体辅以dll、pyd、exe等运行依赖组件以及模型权重pth、配置文件和说明文档结构清晰便于部署调试。当前已有285人学习下载具备一定社区验证度。除完整源码外项目还提供具体应用场景和操作指南可依照流程处理无人机航拍图像完成光伏面板缺陷检测。同时算法模块留有扩展空间可针对图像质量调整滤波、尝试不同特征提取方法或替换分类模型有较强的实用性与可移植性。1. 无人机视角下的光伏缺陷检测比想象中难在哪把“光伏面板缺陷检测”做成无人机巡检方案时最大的错觉是以为它和通用工业缺陷检测一样拿一批标注图、训一个模型、飞一圈输出框就完事。实际飞过几次就会发现无人机画面的视角、光照和分辨率分布跟产线上固定工位的成像完全是两回事。隐裂只有米粒宽热斑在伪彩图里亮得刺眼却可能是接线盒的倒影一条裂纹横跨多个光伏片时普通目标检测框根本框不干净。这个方向真正的难点不是“跑通一个算法”而是把成像、数据、模型和巡检流程串成一条能稳定交付的链路。本文就从拿到那份“附项目源码”的压缩包之后说起按数据格式化、模型训练、大图推理、坑位排查到结果解释的顺序把一套能落地的无人机光伏缺陷检测方案讲清楚。适合正在做电站巡检服务、或想从公开数据集切入缺陷检测算法方向的同学参考。2. 先把数据做成算法能消化的形态模态切分、切图与坐标转换2.1 三种载荷、三类缺陷哪些该用框检测哪些该用分割无人机巡检光伏电站常见载荷只有两种可见光相机和红外热成像。少数方案会提 EL电致发光检测但 EL 需要给组件通电且通常在夜间操作无人机平台很少真正量产使用多数时候是地面设备配合。先说清楚这个前提你才不会把源码包里所有图像一股脑丢进同一个模型。三种载荷对应的缺陷类型差异非常大我把常见的对应关系整理成下表成像模态能看到什么典型缺陷适合的算法粒度可见光 RGB表面外观玻璃破碎、脏污遮挡、断栅、蜗牛纹目标框 少量分割红外热成像温度分布热斑、接线盒异常、二极管失效、整串过热目标框 温度阈值规则EL 电致发光电池片内部电学特性隐裂、PID、断栅、碎片分割为主无人机场景少用这里最关键的判断是可见光和红外必须分开建模不要混训。原因很简单同一个“缺陷”在两个模态下的视觉特征完全不同热斑在红外里是一个亮斑在可见光里可能只是一个不起眼的暗区脏污在可见光里是块状阴影在红外里可能根本不明显。混在一起训模型内部的特征表征会被模态差异搅乱最后 mAP 看着还行一到真实巡检场景就误报漏报交替出现。2.2 把原始飞行图切成训练 Patch切图反而会破坏缺陷怎么切才科学无人机拍出来的单张原图通常是 4000×3000 甚至 6000×4000 像素。训练时如果直接整图缩放成 640×640组件上的隐裂、细碎裂纹会被压缩成两三个像素检测头完全没法学习可靠特征。所以常规做法是滑窗切 Patch把大图切成 640 或 1024 大小的训练块。切图代码本身不难难在参数和过滤逻辑。我一般用下面这个脚本import cv2 import numpy as np from pathlib import Path def split_image(img_path, out_dir, tile640, overlap128, min_brightness40): img cv2.imread(str(img_path)) h, w img.shape[:2] step tile - overlap idx 0 for y in range(0, h - tile 1, step): for x in range(0, w - tile 1, step): patch img[y:y tile, x:x tile] # 跳过天空和暗场占比过高的块减少无效训练样本 hsv cv2.cvtColor(patch, cv2.COLOR_BGR2HSV) v_channel hsv[:, :, 2] if np.mean(v_channel) min_brightness: continue # 亮度方差太低说明是纯色背景同样跳过 if np.std(v_channel) 8: continue out_name f{img_path.stem}_{idx:04d}.jpg cv2.imwrite(str(out_dir / out_name), patch) idx 1 print(f{img_path.name}: 生成 {idx} 个 patch) # 示例调用 split_image(DJI_0001.JPG, Path(./train_patches), tile640, overlap128)这段代码的逻辑分成三块第一用step tile - overlap控制滑窗步长保证相邻 patch 之间有重叠避免裂纹恰好落在 patch 边界被切成两段第二把每个 patch 转到 HSV 空间用 V 通道均值过滤掉天空、纯黑背景这类无效样本第三用 V 通道方差排除光照均匀的空白区域。参数说明tile取 640 是最稳妥的模型输入分辨率不会太低显存压力也可控如果原图里的组件很小、裂纹整体较长可以提到 960 或 1024。overlap至少 128这个值不能省。我见过有人为了省训练时间把 overlap 设成 0结果缺陷在 patch 边缘反复被切半模型学出来的全是残缺陷推理时同样的问题导致漏检率直接翻倍。min_brightness和方差过滤阈值取决于你现场的光照条件阴暗天气或者傍晚采集的数据要适当调低否则过滤比例过高样本直接少掉三分之一。切完图记得按“电站/飞行架次”建子目录后面划分训练验证集时要按目录分不要按文件随机分否则同一个场景的画面会同时出现在训练集和验证集里模型过拟合了你自己还看不出来。2.3 标注格式转换脚本从 VOC/COCO 到 YOLO 的坐标要除以宽和高源码包里给的标注是 VOC XML 格式比较常见也可能有 COCO 的 JSON。而训练脚本用的是 YOLO 的 txt 格式即每行一个目标的归一化坐标class x_center y_center width height。手动在标注软件里导来导去很容易出问题我习惯写一个小脚本统一转换import xml.etree.ElementTree as ET from pathlib import Path # 类别编号先固定后续训练脚本里要一致 LABEL_MAP {crack: 0, hotspot: 1, dust: 2, connector: 3} voc_dir Path(labels_voc) yolo_dir Path(labels_yolo) yolo_dir.mkdir(exist_okTrue) for xml_file in voc_dir.glob(*.xml): tree ET.parse(xml_file) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in LABEL_MAP: continue box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # 关键坐标必须除以原图宽高而不是除以 patch 大小 x_center ((x1 x2) / 2) / img_w y_center ((y1 y2) / 2) / img_h box_w (x2 - x1) / img_w box_h (y2 - y1) / img_h # 过滤掉小于 3 像素的标注这类框大多是标错或噪声 if box_w * img_w 3 or box_h * img_h 3: continue lines.append( f{LABEL_MAP[name]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f} ) out_path yolo_dir / f{xml_file.stem}.txt out_path.write_text(\n.join(lines))这个转换脚本里最容易翻车的点有两个。第一是坐标归一化x_center和y_center必须除以原始图像的宽度和高度哪怕你后续做的是切图训练如果标注是在原图上打的转换时也按原图尺寸归一化切图时再把对应的框坐标换算到 patch 坐标系里。很多人在这一步顺手除以了 640训练数据全错还查不出来。第二是类别编号的一致性脚本里LABEL_MAP的编号如果和训练配置里的names列表对不上模型训练时会把裂纹学到脏污类别上推理结果完全没法用。我习惯把这个映射表单独放在一个classes.yaml里转换和训练都从这里读而不是在两处各写一份。3. 训练一个能上无人机的检测模型从 YOLOv8 到 RT-DETR 的选型与参数3.1 泛化陷阱别一上来就追 RT-DETR先用 YOLOv8 立基线缺陷检测算法领域这两年的热度很大程度上是 RT-DETR 这类端到端模型带起来的。它的优势是去掉了 NMS 后处理推理速度均匀且在大模型版本上精度确实比同量级的 YOLO 系高一点。但放到无人机光伏缺陷检测这个场景里我强烈建议先别急着上 RT-DETR老实用 YOLOv8 把流程跑通。原因有三个。第一RT-DETR 是 Transformer 结构训练收敛对数据质量、batch size 和学习率更敏感光伏缺陷样本本身类不平衡严重、背景差异大新手用默认参数训 RT-DETR 很容易遇到 loss 不降或者过拟合的问题。第二RT-DETR 的预训练权重主要来自通用目标检测数据集对“热斑”“断栅”这类强纹理、弱小目标几乎没有先验优势YOLOv8 有成熟的 yaml 配置和大量教程出问题时社区经验更容易查。第三你的核心目标是快速拿到一个能飞的模型而不是刷榜YOLOv8s 在 640 分辨率下单张推理也就 10ms 上下足够覆盖无人机的批量巡检需求。如果你就是想试 RT-DETR我建议把顺序反过来先用 YOLOv8 训练一版记录 mAP、漏检率和误检率再拿同一份数据换 RT-DETR 训练对比两版的差。这样至少你知道性能差距是模型结构带来的还是数据/参数带来的。3.2 训练脚本batch、imgsz 和增强参数怎么设才不踩坑以下是一份我实际在用的 YOLOv8 训练脚本针对光伏面板场景做了参数调整from ultralytics import YOLO model YOLO(yolov8s.pt) # 用预训练权重做迁移学习 results model.train( datapv_defect.yaml, epochs200, imgsz640, batch16, workers8, lr01e-3, weight_decay5e-4, warmup_epochs3, mosaic0.5, close_mosaic10, hsv_h0.02, hsv_s0.5, hsv_v0.3, degrees0.0, # 关键关闭旋转增强 translate0.1, scale0.4, fliplr0.5, flipud0.0, # 上下翻转会破坏光伏板的纹理方向 patience50, )参数说明按优先级来。imgsz决定了模型能看到的细节粒度如果显存允许用 960 比 640 在隐裂检测上普遍好 35 个点 mAP代价是训练时间和显存翻倍。mosaic是这组参数里最需要小心的一个它把四张图拼在一起训练对提升泛化很有帮助但光伏面板是规则排列的矩形纹理拼接产生的接缝容易让模型误以为“纹理断裂”是缺陷。所以我只开到 0.5并且用close_mosaic10在最后 10 个 epoch 关闭 mosaic让模型在正常分布上收敛。degrees0.0是光伏场景特有的硬性设置。无人机巡检时机头基本保持正对组件阵列缺陷的姿态变化有限而旋转增强会把水平方向的光伏栅线纹理旋转成斜向甚至竖直模型学到了原本不存在的纹理方向推理时对真实缺陷的响应反而变差。flipud同理光伏安装方向固定上下翻转后的组件布局在物理上不存在这点很多通用检测的经验帖不会提。3.3 小目标缺陷不涨点先看数据分布再谈网络结构训练几轮之后如果发现裂纹和热斑的 recall 一直上不去不要急着换更大的模型或用 RT-DETR 硬刚。先跑一段统计脚本看看训练集里每个类别的标注框尺寸分布。光伏缺陷有一个明显特征长条形缺陷非常多比如断栅是长条、隐裂是斜的细长条。YOLO 的矩形框回归对这类目标本身就吃亏因为一个框里大部分区域是正常纹理模型很难判断“到底哪部分算缺陷”。常见做法是两条路。一是把这些长条缺陷从目标框改成分割标注转用 YOLOv8-seg 或者 DeepLab 系做语义分割效果会明显上台阶但标注成本翻倍二是保留目标框但在标注时把框贴着缺陷的边界画得紧一些不要为了省事用一个长方形把整条隐裂都套进去。后者对标注员的手艺要求高但如果你的数据源里已经有一批质量还不错的框标注先用这个方案可以让模型快速收敛后续再逐步引入分割。另外类别不平衡严重的场景比如热斑只有裂纹样本的十分之一可以在 yaml 里配置每个类的权重或者直接复制热斑样本做重采样。数据层面的投入永远是光伏缺陷检测里回报最高的部分。4. 把模型接到巡检流程大图推理、NMS 合并与运维报表4.1 高空大图推理用滑动窗口处理原始分辨率而不是整图缩放训练时切 Patch推理时同样不能把一张 6000×4000 的原图直接塞进模型。Ultralytics 内部会等比缩放但目标缩小十六倍后基本就丢了。所以推理阶段必须用滑动窗口逐块预测再把结果映射回原图坐标。下面这段代码是我常用的推理脚本覆盖了切块、单块预测和全图合并三个步骤import cv2 import numpy as np from ultralytics import YOLO def nms(boxes, scores, iou_thresh0.45): 简单 NMSboxes 是 [N,4] 的 xyxy 坐标 x1, y1 boxes[:, 0], boxes[:, 1] x2, y2 boxes[:, 2], boxes[:, 3] areas (x2 - x1 1) * (y2 - y1 1) order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) inter np.maximum(0, xx2 - xx1 1) * np.maximum(0, yy2 - yy1 1) iou inter / (areas[i] areas[order[1:]] - inter 1e-6) order order[np.where(iou iou_thresh)[0] 1] return keep def tile_infer(model, img_path, tile640, overlap128, conf0.3, iou0.45): img cv2.imread(str(img_path)) h, w img.shape[:2] step tile - overlap all_boxes [] all_scores [] all_labels [] for y in range(0, h - tile 1, step): for x in range(0, w - tile 1, step): patch img[y:y tile, x:x tile] preds model.predict(patch, confconf, iouiou, verboseFalse)[0] for det in preds.boxes.data.cpu().numpy(): x1, y1, x2, y2, score, label det # 映射回原图坐标 all_boxes.append([x1 x, y1 y, x2 x, y2 y]) all_scores.append(score) all_labels.append(label) all_boxes np.array(all_boxes) all_scores np.array(all_scores) # 对同一类别分别做 NMS final_boxes, final_scores, final_labels [], [], [] for cls_id in set(all_labels): mask np.array(all_labels) cls_id cls_boxes all_boxes[mask] cls_scores all_scores[mask] keep nms(cls_boxes, cls_scores, iou_thresh0.45) final_boxes.extend(cls_boxes[keep]) final_scores.extend(cls_scores[keep]) final_labels.extend([cls_id] * len(keep)) return np.array(final_boxes), np.array(final_scores), np.array(final_labels) model YOLO(best.pt) boxes, scores, labels tile_infer(model, flight_tile_001.jpg)这段代码有几个地方容易踩坑。第一每个 Patch 单独过模型时同一个缺陷如果同时出现在两个相邻 Patch 的重叠区域会得到两个检测框这就是为什么最后必须做全图 NMS如果省略这一步一张巡检图会输出几十个重叠框运维人员根本没法看。第二NMS 要按类别分别做不同类别的框即使重叠也不应该被合并比如热斑落在脏污区域里是正常的。第三model.predict内部的iou参数只作用于单个 Patch 的框合并不能替代全图合并所以这里外层的 NMS 是必须的。conf参数控制保留阈值巡检场景我一般设 0.250.3宁可多出几个误检框也不能漏掉真实缺陷。4.2 置信度与 NMS 阈值怎么调到不误报也不漏报置信度阈值的设置没有统一答案但有一个原则巡检任务的代价是不对称的漏检一个热斑可能意味着整串组件持续过热甚至起火而误检一个框最多让运维人员多走几步路现场确认。所以初始阈值建议往低设比如conf0.25。如果误检率太高再把阈值往上提到 0.35 或 0.4用一次 2030 张图的测试集验证即可。NMS 的 IoU 阈值影响的是相邻框的合并策略。光伏组件排列紧密同一块组件上的两个相邻缺陷距离可能只有几厘米IoU 阈值设太高会把本来独立的两个缺陷合并成一个设太低又会留下大量重叠框。我默认用 0.45当检测目标平均尺寸较小时可以降到 0.4。另外不同类别之间永远不要做跨类别的 NMS否则热斑和它周围的接线盒框会被合并成一个输出结果就失真了。4.3 从检测框到运维报表按组件串聚合缺陷检测框本身不是交付物运维人员关心的是“哪一串组件需要处理”。所以推理结果需要映射到物理位置并按组件串做聚合统计。常见做法是根据无人机 POS 数据和相机内参把检测框中心点从图像坐标换算到经纬度再投影到电站的组件排布网格上。这个流程拆成两步。第一步把检测框转成 CSV 导出每条记录包含检测框中心点的像素坐标、所属航线、图片文件名、缺陷类别和置信度。第二步用经纬度或行列网格做 GroupBy统计每个组件串的缺陷数量、最高置信度和缺陷类型分布。简化版的聚合逻辑如下import pandas as pd dets pd.DataFrame({ x1: boxes[:, 0], y1: boxes[:, 1], x2: boxes[:, 2], y2: boxes[:, 3], score: scores, label: labels, }) # 假设沿水平方向每 1000 像素为一个组件串分区实际以阵列间距标定为准 dets[panel_serial] (dets[x1] dets[x2]) / 2 // 1000 summary dets.groupby([panel_serial, label]).agg( defect_count(score, count), max_conf(score, max), ).reset_index() summary.to_csv(inspection_report.csv, indexFalse)这里的分区大小 1000 像素是占位值真正部署时要用航线中已知的组件宽度像素做标定否则串号会偏移。报表里建议再加两列缺陷面积估算用检测框宽高乘上单像素对应物理尺寸和建议动作如“更换组件”“清洗”“复检”。这样运维人员不用打开 GIS 系统只看 Excel 就能排任务。5. 光伏缺陷检测避坑笔记5 个高频翻车现场与修复方法5.1 太阳反光被当成玻璃碎裂现象晴天中午拍摄的可见光图像中模型频繁把组件表面的镜面反射光斑框成“crack”同一个电站的数据误检率能到 40% 以上。原因训练集大多来自阴天或顺光拍摄缺少强反光负样本。模型没有见过“高亮但不属于缺陷”的纹理自然把高对比度亮斑当作裂纹。解决把反光样本作为负样本补进训练集。不需要额外标注直接把含反光的 Patch 放在一个negative/目录里配置成背景类即可。另一个立竿见影的缓解办法是在 HSV 增强里调低hsv_v的变化幅度并增加全局灰度对比度归一化减少光照极端情况的影响。5.2 红外热像伪彩图直接训练导致热斑检不出来现象模型在红外伪彩图上训练完后验证集 mAP 很高但到一个新电站推理时同样的热斑全漏了。原因不同热像仪甚至同一台热像仪在不同量程下的伪彩映射表是不一样的。你在训练集里看到的亮黄色热斑到另一台设备上可能被映射成暗红色模型学的其实是“亮黄色区域”而不是“温度异常区域”。解决不要拿伪彩 JPEG 直接训练。红外热像仪的 SDK 通常能导出温度矩阵或辐射视频配合analytical调色板可以得到每个像素对应的物理温度值。训练时用温度矩阵做输入或者在预处理阶段对全图做温度归一化把每个像素减均值除方差这样模型学到的是相对温差而不是绝对颜色。如果拿不到原始温度数据至少要在训练集里混入多台设备、多个量程的伪彩图增强模型对颜色映射的鲁棒性。5.3 隐裂被切在 Patch 边界模型学了一堆“半截裂纹”现象训练阶段的 loss 正常收敛但推理阶段裂纹检测的召回率只有五成而且检测框普遍只覆盖裂纹的一半另一半被截断。原因切 Patch 时overlap太小或者切 Patch 之后没有同步更新标注框的坐标导致同一段裂纹在不同样本里被以不同比例截断模型不知道完整的裂纹长什么样。解决切 Patch 时把overlap设为目标最大尺寸的 1/3 以上最稳妥是 128 像素起。同时写一个坐标变换函数把原图标注框同步映射到每个 Patch 的局部坐标系并丢弃完全落在 Patch 外的框对部分越界的框做裁剪而非整框舍去。验证集上专门统计一下“框长度与真实裂纹长度比例”这个指标如果低于 0.8基本就是这个问题。5.4 接线盒被识别成热斑或脏污现象可见光模型把组件背板上的白色接线盒框成“dust”红外模型则把接线盒的正常发热误判为“hotspot”误检集中在每块组件的角落。原因接线盒在可见光和红外图像中都有固定的外观特征而训练集的负样本里没有专门收集接线盒。模型无法区分“长得像接线盒的物体”和“真正的接线盒缺陷”。解决把“connector”作为一个独立类别加入标签体系在转换脚本里为它分配单独的类别 ID。这样模型既能区分接线盒和缺陷又能在后续逻辑里用规则过滤掉接线盒区域的检测结果。更彻底的做法是先在巡检图上用模板匹配或一个专用小模型定位组件和接线盒位置再把缺陷检测限制在组件有效区域内从源头排除背景干扰。5.5 验证集按文件随机分模型在陌生电站直接翻车现象训练阶段验证集 mAP 有 0.7换到另一个电站的图片推理时 mAP 跌到 0.3像是模型换了个环境就失灵。原因数据集划分时按图片随机打乱同一个电站的相似画面同时出现在训练集和验证集里模型记住的是这个电站特有的地面背景、光照和组件型号而不是缺陷本身。验证集分数是虚高的。解决按“电站”或“飞行架次”划分数据集。训练集包含 A、B 两个电站验证集用 C 电站的数据测试集再用 D 电站。这样才能真实反映模型跨场景的泛化能力。这个原则同样适用于采样训练数据里至少混入 3 个以上不同建成年代、不同组件品牌、不同朝向的电站画面否则你训出来的模型本质上是个“单电站专属模型”。6. 让缺陷检测结果可解释热斑温差标定与缺陷分级算法输出的检测框只能说明“这里有个异常区域”不能直接回答“这个区域要不要处理”。为了让巡检结果真正落地我会把检测结果和物理量结合起来做分级这里以热斑检测为例说明。第一步是温度标定。红外热像仪 SDK 可以导出每个像素的绝对温度或者通过辐射参量反查得到温度矩阵。检测模型给出热斑框后提取框内像素的平均温度T_defect再提取同串组件正常区域的背景温度T_normal两者相减得到温差 ΔT。经验上ΔT 持续大于 10℃ 就应当安排复检ΔT 大于 20℃ 且面积超过电池片面积的 10% 时建议尽快更换组件。这个阈值不需要写死在代码里放在配置文件里运维工程师可以按季节和环境温度调整。第二步是缺陷分级。我做巡检报表时会把缺陷分成三个等级A 级为紧急处理对应热斑温差大于 20℃ 或玻璃碎裂影响安全的情况B 级为近期安排对应隐裂穿过电池片主栅、断栅数量超过三分之一的情况C 级为记录观察对应轻微脏污、蜗牛纹早期痕迹。分级规则用 if-else 就能实现不需要引入复杂模型但报表可读性会提升一个档次。C 级的缺陷会进入数据库留存下次巡检时对比同一位置的缺陷演化速度这才是无人机巡检相比人工巡检的真正价值。最后说一个我自己的习惯每次训练完模型不看 mAP先拿最近一次真实巡检的原图跑一遍推理把检测框叠加在图上逐张过目至少看 20 张。这一步能发现所有指标掩盖的问题——漏检是集中在小目标还是集中在某类背景误检是集中在反光还是集中在接线盒。调一次参数过一遍图比盯着训练曲线猜效率高得多。这套流程从数据到报表跑通之后再去尝试更大模型或更复杂的网络结构才有意义否则只是把基础流程上的问题原封不动搬到一个更贵的地基上。希望帮到你。本文还有配套的精品资源点击获取