飞桨常规赛:视杯视盘分割 - 11月第2名方案
MICCAI2021 Contest : GAMMA 任务三:对2D眼底图像中的视杯和视盘结构进行分割
MICCAI2021 Contest : GAMMA Task 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∣+∣Y∣2∣X∩Y∣
其中,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.04∗MAE+0.11
# 解压
! zip -q -r result.zip af_result
最终af_result提交得分为: 8.20684
更多推荐
所有评论(0)