风力发电机叶片语义分割实战:U-Net数据集与训练全流程

发布时间:2026/10/1 19:31:22
风力发电机叶片语义分割实战:U-Net数据集与训练全流程 简介本资源为风力发电机风扇叶片语义分割数据集面向从事计算机视觉与智能风电运维的研究者、工程师及学生用于训练和验证像素级叶片状态识别模型可区分正常区域、磨损、裂缝与污渍等状况。压缩包共约2000个文件以1994个tif图像及对应标签图为主另含6个Python脚本整体约810.93MB涵盖数据加载、图像预处理、数据集划分、U-Net网络实现、模型训练与预测等完整流程目录结构清晰便于按模块检索与二次开发。目前已有113人学习下载。数据集覆盖多种工作环境与光照条件配合开箱即用的训练代码读者可快速搭建语义分割实验、复现U-Net基线并迁移到自有数据为风电叶片智能监测提供可落地的算法框架与排错参考。1. 风力发电机叶片语义分割数据集从一堆 tif 到能跑通的 U-Net拿到这个资源包的时候我第一反应是这数据集有点意思。目录里躺着train.py、Unet.py、split_data.py、predict.py、pre_process.py、dataset.py六个脚本外加一个DATASET文件夹里面是成对的叶片图像和标签图格式是.tif。这不是那种只给你一堆图片让你自己猜标注格式的半成品数据集而是把预处理、划分、训练、预测整条链路都铺好了。它解决的核心问题很明确你想验证一个语义分割模型在工业叶片缺陷场景下到底行不行不用从零标注、不用自己搭 U-Net改改路径就能跑。适合谁做风电运维智能巡检的算法工程师、拿语义分割练手的学生、以及想快速对比 U-Net 和其他分割网络在真实工业数据上表现的从业者。下面我按数据长什么样 → 代码怎么串 → 坑在哪 → 怎么改的顺序拆一遍。2. 数据集结构与预处理链路tif 格式、标签对齐与 split_data 的划分逻辑2.1 为什么是 tif 而不是 jpg资源里的图像文件后缀是.tif比如TCGA_DU_6408_19860521_25.tif这种命名。tif 在工业图像里常见原因是它支持无损压缩和多通道存储叶片表面的细微裂纹、污渍在 jpg 压缩下容易产生块效应标注边界会糊。语义分割对边界敏感尤其是裂缝这种细长目标压缩伪影会直接拉低 IoU。所以拿到 tif 不要急着转 jpg先确认你的数据加载链路能不能直接读。常见做法是用Pillow或OpenCV读但两者对多通道 tif 的处理有差异。我一般统一用cv2.imread(path, cv2.IMREAD_UNCHANGED)保留原始位深避免自动转 8 位丢信息。如果显存吃紧再在pre_process.py里做 resize而不是在读取阶段就降质。2.2 pre_process.py 里该盯住的三个参数pre_process.py通常干三件事统一尺寸、归一化、可选的数据增强。这个脚本没有在正文里给出具体实现但按语义分割的常规做法我会重点检查这几个点输入尺寸U-Net 对输入尺寸有下采样倍数要求一般是 16 或 32 的整数倍。如果原图是 512×512 那没问题如果是任意尺寸resize 到 256×256 或 512×512 最稳。归一化方式用 ImageNet 均值方差还是简单除以 255取决于你是否加载预训练权重。从零训练就用/255.0加载预训练就对齐mean[0.485,0.456,0.406]。标签处理语义分割标签必须是单通道类别索引图不是 RGB 彩图。如果DATASET里的标签是彩色掩膜需要在dataset.py里做颜色到类别的映射否则CrossEntropyLoss会直接报维度错误。import cv2 import numpy as np def preprocess_image(img_path, target_size(512, 512)): # IMREAD_UNCHANGED 保留原始位深避免 tif 被截断 img cv2.imread(img_path, cv2.IMREAD_UNCHANGED) if img is None: raise FileNotFoundError(f读不到图: {img_path}) # 统一到 3 通道灰度图也能进 U-Net if len(img.shape) 2: img cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) img cv2.resize(img, target_size, interpolationcv2.INTER_LINEAR) img img.astype(np.float32) / 255.0 return img def preprocess_mask(mask_path, target_size(512, 512)): mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) # 最近邻插值防止类别索引被线性插值搞出小数 mask cv2.resize(mask, target_size, interpolationcv2.INTER_NEAREST) return mask.astype(np.int64)这段代码的关键在INTER_NEAREST。标签图 resize 用线性插值会把类别 1 和类别 2 之间插出 1.5训练时 loss 直接崩。这个坑我见过不止一次现象是 loss 一开始就是 NaN查半天以为是学习率其实是标签被插值污染了。2.3 split_data.py 的划分比例与随机种子split_data.py负责把DATASET切成训练集、验证集、测试集。工业数据集样本量通常不大划分比例我一般用 7:2:1 或 8:1:1。重点不是比例是随机种子固定和按图像对划分。如果脚本是按单张图随机抽可能出现训练集里有原图、验证集里有对应标签的情况指标虚高。正确做法是以图像-标签对为单位划分。另外如果同一叶片拍了多张不同角度的图最好按叶片 ID 分组划分避免同一叶片同时出现在训练和验证集里这叫数据泄漏是语义分割里最隐蔽的翻车点之一。import os import random import shutil def split_dataset(img_dir, mask_dir, out_dir, ratios(0.7, 0.2, 0.1), seed42): random.seed(seed) # 固定种子保证每次划分一致 files sorted(os.listdir(img_dir)) # 只保留有对应标签的图防止训练时找不到 mask pairs [f for f in files if os.path.exists(os.path.join(mask_dir, f))] random.shuffle(pairs) n len(pairs) n_train int(n * ratios[0]) n_val int(n * ratios[1]) splits { train: pairs[:n_train], val: pairs[n_train:n_train n_val], test: pairs[n_train n_val:] } for split, items in splits.items(): for sub in [images, masks]: os.makedirs(os.path.join(out_dir, split, sub), exist_okTrue) for f in items: shutil.copy(os.path.join(img_dir, f), os.path.join(out_dir, split, images, f)) shutil.copy(os.path.join(mask_dir, f), os.path.join(out_dir, split, masks, f)) print(f划分完成: train{len(splits[train])}, val{len(splits[val])}, test{len(splits[test])})参数说明ratios控制三集比例seed固定后每次运行结果一致方便复现实验。pairs的过滤逻辑很重要如果DATASET里存在没有标签的图不过滤就会在训练时抛FileNotFoundError。3. U-Net 网络结构与训练脚本Unet.py 的通道设计、train.py 的损失与学习率3.1 Unet.py 的编码器-解码器通道数怎么定Unet.py实现的是经典 U-Net编码器逐层下采样提特征解码器逐层上采样恢复分辨率中间用跳跃连接把浅层细节拼回来。语义分割里 U-Net 的优势就在跳跃连接叶片裂缝这种细目标没有浅层特征补充解码器根本恢复不出边界。通道数常见配置是[64, 128, 256, 512, 1024]但这是针对 512×512 输入的。如果你的图 resize 到 256×256可以砍到[32, 64, 128, 256]显存占用能降一半以上。改通道数的时候注意编码器每下采样一次通道翻倍解码器每上采样一次通道减半跳跃连接拼接时通道要对齐否则torch.cat会报维度不匹配。import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.net nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.net(x) class UNet(nn.Module): def __init__(self, in_ch3, num_classes2, base_ch64): super().__init__() # 编码器每层通道翻倍 self.enc1 DoubleConv(in_ch, base_ch) self.enc2 DoubleConv(base_ch, base_ch * 2) self.enc3 DoubleConv(base_ch * 2, base_ch * 4) self.enc4 DoubleConv(base_ch * 4, base_ch * 8) self.pool nn.MaxPool2d(2) # 瓶颈层 self.bottleneck DoubleConv(base_ch * 8, base_ch * 16) # 解码器上采样后与跳跃连接拼接通道数要对应 self.up4 nn.ConvTranspose2d(base_ch * 16, base_ch * 8, 2, stride2) self.dec4 DoubleConv(base_ch * 16, base_ch * 8) self.up3 nn.ConvTranspose2d(base_ch * 8, base_ch * 4, 2, stride2) self.dec3 DoubleConv(base_ch * 8, base_ch * 4) self.up2 nn.ConvTranspose2d(base_ch * 4, base_ch * 2, 2, stride2) self.dec2 DoubleConv(base_ch * 4, base_ch * 2) self.up1 nn.ConvTranspose2d(base_ch * 2, base_ch, 2, stride2) self.dec1 DoubleConv(base_ch * 2, base_ch) self.out nn.Conv2d(base_ch, num_classes, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) e4 self.enc4(self.pool(e3)) b self.bottleneck(self.pool(e4)) d4 self.dec4(torch.cat([self.up4(b), e4], dim1)) d3 self.dec3(torch.cat([self.up3(d4), e3], dim1)) d2 self.dec2(torch.cat([self.up2(d3), e2], dim1)) d1 self.dec1(torch.cat([self.up1(d2), e1], dim1)) return self.out(d1)num_classes要按你的标签类别数改。如果标签里只有背景和叶片两类就是 2如果有背景、正常叶片、磨损、裂缝四类就是 4。这个值必须和dataset.py里标签映射的类别数一致不一致的话训练不报错但预测结果全黑因为模型输出的通道和标签索引对不上。3.2 train.py 的损失函数与学习率策略train.py是整条链路的核心。语义分割常用CrossEntropyLoss如果类别极度不平衡比如裂缝像素只占 1%换成DiceLoss或CrossEntropyLoss DiceLoss组合。学习率我一般从1e-3起步配CosineAnnealingLR或ReduceLROnPlateau前者平滑下降后者根据验证集指标动态调。import torch from torch.utils.data import DataLoader from dataset import BladeDataset from Unet import UNet device torch.device(cuda if torch.cuda.is_available() else cpu) model UNet(in_ch3, num_classes2, base_ch64).to(device) criterion torch.nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) train_ds BladeDataset(DATASET/train, augmentTrue) val_ds BladeDataset(DATASET/val, augmentFalse) train_loader DataLoader(train_ds, batch_size4, shuffleTrue, num_workers2) val_loader DataLoader(val_ds, batch_size4, shuffleFalse, num_workers2) for epoch in range(50): model.train() for imgs, masks in train_loader: imgs, masks imgs.to(device), masks.to(device) optimizer.zero_grad() logits model(imgs) loss criterion(logits, masks) loss.backward() optimizer.step() scheduler.step() # 验证阶段只算 loss指标另算 model.eval() with torch.no_grad(): for imgs, masks in val_loader: imgs, masks imgs.to(device), masks.to(device) _ model(imgs) print(fepoch {epoch} done, lr{scheduler.get_last_lr()[0]:.6f})batch_size4是 8G 显存下的保守值显存够可以加到 8 或 16。num_workers在 Windows 上建议设 0否则容易卡在 DataLoader 启动阶段这是 Windows 下 PyTorch 的经典坑。T_max50要和总 epoch 数一致不然余弦退火曲线会提前走完。3.3 dataset.py 的标签映射与增强边界dataset.py负责把图像和标签配对读进来转成 tensor。这里最容易出问题的是标签映射如果标签图里像素值是 0 和 255而你的num_classes2模型期望的是 0 和 1直接送进去 loss 会算错。需要在__getitem__里做mask (mask 0).long()或按颜色映射表转换。数据增强方面图像和标签必须同步变换。翻转、旋转可以同步做但颜色抖动只能作用于图像不能碰标签。如果dataset.py里用了albumentations记得把additional_targets{mask: mask}加上否则标签不会跟着变换训练出来的模型边界全是错的。4. 避坑与排查tif 读取、显存溢出、标签错位、指标虚高4.1 现象训练一开始 loss 就是 NaN原因标签图 resize 用了线性插值类别索引被插成小数CrossEntropyLoss要求标签是int64小数被截断后类别错乱。或者图像归一化时除了 0某些像素全黑导致除零。解决标签 resize 强制用INTER_NEAREST归一化前检查图像像素范围加np.clip兜底。在dataset.py里打印一次 mask 的unique()值确认只有预期类别。4.2 现象CUDA out of memory但 batch_size 已经调到 1原因U-Net 的跳跃连接在拼接时显存占用翻倍512×512 输入下 base_ch64 的模型batch_size1 也可能吃满 8G。另外num_workers过大导致每个 worker 都复制一份数据到显存。解决把输入 resize 到 256×256或把base_ch从 64 降到 32。训练时用torch.cuda.empty_cache()清理缓存num_workers设 2 以内。如果还不够用混合精度训练torch.cuda.amp显存能省 30% 左右。4.3 现象验证集 IoU 很高但 predict.py 跑出来全是背景原因类别不平衡。裂缝像素占比极低模型学会全预测背景就能拿到高准确率但 IoU 对少数类不敏感。或者predict.py里的阈值设错了argmax 之后没有做后处理。解决换DiceLoss或加类别权重weighttorch.tensor([1.0, 10.0])。predict.py里输出 argmax 后检查每个类别的像素占比如果少数类占比为 0说明模型没学到需要回头查标签映射和损失函数。4.4 现象split_data.py 跑完训练集和验证集有同名文件原因脚本按文件名排序后直接切片如果DATASET里图像和标签分两个文件夹且文件名不完全对应切片后可能错位。解决划分前先做一次配对校验只保留图像和标签都存在的文件对。划分后打印三集的文件名列表肉眼扫一遍有没有重叠。这个检查花不了一分钟但能省掉后面几小时的无效训练。4.5 现象Windows 下 DataLoader 卡死不动原因num_workers 0时Windows 的进程启动方式与 Linux 不同PyTorch 的 DataLoader 在 Windows 上容易死锁。解决把num_workers设为 0或者把训练代码包在if __name__ __main__:里。后者是标准做法但很多人写脚本时忘了加导致多进程反复启动主模块。5. 进阶用法从 predict.py 到指标验证以及我改 U-Net 的一个习惯predict.py不只是拿来看效果的它可以改成批量推理脚本输出每张图的预测掩膜和置信度再和标签对比算 IoU、Dice、Precision、Recall。我一般会在predict.py里加一段指标计算而不是另写脚本因为预测和评估用的是同一套预处理逻辑分开写容易不一致。import torch import numpy as np from Unet import UNet from pre_process import preprocess_image def compute_iou(pred, target, num_classes2): ious [] for cls in range(num_classes): inter ((pred cls) (target cls)).sum() union ((pred cls) | (target cls)).sum() # 少数类可能 union 为 0跳过避免除零 if union 0: continue ious.append(inter / union) return np.mean(ious) if ious else 0.0 model UNet(in_ch3, num_classes2).cuda() model.load_state_dict(torch.load(best_unet.pth)) model.eval() img preprocess_image(DATASET/test/images/sample.tif) mask preprocess_image(DATASET/test/masks/sample.tif) # 实际用 mask 读取逻辑 with torch.no_grad(): logits model(torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0).cuda()) pred logits.argmax(dim1).squeeze().cpu().numpy() print(fIoU: {compute_iou(pred, mask):.4f})这段代码里compute_iou对每个类别单独算再平均比整体像素准确率更能反映分割质量。如果某个类别 union 为 0说明测试集里没有这个类跳过而不是算 0否则指标会被拉低。我改 U-Net 的一个习惯在Unet.py的out层之前加一个Dropout2d(0.1)。工业数据集样本少模型容易过拟合加轻量 dropout 后验证集 loss 通常更稳。这个改动不影响推理因为model.eval()时 dropout 自动关闭。另外如果DATASET里的叶片图像背景复杂可以在dataset.py里加随机裁剪让模型多见局部区域对裂缝这种小目标提升明显。从那以后我每次拿到新的分割数据集都强制先跑一遍split_data.py的配对校验和dataset.py的 mask unique 检查再开始训练。这两个检查加起来不到五分钟但能挡掉后面大部分的玄学问题。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询