Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d46197a81a | |||
| 37c645984f | |||
| 0e148667ec | |||
| b5bbca0e85 | |||
| 7f6571bdd1 |
+38
-2
@@ -241,13 +241,49 @@ class LineShapeMatcher:
|
|||||||
bins = np.clip(bins, 0, N_BINS - 1)
|
bins = np.clip(bins, 0, N_BINS - 1)
|
||||||
return mag, bins
|
return mag, bins
|
||||||
|
|
||||||
|
def _hysteresis_mask(self, mag: np.ndarray) -> np.ndarray:
|
||||||
|
"""Edge mask con hysteresis (Halcon Contrast='auto' two-threshold).
|
||||||
|
|
||||||
|
Procedura:
|
||||||
|
1. seed = pixel con mag >= strong_grad (edge nitidi)
|
||||||
|
2. weak = pixel con mag >= weak_grad (edge candidati)
|
||||||
|
3. Espande seed dentro weak via componenti connesse 8-vicini
|
||||||
|
|
||||||
|
Risultato: edge debole connesso a edge forte viene PROMOSSO a
|
||||||
|
feature valida; edge debole isolato (rumore) viene SCARTATO.
|
||||||
|
|
||||||
|
Riduce sia falsi-positivi (rumore puro) sia falsi-negativi
|
||||||
|
(continuita' interrotta su edge sottili a basso contrasto).
|
||||||
|
"""
|
||||||
|
weak = (mag >= self.weak_grad).astype(np.uint8)
|
||||||
|
strong = (mag >= self.strong_grad).astype(np.uint8)
|
||||||
|
# connectedComponentsWithStats su weak: per ogni componente,
|
||||||
|
# se contiene almeno un pixel strong → tutto componente accettato
|
||||||
|
n_lab, labels = cv2.connectedComponents(weak, connectivity=8)
|
||||||
|
if n_lab <= 1:
|
||||||
|
return strong.astype(bool)
|
||||||
|
# Label dei pixel strong: marker per componenti da accettare
|
||||||
|
strong_labels = np.unique(labels[strong > 0])
|
||||||
|
strong_labels = strong_labels[strong_labels > 0] # 0 = bg
|
||||||
|
if len(strong_labels) == 0:
|
||||||
|
return strong.astype(bool)
|
||||||
|
# Mask = appartiene a label di componente "promosso"
|
||||||
|
keep = np.isin(labels, strong_labels)
|
||||||
|
return keep
|
||||||
|
|
||||||
def _extract_features(
|
def _extract_features(
|
||||||
self, mag: np.ndarray, bins: np.ndarray, mask: np.ndarray | None,
|
self, mag: np.ndarray, bins: np.ndarray, mask: np.ndarray | None,
|
||||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||||
if mask is not None:
|
if mask is not None:
|
||||||
mag = np.where(mask > 0, mag, 0)
|
mag = np.where(mask > 0, mag, 0)
|
||||||
strong = mag >= self.strong_grad
|
# Halcon-style edge selection: hysteresis tra weak_grad e strong_grad.
|
||||||
ys, xs = np.where(strong)
|
# Edge weak connessi a edge strong sono inclusi (continuita' bordi).
|
||||||
|
# Se weak_grad >= strong_grad → fallback a soglia singola strong.
|
||||||
|
if self.weak_grad < self.strong_grad:
|
||||||
|
edge = self._hysteresis_mask(mag)
|
||||||
|
else:
|
||||||
|
edge = mag >= self.strong_grad
|
||||||
|
ys, xs = np.where(edge)
|
||||||
if len(xs) == 0:
|
if len(xs) == 0:
|
||||||
return (np.zeros(0, np.int32),) * 3
|
return (np.zeros(0, np.int32),) * 3
|
||||||
vals = mag[ys, xs]
|
vals = mag[ys, xs]
|
||||||
|
|||||||
+3
-1
@@ -607,7 +607,9 @@ def tune(p: TuneParams):
|
|||||||
x, y, w, h = p.roi
|
x, y, w, h = p.roi
|
||||||
roi_img = model[y:y + h, x:x + w]
|
roi_img = model[y:y + h, x:x + w]
|
||||||
t = auto_tune(roi_img)
|
t = auto_tune(roi_img)
|
||||||
return {k: v for k, v in t.items() if not k.startswith("_")}
|
# Esponi parametri tecnici + meta diagnostica (_self_score, _validation,
|
||||||
|
# _symmetry_order, _orient_entropy) per feedback UI.
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
# --- V: Save/Load ricette pre-trained ---
|
# --- V: Save/Load ricette pre-trained ---
|
||||||
|
|||||||
@@ -400,6 +400,53 @@ function setStatus(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Init ----------
|
// ---------- Init ----------
|
||||||
|
// ---------- Auto-tune (Halcon-style) ----------
|
||||||
|
async function doAutoTune() {
|
||||||
|
if (!state.model || !state.roi) {
|
||||||
|
alert("Seleziona modello e disegna ROI prima di Auto-tune.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const status = document.getElementById("status");
|
||||||
|
status.textContent = "Analisi ROI in corso...";
|
||||||
|
try {
|
||||||
|
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) throw new Error(await r.text());
|
||||||
|
const t = await r.json();
|
||||||
|
// Applica ai campi avanzati (override automatico)
|
||||||
|
for (const [key] of ADV_PARAMS) {
|
||||||
|
const el = document.getElementById(`adv-${key}`);
|
||||||
|
if (el && t[key] !== undefined) el.value = String(t[key]);
|
||||||
|
}
|
||||||
|
// Espandi la sezione Avanzate per mostrare i valori applicati
|
||||||
|
const advDetails = document.querySelector("#col-params details:last-of-type");
|
||||||
|
if (advDetails) advDetails.open = true;
|
||||||
|
// Feedback diagnostico
|
||||||
|
const lines = [
|
||||||
|
`weak/strong: ${t.weak_grad} / ${t.strong_grad}`,
|
||||||
|
`feature: ${t.num_features}, piramide: ${t.pyramid_levels}`,
|
||||||
|
`angle: [${t.angle_min}..${t.angle_max}]@${t.angle_step}°`,
|
||||||
|
];
|
||||||
|
if (t._symmetry_order > 1) {
|
||||||
|
lines.push(`simmetria rotaz. ${t._symmetry_order}x (conf ${t._symmetry_conf})`);
|
||||||
|
}
|
||||||
|
if (t._self_score !== undefined) {
|
||||||
|
lines.push(`self-validation: ${t._validation}`);
|
||||||
|
}
|
||||||
|
status.textContent = `Auto-tune OK — ${lines[0]}`;
|
||||||
|
alert("Auto-tune completato:\n\n" + lines.join("\n"));
|
||||||
|
} catch (e) {
|
||||||
|
status.textContent = `Auto-tune errore: ${e.message}`;
|
||||||
|
alert(`Errore auto-tune: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- V: Save recipe ----------
|
// ---------- V: Save recipe ----------
|
||||||
async function saveRecipe() {
|
async function saveRecipe() {
|
||||||
if (!state.model || !state.roi) {
|
if (!state.model || !state.roi) {
|
||||||
@@ -465,6 +512,7 @@ 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-autotune").addEventListener("click", doAutoTune);
|
||||||
document.getElementById("btn-save-recipe").addEventListener("click",
|
document.getElementById("btn-save-recipe").addEventListener("click",
|
||||||
saveRecipe);
|
saveRecipe);
|
||||||
const slider = document.getElementById("p-min-score");
|
const slider = document.getElementById("p-min-score");
|
||||||
|
|||||||
@@ -26,6 +26,10 @@
|
|||||||
<div class="picker-list"></div>
|
<div class="picker-list"></div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-go" id="btn-match">▶ MATCH</button>
|
<button class="btn btn-go" id="btn-match">▶ MATCH</button>
|
||||||
|
<button class="btn" id="btn-autotune"
|
||||||
|
title="Analizza ROI e derivata parametri ottimali (Halcon-style)">
|
||||||
|
⚙ Auto-tune
|
||||||
|
</button>
|
||||||
<label class="btn" title="Carica nuovo file nella cartella immagini">
|
<label class="btn" title="Carica nuovo file nella cartella immagini">
|
||||||
⬆ Carica file
|
⬆ Carica file
|
||||||
<input type="file" id="file-upload" accept="image/*" hidden>
|
<input type="file" id="file-upload" accept="image/*" hidden>
|
||||||
|
|||||||
Reference in New Issue
Block a user