手头同时接过两个活儿一个要把脑部CT里的大脑白质分割出来另一个要从视网膜眼底照片里把血管网络一根根提取出来。当时看着这两张完全不同的图像我还以为是两个独立的项目做到一半才反应过来它们背后的方法论、网络结构、训练技巧几乎完全共用。图像分割这个领域听起来很泛但一旦落到具体医学场景真正吃功夫的地方就全暴露出来了哪里边界模糊、哪里样本太少、哪里标注不一致。这篇就把我处理这两个任务的完整思路、参数细节和踩过的坑都翻出来给同样在做医学图像分割的朋友做个参考。1. 从影像科的真实需求说起分割到底在分什么图像分割说白了就是给图像里的每个像素打标签。自然图像里我们通常分猫狗背景而医学图像里分的是组织、器官、病灶。脑部CT白质分割要回答哪个像素属于大脑白质视网膜血管提取要回答哪个像素属于血管本质上都是像素级分类问题跟目标检测画个框完全是两码事。这两个任务之所以特别有代表性是因为它们正好覆盖了图像分割里的两类典型结构。白质是一个区域型目标面积大、边界相对平滑但和灰质、脑脊液之间的对比度低血管是线状结构纤细、弯曲、有分叉占整张图像面积的比例很小但拓扑结构极其重要。一个考验你对区域边界的把握一个考验你对细线连接性的保持两个都做一遍图像分割里的大部分问题就都见过了。临床角度也很好理解。脑部CT里白质和灰质的分界对脑萎缩评估、多发性硬化病灶定位、脑区定量分析都有直接作用视网膜血管的形态变化可以辅助筛查糖尿病视网膜病变、高血压性视网膜病变、青光眼这类疾病。医生在显示器上手动勾画一张CT片子里的白质轮廓快也要五到十分钟眼底血管标注更是费时费力。自动分割的价值不是取代医生而是把重复性劳动省下来让医生把时间花在诊断本身。但医学图像分割的难度也恰恰来自它的特殊性。自然图像里物体颜色鲜艳、边界清晰而CT图像是灰度图不同组织间的灰度差可能只有十几个HU值眼底图像里血管和背景的对比度更低还叠加了光照不均、视盘、黄斑等复杂结构。加上医学数据量小、标注成本高很多通用视觉方案搬过来直接就不能用。所以真正做医学分割项目不是会调一个深度学习框架就行而是要理解数据本身长什么样以及哪一步会出问题。2. 脑部CT白质分割从DICOM切片到白质掩膜2.1 先看懂CT值白质、灰质、脑脊液在图像里如何区分做CT分割的第一步不是急着搭网络而是先学会看DICOM文件里的像素值。CT图像里每个像素的数值并不是随便的灰度而是对应组织的X射线衰减系数通常用亨氏单位表示。水的CT值是0空气约-1000致密骨在300以上。脑组织内部脑脊液大约在0到10白质大约在20到30灰质大约在35到45。也就是说白质和灰质之间的差距只有十几个HU这比自然图像里一只猫和背景的差异小太多了。所以读CT切片时不能直接拿原始像素喂给网络要先做窗宽窗位调整。窗位决定了显示的中心灰度窗宽决定了显示的灰度范围。脑组织观察常用的窗位是40HU左右窗宽80HU左右。做了这个操作之后原本十几HU的差异被拉伸到整个可见灰度范围白质和灰质的边界才看得见。这也是我第一次做这个项目时忽略的地方直接读DICOM原始存储值丢进网络模型损失一直降不下去后来才发现数值范围根本没有归一化到合理区间组织之间的差异在数值上实在太微弱了。正确做法是把应用窗宽窗位后的图像再归一化到0到1最好再做一次z-score标准化让数据分布稳定下来。2.2 传统分割法的边界在哪里在深度学习普及之前白质分割有人用阈值法、区域生长、活动轮廓模型。阈值法最直接既然白质CT值约20-30那就设一个阈值把这段灰度拉出来。问题在于实际CT图像里噪声大同一组织在不同扫描条件下灰度值不完全一致加上部分容积效应的影响脑沟附近的体素可能同时混合了脑脊液和白质灰度值落在重叠区间阈值一设结果就是这儿缺一块、那儿多一块。区域生长稍微聪明些从种子点出发把灰度相似且四邻域连通的像素吸收进来。但种子点要人工选而且如果图像里存在灰度缓慢变化生长的区域会从白质漏到灰质去边界没法绷住。活动轮廓模型也就是蛇模型倒是能通过能量函数把轮廓推送到真实边界附近但初始化要求高遇到白质这种灰度边界本身就不锐利的结构曲线经常漂移。不是说传统方法没用而是这些方法都建立在目标和背景在灰度上可分这个假设上白质和灰质灰度这么接近假设本身就是脆弱的。后来我换成深度学习最大的感受是网络学到的不只是灰度特征还有空间上下文——周围是什么组织、位置在哪儿这些信息传统方法很难显式建模。2.3 基于UNet的白质分割流程我的白质分割管线大致是这么走的你可以直接参考。先做数据准备。通过DICOM解析库把CT切片读出来保留像素间距等元信息然后用窗宽窗位做显示变换再做一次全图归一化。骨和空气对白质分割是干扰我一般会先做一个粗略的脑组织掩膜用阈值加形态学开运算剔除颅骨等背景只保留颅内区域作为后续网络的输入。这一步能显著减少计算量也避免网络在无关区域浪费表达力。网络主体就是标准UNet。输入是单通道灰度图patch尺寸我用的是256乘256编码器下采样4次每次用两层卷积加ReLU卷积核统一3乘3步长1padding保持尺寸不变下采样用2乘2最大池化。编码器通道数从32开始逐层翻倍到256。解码器对称上采样每次先做2倍上采样然后与对应编码层的特征图做跳跃连接拼接再做两次卷积。输出层用1乘1卷积把通道数压到1接Sigmoid得到每个像素属于白质的概率。这类网络结构简单、参数适中医学小数据集上不容易过拟合而且跳跃连接把浅层的边缘细节直接传给解码器对保持白质边界完整性帮助非常大。训练细节上我用Adam优化器初始学习率1e-4batch size设为8损失是Dice损失和二元交叉熵的加权和权重各0.5。Dice损失直接优化分割结果和真实掩膜的重叠度对处理白质这种目标区域占比不高的场景有奇效交叉熵则保证每个像素的梯度信号不饱和。训练大概跑了150个epoch每20个epoch在验证集上看一次Dice连续10个epoch不提升就保存当前模型并早停。推理时我做了简单的滑动窗口预测因为输入patch比原图小重叠区域会得到多个预测值取平均当作最终概率。然后按0.5阈值二值化。后处理我加了一步取最大连通域把零散的伪阳性区域去掉。几个病例实测下来白质分割的Dice能到0.88以上边界平滑度比纯阈值法好非常多细小脑沟周围偶尔还是会有锯齿但作为临床定量分析的输入已经可以接受。3. 视网膜血管提取把河网从眼底照片里摘出来3.1 眼底图像里血管分割的难点视网膜血管提取和白质分割看图像都知道不一样眼底照片是彩色的血管在红色通道和背景有一定对比但整体对比度非常低而且血管细、弯曲、纵横交错。一个典型的眼底图像里血管宽度从主干道的十几像素到末梢的一两个像素都有这些细分支恰恰是判断病变的重要依据丢不得。光照不均匀是这类图像最大的干扰源。拍照时眼球曲面的反光、瞳孔区域比周边亮导致图像中心亮、四周暗同一根血管在图像不同位置的灰度表现完全不同。此外视盘区域本身就是一个高亮圆盘血管从视盘中心进出背景复杂黄斑区则颜色偏暗、结构特殊。要是把整张图用同一套阈值处理血管在亮区和暗区的响应差异能被拉得很开。还有一个常被忽略的问题眼底图像里的病理变化。糖尿病视网膜病变患者可能有渗出、微动脉瘤这些病灶在形态上和血管末梢很像网络很容易产生混淆。标注这类数据所需的专业程度也比白质分割高得多我请过有一定临床背景的人帮忙核对血管标注才发现初版的标签里不少伪影被当成了正经血管。3.2 传统血管增强思路匹配滤波与Hessian做白质分割时传统方法很快败下阵来但血管分割里传统方法倒是有过一段精彩历史理解它们有助于把握深度网络该做什么。血管在局部看是一条暗线横截面的灰度剖面近似高斯曲线匹配滤波法的思路就是设计一个高斯核模板让模板和血管局部图案相匹配把响应高的地方认为是血管。实际使用时会用多个方向的高斯核旋转版本分别卷积取每个像素的最大响应。这个方法对主干血管很有效对细小分支就无能为力了因为高斯核尺度固定小血管和背景的对比度又太低。Hessian矩阵方法是另一种思路。每个像素的局部二阶导数构成一个2乘2矩阵其特征值反映了局部结构的曲率变化。血管内像素的Hessian特征值有明显规律沿血管方向曲率小垂直方向曲率大。Frangi滤波就是基于这个规律设计的一个响应函数输出一个血管相似度分数。它的问题在于对噪声敏感而且同样存在尺度选择问题不同粗细的血管需要不同尺度的高斯平滑。我在项目里用Frangi滤波做过预分割再配合形态学操作在质量较好的公开眼底图像上灵敏度凑合但特异性不高背景纹理被误判成小血管的情况很多。最终方向还是转向了深度学习不过Frangi响应图被我保留下来作为额外输入通道喂给网络对提升细血管分割稳定性有一点帮助。3.3 深度网络在血管分割上的实践血管分割用的网络也是UNet架构但我做了一些针对性改动。第一是输入尺寸血管是长条形结构patch太小会切断血管主干模型看不到上下文我用的是384乘384的patch配合垂直翻转、水平翻转、随机旋转和弹性变形做数据增强让网络对血管的形态变化更鲁棒。第二是输入通道我把原图提取出的绿色通道和Frangi响应图叠在一起绿色通道在眼底图像里血管对比度最高比直接用三通道彩色图效果更稳定。训练方式上和白质分割类似但损失函数我改成了Dice损失加Focal损失。血管像素占比通常只有百分之几前景背景严重失衡普通交叉熵会被大面积背景主导模型容易学成全部预测为背景。Focal损失会降低易分样本的权重把训练焦点逼向那些难以区分的前景像素对找回细血管末梢很有帮助。推理后处理和白质分割不太一样。血管分割不能简单取最大连通域因为血管网络本身就是多个连通分量强行保留最大连通域反而会丢掉分支。我只做了一次小尺度的形态学开运算用来去掉孤立的单个像素噪点然后基于骨架分析把短小的断裂段做一次连接修补。这套流程在我手里的测试集上血管分割的灵敏度能到0.85左右特异性在0.96以上。4. 两个分割项目共用的训练细节数据、损失和评估4.1 预处理与数据增强的取舍两个项目放在一起看共通性立刻显现。预处理的第一步都是归一化但方式有细微差别。CT图像得益于HU值的物理意义可以做固定窗宽窗位后的统一变换眼底图像没有这个标准我一般用z-score归一化先算全图均值和标准差再减去均值除以标准差。需要提醒的是归一化的统计量应该从训练集算好存下来推理时直接用同一个值不能在每张测试图上重新算否则同一张图像在不同环境下的输入分布会漂移。数据增强方面我需要强调一点不是所有Augmentation都适合医学分割。旋转、翻转、轻微缩放基本安全但过强的亮度对比度扰动在眼底图像上可能会改变血管与背景的真实对比关系让模型学到错误的亮度映射。我在血管项目上就犯了过度调亮度的错训练集表现不错一到验证集就崩了。后来把亮度扰动幅度降下来改为以弹性变形为主因为血管形态的弯曲变化才是真正需要模拟的变体。而脑部CT图像由于是断层扫描空间位置有明确的解剖意义旋转范围我会限制在10度以内避免过度扭曲组织结构。4.2 损失函数如何平衡目标和背景医学图像分割里最经典的问题就是前景背景极度不平衡。白质在脑部CT里占比不算小但血管在眼底图像里真的只有一小撮像素。我开始时两个项目都用朴素交叉熵白质项目勉强能跑血管项目直接全军覆没——模型输出全零验证集Dice为0。换成Dice损失后平衡问题立刻缓解因为它按区域重叠度计算损失天然不受前景占比影响。不过Dice损失也有自身的缺点。在小目标区域Dice损失的梯度变化非常剧烈网络容易进入不稳定的训练状态而且它不考虑像素的难易程度。所以我的经验是混合损失才是最稳的。白质项目用Dice加交叉熵各一半血管项目用Dice加Focal损失Focal的gamma我设为2alpha设为0.25。实际效果表明血管项目把Focal加上之后末梢细血管的灵敏度提升了大约5个百分点。调试损失函数时建议先跑一个小实验集看训练早期损失曲线是否正常下降Dice损失如果出现大幅震荡通常是学习率太高或样本中存在异常标签。4.3 评估指标怎么选才不骗自己两个项目我都用Dice系数和IoU做主要排名指标但单看这两个指标会掩盖很多问题。Dice对区域型目标友好但如果是血管这种细长结构一小段血管断掉Dice数值下降并不多视觉上却是硬伤。所以我在血管项目上额外统计了灵敏度与特异性还做了一个拆分测试把血管按粗细分成主干和末梢两组分别计算灵敏度。这个拆解很有价值因为模型可能主干分割得漂亮但末梢全部丢光汇总Dice看着还行临床应用却完全不合格。给个简单的表格你可以对照看看场景主要指标辅助指标关注点脑部CT白质分割Dice、IoU边界距离区域完整性与边界平滑视网膜血管提取Dice、灵敏度、特异性分尺度的灵敏度细分支保留与拓扑连通评估时我还会看一眼预测掩膜的拓扑结构比如血管分割里血管网络是不是真的连通、有没有大面积断裂。这不需要专门的算法把预测结果和原图叠加肉眼扫一遍就够但这个步骤千万不要省数值指标再漂亮图上一塌糊涂的例子我在项目里见过不止一次。5. 我踩过的分割坑与调试经验5.1 标注不一致导致边界偏移第一个项目也就是白质分割初期模型在验证集上的Dice到了0.85左右就不再涨了看预测图发现一个问题很多切片上模型输出的白质边界位置和自己的标注对不上但和另一个标注者的标注却是吻合的。原来数据来自多个标注者不同人对白质和灰质的边界到底在哪把握的尺度不完全一样模型学到的相当于一个平均标注反而和谁都不完全一致。这个问题没有特别好的捷径。我后来把训练集中每个病例的标注者信息统计出来挑出和主流标注风格差异大的样本请一位有经验的人重新核了一遍边界同时在后处理时对预测概率图做一次小尺度的中值滤波把边界上的锯齿抹平一点。最终Dice升到了0.88左右更重要的是边界视觉上稳定了不少。5.2 跨设备泛化能力不足第二个比较深刻的坑发生在血管提取上。我在A设备拍摄的眼底图像上训练得很好换到B设备拍摄的数据上测试Dice掉了将近10个百分点。原因是不同眼底相机的光照条件、白平衡、色彩空间都不一样数据分布发生了偏移。这种事在真实项目里经常遇到一个模型不可能只在单台设备上工作。应对分成了两步。第一是预处理统一化我在数据进入网络前先做一次对比度受限的自适应直方图均衡化再归一化到统一分布这能在很大程度上抹平不同相机的光照差异。第二是训练时混入多种来源的数据哪怕每种数据量不大也能让模型看到更多样的分布。我实际验证了一下加了这两种处理之后跨设备的Dice下降幅度控制在了3个百分点以内。5.3 小目标丢失与显存限制血管末梢和小分支的丢失是我最后想重点说的事。血管分割里末梢血管对判断病变很重要但它们的信号非常弱容易在编码器的下采样过程中被稀释掉。我试过两种办法一种是在损失函数里加入边界损失项让网络预测的边界更精细另一种是使用多尺度输入把原图和下采样到一半尺寸的图像都送进网络让解码器能从不同尺度把细结构的信息拼回来。两种结合之后末梢血管的灵敏度提高了大约4个百分点。显存不足是另一个实际限制。384乘384的patch加batch size 8单卡显存压力很大。我用了混合精度训练把梯度累积的步数也调了适当倍数显存占用降下来一些。如果你的图像尺寸更大也可以考虑把训练patch拆得更小、batch size设得更低然后用更大的patch做推理拼接效果通常也可以接受。这两个项目做下来我最深的体会是图像分割本身并不难入门难的是理解数据、控制训练细节、接受模型不完美并针对场景去修修补补。不管是脑部CT白质这种区域型结构还是视网膜血管这种线状结构方法框架大体相同差异都藏在数据特殊性里。你把自己手里的图像能想到的干扰因素都列一遍再对照网络结构去排查方向基本不会跑偏。