feat: scale_penalty - score riflette dimensione oltre a forma

Shape matching e invariante scala per design: 3 ruote dentate di dim
diverse avevano tutte score 1.00 confondendo l operatore.

Parametro scale_penalty [0..1]: score_final = score * max(0, 1 - penalty * |scale - 1|)
UI dropdown 'Peso dimensione nel score' con preset 0 / 0.3 / 0.5 / 0.8.

Test rings con penalty 0.5: 1.00 -> 1.00, 0.95 -> 0.97, 0.80 -> 0.90.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 14:37:36 +02:00
parent 37b718e45e
commit cc7d035f66
4 changed files with 30 additions and 0 deletions
+13
View File
@@ -548,7 +548,15 @@ class LineShapeMatcher:
verify_ncc: bool = True, verify_ncc: bool = True,
verify_threshold: float = 0.4, verify_threshold: float = 0.4,
coarse_angle_factor: int = 2, coarse_angle_factor: int = 2,
scale_penalty: float = 0.0,
) -> list[Match]: ) -> list[Match]:
"""
scale_penalty: se > 0, riduce lo score per match a scala diversa da 1.0:
score_final = score_shape * max(0, 1 - scale_penalty * |scale - 1|)
Utile se l'operatore vuole che match "identico al template anche per
dimensione" abbia score più alto di match "stessa forma, dimensione
diversa". scale_penalty=0 (default) = comportamento shape puro.
"""
if not self.variants: if not self.variants:
raise RuntimeError("Matcher non addestrato: chiamare train() prima.") raise RuntimeError("Matcher non addestrato: chiamare train() prima.")
@@ -746,6 +754,11 @@ class LineShapeMatcher:
poly = _oriented_bbox_polygon( poly = _oriented_bbox_polygon(
cx_f, cy_f, tw * var.scale, th * var.scale, ang_f, cx_f, cy_f, tw * var.scale, th * var.scale, ang_f,
) )
# Penalità scala opzionale: score degrada con distanza da 1.0
if scale_penalty > 0.0 and var.scale != 1.0:
score_f = float(score_f) * max(
0.0, 1.0 - scale_penalty * abs(var.scale - 1.0)
)
kept.append(Match( kept.append(Match(
cx=cx_f, cy=cy_f, cx=cx_f, cy=cy_f,
angle_deg=ang_f, angle_deg=ang_f,
+3
View File
@@ -264,6 +264,7 @@ class SimpleMatchParams(BaseModel):
scala: str = "fissa" # chiave SCALE_PRESETS scala: str = "fissa" # chiave SCALE_PRESETS
precisione: str = "normale" # chiave PRECISION_ANGLE_STEP precisione: str = "normale" # chiave PRECISION_ANGLE_STEP
filtro_fp: str = "medio" # chiave FILTRO_FP_MAP filtro_fp: str = "medio" # chiave FILTRO_FP_MAP
penalita_scala: float = 0.0 # 0 = score shape invariante, >0 = penalizza scala != 1
min_score: float = 0.65 min_score: float = 0.65
max_matches: int = 25 max_matches: int = 25
@@ -316,6 +317,7 @@ def _simple_to_technical(
"max_matches": p.max_matches, "max_matches": p.max_matches,
"nms_radius": 0, "nms_radius": 0,
"verify_threshold": FILTRO_FP_MAP.get(p.filtro_fp, 0.35), "verify_threshold": FILTRO_FP_MAP.get(p.filtro_fp, 0.35),
"scale_penalty": p.penalita_scala,
} }
@@ -502,6 +504,7 @@ def match_simple(p: SimpleMatchParams):
matches = m.find( matches = m.find(
scene, min_score=tech["min_score"], max_matches=tech["max_matches"], scene, min_score=tech["min_score"], max_matches=tech["max_matches"],
nms_radius=nms, verify_threshold=tech["verify_threshold"], nms_radius=nms, verify_threshold=tech["verify_threshold"],
scale_penalty=tech.get("scale_penalty", 0.0),
) )
t_find = time.time() - t0 t_find = time.time() - t0
+2
View File
@@ -48,6 +48,8 @@ function readUserParams() {
scala: document.getElementById("p-scala").value, scala: document.getElementById("p-scala").value,
precisione: document.getElementById("p-precisione").value, precisione: document.getElementById("p-precisione").value,
filtro_fp: document.getElementById("p-filtro-fp").value, filtro_fp: document.getElementById("p-filtro-fp").value,
penalita_scala: parseFloat(
document.getElementById("p-penalita-scala").value),
min_score: parseFloat(document.getElementById("p-min-score").value), min_score: parseFloat(document.getElementById("p-min-score").value),
max_matches: parseInt(document.getElementById("p-max-matches").value, 10), max_matches: parseInt(document.getElementById("p-max-matches").value, 10),
}; };
+12
View File
@@ -101,6 +101,18 @@
</select> </select>
</div> </div>
<div class="field">
<label>Peso dimensione nel score
<span class="hint">(penalizza scala ≠ 1.0)</span>
</label>
<select id="p-penalita-scala">
<option value="0" selected>Nessuno (score shape puro)</option>
<option value="0.3">Leggero (30% max)</option>
<option value="0.5">Medio (50% max)</option>
<option value="0.8">Forte (80% max)</option>
</select>
</div>
<div class="field"> <div class="field">
<label>Score minimo <span id="v-score">0.65</span> <label>Score minimo <span id="v-score">0.65</span>
<span class="hint">(più basso = più match anche incerti)</span> <span class="hint">(più basso = più match anche incerti)</span>