Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cb1ae2df7 | |||
| 6ebb08e7a2 |
@@ -48,6 +48,10 @@ IMAGES_DIR = Path(_images_dir_raw)
|
|||||||
if not IMAGES_DIR.is_absolute():
|
if not IMAGES_DIR.is_absolute():
|
||||||
IMAGES_DIR = PROJECT_ROOT / IMAGES_DIR
|
IMAGES_DIR = PROJECT_ROOT / IMAGES_DIR
|
||||||
|
|
||||||
|
# Cartella ricette pre-trained (V feature: save/load matcher)
|
||||||
|
RECIPES_DIR = PROJECT_ROOT / "recipes"
|
||||||
|
RECIPES_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
from pm2d.line_matcher import LineShapeMatcher, Match
|
from pm2d.line_matcher import LineShapeMatcher, Match
|
||||||
from pm2d.auto_tune import auto_tune
|
from pm2d.auto_tune import auto_tune
|
||||||
|
|
||||||
@@ -267,6 +271,20 @@ class SimpleMatchParams(BaseModel):
|
|||||||
penalita_scala: float = 0.0 # 0 = score shape invariante, >0 = penalizza scala != 1
|
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
|
||||||
|
# --- Halcon-mode flags (default off = backward compat) ---
|
||||||
|
# Init-time (richiede ri-train se cambiato)
|
||||||
|
use_polarity: bool = False # F: 16 bin orientation mod 2pi
|
||||||
|
use_gpu: bool = False # R: OpenCL UMat (silent fallback)
|
||||||
|
# Find-time (no retrain)
|
||||||
|
min_recall: float = 0.0 # M: filtra match con poche feature combaciate
|
||||||
|
use_soft_score: bool = False # Y: cosine sim continua dei gradients
|
||||||
|
subpixel_lm: bool = False # Z: precisione 0.05 px
|
||||||
|
nms_iou_threshold: float = 0.3 # A: IoU bbox poligonale
|
||||||
|
coarse_stride: int = 1 # sub-sampling top-level (>=1)
|
||||||
|
pyramid_propagate: bool = False # propagazione candidati top->full
|
||||||
|
greediness: float = 0.0 # early-exit kernel (0..1)
|
||||||
|
refine_pose_joint: bool = False # Nelder-Mead 3D (cx, cy, angle)
|
||||||
|
search_roi: list[int] | None = None # [x, y, w, h] limita area
|
||||||
|
|
||||||
|
|
||||||
def _simple_to_technical(
|
def _simple_to_technical(
|
||||||
@@ -526,6 +544,9 @@ def match_simple(p: SimpleMatchParams):
|
|||||||
tech = _simple_to_technical(p, roi_img)
|
tech = _simple_to_technical(p, roi_img)
|
||||||
|
|
||||||
key = _matcher_cache_key(roi_img, tech)
|
key = _matcher_cache_key(roi_img, tech)
|
||||||
|
# Halcon-mode init params: incidono sul training, includere in cache key
|
||||||
|
halcon_init_key = f"|pol={p.use_polarity}|gpu={p.use_gpu}"
|
||||||
|
key = key + halcon_init_key
|
||||||
m = _cache_get_matcher(key)
|
m = _cache_get_matcher(key)
|
||||||
if m is None:
|
if m is None:
|
||||||
m = LineShapeMatcher(
|
m = LineShapeMatcher(
|
||||||
@@ -537,17 +558,30 @@ def match_simple(p: SimpleMatchParams):
|
|||||||
scale_step=tech["scale_step"],
|
scale_step=tech["scale_step"],
|
||||||
spread_radius=tech["spread_radius"],
|
spread_radius=tech["spread_radius"],
|
||||||
pyramid_levels=tech["pyramid_levels"],
|
pyramid_levels=tech["pyramid_levels"],
|
||||||
|
use_polarity=p.use_polarity,
|
||||||
|
use_gpu=p.use_gpu,
|
||||||
)
|
)
|
||||||
t0 = time.time(); n = m.train(roi_img); t_train = time.time() - t0
|
t0 = time.time(); n = m.train(roi_img); t_train = time.time() - t0
|
||||||
_cache_put_matcher(key, m)
|
_cache_put_matcher(key, m)
|
||||||
else:
|
else:
|
||||||
n = len(m.variants); t_train = 0.0
|
n = len(m.variants); t_train = 0.0
|
||||||
nms = tech["nms_radius"] if tech["nms_radius"] > 0 else None
|
nms = tech["nms_radius"] if tech["nms_radius"] > 0 else None
|
||||||
|
search_roi_t = tuple(p.search_roi) if p.search_roi else None
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
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),
|
scale_penalty=tech.get("scale_penalty", 0.0),
|
||||||
|
# Halcon-mode flags
|
||||||
|
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
|
t_find = time.time() - t0
|
||||||
|
|
||||||
@@ -576,6 +610,70 @@ def tune(p: TuneParams):
|
|||||||
return {k: v for k, v in t.items() if not k.startswith("_")}
|
return {k: v for k, v in t.items() if not k.startswith("_")}
|
||||||
|
|
||||||
|
|
||||||
|
# --- V: Save/Load ricette pre-trained ---
|
||||||
|
|
||||||
|
class SaveRecipeParams(BaseModel):
|
||||||
|
model_id: str
|
||||||
|
scene_id: str | None = None
|
||||||
|
roi: list[int]
|
||||||
|
# Riusa stessi param simple per training equivalente
|
||||||
|
tipo: str = "intero"
|
||||||
|
simmetria: str = "nessuna"
|
||||||
|
scala: str = "fissa"
|
||||||
|
precisione: str = "normale"
|
||||||
|
use_polarity: bool = False
|
||||||
|
use_gpu: bool = False
|
||||||
|
name: str # nome file ricetta (no path)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/recipes")
|
||||||
|
def save_recipe(p: SaveRecipeParams):
|
||||||
|
"""Allena matcher e salva su disco come ricetta riutilizzabile."""
|
||||||
|
model = _load_image(p.model_id)
|
||||||
|
if model is None:
|
||||||
|
raise HTTPException(404, "Modello non trovato")
|
||||||
|
x, y, w, h = p.roi
|
||||||
|
roi_img = model[y:y + h, x:x + w]
|
||||||
|
sp = SimpleMatchParams(
|
||||||
|
model_id=p.model_id, scene_id=p.scene_id or p.model_id, roi=p.roi,
|
||||||
|
tipo=p.tipo, simmetria=p.simmetria, scala=p.scala,
|
||||||
|
precisione=p.precisione,
|
||||||
|
use_polarity=p.use_polarity, use_gpu=p.use_gpu,
|
||||||
|
)
|
||||||
|
tech = _simple_to_technical(sp, roi_img)
|
||||||
|
m = LineShapeMatcher(
|
||||||
|
num_features=tech["num_features"],
|
||||||
|
weak_grad=tech["weak_grad"], strong_grad=tech["strong_grad"],
|
||||||
|
angle_range_deg=(tech["angle_min"], tech["angle_max"]),
|
||||||
|
angle_step_deg=tech["angle_step"],
|
||||||
|
scale_range=(tech["scale_min"], tech["scale_max"]),
|
||||||
|
scale_step=tech["scale_step"],
|
||||||
|
spread_radius=tech["spread_radius"],
|
||||||
|
pyramid_levels=tech["pyramid_levels"],
|
||||||
|
use_polarity=p.use_polarity,
|
||||||
|
use_gpu=p.use_gpu,
|
||||||
|
)
|
||||||
|
m.train(roi_img)
|
||||||
|
safe_name = "".join(c for c in p.name if c.isalnum() or c in "._-")
|
||||||
|
if not safe_name:
|
||||||
|
raise HTTPException(400, "Nome ricetta non valido")
|
||||||
|
if not safe_name.endswith(".npz"):
|
||||||
|
safe_name += ".npz"
|
||||||
|
target = RECIPES_DIR / safe_name
|
||||||
|
m.save_model(str(target))
|
||||||
|
return {"name": safe_name, "size": target.stat().st_size,
|
||||||
|
"n_variants": len(m.variants)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/recipes")
|
||||||
|
def list_recipes():
|
||||||
|
files = []
|
||||||
|
if RECIPES_DIR.is_dir():
|
||||||
|
for f in sorted(RECIPES_DIR.glob("*.npz")):
|
||||||
|
files.append({"name": f.name, "size": f.stat().st_size})
|
||||||
|
return {"files": files, "dir": str(RECIPES_DIR)}
|
||||||
|
|
||||||
|
|
||||||
# Mount static
|
# Mount static
|
||||||
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,39 @@ function readUserParams() {
|
|||||||
document.getElementById("p-penalita-scala").value),
|
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),
|
||||||
|
...readHalconFlags(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readHalconFlags() {
|
||||||
|
// Halcon-mode toggle: tutti i flag default-off, esposti via "Modalità Halcon"
|
||||||
|
const $cb = (id) => document.getElementById(id)?.checked ?? false;
|
||||||
|
const $num = (id, def) => {
|
||||||
|
const v = parseFloat(document.getElementById(id)?.value);
|
||||||
|
return Number.isFinite(v) ? v : def;
|
||||||
|
};
|
||||||
|
const $int = (id, def) => {
|
||||||
|
const v = parseInt(document.getElementById(id)?.value, 10);
|
||||||
|
return Number.isFinite(v) ? v : def;
|
||||||
|
};
|
||||||
|
const roiStr = document.getElementById("hc-search-roi")?.value.trim() ?? "";
|
||||||
|
let search_roi = null;
|
||||||
|
if (roiStr) {
|
||||||
|
const p = roiStr.split(/[ ,;]+/).map((x) => parseInt(x, 10));
|
||||||
|
if (p.length === 4 && p.every((v) => Number.isFinite(v))) search_roi = p;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
use_polarity: $cb("hc-use-polarity"),
|
||||||
|
use_gpu: $cb("hc-use-gpu"),
|
||||||
|
use_soft_score: $cb("hc-soft-score"),
|
||||||
|
subpixel_lm: $cb("hc-subpixel-lm"),
|
||||||
|
refine_pose_joint: $cb("hc-refine-joint"),
|
||||||
|
pyramid_propagate: $cb("hc-pyr-propagate"),
|
||||||
|
min_recall: $num("hc-min-recall", 0),
|
||||||
|
nms_iou_threshold: $num("hc-nms-iou", 0.3),
|
||||||
|
greediness: $num("hc-greediness", 0),
|
||||||
|
coarse_stride: $int("hc-coarse-stride", 1),
|
||||||
|
search_roi: search_roi,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,6 +400,44 @@ function setStatus(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Init ----------
|
// ---------- Init ----------
|
||||||
|
// ---------- V: Save recipe ----------
|
||||||
|
async function saveRecipe() {
|
||||||
|
if (!state.model || !state.roi) {
|
||||||
|
alert("Seleziona modello e disegna ROI prima di salvare la ricetta.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = document.getElementById("hc-recipe-name").value.trim();
|
||||||
|
if (!name) {
|
||||||
|
alert("Inserisci un nome per la ricetta.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const user = readUserParams();
|
||||||
|
const body = {
|
||||||
|
model_id: state.model.id,
|
||||||
|
scene_id: state.scene?.id || state.model.id,
|
||||||
|
roi: state.roi,
|
||||||
|
tipo: user.tipo,
|
||||||
|
simmetria: user.simmetria,
|
||||||
|
scala: user.scala,
|
||||||
|
precisione: user.precisione,
|
||||||
|
use_polarity: user.use_polarity,
|
||||||
|
use_gpu: user.use_gpu,
|
||||||
|
name: name,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const r = await fetch("/recipes", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error(await r.text());
|
||||||
|
const j = await r.json();
|
||||||
|
alert(`Ricetta salvata: ${j.name}\n${j.n_variants} varianti, ${j.size} bytes`);
|
||||||
|
} catch (e) {
|
||||||
|
alert(`Errore salvataggio: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", async () => {
|
window.addEventListener("DOMContentLoaded", async () => {
|
||||||
buildAdvancedForm();
|
buildAdvancedForm();
|
||||||
setupROI();
|
setupROI();
|
||||||
@@ -394,6 +465,8 @@ 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-save-recipe").addEventListener("click",
|
||||||
|
saveRecipe);
|
||||||
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 =
|
||||||
|
|||||||
@@ -129,6 +129,67 @@
|
|||||||
<input type="number" id="p-max-matches" value="25" min="1" max="200">
|
<input type="number" id="p-max-matches" value="25" min="1" max="200">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Modalità Halcon</summary>
|
||||||
|
<div class="halcon-grid">
|
||||||
|
<label class="hc-row" title="16-bin orientation polarity-aware (mod 2π)">
|
||||||
|
<input type="checkbox" id="hc-use-polarity">
|
||||||
|
<span>Polarity 16-bin (F)</span>
|
||||||
|
</label>
|
||||||
|
<label class="hc-row" title="Score continuo cos(θ_t-θ_s) invece di bin">
|
||||||
|
<input type="checkbox" id="hc-soft-score">
|
||||||
|
<span>Soft-margin score (Y)</span>
|
||||||
|
</label>
|
||||||
|
<label class="hc-row" title="Sub-pixel refinement gradient field LM">
|
||||||
|
<input type="checkbox" id="hc-subpixel-lm">
|
||||||
|
<span>Sub-pixel LM 0.05 px (Z)</span>
|
||||||
|
</label>
|
||||||
|
<label class="hc-row" title="Refine congiunto Nelder-Mead (cx,cy,θ)">
|
||||||
|
<input type="checkbox" id="hc-refine-joint">
|
||||||
|
<span>Refine pose joint</span>
|
||||||
|
</label>
|
||||||
|
<label class="hc-row" title="Pyramid candidates propagation">
|
||||||
|
<input type="checkbox" id="hc-pyr-propagate">
|
||||||
|
<span>Pyramid propagate</span>
|
||||||
|
</label>
|
||||||
|
<label class="hc-row" title="OpenCL GPU offload (silent fallback CPU)">
|
||||||
|
<input type="checkbox" id="hc-use-gpu">
|
||||||
|
<span>GPU OpenCL (R)</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="hc-row hc-num">
|
||||||
|
<label>Min recall (M)</label>
|
||||||
|
<input type="number" id="hc-min-recall" value="0.0" min="0" max="1" step="0.05">
|
||||||
|
</div>
|
||||||
|
<div class="hc-row hc-num">
|
||||||
|
<label>NMS IoU thr (A)</label>
|
||||||
|
<input type="number" id="hc-nms-iou" value="0.3" min="0" max="1" step="0.05">
|
||||||
|
</div>
|
||||||
|
<div class="hc-row hc-num">
|
||||||
|
<label>Greediness</label>
|
||||||
|
<input type="number" id="hc-greediness" value="0.0" min="0" max="1" step="0.1">
|
||||||
|
</div>
|
||||||
|
<div class="hc-row hc-num">
|
||||||
|
<label>Coarse stride</label>
|
||||||
|
<input type="number" id="hc-coarse-stride" value="1" min="1" max="4" step="1">
|
||||||
|
</div>
|
||||||
|
<div class="hc-row hc-num" style="grid-column:1/-1">
|
||||||
|
<label title="Limita area di ricerca scena: x,y,w,h (vuoto = tutta scena)">
|
||||||
|
Search ROI (x,y,w,h)
|
||||||
|
</label>
|
||||||
|
<input type="text" id="hc-search-roi" placeholder="es. 100,50,800,400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hc-row" style="grid-column:1/-1; border-top:1px solid #444; padding-top:8px">
|
||||||
|
<label>Ricetta pre-trained (V)</label>
|
||||||
|
<div style="display:flex; gap:6px; margin-top:4px">
|
||||||
|
<input type="text" id="hc-recipe-name" placeholder="nome_ricetta" style="flex:1">
|
||||||
|
<button class="btn" id="btn-save-recipe" type="button">💾 Salva</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Avanzate</summary>
|
<summary>Avanzate</summary>
|
||||||
<div id="adv-form"></div>
|
<div id="adv-form"></div>
|
||||||
|
|||||||
@@ -156,3 +156,20 @@ footer h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#col-model, #col-scene { min-width: 0; }
|
#col-model, #col-scene { min-width: 0; }
|
||||||
|
|
||||||
|
/* Halcon-mode panel */
|
||||||
|
.halcon-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 6px 12px;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.hc-row {
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
}
|
||||||
|
.hc-row.hc-num {
|
||||||
|
flex-direction: column; align-items: flex-start;
|
||||||
|
}
|
||||||
|
.hc-row.hc-num label { font-size: 11px; color: #aaa; }
|
||||||
|
.hc-row.hc-num input { width: 100%; }
|
||||||
|
|||||||
Reference in New Issue
Block a user