-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmetrics.py
More file actions
104 lines (88 loc) · 3.68 KB
/
Copy pathmetrics.py
File metadata and controls
104 lines (88 loc) · 3.68 KB
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import numpy as np
import torch
CLASS_NAMES = ["soil", "bedrock", "sand", "big_rock"]
NUM_CLASSES = len(CLASS_NAMES)
IGNORE_INDEX = 255
def update_confusion_matrix(cm, preds, labels, num_classes=NUM_CLASSES, ignore_index=IGNORE_INDEX):
"""In-place update of a (num_classes x num_classes) numpy confusion matrix.
Rows are true classes, columns are predicted classes.
Pixels where label == ignore_index are excluded.
"""
valid = labels != ignore_index
p = preds[valid].astype(np.int64).ravel()
l = labels[valid].astype(np.int64).ravel()
idx = l * num_classes + p
counts = np.bincount(idx, minlength=num_classes * num_classes)
cm += counts.reshape(num_classes, num_classes)
return cm
def update_confusion_matrix_torch(cm, preds, labels,
num_classes=NUM_CLASSES, ignore_index=IGNORE_INDEX):
"""In-place update of a (num_classes x num_classes) torch confusion matrix.
Stays on the same device as `cm` (so train/val accumulation can stay on the
GPU and avoid a per-batch CPU transfer). `preds` and `labels` must be int
tensors on the same device as `cm`. Rows = true class, columns = predicted
class. Pixels where label == ignore_index are excluded.
"""
valid = labels != ignore_index
p = preds[valid].long().view(-1)
l = labels[valid].long().view(-1)
indices = l * num_classes + p
binc = torch.bincount(indices, minlength=num_classes * num_classes)
cm += binc.view(num_classes, num_classes)
return cm
def compute_metrics(cm):
"""Derive all summary metrics from a (num_classes x num_classes) confusion matrix.
Returns a dict with per-class arrays (iou, precision, recall, f1, support)
and scalar aggregates (pixel_acc, miou, macro_precision, macro_recall,
macro_f1). Aggregates are computed over classes with support > 0.
"""
cm = cm.astype(np.float64)
tp = np.diag(cm)
fn = cm.sum(axis=1) - tp
fp = cm.sum(axis=0) - tp
eps = 1e-12
iou = tp / (tp + fp + fn + eps)
precision = tp / (tp + fp + eps)
recall = tp / (tp + fn + eps)
f1 = 2.0 * precision * recall / (precision + recall + eps)
support = cm.sum(axis=1)
total = cm.sum()
pixel_acc = tp.sum() / (total + eps)
valid_classes = support > 0
if valid_classes.any():
miou = float(iou[valid_classes].mean())
macro_precision = float(precision[valid_classes].mean())
macro_recall = float(recall[valid_classes].mean())
macro_f1 = float(f1[valid_classes].mean())
else:
miou = macro_precision = macro_recall = macro_f1 = 0.0
return {
"iou": iou,
"precision": precision,
"recall": recall,
"f1": f1,
"support": support,
"pixel_acc": float(pixel_acc),
"miou": miou,
"macro_precision": macro_precision,
"macro_recall": macro_recall,
"macro_f1": macro_f1,
"valid_classes": valid_classes,
}
def format_report(cm, title=""):
m = compute_metrics(cm)
iou, prec, rec, sup = m["iou"], m["precision"], m["recall"], m["support"]
lines = []
if title:
lines.append(f"=== {title} ===")
lines.append(f" pixel accuracy : {m['pixel_acc']:.4f}")
lines.append(f" mean IoU : {m['miou']:.4f} (over labelled classes only)")
lines.append("")
lines.append(f" {'class':<10} {'IoU':>7} {'Prec':>7} {'Recall':>7} {'support':>15}")
for i, name in enumerate(CLASS_NAMES):
s = int(sup[i])
if s == 0:
lines.append(f" {name:<10} {'--':>7} {'--':>7} {'--':>7} {s:>15,} (no test pixels)")
else:
lines.append(f" {name:<10} {iou[i]:7.4f} {prec[i]:7.4f} {rec[i]:7.4f} {s:>15,}")
return "\n".join(lines)