最近开始着手一些医学图像分割的项目和比赛,但是这方面的内容比较稀缺。目前来讲医学图像的处理主要面临以下几个方面的问题:
- 图像太大,病理图片有些可以达到10w*10w
- 标注不准确,需要很有经验的医生标注,并多个医生反复检查。通常都会面临标注问题
简介
为了快速进入这一领域,我找了SIIM-ACR肺部图像分割kaggle大赛来学习。该比赛由医学影像信息学学会(SIIM)2019年主办,共1475支队伍参加比赛。任务是于从一组胸部X光片中对气胸进行分类(并,将其分割)。气胸可能是由于钝性胸部损伤,潜在的肺部疾病损害或最令人恐惧的原因引起的,根本没有明显的原因。在某些情况下,肺萎陷可能危及生命。评价指标:Dice coefficient。并且获胜者参加C-MIMI 2019。
数据集介绍
数据由DICOM格式的图像以及图像ID和行程编码(RLE)掩码形式的注释组成。一些图像包含气胸(塌陷的肺),数据分为两个阶段发放。总共包括10679份dicom文件(站立位胸片)。有气胸:无气胸 = 2379:8300。 有气胸的胸片皆有mask (run-length-encoded (RLE))格式 。一个胸片如果有多处气胸,会有多个单独的mask。
解决方案介绍
第一名方案
- 特色:triple rule
传统分割任务在后处理时是基于doublet rule(top_score_threshold,min_contour_area)进行,即大于threshold的阈值,同时最小区域面积像素大于min_area才能被标记为正样本。Aimoldin Anuar使用(top_score_threshold, min_contour_area, bottom_score_threshold)方案,首先利用top_score_threshold二值化,再把contour_area
classification_mask = predicted > top_score_threshold
mask = predicted.copy()
mask[classification_mask.sum(axis=(1,2,3)) < min_contour_area, :,:,:] = np.zeros_like(predicted[0])
mask = mask > bot_score_threshold
return mask
Best triplet on validation: (0.75, 2000, 0.3)
Best triplet on Public Leaderboard: (0.7, 600, 0.3)
- 数据增强
albu.Compose([
albu.HorizontalFlip(),
albu.OneOf([
albu.RandomContrast(),
albu.RandomGamma(),
albu.RandomBrightness(),
], p=0.3),
albu.OneOf([
albu.ElasticTransform(alpha=120, sigma=120 * 0.05, alpha_affine=120 * 0.03),
albu.GridDistortion(),
albu.OpticalDistortion(distort_limit=2, shift_limit=0.5),
], p=0.3),
albu.ShiftScaleRotate(),
albu.Resize(img_size,img_size,always_apply=True),
])
没有使用h通道增强
- 训练
loss:
使用 [combo loss] 即结合BCE, dice and focal loss。(3,1,4) for albunet_valid and seunet; (1,1,1) for albunet_public; (2,1,2) for resnet50.
作者开始只使用1-1-1计划进行训练我获得了最好的public 成绩。但注意到在开始时loss损失比其他损失高大约10倍。为了平衡它们,改用使用3-1-4方案,它为我带来了最佳的验证分数。作为一种折衷,resnet50使用2-1-2方案。
采样:
在输入模型的时候在torch dataset上采样。分为了四个阶段,
第0部分-从具有大学习率(大约1e-3或1e-4),大采样率(0.8)和ReduceLROnPlateau的预训练模型中训练10-12个epoch。可以在imagenet或较低分辨率(512x512)的数据集上对模型进行预训练。这部分的目标是:快速获得足够好的模型,其验证分数约为0.835。
第1部分-使用正常学习率(〜1e-5),大采样率(0.6)和CosineAnnealingLR或CosineAnnealingWarm重启上一步中的最佳模型,重复直到达到最佳收敛。
第2部分-使用正常学习率(〜1e-5),小采样率(0.4)和CosineAnnealingLR或CosineAnnealingWarm重新启动调度程序,从上一步中升级最佳模型。重复直到达到最佳收敛。
所有实验(resnet50除外)在512x512分辨率下使用早期编码器上的冻结编码器进行训练,其尺寸为1024x1024。 - 每个pipeline 的每折的平均Top3模型融合
- Horizontal flip TTA
第二名方案
- 特色之处:分为两个任务
- 分类:这部分用于分类图像是否与气胸有关。模型是一个基于unet的多任务模型,带有一个用于分类的分支。
数据:所有数据
Cls损失:BCE +Focal损失
Seg损失:BCE
增强:hflip, scale, rotate, bright, blur
主干:seresnext 50,seresnext101,efficiencynet-b3
集成:stacking - 分割:
有两种用于分割的模型: unet和deeplabv3。
数据:带有气胸的数据
loss:dice loss
增强:与分类相同
主干:seresnext50,seresnext101,efficiencynet-b3,efficiencynet-b5
集成:平均
分类结果:seresnext50>serenext101>efficientnet-b3.
分割结果:seresnext101>efficientnet-b5>seresnext50>efficientnet-b3.
- 增强
RESIZE_SIZE = 1024 # or 768
train_transform = albumentations.Compose([
albumentations.Resize(RESIZE_SIZE, RESIZE_SIZE),
albumentations.OneOf([
albumentations.RandomGamma(gamma_limit=(60, 120), p=0.9),
albumentations.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.9),
albumentations.CLAHE(clip_limit=4.0, tile_grid_size=(4, 4), p=0.9),
]),
albumentations.OneOf([
albumentations.Blur(blur_limit=4, p=1),
albumentations.MotionBlur(blur_limit=4, p=1),
albumentations.MedianBlur(blur_limit=4, p=1)
], p=0.5),
albumentations.HorizontalFlip(p=0.5),
albumentations.ShiftScaleRotate(shift_limit=0.2, scale_limit=0.2, rotate_limit=20,
interpolation=cv2.INTER_LINEAR, border_mode=cv2.BORDER_CONSTANT, p=1),
albumentations.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225), max_pixel_value=255.0, p=1.0)
])
第三名方案
-
特色:分为两个阶段,先分割肺部,在crop之后分割气胸部位
由于图像尺寸很小,为了节省内存和计算量,首先训练了一个UNET模型,以从1024x1024图像中预测出肺部,所有模型均基于裁剪后的肺部。而576x576裁剪后的图像对于模型来说已经足够好了。
无阈值搜索,仅使用0.5
Optimizer:Adam具有0.0001的学习率,训练期间无学习率变化。
时期:6个时期后模型参数的EMA为15
第四名方案
型号:UNet。
backbone:具有冻结批归一化功能的ResNet34骨干。
预处理:随机裁剪(512, 512)大小,到(768, 768)大小
增强:ShiftScaleRotate,RandomBrightnessContrast,ElasticTransform,HorizontalFlip
优化器:Adam,batch_size = 8
Scheduler:CosineAnnealingLR
附加功能:非空样本的比例线性地从0.8减少到0.22(如在数据集中一样),取决于时期。它有助于更快地收敛。
损失:2.7 * BCE(pred_mask, gt_mask) + 0.9 * DICE(pred_mask, gt_mask) + 0.1 * BCE(pred_empty, gt_empty)。这pred_mask是UNet的预测,pred_empty是空掩码分类的分支的预测。
后处理:if pred_empty > 0.4 or area(pred_mask) < 800: pred_mask = empty。在验证集上选择参数。
融合:将8折的4个最佳检查点平均,水平翻转TTA。
硬件:4 x RTX 2080
第五名方案
与第三名类似,利用外部数据集,先分类再分割
Network : Unet with Aspp
Backbone : se50 & se101
Image size: (1024, 1024)
Optimizer : Adam
Loss : 1024 * BCE(results, masks) + BCE(cls, cls_target)
Semi-supervision: mean-teacher[1-2] with NIH Dataset (0.874 ---> 0.880)
Scores:
stage1 0.8821
stage2 0.8643
第六名方案
基于EncodingNet(ResNets,512和1024大小)和UNet(EfficientNet4,se-resnext50,具有512、640和1024大小的SENet154)的最终解决方案。
最佳增强与crops and rotations有关。没有使用对比度和亮度转换。
损失:BCE + Dice(尝试过Focal,没有用)
较低的图像尺寸会降低很多分数,因此全尺寸模型应该更好。
技巧:
带有大量TTA的EncondingNet上的分类(11种方法)
删除小区域Segmentation
也注意到水平翻转增强降低了局部CV,但是没有在LB上检查它。
验证使用了4折交叉。对于每个样本,都进行了4混合模型。
- 增强
AUG = Compose(
[
HorizontalFlip(p=0.5),
OneOf(
[
ElasticTransform(
alpha=300,
sigma=300 * 0.05,
alpha_affine=300 * 0.03
),
GridDistortion(),
OpticalDistortion(distort_limit=2, shift_limit=0.5),
],
p=0.3
),
RandomSizedCrop(min_max_height=(900, 1024), height=1024, width=1024, p=0.5),
ShiftScaleRotate(rotate_limit=20, p=0.5)
],
p=1
)
Model1:在Pascal数据集上预训练了Resnet50的EncNet。 首先使用AUG对512 x 512分辨率的4折合模型进行了训练。 其次使用AUG训练了1024 x 1024分辨率的4折合模型。 4折。
损失:DICE
增强:AUG
TTA:翻转,Clahe和np.arange(-20, 21, 5)角度旋转(总计10 TTA)
模型2-3:以 1024 x 1024分辨率训练了两个具有SEResnext50和SEResnet152主干的类似Unet的模型。每个模型也为4折。
损失:骰子
增强:AUG
TTA:翻转
Model4:具有EfficientNetB4主干的类似Unet的模型,经过640 x 640分辨率训练。 也做4折混合。
损失:Dice+ BCE
增强:AUG
TTA:无TTA
最终模型:
模型1,2,3和4平均
分类阈值:〜0.32
细分阈值:〜0.375
第七名方案
解决方案非常简单,它是不同模型(每个都有几折)的整体简单平均值:
- FPNetResNet50(5折)
- FPNetResNet101(5折)
- FPNetResNet101(7倍,种子不同)
- PANetDilatedResNet34(4折)
- PANetResNet50(4折)
- EMANetResNet101(2折)
使用AdamW优化器以768x768(或更接近)训练的模型 。对于FPN模型,使用Flip TTA,其余使用比例尺(1024)TTA。我们使用了两个阈值,一个用于细分,另一个(较高值)用于分类。
对于EMANets,使用PANet的GAU模块将8x的陡峭双线性上采样更改为2x逐渐上采样。它大大提高了性能。
为了进行汇总,使用了模型的软预测的简单平均值(S型之后,阈值之前)。最后使用两个值进行阈值化。较小的值(0.2)用于分割,另一个(0.72)用于非气胸图像的分类。
没用:HRNet
FishNet as encoder (not bad but no improvements either)
A second-level classifier based on gradient boosting using segnets' outputs and metadata as features
第七名方案
- 使用单一模型deeplabV3+
数据分割:整体保持率10%,剩余90%保持率10倍,按气胸大小分层
架构:DeepLabV3 +
骨干网:具有组标准化功能的ResNet50 / 101和ResNeXt50 / 101
损失:加权BCE(在所有图像上训练)或Dice(仅在正片上训练)
优化程序:Vanilla SGD,动量0.9
培训:批量大小为4,1024 x 1024; 批量大小为1,1280 x 1280仅分割(未在阶段2重新训练)
方法:余弦退火,100个epoch,5个snapshots,初始LR 0.01至0.0001
合计:
-总共12个模型( 每个模型x3个snapshots)
-仅在正样本上使用softloss,训练了4个模型
-在加权BCE的所有图像上训练了8个模型
-在所有图像上训练了4个模型作为“分类器”
-最大像素值为用作分类得分,在4个模型中平均
-将仅对正值训练的4个模型的像素级得分乘以该分类得分,然后取平均值-最终合奏:将上述平均得分乘以基于其他4/8模型的像素级得分在所有图像上进行训练
-Hflip TTA
后期处理:
-删除区域总大小小于2048、4096像素的图像 - 总结:
与Felipe的整合模型:他使用Unet和EfficientNetB4内核,使用512 x 512的softDice训练的所有图像达到0.8750。无法正确地整合我们的模型,最终放弃了它,转而支持稍微好一点的整合。
亲自尝试了较低的分辨率和Unet,LinkNet,PSPNet,EncNet,HRNet等。所有这些都比DeepLab差,分辨率为1024 x 1024
全图像分类器:效果不如分割
SGD比Adabound优化器Adam更好
Hflip TTA +去除小区域起到效果
加权BCE损失最稳定,Lovasz,softloss不稳定或根本没有收敛
validation:0.8737
阶段1public:0.8780(删除4096像素)
阶段2私有:0.8627(删除2048像素)
第八名方案
常见:
数据拆分:CV5
优化器:Adam
Scheduler:Reduce lr on plateau
增强:relatively aggressive: ShiftScaleRotate, Grid- and Elastic-transformation, GaussianNoise,GaussianNoise
分类:
型号:se_resnext101(2个快照),senet154
分辨率:768x768
Loss:BCE
附加功能:TTAx2(水平),伪标记,梯度累加(bs = 100-200)
这里的关键是模型的选择。
分割:
型号:Unets:dpn98,se_resnet101,se_densenet121(每个快照2张)
训练过程包括四个阶段:
- 损失:BCE + dice;尺寸:512x512
- 损失:BCE + dice;尺寸:1024x1024
- 损失:BCE + dice;尺寸:1024x1024
- 损失:symmetric lovasz;尺寸:1024x1024
附加:TTAx6(水平+重新缩放),梯度累加(bs = 50-100)
什么没有奏效:
- 与其他编码器一起使用的Unets的效果更差(resnet34,resnet50,dpn107,dpn131,se_resnext50_32x4d,se_resnext101_32x4d,senet154)。senet154在尺寸为512x512的情况下比其他型号要好得多,但在扩展到1024x1024时完全失败。
- Lookahead optimizer优化器未改善优化过程
- 分类的其他损失:FocalLoss,SoftF1Loss。FocalLoss的准确性优于BCE,但f0.5度量则较差。
第九名方案
- 两个步骤:
分类:使用Unet模型。并通过像素数阈值确定图片中是否存在气胸。根据验证,最佳阈值为2000。
骨干:seresnext50
数据:在此步骤中,使用了所有图像和平衡的批次(气胸/非气胸),这大大加快了收敛速度。
数据拆分:5倍和10倍按气胸区域分层。
输入大小:768x768
损失:BCE
增强: hflips, rotations(up to 10 degree), random brightness, contrast and gamma, blur
Lr调度:reduce lr on plateau with patience=3 epochs.
这种方法给出了很好的结果,但是仍然存在大量的假否定示例。因此决定使用一开始就训练的分类模型,如果所有模型都将其归因于肺气胸,则将其标记为包含气胸。
分割:
这个阶段的核心要素也是Unet模型,但仅在分类阶段被确定为包含气胸的图片中进行模型预测。
骨干:seresnext50
数据:仅包含气胸的图像
数据分割:按气胸区域分层的5折和10折
输入大小:928x928、768x768
损失:BCE +DICE
增强:与分类阶段相同
Lr调度: reduce lr on plateau with patience=5 epochs.
第十名方案UNet with mask scoring head
预测分割:img大小896
预测分类:img尺寸768
整合预测:删除低概率mask
在此术语中,MS-EUNet的意思是“用EfifcientNet B4 UNet”。
TTA采用水平翻转
class BCEDiceLoss(nn.Module):
"""
Loss defined as alpha * BCELoss - (1 - alpha) * DiceLoss
"""
def __init__(self, alpha=0.5):
super(BCEDiceLoss, self).__init__()
self.bce_loss = nn.BCEWithLogitsLoss()
self.dice_loss = DiceLoss()
self.alpha = alpha
def forward(self, logits, targets):
bce_loss = self.bce_loss(logits, targets)
dice_loss = self.dice_loss(logits, targets)
loss = self.alpha * bce_loss + (1. - self.alpha) * dice_loss
return loss
class DiceScoreL2Loss(nn.Module):
"""
Loss for mask scoring UNet
Caution: This loss for 1 class !!!!!
"""
def __init__(self):
super(DiceScoreL2Loss, self).__init__()
self.smooth = 1e-8
self.sum_dim = (2, 3)
def forward(self, dice_logits, logits, masks_gt):
dice_preds = torch.sigmoid(dice_logits)
# BATCH x 1 x H x W
masks_preds = torch.sigmoid(logits)
# BATCH x 1
intersect = (masks_preds * masks_gt).sum(self.sum_dim).float()
union = (masks_preds.sum(self.sum_dim) + masks_gt.sum(self.sum_dim))
# Calc dice(BATCH x 1)
dice_gt = (2. * intersect + self.smooth) / (union + self.smooth)
# Calc L2 Loss
cond = torch.abs(dice_gt - dice_preds)
loss = 0.5 * cond ** 2
return loss.mean()
loss_func1 = BCEDiceLoss(alpha=0.7)
loss_func2 = DiceScoreL2Loss()
loss = loss_func1(logits, masks) + 0.2 * loss_func2(logits_score, logits, masks)