最近一个月都在做肺结节的检测,学到了不少东西,运行的项目主要是基于这篇论文,在github上可以查到项目代码。

我个人总结的肺结节检测可以分为三个阶段,数据预处理,网络搭建及训练,结果评估。

这篇博客主要分析一下项目预处理部分的代码实现。

预处理的全部代码都在prepare.py中,对原始数据进行处理,输出预处理后的数据。

首先是主函数

def preprocess_luna():
luna_segment = config['luna_segment']#存放CT掩码的路径
savepath = config['preprocess_result_path']#存放预处理后数据的路径
luna_data = config['luna_data']#LUNA16的原始数据
luna_label = config['luna_label']#存放所有病例标签的文件
finished_flag = '.flag_preprocessluna'#是否已经预处理过的标志
print('starting preprocessing luna')
if not os.path.exists(finished_flag):
annos = np.array(pandas.read_csv(luna_label))
pool = Pool()#开启线程池
if not os.path.exists(savepath):
os.mkdir(savepath)
for setidx in xrange(10):#十份数据
print 'process subset', setidx
filelist = [f.split('.mhd')[0] for f in os.listdir(luna_data+'subset'+str(setidx)) if f.endswith('.mhd') ]#原始数据为.mhd文件,只保留文件名,去掉.mhd后缀
if not os.path.exists(savepath+'subset'+str(setidx)):
os.mkdir(savepath+'subset'+str(setidx))#为每份数据创建存放预处理结果的文件夹
partial_savenpy_luna = partial(savenpy_luna, annos=annos, filelist=filelist,#函数修饰器,将一些参数预先设定,后面调用更简洁
luna_segment=luna_segment, luna_data=luna_data+'subset'+str(setidx)+'/',
savepath=savepath+'subset'+str(setidx)+'/')
N = len(filelist)
#savenpy(1)
_=pool.map(partial_savenpy_luna,range(N))#将函数调用在序列的每个元素上,返回一个含有所有返回值的列表
pool.close()#关闭线程池
pool.join()
print('end preprocessing luna')
f= open(finished_flag,"w+")#预处理结束,写入结束标志

上面的代码就是预处理的全部代码,当然里面还调用了其它函数,主要的流程就是针对十份数据中的每一份,针对每一份中的每一个case(CT图像,以.mhd格式存储),分别处理,为加快速度,在每份数据中,针对每个文件分别开启一个线程,我试过不用线程,采取循环处理,速度确实慢了很多。

上面的代码中最关键的就是数据预处理函数savenpy_luna,在主函数中已经设定好一部分参数,如文件名列表,标签,掩码,原始数据,预处理存储路径,万事俱备,只欠实现,接下来就看一下savenpy_luna的代码。

def savenpy_luna(id, annos, filelist, luna_segment, luna_data,savepath):
islabel = True
isClean = True
resolution = np.array([1,1,1])
# resolution = np.array([2,2,2])
name = filelist[id] sliceim,origin,spacing,isflip = load_itk_image(os.path.join(luna_data,name+'.mhd'))#加载原始数据 Mask,origin,spacing,isflip = load_itk_image(os.path.join(luna_segment,name+'.mhd'))#加载相应的掩码
if isflip: #这一步没看懂
Mask = Mask[:,::-1,::-1]
newshape = np.round(np.array(Mask.shape)*spacing/resolution).astype('int')#获取mask在新分辨率下的尺寸
m1 = Mask==3 #LUNA16的掩码有两种值,3和4
m2 = Mask==4
Mask = m1+m2 #将两种掩码合并 xx,yy,zz= np.where(Mask) #确定掩码的边界
box = np.array([[np.min(xx),np.max(xx)],[np.min(yy),np.max(yy)],[np.min(zz),np.max(zz)]])
box = box*np.expand_dims(spacing,1)/np.expand_dims(resolution,1) #对边界即掩码的最小外部长方体应用新分辨率
box = np.floor(box).astype('int')
margin = 5
extendbox = np.vstack([np.max([[0,0,0],box[:,0]-margin],0),np.min([newshape,box[:,1]+2*margin],axis=0).T]).T #对box留置一定空白 this_annos = np.copy(annos[annos[:,0]==(name)]) #读取该病例对应标签 if isClean:
convex_mask = m1
dm1 = process_mask(m1) #对掩码采取膨胀操作,去除肺部黑洞
dm2 = process_mask(m2)
dilatedMask = dm1+dm2
Mask = m1+m2 extramask = dilatedMask ^ Mask
bone_thresh = 210
pad_value = 170 if isflip:
sliceim = sliceim[:,::-1,::-1]
print('flip!')
sliceim = lumTrans(sliceim) #对原始数据阈值化,并归一化
sliceim = sliceim*dilatedMask+pad_value*(1-dilatedMask).astype('uint8') #170对应归一化话后的水,掩码外的区域补充为水
bones = (sliceim*extramask)>bone_thresh #210对应归一化后的骨头,凡是大于骨头的区域都填充为水
sliceim[bones] = pad_value sliceim1,_ = resample(sliceim,spacing,resolution,order=1) #对原始数据重采样,即采用新分辨率
sliceim2 = sliceim1[extendbox[0,0]:extendbox[0,1], #将extendbox内数据取出作为最后结果
extendbox[1,0]:extendbox[1,1],
extendbox[2,0]:extendbox[2,1]]
sliceim = sliceim2[np.newaxis,...]
np.save(os.path.join(savepath, name+'_clean.npy'), sliceim)
np.save(os.path.join(savepath, name+'_spacing.npy'), spacing)
np.save(os.path.join(savepath, name+'_extendbox.npy'), extendbox)
np.save(os.path.join(savepath, name+'_origin.npy'), origin)
np.save(os.path.join(savepath, name+'_mask.npy'), Mask) if islabel:
this_annos = np.copy(annos[annos[:,0]==(name)]) #一行代表一个结节,所以一个病例可能对应多行标签
label = []
if len(this_annos)>0: for c in this_annos:
pos = worldToVoxelCoord(c[1:4][::-1],origin=origin,spacing=spacing) #将世界坐标转换为体素坐标
if isflip:
pos[1:] = Mask.shape[1:3]-pos[1:]
label.append(np.concatenate([pos,[c[4]/spacing[1]]])) label = np.array(label)
if len(label)==0:
label2 = np.array([[0,0,0,0]]) #若没有结节则设为全0
else:
label2 = np.copy(label).T
label2[:3] = label2[:3]*np.expand_dims(spacing,1)/np.expand_dims(resolution,1) #对标签应用新的分辨率
label2[3] = label2[3]*spacing[1]/resolution[1] #对直径应用新的分辨率
label2[:3] = label2[:3]-np.expand_dims(extendbox[:,0],1) #将box外的长度砍掉,也就是相对于box的坐标
label2 = label2[:4].T
np.save(os.path.join(savepath,name+'_label.npy'), label2) print(name)

这段代码属实是长,慢慢看。  

主要分为以下几步。

  1. 加载原始数据和掩码,用的是load_itk_image函数
  2. 求取掩码的边界,即非零部分的边缘,求出一个box,然后对其应用新的分辨率,也就是重采样,将分辨率统一,采用的函数是resample
  3. 将数据clip至-1200~600,此范围外的数据置为-1200或600,然后再将数据归一化至0~255,采用的是lum_trans函数
  4. 对掩码进行一下膨胀操作,去除肺部的小空洞,采用的函数是process_mask,然后对原始数据应用新掩码,并将掩码外的数据值为170(水的HU值经过归一化后的新数值)
  5. 将原始数据重采样,再截取box内的数据即可。
  6. 读取标签,将其转换为体素坐标,采用的函数是worldToVoxelCoord,再对其应用新的分辨率,最后注意,数据是box内的数据,所以坐标是相对box的坐标。
  7. 将预处理后的数据和标签以.npy格式存储

针对上面用到的各种工具函数,分别解析下。

加载图像:主要用到sitk模块,读取原始数据的numpy表示,以及origin和space,origin就是真实坐标的原点,space就是分辨率

def load_itk_image(filename):
with open(filename) as f:
contents = f.readlines()
line = [k for k in contents if k.startswith('TransformMatrix')][0]
transformM = np.array(line.split(' = ')[1].split(' ')).astype('float')
transformM = np.round(transformM)
if np.any( transformM!=np.array([1,0,0, 0, 1, 0, 0, 0, 1])):
isflip = True
else:
isflip = False itkimage = sitk.ReadImage(filename)
numpyImage = sitk.GetArrayFromImage(itkimage) numpyOrigin = np.array(list(reversed(itkimage.GetOrigin())))
numpySpacing = np.array(list(reversed(itkimage.GetSpacing()))) return numpyImage, numpyOrigin, numpySpacing,isflip

重采样:原始CT分辨率往往不一致,为便于应用网络,需要统一分辨率

def resample(imgs, spacing, new_spacing,order=2):
if len(imgs.shape)==3:
new_shape = np.round(imgs.shape * spacing / new_spacing)
true_spacing = spacing * imgs.shape / new_shape
resize_factor = new_shape / imgs.shape
imgs = zoom(imgs, resize_factor, mode = 'nearest',order=order)
return imgs, true_spacing
elif len(imgs.shape)==4:
n = imgs.shape[-1]
newimg = []
for i in range(n):
slice = imgs[:,:,:,i]
newslice,true_spacing = resample(slice,spacing,new_spacing)
newimg.append(newslice)
newimg=np.transpose(np.array(newimg),[1,2,3,0])
return newimg,true_spacing
else:
raise ValueError('wrong shape')

坐标转换:给定的标签是世界坐标,单位是mm,需要转换为体素坐标,也就是在像素体内的坐标

def worldToVoxelCoord(worldCoord, origin, spacing):

    stretchedVoxelCoord = np.absolute(worldCoord - origin)
voxelCoord = stretchedVoxelCoord / spacing
return voxelCoord

掩码处理:这里对掩码进行膨胀处理

def process_mask(mask):
convex_mask = np.copy(mask)
for i_layer in range(convex_mask.shape[0]):
mask1 = np.ascontiguousarray(mask[i_layer])
if np.sum(mask1)>0:
mask2 = convex_hull_image(mask1)
if np.sum(mask2)>1.5*np.sum(mask1):
mask2 = mask1
else:
mask2 = mask1
convex_mask[i_layer] = mask2
struct = generate_binary_structure(3,1)
dilatedMask = binary_dilation(convex_mask,structure=struct,iterations=10)
return dilatedMask

归一化:将数据归一化至0~255

def lumTrans(img):
lungwin = np.array([-1200.,600.])
newimg = (img-lungwin[0])/(lungwin[1]-lungwin[0])
newimg[newimg<0]=0
newimg[newimg>1]=1
newimg = (newimg*255).astype('uint8')
return newimg

  

  

  

  

  

项目笔记《DeepLung:Deep 3D Dual Path Nets for Automated Pulmonary Nodule Detection and Classification》(一)预处理的更多相关文章

  1. 项目笔记《DeepLung:Deep 3D Dual Path Nets for Automated Pulmonary Nodule Detection and Classification》(二)(上)模型设计

    我只讲讲检测部分的模型,后面两样性分类的试验我没有做,这篇论文采用了很多肺结节检测论文都采用的u-net结构,准确地说是具有DPN结构的3D版本的u-net,直接上图. DPN是颜水成老师团队的成果, ...

  2. 项目笔记《DeepLung:Deep 3D Dual Path Nets for Automated Pulmonary Nodule Detection and Classification》(三)(下)结果评估

    在(上)中讲了如何得到csv文件并调用noduleCADEvaluationLUNA16.py求取froc值,这里就讲一讲froc值是如何求取的. annotations_filename = './ ...

  3. 项目笔记《DeepLung:Deep 3D Dual Path Nets for Automated Pulmonary Nodule Detection and Classification》(三)(上)结果评估

    在(一)中,我将肺结节检测项目总结为三阶段,这里我要讲讲这个项目的第三阶段,至于第二阶段,由于数据增强部分的代码我始终看不大懂,先不讲. 结果评估的程序在evaluationScript文件夹下,这个 ...

  4. Django商城项目笔记No.2项目准备工作

    Django商城项目笔记No.2项目准备工作 接着上篇开始,创建好工程之后,随之而来的是怎么配置工程,这篇文章记录如何进行相关的配置 1.pycharm打开工程,进行相关的配置 通过pycharm打开 ...

  5. Django项目笔记:sessions处理以及复杂对象序列化

    前言:一点题外话 我发现,不更新博客的时候,不是非常忙,就是效率非常低.最近没怎么更新博客,原因是第二种= =.惭愧惭愧. 今天效率出奇的高,一天时间把PassNote后端的接口全部写完了,Djang ...

  6. Dual Path Networks(DPN)——一种结合了ResNet和DenseNet优势的新型卷积网络结构。深度残差网络通过残差旁支通路再利用特征,但残差通道不善于探索新特征。密集连接网络通过密集连接通路探索新特征,但有高冗余度。

    如何评价Dual Path Networks(DPN)? 论文链接:https://arxiv.org/pdf/1707.01629v1.pdf在ImagNet-1k数据集上,浅DPN超过了最好的Re ...

  7. 《BI项目笔记》——微软BI项目笔记连载

    本系列文章主要是结合实际项目,加上自己的总结,整理出来的一系列项目笔记,涉及微软SQL Server2008中商务智能开发中的SSAS.SSIS模块:  准备工作: <BI项目笔记>基于雪 ...

  8. A Statistical View of Deep Learning (IV): Recurrent Nets and Dynamical Systems

    A Statistical View of Deep Learning (IV): Recurrent Nets and Dynamical Systems Recurrent neural netw ...

  9. 项目笔记---CSharp图片处理

    原文:项目笔记---CSharp图片处理 项目笔记---CSharp图片处理 最近由于项目上需要对图片进行二值化处理,就学习了相关的图片处理上的知识,从开始的二值化的意义到动态阀值检测二值化等等,并用 ...

随机推荐

  1. Markdown编辑器及图床推荐

    Typora和自动图床工具 Typora 地址 ,极致简洁,界面很漂亮,最重要的是所见即所得 百度云搬运 密码:xi01 自动图床工具 需要七牛云做图床,感谢作者,详见博客 使用方法,只需两步即可完成 ...

  2. 分步编译一个C语言文件

    一. 以下是C程序一般的编译过程: 从图中看到: 将编写的一个c程序(源代码 )转换成可以在硬件上运行的程序(可执行代码 ),需要进行编译阶段  和链接这两个阶段. 其中, 1.  编译阶段先通过“编 ...

  3. php命令行操作

    1.php -v返回版本 -i选项返回安装的有关信息 -h访问帮助文件 -m列出编译到当前PHP安装的全部模块   CLI与CGI区别: 在命令行脚本环境中,有两种版本的PHP可以使用,旧版本是CGI ...

  4. python NLTK 环境搭建

    这里是我之前亲自操作过安装nltk,安装成功了.当时记得是参考这篇博文:http://www.tuicool.com/articles/VFf6Bza 其中,nltk安装时,遇到模块未找到,依次根据提 ...

  5. 全文检索技术---Lucene

    1       Lucene介绍 1.1   什么是Lucene Lucene是apache下的一个开源的全文检索引擎工具包.它为软件开发人员提供一个简单易用的工具包(类库),以方便的在目标系统中实现 ...

  6. HDU 4348(主席树 标记永久化)

    题面一看就是裸的数据结构题,而且一看就知道是主席树... 一共四种操作:1:把区间[l, r]的数都加上d,并且更新时间.2:查询当前时间的区间和.3:查询历史时间的区间和.4:时光倒流到某个时间. ...

  7. python3-字典的循环

    # Auther: Aaron Fan info = { 'stu1102': 'LongZe Luola', 'stu1103': 'XiaoZe Maliya', 'stu1106': 'Alex ...

  8. 前端基础 之 CSS

    浏览目录 CSS介绍 CSS语法 CSS的几种引入方式 CSS选择器 CSS属性相关 一.CSS介绍 CSS(Cascading Style Sheet,层叠样式表)定义如何显示HTML元素. 当浏览 ...

  9. Xamarin Mono for VS开发窗体标题(Title)乱码解决方案

    利用mono for VS开发一个手机程序,结果只有窗体的标题 title部分是乱码,其他所有地方中文都显示正常,很郁闷.百度很久无果.最后发现只要在 VS菜单中 的 文件->高级保存选型中奖编 ...

  10. 有趣的setTimeout

    今天在回顾JavaScript进阶用法的时候,发现一个有趣的问题,话不多说,先上代码: for(var j=0;j<10;j++){ setTimeout(function(){console. ...