"""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))