
简介这是一套基于Python的眼底图像视杯视盘分割项目源码与文档齐全适合计算机视觉、医学图像处理方向的学生用于课程设计或毕业设计参考。项目围绕血管特征提取、杯盘比vCDR计算、血管分支点定位和眼科疾病分级展开服务器端完成视盘区域特征与血管特征计算客户端支持关键点可视化与特征列表展示并涉及早产儿视网膜病变分类、青光眼分级及39类疾病分类任务。资源共18个文件9.22MB包含Python源码、ONNX模型、PNG/JPG效果截图、Markdown说明文档等代码经测试可运行便于对照学习与二次开发。压缩包内附README及Competition说明帮助快速理解项目结构与复现流程。已有348人浏览学习下载后可结合文档逐步理解眼底图像分割、特征计算与疾病分类实现细节适合作为医学影像AI课题的完整示例。1. 为什么眼底图像视杯视盘分割是医学图像分割里最不好糊弄的方向医学图像分割是图像分割算法里落地条件最苛刻的方向之一而眼底图像里的视杯与视盘分割又格外特殊。视盘是眼底照片中央那块颜色偏亮的椭圆区域视杯是视盘中心颜色更浅的凹陷临床判断青光眼进展的杯盘比CDR就是这两个区域垂直直径的比值。最容易被低估的是尺寸问题视盘通常只占整张眼底图的 1/5 到 1/7视杯又只有视盘一半大目标小、边缘模糊杯盘边界在不少病例里几乎靠医生经验判断。课堂作业里跑得通的分类网络切到这个任务上往往输出一团没实用价值的色块。这篇文章讲我如何用 Python 把一个眼底图像视杯视盘分割项目从零做到能交付的完整流程先讲透为什么选 Unet再给一条能直接照抄的数据准备和训练管线最后把踩过的坑和排查方法列出来。适合正在做课程设计或毕业设计、需要可复现源码方案的人也适合刚接触医学图像分割、想知道小样本方向怎么落地的新手。2. 视杯视盘分割解剖结构、算法选型和为什么 Unet 是稳的选择2.1 认识分割目标视盘和视杯在眼底图里长什么样在彩色眼底照片中视盘是视网膜上颜色最亮的椭圆形区域位置偏图像中心鼻侧大小约占整幅眼底图的 1/5 至 1/7。视神经纤维在这里汇聚并穿出眼球也是医生观察视网膜动脉、静脉进入眼球的主要解剖标记。视杯则是视盘正中央的生理凹陷颜色比周围视盘组织更浅形状近似圆形。临床上用杯盘比——视杯垂直径与视盘垂直径的比值——来评估青光眼风险正常人多在 0.3 以下超过 0.5 就要高度怀疑青光眼。分割结果直接决定这个比值算得准不准误差几个像素就可能把一个病例从“正常”推到“疑似”。分割任务要做的是把这两个区域分别从眼底图里“抠”出来。注意学术数据集上的标准定义是视盘分割和视杯分割两个二分类任务而不是一个三分类任务。因为视杯完全包含在视盘内部两者是嵌套关系如果硬做三分类背景、视盘、视杯类别不平衡会更严重且重叠边界区的梯度会互相干扰。常见做法是一个模型的两个解码头分别输出视盘掩码与视杯掩码或者干脆训练两个独立的 Unet——课程设计里我推荐前者参数少、部署简单、答辩时也好讲清“共享编码器”的设计动机。还有一层临床约束容易被忽略评价分割结果时视盘垂直直径和视杯垂直直径都要从掩码上量出来。如果模型把视杯多扩了 5 个像素杯盘比就可能从 0.48 跳到 0.52直接跨过疑似青光眼的阈值。所以后处理阶段不能随便做膨胀腐蚀任何形态学操作都可能改变垂直直径后面讲推理时我会再说这个坑。2.2 传统方法为什么搞不定边界对比度低、光照干扰多在深度学习普及之前视盘分割主要靠灰度阈值、霍夫变换找椭圆、区域生长和形态学操作。它们对图像质量要求极高眼底拍摄角度稍有偏移椭圆的长短轴就不准糖尿病患者的眼底常有渗出和出血点亮度直方图反复震荡全局阈值瞬间失效。视杯分割比视盘分割更困难因为视杯边界本质是灰度渐变带不是清晰边缘形态学方法几乎只能靠人工调参数硬撑泛化能力很差。这也是为什么这个领域在 2015 年前后整体转向 ConvNet 和 Unet 这类端到端学习方法——特征是模型从标注数据里学出来的不是靠人手工设计滤波器。放到图像分割的整体脉络里看眼底图像视杯视盘分割属于典型的“标注贵、数据少、边界相对明确”的小样本医学图像分割场景。公开数据集规模通常只有几百张到一千多张相比自然图像几十万张的量级完全是零头。设计算法时样本效率必须放在第一位这一条直接决定了后文的选型方向。2.3 为什么 Unet 是课程设计最稳的选择选 Unet 不是因为它最新而是因为这个任务的一切约束都落在它的优势区间。第一是样本效率标准 Unet 在两百张左右的小数据集上能稳定收敛而 Transformer 类结构往往需要大规模预训练权重或很强的数据增强兜底调参空间大课程设计的时间不允许。第二是结构可解释Unet 的跳接把浅层边缘信息直接送回解码路径与深层语义特征 concat 融合视盘边缘不会出现大块空洞或锯齿。答辩时从“为什么用跳接”讲起比背一堆复杂模块更容易让评阅老师听懂。第三是部署成本在 256×256 输入下标准 Unet 用单张 GTX 1660 就能完成训练和推理不需要混合精度、多卡分布式也不会一个 epoch 等到天黑。方案小样本收敛边缘细节显存/推理成本课程设计翻车风险Unet约 200 张可收敛高靠跳接低低DeepLabv3ASPP需要 500 张以上中中中等SegFormer / ViT 类需要预训练或大量增强中高高还有一个实际理由Unet 的开源实现是医学图像分割领域最多的搜“python unet 图像分割”就能看到大量可对答案的代码。遇到问题能查、能比、能抄到作业对课程设计来说比模型先进重要得多。我见过不少同学选了一个顶会新结构卡在梯度震荡和显存溢出上最后换回 Unet属于来回绕路的血泪经验。2.4 高分课程设计的源码目录应该长什么样一个能被评阅老师看懂的课程设计代码组织比算法创新更能拉开差距。我一般把项目按五个模块组织正好也覆盖标题里“源码 文档说明 截图演示 详细注释”的交付要求data/存放原始眼底图、标注掩码、训练集与验证集划分清单。models/Unet 网络定义独立成一个文件不跟训练脚本混在一起。train.py数据加载、增强、训练循环、验证指标输出。infer.py读入单张眼底图输出视盘与视杯掩码和叠加可视化。utils/预处理、后处理、Dice 等评估指标计算。每部分都能单独运行、单独出图截图演示时层次分明。下面章节的代码都按这个目录结构来写方便直接复用。3. 数据准备从公开眼底数据集到能直接喂进 Unet 的样本对3.1 公开数据集的组织方式与预处理流程视杯视盘分割常用的公开数据集有 RIM-ONE、ORIGA、DRIVE 这类已标注眼底图像集。它们的共同特点是图像尺寸不统一从 560×720 到 2144×1424 都有标注格式也不一样有的是灰度 PNG 掩码有的是坐标点集合有的把视盘和视杯分别放在两张图里。统一尺寸是第一步我一般缩放到 512×512 或 256×256。分辨率决定了训练速度和边缘精细度256 适合先跑通流程512 适合最后出效果图。这里有一个非常容易踩的坑不要用 OpenCV 默认的双线性插值去 resize 掩码。掩码是二值图双线性插值会在边缘产生灰色过渡带比如原本 0/255 的边界会变成 128 左右的灰像素。拿这种标签去训练Unet 会在过渡带上输出 0.5 而不是 0 或 1直接拉低最终 Dice。正确做法是原图用双线性插值保留纹理掩码用最近邻插值保持锐利import cv2 import numpy as np def load_and_resize(img_path, mask_path, size(512, 512)): # 读取眼底彩色图和对应掩码 img cv2.imread(img_path) # OpenCV 读进来是 BGR 顺序 mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) # 以灰度图读入 # 原图用双线性插值保留血管纹理和视盘边缘细节 img cv2.resize(img, size, interpolationcv2.INTER_LINEAR) # 掩码用最近邻插值避免边缘出现灰色过渡像素 mask cv2.resize(mask, size, interpolationcv2.INTER_NEAREST) # 统一二值化任何大于 127 的像素都变成 1 mask (mask 127).astype(np.uint8) return img, mask这段代码最关键的是最后一行。不同数据集掩码的亮度标准不一样有的用 0/255有的用 0/1甚至有人把视杯用 128 灰度单独存。统一二值化之后再转成浮点数训练时才能直接当交叉熵标签用。另外注意 OpenCV 读入的是 BGR如果后面要可视化或做数据增强记得在某个环节转回 RGB否则截图里红色和蓝色会互换。3.2 视盘掩码与视杯掩码嵌套关系怎么拆标签以 RIM-ONE 这类把视盘disc和视杯cup分开标注的数据集为例预处理时要把它们拆成两个独立的标签通道一个通道是“背景 0、视盘 1”不管视杯在不在里面另一个通道是“背景 0、视杯 1”。网络用两份二分类交叉熵损失分别监督两个输出头而不是用一个三分类 Softmax。原因前面说过——视杯嵌在视盘里类别是包含关系而不是互斥关系Softmax 的归一化会强迫模型在两个输出之间做非此即彼的选择杯盘边界处梯度互相拉扯最终视杯边界会整体向内收缩def split_disc_cup(disc_mask, cup_mask): # disc_mask: 背景0, 视盘区域1含视杯 # cup_mask: 背景0, 视杯区域1 disc_label (disc_mask 127).astype(np.uint8) cup_label (cup_mask 127).astype(np.uint8) # 防御性检查视杯区域理论上必须完全落在视盘内部 if cup_label.max() disc_label.max(): raise ValueError(cup mask 和 disc mask 的范围不一致) return disc_label, cup_label这个防御性检查算是我自己的血泪经验。有些数据集的 cup 图和 disc 图虽然尺寸一致但其中一张是用不同坐标系导出的缩放到 512 后错开几个像素。直接拿去训练验证 Dice 看着还挺高但保存出来叠加到原图上一眼就能看出边界对不上。加这个检查之后数据错位会在训练前暴露而不是等三个小时训练完才发现问题。3.3 数据增强不要照搬自然图像的套路眼底图的数据增强不要盲目套用自然图像那一套。水平翻转、垂直翻转、小角度旋转都是安全的原因在于眼底解剖结构的左右对称性有限翻转不会破坏临床语义。但有两件事不能做第一旋转掩码时不能只旋转图像不旋转标注两者旋转角度和插值方式必须一致第二不要做随机裁剪把视盘裁出视野后模型会学到不完整的空间先验而且裁剪后的类别占比会被改变。我的做法是无裁剪、固定分辨率、只做翻转和轻微旋转import random class RandomFlip: 同一个随机操作同时作用于原图和掩码防止图文不对齐 def __call__(self, img, mask): if random.random() 0.5: img img[:, ::-1].copy() mask mask[:, ::-1].copy() return img, mask这里的img和mask是 NumPy 数组用[:, ::-1]沿宽度方向翻转。.copy()不能省因为切片返回的是原数组的视图后面如果做原地归一化或 ToTensor会连原图一起改掉相当于同一份数据被污染。翻转虽然简单但眼底图的视盘位置在一部分病例里偏左、一部分偏右翻转能把这种位置偏差抹平一部分对小数据集来说是最划算的增强。训练集划分也要固定随机种子import random def make_dataset_split(img_list, val_ratio0.15): random.seed(42) # 固定种子保证每次运行划分一致 idx list(range(len(img_list))) random.shuffle(idx) val_count int(len(idx) * val_ratio) val_idx set(idx[:val_count]) train_idx [i for i in idx if i not in val_idx] return train_idx, val_idx固定随机种子非常关键。课程设计后期你一定会反复调参如果不固定种子每次划分的验证集都不一样Dice 指标的波动就会被你误以为是“这次改了参数变好了”实际只是换了一批验证样本。3.4 标注质量自查开训之前先人眼扫一遍数据准备好以后不要急着开训。先在验证集上做一次静态自检把眼底图和叠加掩码逐张存成 PNG人眼扫一遍。重点看三类问题一是掩码有没有缺角、孤立噪点二是视杯掩码是否全部落在视盘掩码内部三是图像有没有严重暗角或曝光不均。如果某张图的视杯边缘出现多个像素宽的灰色过渡带通常说明原始标注是从浮点坐标插值生成的这类标签要么重做要么在训练时再阈值化一次。这个习惯决定后续所有步骤能不能成立。视杯视盘分割的标签噪声容忍度比自然图像分割低很多因为目标区域本身小几十个像素的错位就能把杯盘比从 0.3 拉到 0.5。对课程设计来说评阅老师很大概率会打开你提供的掩码截图逐张看掩码干净、没有错位比网络结构新颖度更能拿分。4. 用 Python 在本地跑通视杯视盘分割Unet 训练与推理全流程4.1 环境准备Python 版本、依赖与可见性先用最小依赖把环境跑起来。如果你是刚装好 Python记得把 pip 的镜像源配置好否则下载 PyTorch 那一大包依赖会等到怀疑人生。我常用的一套组合是 Python 3.8、PyTorch 1.13、OpenCV 4.5兼容性和社区排错资料都比较齐全pip install torch1.13.1 opencv-python pillow numpy这三个库就够了torch 提供网络定义和自动求导opencv-python 负责图像的读取、缩放和可视化numpy 处理数组运算。不要一上来就装一堆辅助库依赖越少别人复现你源码时越不容易因为环境不一致失败。4.2 双输出头的视杯视盘 Unet 定义这里给出一个直接可跑的 Unet 实现和很多网上教程的区别是末尾输出是out_ch2第 0 个通道预测视杯第 1 个通道预测视盘。两个通道共用同一个编码器互相共享眼底图的底层特征训练时只需要一份 BCE 损失分别监督两个通道即可import torch import torch.nn as nn class ConvBlock(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), ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_ch3, base64, out_ch2): super().__init__() # 编码器逐层下采样通道数翻倍 self.enc1 ConvBlock(in_ch, base) self.enc2 ConvBlock(base, base * 2) self.enc3 ConvBlock(base * 2, base * 4) self.enc4 ConvBlock(base * 4, base * 8) self.pool nn.MaxPool2d(2) # 瓶颈层 self.bottleneck ConvBlock(base * 8, base * 16) # 解码器逐个上采样与对应编码器输出 concat self.up1 nn.ConvTranspose2d(base * 16, base * 8, 2, stride2) self.dec1 ConvBlock(base * 16, base * 8) self.up2 nn.ConvTranspose2d(base * 8, base * 4, 2, stride2) self.dec2 ConvBlock(base * 8, base * 4) self.up3 nn.ConvTranspose2d(base * 4, base * 2, 2, stride2) self.dec3 ConvBlock(base * 4, base * 2) self.up4 nn.ConvTranspose2d(base * 2, base, 2, stride2) self.dec4 ConvBlock(base * 2, base) self.out nn.Conv2d(base, out_ch, 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 torch.cat([self.up1(b), e4], dim1) d4 self.dec1(d4) d3 torch.cat([self.up2(d4), e3], dim1) d3 self.dec2(d3) d2 torch.cat([self.up3(d3), e2], dim1) d2 self.dec3(d2) d1 torch.cat([self.up4(d2), e1], dim1) d1 self.dec4(d1) return torch.sigmoid(self.out(d1)) # 输出范围 0~1两个通道这个网络的关键参数集中在base上默认 64。如果显存吃紧把base改成 32参数量会下降到原来的四分之一左右如果数据集特别小base32还能起到隐式正则的作用防止模型把训练样本背下来。out_ch2是双通道输出的核心推理时对每个通道分别做阈值化就能拿到视杯和视盘两套掩码。4.3 训练循环损失函数怎么同时管住两个分割头训练循环本身不复杂难点在损失函数。单独用标准 BCE 在眼底分割上有个明显问题背景像素占绝对多数网络很容易收敛到“全图预测为背景”的局部解因为这样损失已经很低了。我常用的做法是 BCE 加 Dice 损失两个通道分别计算再平均兼顾像素级准确率和区域重叠率def dice_loss(pred, target, eps1e-6): inter (pred * target).sum() union pred.sum() target.sum() return 1 - (2 * inter eps) / (union eps) def compute_loss(preds, cups, discs): # preds: (B, 2, H, W)cups/discs: (B, 1, H, W) loss_cup torch.nn.functional.binary_cross_entropy(preds[:, 0:1], cups) \ dice_loss(preds[:, 0:1], cups) loss_disc torch.nn.functional.binary_cross_entropy(preds[:, 1:2], discs) \ dice_loss(preds[:, 1:2], discs) return loss_cup loss_disc def train_one_epoch(model, loader, optimizer, device): model.train() total_loss 0.0 for imgs, cups, discs in loader: imgs imgs.to(device) cups cups.to(device) discs discs.to(device) optimizer.zero_grad() preds model(imgs) loss compute_loss(preds, cups, discs) loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) return total_loss / len(loader.dataset)注意cups[:, 0:1]的写法保留通道维度BCE 要求预测和标签维度一致。optimizer.zero_grad()、loss.backward()、optimizer.step()是标准三步如果发现训练震荡优先把学习率从 1e-4 降到 3e-5而不是改网络结构。训练主流程里还要记录验证集 Dice 并保存 checkpointdef evaluate(model, loader, device): model.eval() disc_dice, cup_dice 0.0, 0.0 with torch.no_grad(): for imgs, cups, discs in loader: imgs, cups, discs imgs.to(device), cups.to(device), discs.to(device) preds model(imgs) disc_dice dice_score(preds[:, 1:2], discs).item() cup_dice dice_score(preds[:, 0:1], cups).item() return disc_dice / len(loader), cup_dice / len(loader)数据加载器我按(img, cup_label, disc_label)三个元素返回而不是把两张掩码叠成一个向量。这样训练循环里每个头的标签单独取用逻辑清楚出问题时也好定位。4.4 推理与可视化输出叠加图和交点标尺推理阶段要做的不是“出一张掩码”就完事而是生成三样东西视盘掩码、视杯掩码、叠加在眼底图上的可视化结果。叠加图是课程设计截图演示的核心素材评阅老师第一眼看到的就是它import cv2 import numpy as np import torch def infer_one_image(model, img_path, device, size(512, 512)): img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, size, interpolationcv2.INTER_LINEAR) tensor torch.from_numpy(img).permute(2, 0, 1).float().unsqueeze(0) / 255.0 tensor tensor.to(device) with torch.no_grad(): preds model(tensor)[0].cpu().numpy() # (2, H, W) cup_mask (preds[0] 0.5).astype(np.uint8) disc_mask (preds[1] 0.5).astype(np.uint8) # 可视化视盘轮廓用绿色视杯轮廓用红色 vis (img.copy() * 0.9).astype(np.uint8) contours_disc, _ cv2.findContours(disc_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours_cup, _ cv2.findContours(cup_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cv2.drawContours(vis, contours_disc, -1, (0, 255, 0), 2) cv2.drawContours(vis, contours_cup, -1, (255, 0, 0), 2) return vis, disc_mask, cup_mask这里preds[0]是视杯通道preds[1]是视盘通道。阈值取 0.5 是默认选择如果发现预测掩码偏碎可以把阈值提高到 0.6。轮廓绘制用cv2.findContours配合drawContours比逐像素描边更干净。注意先转 RGB 再可视化否则 OpenCV 默认的 BGR 顺序会让眼底图的红色血管显示成蓝色。5. 避坑排查视杯视盘分割最常见的四个翻车现场5.1 训练损失怎么都降不下去验证 Dice 一直低于 0.7现象训练十几个 epochloss 始终在 0.6 到 0.7 之间震荡验证集 Dice 也上不去。原因最常见的是负样本压倒正样本。一张 512×512 的眼底图里视盘区域只有几万像素背景占了几十万像素。纯 BCE 下网络把全部像素预测成背景就已经有了一个很低的初始损失梯度信号被背景淹没视盘的边缘特征学不到。解决换成 BCE Dice 的组合损失并且给视杯和视盘分别计算损失再相加。Dice 损失不关心类别比例只关心预测区域和真实区域的重叠度背景占多少像素不参与主导。我一般这样写loss bce(pred, target) dice_loss(pred, target)两个头各自算再相加。如果还不行检查学习率1e-4 是一个偏保守但可靠的起始值。5.2 Dice 看着不错但画出来的掩码边缘有一圈灰色毛边现象验证集 Dice 到 0.9 了但把预测掩码叠加到原图上以后边缘像被滤镜糊了一层不是锐利的黑白边界。原因网络输出是浮点概率图做可视化时如果不经过阈值化直接乘以 255 保存低于 1.0 的像素就会变成灰色。另一种情况是推理时为了把掩码恢复到原图尺寸用了双线性插值插值把概率图抹成了渐变带。解决推理阶段只做两件事一是阈值化(pred 0.5).astype(np.uint8)二是尺寸恢复用cv2.INTER_NEAREST最近邻插值。不要在可视化脚本里做任何形态学膨胀腐蚀那些操作会改变视盘的垂直直径直接污染杯盘比指标。5.3 验证集 Dice 忽高忽低昨天 0.88 今天 0.82现象什么都没改只是重新跑了一次训练脚本验证集 Dice 就出现了明显波动。原因随机种子没有固定。数据集划分每次重新 shuffle验证集换了一批样本同时 PyTorch 的权重初始化也是随机的两个随机性叠加指标自然来回跳。解决在脚本开头固定三处——random.seed(42)、torch.manual_seed(42)、np.random.seed(42)。如果有 DataLoader 的 shuffle把generatortorch.Generator().manual_seed(42)也传进去。固定之后任何指标变化都只能来自网络结构、损失函数或超参数调整课程设计的实验对比才有说服力。5.4 显存溢出或单 epoch 时间太长现象batch size 设了 16输入 512×512一跑就报 CUDA out of memory或者 60 个 epoch 要跑一整晚。原因512×512 输入在base64的标准 Unet 下单张 forward 约占 300MB 显存batch 16 再加反向传播的中间变量6GB 显存根本扛不住。解决先确认自己显卡的显存大小。如果不足 8GB把输入降到 256×256batch size 降到 4base降到 32。256×256 对视盘分割来说边缘精度略降但训练速度会快 4 倍以上。先用小配置跑通流程出第一版结果再上 512 精修。如果显存还是不够加梯度累积# 等效 batch size 4 * 4 16但显存只需 batch4 for i, (imgs, cups, discs) in enumerate(loader): loss compute_loss(model(imgs), cups, discs) / 4 loss.backward() if (i 1) % 4 0: optimizer.step() optimizer.zero_grad()梯度累积的本质是把大 batch 拆成若干小 batch各自算梯度后累加再统一更新一次参数。注意每次子 batch 的 loss 要除以累积步数否则梯度大小会被放大学习率就白调了。6. 验证与交付课程设计最后一步是让结果可复现、可展示6.1 用 Dice 和杯盘比双重验证分割质量训练结束后的第一件事不是截图而是算两组指标视盘 Dice、视杯 Dice。Dice 衡量区域重叠度0.92 以上的视盘 Dice 在公开数据集上属于合格水平。但光看 Dice 不够还要算杯盘比。杯盘比的计算方法是取视盘掩码的垂直直径和视杯掩码的垂直直径做比值。垂直直径的定义是掩码轮廓线上纵坐标最大和最小的两个点之差不要用 OpenCV 的外接矩形宽度那是水平直径。计算出来以后将自动分割结果换算的杯盘比与人工标注换算的杯盘比做差差值在 0.05 以内说明分割结果在临床尺度上可靠。这个验证方式在文档说明里写清楚比只贴一张 loss 曲线图有力得多。6.2 交付物六张图撑起一份高分课程设计报告截图演示是课程设计评分里最直观的一部分我一般保存六张图按训练进程排列数据集中一张原始眼底图、一张叠加人工标注的图训练中一张 loss 曲线、一张验证 Dice 曲线推理后一张预测掩码图、一张预测轮廓叠加原图。这六张图基本覆盖了“数据没喂错、训练在收敛、结果能落地”三个评阅老师最关心的点。还有两个容易被忽略的交付细节一是给train.py加--epochs、--batch_size、--lr三个命令行参数不要让人改代码才能换参数二是在models/目录里把网络结构图和每一层输出通道数写成 README答辩时可以直接照着讲。至于可视化截图存成 PNG 不要存 JPGJPG 压缩会在血管边缘留下块状伪影评阅老师放大看会以为你的掩码错了。6.3 我的习惯与收尾我自己做这类医学图像分割项目时习惯是每次训练完先看一眼最差的那个验证样本——不是看平均 Dice而是把预测结果和人工标注差距最大的那张图挑出来分析。十年前我拿到一份眼底数据集平均 Dice 0.93觉得已经很好结果打开最差样本发现视角特别偏视杯边缘几乎和背景融在一起模型把它多扩了一大圈。从那以后我就养成了这个习惯也建议你保留一份“失败样本与失败原因”的记录写进文档说明的附录里。评阅老师看到你连失败案例都能分析清楚会比看到一张完美的预测图更能确认这是你自己做的。希望帮到你。本文还有配套的精品资源点击获取