简介面向医学图像处理与深度学习初学者的UNet视网膜血管分割完整项目基于PyTorch框架实现选用DRIVE公开数据集完成模型训练与测试。项目聚焦眼底图像中血管结构的自动提取适用于疾病早期筛查及相关科研教学场景。压缩包共包含34个文件以7个Python源码文件为核心覆盖数据预处理、模型构建、损失函数、训练与测试等完整流程20张PNG图片为分割结果可视化展示便于直观对比效果另含README说明、附赠文档及数据集压缩包等辅助材料整体大小36.81MB。资源已有133人学习下载。通过该项目读者可系统掌握UNet在医学图像分割中的落地方法获得一套可在本地复现的工程结构包括数据增强与标准化脚本、训练评估工具链以及带标注的DRIVE数据集为开展血管分割实验或论文复现提供了直接可用的参考实现。1. 视网膜血管分割项目为什么UNet PyTorch DRIVE能成为医学图像分割的入门标配想跑一个UNet网络做医学图像分割最经典的起点就是视网膜血管分割。这个项目用PyTorch框架在DRIVE公开数据集上完成从数据预处理、模型训练到测试评估的完整深度学习流程还带上了预处理脚本和可视化工具几乎把医学图像分割的每个环节都覆盖了一遍。很多人觉得UNet难上手其实难点不在模型本身而在数据怎么处理、训练怎么调参、结果怎么验证。这篇文章就按我实际跑通这个项目的顺序把每一步拆开讲清楚包括参数设置和踩过的坑让新手能照着复现让熟手能快速定位问题。2. 先从数据下手DRIVE数据集结构与预处理脚本的四个关键步骤2.1 DRIVE数据集文件构成与标注格式解析DRIVEDigital Retinal Images for Vessel Extraction是视网膜血管分割最常用的公开基准数据集40张眼底彩照分成20张训练、20张测试每张都是565×584像素的RGB图像。标注是手工逐像素画的血管掩码另外还有一个FOV掩码文件标明眼底图像的有效区域测试集还额外提供了第二组人工标注用来算分割的一致性和AUC。第一次拿到这份数据时很多人会直接拿原始图像去喂模型结果训练半天指标上不去。原因很简单眼底图像本身存在光照不均、对比度低、血管与背景灰度接近的问题不做预处理UNet即使能收敛分割出的血管也会断断续续。所以项目里配套的数据预处理脚本是关键不是可有可无的辅助工具。训练集的文件结构一般是这样的images目录放原始视网膜图manual目录放手工标注的血管掩码mask目录放FOV掩码。测试集多一个manual2目录用于评估时计算分割一致性和平均精度AP。如果拿到手的文件是.zip解压出来的先确认目录结构匹配再写脚本读取。2.2 用Python脚本完成CLAHE增强与归一化预处理的核心是CLAHEContrast Limited Adaptive Histogram Equalization即限制对比度的自适应直方图均衡化。普通直方图均衡化对整张图做全局拉伸眼底图像光照不均时容易过曝或欠曝CLAHE把图像切成小块默认8×8网格在每个小块内做均衡化再限制对比度幅度避免噪声被过度放大。对血管分割来说CLAHE能让细小的毛细血管从背景里浮现出来这是整个预处理里性价比最高的一步。一个常见的预处理脚本如下import cv2 import numpy as np from PIL import Image def preprocess_image(img_path, clip_limit2.0, tile_grid_size(8, 8)): # 读取图像并转为灰度 img cv2.imread(str(img_path)) img cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 创建CLAHE对象并应用 clahe cv2.createCLAHE(clipLimitclip_limit, tileGridSizetile_grid_size) enhanced clahe.apply(img) # 归一化到[0,1]保持浮点精度 normalized enhanced.astype(np.float32) / 255.0 return normalized这段代码的逻辑分三步先转灰度因为血管分割本质上是单通道任务彩色信息对UNet的帮助有限反而增加计算量然后用CLAHE增强对比度clipLimit控制对比度限制阈值取2.0是比较稳的起点调大到4.0会更强地拉伸对比度但噪声也会更明显最后除以255归一化到[0,1]区间避免模型输入数值范围过大导致梯度不稳定。注意tileGridSize取(8,8)是OpenCV默认值对565×584这种小图足够细致不需要额外调大。很多人在这一步翻车是因为归一化时机错了。比如先在CLAHE之前做了归一化再调CLAHE那clipLimit的语义就变了数值范围都不同增强效果完全不对。正确顺序一定是灰度化 → CLAHE增强 → 归一化。2.3 标签处理与数据增强翻转、旋转、裁剪的参数怎么定标签和图像必须做完全一致的处理。血管掩码是二值的0和255模型输出是概率图我们用BCEWithLogitsLoss时标签要转成0和1的浮点型。如果标签还是0和255损失函数算出的值会异常偏大模型怎么训练都收敛不了。合理的处理方式是def load_label(mask_path): mask cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) mask (mask 127).astype(np.float32) # 二值化并转为0/1 return mask这一步的关键是把255的像素值压成1.0。阈值取127是因为血管标注是纯黑白的只要大于127就认为是前景。拿到FOV掩码后也要做同样的二值化后续计算Dice时可以只统计FOV内部的像素避免把黑色背景区域计入。数据增强方面医学图像样本量少训练集只有20张必须用增强来扩充。常见参数组合是随机水平翻转概率0.5、随机垂直翻转概率0.5、随机旋转范围[-10°, 10°]、随机裁剪输入尺寸256×256。旋转角度不要太大视网膜图像里血管的走向有解剖学意义旋转超过45度会产生大量不真实样本模型学到的是奇怪的血管形态。裁剪尺寸选256×256是UNet比较常见的输入尺寸显存占用适中batch size取8时大约占用6GB左右而且能保留足够的上下文信息。这里有个隐含坑对标签做旋转或翻转时插值方法要用INTER_NEAREST最近邻插值不能用INTER_LINEAR双线性插值。因为线性插值会在血管边缘产生中间灰度值比如0.5本来二值的标签被污染成灰度图训练时模型会被这些中间值搞糊涂。图像用线性插值没问题标签必须用最近邻。3. 搭建UNet网络PyTorch实现编码器-解码器与跳跃连接3.1 UNet架构拆解编码器、瓶颈、解码器各自承担什么UNet之所以叫U型网络是因为它由左边一条收缩路径编码器和右边一条扩张路径解码器组成中间通过跳跃连接把同尺度的特征拼接起来。编码器由多个卷积块和下采样组成每下采样一次特征图分辨率减半、通道数翻倍提取的是从细到粗的语义特征解码器逐步上采样把低分辨率的语义特征恢复到原始分辨率同时通过跳跃连接把编码器各层的细节特征拼回来弥补下采样丢失的边界信息。这个设计对血管分割特别友好血管是细长结构下采样太深会丢掉细血管的轮廓跳跃连接让解码器能直接访问编码器各层的细节特征血管边缘就不再是模糊的一团。这也是UNet比普通FCN在医学图像分割上效果好的核心原因——不是模型更深而是信息传递路径更完整。PyTorch实现UNet时一般把编码器和解码器的重复模块抽象成DoubleConv和Down、Up几个类代码清晰也方便改通道数。一个完整的UNet定义结构如下不依赖第三方库只用PyTorch基础模块import torch import torch.nn as nn import torch.nn.functional as F class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, kernel_size3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, kernel_size3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x) class Down(nn.Module): 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): def __init__(self, in_ch, out_ch): super().__init__() # 转置卷积上采样通道减半 self.up nn.ConvTranspose2d(in_ch, in_ch // 2, kernel_size2, stride2) self.conv DoubleConv(in_ch, out_ch) def forward(self, x1, x2): x1 self.up(x1) # 跳跃连接拼接同尺寸的编码器特征 diffY x2.size()[2] - x1.size()[2] diffX x2.size()[3] - x1.size()[3] x1 F.pad(x1, [diffX // 2, diffX - diffX // 2, diffY // 2, diffY - diffY // 2]) x torch.cat([x2, x1], dim1) return self.conv(x)重点说明三点。第一DoubleConv里每个卷积后面跟了BatchNorm2d这个对血管分割很关键因为输入图像经过CLAHE后虽然对比度改善了但不同批次图像的灰度分布还是有差异BatchNorm能稳定训练。去掉BatchNorm的话同样学习率下训练波动会明显变大。第二Down先用MaxPool2d下采样再接DoubleConv这是UNet原论文的结构池化能保留平移不变性。第三Up里用转置卷积做上采样kernel_size2, stride2输出尺寸刚好翻倍。转置卷积比双线性插值多一组可学习参数能更精细地恢复血管细节代价是参数量略增、训练时更容易出现过拟合所以数据增强在这种结构下更必要。拼接前有个细节如果输入尺寸不是2的整数次幂下采样后x1和x2的尺寸可能不一致需要用F.pad做对称填充。代码里的diffY和diffX就是干这个的。实际跑DRIVE时输入图统一裁剪成256×256不会触发这个分支但改成384×384或其他不规整尺寸时这个逻辑能自动处理省事很多。完整的UNet主体则按原论文的通道数设定第一层64通道之后每次下采样翻倍直到512class UNet(nn.Module): def __init__(self, n_channels1, n_classes1): super().__init__() self.inc DoubleConv(n_channels, 64) self.down1 Down(64, 128) self.down2 Down(128, 256) self.down3 Down(256, 512) self.down4 Down(512, 512) self.up1 Up(1024, 256) self.up2 Up(512, 128) self.up3 Up(256, 64) self.up4 Up(128, 64) self.outc nn.Conv2d(64, n_classes, kernel_size1) def forward(self, x): x1 self.inc(x) x2 self.down1(x1) x3 self.down2(x2) x4 self.down3(x3) x5 self.down4(x4) x self.up1(x5, x4) x self.up2(x, x3) x self.up3(x, x2) x self.up4(x, x1) return self.outc(x)注意up1的输入通道是1024因为x5是512通道拼接了x4的512通道拼接后是1024。通道数写错是最容易出的编译错误PyTorch在torch.cat时会直接报维度不匹配看到“Given groups1, weight of size…”这类错误时先检查拼接处的通道数。n_classes1是因为我们只分割血管这一类前景输出单通道的概率图用sigmoid做二分类。如果要扩展成动脉/静脉多分类把n_classes改成对应数量即可但训练数据标注也需要对应。3.2 UNet训练自己的数据集时输入尺寸和通道数怎么改最稳换到自己数据集时很多人有两个直觉错误一是觉得输入尺寸越大越好直接把整张原始图丢进去二是觉得UNet是固定结构通道数不能改。真实情况是DRIVE原始图565×584直接放进GPU训练batch size只能设为1UNet在深层特征图上的感受野覆盖不到全局血管分布效果反而差。我一般做法是统一裁剪到256×256训练时随机裁剪测试时中心裁剪这样batch size能稳定在8-16之间训练速度和效果都比较均衡。通道数方面如果显存不够比如只有6GB可以统一把64/128/256/512/512改成32/64/128/256/256模型参数量会降到原来的1/4左右。代价是特征表达变弱血管细分支的还原度会差一些。如果显存充裕12GB以上可以试着把首层提到96或128分割精度有小幅提升但训练时间会增加。项目里默认用64起步是兼顾速度和精度的折中值数据量不大的情况下加大通道数带来的提升很有限反而容易过拟合。4. 训练与测试完整深度学习流程的八个必调参数4.1 DataLoader配置与训练循环细节预处理完成、模型定义好后接下来是把数据送进训练流程。DRIVE训练集只有20张图按256×256随机裁剪做增强后理论上每张图可以裁剪出大量patch所以不用像分类任务那样做多轮epoch才能收敛。项目里常见的配置是batch_size8、epochs60左右配合Adam优化器、学习率1e-3到1e-4的衰减策略。DataLoader的写法有讲究num_workers、pin_memory这些参数直接影响训练速度。一个典型的训练脚本片段如下from torch.utils.data import Dataset, DataLoader from torchvision import transforms class DRIVEDataset(Dataset): def __init__(self, image_dir, mask_dir, crop_size256, trainTrue): self.image_paths sorted(list(image_dir.glob(*.tif))) self.mask_paths sorted(list(mask_dir.glob(*.gif))) self.crop_size crop_size self.train train def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img preprocess_image(self.image_paths[idx]) # 读图CLAHE归一化 mask load_label(self.mask_paths[idx]) # 读标签二值化 if self.train: # 随机裁剪到crop_size x np.random.randint(0, img.shape[1] - self.crop_size) y np.random.randint(0, img.shape[0] - self.crop_size) img img[y:yself.crop_size, x:xself.crop_size] mask mask[y:yself.crop_size, x:xself.crop_size] # 随机翻转和旋转代码略注意标签用最近邻插值 return (torch.from_numpy(img).unsqueeze(0).float(), torch.from_numpy(mask).unsqueeze(0).float()) train_loader DataLoader( DRIVEDataset(train_img_dir, train_mask_dir, trainTrue), batch_size8, shuffleTrue, num_workers4, pin_memoryTrue )num_workers决定了用几个子进程加载数据Windows上设0或2比较安全Linux上可以设4-8。设太高时如果机器CPU核数不够反而会因为进程切换开销拖慢训练。pin_memoryTrue把数据放到锁页内存GPU显存拷贝快一个档次但会占用更多的物理内存16GB内存以下不建议开。训练循环里三个容易忽略的细节一是每个epoch开始前调用model.train()验证前调用model.eval()否则BatchNorm和Dropout的行为在训练和推理时不一致验证指标会虚高二是optimizer.zero_grad()一定要在loss.backward()之前做忘了清梯度的话梯度会累加loss曲线会莫名其妙震荡三是学习率调整项目里常用ReduceLROnPlateau当验证集Dice连续5个epoch不涨时把学习率降到原来的1/10。optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemax, factor0.5, patience5 ) criterion nn.BCEWithLogitsLoss() for epoch in range(60): model.train() for img, mask in train_loader: img, mask img.to(device), mask.to(device) optimizer.zero_grad() logits model(img) loss criterion(logits, mask) loss.backward() optimizer.step() # 验证后调用scheduler.step(val_dice)实现动态学习率modemax表示监控指标是越大越好这里是Dice系数factor0.5表示学习率减半patience5表示容忍5个epoch不提升才衰减。这套配置对DRIVE这种小数据集非常稳定一般前10个epoch就会看到Dice从0.3左右爬到0.7以上30个epoch后趋于平稳。4.2 损失函数选型BCEWithLogitsLoss和Dice Loss怎么组合血管分割是典型的类别不平衡问题血管像素只占整个图像约10%-12%背景占了近90%。如果直接用普通的BCELoss模型会倾向把所有像素预测为背景因为这样就能拿到约90%的准确率但血管全没分割出来。BCEWithLogitsLoss自带sigmoid和数值稳定的对数计算比手写F.sigmoid F.binary_cross_entropy更安全这是首选基础损失。但BCE在极度不平衡时仍然会偏向多数类所以项目里通常会把它和Dice Loss组合使用。Dice Loss直接优化Dice系数本身对前景和背景的权重天然相等。组合方式常见有两种加权求和loss bce_loss dice_loss或者线性组合loss 0.5 * bce_loss 0.5 * dice_loss。实践中第一种更直接不需要调权重因为量纲上BCE在0.1-1之间Dice Loss在0-1之间直接相加即可。以下是Dice Loss的实现def dice_loss(logits, targets, smooth1e-6): probs torch.sigmoid(logits) # 将拉平成向量逐像素计算交集 probs probs.reshape(probs.size(0), -1) targets targets.reshape(targets.size(0), -1) intersection (probs * targets).sum(dim1) total probs.sum(dim1) targets.sum(dim1) dice (2.0 * intersection smooth) / (total smooth) return 1.0 - dice.mean()smooth加在分子分母上是为了防止除零当预测和标签都是全零时取1e-6即可。当模型预测全为背景且标签也是全背景时dice算出来是1loss是0这是合理的。但要注意如果整批样本里包含了大量纯背景区域裁剪到图像边缘时可能出现Dice Loss在训练初期会不稳定因为纯背景样本的损失天然偏低模型会更偏向保守预测。这时候把数据增强里的随机裁剪改成裁剪时保证至少20%区域在FOV内或者过滤掉血管像素占比低于5%的patch能显著改善训练的稳定性。4.3 测试流程加载最佳模型、计算Dice和AUROC训练完后测试不是简单跑一遍测试集就完事。DRIVE的测试集有专门的评估协议用mask目录下的FOV掩码限定评估区域只统计FOV内部的像素。原因很直接眼底图像的黑色边框区域既不属于视网膜也不属于血管如果把这些像素算进去背景占比更高Dice系数看起来虚高但实际分割质量没有提升。测试脚本的关键部分如下def evaluate(model, dataloader, device): model.eval() dices, aucs [], [] with torch.no_grad(): for img, mask, fov in dataloader: img, mask, fov img.to(device), mask.to(device), fov.to(device) logits model(img) probs torch.sigmoid(logits) # 只保留FOV内的预测 pred (probs 0.5).float() * fov mask mask * fov # 计算Dice inter (pred * mask).sum() dice (2 * inter) / (pred.sum() mask.sum() 1e-6) dices.append(dice.item()) return np.mean(dices)阈值0.5是默认选择但对血管分割来说调低阈值比如0.4可以召回更多细血管调高阈值0.6则更精确但会漏掉末端毛细血管。具体阈值取决于用途如果是辅助医生做形态学测量精度优先阈值取高如果是筛查血管异常召回率优先阈值取低。项目里如果提供了prob_maps保存功能建议把预测概率图存成.npy后续算阈值曲线、ROC都用得上不用重新跑一遍推理。5. 可视化工具与避坑指南从损失曲线到分割结果5.1 用Matplotlib和OpenCV完成损失曲线与预测结果可视化项目里的可视化工具不是附加功能而是定位训练问题的关键手段。最核心的两个可视化一是训练过程中的损失曲线和Dice曲线二是模型在测试集上的预测结果叠加图。损失曲线能告诉你模型是否收敛、是否过拟合预测结果图能直观看出哪些血管被漏掉了、哪些区域噪声重。损失曲线的画法很简单用Matplotlib记录每个epoch的train loss和val loss即可。我一般会记录loss和val_dice两个量训练完一次性画出来import matplotlib.pyplot as plt def plot_curves(history): epochs range(1, len(history[train_loss]) 1) fig, ax1 plt.subplots(figsize(10, 4)) ax1.plot(epochs, history[train_loss], labelTrain Loss, colortab:blue) ax1.set_xlabel(Epoch); ax1.set_ylabel(Loss) ax2 ax1.twinx() ax2.plot(epochs, history[val_dice], labelVal Dice, colortab:orange) ax2.set_ylabel(Dice); fig.tight_layout() plt.savefig(training_history.png, dpi150)查看曲线时有一个很实用的判断标准train loss持续下降但val dice不涨甚至下降说明过拟合了需要加强数据增强、增加weight decay或把学习率调低train loss和val dice都不动比如val dice卡在0.6以下说明模型欠拟合优先检查学习率是不是太低、BatchNorm是否写进了模型而不是光写了卷积。预测结果可视化用于定性观察。用OpenCV时注意最终输出合成分割图血管部分要叠加在原始图上背景透明这样能一眼看到漏检区域def visualize_prediction(image, prob_map, save_path, threshold0.5): image (image.numpy() * 255).astype(np.uint8) mask (prob_map threshold).astype(np.uint8) * 255 # 血管掩码转为绿色叠加在灰度图上 overlay cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) overlay[mask 0] (0, 255, 0) cv2.imwrite(str(save_path), overlay)prob_map是sigmoid输出的概率图threshold0.5是默认分割阈值。如果看到大量细小的绿色散点孤立地分布在背景区说明模型把噪声当成了血管通常可以从三个方向找原因训练标签是否太粗糙、输入图像的CLAHE强度是否过高噪声被放大、模型是否在训练集小血管上过拟合了。相反如果看到连续血管有很多断口说明模型对细血管的召回不够考虑调低阈值、增加数据增强中旋转的角度范围或增大输入分辨率。5.2 踩坑记录显存不足、路径含中文、FOV掩码未对齐等五个常见问题坑一训练时报“CUDA out of memory”但代码看起来没毛病。现象是训练到第一个epoch中途直接抛OOM试过把batch size从8降到4也没用。原因通常是PyTorch在训练循环里累积了计算图比如在循环体里不小心把loss.item()写成loss追加到列表或者把pred和mask都放进了dice的计算但没有用with torch.no_grad()包住验证部分导致梯度图始终存在于内存里。解决方法是把每一轮loss.item()存列表验证部分用torch.no_grad()包起来同时用torch.cuda.empty_cache()在epoch之间清理缓存。另外输入图片如果是整张565×584直接进模型显存占用比256×256的patch高好几倍优先裁剪输入。坑二模型训练完验证Dice很高但测试集效果差很多。现象是val dice到了0.8测试环境下dice只有0.4。最常见的原因是验证集和训练集来自同一批patch或同一张图的不同区域数据分布高度重叠另一个隐藏原因是验证时用的预处理与训练不一致比如训练时做了CLAHE但验证时忘了做或者mean/std归一化的参数用的是ImageNet的默认值而不是数据集自算的。解决方法是把测试集的预处理写成独立的函数和训练流程共用同一份代码避免手写两遍导致参数不一致验证时严格从训练集之外取样。坑三Windows下数据路径包含中文报错找不到文件。现象是Path.glob匹配不到任何文件或者读取图像时imread返回None。原因很简单OpenCV的imread不支持中文路径这是老问题了。解决方法是项目目录和数据集路径全部用英文.zip解压时别解压到“桌面/我的文档”这类带中文的路径下。如果非要用中文路径先os.chdir切换工作目录再处理或者改用img Image.open(path)的方式PIL对中文路径兼容性比OpenCV好。坑四FOV掩码和图像尺寸不匹配。现象是运行时torch.cat报维度错误或者叠加可视化时图像和掩码对不齐。原因通常是mask目录下的文件尺寸和images目录下的图像尺寸不一致尤其是从某些渠道下载的数据集被重新压缩过。解决方法是在预处理脚本里加一行校验assert img.shape mask.shape fov.shape不满足就直接报错并打印三者的shape。宁可多跑一次校验也不要在训练到一半时才发现数据对不上。坑五标签里血管太细loss降不下去Dice上不去。现象是训练10个epoch后Dice卡在0.5左右不动预测结果全是粗大的血管块细血管完全没了。原因是血管标注是逐像素的手工标注细血管占的比例极小BCE和Dice的组合损失在面对细结构时偏向粗血管的召回。解决方法是把数据增强里加入弹性变形elastic deformation让细血管有更多样化的形态或者对损失函数做修正给细血管像素更高的权重比如用Focal Loss替代普通BCE让模型更关注难分的像素。另一个有效做法是修改损失的权重loss 0.3 * bce 0.7 * dice让Dice项主导训练方向。6. 验证模型有没有真的学会分割血管ROC曲线与三个进阶检查训练结束后不能只看Dice系数就收工。Dice对血管粗细分布敏感可能出现中等程度上可以接受的Dice值但细血管分割质量很差的情况。项目里要提供最终验证能力一定要计算ROC曲线和AUC同时看预测概率图在不同阈值下的表现。ROC曲线的横轴是假阳性率FPR纵轴是真阳性率TPR曲线下的面积AUC衡量模型对前景和背景的区分能力。DRIVE测试集因为提供了第二组人工标注manual2可以同时给出与第一组标注的AUC——这叫“人类观察者一致性”用来作为性能上限参考。计算AUC可以用sklearn.metrics.roc_auc_score但要注意输入是预测概率而不是二值化后的mask并且需要把FOV外的区域当作忽略项只统计FOV内的像素from sklearn.metrics import roc_auc_score # 把所有测试图像的像素收集起来仅保留FOV1的位置 y_true_all, y_prob_all [], [] for img, mask, fov in test_loader: with torch.no_grad(): prob torch.sigmoid(model(img)).cpu().numpy() y_true_all.append(mask[fov 0].numpy()) y_prob_all.append(prob[fov 0].reshape(-1)) y_true np.concatenate(y_true_all) y_prob np.concatenate(y_prob_all) auc roc_auc_score(y_true, y_prob)AUC大于0.95算合格DRIVE上UNet的正常水平在0.96-0.98之间。如果AUC低于0.92问题大概率出在训练阶段而非测试代码回头检查预处理和损失函数。进阶检查里我一般还会做两个额外验证。第一是血管骨骼化skeletonization用skimage.morphology.skeletonize对预测的血管掩码做细化统计骨骼断点数量——断点越多说明分割出的血管连续性越差Dice可能看起来还行但实际无法用于下游的血管形态分析。第二是分叉点计数视网膜血管的分叉特征对糖尿病视网膜病变诊断有参考价值统计预测结果和真值的分叉点数量是否在同一量级。这是我做这类医学图像分割项目时反复使用的收尾检查流程每次跑完都会做这三件事看单张预测叠加图、算AUC、做骨架连续性分析。这么做下来模型能不能用、能用在哪心里就有数了。跑UNet和PyTorch组合的视网膜血管分割项目很多时候不是模型不够好而是数据预处理和验证细节决定了上限。如果这套流程能帮你少走点弯路那就最好不过了希望帮到你。本文还有配套的精品资源点击获取
