在(一)中,我將肺結節檢測項目總結為三階段,這里我要講講這個項目的第三階段,至於第二階段,由於數據增強部分的代碼我始終看不大懂,先不講。
結果評估的程序在evaluationScript文件夾下,這個文件夾下的文件名比較煩,看的比較懵。
annotations文件夾里面放的是結節標簽文件,無關結節標簽文件(是結節,但是不統計在內,也不作為非結節區域,就是評估的時候如果你檢測到了它,既不算正確,也不算錯誤,略過),還有用戶id文件。
tool文件夾放的是讀取csv文件的模塊。
frocwrtdetpepchluna16.py用來將輸出的.npy格式的結果轉化為.csv格式。
noduleCADEvaluationLUNA16.py用來對.csv文件中的結果進行評估,得出froc曲線圖。
下面詳細分析下代碼,首先是frocwrtdetpepchluna16.py。
def getcsv(detp, eps): #給定閾值和epoch for ep in eps:#針對每個epoch bboxpath = results_path + str(ep) + '/' #找到每個epoch的路徑 for detpthresh in detp: print 'ep', ep, 'detp', detpthresh f = open(bboxpath + 'predanno'+ str(detpthresh) + 'd3.csv', 'w') #根據閾值分別創建與之對應的文件 fwriter = csv.writer(f) fwriter.writerow(firstline) #寫入第一行,包括用戶id,結節坐標x,y,z,結節概率p fnamelist = [] for fname in os.listdir(bboxpath): if fname.endswith('_pbb.npy'): #找到以_pbb.npy結尾的文件(輸出的結節預測值),添加進文件列表 fnamelist.append(fname) print(len(fnamelist)) convert = functools.partial(convertcsv, bboxpath = bboxpath, detp = detpthresh) #這個函數對convertcsv函數進行修飾,其實就是預設定幾個參數,不用再輸入 for fname in fnamelist: print fname rowlist = convert(fname) #將每一個_pbb.npy文件轉換為csv文件 for row in rowlist: print row fwriter.writerow(row)
#這段注釋掉的是原來的代碼,用來對那末多pbb.npy文件並行處理,事實證明,確實快了好幾倍,實際運行應該用下面這段
#map函數也是修飾函數,將fnamelist的元素並行送給convert處理
#predannolist = p.map(functools.partial(convertcsv, bboxpath=bboxpath, detp=detpthresh), fnamelist) #for predanno in predannolist: # print predanno # for row in predanno: # print row # fwriter.writerow(row) f.close()
上面這段代碼不是核心代碼,這段的作用是對輸出的結果文件調用convertcsv函數處理,結果就是每一個epoch都生成一個csv文件,存放80多個測試病例的預測結節位置及概率。
那么接下來就說說比較核心的convertcsv函數
def convertcsv(bboxfname, bboxpath, detp):#給定pbb.npy的文件名,pbb.npy的路徑,閾值 sliceim,origin,spacing,isflip = load_itk_image(datapath+bboxfname[:-8]+'.mhd')#加載原始數據 origin = np.load(sideinfopath+bboxfname[:-8]+'_origin.npy', mmap_mode='r') #以下幾行加載預處理后的坐標原點,分辨率,拓展box spacing = np.load(sideinfopath+bboxfname[:-8]+'_spacing.npy', mmap_mode='r') resolution = np.array([1, 1, 1]) extendbox = np.load(sideinfopath+bboxfname[:-8]+'_extendbox.npy', mmap_mode='r') pbb = np.load(bboxpath+bboxfname, mmap_mode='r') #加載pbb.npy文件 #print "load finished!" pbbold = np.array(pbb[pbb[:,0] > detp]) #根據閾值過濾掉概率低的 pbbold = np.array(pbbold[pbbold[:,-1] > 3]) #根據半徑過濾掉小於3mm的 pbbold = pbbold[np.argsort(-pbbold[:,0])][:1000] #這條是我加上的,取概率值前1000的結節作為輸出,不然直接進行nms耗時太長 pbb = nms(pbbold, nmsthresh) #對輸出的結節進行nms #print "after nms bboxs:",pbb.shape[0] # print len(pbb), pbb[0] # print bboxfname, pbbold.shape, pbb.shape, pbbold.shape pbb = np.array(pbb[:, :-1]) #去掉直徑 # print pbb[:, 0] pbb[:, 1:] = np.array(pbb[:, 1:] + np.expand_dims(extendbox[:,0], 1).T) #對輸出加上拓展box的坐標,其實就是恢復為原來的坐標,我對這個拓展box深惡痛絕 pbb[:, 1:] = np.array(pbb[:, 1:] * np.expand_dims(resolution, 1).T / np.expand_dims(spacing, 1).T) #將輸出恢復為原來的分辨率,這樣就對應了原始數據中的體素坐標 if isflip:#如果有翻轉的情況,將坐標翻轉(我理解是這樣的,原始數據有翻轉的情況,但是label還是未翻轉的label,那么將label翻轉,所以模型的輸出也是翻轉的,現在要再翻轉回去,與label對應)
#拓展box與翻轉這兩個操作讓我神煩 Mask = np.load(sideinfopath+bboxfname[:-8]+'_mask.npy', mmap_mode='r') pbb[:, 2] = Mask.shape[1] - pbb[:, 2] pbb[:, 3] = Mask.shape[2] - pbb[:, 3] pos = VoxelToWorldCoord(pbb[:, 1:], origin, spacing) #將輸出轉換為世界坐標 #print "voxel to world finished!" rowlist = [] # print pos.shape for nk in xrange(pos.shape[0]): # pos[nk, 2], pos[nk, 1], pos[nk, 0] #現在依次將文件名,z,y,x,概率(經過sigmoid處理)寫入rowlist,每行都是一個輸出結節 rowlist.append([bboxfname[:-8], pos[nk, 2], pos[nk, 1], pos[nk, 0], 1/(1+np.exp(-pbb[nk,0]))]) # print len(rowlist), len(rowlist[0]) return rowlist#bboxfname[:-8], pos[:K, 2], pos[:K, 1], pos[:K, 0], 1/(1+np.exp(-pbb[:K,0]))
這段代碼各種操作是真的繁瑣,看着都頭疼。這段主要完成的就是對輸出結節應用閾值和nms,輸出的結果再轉換為與label一樣的坐標體系,因為最后的准確與否全看與label是否一致。看一下打印的輸出信息,這里用的是subset9為驗證集,我只貼出了一個epoch的測試結果,閾值取為-0.125,這是sigmoid函數的輸入,對應概率值0.4695。
ep 1 detp -0.125 write to/home/code/DeepLung/detector/results/new_arch/retrft969/val1/predanno-0.125pbb.csv
至此,輸出處理完畢,可以求取最后的FROC值了,離大功告成只差一步。
不過,每個epoch都會對應一個csv結果文件,我們還要選取一個最好的結果,選取標准就是FROC值,這些都是如何做的呢,不妨看一下代碼。
def getfroc(detp, eps): maxfroc = 0 maxep = 0 for ep in eps: #對每個epoch分別處理 bboxpath = results_path + str(ep) + '/' predannofnamalist = [] for detpthresh in detp: #此處的detp就是閾值,只不過這里采用的是一個閾值列表,就我自己而言,我采用的閾值是-0.125,列表中只有一個元素 predannofnamalist.append(bboxpath + 'predanno'+ str(detpthresh) + 'pbb.csv') #froclist = p.map(getfrocvalue, predannofnamalist) froclist = [getfrocvalue(predanno) for predanno in predannofnamalist] #調用getfrocvalue求取froc值 if maxfroc < max(froclist): maxep = ep maxfroc = max(froclist) print froclistprint maxfroc, maxep
接下來調用getfrocvalue函數,這個函數其實是調用了noduleCADEvaluationLUNA16.py,也就是核心模塊,剛才的frocwrtdetpepchluna16.py只是做了一層處理,對輸出進行篩選和變換,以用來進行最后的大決戰。
def getfrocvalue(results_filename): return noduleCADEvaluation(annotations_filename,annotations_excluded_filename,seriesuids_filename,results_filename,'./')
可以看到,我們將標簽文件,無關標簽文件,用戶id文件,以及最后的結果文件也就是之前的csv文件作為輸入,輸出會是一個標量值,也就是評分。
對noduleCADEvaluationLUNA16.py的分析我想放到(三)(下)去講,因為會比較長。