0420c4a863
11 test senza dipendenza dalle immagini Test/ (non versionate): - precisione/recall su 7 pose GT (soglie 0.2-0.5 deg, 0.3-1.0 px, margine 3-4x sulle misure Fase 2) - unit: angle_list con estremi, clamp piramide, save/load roundtrip, no collisione cache scena, mask poligonale, find non addestrato Config ruff in pyproject (E702/E402 idiomi del codebase esclusi). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
85 lines
3.0 KiB
Python
85 lines
3.0 KiB
Python
"""Unit test rapidi su componenti del matcher (no matching pesante)."""
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
import cv2
|
|
import pytest
|
|
|
|
from pm2d import LineShapeMatcher
|
|
from tests.conftest import GT_POSES, make_scene, match_errors
|
|
|
|
|
|
def test_angle_list_includes_range_end():
|
|
# Range parziale ±15: l'estremo +15 deve essere testato (era escluso).
|
|
m = LineShapeMatcher(angle_range_deg=(-15.0, 15.0), angle_step_deg=5.0)
|
|
angles = m._angle_list()
|
|
assert -15.0 in angles and 15.0 in angles
|
|
assert len(angles) == 7
|
|
|
|
|
|
def test_angle_list_full_circle_no_duplicate():
|
|
# (0, 360): 360 coincide con 0 → escluso, niente variante duplicata.
|
|
m = LineShapeMatcher(angle_range_deg=(0.0, 360.0), angle_step_deg=5.0)
|
|
angles = m._angle_list()
|
|
assert len(angles) == 72
|
|
assert 360.0 not in angles
|
|
|
|
|
|
def test_pyramid_clamp_small_template():
|
|
# Template 40px di lato minimo: al top /4 le feature collassano →
|
|
# i livelli vengono clampati (40/2=20 >= 12, 40/4=10 < 12 → 2 livelli).
|
|
m = LineShapeMatcher(pyramid_levels=4, angle_range_deg=(0.0, 10.0),
|
|
angle_step_deg=5.0)
|
|
tpl = np.full((40, 200), 60, np.uint8)
|
|
cv2.rectangle(tpl, (30, 8), (170, 32), 200, -1)
|
|
m.train(tpl)
|
|
assert m.pyramid_levels == 2
|
|
|
|
|
|
def test_save_load_roundtrip(tmp_path, template, scene):
|
|
m = LineShapeMatcher(angle_step_deg=10.0)
|
|
m.train(template)
|
|
path = str(tmp_path / "model.npz")
|
|
m.save_model(path)
|
|
m2 = LineShapeMatcher.load_model(path)
|
|
assert len(m2.variants) == len(m.variants)
|
|
matches = m2.find(scene, min_score=0.5, max_matches=10)
|
|
_, _, miss = match_errors(matches, GT_POSES)
|
|
assert miss == 0
|
|
|
|
|
|
def test_scene_cache_no_collision(template):
|
|
# Due scene IDENTICHE nella banda superiore ma diverse sotto: la cache
|
|
# (che prima hashava solo i primi 64KB) non deve restituire i risultati
|
|
# della scena sbagliata.
|
|
poses_a = [GT_POSES[0], (450.0, 560.0, 33.7)]
|
|
poses_b = [GT_POSES[0], (700.0, 560.0, 91.2)]
|
|
scene_a = make_scene(template, poses_a)
|
|
scene_b = make_scene(template, poses_b)
|
|
# Stessa banda superiore (le pose extra sono in basso, y >= 430)
|
|
assert np.array_equal(scene_a[:80], scene_b[:80])
|
|
m = LineShapeMatcher(angle_step_deg=10.0)
|
|
m.train(template)
|
|
ma = m.find(scene_a, min_score=0.5, max_matches=5)
|
|
mb = m.find(scene_b, min_score=0.5, max_matches=5)
|
|
_, _, miss_a = match_errors(ma, poses_a)
|
|
_, _, miss_b = match_errors(mb, poses_b)
|
|
assert miss_a == 0 and miss_b == 0
|
|
|
|
|
|
def test_train_mask_polygonal(template, scene):
|
|
# ROI poligonale: mask che copre solo la L verticale del template.
|
|
mask = np.zeros_like(template)
|
|
cv2.rectangle(mask, (10, 10), (70, template.shape[0] - 10), 255, -1)
|
|
m = LineShapeMatcher(angle_step_deg=10.0)
|
|
n = m.train(template, mask=mask)
|
|
assert n > 0
|
|
matches = m.find(scene, min_score=0.5, max_matches=10)
|
|
assert len(matches) >= 1
|
|
|
|
|
|
def test_untrained_find_raises():
|
|
m = LineShapeMatcher()
|
|
with pytest.raises(RuntimeError):
|
|
m.find(np.zeros((100, 100), np.uint8))
|