ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Unet++裂缝分割实战:从数据标注到多尺度TTA的完整指南

Unet++裂缝分割实战:从数据标注到多尺度TTA的完整指南 简介本资源面向计算机视觉初学者与图像分割方向的开发者提供一套基于Unet网络模型对混凝土墙面、道路裂缝进行像素级分割的完整Python项目可用于裂缝检测、结构健康监测等工程场景的实战练习。压缩包共约2000个文件以1877张png与116张jpg图像为主涵盖原始裂缝图片及对应标注掩膜另有5个py脚本承载模型训练与推理逻辑、2个txt说明文件整体约320.54MB采用7z格式打包。数据集为2类别、约2300张图像及标注规模适中便于快速复现。训练环节支持Adam、SGD、RMSProp多种优化器损失函数采用BCE逻辑损失学习率可选恒定、余弦退火与step衰减策略训练过程会输出最优与最终权重并生成数据预处理可视化图、dice与loss曲线及训练日志便于观察收敛过程与调参对比。目前已有223人学习适合希望掌握Unet分割全流程、积累裂缝检测项目经验的读者参考。1. 裂缝分割这件事为什么 Unet 比你想的更值得上手混凝土墙面和道路裂缝的自动检测本质上是一个像素级二分类问题每个像素要么是裂缝要么是背景。听起来简单但真正做过的人都知道裂缝的宽度可能只有几个像素背景纹理却极其复杂——混凝土的蜂窝麻面、道路的骨料颗粒、光照不均产生的阴影都会让模型把背景误判成裂缝。我最早用传统阈值分割做这件事换了三种滤波核一到阴天拍摄的墙面图就集体翻车。Unet 在这个任务上的价值在于它的嵌套密集跳跃连接。普通 Unet 的跳跃连接是浅层特征直接拼到深层语义鸿沟比较大Unet 在编码器和解码器之间插入了一系列嵌套的卷积层让不同尺度的特征先“对齐”再融合。对于裂缝这种细长、断续、对比度低的目标这个设计能明显减少漏检和断线。这篇笔记面向的是想用 Python 从零跑通裂缝分割的从业者——不管你是做建筑检测、道路养护还是想拿这个方向入门图像分割下面这套流程和参数都能直接复现。2. 数据准备与标注裂缝数据集怎么攒、怎么转、怎么分2.1 裂缝数据的来源与标注方式裂缝分割的数据集不像 ImageNet 那样现成常见做法是自采加公开数据混合。公开数据方面混凝土裂缝领域有几个常被引用的数据集比如 SDNET2018、CrackForest道路裂缝方面有 Crack500、DeepCrack。这些数据集规模不大但作为预训练或验证够用。自采的话手机或工业相机都行关键是拍摄距离和光照要尽量覆盖实际部署场景——我一般会刻意拍一部分逆光、一部分阴影遮挡的图否则模型上线后遇到这些情况会崩。标注格式上裂缝分割需要的是像素级 mask不是边界框。常用工具是 LabelMe 导出 JSON 再转 PNG或者用 CVAT 直接画多边形。标注时有一个血泪经验裂缝边缘的判定标准要统一。同一条裂缝不同人标出来的宽度可能差一倍这会让模型学到一个模糊的边界。我的做法是定一个规则——以裂缝最暗的中心线为基准向两侧扩展到灰度明显回升的位置标注时按这个规则执行。2.2 从原始图像到训练对格式转换与划分脚本假设你已经有了原图和一个存放 mask 的文件夹下面这段脚本负责把数据整理成images/和masks/一一对应的结构并按 8:1:1 划分训练、验证、测试集。import os import random import shutil from pathlib import Path # 原始数据路径 RAW_IMG_DIR Path(raw/images) RAW_MASK_DIR Path(raw/masks) # 输出路径 OUT_DIR Path(dataset) SPLIT_RATIO {train: 0.8, val: 0.1, test: 0.1} random.seed(42) def build_dataset(): # 收集所有原图文件名不含扩展名 img_files sorted([f.stem for f in RAW_IMG_DIR.glob(*.jpg)]) # 只保留同时存在 mask 的样本 valid [name for name in img_files if (RAW_MASK_DIR / f{name}.png).exists()] random.shuffle(valid) n len(valid) n_train int(n * SPLIT_RATIO[train]) n_val int(n * SPLIT_RATIO[val]) splits { train: valid[:n_train], val: valid[n_train:n_train n_val], test: valid[n_train n_val:] } for split, names in splits.items(): img_out OUT_DIR / split / images mask_out OUT_DIR / split / masks img_out.mkdir(parentsTrue, exist_okTrue) mask_out.mkdir(parentsTrue, exist_okTrue) for name in names: shutil.copy(RAW_IMG_DIR / f{name}.jpg, img_out / f{name}.jpg) shutil.copy(RAW_MASK_DIR / f{name}.png, mask_out / f{name}.png) print(f{split}: {len(names)} samples) if __name__ __main__: build_dataset()这段脚本的逻辑很直接先取原图和 mask 文件名的交集保证每个样本都有对应标注然后用固定随机种子打乱按比例切分。参数方面SPLIT_RATIO可以根据数据量调整如果样本少于 500 张建议把验证集比例降到 0.05把更多数据留给训练。random.seed(42)是为了保证每次运行划分结果一致方便复现实验。注意mask 必须是单通道二值图裂缝像素值为 1或 255背景为 0。如果标注工具导出的是彩色 mask需要先转成灰度再二值化否则训练时损失计算会出错。2.3 数据增强裂缝场景下哪些增强有效、哪些是负收益裂缝分割的数据增强不能照搬分类任务的套路。水平翻转、垂直翻转、90 度旋转是安全的因为裂缝的走向不依赖绝对方向。随机裁剪也有效尤其是当原图分辨率较高时裁剪到 512×512 能让模型关注局部细节。但有几类增强要慎用。颜色抖动中的色调偏移对裂缝检测帮助不大因为裂缝的判别主要靠亮度和纹理对比色调变化反而可能让模型学到无关特征。高斯模糊要控制强度轻微的模糊可以模拟失焦但强模糊会把细裂缝直接抹掉相当于给模型喂了错误标签。Cutout 或随机遮挡也要小心如果遮挡恰好盖住裂缝而 mask 没有相应调整模型会学到“这里没有裂缝”的错误信息。我一般用的增强组合是随机水平翻转p0.5、随机垂直翻转p0.3、随机旋转 90 度的整数倍p0.5、随机裁剪到 512×512p1.0。这套组合在多个裂缝数据集上验证过稳定且不会引入标签噪声。3. Unet 模型搭建嵌套跳跃连接怎么实现、参数怎么定3.1 Unet 的核心结构差异与选型理由Unet 和普通 Unet 最大的区别在于跳跃连接的处理方式。普通 Unet 是把编码器第 i 层的特征直接拼接到解码器对应层而 Unet 在编码器和解码器之间构建了一个密集的卷积块网格。以四层为例编码器输出记为 X(0,0) 到 X(0,4)解码器节点 X(i,j) 会接收同一层的前一个节点 X(i,j-1) 和下一层的上采样结果同时还会接收同一列所有浅层节点的特征。这个设计的实际收益是浅层的高分辨率特征在融合前会经过额外的卷积层做非线性变换缩小了与深层特征的语义差距。对于裂缝这种需要精确定位的任务这个差距缩小意味着边界更准、断线更少。选型上如果你的数据量在几千张以内建议用 Unet 而不是普通 Unet因为嵌套结构本身有一定的正则化效果。如果数据量很大几万张以上普通 Unet 加足够的训练轮次也能达到接近的效果但 Unet 收敛通常更快。3.2 用 PyTorch 实现一个可训练的 Unet下面是一个精简但完整的 Unet 实现编码器用 ResNet34 的预训练权重解码器部分按嵌套结构搭建。import torch import torch.nn as nn import torchvision.models as models class ConvBlock(nn.Module): 两次 3x3 卷积 BN ReLU def __init__(self, in_ch, out_ch): super().__init__() self.block nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1, biasFalse), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1, biasFalse), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.block(x) class UnetPlusPlus(nn.Module): def __init__(self, in_ch3, out_ch1, deep_supervisionTrue): super().__init__() self.deep_supervision deep_supervision # 编码器ResNet34 的前几层 resnet models.resnet34(weightsmodels.ResNet34_Weights.IMAGENET1K_V1) self.enc0 nn.Sequential(resnet.conv1, resnet.bn1, resnet.relu, resnet.maxpool) # 1/4 self.enc1 resnet.layer1 # 1/4 self.enc2 resnet.layer2 # 1/8 self.enc3 resnet.layer3 # 1/16 self.enc4 resnet.layer4 # 1/32 # 解码器节点通道数统一设为 64 filters [64, 64, 128, 256, 512] self.up nn.Upsample(scale_factor2, modebilinear, align_cornersTrue) # 嵌套卷积块按 Unet 的节点命名 self.conv_0_1 ConvBlock(filters[0] filters[1], 64) self.conv_1_1 ConvBlock(filters[1] filters[2], 64) self.conv_2_1 ConvBlock(filters[2] filters[3], 64) self.conv_3_1 ConvBlock(filters[3] filters[4], 64) self.conv_0_2 ConvBlock(64 * 2 filters[1], 64) self.conv_1_2 ConvBlock(64 * 2 filters[2], 64) self.conv_2_2 ConvBlock(64 * 2 filters[3], 64) self.conv_0_3 ConvBlock(64 * 3 filters[1], 64) self.conv_1_3 ConvBlock(64 * 3 filters[2], 64) self.conv_0_4 ConvBlock(64 * 4 filters[1], 64) # 输出层每个监督节点一个 1x1 卷积 self.out_0_1 nn.Conv2d(64, out_ch, 1) self.out_0_2 nn.Conv2d(64, out_ch, 1) self.out_0_3 nn.Conv2d(64, out_ch, 1) self.out_0_4 nn.Conv2d(64, out_ch, 1) def forward(self, x): # 编码器 x0_0 self.enc0(x) # 1/4 x1_0 self.enc1(x0_0) # 1/4 x2_0 self.enc2(x1_0) # 1/8 x3_0 self.enc3(x2_0) # 1/16 x4_0 self.enc4(x3_0) # 1/32 # 第一列嵌套 x0_1 self.conv_0_1(torch.cat([x0_0, self.up(x1_0)], dim1)) x1_1 self.conv_1_1(torch.cat([x1_0, self.up(x2_0)], dim1)) x2_1 self.conv_2_1(torch.cat([x2_0, self.up(x3_0)], dim1)) x3_1 self.conv_3_1(torch.cat([x3_0, self.up(x4_0)], dim1)) # 第二列 x0_2 self.conv_0_2(torch.cat([x0_0, x0_1, self.up(x1_1)], dim1)) x1_2 self.conv_1_2(torch.cat([x1_0, x1_1, self.up(x2_1)], dim1)) x2_2 self.conv_2_2(torch.cat([x2_0, x2_1, self.up(x3_1)], dim1)) # 第三列 x0_3 self.conv_0_3(torch.cat([x0_0, x0_1, x0_2, self.up(x1_2)], dim1)) x1_3 self.conv_1_3(torch.cat([x1_0, x1_1, x1_2, self.up(x2_2)], dim1)) # 第四列 x0_4 self.conv_0_4(torch.cat([x0_0, x0_1, x0_2, x0_3, self.up(x1_3)], dim1)) # 输出 out self.out_0_4(x0_4) out nn.functional.interpolate(out, scale_factor4, modebilinear, align_cornersTrue) if self.deep_supervision and self.training: d1 nn.functional.interpolate(self.out_0_1(x0_1), scale_factor4, modebilinear, align_cornersTrue) d2 nn.functional.interpolate(self.out_0_2(x0_2), scale_factor4, modebilinear, align_cornersTrue) d3 nn.functional.interpolate(self.out_0_3(x0_3), scale_factor4, modebilinear, align_cornersTrue) return [out, d1, d2, d3] return out这段代码的关键点在于嵌套节点的拼接顺序。以x0_2为例它接收了x0_0、x0_1和x1_1的上采样结果这正是 Unet 密集连接的核心。通道数方面我统一把解码器节点设为 64这是为了控制显存占用如果你的显卡显存充足比如 24GB 以上可以适当增加到 128分割精度会有小幅提升。深度监督deep supervision在训练时返回四个尺度的输出推理时只返回最终输出。这个机制让浅层节点也能收到梯度加速收敛。参数deep_supervision默认开启如果你发现训练不稳定可以先关掉它等模型能正常收敛后再打开。3.3 损失函数与评估指标的选择裂缝分割有一个绕不开的问题正负样本极度不平衡。裂缝像素通常只占全图的 2% 到 10%如果用普通交叉熵模型会倾向于全部预测为背景准确率看起来很高但裂缝一个都分不出来。我一般用 Dice Loss 加 BCE Loss 的组合。Dice Loss 直接优化预测 mask 和真实 mask 的重叠度对不平衡数据更鲁棒BCE 提供稳定的梯度。权重上Dice 占 0.7BCE 占 0.3这个比例在多个裂缝数据集上表现稳定。评估指标不能只看准确率。我主要看三个IoU交并比、Dice 系数、以及裂缝的召回率。召回率尤其重要因为漏检一条裂缝的代价远大于误检——误检可以人工复核漏检可能导致结构安全隐患。import torch.nn as nn class DiceBCELoss(nn.Module): def __init__(self, dice_weight0.7, bce_weight0.3): super().__init__() self.dice_weight dice_weight self.bce_weight bce_weight self.bce nn.BCEWithLogitsLoss() def forward(self, pred, target): # pred: logits, target: 0/1 bce_loss self.bce(pred, target) pred_sigmoid torch.sigmoid(pred) # 计算 Dice intersection (pred_sigmoid * target).sum() union pred_sigmoid.sum() target.sum() dice_loss 1 - (2 * intersection 1e-6) / (union 1e-6) return self.dice_weight * dice_loss self.bce_weight * bce_loss参数说明dice_weight和bce_weight可以根据验证集表现微调。如果发现模型对细裂缝不敏感把 Dice 权重提高到 0.8如果训练初期损失震荡严重把 BCE 权重提高到 0.4 帮助稳定。平滑项1e-6是防止分母为零不要去掉。4. 训练、调参与推理从能跑到跑好的关键设置4.1 训练脚本与超参数配置训练脚本的核心是数据加载、模型前向、损失计算和反向传播。下面是一个最小可运行的训练循环包含验证和模型保存。import torch from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image import numpy as np import os class CrackDataset(Dataset): def __init__(self, root, splittrain, img_size512): self.img_dir os.path.join(root, split, images) self.mask_dir os.path.join(root, split, masks) self.names sorted(os.listdir(self.img_dir)) self.img_size img_size self.img_transform transforms.Compose([ transforms.Resize((img_size, img_size)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img Image.open(os.path.join(self.img_dir, name)).convert(RGB) mask_name name.replace(.jpg, .png) mask Image.open(os.path.join(self.mask_dir, mask_name)).convert(L) img self.img_transform(img) mask transforms.Resize((self.img_size, self.img_size))(mask) mask torch.from_numpy(np.array(mask)).float() / 255.0 mask (mask 0.5).float().unsqueeze(0) return img, mask def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 for imgs, masks in loader: imgs, masks imgs.to(device), masks.to(device) optimizer.zero_grad() outputs model(imgs) if isinstance(outputs, list): # 深度监督对每个输出计算损失并求和 loss sum(criterion(o, masks) for o in outputs) / len(outputs) else: loss criterion(outputs, masks) loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(loader) # 超参数 DEVICE torch.device(cuda if torch.cuda.is_available() else cpu) EPOCHS 100 BATCH_SIZE 4 LR 1e-4 WEIGHT_DECAY 1e-5 model UnetPlusPlus(in_ch3, out_ch1).to(DEVICE) criterion DiceBCELoss(dice_weight0.7, bce_weight0.3) optimizer torch.optim.AdamW(model.parameters(), lrLR, weight_decayWEIGHT_DECAY) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxEPOCHS) train_dataset CrackDataset(dataset, splittrain) train_loader DataLoader(train_dataset, batch_sizeBATCH_SIZE, shuffleTrue, num_workers4) for epoch in range(EPOCHS): loss train_one_epoch(model, train_loader, optimizer, criterion, DEVICE) scheduler.step() print(fEpoch {epoch1}/{EPOCHS}, Loss: {loss:.4f}) if (epoch 1) % 10 0: torch.save(model.state_dict(), funetpp_epoch{epoch1}.pth)超参数方面BATCH_SIZE4是针对 512×512 输入和 8GB 显存的保守设置。如果显存不够把输入降到 384×384或者把 batch size 降到 2。学习率用1e-4配合 AdamW 和余弦退火这是我在裂缝分割任务上最常用的组合。WEIGHT_DECAY1e-5是为了轻微正则化防止过拟合。注意如果训练损失在前 10 个 epoch 内不下降先检查 mask 的像素值是否在 0 到 1 之间。我见过好几次因为 mask 是 0/255 导致损失爆炸的情况。4.2 推理与后处理把概率图变成可用的裂缝掩码推理阶段模型输出的是 logits经过 sigmoid 后得到每个像素是裂缝的概率。直接取 0.5 作为阈值往往不够好因为裂缝边缘的概率值通常在 0.3 到 0.7 之间波动。我的做法是在验证集上扫一遍阈值选使 F1 分数最高的那个值通常在 0.4 到 0.6 之间。后处理还包括去除小连通区域。裂缝应该是连续的细长区域如果预测结果里有一些孤立的几个像素大概率是噪声。用 OpenCV 的connectedComponentsWithStats去掉面积小于 50 像素的连通域能明显减少误检。import cv2 import numpy as np import torch def predict(model, image_path, threshold0.5, min_area50): model.eval() img Image.open(image_path).convert(RGB) img_tensor transforms.Compose([ transforms.Resize((512, 512)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])(img).unsqueeze(0).to(DEVICE) with torch.no_grad(): output model(img_tensor) if isinstance(output, list): output output[0] prob torch.sigmoid(output).squeeze().cpu().numpy() # 二值化 binary (prob threshold).astype(np.uint8) * 255 # 去除小连通域 num_labels, labels, stats, _ cv2.connectedComponentsWithStats( binary, connectivity8) clean np.zeros_like(binary) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] min_area: clean[labels i] 255 return cleanthreshold和min_area是两个需要根据实际数据调的参数。如果漏检严重降低阈值到 0.35如果误检太多提高到 0.6。min_area对于高分辨率图像可以设到 100 甚至 200对于低分辨率图像设 30 到 50 即可。4.3 训练过程监控与早停策略裂缝分割的训练很容易过拟合尤其是自采数据只有几百张的时候。我一般会监控验证集的 Dice 系数如果连续 15 个 epoch 没有提升就触发早停回滚到验证集最优的模型权重。另一个实用技巧是保存每个 epoch 的验证集预测图每隔几个 epoch 肉眼看一下。有时候指标在涨但预测结果反而变得更碎、更断这通常是过拟合的早期信号。指标是黑匣子肉眼看的图才是后悔药。5. 裂缝分割避坑记录从数据到部署的五个翻车现场5.1 现象训练损失正常下降但验证集 Dice 始终在 0.3 以下原因最常见的是 mask 和原图没有对齐。标注工具导出时可能做了 resize或者文件名匹配错了导致模型在学“错位的标签”。另一个可能是数据增强中的随机裁剪对原图和 mask 用了不同的随机参数。解决写一个可视化脚本把原图和 mask 叠加显示随机抽 20 张检查。如果是裁剪不同步确保 Dataset 中对原图和 mask 使用同一个随机种子或同一组变换参数。5.2 现象模型在训练集上表现很好一到测试集就大量漏检原因训练集和测试集的拍摄条件差异太大。比如训练集全是晴天顺光测试集有大量阴影或逆光。裂缝的视觉特征在光照变化下波动很大模型没有学到不变的特征。解决在数据增强中加入随机亮度、对比度调整幅度控制在 ±20% 以内。如果测试集条件已知刻意采集一些类似条件的图加入训练集。另外可以在输入前做局部直方图均衡化减少光照影响。5.3 现象预测结果中裂缝断断续续不连续原因Unet 的深层特征分辨率较低细裂缝在深层可能已经丢失。如果只使用最终输出浅层的细节信息利用不充分。解决开启深度监督让浅层节点也参与损失计算。推理时可以把多个尺度的输出做加权平均浅层输出权重 0.3最终输出权重 0.7。另外适当增大输入分辨率比如从 512 提到 768也能改善细裂缝的连续性。5.4 现象显存溢出batch size 降到 1 还是报错原因Unet 的嵌套结构参数量和中间激活值比普通 Unet 大不少。如果输入分辨率是 1024×1024即使 batch size 为 1显存占用也可能超过 8GB。解决用混合精度训练AMP能省大约 40% 显存。如果还不够把输入降到 512×512或者用梯度累积模拟更大的 batch size。另一个选择是把编码器换成更轻量的 MobileNetV2但分割精度会有所下降。5.5 现象推理速度太慢单张图要好几秒原因Unet 的嵌套卷积块在推理时计算量较大尤其是高分辨率输入。如果部署在 CPU 上速度会更慢。解决推理时关闭深度监督只走最终输出路径。用 TorchScript 或 ONNX 导出模型再做量化INT8速度能提升 2 到 3 倍。如果对精度要求不极端可以把输入降到 384×384速度提升明显Dice 通常只掉 1 到 2 个百分点。6. 把 Unet 裂缝分割推到可用边缘的一个技巧多尺度测试时增强模型训练完之后如果想把 Dice 再往上推一两个点又不想重新训练我一般会做多尺度测试时增强multi-scale TTA。做法很简单把同一张测试图缩放到 0.75 倍、1.0 倍、1.25 倍分别推理然后把三张概率图缩回原尺寸取平均再二值化。这个技巧对裂缝分割特别有效因为不同尺度的裂缝在不同分辨率下的响应不同。细裂缝在放大后更容易被检测到粗裂缝在缩小后整体结构更清晰。平均之后漏检和误检会互相抵消一部分。def predict_tta(model, image_path, scales[0.75, 1.0, 1.25], threshold0.5): model.eval() img Image.open(image_path).convert(RGB) orig_size img.size # (W, H) prob_sum None for scale in scales: new_size (int(orig_size[0] * scale), int(orig_size[1] * scale)) img_scaled img.resize(new_size, Image.BILINEAR) tensor transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])(img_scaled).unsqueeze(0).to(DEVICE) with torch.no_grad(): out model(tensor) if isinstance(out, list): out out[0] prob torch.sigmoid(out) # 缩回原尺寸 prob torch.nn.functional.interpolate( prob, size(orig_size[1], orig_size[0]), modebilinear, align_cornersTrue) prob prob.squeeze().cpu().numpy() if prob_sum is None: prob_sum prob else: prob_sum prob prob_avg prob_sum / len(scales) binary (prob_avg threshold).astype(np.uint8) * 255 return binary参数方面scales列表一般取三个值就够了太多会拖慢推理速度。threshold在 TTA 之后可以适当调低因为平均操作会让概率分布更平滑我通常从 0.45 开始试。这个技巧的代价是推理时间变成三倍如果对实时性要求高可以只保留 0.75 和 1.25 两个尺度效果打八折但速度快一半。还有一个验证 TTA 是否有效的习惯在测试集上分别跑单尺度和 TTA对比 Dice 和召回率。如果 TTA 后 Dice 没涨甚至降了说明模型本身已经过拟合到某个特定尺度这时候应该回头检查训练时的数据增强是否过于单一。我自己在这个方向上踩过最大的坑是一开始迷信“模型越复杂越好”把 Unet 的每一层都加宽结果训练时间翻倍Dice 只涨了 0.5 个点部署时还因为显存问题折腾了一周。后来把通道数降回来用 TTA 补精度整体性价比高得多。裂缝分割这个任务数据质量和后处理的重要性往往不亚于模型结构本身。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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