简介:面向能源行业的YOLOv11光伏板表面污渍检测与清洁机器人路径规划PDF文档,适合从事光伏电站运维、计算机视觉算法研究及清洁机器人开发的技术读者。文档共30页,压缩包内仅1个PDF文件,大小1.88MB,已生成完整目录,支持章节快速跳转与大纲定位。内容围绕光伏板污渍检测与清洁任务展开,从能源行业背景、YOLOv11网络架构与损失函数,到污渍检测数据集的采集、标注与划分,再到模型训练优化、路径规划算法选型(贪心算法、A*算法、遗传算法),以及检测结果与路径规划的结合实现;系统测试、沙漠与沿海光伏电站应用案例和未来展望一并覆盖。目前已有153人浏览学习,该文档既可作算法应用参考手册,也能为光伏检测与清洁机器人方案落地提供结构化思路。
1. 光伏板污渍检测为什么非用YOLOv11不可:从巡检成本到清洁调度的现实账
光伏电站运维里最磨人的不是组件故障,而是面板表面那层说不清道不明的污渍。鸟粪、灰尘、落叶堆积在钢化玻璃表面,轻则影响单块组件功率,重则形成热斑,直接威胁组串寿命。围绕“能源行业应用:YOLOv11光伏板表面污渍检测与清洁机器人路径规划”的落地方案,核心是用YOLOv11把污渍识别从“人眼巡检”变成算法实时输出,再把检测框转成清洁机器人能执行的清扫路线,让设备只去扫有污渍的区域,而不是按固定周期盲扫整片阵列。这篇笔记适合光伏电站运维、清洁机器人厂商和做智能巡检系统的工程师,通篇按环境配置、模型训练、坐标映射和路径规划的顺序讲,每条命令都按0基础能复现的标准写。
2. 把YOLOv11跑起来之前:环境配置、权重下载与数据集准备
2.1 Ultralytics环境配置的版本坑与CUDA选型
很多新人拿到的是一台已经装好显卡驱动的Windows机器,或者一台无GPU的服务器。YOLOv11对CPU也能做推理,但训练会很吃力。我一般会建议先确认三件事:Python版本、PyTorch版本、CUDA运行时版本。这三者有一个对不上,后面全是报错。
在写“超详细环境配置”这件事上,我习惯用conda把环境隔离干净,避免把系统Python搞乱。常见做法是新建一个Python 3.10的虚拟环境,再指定PyTorch的CUDA版本安装:
conda create -n yolo11 python=3.10 -y conda activate yolo11 pip install ultralytics==8.3.60 torch==2.4.1 torchvision==0.19.1 --index-url https://download.pytorch.org/whl/cu121ultralytics==8.3.60是YOLOv11对应的稳定版本,锁版本是给自己留后悔药,后面导出推理结果时不会因为小版本升级导致输出格式变化。Python 3.10是为了兼容后续的TensorRT导出链路。cu121表示CUDA 12.1,如果显卡驱动只支持到CUDA 11.8,就把这个参数换成cu118;完全没有GPU的机器,直接删掉--index-url参数,PyTorch会自动装CPU版,训练慢但能跑通流程。
装完以后一定要验证一下环境:
python -c "import ultralytics; print(ultralytics.__version__)"这一步能打印出版本号,环境基本就通了。很多人卡在torchvision和torch版本不匹配,ImportError直接报出来;还有人torch.cuda.is_available()返回False,表面上看是环境问题,实际是显卡驱动太老,CUDA运行时压根起不来。
2.2 YOLOv11权重文件下载与结构选型
YOLOv11在Ultralytics里不是一个单独的模型,而是n、s、m、l、x一整族按深度和宽度区分的网络结构。对光伏板污渍检测这种单类别目标,我一般从yolo11n.pt起步,先把整个流程跑通,数据量大、精度要求上去了再换yolo11m.pt。
这里要区分两个文件:yolo11n.yaml只有网络结构,没有权重,训练等于从头学;yolo11n.pt是预训练权重,可以基于COCO数据集学到的纹理特征做迁移学习。光伏板污渍和COCO里的常见物体差异不小,但浅层卷积提取的边缘、颜色、纹理特征仍然非常有用,所以强烈建议用.pt而不是.yaml起步。
权重文件下载不需要手动找,Ultralytics会在第一次调用时自动缓存到本地。如果公司内网机器不能联网,需要先在有网的机器上执行一次YOLO("yolo11n.pt"),再把缓存的pt文件拷贝到内网路径下,放在项目目录里直接用。
from ultralytics import YOLO model = YOLO("yolo11n.pt") # 加载预训练权重 results = model.train( data="pv_stain.yaml", epochs=120, imgsz=640, batch=16, device=0, workers=4, val=True, )参数说明:epochs=120对光伏板这种单类目标来说偏长,一般60到80轮就会收敛,设120是为了留出余地早停。imgsz=640是输入分辨率,如果污渍目标很小可以改成960,但显存占用明显上升。workers=4是数据加载线程数,Windows上这个值超过0有时会报Dataloader错误,遇到就改成0。val=True表示每个epoch结束都跑一次验证集,方便观察mAP曲线决定要不要提前停。
2.3 光伏板污渍数据集的组织与半自动标注
光伏板表面污渍没有公开大库,基本都是电站现场攒图。我建议按YOLO标准目录结构组织:
datasets/ pv_stain/ images/ train/ val/ labels/ train/ val/训练集放80%图片,验证集放20%。拍摄时尽量覆盖清晨、正午、傍晚三个时段,因为阳光角度会让污渍的对比度完全不一样,训练数据只覆盖一个时段,模型现场大概率翻车。
标注格式是YOLO的txt,每行一个目标:class x_center y_center width height,坐标全部归一化到0到1。第一版标注建议用LabelImg手工框,标完200到300张后先训一个粗糙模型,再用这个模型去预标注剩余几千张图片,人工只修正漏框和错框。这个半自动流程能把标注时间压缩到三分之一左右。污渍这类目标的边界本身模糊,标注时我会把框稍微往外扩一点,让模型多学一点背景纹理,比切掉污渍边缘更稳。
2.4 推理结果保存:save_txt与置信度阈值的取舍
训练完的模型最终要给现场用。YOLOv11的推理和保存一条命令搞定:
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( source="datasets/pv_stain/images/val/", conf=0.25, save=True, save_txt=True, project="runs/infer", name="pv_stain_test", )save=True保存带框的可视化图片,方便人眼复查;save_txt=True才是给路径规划用的核心,它会把每个检测框的类别和归一化坐标写进txt文件。光伏场景的置信度阈值建议从0.25起步:低于0.25会出现大量误检,光伏板边框、逆变器阴影都被当成污渍;高于0.5会漏掉浅色鸟粪或干涸水痕,这些目标跟蓝黑色光伏板背景的纹理对比度实在太低。如果你的验证集mAP50已经到0.9以上,可以把conf调高到0.4,减少误检带来的无效清扫。
3. 把污渍坐标变成清洁指令:相机标定与栅格地图的构建
3.1 像素坐标到光伏板物理坐标的单应性变换
检测模型输出的是图像里的像素坐标,清洁机器人要动的是光伏板平面上的物理坐标。两者之间用一个3x3的单应性矩阵H关联。常见安装方式是清洁机器人头部装朝下的广角相机,或者场站固定机位俯拍整块组件,无论哪种,第一步都是标定。
标定的本质是找图像平面到板面平面的投影关系。光伏板本身是一个平面,所以单应性矩阵完全够用。如果板面有弧度,单应性就不成立,得改成相机内参加外参的位姿估计,工程复杂度高一整个量级。
import cv2 import numpy as np # 图像上的四个角点(像素坐标) img_pts = np.array([[856, 633], [2350, 548], [1330, 1680], [130, 1490]], dtype=np.float32) # 对应的光伏板平面坐标(毫米) board_pts = np.array([[0, 0], [1200, 0], [0, 800], [1200, 800]], dtype=np.float32) H, status = cv2.findHomography(img_pts, board_pts, cv2.RANSAC, 5.0) pixel_point = np.array([[[640, 360]]], dtype=np.float32) world_point = cv2.perspectiveTransform(pixel_point, H) print("板面坐标(mm):", world_point[0][0])findHomography用RANSAC剔除外点,5.0是重投影误差阈值,单位是像素。阈值调大能容忍更多误匹配,但坐标换算精度下降。标定时至少取6到8组点对做RANSAC估计,比4组点更稳。这里有一个我在现场踩过好几次的坑:标定板必须放在光伏板平面上,而不是地面上。光伏板本身有倾角,地面标定的H矩阵最多只能保证机器人走到大致位置,误差累计起来会偏出五六厘米。
3.2 把光伏板切成栅格:分辨率与代价地图
路径规划通常不在连续坐标上做,而是在栅格地图上做,因为栅格能把“能不能走、脏不脏”这些信息直接编码成代价。栅格分辨率直接决定规划效果和计算量:5mm一格,一块2米乘1米的板就要8万个格子,A*搜索明显变慢;50mm一格又太粗,机器人对清扫宽度的感知不足,路径容易压到边缘。
我一般用10mm到20mm的栅格,具体看机器人最小转弯半径。栅格值0表示可通行,1表示障碍(边框、支架),0.5表示轻度污染,0.8表示重度污染。把YOLOv11的检测框映射到栅格时,检测框中心点对应的格子被标记为对应污染等级,同一格被多个检测框覆盖时取最大值。
import numpy as np grid_res = 0.02 # 20mm每格 board_width = 2000 board_height = 1000 grid_w = int(board_width / grid_res) grid_h = int(board_height / grid_res) costmap = np.zeros((grid_h, grid_w), dtype=np.float32) detections = [ {"cx_mm": 500, "cy_mm": 300, "w_mm": 180, "h_mm": 120, "conf": 0.83}, ] for det in detections: cx = int(det["cx_mm"] / grid_res) cy = int(det["cy_mm"] / grid_res) w = max(1, int(det["w_mm"] / grid_res)) h = max(1, int(det["h_mm"] / grid_res)) x0 = max(0, cx - w // 2) y0 = max(0, cy - h // 2) cost = 0.8 if det["conf"] > 0.7 else 0.5 costmap[y0:y0+h, x0:x0+w] = np.maximum(costmap[y0:y0+h, x0:x0+w], cost)np.maximum保证重叠区域取最高污染等级,max(0, ...)做边界裁剪,防止检测框靠近板边缘时索引越界。这个代价地图就是后续路径规划的“黑匣子”,里面既有障碍信息又有脏污程度,规划器读它决定怎么走。
3.3 覆盖优先还是能耗优先:清洁策略的选型逻辑
有了代价地图,路径规划要回答“怎么扫”。两种典型策略:覆盖优先和能耗优先。
覆盖优先适合污渍面积占比大、组件已经积灰已久的场景,机器人沿弓字形走完全部清扫带,保证每寸板面都被刷到。能耗优先适合日常保洁型场景,污渍点稀疏,机器人只需要做点到点的最短路径规划,本质上是一个带避障的旅行商问题。
很多团队一上来就做全覆盖路径规划,结果日常清洁时机器人绕一大圈,电耗翻倍。我的习惯是让业务目标决定策略,而不是让算法决定业务。对光伏组件来说,通常做法是“日常覆盖加重点二次清扫”:先用覆盖路径保证基本清洁度,再用局部螺旋路径处理重污渍区。这样既不会漏扫,也不会在脏污稀疏时白白浪费电量。
4. 清洁机器人的路径规划实现:弓字形覆盖、螺旋清扫与避障
4.1 弓字形覆盖路径的生成逻辑和参数
弓字形路径(Boustrophedon)是覆盖式清扫最常见的基础路径。核心思路:沿一个方向直线扫过去,到头后垂直平移一个清扫宽度,再反向扫回来,所有区域都被覆盖且转弯最少。
def generate_boustrophedon(width_m, height_m, sweep_m): paths = [] y = 0.0 direction = 1 while y < height_m: if direction == 1: paths.append([(0, y), (width_m, y)]) else: paths.append([(width_m, y), (0, y)]) y += sweep_m direction *= -1 return paths waypoints = generate_boustrophedon(2.0, 1.0, 0.45) for row in waypoints: print(row)sweep_m=0.45是有效清扫宽度,我一般取毛刷物理宽度的80%,这样相邻两次清扫之间至少有20%重叠,避免漏扫。这个参数是整条路径规划里最值得调的:设大了省时间但可能漏扫,设小了清洁时间长但覆盖密。对2米乘1米的板,45cm有效宽度会生成三行路径,机器人沿这三条线走完,整块板面全部覆盖。
生成的路径点送到机器人的底层运动控制模块,按直线段依次执行。弓字形逻辑简单,现场调试时不容易出问题,这也是很多清洁机器人厂商把它作为默认方案的原因。
4.2 污渍密集区的局部螺旋清扫:参数怎么定
覆盖路径保证的是“都扫到”,但重度污渍一次扫不干净。鸟粪堆积或油污这类目标,检测置信度通常很高,我拿0.7做分界:大于0.7的污渍区在覆盖路径走完后切入一个局部螺旋清扫子程序,以污渍框中心为圆心,生成一条半径略大于污渍框尺寸的螺旋路径。
import math def generate_spiral(center_x_m, center_y_m, max_radius_m, pitch_m=0.05): points = [] theta = 0.0 r = 0.0 while r < max_radius_m: r = pitch_m * theta / (2 * math.pi) x = center_x_m + r * math.cos(theta) y = center_y_m + r * math.sin(theta) points.append((x, y)) theta += 0.05 return points spiral = generate_spiral(0.8, 0.5, 0.15)pitch_m=0.05是螺旋线的间距,每转一圈半径增加5cm,这个值必须小于毛刷有效清扫宽度才能保证重叠。max_radius_m=0.15是螺旋清扫范围半径,重度污渍检测框的宽高通常不会太大,0.15m足够覆盖。theta += 0.05是角度步长,步长越小路径点越密、运动越平滑,但点太多会加重底层控制器的计算负担。中度污渍(置信度0.4到0.7)不需要单独走螺旋,覆盖路径通过时把行进速度降下来,让毛刷多接触一会儿即可。
4.3 动态避障与路径重规划:高代价区域怎么融合
场内可能有杂草、石头、检修工具这些障碍物,路径规划不能只离线生成一条弓字形路径,还得在行进中实时避障。常见做法是把A*搜索放在代价地图上,如果前方被障碍物挡住,就触发局部重规划。
动态避障小车路径规划在ROS2生态里用得最多的是Nav2,我的做法是把YOLOv11的检测结果作为“动态高代价区域”融合进代价地图。机器人接近重度污渍区时会减速,接近真正的障碍物时会绕行,而不是把污渍当障碍直接绕开。
import numpy as np def merge_costmap(base_grid, stain_grid, obstacle_grid): # 障碍物区域保持1.0,不可通行 combined = np.maximum(base_grid, obstacle_grid) # 污渍区域叠加为高代价,但不会被当成硬障碍 combined = np.maximum(combined, stain_grid * 0.8) return combined这段代码的逻辑是分层融合:障碍物格的代价是1.0,A*直接视为不可通行;污渍格的代价是0.8,规划器能走但会优先绕行。实际接入Nav2时,把这张合并后的地图发布给代价地图层,全局规划器就会在避开障碍物的同时把重污渍区视为高代价区域。如果你用的是自己的规划框架,思路完全一样,只是数据接口不同。
4.4 A与混合A:什么时候换算法
弓字形覆盖路径和螺旋清扫都不需要A参与,因为它们本身就是确定性的几何路径。但“从当前位置移动到下一个污渍点”这类点到点任务,A是首选。光伏板场景的地图通常不算大,A在20mm栅格下完全够用,不需要上RRT或混合A这些更复杂的算法。
混合A适合的是带阿克曼转向模型的车辆,转弯半径大,需要让路径满足运动学约束。清洁机器人如果是差速驱动,原地转弯很轻松,普通A就够;如果是类车底盘,最好直接用混合A*,否则规划出来的路径机器人根本转不过去。选型时先确认底盘模型,再选规划器,这个顺序不能反。
5. 光伏板污渍检测与路径规划的避坑:5个高频翻车现场
5.1 翻车现场一:换台机器后权重文件无法加载
现象:训练正常完成,把best.pt拷到另一台机器上推理时报RuntimeError: Unknown torch version,或者权重能加载但输出结果完全不对。
原因:训练机和推理机的ultralytics或torch版本不一致,序列化格式变了。
解决:训练和推理用同一个版本环境,锁进requirements.txt,部署时用pip install -r requirements.txt恢复。如果环境实在无法统一,在训练机上把权重导出为ONNX格式,用ONNX Runtime推理,直接绕开PyTorch版本问题:
model.export(format="onnx", imgsz=640, dynamic=True)dynamic=True让ONNX支持动态输入尺寸,现场换相机分辨率时不用重新导出。
5.2 翻车现场二:小目标漏检,实测检出率比验证集差很多
现象:验证集mAP50有0.9,现场一跑,小块鸟粪和远处灰尘大面积漏检。
原因:imgsz=640输入下,小污渍块只占几个像素,经过连续下采样后特征被丢掉。
解决:两条路。一是把imgsz改成960或1280,小目标保留的像素更多;二是做切片推理,把大图按网格切成小块,每块单独送进模型,再把结果拼回原图坐标。YOLOv11的小目标优化方向一般就是这两条路,调anchors没用,因为模型本身是anchor-free的。
5.3 翻车现场三:规划的路径穿过光伏板边缘
现象:机器人的路径从板面边缘穿出去,或者直接压过边框支架。
原因:栅格地图没有做障碍物膨胀,机器人尺寸没建模,规划器以为贴着障碍物走也没问题。
解决:按机器人半径做膨胀。常见做法是用scipy.ndimage.binary_dilation把障碍物栅格向外扩一圈:
from scipy.ndimage import binary_dilation robot_radius_cells = int(0.2 / grid_res) inflated = binary_dilation(costmap == 1, iterations=robot_radius_cells)0.2是机器人半径,单位米;grid_res是栅格分辨率。膨胀后的障碍区单独存一层,规划器就不会贴着边框走。这个参数设太小会刮擦组件,设太大又会让机器人离可清扫区域太远,建议按机器人实际外径的110%来取。
5.4 翻车现场四:同一块污渍,上午检得到下午检不到
现象:模型在早晚表现稳定,中午强光下漏检率飙升,同一块鸟粪时有时无。
原因:训练数据集中在少数时间段,模型学到的是“特定光照下的纹理”,而不是污渍本身。
解决:训练集补入不同日照角度的图片,同时做亮度增强:
import cv2 def augment_brightness(img): hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) hsv[:, :, 2] = cv2.add(hsv[:, :, 2], 30) return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)这个增强会让模型见过更宽的亮度范围,而不是依赖某个固定光照条件。现场如果中午强光下仍然漏检,最直接的办法是给相机加偏振片,滤掉玻璃表面的反光,污渍纹理一下就清楚了。
5.5 翻车现场五:坐标误差累计,机器人扫偏了位置
现象:检测框明明标在污渍处,机器人走过去却偏了5到10厘米,螺旋清扫中心对不准。
原因:单应性矩阵是用地面上的标定板算的,光伏板有仰角,标定平面和实际工作平面不重合;或者机器人运动时相机高度发生轻微变化。
解决:把标定点贴在光伏板表面,再算H矩阵。最好让机器人在每次开工前花10秒做一次在线标定,用固定在板面角落的标定点重新计算H,修正机械振动带来的漂移。这个习惯救了我很多次,坐标对齐的精度从厘米级直接回到毫米级。
6. 进阶:把YOLOv11检测结果做成清洁机器人的实时决策闭环
6.1 用置信度动态调整清扫动作
把YOLOv11输出的置信度直接当作“污渍严重程度”的代理指标,按阈值设定三档清洁策略:大于0.7的重度污渍,执行螺旋清扫;0.4到0.7的中度污渍,覆盖路径经过时减速慢扫;低于0.4的轻微污渍,正常速度带过。
if det.conf >= 0.7: follow_spiral(center, radius=det.w_mm * 0.8) elif det.conf >= 0.4: set_speed(0.3) else: set_speed(0.8)det.w_mm是检测框映射到物理坐标系后的宽度,螺旋清扫半径取宽度的0.8倍,保证覆盖范围完全包住污渍扩散区。这样检测结果直接变成执行参数,而不是只输出一个“有没有框”的信号。
6.2 导出TensorRT引擎,把推理时间压进实时区间
现场清洁机器人大多用嵌入式GPU,比如Jetson系列。PyTorch直接推理的延迟通常不够,建议用Ultralytics一键导出TensorRT引擎:
model.export(format="engine", imgsz=640, half=True, device=0)half=True表示FP16精度,显存占用和推理延迟都明显下降。导出后再加载.engine文件做推理,速度一般能到实时。这里有一件必须注意的事:TensorRT引擎和GPU型号强绑定,换一块卡就要重新导出,不能直接拷贝到别的机器上。
整条链路到这里已经打通:摄像头采集画面,YOLOv11输出检测框,单应性矩阵把像素坐标转成板面物理坐标,栅格代价地图记录污渍等级,弓字形覆盖路径打底,螺旋路径处理重度污渍,置信度反过来调节清扫强度。
我自己的习惯是每走一步都单独写验证脚本:检测模型单独看mAP,单应性标定单独量坐标误差,路径规划单独跑仿真。集成之后出问题,八成不是哪个模块的算法错了,而是坐标系没对齐或者阈值调得太随意。这个顺序能帮你少走很多弯路,希望帮到你。
本文还有配套的精品资源,点击获取