影像分割 Image Segmentation 評估指標總覽

李謦伊
謦伊的閱讀筆記
21 min readDec 1, 2022

當在衡量模型的表現結果 (performance) 時,會藉由評估指標來進行,本文將要介紹在影像分割 (Image Segmentation) 任務上經常使用的評估指標,並進行實作,所有 code 會放置在文章下方。

Confusion Matrix

混淆矩陣 (Confusion Matrix) 是由 TP、FP、FN、TN 四種狀況所組合而成,可以很清楚地反映各類別之間被劃分的關係,並且藉由 confusion matrix 可以再延伸出其他評估指標。

來看一下 TP、FP、FN、TN 的定義,假設我們要預測的類別有兩個類別,分別為有病 (label=1) 以及沒病 (label=0)

  • TP (True Positive,真陽性):該類別實際為有病,預測也為有病,表示檢測正確
  • FP (False Positive,真陰性):該類別實際為沒有病,預測成有病,表示誤檢
  • FN (False Negative,假陰性):該類別實際為有病,預測為沒有病,表示漏檢
  • TN (True Negative,假陽性):該類別實際為沒有病,預測也為沒有病,即表示不需要被檢測的地方沒被檢測出來

接著,來試著實作看看吧!假設我們的圖像資料為 9 個 pixel,實際 label 如左圖 (稱為 Ground Truth)、預測 label 如右圖。

用程式碼表示如下

ground_truth = np.array([1, 1, 0, 1, 0, 0, 1, 1, 0])
pred_label = np.array([1, 0, 0, 1, 1, 0, 1, 1, 1])
  • 計算 TP

先比對 Ground Truth 與 Predict 的每個 pixel,若該 pixel 滿足兩者相同的條件則賦值為 1、反之為 0,也就是要找出實際為真、預測也為真或是實際為假、預測也為假的 pixel。

接著將上述的輸出值與 Ground Truth 的每個 pixel 相乘再加總,由於 Ground Truth 為有病的 pixel 為 1,因此這樣的計算能得到所有 Ground Truth 為有病、Predict 也為有病的數量,即 TP (實際為真、預測也為真)。

### tp
# 所有實際與預測相同的值
gt_equal_pred = np.where(ground_truth==pred_label, 1, 0)

# 實際值與預測值相同,並且實際為真
tp = np.sum(gt_equal_pred * ground_truth)
  • 計算 FP

從 confusion matrix 中可得知所有預測為真的數量為 TP 和 FP 的加總,因此可以先計算 pixel 預測為 1 的數量,再減去 TP。

### fp
# 所有預測為真的數量去除掉 TP
fp = np.sum(pred_label) - tp
  • 計算 FN

一樣藉由 confusion matrix 得知所有實際為真的數量為 TP 和 FN 的加總,因此可以先計算 Ground Truth 中 pixel 為 1 的數量,再減去 TP。

### fn
# 所有實際為真的數量去除掉 TP
fn = np.sum(ground_truth) - tp
  • 計算 TN

跟 TP 的計算一樣,要先找出實際與預測皆相同的 pixel。接著將所有 Ground Truth 為沒有病的 pixel 賦值為 1、反之為 0。最後將兩者的輸出相乘再加總,就能得到所有 Ground Truth 為沒有病、Predict 也沒有病的數量,即 TN (實際為假、預測也為假)。

### tn
# 所有實際與預測相同的值
gt_equal_pred = np.where(ground_truth==pred_label, 1, 0)

# 所有實際為假的值
gt_is_false = np.where(np.logical_not(ground_truth), 1, 0)

# 實際值與預測值相同,並且預測為假
tn = np.sum(gt_equal_pred * gt_is_false)

計算結果如下圖所示,粉紅框為 TP,數量為 4;紫色框為 FP,數量為 2;藍色框為 FN,數量為 1;沒有框的為 TN,數量為 2。

也可以使用 sklearn 來計算 confusion matrix

from sklearn.metrics import confusion_matrix
tn, fp, fn, tp = confusion_matrix(ground_truth, pred_label).ravel()

TP、FP、FN、TN

那 TP、FP、FN、TN 是怎麼得到的呢?我們知道模型預測的輸出表示為可能屬於各個類別的機率值,當某個類別的機率值越大,表示模型認為屬於該類別的可能性越大。接著我們需要從該機率值來判定要歸類給哪個類別,後就可以來計算各個類別的 TP、FP、FN、TN。

以二分類模型為例,預測是否有病。下圖左為圖像中每個 pixel 的預測結果,此時要設定一個閾值 (threshold) 來表示是否要將該 pixel 歸類為有病。

假設 threshold 設定為 0.5,若該 pixel 的預測結果大於 threshold,則將其歸類為有病,反之則歸類為沒病。

來看一下實際圖片在影像分割任務上的 TP、FP、FN、TN 會更清楚,左圖為 Ground Truth、右圖為 Prediction 結果。中間黃色區塊指 Ground Truth 為真、Prediction 也為真的狀況,表示為 TP;左邊藍色斜線指 Ground Truth 為假、Prediction 為真的狀況,表示為 FP;右邊紅色斜線指 Ground Truth 為真、Prediction 為假的狀況,表示為 FN;白色斜線 (背景) 指 Ground Truth 為假、Prediction 也為假的狀況,表示為 TN。

IoU (Intersection over Union)

IoU 是指 Ground Truth 與 Prediction 之間的交集除以聯集,由下圖的公式可以計算每個類別的 IoU。

以上述的例子:tp = 4、fn = 1、fp = 2、tn = 2 來計算 IoU。

iou = tp / (tp + fp + fn)
print("iou:", iou)

# ===================
iou: 0.5714285714285714

Precision、Recall

Precision 表示精確率, 定義為所有預測為真的數據中,實際也為真的機率;Recall 表示召回率,定義為所有實際為真的數據中,預測也為真的機率。兩者都是屬於關注於正樣本的指標,差別在於關注角度不同。

使用時機在於:當 FP 的代價很高,期望盡可能地降低誤檢,應著重提高 Precision;當 FN 的代價很高,期望盡可能地降低漏檢,應著重提高 Recall。

下圖為 Precision、Recall 的公式及在 confusion matrix 的關係。

接著一樣來進行實作,假設我們的圖像 Prediction 和 Ground Truth 如下圖,threshold 設定為 0.5。

藉由 threshold 以得到 Prediction 右邊的結果。

ground_truth = np.array([1, 1, 0, 1, 0, 0, 1, 1, 0])
pred_score = np.array([0.8, 0.4, 0.1, 0.7, 0.6, 0.2, 0.9, 0.8, 0.6])
threshold = 0.5
pred_label = np.where(pred_score > threshold, 1, 0)
print("pred_label:", pred_label)

# =======================
pred_label: [1 0 0 1 1 0 1 1 1]

將 Prediction 結果與 Ground Truth 進行 TP、FP、FN、TN 的計算後,就能夠計算 Precision、Recall。

precision = tp / (tp + fp)
print("precision:", precision)
recall = tp / (tp + fn)
print("recall:", recall)

# =======================
precision: 0.6666666666666666
recall: 0.8

F1-score

當 Precision 和Recall 難以取捨、需要考慮到兩者時,可以使用 F1-score, 是Precision 及 Recall的調和平均數。

f1_score = 2*tp / (2*tp + fp + fn)
print("f1_score:", f1_score)

# =======================
f1_score: 0.7272727272727273

F-measure (F-score)

若需要對 Precision、Recall 取不同的權重,則可以將 F1-score 進行擴展,公式如下,當 β²=1 時,就會變為 F1-score。

Sensitivity、Specificity

Sensitivity 表示靈敏度,又稱真陽性率、Recall,定義為所有實際為真的數據中,預測也為真的機率;Specificity 表示特異度,又稱真陰性率,定義為所有實際為假的數據中,預測也為假的機率。

在醫學上經常使用這兩個指標,Sensitivity 指有多少真正得病的人被診斷出有得病,而 Specificity 指有多少沒有得病的人被檢驗正確 (沒得病)。

下圖為 Sensitivity、Specificity 的公式及在 confusion matrix 的關係。

以 Precision、Recall 的例子來計算 Sensitivity 和 Specificity 會得到以下結果。

sensitivity = tp / (tp + fn)
print("sensitivity:", sensitivity)
specificity = tn / (tn + fp)
print("specificity:", specificity)

# =======================
sensitivity: 0.8
specificity: 0.5

P-R Curve

P-R Curve 是以 Recall 為 x 軸、 Precision 為 y 軸所繪製出來的圖,P-R Curve 下的面積 (Area under curve,AUC) 稱為 AP (Average Precision),範圍介於 0 ~ 1 之間。

當 Precision 和 Recall 值越高,P-R Curve 會越往右上方靠近,此時 AUC 越大,表示模型越好。

source

那我們來計算並畫出 P-R Curve 吧!

  • 第一步,整理成表格

一樣以二分類模型為例,下圖為是否有病的 Ground Truth 與 Prediction 結果,先將這些數據整理成表格。

ground_truth = np.array([1, 1, 0, 1, 0, 0, 1, 1, 0])
pred_score = np.array([0.8, 0.4, 0.1, 0.7, 0.6, 0.2, 0.9, 0.8, 0.6])
  • 第二步,排序

根據預測結果 (score) 由高至低排序後,令 threshold = 0.5,若 score 大於 threshold 設定為 1,反之則為 0。

threshold = 0.5
zip_list = zip(ground_truth, pred_score)
sort_zip_list = sorted(zip_list, key=lambda x:x[1], reverse=True)
zip_list = zip(*sort_zip_list)
ground_truth, pred_score = [np.array(list(x)) for x in zip_list]
print("ground_truth: ", ground_truth)
print("pred_score: ", pred_score)

pred_label = np.where(pred_score>=threshold, 1, 0)
print("pred_label: ", pred_label)

# =======================
ground_truth: [1 1 1 1 0 0 1 0 0]
pred_score: [0.9 0.8 0.8 0.7 0.6 0.6 0.4 0.2 0.1]
pred_label: [1 1 1 1 1 1 0 0 0]
  • 第三步,歸類 TP、FP、FN、TN

再對照 Ground Truth 來分別歸類各個 pixel 為 TP、FP、FN、TN。

def cal_state_list(ground_truth, pred_label):
nd = len(ground_truth)
tp_list = [0] * nd
tn_list = [0] * nd
fn_list = [0] * nd
fp_list = [0] * nd
for i in range(len(ground_truth)):
gt = ground_truth[i]
pred = pred_label[i]

if gt == 1 and pred == 1:
tp_list[i] = 1

elif gt == 1 and pred == 0:
fn_list[i] = 1

elif gt == 0 and pred == 1:
fp_list[i] = 1

else:
tn_list[i] = 1

return tp_list, tn_list, fn_list, fp_list

tp_list, tn_list, fn_list, fp_list = cal_state_list(ground_truth, pred_label)
tp_fn_len = np.sum(fn_list) + np.sum(tp_list)

print("tp_list: ", tp_list)
print("tn_list: ", tn_list)
print("fn_list: ", fn_list)
print("fp_list: ", fp_list)

# =======================
tp_list: [1, 1, 1, 1, 0, 0, 0, 0, 0]
tn_list: [0, 0, 0, 0, 0, 0, 0, 1, 1]
fn_list: [0, 0, 0, 0, 0, 0, 1, 0, 0]
fp_list: [0, 0, 0, 0, 1, 1, 0, 0, 0]
  • 第四步,進行計算

然後就可以來開始計算啦~從 rank = 1 開始,代入公式計算 Precision、Recall。要注意的是,分子是累計的,因此 Precision 和 Recall 的分子為皆 1,而不是 4。

計算完 rank 1 後,繼續算 rank = 2。

以此類推,就能夠算出所有 rank 的 Precision 和 Recall。

cumsum = 0
for idx, val in enumerate(fp_list):
fp_list[idx] += cumsum
cumsum += val

cumsum = 0
for idx, val in enumerate(tp_list):
tp_list[idx] += cumsum
cumsum += val

recall = tp_list[:]
for idx, val in enumerate(tp_list):
recall[idx] = float(tp_list[idx]) / tp_fn_len

precision = tp_list[:]
for idx, val in enumerate(tp_list):
precision[idx] = float(tp_list[idx]) / (fp_list[idx] + tp_list[idx])

print("recall: ", recall)
print("precision: ", precision)

# =======================
recall: [0.2, 0.4, 0.6, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8]
precision: [1.0, 1.0, 1.0, 1.0, 0.8, 0.6666666666666666, 0.6666666666666666, 0.6666666666666666, 0.6666666666666666]
  • 第五步,畫 P-R Curve

藍色的線是畫圖的結果,P-R Curve 下的面積稱為 AP (Average Precision),即紫色面積的部分。

plt.title('PR Curve', fontsize=15)
plt.xlabel("Recall", fontsize=15)
plt.ylabel("Precision", fontsize=15)
plt.xlim(0, 1.05)
plt.ylim(0.5, 1.05)
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
plt.scatter(recall, precision)
plt.plot(recall, precision)
plt.show()

ROC Curve

ROC Curve (Receiver Operating Characteristic Curve) 是以 FPR 為 x 軸、 TPR 為 y 軸所繪製出來的圖,其中 FPR (False Positive Rate,偽陽性率) 是指 1- Specificity、TPR (True Positive Rate,真陽性率) 指 Sensitivity。ROC Curve 下的面積 (Area under curve,AUC) 範圍也是介於 0 ~ 1 之間。

當 FPR 越低 (Specificity — 正確檢測出負樣本的機率越高)、TPR 越高,ROC Curve 會越往左上方靠近,此時 AUC 越大,表示模型越好。

source

接下來,一樣來計算並畫出 ROC Curve 吧!

  • 第一步,整理成表格

一樣以二分類模型為例,下圖為是否有病的 Ground Truth 與 Prediction 結果,先將這些數據整理成表格。

ground_truth = np.array([1, 1, 0, 1, 0, 0, 1, 1, 0])
pred_score = np.array([0.8, 0.4, 0.1, 0.7, 0.6, 0.2, 0.9, 0.8, 0.6])
  • 第二步,排序

根據預測結果 (score) 由高至低排序。

zip_list = zip(ground_truth, pred_score)
sort_zip_list = sorted(zip_list, key=lambda x:x[1], reverse=True)
zip_list = zip(*sort_zip_list)
ground_truth, pred_score = [np.array(list(x)) for x in zip_list]
print("ground_truth: ", ground_truth)
print("pred_score: ", pred_score)

# =======================
ground_truth: [1 1 1 1 0 0 1 0 0]
pred_score: [0.9 0.8 0.8 0.7 0.6 0.6 0.4 0.2 0.1]
  • 第三步,根據 rank 來歸類 TP、FP、FN、TN

這邊跟 P-R Curve 不同,會根據 rank 分別進行計算。設定該 rank 的 score 作為 threshold 以歸類每個 pixel 為 0 或 1,再對照 Ground Truth 求得 TP、FP、FN、TN。

從 rank =1 開始,將 score = 0.9 作為 threshold 來歸類每個 pixel。

  • 第四步,進行計算

然後就可以來開始計算啦~代入公式計算 TPR、FPR,分子一樣是使用累計的方式計算。

以此類推,將 rank 1~9 重複進行第三、四步驟,以得到所有 rank 的 TPR 和 FPR。

  • 第五步,畫 ROC Curve

藍色的線是畫圖的結果,ROC Curve 下的面積為 AUC (Area under curve),即紫色面積的部分。

plt.title('ROC Curve', fontsize=15)
plt.xlabel("FPR", fontsize=15)
plt.ylabel("TPR", fontsize=15)
plt.xlim(0, 1)
plt.ylim(0, 1.05)
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
plt.scatter(fpr, tpr)
plt.plot(fpr, tpr)
plt.show()

Dice

在語義分割任務中,經常使用 Dice Loss 作為 loss function,其公式如下:

其中 Dice 係數 (coefficient) 是用於計算 Ground Truth 和 prediction 之間的相似度,|𝑋|、|𝑌| 分別表示為 Ground Truth 和 prediction,其值範圍在 0 ~ 1 之間。

將 Dice 係數公式展開會發現與 F1-score 相同。

接下來試著計算 Dice 係數吧!

假設我們的圖像資料為 9 個 pixel,實際 label 如左圖 (稱為 Ground Truth)、預測 label 如右圖。

用程式碼表示如下

ground_truth = np.array([1, 1, 0, 1, 0, 0, 1, 1, 0])
pred_label = np.array([1, 0, 0, 1, 1, 0, 1, 1, 1])

計算 TP、FP、FN、TN 以及 Dice 係數

### tp
# 所有實際與預測相同的值
gt_equal_pred = np.where(ground_truth==pred_label, 1, 0)

# 實際值與預測值相同,並且實際為真
tp = np.sum(gt_equal_pred * ground_truth)

### fn
# 所有實際為真的數量去除掉 TP
fn = np.sum(ground_truth) - tp

### fp
# 所有預測為真的數量去除掉 TP
fp = np.sum(pred_label) - tp

### tn
# 所有實際為假的值
gt_is_false = np.where(np.logical_not(ground_truth), 1, 0)

# 實際值與預測值相同,並且預測為假
tn = np.sum(gt_equal_pred * gt_is_false)

dice = 2*tp / (2*tp + fn + fp)
print("dice: ", dice)

# =======================
dice: 0.7272727272727273

--

--