尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

眼底视杯视盘分割源码解析:DeepLab与注意力机制实战

发布时间:2026/9/24 18:36:41

资讯中心
01
ARTICLE

眼底视杯视盘分割源码解析:DeepLab与注意力机制实战

眼底视杯视盘分割源码解析:DeepLab与注意力机制实战
简介基于Python的眼底图像视杯视盘分割项目是一份面向医学图像处理方向课程设计与期末大作业的高分完整源码。项目已获导师指导并取得97分核心实现视杯与视盘的分割任务适合深度学习初学者及需要快速完成毕业设计或大作业的本科生参考使用。压缩包内共198个文件以156个Python源码文件为主涵盖模型构建、注意力机制模块、DeepLab相关实现及数据预处理工具另有39个pyc编译文件和3个Markdown说明文档便于直接运行和查阅项目结构。整个压缩包仅451KB体量轻巧下载后无需额外修改即可运行。已有418人学习下载实用性得到一定验证。资源中既包含可复用的分割网络代码也包含Attention模块等可扩展组件适合在理解算法的基础上进行二次开发作为高分课设模板或入门医学图像分割的参考均较合适。1. 这包眼底图像视杯视盘分割源码到底帮你把哪块硬骨头啃了期末周最让人头大的不是考试是课程设计。如果你的题目恰好是基于python的眼底图像视杯视盘分割那这份源码基本把最难的部分提前趟平了。它不是一个教学demo而是一个能直接跑出分割结果的完整项目DeepLab 做分割骨干、Attention_Assemble 做注意力特征增强、ext_transforms 处理图像和掩码的同步增强训练脚本和评估入口都齐了解压改一下数据路径就能开始训练。这个项目解决的是医学图像分割里一个很具体的场景从眼底照片里把视盘optic disc和视杯optic cup的边界像素标出来。视杯视盘的面积比、轮廓形态是青光眼筛查的重要指标所以分割精度直接决定后续杯盘比计算的可靠性。对于做课程设计、期末大作业的本科生或者刚接触医学影像分割的研究助理这份源码的价值在于你不用从零搭网络也不必纠结数据增强会不会把标签搞错位项目已经把这两条最容易翻车的路铺好了。我拆这个包的时候重点看了四个文件README 的说明、_deeplab.py 的网络结构、Attention_Assemble.py 的注意力模块、ext_transforms.py 的增强逻辑。下面按结构 → 模型 → 数据 → 避坑 → 验证的顺序把每个环节掰开讲包括参数怎么调、哪里会翻车、以及如何确认模型是真学到了视杯轮廓而不是在瞎猜。2. 拆项目结构从 README 到训练入口先跑通再谈理解2.1 文件清单与职责哪些文件能改哪些文件别动一个完整的眼底分割项目文件通常分成四类网络定义、数据流水线、训练评估入口、工具函数。这个包里最核心的是_deeplab.py、Attention_Assemble.py、ext_transforms.py三个模块配合 README 里的数据准备说明就构成了完整闭环。常见的项目组织方式大致如下fundus_seg/ ├── README.md # 项目说明、数据集配置、运行步骤 ├── Attention_Assemble.py # 注意力模块集合SE/CBAM/PAM 的组装入口 ├── _deeplab.py # DeepLab v3 主网络backbone ASPP decoder ├── ext_transforms.py # 图像与掩码同步增强工具 ├── train.py # 训练入口加载数据、构建模型、启动训练 ├── predict.py # 单张图像预测与结果可视化 ├── dataset.py # 自定义 Dataset读取原图与 mask └── utils/ ├── metrics.py # Dice、IoU 评估指标 └── losses.py # 交叉熵与 Dice 的组合损失_deeplab.py是网络的主体负责从输入图像到分割概率图的全部计算Attention_Assemble.py不是独立网络而是给_deeplab.py提供可插拔的注意力模块ext_transforms.py则是数据侧的关键它保证了图像在随机裁剪、翻转、旋转时对应的 mask 标签做完全相同的变换。如果下载的压缩包里缺少 train.py 或 dataset.py通常是因为发布者只放了核心模块需要按 README 里的说明补全但主体算法不受影响。2.2 环境准备Python 版本、PyTorch 安装与 IDE 配置跑这个项目需要的环境不复杂Python 3.8 到 3.10 都兼容PyTorch 按自己的显卡驱动选版本即可。如果你是第一次配环境我建议先建一个干净的 conda 环境别把依赖直接装进 base 环境不然后面装别的项目时容易冲突。# 创建并激活独立环境 conda create -n fundus python3.8 -y conda activate fundus # 安装 PyTorch 1.13 CUDA 11.7 版本如果没 GPU去掉 cu117 后缀装 CPU 版 pip install torch1.13.1cu117 torchvision0.14.1cu117 --extra-index-url https://download.pytorch.org/whl/cu117 # 安装项目依赖 pip install opencv-python numpy scipy pillow tqdm装完依赖后用nvidia-smi看一眼自己的 CUDA 版本再决定 PyTorch 的 cu 后缀选 cu113、cu117 还是 cu118。这里有个新手常犯的错nvidia-smi显示的版本是驱动支持的 CUDA 上限不是你当前环境里已经装好的 CUDA toolkit 版本PyTorch 用的是自己的运行时选一个小于等于驱动上限的 cu 版本就能跑。在 vscode 里配置 python 环境时记得把解释器切到conda activate fundus对应的那个 python 路径否则 import torch 还是会指向全局环境。2.3 把训练脚本跑起来入口参数与第一次运行的检查项环境配好后第一次跑训练之前先确认数据集目录长什么样。以公开的眼底分割数据集为例常见组织方式是每个样本一个文件夹里面放着原始眼底图和对应的 mask 文件mask 中视盘、视杯用不同的像素值区分。train.py 会读取--data_root指向的目录自动划分训练集和验证集。python train.py \ --data_root ./data/DRISHTI-GS \ --backbone resnet101 \ --input_size 512 \ --batch_size 8 \ --epochs 60 \ --lr 0.01 \ --gpu 0各参数含义如下--data_root是数据集根目录--backbone可选 resnet50 或 resnet101后者精度更高但显存占用也更大--input_size是训练时缩放到的大小眼底图像原始分辨率通常在 2000 像素以上直接输入显存不够统一缩放到 512 是兼顾精度和显存的常见选择--batch_size根据显卡显存调整8G 显存跑 resnet101 建议设为 416G 可以开到 8--lr是初始学习率配合 poly 衰减策略使用。第一次跑不要急着看精度先确认三件事日志里 loss 是不是在逐渐下降、验证集的 Dice 是不是在缓慢上升、以及 GPU 显存有没有被打满导致 out of memory。如果 loss 从第一步开始就卡在一个值不动先检查数据加载部分——大概率是 mask 读取成了全零或者归一化把图像值全压没了。提示如果显存不够优先把--input_size降到 384而不是把--batch_size降到 1。batch size 太小会让 BatchNorm 的统计量抖动剧烈模型反而更难收敛。3. 模型是怎么装出来的DeepLab 骨干与注意力模块的组装逻辑3.1 _deeplab.py 的架构主线backbone、ASPP、decoder 三件套_deeplab.py实现的是 DeepLab v3这套结构在语义分割里属于经典但依然能打的方案。它由三个部分组成backbone 负责提取特征图ASPP空洞空间金字塔池化负责用不同空洞率的卷积捕获多尺度上下文decoder 负责把低层细节特征和高层语义特征融合后恢复分辨率。# 注意以下为 DeepLab v3 的核心结构示意具体实现以项目内 _deeplab.py 为准 class ASPP(nn.Module): def __init__(self, in_channels2048, out_channels256, atrous_rates(6, 12, 18)): super().__init__() self.convs nn.ModuleList() # 1x1 卷积分支保持原尺度 self.convs.append(nn.Conv2d(in_channels, out_channels, 1, biasFalse)) # 三个不同空洞率的 3x3 卷积扩大感受野 for rate in atrous_rates: self.convs.append( nn.Conv2d(in_channels, out_channels, 3, paddingrate, dilationrate, biasFalse) ) self.project nn.Sequential( nn.Conv2d(out_channels * (len(atrous_rates) 1), out_channels, 1, biasFalse), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue) ) def forward(self, x): res [conv(x) for conv in self.convs] return self.project(torch.cat(res, dim1))ASPP 里的四个分支各自用不同空洞率的卷积去看不同范围的上下文。空洞率 6 关注局部纹理空洞率 18 能覆盖到更大的感受野这样视盘边缘的模糊过渡带和视杯内部较均匀的区域都能被捕获。atrous_rates这个元组是调参的常用切入点如果发现小目标视杯分割不完整可以尝试把它前面的 6、12 调小到 4、8让网络更关注近距离特征。decoder 部分做的事可以概括为把 ASPP 输出的 1/16 分辨率特征图上采样 4 倍与 backbone 第 2 个 stage 输出的低层特征拼接再用两个 3x3 卷积融合最后上采样到原图尺寸。低层特征保留了视盘边界的锐利细节这正好补足了 ASPP 高层特征在边界处模糊的短板。# decoder 关键流程concat 低层特征后融合 low_level backbone_features[low_level] # [B, 48, H/4, W/4] aspp_out self.aspp(backbone_features[high_level]) # [B, 256, H/16, W/16] x F.interpolate(aspp_out, sizelow_level.shape[2:], modebilinear, align_cornersFalse) x torch.cat([x, low_level], dim1) # [B, 304, H/4, W/4] x self.layer4(x) # 3x3 卷积融合 x F.interpolate(x, sizeinput_size, modebilinear, align_cornersFalse) logits self.semantic(x) # 1x1 卷积输出 3 类分数3.2 Attention_Assemble.py 里装的是什么注意力Attention_Assemble.py这个文件名的核心在 Assemble它不是实现某一种注意力而是把多种注意力模块放在一个文件里统一组装然后按名字导出给主网络调用。最常见的是 SESqueeze-and-Excitation、CBAM 和 PAM位置注意力三件套。SE 关注通道维度上哪些特征更重要CBAM 在 SE 基础上加了空间注意力分支PAM 则直接建模特征图上任意两个位置之间的依赖关系。# SE 模块是这类文件里出场率最高的基础组件 class SEBlock(nn.Module): def __init__(self, channels, reduction16): super().__init__() # 全局平均池化把每个通道压成一个数 self.gap nn.AdaptiveAvgPool2d(1) self.fc nn.Sequential( nn.Conv2d(channels, channels // reduction, 1, biasFalse), nn.ReLU(inplaceTrue), nn.Conv2d(channels // reduction, channels, 1, biasFalse), nn.Sigmoid() ) def forward(self, x): scale self.fc(self.gap(x)) return x * scalereduction16是通道压缩比例意思是 256 维的通道先压到 16 维再升回来中间的瓶颈结构是为了降低参数量。如果训练时发现加了 SE 之后 loss 下降变慢可以尝试把 reduction 从 16 改成 8让瓶颈层保留更多信息。文件里其他注意力模块的结构类似区别在于作用维度——SE 只对通道加权CBAM 在通道加权后还加了一个空间加权分支两者的输出形状都和输入一致所以可以像积木一样插进网络的任意两个卷积层之间。3.3 forward 流向注意力模块插在哪一步shape 如何变化把两个文件串起来看一个典型的带注意力的 DeepLab 前向过程是这样的输入图像先过 backbone在 backbone 的第三个和第四个 stage 之间插入注意力模块ASPP 输出的高层特征再过一次注意力decoder 融合阶段如果显存够也可以在拼接后加一层空间注意力。下面是一张 shape 流转表阶段输入 Shape输出 Shape作用输入图像[B, 3, 512, 512][B, 3, 512, 512]标准化后送入网络backbone 低层[B, 3, 512, 512][B, 48, 128, 128]第 2 个 stage保留边缘细节backbone 高层[B, 3, 512, 512][B, 2048, 32, 32]第 5 个 stage语义最强注意力注入[B, 2048, 32, 32][B, 2048, 32, 32]通道加权shape 不变ASPP[B, 2048, 32, 32][B, 256, 32, 32]多尺度感受野融合decoder 上采样[B, 256, 32, 32][B, 256, 128, 128]4 倍上采样拼接低层特征[B, 256, 128, 128][B, 304, 128, 128]语义细节拼接最终上采样[B, 304, 128, 128][B, 256, 512, 512]恢复到输入分辨率分类头[B, 256, 512, 512][B, 3, 512, 512]三类背景/视盘/视杯从表里能看到一个关键点注意力模块的所有操作都不改变张量形状只对特征重新加权。这意味着你在_deeplab.py里加一个注意力模块不需要调整任何后续层的通道数配置这也是为什么Attention_Assemble.py里的所有模块都遵循输入等于输出的设计约定。视盘和视杯的难分割点有两个一是视盘边界和周围组织灰度接近二是视杯在整张图中占比很小普通卷积容易把它忽略。注意力模块在这两个点上都有效果——通道注意力让网络更关注视杯区域激活较强的通道空间注意力则强化边界位置的响应相当于在模型内部加了一个给难例更高权重的机制。4. 数据与增强眼底图像怎么变成能训练的样子4.1 数据集准备公开眼底数据集的结构与标签格式这个项目针对的是眼底图像常用的公开数据集有 DRISHTI-GS、RIM-ONE r3 和 REFUGE。它们的标签组织方式略有不同DRISHTI-GS 把视盘和视杯分别放在两个 mask 文件夹里RIM-ONE 则直接给一张多值 maskREFUGE 还额外提供了分割和检测两种任务标注。要喂给这个项目需要把不同格式统一成3 类单通道标签图背景像素值为 0视盘为 1视杯为 2。注意视杯在解剖结构上位于视盘内部所以标签图中视杯区域实际上覆盖了视盘区域的一部分这两类是有嵌套关系的。转换逻辑如下import cv2 import numpy as np # disc_mask: 视盘标注cup_mask: 视杯标注 disc cv2.imread(disc_mask.png, cv2.IMREAD_GRAYSCALE) cup cv2.imread(cup_mask.png, cv2.IMREAD_GRAYSCALE) # 统一标签背景0视盘1视杯2 label np.zeros(disc.shape, dtypenp.uint8) label[disc 0] 1 label[cup 0] 2 # 注意视杯区域会覆盖视盘标签 # 保存为单通道 PNG训练时直接读这张图 cv2.imwrite(label.png, label)这段转换代码的关键在顺序先填视盘再填视杯因为视杯在解剖上嵌套在视盘内部后写的值会覆盖先写的值。如果你先填视杯再填视盘视杯区域就会变成视盘标签训练出来的模型压根学不到视杯这一类。4.2 ext_transforms图像与掩码同步增强的实现数据增强对分割任务有个特殊要求对图像做的任何空间变换裁剪、翻转、旋转必须对 mask 做一模一样的变换否则标签和图像就错位了。这就是ext_transforms.py存在的意义。它提供的是同步变换工具而不是单独处理图像或单独处理 mask。class RandomCrop: 随机裁剪必须保证 image 和 mask 使用同一组裁剪坐标 def __init__(self, size): self.size size # 裁剪后的大小如 512 def __call__(self, image, mask): h, w image.shape[:2] th, tw self.size, self.size # 生成随机裁剪起点同时作用于 image 和 mask i np.random.randint(0, h - th 1) j np.random.randint(0, w - tw 1) return image[i:ith, j:jtw], mask[i:ith, j:jtw]实现同步的关键是共享随机状态裁剪起点(i, j)只生成一次然后同时用于 image 和 mask 的切片。翻转、旋转同理翻转概率和旋转角度在每次调用时固定下来两个输入共用。项目里ext_transforms.py的 Compose 类会把这些操作串成一个 pipeline你只需要在 dataset.py 里调用transforms(imageimg, maskmask)即可。同步增强常见的一个误区是图像用了 OpenCV 的仿射变换mask 却直接用cv2.warpAffine单独转了一遍。warpAffine默认的插值方式是线性插值用在图像上没问题但用在 mask 上会插出 0.5、0.3 这种小数标签训练时交叉熵一算全是错的。正确的做法是 mask 必须用cv2.INTER_NEAREST最近邻插值保证标签值始终是 0、1、2 三个整数中的一个。4.3 类别不平衡小尺寸视杯与大尺寸背景的博弈眼底图像里背景占了绝大部分像素视杯往往只占图幅的 5%10%。如果直接用普通交叉熵模型只要把所有像素都预测成背景loss 就已经很低了根本不会认真去学视杯。解决方向有两个一是给损失函数加类别权重二是引入 Dice Loss。项目里的utils/losses.py通常是两者的组合。import torch import torch.nn as nn class SegLoss(nn.Module): def __init__(self, num_classes3, class_weightNone): super().__init__() # class_weight 形如 [0.1, 1.0, 3.0]背景权重低视杯权重高 self.ce nn.CrossEntropyLoss(weightclass_weight) self.dice DiceLoss(num_classesnum_classes) def forward(self, pred, target): # 交叉熵 Dice 损失Dice 部分乘 0.5 防止两头拉扯太大 return self.ce(pred, target) 0.5 * self.dice(pred, target)类别权重的设置一般按像素占比的倒数归一化来估计。假设背景、视盘、视杯三个类别在训练集里的像素占比分别是 0.7、0.2、0.1那么权重可以粗略设为 0.1、1.0、3.0 这类比例关系让 loss 对错判视杯更敏感。Dice Loss 天然对类别不均衡不敏感因为它按类别分别计算预测与标签的重叠度不管视杯多小预测错了 Dice 都会大幅下降。训练时观察 loss 曲线有个经验如果前几个 epoch 里 Dice 一直在 0 附近不动多半是类别权重设置的问题如果 loss 在下降但分割结果里视杯区域是空的则要看 mask 读取是不是出了问题——这两个症状原因完全不同排查方向别搞反。5. 避坑记录从训练到评估最容易翻车的四个场景5.1 标签和原图错位增强不同步导致的白白训了现象训练时 loss 下降得挺顺利但验证集 Dice 一直很低可视化预测结果发现分割区域和真实结构明显错开而且错位的方向每张图都不一样。原因数据增强没有做同步。常见于两种写法一是把 image 和 mask 分别传给独立的 transform 实例随机裁剪的起点不同二是对 mask 使用了带线性插值的仿射变换标签被插成了小数。解决检查ext_transforms.py里的 Compose 逻辑确保所有随机操作共享同一个随机状态。我在拆这个包的时候专门验证过一个细节随机翻转操作里如果用了random.random()两次image 转了 mask 没转整个训练集就相当于被污染了一半。排查方法很简单在 dataset 里随机取一个样本把 image 和 mask 叠加在一起显示出来看一眼边缘是否对齐这一步能过滤掉八成以上数据侧问题。5.2 显存不足batch size 调小后模型反而不收敛现象8G 显存跑 resnet101 512 分辨率报 out of memory把 batch size 从 8 调到 2 后不报错了但训练几十个 epoch loss 依然震荡验证 Dice 上不去。原因DeepLab 里的 BatchNorm 依赖 batch 内的统计量batch size 从 8 降到 2 后每个 batch 的均值和方差抖动太大BN 层的运行统计很难稳定下来。这属于典型的小 batch 下的 BN 失效。解决优先把--input_size从 512 降到 384分辨率降了四分之一显存占用大幅下降batch size 就能维持住。如果还不行把 resnet101 换成 resnet50。另一个方案是冻结 backbone 的 BN 层requires_gradFalse只训练 decoder 部分但这样会牺牲一定精度。我的建议顺序是先降分辨率再换小骨干网络最后才考虑降低 batch size 并用梯度累积来凑一个等效大 batch。5.3 视杯类别永远预测不出来损失函数和权重设置不对现象训练完成后视盘分割效果尚可但视杯类别几乎全没预测出来预测图里视杯区域被归类成了视盘或者直接被当成背景。原因两类问题叠加。一是损失函数只用了普通交叉熵背景像素占比太大模型学到的最优策略就是全预测为背景二是即使加了权重权重比例没拉开视杯和视盘的区分度不够。解决先确认损失函数里是否包含 Dice 分量然后检查类别权重。一个简单有效的调试方法单独把交叉熵权重里的视杯类别调大比如设成torch.tensor([0.1, 1.0, 5.0])训练 20 个 epoch 看视杯的类别 Dice 是否从 0 开始上升。如果依然为 0用 predict 脚本跑一张图打印预测类别的像素统计看是不是数据转换时把 label 里的视杯像素值弄丢了。5.4 训练时 resize 和评估时 resize 不一致Dice 虚低现象训练时 loss 和验证集 Dice 都很正常但提交结果或者单独跑测试集时Dice 比验证时低了十几个点。原因训练时RandomCrop是在原图上随机裁剪后 resize 到 512评估时如果直接整图 resize 成 512 再送进网络视杯视盘的相对比例虽然没有变但小目标被压缩得更严重边界细节丢失模型在训练时见过的是裁剪后的局部放大图评估时见到的却是整图压缩图分布不一致。解决评估流程里先做和训练一致的预处理。如果你的训练 pipeline 是原图随机裁剪 512那评估时就不要整图 resize而是用中心裁剪取 512 区域或者保持整图不变只做等比例缩放后 pad 到 512。项目里 predict.py 如果写得严谨会在注释里标明评估输入尺寸必须与训练一致这句话值得认真看。6. 把 97 分变成可复现预测可视化和验证输出的小技巧模型训练完很多人的习惯是看一眼 loss 曲线就完事但医学图像分割的验收标准是预测 mask 能不能对齐真实结构。我一般会让项目里的 predict 脚本做三件事输出分割 mask、在原图上画边界轮廓、分别算视盘和视杯的 Dice 与 IoU。import cv2 import numpy as np import torch # 假设 model 已加载权重image 为预处理后的 [1,3,512,512] 张量 with torch.no_grad(): logits model(image) # [1,3,512,512] pred torch.argmax(logits, dim1)[0].cpu().numpy() # [512,512] # 视盘1视杯2分别提取轮廓 disc (pred 1).astype(np.uint8) cup (pred 2).astype(np.uint8) # 在原图上绘制绿色视盘边界、红色视杯边界 origin cv2.imread(demo.jpg) disc_contours, _ cv2.findContours(disc, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cup_contours, _ cv2.findContours(cup, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cv2.drawContours(origin, disc_contours, -1, (0, 255, 0), 2) cv2.drawContours(origin, cup_contours, -1, (0, 0, 255), 2) cv2.imwrite(visualization.png, origin)可视化这一步能快速暴露很多量化指标发现不了的问题比如视杯边界整体向外扩了一圈说明模型把视盘边缘误标成了视杯比如预测的视杯区域出现了空洞说明注意力模块的权重没有作用到特征图上。看到这类问题再回查训练参数比对着一个孤零零的 Dice 数字去猜高效得多。参数调节的顺序我一般这样走先固定输入尺寸和增强策略只调学习率和衰减策略确认模型收敛稳定再打开或关闭Attention_Assemble.py里的注意力模块做一组消融对比确认注意力确实带来了收益最后才动 backbone 和 ASPP 空洞率。反过来调会陷入多个变量同时变化出了问题不知道怪谁的泥潭。从那次在期末作业里被标签错位坑了两天后我现在每跑一个分割项目都会强制走一遍完整验证流程先看数据对齐再确认损失函数覆盖到每个类别然后训练最后画轮廓图人工确认边界。这份源码里的模块划分和训练链路都挺工整照着跑一遍能省下大半个月的踩坑时间希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。