Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dae49eb4a3 | |||
| 9218cb2741 | |||
| 159f9089a5 | |||
| b718e81ccf | |||
| d46197a81a | |||
| 37c645984f |
@@ -8,3 +8,5 @@ __pycache__/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
models/
|
models/
|
||||||
|
# Ricette pre-trained (generate da utente, non versionare)
|
||||||
|
recipes/*.npz
|
||||||
|
|||||||
@@ -1309,6 +1309,7 @@ class LineShapeMatcher:
|
|||||||
min_recall: float = 0.0,
|
min_recall: float = 0.0,
|
||||||
use_soft_score: bool = False,
|
use_soft_score: bool = False,
|
||||||
subpixel_lm: bool = False,
|
subpixel_lm: bool = False,
|
||||||
|
debug: bool = False,
|
||||||
) -> list[Match]:
|
) -> list[Match]:
|
||||||
"""
|
"""
|
||||||
scale_penalty: se > 0, riduce lo score per match a scala diversa da 1.0:
|
scale_penalty: se > 0, riduce lo score per match a scala diversa da 1.0:
|
||||||
@@ -1326,6 +1327,32 @@ class LineShapeMatcher:
|
|||||||
if not self.variants:
|
if not self.variants:
|
||||||
raise RuntimeError("Matcher non addestrato: chiamare train() prima.")
|
raise RuntimeError("Matcher non addestrato: chiamare train() prima.")
|
||||||
|
|
||||||
|
# Diagnostic counter: traccia perche' candidati sono droppati lungo
|
||||||
|
# la pipeline. Esposto via get_last_diag() o ritornato implicitamente
|
||||||
|
# se debug=True (vedi sotto).
|
||||||
|
diag = {
|
||||||
|
"n_variants_total": len(self.variants),
|
||||||
|
"n_variants_top_evaluated": 0,
|
||||||
|
"n_variants_top_passed": 0,
|
||||||
|
"n_variants_full_evaluated": 0,
|
||||||
|
"n_raw_candidates": 0,
|
||||||
|
"n_after_pre_nms": 0,
|
||||||
|
"drop_ncc_low": 0,
|
||||||
|
"drop_min_score_post_avg": 0,
|
||||||
|
"drop_recall_low": 0,
|
||||||
|
"drop_bbox_out_of_scene": 0,
|
||||||
|
"drop_nms_iou": 0,
|
||||||
|
"n_final": 0,
|
||||||
|
"top_thresh_used": 0.0,
|
||||||
|
"verify_threshold_used": float(verify_threshold),
|
||||||
|
"min_score_used": float(min_score),
|
||||||
|
"min_recall_used": float(min_recall),
|
||||||
|
"use_polarity": bool(self.use_polarity),
|
||||||
|
"use_soft_score": bool(use_soft_score),
|
||||||
|
"subpixel_lm": bool(subpixel_lm),
|
||||||
|
}
|
||||||
|
self._last_diag = diag
|
||||||
|
|
||||||
gray_full = self._to_gray(scene_bgr)
|
gray_full = self._to_gray(scene_bgr)
|
||||||
# Applica ROI di ricerca: restringe scena a crop, ricorda offset per
|
# Applica ROI di ricerca: restringe scena a crop, ricorda offset per
|
||||||
# ri-traslare le coordinate dei match a fine pipeline.
|
# ri-traslare le coordinate dei match a fine pipeline.
|
||||||
@@ -1368,6 +1395,7 @@ class LineShapeMatcher:
|
|||||||
top_factor = max(top_factor, 0.7)
|
top_factor = max(top_factor, 0.7)
|
||||||
cf_eff = 1
|
cf_eff = 1
|
||||||
top_thresh = min_score * top_factor
|
top_thresh = min_score * top_factor
|
||||||
|
diag["top_thresh_used"] = float(top_thresh)
|
||||||
|
|
||||||
tw, th = self.template_size
|
tw, th = self.template_size
|
||||||
density_top = _jit_popcount(spread_top)
|
density_top = _jit_popcount(spread_top)
|
||||||
@@ -1453,6 +1481,7 @@ class LineShapeMatcher:
|
|||||||
|
|
||||||
kept_coarse: list[tuple[int, float]] = []
|
kept_coarse: list[tuple[int, float]] = []
|
||||||
all_top_scores: list[tuple[int, float]] = []
|
all_top_scores: list[tuple[int, float]] = []
|
||||||
|
diag["n_variants_top_evaluated"] = len(coarse_idx_list)
|
||||||
# batch_top: usa kernel batch single-call con prange-esterno su
|
# batch_top: usa kernel batch single-call con prange-esterno su
|
||||||
# varianti. Vince su threadpool quando n_vars >> n_threads e quando
|
# varianti. Vince su threadpool quando n_vars >> n_threads e quando
|
||||||
# H*W top e' piccolo (overhead chiamate JIT > costo kernel).
|
# H*W top e' piccolo (overhead chiamate JIT > costo kernel).
|
||||||
@@ -1516,6 +1545,8 @@ class LineShapeMatcher:
|
|||||||
kept_variants.sort(key=lambda t: -t[1])
|
kept_variants.sort(key=lambda t: -t[1])
|
||||||
max_vars_full = max(max_matches * 8, len(self.variants) // 2)
|
max_vars_full = max(max_matches * 8, len(self.variants) // 2)
|
||||||
kept_variants = kept_variants[:max_vars_full]
|
kept_variants = kept_variants[:max_vars_full]
|
||||||
|
diag["n_variants_top_passed"] = len(kept_coarse)
|
||||||
|
diag["n_variants_full_evaluated"] = len(kept_variants)
|
||||||
|
|
||||||
# Full-res (parallelizzato) con bitmap
|
# Full-res (parallelizzato) con bitmap
|
||||||
spread0 = self._spread_bitmap(gray0)
|
spread0 = self._spread_bitmap(gray0)
|
||||||
@@ -1601,6 +1632,7 @@ class LineShapeMatcher:
|
|||||||
raw.append((float(vals[i]), int(xs[i]), int(ys[i]), vi))
|
raw.append((float(vals[i]), int(xs[i]), int(ys[i]), vi))
|
||||||
|
|
||||||
raw.sort(key=lambda c: -c[0])
|
raw.sort(key=lambda c: -c[0])
|
||||||
|
diag["n_raw_candidates"] = len(raw)
|
||||||
|
|
||||||
# Mappa vi → score_map per subpixel/refinement
|
# Mappa vi → score_map per subpixel/refinement
|
||||||
score_maps = dict(candidates_per_var)
|
score_maps = dict(candidates_per_var)
|
||||||
@@ -1632,6 +1664,7 @@ class LineShapeMatcher:
|
|||||||
preliminary_int.append((score, xi, yi, vi))
|
preliminary_int.append((score, xi, yi, vi))
|
||||||
if len(preliminary_int) >= pre_cap:
|
if len(preliminary_int) >= pre_cap:
|
||||||
break
|
break
|
||||||
|
diag["n_after_pre_nms"] = len(preliminary_int)
|
||||||
|
|
||||||
# Subpixel + refine + verify solo sui candidati pre-NMS (max pre_cap)
|
# Subpixel + refine + verify solo sui candidati pre-NMS (max pre_cap)
|
||||||
kept: list[Match] = []
|
kept: list[Match] = []
|
||||||
@@ -1678,6 +1711,7 @@ class LineShapeMatcher:
|
|||||||
view_idx=getattr(var, "view_idx", 0),
|
view_idx=getattr(var, "view_idx", 0),
|
||||||
)
|
)
|
||||||
if ncc < verify_threshold:
|
if ncc < verify_threshold:
|
||||||
|
diag["drop_ncc_low"] += 1
|
||||||
continue
|
continue
|
||||||
score_f = (float(score_f) + max(0.0, ncc)) * 0.5
|
score_f = (float(score_f) + max(0.0, ncc)) * 0.5
|
||||||
# Soft-margin gradient similarity: sostituisce o integra lo
|
# Soft-margin gradient similarity: sostituisce o integra lo
|
||||||
@@ -1692,6 +1726,7 @@ class LineShapeMatcher:
|
|||||||
# abbattere lo shape-score sotto la soglia user. Senza questo
|
# abbattere lo shape-score sotto la soglia user. Senza questo
|
||||||
# check apparivano match con score < min_score (UI confusing).
|
# check apparivano match con score < min_score (UI confusing).
|
||||||
if float(score_f) < min_score:
|
if float(score_f) < min_score:
|
||||||
|
diag["drop_min_score_post_avg"] += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Feature recall (Halcon MinScore-style): conta quante feature
|
# Feature recall (Halcon MinScore-style): conta quante feature
|
||||||
@@ -1703,6 +1738,7 @@ class LineShapeMatcher:
|
|||||||
spread0, var, cx_f, cy_f, ang_f,
|
spread0, var, cx_f, cy_f, ang_f,
|
||||||
)
|
)
|
||||||
if recall < min_recall:
|
if recall < min_recall:
|
||||||
|
diag["drop_recall_low"] += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Ri-traslo coord da spazio crop ROI a spazio scena originale.
|
# Ri-traslo coord da spazio crop ROI a spazio scena originale.
|
||||||
@@ -1726,6 +1762,7 @@ class LineShapeMatcher:
|
|||||||
)
|
)
|
||||||
inside_ratio = float(inter) / poly_area
|
inside_ratio = float(inter) / poly_area
|
||||||
if inside_ratio < 0.75:
|
if inside_ratio < 0.75:
|
||||||
|
diag["drop_bbox_out_of_scene"] += 1
|
||||||
continue
|
continue
|
||||||
# Penalità scala opzionale: score degrada con distanza da 1.0
|
# Penalità scala opzionale: score degrada con distanza da 1.0
|
||||||
if scale_penalty > 0.0 and var.scale != 1.0:
|
if scale_penalty > 0.0 and var.scale != 1.0:
|
||||||
@@ -1750,6 +1787,7 @@ class LineShapeMatcher:
|
|||||||
dup = True
|
dup = True
|
||||||
break
|
break
|
||||||
if dup:
|
if dup:
|
||||||
|
diag["drop_nms_iou"] += 1
|
||||||
continue
|
continue
|
||||||
kept.append(Match(
|
kept.append(Match(
|
||||||
cx=cx_out, cy=cy_out,
|
cx=cx_out, cy=cy_out,
|
||||||
@@ -1760,4 +1798,35 @@ class LineShapeMatcher:
|
|||||||
))
|
))
|
||||||
if len(kept) >= max_matches:
|
if len(kept) >= max_matches:
|
||||||
break
|
break
|
||||||
|
diag["n_final"] = len(kept)
|
||||||
|
if debug:
|
||||||
|
# Debug mode: stampa diagnostica su stderr per visibilita' immediata.
|
||||||
|
import sys as _sys
|
||||||
|
_sys.stderr.write(f"[pm2d.find debug] {self._format_diag(diag)}\n")
|
||||||
return kept
|
return kept
|
||||||
|
|
||||||
|
def _format_diag(self, diag: dict) -> str:
|
||||||
|
"""Formatta dict diagnostica in una linea leggibile."""
|
||||||
|
return (
|
||||||
|
f"vars: {diag['n_variants_total']} -> "
|
||||||
|
f"top_eval={diag['n_variants_top_evaluated']} "
|
||||||
|
f"top_pass={diag['n_variants_top_passed']} "
|
||||||
|
f"full_eval={diag['n_variants_full_evaluated']} | "
|
||||||
|
f"raw={diag['n_raw_candidates']} "
|
||||||
|
f"pre_nms={diag['n_after_pre_nms']} -> "
|
||||||
|
f"drop[ncc={diag['drop_ncc_low']}, "
|
||||||
|
f"score={diag['drop_min_score_post_avg']}, "
|
||||||
|
f"recall={diag['drop_recall_low']}, "
|
||||||
|
f"bbox={diag['drop_bbox_out_of_scene']}, "
|
||||||
|
f"nms={diag['drop_nms_iou']}] = "
|
||||||
|
f"final={diag['n_final']} (top_thresh={diag['top_thresh_used']:.2f})"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_last_diag(self) -> dict | None:
|
||||||
|
"""Ritorna dict diagnostica dell'ultima chiamata find().
|
||||||
|
|
||||||
|
Halcon-equivalent: oggi inspect_shape_model espone parziali contatori.
|
||||||
|
Util per debug 'perche' 0 match', tuning interattivo, validation.
|
||||||
|
Vedi diag keys per significato (n_variants_top_evaluated, drop_*, ...).
|
||||||
|
"""
|
||||||
|
return getattr(self, "_last_diag", None)
|
||||||
|
|||||||
+99
-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 ---
|
||||||
@@ -674,6 +676,102 @@ def list_recipes():
|
|||||||
return {"files": files, "dir": str(RECIPES_DIR)}
|
return {"files": files, "dir": str(RECIPES_DIR)}
|
||||||
|
|
||||||
|
|
||||||
|
# Cache di matcher caricati da .npz (V feature). Key: nome ricetta.
|
||||||
|
_RECIPE_MATCHERS: OrderedDict = OrderedDict()
|
||||||
|
_RECIPE_MATCHERS_SIZE = 4
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/recipes/{name}/load")
|
||||||
|
def load_recipe(name: str):
|
||||||
|
"""Carica ricetta .npz e popola cache matcher in memoria.
|
||||||
|
|
||||||
|
Una volta caricata, /match_recipe la usa direttamente senza
|
||||||
|
re-train. Halcon-equivalent read_shape_model + handle.
|
||||||
|
"""
|
||||||
|
safe_name = "".join(c for c in name if c.isalnum() or c in "._-")
|
||||||
|
if not safe_name.endswith(".npz"):
|
||||||
|
safe_name += ".npz"
|
||||||
|
path = RECIPES_DIR / safe_name
|
||||||
|
if not path.is_file():
|
||||||
|
raise HTTPException(404, f"Ricetta non trovata: {safe_name}")
|
||||||
|
m = LineShapeMatcher.load_model(str(path))
|
||||||
|
_RECIPE_MATCHERS[safe_name] = m
|
||||||
|
_RECIPE_MATCHERS.move_to_end(safe_name)
|
||||||
|
while len(_RECIPE_MATCHERS) > _RECIPE_MATCHERS_SIZE:
|
||||||
|
_RECIPE_MATCHERS.popitem(last=False)
|
||||||
|
return {
|
||||||
|
"name": safe_name,
|
||||||
|
"n_variants": len(m.variants),
|
||||||
|
"template_size": list(m.template_size),
|
||||||
|
"use_polarity": m.use_polarity,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeMatchParams(BaseModel):
|
||||||
|
recipe: str
|
||||||
|
scene_id: str
|
||||||
|
# Solo find-time params (training gia' fatto offline)
|
||||||
|
min_score: float = 0.65
|
||||||
|
max_matches: int = 25
|
||||||
|
min_recall: float = 0.0
|
||||||
|
use_soft_score: bool = False
|
||||||
|
subpixel_lm: bool = False
|
||||||
|
nms_iou_threshold: float = 0.3
|
||||||
|
coarse_stride: int = 1
|
||||||
|
pyramid_propagate: bool = False
|
||||||
|
greediness: float = 0.0
|
||||||
|
refine_pose_joint: bool = False
|
||||||
|
search_roi: list[int] | None = None
|
||||||
|
verify_threshold: float = 0.5
|
||||||
|
scale_penalty: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/match_recipe", response_model=MatchResp)
|
||||||
|
def match_recipe(p: RecipeMatchParams):
|
||||||
|
"""Match con ricetta pre-trained: zero training, solo find."""
|
||||||
|
safe_name = p.recipe if p.recipe.endswith(".npz") else f"{p.recipe}.npz"
|
||||||
|
m = _RECIPE_MATCHERS.get(safe_name)
|
||||||
|
if m is None:
|
||||||
|
# Auto-load on demand
|
||||||
|
path = RECIPES_DIR / safe_name
|
||||||
|
if not path.is_file():
|
||||||
|
raise HTTPException(404, f"Ricetta non trovata: {safe_name}")
|
||||||
|
m = LineShapeMatcher.load_model(str(path))
|
||||||
|
_RECIPE_MATCHERS[safe_name] = m
|
||||||
|
scene = _load_image(p.scene_id)
|
||||||
|
if scene is None:
|
||||||
|
raise HTTPException(404, "Scena non trovata")
|
||||||
|
search_roi_t = tuple(p.search_roi) if p.search_roi else None
|
||||||
|
t0 = time.time()
|
||||||
|
matches = m.find(
|
||||||
|
scene,
|
||||||
|
min_score=p.min_score, max_matches=p.max_matches,
|
||||||
|
verify_threshold=p.verify_threshold,
|
||||||
|
scale_penalty=p.scale_penalty,
|
||||||
|
min_recall=p.min_recall,
|
||||||
|
use_soft_score=p.use_soft_score,
|
||||||
|
subpixel_lm=p.subpixel_lm,
|
||||||
|
nms_iou_threshold=p.nms_iou_threshold,
|
||||||
|
coarse_stride=p.coarse_stride,
|
||||||
|
pyramid_propagate=p.pyramid_propagate,
|
||||||
|
greediness=p.greediness,
|
||||||
|
refine_pose_joint=p.refine_pose_joint,
|
||||||
|
search_roi=search_roi_t,
|
||||||
|
)
|
||||||
|
t_find = time.time() - t0
|
||||||
|
tg = m.template_gray if m.template_gray is not None else np.zeros((1, 1), np.uint8)
|
||||||
|
annotated = _draw_matches(scene, matches, tg)
|
||||||
|
ann_id = _store_image(annotated)
|
||||||
|
return MatchResp(
|
||||||
|
matches=[MatchResult(
|
||||||
|
cx=mt.cx, cy=mt.cy, angle_deg=mt.angle_deg, scale=mt.scale,
|
||||||
|
score=mt.score, bbox_poly=mt.bbox_poly.tolist(),
|
||||||
|
) for mt in matches],
|
||||||
|
train_time=0.0, find_time=t_find,
|
||||||
|
num_variants=len(m.variants), annotated_id=ann_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Mount static
|
# Mount static
|
||||||
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const PALETTE = [
|
|||||||
const state = {
|
const state = {
|
||||||
model: null, scene: null, roi: null, drag: null,
|
model: null, scene: null, roi: null, drag: null,
|
||||||
matches: [], annotatedImg: null,
|
matches: [], annotatedImg: null,
|
||||||
|
active_recipe: null, // V: ricetta caricata (string nome) o null
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------- Forms ----------
|
// ---------- Forms ----------
|
||||||
@@ -307,7 +308,42 @@ function setupROI() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Match action ----------
|
// ---------- Match action ----------
|
||||||
|
async function doMatchRecipe() {
|
||||||
|
if (!state.scene) { setStatus("Carica scena"); return; }
|
||||||
|
setStatus(`Match ricetta ${state.active_recipe}...`);
|
||||||
|
const hc = readHalconFlags();
|
||||||
|
const body = {
|
||||||
|
recipe: state.active_recipe,
|
||||||
|
scene_id: state.scene.id,
|
||||||
|
min_score: parseFloat(document.getElementById("p-min-score").value),
|
||||||
|
max_matches: parseInt(document.getElementById("p-max-matches").value, 10),
|
||||||
|
verify_threshold: 0.50,
|
||||||
|
...hc,
|
||||||
|
};
|
||||||
|
const r = await fetch("/match_recipe", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
if (!r.ok) { setStatus(`Errore: ${await r.text()}`); return; }
|
||||||
|
const data = await r.json();
|
||||||
|
state.matches = data.matches;
|
||||||
|
state.annotatedImg = await loadImage(
|
||||||
|
`/image/${data.annotated_id}/raw?t=${Date.now()}`);
|
||||||
|
renderScene();
|
||||||
|
renderLegend();
|
||||||
|
document.getElementById("t-train").textContent = "—";
|
||||||
|
document.getElementById("t-find").textContent = `${data.find_time.toFixed(2)}s`;
|
||||||
|
document.getElementById("t-var").textContent = data.num_variants;
|
||||||
|
document.getElementById("t-match").textContent = data.matches.length;
|
||||||
|
setStatus(`${data.matches.length} match trovati (ricetta ${state.active_recipe})`);
|
||||||
|
}
|
||||||
|
|
||||||
async function doMatch() {
|
async function doMatch() {
|
||||||
|
// Path V: ricetta caricata → bypass training, solo find su scena
|
||||||
|
if (state.active_recipe) {
|
||||||
|
return doMatchRecipe();
|
||||||
|
}
|
||||||
if (!state.model) { setStatus("Carica modello"); return; }
|
if (!state.model) { setStatus("Carica modello"); return; }
|
||||||
if (!state.scene) { setStatus("Carica scena"); return; }
|
if (!state.scene) { setStatus("Carica scena"); return; }
|
||||||
if (!state.roi) { setStatus("Seleziona ROI sul modello"); return; }
|
if (!state.roi) { setStatus("Seleziona ROI sul modello"); return; }
|
||||||
@@ -400,6 +436,104 @@ 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: Recipe load/list/unload ----------
|
||||||
|
async function refreshRecipeList() {
|
||||||
|
try {
|
||||||
|
const r = await fetch("/recipes");
|
||||||
|
if (!r.ok) return;
|
||||||
|
const j = await r.json();
|
||||||
|
const sel = document.getElementById("hc-recipe-list");
|
||||||
|
const cur = sel.value;
|
||||||
|
sel.innerHTML = '<option value="">— ricette disponibili —</option>';
|
||||||
|
for (const f of j.files) {
|
||||||
|
const o = document.createElement("option");
|
||||||
|
o.value = f.name;
|
||||||
|
o.textContent = `${f.name} (${(f.size / 1024).toFixed(1)} KB)`;
|
||||||
|
sel.appendChild(o);
|
||||||
|
}
|
||||||
|
if (cur) sel.value = cur;
|
||||||
|
} catch (e) { /* silent */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRecipe() {
|
||||||
|
const sel = document.getElementById("hc-recipe-list");
|
||||||
|
const name = sel.value;
|
||||||
|
if (!name) {
|
||||||
|
alert("Seleziona una ricetta dalla lista.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/recipes/${encodeURIComponent(name)}/load`, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error(await r.text());
|
||||||
|
const j = await r.json();
|
||||||
|
state.active_recipe = j.name;
|
||||||
|
document.getElementById("recipe-status").textContent =
|
||||||
|
`Caricata: ${j.name} — ${j.n_variants} varianti, ` +
|
||||||
|
`${j.template_size[0]}x${j.template_size[1]} px` +
|
||||||
|
(j.use_polarity ? " (polarity)" : "");
|
||||||
|
document.getElementById("recipe-status").style.color = "#0c0";
|
||||||
|
document.getElementById("btn-unload-recipe").disabled = false;
|
||||||
|
} catch (e) {
|
||||||
|
alert(`Errore caricamento: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unloadRecipe() {
|
||||||
|
state.active_recipe = null;
|
||||||
|
document.getElementById("recipe-status").textContent = "Nessuna ricetta caricata";
|
||||||
|
document.getElementById("recipe-status").style.color = "#888";
|
||||||
|
document.getElementById("btn-unload-recipe").disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- V: Save recipe ----------
|
// ---------- V: Save recipe ----------
|
||||||
async function saveRecipe() {
|
async function saveRecipe() {
|
||||||
if (!state.model || !state.roi) {
|
if (!state.model || !state.roi) {
|
||||||
@@ -433,6 +567,7 @@ async function saveRecipe() {
|
|||||||
if (!r.ok) throw new Error(await r.text());
|
if (!r.ok) throw new Error(await r.text());
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
alert(`Ricetta salvata: ${j.name}\n${j.n_variants} varianti, ${j.size} bytes`);
|
alert(`Ricetta salvata: ${j.name}\n${j.n_variants} varianti, ${j.size} bytes`);
|
||||||
|
refreshRecipeList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(`Errore salvataggio: ${e.message}`);
|
alert(`Errore salvataggio: ${e.message}`);
|
||||||
}
|
}
|
||||||
@@ -465,8 +600,14 @@ 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);
|
||||||
|
document.getElementById("btn-load-recipe").addEventListener("click",
|
||||||
|
loadRecipe);
|
||||||
|
document.getElementById("btn-unload-recipe").addEventListener("click",
|
||||||
|
unloadRecipe);
|
||||||
|
refreshRecipeList();
|
||||||
const slider = document.getElementById("p-min-score");
|
const slider = document.getElementById("p-min-score");
|
||||||
slider.addEventListener("input", (e) => {
|
slider.addEventListener("input", (e) => {
|
||||||
document.getElementById("v-score").textContent =
|
document.getElementById("v-score").textContent =
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -186,6 +190,16 @@
|
|||||||
<input type="text" id="hc-recipe-name" placeholder="nome_ricetta" style="flex:1">
|
<input type="text" id="hc-recipe-name" placeholder="nome_ricetta" style="flex:1">
|
||||||
<button class="btn" id="btn-save-recipe" type="button">💾 Salva</button>
|
<button class="btn" id="btn-save-recipe" type="button">💾 Salva</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display:flex; gap:6px; margin-top:6px; align-items:center">
|
||||||
|
<select id="hc-recipe-list" style="flex:1">
|
||||||
|
<option value="">— ricette disponibili —</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn" id="btn-load-recipe" type="button">📂 Carica</button>
|
||||||
|
<button class="btn" id="btn-unload-recipe" type="button" disabled>✖ Stacca</button>
|
||||||
|
</div>
|
||||||
|
<div id="recipe-status" style="margin-top:4px; font-size:11px; color:#888">
|
||||||
|
Nessuna ricetta caricata
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
Reference in New Issue
Block a user