做风电叶片缺陷检测项目的时候,最让我头疼的不是模型选型,而是找不到合适的数据。风力叶片缺陷图像识别这个方向,模型结构和训练技巧大家都能在网上找到,真正拉开差距的是无人机航拍巡检数据集的质量——样本能不能覆盖真实巡检环境、标注规不规范、坐标格式能不能直接喂给训练框架。后来我们团队整理了一套包含YOLO格式和VOC格式标注的发电机叶片缺陷识别数据集,内部项目代号取第10077期,从航拍图像整理、标签体系设计到模型训练验证,一整套流程走下来,才把检测方案从纸上思路变成能跑通的系统。这篇文章把数据集结构、格式转换、训练调优和踩坑复盘完整展开,正在做风机叶片巡检、工业视觉检测或者无人机图像AI分析的同行可以直接参考。
1. 叶片巡检场景:为什么数据集的定位直接决定模型上限
1.1 真实巡检里的缺陷长什么样,先搞清楚需求再谈算法
在接触风电巡检项目之前,我以为叶片缺陷检测和普通工业质检差不多:拍图、标注、训练、部署,一条通用流程走完。等真正拿到无人机航拍巡检的原始素材时,我才意识到问题远没有这么简单。风力发电机叶片长度从三十多米到一百米以上,主体是玻璃纤维增强复合材料或碳纤维复合材料,常年暴露在强风、沙尘、紫外线和雷击环境下。从高空视角看,叶片表面是一条顺势延伸的曲面,而裂纹、前缘腐蚀、雷击烧蚀这些缺陷往往只有指甲盖到手掌大小,和整片叶子的尺度完全不在一个数量级。
所谓发电机叶片缺陷识别,严格说起来叶片属于风轮组件而非发电机转子,但行业里口语早就习惯了“发电机叶片”这个叫法。真正重要的不是叫法,而是缺陷分类背后的运维语义。同样是表面异常,一条几毫米的表层裂纹可能只需登记观察,一条穿透蒙皮的深裂纹就可能触发停机和检修;前缘腐蚀如果扩展到大面积范围,直接关系到气动效率。正因为缺陷的尺寸、位置、性质决定了后续是“继续监测”“安排修补”还是“更换叶片”,算法输出的“这个区域有缺陷”才有实际价值。
人工巡检的问题在于成本和安全。传统方式要搭吊篮或者安排高空作业人员,一个机组停机检修的时间窗口有限,巡检员在高空凭肉眼和经验判断,漏检风险不低。无人机航拍巡检让人员在地面完成数据采集,单台无人机可以覆盖机组群叶片,效率明显提升,但代价是产生了大量图像数据。把这些图像全部交给人工看图,虽然比高空作业安全,疲劳程度和漏检率依然存在。所以“无人机采集 + 算法自动识别 + 人工复核”才成了风电运维数字化的主流路线,而算法效果的起点,正是训练用的图像数据集。
1.2 公开数据集不好用,自建样本库才是项目起点
做目标检测的人都知道,公开数据集丰富,但那是针对通用目标而言。风力叶片缺陷识别这种细分场景,公开样本量本就稀少,更麻烦的是标注口径不统一。有的数据集把裂纹、划痕、腐蚀混在一类里,有的把背景中的塔筒反光也标了进去,有的只给正样本不给负样本。模型在这样一套数据上训练,指标再漂亮,放到真实航拍场景里很容易产生让人哭笑不得的误检。
所以我在预研阶段就确定了一条原则:与其到处拼凑公开数据,不如认真整理一套贴合真实巡检工况的样本库。这套数据的归档编号是第10077期,团队内部习惯以“期”为单位管理数据集版本,一期对应一次完整的整理与校验流程。收集整理的目标很明确:覆盖不同光照、不同叶片涂装、不同缺陷类型、不同拍摄角度,按统一的类别体系完成标注,同时输出YOLO格式与VOC格式两套标准,方便不同训练框架直接使用。
后来事实证明这个决定是对的。同一个YOLO系列模型,用通用数据集预训练后在叶片缺陷样本上调优,和完全只用整理后的风电样本训练,在真实航拍测试集上的表现差距非常大。原因也简单:叶片表面纹理、前缘高光、蒙皮漆面颗粒,以及风电场常见的草地和海水背景,这些内容只有风电样本才能提供。数据集质量越高,模型的上限越高,后续所有训练技巧都是在这个基础上做文章。
2. 数据集构成拆解:采集规格、类别定义与两套标注格式
2.1 图像从哪里来:航拍采集与切片处理细节
第10077期数据的原始图像来自一次完整的风机叶片巡飞计划。无人机按规划航线沿着叶片长度方向飞行,采用航向重叠率较高的拍摄方式,保证同一片叶片的不同区段被相邻帧覆盖。工业级无人机的相机分辨率普遍在4000×3000像素级别,搭配云台稳定后,可以保证在十几米到几十米的拍摄距离下,叶片表面细节不至于完全糊掉。
这里有个关键细节:原始大图直接喂给检测模型并不合适。叶片在画面中的占比可能不到三分之一,背景的天空、草地、海面占了大量像素,直接训练会让模型花费大量计算资源去“认识”背景,而且缩小到训练输入尺寸后,叶片区域变得更小,缺陷更难看见。常见的做法是先把原始图像按叶片位置裁切成若干子图,每张子图尽量让单个叶片段占据主体位置,同时保留足够的周围纹理用于区分上下文。
子图是否保留原始分辨率,取决于显存和训练尺寸的设置。我通常保留在2000像素级别,训练时再按目标检测框架的输入尺寸缩放。图像整理时还要考虑样本多样性:白色涂装叶片和灰色涂装叶片在缺陷表现上完全不同,晴天顺光、逆光、阴天漫射光都会影响缺陷的可见性。数据集里需要刻意保留这些差异,而不是只挑“清晰好看”的图。因为检测模型要在真实巡检中落地,遇到的反光、阴影、雾气情况远比“标准图”复杂。
2.2 缺陷类别怎么分:从运维动作反推标签体系
标签体系不是拍脑袋定的,我做了一次运维访谈式的需求梳理:先请运维工程师列出日常巡检需要关注的叶片表面异常,再逐条确认哪些异常会触发维修工单、哪些只需要记录观察。以此为据,我们把数据集中的缺陷归纳为六类。
| 类别标识 | 缺陷类型 | 典型形态 | 主要关注点 |
|---|---|---|---|
| crack | 裂纹 | 细线状或树枝状纹路,方向不一 | 裂纹深度与长度决定维修级别 |
| ero | 前缘腐蚀 | 叶片前缘区域的砂眼、凹坑、麻面 | 气动效率下降的主要来源 |
| lt | 雷击烧蚀 | 不规则黑斑、碳化痕迹 | 可能伴随结构分层 |
| del | 分层鼓包 | 蒙皮下鼓起、边缘翘起 | 结构强度风险高 |
| psp | 漆面剥落 | 漆皮脱落、颜色差异 | 早期预警信号 |
| oil | 污渍油渍 | 油脂、灰泥覆盖 | 干扰视觉判断,需与渗漏区分 |
这里想多说一句易混淆类的处理。雷击烧蚀和油污在低分辨率图像上很像,都是深色痕迹;前缘腐蚀初始阶段和漆面磨损也很像。如果类别划分过细,标注一致性会变差,模型训练时类别之间的特征重叠会让边界更模糊。我们的处理原则是“先保证可区分性,再追求细粒度”:雷击烧蚀合并了轻微碳化和严重分层,统一标记为lt;油污单独保留,因为它对后续维护策略有完全不同的含义。这个取舍不一定适合所有项目,但至少让标注人员不会产生“看着像a又像b”的纠结。
2.3 YOLO格式和VOC格式到底差在哪
这套数据集同时提供两种标注格式,很多人会问有没有必要。先说结论:有。YOLO系列框架原生读取txt标注,训练配置最省事;而很多开源标注工具、辅助标注脚本、评估工具默认导出VOC风格的XML文件,或者需要从XML解析出目标信息。手里有两套格式,相当于同时兼容了两个生态,省去来回转换的时间。
从数据形态看,两种格式的核心差异是坐标体系和类别表示方式。
| 对比项 | VOC格式(XML) | YOLO格式(txt) |
|---|---|---|
| 文件内容 | 图片信息 + 目标框 + 类别名 | 每行一个目标框 + 类别索引 |
| 坐标表示 | 绝对像素坐标 xmin/ymin/xmax/ymax | 归一化坐标 中心点x/y + 宽高 |
| 尺度依赖 | 依赖图片原始尺寸 | 与图片尺寸无关 |
| 类别表示 | 字符串类别名 | 从0开始的整数索引 |
| 读取工具 | 通用标注工具、XML解析器 | YOLO系训练框架 |
我遇到过不止一次有人拿着VOC标注文件直接喂给YOLO系框架,结果训练报错“label index out of range”的情况。这通常不是数据坏了,而是没有把XML里的类别名正确映射成txt里的数字索引。下一节就把这套换算逻辑和转换代码完整写出来。
3. 格式转换实操:VOC的XML与YOLO的txt互转代码
3.1 坐标换算的数学逻辑
坐标换算其实就两步:把绝对像素坐标变成相对坐标,再根据是否需要归一化决定是否除以图片宽高。VOC里一个目标框是xmin、ymin、xmax、ymax,单位是像素;YOLO里是cx、cy、w、h,单位是归一化比例,取值在0到1之间。换算公式如下:
- cx = ((xmin + xmax) / 2) / img_width
- cy = ((ymin + ymax) / 2) / img_height
- box_w = (xmax - xmin) / img_width
- box_h = (ymax - ymin) / img_height
反向换算就是把乘除换成乘回图片尺寸:
- xmin = (cx - box_w / 2) * img_width
- ymin = (cy - box_h / 2) * img_height
- xmax = (cx + box_w / 2) * img_width
- ymax = (cy + box_h / 2) * img_height
这里要注意边界处理。有些标注工具允许框边缘略微超出图片边界,转换前先做截断。YOLO的txt如果出现小于0或大于1的坐标值,训练时可能报错或引入无意义的边界框。所以我会在转换函数里对xmin、ymin、xmax、ymax先做clip,再计算归一化值,并且对最终cx、cy、bw、bh也做一次clip到0到1区间的保护。
还有一点容易被忽略:坐标系原点。图像标注一般基于左上角原点坐标系,从左上角向右为x正方向、向下为y正方向。如果数据来自某些特殊制作工具,或者后续要和遥感影像融合,就需要先确认坐标系定义,不然画框位置会整体偏移甚至上下翻转。
3.2 一个可直接复用的转换脚本
下面这个脚本可以把VOC的XML标注批量转成YOLO的txt标注。核心思路是遍历XML文件里的每个object,读取类别名和bndbox坐标,按上一节的公式换算后写入txt。
import os import xml.etree.ElementTree as ET # 类别清单,顺序必须与训练时的data.yaml一致 CLASS_NAMES = ["crack", "ero", "lt", "del", "psp", "oil"] def voc_to_yolo(xml_path, txt_path, img_width, img_height): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter("object"): name = obj.findtext("name") if name not in CLASS_NAMES: continue class_id = CLASS_NAMES.index(name) bndbox = obj.find("bndbox") xmin = float(bndbox.findtext("xmin")) ymin = float(bndbox.findtext("ymin")) xmax = float(bndbox.findtext("xmax")) ymax = float(bndbox.findtext("ymax")) # 边界保护,避免超出图像范围 xmin = max(0, min(xmin, img_width - 1)) ymin = max(0, min(ymin, img_height - 1)) xmax = max(0, min(xmax, img_width - 1)) ymax = max(0, min(ymax, img_height - 1)) if xmax <= xmin or ymax <= ymin: continue cx = ((xmin + xmax) / 2.0) / img_width cy = ((ymin + ymax) / 2.0) / img_height bw = (xmax - xmin) / img_width bh = (ymax - ymin) / img_height cx = max(0.0, min(cx, 1.0)) cy = max(0.0, min(cy, 1.0)) bw = max(0.0, min(bw, 1.0)) bh = max(0.0, min(bh, 1.0)) lines.append(f"{class_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open(txt_path, "w", encoding="utf-8") as f: f.write("\n".join(lines))反向转换的逻辑也一样,只是把公式反过来,输出XML结构体。常用到的场景是把YOLO标签导回标注工具复查,或者给不支持txt格式的检测框架使用。
def yolo_to_voc(txt_line, img_width, img_height, class_names): parts = txt_line.strip().split() class_id = int(parts[0]) cx = float(parts[1]) cy = float(parts[2]) bw = float(parts[3]) bh = float(parts[4]) xmin = (cx - bw / 2.0) * img_width ymin = (cy - bh / 2.0) * img_height xmax = (cx + bw / 2.0) * img_width ymax = (cy + bh / 2.0) * img_height # 这里只做基础数值计算,实际写入XML需要自行构造Element return class_names[class_id], xmin, ymin, xmax, ymax脚本里有一个很容易忽略的地方:img_width和img_height必须和XML对应的原图完全一致。我的建议是转换前用图像库直接读取尺寸,不要从XML的size节点抄,也不要在代码里写死,避免批处理时某张图尺寸不一样导致所有标注偏移。
3.3 类别映射是本转换里最容易忽略的坑
YOLO的txt用数字索引,VOC的XML用类别名。一旦classes.txt中的类别顺序和转换代码中的CLASS_NAMES列表不一致,轻则标签错位,重则把“雷击烧蚀”训练成“油污”。转换前先把类别清单固定成一份,所有脚本读取同一份classes.txt,而不是在代码里写死多份列表。
另外,如果XML里出现未登记类别,比如标注人员手误写成“cracks”而不是“crack”,转换脚本默认跳过,这会导致目标丢失但没有任何提示。更稳妥的做法是在跳过时打印警告,并统计每个XML里被跳过的目标数量,方便批量清理。
转换完成后的第一件事永远是可视化校验。用OpenCV在图像上把txt标注框画出来,抽查几十张图,确认框贴合目标、类别正确。这个过程花不了十分钟,能省下后面几小时的排查时间。肉眼都看不下去的标注,模型不会学得好。
4. 用这套数据训练检测模型的调优记录
4.1 模型和训练参数怎么选
我们的目标部署环境包括工作站和边缘设备,所以模型不能一味求大。从YOLOv8n起步,先用默认配置跑通流程,再逐步换到s、m看收益。这里给出实际训练中比较稳定的一套参数。
- 输入尺寸:1280×1280。叶片缺陷是小目标,640尺寸下很多裂纹只剩几个像素,无法有效学习
- batch size:根据显卡显存调整,20GB显存跑YOLOv8s大约8到16
- epochs:150左右,配合早停防止过拟合
- optimizer:AdamW,初始学习率0.001,权重衰减0.0005
- 数据增强:mosaic前80个epoch开启,后30个epoch关闭
训练命令大致是这样:
yolo detect train model=yolov8n.pt data=windblade.yaml imgsz=1280 epochs=150 batch=8 optimizer=AdamW lr0=0.001为什么要强调输入尺寸?叶片缺陷检测的典型难点就是目标尺度小。在4000像素宽的原图里,一个裂纹框可能只有几十像素宽,缩放到640输入尺寸以后,小缺陷几乎消失,模型根本学不到。把训练尺寸提高到1280后,小目标AP的提升非常明显。显存不够时,宁可使用切片训练,也不要把全局分辨率压得太低。
4.2 小目标缺陷的训练对策:切片推理和多尺度测试
训练时提高分辨率能改善小目标感知,但推理阶段如果仍要处理4000像素的原始大图,显存和耗时都会增加。这里推荐切片推理:把大图按1280×1280的窗口切割,相邻切片重叠10%左右,分别送入模型,再把检测结果映射回原图坐标。切片推理也和数据集切分子图的过程互相呼应,模型在切片上训练,推理时切片尺度接近训练分布,不会因为图像缩放产生巨大的尺度漂移。
从实测结果看,对同一批真实航拍图,直接整图缩放推理的召回率大约在0.78,切片推理能到0.85左右,代价是推理时间增加两三倍。对于巡检任务来说,准确率优先级高于速度,这个代价完全可以接受。多尺度测试也能提点,但工业项目里往往不值得在生产环境长期开启,更适合作为离线评估参考。
另外提醒一点:切片推理后要做非极大值抑制去重。相邻切片重叠区域的同一缺陷会被检测两次,坐标映射回原图后直接按原始置信度做NMS,阈值设在0.45左右比较合适。
4.3 类别不平衡、难例处理和评价指标
缺陷类别天然不平衡。前缘腐蚀样本最多,雷击烧蚀和分层鼓包样本较少。如果不做任何处理,模型会把稀疏类别学成一个“从未见过的类别”,推理时几乎全部漏检。我采用的组合方案有三个:
一是类别加权损失,给稀疏类别更高的loss权重,通常在1.5到2.5之间;二是copy-paste增强,把稀疏类别的目标贴到其他图像上,同时修正标注框;三是困难样本挖掘,每一轮训练结束后挑出置信度低但实际有标注的样本,下个epoch重复采样。
评估指标不能只看总mAP。风电叶片缺陷场景里,我会分别统计每个类别的AP@0.5和AP@0.5:0.95,尤其关注小目标AP_S。总mAP被样本量大的类别拉高后,很容易掩盖雷击烧蚀这类关键缺陷的低识别率。项目验收标准也要按类别分别要求,不能给一个笼统的总指标糊弄过去。
5. 实测踩坑复盘:数据划分、误检来源与针对性修正
5.1 最大的坑:同一叶片图像泄漏进验证集
第一轮训练结束,验证集mAP@0.5达到0.93,我很兴奋,觉得模型已经可以用了。结果拿到一批真实的航拍图上测试,mAP直接掉到0.7左右。我反复检查代码和数据,最后发现问题出在数据集划分方式上:原始数据是按“图片”随机划分的,而航拍巡检中对同一叶片连续拍摄了几十张高度相似的图像,这些图像里的一部分进了训练集,另一部分进了验证集。模型在验证集上“见过”了几乎一样的叶片区域,指标自然虚高。
这也解释了为什么新场景表现会暴跌:模型学到的其实是特定叶片纹理、特定光照条件的“记忆”,而不是泛化到缺陷形态本身的能力。修正方法很简单,按叶片个体分组划分数据集。先统计每张图像属于哪个叶片、哪一段,保证同一个叶片的所有图像全部进入同一侧。我当时用了一个很简单的字段:文件名前缀提取叶片编号,再按前缀分组划分。
这里再提醒一句,如果数据来自多个风电场,划分时还应尽量按风电场或航次分组,避免位置分布泄漏。这个坑属于典型的“指标骗人”问题,尤其容易出现在航拍连续帧类数据集上,越像真数据的场景越容易忽略。
5.2 误检来自哪里:高光、露珠、背景杂物
真实航拍场景中最常见的误检来源不是模型结构,而是光照和背景干扰。叶片前缘在顺光条件下经常出现一条狭窄的亮带,模型很容易把它当成裂纹边界;露珠和雨水在叶片表面聚集成点串状,和漆面剥落的视觉特征非常接近;背景里的塔筒、缆绳、草地纹理也会被识别成异常区域。
针对这些问题,我给训练集补充了负样本批次:只包含正常叶片和复杂背景、没有标注框的图像,在训练配置里把这些图像以负样本形式加入。同时在后处理阶段增加了一条规则,对检测框覆盖区域的亮度梯度做统计,高光区域往往伴随高饱和度和低纹理复杂度,可以据此过滤一部分误检。
更系统的做法是引入多模态信息,比如可见光图像叠加红外热像图。雷击烧蚀和分层区域在热像上会有温度异常,与高光反射完全不同。如果条件允许,这是提升准确率的最佳路径之一。我当时的项目里只在部分机组验证了红外叠加方案,检测置信度比纯可见光高出不少,但采集成本也明显增加。
5.3 漏检的细节丢失问题
漏检的情况则更多出在尺度处理上。前缘腐蚀初期缺陷很小,漆面剥落也只呈现为细微的颜色差异,原图能看清,缩放后特征被抹平。处理方式是前面提到的切片推理加高分辨率训练,但在实际项目中我发现还有一个更隐蔽的问题:训练集里“清晰小缺陷”和“模糊小缺陷”被同等对待,模型会倾向于学习前者,对运动模糊、对焦不实的小缺陷完全无感。
于是我们专门收集了一批轻微模糊的缺陷图像,把它们放入训练集,并给这类样本设置较低的loss权重,模型反而更容易学会聚焦真正的结构特征。这个反直觉的操作效果不错,从侧面说明数据集不只是“越多越好”,质量构成比总量更重要。
另外,漏检还有一类是“标注本身就漏了”。无人机航拍图像里部分缺陷隐藏在高光区域或叶片边缘,标注人员可能没看到,导致这些目标被当作背景。解决办法是定期抽检标注质量,让两位标注人员独立标注同一批次,计算标注框重叠度,低于阈值的目标重新审核。数据集的错误标签对模型的影响,往往比想象中更大。
6. 从缺陷检测走向运维闭环:这套数据的下一步扩展
6.1 检测到分级:让算法输出可执行的运维建议
缺陷检测只是第一步。运维工程师真正需要的是“这个缺陷该不该修、什么时候修”。在第10077期数据的基础上,我的下一步规划是加入缺陷分级标签。
分级规则来源于运维手册和工程经验,比如:裂纹长度小于20毫米且位于非承力区域时标记为“观察级”,大于50毫米或位于主梁区域时标记为“检修级”;前缘腐蚀覆盖面积超过前缘某一段的30%时进入“计划维修”。实现上可以有两种路径:一种是在YOLO检测框基础上增加一个分类头,另一个是单独训练一个分类网络,输入检测框裁剪后的图像,输出缺陷等级。
这个扩展会让数据集价值翻倍。原来模型回答“哪里有异常”,现在回答“异常有多严重、该采取什么动作”,对接工单系统时信息密度完全不同。同时,结合无人机航拍的位置信息,可以把检测结果映射到叶片的三维位置,生成缺陷分布图,为每次停机检修窗口提前制定维修计划。
6.2 半自动标注与数据滚动迭代
数据永远是活水。第10077期数据集成型后,我们开始用训练好的模型做半自动标注:对新增航拍图先跑一遍推理,生成伪标签,再由标注人员修正微调。相比从零标注,效率能提高两倍以上,标注一致性也更好。
关键是要建立数据反馈闭环。每次真实巡检中,模型产生的误检和漏检结果都应该回灌到训练集,特别是漏检,人工复看后确认的真实缺陷样本是最珍贵的难例。数据集版本要像代码一样做版本管理,建议把图像、标注、类别清单、划分规则、训练配置绑定成同一个版本号,方便复现实验和追溯问题。
我给这套数据建立了简单的版本字段,后续已经演进了V1.1和V2.0两个版本。做这个工作的过程中,我深刻体会到:一类场景的数据集建设,本质上是在积累对这个行业故障模式的理解。初期需要靠人工一点点抠,后面模型和人的协作会让迭代越来越快。
最后分享两个实操细节。第一是航拍采集阶段,巡飞航线尽量保证同一区段的航向重叠率在60%以上,这样同一个缺陷会出现在多张连续图像里,标注时可以挑清晰的框,训练时可以天然获得多角度正样本。第二是标注前先让运维工程师圈定“哪些缺陷类型是必须修的”,再利用这个清单反向定义标签体系,这比先看数据集样图再猜类别要高效得多。风电叶片缺陷识别不是一锤子买卖,数据、模型、现场反馈三者循环起来,才能把检测系统真正落到巡检工单里。