Compare commits

...

1 Commits

Author SHA1 Message Date
Adriano ca3882c59c feat: auto_tune self-validation (Halcon-style inspect_shape_model)
Nuovo helper _self_validate(): post-stima parametri, esegue dry-run
training+find sul template stesso e regola i parametri se subottimali.

Loop di auto-correzione (analogo a Halcon inspect_shape_model):
1. Se top-level piramide ha <8 feature → riduce pyramid_levels
2. Se train produce 0 varianti → dimezza weak/strong_grad
3. Se find sul template fallisce → riduce soglie + num_features
4. Se self-score < 0.7 → abbassa weak_grad

Costo: 1 train minimale (1 variante) + 1 find su canvas tpl + padding,
~50ms su template 100x100. Ne vale la pena per evitare match-time
errors su scene reali con parametri estimato male.

Esposto via auto_tune(self_validate=True) default; meta '_self_score'
e '_validation' nel dict risultato per logging UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 23:04:01 +02:00
+105
View File
@@ -152,11 +152,103 @@ def _cache_key(template_bgr: np.ndarray, mask: np.ndarray | None) -> str:
return h.hexdigest() return h.hexdigest()
def _self_validate(template_bgr: np.ndarray, params: dict,
mask: np.ndarray | None = None) -> dict:
"""Halcon-style self-validation: train il matcher coi parametri tentativi
e verifica che il template stesso sia trovato con recall ≥ 1.0.
Se recall < target o score basso, regola i parametri:
- alza weak_grad se troppi edge spuri (recall solido ma molti picchi falsi)
- abbassa strong_grad se troppe feature scartate (low feature count)
- riduce pyramid_levels se variants[0].levels[top] ha <8 feature
Halcon usa internamente questo loop in inspect_shape_model. Costo: 1
train + 1 find sul template (~50ms su template 100x100). Ne vale la
pena se evita match-time errors su scene reali.
Mutates `params` in place e ritorna lo stesso dict per chaining.
"""
# Import lazy: evita ciclo (line_matcher importa nulla da auto_tune)
from pm2d.line_matcher import LineShapeMatcher
# Caso degenerato: troppe poche feature pre-validation → riduci soglia
if params.get("_n_strong_pixels", 0) < 30:
params["weak_grad"] = max(15.0, params["weak_grad"] * 0.6)
params["strong_grad"] = max(30.0, params["strong_grad"] * 0.6)
# Train minimale: 1 sola pose orientazione 0 (range degenerato che
# produce comunque 1 variante via fallback in _angle_list).
m = LineShapeMatcher(
num_features=params["num_features"],
weak_grad=params["weak_grad"],
strong_grad=params["strong_grad"],
angle_range_deg=(0.0, 0.0), # fallback _angle_list = [0.0]
angle_step_deg=10.0,
scale_range=(1.0, 1.0),
spread_radius=params["spread_radius"],
pyramid_levels=params["pyramid_levels"],
)
n_var = m.train(template_bgr, mask=mask)
if n_var == 0:
# Soglie troppo alte: nessuna variante generata → dimezza
params["weak_grad"] = max(15.0, params["weak_grad"] * 0.5)
params["strong_grad"] = max(30.0, params["strong_grad"] * 0.5)
params["_validation"] = "fallback: soglie dimezzate (no variants)"
return params
# Verifica densita' feature al top-level (rischio collasso)
top_lvl = m.variants[0].levels[-1]
if top_lvl.n < 8 and params["pyramid_levels"] > 1:
params["pyramid_levels"] = max(1, params["pyramid_levels"] - 1)
params["_validation"] = (
f"pyramid_levels ridotto a {params['pyramid_levels']} "
f"(top aveva {top_lvl.n} feature)"
)
return params
# Self-find: cerca il template stesso nella propria immagine
h, w = template_bgr.shape[:2]
# Embed template in scena leggermente più grande per evitare bordo
pad = 20
canvas = np.full(
(h + 2 * pad, w + 2 * pad, 3 if template_bgr.ndim == 3 else 1),
128, dtype=np.uint8,
)
canvas[pad:pad + h, pad:pad + w] = template_bgr
matches = m.find(
canvas, min_score=0.3, max_matches=5,
verify_ncc=False, # template stesso → NCC = 1 sempre, skip per velocita'
refine_angle=False, subpixel=False,
nms_iou_threshold=0.3,
)
if not matches:
# Nessun match sul proprio template: parametri troppo restrittivi
params["weak_grad"] = max(15.0, params["weak_grad"] * 0.7)
params["strong_grad"] = max(30.0, params["strong_grad"] * 0.7)
params["num_features"] = max(48, int(params["num_features"] * 0.8))
params["_validation"] = "soglie/feature ridotte (no self-match)"
return params
# Misura score top match
top_score = float(matches[0].score)
params["_self_score"] = round(top_score, 3)
if top_score < 0.7:
# Score basso sul template stesso = parametri davvero subottimali
params["weak_grad"] = max(15.0, params["weak_grad"] * 0.85)
params["_validation"] = (
f"weak_grad ridotto (self-score era {top_score:.2f})"
)
else:
params["_validation"] = f"OK (self-score {top_score:.2f})"
return params
def auto_tune( def auto_tune(
template_bgr: np.ndarray, template_bgr: np.ndarray,
mask: np.ndarray | None = None, mask: np.ndarray | None = None,
angle_tolerance_deg: float | None = None, angle_tolerance_deg: float | None = None,
angle_center_deg: float = 0.0, angle_center_deg: float = 0.0,
self_validate: bool = True,
) -> dict: ) -> dict:
"""Analizza template e ritorna dict parametri suggeriti. """Analizza template e ritorna dict parametri suggeriti.
@@ -168,6 +260,11 @@ def auto_tune(
meccanico): training molto piu rapido (24x meno varianti per meccanico): training molto piu rapido (24x meno varianti per
tol=15° vs 360° pieno). tol=15° vs 360° pieno).
self_validate: se True (default), dopo la stima dei parametri
esegue un dry-run del matching sul template stesso e regola
weak_grad/strong_grad/pyramid_levels se i parametri tentativi
non garantiscono auto-match (Halcon-style inspect_shape_model).
Risultato cachato in-memory (LRU): ri-chiamare con stessa ROI è O(1). Risultato cachato in-memory (LRU): ri-chiamare con stessa ROI è O(1).
""" """
ck = _cache_key(template_bgr, mask) ck = _cache_key(template_bgr, mask)
@@ -265,7 +362,15 @@ def auto_tune(
"_symmetry_order": sym["order"], "_symmetry_order": sym["order"],
"_symmetry_conf": round(sym["confidence"], 2), "_symmetry_conf": round(sym["confidence"], 2),
"_orient_entropy": round(stats["orient_entropy"], 2), "_orient_entropy": round(stats["orient_entropy"], 2),
"_n_strong_pixels": stats["n_strong"],
} }
# Halcon-style self-validation: dry-run training+find sul template per
# auto-correggere parametri tentativi che non garantirebbero match.
if self_validate:
result = _self_validate(template_bgr, result, mask=mask)
# Round numerici dopo eventuali aggiustamenti
result["weak_grad"] = round(result["weak_grad"], 1)
result["strong_grad"] = round(result["strong_grad"], 1)
# Store in LRU cache # Store in LRU cache
_TUNE_CACHE[ck] = dict(result) _TUNE_CACHE[ck] = dict(result)
_TUNE_CACHE.move_to_end(ck) _TUNE_CACHE.move_to_end(ck)