BSB_Sim/src/sigverify/evaluation/metrics.py
2026-10-19 08:11:33 +02:00

37 lines
1.4 KiB
Python

"""Threshold-independent and threshold-dependent verification metrics."""
import numpy as np
from numpy.typing import ArrayLike
from sklearn.metrics import det_curve, roc_curve
def verification_metrics(labels: ArrayLike, scores: ArrayLike) -> dict[str, object]:
"""Compute FAR/FRR curves, ROC, DET and the approximate EER.
Labels use 0 for forgery and 1 for genuine. Scores are lower for more
genuine-looking samples.
"""
y = np.asarray(labels, dtype=np.int8)
values = np.asarray(scores, dtype=np.float64)
if y.ndim != 1 or values.ndim != 1 or len(y) != len(values) or len(y) == 0:
raise ValueError("labels and scores must be non-empty 1D arrays of equal length")
if not np.isin(y, [0, 1]).all() or len(np.unique(y)) != 2:
raise ValueError("labels must contain both classes, encoded as 0=forgery and 1=genuine")
fpr, tpr, roc_thresholds = roc_curve(y, -values)
false_accept, false_reject, det_thresholds = det_curve(y, -values)
fnr = 1.0 - tpr
crossing = int(np.argmin(np.abs(fpr - fnr)))
return {
"far": fpr,
"frr": fnr,
"roc_fpr": fpr,
"roc_tpr": tpr,
"roc_thresholds": -roc_thresholds,
"det_fpr": false_accept,
"det_fnr": false_reject,
"det_thresholds": -det_thresholds,
"eer": float((fpr[crossing] + fnr[crossing]) / 2.0),
"eer_threshold": float(-roc_thresholds[crossing]),
}