BSB_Sim/tests/test_baseline.py
2026-10-19 08:11:33 +02:00

110 lines
4.1 KiB
Python

import numpy as np
import pytest
from sigverify.evaluation.metrics import verification_metrics
from sigverify.features import extract_features, global_features
from sigverify.io.schema import Signature
from sigverify.io.loaders import load_mobisig
from sigverify.matching.dtw import dtw_distance
from sigverify.matching.models import DTWVerifier
from sigverify.preprocessing import preprocess_signature
from sigverify.security import AttemptLockout, TemplateCipher
def sample(label="genuine", scale=1.0):
t = np.array([2.0, 0.0, 1.0, 1.0, 3.0])
x = scale * np.array([2.0, 0.0, 1.0, 99.0, 3.0])
y = scale * np.array([0.0, 0.0, 1.0, 99.0, 1.0])
return Signature("person-1", label, t, x, y, p=np.ones(5))
def test_preprocessing_sorts_deduplicates_and_retains_original_size():
cleaned = preprocess_signature(sample(), points=20, smoothing_sigma=0)
assert len(cleaned.t) == 20
assert np.all(np.diff(cleaned.t) > 0)
assert cleaned.size_normalized
assert cleaned.original_size == pytest.approx(np.hypot(3, 1))
assert np.max(np.abs(cleaned.x)) == pytest.approx(0.5)
assert cleaned.p is not None and len(cleaned.p) == 20
def test_pen_up_points_are_removed_before_resampling():
raw = Signature(
"person-1", "genuine", [0, 1, 2], [0, 100, 2], [0, 100, 2],
pen_down=[True, False, True],
)
cleaned = preprocess_signature(raw, points=3, smoothing_sigma=0, normalize_position=False)
assert cleaned.x.tolist() == pytest.approx([0, 1, 2])
assert cleaned.pen_down is not None and cleaned.pen_down.all()
def test_features_support_optional_pressure_and_global_descriptors():
clean = preprocess_signature(sample(), points=10, smoothing_sigma=0)
base = extract_features(clean, include_acceleration=False, include_curvature=False)
with_pressure = extract_features(
clean, include_pressure=True, include_acceleration=False, include_curvature=False
)
assert base.shape == (10, 5)
assert with_pressure.shape == (10, 6)
assert global_features(clean)["original_size"] > 0
def test_dtw_is_zero_for_identical_sequences_and_handles_band():
sequence = np.array([[0.0], [1.0], [2.0]])
assert dtw_distance(sequence, sequence) == 0
assert dtw_distance(sequence, sequence[::-1]) > 0
assert np.isfinite(dtw_distance(sequence, sequence[:2]))
def test_dtw_verifier_enrollment_and_normalized_score():
first = preprocess_signature(sample(), points=20, smoothing_sigma=0)
second = preprocess_signature(sample(scale=1.02), points=20, smoothing_sigma=0)
verifier = DTWVerifier()
verifier.enroll([first, second])
assert verifier.score(first) < 0.1
with pytest.raises(ValueError, match="genuine"):
verifier.enroll([first, first.copy(label="forgery")])
def test_metrics_detect_perfect_separation():
result = verification_metrics([1, 1, 0, 0], [0.1, 0.2, 0.8, 0.9])
assert result["eer"] == 0
def test_template_cipher_round_trip_and_lockout():
from cryptography.fernet import Fernet
signature = preprocess_signature(sample(), points=8)
cipher = TemplateCipher(Fernet.generate_key())
restored = cipher.decrypt(cipher.encrypt(signature))
np.testing.assert_allclose(restored.x, signature.x)
lockout = AttemptLockout(max_failures=2, lockout_seconds=1)
lockout.record_failure("person-1")
assert lockout.remaining_lockout("person-1") == 0
lockout.record_failure("person-1")
assert lockout.remaining_lockout("person-1") > 0
def test_mobisig_loader_maps_columns_and_filename_metadata(tmp_path):
user_dir = tmp_path / "USER1"
user_dir.mkdir()
(user_dir / "SIGN_GEN_USER1_USER1_1.csv").write_text(
"x,y,timestamp,pressure,fingerarea\n1,2,10,0.5,3\n2,4,20,0.6,4\n",
encoding="utf-8",
)
(user_dir / "SIGN_FOR_USER1_USER2_1.csv").write_text(
"x,y,timestamp,pressure,fingerarea\n1,2,10,0.5,3\n2,3,20,0.6,4\n",
encoding="utf-8",
)
signatures = load_mobisig(tmp_path)
assert [(item.user_id, item.label) for item in signatures] == [
("USER1", "forgery"),
("USER1", "genuine"),
]
assert signatures[0].p is not None and signatures[0].area is not None