Unet医学影像分割:一套Python源码跑通从标注到评估的完整管线

发布时间:2026/10/1 11:55:36
Unet医学影像分割:一套Python源码跑通从标注到评估的完整管线
简介基于Unet的医学影像分割系统是一套完整可运行的高分项目包含Python源码、文档说明、安装教程、截图演示、训练数据与模型文件并附有原版论文PDF适合计算机、人工智能、电子信息等专业学生用于毕业设计、课程设计或项目初期立项。代码全部经测试可行作者还提供远程教学与答疑。整个资源包共76个文件主要以Python脚本、图像样本、配置文件和说明文档为主大小仅4.61MB目录结构清晰涵盖网络构建模块、数据集划分与标注转换脚本、训练与预测主流程、UI可视化界面以及基于ISIC数据集的评估结果。已有217人学习下载内含README与截图演示可帮助用户快速理解Unet在医学影像分割中的完整流程也便于在此基础上二次开发或移植到其他分割任务。1. Unet 医学影像分割这套 python 源码不是一个模型文件而是一条完整管线帮学弟调课设时他要把一批皮肤镜图片里的病灶切出来。一开始用分类网络硬改mIoU 只有 0.3换成 Unet 之后一个周末上到 0.85。这就是这套 python 源码的价值它不是孤零零的模型文件而是从 labelme 标注转换、数据划分、训练、指标评估到 UI 预测的完整管线unet_42-master 里二十多个 py 文件加 PDF 原文、模型权重都是为这条管线准备的。适合三类人做毕设课设的学生需要一套能跑通、能出图、能写进论文的医学影像分割基线想在自己数据集上跑 Unet 的从业者需要搞清楚标注格式和参数设置没碰过分割任务的新手想找个从零到一能照抄的样本。数据、模型、教程、截图都在压缩包里环境装好后两个命令就能看到预测效果。我完整跑过三遍才敢说这个项目真正卡人的地方不在模型代码而在数据准备和参数对齐。后面按「环境 → 标注转换 → 训练 → 避坑 → 评估预测」的顺序把每条命令和每个参数讲透。2. 环境与数据准备requirements.txt 和两个标注转换脚本怎么配合解压 unet_42-master 之后第一件事不是读 unet_model.py而是先把环境装到能跑的程度。我第一次跑这套源码时半小时全耗在 torch 和 numpy 版本不匹配上后来才发现压缩包里那份「切换镜像.txt」就是干这个用的——把 pip 默认源换成国内镜像避免装 torch 这种几百 MB 的包时反复超时。这一章按 README.md 推荐的执行顺序拆成三块依赖安装、标注转换、数据划分。2.1 依赖清单与镜像切换requirements.txt 里的版本为什么不能乱动requirements.txt 是纯文本每行一个包名。这份源码的依赖基本是深度学习分割的标配torch、torchvision 负责模型和数据集opencv-python 负责图像读写和标注绘制numpy 处理数组Pillow 辅助图像格式labelme 是标注工具tqdm 显示训练进度matplotlib 画指标曲线。如果你已经装了 python 3.8~3.10接下来的事就是一条命令的问题。# 1) 换 pip 镜像源压缩包里的「切换镜像.txt」就是这个作用 pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple # 2) 安装项目依赖 pip install -r requirements.txt # 3) 确认 torch 能用 GPU python -c import torch; print(torch.__version__, torch.cuda.is_available())pip config set 是写进用户级配置的换源后装其他项目也会走清华镜像速度明显比默认源快不需要就pip config unset global.index-url还原。公司网络访问不了清华源的把 URL 换成阿里云https://mirrors.aliyun.com/pypi/simple/效果一样。第三行命令必须输出 True 才说明 GPU 可用输出 False 也别慌——CPU 版 torch 一样能训练只是慢三五倍后面第 3 章有低显存配置兜底。装 torch 时最容易翻车的是版本和 CUDA 对不上。requirements.txt 里写的版本未必匹配你机器的驱动我一般先单独装 torch 再装其余依赖。默认源装的是较新的 CUDA 版本老显卡会提示 driver too old这时候去 PyTorch 官网复制对应 CUDA 版本的安装命令即可装完再用第三行命令验证。网络不好导致下载中断的话清华源的 torch 目录也提供历史版本选和 requirements.txt 相近的版本号不会有兼容问题。2.2 labelme2seg.py把 labelme 的 JSON 多边形转成单通道掩码labelme 标注后每一张图对应一个 JSON 文件里面每个 shape 记录多边形顶点 points 和类别 label。Unet 训练要的不是这个 JSON而是一张和原图等大的单通道灰度图背景像素 0、病灶像素 1多类分割就是 0、1、2……N-1。labelme2seg.py 干的就是这个转换——遍历 jsons 目录读取坐标后用 OpenCV 填充多边形。# labelme2seg.py 的核心转换逻辑关键摘录 import json import numpy as np import cv2 def json_to_mask(json_path, height, width): with open(json_path, r, encodingutf-8) as f: data json.load(f) mask np.zeros((height, width), dtypenp.uint8) for shape in data[shapes]: pts np.array(shape[points], dtypenp.int32) # 单类分割非 background 的标注统一填 1 class_id 0 if shape[label] background else 1 cv2.fillPoly(mask, [pts], colorclass_id) return mask # 调用时 height/width 必须取原图的 shapecv2.fillPoly 的填充边界默认是抗锯齿的边缘会出现 127 这种中间灰度。如果训练代码没有做二值化这些灰色像素就是脏标签会在评估阶段变成莫名其妙的误差。稳妥做法是在 fillPoly 之后加一行mask (mask 0).astype(np.uint8)硬性保证输出只有 0 和 1。另一个关键点是 height/width 必须来自原图读取时的 shape不能拿 labelme 窗口里显示的尺寸——labelme 保存的坐标永远基于原图像素坐标系缩放只影响显示不影响数据。多类分割时这一段要改成 label 到 id 的映射字典比如{liver: 1, tumor: 2, background: 0}后续 unet_model.py 里的 n_classes 也要对应改成 3。映射关系建议写成一个常量字典放在脚本顶部别散落在循环里不然跑十几个文件后想改类别要到处找。2.3 gen_split.py 与 label2png.py数据划分和格式归一label2png.py 负责把 labels 目录里的掩码统一转成 8bit PNG同时解决两个常见问题一是标注导出的格式可能是 BMP 或 npy格式不统一会读出奇怪的通道数二是原图和掩码的命名要对齐jpg 和 png 后缀不同但文件名前缀必须一致。gen_split.py 负责划分训练集和验证集常见默认是 8:2。划分逻辑不复杂但关键在它保证同一个样本不会同时出现在两边避免数据泄漏——这是分割任务里最容易忽略、又最影响指标真实性的一点。# 完整执行顺序项目根目录下 # 1) JSON → 灰度掩码 python labelme2seg.py --input jsons --output labels # 2) 掩码统一为 8bit PNG并按原图名对齐 python label2png.py --mask-dir labels --img-dir images # 3) 按 8:2 划分训练/验证集 python gen_split.py --train-ratio 0.8 --out data三个脚本跑完后data 目录应当是这样的结构dataset.py 默认按这个路径找图路径内容data/train/images/训练原图.jpgdata/train/masks/训练掩码.pngdata/val/images/验证原图.jpgdata/val/masks/验证掩码.pngtestdata/单独留的测试图片predict.py 演示用划分完先做一次数量核对len(os.listdir(data/train/images))必须等于 mask 目录的文件数不一致就是有标注文件缺失。我在这步踩过最惨的坑是 labelme 标注时原图文件名带中文Windows 下 cv2.imread 读不到返回 None转换和训练全程不报错但数据全是空的——这是血泪经验建议第一轮标注前就把文件名统一改成纯英文加数字。3. 模型与训练unet_parts.py 的跳连接结构、train.py 参数与低显存配置Unet 能成为医学影像分割的默认基线不是因为结构复杂恰恰因为它把低层细节和高层语义通过跳连接拼在一起对病灶这种边界模糊、形状不固定的目标特别有效。这套源码的网络定义集中在 unet_parts.py 和 unet_model.py 两个文件里训练入口是 train.py。这一章先拆网络结构再讲训练参数最后给低显存方案——目标是让新手能解释自己跑的每一个参数让熟手知道哪些默认值该动。3.1 编码器与解码器DoubleConv、下采样和上采样的拼接逻辑unet_parts.py 里定义了三个基础组件DoubleConv、Down、Up。DoubleConv 是两次 3×3 卷积加批归一化加 ReLU是 Unet 的最小单元Down 是最大池化下采样每次分辨率减半、通道数翻倍Up 是转置卷积上采样分辨率翻倍后再和编码器对应层的特征做通道拼接。unet_model.py 把这三个组件按 U 形排出来左侧 4 次 Down 把通道从 64 推到 1024右侧 4 次 Up 把特征图拼回原分辨率最后一层 1×1 卷积把通道数压到类别数。# unet_parts.py 的三个核心组件与源码结构一致 import torch import torch.nn as nn class DoubleConv(nn.Module): 两次 3x3 卷积 BN ReLUUnet 的最小单元 def __init__(self, in_ch, out_ch): super().__init__() self.conv 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.conv(x) class Down(nn.Module): 下采样最大池化 DoubleConv 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): 上采样转置卷积后与编码器同层特征 concat def __init__(self, in_ch, out_ch): super().__init__() self.up nn.ConvTranspose2d(in_ch, in_ch // 2, 2, stride2) self.conv DoubleConv(in_ch, out_ch) def forward(self, x1, x2): x1 self.up(x1) return self.conv(torch.cat([x2, x1], dim1))注意 Up 的 in_ch 是编码器同层通道数乘 2因为 concat 后通道翻倍up 先把 x1 通道减半cat 之后正好回到 in_ch再经过 DoubleConv 输出 out_ch。这个参数设计是 Unet 最容易被改坏的地方——很多人把转置卷积换成双线性插值上采样时没同步改通道数跑起来直接报维度错误。改输入尺寸不影响这段代码模型是按通道数定义的跟图片分辨率无关。3.2 dataset.py 与 train.py一条命令训练背后的数据流dataset.py 的 BasicDataset 负责三件事读图、预处理、返回张量。预处理里最容易忽略的是掩码的 resize 插值方式——原图用双线性没问题掩码必须用最近邻 INTER_NEAREST否则 0/1 标签会被插值成 0.3、0.7 这种模糊值模型学到的就是一坨糊的边界。train.py 里模型用 UNet(n_channels3, n_classes2)loss 用 BCEWithLogitsLoss优化器 Adam lr1e-4每 10 个 epoch 在验证集上算一次 mIoU 并保存最优权重。# dataset.py 的 __getitem__ 常见实现 def __getitem__(self, idx): img cv2.imread(str(self.imgs_dir / f{idx}.jpg)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 统一通道顺序 img cv2.resize(img, (self.img_size, self.img_size)) mask cv2.imread(str(self.masks_dir / f{idx}.png), cv2.IMREAD_GRAYSCALE) mask cv2.resize(mask, (self.img_size, self.img_size), interpolationcv2.INTER_NEAREST) # 掩码禁止线性插值 img img.astype(np.float32) / 255.0 mask mask.astype(np.float32) / 255.0 return (torch.from_numpy(img).permute(2, 0, 1), torch.from_numpy(mask).unsqueeze(0))permute(2,0,1) 是把 HWC 转成 CHW 才能进卷积mask 读出来是 (H,W)unsqueeze(0) 变成 (1,H,W)和模型输出的形状对齐才能算 loss。归一化直接除以 255 就够了医学影像数据集本身小套 ImageNet 的 mean/std 反而可能因为分布差异把结果搞差。如果训练时偶尔报 mask 和 preds 形状对不上优先检查是不是在 resize 原图时把尺寸写成了 (width, height)——OpenCV 的 resize 参数是 (宽, 高)很多人第一次写反。提示掩码的 resize 永远用 INTER_NEAREST这是分割任务的基本纪律。训练循环本身不复杂但有两个细节值得强调BCEWithLogitsLoss 内部自带 sigmoid所以模型最后一层不要额外加 sigmoid加了会让梯度在饱和区消失多类分割时要把 loss 换成 nn.CrossEntropyLoss掩码也不再除以 255dataset.py 里直接返回整型张量。模型保存用 state_dict加载时也要用 load_state_dict不要直接 torch.save 整个 model前者换设备后加载更省心。3.3 低显存怎么跑batch_size、输入尺寸和半精度的组合4GB 显存跑这套 Unet 是可行的我实测的组合是 batch_size2、输入 256、AMP 半精度显存占用压到 2.5GB 左右。优先顺序是先减 batch_size再减输入尺寸最后开半精度。减输入尺寸会直接丢细节半精度基本不丢精度所以后两者看似差不多实际取舍完全不同。# train.py 中的低显存改造三选一或组合用 batch_size 2 # 显存 4GB 以下的起手值 img_size 256 # dataset.py 里统一 resize 的目标尺寸 # 半精度训练loss 用 scaler 包裹 scaler torch.cuda.amp.GradScaler() for imgs, masks in train_loader: optimizer.zero_grad() with torch.cuda.amp.autocast(): preds model(imgs) loss criterion(preds, masks) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()AMP 在新一点的显卡上收益最大老显卡也能用只是加速不明显。半精度的坑在于如果你的 loss 里手动加了 sigmoid或者用了自定义 loss数值范围可能溢出——BCEWithLogitsLoss 是全精度下数值最省心的选择这也是源码默认它的原因。另外 DataLoader 的 num_workers 在 Windows 上别超过 4否则会触发多进程 spawn 的 RuntimeError这个报错和显卡没关系是平台的进程模型限制。4. 训练避坑与常见问题排查五个实测翻车点这套源码我完整跑过三遍前两遍都翻车了。翻车位置高度集中——不在模型代码而在数据准备和训练参数的隐性要求。下面五条都是我真实复现过的问题按「现象 → 原因 → 解决」写顺序就是排查顺序比对着报错乱搜高效得多。4.1 生成的掩码全黑或全白训练 loss 一开始就不收敛现象labelme2seg.py 跑完打开 labels 目录的 PNG 全是纯黑或纯白train.py 的 loss 要么直接崩到 6 以上要么从头到尾几乎不变。原因最常见是转换时 class_id 写错把背景和前景都填成了同一个值其次是 mask 的 dtype 是 float 且没做二值化0/1 之外还有抗锯齿灰值BCE loss 看到一堆 0.5 就不知道怎么更新梯度了。解决转换脚本里在 fillPoly 后强制mask (mask 0).astype(np.uint8)然后随机抽一张原图和掩码打印np.unique(mask)确认只有 [0, 1] 两个值再放一张叠加图检查病灶位置是否和原图对得上。这一步三十秒能省掉一晚上的无效训练。4.2 CUDA out of memory通宵训练白跑现象epoch 跑到一半爆显存报错指向某个 conv 层更难受的是脚本只在最优模型时才保存OOM 后整个训练进度清零。原因默认 img_size 是 512 或 batch_size4 时4GB 卡撑不住另外没有周期性 checkpoint属于代码设计上的后悔药缺失。解决按第 3 章的配置降到 batch_size2 加 img_size256 加 AMP。然后务必改成每个 epoch 都保存checkpoints/last.pth最优权重另存best_model.pth。这样中途断了也能从 last.pth 续训代价只是多占几百 MB 磁盘但不会再出现白跑一夜的惨案。4.3 训练了十几个 epochmIoU 一直卡在 0.3 以下现象loss 在降验证集 mIoU 却始终上不去预测图基本是一坨背景。原因最常见是数据集里病灶面积太小整图 512×512病灶只有几十像素背景占比 99%。BCE loss 对类别极不平衡不敏感模型学出来的最优解就是全预测背景。其次是数据集划分没随机train 和 val 的分布差别大。解决先算掩码里前景像素占比foreground_ratio (mask 0).mean()低于 5% 就换 DiceLoss或者 BCE 和 Dice 按 0.5:0.5 混合同时把输入 resize 到 512 保留细节。划分问题则回头重跑 gen_split.py手动确认 val 里确实包含几个前景占比高的样本。不要一上来就换网络结构Unet 对小目标不差差的是 loss 和目标尺度。4.4 预测结果比原图小一圈边缘还有黑边现象predict.py 输出的分割图尺寸不等于原图四周有黑边和原图叠加后病灶位置明显错位。原因推理时把原图直接 resize 到 256×256 输入输出再 resize 回原尺寸。如果原图长宽不是 256 的整数倍缩放比例不一致就会产生黑边和错位另外输出掩码 resize 回去时用了双线性插值边缘灰值叠加后看着像描边。解决推理路径照抄 dataset.py 的预处理尺寸、插值、通道顺序一个字都不能差。如果必须保持原图分辨率输出用 letterbox 方式先等比缩放到最长边 256剩余区域补 0模型输出后裁掉补丁区域再还原这样不会产生畸变。掩码的 resize 一律用 INTER_NEAREST。提示推理时的预处理参数和训练时必须一字不差这是分割项目最常见的翻车源头。4.5 换自己的数据后报错输入通道 4 或类别数对不上现象训练时终端报RuntimeError: expected 3 channels, got 4或者模型输出形状和 mask 不一致预测阶段输出全是一个类别。原因原图是 PNG 带 alpha 通道cv2.imread 默认读出 BGRA 四通道或者你的数据是 4 类但 unet_model.py 里 n_classes 还是 2掩码里出现 2、3 这些值模型根本没学过这些标签。解决读图统一cv2.cvtColor(img, cv2.COLOR_BGRA2RGB)或加cv2.IMREAD_COLOR强制转三通道。改类别前先确认掩码最大值mask.max() 1就是要填的 n_classes然后只改 unet_model.py 里的 n_classes 和 train.py 里的 loss多类用 CrossEntropyLoss。不要顺手改 unet_parts.py网络结构不需要动改错了反而引入维度 bug。5. 评估与预测utils_metrics.py 指标计算、Results 曲线与两条推理路径训练完不是终点。毕设答辩和实际交付前都要回答同一个问题分割效果到底怎么样。这套源码的 Results 目录已经替你生成了 Precision.png、Recall.png、mPA.png、mIoU.png 和 confusion_matrix.csv这些都来自 utils_metrics.py。这一章先把四个指标讲清楚再讲怎么读曲线最后说 predict.py 和 ui.py 两条推理路径怎么选。5.1 utils_metrics.py从混淆矩阵到每个类别的 Precision、Recall、IoUutils_metrics.py 做的事情分成两步先把模型预测结果和真值掩码逐像素比对累加出混淆矩阵再基于混淆矩阵算每个类别和总体指标。混淆矩阵的行是真值类别、列是预测类别confusion_matrix.csv 就是测试集全部图片累加后的结果。四个指标的关系可以用这个表说明指标公式含义PrecisionTP / (TP FP)预测前景中有多少是对的对误检敏感RecallTP / (TP FN)真实病灶被找回多少对漏检敏感IoUTP / (TP FP FN)交并比分割任务的核心指标mIoU / mPA各类别取平均多类场景下的汇总得分# utils_metrics.py 中指标计算的摘录含防除零处理 import numpy as np def compute_metrics(confusion_matrix): eps 1e-9 tp np.diag(confusion_matrix) fp confusion_matrix.sum(axis0) - tp fn confusion_matrix.sum(axis1) - tp precision tp / (tp fp eps) recall tp / (tp fn eps) iou tp / (tp fp fn eps) miou np.nanmean(iou) # 忽略空类防止 NaN 污染总分 mpa np.nanmean(tp / (confusion_matrix.sum(axis1) eps)) return precision, recall, iou, miou, mpaeps 是为了防止某类在测试集中完全没出现时除零nanmean 会把这一类跳过而不是直接把总分变成 NaN。看混淆矩阵 CSV 时重点关注次对角线如果背景被大量预测成病灶说明训练数据里阴性样本太少或者有标注噪点如果病灶被大量预测成背景优先怀疑小目标问题回头调 loss 而不是调网络。5.2 Results 目录Precision.png、Recall.png、mIoU.png 怎么读这三张图是每个 epoch 在验证集上的指标曲线生成逻辑在 train.py 的 validate 函数里完整测试集的评估由 test.py 统一完成跑完把混淆矩阵累加并输出 Results 下的所有图和 CSV。读曲线看三件事起步值、收敛值、波动幅度。mIoU 起步就在 0.5 以上说明数据干净、模型初始化合理收敛后还在上下乱跳通常是 batch_size 太小或学习率太高先降 Adam 的 lr 到 3e-5 试试。还有一个容易忽略的组合信号如果 mIoU 和 Recall 都在涨但 Precision 一路跌大概率是模型倾向把什么都预测成前景去查 val 里阴性样本占比偏少就补数据。指标图本身不用再加工答辩时截图放进 slide 就是现成的实验证据配合 confusion_matrix.csv 能讲清楚「哪个类别最难分、为什么」这比只报一个 mIoU 数字的答辩效果好得多。论文里如果要求 error bar可以固定随机种子跑三遍取均值曲线和 CSV 都留了原始数据二次加工不费劲。5.3 predict.py 单张推理与 ui.py 交互界面predict.py 走命令行批处理路线适合做完整测试集的定量评估ui.py 是 PyQt 做的交互界面适合现场演示和答辩。两条路径内部共用同一套预处理这是源码设计得比较聪明的地方——你只需要改 dataset.py 里的参数两处自动同步。# 单张图片推理输入原图输出带轮廓叠加的结果图 python predict.py -i images/ISIC_0000000.jpg -o results/demo # 启动交互界面选图、预览、拖动阈值滑块看效果 python ui.pypredict.py 的 -i 支持单张图片路径也能传目录批量跑-o 指定输出目录结果图保存为叠加了轮廓的 PNG方便直接贴报告。ui.py 启动后如果界面空白先检查是不是没装 PyQt5——requirements.txt 里漏了就跑pip install PyQt5。UI 里如果有阈值滑块默认 0.5皮肤镜病灶这类边界模糊的场景可以往 0.3~0.4 调Recall 更高代价是边缘会毛糙具体取舍看你的指标侧重。6. 换自己的数据集改类别数、输入尺寸和去噪后处理三个技巧这个项目自带的皮肤镜数据能跑通但多数人下载它是为了换成自己的医学影像——CT、超声、内镜都行。换数据只需要动三个地方其他代码一个字都不用改。第一是类别数。改成多类分割时先确认掩码里最大类别值mask.max() 1就是要填的 n_classes然后 unet_model.py 里UNet(n_channels3, n_classesN)改掉train.py 的 loss 从 BCEWithLogitsLoss 换成 nn.CrossEntropyLoss掩码就不需要除以 255 了dataset.py 里 mask 张量直接保持整型。这三处改完模型结构自动适配unet_parts.py 完全不用碰。第二是输入尺寸。原图如果是 1024×1024 的病理切片全图直接喂 4GB 显存必爆常见做法是切 patch 训练或者先等比缩到 512。改尺寸时记住两条尺寸选 256 或 512因为 Unet 要经历 4 次下采样尺寸得是 2 的幂附近dataset.py 里原图和掩码的 resize 必须用同一个尺寸掩码继续用 INTER_NEAREST。第三个技巧是后处理去噪。医学影像预测结果常见两类问题病灶内部出现零星小洞或者背景里出现几颗孤立的假阳性像素。这些不需要改网络预测后在掩码上做连通域过滤就够了。# 后处理移除面积小于阈值的连通域 import cv2 import numpy as np def remove_small_regions(mask, min_area50): mask (mask 0).astype(np.uint8) num, labels, stats, _ cv2.connectedComponentsWithStats(mask, connectivity8) cleaned np.zeros_like(mask) for i in range(1, num): if stats[i, cv2.CC_STAT_AREA] min_area: cleaned[labels i] 1 return cleanedconnectedComponentsWithStats 会把掩码拆成若干独立连通域并给出每个区域的像素面积面积小于阈值的直接丢弃。阈值 50 是皮肤镜场景的经验值CT 大器官分割可以放到 200~500先看一眼病灶总面积再定原则是只滤噪点、不动主病灶盲目调大只会把真病灶也抹掉。这三处改完一套源码就能在不同模态的医学影像上复用了。我现在接手任何一份分割源码都会强制走一遍检查清单先确认掩码只有 0 和 1再确认 n_classes 等于掩码最大值加 1最后核对 predict.py 的预处理和 dataset.py 一字不差。这三步让我后来几乎没有再在分割项目上翻过车也把这份源码跑成了一块趁手的试验田。希望帮到你。本文还有配套的精品资源点击获取