UNet-肝脏肿瘤图像语义分割

目录

一. 语义分割

二. 数据集

三. 数据增强

图像数据处理步骤

CT图像增强方法 :windowing方法

直方图均衡化

获取掩膜图像深度

在肿瘤CT图中提取肿瘤

保存肿瘤数据

 四. 数据加载

数据批处理

​编辑​编辑

数据集加载

  五. UNet神经网络模型搭建

         单张图片预测图


一. 语义分割

第三代图像分割:语义分割

        图像分割(Image Segmentation)是计算机视觉领域中的一项重要基础技术。图像分割是将数字图像细分为多个图像子区域的过程,通过简化或改变图像的表示形式,让图像能够更加容易被理解。更简单地说,图像分割就是为数字图像中的每一个像素附加标签,使得具有相同标签的像素具有某种共同的视觉特性。

        医学图像的模态(格式)更加多样化,如X-ray、CT、MRI以及超声等等,当然也包括一些常见的RGB图像(如眼底视网膜图像)。不同模态图像反应的信息侧重点是不一样的。比如X-ray观察骨骼更清晰,CT可以反应组织和器官出血,MRI适合观察软组织。而且不同型号的成像设备得到的成像结果有一定差异。

一图为一肝脏CT图像切片可视化结果,已经过预处理转化为灰度图像,组织与器官之间的分界线比较模糊。

二图为不同个体的肝脏CT图像,差异巨大,这给肝脏组织提取带来了很大的困难。

二. 数据集

3D-IRCADb (3D Image Reconstruction for Comparison of Algorithm Database,用于算法数据库比较的三维图像重建),数据库由75%病例中10名女性和10名男性肝脏肿瘤患者的CT扫描组成,每个文件夹对应不同的病人,提供了有关图像的信息,如肝脏大小(宽度、深度、高度)或根据Couninurd分割的肿瘤位置。它还表明,由于与邻近器官的接触、肝脏的非典型形状或密度,甚至图像中的伪影,肝脏分割可能会遇到重大困难。

链接:https://pan.baidu.com/s/1P76AF-wvrFjElc6tR82tRA 
提取码:5el7 

三. 数据增强

图像数据处理步骤

1.数据加载

2.CT图像增强

3.直方图均衡化增强

4.获取肿瘤对应CT图、肝脏肿瘤掩模图

5.保存图像

DICOM数据读取

使用pydicom库读取文件,pydicom.dcmread函数读取文件,对文件排序,pixel_array属性提取图像像素信息,展示图片:

批量数据读取

# 批量读取数据
img_slices = [pydicom.dcmread(os.path.join(data_path, file_name)) for file_name in os.listdir(data_path)]
os.listdir(data_path)
# 排序,避免CT图乱序
img_slices.sort(key=lambda x:x.InstanceNumber)  # 顺序属性
img_array = np.array([i.pixel_array for i in img_slices])		# 提取像素值

 

CT图像增强方法 :windowing方法

CT图像的范围很大导致了对比度很差,需要针对具体的器官进行处理。

CT值的物理意义就是CT射线照了你的身体,辐射经过你的身体时的辐射强度的衰减程度。

CT 的特点是能够分辨人体组织密度的轻微差别,所采用的标准是根据各种组织对x线的线性吸收系数(μ值)来决定的。

根据HuCT)值来筛选我们想要的部位的图片,其他部位全部抹黑或者抹白,目的是为了增加图像对比度。使用windowing方法。观察的CT值范围:窗宽。观察的中心CT值即为窗位,然后对肿瘤部分进行二值化处理。

def windowing(img, window_width, window_center):
    # params:需要增强的图片, 窗口宽度, 窗中心   通过窗口最小值来线性移动窗口增强
    min_windows = float(window_center)-0.5*float(window_width)
    new_img = (img-min_windows)/float(window_width)
    new_img[new_img<0] = 0			# 二值化处理 抹白
    new_img[new_img>1] = 1			# 抹黑
    return (new_img * 255).astype('uint8')  # 把数据整理成标准图像格式
img_ct = windowing(img_array, 500, 150)

直方图均衡化

直方图均衡化函数:把整个图像分成许多小块(比如按10*10作为一个小块),对每个小块进行均衡化。主要对于图像直方图不是那么单一的(比如存在多峰情况)图像比较实用。0pencv中将这种方法为:cv2.createCLAHE()

def clahe_equalized(imgs):
    # 输入imgs的形状必须是3维
    assert (len(imgs.shape) == 3)
    # 定义均衡化函数
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
    # 新数组存放均衡化后的数据
    img_res = np.zeros_like(imgs)
    for i in range(len(imgs)):
        img_res[i,:,:] = clahe.apply(np.array(imgs[i,:,:], dtype=np.uint8))
    return img_res/255

plt.hist(img_array.reshape(-1,),bins=50)    # 降成一维,分成50分

 

获取掩膜图像深度

为加快训练速度,自定义函数实现提取肝脏肿瘤对应的CT图像(处理后)和对应的掩模图,并分别保存到不同的文件夹中,分别作为模型的输入与输出。读取肿瘤CT图:

tumor_slice = [pydicom.dcmread(os.path.join(data_path_mask, file_name)) for file_name in os.listdir(data_path_mask)]
#避免CT图乱序,顺序属性
tumor_slice.sort(key=lambda x: x.InstanceNumber) 
#提取像素值
tumor_array = np.array([i.pixel_array for i in tumor_slice])
print(tumor_array.shape)    # (129, 512, 512)

白色部分为肿瘤掩模图,黑色部分对应的像素数组全为0。

在肿瘤CT图中提取肿瘤

index = [i.sum() > 0 for i in tumor_array]  # 提取含肿瘤部分
# print(len(index))   # = tumor_array.shape[0] 129
# 提取掩模图的肿瘤部分
img_tumor = tumor_array[index]
# 对增强后的CT图提取肿瘤部分
img_patient = img_clahe[index]

   

保存肿瘤数据

# 设置保存文件路径
patient_save_path = r'E:/datasets/liver/tmp/patient/'
tumor_save_path = r'E:/datasets/liver/tmp/tumor/'
for path in [patient_save_path, tumor_save_path]:
    if os.path.exists(path):  # 判断文件夹是否存在
        shutil.rmtree(path)  # 清空
    os.mkdir(path)
# 保留一个肿瘤的数据
# plt.imsave(os.path.join(patient_save_path, '0.jpg'), img_patient[0],cmap='gray')
for i in range(len(img_patient)):
    plt.imsave(os.path.join(patient_save_path, f'{i}.jpg'), img_patient[i], cmap='gray')
    plt.imsave(os.path.join(tumor_save_path, f'{i}.jpg'), img_tumor[i], cmap='gray')

 四. 数据加载

数据批处理

取3dircadb1文件夹中数据作为实验对象,取前1-10个病人的数据作为训练样本,10-20个病人的数据作为测试样本。

  • 1.读取CT图图像片段
  • 2.提取像素值
  • 3.CT图增强、均衡化
  • 4.处理肿瘤掩模图
  • 5.对每个病人的肿瘤图进行排序,提取肿瘤片段像素值
  • 6.提取肿瘤肿瘤部分像素编号
  • 7.找到CT图中对应位置
  • 8.保存所有肿瘤数据
def processImage(start, end):
    for num in range(start, end):
        print('正在处理第%d号病人' % num)
        data_path = fr'G:/dataPack/基于深度学习的肝脏肿瘤图像分割/3dircadb1/3dircadb1.{num}/PATIENT_DICOM'
        # 读取CT图图像片段
        image_slices = [pydicom.dcmread(os.path.join(data_path, file_name)) for file_name in os.listdir(data_path)]
        os.listdir(data_path)
        image_slices.sort(key=lambda x: x.InstanceNumber)  # 排序
        # 提取像素值
        image_array = np.array([i.pixel_array for i in image_slices])
        # CT图增强-windowing
        img_ct = windowing(image_array, 250, 0)
        # 直方图均衡化
        img_clahe = clahe_equalized(img_ct)
        # 肿瘤掩模图处理
        livertumor_path = fr'G:/dataPack/基于深度学习的肝脏肿瘤图像分割/3dircadb1/3dircadb1.{num}/MASKS_DICOM'
        tumor_paths = [os.path.join(livertumor_path, i) for i in os.listdir(livertumor_path) if 'livertumor' in i]
        # 重新排序
        tumor_paths.sort()
        # 提取所有肿瘤数据
        j = 0
        for tumor_path in tumor_paths:
            print("正在处理第%d个肿瘤" % j)
            tumor_slices = [pydicom.dcmread(os.path.join(tumor_path, file_name)) for file_name in
                            os.listdir(tumor_path)]
            # 重新对肿瘤片段图排序
            tumor_slices.sort(key=lambda x: x.InstanceNumber)
            # 提取像素值
            tumor_array = np.array([i.pixel_array for i in tumor_slices])
            # 没有肿瘤的掩模图全为黑色,对应像素全为0
            index = [i.sum() > 0 for i in tumor_array]  # 提取肿瘤部分编号
            img_tumor = tumor_array[index]
            # 对增强后的CT图提取肿瘤
            img_patient = img_clahe[index]
            # 保存所有肿瘤数据
            for i in range(len(img_patient)):
                plt.imsave(os.path.join(patient_save_path, f'{num}_{j}_{i}.jpg'), img_patient[i], cmap='gray')  # 保存CT图
                plt.imsave(os.path.join(tumor_save_path, f'{num}_{j}_{i}.jpg'), img_tumor[i], cmap='gray')  # 保存肿瘤掩模图
            j += 1
    return img_patient, img_tumor

 处理后保存的CT增强图像与肿瘤掩模图

数据集加载

定义Dataset数据加载器,对样本中每个图片进行进一步处理,转成np数组并转换维度方便UNet网络训练,通过Dataloader中定义batch_size设置每批数据大小为2(从计算角度来提高训练效率)

transform = transforms.Compose([
    transforms.Resize((512, 512)),
    transforms.ToTensor()
])

 首先读取本地文件,设置训练集与测试集文件路径,通过torch.squeeze(anno_tensor).type(torch.long)将肿瘤图转化为单通道数组

并对肿瘤图进行二值化处理

# 读取之前保存的处理后的病人CT图片与肿瘤图片
train_img_patient, train_img_tumor = processImage(1, 5)
test_img_patient, test_img_tumor = processImage(5, 7)
patient_images = glob.glob(r'E:\\datasets\liver\tmp\patient\*.jpg')
tumor_images = glob.glob(r'E:\\datasets\liver\tmp\tumor\*.jpg')
train_images = [p for p in patient_images if '1_' in p]
train_labels = [p for p in tumor_images if '1_' in p]
test_images = [p for p in patient_images if '2_' in p]
test_labels = [p for p in tumor_images if '2_' in p]
train_images = np.array(train_images)
train_labels = np.array(train_labels)
test_images = np.array(test_images)
test_labels = np.array(test_labels)
# img = Image.open(train_images[1])
# plt.imshow(img)
# plt.show()

class Portrait_dataset(data.Dataset):
    def __init__(self, img_paths, anno_paths):
        self.imgs = img_paths
        self.annos = anno_paths

    def __getitem__(self, index):
        img = self.imgs[index]
        anno = self.annos[index]
        pil_img = Image.open(img)
        img_tensor = transform(pil_img)
        pil_anno = Image.open(anno)
        anno_tensor = transform(pil_anno)
        # 由于蒙版图都是黑白图,会产生channel为1的维度。经过转换后,256x256x1,这个1并不是我们需要的。
        anno_tensor = torch.squeeze(anno_tensor).type(torch.long)
        anno_tensor[anno_tensor > 0] = 1    # 语义分割。二分类。
        return img_tensor, anno_tensor

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

BATCH_SIZE = 2
train_set = Portrait_dataset(train_images, train_labels)
test_set = Portrait_dataset(test_images, test_labels)
train_dataloader = data.DataLoader(train_set, batch_size=BATCH_SIZE, shuffle=True)
test_dataloader  = data.DataLoader(test_set, batch_size=BATCH_SIZE)
img_batch, anno_batch = next(iter(train_dataloader))

通过DataSet读取数据集,在通过DataLoader设置批处理图片数量=2(提高计算效率)

随机选取病人CT图和肿瘤图展示:

 五. UNet神经网络模型搭建

Unet网络结构是对称的,蓝/白色框表示 feature map;蓝色箭头表示 3x3 卷积,用于特征提取;灰色箭头表示 skip-connection,用于特征融合;红色箭头表示池化 pooling,用于降低维度;绿色箭头表示上采样 upsample,用于恢复维度;青色箭头表示 1x1 卷积,用于输出结果

class downSample(nn.Module):
    def __init__(self, in_channels, out_channels):
        super(downSample, self).__init__()
        # 两层*(卷积+激活)
        self.conv_relu = nn.Sequential(
            # padding=1,希望图像经过卷积之后大小不变
            nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
            nn.ReLU(inplace=True)
        )
        # 下采样(池化)
        self.pool = nn.MaxPool2d(kernel_size=2)

    def forward(self, x, is_pool=True):
        if is_pool:
            x = self.pool(x)
        print("downSample forward x.shape",x.shape)
        x = self.conv_relu(x)
        print("downSample forward after conv_relu(x) x.shape",x.shape)
        return x


# 上采样模型。卷积、卷积、上采样(反卷积实现上采样)
class upSample(nn.Module):
    def __init__(self, channels):
        # 两层*(卷积层+激活层)
        super(upSample, self).__init__()
        self.conv_relu = nn.Sequential(
            nn.Conv2d(2 * channels, channels, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(channels, channels, kernel_size=3, padding=1),
            nn.ReLU(inplace=True)
        )
        # 上采样激活层(ConvTransposed)将输出层的channel变成原来的一半
        self.upConv_relu = nn.Sequential(
            nn.ConvTranspose2d(channels, channels // 2,
                               kernel_size=3, stride=2,
                               padding=1, output_padding=1),
            nn.ReLU(inplace=True)
        )

    def forward(self, x):
        print("upSample - forward x.shape",x.shape)
        x = self.conv_relu(x)
        x = self.upConv_relu(x)
        return x

# 创建Unet。要初始化上、下采样层,还有其他的一些层
class Unet(nn.Module):
    def __init__(self):
        super(Unet, self).__init__()
        self.down1 = downSample(3, 64)
        self.down2 = downSample(64, 128)
        self.down3 = downSample(128, 256)
        self.down4 = downSample(256, 512)
        self.down5 = downSample(512, 1024)
        self.up = nn.Sequential(
            nn.ConvTranspose2d(1024, 512,
                               kernel_size=3,
                               stride=2,
                               padding=1,
                               output_padding=1),
            nn.ReLU(inplace=True)
        )
        self.up1 = upSample(512)
        self.up2 = upSample(256)
        self.up3 = upSample(128)
        self.conv_2 = downSample(128, 64)  # 最后两层卷积
        self.last = nn.Conv2d(64, 2, kernel_size=1)  # 输出层为2分类

    def forward(self, x):
        x1 = self.down1(x, is_pool=False)
        x2 = self.down2(x1)
        x3 = self.down3(x2)
        x4 = self.down4(x3)     # ([512, 64, 64])
        print("x4.shape",x4.shape)  # x4.shape torch.Size([512, 64, 64])
        x5 = self.down5(x4)
        print("x5.shape",x5.shape)  # x5.shape torch.Size([1024, 32, 32])
        x6 = self.up(x5)
        print("x6.shape",x6.shape)  # x6.shape torch.Size([512, 64, 64])
        # 将下采用过程x4的输出与上采样过程x5的输出做一个合并
        x6 = torch.cat([x4, x6], dim=0) # dim=0
        print("x6.shape",x6.shape)  # x6.shape torch.Size([512, 128, 64])
        x7 = self.up1(x6)

        x7 = torch.cat([x3, x7], dim=0)
        x8 = self.up2(x7)
        x8 = torch.cat([x2, x8], dim=0)
        x9 = self.up3(x8)
        x9 = torch.cat([x1, x9], dim=0)
        x10 = self.conv_2(x9, is_pool=False)
        result = self.last(x10)
        return result

神经网络中图像shape变化图:

 单张图片预测图

猜你喜欢

转载自blog.csdn.net/qq_37504771/article/details/129291247