基於 keras的全卷積網路u-net端到端醫學影象多型別影象分割(一)
有醫院的朋友,需要幫忙完成一個影象分割的任務,提供了一些資料,看了下資料,灰度圖,覺得設計特徵再做分割太麻煩。直接整神經網路吧。不用費神設計特徵,省事,畢竟只是幫個忙而已。
1. 查詢方案
顯然,這個任務,早有前人做過無數次了,這麼熱點的領域,簡直一搜一大把。搜尋結果,是用 u-net 做醫學影象分割的較多,於是決定使用u-net。關於FCN的介紹,看這個部落格吧,本文著重於程式碼實現!
考慮到任務的價值和擼程式碼的便利性,決定使用keras,畢竟這只是一個任務。
使用kears 做影象分割,CSDN 有一篇很容易搜到的文章(文章連結在本文末尾),還附了github地址,簡直得來全不費工夫,立馬下下來,準備直接換資料跑完程式碼收工。顯然… 我還是太年輕。
2. 坑
原作者的程式碼的測試是二分類的,但我要跑的資料與標記如下:
注:左邊原圖,右邊mask,三類,mask=0,128,255 各為一類。
資料是多分類的,從此埋下了深深地禍根!
先來一個個看吧。
-
ValueError: Error when checking target: expected conv2d_24 to have 4 dimensions, but got array with shape (2, 65536, 3)
這個問題其實見得比較多了,神經網路影象初學時比較容易出現類似的問題,於是檢查程式碼,根據提示定位到如下程式碼段:
def adjustData(img,mask,flag_multi_class,num_class):
if(flag_multi_class):
img = img / 255
mask = mask[:,:,:,0] if(len(mask.shape) == 4) else mask[:,:,0]
new_mask = np.zeros(mask.shape + (num_class,))
for i in range(num_class):
#for one pixel in the image, find the class in mask and convert it into one-hot vector
#index = np.where(mask == i)
#index_mask = (index[0],index[1],index[2],np.zeros(len(index[0]),dtype = np.int64) + i) if (len(mask.shape) == 4) else (index[0],index[1],np.zeros(len(index[0]),dtype = np.int64) + i)
#new_mask[index_mask] = 1
new_mask[mask == i,i] = 1
new_mask = np.reshape(new_mask,(new_mask.shape[0],new_mask.shape[1]*new_mask.shape[2],new_mask.shape[3])) if flag_multi_class else np.reshape(new_mask,(new_mask.shape[0]*new_mask.shape[1],new_mask.shape[2]))
mask = new_mask
elif(np.max(img) > 1):
img = img / 255
mask = mask /255
mask[mask > 0.5] = 1
mask[mask <= 0.5] = 0
return (img,mask)
由於是多分類,設定flag_multi_class=True,num_class=3,可以看到程式碼將走向前段,這樣mask將會被reshape成(65536,3),至於前面的2 是 batch_size,此時明確了label的形狀,就證明網路輸出層與label不匹配導致錯誤,於是檢視模型程式碼。
conv10 = Conv2D(1, 1, activation='sigmoid')(conv9) #其實conv2d_24 就是這裡的conv10
model = Model(input=inputs, output=conv10)
model.compile(optimizer=Adam(lr=1e-4), loss='binary_crossentropy', metrics=['accuracy'])
根據model的輸出形狀可推出 label形狀應該是(2,256,256,1)。而我們提供的mask是(2,256*256,3),所以報錯。
明確了錯誤就很好修改了!
for i in range(num_class):
new_mask[mask == i,i] = 1 #去掉了註釋
這段程式碼是指mask中有與型別相等的值時,新增為這層的label。 我的mask怎麼會是0,1,2這種呢,而且一般的mask都是0,128,256這種易於區分的值啊…尷尬
自己擼程式碼:
def adjustData(img,mask,flag_multi_class,num_class):
if(flag_multi_class):
img = img / 255.
mask = mask[:,:,:,0] if(len(mask.shape) == 4) else mask[:,:,0]
new_mask = np.zeros(mask.shape + (num_class,))
new_mask[mask == 0,0] = 1
new_mask[mask == 128, 1] = 1
new_mask[mask > 200, 2] = 1
mask = new_mask
# print('new 0 :',np.sum(mask==1))
# print('new 128 :', np.sum(mask ==2))
# print('new 255 :', np.sum(mask == 3))
# print('new sum :',np.sum(mask==1)+np.sum(mask==2)+np.sum(mask==3))
elif(np.max(img) > 1):
img = img / 255.
mask = mask /255.
mask[mask > 0.5] = 1
mask[mask <= 0.5] = 0
return (img,mask)
這個修改不再reshape mask,並且將mask處理成one-hot編碼。即按照資料集中mask的值 0,128,256 進行分類疊加。如果mask影象相應的值是 0
,那麼,處理後的值為[1,0,0],當mask=128時為[0,1,0],255時為[0,0,1]。這樣就完成了標記資料的轉換。處理後的label shape 為(2,256,256,3),與網路輸出還有區別網路為(2,256,256,1)所以我們要對網路再進行改進。
conv10 = Conv2D(3, 1, activation='sigmoid')(conv9) #修改原來的1為3,此時網路有3通道輸出。
model = Model(input=inputs, output=conv10)
model.compile(optimizer=Adam(lr=1e-4), loss='binary_crossentropy', metrics=['accuracy'])
這樣就能完成多分類的訓練了。
後面發現,訓練了多個epoch(40次左右),輸出影象仍為純白,再次回顧網路結構,與loss函式。發現居然用的binary_crossentropy,坑了個爹的,只能用於二分類。修改成categorical_crossentropy 又不收斂,唉,本來不想花時間的東西,居然已經弄了幾個小時。
至此,已經發現要改的東西比較多。老老實實再繼續吧弄吧。
3. 目標很明確,端到端多型別影象分割!
後面借鑑github的程式碼做了端到端的多分類,將在(二)裡面介紹,考慮用u-net做多分類的可以看下,github地址:Keras-u-net,歡迎star。
注:分析程式碼原文地址:文章連結
端到端多分類程式碼地址:Keras-u-net