feat(web): wiring UI per modalita Halcon (M, Y, Z, V, X, R + altri)
UI espone tutti i nuovi flag tramite sezione pieghevole "Modalita Halcon" nel pannello impostazioni. Default off = comportamento backward compat. Flag esposti (checkbox + numerici): - use_polarity (F): 16-bin orientation mod 2pi - use_gpu (R): OpenCL UMat con silent fallback CPU - use_soft_score (Y): score continuo cos(theta_t-theta_s) - subpixel_lm (Z): refinement 0.05 px gradient field - refine_pose_joint: Nelder-Mead 3D (cx,cy,theta) - pyramid_propagate: top-K propagation a full-res - min_recall (M): filtro feature-recall - nms_iou_threshold (A): IoU bbox poligonale - greediness: early-exit kernel - coarse_stride: sub-sampling top-level - search_roi: x,y,w,h area di ricerca Persistenza ricette (V): - Endpoint POST /recipes: training + save .npz in recipes/ - Endpoint GET /recipes: lista - UI: campo nome + bottone "Salva" sotto i flag Server SimpleMatchParams esteso con tutti i campi; pipeline match_simple propaga init-flags al cache key (use_polarity/use_gpu = retrain) e find-flags al m.find(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,10 @@ IMAGES_DIR = Path(_images_dir_raw)
|
||||
if not IMAGES_DIR.is_absolute():
|
||||
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.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
|
||||
min_score: float = 0.65
|
||||
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(
|
||||
@@ -526,6 +544,9 @@ def match_simple(p: SimpleMatchParams):
|
||||
tech = _simple_to_technical(p, roi_img)
|
||||
|
||||
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)
|
||||
if m is None:
|
||||
m = LineShapeMatcher(
|
||||
@@ -537,17 +558,30 @@ def match_simple(p: SimpleMatchParams):
|
||||
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,
|
||||
)
|
||||
t0 = time.time(); n = m.train(roi_img); t_train = time.time() - t0
|
||||
_cache_put_matcher(key, m)
|
||||
else:
|
||||
n = len(m.variants); t_train = 0.0
|
||||
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()
|
||||
matches = m.find(
|
||||
scene, min_score=tech["min_score"], max_matches=tech["max_matches"],
|
||||
nms_radius=nms, verify_threshold=tech["verify_threshold"],
|
||||
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
|
||||
|
||||
@@ -576,6 +610,70 @@ def tune(p: TuneParams):
|
||||
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
|
||||
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user