目录
二、创建数据预处理文件prepare_dataset.py:
前言
上一篇《基于图像分割的LIDC-IDRI 数据集预处理之数据下载篇》介绍了关于lidc的数据下载,本篇将针对数据集进行具体的处理,包括肺实质分割,结节标注,数据集划分等,针对数据集预处理所用到的有关技术参考了博主小河梦《深度学习】肺结节分割项目实战一:处理数据集》,本篇现已经成功运行并运用,并针对运行过程出现的问题与诸君共享。
一、创建参数配置文件lung.conf
我们后续所涉及到的数据集和分割网络是一个较为庞大的项目,故此需要设置一个配置文件来管理相关路径,使得项目整体控制便捷灵活。而数据集是由肺图像数据库协会的图像收集(LIDC-IDRI)包括诊断和带有病变注释标记的肺癌筛查胸部CT。数据集中包含1018个病例,每个病例包括来自临床胸部CT扫描的图像和一个相关的XML文件,该文件记录了由四名经验丰富的胸部放射科医生进行的两阶段图像注释过程的结果。
在预处理图像过程中,我们不需要自己写程序去解析所有的文件。已经有人早就写好了免费的库文件去解析数据集。即为python中的pylidc库,所以我们不用额外调用xml文件进行结节解析,pylidc库的安装命令:
pip install pylidc
病例中有的存在结节注释,有的不含结节注释,故此没有的保存在Clean文件夹(可用于后续模型的泛化能力验证),而含有结节的保存在data目录下的对应文件夹。首先先运行以下代码,会自动生成一个lung.conf文件。
from configparser import ConfigParser # 导入ConfigParser模块,用于读取和写入配置文件
if __name__ == "__main__": # 当此脚本作为主程序运行时,执行以下代码块
# 这个python文件用于创建一个配置文件。请根据您的应用程序更改下面的目录
config = ConfigParser() # 创建一个ConfigParser对象,用于处理配置文件
# prepare_dataset.py的配置
config['prepare_dataset'] = { # 创建一个名为'prepare_dataset'的section(部分)
# LIDC数据集的路径
'LIDC_DICOM_PATH': './LIDC-IDRI', # 指定LIDC数据集的DICOM文件路径
# 保存输出文件的目录
# 掩码的目录
'MASK_PATH': './data/Mask', # 指定保存掩码文件的目录
# 图像的目录
'IMAGE_PATH': './data/Image', # 指定保存图像文件的目录
# 保存不包含任何结节或癌症的图像和掩码
# 这些图像将在稍后用于评估我们的模型
'CLEAN_PATH_IMAGE': './data/Clean/Image', # 指定保存干净图像(无结节)的目录
'CLEAN_PATH_MASK': './data/Clean/Mask', # 指定保存干净掩码(无结节)的目录
# 包含结节信息、恶性程度、训练测试分割的CSV文件
'META_PATH': './data/Meta/', # 指定保存元数据(如CSV文件)的目录
# 掩码阈值是np.sum(MASK)的阈值。一些掩码太小,我们去除这些小的图像和掩码,因为它们可能会作为异常值
# 阈值8是通过经验评估决定的
'Mask_Threshold': 8 # 设置掩码阈值
}
# 这是pylidc库的配置文件
config['pylidc'] = { # 创建一个名为'pylidc'的section
# 置信度级别决定了四位医生之间注释的重叠程度
'confidence_level': 0.5, # 设置置信度级别
# 512决定了图像的大小
'padding_size': 512 # 设置图像填充大小
}
# 创建名为lung.conf的配置文件
with open('./lung.conf', 'w') as f: # 使用'w'模式打开文件,如果文件已存在则会被覆盖
config.write(f) # 将配置文件的内容写入到文件中
lung.conf文件的详细内容如下:
[prepare_dataset]
lidc_dicom_path = ./LIDC-IDRI #数据集路径
mask_path = ./data/Mask #经过预处理之后的掩膜路径(标签)
image_path = ./data/Image #经过预处理之后的含结节图像路径
clean_path_image = ./data/Clean/Image #经过预处理之后的不含结节的图像,即干净图像路径
clean_path_mask = ./data/Clean/Mask #经过预处理之后的不含结节的图像,即干净图像掩膜(标签)
meta_path = ./data/Meta/ #经过预处理之后图像元数据信息
mask_threshold = 8 #经验分割阈值
[pylidc]
confidence_level = 0.5 #置信度水平
padding_size = 512 #输入模型的图像大小
二、创建数据预处理文件prepare_dataset.py:
通过自定义MakeDataSet类的prepare_dataset方法创建数据集和标注信息文件,MakeDataSet实例的属性包括:
LIRI_list:遍历保存LIDC_IDRI数据的文件夹,得到包含所有病例文件夹名字的列表(按照上一篇博客成功下载数据集之后,将对应的数据集放置于相应的文件夹中即可)。LIDC_IDRI目录应包含的文件结构如下所示:
+-- LIDC-IDRI
| |-- LIDC-IDRI-0001
| |-- LIDC-IDRI-0002
...
img_path,mask_path:包含结节的肺部图像和掩码的保存路径
clean_path_img,clean_path_mask:不包含结节的肺部图像和掩码的保存路径
meta_path:结节元信息保存路径
meta:结节信息属性名
import sys
import os
from pathlib import Path
import glob
from configparser import ConfigParser
import pandas as pd
import numpy as np
import warnings
import pylidc as pl
from tqdm import tqdm
from statistics import median_high
from utils import is_dir_path,segment_lung
from pylidc.utils import consensus
from PIL import Image
warnings.filterwarnings(action='ignore')
# Read the configuration file generated from config_file_create.py
parser = ConfigParser()
parser.read('lung.conf')
#Get Directory setting
DICOM_DIR = is_dir_path(parser.get('prepare_dataset','LIDC_DICOM_PATH'))
MASK_DIR = is_dir_path(parser.get('prepare_dataset','MASK_PATH'))
IMAGE_DIR = is_dir_path(parser.get('prepare_dataset','IMAGE_PATH'))
CLEAN_DIR_IMAGE = is_dir_path(parser.get('prepare_dataset','CLEAN_PATH_IMAGE'))
CLEAN_DIR_MASK = is_dir_path(parser.get('prepare_dataset','CLEAN_PATH_MASK'))
META_DIR = is_dir_path(parser.get('prepare_dataset','META_PATH'))
#Hyper Parameter setting for prepare dataset function
mask_threshold = parser.getint('prepare_dataset','Mask_Threshold')
#Hyper Parameter setting for pylidc
confidence_level = parser.getfloat('pylidc','confidence_level')
padding = parser.getint('pylidc','padding_size')
class MakeDataSet:
def __init__(self, LIDC_Patients_list, IMAGE_DIR, MASK_DIR,CLEAN_DIR_IMAGE,CLEAN_DIR_MASK,META_DIR, mask_threshold, padding, confidence_level=0.5):
self.IDRI_list = LIDC_Patients_list
self.img_path = IMAGE_DIR
self.mask_path = MASK_DIR
self.clean_path_img = CLEAN_DIR_IMAGE
self.clean_path_mask = CLEAN_DIR_MASK
self.meta_path = META_DIR
self.mask_threshold = mask_threshold
self.c_level = confidence_level
self.padding = [(padding,padding),(padding,padding),(0,0)]
self.meta = pd.DataFrame(index=[],columns=['patient_id','nodule_no','slice_no','original_image','mask_image','malignancy','is_cancer','is_clean'])
def calculate_malignancy(self,nodule):
# Calculate the malignancy of a nodule with the annotations made by 4 doctors. Return median high of the annotated cancer, True or False label for cancer
# if median high is above 3, we return a label True for cancer
# if it is below 3, we return a label False for non-cancer
# if it is 3, we return ambiguous
list_of_malignancy =[]
for annotation in nodule:
list_of_malignancy.append(annotation.malignancy)
malignancy = median_high(list_of_malignancy)
if malignancy > 3:
return malignancy,True
elif malignancy < 3:
return malignancy, False
else:
return malignancy, 'Ambiguous'
def save_meta(self,meta_list):
"""Saves the information of nodule to csv file"""
tmp = pd.Series(meta_list,index=['patient_id','nodule_no','slice_no','original_image','mask_image','malignancy','is_cancer','is_clean'])
self.meta = self.meta.append(tmp,ignore_index=True)
def prepare_dataset(self):
# This is to name each image and mask
prefix = [str(x).zfill(3) for x in range(1000)]
# Make directory
if not os.path.exists(self.img_path):
os.makedirs(self.img_path)
if not os.path.exists(self.mask_path):
os.makedirs(self.mask_path)
if not os.path.exists(self.clean_path_img):
os.makedirs(self.clean_path_img)
if not os.path.exists(self.clean_path_mask):
os.makedirs(self.clean_path_mask)
if not os.path.exists(self.meta_path):
os.makedirs(self.meta_path)
IMAGE_DIR = Path(self.img_path)
MASK_DIR = Path(self.mask_path)
CLEAN_DIR_IMAGE = Path(self.clean_path_img)
CLEAN_DIR_MASK = Path(self.clean_path_mask)
for patient in tqdm(self.IDRI_list):
pid = patient #LIDC-IDRI-0001~
scan = pl.query(pl.Scan).filter(pl.Scan.patient_id == pid).first()
nodules_annotation = scan.cluster_annotations()
vol = scan.to_volume()
print("Patient ID: {} Dicom Shape: {} Number of Annotated Nodules: {}".format(pid,vol.shape,len(nodules_annotation)))
patient_image_dir = IMAGE_DIR / pid
patient_mask_dir = MASK_DIR / pid
Path(patient_image_dir).mkdir(parents=True, exist_ok=True)
Path(patient_mask_dir).mkdir(parents=True, exist_ok=True)
if len(nodules_annotation) > 0:
# Patients with nodules
for nodule_idx, nodule in enumerate(nodules_annotation):
# Call nodule images. Each Patient will have at maximum 4 annotations as there are only 4 doctors
# This current for loop iterates over total number of nodules in a single patient
mask, cbbox, masks = consensus(nodule,self.c_level,self.padding)
lung_np_array = vol[cbbox]
# We calculate the malignancy information
malignancy, cancer_label = self.calculate_malignancy(nodule)
for nodule_slice in range(mask.shape[2]):
# This second for loop iterates over each single nodule.
# There are some mask sizes that are too small. These may hinder training.
if np.sum(mask[:,:,nodule_slice]) <= self.mask_threshold:
continue
# Segment Lung part only
lung_segmented_np_array = segment_lung(lung_np_array[:,:,nodule_slice])
# I am not sure why but some values are stored as -0. <- this may result in datatype error in pytorch training # Not sure
lung_segmented_np_array[lung_segmented_np_array==-0] =0
# This itereates through the slices of a single nodule
# Naming of each file: NI= Nodule Image, MA= Mask Original
nodule_name = "{}_NI{}_slice{}".format(pid[-4:],prefix[nodule_idx],prefix[nodule_slice])
mask_name = "{}_MA{}_slice{}".format(pid[-4:],prefix[nodule_idx],prefix[nodule_slice])
meta_list = [pid[-4:],nodule_idx,prefix[nodule_slice],nodule_name,mask_name,malignancy,cancer_label,False]
self.save_meta(meta_list)
np.save(patient_image_dir / nodule_name,lung_segmented_np_array)
np.save(patient_mask_dir / mask_name,mask[:,:,nodule_slice])
else:
print("Clean Dataset",pid)
patient_clean_dir_image = CLEAN_DIR_IMAGE / pid
patient_clean_dir_mask = CLEAN_DIR_MASK / pid
Path(patient_clean_dir_image).mkdir(parents=True, exist_ok=True)
Path(patient_clean_dir_mask).mkdir(parents=True, exist_ok=True)
#There are patients that don't have nodule at all. Meaning, its a clean dataset. We need to use this for validation
for slice in range(vol.shape[2]):
if slice >50:
break
lung_segmented_np_array = segment_lung(vol[:,:,slice])
lung_segmented_np_array[lung_segmented_np_array==-0] =0
lung_mask = np.zeros_like(lung_segmented_np_array)
#CN= CleanNodule, CM = CleanMask
nodule_name = "{}/{}_CN001_slice{}".format(pid,pid[-4:],prefix[slice])
mask_name = "{}/{}_CM001_slice{}".format(pid,pid[-4:],prefix[slice])
meta_list = [pid[-4:],slice,prefix[slice],nodule_name,mask_name,0,False,True]
self.save_meta(meta_list)
np.save(patient_clean_dir_image / nodule_name, lung_segmented_np_array)
np.save(patient_clean_dir_mask / mask_name, lung_mask)
print("Saved Meta data")
self.meta.to_csv(self.meta_path+'meta_info.csv',index=False)
if __name__ == '__main__':
# I found out that simply using os.listdir() includes the gitignore file
LIDC_IDRI_list= [f for f in os.listdir(DICOM_DIR) if not f.startswith('.')]
LIDC_IDRI_list.sort()
test= MakeDataSet(LIDC_IDRI_list,IMAGE_DIR,MASK_DIR,CLEAN_DIR_IMAGE,CLEAN_DIR_MASK,META_DIR,mask_threshold,padding,confidence_level)
test.prepare_dataset()
运行上述代码之后,用于分割的数据预处理便完成。 整个工程文件目录如下:
+-- LIDC-IDRI
| # This file should contain the original LIDC dataset
+-- data
| # This file contains the preprocessed data
| |-- _Clean
| +-- Image
| +-- Mask
| |-- Image
| +-- LIDC-IDRI-0001
| +-- LIDC-IDRI-0002
| +-- ...
| |-- Mask
| +-- LIDC-IDRI-0001
| +-- LIDC-IDRI-0002
| +-- ...
| |-- Meta
| +-- meta.csv
+-- figures
| # Save figures here
+-- notebook
| # This notebook file edits the meta.csv file to make indexing easier
+-- config_file_create.py
| # Creates configuration file. You can edit the hyperparameters of the Pylidc library here
+-- prepare_dataset.py
| # Run this file to preprocess the LIDC-IDRI dicom files. Results would be saved in the data folder
+-- utils.py
# Utility script
总结
在这里针对文章《深度学习】肺结节分割项目实战一:处理数据集》中部分浏览者所出现的问题进行解答。
问题1:IndexError: only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and integer or boolean arrays are valid indices
解答:可以更新下一下自己的python版本,将python的版本降到3.6应该就可以正常运行。
问题2:Could not establish path to dicom files. Have you specified the `path` option in the configuration file C:\Users\yifuh\pylidc.conf?
解答:根据问题中的提示“C:\Users\yifuh\pylidc.conf”找到自己电脑上面的pylidc.conf文件,将pylidc.config的设置成:
[dicom]
path = ./LIDC-IDRI 注意这里的path应该尽量设置成上述配置文件中的[prepare_dataset]的路径:lidc_dicom_path = ./LIDC-IDRI 这样才不会容易出错。
问题3:E:\ANACONDA\envs\pytorch\python.exe E:\lidc-idri-preprocessing-master\prepare_dataset.py
0%| | 0/2 [00:00<?, ?it/s]
Traceback (most recent call last):
File "E:\lidc-idri-preprocessing-master\prepare_dataset.py", line 177, in <module>
test.prepare_dataset()
File "E:\lidc-idri-preprocessing-master\prepare_dataset.py", line 103, in prepare_dataset
nodules_annotation = scan.cluster_annotations()
AttributeError: 'NoneType' object has no attribute 'cluster_annotations'
解答:问题原因是因为导入的dicom文件夹中并不存在dicom文件,所以导致空类型并不存在结节信息。所以问题根源是没有成功将下载的数据置于LIDC-IDRI 文件夹中。需要按照上述所说的那样,按照LIDC-IDRI 文件结构放置对应的病人文件。
......(后续问题待更新)