1 赛题背景

本赛题原型为ISBI2019PALM眼科大赛。 近视已成为全球公共卫生负担。在近视患者中,约35%为高度近视。近视导致眼轴长度的延长,可能引起视网膜和脉络膜的病理改变。随着近视屈光度的增加,高度近视将发展为病理性近视,其特点是病理改变的形成:
(1)后极,包括镶嵌型眼底、后葡萄肿、视网膜脉络膜变性等;
(2)视盘,包括乳头旁萎缩、倾斜等;
(3)近视性黄斑,包括漆裂、福氏斑、CNV等。

病理性近视对患者造成不可逆的视力损害。因此,早期诊断和定期随访非常重要。

视网膜由黄斑向鼻侧约3mm处有一直径约1.5mm、境界清楚的淡红色圆盘状结构,称为视神经盘,简称视盘。视盘是眼底图像的一个重要特征,对其进行准确、快速地定位与分割对利用眼底图像进行疾病辅助诊断具有重要意义。


2 赛题介绍

PALM眼底视盘检测与分割常规赛的重点是研究和发展与患者眼底照片结构分割相关的算法。该常规赛的目标是评估和比较在一个常见的视网膜眼底图像数据集上分割视盘的自动算法。该任务目的是对眼底图像的视盘进行检测,若存在视盘结构,需从眼底图像中分割出视盘区域;若无视盘结构,分割结果直接置全背景。


【数据说明】

本次常规赛提供的金标准由中山大学中山眼科中心的7名眼科医生手工进行视盘像素级标注,之后由另一位高级专家将它们融合为最终的标注结果。存储为 BMP 图像,与对应的眼底图像大小相同,标签为 0 代表视盘(黑色区域);标签为 255 代表其他(白色区域)。

  • 训练数据集

文件名称:Train:Train文件夹里有 fundus_images 文件夹和 Disc_Masks 文件夹。

  1. fundus_images文件夹内包含800张眼底彩照,分辨率为1444×1444,或2124×2056。命名形如H0001.jpg、N0001.jpg、P0001.jpg和V0001.jpg。
  2. Disc_Masks文件夹内包含fundus_images里眼底彩照的视盘分割金标准,大小与对应的眼底彩照一致。命名前缀和对应的fundus_images文件夹里的图像命名一致,后缀为bmp。
  • 测试数据集

文件名称:PALM-Testing400-Images:包含400张眼底彩照,命名形如T0001.jpg。


3 技术方案

通过【赛题介绍】可知,本赛题为医学图像——眼底彩照图像中的语义分割 2 分类任务,尝试使用 PaddleSeg - API 版本实现数据变换、读取、训练和预测全流程。在此之前,先进行数据清洗转换成套件标准格式,使用 [PaddleX] 进行数据划分

PaddleX: 飞桨全流程开发工具,集飞桨核心框架、模型库、工具及组件等深度学习开发所需全部能力于一身,打通深度学习开发全流程。PaddleX同时提供简明易懂的Python API,及一键下载安装的图形化开发客户端。用户可根据实际生产需求选择相应的开发方式,获得飞桨全流程开发的最佳体验。

PaddleSeg: 基于飞桨 PaddlePaddle 开发的端到端图像分割开发套件,涵盖了高精度和轻量级等不同方向的大量高质量分割模型。通过模块化的设计,提供了配置化驱动和API调用两种应用方式,帮助开发者更便捷地完成从训练到部署的全流程图像分割应用。

3.1 安装开发套件

其中PaddleX用于命令行下的数据划分,其他部分用PaddleSeg实现。

!pip install paddlex
!pip install paddleseg

3.2 导入依赖模块

import warnings
warnings.filterwarnings('ignore')

import paddle
import paddleseg
from paddleseg import transforms as T

import os
import shutil
import glob

import numpy as np
import pandas as pd

import cv2
import imghdr
from PIL import Image

3.3 数据获取与清洗

  • PaddleX 要求目录结构是PascalVOC结构的目录,方便进行数据划分。
  • 我们将对源数据目录重定义至如下格式:data/dataset/Train/{JPEGImages, Annotations}

这里是对官方数据进行下载,然后对相应的目录重命名,达到上述目录结构。

!wget https://bj.bcebos.com/v1/dataset-bj/%E5%8C%BB%E7%96%97%E6%AF%94%E8%B5%9B/%E5%B8%B8%E8%A7%84%E8%B5%9B%EF%BC%9APALM%E7%9C%BC%E5%BA%95%E5%BD%A9%E7%85%A7%E8%A7%86%E7%9B%98%E6%8E%A2%E6%B5%8B%E4%B8%8E%E5%88%86%E5%89%B2.zip -O data/dataset.zip
!unzip -oq data/dataset.zip -d data

!rm -r data/__MACOSX
!mv data/常规赛:PALM眼底彩照视盘探测与分割 data/dataset

!mv data/dataset/Train/fundus_image data/dataset/Train/JPEGImages
!mv data/dataset/Train/Disc_Masks data/dataset/Train/Annotations
!mv data/dataset/PALM-Testing400-Images data/dataset/Test

以上单元格运行完成后,得到如下目录结构。

!tree data/dataset -d
data/dataset
├── Test
└── Train
    ├── Annotations
    └── JPEGImages

4 directories
  • 数据清洗:原图和标注图的模式分别为 RGB(真彩色、三通道, JPG) 和 L(8位灰度图, PNG),并且尺寸(size)需要一致;
  • 另外,标注图的数值应该是从0开始递增的,所以对 0-255 标签映射到 1-0 标签。
image_path_list = glob.glob('data/dataset/Train/JPEGImages/*.jpg')

for i in range(len(image_path_list)):
    image_path = image_path_list[i]
    image_name = str(image_path.split('/')[-1]).split('.')[0]
    label_path = os.path.join('data/dataset/Train/Annotations', f'{image_name}.bmp')

    try:
        assert imghdr.what(image_path) and imghdr.what(label_path)
    except:
        print(image_path, label_path)

    image = Image.open(image_path).convert('RGB')
    label = Image.open(label_path).convert('L')

    if image.size != label.size:
        image = image.resize(label.size, Image.ANTIALIAS)
    image.save(image_path)

    label = np.array(label, dtype='uint8')
    label = np.uint8((label == 0) * 1)
    cv2.imwrite(label_path.split('.')[0]+'.png', label)
    os.remove(label_path)

3.4 数据划分

  • 20%用于验证,0%用于测试(测试集为 data/dataset/Test/*.jpg),则80%用于训练。
!paddlex --split_dataset --format SEG\
    --dataset_dir data/dataset/Train\
    --val_value 0.2\
    --test_value 0

以上,得到 data/dataset/Train/{train_list.txt, val_list.txt, labels.txt} 三个文件。

笔者的得到的划分情况在 work/{train_list.txt, val_list.txt}

3.5 数据变换与读取

为了训练时 GPU 内存的固定,加快训练速度,这里将图像 Resize 到 512x512,另外做了 [-25, 25]°的随机旋转和垂直翻转,最后将图像归一化到 [-1, 1]。

train_transforms = [
    T.RandomRotation(max_rotation=25),
    T.Resize(target_size=(512, 512), interp='AREA'),
    T.RandomVerticalFlip(0.5),
    T.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]),
]

eval_transforms = [
    T.Resize(target_size=(512, 512), interp='AREA'),
    T.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]),
]

定义训练数据和验证数据的读取器(paddleseg.datasets.Dataset 为自定义数据集),配置相应的参数。

train_dataset = paddleseg.datasets.Dataset(
    mode='train',
    num_classes=2,
    dataset_root='data/dataset/Train',
    train_path='work/train_list.txt',
    transforms=train_transforms)

val_dataset = paddleseg.datasets.Dataset(
    mode='val',
    num_classes=2,
    dataset_root='data/dataset/Train',    
    val_path='work/val_list.txt',
    transforms=eval_transforms)

3.6 模型与超参数

先定义模型,配置FCN的backbone及相应的pretrain (IMAGENET+CITYSCAPES)。

其中的预训练权重来源为:https://github.com/paddlepaddle/PaddleSeg/tree/release/2.3/configs/fcn 。

backbone = paddleseg.models.backbones.HRNet_W18(
    pretrained='https://bj.bcebos.com/paddleseg/dygraph/hrnet_w18_ssld.tar.gz'
)

model = paddleseg.models.FCN(
    num_classes=2,
    backbone=backbone,
    backbone_indices=(-1,),
    pretrained='https://bj.bcebos.com/paddleseg/dygraph/cityscapes/fcn_hrnetw18_cityscapes_1024x512_80k/model.pdparams')

构建自定义 Momentum 优化器,采用Warmup + Cosine-decay 学习率衰减策略,模型更新 10k 次参数,L2正则化系数 0.0001。

iters = 10000
train_batch_size = 4
learning_rate = 0.01

decayed_lr = paddle.optimizer.lr.CosineAnnealingDecay(
    learning_rate=learning_rate,
    T_max=iters)

decayed_lr = paddle.optimizer.lr.LinearWarmup(
    learning_rate=decayed_lr,
    warmup_steps=1000,
    start_lr=0.0,
    end_lr=learning_rate)

optimizer = paddle.optimizer.Momentum(
    learning_rate=decayed_lr,
    momentum=0.9,
    weight_decay=paddle.regularizer.L2Decay(1e-4),
    parameters=model.parameters())

定义损失函数,这里采用加权的 CE 和 DiceLoss。

from paddleseg.models import MixedLoss, CrossEntropyLoss, DiceLoss

losses = {
    'types': [
        MixedLoss([CrossEntropyLoss(), DiceLoss()], [0.8, 0.2]),
    ],
    'coef': [1]
}

开始训练:

  • 训练集和验证集;
  • 模型、优化器、损失函数;
  • 每步的批大小和总步数;
  • 配置训练过程,保存模型间隔步长,日志打印间隔步长,线程数,模型保存文件夹路径,默认最多保存 5 个。

输入终端命令 watch -n 0 nvidia-smi:得到内存使用情况为 4246MiB / 16160MiB。

from paddleseg.core import train

train(
    train_dataset=train_dataset,
    val_dataset=val_dataset,

    model=model,
    optimizer=optimizer,
    losses=losses,

    iters=iters,
    batch_size=train_batch_size,

    save_interval=500,
    log_iters=50,
    num_workers=0,
    save_dir='output/FCN_HRNetW18_512x512_B8_CE.8+D.2_10k',
    use_vdl=True)

训练结束得到保存下来的模型权重文件 *.pdparams

4 评估与预测

这里是评估和预测的两个接口。

from paddleseg.core import evaluate
from paddleseg.core import predict

首先初始化训练时的模型架构 FCN+HRNetW18,之后载入训练好的参数(work/model.pdparams)。

model = paddleseg.models.FCN(
    num_classes=2,
    backbone=paddleseg.models.backbones.HRNet_W18(),
    backbone_indices=(-1,))

params_path = 'work/model.pdparams'
model_state_dict = paddle.load(params_path)
model.set_dict(model_state_dict)

模型增强评估:包含水平翻转和垂直翻转。

evaluate(
    model,
    val_dataset,

    aug_eval=True,
    flip_horizontal=True,
    flip_vertical=True)

单张图片的预测结果可视化(这里要注意加上 T.Compose)。

image_list = ['data/dataset/Test/T0002.jpg']

predict(
    model=model,
    model_path=params_path,
    transforms=T.Compose(eval_transforms),
    image_list=image_list,
    save_dir='one_predict')

PaddleSeg 预测得到两种类型图片,一种是叠加图(added_prediction),另一种是 3 通道伪彩色图像(pseudo_prediction)。

import matplotlib.pyplot as plt
%matplotlib inline

added_prediction = cv2.imread('one_predict/added_prediction/T0002.jpg')
pseudo_prediction = cv2.imread('one_predict/pseudo_color_prediction/T0002.png')
gray_prediction = cv2.imread('one_predict/pseudo_color_prediction/T0002.png', cv2.IMREAD_GRAYSCALE)

plt.figure(figsize=(8, 8))
plt.subplot(221)
plt.title('added_prediction')
plt.imshow(added_prediction[:, :, [2, 1, 0]])
plt.subplot(222)
plt.title('pseudo_color_prediction')
plt.imshow(pseudo_prediction[:, :, [2, 1, 0]])
plt.subplot(223)
plt.title('pseudo_color_prediction gray')
plt.imshow(gray_prediction, cmap='gray')
plt.tight_layout()
plt.show()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n2FmJzG4-1640412387228)(output_47_0.png)]

np.unique(gray_prediction, return_counts=True)
(array([38, 75], dtype=uint8), array([4309420,   57524]))

考虑如何生成比赛的提交文件(黑色区域 0 代表视盘,白色区域 255 代表其他):

  • 观察伪彩色图片的灰度图,可以知道灰度图的数值(黑色区域 38 - 白色区域 75)需要分别转换成(0 - 255)。

预测所有测试集图片,即将测试集图片路径都放入上方的 image_list 中即可。

image_list = glob.glob('data/dataset/Test/*.jpg')

predict(
    model=model,
    model_path=params_path,
    transforms=T.Compose(eval_transforms),
    image_list=image_list,
    save_dir='test_predict')

对预测结果图像中的伪彩色图像进行数值映射,将图像另存为 work/Disc_Segmentation/*.png

if not os.path.exists('work/Disc_Segmentation'):
    os.makedirs('work/Disc_Segmentation')

label_path_list = glob.glob('test_predict/pseudo_color_prediction/*.png')

for path in label_path_list:
    img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
    img = np.uint8((img == 38) * 255)
    cv2.imwrite(f"work/Disc_Segmentation/{path.split('/')[-1]}", img)

压缩文件夹,生成提交文件。

%cd work/
!zip -rq Disc_Segmentation.zip Disc_Segmentation
t8((img == 38) * 255)
    cv2.imwrite(f"work/Disc_Segmentation/{path.split('/')[-1]}", img)

压缩文件夹,生成提交文件。

%cd work/
!zip -rq Disc_Segmentation.zip Disc_Segmentation
%cd ../

5 项目总结

本项目使用 PaddleSeg-API 搭建 FCN 模型,以具有 CITYSCPES 预训练权重的 HRNet18 作为 Backbone,利用随机旋转、随机垂直翻转的数据增强,配置 Dice Loss + CE 混合损失函数和 Warmup + Cosine decay 的训练策略,完成了眼底视盘检测与分割赛题,提交分数可达到 0.96701。

改进方向

  • 训练数据的预处理、训练时的数据增强方法;
  • 模型和超参数的配置与调优;

我的 AI Studio 主页

Logo

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

更多推荐