★★★ 本文源自AlStudio社区精品项目,【点击此处】查看更多精品内容 >>>

项目背景介绍

本项使用注意力机制融合的NA-Unet完成眼底血管语义分割,本项目使用自荷兰的糖尿病视网膜病变筛查项目数据集,自搭建Unet模型的encoder与decoder部分。在实现过程中,加入NA注意力模块,经实际测试对比,对眼底语义分割效果有较为明显的提升。

项目使用包的引入

import os
import shutil
import numpy as np
from paddle.io import Dataset,DataLoader
from paddle.vision import transforms as T
from paddle.nn import functional as F
import cv2
import paddle
import matplotlib.pyplot as plt
import paddle.nn as nn
from tqdm import tqdm
/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/matplotlib/__init__.py:107: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working
  from collections import MutableMapping
/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/matplotlib/rcsetup.py:20: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working
  from collections import Iterable, Mapping
/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/matplotlib/colors.py:53: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working
  from collections import Sized

数据介绍

本项目使用自荷兰的糖尿病视网膜病变筛查项目数据集,这个数据集在原数据集的基础上做了数据增强(旋转、缩放)。

显示原数据集标注格式

  • 原数据集的一组image和annotation如下:

image
annotation

np.set_printoptions(threshold=np.inf)
import matplotlib
path = "FundusVessels/Annotations/0.png"
# 标注数据RGB各颜色通道标记为1或0
# 各通道相同
im = cv2.imread(path)
im = im.transpose((2,0,1))
im_0 = im[0,:,:]
im_1 = im[1,:,:]
im_2 = im[2,:,:]

由下面两个cell可知,数据集Annotation中的图像RGB三通道每个通道的都使用0,1进行标记,各通道标记情况相同。

np.count_nonzero(im_0)
im_0
np.count_nonzero(im_1)
im_1
np.count_nonzero(im_2)
#验证集的数量
eval_num=99
#所有图像的大小
image_size=(224,224)
#训练图片路径
train_images_path=r"FundusVessels\JPEGImages"
#标签图像路径
label_images_path=r"FundusVessels\Annotations"
#测试图片路径
test_images_path=r"FundusVessels\Prediction"
#批量大小
batch_size=4

数据集的获取与预处理

这部分从路径以列表形式获取图像与测试图像,同时以元组形式合并一对image和label。

def get_path(image_path):
    files=[]
    for image_name in os.listdir(image_path):
        files.append(os.path.join(image_path,image_name))
    return sorted(files)

def get_test_data(test_images_path):
    test_data=[]
    for name in os.listdir(test_images_path):
        img_path=os.path.join(test_images_path,name)
        test_data.append(img_path)
    test_data=np.expand_dims(np.array(test_data),axis=1)
    return test_data


数据集的划分

train_transform=T.Compose([
            T.Resize(image_size),  #裁剪
            T.ColorJitter(0.1,0.1,0.1,0.1), #亮度,对比度,饱和度和色调
            T.Transpose(), #CHW
            T.Normalize(mean=0.,std=255.) #归一化
        ])
eval_transform=T.Compose([
            T.Resize(image_size),
            T.Transpose(),
            T.Normalize(mean=0.,std=255.)
        ])

创建数据读取器

class ImageDataset(Dataset):
    def __init__(self,path,transform):
        super(ImageDataset, self).__init__()
        self.path=path #[]
        self.transform=transform

    def _load_image(self,path):
        '''
        该方法作用为通过路径获取图像
        '''
        img=cv2.imread(path)
        img=cv2.resize(img,image_size)
        return img

    def __getitem__(self,index):
        
        # 是否transform
        path=self.path[index]
        if len(path)==2:# train,eval
            #print("len2:",path)
            data_path,label_path=path
            data,label=self._load_image(data_path),self._load_image(label_path)
            data,label=self.transform(data),label
            label = label.transpose((2, 0, 1))
            # print(label.shape) (3,224,224)
            label = label[0, :, :] # 取出一个通道
            # print("label") (224,224)
            label = np.expand_dims(label, axis=0)
            label = label.astype("int64")
            return data,label
            
        
        if len(path)==1:# test
            #print("len1:",path)
            data=self._load_image(path[0])
            data=self.transform(data)
            return data

    def __len__(self):
        return len(self.path)

#获取数据读取器
train_dataset=ImageDataset(train_data,train_transform)
eval_dataset=ImageDataset(eval_data,eval_transform)
test_dataset=ImageDataset(test_data,eval_transform)
train_dataloader=DataLoader(train_dataset,batch_size=batch_size,shuffle=True,drop_last=True)
eval_dataloader=DataLoader(eval_dataset,batch_size=batch_size,shuffle=True,drop_last=True)

查看paddle中gpu是否存在。

print(paddle.device.get_device())
cpu

模型的建立

下图为Unet模型架构图,
Unet模型架构图

Encoder下采样部分

Encoder下采样部分两层卷积,两层归一化,最后池化。

class Encoder(nn.Layer):#下采样:两层卷积,两层归一化,最后池化。
    def __init__(self, num_channels, num_filters):
        super(Encoder,self).__init__()#继承父类的初始化
        self.features = nn.Sequential(
            nn.Conv2D(in_channels=num_channels,
                              out_channels=num_filters,
                              kernel_size=3,#3x3卷积核,步长为1,填充为1,不改变图片尺寸[H W]
                              stride=1,
                              padding=1),
            nn.BatchNorm(num_filters,act="relu"),
            nn.Conv2D(in_channels=num_filters,
                              out_channels=num_filters,
                              kernel_size=3,
                              stride=1,
                              padding=1),
            NA(),
            nn.BatchNorm(num_filters,act="relu"),
        )
        
        self.pool  = nn.MaxPool2D(kernel_size=2,stride=2,padding="SAME")#池化层,图片尺寸减半[H/2 W/2]
    def forward(self,inputs):
        x = self.features(inputs)
        x_conv = x           #两个输出,灰色 ->
        x_pool = self.pool(x)#两个输出,红色 | 
        return x_conv, x_pool
    

Decoder上采样部分

Decoder部分一层反卷积,两层卷积层,两层归一化

class Decoder(nn.Layer):#上采样:一层反卷积,两层卷积层,两层归一化
    def __init__(self, num_channels, num_filters):
        super(Decoder,self).__init__()
        self.features = nn.Sequential(
            nn.Conv2D(in_channels=num_filters*2,
                              out_channels=num_filters,
                              kernel_size=3,
                              stride=1,
                              padding=1),
            nn.BatchNorm(num_filters,act="relu"),
            nn.Conv2D(in_channels=num_filters,
                              out_channels=num_filters,
                              kernel_size=3,
                              stride=1,
                              padding=1),
            NA(),
            nn.BatchNorm(num_filters,act="relu")
        )
        self.up = nn.Conv2DTranspose(in_channels=num_channels,
                                    out_channels=num_filters,
                                    kernel_size=2,
                                    stride=2,
                                    padding=0)#图片尺寸变大一倍[2*H 2*W]

       
        
    def forward(self,input_conv,input_pool):
        x = self.up(input_pool)
        h_diff = (input_conv.shape[2]-x.shape[2])
        w_diff = (input_conv.shape[3]-x.shape[3])
        pad = nn.Pad2D(padding=[h_diff//2, h_diff-h_diff//2, w_diff//2, w_diff-w_diff//2])
        x = pad(x)                                #以下采样保存的feature map为基准,填充上采样的feature map尺寸
        x = paddle.concat(x=[input_conv,x],axis=1)#考虑上下文信息,in_channels扩大两倍
        x = self.features(x)
        return x

NA注意力模块

NA即归一化注意模块,学习了channel之间的相关性,筛选出了针对通道的注意力,并进行归一化。

NA注意力模块类

class NA(nn.Layer):
    def __init__(self):
        super().__init__()
        self.gamma = self.create_parameter([1], default_initializer=nn.initializer.Assign(paddle.ones([1])))
        self.beta = self.create_parameter([1], default_initializer=nn.initializer.Assign(paddle.zeros([1])))
        self.softmax = nn.Softmax(axis=2)
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        C = paddle.mean(x, axis=[2, 3], keepdim=True)
        
        g = -paddle.abs(x - C)
        b, c, h, w = g.shape
        g = paddle.sum(self.softmax(g.reshape((b, c, -1))), axis=2, keepdim=True).reshape((b, c, 1, 1))

        mean = paddle.mean(g, axis=1, keepdim=True)
        std = paddle.std(g, axis=1, keepdim=True) + 1e-5

        g = self.gamma * (g - mean) / std + self.beta

        out = x * self.sigmoid(g)

        return out

NA注意力模块的效果比较

当模型训练参数均设置为epochs=2,verbose=1,log_freq=1,learning_rate=1e-2,weight_decay=1e-3时

在模块构建中不添加NA模块,

示例训练结果如下图

在模块构建中添加NA模块,

示例训练结果如下图

由上面两张图可以看出,NA模块对眼底血管语义分割结果有较为明显的改进。

Unet模型搭建(利用Encoder,Decoder部分)

class UNet(nn.Layer):
    def __init__(self,num_classes=59):
        super(UNet,self).__init__()
        self.down1 = Encoder(num_channels=  3, num_filters=64) #下采样
        self.down2 = Encoder(num_channels= 64, num_filters=128)
        self.down3 = Encoder(num_channels=128, num_filters=256)
        self.down4 = Encoder(num_channels=256, num_filters=512)
        
        self.mid_conv1 = nn.Conv2D(512,1024,1)                 #中间层
        self.mid_bn1   = nn.BatchNorm(1024,act="relu")
        self.mid_conv2 = nn.Conv2D(1024,1024,1)
        self.mid_bn2   = nn.BatchNorm(1024,act="relu")

#         self.mid_conv1 = nn.Conv2D(256,512,1)                 #中间层
#         self.mid_bn1   = nn.BatchNorm(512,act="relu")
#         self.mid_conv2 = nn.Conv2D(512,512,1)
#         self.mid_bn2   = nn.BatchNorm(512,act="relu")

        self.up4 = Decoder(1024,512)                           #上采样
        self.up3 = Decoder(512,256)
        self.up2 = Decoder(256,128)
        self.up1 = Decoder(128,64)
        
        self.last_conv = nn.Conv2D(64,num_classes,1)           #1x1卷积,softmax做分类
        
    def forward(self,inputs):
        x1, x = self.down1(inputs)
        x2, x = self.down2(x)
        x3, x = self.down3(x)
        x4, x = self.down4(x)
        
        x = self.mid_conv1(x)
        x = self.mid_bn1(x)
        x = self.mid_conv2(x)
        x = self.mid_bn2(x)
        
        x = self.up4(x4, x)
        x = self.up3(x3, x)
        x = self.up2(x2, x)
        x = self.up1(x1, x)
        
        x = self.last_conv(x)
        
        return x

模型训练

定义优化器和损失函数,启动模型训练。

model = paddle.Model(UNet(2))    
print('model finish')
#定义优化器和损失函数
opt=paddle.optimizer.Momentum(learning_rate=1e-2,parameters=model.parameters(),weight_decay=1e-3)
model.prepare(opt, paddle.nn.CrossEntropyLoss(axis=1))
print('definition finish')
#启动模型训练
model.fit(train_dataloader, eval_dataloader, epochs=4,verbose=1,save_dir="./net_params",log_freq=1)
print('train')

模型的测试

读取训练好的模行参数文件,对Unet模型进行实例化,将预测结果写入"predict_result/"中。

from paddle.static import InputSpec

#预测图像保存路径
work_path = "predict_result/"
if os.path.exists(work_path):
    shutil.rmtree(work_path)
os.mkdir(work_path)

#读取模型参数文件路径
save_dir=work_path
checkpoint_path="./net_params/final"
                  
#实例化
model = paddle.Model(UNet(2))       #U-Net
model.load(checkpoint_path)

for i,img in tqdm(enumerate(test_dataset)):
    img=paddle.to_tensor(img).unsqueeze(0)
    predict=np.array(model.predict_batch(img)).squeeze(0).squeeze(0)
    predict=predict.argmax(axis=0)
    image_path=test_dataset.path[i]
    path_lst=image_path[0].split("/")
    lst = path_lst[-1][-13:-4]
    print(lst)
    lst = lst.replace("\\","/")
    print(lst)
    save_path=os.path.join(save_dir,lst)+".jpg"
    print(save_path)
  image_path=test_dataset.path[i]
    path_lst=image_path[0].split("/")
    lst = path_lst[-1][-13:-4]
    print(lst)
    lst = lst.replace("\\","/")
    print(lst)
    save_path=os.path.join(save_dir,lst)+".jpg"
    print(save_path)
    cv2.imwrite(save_path,predict*255)

测试结果如下:


参考文章

图像分割的打怪升级之路——UNet、PSPNet、Deeplab

U-Net: Convolutional Networks for Biomedical Image Segmentation

NA:归一化注意力机制

请点击此处查看本环境基本用法.

Please click here for more detailed instructions.

此文章为搬运
原项目链接

Logo

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

更多推荐