點擊上方「小白學視覺」,選擇加"星標"或「置頂」html
重磅乾貨,第一時間送達python
本期咱們將一塊兒來實現一個有趣的問題 -圖像分割的算法。nginx
本文的示例代碼能夠在如下連接中找到:git
https://github.com/kiteco/kite-python-blog-post-code/tree/master/image-segmentationgithub
做爲咱們的例子,咱們將對KESM顯微鏡獲取的圖像進行分割以獲取其中的血管組織。算法
數據科學家和醫學研究人員能夠將這種方法做爲模板,用於更加複雜的圖像的數據集(如天文數據),甚至一些非圖像數據集中。因爲圖像在計算機中表示爲矩陣,咱們有一個專門的排序數據集做爲基礎。在整個處理過程當中,咱們將使用 Python 包,以及OpenCV、scikit 圖像等幾種工具。除此以外,咱們還將使用 numpy ,以確保內存中的值一致存儲。swift
主要內容後端
爲了消除噪聲,咱們使用簡單的中位數濾波器來移除異常值,但也可使用一些不一樣的噪聲去除方法或僞影去除方法。這項工件由採集系統決定(顯微鏡技術),可能須要複雜的算法來恢復丟失的數據。工件一般分爲兩類:數組
1. 模糊或焦點外區域ruby
2. 不平衡的前景和背景(使用直方圖修改正確)
分割
對於本文,咱們使用Otsu 的方法分割,使用中位數濾波器平滑圖像後,而後驗證結果。只要分段結果是二進制的,就能夠對任何分段算法使用相同的驗證方法。這些算法包括但不限於考慮不一樣顏色空間的各類循環閾值方法。
一些示例包括:
1. 李閾值
2. 依賴於局部強度的自適應閾值方法
3. 在生物醫學圖像分割中經常使用的Unet等深度學習算法
4. 在語義上對圖像進行分段的深度學習方法
驗證
咱們從已手動分割的基礎數據集開始。爲了量化分段算法的性能,咱們將真實數據與預測數據的二進制分段進行比較,同時顯示準確性和更有效的指標。儘管真陽性 (TP) 或假陰性 (FN) 數量較低,但精度可能異常高。在這種狀況下,F1 分數和 MCC是二進制分類的更好量化指標。稍後咱們將詳細介紹這些指標的優缺點。
爲了定性驗證,咱們疊加混淆矩陣結果,即真正的正極、真負數、假陽性、假負數像素正好在灰度圖像上。此驗證也能夠應用於二進制圖像分割結果上的顏色圖像,儘管本文中使用的數據是灰度圖像。最後,咱們將介紹整個實現過程。如今,讓咱們看看數據和用於處理這些數據的工具。
Loading and visualizing data
咱們將使用如下模塊加載、可視化和轉換數據。這些對於圖像處理和計算機視覺算法很是有用,具備簡單而複雜的數組數學。若是單獨安裝,括號中的模塊名稱會有所幫助。
若是在運行示例代碼中,遇到 matplotlib 後端的問題,請經過刪除 plt.ion() 調用來禁用交互式模式,或是經過取消註釋示例代碼中的建議調用來在每一個部分的末尾調用 plt.show()。"Agg"或"TkAgg"將做爲圖像顯示的後端。繪圖將顯示在文章中。
代碼導入
import cv2 import matplotlib.pyplot as plt import numpy as np import scipy.misc import scipy.ndimage import skimage.filters import sklearn.metrics # Turn on interactive mode. Turn off with plt.ioff() plt.ion()
在本節中,咱們將加載可視化數據。數據是小老鼠腦組織與印度墨水染色的圖像,由顯微鏡(KESM)生成。此 512 x 512 圖像是一個子集,稱爲圖塊。完整的數據集爲 17480 x 8026 像素,深度爲 799,大小爲 10gb。所以,咱們將編寫算法來處理大小爲 512 x 512 的圖塊,該圖塊只有 150 KB。
各個圖塊能夠映射爲在多處理/多線程(即分佈式基礎結構)上運行,而後再縫合在一塊兒即得到完整的分段圖像。咱們不介紹具體的縫合方法。簡而言之,拼接涉及對整個矩陣的索引並根據該索引將圖塊從新組合。可使用map-reduce進行,Map-Reduce的指標例如全部圖塊的全部F1分數之和等。咱們只需將結果添加到列表中,而後執行統計摘要便可。
左側的黑色橢圓形結構是血管,其他的是組織。所以,此數據集中的兩個類是:
• 前景(船隻)—標記爲255
• 背景(組織)—標記爲0
右下方的最後一個圖像是真實圖像。經過繪製輪廓並填充輪廓以手動方式對其進行追蹤,經過病理學家得到真實狀況。咱們可使用專家提供的相似示例來訓練深度學習網絡,並進行大規模驗證。咱們還能夠經過將這些示例提供給其餘平臺並讓他們以更大的比例手動跟蹤一組不一樣的圖像以進行驗證和培訓來擴充數據。
grayscale = scipy.misc.imread('grayscale.png')grayscale = 255 - grayscalegroundtruth = scipy.misc.imread('groundtruth.png')plt.subplot(1, 3, 1)plt.imshow(255 - grayscale, cmap='gray')plt.title('grayscale')plt.axis('off')plt.subplot(1, 3, 2)plt.imshow(grayscale, cmap='gray')plt.title('inverted grayscale')plt.axis('off')plt.subplot(1, 3, 3)plt.imshow(groundtruth, cmap='gray')plt.title('groundtruth binary')plt.axis('off')
前處理
在分割數據以前,咱們應該檢查一下數據集,以肯定是否存在因爲成像系統而形成了僞影。在此示例中,咱們僅討論一個圖像。經過查看圖像,咱們能夠看到沒有任何明顯的僞影會干擾分割。可是,小夥伴們可使用中值濾鏡消除離羣值噪聲並平滑圖像。中值過濾器用中值(在給定大小的內核內)替換離羣值。
內核大小3的中值過濾器
median_filtered = scipy.ndimage.median_filter(grayscale, size=3) plt.imshow(median_filtered, cmap='gray') plt.axis('off') plt.title("median filtered image")
要肯定哪一種閾值技術最適合分割,咱們能夠先經過閾值肯定是否存在將這兩個類別分開的獨特像素強度。在這種狀況下,可使用經過目視檢查得到的強度對圖像進行二值化處理。咱們使用的圖像許多像素的強度小於50,這些像素與反轉灰度圖像中的背景類別相對應。
儘管類別的分佈不是雙峯的,但仍然在前景和背景之間有所區別,在該區域中,較低強度的像素達到峯值,而後到達谷底。能夠經過各類閾值技術得到該精確值。分割部分將詳細研究一種這樣的方法。
可視化像素強度的直方圖
counts, vals = np.histogram(grayscale, bins=range(2 ** 8)) plt.plot(range(0, (2 ** 8) - 1), counts) plt.title("Grayscale image histogram") plt.xlabel("Pixel intensity") plt.ylabel("Count")
分割
去除噪聲後,咱們能夠用skimage濾波器模塊對全部閾值的結果進行比較,來肯定所須要使用的像素。有時,在圖像中,其像素強度的直方圖不是雙峯的。所以,可能會有另外一種閾值方法能夠比基於閾值形狀在內核形狀中進行閾值化的自適應閾值方法更好。Skimage中的函數能夠方便看到不一樣閾值的處理結果。
嘗試全部閾值
result = skimage.filters.thresholding.try_all_threshold(median_filtered)
最簡單的閾值處理方法是爲圖像使用手動設置的閾值。可是在圖像上使用自動閾值方法能夠比人眼更好地計算其數值,而且能夠輕鬆複製。對於本例中的圖像,彷佛Otsu,Yen和Triangle方法的效果很好。
在本文中,咱們將使用Otsu閾值技術將圖像分割成二進制圖像。Otsu經過計算一個最大化類別間方差(前景與背景之間的方差)並最小化類別內方差(前景內部的方差或背景內部的方差)的值來計算閾值。若是存在雙峯直方圖(具備兩個不一樣的峯)或閾值能夠更好地分隔類別,則效果很好。
Otsu閾值化和可視化
threshold = skimage.filters.threshold_otsu(median_filtered) print("Threshold value is {}".format(threshold)) predicted = np.uint8(median_filtered > threshold) * 255 plt.imshow(predicted, cmap='gray') plt.axis('off') plt.title("otsu predicted binary image")
若是上述簡單技術不能用於圖像的二進制分割,則可使用UNet,帶有FCN的ResNet或其餘各類受監督的深度學習技術來分割圖像。要去除因爲前景噪聲分段而產生的小物體,也能夠考慮嘗試skimage.morphology.remove_objects()。
驗證方式
通常狀況下,咱們都須要由具備圖像類型專長的人員手動生成基本事實,來驗證準確性和其餘指標,並查看圖像的分割程度。
confusion矩陣
咱們sklearn.metrics.confusion_matrix()用來獲取該矩陣元素,以下所示。假設輸入是帶有二進制元素的元素列表,則Scikit-learn混淆矩陣函數將返回混淆矩陣的4個元素。對於一切都是一個二進制值(0)或其餘(1)的極端狀況,sklearn僅返回一個元素。咱們包裝了sklearn混淆矩陣函數,並編寫了咱們本身的這些邊緣狀況,以下所示:
get_confusion_matrix_elements()
def get_confusion_matrix_elements(groundtruth_list, predicted_list): """returns confusion matrix elements i.e TN, FP, FN, TP as floats See example code for helper function definitions """ _assert_valid_lists(groundtruth_list, predicted_list)
if _all_class_1_predicted_as_class_1(groundtruth_list, predicted_list) is True: tn, fp, fn, tp = 0, 0, 0, np.float64(len(groundtruth_list))
elif _all_class_0_predicted_as_class_0(groundtruth_list, predicted_list) is True: tn, fp, fn, tp = np.float64(len(groundtruth_list)), 0, 0, 0
else: tn, fp, fn, tp = sklearn.metrics.confusion_matrix(groundtruth_list, predicted_list).ravel() tn, fp, fn, tp = np.float64(tn), np.float64(fp), np.float64(fn), np.float64(tp)
return tn, fp, fn, tp
準確性
![](http://static.javashuo.com/static/loading.gif)
get_accuracy()
def get_accuracy(groundtruth_list, predicted_list):
fp, fn, tp = get_confusion_matrix_elements(groundtruth_list, predicted_list) total = tp + fp + fn + tn accuracy = (tp + tn) / total return accuracy
它在0到1之間變化,0是最差的,1是最好的。若是算法將全部東西都檢測爲整個背景或前景,那麼仍然會有很高的準確性。所以,咱們須要一個考慮班級人數不平衡的指標。特別是因爲當前圖像比背景0具備更多的前景像素(類1)。
F1分數從0到1不等,計算公式爲:
0是最差的預測,而1是最好的預測。如今,考慮邊緣狀況,處理F1分數計算。
get_f1_score()
def get_f1_score(groundtruth_list, predicted_list): """Return f1 score covering edge cases"""
tn, fp, fn, tp = get_confusion_matrix_elements(groundtruth_list, predicted_list) if _all_class_0_predicted_as_class_0(groundtruth_list, predicted_list) is True: f1_score = 1 elif _all_class_1_predicted_as_class_1(groundtruth_list, predicted_list) is True: f1_score = 1 else: f1_score = (2 * tp) / ((2 * tp) + fp + fn)
return f1_score
高於0.8的F1分數被認爲是良好的F1分數,代表預測表現良好。
客戶中心
MCC表明馬修斯相關係數,其計算公式爲:
它位於-1和+1之間。-1是實際狀況與預測之間絕對相反的相關性,0是隨機結果,其中某些預測匹配,而+1是實際狀況與預測之間絕對匹配,保持正相關。所以,咱們須要更好的驗證指標,例如MCC。
在MCC計算中,分子僅由四個內部單元(元素的叉積)組成,而分母由混淆矩陣的四個外部單元(點的積)組成。在分母爲0的狀況下,MCC將可以注意到咱們的分類器方向錯誤,而且會經過將其設置爲未定義的值(即numpy.nan)進行警告。可是,爲了得到有效值,並可以在必要時對不一樣圖像平均MCC,咱們將MCC設置爲-1(該範圍內最差的值)。其餘邊緣狀況包括將MCC和F1分數設置爲1的全部正確檢測爲前景和背景的元素。不然,將MCC設置爲-1且F1分數爲0。
想要了解有關MCC和邊緣案例,以及MCC爲何比準確性或F1分數更好,能夠閱讀下面這篇文章:
https://lettier.github.io/posts/2016-08-05-matthews-correlation-coefficient.html
https://en.wikipedia.org/wiki/Matthews_correlation_coefficient#Advantages_of_MCC_over_accuracy_and_F1_score
get_mcc()
def get_mcc(groundtruth_list, predicted_list): """Return mcc covering edge cases"""
tn, fp, fn, tp = get_confusion_matrix_elements(groundtruth_list, predicted_list) if _all_class_0_predicted_as_class_0(groundtruth_list, predicted_list) is True: mcc = 1 elif _all_class_1_predicted_as_class_1(groundtruth_list, predicted_list) is True: mcc = 1 elif _all_class_1_predicted_as_class_0(groundtruth_list, predicted_list) is True: mcc = -1 elif _all_class_0_predicted_as_class_1(groundtruth_list, predicted_list) is True : mcc = -1
elif _mcc_denominator_zero(tn, fp, fn, tp) is True: mcc = -1
# Finally calculate MCC else: mcc = ((tp * tn) - (fp * fn)) / ( np.sqrt((tp + fp) * (tp + fn) * (tn + fp) * (tn + fn))) return mcc
最後,咱們能夠按結果並排比較驗證指標。
> validation_metrics = get_validation_metrics(groundtruth, predicted) {'mcc': 0.8533910225863214, 'f1_score': 0.8493358633776091, 'tp': 5595.0, 'fn': 1863.0, 'fp': 122.0, 'accuracy': 0.9924278259277344, 'tn': 254564.0}
精度接近1,由於示例圖像中有不少背景像素可被正確檢測爲背景(即,真實的底片天然更高)。這說明了爲何精度不是二進制分類的好方法。
F1分數是0.84。所以,在這種狀況下,咱們可能不須要用於二進制分割的更復雜的閾值算法。若是堆棧中的全部圖像都具備類似的直方圖分佈和噪聲,則可使用Otsu並得到至關不錯的預測結果。
所述MCC 0.85高時,也表示地面實況和預測圖像具備高的相關性,從在上一節的預測圖像圖片清楚地看到。
如今,讓咱們可視化並查看混淆矩陣元素TP,FP,FN,TN在圖像周圍的分佈位置。它向咱們顯示了在不存在閾值(FP)的狀況下閾值正在拾取前景(容器),在未檢測到真實血管的位置(FN),反之亦然。
驗證可視化
爲了可視化混淆矩陣元素,咱們精確地找出混淆矩陣元素在圖像中的位置。例如,咱們發現TP陣列(即正確檢測爲前景的像素)是經過找到真實狀況和預測陣列的邏輯「與」。一樣,咱們使用邏輯布爾運算一般稱爲FP,FN,TN數組。
get_confusion_matrix_intersection_mats()
def get_confusion_matrix_intersection_mats(groundtruth, predicted): """ Returns dict of 4 boolean numpy arrays with True at TP, FP, FN, TN """
confusion_matrix_arrs = {}
groundtruth_inverse = np.logical_not(groundtruth) predicted_inverse = np.logical_not(predicted)
confusion_matrix_arrs['tp'] = np.logical_and(groundtruth, predicted) confusion_matrix_arrs['tn'] = np.logical_and(groundtruth_inverse, predicted_inverse) confusion_matrix_arrs['fp'] = np.logical_and(groundtruth_inverse, predicted) confusion_matrix_arrs['fn'] = np.logical_and(groundtruth, predicted_inverse)
return confusion_matrix_arrs
而後,咱們能夠將每一個數組中的像素映射爲不一樣的顏色。對於下圖,咱們將TP,FP,FN,TN映射到CMYK(青色,品紅色,黃色,黑色)空間。一樣能夠將它們映射到(綠色,紅色,紅色,綠色)顏色。而後,咱們將得到一張圖像,其中全部紅色均表示錯誤的預測。CMYK空間使咱們可以區分TP,TN。
get_confusion_matrix_overlaid_mask()
def get_confusion_matrix_overlaid_mask(image, groundtruth, predicted, alpha, colors): """ Returns overlay the 'image' with a color mask where TP, FP, FN, TN are each a color given by the 'colors' dictionary """ image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) masks = get_confusion_matrix_intersection_mats(groundtruth, predicted) color_mask = np.zeros_like(image) for label, mask in masks.items(): color = colors[label] mask_rgb = np.zeros_like(image) mask_rgb[mask != 0] = color color_mask += mask_rgb return cv2.addWeighted(image, alpha, color_mask, 1 - alpha, 0)
alpha = 0.5confusion_matrix_colors = { 'tp': (0, 255, 255), #cyan 'fp': (255, 0, 255), #magenta 'fn': (255, 255, 0), #yellow 'tn': (0, 0, 0) #black }validation_mask = get_confusion_matrix_overlaid_mask(255 - grayscale, groundtruth, predicted, alpha, confusion_matrix_colors)print('Cyan - TP')print('Magenta - FP')print('Yellow - FN')print('Black - TN')plt.imshow(validation_mask)plt.axis('off')plt.title('confusion matrix overlay mask')
咱們在此處使用OpenCV將此顏色蒙版做爲透明層覆蓋到原始(非反轉)灰度圖像上。這稱爲Alpha合成:
總結
存儲庫中的最後兩個示例經過調用測試函數來測試邊緣狀況和在小的數組(少於10個元素)上的隨機預測場景。若是咱們測試該算法的簡單邏輯,則測試邊緣狀況和潛在問題很重要。
Travis CI對於測試咱們的代碼是否能夠在需求中描述的模塊版本上工做以及在新更改合併到主版本中時全部測試經過均很是有用。最佳作法是保持代碼整潔,文檔完善,並對全部語句進行單元測試和覆蓋。這些習慣限制了在複雜的算法創建在可能已經進行了單元測試的簡單功能塊之上時,消除錯誤的需求。一般,文檔和單元測試可幫助其餘人隨時瞭解功能意圖。整理有助於提升代碼的可讀性,而flake8是實現此目的的良好Python包。
如下是本文的重要內容:
1. 適用於內存中不適合的數據的拼接和拼接方法
2. 嘗試不一樣的閾值技術
3. 驗證指標的精妙之處
4. 驗證可視化
5. 最佳實踐
交流羣
歡迎加入公衆號讀者羣一塊兒和同行交流,目前有SLAM、三維視覺、傳感器、自動駕駛、計算攝影、檢測、分割、識別、醫學影像、GAN、算法競賽等微信羣(之後會逐漸細分),請掃描下面微信號加羣,備註:」暱稱+學校/公司+研究方向「,例如:」張三 + 上海交大 + 視覺SLAM「。請按照格式備註,不然不予經過。添加成功後會根據研究方向邀請進入相關微信羣。請勿在羣內發送廣告,不然會請出羣,謝謝理解~
本文分享自微信公衆號 - 小白學視覺(NoobCV)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。