110 lines
4.1 KiB
Python
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
|