Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d46197a81a | |||
| 37c645984f | |||
| 0e148667ec | |||
| b5bbca0e85 | |||
| ca3882c59c |
@@ -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)
|
||||||
|
|||||||
+3
-1
@@ -607,7 +607,9 @@ def tune(p: TuneParams):
|
|||||||
x, y, w, h = p.roi
|
x, y, w, h = p.roi
|
||||||
roi_img = model[y:y + h, x:x + w]
|
roi_img = model[y:y + h, x:x + w]
|
||||||
t = auto_tune(roi_img)
|
t = auto_tune(roi_img)
|
||||||
return {k: v for k, v in t.items() if not k.startswith("_")}
|
# Esponi parametri tecnici + meta diagnostica (_self_score, _validation,
|
||||||
|
# _symmetry_order, _orient_entropy) per feedback UI.
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
# --- V: Save/Load ricette pre-trained ---
|
# --- V: Save/Load ricette pre-trained ---
|
||||||
|
|||||||
@@ -400,6 +400,53 @@ function setStatus(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Init ----------
|
// ---------- Init ----------
|
||||||
|
// ---------- Auto-tune (Halcon-style) ----------
|
||||||
|
async function doAutoTune() {
|
||||||
|
if (!state.model || !state.roi) {
|
||||||
|
alert("Seleziona modello e disegna ROI prima di Auto-tune.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const status = document.getElementById("status");
|
||||||
|
status.textContent = "Analisi ROI in corso...";
|
||||||
|
try {
|
||||||
|
const r = await fetch("/auto_tune", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model_id: state.model.id,
|
||||||
|
roi: state.roi,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error(await r.text());
|
||||||
|
const t = await r.json();
|
||||||
|
// Applica ai campi avanzati (override automatico)
|
||||||
|
for (const [key] of ADV_PARAMS) {
|
||||||
|
const el = document.getElementById(`adv-${key}`);
|
||||||
|
if (el && t[key] !== undefined) el.value = String(t[key]);
|
||||||
|
}
|
||||||
|
// Espandi la sezione Avanzate per mostrare i valori applicati
|
||||||
|
const advDetails = document.querySelector("#col-params details:last-of-type");
|
||||||
|
if (advDetails) advDetails.open = true;
|
||||||
|
// Feedback diagnostico
|
||||||
|
const lines = [
|
||||||
|
`weak/strong: ${t.weak_grad} / ${t.strong_grad}`,
|
||||||
|
`feature: ${t.num_features}, piramide: ${t.pyramid_levels}`,
|
||||||
|
`angle: [${t.angle_min}..${t.angle_max}]@${t.angle_step}°`,
|
||||||
|
];
|
||||||
|
if (t._symmetry_order > 1) {
|
||||||
|
lines.push(`simmetria rotaz. ${t._symmetry_order}x (conf ${t._symmetry_conf})`);
|
||||||
|
}
|
||||||
|
if (t._self_score !== undefined) {
|
||||||
|
lines.push(`self-validation: ${t._validation}`);
|
||||||
|
}
|
||||||
|
status.textContent = `Auto-tune OK — ${lines[0]}`;
|
||||||
|
alert("Auto-tune completato:\n\n" + lines.join("\n"));
|
||||||
|
} catch (e) {
|
||||||
|
status.textContent = `Auto-tune errore: ${e.message}`;
|
||||||
|
alert(`Errore auto-tune: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- V: Save recipe ----------
|
// ---------- V: Save recipe ----------
|
||||||
async function saveRecipe() {
|
async function saveRecipe() {
|
||||||
if (!state.model || !state.roi) {
|
if (!state.model || !state.roi) {
|
||||||
@@ -465,6 +512,7 @@ window.addEventListener("DOMContentLoaded", async () => {
|
|||||||
e.target.value = ""; // consente re-upload stesso file
|
e.target.value = ""; // consente re-upload stesso file
|
||||||
});
|
});
|
||||||
document.getElementById("btn-match").addEventListener("click", doMatch);
|
document.getElementById("btn-match").addEventListener("click", doMatch);
|
||||||
|
document.getElementById("btn-autotune").addEventListener("click", doAutoTune);
|
||||||
document.getElementById("btn-save-recipe").addEventListener("click",
|
document.getElementById("btn-save-recipe").addEventListener("click",
|
||||||
saveRecipe);
|
saveRecipe);
|
||||||
const slider = document.getElementById("p-min-score");
|
const slider = document.getElementById("p-min-score");
|
||||||
|
|||||||
@@ -26,6 +26,10 @@
|
|||||||
<div class="picker-list"></div>
|
<div class="picker-list"></div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-go" id="btn-match">▶ MATCH</button>
|
<button class="btn btn-go" id="btn-match">▶ MATCH</button>
|
||||||
|
<button class="btn" id="btn-autotune"
|
||||||
|
title="Analizza ROI e derivata parametri ottimali (Halcon-style)">
|
||||||
|
⚙ Auto-tune
|
||||||
|
</button>
|
||||||
<label class="btn" title="Carica nuovo file nella cartella immagini">
|
<label class="btn" title="Carica nuovo file nella cartella immagini">
|
||||||
⬆ Carica file
|
⬆ Carica file
|
||||||
<input type="file" id="file-upload" accept="image/*" hidden>
|
<input type="file" id="file-upload" accept="image/*" hidden>
|
||||||
|
|||||||
Reference in New Issue
Block a user