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)
|
||||
|
||||
+93
-99
@@ -1,21 +1,14 @@
|
||||
// Pattern Matching 2D - frontend
|
||||
// Pattern Matching 2D - frontend (UI semplificata operator-friendly)
|
||||
|
||||
const PARAMS = [
|
||||
["angle_min", "Angolo min [°]", "float", 0],
|
||||
["angle_max", "Angolo max [°]", "float", 360],
|
||||
["angle_step", "Angolo step [°]", "float", 5],
|
||||
["scale_min", "Scala min", "float", 1],
|
||||
["scale_max", "Scala max", "float", 1],
|
||||
["scale_step", "Scala step", "float", 0.1],
|
||||
["min_score", "Score min [0..1]", "float", 0.55],
|
||||
["max_matches", "Max match", "int", 25],
|
||||
["nms_radius", "NMS radius (0=auto)", "int", 0],
|
||||
["num_features", "Num feature", "int", 96],
|
||||
["weak_grad", "Weak grad", "float", 30],
|
||||
["strong_grad", "Strong grad", "float", 60],
|
||||
["spread_radius", "Spread radius", "int", 5],
|
||||
["pyramid_levels", "Pyramid levels", "int", 3],
|
||||
// Parametri avanzati (sezione collassabile)
|
||||
const ADV_PARAMS = [
|
||||
["num_features", "Num feature", "int", ""],
|
||||
["weak_grad", "Weak grad", "float", ""],
|
||||
["strong_grad", "Strong grad", "float", ""],
|
||||
["spread_radius", "Spread radius", "int", ""],
|
||||
["pyramid_levels", "Pyramid levels", "int", ""],
|
||||
["verify_threshold", "Verify NCC thr", "float", 0.4],
|
||||
["nms_radius", "NMS radius (0=auto)", "int", 0],
|
||||
];
|
||||
|
||||
const PALETTE = [
|
||||
@@ -24,27 +17,22 @@ const PALETTE = [
|
||||
];
|
||||
|
||||
const state = {
|
||||
model: null, // {id, w, h, img:HTMLImageElement, scale, ox, oy}
|
||||
scene: null,
|
||||
roi: null, // [x, y, w, h] in originale
|
||||
drag: null, // {x0,y0,x1,y1} in canvas coords
|
||||
matches: [],
|
||||
annotatedImg: null, // HTMLImageElement annotated
|
||||
model: null, scene: null, roi: null, drag: null,
|
||||
matches: [], annotatedImg: null,
|
||||
};
|
||||
|
||||
// ---------- Params form ----------
|
||||
function buildForm() {
|
||||
const form = document.getElementById("params-form");
|
||||
// ---------- Forms ----------
|
||||
function buildAdvancedForm() {
|
||||
const form = document.getElementById("adv-form");
|
||||
form.innerHTML = "";
|
||||
for (const [key, label, type, def] of PARAMS) {
|
||||
for (const [key, label, , def] of ADV_PARAMS) {
|
||||
const lbl = document.createElement("label");
|
||||
lbl.textContent = label;
|
||||
lbl.htmlFor = `p-${key}`;
|
||||
const inp = document.createElement("input");
|
||||
inp.id = `p-${key}`;
|
||||
inp.name = key;
|
||||
inp.value = String(def);
|
||||
inp.id = `adv-${key}`;
|
||||
inp.type = "text";
|
||||
inp.placeholder = "auto";
|
||||
inp.value = def === "" ? "" : String(def);
|
||||
inp.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") doMatch();
|
||||
});
|
||||
@@ -53,23 +41,27 @@ function buildForm() {
|
||||
}
|
||||
}
|
||||
|
||||
function readParams() {
|
||||
function readUserParams() {
|
||||
return {
|
||||
tipo: document.getElementById("p-tipo").value,
|
||||
simmetria: document.getElementById("p-simmetria").value,
|
||||
scala: document.getElementById("p-scala").value,
|
||||
precisione: document.getElementById("p-precisione").value,
|
||||
min_score: parseFloat(document.getElementById("p-min-score").value),
|
||||
max_matches: parseInt(document.getElementById("p-max-matches").value, 10),
|
||||
};
|
||||
}
|
||||
|
||||
function readAdvancedOverrides() {
|
||||
const out = {};
|
||||
for (const [key, , type] of PARAMS) {
|
||||
const v = document.getElementById(`p-${key}`).value.trim();
|
||||
for (const [key, , type] of ADV_PARAMS) {
|
||||
const v = document.getElementById(`adv-${key}`).value.trim();
|
||||
if (v === "") continue;
|
||||
out[key] = type === "int" ? parseInt(v, 10) : parseFloat(v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function setParams(values) {
|
||||
for (const [key] of PARAMS) {
|
||||
if (values[key] !== undefined) {
|
||||
document.getElementById(`p-${key}`).value = String(values[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Upload ----------
|
||||
async function uploadFile(file) {
|
||||
const fd = new FormData();
|
||||
@@ -103,8 +95,7 @@ async function onLoadScene(file) {
|
||||
const meta = await uploadFile(file);
|
||||
const img = await loadImage(`/image/${meta.id}/raw`);
|
||||
state.scene = { id: meta.id, w: meta.width, h: meta.height, img };
|
||||
state.matches = [];
|
||||
state.annotatedImg = null;
|
||||
state.matches = []; state.annotatedImg = null;
|
||||
setStatus(`Scena: ${file.name} ${meta.width}x${meta.height}`);
|
||||
renderScene();
|
||||
}
|
||||
@@ -112,11 +103,8 @@ async function onLoadScene(file) {
|
||||
// ---------- Rendering ----------
|
||||
function fitToCanvas(img, cw, ch) {
|
||||
const sc = Math.min(cw / img.width, ch / img.height);
|
||||
const dw = img.width * sc;
|
||||
const dh = img.height * sc;
|
||||
const ox = (cw - dw) / 2;
|
||||
const oy = (ch - dh) / 2;
|
||||
return { sc, ox, oy, dw, dh };
|
||||
const dw = img.width * sc, dh = img.height * sc;
|
||||
return { sc, ox: (cw - dw) / 2, oy: (ch - dh) / 2, dw, dh };
|
||||
}
|
||||
|
||||
function renderModel() {
|
||||
@@ -127,26 +115,22 @@ function renderModel() {
|
||||
if (!state.model) return;
|
||||
const fit = fitToCanvas(state.model.img, cnv.width, cnv.height);
|
||||
state.model.scale = fit.sc;
|
||||
state.model.ox = fit.ox;
|
||||
state.model.oy = fit.oy;
|
||||
state.model.ox = fit.ox; state.model.oy = fit.oy;
|
||||
ctx.drawImage(state.model.img, fit.ox, fit.oy, fit.dw, fit.dh);
|
||||
if (state.roi) {
|
||||
const [x, y, w, h] = state.roi;
|
||||
ctx.strokeStyle = "#00ff80";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = "#00ff80"; ctx.lineWidth = 2;
|
||||
ctx.strokeRect(fit.ox + x * fit.sc, fit.oy + y * fit.sc,
|
||||
w * fit.sc, h * fit.sc);
|
||||
}
|
||||
if (state.drag) {
|
||||
ctx.strokeStyle = "#ffff00";
|
||||
ctx.setLineDash([4, 2]);
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([4, 2]); ctx.lineWidth = 2;
|
||||
ctx.strokeRect(
|
||||
Math.min(state.drag.x0, state.drag.x1),
|
||||
Math.min(state.drag.y0, state.drag.y1),
|
||||
Math.abs(state.drag.x1 - state.drag.x0),
|
||||
Math.abs(state.drag.y1 - state.drag.y0),
|
||||
);
|
||||
Math.abs(state.drag.y1 - state.drag.y0));
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
}
|
||||
@@ -156,13 +140,10 @@ function renderScene() {
|
||||
const ctx = cnv.getContext("2d");
|
||||
ctx.fillStyle = "#141414";
|
||||
ctx.fillRect(0, 0, cnv.width, cnv.height);
|
||||
if (state.annotatedImg) {
|
||||
const fit = fitToCanvas(state.annotatedImg, cnv.width, cnv.height);
|
||||
ctx.drawImage(state.annotatedImg, fit.ox, fit.oy, fit.dw, fit.dh);
|
||||
} else if (state.scene) {
|
||||
const fit = fitToCanvas(state.scene.img, cnv.width, cnv.height);
|
||||
ctx.drawImage(state.scene.img, fit.ox, fit.oy, fit.dw, fit.dh);
|
||||
}
|
||||
const img = state.annotatedImg || (state.scene && state.scene.img);
|
||||
if (!img) return;
|
||||
const fit = fitToCanvas(img, cnv.width, cnv.height);
|
||||
ctx.drawImage(img, fit.ox, fit.oy, fit.dw, fit.dh);
|
||||
}
|
||||
|
||||
// ---------- ROI drag ----------
|
||||
@@ -185,12 +166,10 @@ function setupROI() {
|
||||
state.drag.x1 = p.x; state.drag.y1 = p.y;
|
||||
renderModel();
|
||||
});
|
||||
cnv.addEventListener("mouseup", (e) => {
|
||||
cnv.addEventListener("mouseup", () => {
|
||||
if (!state.drag || !state.model) return;
|
||||
const d = state.drag;
|
||||
state.drag = null;
|
||||
const d = state.drag; state.drag = null;
|
||||
if (Math.abs(d.x1 - d.x0) < 4 || Math.abs(d.y1 - d.y0) < 4) return;
|
||||
// Canvas → originale
|
||||
const m = state.model;
|
||||
const ix0 = Math.round((Math.min(d.x0, d.x1) - m.ox) / m.scale);
|
||||
const iy0 = Math.round((Math.min(d.y0, d.y1) - m.oy) / m.scale);
|
||||
@@ -207,57 +186,68 @@ function setupROI() {
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Actions ----------
|
||||
// ---------- Match action ----------
|
||||
async function doMatch() {
|
||||
if (!state.model) { setStatus("Carica modello"); return; }
|
||||
if (!state.scene) { setStatus("Carica scena"); return; }
|
||||
if (!state.roi) { setStatus("Seleziona ROI sul modello"); return; }
|
||||
const params = readParams();
|
||||
const user = readUserParams();
|
||||
const adv = readAdvancedOverrides();
|
||||
setStatus("Match in corso...");
|
||||
const body = {
|
||||
model_id: state.model.id,
|
||||
scene_id: state.scene.id,
|
||||
roi: state.roi,
|
||||
...params,
|
||||
|
||||
// Se utente ha fornito override avanzati → usa /match (tecnico)
|
||||
// altrimenti /match_simple (operator mode)
|
||||
const hasAdv = Object.keys(adv).length > 0;
|
||||
const url = hasAdv ? "/match" : "/match_simple";
|
||||
let body;
|
||||
if (hasAdv) {
|
||||
// Merge simple → tecnici base, poi overrides
|
||||
const SYM_MAP = {nessuna:360, bilaterale:180, rot_3:120,
|
||||
rot_4:90, rot_6:60, rot_8:45};
|
||||
const SCALE_MAP = {fissa:[1,1,0.1], mini:[0.9,1.1,0.05],
|
||||
medio:[0.75,1.25,0.05], max:[0.5,1.5,0.05]};
|
||||
const PREC_MAP = {veloce:10, normale:5, preciso:2};
|
||||
const [smin, smax, sstep] = SCALE_MAP[user.scala];
|
||||
body = {
|
||||
model_id: state.model.id, scene_id: state.scene.id, roi: state.roi,
|
||||
angle_min: 0, angle_max: SYM_MAP[user.simmetria] || 360,
|
||||
angle_step: PREC_MAP[user.precisione] || 5,
|
||||
scale_min: smin, scale_max: smax, scale_step: sstep,
|
||||
min_score: user.min_score, max_matches: user.max_matches,
|
||||
num_features: adv.num_features ?? 96,
|
||||
weak_grad: adv.weak_grad ?? 30,
|
||||
strong_grad: adv.strong_grad ?? 60,
|
||||
spread_radius: adv.spread_radius ?? 5,
|
||||
pyramid_levels: adv.pyramid_levels ?? 3,
|
||||
verify_threshold: adv.verify_threshold ?? 0.4,
|
||||
nms_radius: adv.nms_radius ?? 0,
|
||||
};
|
||||
const r = await fetch("/match", {
|
||||
} else {
|
||||
body = {
|
||||
model_id: state.model.id, scene_id: state.scene.id, roi: state.roi,
|
||||
...user,
|
||||
};
|
||||
}
|
||||
|
||||
const r = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!r.ok) {
|
||||
const t = await r.text();
|
||||
setStatus(`Errore: ${t}`);
|
||||
return;
|
||||
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()}`,
|
||||
);
|
||||
`/image/${data.annotated_id}/raw?t=${Date.now()}`);
|
||||
renderScene();
|
||||
renderLegend();
|
||||
document.getElementById("t-train").textContent = `${data.train_time.toFixed(2)}s`;
|
||||
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`);
|
||||
}
|
||||
|
||||
async function doAutoTune() {
|
||||
if (!state.model || !state.roi) {
|
||||
setStatus("Carica modello + seleziona ROI");
|
||||
return;
|
||||
}
|
||||
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) { setStatus("Errore auto-tune"); return; }
|
||||
const tune = await r.json();
|
||||
setParams(tune);
|
||||
setStatus("Auto-tune applicato — premi MATCH");
|
||||
setStatus(`${data.matches.length} match trovati${hasAdv ? " (avanzato)" : ""}`);
|
||||
}
|
||||
|
||||
function renderLegend() {
|
||||
@@ -285,7 +275,7 @@ function setStatus(s) {
|
||||
|
||||
// ---------- Init ----------
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
buildForm();
|
||||
buildAdvancedForm();
|
||||
setupROI();
|
||||
document.getElementById("file-model").addEventListener("change", (e) => {
|
||||
if (e.target.files[0]) onLoadModel(e.target.files[0]);
|
||||
@@ -294,7 +284,11 @@ window.addEventListener("DOMContentLoaded", () => {
|
||||
if (e.target.files[0]) onLoadScene(e.target.files[0]);
|
||||
});
|
||||
document.getElementById("btn-match").addEventListener("click", doMatch);
|
||||
document.getElementById("btn-tune").addEventListener("click", doAutoTune);
|
||||
const slider = document.getElementById("p-min-score");
|
||||
slider.addEventListener("input", (e) => {
|
||||
document.getElementById("v-score").textContent =
|
||||
parseFloat(e.target.value).toFixed(2);
|
||||
});
|
||||
renderModel();
|
||||
renderScene();
|
||||
});
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
<label class="btn">📂 Scena
|
||||
<input type="file" id="file-scene" accept="image/*" hidden>
|
||||
</label>
|
||||
<button class="btn" id="btn-tune">Auto-tune</button>
|
||||
<button class="btn btn-go" id="btn-match">▶ MATCH</button>
|
||||
<span id="status">Carica modello + disegna ROI + carica scena</span>
|
||||
<span id="status">Carica modello, disegna ROI, carica scena</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -38,9 +37,63 @@
|
||||
</section>
|
||||
|
||||
<section class="col" id="col-params">
|
||||
<h2>PARAMETRI</h2>
|
||||
<div id="params-form"></div>
|
||||
<h2 style="margin-top:16px">TEMPI</h2>
|
||||
<h2>IMPOSTAZIONI</h2>
|
||||
|
||||
<div class="field">
|
||||
<label>Tipo modello</label>
|
||||
<select id="p-tipo">
|
||||
<option value="intero">Oggetto intero</option>
|
||||
<option value="parziale">Parte di oggetto</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Simmetria</label>
|
||||
<select id="p-simmetria">
|
||||
<option value="nessuna">Nessuna (0..360°)</option>
|
||||
<option value="bilaterale">Bilaterale (speculare 180°)</option>
|
||||
<option value="rot_3">Rotazionale 3× (120°)</option>
|
||||
<option value="rot_4">Rotazionale 4× (90°)</option>
|
||||
<option value="rot_6">Rotazionale 6× (60°)</option>
|
||||
<option value="rot_8">Rotazionale 8× (45°)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Variazione scala</label>
|
||||
<select id="p-scala">
|
||||
<option value="fissa">Fissa (setup calibrato)</option>
|
||||
<option value="mini">±10%</option>
|
||||
<option value="medio">±25%</option>
|
||||
<option value="max">±50%</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Precisione angolare</label>
|
||||
<select id="p-precisione">
|
||||
<option value="veloce">Veloce (10°)</option>
|
||||
<option value="normale" selected>Normale (5°)</option>
|
||||
<option value="preciso">Preciso (2°)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Score minimo <span id="v-score">0.70</span></label>
|
||||
<input type="range" id="p-min-score" min="0.30" max="0.95" step="0.05" value="0.70">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Max match</label>
|
||||
<input type="number" id="p-max-matches" value="25" min="1" max="200">
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary>Avanzate</summary>
|
||||
<div id="adv-form"></div>
|
||||
</details>
|
||||
|
||||
<h2 style="margin-top:14px">TEMPI</h2>
|
||||
<div class="kv"><span>train:</span><span id="t-train">-</span></div>
|
||||
<div class="kv"><span>find:</span><span id="t-find">-</span></div>
|
||||
<div class="kv"><span>varianti:</span><span id="t-var">-</span></div>
|
||||
|
||||
@@ -56,18 +56,41 @@ canvas {
|
||||
margin-top: 6px; font-size: 12px; color: #aaa;
|
||||
}
|
||||
|
||||
#params-form {
|
||||
display: grid; grid-template-columns: 1fr 100px; gap: 4px 8px;
|
||||
.field {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
#params-form label {
|
||||
font-size: 12px; display: flex; align-items: center;
|
||||
.field label {
|
||||
display: block; font-size: 12px; color: #b0b0b0;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
#params-form input {
|
||||
.field select, .field input {
|
||||
width: 100%; background: #2a2a2a; color: #dcdcdc;
|
||||
border: 1px solid #444; padding: 5px 6px; border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.field input[type="range"] {
|
||||
padding: 0; height: 26px;
|
||||
}
|
||||
.field select:focus, .field input:focus {
|
||||
outline: 1px solid #00c8ff;
|
||||
}
|
||||
|
||||
#v-score { color: #00c8ff; font-weight: bold; }
|
||||
|
||||
details { margin-top: 10px; font-size: 12px; }
|
||||
details summary {
|
||||
cursor: pointer; padding: 4px 0; color: #00c8ff;
|
||||
}
|
||||
#adv-form {
|
||||
display: grid; grid-template-columns: 1fr 80px; gap: 3px 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
#adv-form label { font-size: 11px; color: #999; }
|
||||
#adv-form input {
|
||||
background: #2a2a2a; color: #dcdcdc; border: 1px solid #444;
|
||||
padding: 4px 6px; border-radius: 3px; font-size: 12px;
|
||||
padding: 3px 5px; border-radius: 3px; font-size: 11px;
|
||||
width: 100%;
|
||||
}
|
||||
#params-form input:focus { outline: 1px solid #00c8ff; }
|
||||
|
||||
.kv {
|
||||
display: flex; justify-content: space-between;
|
||||
|
||||
Reference in New Issue
Block a user