简介:面向风力发电智能运维与缺陷检测场景,这份数据集包含1162张涡轮机叶片图像,其中训练集1017张、验证集97张、测试集48张,覆盖裂纹、侵蚀、机械损伤、油漆脱落、划痕及正常状态六类样本,采用YOLO格式多边形点坐标精细标注,可直接用于实例分割模型的训练、验证与测试。压缩包共2000个文件,836张jpg原图与1162个txt标注文件一一对应,另附1个yaml配置和1份说明文档,整体仅37.56MB,轻量易集成,方便直接接入YOLOv8等主流框架。目前已有429人学习/下载,适合人工智能研究者、能源行业工程师、风场运维人员及培训机构学员,用于叶片缺陷识别、状态评估与自动巡检系统开发。样本源自真实风电检修场景,多类别覆盖贴合实际工况,能有效降低采集与标注成本;同时支持从目标检测到实例分割的扩展任务,既可用于学术算法研究,也可为风机预防性维护和故障预警提供可靠数据支撑。
1. 风力叶片缺陷数据集:一只zip决定模型上限
风力涡轮机叶片缺陷实例分割数据集,听起来就是一个普通的zip压缩包,但它往往决定了整套视觉方案的上限。叶片表面的缺陷,从毫米级的疲劳裂纹,到一米长的前缘腐蚀,在无人机巡检图像里尺度跨度极大。而目标检测的矩形框既圈不齐裂纹的走向,又会在框里混进大量非缺陷纹理,漏检和误检此起彼伏。实例分割因为输出的是像素级轮廓,能把缺陷边界吃得更准,这就是所谓“检测 + 分割”联合框架的价值所在。这个zip数据集,就是训练这类模型需要的原料。它值不值得整理、怎么解包和转格式、训练时机器要留多少余量,正是下面要展开的内容。适合正在做风电巡检算法落地的工程师、无人机视觉团队,以及想用现成数据集快速起一个实例分割模型的从业者。
2. 拆开zip先看懂标注:目录结构、格式与设备来源
拿到这类zip,先别急着解压后直接开训练脚本。第一步是搞清楚数据集的内部组织方式,这决定了后面每一行转换代码怎么写。
2.1 用目录树快速分辨三种打包形态
风力风电巡检的缺陷实例分割数据集,我见过三种常见打包习惯:COCO风格,annotations/train.json和images/train/分开存放;YOLO风格,images/train/与labels/train/成对出现,每张图对应一个txt;还有一种带masks/目录的,灰度PNG掩码单独放,需要自己写加载器去对齐原图。三种形态没有绝对好坏,但后续处理路径差别很大。
先看顶层的目录结构,用一条命令就够了:
unzip -l blade_defect.zip | head -40 # 或者完整解压后 tree -L 2 ./blade_defect/unzip -l只列出压缩包内容不实际解压,head -40截取前40行足够判断风格。需要注意有些打包者会加一层顶层文件夹,解压后路径整体深一层,后面所有脚本的相对路径都要先剥掉这层目录,否则跨机器拷贝时容易踩路径坑。YOLO格式离训练输入最近,但训练前无法直接做可视化审查——txt里坐标多一个零少一个零点一都看不出来;COCO JSON至少还能用程序批量还原出mask叠在原图上人工检查。所以我更习惯优先从JSON或mask形态入手看数据。
2.2 用pycocotools检查polygon与RLE标注
拆开JSON后第一件事,不是数有多少张图,而是确认segmentation字段用的是polygon还是RLE。COCO JSON里每个annotation的segmentation字段,要么是polygon(二维list,第一层是轮廓编号,第二层是x,y交替坐标点),要么是RLE(dict,包含size和counts两个key)。polygon直观、好编辑;但高分辨率叶片图像标注量极大,很多标注工具导出RLE来节省空间。这两条分支在转换时处理方式完全不同。
用下面这段脚本摸清底细:
import json from collections import Counter with open("annotations/train.json", "r", encoding="utf-8") as f: coco = json.load(f) print("图片数:", len(coco["images"])) print("标注框数:", len(coco["annotations"])) print("类别:", [(c["id"], c["name"]) for c in coco["categories"]]) seg_types = Counter() for ann in coco["annotations"]: if isinstance(ann["segmentation"], list): seg_types["polygon"] += 1 else: seg_types["rle"] += 1 print("分割标注类型分布:", dict(seg_types))参数说明:coco["categories"]里通常能看到缺陷类别体系,比如开裂、前缘腐蚀、涂层剥落、雷击损伤,但也有不少数据集只做二分类“缺陷/正常”。isinstance(ann["segmentation"], list)是区分polygon与RLE最稳的判断方式,少用type()去匹配,避免numpy数组类型干扰。如果输出显示大量RLE,说明数据集做了压缩,转训练格式前必须先解码。快速试一条RLE的还原方式:
from pycocotools import mask as mask_utils ann = coco["annotations"][0] if isinstance(ann["segmentation"], dict): m = mask_utils.decode(ann["segmentation"]) print("mask shape:", m.shape, "面积:", int(m.sum()))mask_utils.decode返回0/1二值mask,维度与对应原图一致。COCO里有少数标注用UncompressedRLE列表同时表示多个不相连区域,需要先用mask_utils.frPyObjects合并,这一块我在第3章转换脚本里还会再遇到。
2.3 类别与设备来源决定训练策略:先看面积和宽高比
类别名只是第一步,更关键的是判断图像来源。无人机吊舱相机拍的远景叶片,和近景手持相机拍的叶片,缺陷形态一样,尺度差异却很大:远景里叶片占整图的五分之一,裂纹只占几十像素;近景里裂纹能占上千像素。两类图混在一个数据集里训练,实例分割的mask收敛往往被远景小mask拖累。我先统计每个bbox的面积和宽高比分布,把数据形态量化出来:
import numpy as np boxes = np.array([ann["bbox"] for ann in coco["annotations"]]) areas = boxes[:, 2] * boxes[:, 3] wh_ratio = boxes[:, 2] / (boxes[:, 3] + 1e-6) print("框面积 中位数/90分位:", np.percentile(areas, [50, 90])) print("宽高比 > 4 的占比:", (wh_ratio > 4).mean())如果宽高比大于4的占比很高,说明长条形缺陷不少,这类对象特别依赖mask分支对细长形态的建模能力,训练时要配合更大的输入分辨率。这个观察步骤和CCPD、HRSC2016这类高分辨率/旋转目标数据集的处理思路一致:数据形态决定模型策略。虽然叶片缺陷用旋转框并不合适,但MMRotate训练DOTA数据集时那种按图像块切分再聚合的思路,在叶片全景图上是可以借鉴的。
2.4 为什么这份数据必须走实例分割而不是目标检测
风力叶片缺陷实例分割之所以要落在“实例分割”而不是目标检测,根源在缺陷形态。前缘腐蚀、涂层剥落连成一片不规则长条,矩形框会框进大量正常纹理;疲劳裂纹又细又长,矩形框的长宽比可能到十倍,框内背景特征会把裂纹边缘信息淹没。实例分割输出逐像素掩码,训练时模型同时学“框住什么”和“边界在哪”,这正是“检测 + 分割”联合框架在工程上的落地。我见过不少项目组先用检测模型跑通,评估时发现漏检集中在长条缺陷上,才回头换分割模型,培训标注和转换数据的成本相当于重复一遍——数据从一开始就用分割标注,是这里最省事的选择。
3. 格式转换:把不规则标注转成YOLO训练格式
3.1 为什么要统一到COCO再转YOLO
原始标注可能是polygon、RLE或PNG mask,而YOLO实例分割训练要求每张图对应一个同名txt,每行格式是class_id x1 y1 x2 y2 ... xn yn,坐标全部0-1归一化。直接对着txt训练虽然省一步,但有两个硬伤:训练前无法批量可视化审查,txt里某个坐标多个小数点都看不出来;不同来源数据集的polygon、RLE、mask要分别写转换脚本,统一到COCO JSON后,转任何训练框架(Detectron2、MMDetection、Ultralytics)都能复用同一套中间格式。所以我的流水线固定为:检查原始格式 → 统一转COCO JSON → 再由COCO JSON转YOLO txt。
3.2 转换脚本的polygon与RLE两条分支
核心转换脚本如下,两条分支都落在同一个输出格式上:
import json import os from collections import defaultdict import cv2 import numpy as np from pycocotools import mask as mask_utils def polygon_to_norm_polygon(seg, img_w, img_h): xs = seg[0][0::2] # 偶数位是x ys = seg[0][1::2] # 奇数位是y coords = [f"{x / img_w} {y / img_h}" for x, y in zip(xs, ys)] return " ".join(coords) def rle_to_norm_polygon(rle, img_w, img_h): m = mask_utils.decode(rle) # RLE -> 二值mask contours, _ = cv2.findContours( m.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) max_contour = max(contours, key=len) pts = max_contour.reshape(-1, 2).astype(float) pts[:, 0] /= img_w pts[:, 1] /= img_h return " ".join(f"{x:.6f} {y:.6f}" for x, y in pts) def coco_to_yolo(coco_json, out_label_dir): with open(coco_json, "r", encoding="utf-8") as f: coco = json.load(f) id2file = {img["id"]: img["file_name"] for img in coco["images"]} img_size = {img["id"]: (img["width"], img["height"]) for img in coco["images"]} anns_by_img = defaultdict(list) for ann in coco["annotations"]: anns_by_img[ann["image_id"]].append(ann) os.makedirs(out_label_dir, exist_ok=True) for img_id, anns in anns_by_img.items(): w, h = img_size[img_id] stem = id2file[img_id].rsplit(".", 1)[0] lines = [] for ann in anns: cat = ann["category_id"] seg = ann["segmentation"] if isinstance(seg, dict): lines.append(f"{cat} {rle_to_norm_polygon(seg, w, h)}") else: lines.append(f"{cat} {polygon_to_norm_polygon(seg, w, h)}") with open(os.path.join(out_label_dir, stem + ".txt"), "w") as f: f.write("\n".join(lines))逻辑说明:先把COCO的images索引成文件名和尺寸,再按image_id聚合标注。写入txt时class_id直接用category_id,前提是后续data.yaml的names顺序和category_id一一对应。polygon分支里seg[0][0::2]取第一个轮廓的所有x坐标,YOLO实例分割只支持单轮廓环绕,所以默认取第一个多边形。RLE分支先用mask_utils.decode还原二值mask,再交给cv2.findContours取最长外轮廓。CHAIN_APPROX_SIMPLE会压缩直线段上的冗余点,高分辨率叶片标注一个mask上千点,压缩后通常只剩几百点,txt体积能小不少。
参数说明:cv2.RETR_EXTERNAL只取外轮廓,避免RLE里包含内环把多边形坐标画乱;max(contours, key=len)取点数最多的轮廓,多数情况下这就是主缺陷区域。但这里有个边界:如果一个annotation被RLE拆成两个互不相连的分量,这条分支会丢较小的分量。转换前最好先统计每个annotation的连通分量数量,遇到拆分就应各自生成新标注,否则训练时模型学到的轮廓是残缺的。
3.3 按类别组合切分训练/验证集
风力叶片数据集类别不平衡,而且一张图里常同时出现多种缺陷。如果只按图片做随机切分,可能出现某类缺陷在验证集里只有一两张,评估指标方差大到没法看。我按“类别组合分组”切分,保证训练集和验证集覆盖相同类别分布:
import random from collections import defaultdict random.seed(42) # 承接上文读入的 coco cat_of_img = defaultdict(set) for ann in coco["annotations"]: cat_of_img[ann["image_id"]].add(ann["category_id"]) groups = defaultdict(list) for img_id, cats in cat_of_img.items(): groups[tuple(sorted(cats))].append(img_id) train_ids, val_ids = [], [] for combo, ids in groups.items(): random.shuffle(ids) split = int(len(ids) * 0.85) train_ids.extend(ids[:split]) val_ids.extend(ids[split:])参数说明:按类别组合分组切分,能保证同时含裂纹和剥落的图片不会只进训练集,验证集里仍能看到组合形态。85/15是起步值,叶片缺陷图往往只有数百到两三千张,val少于50张时评估方差太大,这时我会把比例改成90/10。随机种子固定下来,是为了实验可复现,不然每次重跑切分结果都在变,模型对比就失去基线意义。
3.4 转完后的三分钟自检
转换完成后直接开训是挺危险的。我固定抽查三件事:第一,打印几个txt第一行的坐标范围,确认都在0到1之间;第二,随机选十张原图,把mask叠加画出来人工扫一遍,看起来像不像真实的缺陷边界;第三,统计每个txt的行数分布,空txt对应没有标注的负样本图——这类图我一般保留,让模型见过正常叶片纹理,但验证集里要单独统计,避免负样本稀释mAP的参考意义。这三步做完,再进训练阶段才稳。
4. 训练与推理:用YOLO实例分割把数据集变成模型
4.1 data.yaml的两个关键点:路径与类别顺序
训练前先准备数据配置,data.yaml里最容易坑人的是两个字段:路径写相对还是绝对,names的顺序是否和txt里的class_id对齐。
path: ./wind_leaf_seg train: images/train val: images/val nc: 4 names: 0: crack 1: erosion_front_edge 2: coating_peeling 3: lightning_damage参数说明:path用相对路径,跨机器拷贝后不用改配置;train和val是相对path的目录路径。如果原始zip解压后只有单一目录,先用第3章的切分脚本把图片和成品txt分别归置到images/train、images/val再填配置。nc和names的数量必须一致,顺序必须和转换时写入的category_id一一对应——模型本身不关心名字,但评估和推理结果展示全依赖这层映射,排错时最疼的就是预测坐标全对、类别名对不上。
4.2 模型选择与训练命令:从yolov8s-seg起步
模型选择这块我给个固定结论:叶片缺陷实例分割从yolov8s-seg起步,而不是直接上yolov8l-seg。风力叶片数据集通常不大,几百到两三千张是常态,大模型在这个量级容易过拟合;而且高分辨率输入下大模型显存占用高,迭代周期拉长。n-seg更轻,但mask分支细节能力偏弱,长条裂纹和轻微剥落容易被漏掉。s是性价比起点,先看baseline再决定是否往m或l走。
训练命令:
yolo segment train \ data=wind_leaf_seg/data.yaml \ model=yolov8s-seg.pt \ epochs=150 imgsz=1280 batch=8 \ patience=30 \ project=runs name=blade_v1参数说明:imgsz=1280是最容易被低估的参数——长条形缺陷在640分辨率下缩成几个像素,mask分支基本学不到细节,很多人在叶片数据集上复现AP上不去,先怀疑网络再怀疑数据,其实只是输入分辨率不够。batch=8是单卡16G显存的经验值,显存只有8G时降到4并保持AMP混合精度开。patience=30表示验证指标连续30轮不涨就提前停,叶片数据集小,训练早期就能看到明显loss下降,patience设太长只会白烧算力。多卡机器上batch按卡数翻倍,但总batch增大时学习率需要同步上调,初学者建议先单卡跑通复现再上DDP。
提示:如果你的机器只有8G显存,
imgsz=1280+batch=8大概率OOM,先降到batch=4再试;1280分辨率对叶片长条缺陷是值得保住的,不要为了省显存砍到640。
4.3 推理与可视化审查:先跑一批没有参与训练的原图
训练结束的第一个动作不看训练曲线,而是让模型跑一批val集没见过的原图,把mask可视化出来人工扫一遍。这一步能在五分钟内暴露九成的问题:
from ultralytics import YOLO model = YOLO("runs/blade_v1/weights/best.pt") results = model.predict( source="wind_leaf_seg/val_images_external", imgsz=1280, conf=0.25, iou=0.5, save=True, save_txt=True, line_width=2, )conf=0.25是起步阈值,叶片缺陷类别多且形态差异大,阈值拉太高会把残缺裂纹漏掉;但我建议用0.25和0.5各跑一遍,对比看哪些缺陷先消失,能快速判断哪些类别在训练数据里偏弱。save_txt=True同时输出YOLO格式坐标,为第6章的验收面积统计提前生成中间产物。line_width=2只影响可视化线条,高分辨率图用1或2,太粗会把细小裂纹盖住。
4.4 训练曲线不看过程,先看数据再看AP
训练结束后打开results.csv,重点看metrics/mAP50(B)和metrics/mAP50-95(B)。风力叶片这个场景,mAP50从45到80我都见过,主要差异来源于缺陷面积分布——小裂纹占比高时整体AP普遍难看。如果train loss收敛但val AP不涨,先回头检查标注一致性,不要急着调lr和epochs;如果训练还没收敛就停了,跑完patience后直接续训比重新开一轮更划算。YOLOv8训练自己的数据集,流程上大家都会跑,但过程中的数据审查密度,往往才是项目最终能否落地到现场的关键。
5. 实例分割数据集的常见问题与排查:解压、归一化、OOM与漏检
5.1 解压报错,zip包像缺了一半
现象:unzip运行到一半报CRC错误,或者某个子目录干脆没解出来,但前面的文件看起来都正常。
原因:大型实例分割数据集多经网络传输,中断或存储介质问题导致zip损坏;分卷zip缺了某个卷还会静默跳过,不一定会立刻报错。
解决:先跑unzip -t做完整性测试,损坏的重新下载对应分卷,不要硬解出半套数据继续训练。半套数据会让类别和场景分布整体偏向未损坏的部分,训练曲线看着正常,现场新图表现却完全失真。下载方式如果不稳定,至少把文件字节数对比做一遍再解压。
注意:分卷zip缺卷时不一定马上报错,可能在解到某个大文件时才突然中断,完整性测试务必用
unzip -t,不要只看解压结束码。
5.2 换机器后标签全挤在图像一角
现象:模型训练正常,loss下降也正常,但推理时所有mask都堆在图像左下角一小块区域。
原因:归一化时用的width/height不是原图真实尺寸。标注JSON里images的宽高字段有时与实际像素不一致——标注工具自动resize过,或者转换脚本误用了预处理旋转后的尺寸。
解决:转换阶段用PIL.Image.open(img_path).size读真实尺寸来求除数,不要无条件信任JSON字段。转完后随机抽10个txt打印坐标范围,如果都在0到0.2附近,基本就是归一化除数搞错。这是我被坑过一次后固定下来的自检步骤,宁可多花三分钟也不让整轮训练白跑。
5.3 训练中途Killed,显存和内存一起爆
现象:日志最后一行是Killed,进程直接消失,前面没有明显CUDA OOM报错。
原因:imgsz=1280加batch=8在高分辨率图下把显存打满,CUDA OOM有时会被容器或任务调度系统吞掉;另一个隐蔽原因是pin_memory挂在主机内存上,叶片高分辨率图一个batch的原始解码数据非常大,主机内存不够也会被Killed。
解决:batch降到4是第一步,同时把workers降到2减少数据加载峰值内存;如果仍要保1280分辨率,先观察显存峰值曲线再决定。多卡并行时确保每张卡只持有自己的batch分片,不要用数据并行把整份数据复制到所有卡上。
5.4 验证集AP很高,新风机照片漏检一堆
现象:val集上mAP50有70多,换成另一风场的实拍图,裂纹漏掉一半。
原因:训练集和val集来自同一批风机、同一相机、同一时段,模型记住的是这批样本的纹理和光照分布,并没有学到叶片缺陷的通用形态。不同风场的叶片脏污程度、漆面老化颜色完全不同,这就是典型的域漂移,和PHM2012这类轴承故障数据集跨工况复现的问题本质一样。
解决:预留一份“域外测试集”,最好来自不同风场、不同相机、不同季节;数据集本身如果没有这部分,宁可压缩训练集也要换新图做验证。数据增强优先加亮度抖动和随机仿射,裂纹边缘的阴影变化是对泛化最敏感的破坏项。
5.5 背景和缺陷互相污染,越训越迷惑
现象:两个类别的AP都上不去,细看预测mask,把阴影当剥落、把前缘反光当裂纹。
原因:标注标准没有定死。多人标注时,“裂纹断点是否连成一条”“剥落边缘要不要把浮色带进去”“阴影算不算缺陷”这些问题没有仲裁机制,导致同一类别边界漂移,模型只能学到一个折中的平均形状。
解决:训练前做一轮标注标准评审,把边界案例截图贴到共享文档里;已标注完成的数据集,至少用可视化脚本按类别批量抽查,把标准漂移的样例挑出来重标。训练后如果某类别mask AP明显低于其他类,先看这一类别的标注一致性和数量,不要先调模型——调模型大概率只能骗过验证集。
6. 验收不只看mAP:面积统计与域外挑战集
mAP是训练过程的参考,不是交付验收的终点。叶片缺陷实例分割的验收,我习惯拆成“召回优先、误检可控”:安全相关的缺陷宁可多报也怕漏报,所以先看Recall;误检用PR曲线和mask IoU控制。下表是项目里常用的一组验收口径:
| 指标 | 计算方式 | 现场含义 |
|---|---|---|
| Recall@0.5 | 预测mask与GT mask的IoU≥0.5即算命中 | 裂纹能不能被看到 |
| mask mAP50 | 全部类别的平均AP | 整体模型好坏 |
| 误检率 | 每百张图无GT样本的预测mask数 | 现场复检成本 |
| 面积误差 | 预测mask面积 / GT mask面积 - 1 | 风险等级量化 |
推理输出后直接做面积统计,把每个掩码的像素面积算出来,这是巡检报告的第一手数据:
from ultralytics import YOLO model = YOLO("runs/blade_v1/weights/best.pt") results = model.predict("blade_field_017.jpg", imgsz=1280, conf=0.25) for r in results: for i, mask in enumerate(r.masks.data): area_px = int((mask > 0).sum()) print(i, area_px)mask是0/1张量,直接求和就是像素数。想换算真实面积,需要把无人机拍摄距离和相机焦距投影算成每像素对应的毫米值,这中间会引入不少误差,我的习惯是先用像素面积做相对比较,现场测量后再校准整套换算系数。
我一向的习惯是训练完保留一个独立的“外场挑战集”,每次拿到新数据集都先跑一遍它,宁可指标难看也不让现场翻车后再回头调。数据格式、切分脚本、训练基线、验收口径这些,一次跑通后都要沉淀成项目模板,下一次新数据集进来直接复用,能省掉大量重复排错的成本。希望帮到你。
本文还有配套的精品资源,点击获取