
做风电叶片缺陷检测项目的时候最让我头疼的不是模型选型而是找不到合适的数据。风力叶片缺陷图像识别这个方向模型结构和训练技巧大家都能在网上找到真正拉开差距的是无人机航拍巡检数据集的质量——样本能不能覆盖真实巡检环境、标注规不规范、坐标格式能不能直接喂给训练框架。后来我们团队整理了一套包含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格式XMLYOLO格式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_widthcy ((ymin ymax) / 2) / img_heightbox_w (xmax - xmin) / img_widthbox_h (ymax - ymin) / img_height反向换算就是把乘除换成乘回图片尺寸xmin (cx - box_w / 2) * img_widthymin (cy - box_h / 2) * img_heightxmax (cx box_w / 2) * img_widthymax (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, encodingutf-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到16epochs150左右配合早停防止过拟合optimizerAdamW初始学习率0.001权重衰减0.0005数据增强mosaic前80个epoch开启后30个epoch关闭训练命令大致是这样yolo detect train modelyolov8n.pt datawindblade.yaml imgsz1280 epochs150 batch8 optimizerAdamW lr00.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。风电叶片缺陷场景里我会分别统计每个类别的AP0.5和AP0.5:0.95尤其关注小目标AP_S。总mAP被样本量大的类别拉高后很容易掩盖雷击烧蚀这类关键缺陷的低识别率。项目验收标准也要按类别分别要求不能给一个笼统的总指标糊弄过去。5. 实测踩坑复盘数据划分、误检来源与针对性修正5.1 最大的坑同一叶片图像泄漏进验证集第一轮训练结束验证集mAP0.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%以上这样同一个缺陷会出现在多张连续图像里标注时可以挑清晰的框训练时可以天然获得多角度正样本。第二是标注前先让运维工程师圈定“哪些缺陷类型是必须修的”再利用这个清单反向定义标签体系这比先看数据集样图再猜类别要高效得多。风电叶片缺陷识别不是一锤子买卖数据、模型、现场反馈三者循环起来才能把检测系统真正落到巡检工单里。