实战:使用yolov3完成肺结节检测(Luna16数据集)

yolov3是一个比较常用的端到端的目标检测深度学习模型,这里加以应用,实现肺结节检测。由于Luna16数据集是三维的,需要对其进行切片操作,转换成yolov3可以处理的二维图片。

1. yolov3代码及原理
* 代码
* 原理
2. Luna16数据集
* 数据集介绍
* 转换成voc数据集格式
3. 检测效果预览

更新:2019.9.10

对数据集进行肺实质分割。

更新:2019.9.14

使用分割后的数据进行训练

更新:2019.9.23

修改anchor size后重新训练

更新:2019.12.5

发现一个注释的错误:

 a = image_array.transpose(1,2,0)[:,:,20] #transpose是将(z,x,y)的三维矩阵转为(x,y,z)的矩阵

应该是transpose是将(z,y,x)的三维矩阵转为(y,x,z)的矩阵

更新:2019.12.11

发现应该坐标换算错误:

        #如果图像进行旋转了,那么坐标也要旋转if isflip :            x_max = 512 - x_min#修改前是 x_min = 512 - x_min 以下类似y_max = 512 - y_minx_min = 512 - x_maxy_min = 512 - y_maxx = 512 - xy = 512 - y

补充 2020.11.19

博主临近毕业,可能有消息无法及时回复,见谅,有问题请留言。

有很多csdner向我要数据集和代码,关于代码核心部分已经在博客中讲解(已上传csdn,存在些许bug),如果要提高自己能力,建议看懂,自己上手。如果是想要快速上手,测试相关数据集,我已经上传百度云VOC数据集,密码:zc0p。

赠人玫瑰,手留余香,如果我的博文/数据集/代码对你有所帮助,麻烦点个赞~~

补充:关于为什么分享数据集size不是512*512的,这里我对原始CT是进行了归一化的,即1mm*1mm*1mm,所以尺寸不一样。

1. yolov3代码及原理

1.1代码

基于c的:
https://github.com/AlexeyAB/darknet
基于pytorch的:
https://github.com/eriklindernoren/PyTorch-YOLOv3

1.2原理

可查看:
Pytorch | yolov3原理及代码详解(一)
Pytorch | yolov3原理及代码详解(一)_NotFound1911的博客-CSDN博客_pytorch和yolo的关系
Pytorch | yolov3原理及代码详解(二)
Pytorch | yolov3原理及代码详解(二)_NotFound1911的博客-CSDN博客_yolov3pytorch代码详解
Pytorch | yolov3原理及代码详解(三)
Pytorch | yolov3原理及代码详解(三)_NotFound1911的博客-CSDN博客_yolov3pytorch代码详解
Pytorch | yolov3原理及代码详解(四)

Pytorch | yolov3原理及代码详解(四)_NotFound1911的博客-CSDN博客

2.Luna16数据集

2.1 Luna16数据集介绍

LUNA16数据集介绍_pursuit_zhangyu的博客-CSDN博客_luna16
Luna16数据集_一一|一一一亅的博客-CSDN博客_luna16数据集
上面说的很全,这里只进行补充:

2.1.1.图像的翻转

.mhd文件中有CT图像的属性,其中有一条是:

TransformMatrix = 1 0 0 0 1 0 0 0 1
  • TransformMatric:图像矩阵是否翻转的标志。(在现实中CT扫描的时候有的人是正卧,有的人是仰卧的,所以会导致图像会出现翻转的情况。)
  • TransformMatrix = 1 0 0 0 1 0 0 0 1是正卧的,这里举一个反卧的例子:TransformMatrix = -1 0 0 0 -1 0 0 0 1。当图像反卧的时候我们把图像的x,y坐标进行倒序调整,使其正卧。如下面代码所示(加载CT图像):
def load_itk_image(filename):with open(filename) as f:contents = f.readlines()line = [k for k in contents if k.startswith('TransformMatrix')][0]offset = [k for k in contents if k.startswith('Offset')][0]EleSpacing = [k for k in contents if k.startswith('ElementSpacing')][0]# 把值进行分割offArr = np.array(offset.split(' = ')[1].split(' ')).astype('float')eleArr = np.array(EleSpacing.split(' = ')[1].split(' ')).astype('float')CT = CTImage(offArr[0],offArr[1],offArr[2],eleArr[0],eleArr[1],eleArr[2])transform = np.array(line.split(' = ')[1].split(' ')).astype('float')transform = np.round(transform)#round() 方法返回浮点数x的四舍五入值if np.any(transform != np.array([1, 0, 0, 0, 1, 0, 0, 0, 1])):#判断是否相等isflip = Trueelse:isflip = Falseitkimage = sitk.ReadImage(filename)numpyimage = sitk.GetArrayFromImage(itkimage)if(isflip == True):numpyimage = numpyimage[:,::-1,::-1] #::-1 倒序return (numpyimage, CT, isflip)

因此,如果图像要翻转,那么坐标也要进行处理(512是CT的宽和高)(2019.12.11进行修正)(2019.12.25进行修正):

        #如果图像进行旋转了,那么坐标也要旋转if isflip :            x_max1 = 512 - x_miny_max1 = 512 - y_minx_min1 = 512 - x_maxy_min1 = 512 - y_maxx_max = x_max1 y_max = y_max1 x_min = x_min1 y_min = y_min1 x = 512 - xy = 512 - y

2.1.2图像的预处理

正如上面博客所说的,CT图像的像素值是 [-1000,+ 400],所以我们进行了预处理,对像素值进行截断处理。同时由于CT图像是单通道图片,如果直接截断保存单张图片,是不行的,必须要先转换成RGB格式,在进行保存,否则保存下来就是一张全黑的图片。

def truncate_hu(image_array):image_array[image_array > 400] = 400image_array[image_array <-1000] = -1000def normalazation(image_array):max = image_array.max()min = image_array.min()#归一化image_array = (image_array - min)/(max - min)*255#image_array = image_array.astype(int)#整型image_array = np.round(image_array)return image_array  
    #这个图的x,y坐标是反的,在生成标注的时候需要注意。case_path = 'data/subset3/1.3.6.1.4.1.14519.5.2.1.6279.6001.123697637451437522065941162930.mhd' numpyimage, _, _ = load_itk_image(case_path)truncate_hu(numpyimage)image_array = normalazation(numpyimage)#3维图像切割a = image_array.transpose(1,2,0)[:,:,20] #transpose是将(z,y,x)的三维矩阵转为(y,x,z)的矩阵#转换成RGB类型并保存im = Image.fromarray(a)im = im.convert("RGB")    im.save("error/test1.png")

2.2 Luna16数据处理成VOC格式

2.2.1 遍历Luna16的标注文件

Luna16的所有标注均在annotations.csv文件中。这个标注的含义上述博客有讲解,这里遍历标注文件并转换成相应的x,y,w,h标注时需要注意两点:

1.同一个CT图可能具有多个肺结节。

所以在遍历文件的时候,一定要把一个CT图中所有的肺结节遍历完,才能遍历下一站CT图。这里提供一个遍历文件的代码:

#查询文件 返回路径   -1表示无
def search(path=".", name="",fileDir = []):for item in os.listdir(path):item_path = os.path.join(path, item)if os.path.isdir(item_path):search(item_path, name)elif os.path.isfile(item_path):            if name in item:fileDir.append(item_path)#print("fileDir:",fileDir)return fileDir

 2.坐标换算

上述博客中也提到的了,要完成世界坐标到图像坐标的换算。

#世界坐标转换到图像中的坐标
def worldToVoxelCoord(worldCoord, offset, EleSpacing):stretchedVoxelCoord = np.absolute(worldCoord - offset)voxelCoord = stretchedVoxelCoord / EleSpacingreturn voxelCoord

肺结节的x,y,z的换算不难理解,补充的是w和h的换算。w是在x轴方向上,h是在y轴方向上,换算方式类似,但注意w和h是长度单位,所有不会有偏置(坐标原点)。

        w = np.round(r/CT.x_ElementSpacing).astype(int)h = np.round(r/CT.y_ElementSpacing).astype(int)

这里的CT是我建的一个类,目的是方便CT各项数据的传递。

#定义CT图像类保存 中心点
class CTImage(object):"""docstring for Hotel"""def __init__(self, x_offset, y_offset, z_offset, x_ElementSpacing, y_ElementSpacing, z_ElementSpacing):self.x_offset = x_offsetself.y_offset = y_offsetself.z_offset = z_offsetself.x_ElementSpacing = x_ElementSpacingself.y_ElementSpacing = y_ElementSpacingself.z_ElementSpacing = z_ElementSpacing

2.2.2 生成.xml文件

VOC数据集格式介绍:

Pascal Voc数据集详细分析_持久决心的博客-CSDN博客_pascal voc

VOC具有很多属性,但是我们这里在做肺结节检测的时候,有些属性是不需要的,我们需要的属性只有物体的名词以及坐标等。下面就是生成的一个.xml标签。

<annotation><folder>VOC</folder><filename>0003.png</filename><size><width>512</width><height>512</height><depth>3</depth></size><object><name>nodule</name><bndbox><xmin>381</xmin><ymin>266</ymin><xmax>389</xmax><ymax>389</ymax></bndbox></object>
</annotation>

生成.xml的函数为(Luna16的CT图切割之后都是3*512*512的):

def beatau(e,level=0):if len(e)>0:e.text='\n'+'\t'*(level+1)for child in e:beatau(child,level+1)child.tail=child.tail[:-1]e.tail='\n' + '\t'*leveldef ToXml(name, x, y, w, h):root = Element('annotation')#根节点erow1 = Element('folder')#节点1erow1.text= "VOC"erow2 = Element('filename')#节点2erow2.text= str(name)erow3 = Element('size')#节点3erow31 = Element('width')erow31.text = "512"erow32 = Element('height')erow32.text = "512"erow33 = Element('depth')erow33.text = "3" erow3.append(erow31)erow3.append(erow32)erow3.append(erow33)erow4 = Element('object')erow41 = Element('name')erow41.text = 'nodule'erow42 = Element('bndbox')erow4.append(erow41)erow4.append(erow42)erow421 = Element('xmin')erow421.text = str(x - np.round(w/2).astype(int))erow422 = Element('ymin')erow422.text = str(y - np.round(h/2).astype(int))erow423 = Element('xmax')erow423.text = str(x + np.round(w/2).astype(int))erow424 = Element('ymax')erow424.text = str(y + np.round(h/2).astype(int))erow42.append(erow421)erow42.append(erow422)erow42.append(erow423)erow42.append(erow424)root.append(erow1)root.append(erow2)root.append(erow3)            root.append(erow4)  beatau(root)      return ElementTree(root)

2.2.3 生成训练集、测试集、验证集

Luna16一共有1186个肺结节,我使用其中70%为训练集,15%为测试集,15%为验证。首先要生成train.txt, test.txt, val.txt。

import random
import numpy as npgetTrain = []
getTest = []
getVal = []arrTrain = []
arrTest = []
arrVal = []arr = np.arange(1,1186)
#训练集
num = 0
for i in np.random.permutation(arr):if num >829:breakarrTrain.append(i)num  = num + 1
getTrain=np.array(arrTrain)
getTrain.sort()f=open('train.txt','w')
for i in getTrain:f.write('{:04d}\n'.format(i))
f.close()
#测试集
num = 0
for i in np.random.permutation(arr):if num >178:breakarrTest.append(i)num  = num + 1
getTest=np.array(arrTest)
getTest.sort()f=open('test.txt','w')
for i in getTest:f.write('{:04d}\n'.format(i))
f.close()
#验证集
num = 0
for i in np.random.permutation(arr):if num >178:breakarrVal.append(i)num  = num + 1
getVal=np.array(arrVal)
getVal.sort()f=open('val.txt','w')
for i in getVal:f.write('{:04d}\n'.format(i))
f.close()

生成之后.txt就有对应的图片ID。

下一步就有yolov3自带的voc_label.py进行解析,生成相应的label。为了使用,下列代码的difficult默认为0了。

import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import joinsets=[('', 'train'), ('', 'val'), ('', 'test')]classes = ["nodule"]def convert(size, box):dw = 1./(size[0])dh = 1./(size[1])x = (box[0] + box[1])/2.0 - 1y = (box[2] + box[3])/2.0 - 1w = box[1] - box[0]h = box[3] - box[2]x = x*dww = w*dwy = y*dhh = h*dhreturn (x,y,w,h)def convert_annotation(year, image_id):in_file = open('E:/LUNA/VOCdevkit/VOC%s/Annotations/%s.xml'%(year, image_id))out_file = open('E:/LUNA/VOCdevkit/VOC%s/labels/%s.txt'%(year, image_id), 'w')tree=ET.parse(in_file)root = tree.getroot()size = root.find('size')w = int(size.find('width').text)h = int(size.find('height').text)for obj in root.iter('object'):difficult = 0cls = obj.find('name').textif cls not in classes or int(difficult)==1:continuecls_id = classes.index(cls)xmlbox = obj.find('bndbox')b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text), float(xmlbox.find('ymax').text))bb = convert((w,h), b)out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')wd = getcwd()for year, image_set in sets:if not os.path.exists('VOCdevkit/VOC%s/labels/'%(year)):os.makedirs('VOCdevkit/VOC%s/labels/'%(year))image_ids = open('VOCdevkit/VOC%s/ImageSets/Main/%s.txt'%(year, image_set)).read().strip().split()list_file = open('%s_%s.txt'%(year, image_set), 'w')for image_id in image_ids:list_file.write('%s/VOCdevkit/VOC%s/JPEGImages/%s.png\n'%(wd, year, image_id))convert_annotation(year, image_id)list_file.close()os.system("cat 2007_train.txt 2007_val.txt 2012_train.txt 2012_val.txt > train.txt")
os.system("cat 2007_train.txt 2007_val.txt 2007_test.txt 2012_train.txt 2012_val.txt > train.all.txt")

这一步完成之后可以看到根目录下有以下文件:

打开后,里面包含的是路径:

对应训练文件的Labels也出现了:

2.2.4  修改anchor size

anchor size是需要根据自己的数据集进行修改的,在这里我没有修改,想要修改的同学可以参考以下链接:

YOLO-v3模型参数anchor设置_m_buddy的博客-CSDN博客_anchor设置

YOLOv3使用笔记——Kmeans聚类计算anchor boxes_Gotta-C的博客-CSDN博客_yolov3聚类

3. 检测效果预览

3.1 训练

在检测之前,需要对生成的VOC格式的Luna16进行训练。在进行这部之前,一定检查第二部分生成的:.xml, .jpgs, .txt是否存在。下一步则是要改yolov3的网络结构

每一个yolo层的class修改为1(这里只检测肺结节),filters修改为18(3*(1+5))。同时.data和.names均进行修改。

待.cfg,.data, .names文件修改完毕后,则可以开始训练。训练过程(训练中,待更新,这里使用的是C版本的yolov3):

3.2 训练结果显示

还在训练中,训练完了更新。

训练了大概3天,迭代了48000次,loss值约0.03(GTX1080)。

原图:

预测:

标注图(根据标注生成):

可以发现yolov3检测肺结节还是有点吃力的,存在较多的漏检、误检。原因可能是:1.迭代次数还不够,模型没有完全收敛。2.需要对数据集进行聚类分析,修改anchors的尺寸和比例。

3.3 存在的问题

1. 数据不平衡问题。

在本次训练的时候,只使用了正例(有肺结节)的例子,其实还有很多类似肺结节但不是肺结节的例子。

比如上图是根据标注生成的图片,我们看到肺结节很小,但其实图像左下方还有很多类似肺结节的结点,但却不是肺结节。这是因为我只使用正例(是真实肺结节)的标注。

class=1表示有肺结节。但如果把全部类似肺结节进行考虑的话,其实应该分两步做。第一步:检测出类似肺结节区域。第二部:从类似肺结节区域中得到真实肺结节。像这种正负样布不均衡问题,应该可以考虑focal loss优化。

2.数据结构信息不完整

这点主要体现在CT图像从3维转换到2维,损失了结构信息。Luna16数据集一共112GB,经过处理成二维之后,只有247MB。下图均是根据标注生成的图片(labelImage)。我们发现第一张图只有右下角是肺结节,但左上角不是。第二张图左上角是肺结节,但右下角不是肺结节,而两张图却及其类似。这是因为CT图像是三维的,我是按Z轴方向进行切割,也是说标注肺结节的Z轴是不同的,但下面两张图的Z轴坐标却非常的相近,所以会造成这个情况。我认为应该处理的方法,给每个结节赋予一个概率值,当大于某个概率值才会显示。

以上,简单的把yolov3用于实际数据集(Luna16)处理。

把Luna转换成VOC格式的脚本已上传:

https://download.csdn.net/download/qq_24739717/11422858

里面包含了两个脚本,使用到的所有函数已在第二部分讲解。

这两个脚本不是最新的。

Luna16数据集转VOC数据集&肺实质分割&生成,Mat.zip_luna16分割-深度学习代码类资源-CSDN下载

更新:2019.9.10

对数据集进行肺实质分割。

1.标准化数据,查看数值分布:

2.找出肺部附近的平均像素值,对洗掉的图像进行重新正规化。使用Kmeans分离前景(不透明组织)和背景(透明组织,即肺)。这样做只在图像的中心,尽量避免非组织部分的图像。

3.使用腐蚀和膨胀。可参考https://www.jianshu.com/p/05ef50ac89ac。腐蚀操作对去除某些区域的颗粒状很有帮助;然后使用大的膨胀去吞噬肺部区域的血管,,并且去除黑色噪声,特别是不透明射线造成的黑色肺部区域。

4.提取肺部mask。选取CT图像的中间部分,是为了把四周区域(床板)给剔除,接下来计算图像的连通域,获取较大的连通域为肺部mask。对该mask再做一次大的膨胀来填充和取出肺部mask。这个时候边缘可能会不整齐,可以使用腐蚀来优化边缘,但同时有可能会造成肺部空洞的风险(因为前面的膨胀操作并不一定能完全填充空洞,会由于腐蚀的作用再次放大)。

参考:https://github.com/booz-allen-hamilton/DSB3Tutorial

更新:2019.9.14

使用分割后的数据进行训练

训练了大概四天,确实比没有分割前有较大提升。使用肺实质分割之后,图片背景更干净了,对检测效果有很大的帮助。不过我忘了使用kMeans对数据集聚类,修改anchor size了,不然IOU应该还有更大的提升。

下图左边是预测,右边是根据标注生成的ground truth。

更新:2019.9.23

修改anchor size后重新训练

对数据集进行kmean是聚类,修改anchor size,这里需要主要几点:

1.kmean聚类在yolo中是选择了9个聚类中心,但是由于肺结节数据集的特殊性,其聚类中心基本上不可能为9个,使用9个聚类中心进行聚类会出现nan或者一直运行的情况。

2.数据集的bounding boxes占整幅图片的比例很小,当归一化到0~1之间之后,数值大部分只有0.01x,会加大运算量,甚至无法收敛,这里我先把其尺寸全部放到10倍,在聚类完毕之后再除以10

3.根据标注生成的标注图片,不能拿发现肺结节的bounding boxes基本上为1:1,同时考虑到yolov3使用了3个特征尺度进行预测,所这里选择聚类中心为3。

多次聚类求得比较好的anchor size(使用3个聚类中心有时仍然可能无法收敛或者出现nan):

这个之84.07%已经很高了,比voc 67%左右高出不少。

当然也测试了一下其他聚类中心(1个或者2个):

但都没有使用3个聚类中心的好。

训练:

检测效果:

实战:使用yolov3完成肺结节检测(Luna16数据集)及肺实质分割相关推荐

  1. 实战:使用Faster R-CNN完成肺结节检测(LUNA16)(1)/LUNA16数据集初探

    LUNA16 LUNA16,全称Lung Nodule Analysis 16.该数据集来自另一个更大的数据集LIDC-IDRI,旨在推动更多计算机视觉领域的SOTA算法用于CAD领域.官方网站 比赛 ...

  2. 肺结节目标检测_一种基于CT图像的肺结节检测方法及系统与流程

    本发明属于医学图像分析和计算机辅助诊断等技术领域,更具体地,涉及一种基于CT图像的肺结节检测方法及系统. 背景技术: 肺癌是导致患癌死亡的最危险的疾病之一,其发病率占所有癌症的三分之二,且5年存活率为 ...

  3. 肺结节检测相关文章记录

    1.3DFPN-HS^2: 3D Feature Pyramid Network Based High Sensitivity and Specificity Pulmonary Noudle Det ...

  4. 肺结节检测(一):数据集介绍及处理

    一.LUNA16数据集介绍 1.简介 LUNA16数据集包括888低剂量肺部CT影像(mhd格式)数据,每个影像包含一系列胸腔的多个轴向切片.原始图像为三维图像.每个图像包含一系列胸腔的多个轴向切片. ...

  5. 肺结节目标检测_基于改进Faster R-CNN的肺结节检测

    基于改进 Faster R-CNN 的肺结节检测 肺癌在我国乃至全球范围内 , 都是发病率及死亡率最高的恶性肿瘤.其早期 的表现形式是直径不超过 30mm 的肺内圆形或不规则形结节. 肺癌的早期诊断与 ...

  6. LSSANet:一种用于肺结节检测的长、短切片感知网络

    文章目录 LSSANet: A Long Short Slice-Aware Network for Pulmonary Nodule Detection 摘要 方法 Long Short Slice ...

  7. 肺结节检测的一些要点总结

    肺结节检测实际上就是目标检测,但是有一些地方要注意. 1:在二维平面图上检测不准,还需3D方式进行检测. 2: 一个患者的片子可能包括几百张切片,重构为3D后占用内存很大,需要采用补丁输入的方式,就是 ...

  8. [毕设自用]图像分割之肺部肺结节医学影像数据集LUNA16的基础知识汇总

    浏览器的书签存了一些链接,有很多以后用不到了,发篇文章存一下. 我的毕业设计是用深度学习方法处理LUNA16,实现肺结节的分割. 感谢各位大佬的文章与想法(以下链接标注了文章来源及作者),也欢迎大家也 ...

  9. 基于Faster RCNN的医学图像检测(肺结节检测)

    Faster-R-CNN算法由两大模块组成:1.PRN候选框提取模块 2.Fast R-CNN检测模块.其中,RPN是全卷积神经网络,用于提取候选框:Fast R-CNN基于RPN提取的proposa ...

最新文章

  1. 图解NumPy,这是理解数组最形象的一份教程了
  2. 动手写一个简单版的谷歌TPU
  3. Activiti实战. 1.5Activiti架构与组件
  4. springboot通过Intellij指定端口
  5. dz3.0数据库操作函数分析说明
  6. sql2005 分页 ROW_NUMBER()
  7. 远程sun服务器,向日葵远程控制(com.oray.sunlogin) - 11.1.0.39347 - 应用 - 酷安
  8. 589页22万字城市智慧应急指挥中心大数据信息化系统整体设计方案
  9. React 还是 Vue: 你应该选择哪一个Web前端框架?
  10. 012-P2P加速Docker镜像分发(阿里Dragonfly)
  11. Android项目 moudle和library转换
  12. centos 设置为北京时间
  13. html与word字体大小转换,字体大小不一样,当html转换为Word使用Microsoft.Office.Interop.Word...
  14. 响铃:抖音微视撕逼,但音乐短视频只有“神曲”逻辑吗?
  15. MySql项目查询优化经验总结
  16. avast 免费好用的杀毒软件
  17. 字节三面:对于 Spring 你了解多少?如果你会了这 150 道题 ,吊打面试官岂不是洒洒水
  18. 用公式π/4≈1-1/3+1/5……求π的近似值,要求直到最后一项的绝对值小于0.000001为止。
  19. 汇编语言---算术运算指令
  20. 正面管教之PHP_正面管教家长互助(PHP)体验课1

热门文章

  1. 号牌号码识别,支持新能源车牌,离线识别,喜欢就收下吧
  2. if……else……if语句实例
  3. jsr303常用注解
  4. 禅道使用流程——产品经理
  5. spark系列3:spark入门编程与介绍
  6. 计算机基础知识系列·进制转换的简易方法
  7. 来电归属地java_来电归属地数据查询Java实现
  8. 解决服务器80端口监听异常导致无法打开和访问网站的问题
  9. 使用NoteExpress实现Word参考文献插入及排版
  10. 基于ICE方式SIP信令穿透Symmetric NAT技术研究