deeplung中这部分的实现代码同样都是参照的DSB2017的第一名团队的代码,主要的思路就是一个完整的CT由于GPU内存的限制很难全部直接送入网络进行计算,所以需要进行切分,切分成更小的尺寸,这样才能送入网络进行计算。这部分的代码主要在data.py中的DataBowl3Detector类中的__getitem__函数。
imgs = np.load(self.filenames[idx])
# print('imgs.shape',imgs.shape)
bboxes = self.sample_bboxes[idx] # 为啥还要有标签
nz, nh, nw = imgs.shape[1:]#(197, 181, 224)
pz = int(np.ceil(float(nz) / self.stride)) * self.stride # 补成stride的倍数
ph = int(np.ceil(float(nh) / self.stride)) * self.stride
pw = int(np.ceil(float(nw) / self.stride)) * self.stride
imgs = np.pad(imgs, [[0,0],[0, pz - nz], [0, ph - nh], [0, pw - nw]], 'constant',constant_values = self.pad_value) # 用170将长宽高填充成stride的整数倍,为了方便下面计算coord
# print('1------pad',self.stride, pz,ph,pw)
xx,yy,zz = np.meshgrid(np.linspace(-0.5,0.5,imgs.shape[1]/self.stride),
np.linspace(-0.5,0.5,imgs.shape[2]/self.stride),
np.linspace(-0.5,0.5,imgs.shape[3]/self.stride),indexing ='ij')
coord = np.concatenate([xx[np.newaxis,...], yy[np.newaxis,...],zz[np.newaxis,:]],0).astype('float32') # np.newaxis用来增加一个维度,使x,y,z坐标可以堆叠起来,
# print('------1',imgs.shape)
imgs, nzhw = self.split_comber.split(imgs) # 获得分割好的patch
# print('------2',imgs.shape)
# print('------',self.split_comber.max_stride,self.stride)
coord2, nzhw2 = self.split_comber.split(coord,
side_len = self.split_comber.side_len//self.stride,
max_stride = self.split_comber.max_stride//self.stride,
margin = self.split_comber.margin//self.stride) # 相应的coord也要进行分割
assert np.all(nzhw==nzhw2) # 如果没有问题,nzhw和nzhw2肯定是一样的
imgs = (imgs.astype(np.float32)-128)/128
# print('sample.shape',imgs.shape,coord2.shape)
return torch.from_numpy(imgs), bboxes, torch.from_numpy(coord2), np.array(nzhw)
这部分的代码都做了详细的注释,其中最主要的就是利用了split_comber类中的split函数对一整张CT图像进行划分(在deeplung中作者将一整张CT在测试的时候划分成了(160x160x160)的patch,并且patch之间有32像素的重叠区域,防止边缘效应)。并对每个patch相应的coord进行了计算。
一开始我在看这部分的代码时产生了一个疑问,就是以上的代码虽然将一张CT分割成了很多patch,但是最终返回的结果是将所有的patch堆叠在一起,这样不是和将一整张CT送入网络没啥区别吗?
这个疑问在我看main.py的test函数得到了解答,在这个函数中会将所有的patch再进行拆分,即保证每次只将一个patch送入网络。
下面贴出split函数,关键部分都有详细的注释
def split(self, data, side_len = None, max_stride = None, margin = None):
if side_len==None:
side_len = self.side_len
if max_stride == None:
max_stride = self.max_stride
if margin == None:
margin = self.margin
assert(side_len > margin)
assert(side_len % max_stride == 0)
# print('margin, max_stride',margin, max_stride)
assert(margin % max_stride == 0)
splits = []
_, z, h, w = data.shape
# print('READdata',data.shape)
nz = int(np.ceil(float(z) / side_len)) # 补成side_len的倍数
nh = int(np.ceil(float(h) / side_len))
nw = int(np.ceil(float(w) / side_len))
nzhw = [nz,nh,nw] # 里面存的是z,y,x三个轴可以划分的patch个数,一个CT的总的patch个数就是nz*nh*nw
# print('nzhw',nzhw)
# print('nzhw',nz * side_len - z + margin)
self.nzhw = nzhw
pad = [ [0, 0],
[margin, nz * side_len - z + margin], # 算出在z轴前后应该padding的大小
[margin, nh * side_len - h + margin], # 算出在y轴前后应该padding的大小
[margin, nw * side_len - w + margin]] # 算出在x轴前后应该padding的大小
data = np.pad(data, pad, 'edge') # 将原来的CT padding成nz * side_len+2*margin大小,但是为啥要padding成这样呢?看到下面的划分patch的过程就悟了(这样做是为了保证一个CT可以恰好划分出整数个patch并且每两个patch之间具有32像素的重叠区域)
# print('PADdata',data.shape)
# print('PADdata',stop)
for iz in range(nz):
for ih in range(nh):
for iw in range(nw):
sz = iz * side_len # 切分的每个patch的z轴起始位置
ez = (iz + 1) * side_len + 2 * margin # 切分的每个patch的z轴终止位置,加上2*margin是为了保证下一个patch的起始位置与上一个patch的终止位置有32个像素的重叠
sh = ih * side_len # 切分的每个patch的y轴起始位置
eh = (ih + 1) * side_len + 2 * margin # 切分的每个patch的y轴终止位置,加上2*margin是为了保证下一个patch的起始位置与上一个patch的终止位置有32个像素的重叠
sw = iw * side_len # 切分的每个patch的x轴起始位置
ew = (iw + 1) * side_len + 2 * margin # 切分的每个patch的x轴终止位置,加上2*margin是为了保证下一个patch的起始位置与上一个patch的终止位置有32个像素的重叠
split = data[np.newaxis, :, sz:ez, sh:eh, sw:ew] # 取出patch的数据(160*160*160),并增加一个batch_size维度
splits.append(split) # 将所有的patch放到一个列表里
# splits.append(split)
splits = np.concatenate(splits, 0) # 将所有的patch在batch_size维度进行拼接,最后得到的是(patch个数,1,160,160,160)
return splits,nzhw



