ui: parametri user-friendly (tipo/simmetria/scala/precisione)
Nascosti i parametri tecnici (num_features, weak/strong_grad, spread, pyramid) incomprensibili per operatori. Sostituiti da scelte semantiche: - Tipo modello: intero | parziale - Simmetria: nessuna | bilaterale (180) | rotazionale 3/4/6/8x - Variazione scala: fissa | 10% | 25% | 50% - Precisione: veloce 10 | normale 5 | preciso 2 - Score minimo: slider - Max match: input Server: nuovo endpoint POST /match_simple. Deriva tecnici via _simple_to_technical(roi) che analizza la ROI: - weak/strong_grad da percentili - num_features da densita edge x tipo - pyramid_levels da min(h,w) ROI - spread_radius proporzionale Frontend: select + slider, sezione Avanzate collassabile per override. Test rings_and_nuts preset intero/nessuna/medio/normale: 3/3 ruote in 2.14s Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -146,6 +146,94 @@ class TuneParams(BaseModel):
|
||||
roi: list[int]
|
||||
|
||||
|
||||
# ---------- User-facing (simple) params ----------
|
||||
|
||||
SYMMETRY_TO_ANGLE_MAX = {
|
||||
"nessuna": 360.0,
|
||||
"bilaterale": 180.0,
|
||||
"rot_3": 120.0,
|
||||
"rot_4": 90.0,
|
||||
"rot_6": 60.0,
|
||||
"rot_8": 45.0,
|
||||
}
|
||||
|
||||
SCALE_PRESETS = {
|
||||
"fissa": (1.0, 1.0, 0.1),
|
||||
"mini": (0.9, 1.1, 0.05), # ±10%
|
||||
"medio": (0.75, 1.25, 0.05), # ±25%
|
||||
"max": (0.5, 1.5, 0.05), # ±50%
|
||||
}
|
||||
|
||||
PRECISION_ANGLE_STEP = {
|
||||
"veloce": 10.0,
|
||||
"normale": 5.0,
|
||||
"preciso": 2.0,
|
||||
}
|
||||
|
||||
|
||||
class SimpleMatchParams(BaseModel):
|
||||
model_id: str
|
||||
scene_id: str
|
||||
roi: list[int]
|
||||
tipo: str = "intero" # "intero" | "parziale"
|
||||
simmetria: str = "nessuna" # chiave SYMMETRY_TO_ANGLE_MAX
|
||||
scala: str = "fissa" # chiave SCALE_PRESETS
|
||||
precisione: str = "normale" # chiave PRECISION_ANGLE_STEP
|
||||
min_score: float = 0.70
|
||||
max_matches: int = 25
|
||||
|
||||
|
||||
def _simple_to_technical(
|
||||
p: SimpleMatchParams, roi_img: np.ndarray,
|
||||
) -> dict:
|
||||
"""Converti parametri user-facing → tecnici usando analisi della ROI."""
|
||||
from pm2d.auto_tune import auto_tune as _auto
|
||||
|
||||
tune = _auto(roi_img)
|
||||
h, w = roi_img.shape[:2]
|
||||
min_side = min(h, w)
|
||||
|
||||
# Feature count: parziale = meno feature (area minore)
|
||||
nf = tune["num_features"]
|
||||
if p.tipo == "parziale":
|
||||
nf = max(32, int(nf * 0.6))
|
||||
|
||||
# Piramide derivata da dimensione ROI
|
||||
if min_side < 60:
|
||||
pyr = 1
|
||||
elif min_side < 150:
|
||||
pyr = 2
|
||||
elif min_side < 400:
|
||||
pyr = 3
|
||||
else:
|
||||
pyr = 4
|
||||
|
||||
# Spread radius ~2-3% del lato minimo
|
||||
spread = max(3, min(10, int(round(min_side * 0.03))))
|
||||
|
||||
angle_max = SYMMETRY_TO_ANGLE_MAX.get(p.simmetria, 360.0)
|
||||
smin, smax, sstep = SCALE_PRESETS.get(p.scala, (1.0, 1.0, 0.1))
|
||||
ang_step = PRECISION_ANGLE_STEP.get(p.precisione, 5.0)
|
||||
|
||||
return {
|
||||
"num_features": nf,
|
||||
"weak_grad": tune["weak_grad"],
|
||||
"strong_grad": tune["strong_grad"],
|
||||
"spread_radius": spread,
|
||||
"pyramid_levels": pyr,
|
||||
"angle_min": 0.0,
|
||||
"angle_max": angle_max,
|
||||
"angle_step": ang_step,
|
||||
"scale_min": smin,
|
||||
"scale_max": smax,
|
||||
"scale_step": sstep,
|
||||
"min_score": p.min_score,
|
||||
"max_matches": p.max_matches,
|
||||
"nms_radius": 0,
|
||||
"verify_threshold": 0.4,
|
||||
}
|
||||
|
||||
|
||||
# ---------------- Endpoints ----------------
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
@@ -220,6 +308,58 @@ def match(p: MatchParams):
|
||||
)
|
||||
|
||||
|
||||
@app.post("/match_simple", response_model=MatchResp)
|
||||
def match_simple(p: SimpleMatchParams):
|
||||
"""Match con parametri user-facing (tipo/simmetria/scala/precisione).
|
||||
|
||||
Il server deriva i parametri tecnici (num_features, soglie gradiente,
|
||||
piramide, ecc.) dall'analisi automatica della ROI.
|
||||
"""
|
||||
model = _IMAGES.get(p.model_id)
|
||||
scene = _IMAGES.get(p.scene_id)
|
||||
if model is None or scene is None:
|
||||
raise HTTPException(404, "Immagini non trovate")
|
||||
x, y, w, h = p.roi
|
||||
x = max(0, x); y = max(0, y)
|
||||
w = max(1, min(w, model.shape[1] - x))
|
||||
h = max(1, min(h, model.shape[0] - y))
|
||||
roi_img = model[y:y + h, x:x + w]
|
||||
|
||||
tech = _simple_to_technical(p, 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"],
|
||||
)
|
||||
t0 = time.time(); n = m.train(roi_img); t_train = time.time() - t0
|
||||
nms = tech["nms_radius"] if tech["nms_radius"] > 0 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"],
|
||||
)
|
||||
t_find = time.time() - t0
|
||||
|
||||
tg = cv2.cvtColor(roi_img, cv2.COLOR_BGR2GRAY)
|
||||
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=t_train, find_time=t_find,
|
||||
num_variants=n, annotated_id=ann_id,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/auto_tune")
|
||||
def tune(p: TuneParams):
|
||||
model = _IMAGES.get(p.model_id)
|
||||
|
||||
Reference in New Issue
Block a user