U-Net分割目标骨骼

U-Net网络架构图

​ U-Net网络架构,下采样每层包括一个33的卷积核的,每经过2个卷积层后,会经过一个大小为22的最大池化层;在第三、四次池化后,会丢弃掉50%的神经元避免过拟合。

​ 上采样中,上采样部分先上采样放大,再进行卷积操作,相当于转置卷积,上采样的卷积核大小为22,每次上采样完成后将前面下采样中拼接操作。在最后一步中采用1一个11的卷积核。

Accuracy 采用的是dice系数 X Y 分别表示预测的结果和真实的结果,在这里插入图片描述
损失函数则就就采用1-dice 系数 dice系数越高 loss 就降得越低 ,在这里插入图片描述
网络架构图图下图所示:

在这里插入图片描述

数据集

​ 本实验是对髋骨近端的股骨进行分割,下图是股骨CT图像的部分图像,目前股骨分割所面临的问题主要有以下几点:

  1. 人工分割股骨耗时耗力,且需要一定的专业知识;
  2. 部分股骨CT图像对比度不高,骨与骨之间连接紧密,分割难度大;
  3. 分割算法复杂,分割结果精确度不高

在这里插入图片描述

​ 我们标注了来自20名不同患者的左股骨的作为数据集进行训练,并用一组左股骨序列图片作为测试集,得出最终结果如下图所示:

在这里插入图片描述

从上至下每一行分别为股骨CT图片、股骨真实轮廓线、人工标注股骨区域、模型预测结果,最终模型预测的精度能够达到98.56±0.02,为了提高模型精度,后期会股骨远端和近端分别训练,以达到更好的分割效果。

代码部分

数据处理部分

data.py

from __future__ import print_function
from keras.preprocessing.image import ImageDataGenerator
import numpy as np 
import os
import glob
import skimage.io as io
import skimage.transform as trans
from skimage import img_as_uint

Sky = [128,128,128]
Building = [128,0,0]
Pole = [192,192,128]
Road = [128,64,128]
Pavement = [60,40,222]
Tree = [128,128,0]
SignSymbol = [192,128,128]
Fence = [64,64,128]
Car = [64,0,128]
Pedestrian = [64,64,0]
Bicyclist = [0,128,192]
Unlabelled = [0,0,0]

COLOR_DICT = np.array([Sky, Building, Pole, Road, Pavement,
                          Tree, SignSymbol, Fence, Car, Pedestrian, Bicyclist, Unlabelled])


def adjustData(img,mask,flag_multi_class,num_class):    # 将图片归一化
    if(flag_multi_class):   # 此程序不是多类情况,所以不考虑这个
        img = img / 255
        mask = mask[:, :, :, 0] if(len(mask.shape) == 4) else mask[:, :, 0]
        new_mask = np.zeros(mask.shape + (num_class,))
        # np.zeros里面是shape元组,此目的是将数据厚度扩展num_class层,以在层的方向实现one-hot结构
        for i in range(num_class):
            #for one pixel in the image, find the class in mask and convert it into one-hot vector
            #index = np.where(mask == i)
            #index_mask = (index[0],index[1],index[2],np.zeros(len(index[0]),dtype = np.int64) + i)
            # if (len(mask.shape) == 4) else (index[0],index[1],np.zeros(len(index[0]),dtype = np.int64) + i)
            #new_mask[index_mask] = 1
            new_mask[mask == i, i] = 1  #将平面的mask的每类,都单独变成一层
        new_mask = np.reshape(new_mask,
                              (new_mask.shape[0], new_mask.shape[1]*new_mask.shape[2], new_mask.shape[3])) if\
            flag_multi_class else np.reshape(new_mask,
                                             (new_mask.shape[0]*new_mask.shape[1], new_mask.shape[2]))
        mask = new_mask
    elif(np.max(img) > 1):
        img = img / 255
        mask = mask /255
        mask[mask > 0.5] = 1    # 二值化
        mask[mask <= 0.5] = 0
    return (img, mask)


    # 生成训练所需要的图片和标签

def trainGenerator(batch_size, train_path, image_folder, mask_folder, aug_dict, image_color_mode = "grayscale",
                    mask_color_mode = "grayscale", image_save_prefix  = "image", mask_save_prefix  = "mask",
                    flag_multi_class = False, num_class = 2, save_to_dir = None, target_size = (256,256),seed = 1):
    '''
    can generate image and mask at the same time
    use the same seed for image_datagen and mask_datagen to ensure the transformation for image and mask is the same
    if you want to visualize the results of generator, set save_to_dir = "your path"
    '''
    image_datagen = ImageDataGenerator(**aug_dict)  # 图片生成器对数据进行增强,扩充数据集大小,增强模型的泛化能力。比如进行旋转
    mask_datagen = ImageDataGenerator(**aug_dict)
    image_generator = image_datagen.flow_from_directory(    # 根据文件路径,增强数据
        train_path,     # 训练数据文件夹路径
        classes = [image_folder],   # 类别文件夹,对哪一个类进行增强
        class_mode = None,      # 不返回标签
        color_mode = image_color_mode,  # 灰度,单通道模式
        target_size = target_size,  # 转换后的目标图片大小
        batch_size = batch_size,    # 每次产生的(进行转换)图片张数
        save_to_dir = save_to_dir,  # 保存图片的路径
        save_prefix  = image_save_prefix,   # 生成图片的前缀,仅当提供save_to_dir时有效
        seed = seed)
# flow_from_directory():
# directory: 目标文件夹路径
# classes: 子文件夹列表
# class_mode: 确定返回的标签数组的类型
# color_mode: 颜色模式,grayscale为单通道,rgb三通道
# target_size: 目标图像的尺寸
# batch_size: 每批数据的大小
# save to dir: 保存图片
# save_prefix: 保存提升后图片时使用的前缀,仅当设置了save_to_dir时生效
# seed: 随机种子数,用于shuffle

mask_generator = mask_datagen.flow_from_directory(
    train_path,
    classes = [mask_folder],
    class_mode = None,
    color_mode = mask_color_mode,
    target_size = target_size,
    batch_size = batch_size,
    save_to_dir = save_to_dir,
    save_prefix  = mask_save_prefix,
    seed = seed)

train_generator = zip(image_generator, mask_generator)  # 将image和mask打包成元组的列表[(iamge1,masek1),(image2,mask2),...]
for (img,mask) in train_generator:
    # 由于batch是2,所以一次返回两张,即img是一个2张灰度图片的数组,[2,256,256]
    img, mask = adjustData(img,mask, flag_multi_class, num_class)
    yield (img, mask)
# 每次分别产出两张图片和标签
# 上面这个函数主要是产生一个数据增强的图片生成器,方便后面使用这个生成器不断生成图片


def testGenerator(test_path, num_image = 328, target_size = (256,256), flag_multi_class = False, as_gray = False):
    for i in range(num_image):
        img = io.imread(os.path.join(test_path,"%d.png"%i),as_gray = as_gray)
        img = img / 255.0     # 归一化
        img = trans.resize(img, target_size)
        img = np.reshape(img, img.shape+(1,)) if (not flag_multi_class) else img
        img = np.reshape(img, (1,)+img.shape)    # (1,width,height)
    # 将测试图片扩展一个维度,与训练时的输入[2,256,256]保持一致
        yield img
    # 上面这个函数主要是对测试图片进行规范,使其尺寸和维度上和训练图片一致

def geneTrainNpy(image_path,mask_path,flag_multi_class = False,num_class = 2,image_prefix = "image",mask_prefix = "mask",image_as_gray = True,mask_as_gray = True):
    image_name_arr = glob.glob(os.path.join(image_path,"%s*.png"%image_prefix))
    # 相当于文件搜索,搜索某路径下与字符相匹配的文件
    image_arr = []
    mask_arr = []
    for index, item in enumerate(image_name_arr):   # enumerate是枚举,输出[(0,item0),(1,item1),(2,item2)]
        img = io.imread(item, as_gray = image_as_gray)
        img = np.reshape(img, img.shape + (1,)) if image_as_gray else img
        mask = io.imread(item.replace(image_path, mask_path).replace(image_prefix, mask_prefix), as_gray = mask_as_gray)
    # 重新在mask_path文件夹下搜索带有mask字符的图片(标签图片)
        mask = np.reshape(mask, mask.shape + (1,)) if mask_as_gray else mask
        img, mask = adjustData(img, mask, flag_multi_class, num_class)
        image_arr.append(img)
        mask_arr.append(mask)
    image_arr = np.array(image_arr)
    mask_arr = np.array(mask_arr)
    return image_arr, mask_arr
    # 该函数主要是分别在训练集文件夹下和标签文件夹下搜索图片,然后扩展一个维度后以array的形式返回,
    # 是为了在没用数据增强时的读取文件夹内自带的数据


def labelVisualize(num_class,color_dict,img):
    img = img[:,:,0] if len(img.shape) == 3 else img
    img_out = np.zeros(img.shape + (3,))
    # 变成RGB空间,因为其他颜色只能在RGB空间才会显示
    for i in range(num_class):
        img_out[img == i,:] = color_dict[i]
    # 上面函数是给出测试后的输出之后,为输出涂上不同的颜色,多类情况下才起作用,两类的话无用
    return img_out / 255.0



def saveResult(save_path, npyfile, flag_multi_class = False, num_class = 2):
    for i, item in enumerate(npyfile):
        img = labelVisualize(num_class, COLOR_DICT, item) if flag_multi_class else item[:, :, 0]

    # 多类的话就图成彩色,非多类(两类)的话就是黑白色

io.imsave(os.path.join(save_path, "%d.png"%(i)), img_as_uint(img))

U-Net网络模型


from tensorflow import keras
import numpy as np
import os
import skimage.io as io
import skimage.transform as trans
from keras.models import *
from keras.layers import *
from keras.optimizers import *

from keras.layers.merge import concatenate
from keras.callbacks import ModelCheckpoint, LearningRateScheduler
from keras import backend as keras


# 多分类dice

def dice_coef(y_true, y_pred):  # flatten是numpy.ndarray.flatten的一个函数,即返回一个折叠成一维的数组。但是该函数只能适用于numpy对象,即array或者mat
    # ,普通的list列表是不行的。
    smooth = 1.
    y_true_f = K.flatten(y_true)
    y_pred_f = K.flatten(y_pred)
    intersection = K.sum(y_true_f * y_pred_f)
    return (2. * intersection + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth)


def dice_coef_loss(y_true, y_pred):
    return 1 - dice_coef(y_true, y_pred)


def unet(pretrained_weights=None, input_size=(256, 256, 1)):  # 长宽256 通道为1(灰度图)
    inputs = Input(input_size)  # 初始化keras张量
    conv1 = Conv2D(64, 3, activation='relu', padding='same', kernel_initializer='he_normal')(inputs)
    # filters: 输出的维度
    # kernel_size:卷积核的尺寸
    # activation: 激活函数
    # padding: 边缘填充
    # kernel_initializer: kenerl权值初始化
    conv1 = Conv2D(64, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv1)  # 64 he_normal(
    # ):初始loss约为150,训练0.5个epoch后loss下降到3.5,一个epoch就看达到97.82%的Test Error
    pool1 = MaxPooling2D(pool_size=(2, 2))(conv1)
    conv2 = Conv2D(128, 3, activation='relu', padding='same', kernel_initializer='he_normal')(pool1)
    conv2 = Conv2D(128, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv2)
    pool2 = MaxPooling2D(pool_size=(2, 2))(conv2)
    conv3 = Conv2D(256, 3, activation='relu', padding='same', kernel_initializer='he_normal')(pool2)
    conv3 = Conv2D(256, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv3)
    pool3 = MaxPooling2D(pool_size=(2, 2))(conv3)
    conv4 = Conv2D(512, 3, activation='relu', padding='same', kernel_initializer='he_normal')(pool3)
    conv4 = Conv2D(512, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv4)
    drop4 = Dropout(0.5)(conv4)  # 每次训练时随机忽略50%的神经元,减少过拟合
    pool4 = MaxPooling2D(pool_size=(2, 2))(drop4)
conv5 = Conv2D(1024, 3, activation='relu', padding='same', kernel_initializer='he_normal')(pool4)
conv5 = Conv2D(1024, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv5)
drop5 = Dropout(0.4)(conv5)

up6 = Conv2D(512, 2, activation='relu', padding='same', kernel_initializer='he_normal')(
    UpSampling2D(size=(2, 2))(drop5))  # 先上采样放大,再进行卷积操作,相当于转置卷积
merge6 = concatenate([drop4, up6], axis=3)  # (width,height,channels) 拼接通道数
conv6 = Conv2D(512, 3, activation='relu', padding='same', kernel_initializer='he_normal')(merge6)
conv6 = Conv2D(512, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv6)

up7 = Conv2D(256, 2, activation='relu', padding='same', kernel_initializer='he_normal')(
    UpSampling2D(size=(2, 2))(conv6))
merge7 = concatenate([conv3, up7], axis=3)
conv7 = Conv2D(256, 3, activation='relu', padding='same', kernel_initializer='he_normal')(merge7)
conv7 = Conv2D(256, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv7)

up8 = Conv2D(128, 2, activation='relu', padding='same', kernel_initializer='he_normal')(
    UpSampling2D(size=(2, 2))(conv7))
merge8 = concatenate([conv2, up8], axis=3)
conv8 = Conv2D(128, 3, activation='relu', padding='same', kernel_initializer='he_normal')(merge8)
conv8 = Conv2D(128, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv8)

up9 = Conv2D(64, 2, activation='relu', padding='same', kernel_initializer='he_normal')(
    UpSampling2D(size=(2, 2))(conv8))
merge9 = concatenate([conv1, up9], axis=3)
conv9 = Conv2D(64, 3, activation='relu', padding='same', kernel_initializer='he_normal')(merge9)
conv9 = Conv2D(64, 3, activation='relu', padding='same', kernel_initializer='he_normal')(conv9)
conv10 = Conv2D(1, 1, activation='sigmoid')(conv9)

model = Model(input=inputs, output=conv10)

model.compile(optimizer=Adam(lr=1e-5), loss=dice_coef_loss, metrics=[dice_coef])
# optimizer: 优化器
# binary_crossentropy: 与sigmoid相对应的损失函数
# metrics: 评估模型在训练和测试时的性能指标

# model.summary()

if (pretrained_weights):
    model.load_weights(pretrained_weights)

return model

主函数

from model import *
from data import *  # 导入这两个文件中所有函数

gpuNo='1'
print("gpuNo=", gpuNo)
os.environ['CUDA_VISIBLE_DEVICES'] = gpuNo

# os.environ["CUDA_VISIBLE_DEVICES"] = "0"

data_gen_args = dict(rotation_range=0.2,    # 旋转
                    width_shift_range=0.05,     # 宽度变化
                    height_shift_range=0.05,    # 高度变化
                    shear_range=0.05,   # 错切变换
                    zoom_range=0.05,    # 缩放
                    horizontal_flip=True,   # 水平翻转
                    fill_mode='nearest')    # 数据增强时的变换方式的字典



myGene = trainGenerator(20, '0', 'image', 'label',  data_gen_args, save_to_dir=None)
# 得到一个生成器,以batch=2的速率无限生成增强后的数据

model = unet()
model_checkpoint = ModelCheckpoint('rgbfemur1.hdf5', monitor='loss', verbose=1, save_best_only=True)
# 回调函数,在每个epoch后保存模型到filepath
# filename: 保存模型
# monitor:需要监视的值,检测loss使他最小
# verbose: 信息展示模式,1展示,0不展示;
# save_best_only: 保存在验证集上性能最好的模型

model.fit_generator(myGene, steps_per_epoch=100, epochs=300, callbacks=[model_checkpoint])
# 上面一行是利用生成器进行batch_size数量的训练,样本和标签通过myGene传入
# generator: 生成器(image,mask)
# steps_per_epoch: 训练steps_per_epoch个数据时记一个epoch结束,
# epoch: 数据迭代轮数
# callbacks: 回调函数
#
# testGene = testGenerator("data/membrane/test")
# results = model.predict_generator(testGene, 30, verbose=1)
# # 30是step,steps:在停止之前,来自generator的总步数(样本批次)。可选参数Sequence:如果未指定,则将使用len(generator)作为步数
# # 上面返回值是:预测值的Numpy数组
# # 为来自数据生成器的输入样本生成预测
# # generator: 生成器
# # verbose = 0 为不在标准输出流输出日志信息
# # verbose = 1 为输出进度条记录
# # verbose = 2 为每个epoch输出一行记录
# # steps: 在声明一个epoch完成,并开始下一个epoch之前从生成器产生的总步数
# # workers: 最大进程数
# # use_multiprocessing: 多线程
#
# saveResult("data/membrane/test", results)

测试函数

from model import *
from data import *  # 导入这两个文件中所有函数

gpuNo='1'
print("gpuNo=", gpuNo)
os.environ['CUDA_VISIBLE_DEVICES'] = gpuNo

model = unet(pretrained_weights='rgbfemur1.hdf5')
testGene = testGenerator("test/")
results = model.predict_generator(testGene, 69, verbose=1)
# 30是step,steps:在停止之前,来自generator的总步数(样本批次)。可选参数Sequence:如果未指定,则将使用len(generator)作为步数
# 上面返回值是:预测值的Numpy数组
# 为来自数据生成器的输入样本生成预测
# generator: 生成器
# verbose = 0 为不在标准输出流输出日志信息
# verbose = 1 为输出进度条记录
# verbose = 2 为每个epoch输出一行记录
# steps: 在声明一个epoch完成,并开始下一个epoch之前从生成器产生的总步数
# workers: 最大进程数
# use_multiprocessing: 多线程

saveResult("test/gray/", results)
  • 5
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值