Skip to content

Commit

Permalink
base
Browse files Browse the repository at this point in the history
  • Loading branch information
lfz committed May 5, 2017
0 parents commit 76c6d60
Show file tree
Hide file tree
Showing 73 changed files with 15,579 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.pyc
*.ipynb*
training/.flag*
prediction*.csv
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Dependencies

Ubuntu 14.04, python 2.7, CUDA 8.0, cudnn 5.1, h5py (2.6.0), SimpleITK (0.10.0), numpy (1.11.3), nvidia-ml-py (7.352.0), matplotlib (2.0.0), scikit-image (0.12.3), scipy (0.18.1), pyparsing (2.1.4), pytorch (0.1.10+ac9245a) (anaconda is recommended)

This is my configuration, I am not sure about the compatability of other versions



# Instructions for runing

Testing
1. unzip the stage 2 data
2. go to root folder
3. open config_submit.py, filling in datapath with the stage 2 data path
4. python main.py
5. get the results from prediction.csv

if you have bug about short of memory, set the 'n_worker_preprocessing' in config\_submit.py to a int that is smaller than your core number.

Training
1. Install all dependencies
2. Prepare stage1 data, LUNA data, and LUNA segment results (https://luna16.grand-challenge.org/download/), unzip them to separate folders
3. Go to ./training and open config_training.py
4. Filling in stage1_data_path, luna_raw, luna_segment with the path mentioned above
5. Filling in luna_data, preprocess_result_path, with tmp folders
6. bash run_training.sh and wait for the finishing of training (it may take several days)

If you do not have 8 GPUs or your the memory of your GPUs is less than 12 GB, decrease the number of -b and -b2 in run\_training.sh, and modify the 'CUDA\_VISIBLE\_DEVICES=0,1,..,n\_your\_gpu'. The time of training is very long (3~4 days with 8 TITANX).



# Brief Introduction to algorithm
Extra Data and labels: we use LUNA16 as extra data, and we manually labeled the locations of nodules in the stage1 training dataset. We also manually washed the label of LUNA16, deleting those that we think irrelavent to cancer. The labels are stored in ./training./detector./labels.

The training involves four steps
1. prepare data

All data are resized to 1x1x1 mm, the luminance is clipped between -1200 and 600, scaled to 0-255 and converted to uint8. A mask that include the lungs is calculated, luminance of every pixel outside the mask is set to 170. The results will be stored in 'preprocess_result_path' defined in config_training.py along with their corresponding detection labels.

2. training a nodule detector

in this part, a 3d faster-rcnn is used as the detector. The input size is 128 x 128 x 128, an online hard negative sample mining method is used. The network structure is based on U-net.

3. get all proposals

The model trained in part 2 was tested on all data, giving all suspicious nodule locations and confidences (proposals)

4. training a cancer classifier

For each case, 5 proposals are samples according to its confidence, and for each proposal a 96 x 96 x 96 cubes centered at the proposal center is cropped.

These proposals are fed to the detector and the feature in the last convolutional layer is extracted for each proposal. These features are fed to a fully-connected network and a cancer probability $P_i$ is calculated for each proposal. The cancer probability for this case is calculated as:

$P = 1-(1-P_d)\Pi(1-P_i)$,

where the $P_d$ stand for the probability of cancer of a dummy nodule, which is a trainable constant. It account for any possibility that the nodule is missed by the detector or this patient do not have a nodule now. Then the classification loss is calculated as the cross entropy between this $P$ and the label.

The second loss term is defined as: $-\log(P)\boldsymbol{1}(y_{nod}=1 \& P<0.03)$, which means that if this proposal is manually labeled as nodule and its probability is lower than 3%, this nodule would be forced to have higher cancer probability. Yet the effect of this term has not been carefully studied.

To prevent overfitting, the network is alternatively trained on detection task and classification task.

The network archetecture is shown below

<img src="./images/nodulenet.png" width=50%>

<img src="./images/casenet.png" width=50%>
5 changes: 5 additions & 0 deletions bbox_result/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

13 changes: 13 additions & 0 deletions config_submit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
config = {'datapath':'/work/DataBowl3/stage2/stage2/',
'preprocess_result_path':'./prep_result/',
'outputfile':'prediction.csv',

'detector_model':'net_detector',
'detector_param':'./model/detector.ckpt',
'classifier_model':'net_classifier',
'classifier_param':'./model/classifier.ckpt',
'n_gpu':8,
'n_worker_preprocessing':None,
'use_exsiting_preprocessing':False,
'skip_preprocessing':False,
'skip_detect':False}
219 changes: 219 additions & 0 deletions data_classifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import numpy as np
import torch
from torch.utils.data import Dataset
import os
import time
import collections
import random
from layers import iou
from scipy.ndimage import zoom
import warnings
from scipy.ndimage.interpolation import rotate
from layers import nms,iou
import pandas

class DataBowl3Classifier(Dataset):
def __init__(self, split, config, phase = 'train'):
assert(phase == 'train' or phase == 'val' or phase == 'test')

self.random_sample = config['random_sample']
self.T = config['T']
self.topk = config['topk']
self.crop_size = config['crop_size']
self.stride = config['stride']
self.augtype = config['augtype']
self.filling_value = config['filling_value']

#self.labels = np.array(pandas.read_csv(config['labelfile']))

datadir = config['datadir']
bboxpath = config['bboxpath']
self.phase = phase
self.candidate_box = []
self.pbb_label = []

idcs = split
self.filenames = [os.path.join(datadir, '%s_clean.npy' % idx.split('-')[0]) for idx in idcs]
if self.phase!='test':
self.yset = 1-np.array([f.split('-')[1][2] for f in idcs]).astype('int')


for idx in idcs:
pbb = np.load(os.path.join(bboxpath,idx+'_pbb.npy'))
pbb = pbb[pbb[:,0]>config['conf_th']]
pbb = nms(pbb, config['nms_th'])

lbb = np.load(os.path.join(bboxpath,idx+'_lbb.npy'))
pbb_label = []

for p in pbb:
isnod = False
for l in lbb:
score = iou(p[1:5], l)
if score > config['detect_th']:
isnod = True
break
pbb_label.append(isnod)
# if idx.startswith()
self.candidate_box.append(pbb)
self.pbb_label.append(np.array(pbb_label))
self.crop = simpleCrop(config,phase)


def __getitem__(self, idx,split=None):
t = time.time()
np.random.seed(int(str(t%1)[2:7]))#seed according to time

pbb = self.candidate_box[idx]
pbb_label = self.pbb_label[idx]
conf_list = pbb[:,0]
T = self.T
topk = self.topk
img = np.load(self.filenames[idx])
if self.random_sample and self.phase=='train':
chosenid = sample(conf_list,topk,T=T)
#chosenid = conf_list.argsort()[::-1][:topk]
else:
chosenid = conf_list.argsort()[::-1][:topk]
croplist = np.zeros([topk,1,self.crop_size[0],self.crop_size[1],self.crop_size[2]]).astype('float32')
coordlist = np.zeros([topk,3,self.crop_size[0]/self.stride,self.crop_size[1]/self.stride,self.crop_size[2]/self.stride]).astype('float32')
padmask = np.concatenate([np.ones(len(chosenid)),np.zeros(self.topk-len(chosenid))])
isnodlist = np.zeros([topk])


for i,id in enumerate(chosenid):
target = pbb[id,1:]
isnod = pbb_label[id]
crop,coord = self.crop(img,target)
if self.phase=='train':
crop,coord = augment(crop,coord,
ifflip=self.augtype['flip'],ifrotate=self.augtype['rotate'],
ifswap = self.augtype['swap'],filling_value = self.filling_value)
crop = crop.astype(np.float32)
croplist[i] = crop
coordlist[i] = coord
isnodlist[i] = isnod

if self.phase!='test':
y = np.array([self.yset[idx]])
return torch.from_numpy(croplist).float(), torch.from_numpy(coordlist).float(), torch.from_numpy(isnodlist).int(), torch.from_numpy(y)
else:
return torch.from_numpy(croplist).float(), torch.from_numpy(coordlist).float()
def __len__(self):
if self.phase != 'test':
return len(self.candidate_box)
else:
return len(self.candidate_box)



class simpleCrop():
def __init__(self,config,phase):
self.crop_size = config['crop_size']
self.scaleLim = config['scaleLim']
self.radiusLim = config['radiusLim']
self.jitter_range = config['jitter_range']
self.isScale = config['augtype']['scale'] and phase=='train'
self.stride = config['stride']
self.filling_value = config['filling_value']
self.phase = phase

def __call__(self,imgs,target):
if self.isScale:
radiusLim = self.radiusLim
scaleLim = self.scaleLim
scaleRange = [np.min([np.max([(radiusLim[0]/target[3]),scaleLim[0]]),1])
,np.max([np.min([(radiusLim[1]/target[3]),scaleLim[1]]),1])]
scale = np.random.rand()*(scaleRange[1]-scaleRange[0])+scaleRange[0]
crop_size = (np.array(self.crop_size).astype('float')/scale).astype('int')
else:
crop_size = np.array(self.crop_size).astype('int')
if self.phase=='train':
jitter_range = target[3]*self.jitter_range
jitter = (np.random.rand(3)-0.5)*jitter_range
else:
jitter = 0
start = (target[:3]- crop_size/2 + jitter).astype('int')
pad = [[0,0]]
for i in range(3):
if start[i]<0:
leftpad = -start[i]
start[i] = 0
else:
leftpad = 0
if start[i]+crop_size[i]>imgs.shape[i+1]:
rightpad = start[i]+crop_size[i]-imgs.shape[i+1]
else:
rightpad = 0
pad.append([leftpad,rightpad])
imgs = np.pad(imgs,pad,'constant',constant_values =self.filling_value)
crop = imgs[:,start[0]:start[0]+crop_size[0],start[1]:start[1]+crop_size[1],start[2]:start[2]+crop_size[2]]

normstart = np.array(start).astype('float32')/np.array(imgs.shape[1:])-0.5
normsize = np.array(crop_size).astype('float32')/np.array(imgs.shape[1:])
xx,yy,zz = np.meshgrid(np.linspace(normstart[0],normstart[0]+normsize[0],self.crop_size[0]/self.stride),
np.linspace(normstart[1],normstart[1]+normsize[1],self.crop_size[1]/self.stride),
np.linspace(normstart[2],normstart[2]+normsize[2],self.crop_size[2]/self.stride),indexing ='ij')
coord = np.concatenate([xx[np.newaxis,...], yy[np.newaxis,...],zz[np.newaxis,:]],0).astype('float32')

if self.isScale:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
crop = zoom(crop,[1,scale,scale,scale],order=1)
newpad = self.crop_size[0]-crop.shape[1:][0]
if newpad<0:
crop = crop[:,:-newpad,:-newpad,:-newpad]
elif newpad>0:
pad2 = [[0,0],[0,newpad],[0,newpad],[0,newpad]]
crop = np.pad(crop,pad2,'constant',constant_values =self.filling_value)

return crop,coord

def sample(conf,N,T=1):
if len(conf)>N:
target = range(len(conf))
chosen_list = []
for i in range(N):
chosenidx = sampleone(target,conf,T)
chosen_list.append(target[chosenidx])
target.pop(chosenidx)
conf = np.delete(conf, chosenidx)


return chosen_list
else:
return np.arange(len(conf))

def sampleone(target,conf,T):
assert len(conf)>1
p = softmax(conf/T)
p = np.max([np.ones_like(p)*0.00001,p],axis=0)
p = p/np.sum(p)
return np.random.choice(np.arange(len(target)),size=1,replace = False, p=p)[0]

def softmax(x):
maxx = np.max(x)
return np.exp(x-maxx)/np.sum(np.exp(x-maxx))


def augment(sample, coord, ifflip = True, ifrotate=True, ifswap = True,filling_value=0):
# angle1 = np.random.rand()*180
if ifrotate:
validrot = False
counter = 0
angle1 = np.random.rand()*180
size = np.array(sample.shape[2:4]).astype('float')
rotmat = np.array([[np.cos(angle1/180*np.pi),-np.sin(angle1/180*np.pi)],[np.sin(angle1/180*np.pi),np.cos(angle1/180*np.pi)]])
sample = rotate(sample,angle1,axes=(2,3),reshape=False,cval=filling_value)

if ifswap:
if sample.shape[1]==sample.shape[2] and sample.shape[1]==sample.shape[3]:
axisorder = np.random.permutation(3)
sample = np.transpose(sample,np.concatenate([[0],axisorder+1]))
coord = np.transpose(coord,np.concatenate([[0],axisorder+1]))

if ifflip:
flipid = np.array([np.random.randint(2),np.random.randint(2),np.random.randint(2)])*2-1
sample = np.ascontiguousarray(sample[:,::flipid[0],::flipid[1],::flipid[2]])
coord = np.ascontiguousarray(coord[:,::flipid[0],::flipid[1],::flipid[2]])
return sample, coord
Loading

0 comments on commit 76c6d60

Please sign in to comment.