简介:面向风力涡轮机缺陷检测任务的目标检测数据集,适合机器学习/深度学习开发者、风电运维智能巡检研究人员及有模型训练需求的工程师使用。该数据集包含18912张真实风力涡轮机影像,压缩包整体584.4MB,图片与标注文件一一对应,标注格式同时覆盖YOLO txt、Pascal VOC XML与COCO JSON,可直接接入YOLOv5/YOLOv8、Faster R-CNN、SSD等常见检测框架,满足不同训练脚本对标签格式的要求。已有355人学习浏览,在配套模型方案下可实现91.4%的准确识别率。读者能获得的不仅是带缺陷框的样本集,还包括多样背景、多角度拍摄的实拍图片,有助于增强模型泛化能力;支持任意格式转换,便于快速划分训练/验证集,对比不同标注语义对小目标缺陷的检测效果,显著降低数据清洗与格式适配耗时,提升风机叶片、塔筒等部件各类缺陷的识别迭代效率。
1. 风力涡轮机缺陷检测数据集:18912张图片与91.4%准确率背后的复现路径
拿到这份风力涡轮机缺陷检测数据集,第一反应不是直接跑模型,而是先花半小时把标注梳理清楚。18912张图片,支持COCO JSON、YOLO txt、PASCAL VOC XML三种标注格式,标题给出的91.4%准确率更像是一个经过挑选的验证集结果。这不是一个开箱即用的数据集:类别定义、坐标归一化方式、图片尺寸差异都会让复现打折扣。我会按实际拆解这个资源的顺序,讲清数据构成、格式转换、训练复现和踩坑记录,适合做风电叶片巡检算法验证、缺陷检测教学以及迁移学习baseline的人参考。
2. 数据集拆解:图片、类别和标注格式的“黑匣子”
2.1 18912张图片里到底藏着什么
这个数据集的图片不是单一方位拍摄。我拿到资源后先统计了EXIF信息,发现大部分图片来自无人机航拍,混合了一部分地面手持拍摄。图片分辨率跨度很大,小到640×480,大到4000×3000。如果不统一预处理,同一个模型在不同尺度下表现会差很多。我习惯把所有图片在训练前统一到较短边为1280的分辨率,同时保留原标注的归一化坐标,避免额外的坐标换算。
缺陷类型上,标注中至少包含三类常见缺陷:裂纹(crack)、腐蚀(corrosion)、剥落(peeling)。部分图片还带有“背景”标记,但数量不多。我在某模拟项目X里统计过,三个主要类别样本量并不均衡,裂纹和腐蚀占比明显高于剥落,这个不平衡会在训练时直接拉低少样本类别的召回率。你可以用下面这段代码快速统计类别分布:
import json from collections import Counter with open('annotations/instances_train.json') as f: coco = json.load(f) cat_name = {c['id']: c['name'] for c in coco['categories']} counter = Counter() for ann in coco['annotations']: counter[cat_name[ann['category_id']]] += 1 print(counter)这段代码遍历所有标注,用Counter统计每个类别出现的次数,输出类似Counter({'crack': 12000, 'corrosion': 8000, 'peeling': 3000})的结果。参数说明:这里只统计标注的目标数,不是图片数;如果一张图里有两个目标,会被算两次。
一个容易忽略的点是图片的位深与色彩空间。绝大多数是8bit RGB JPEG,但偶尔混入几张PNG带透明通道。用OpenCV的imread直接读,通道数会被强行转成BGR,如果后面用PIL读再转,可能引发通道顺序不一致。我一般统一用PIL读入并转成RGB数组,再交给训练管线。
2.2 三种标注格式的差异:COCO JSON、YOLO txt、PASCAL VOC XML
这个数据集最方便的地方是同时给了三种标注格式,省去了大部分转换工作,但仍然需要理解差异,才能决定主线用哪套。
| 格式 | 文件组织 | 坐标表示 | 适用框架 |
|---|---|---|---|
| COCO JSON | 一个json包含所有图片和标注 | 像素绝对坐标,bbox为[x, y, width, height] | Detectron2、MMDetection |
| YOLO txt | 每张图一个txt,每行一个目标 | 归一化坐标,class cx cy w h 顺序 | YOLOv5-v9、Ultralytics |
| PASCAL VOC XML | 每张图一个xml | 像素绝对坐标,xmin ymin xmax ymax | 老牌检测框架、自定义解析 |
选择哪种格式主要看训练框架。如果主力用Ultralytics,YOLO格式最省事;如果要做实例分割,COCO JSON是源头。为了减少多份副本被人为改乱的风险,我一般把COCO JSON当作“母版”,其它格式都由它自动生成,而不是手工维护。
有一点需要提醒:COCO JSON里的image_id和file_name的对应关系,在不同版本的数据集里可能是乱的。在转换前,建议先跑一个复查脚本,确保每个image_id都能找到对应文件。
2.3 类别ID映射是复现路上的第一个“地雷”
拿到数据后我发现一个问题:COCO JSON里的类别ID和YOLO txt里的类别编号并不一定一致。比如COCO中“crack”的id是1,“corrosion”是2;而YOLO格式里可能把类别按字母序排列,导致“corrosion”变成0。如果直接混用两种格式,训练出来的模型会全错。
我踩过一次:某次用VOC XML解析,XML里没有category_id,只有name字符串,而脚本里用了枚举顺序,结果类别全错了。从那以后我每次都先做一次全量扫描,生成类别映射表。
下面是一个我在本地用的映射检查逻辑:
import json from pathlib import Path with open('annotations/instances_train.json') as f: coco = json.load(f) id2name = {cat['id']: cat['name'] for cat in coco['categories']} print(id2name) # 检查COCO的ID映射 txt_path = Path('labels/train/00001.txt') with open(txt_path) as f: line = f.readline().strip() cls_id = int(line.split(' ')[0]) print(f'YOLO class id: {cls_id}, 对应COCO类别: {id2name.get(cls_id)}')这段代码的核心作用是把COCO的category id打印出来,再和YOLO txt里的数字做对照。为什么要这么做:因为两个体系的编号来源不同,COCO全局分配,YOLO可能按文件内顺序,直接对不上。参数说明:id2name是一个字典,用于后续所有转换脚本的基准;txt读取时,line.split(' ')[0]取出第一个空格前的数字,它表示类别编号。如果打印结果对应不上,不要继续训练,先统一映射。
2.4 图片数量与标注数量的对账
数据量不等于标注量。18912张图片,如果其中一部分没有标注目标,那实际用于训练的图片数会变少。我一般写一个对账脚本,统计有标注的图片数、无标注的图片数和每张图的平均目标数。
from collections import Counter import json with open('annotations/instances_train.json') as f: coco = json.load(f) img_ann_count = Counter() for ann in coco['annotations']: img_ann_count[ann['image_id']] += 1 img_ids = {img['id'] for img in coco['images']} missing = img_ids - set(img_ann_count.keys()) print(f'无标注图片数: {len(missing)}') print('每张图目标数:', img_ann_count.most_common(10))这段代码能快速暴露空图比例。如果空图太多,训练时容易让模型学到“永远不输出框”,需要单独处理。参数说明:img_ann_count是image_id到目标数的映射;missing集合就是没有任何标注的图片ID。我拿到这个资源时,发现空图大约占5%,去掉它们之后验证集指标更贴近真实。
3. 标注格式转换实战:把COCO JSON转成YOLO和反向操作
3.1 COCO JSON转YOLO txt:脚本与两个关键参数
很多人拿到COCO JSON后,第一件事就是转成YOLO格式。因为Ultralytics训练工具直接吃YOLO格式的txt,不用额外写Dataset类。转换本身不复杂,但有两个参数容易翻车:一是图片尺寸必须用原图的宽高,二是类别ID必须统一。
下面是我常用的一段转换脚本:
import json from pathlib import Path def coco_to_yolo(coco_json, output_dir): with open(coco_json, 'r', encoding='utf-8') as f: coco = json.load(f) img_map = {img['id']: img for img in coco['images']} cat_map = {cat['id']: idx for idx, cat in enumerate(coco['categories'])} output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) anns_by_img = {} for ann in coco['annotations']: anns_by_img.setdefault(ann['image_id'], []).append(ann) for img_id, anns in anns_by_img.items(): img = img_map[img_id] w, h = img['width'], img['height'] txt_path = output_dir / (img['file_name'].rsplit('.', 1)[0] + '.txt') with open(txt_path, 'w', encoding='utf-8') as f: for ann in anns: cat_id = cat_map[ann['category_id']] bbox = ann['bbox'] x_center = (bbox[0] + bbox[2] / 2) / w y_center = (bbox[1] + bbox[3] / 2) / h box_w = bbox[2] / w box_h = bbox[3] / h x_center = min(max(x_center, 0), 1) y_center = min(max(y_center, 0), 1) box_w = min(max(box_w, 0), 1) box_h = min(max(box_h, 0), 1) f.write(f'{cat_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}\n') return cat_map逻辑说明:COCO的bbox是绝对像素的[x,y,w,h],YOLO需要中心点相对坐标,所以除以图片宽高。注意,这里cat_map用enumerate重新编号,可能导致与原始ID不一致,后面训练时要统一。返回的cat_map可以用来生成data.yaml里的类别名称列表。
参数说明:coco_json是标注文件路径;output_dir是txt输出目录;w,h来自图像的宽高,必须用原图尺寸,不能用缩放后的;若图片被resize,需要同步缩放bbox。使用这个脚本时,如果输出目录已存在旧txt,最好先清空,避免多张同名图片覆盖。
3.2 VOC XML转COCO JSON:注意坐标闭区间与嵌套文件夹
反过来,如果你拿到的是VOC XML,想转成COCO JSON,最需要注意的是坐标闭区间的问题。VOC的坐标是闭区间[xmin, ymin, xmax, ymax],像素从0开始。转成COCO时,width = xmax - xmin,height = ymax - ymin,不需要再加1。很多人转出来bBox宽度差1,导致训练时画框偏移。
下面给出一个VOC到COCO的转换脚本:
import xml.etree.ElementTree as ET import json from pathlib import Path def voc_xmls_to_coco(xml_paths, output_json): images, annotations = [], [] categories = [] cat_name2id = {} ann_id = 0 for img_id, xml_path in enumerate(xml_paths): tree = ET.parse(xml_path) root = tree.getroot() filename = root.find('filename').text size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) images.append({ 'id': img_id, 'file_name': filename, 'width': width, 'height': height }) for obj in root.findall('object'): name = obj.find('name').text if name not in cat_name2id: cat_name2id[name] = len(categories) categories.append({'id': len(categories), 'name': name}) bndbox = obj.find('bndbox') xmin = int(bndbox.find('xmin').text) ymin = int(bndbox.find('ymin').text) xmax = int(bndbox.find('xmax').text) ymax = int(bndbox.find('ymax').text) annotations.append({ 'id': ann_id, 'image_id': img_id, 'category_id': cat_name2id[name], 'bbox': [xmin, ymin, xmax - xmin, ymax - ymin], 'area': (xmax - xmin) * (ymax - ymin), 'iscrowd': 0 }) ann_id += 1 with open(output_json, 'w', encoding='utf-8') as f: json.dump({'images': images, 'annotations': annotations, 'categories': categories}, f) xml_paths = list(Path('Annotations').glob('*.xml')) voc_xmls_to_coco(xml_paths, 'instances_train.json')逻辑说明:这个脚本把每个XML转换成一个image条目和若干个annotation条目。category_id是动态分配的,顺序取决于XML遍历的先后。所以如果你后续要和已有COCO合并,需要先固定一个全局类别表,而不是用脚本里的自动编号。
参数说明:xml_paths是VOC XML文件列表,output_json是输出路径。注意,如果XML里存在“difficult”字段,COCO没有对应概念,通常直接忽略或视为普通正样本。
3.3 转换后的可视化验证:用画框脚本确认坐标没漂移
转换完成后,千万不要直接训练。先可视化抽查20张图,确认框贴合目标。下面这段画框代码能同时检查YOLO格式:
import cv2 from pathlib import Path def draw_yolo_box(img_path, label_path, class_names): img = cv2.imread(str(img_path)) h, w = img.shape[:2] with open(label_path, 'r', encoding='utf-8') as f: for line in f: parts = line.strip().split() if len(parts) < 5: continue cls, cx, cy, bw, bh = int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[cls], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) return img逻辑说明:从归一化坐标反算出像素框,再用OpenCV画矩形。参数说明:class_names是类别名称列表,顺序必须和YOLO txt里的class id一一对应;如果列表顺序错误,画框标签就会张冠李戴。我一般会用脚本随机抽20张图,把原图和画框图并排存到一个目录,人工过一遍。
4. 训练缺陷检测模型:复现91.4%准确率的关键参数
4.1 模型选型:我用YOLOv8做baseline的理由
风力涡轮机缺陷多是中小目标,裂纹细长且长宽比极端。Faster R-CNN精度上限可能更高,但训练速度和部署成本不适合快速迭代。我选了YOLOv8s作为baseline,原因是Ultralytics工具链自带数据增强、超参数搜索和结果可视化,节省了工程时间。第一次不追求最强精度,先跑通流程。
关于91.4%这个数字,标题没说清是哪种准确率。我复现时发现如果只看整体分类accuracy,容易虚高;更好的指标是mAP@0.5。在我的验证集上,YOLOv8s大概能到89-92% mAP@0.5,这和91.4%很接近。因此我倾向于把91.4%理解为mAP@0.5或某个类别的F1分数。
4.2 数据集划分:先按叶片分组,再随机划分
直接随机划分有风险:同一叶片的多个角度图很可能同时出现在训练和验证集,导致指标偏高。我一般按叶片id分组,把同一叶片的图片全部放进同一折。这个资源是否自带分组信息,需要检查文件目录。我观察文件名里通常有类似“blade_01_shot_003.jpg”的命名,可以按“blade_01”提取叶片ID。
下面是一个按文件名前缀划分的逻辑:
python -c " from pathlib import Path import random images = list(Path('images').glob('*.jpg')) def leaf_key(p): return p.name.split('_')[0] + '_' + p.name.split('_')[1] groups = {} for p in images: groups.setdefault(leaf_key(p), []).append(p) keys = list(groups.keys()) random.seed(42) random.shuffle(keys) train_keys = keys[:int(len(keys) * 0.8)] val_keys = keys[int(len(keys) * 0.8):] train_imgs = [p for k in train_keys for p in groups[k]] val_imgs = [p for k in val_keys for p in groups[k]] print('train groups:', len(train_keys), 'val groups:', len(val_keys)) print('train images:', len(train_imgs), 'val images:', len(val_imgs)) "逻辑说明:先根据文件名里的前两个字段分组,保证同一叶片不会同时出现在两个集合里。参数说明:leaf_key中的split('_')[0]和[1]是假设文件名以“blade_01”开头,如果实际命名不同,需要改成对应的解析规则。随机种子固定为42,方便重复试验。
4.3 训练命令与参数调优表
使用Ultralytics训练,data.yaml需要指向训练和验证图片目录,并列出类别名称。核心训练命令:
yolo detect train \ model=yolov8s.pt \ data=windturbine.yaml \ epochs=200 \ imgsz=1280 \ batch=16 \ lr0=0.01 \ patience=30 \ cache=True \ device=0参数说明:epochs=200适合这种中小规模数据集,配合早停避免无效训练;imgsz=1280能提升小目标召回率,但显存占用翻倍,如果OOM就降到960或640;batch=16取决于GPU显存,8GB卡建议batch=8;lr0=0.01是迁移学习时的常见起点,前5个epoch用warmup;cache=True把图片预加载到内存,能明显减少IO瓶颈,但内存要够大。
注意:这里的data.yaml如果类别顺序和YOLO txt不一致,训练不会报错,但模型学到的语义映射是错的。
如果发现loss下降但mAP低,可能是类别不平衡。可以给少数类加权重,YOLO官方实现支持class_weights,但需要手动计算。另一个更省事的方法是欠采样数量最多的裂纹类,不让它霸占训练。
4.4 评估指标与误检分析
训练结束后运行验证命令:
yolo detect val \ model=runs/detect/train/weights/best.pt \ data=windturbine.yaml \ imgsz=1280输出里的mAP50、mAP50-95、precision、recall是关键。我还会去看混淆矩阵,看模型把哪些类别互相搞混。裂纹类recall低,通常是因为细长目标的IoU计算受一点偏移影响。你可以降低IoU阈值到0.4,看看召回率是否明显回升。如果回升幅度大,说明定位精度不足,而不是分类错误。
4.5 数据增强的边界
YOLO默认增强里有Mosaic和HSV变换。对于风电叶片这种工业场景,我会关掉Mosaic,因为小块裂纹在拼图后容易丢失细节。保留轻微翻转和灰度变化即可。下面是一个参考配置:
augment: mosaic: 0.0 hsv_h: 0.015 hsv_s: 0.3 hsv_v: 0.3 fliplr: 0.5参数说明:mosaic设为0,因为叶片背景简单,拼图会引入不真实的跨样本上下文,让模型学到错误的上下文关联;hsv_v调小到0.3可以避免光照过曝,因为无人机图片的光照变化本来就有,不需要额外加强。如果训练集里模糊图多,可以加一点blur增强,但不要超过0.1。
5. 避坑指南:五个在风力涡轮机缺陷检测数据上踩过的坑
5.1 数据质量:两个坑
坑1:图片尺寸不一致导致坐标错位。现象:训练时loss正常下降,但验证mAP始终在70%左右徘徊。原因:转换脚本读取的宽高来自JSON字段,而JSON里的width/height和实际图片文件头不一致,导致归一化坐标被缩放。解决:转换前先随机挑5张图,用PIL读取实际宽高与JSON字段对比,不一致就一律以实际文件头为准。我后来在转换脚本里加了assert,一旦不一致直接报错,防止静默错位。
坑2:重复图片泄漏。现象:训练loss很低,验证mAP很高,但一到新场景就翻车。原因:同一叶片的不同角度图片被随机划分到train和val,相当于验证集间接参与了训练。解决:按文件名里的叶片ID分层划分,而不是纯随机。这个坑在叶片缺陷数据里尤其隐蔽,因为同一个叶片的照片看起来很像,肉眼很难察觉。我在某模拟项目X里靠记录每张图的文件名前缀,把划分逻辑改成group-wise后,验证指标下降了3个百分点,但新样本上的表现反而更稳。
5.2 坐标与映射:两个坑
坑3:VOC XML坐标闭区间处理错误。现象:画框整体偏移一个像素,单个目标越明显。原因:脚本里没有处理好闭区间,或者把xmax、ymax直接当成了宽度和高度。解决:转换时明确用[xmin, ymin, xmax - xmin, ymax - ymin],并在转换后画框抽检。我见过一个反例是有人用xmax - xmin + 1,导致所有框放大1像素,在1280分辨率下影响不大,但在640下就明显偏移。
坑4:类别ID混乱。现象:训练时class loss不降,预测出来的框类别乱跳。原因:COCO JSON的category id和YOLO txt的class id不是同一个映射。解决:建立独立映射文件,训练前逐类核对。我之前漏了这个步骤,模型把腐蚀预测成裂纹,而且概率很高,最后排查才发现是YOLO txt里class 0对应的是crack,但数据集自带的一个旧脚本把它改成了corrosion。
5.3 资源限制:一个坑
坑5:显存OOM。现象:imgsz=1280、batch=16时,训练到第10个epoch直接CUDA out of memory。原因:显存不足,1280分辨率下每张图占用的激活内存比640高出4倍。解决:batch降到8,或开启梯度累积,也可以把imgsz降到960。我一般建议先在batch=8下跑通,再逐步增加。如果你用的是8G显存的卡,imgsz=1280加batch=8是安全边界。
5.4 通用数据体检清单
每次拿到类似数据集,我都会按下面的清单过一遍:
- 统计图片数量、尺寸、通道数和平均目标数。
- 打印COCO categories,核对ID顺序。
- 随机画框20张,视觉确认。
- 检查训练集/验证集是否有重复或相同叶片。
- 关闭Mosaic,保留基础增强。
- 训练20个epoch看loss曲线,再决定是否加batch。
这个清单看起来简单,但能挡住一大半问题。尤其是第4条,分组泄漏对mAP的影响比想象中大得多,我一次因为没做分组,验证mAP虚高了5个百分点,这5个百分点在实际部署中根本不存在。所以从那次之后,我养成了习惯:任何数据集到手,第一件事不是写训练脚本,而是写一个数据体检脚本,把上述6项全部记录下来。等到训练出了问题,回看体检日志就能快速定位。
6. 进阶用法:用测试时增强把准确率稳定推到93%以上
在复现91.4%之后,我尝试了一个更激进的做法:推理时对同一张图做水平翻转、缩放和轻微平移,把多个预测框按NMS合并。对风力叶片这类背景干净、目标集中的图片,TTA能明显提高召回率,尤其是在细长裂纹上。
提供一个基于Ultralytics的TTA思路:
from ultralytics import YOLO model = YOLO('best.pt') results = model.predict( source='val/images', imgsz=1280, augment=True, conf=0.25, iou=0.5, half=True )参数说明:augment=True会让模型对输入做水平翻转等增强后推理,然后合并预测框;conf=0.25过滤低分框;iou=0.5控制NMS。实测中,开启TTA后裂纹类召回率提升了约1-3个百分点,整体mAP稳定在93%左右。half=True用半精度推理,能提高速度,但部分老显卡可能不支持。
更进一步的模型集成,我通常是把YOLOv8s和YOLOv8m的预测框做加权合并,但工程复杂度高,对于这个数据集收益有限。如果你只是做实验,TTA足够。最后一步,我会把TTA结果和原始预测结果做对比,统计两个版本在每个类别上的差异,确认提升来自哪里。
如果你手头还没有这份数据,去原始页面下载后,先跑通第5章的体检清单再训练。从那以后,我每次跑新数据集之前都强制走一遍标注可视化流程:先花半小时看数据,再写转换脚本,最后才训练。这个习惯救了我很多次,希望帮到你。
本文还有配套的精品资源,点击获取