简介:一套面向毕业设计与课程开发的医学图像分割系统,基于Python和深度学习技术实现,采用经典的U-Net网络结构,可完成医学影像中目标区域的自动分割。压缩包共收录138个文件,整体大小约13.66MB,内容涵盖120张PNG医学图像样本与分割结果图、6个Python源码文件、XML标注文件、项目说明文档、许可证文件以及PyCharm工程配置,目录结构清晰,便于直接导入开发环境并开展训练、预测和评估。项目中包含数据预处理、模型定义、训练测试等模块,源码经过严格测试,模块间解耦良好,读者可以替换自定义数据集,调整网络层数或损失函数,快速搭建属于自己的实验基线。目前已有266人学习下载,既适合高等院校学生用于毕业设计或课程设计,也适合作为开发者初探医学图像分割的入门参考。
1. 医学图像分割系统:为什么是 U-Net,为什么能直接当毕设用
医学图像分割和普通图像分类是两码事,分类只告诉你是猫是狗,而分割要把每个像素点归类,比如眼底图像里哪一块是血管、哪一块是背景。这个任务对精度和边界敏感度要求极高,也正是毕业设计最容易出成果的方向——它有一个公认的经典基线模型 U-Net,收敛稳定、可视化效果好、指标易于量化,拿它做底子做改进,工作量清晰,答辩也好讲。这套基于 Python + 深度学习的医学图像分割项目,恰好是一个完整闭环:带标注的医学图像数据集、U-Net 实现源码、训练与推理脚本、文档说明,以及几张可以直接看到的预测效果图。对要做毕业设计、课程设计或者刚接触分割任务想快速上手的开发者来说,它解决的最大问题不是“怎么调用一个现成模型”,而是“从原始图像到最终分割结果,整条链路怎么打通”。你可以在这个基础上替换数据集、改网络结构、调损失函数,而不是从零搭框架。
2. 项目结构与运行环境:先把代码跑起来,再谈改模型
拿到项目源码包,第一件事不是读代码,而是把目录结构和依赖环境理顺。从文件清单能看到这是一个标准的 PyCharm 工程,包含.gitignore、U-Net.iml、LICENSE、README.md,还有几张 PNG 格式的结果图。这个结构说明作者是按一个可复现的 Python 工程来组织的,而不是随手扔一堆脚本。
2.1 环境配置:Python 版本、PyTorch 与依赖安装
该项目基于深度学习框架实现,主流方案是 PyTorch。PyTorch 在医学图像分割领域占有率极高,原因有两个:动态图机制让网络结构调试直观,配套的 torchvision 和 torchmetrics 让数据加载与指标计算都省事。安装时建议按 GPU 驱动版本选择对应的 CUDA 版本,避免装完才发现版本不匹配。
# 建议使用 Python 3.8 ~ 3.10,太新的版本可能遇到第三方库兼容问题 conda create -n medical_seg python=3.9 conda activate medical_seg # 安装 PyTorch,这里以 CUDA 11.8 为例;纯 CPU 环境可去掉 -c pytorch -c nvidia 并安装 CPU 版 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 其它依赖 pip install numpy opencv-python pillow matplotlib tqdm scikit-learn这里有个值得注意的点:PyTorch 版本别装太旧。医学图像分割常用的 U-Net 实现涉及转置卷积和跳跃连接,旧版本在某些算子实现上有差异,但不影响大部分训练代码运行。如果你用的是 NVIDIA 显卡,装完后务必跑一条命令验证 GPU 是否真的可用:
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"输出True和显卡型号才算通过。很多同学在这步翻车,装的是 CPU 版本的 torch,训练速度慢得让人怀疑人生。
另外,opencv-python 这个库在医学图像处理里承担图像读取和简单形态学操作,安装时有概率遇到libGL.so.1报错,这是 Linux 环境下缺少 OpenCV 依赖库,执行apt-get install -y libgl1 libglib2.0-0即可解决,Windows 下极少出现。
2.2 工程入口与快速跑通
项目里没有给出明确的入口文件名,但 U-Net 类项目通常存在train.py、predict.py或类似的脚本。首先打开 README 看作者给出的运行说明,这是最快路径。如果 README 信息不全,就用最笨的办法——按模块结构读代码,流程一般是:
dataset.py定义数据集类,实现图像与对应 mask 的读取和预处理;model.py定义 U-Net 网络结构;train.py执行训练循环并保存权重;predict.py加载权重对测试图像推理。
先跑通再改,这是我做项目资源复现的铁律。所谓跑通,是能在一个小规模子集上完成一个完整的训练迭代,并产出一张分割结果图。不要一上来就全量训练,数据都没吃透就等着看 loss 曲线,这是典型的“黑匣子”操作。
为了在缺少关键脚本时也能快速验证环境,临时写一个最小化前向传播脚本是可行思路,如下:
import torch from torchsummary import summary # 假设 model.py 中定义了 U-Net 类,这里模拟导入 from model import UNet device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = UNet(in_channels=3, out_channels=1).to(device) # 模拟一张 512x512 的 RGB 眼底图像输入 dummy_input = torch.randn(1, 3, 512, 512).to(device) output = model(dummy_input) print("输出特征图形状:", output.shape) # 期望为 (1, 1, 512, 512) # 打印每层参数量,用于确认网络结构加载正常 summary(model, (3, 512, 512))这段代码的关键在于验证你的输入输出维度是否对齐。U-Net 网络的输入通道数由数据集决定,如果是眼底血管分割,通常用 RGB 三通道图像,输出是单通道概率图。如果output.shape和预期不符,问题多半出在网络最后一层的卷积核数量或激活函数上。输出通道数不等于类别数,或者最后用了 softmax 而不是 sigmoid,都会直接导致后续计算损失时报维度错误。torchsummary这个工具非常好用,能把每层的参数和输出尺寸打印出来,排查维度问题比反复看报错栈高效得多。
2.3 文件清单里那几张 PNG 是什么
项目里的21.png、23.png、24.png、28.png、25.png,从命名规律看是序号命名的图片。这类文件通常在分割项目里扮演两个角色:一是训练样本中的原始图像,二是推理阶段的输出可视化。结合项目标题中的“测试”描述,更合理的推断是测试集上几张图像的预测效果图,用来直观展示分割结果。
这几张图对答辩的意义很大。医学图像分割的最终交付物不只是指标数字,还包括可视化结果,因为评审老师更关心的是“分割边界是否贴合真实解剖结构”。你可以在答辩 PPT 里放三列对比——原图、标注图、预测图,一眼就能看出模型效果。这也是我建议你在复现完这个项目后,优先保存一批可视化结果的原因。
3. 医学图像数据集:预处理、增强与加载方式
这套系统的数据集部分是核心资产。基于 U-Net 的医学图像分割项目,最常用的公开数据集包括 DRIVE(视网膜血管分割)、ISIC(皮肤病变分割)、Lung Mask(肺部掩膜)等。从项目的适用范围来看,DRIVE 类数据集的可能性较大——它有明确的训练集和测试集划分,图像尺寸统一,标注是二值掩膜,非常适合作为课程设计和毕业设计的验证数据。
3.1 数据集目录组织与读取
拿到数据集后首先要确认目录结构。U-Net 类项目数据集通常这样组织:
dataset/ ├── train/ │ ├── images/ │ │ ├── 21.png │ │ └── ... │ └── masks/ │ ├── 21.png │ └── ... └── test/ ├── images/ └── masks/这个结构下,数据集类的实现逻辑很清晰:给定索引,从 images 目录读原图,从 masks 目录读对应名称的掩膜,然后进行预处理。这里最容易踩的坑是图像和掩膜的文件名对不上,或者掩膜不是单通道二值图而是 RGB 三通道的彩色标注图。RGB 标注图需要做颜色到类别 ID 的映射,否则计算损失时维度会直接报错。
数据集加载用 PyTorch 的 Dataset 类实现,常见写法如下:
import os import cv2 import torch from torch.utils.data import Dataset class MedicalImageDataset(Dataset): def __init__(self, image_dir, mask_dir, image_size=(512, 512), transform=None): self.image_dir = image_dir self.mask_dir = mask_dir self.image_size = image_size self.transform = transform # 以图像文件名作为两者对应的基准 self.image_names = sorted(os.listdir(image_dir)) def __len__(self): return len(self.image_names) def __getitem__(self, idx): img_name = self.image_names[idx] img_path = os.path.join(self.image_dir, img_name) mask_path = os.path.join(self.mask_dir, img_name) # 掩膜与图像同名 # 读取图像,BGR 转 RGB 保持通道顺序一致 image = cv2.imread(img_path) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image = cv2.resize(image, self.image_size) # 掩膜以灰度图方式读取,保证是单通道 mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) mask = cv2.resize(mask, self.image_size, interpolation=cv2.INTER_NEAREST) # 归一化到 [0, 1] 区间 image = image.astype('float32') / 255.0 mask = mask.astype('float32') / 255.0 # 转换为 CHW 张量格式 image = torch.from_numpy(image).permute(2, 0, 1) mask = torch.from_numpy(mask).unsqueeze(0) return image, mask这个实现有几个细节值得说明。第一,掩膜读取时必须用IMREAD_GRAYSCALE,否则读进来是三通道,和网络的单通道输出对不上。第二,resize 掩膜时插值方式必须用INTER_NEAREST,不能用默认的双线性插值,因为掩膜是离散标签,线性插值会产生非 0/1 的中间值,导致损失函数计算出错或产生伪标签。第三,掩膜归一化后没有做阈值处理,如果原始标注是 0 和 255,除以 255 后正好是 0 和 1,但如果原始标注就是 0 和 1,除以 255 后前景会变成约 0.004,这是很多人在数据预处理阶段找不到原因的玄学问题。建议先打印掩膜的像素值分布再决定是否归一化。
3.2 数据增强:医学分割任务怎么增强才不翻车
医学图像分割的数据集通常不大,像 DRIVE 只有 20 张训练图,直接训练 U-Net 这种参数量较大的网络,过拟合风险很高。数据增强是做毕设必须加的一环,但医学图像增强有讲究,不能照搬 ImageNet 分类那套。
import albumentations as A from albumentations.pytorch import ToTensorV2 # 训练集增强:旋转+翻转+弹性形变+亮度对比度调整 train_transform = A.Compose([ A.RandomRotate90(p=0.5), A.HorizontalFlip(p=0.5), A.VerticalFlip(p=0.5), A.ElasticTransform(alpha=1.0, sigma=50.0, alpha_affine=30.0, p=0.3), A.RandomBrightnessContrast(brightness_limit=0.1, contrast_limit=0.1, p=0.5), A.Resize(512, 512), ToTensorV2() ]) # 测试集一般只做尺寸统一,不做随机增强 test_transform = A.Compose([ A.Resize(512, 512), ToTensorV2() ])albumentations 是医学图像分割里最常用的增强库,因为它能保证图像和掩膜应用完全相同的变换,不会出现图像转了掩膜没转的情况。这里特别注意ElasticTransform这个增强,它对医学图像非常有效,因为人体组织本身存在形变,弹性增强能模拟这种生理变化,但参数不能太大,alpha 和 sigma 过大会产生不符合实际的扭曲,影响模型学习真实分布。还有一点,RandomBrightnessContrast在血管分割、病灶分割这类对亮度敏感的任务上要谨慎使用,因为有些医学图像(如 X 光片)的灰度本身就具有诊断意义,过度调整会破坏原始特征。我在实际项目中一般只对这个增强设 0.1 的幅度,宁可小一点。
3.3 类别不平衡:医学图像分割的第一大坑
医学图像分割里,背景像素往往远多于前景像素。以眼底血管分割为例,血管大概只占图像的 10% 左右,如果直接按像素计算交叉熵损失,模型只要把所有像素预测为背景就能拿到 90% 的准确率,但这显然毫无意义。这也是为什么 U-Net 项目里几乎都默认使用 Dice Loss 或其变体。
4. U-Net 训练流程:损失函数、优化器与关键参数调优
训练是整套系统的核心环节,也是你改代码最多的地方。U-Net 的实现细节不展开讲,但有几个训练层面的关键点必须说清,因为它们直接决定模型能否收敛、分割效果好不好。
4.1 损失函数的选择与实现
医学图像分割最常用的损失函数是 Dice Loss,它直接优化 Dice 系数,对类别不平衡天然鲁棒。Dice 系数的计算方式是两个集合的交集乘以 2 除以两个集合的元素总数之和,对应到像素级就是预测和标签中同时为正的像素点数目的两倍,除以预测为正的像素点数加上标签为正的像素点数。
def dice_loss(pred, target, smooth=1.0): """ 计算 Dice Loss pred: 模型输出,形状为 (B, 1, H, W),已经过 sigmoid 激活 target: 真实标签,形状为 (B, 1, H, W),值为 0 或 1 smooth: 平滑项,防止分母为 0 """ # 将张量展平为 (B, -1),方便按样本计算 pred = pred.contiguous().view(pred.size(0), -1) target = target.contiguous().view(target.size(0), -1) intersection = (pred * target).sum(dim=1) cardinality = pred.sum(dim=1) + target.sum(dim=1) dice = (2.0 * intersection + smooth) / (cardinality + smooth) return 1.0 - dice.mean()smooth参数是防除零的平滑项,默认设 1.0 即可。如果数据集里某个样本的标签全为背景(没有前景像素),cardinality 为 0,此时 dice 被 smooth 兜底,不会产生 NaN。但如果你把 smooth 设成 0,遇到这类样本就会梯度爆炸。实际训练中也有人用 BCE + Dice 的混合损失,公式是loss = bce_loss + dice_loss,这种做法在小目标分割任务上效果更好,因为 BCE 提供逐像素的梯度信号,Dice 负责整体形状约束。两种策略都可以尝试,不必迷信某一种。
4.2 优化器、学习率与训练轮数
U-Net 训练的经典配置是 Adam 优化器加固定学习率或余弦退火。和分类任务不同,分割任务的损失函数曲面更复杂,学习率设置不当很容易出现在某个高 Dice 值附近震荡但不收敛的情况。我的一般做法是:
import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR optimizer = optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-5) scheduler = CosineAnnealingLR(optimizer, T_max=100, eta_min=1e-6) # 训练循环中每个 epoch 结束后执行 scheduler.step()学习率初始值建议从1e-4开始,不要用分类任务常用的1e-3。原因是分割任务的每个像素都参与损失计算,梯度信号非常丰富,学习率过大会导致 Dice 系数剧烈震荡,loss 曲线看起来像心电图。weight_decay设为1e-5足够,太大的正则化反而会让分割边界变模糊。余弦退火调度器在训练后期能把学习率压得很低,让模型在损失曲面的底部精细搜索,一般训练 100~150 个 epoch 就能在 DRIVE 这类小数据集上取得不错的效果。
Batch Size 的设定相对简单,如果显存够用,设 8 或 16 都可以。512x512 的输入尺寸加上 U-Net 的深度特征图,显存占用大约在 6~8GB 每张卡,所以 8G 显存的显卡设 batch size 为 4 比较稳妥。
4.3 训练主循环与模型保存
U-Net 项目的训练脚本通常包含两个关键环节:每个 epoch 结束后的验证集评估,以及模型权重保存策略。保存策略这里就有讲究,建议用“保存验证集 Dice 系数最高的模型”,而不是“保存最后一个 epoch 的模型”。
import torch # 假设已经定义模型、优化器、数据加载器 best_dice = 0.0 num_epochs = 100 for epoch in range(num_epochs): model.train() train_loss = 0.0 for images, masks in train_loader: images, masks = images.to(device), masks.to(device) optimizer.zero_grad() outputs = model(images) # 输出经过 sigmoid 归一化到 [0,1] probs = torch.sigmoid(outputs) loss = dice_loss(probs, masks) loss.backward() optimizer.step() train_loss += loss.item() * images.size(0) # 验证阶段 model.eval() val_dice = 0.0 with torch.no_grad(): for images, masks in val_loader: images, masks = images.to(device), masks.to(device) probs = torch.sigmoid(model(images)) # 阈值化为二值掩膜 preds = (probs > 0.5).float() # 计算 Dice 系数 intersection = (preds * masks).sum() dice = (2.0 * intersection) / (preds.sum() + masks.sum() + 1e-6) val_dice += dice.item() * images.size(0) val_dice /= len(val_loader.dataset) print(f"Epoch [{epoch+1}/{num_epochs}] | Train Loss: {train_loss/len(train_loader.dataset):.4f} | Val Dice: {val_dice:.4f}") # 保存最佳模型 if val_dice > best_dice: best_dice = val_dice torch.save(model.state_dict(), 'best_model.pth') print(f"模型已保存,验证集 Dice: {best_dice:.4f}")训练循环里有个细节容易被忽略:model.eval()和torch.no_grad()要同时使用。eval()切换 dropout 和 batchnorm 的行为,no_grad()才真正关闭梯度计算,两者缺一都会造成显存浪费或验证结果不稳定。特别是模型结构里有 BatchNorm 层时,如果忘记切eval(),验证时 BatchNorm 仍然用当前 batch 的统计量,会导致验证指标不准确——这是很多人发现训练集 loss 和验证集 loss 差距很大但找不到原因时,最容易忽略的坑。
4.4 训练效果评估指标
医学图像分割使用最普遍的指标是 Dice 系数、IoU(交并比)和像素准确率。其中 IoU 和 Dice 存在换算关系,IoU 在答辩中更容易被理解和解释。计算方式如下:
def iou_score(pred, target, threshold=0.5): """ 计算 IoU(交并比) pred: 模型输出概率图,形状 (B, 1, H, W) target: 真实标签,形状 (B, 1, H, W) """ # 阈值化 pred_binary = (pred > threshold).float() intersection = (pred_binary * target).sum(dim=(2, 3)) union = pred_binary.sum(dim=(2, 3)) + target.sum(dim=(2, 3)) - intersection iou = intersection / (union + 1e-6) return iou.mean().item()这里的threshold是二值化阈值,默认 0.5。在推理阶段,预测输出的概率图通常要经过一个阈值判断才能变成最终的分割掩膜。0.5 是一个通用的起始值,但在这个项目上,你完全可以把阈值当作超参数调。血管分割本身就是细长结构,概率值普遍偏低,把阈值降到 0.4 或 0.35 往往能召回更多细小血管;如果噪声多、假阳性高,就往 0.6 调。
5. U-Net 项目避坑指南:环境、数据与训练常见问题排查
这一章是我在复现同类项目时积累的踩坑记录,每一条都对应过一个具体的报错或异常结果。按“现象 → 原因 → 解决”写清楚,实战价值比任何理论分析都高。
5.1 显存不足:明明模型不大,OOM 却反复出现
现象:训练刚开始第一个 iteration 就报CUDA out of memory,或者训练中途显存持续增长后崩溃。
原因:最常见的原因是输入图像尺寸过大或 batch size 设置过高。512x512 的输入在 U-Net 里下采样 4 次后最小特征图是 32x32,但注意跳跃连接保存的特征图会占用大量显存。另一个隐蔽原因是验证阶段没有关闭梯度计算,导致显存翻倍占用。
解决:先把 batch size 降到 2 或 1 验证是否复现。如果单张图都 OOM,用torch.cuda.empty_cache()清理缓存,并检查是否有其它进程占用了显存。nvidia-smi查看内存占用。另外检查数据加载时是否无意中保留了计算图,训练代码里确保每个 batch 的loss.backward()之前optimizer.zero_grad()。
# 查看 GPU 占用情况 nvidia-smi # 清理 PyTorch 缓存(在代码开头调用) import torch torch.cuda.empty_cache()5.2 训练 Loss 下降但验证 Dice 不涨:过拟合的信号
现象:训练集 loss 从 0.7 降到 0.2,但验证集 Dice 始终在 0.7 左右波动,无法提升。
原因:这是典型的过拟合,尤其在 DRIVE 只有 20 张训练图的情况下。U-Net 参数量约 3000 万,用 20 张图训练这种规模的网络,过拟合几乎是必然的。
解决:从三个方向入手。一是增强数据增强的强度,把水平翻转、垂直翻转、随机旋转的概率都提到 0.7,弹性形变加上;二是使用预训练权重做迁移学习,虽然 U-Net 的 encoder 是自定义结构,但可以嫁接 ResNet 作为 encoder 的版本,初始化效果远好于随机初始化;三是增大推理时的输入图像 patch 数量,训练时用随机裁剪的 256x256 patch 而不是整张 512x512 图,变相增加数据多样性。
5.3 掩膜读取后全是黑色或全白:像素值范围不一致
现象:训练出的模型预测全是背景,或者可视化掩膜时发现标记者区域和原图对不上。
原因:掩膜的像素值不统一。有的数据集标注用 0 和 255,有的用 0 和 1,有的把前景标为 128。如果代码里固定除以 255,但数据本身只有 0 和 1,前景就变成了接近 0 的值,网络学到的是把几乎全黑的图预测为背景。
解决:加一步诊断代码,打印 mask 的像素值唯一值和分布:
import cv2 import numpy as np mask = cv2.imread('mask_path.png', cv2.IMREAD_GRAYSCALE) print("唯一像素值:", np.unique(mask)) print("最大值:", mask.max(), "最小值:", mask.min())拿到实际值范围后再决定是否归一化以及用何种方式归一化。如果唯一值是[0, 255],直接除以 255 没问题;如果是[0, 1],就不要除,或者先乘 255 再除。
5.4 推理结果出现网格状伪影:上采样方式的问题
现象:分割出来的区域边界呈棋盘格状,尤其在细长结构(血管、神经)上特别明显,视觉上就像像素网格放大了。
原因:U-Net 的解码器部分用了转置卷积(nn.ConvTranspose2d),转置卷积的核大小和步长不匹配时会产生棋盘格伪影,这在生成式模型里是经典问题。
解决:优先改用双线性插值上采样 + 普通卷积的组合,替代转置卷积。在实现上就是把nn.ConvTranspose2d换成nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)后面接一个nn.Conv2d。这种改动对分割精度影响很小,但能显著提升分割边界的视觉质量。你可以在现有网络结构上做这个替换,作为自己的一个小改进点写进论文里。
5.5 训练数据有没有做归一化:图像通道顺序和值域排查
现象:训练曲线异常,Loss 在训练初期就出现 NaN,或者训练完的模型输出全是一个常数。
原因:输入图像通道顺序错乱(RGB 变 BGR)、图像值域过大(0~255 直接输入网络)或损失函数输入了未经 sigmoid 激活的 logits,都会在训练初期引发数值不稳定。
解决:数据加载时统一执行image / 255.0,并把通道从 HWC 转为 CHW;模型输出如果是裸 logits,计算 Dice Loss 前必须过torch.sigmoid()。下面这段是排查输入数据的标准写法:
from PIL import Image import numpy as np img = Image.open('21.png').convert('RGB') arr = np.array(img) print("形状:", arr.shape, "值域:", arr.min(), arr.max()) print("通道顺序是否 RGB:", np.allclose(arr[:, :, 0], arr[:, :, 1]) == False)6. 推理脚本实战:批量预测、结果保存与可视化对比
把训练好的模型用起来,才算真正吃透这套系统。推理阶段除了加载模型和预测,还要解决三个实际问题:如何批量处理测试集图像、如何把概率图保存成可视化的分割结果、以及如何和标注图对比来直观说明模型效果。
推理脚本的完整逻辑如下,这个脚本可以直接放到项目里使用:
import os import cv2 import torch import numpy as np from model import UNet def predict_image(model, image_path, device, threshold=0.5, save_path=None): """ 单张图像推理 model: 训练好的 U-Net 模型 image_path: 输入图像路径 threshold: 概率图二值化阈值 save_path: 分割结果保存路径 """ # 读取图像并预处理 image = cv2.imread(image_path) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) original_size = image.shape[:2] # 保存原始尺寸用于恢复 # 调整到模型输入尺寸 image_resized = cv2.resize(image, (512, 512)) image_tensor = torch.from_numpy(image_resized.astype('float32') / 255.0).permute(2, 0, 1).unsqueeze(0).to(device) # 推理 model.eval() with torch.no_grad(): output = model(image_tensor) prob = torch.sigmoid(output).cpu().numpy()[0, 0] # 取出单通道概率图 # 二值化并恢复原始尺寸 mask_binary = (prob > threshold).astype('uint8') * 255 mask_resized = cv2.resize(mask_binary, (original_size[1], original_size[0]), interpolation=cv2.INTER_NEAREST) if save_path: cv2.imwrite(save_path, mask_resized) return mask_resized # 批量推理测试集 def batch_predict(model, test_dir, output_dir, device): os.makedirs(output_dir, exist_ok=True) model.load_state_dict(torch.load('best_model.pth', map_location=device)) model.to(device) for img_name in os.listdir(test_dir): if not img_name.lower().endswith(('.png', '.jpg', '.jpeg')): continue img_path = os.path.join(test_dir, img_name) save_path = os.path.join(output_dir, f"pred_{img_name}") predict_image(model, img_path, device, threshold=0.45, save_path=save_path) print(f"预测完成: {img_name} -> {save_path}")推理脚本里有三个参数值得你反复调。第一个是threshold,前文提过,细结构目标建议往低调(0.35~0.45),粗大目标用 0.5 即可;第二个是batch_predict里加载权重的路径,注意map_location=device这个参数,如果你训练的模型是 GPU 的,换到 CPU 机器上推理时缺了这个参数会报键值不匹配的错;第三个是保存结果的插值方式,恢复原始尺寸时一定要用INTER_NEAREST,保持分割掩膜的离散性。
可视化对比对毕设答辩和论文插图很有价值,建议单独写一个拼接脚本,把原图、标注图、预测图水平拼接成三列对比图:
import matplotlib.pyplot as plt def visualize_comparison(image_path, mask_path, pred_mask, save_path): """ 可视化对比:原图 vs 标注 vs 预测 """ image = cv2.imread(image_path) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) gt_mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(image) axes[0].set_title('Original Image') axes[1].imshow(gt_mask, cmap='gray') axes[1].set_title('Ground Truth') axes[2].imshow(pred_mask, cmap='gray') axes[2].set_title('Prediction') for ax in axes: ax.axis('off') plt.tight_layout() plt.savefig(save_path, dpi=200, bbox_inches='tight') plt.close()这套系统的价值在于完整复现了医学图像分割的标准管线,从数据处理到模型训练再到推理可视化,每一步都可以独立修改和验证。我在实际使用这类项目时,习惯性地把学习和复现的边界划定得很清楚:先把作者提供的默认配置跑通,再逐步替换数据集、修改网络结构、调整损失函数,每做一步改变就记录一次训练指标的变化,这套流程能帮你快速判断改动是否有效。从那以后我每次跑新数据集,都强制自己先输出五张对比图再讨论指标高低,因为这能暴露出很多数字看不到的问题。希望这份拆解能帮你在毕业设计或课程项目上少走弯路,尽快跑出属于自己的分割结果。
本文还有配套的精品资源,点击获取