MICCAI2021 Contest : GAMMA Task 3

视杯视盘分割

项目主要参考了第五名之前开源的方案:

  1. 主要思路是将图片目标位置裁剪出来,随后填补到需要的分辨率,而不是直接使用插值算法,减少了噪声的引入。

  2. 此外后处理的引入也有效提高了模型的表现。

  3. 在此基础上重新进行数据划分,调整模型参数,进行二次训练,就能得到更好的效果。

改动的地方在:

  • :重新分割训练集和验证集,对生成的模型进行二次训练
  • :加入垂直翻转对数据进行进一步增强
  • :初次训练时分辨率为1024 * 1024,二次训练时分辨率为512 * 512 增强模型泛化能力
  • :调整后处理函数中的填补参数

尝试过的无效改进:

  • :修改模型主干网络
  • :将提交的最好结果作为伪标签加入训练集
  • :深监督和损失函数的修改

本项目支持一键运行,一键生成提交结果

参考:

[1] https://aistudio.baidu.com/aistudio/projectdetail/2071742

0、数据解压及项目准备

# 数据集解压
! mkdir -p datasets
! unzip -oq work/Disc_Cup.zip -d datasets
# 解压测试数据
! mkdir -p tests
! unzip -oq work/test.zip -d tests

# 项目准备
! git clone --depth=1 https://gitee.com/paddlepaddle/PaddleSeg.git  # paddleseg,github太慢
! pip install -q patta  # patta
! pip install imgaug
# ! pip install -q albumentations  # 这个可以在终端通过pip3安装
import sys
sys.path.append('PaddleSeg')  # paddleseg

1、数据准备

1.1 数据信息查看

# import os
# import numpy as np
# from PIL import Image

# 查看大小
# img_size = []
# imgs_folder_path = 'datasets/Disc_Cup_Mask'
# imgs_name = os.listdir(imgs_folder_path)
# for name in imgs_name:
#     img_path = os.path.join(imgs_folder_path, name)
#     img = np.asarray(Image.open(img_path))
#     img_size.append(img.shape)
# print(set(img_size))
# 查看标签数值
# label = np.asarray(Image.open('datasets/Disc_Cup_Mask/0004.png'))
# print(set(label.flatten()))

1.2 划分数据集

# import os
# # import random
# from PIL import Image

# # 手动选择了10张特征各异的,评估比较有说服力
# val_name_list = ['0080.jpg', '0070.jpg', '0006.jpg', '0063.jpg', '0086.jpg', \
#                  '0075.jpg', '0096.jpg', '0030.jpg', '0062.jpg', '0081.jpg']

# def create_list(data_path):
#     image_path = os.path.join(data_path, 'Image')
#     label_path = os.path.join(data_path, 'Disc_Cup_Mask')
#     data_names = os.listdir(image_path)
#     # random.shuffle(data_names)  # 打乱数据
#     with open(os.path.join(data_path, 'train_list.txt'), 'w') as tf:
#         with open(os.path.join(data_path, 'val_list.txt'), 'w') as vf:
#             for idx, data_name in enumerate(data_names):
#                 img = os.path.join('Image', data_name)
#                 lab = os.path.join('Disc_Cup_Mask', data_name.replace('jpg', 'png'))
#                 # if idx % 9 == 0:  # 90%的作为训练集
#                 if data_name in val_name_list:
#                     vf.write(img + ' ' + lab + '\n')
#                 else:
#                     tf.write(img + ' ' + lab + '\n')
#     print('数据列表生成完成')

# data_path = 'datasets'
# create_list(data_path)  # 生成数据列表

1.3 构建数据集

import paddleseg.transforms as T
from paddleseg.datasets import Dataset

# 构建训练集
train_transforms = [
    T.LocalEqualHist(),  # 自适应局部直方图均衡化
    T.RandomHorizontalFlip(),  # 水平翻转
    T.RandomVerticalFlip(), # 垂直翻转
    T.RandomRotation(im_padding_value=[0, 0, 0]),  # 随机旋转
    T.Resize(target_size=( 512, 512)),  # 兼顾大小
    T.Normalize(
        [0.3883131, 0.25449154, 0.110598095], 
        [0.26274413, 0.1827712, 0.12587263]),  # 标准化
]
train_dataset = Dataset(
    transforms=train_transforms,
    dataset_root='datasets',
    num_classes=3,
    mode='train',
    train_path='datasets/train_list.txt',
    separator=' ',
)
# 构建验证集
val_transforms = [
    T.NormSize(),
    T.LocalEqualHist(),
    T.Resize(target_size=(512, 512)),
    T.Normalize(
        [0.3883131, 0.25449154, 0.110598095], 
        [0.26274413, 0.1827712, 0.12587263]),    
]
val_dataset = Dataset(
    transforms=val_transforms,
    dataset_root='datasets',
    num_classes=3,
    mode='train',  # 这里用train,不然无法对标签进行NormSize
    train_path='datasets/val_list.txt',
    separator=' ',
)
PaddleSeg/paddleseg/cvlibs/param_init.py:89: DeprecationWarning: invalid escape sequence \s
  """
PaddleSeg/paddleseg/models/losses/binary_cross_entropy_loss.py:82: DeprecationWarning: invalid escape sequence \|
  """
PaddleSeg/paddleseg/models/losses/lovasz_loss.py:50: DeprecationWarning: invalid escape sequence \i
  """
PaddleSeg/paddleseg/models/losses/lovasz_loss.py:77: DeprecationWarning: invalid escape sequence \i
  """
PaddleSeg/paddleseg/models/losses/lovasz_loss.py:120: DeprecationWarning: invalid escape sequence \i
  """

1.4 均值、方差统计

# import os
# import cv2
# import numpy as np
# from tqdm import tqdm
 
# means, stdevs = [], []
# img_list = []
# imgs_path = 'vis_aug/img'
# imgs_name = os.listdir(imgs_path)
# for name in tqdm(imgs_name):
#     img = cv2.cvtColor(cv2.imread(os.path.join(imgs_path, name)), cv2.COLOR_BGR2RGB)
#     img = img[:, :, :, np.newaxis]
#     img_list.append(img)
# imgs = np.concatenate(img_list, axis=-1)
# imgs = imgs.astype(np.float32) / 255.
# for i in range(3):
#     pixels = imgs[:, :, i, :].ravel()  # 拉成一行
#     means.append(np.mean(pixels))
#     stdevs.append(np.std(pixels))
# print(means, stdevs)
[0.3883131, 0.25449154, 0.110598095] [0.26274413, 0.1827712, 0.12587263]

2 、训练准备

import paddle
import paddle.nn as nn
from paddleseg.models import OCRNet, HRNet_W18

model = OCRNet(
    backbone=HRNet_W18(),
    backbone_indices=[0],
    num_classes=3)
params = paddle.load('output_ocrnet_hrnet18/best_model/model.pdparams')
model.set_state_dict(params)
W1223 14:30:39.615933   102 device_context.cc:404] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.1, Runtime API Version: 10.1
W1223 14:30:39.622359   102 device_context.cc:422] device: 0, cuDNN Version: 7.6.

3、模型训练

from paddleseg.core import train
from paddleseg.models.losses import CrossEntropyLoss, DiceLoss, MixedLoss

iters = 5000
batch_size = 4
base_lr = 3e-4

lr = paddle.optimizer.lr.CosineAnnealingDecay(base_lr, T_max=int(iters // 1.5))
optimizer = paddle.optimizer.Adam(
    learning_rate=lr,
    parameters=model.parameters(),
    weight_decay=paddle.regularizer.L2Decay(1e-7),
    grad_clip=paddle.nn.ClipGradByGlobalNorm(clip_norm=1.0))

losses = {}
losses['types'] = [MixedLoss([CrossEntropyLoss(), DiceLoss()], [1, 1])] * 2  # 2
losses['coef'] = [1] * 2  # 2



train(
    model=model,
    train_dataset=train_dataset,
    val_dataset=val_dataset,
    optimizer=optimizer,
    save_dir='output_ocrnet_hrnet18',
    iters=iters,
    batch_size=batch_size,
    save_interval=int(iters/10),
    log_iters=200,
    num_workers=0,
    losses=losses,
    use_vdl=True)

4、模型预测

  • 使用了水平翻转的tta
  • 需要将大小和位置统一回原数据
import paddle
from paddleseg.models import OCRNet, HRNet_W18
import paddleseg.transforms as T
from paddleseg.core import infer
import os
from tqdm import tqdm
from PIL import Image
import numpy as np
import patta as tta
from mytools import tensor2result, restore

def nn_infer(model, imgs_path, is_tta=True):
    if not os.path.exists('result'):
        os.mkdir('result')
    # 预测结果
    transforms = T.Compose([
        T.NormSize(),
        T.LocalEqualHist(),
        T.Resize(target_size=(512, 512)),
        T.Normalize(
            [0.3883131, 0.25449154, 0.110598095], 
            [0.26274413, 0.1827712, 0.12587263])
    ])
    # 循环预测和保存
    for img_path in tqdm(imgs_path):
        H, W = np.asarray(Image.open(img_path)).shape[:2]  # 获取原始的H和W
        img, _ = transforms(img_path)  # 进行数据预处理
        img = paddle.to_tensor(img[np.newaxis, :])  # C,H,W -> 1,C,H,W
        # TTA
        if is_tta == True:
            tta_pres = paddle.zeros([1, 3,  512, 512])  # 图像大小1024
            for tta_transform in tta.aliases.hflip_transform ():
                tta_img = tta_transform.augment_image(img)  # TTA_transforms
                tta_pre = infer.inference(model, tta_img)  # 预测
                deaug_pre = tta_transform.deaugment_mask(tta_pre)
                tta_pres += deaug_pre
            pre = tta_pres / 2.
        else:
            pre = infer.inference(model, img)  # 预测
        pred = tensor2result(pre)  # 转为颜色对应的array
        pred = restore(pred, H, W)  # 恢复原始大小
        pil_img = Image.fromarray(pred)
        pil_img.save(os.path.join('result', img_path.split('/')[-1].replace('jpg', 'bmp')), 'bmp')

# 网络准备


model_path = 'output_ocrnet_hrnet18/best_model/model.pdparams'
model = OCRNet(
    backbone=HRNet_W18(),
    backbone_indices=[0],
    num_classes=3)
params = paddle.load(model_path)
model.set_state_dict(params)


model.eval()

# 预测文件
# set_path = 'datasets'
# list_file = 'datasets/val_list.txt'
# imgs_path = []
# with open(list_file, 'r') as f:
#     datas_path = f.readlines()
#     for data_path in datas_path:
#         imgs_path.append(os.path.join(set_path, data_path.split(' ')[0].strip()))

test_folder = "tests"
imgs_path = os.listdir(test_folder)
imgs_path = [os.path.join(test_folder, name) for name in imgs_path]
# 预测
nn_infer(model, imgs_path, is_tta=True)
100%|██████████| 100/100 [01:05<00:00,  1.52it/s]

5、预测后处理

  • 闭运算填充孔洞
  • 保留最大联通区,去掉其他小的联通区
import os
import numpy as np
from tqdm import tqdm
from PIL import Image
from aftercure import one_package_service

pred_folder = 'result'
save_folder = 'af_result'
if not os.path.exists(save_folder):
    os.mkdir(save_folder)
imgs_name = os.listdir(pred_folder)
for name in tqdm(imgs_name):
    img_path = os.path.join(pred_folder, name)
    img = np.asarray(Image.open(img_path))
    result = Image.fromarray(one_package_service(img, k_size=300))
    result.save(img_path.replace(pred_folder, save_folder))
100%|██████████| 100/100 [00:18<00:00,  5.54it/s]

6、结果评价

Dice系数作为分割结果测评的评价指标:

 Dice  = 2 ∣ X ∩ Y ∣ ∣ X ∣ + ∣ Y ∣ \text { Dice }=\frac{2|X \cap Y|}{|X|+|Y|}  Dice =X+Y2XY

其中,X代表金标准中分割目标像素点集合,Y代表预测结果中分割目标像素点集合,公式中 |X∩Y| 是X和Y之间的交集,|X|和|Y|分表表示X和Y的元素的个数。
此外,我们使用平均绝对误差(Mean Absolute Error)来测量样本视杯视盘分割结果与金标准之间的垂直杯盘比差异。垂直杯盘比有直接的临床相关性,可辅助评估青光眼的进展。求解方式为计算垂直方向上视杯区域和视盘区域最大直径的比值。对于任务三,最终的评分综合了视杯分割结果Dice系数、视盘分割结果Dice系数,和垂直杯盘比数值的MAE:

 Score  task  3 = 0.25 ∗  Dice  cup  + 0.35 ∗  Dice  d i s c + 0.04 ∗ 1 M A E + 0.1 \text { Score }_{\text {task } 3}=0.25 * \text { Dice }_{\text {cup }}+0.35 * \text { Dice }_{d i s c}+0.04 * \frac{1}{M A E+0.1}  Score task 3=0.25 Dice cup +0.35 Dice disc+0.04MAE+0.11

# 解压
! zip -q -r result.zip af_result

最终af_result提交得分为: 8.20684

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐