Compare commits
2 Commits
543ae0f643
...
852597ed51
| Author | SHA1 | Date | |
|---|---|---|---|
| 852597ed51 | |||
| a78884f950 |
@@ -631,6 +631,107 @@ class SaveRecipeParams(BaseModel):
|
||||
name: str # nome file ricetta (no path)
|
||||
|
||||
|
||||
class EdgePreviewParams(BaseModel):
|
||||
model_id: str
|
||||
roi: list[int]
|
||||
weak_grad: float = 30.0
|
||||
strong_grad: float = 60.0
|
||||
num_features: int = 96
|
||||
min_feature_spacing: int = 3
|
||||
use_polarity: bool = False
|
||||
|
||||
|
||||
@app.post("/preview_edges")
|
||||
def preview_edges(p: EdgePreviewParams):
|
||||
"""Estrae edge feature dalla ROI con i parametri dati e ritorna
|
||||
immagine annotata con i pixel selezionati come overlay.
|
||||
|
||||
Permette tuning interattivo delle soglie weak/strong_grad e
|
||||
num_features per "togliere le sporcizie" (rumore di sfondo,
|
||||
edge spuri) prima di trainare il matcher vero.
|
||||
"""
|
||||
model = _load_image(p.model_id)
|
||||
if model is None:
|
||||
raise HTTPException(404, "Modello non trovato")
|
||||
x, y, w, h = p.roi
|
||||
H_m, W_m = model.shape[:2]
|
||||
x = max(0, min(int(x), W_m - 1)); y = max(0, min(int(y), H_m - 1))
|
||||
w = max(1, min(int(w), W_m - x)); h = max(1, min(int(h), H_m - y))
|
||||
roi_img = model[y:y + h, x:x + w]
|
||||
# Matcher temporaneo solo per estrazione feature (no train completo)
|
||||
m = LineShapeMatcher(
|
||||
weak_grad=p.weak_grad,
|
||||
strong_grad=p.strong_grad,
|
||||
num_features=p.num_features,
|
||||
min_feature_spacing=p.min_feature_spacing,
|
||||
use_polarity=p.use_polarity,
|
||||
)
|
||||
gray = cv2.cvtColor(roi_img, cv2.COLOR_BGR2GRAY) if roi_img.ndim == 3 else roi_img
|
||||
mag, bins = m._gradient(gray)
|
||||
fx, fy, fb = m._extract_features(mag, bins, None)
|
||||
# Mostra anche i pixel "weak/strong" come heatmap di sfondo
|
||||
out = roi_img.copy() if roi_img.ndim == 3 else cv2.cvtColor(roi_img, cv2.COLOR_GRAY2BGR)
|
||||
# Overlay magnitude leggera
|
||||
mag_norm = np.clip(mag / max(1.0, mag.max()) * 255, 0, 255).astype(np.uint8)
|
||||
mag_color = cv2.applyColorMap(mag_norm, cv2.COLORMAP_BONE)
|
||||
out = cv2.addWeighted(out, 0.6, mag_color, 0.4, 0)
|
||||
# Pixel "strong" con hysteresis: contorno verde scuro tenue
|
||||
if m.weak_grad < m.strong_grad:
|
||||
edge_mask = m._hysteresis_mask(mag).astype(np.uint8) * 255
|
||||
else:
|
||||
edge_mask = (mag >= m.strong_grad).astype(np.uint8) * 255
|
||||
edge_overlay = np.zeros_like(out)
|
||||
edge_overlay[edge_mask > 0] = (0, 80, 0) # verde scuro
|
||||
out = cv2.addWeighted(out, 1.0, edge_overlay, 0.5, 0)
|
||||
# Feature scelte: cerchietti colorati per bin
|
||||
bin_colors = [
|
||||
(255, 0, 0), (255, 128, 0), (255, 255, 0), (0, 255, 0),
|
||||
(0, 255, 255), (0, 128, 255), (0, 0, 255), (255, 0, 255),
|
||||
(255, 100, 100), (255, 180, 100), (255, 230, 100), (180, 255, 100),
|
||||
(100, 255, 200), (100, 180, 255), (180, 100, 255), (255, 100, 200),
|
||||
]
|
||||
for i in range(len(fx)):
|
||||
b = int(fb[i])
|
||||
col = bin_colors[b % len(bin_colors)]
|
||||
cv2.circle(out, (int(fx[i]), int(fy[i])), 2, col, -1, cv2.LINE_AA)
|
||||
# UCS sul baricentro feature (richiesta utente): assi X rosso, Y verde
|
||||
bary_cx = bary_cy = None
|
||||
if len(fx) > 0:
|
||||
bary_cx = float(np.mean(fx))
|
||||
bary_cy = float(np.mean(fy))
|
||||
bx, by = int(round(bary_cx)), int(round(bary_cy))
|
||||
axis_len = max(20, int(0.15 * max(out.shape[:2])))
|
||||
# X axis (rosso, verso destra)
|
||||
cv2.arrowedLine(out, (bx, by), (bx + axis_len, by),
|
||||
(0, 0, 255), 2, cv2.LINE_AA, tipLength=0.2)
|
||||
cv2.putText(out, "X", (bx + axis_len + 4, by + 5),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA)
|
||||
# Y axis (verde, verso il basso = convenzione image y-down)
|
||||
cv2.arrowedLine(out, (bx, by), (bx, by + axis_len),
|
||||
(0, 255, 0), 2, cv2.LINE_AA, tipLength=0.2)
|
||||
cv2.putText(out, "Y", (bx + 4, by + axis_len + 12),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1, cv2.LINE_AA)
|
||||
# Origine: cerchio bianco con bordo nero
|
||||
cv2.circle(out, (bx, by), 4, (0, 0, 0), -1, cv2.LINE_AA)
|
||||
cv2.circle(out, (bx, by), 3, (255, 255, 255), -1, cv2.LINE_AA)
|
||||
img_id = _store_image(out)
|
||||
n_edge_strong = int((mag >= m.strong_grad).sum())
|
||||
n_edge_total = int(edge_mask.sum() / 255)
|
||||
return {
|
||||
"preview_id": img_id,
|
||||
"n_features": len(fx),
|
||||
"n_edge_strong": n_edge_strong,
|
||||
"n_edge_after_hysteresis": n_edge_total,
|
||||
"mag_max": float(mag.max()),
|
||||
"mag_p50": float(np.percentile(mag, 50)),
|
||||
"mag_p85": float(np.percentile(mag, 85)),
|
||||
"ucs_baricentro": (
|
||||
{"cx": round(bary_cx, 2), "cy": round(bary_cy, 2)}
|
||||
if bary_cx is not None else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@app.post("/recipes")
|
||||
def save_recipe(p: SaveRecipeParams):
|
||||
"""Allena matcher e salva su disco come ricetta riutilizzabile."""
|
||||
|
||||
@@ -438,6 +438,109 @@ function setStatus(s) {
|
||||
}
|
||||
|
||||
// ---------- Init ----------
|
||||
// ---------- Edge preview (clean rumore) ----------
|
||||
let _epDebounce = null;
|
||||
let _epLastImg = null;
|
||||
|
||||
async function fetchEdgePreview() {
|
||||
if (!state.model || !state.roi) {
|
||||
document.getElementById("edge-preview-info").textContent =
|
||||
"Disegna prima la ROI sul modello";
|
||||
return;
|
||||
}
|
||||
const body = {
|
||||
model_id: state.model.id,
|
||||
roi: state.roi,
|
||||
weak_grad: parseFloat(document.getElementById("ep-weak").value),
|
||||
strong_grad: parseFloat(document.getElementById("ep-strong").value),
|
||||
num_features: parseInt(document.getElementById("ep-nf").value, 10),
|
||||
min_feature_spacing: parseInt(document.getElementById("ep-sp").value, 10),
|
||||
use_polarity: document.getElementById("ep-pol").checked,
|
||||
};
|
||||
try {
|
||||
const r = await fetch("/preview_edges", {
|
||||
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();
|
||||
_epLastImg = await loadImage(`/image/${j.preview_id}/raw?t=${Date.now()}`);
|
||||
drawEdgePreview();
|
||||
const ucs = j.ucs_baricentro
|
||||
? ` | UCS=(${j.ucs_baricentro.cx},${j.ucs_baricentro.cy})`
|
||||
: "";
|
||||
document.getElementById("edge-preview-info").innerHTML =
|
||||
`<b>${j.n_features}</b> feature scelte (di ${j.n_edge_after_hysteresis} edge totali)<br>` +
|
||||
`mag: max=${j.mag_max.toFixed(0)} p50=${j.mag_p50.toFixed(0)} ` +
|
||||
`p85=${j.mag_p85.toFixed(0)}${ucs}`;
|
||||
} catch (e) {
|
||||
document.getElementById("edge-preview-info").textContent =
|
||||
`Errore preview: ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function drawEdgePreview() {
|
||||
const cnv = document.getElementById("c-edge-preview");
|
||||
if (!_epLastImg) return;
|
||||
const ctx = cnv.getContext("2d");
|
||||
// Fit-contain
|
||||
const r = Math.min(cnv.width / _epLastImg.width,
|
||||
cnv.height / _epLastImg.height);
|
||||
const w = _epLastImg.width * r;
|
||||
const h = _epLastImg.height * r;
|
||||
const ox = (cnv.width - w) / 2;
|
||||
const oy = (cnv.height - h) / 2;
|
||||
ctx.fillStyle = "#000"; ctx.fillRect(0, 0, cnv.width, cnv.height);
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.drawImage(_epLastImg, ox, oy, w, h);
|
||||
}
|
||||
|
||||
function scheduleEdgePreview() {
|
||||
if (_epDebounce) clearTimeout(_epDebounce);
|
||||
_epDebounce = setTimeout(fetchEdgePreview, 200);
|
||||
}
|
||||
|
||||
function bindEdgePreviewControls() {
|
||||
const slid = (id, valEl) => {
|
||||
const el = document.getElementById(id);
|
||||
const v = document.getElementById(valEl);
|
||||
el.addEventListener("input", () => {
|
||||
v.textContent = el.value;
|
||||
scheduleEdgePreview();
|
||||
});
|
||||
};
|
||||
slid("ep-weak", "ep-weak-v");
|
||||
slid("ep-strong", "ep-strong-v");
|
||||
slid("ep-nf", "ep-nf-v");
|
||||
slid("ep-sp", "ep-sp-v");
|
||||
document.getElementById("ep-pol").addEventListener("change",
|
||||
scheduleEdgePreview);
|
||||
// Auto-refresh quando il pannello viene aperto
|
||||
document.getElementById("edge-preview-panel").addEventListener("toggle",
|
||||
(e) => { if (e.target.open) fetchEdgePreview(); });
|
||||
document.getElementById("btn-edge-apply").addEventListener("click", () => {
|
||||
// Copia i valori correnti nei campi avanzati
|
||||
const map = {
|
||||
"ep-weak": "adv-weak_grad",
|
||||
"ep-strong": "adv-strong_grad",
|
||||
"ep-nf": "adv-num_features",
|
||||
"ep-sp": "adv-min_feature_spacing",
|
||||
};
|
||||
for (const [src, dst] of Object.entries(map)) {
|
||||
const dstEl = document.getElementById(dst);
|
||||
if (dstEl) dstEl.value = document.getElementById(src).value;
|
||||
}
|
||||
// use_polarity: alla checkbox della modalita Halcon
|
||||
const polCb = document.getElementById("hc-use-polarity");
|
||||
if (polCb) polCb.checked = document.getElementById("ep-pol").checked;
|
||||
// Apri pannello Avanzate per feedback
|
||||
const advDetails = document.querySelectorAll("#col-params details");
|
||||
advDetails.forEach((d) => { d.open = true; });
|
||||
alert("Parametri edge applicati. Esegui MATCH per usare i valori scelti.");
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- CC: Diagnostica match ----------
|
||||
function renderDiag(diag, n_matches) {
|
||||
const el = document.getElementById("diag-content");
|
||||
@@ -665,6 +768,7 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
document.getElementById("btn-unload-recipe").addEventListener("click",
|
||||
unloadRecipe);
|
||||
refreshRecipeList();
|
||||
bindEdgePreviewControls();
|
||||
const slider = document.getElementById("p-min-score");
|
||||
slider.addEventListener("input", (e) => {
|
||||
document.getElementById("v-score").textContent =
|
||||
|
||||
@@ -45,6 +45,40 @@
|
||||
<canvas id="c-model" width="380" height="420"></canvas>
|
||||
</div>
|
||||
<div id="roi-info">ROI: (nessuna)</div>
|
||||
<details id="edge-preview-panel" style="margin-top:10px">
|
||||
<summary>🔬 Anteprima edge / pulizia rumore</summary>
|
||||
<div style="font-size:11px; color:#aaa; margin:4px 0">
|
||||
Regola le soglie per togliere edge spuri (sporcizie). UCS rosso/verde
|
||||
sul baricentro feature.
|
||||
</div>
|
||||
<div class="ep-grid">
|
||||
<label class="ep-row">weak_grad <span id="ep-weak-v">30</span>
|
||||
<input type="range" id="ep-weak" min="5" max="200" value="30" step="1">
|
||||
</label>
|
||||
<label class="ep-row">strong_grad <span id="ep-strong-v">60</span>
|
||||
<input type="range" id="ep-strong" min="10" max="400" value="60" step="1">
|
||||
</label>
|
||||
<label class="ep-row">num_features <span id="ep-nf-v">96</span>
|
||||
<input type="range" id="ep-nf" min="16" max="300" value="96" step="1">
|
||||
</label>
|
||||
<label class="ep-row">spacing <span id="ep-sp-v">3</span>
|
||||
<input type="range" id="ep-sp" min="1" max="15" value="3" step="1">
|
||||
</label>
|
||||
<label class="ep-row" style="flex-direction:row; gap:6px">
|
||||
<input type="checkbox" id="ep-pol"> polarity
|
||||
</label>
|
||||
<button class="btn" id="btn-edge-apply" type="button"
|
||||
style="grid-column:1/-1">
|
||||
✓ Applica ai parametri Avanzate
|
||||
</button>
|
||||
</div>
|
||||
<div class="canvas-wrap" style="margin-top:6px">
|
||||
<canvas id="c-edge-preview" width="380" height="380"></canvas>
|
||||
</div>
|
||||
<div id="edge-preview-info" style="font-size:11px; color:#888; margin-top:4px">
|
||||
Disegna ROI e apri questo pannello per generare anteprima
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="col" id="col-scene">
|
||||
|
||||
@@ -173,3 +173,18 @@ footer h2 {
|
||||
}
|
||||
.hc-row.hc-num label { font-size: 11px; color: #aaa; }
|
||||
.hc-row.hc-num input { width: 100%; }
|
||||
|
||||
/* Edge preview panel */
|
||||
.ep-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px 12px;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.ep-row {
|
||||
display: flex; flex-direction: column; gap: 2px;
|
||||
font-size: 11px; color: #aaa;
|
||||
}
|
||||
.ep-row input[type="range"] { width: 100%; }
|
||||
.ep-row span { color: #fff; font-weight: bold; font-family: monospace; }
|
||||
|
||||
Reference in New Issue
Block a user