-
Notifications
You must be signed in to change notification settings - Fork 417
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 76c6d60
Showing
73 changed files
with
15,579 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
*.pyc | ||
*.ipynb* | ||
training/.flag* | ||
prediction*.csv |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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%> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Ignore everything in this directory | ||
* | ||
# Except this file | ||
!.gitignore | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.