news 2026/9/14 2:45:30

基于U-Net的风机叶片语义分割实战:从数据预处理到推理部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于U-Net的风机叶片语义分割实战:从数据预处理到推理部署

简介:面向风电叶片监测场景的风扇语义分割数据集及配套Python训练代码,适合计算机视觉研究人员、风电运维算法工程师及深度学习者使用。全部数据由1994个tif文件构成,包含风扇叶片图像及对应标签图,涵盖多种工作环境和光照条件,可用于磨损、裂缝、污渍等区域的分割识别;6个Python脚本覆盖数据集随机划分、图像预处理、U-Net网络定义、模型训练与预测推理,可直接运行或在此框架上做二次开发。压缩包共2000个文件,整体大小约810.93MB,当前已有112人学习。这份资源既提供了可直接用于训练的标注数据,也给出了从数据准备到模型部署的完整代码框架,可有效缩短风力发电机叶片检测模型的开发周期,适用于高校课题、算法对比与工程预研等场景。

1. 从 TCGA 文件名到风机叶片:这个数据集的第一眼陷阱

拿到资源先看目录,几个.tif文件以TCGA_DU_开头,第一反应是病理切片数据——TCGA 是癌症基因组图谱的缩写。但文件名只是历史遗留的命名习惯,里面装的全是风力发电机风扇叶片图像和对应的逐像素标签。这种「文件名迷惑」在工业数据集里很常见,真正需要关注的是结构和代码。

这套资源解决的痛点很具体:风机叶片在图像里是细长、弯曲、与背景(天空、山体、云层)对比度不稳定的目标。用目标检测只能给矩形框,框里既有叶片也有背景;用语义分割才能逐像素区分叶片与背景,为后续磨损、裂缝检测做前置分割。对做工业视觉语义分割、想快速跑通 U-Net 基线、或者要自建分割数据集的人来说,这套代码把数据划分、预处理、训练、预测串成了一条完整链路,省去从零搭 pipeline 的时间。资源不是生产级工程,但作为基线足够扎实。

2. 数据组织与预处理:先把标签和图像对齐

在处理任何语义分割任务之前,第一步永远是确认标签格式是否与模型输出一致。这套资源的 DATASET 文件夹下存放图像和对应 mask,虽然未给出具体目录树,但根据dataset.py的常见写法可以推断:图像与标签同名、同尺寸、统一放在两个子目录或混放后用后缀区分。先按这个思路拆pre_process.py

2.1 预处理脚本的逻辑拆解

pre_process.py负责原始图像到模型输入的转换。针对风机叶片场景,至少有四个处理步骤是必须的:

import cv2 import numpy as np from glob import glob import os def preprocess_images(src_dir, dst_dir, target_size=(512, 512)): os.makedirs(dst_dir, exist_ok=True) for img_path in glob(os.path.join(src_dir, "*.tif")): img = cv2.imread(img_path, cv2.IMREAD_UNCHANGED) # 如果 tif 是 16bit,需先转到 8bit if img.dtype == np.uint16: img = (img / 257).astype(np.uint8) # 叶片图像常因逆光产生强烈阴影,做 CLAHE 增强局部对比 lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)) l = clahe.apply(l) lab = cv2.merge([l, a, b]) img = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR) # 统一尺寸,mask 用最近邻插值避免标签错乱 img = cv2.resize(img, target_size, interpolation=cv2.INTER_LINEAR) name = os.path.basename(img_path).replace(".tif", ".jpg") cv2.imwrite(os.path.join(dst_dir, name), img, [cv2.IMWRITE_JPEG_QUALITY, 95])

这段代码把 16bit 深度的 tif 压缩到 8bit,原因是大部分语义分割模型输入约定在 0-255 范围,直接用 16bit 数值会导致权重初始化后梯度异常。CLAHE 局部对比度增强针对的是风机叶片最常见的逆光场景——叶片背光面与天空灰度接近,不做增强的话分割模型很容易把叶片暗部并进背景。tileGridSize=(8, 8)表示将图像分成 8×8 的块分别做直方图均衡,太大无法修正局部光照,太小则过度放大噪声。

标签图(mask)一般不需要做 CLAHE,直接转灰度 + 二值化 + 最近邻缩放。常见错误是用线性插值缩放 mask,这会在叶片边缘产生介于 0 和 1 之间的灰度值,训练时 loss 无法收敛。这里补充一句:预处理脚本应当将src_dirdst_dir分离,避免处理后的图像覆盖原始数据,一旦参数调错还能回退。

2.2 数据划分的一个隐蔽问题

split_data.py的作用是划分 train/val/test。表面上是随机 shuffle 后按比例切分,但工业数据里有个容易被忽略的问题:风机的多张连续帧图像高度相似,如果随机分割,同一风机的相似帧会同时进入训练集和验证集,导致验证分数虚高。

import os import random import shutil from glob import glob def split_dataset(img_dir, mask_dir, train_ratio=0.8, val_ratio=0.1, seed=42): images = sorted(glob(os.path.join(img_dir, "*.jpg"))) random.seed(seed) random.shuffle(images) total = len(images) train_end = int(total * train_ratio) val_end = int(total * (train_ratio + val_ratio)) splits = { "train": images[:train_end], "val": images[train_end:val_end], "test": images[val_end:] } for split_name, img_list in splits.items(): os.makedirs(f"data/{split_name}/images", exist_ok=True) os.makedirs(f"data/{split_name}/masks", exist_ok=True) for img_path in img_list: base = os.path.basename(img_path).replace(".jpg", ".png") shutil.copy(img_path, f"data/{split_name}/images/{base}") mask_path = os.path.join(mask_dir, base) if os.path.exists(mask_path): shutil.copy(mask_path, f"data/{split_name}/masks/{base}")

如果有时间戳或风机编号信息,更好的做法是按风机分组后划分——同一风机的所有帧进同一个集合。资源里没有给出分组信息,退而求其次用随机划分时,建议固定seed=42,保证每次实验可复现。验证集的作用是判断早停时机,test 集是最终评估标准,二者混用会导致模型选择失真。train_ratio=0.8, val_ratio=0.1的比例在数据集超过 500 张时是合理起点;数据少则调整到 0.7/0.15/0.15,保证验证集有足够样本。

2.3 语义分割任务的标签语义

风机叶片分割在标签编码上通常是二分类:0 表示背景(天空、大地、塔架),1 表示叶片像素。之所以不做多分类,是因为资源提供的数据只标注了前景/背景,磨损、裂缝、污渍等细粒度类别需要更高成本的标注。二类分割也是验证分割 pipeline 是否跑通的最短路径——先确认模型能精确区分叶片和背景,再谈细粒度分类。如果后续要扩展多分类,只需将mask中的灰度值按类别映射到 0、1、2、3 等索引,损失函数换成CrossEntropyLoss即可。

3. U-Net 结构与实现要点:分割小数据集的理性选择

Unet.py实现了经典的 U-Net 结构。在数据集规模不大的前提下,选择 U-Net 不是因为它先进,而是因为它在小样本语义分割上的收敛速度和标注需求达到最优平衡。风机叶片图像通常只有数百到数千张,Vision Transformer 类模型需要大规模预训练权重才能发挥效果,而 U-Net 从零训练即可获得可接受的精度。

3.1 编码器中的关键设计

import torch import torch.nn as nn import torch.nn.functional as F class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv = nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True) ) def forward(self, x): return self.conv(x) class Down(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.mpconv = nn.Sequential( nn.MaxPool2d(2), DoubleConv(in_ch, out_ch) ) def forward(self, x): return self.mpconv(x) class Up(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.up = nn.ConvTranspose2d(in_ch, in_ch // 2, 2, stride=2) self.conv = DoubleConv(in_ch, out_ch) def forward(self, x1, x2): x1 = self.up(x1) diffY = x2.size()[2] - x1.size()[2] diffX = x2.size()[3] - x1.size()[3] x1 = F.pad(x1, [diffX // 2, diffX - diffX // 2, diffY // 2, diffY - diffY // 2]) x = torch.cat([x2, x1], dim=1) return self.conv(x)

DoubleConv是 U-Net 的基本单元,连续两个 3×3 卷积后接批归一化。批归一化在这类小数据集上的作用是稳定训练——叶片图像的光照分布差异较大,BN 可以缓解不同 batch 之间分布偏移对梯度的影响。Down模块采用 MaxPooling 降采样,保留纹理响应最强的特征;Up模块用转置卷积上采样并用F.pad处理特征图尺寸不一致的问题。torch.cat([x2, x1], dim=1)这一步是跳跃连接的核心:将编码器下采样前的细节特征(位置信息)直接拼到解码器对应层,弥补上采样过程丢失的空间分辨率。叶片边缘的精细分割依赖这些跳跃连接来恢复。

3.2 U-Net 在处理风机叶片上的偏差

叶片这项任务有两个独特之处:目标细长且跨越图像对角线;背景区域占比极大,常超过 90%。

U-Net 能较好地应对这两种情况:

第一,细长目标依赖多尺度特征。编码器下采样四次后,整个叶片可能收缩成几个像素宽的线条,但跳跃连接将每一层的高分辨率特征直接传递给解码器,低层特征保留边缘锐度,高层特征提供语义完整性。实验里如果把跳跃连接去掉,IoU 至少掉 10 个点。

第二,类别不平衡问题。背景占比过高导致模型倾向把所有像素预测为背景。常见做法是在损失函数中加入 Dice 项,缓解前景和背景的像素数量差异。U-Net 的最后一层输出通道数为 1,前向函数如下:

class UNet(nn.Module): def __init__(self, n_channels=3, n_classes=1): super().__init__() self.inc = DoubleConv(n_channels, 64) self.down1 = Down(64, 128) self.down2 = Down(128, 256) self.down3 = Down(256, 512) self.down4 = Down(512, 512) self.up1 = Up(1024, 256) self.up2 = Up(512, 128) self.up3 = Up(256, 64) self.up4 = Up(128, 64) self.outc = nn.Conv2d(64, n_classes, 1) def forward(self, x): x1 = self.inc(x) x2 = self.down1(x1) x3 = self.down2(x2) x4 = self.down3(x3) x5 = self.down4(x4) x = self.up1(x5, x4) x = self.up2(x, x3) x = self.up3(x, x2) x = self.up4(x, x1) logits = self.outc(x) return logits

输入 3 通道彩色图,输出是未经过 sigmoid 的 logits,形状为(B, 1, H, W)。第一层通道数从 64 起是在显存和表达能力的折中。如果显卡只有 6GB,可以把初始通道改为 32;如果叶片背景特别复杂,则增到 96——但要注意下采样后每层翻倍,总显存消耗会指数级增长。n_classes=1表示二分类输出;若做多分类,输出通道数改为类别数,并配合CrossEntropyLoss

4. 训练闭环:从 Dataset 加载到 loss 收敛

分割项目的核心难点不在模型而在训练 pipeline。dataset.pytrain.py各自承担关键职责:Dataset 决定模型看到什么数据,训练循环决定模型如何从数据中学到区分叶片和背景的能力。

4.1 Dataset 的加载方式与 On-the-fly 增强

dataset.py的标准实现逻辑是:遍历图像列表,在__getitem__中同步读取图像和 mask,应用数据增强,返回(tensor_image, tensor_mask)对。

import torch from torch.utils.data import Dataset import cv2 import numpy as np from albumentations import Compose, HorizontalFlip, RandomBrightnessContrast class WindTurbineDataset(Dataset): def __init__(self, img_dir, mask_dir, image_size=(512, 512), augment=False): self.img_paths = sorted(os.listdir(img_dir)) self.mask_dir = mask_dir self.img_dir = img_dir self.image_size = image_size self.augment = augment def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img_name = self.img_paths[idx] img = cv2.imread(os.path.join(self.img_dir, img_name)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask = cv2.imread(os.path.join(self.mask_dir, img_name).replace('.jpg', '.png'), cv2.IMREAD_GRAYSCALE) mask = (mask > 127).astype(np.float32) if self.augment: aug = Compose([ HorizontalFlip(p=0.5), RandomBrightnessContrast(p=0.3) ]) augmented = aug(image=img, mask=mask) img, mask = augmented["image"], augmented["mask"] img = torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 mask = torch.from_numpy(mask).unsqueeze(0).float() return img, mask

这里是几个值得注意的参数。image_size=(512, 512)是训练分辨率的默认值,风机叶片原始 tif 往往是 1024 以上,直接全分辨率训练显存不够;512 是速度与精度的平衡点。RandomBrightnessContrast是专门针对叶片场景设计的增强——叶片在不同天候下的亮度变化极大,模拟这些光照变化能提高模型泛化性。mask 读取后做了(mask > 127)二值化,防止标签图像中的 JPEG 压缩伪影或者标注软化边缘产生中间值。对比常见的RandomResizedCrop,这里刻意没有使用,因为叶片占图像比例较大,随机裁剪可能把整片叶片裁掉一半,造成不必要的学习困难。

4.2 损失函数与训练超参

train.py中损失函数的选择直接影响分割边缘质量。纯BCEWithLogitsLoss在背景占 90% 以上时会让模型快速收敛到全背景预测;加入 Dice loss 将梯度重心放到前景区域,这是工业分割项目的标准组合。

import torch import torch.nn.functional as F def dice_loss(pred, target, smooth=1.0): pred = torch.sigmoid(pred) intersection = (pred * target).sum(dim=(2, 3)) dice = (2.0 * intersection + smooth) / (pred.sum(dim=(2, 3)) + target.sum(dim=(2, 3)) + smooth) return 1.0 - dice.mean() def combined_loss(pred, target): bce = F.binary_cross_entropy_with_logits(pred, target) dice = dice_loss(pred, target) return bce + dice

dice_losssmooth=1.0防止分母为 0,同时缓解小区块(如叶片尖端)带来的数值不稳定。pred.sum(dim=(2, 3))是对 H/W 维求和,得到的形状是(B,)——每个样本单独计算 Dice,再取平均。这样做的原因是不同图像中叶片占比差异大,逐样本归一化比全局归一化稳定。组合损失里 BCE 保证分类正确,Dice 保证前景/背景不平衡时分割区域完整。

训练循环中常用的超参数组合:

参数名推荐值调整说明
batch_size86GB 显存(512×512 输入)上限
epochs100配合早停,实际收敛约 50-80 轮
optimizerAdam, lr=1e-4Adam 适合分割初跑;调优可换 SGD + momentum 0.9
lr_schedulerReduceLROnPlateaupatience=10, factor=0.5
early_stop_patience20连续 20 轮 val_loss 不降则终止

patience=20意味着验证集上连续 20 轮没有更好则停止训练。叶片数据集小,训练波动较大,patience 太小会过早停止;20 是经验值。ReduceLROnPlateau 降低学习率时模型已经从陡峭区域进入平缓区域,factor=0.5 减半是保守策略,防止跳过局部最优点。

4.3 评估指标的计算细节

train.py 里通常每轮结束计算验证集 IoU。二分类场景 IoU 计算如下:

def compute_iou(pred_mask, true_mask): pred_mask = (torch.sigmoid(pred_mask) > 0.5) intersection = (pred_mask & true_mask).sum().float() union = (pred_mask | true_mask).sum().float() iou = intersection / (union + 1e-6) return iou.item()

阈值取0.5是缺省做法。但风机叶片前景占比低,负样本占多数时,0.5 阈值往往得到保守结果(漏检多),实践中可以降低阈值到 0.35 增加召回率。IoU 不能只看单张,要在整个验证集上累计 intersection 和 union 后统一计算,逐张平均和先累积再除有差异,后者更贴近全局指标。

5. 推理部署的三个细节:滑窗、TTA 与可视化输出

predict.py走的是「加载权重 → 推理 → 保存可视化结果」链路,但直接对原图推理会遇到一个实际问题:训练时图像经过 resize,推理时输入任意尺寸会破坏 U-Net 的空间假设。卷积天然支持任意尺寸输入,但多次下采样后特征图尺寸可能不是 2 的整数倍,导致解码器转置卷积与跳跃连接拼接时尺寸不匹配。常见做法是推理时也统一 resize 到与训练一致的 512×512,输出后再 resize 回原始尺寸,但叶片边缘的精细结构会有一定程度损失。更稳妥的做法是滑窗推理:将大图切块,每块输入模型,再按位置拼接。

def sliding_window_predict(model, image, window_size=512, stride=384): model.eval() h, w = image.shape[:2] padded_h = ((h + stride - 1) // stride) * stride padded_w = ((w + stride - 1) // stride) * stride padded = cv2.copyMakeBorder(image, 0, padded_h - h, 0, padded_w - w, cv2.BORDER_REFLECT) prob_map = np.zeros((padded_h, padded_w), dtype=np.float32) weight_map = np.zeros((padded_h, padded_w), dtype=np.float32) for y in range(0, padded_h - window_size + 1, stride): for x in range(0, padded_w - window_size + 1, stride): patch = padded[y:y + window_size, x:x + window_size] patch_tensor = torch.from_numpy(patch).permute(2, 0, 1).unsqueeze(0).float() / 255.0 with torch.no_grad(): logits = model(patch_tensor) prob = torch.sigmoid(logits).cpu().numpy()[0, 0] prob_map[y:y + window_size, x:x + window_size] += prob weight_map[y:y + window_size, x:x + window_size] += 1.0 prob_map /= np.maximum(weight_map, 1.0) return prob_map[:h, :w]

stride=384小于window_size=512,相邻窗口有 128 像素重叠,重叠区域的概率取两次预测的平均值,消除边缘块因缺少上下文导致的预测跳变。BORDER_REFLECT在图像边界处做镜像填充,避免零填充在边缘产生黑色区块影响概率输出。多尺度预测是另一个有用的技巧——同一张图缩放 0.75、1.0、1.25 倍分别推理,再按原始坐标平均,能提高叶片边界稳定性。

最后是可视化输出。分割结果叠加到原图上的推荐方式是生成半透明的绿色蒙版,配合保存概率图供后续分析。标签连通域分析(cv2.connectedComponentsWithStats)可以去除面积小于设定阈值的孤立预测块——这种误检通常来自云层边缘的纹理干扰,而不是真正的叶片。打印最终的 IoU、每类像素数,再输出output/overlay_*.jpg,整条推理链路就完整闭合了。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 2:42:34

FastExcel替代EasyExcel:高并发Excel导出性能优化实战

1. 项目概述:从EasyExcel切换到Apache Fesod的真实动因“再见了EasyExcel,我决定用Apache Fesod”——这句话不是标题党,而是我在连续三个高并发Excel导入导出项目踩坑后,亲手写下的技术迁移声明。过去五年,我主导过12…

作者头像 李华
网站建设 2026/9/14 2:40:49

光学系统设计:波段、光源、光纤与探测器匹配指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 2:37:37

机器学习算法源码实战:从DCGAN到DDPG的工程解析

简介:一套基于Python的机器学习算法设计源码,面向机器学习开发者、学生及科研人员,覆盖数据预处理、特征工程、经典监督/无监督学习和深度学习模型,既可作为算法学习的配套代码,也能帮助快速搭建实验原型。压缩包共35个…

作者头像 李华
网站建设 2026/9/14 2:36:30

猕猴桃遗传转化技术研究与应用

1. 猕猴桃遗传转化的背景与意义猕猴桃作为一种经济价值极高的水果作物,其遗传改良一直是农业生物技术领域的研究热点。传统育种方法周期长、效率低,而遗传转化技术能够直接导入目标基因,大幅缩短育种周期。我在实验室从事猕猴桃遗传转化研究已…

作者头像 李华
网站建设 2026/9/14 2:35:55

基于LSTM的蔬菜价格预测:从数据预处理到模型部署全流程解析

简介:这是一份面向计算机专业毕业设计及课程设计场景的深度学习实战资源,聚焦基于LSTM的蔬菜价格预测任务。项目包含Python源码、项目说明文档与真实蔬菜价格数据集,能够覆盖数据预处理、模型训练、评估与预测的完整流程,适合正在…

作者头像 李华