ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

YOLOv8风机叶片检测实战:数据切片、训练部署与避坑指南

YOLOv8风机叶片检测实战:数据切片、训练部署与避坑指南

简介:这份YOLOv8风机叶片检测源码包,面向风电运维工程师、深度学习开发者和智能制造研究人员,针对人工巡检效率低、漏检率高等痛点,提供了从叶片图像采集标注、YOLOv8模型训练优化,到PyQt5界面实时检测的完整参考实现。包内共17个文件,压缩包约5.76MB,主要包含Python训练与推理脚本、预训练的yolov8n模型权重、数据集配置文件、依赖库清单、界面程序以及图文说明等,结构清晰,便于按模块复用。资源目前已有128人学习,适合具备基础Python和深度学习知识、希望快速复现或二次开发检测系统的读者。尤为实用的是,资源内置了训练好的模型权重和完整配置,可直接运行检测流程;同时附带了数据增强、多尺度检测、边缘计算部署等优化思路,结合UI界面代码,能便捷地完成视频或摄像头实时叶片缺陷识别,为风电设备智能运维提供可落地的技术范本。

1. YOLOv8风机叶片检测:模型先放一边,数据才是最大的坑

YOLOv8风机叶片检测,真正劝退人的不是模型训练本身,而是数据。我接过一个类似的巡检需求,起初拿官方预训练权重直接跑现场照片,结果细裂纹漏掉一半,塔筒阴影全被当成缺陷。看验证集指标mAP到了0.8,以为稳了,拿到新风机上直接翻车。

后来把精力移到图像切片、标注清洗和训练参数上,模型才真正可用。这篇按我自己做这类项目的落地顺序来写:先把检测任务拆明白,再处理数据,接着训练部署,然后讲五个高频踩坑点,最后说两个提升精度的进阶技巧。适合手里已经攒了几百张缺陷照片、想把检测跑起来并部署到现场的人。

2. 任务拆解与选型:风机叶片检测到底要检测什么

2.1 检测目标怎么定:裂纹、剥落、雷击损伤与污损

风机叶片巡检的照片主要来自无人机可见光拍摄,少数项目用工业相机或红外。可见光图像里,需要检测的表面缺陷基本可以归成四类。第一是裂纹,细线状、方向不定,颜色和浅色叶片背景接近,是四类里最难检的。第二是剥落,多出现在前缘和后缘等受力部位,块状缺失,尺寸跨度很大,小的几厘米,大的几十厘米。第三是雷击损伤,有烧蚀痕迹、颜色发黑、边缘不规则,经常和裂纹同时出现。第四是表面污损或油污,大块暗色区域,最容易和阴影混淆。

类别体系怎么定,我的建议是宁少勿多。缺陷类别控制在三到五类最稳,某类样本数量不到三百个实例,就先并入相近类别,或者干脆做“有无缺陷”二分类。我见过有人把“边缘破损”和“前缘剥落”拆成两类,结果每类只有几十个样本,模型怎么调都过拟合,这就是类别设计给自己挖坑。复合损伤的处理也要提前定规范:同一个目标既有雷击烧蚀又有裂纹,是单独成类,还是允许一个框打两个标签?我一般按“单一主要缺陷”标注,规范里写明“优先标最明显、面积最大的一类”,这样标注一致性高,训练出来的模型边界更干净。

2.2 为什么选 YOLOv8:anchor-free、多尺度头与部署生态

选YOLOv8而不是传统图像处理或两阶段检测器,核心有三个理由。第一是anchor-free设计。裂纹这类缺陷形状极不规则,长宽比从1:5到1:40都可能,传统anchor-based方案要先统计标注框的长宽比分布来预设anchor,细长裂纹经常匹配不上预设框,训练时正样本稀缺。YOLOv8去掉anchor之后,模型直接回归目标中心点和宽高,对不规则形状的适应性好了很多,不需要做anchor统计这一步。

第二是多尺度检测头。YOLOv8在8倍、16倍、32倍下采样处分别输出检测结果,小目标主要靠8倍下采样的P3头。风机叶片上的细裂纹在原始照片里可能只有几十像素宽,如果输入尺寸提到1024,P3头的感受野和分辨率是能覆盖到的。这也是后面训练时我坚持用imgsz=1024而不是默认640的原因。第三是部署生态完整。训练完导出ONNX、转TensorRT、量化、边缘设备推理,整条链路都有现成实现,不用自己从零写推理框架。

对比传统图像处理方案,边缘检测加形态学在固定光源的工厂场景确实好用,但风机叶片在户外,光照从早到晚变化极大,阴影、反光、背景纹理都会让阈值崩掉。两阶段检测器精度高,但训练和推理成本都高,边缘设备上跑不动,落地性价比不划算。YOLOv8属于精度和速度都够用的平衡点。

2.3 三个难点:小目标、低对比度与样本稀缺

这个场景有三个绕不开的难点,后面的每一行脚本和参数都是针对它们设计的。第一是小目标:一条1厘米宽的裂纹在4000乘3000的无人机照片里,占整图面积往往不到0.5%,直接resize到640会把缺陷缩成两三个像素,等于让模型去认一个点。第二是低对比度:叶片本身是白色或浅灰色,裂纹也是浅色,边缘很淡,加上阳光阴影和表面反光,模型很容易把背景纹理当成缺陷。第三是样本稀缺:缺陷不是常态,一个风场一年巡检下来,有效缺陷图可能只有几十到几百张,不足以支撑从零训练一个检测模型。

这三个难点决定了项目的基本打法:迁移学习解决样本量不足,切片和输入尺寸解决小目标,负样本和类别设计解决低对比度误检。如果你拿到的数据情况和上面不同,比如用红外热像图,裂纹会显示成亮温线,对比度问题反而小很多,重点就转移到小目标和温度标定上。

3. 数据准备:切片、标注转换与增强,决定模型上限

3.1 图像切片:把 4000×3000 大图切成可训练样本

检测模型输入一般是640或1024,直接把大图resize会把小缺陷缩没,所以第一步是切片。常见做法是滑窗切片,窗口尺寸和训练输入保持一致,这里用1024,重叠率50%。切片要同步处理标注框:窗口内的框被切掉超过一半,这个样本直接丢弃,否则模型学到的是残缺目标。

import os import cv2 def sliding_window_crop(src_dir, save_dir, target_size=1024, overlap=0.5): """ 滑窗切片:把大图切成 target_size 的方块 src_dir 下每张图对应一个同名 txt(YOLO 格式),切片时同步裁剪标注框 """ os.makedirs(os.path.join(save_dir, "images"), exist_ok=True) os.makedirs(os.path.join(save_dir, "labels"), exist_ok=True) step = int(target_size * (1 - overlap)) for img_name in os.listdir(os.path.join(src_dir, "images")): if not img_name.endswith(".jpg"): continue img_path = os.path.join(src_dir, "images", img_name) label_path = os.path.join(src_dir, "labels", img_name[:-4] + ".txt") img = cv2.imread(img_path) h, w = img.shape[:2] boxes = [] if os.path.exists(label_path): with open(label_path) as f: for line in f: parts = line.strip().split() cls = int(parts[0]) xc, yc, bw, bh = map(float, parts[1:]) x1 = (xc - bw / 2) * w y1 = (yc - bh / 2) * h x2 = (xc + bw / 2) * w y2 = (yc + bh / 2) * h boxes.append([cls, x1, y1, x2, y2]) for i, x in enumerate(range(0, w - target_size + 1, step)): for j, y in enumerate(range(0, h - target_size + 1, step)): crop = img[y:y + target_size, x:x + target_size] new_boxes = [] for cls, x1, y1, x2, y2 in boxes: cx1, cy1 = max(x1, x), max(y1, y) cx2, cy2 = min(x2, x + target_size), min(y2, y + target_size) if cx2 <= cx1 or cy2 <= cy1: continue if (cx2 - cx1) * (cy2 - cy1) < 0.5 * (x2 - x1) * (y2 - y1): continue nx1, ny1 = (cx1 - x) / target_size, (cy1 - y) / target_size nx2, ny2 = (cx2 - x) / target_size, (cy2 - y) / target_size nxc, nyc = (nx1 + nx2) / 2, (ny1 + ny2) / 2 nw, nh = nx2 - nx1, ny2 - ny1 new_boxes.append(f"{cls} {nxc:.6f} {nyc:.6f} {nw:.6f} {nh:.6f}") if new_boxes: crop_name = f"{img_name[:-4]}_{i}_{j}.jpg" cv2.imwrite(os.path.join(save_dir, "images", crop_name), crop) with open(os.path.join(save_dir, "labels", crop_name[:-4] + ".txt"), "w") as f: f.write("\n".join(new_boxes))

逻辑说明:step等于target_size乘以(1 - overlap),1024窗口配50%重叠就是512步长。坐标从归一化转回像素,切片后再重新归一化保存,中间没有丢失精度。这里只保存包含有效缺陷的切片,无缺陷的背景切片需要单独抽样。

负样本怎么加:从没有缺陷的大图上随机切同等尺寸的块,按正样本数量的三分之一到二分之一加入训练集。这些负样本没有标签txt,模型会从中学到“没有目标时不要输出”。这个比例我踩过坑:负样本加太多,模型偏向预测“无缺陷”,漏检率上升;加太少,误检压不住。三分之一到二分之一是比较稳的区间。

3.2 标注格式转换:从 VOC/COCO 到 YOLO 的 txt

如果你用的是常见标注工具,导出格式通常是VOC的XML或COCO的JSON,而YOLO训练需要的是每张图对应一个txt,每行一个目标,格式是“类别ID 中心点x 中心点y 宽 高”,全部归一化到0到1。下面是VOC XML转YOLO txt的脚本。

import glob import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_dir, out_dir, class_map): """ class_map: {'crack': 0, 'peeling': 1, 'lightning': 2, 'dirt': 3} 读取 VOC 格式 XML,输出 YOLO 格式 txt """ os.makedirs(out_dir, exist_ok=True) for xml_path in glob.glob(os.path.join(xml_dir, "*.xml")): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.iter("object"): cls = obj.find("name").text if cls not in class_map: continue bnd = obj.find("bndbox") x1 = float(bnd.find("xmin").text) y1 = float(bnd.find("ymin").text) x2 = float(bnd.find("xmax").text) y2 = float(bnd.find("ymax").text) x1, x2 = min(x1, x2), max(x1, x2) y1, y2 = min(y1, y2), max(y1, y2) x1 = max(0, min(x1, img_w - 1)) x2 = max(0, min(x2, img_w - 1)) y1 = max(0, min(y1, img_h - 1)) y2 = max(0, min(y2, img_h - 1)) if x2 <= x1 or y2 <= y1: continue xc = (x1 + x2) / 2 / img_w yc = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{class_map[cls]} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") yolo_path = os.path.join(out_dir, os.path.basename(xml_path)[:-4] + ".txt") with open(yolo_path, "w") as f: f.write("\n".join(lines))

参数说明:class_map的类别ID顺序必须和后面的data.yaml完全一致,这是项目里最常见的低级错误,类别ID错位会让模型学到完全错误的目标。脚本里做了两件容易忽略的事:一是把xmin/xmax乱序的框修正,二是把越界的坐标clip到图像范围内,这两类脏数据在真实标注里出现频率很高,不处理会让loss曲线剧烈震荡。

COCO JSON转YOLO的流程是类似的,解析 annotations 数组,读取 segmentation 或 bbox 字段,把 [x, y, w, h] 转成归一化中心点格式,关键步骤和上面一致,不再重复写。

3.3 数据增强:样本少的时候怎么补

数据增强分两步走,离线增强针对少数类,在线增强交给训练参数。离线增强只做在少数类样本上:剥落只有五十张,就把它复制增强到两百张的量级。操作包括水平翻转、垂直翻转、旋转90度和180度、亮度加减30%、对比度加减20%、高斯噪声和轻微模糊模拟雨天。叶片的方向性很强,垂直翻转对叶片形态的模拟比较真实,但翻转后要人工抽检一批,确认缺陷形态没有变得不合理,特别是方向性强的剥落。

在线增强用YOLOv8训练参数自带的配置。hsv_h、hsv_s、hsv_v控制色域扰动,degrees控制旋转,fliplr和flipud控制翻转概率,mosaic控制四图拼接。数据量少的时候mosaic能显著提高样本多样性,但训练最后10到15个epoch必须关闭,否则模型会学到拼接痕迹,推理时对完整目标反而犹豫。对比度扰动不要拉太强,浅色裂纹在强对比度扰动下会被“洗掉”,模型学不到这个类,这是我在增强参数上调出来的血泪经验。

4. 训练到部署:完整命令、参数说明与推理脚本

4.1 数据集目录与 data.yaml

训练前先把数据组织好,目录结构如下。images和labels下各分train、val,图片和标签同名一一对应。

blade_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── blade.yaml

data.yaml内容就是路径、类别数和类别名。注意class_map顺序是crack在第0位,这里names列表必须和它一致。

path: /path/to/blade_dataset train: images/train val: images/val nc: 4 names: 0: crack 1: peeling 2: lightning 3: dirt

划分train/val有个容易忽略的点:按风机编号划分,不要按单张图片随机划分。同一台风机的照片背景高度相似,随机划分会把相似背景同时放进训练集和验证集,后面第5章会专门讲这个坑。模型选择上,样本量少就用yolov8n或yolov8s起步,样本量超过两千再考虑m,边缘设备部署优先n和s。

4.2 训练命令与超参数怎么调

用YOLOv8官方实现的命令行训练,一条命令跑起来。

yolo detect train \ model=yolov8s.pt \ data=blade.yaml \ epochs=200 \ imgsz=1024 \ batch=8 \ close_mosaic=10 \ patience=30 \ optimizer=AdamW \ lr0=0.001

逐个说参数。model=yolov8s.pt用的是COCO预训练权重,迁移学习的意义在于模型已经学会通用纹理特征,叶片表面和自然图像有足够的共性,收敛速度比从零训练快几倍。imgsz=1024是风机叶片场景的关键参数,前面说过,小目标靠它保召回。batch=8在24G显存下刚好够用,12G显存降到4,batch小了学习率也要跟着降。epochs=200配合patience=30做早停,模型在验证集上连续30轮不涨就自动停,防止无效训练。close_mosaic=10表示最后10轮关闭mosaic增强。

optimizer=AdamW和lr0=0.001是数据少时的稳定组合。YOLOv8默认lr是0.01配SGD,在几百张叶片缺陷数据上容易震荡,AdamW配合0.001学习率收敛更稳。如果坚持用SGD,学习率降到0.002左右,epochs拉到300。

显存不够还有一个办法:先用imgsz=640把流程跑通,确认数据和标注没问题,再加载训练完的权重用imgsz=1024微调50轮。这样既验证了管线,又保住了精度,代价是训练时间增加。训练卡在loss不降时,先看是不是数据问题,不要急着调模型结构。

4.3 推理脚本:置信度、NMS 与结果输出

训练完用best.pt做推理,脚本很短。

from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( source="test_images/", conf=0.35, iou=0.5, imgsz=1024, save_txt=True, save_conf=True, save_crop=True, project="inference_output", ) for r in results: for box in r.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) x1, y1, x2, y2 = [float(v) for v in box.xyxy[0]] print(f"{r.path}: class={model.names[cls_id]} conf={conf:.3f} " f"box=({x1:.0f},{y1:.0f},{x2:.0f},{y2:.0f})")

参数说明:conf=0.35是起步阈值,误检多就往0.5调,漏检多就往0.25调,这个值是误检和漏检的平衡旋钮。iou=0.5是NMS的IoU阈值,同一个缺陷被多个框覆盖时,高于这个比例的框会被抑制。imgsz推理时保持和训练一致,用1024而不是640,否则尺度不一致会掉点。save_crop=True会把检测到的缺陷裁剪成小图单独存放,我建议项目现场一定要开这个,工程师复核缺陷比看锚框高效得多。

推理输出里还要做一件事:按类别分别统计缺陷数量。某个风机的裂纹数量突然比相邻机组高一个数量级,往往不是检测变好了,而是该机组叶片状态确实异常,这个数字本身就是巡检报告的素材。

5. 避坑:风机叶片检测最常踩的五个坑

5.1 小目标漏检:为什么模型学不到小缺陷

现象:验证集mAP50到了0.8以上,现场新照片里细裂纹一个都框不出来。原因有两个:现场大图直接按默认尺寸resize,裂纹缩成两三个像素;训练集里小目标占比低,模型根本没学到小缺陷的形态。

解决:训练和推理都坚持imgsz=1024;对包含小缺陷的切片做额外过采样;推理时对大图切片检测再把结果合并到大图坐标系,而不是整图resize,这个操作对召回率的提升比换模型结构更明显。有人会尝试给模型加P2检测头,代价是显存和推理时间都涨,我一般先用切片加高分辨率,效果不够再考虑改结构。

5.2 类不均衡:少数类被大类淹没

现象:剥落样本只有几十个,裂纹样本上千个,训练出来的模型把所有缺陷都预测成裂纹,置信度还挺高。原因很直白:模型学到的是训练集里的类别先验频率,少数类几乎没有话语权。

解决:离线对少数类做复制增强,把剥落增强到裂纹样本的四分之一到三分之一;推理时按类别分别设阈值,剥落用conf=0.2,裂纹用conf=0.45,代码里按cls_id判断阈值即可。少数类阈值单独调,比整体压低conf更有效,不会引入大量误检。

5.3 过拟合与数据泄露:验证集指标虚高

现象:训练loss降到很低,验证集指标也好看,换到没参与训练的新风场照片上表现明显变差。原因大概率是数据划分方式有问题:同一个风场、同一天拍的相似背景照片,同时被随机分进了训练集和验证集,验证集和训练集背景高度重合,指标自然是虚高的。

解决:按风机编号或拍摄日期划分数据,保证验证集和训练集没有同源图像,最好留一批完全独立的测试集,只在最后评估用。这个操作会让验证集指标比随机划分掉两三个点,但换来的才是真实泛化能力。训练时做更强的画面增强,让模型不能依赖背景特征判断缺陷。

5.4 光照与遮挡:误检来自环境而非缺陷

现象:塔筒阴影、地面围栏、叶片反光被频繁误检成裂纹或污损。原因是在灰度特征上,阴影边缘和裂纹确实很像,模型在低对比度场景下分不清。

解决:采集无缺陷但有阴影、反光、复杂纹理的切片作为负样本加入训练,这部分数据不需要标注,采集成本很低;标注时严格不要把阴影框进去,边界错误的缺陷框会教坏模型;推理时conf阈值往0.45以上调,误检会明显下降。如果项目有叶片分割结果,可以用mask过滤检测框位置,检测框落在叶片区域外直接丢弃。

5.5 标注质量参差:loss曲线震荡的一个隐藏原因

现象:训练loss降不下去,曲线来回震荡,训练完错误框很多,框大小忽大忽小。原因常常不是模型配置,而是标注标准不统一:有人框整条裂纹,有人只框最明显的一段,模型要同时拟合两种分布,自然会震荡。

解决:动手训练前先写一页标注规范,写明缺陷边界定义和最小框尺寸,比如裂纹框包含全部可见裂缝、短于20像素的裂纹不标;随机抽10%标注复核,派两个人交叉看;训练完再用模型反查训练集,高置信度但没标注的区域大概率是漏标,回补后训练一轮,效果往往立竿见影。

6. 进阶:跨场景验证、TTA 与二次分类

6.1 跨场景验证:用没见过的风场照片检验模型

验证集mAP只能代表同源数据下的表现,我习惯额外准备一个“新场景测试集”:选没参与训练的风场、不同季节、不同光照、不同拍摄高度的照片,单独跑一遍推理。统计每个类别的精确率和召回率,裂纹P高R低说明漏检,P低R高说明误检多。这两个指标和conf阈值、imgsz直接相关,现场调参就对着它来。

6.2 TTA 与二次分类:提升精度的两个实用技巧

TTA(Test Time Augmentation)做水平翻转和多尺度推理,对细裂纹召回有提升,但推理时间涨三倍左右,适合离线批量复检,不适合实时视频流。更实用的方案是二次分类:YOLOv8输出的候选框裁剪下来,送一个轻量分类网络做“真缺陷/伪缺陷”二分类,几百张正负样本就能训,误检能压掉大半。

我自己现在的习惯是,实时检测用单次推理加类别阈值,每周做一次离线TTA抽检,用二次分类过滤可疑结果。这几个步骤配合起来,项目交付后返工率降了很多,这个习惯让我少翻了几次车,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进