Spaces:
Sleeping
Sleeping
File size: 2,471 Bytes
99e1f27 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | """Binary change-mask evaluation metrics.
All functions take boolean / 0-or-255 / 0-or-1 masks of identical shape and
return plain Python floats so results are easy to log and serialize. These are
the standard pixel-wise change-detection metrics: IoU, Dice/F1, precision,
recall, pixel accuracy and the false-positive / false-negative rates.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
import numpy as np
def _to_bool(mask: np.ndarray) -> np.ndarray:
arr = np.asarray(mask)
if arr.dtype == bool:
return arr
return arr > (127 if arr.max() > 1 else 0)
@dataclass
class ConfusionCounts:
tp: int
fp: int
fn: int
tn: int
def confusion_counts(pred: np.ndarray, gt: np.ndarray) -> ConfusionCounts:
"""Pixel-wise true/false positive/negative counts for two binary masks."""
p = _to_bool(pred)
g = _to_bool(gt)
if p.shape != g.shape:
raise ValueError(f"shape mismatch: pred {p.shape} vs gt {g.shape}")
tp = int(np.sum(p & g))
fp = int(np.sum(p & ~g))
fn = int(np.sum(~p & g))
tn = int(np.sum(~p & ~g))
return ConfusionCounts(tp=tp, fp=fp, fn=fn, tn=tn)
def _safe_div(num: float, den: float) -> float:
return float(num) / float(den) if den else 0.0
def binary_metrics(pred: np.ndarray, gt: np.ndarray) -> dict:
"""Return IoU, Dice/F1, precision, recall, accuracy, FPR and FNR.
Edge case: when ground truth has no positive pixels and the prediction is
also empty, IoU/Dice/precision/recall are defined as 1.0 (perfect).
"""
c = confusion_counts(pred, gt)
tp, fp, fn, tn = c.tp, c.fp, c.fn, c.tn
if (tp + fp + fn) == 0:
precision = recall = iou = dice = 1.0
else:
precision = _safe_div(tp, tp + fp)
recall = _safe_div(tp, tp + fn)
iou = _safe_div(tp, tp + fp + fn)
dice = _safe_div(2 * tp, 2 * tp + fp + fn)
f1 = _safe_div(2 * precision * recall, precision + recall) if (precision + recall) else dice
accuracy = _safe_div(tp + tn, tp + fp + fn + tn)
fpr = _safe_div(fp, fp + tn)
fnr = _safe_div(fn, fn + tp)
return {
"iou": round(iou, 4),
"dice": round(dice, 4),
"f1": round(f1, 4),
"precision": round(precision, 4),
"recall": round(recall, 4),
"pixelAccuracy": round(accuracy, 4),
"falsePositiveRate": round(fpr, 4),
"falseNegativeRate": round(fnr, 4),
"counts": asdict(c),
}
|