Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37c645984f | |||
| 0e148667ec | |||
| b5bbca0e85 | |||
| ca3882c59c | |||
| 7f6571bdd1 | |||
| 7cb1ae2df7 | |||
| 6ebb08e7a2 | |||
| eba9d478a7 | |||
| 0df0d98aa5 | |||
| b2b959e801 | |||
| b05246b492 | |||
| aeaa7fb5f7 | |||
| f347a10fad | |||
| 0b24be4d94 | |||
| 0296083e3c | |||
| 39208aadab | |||
| 2b7ee6799c | |||
| 5059ce1d89 | |||
| f05dec5183 | |||
| f8f6a15166 | |||
| 5bd8fca248 | |||
| 796ccb8052 | |||
| 0a8a9365bb | |||
| 9ed779637e | |||
| 077d44c3c8 | |||
| e038ee3a1d | |||
| 041b26e791 | |||
| 84b73dc651 | |||
| 8d8a89ac35 | |||
| 41976f574d | |||
| 4ef7a4a85f | |||
| 7de7f35b7c | |||
| 7b014b7f69 | |||
| 367ee9aaac | |||
| 74e5a45a39 | |||
| 11c5160385 | |||
| 07bab87cb9 | |||
| a247484f36 | |||
| e188df0adb | |||
| b35d47669c | |||
| fc3b0dbc3a | |||
| 6da4dd5329 | |||
| b143c6607a | |||
| 6704d66cd5 | |||
| f00cf9b621 | |||
| 4b7271094b | |||
| 746d1668c6 | |||
| d9a40952c4 | |||
| 6db2086ead | |||
| 27a0ef1a45 | |||
| ba4024d252 |
+296
-13
@@ -110,6 +110,63 @@ if HAS_NUMBA:
|
|||||||
acc[y, x] *= inv
|
acc[y, x] *= inv
|
||||||
return acc
|
return acc
|
||||||
|
|
||||||
|
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||||
|
def _jit_score_bitmap_rescored_strided(
|
||||||
|
spread: np.ndarray,
|
||||||
|
dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
||||||
|
bit_active: np.uint8,
|
||||||
|
bg: np.ndarray,
|
||||||
|
stride: nb.int32,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Variante con sub-sampling: valuta solo pixel su griglia stride×stride.
|
||||||
|
Score restituito ha stessa shape (H, W); celle non valutate = 0.
|
||||||
|
|
||||||
|
4× speed-up con stride=2 (NMS recupera precisione in full-res).
|
||||||
|
Numba prange richiede step costante: itero su indici griglia e
|
||||||
|
moltiplico per stride dentro il body.
|
||||||
|
"""
|
||||||
|
H, W = spread.shape
|
||||||
|
N = dx.shape[0]
|
||||||
|
acc = np.zeros((H, W), dtype=np.float32)
|
||||||
|
ny = (H + stride - 1) // stride
|
||||||
|
nx = (W + stride - 1) // stride
|
||||||
|
for yi in nb.prange(ny):
|
||||||
|
y = yi * stride
|
||||||
|
for i in range(N):
|
||||||
|
b = bins[i]
|
||||||
|
mask = np.uint8(1) << b
|
||||||
|
if (bit_active & mask) == 0:
|
||||||
|
continue
|
||||||
|
ddy = dy[i]
|
||||||
|
yy = y + ddy
|
||||||
|
if yy < 0 or yy >= H:
|
||||||
|
continue
|
||||||
|
ddx = dx[i]
|
||||||
|
x_lo = 0 if ddx >= 0 else -ddx
|
||||||
|
x_hi = W if ddx <= 0 else W - ddx
|
||||||
|
rem = x_lo % stride
|
||||||
|
if rem != 0:
|
||||||
|
x_lo += stride - rem
|
||||||
|
x = x_lo
|
||||||
|
while x < x_hi:
|
||||||
|
if spread[yy, x + ddx] & mask:
|
||||||
|
acc[y, x] += 1.0
|
||||||
|
x += stride
|
||||||
|
if N > 0:
|
||||||
|
inv = 1.0 / N
|
||||||
|
for yi in nb.prange(ny):
|
||||||
|
y = yi * stride
|
||||||
|
for xi in range(nx):
|
||||||
|
x = xi * stride
|
||||||
|
v = acc[y, x] * inv
|
||||||
|
bgv = bg[y, x]
|
||||||
|
if bgv < 1.0:
|
||||||
|
r = (v - bgv) / (1.0 - bgv + 1e-6)
|
||||||
|
acc[y, x] = r if r > 0.0 else 0.0
|
||||||
|
else:
|
||||||
|
acc[y, x] = 0.0
|
||||||
|
return acc
|
||||||
|
|
||||||
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||||
def _jit_score_bitmap_greedy(
|
def _jit_score_bitmap_greedy(
|
||||||
spread: np.ndarray,
|
spread: np.ndarray,
|
||||||
@@ -142,7 +199,6 @@ if HAS_NUMBA:
|
|||||||
b = bins[i]
|
b = bins[i]
|
||||||
mask = np.uint8(1) << b
|
mask = np.uint8(1) << b
|
||||||
if (bit_active & mask) == 0:
|
if (bit_active & mask) == 0:
|
||||||
# Nessun chance per questa feature
|
|
||||||
if hits + (N - i - 1) < min_req:
|
if hits + (N - i - 1) < min_req:
|
||||||
break
|
break
|
||||||
continue
|
continue
|
||||||
@@ -215,6 +271,122 @@ if HAS_NUMBA:
|
|||||||
acc[y, x] = 0.0
|
acc[y, x] = 0.0
|
||||||
return acc
|
return acc
|
||||||
|
|
||||||
|
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||||
|
def _jit_top_max_per_variant(
|
||||||
|
spread: np.ndarray, # uint8 (H, W)
|
||||||
|
dx_flat: np.ndarray, # int32 (sum_N,)
|
||||||
|
dy_flat: np.ndarray, # int32 (sum_N,)
|
||||||
|
bins_flat: np.ndarray, # int8 (sum_N,)
|
||||||
|
offsets: np.ndarray, # int32 (n_vars+1,) prefix sum
|
||||||
|
bit_active: np.uint8,
|
||||||
|
bg_per_variant: np.ndarray, # float32 (n_vars, H, W) - 1 per scala
|
||||||
|
scale_idx: np.ndarray, # int32 (n_vars,) idx in bg_per_variant
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Batch: per ogni variante calcola max score (rescored bg), ritorna
|
||||||
|
array float32 (n_vars,). Parallelismo prange ESTERNO sulle varianti
|
||||||
|
elimina overhead di n_vars chiamate JIT separate (avg ~20us per
|
||||||
|
chiamata su template piccoli) + pool thread Python.
|
||||||
|
|
||||||
|
Pensato per fase TOP del pruning quando n_vars >> n_threads.
|
||||||
|
"""
|
||||||
|
n_vars = offsets.shape[0] - 1
|
||||||
|
H, W = spread.shape
|
||||||
|
out = np.zeros(n_vars, dtype=np.float32)
|
||||||
|
for vi in nb.prange(n_vars):
|
||||||
|
i0 = offsets[vi]; i1 = offsets[vi + 1]
|
||||||
|
N = i1 - i0
|
||||||
|
if N == 0:
|
||||||
|
out[vi] = -1.0
|
||||||
|
continue
|
||||||
|
si = scale_idx[vi]
|
||||||
|
inv = nb.float32(1.0 / N)
|
||||||
|
best = nb.float32(-1.0)
|
||||||
|
for y in range(H):
|
||||||
|
for x in range(W):
|
||||||
|
s = nb.float32(0.0)
|
||||||
|
for k in range(N):
|
||||||
|
b = bins_flat[i0 + k]
|
||||||
|
mask = np.uint8(1) << b
|
||||||
|
if (bit_active & mask) == 0:
|
||||||
|
continue
|
||||||
|
ddy = dy_flat[i0 + k]
|
||||||
|
yy = y + ddy
|
||||||
|
if yy < 0 or yy >= H:
|
||||||
|
continue
|
||||||
|
ddx = dx_flat[i0 + k]
|
||||||
|
xx = x + ddx
|
||||||
|
if xx < 0 or xx >= W:
|
||||||
|
continue
|
||||||
|
if spread[yy, xx] & mask:
|
||||||
|
s += nb.float32(1.0)
|
||||||
|
s *= inv
|
||||||
|
bgv = bg_per_variant[si, y, x]
|
||||||
|
if bgv < 1.0:
|
||||||
|
r = (s - bgv) / (1.0 - bgv + 1e-6)
|
||||||
|
if r > best:
|
||||||
|
best = r
|
||||||
|
out[vi] = best if best > 0.0 else 0.0
|
||||||
|
return out
|
||||||
|
|
||||||
|
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||||
|
def _jit_score_bitmap_rescored_u16(
|
||||||
|
spread: np.ndarray, # uint16 (H, W) - 16 bit di polarity-aware
|
||||||
|
dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
||||||
|
bit_active: np.uint16,
|
||||||
|
bg: np.ndarray,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Versione uint16 di _jit_score_bitmap_rescored per polarity 16-bin.
|
||||||
|
|
||||||
|
Identica logica ma mask = uint16(1) << b dove b in [0..15]
|
||||||
|
(orientamento mod 2π invece di mod π).
|
||||||
|
"""
|
||||||
|
H, W = spread.shape
|
||||||
|
N = dx.shape[0]
|
||||||
|
acc = np.zeros((H, W), dtype=np.float32)
|
||||||
|
for y in nb.prange(H):
|
||||||
|
for i in range(N):
|
||||||
|
b = bins[i]
|
||||||
|
mask = np.uint16(1) << b
|
||||||
|
if (bit_active & mask) == 0:
|
||||||
|
continue
|
||||||
|
ddy = dy[i]
|
||||||
|
yy = y + ddy
|
||||||
|
if yy < 0 or yy >= H:
|
||||||
|
continue
|
||||||
|
ddx = dx[i]
|
||||||
|
x_lo = 0 if ddx >= 0 else -ddx
|
||||||
|
x_hi = W if ddx <= 0 else W - ddx
|
||||||
|
for x in range(x_lo, x_hi):
|
||||||
|
if spread[yy, x + ddx] & mask:
|
||||||
|
acc[y, x] += 1.0
|
||||||
|
if N > 0:
|
||||||
|
inv = 1.0 / N
|
||||||
|
for y in nb.prange(H):
|
||||||
|
for x in range(W):
|
||||||
|
v = acc[y, x] * inv
|
||||||
|
bgv = bg[y, x]
|
||||||
|
if bgv < 1.0:
|
||||||
|
r = (v - bgv) / (1.0 - bgv + 1e-6)
|
||||||
|
acc[y, x] = r if r > 0.0 else 0.0
|
||||||
|
else:
|
||||||
|
acc[y, x] = 0.0
|
||||||
|
return acc
|
||||||
|
|
||||||
|
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||||
|
def _jit_popcount_density_u16(spread: np.ndarray) -> np.ndarray:
|
||||||
|
"""Popcount per uint16 (16 bin polarity)."""
|
||||||
|
H, W = spread.shape
|
||||||
|
out = np.zeros((H, W), dtype=np.float32)
|
||||||
|
for y in nb.prange(H):
|
||||||
|
for x in range(W):
|
||||||
|
v = spread[y, x]
|
||||||
|
cnt = 0
|
||||||
|
for b in range(16):
|
||||||
|
if v & (np.uint16(1) << b):
|
||||||
|
cnt += 1
|
||||||
|
out[y, x] = float(cnt)
|
||||||
|
return out
|
||||||
|
|
||||||
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||||
def _jit_popcount_density(spread: np.ndarray) -> np.ndarray:
|
def _jit_popcount_density(spread: np.ndarray) -> np.ndarray:
|
||||||
"""Conta bit set per pixel: ritorna (H, W) float32 in [0..8]."""
|
"""Conta bit set per pixel: ritorna (H, W) float32 in [0..8]."""
|
||||||
@@ -241,11 +413,25 @@ if HAS_NUMBA:
|
|||||||
_jit_score_bitmap(spread, dx, dy, b, np.uint8(0xFF))
|
_jit_score_bitmap(spread, dx, dy, b, np.uint8(0xFF))
|
||||||
bg = np.zeros((32, 32), dtype=np.float32)
|
bg = np.zeros((32, 32), dtype=np.float32)
|
||||||
_jit_score_bitmap_rescored(spread, dx, dy, b, np.uint8(0xFF), bg)
|
_jit_score_bitmap_rescored(spread, dx, dy, b, np.uint8(0xFF), bg)
|
||||||
|
_jit_score_bitmap_rescored_strided(
|
||||||
|
spread, dx, dy, b, np.uint8(0xFF), bg, np.int32(2),
|
||||||
|
)
|
||||||
_jit_score_bitmap_greedy(
|
_jit_score_bitmap_greedy(
|
||||||
spread, dx, dy, b, np.uint8(0xFF),
|
spread, dx, dy, b, np.uint8(0xFF),
|
||||||
np.float32(0.5), np.float32(0.8),
|
np.float32(0.5), np.float32(0.8),
|
||||||
)
|
)
|
||||||
|
offsets = np.array([0, 1], dtype=np.int32)
|
||||||
|
scale_idx = np.zeros(1, dtype=np.int32)
|
||||||
|
bg_pv = np.zeros((1, 32, 32), dtype=np.float32)
|
||||||
|
_jit_top_max_per_variant(
|
||||||
|
spread, dx, dy, b, offsets, np.uint8(0xFF), bg_pv, scale_idx,
|
||||||
|
)
|
||||||
_jit_popcount_density(spread)
|
_jit_popcount_density(spread)
|
||||||
|
spread16 = np.zeros((32, 32), dtype=np.uint16)
|
||||||
|
_jit_score_bitmap_rescored_u16(
|
||||||
|
spread16, dx, dy, b, np.uint16(0xFFFF), bg,
|
||||||
|
)
|
||||||
|
_jit_popcount_density_u16(spread16)
|
||||||
|
|
||||||
else: # pragma: no cover
|
else: # pragma: no cover
|
||||||
|
|
||||||
@@ -258,9 +444,24 @@ else: # pragma: no cover
|
|||||||
def _jit_score_bitmap_rescored(spread, dx, dy, bins, bit_active, bg):
|
def _jit_score_bitmap_rescored(spread, dx, dy, bins, bit_active, bg):
|
||||||
raise RuntimeError("numba non disponibile")
|
raise RuntimeError("numba non disponibile")
|
||||||
|
|
||||||
|
def _jit_score_bitmap_rescored_strided(spread, dx, dy, bins, bit_active, bg, stride):
|
||||||
|
raise RuntimeError("numba non disponibile")
|
||||||
|
|
||||||
def _jit_score_bitmap_greedy(spread, dx, dy, bins, bit_active, min_score, greediness):
|
def _jit_score_bitmap_greedy(spread, dx, dy, bins, bit_active, min_score, greediness):
|
||||||
raise RuntimeError("numba non disponibile")
|
raise RuntimeError("numba non disponibile")
|
||||||
|
|
||||||
|
def _jit_top_max_per_variant(
|
||||||
|
spread, dx_flat, dy_flat, bins_flat, offsets, bit_active,
|
||||||
|
bg_per_variant, scale_idx,
|
||||||
|
):
|
||||||
|
raise RuntimeError("numba non disponibile")
|
||||||
|
|
||||||
|
def _jit_score_bitmap_rescored_u16(spread, dx, dy, bins, bit_active, bg):
|
||||||
|
raise RuntimeError("numba non disponibile")
|
||||||
|
|
||||||
|
def _jit_popcount_density_u16(spread):
|
||||||
|
raise RuntimeError("numba non disponibile")
|
||||||
|
|
||||||
def _jit_popcount_density(spread):
|
def _jit_popcount_density(spread):
|
||||||
raise RuntimeError("numba non disponibile")
|
raise RuntimeError("numba non disponibile")
|
||||||
|
|
||||||
@@ -291,19 +492,33 @@ def score_bitmap(
|
|||||||
|
|
||||||
def score_bitmap_rescored(
|
def score_bitmap_rescored(
|
||||||
spread: np.ndarray, dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
spread: np.ndarray, dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
||||||
bit_active: int, bg: np.ndarray,
|
bit_active: int, bg: np.ndarray, stride: int = 1,
|
||||||
) -> np.ndarray:
|
) -> np.ndarray:
|
||||||
"""Score bitmap + rescore fusi in un solo pass (JIT)."""
|
"""Score bitmap + rescore fusi in un solo pass (JIT).
|
||||||
|
|
||||||
|
Dispatch per dtype: uint16 → kernel polarity 16-bin, uint8 → kernel
|
||||||
|
standard 8-bin (con eventuale stride > 1 per coarse top-level).
|
||||||
|
"""
|
||||||
if HAS_NUMBA and len(dx) > 0:
|
if HAS_NUMBA and len(dx) > 0:
|
||||||
return _jit_score_bitmap_rescored(
|
dx_c = np.ascontiguousarray(dx, dtype=np.int32)
|
||||||
np.ascontiguousarray(spread, dtype=np.uint8),
|
dy_c = np.ascontiguousarray(dy, dtype=np.int32)
|
||||||
np.ascontiguousarray(dx, dtype=np.int32),
|
bins_c = np.ascontiguousarray(bins, dtype=np.int8)
|
||||||
np.ascontiguousarray(dy, dtype=np.int32),
|
bg_c = np.ascontiguousarray(bg, dtype=np.float32)
|
||||||
np.ascontiguousarray(bins, dtype=np.int8),
|
if spread.dtype == np.uint16:
|
||||||
np.uint8(bit_active),
|
spread_c = np.ascontiguousarray(spread, dtype=np.uint16)
|
||||||
np.ascontiguousarray(bg, dtype=np.float32),
|
return _jit_score_bitmap_rescored_u16(
|
||||||
|
spread_c, dx_c, dy_c, bins_c, np.uint16(bit_active), bg_c,
|
||||||
)
|
)
|
||||||
# Fallback: chiamate separate
|
spread_c = np.ascontiguousarray(spread, dtype=np.uint8)
|
||||||
|
if stride > 1:
|
||||||
|
return _jit_score_bitmap_rescored_strided(
|
||||||
|
spread_c, dx_c, dy_c, bins_c, np.uint8(bit_active), bg_c,
|
||||||
|
np.int32(stride),
|
||||||
|
)
|
||||||
|
return _jit_score_bitmap_rescored(
|
||||||
|
spread_c, dx_c, dy_c, bins_c, np.uint8(bit_active), bg_c,
|
||||||
|
)
|
||||||
|
# Fallback: chiamate separate (stride ignorato in fallback)
|
||||||
score = score_bitmap(spread, dx, dy, bins, bit_active)
|
score = score_bitmap(spread, dx, dy, bins, bit_active)
|
||||||
out = (score - bg) / (1.0 - bg + 1e-6)
|
out = (score - bg) / (1.0 - bg + 1e-6)
|
||||||
return np.maximum(0.0, out).astype(np.float32)
|
return np.maximum(0.0, out).astype(np.float32)
|
||||||
@@ -331,10 +546,78 @@ def score_bitmap_greedy(
|
|||||||
return score_bitmap(spread, dx, dy, bins, bit_active)
|
return score_bitmap(spread, dx, dy, bins, bit_active)
|
||||||
|
|
||||||
|
|
||||||
|
def top_max_per_variant(
|
||||||
|
spread: np.ndarray,
|
||||||
|
dx_list: list, dy_list: list, bin_list: list,
|
||||||
|
bg_per_scale: dict,
|
||||||
|
variant_scales: list,
|
||||||
|
bit_active: int,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Wrapper: prepara buffer flat e chiama kernel batch su tutte le varianti.
|
||||||
|
|
||||||
|
Parallelismo Numba prange-esterno sulle varianti (n_vars >> n_threads
|
||||||
|
tipicamente per top-pruning) → meglio del thread-pool Python che paga
|
||||||
|
overhead di n_vars chiamate JIT separate.
|
||||||
|
"""
|
||||||
|
if not HAS_NUMBA or len(dx_list) == 0:
|
||||||
|
return np.array([], dtype=np.float32)
|
||||||
|
n_vars = len(dx_list)
|
||||||
|
sizes = [len(d) for d in dx_list]
|
||||||
|
offsets = np.zeros(n_vars + 1, dtype=np.int32)
|
||||||
|
offsets[1:] = np.cumsum(sizes)
|
||||||
|
total = int(offsets[-1])
|
||||||
|
dx_flat = np.empty(total, dtype=np.int32)
|
||||||
|
dy_flat = np.empty(total, dtype=np.int32)
|
||||||
|
bins_flat = np.empty(total, dtype=np.int8)
|
||||||
|
for vi, (dx, dy, bn) in enumerate(zip(dx_list, dy_list, bin_list)):
|
||||||
|
i0 = int(offsets[vi]); i1 = int(offsets[vi + 1])
|
||||||
|
dx_flat[i0:i1] = dx
|
||||||
|
dy_flat[i0:i1] = dy
|
||||||
|
bins_flat[i0:i1] = bn
|
||||||
|
# bg per variante: indicizzato per scala
|
||||||
|
scales_unique = sorted(bg_per_scale.keys())
|
||||||
|
scale_to_idx = {s: i for i, s in enumerate(scales_unique)}
|
||||||
|
H, W = spread.shape
|
||||||
|
bg_pv = np.empty((len(scales_unique), H, W), dtype=np.float32)
|
||||||
|
for s, idx in scale_to_idx.items():
|
||||||
|
bg_pv[idx] = bg_per_scale[s]
|
||||||
|
scale_idx = np.array(
|
||||||
|
[scale_to_idx[s] for s in variant_scales], dtype=np.int32,
|
||||||
|
)
|
||||||
|
return _jit_top_max_per_variant(
|
||||||
|
np.ascontiguousarray(spread, dtype=np.uint8),
|
||||||
|
dx_flat, dy_flat, bins_flat, offsets, np.uint8(bit_active),
|
||||||
|
bg_pv, scale_idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_HAS_NP_BITCOUNT = hasattr(np, "bitwise_count")
|
||||||
|
|
||||||
|
|
||||||
def popcount_density(spread: np.ndarray) -> np.ndarray:
|
def popcount_density(spread: np.ndarray) -> np.ndarray:
|
||||||
|
"""Conta bit set per pixel.
|
||||||
|
|
||||||
|
Order:
|
||||||
|
1) Numba JIT parallel (preferito: piu veloce su 1080p, 0.5ms vs 1.6ms)
|
||||||
|
2) numpy.bitwise_count (NumPy 2.0+, SIMD ma single-thread)
|
||||||
|
3) Fallback numpy bit-shift puro
|
||||||
|
"""
|
||||||
|
if spread.dtype == np.uint16:
|
||||||
|
spread_c = np.ascontiguousarray(spread, dtype=np.uint16)
|
||||||
if HAS_NUMBA:
|
if HAS_NUMBA:
|
||||||
return _jit_popcount_density(np.ascontiguousarray(spread, dtype=np.uint8))
|
return _jit_popcount_density_u16(spread_c)
|
||||||
# Fallback
|
if _HAS_NP_BITCOUNT:
|
||||||
|
return np.bitwise_count(spread_c).astype(np.float32, copy=False)
|
||||||
|
H, W = spread_c.shape
|
||||||
|
out = np.zeros((H, W), dtype=np.float32)
|
||||||
|
for b in range(16):
|
||||||
|
out += ((spread_c >> b) & 1).astype(np.float32)
|
||||||
|
return out
|
||||||
|
spread_c = np.ascontiguousarray(spread, dtype=np.uint8)
|
||||||
|
if HAS_NUMBA:
|
||||||
|
return _jit_popcount_density(spread_c)
|
||||||
|
if _HAS_NP_BITCOUNT:
|
||||||
|
return np.bitwise_count(spread_c).astype(np.float32, copy=False)
|
||||||
H, W = spread.shape
|
H, W = spread.shape
|
||||||
out = np.zeros((H, W), dtype=np.float32)
|
out = np.zeros((H, W), dtype=np.float32)
|
||||||
for b in range(8):
|
for b in range(8):
|
||||||
|
|||||||
+131
-5
@@ -152,14 +152,124 @@ def _cache_key(template_bgr: np.ndarray, mask: np.ndarray | None) -> str:
|
|||||||
return h.hexdigest()
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def auto_tune(template_bgr: np.ndarray, mask: np.ndarray | None = None) -> dict:
|
def _self_validate(template_bgr: np.ndarray, params: dict,
|
||||||
|
mask: np.ndarray | None = None) -> dict:
|
||||||
|
"""Halcon-style self-validation: train il matcher coi parametri tentativi
|
||||||
|
e verifica che il template stesso sia trovato con recall ≥ 1.0.
|
||||||
|
|
||||||
|
Se recall < target o score basso, regola i parametri:
|
||||||
|
- alza weak_grad se troppi edge spuri (recall solido ma molti picchi falsi)
|
||||||
|
- abbassa strong_grad se troppe feature scartate (low feature count)
|
||||||
|
- riduce pyramid_levels se variants[0].levels[top] ha <8 feature
|
||||||
|
|
||||||
|
Halcon usa internamente questo loop in inspect_shape_model. Costo: 1
|
||||||
|
train + 1 find sul template (~50ms su template 100x100). Ne vale la
|
||||||
|
pena se evita match-time errors su scene reali.
|
||||||
|
|
||||||
|
Mutates `params` in place e ritorna lo stesso dict per chaining.
|
||||||
|
"""
|
||||||
|
# Import lazy: evita ciclo (line_matcher importa nulla da auto_tune)
|
||||||
|
from pm2d.line_matcher import LineShapeMatcher
|
||||||
|
|
||||||
|
# Caso degenerato: troppe poche feature pre-validation → riduci soglia
|
||||||
|
if params.get("_n_strong_pixels", 0) < 30:
|
||||||
|
params["weak_grad"] = max(15.0, params["weak_grad"] * 0.6)
|
||||||
|
params["strong_grad"] = max(30.0, params["strong_grad"] * 0.6)
|
||||||
|
|
||||||
|
# Train minimale: 1 sola pose orientazione 0 (range degenerato che
|
||||||
|
# produce comunque 1 variante via fallback in _angle_list).
|
||||||
|
m = LineShapeMatcher(
|
||||||
|
num_features=params["num_features"],
|
||||||
|
weak_grad=params["weak_grad"],
|
||||||
|
strong_grad=params["strong_grad"],
|
||||||
|
angle_range_deg=(0.0, 0.0), # fallback _angle_list = [0.0]
|
||||||
|
angle_step_deg=10.0,
|
||||||
|
scale_range=(1.0, 1.0),
|
||||||
|
spread_radius=params["spread_radius"],
|
||||||
|
pyramid_levels=params["pyramid_levels"],
|
||||||
|
)
|
||||||
|
n_var = m.train(template_bgr, mask=mask)
|
||||||
|
if n_var == 0:
|
||||||
|
# Soglie troppo alte: nessuna variante generata → dimezza
|
||||||
|
params["weak_grad"] = max(15.0, params["weak_grad"] * 0.5)
|
||||||
|
params["strong_grad"] = max(30.0, params["strong_grad"] * 0.5)
|
||||||
|
params["_validation"] = "fallback: soglie dimezzate (no variants)"
|
||||||
|
return params
|
||||||
|
|
||||||
|
# Verifica densita' feature al top-level (rischio collasso)
|
||||||
|
top_lvl = m.variants[0].levels[-1]
|
||||||
|
if top_lvl.n < 8 and params["pyramid_levels"] > 1:
|
||||||
|
params["pyramid_levels"] = max(1, params["pyramid_levels"] - 1)
|
||||||
|
params["_validation"] = (
|
||||||
|
f"pyramid_levels ridotto a {params['pyramid_levels']} "
|
||||||
|
f"(top aveva {top_lvl.n} feature)"
|
||||||
|
)
|
||||||
|
return params
|
||||||
|
|
||||||
|
# Self-find: cerca il template stesso nella propria immagine
|
||||||
|
h, w = template_bgr.shape[:2]
|
||||||
|
# Embed template in scena leggermente più grande per evitare bordo
|
||||||
|
pad = 20
|
||||||
|
canvas = np.full(
|
||||||
|
(h + 2 * pad, w + 2 * pad, 3 if template_bgr.ndim == 3 else 1),
|
||||||
|
128, dtype=np.uint8,
|
||||||
|
)
|
||||||
|
canvas[pad:pad + h, pad:pad + w] = template_bgr
|
||||||
|
matches = m.find(
|
||||||
|
canvas, min_score=0.3, max_matches=5,
|
||||||
|
verify_ncc=False, # template stesso → NCC = 1 sempre, skip per velocita'
|
||||||
|
refine_angle=False, subpixel=False,
|
||||||
|
nms_iou_threshold=0.3,
|
||||||
|
)
|
||||||
|
if not matches:
|
||||||
|
# Nessun match sul proprio template: parametri troppo restrittivi
|
||||||
|
params["weak_grad"] = max(15.0, params["weak_grad"] * 0.7)
|
||||||
|
params["strong_grad"] = max(30.0, params["strong_grad"] * 0.7)
|
||||||
|
params["num_features"] = max(48, int(params["num_features"] * 0.8))
|
||||||
|
params["_validation"] = "soglie/feature ridotte (no self-match)"
|
||||||
|
return params
|
||||||
|
|
||||||
|
# Misura score top match
|
||||||
|
top_score = float(matches[0].score)
|
||||||
|
params["_self_score"] = round(top_score, 3)
|
||||||
|
if top_score < 0.7:
|
||||||
|
# Score basso sul template stesso = parametri davvero subottimali
|
||||||
|
params["weak_grad"] = max(15.0, params["weak_grad"] * 0.85)
|
||||||
|
params["_validation"] = (
|
||||||
|
f"weak_grad ridotto (self-score era {top_score:.2f})"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
params["_validation"] = f"OK (self-score {top_score:.2f})"
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def auto_tune(
|
||||||
|
template_bgr: np.ndarray,
|
||||||
|
mask: np.ndarray | None = None,
|
||||||
|
angle_tolerance_deg: float | None = None,
|
||||||
|
angle_center_deg: float = 0.0,
|
||||||
|
self_validate: bool = True,
|
||||||
|
) -> dict:
|
||||||
"""Analizza template e ritorna dict parametri suggeriti.
|
"""Analizza template e ritorna dict parametri suggeriti.
|
||||||
|
|
||||||
Chiavi compatibili con edit_params PARAM_SCHEMA.
|
Chiavi compatibili con edit_params PARAM_SCHEMA.
|
||||||
|
|
||||||
|
angle_tolerance_deg: se != None, restringe angle_range a
|
||||||
|
(center - tol, center + tol). Usare quando l'orientamento del
|
||||||
|
pezzo e' noto a priori (feeder con guida, posizionamento
|
||||||
|
meccanico): training molto piu rapido (24x meno varianti per
|
||||||
|
tol=15° vs 360° pieno).
|
||||||
|
|
||||||
|
self_validate: se True (default), dopo la stima dei parametri
|
||||||
|
esegue un dry-run del matching sul template stesso e regola
|
||||||
|
weak_grad/strong_grad/pyramid_levels se i parametri tentativi
|
||||||
|
non garantiscono auto-match (Halcon-style inspect_shape_model).
|
||||||
|
|
||||||
Risultato cachato in-memory (LRU): ri-chiamare con stessa ROI è O(1).
|
Risultato cachato in-memory (LRU): ri-chiamare con stessa ROI è O(1).
|
||||||
"""
|
"""
|
||||||
ck = _cache_key(template_bgr, mask)
|
ck = _cache_key(template_bgr, mask)
|
||||||
|
if angle_tolerance_deg is not None:
|
||||||
|
ck = f"{ck}|tol={angle_tolerance_deg}|c={angle_center_deg}"
|
||||||
cached = _TUNE_CACHE.get(ck)
|
cached = _TUNE_CACHE.get(ck)
|
||||||
if cached is not None:
|
if cached is not None:
|
||||||
_TUNE_CACHE.move_to_end(ck)
|
_TUNE_CACHE.move_to_end(ck)
|
||||||
@@ -208,7 +318,12 @@ def auto_tune(template_bgr: np.ndarray, mask: np.ndarray | None = None) -> dict:
|
|||||||
# spread_radius proporzionale a risoluzione + pyramid (tolleranza ~1% dim)
|
# spread_radius proporzionale a risoluzione + pyramid (tolleranza ~1% dim)
|
||||||
spread_radius = int(np.clip(max(3, min_side * 0.02), 3, 8))
|
spread_radius = int(np.clip(max(3, min_side * 0.02), 3, 8))
|
||||||
|
|
||||||
# angle range ridotto se simmetria rotazionale
|
# angle range: priorita' a tolerance hint utente, poi simmetria rotazionale.
|
||||||
|
if angle_tolerance_deg is not None:
|
||||||
|
angle_min = float(angle_center_deg - angle_tolerance_deg)
|
||||||
|
angle_max = float(angle_center_deg + angle_tolerance_deg)
|
||||||
|
else:
|
||||||
|
angle_min = 0.0
|
||||||
angle_max = 360.0 / sym["order"] if sym["order"] > 1 else 360.0
|
angle_max = 360.0 / sym["order"] if sym["order"] > 1 else 360.0
|
||||||
|
|
||||||
# min_score: se entropia orient alta → template distintivo → soglia alta ok
|
# min_score: se entropia orient alta → template distintivo → soglia alta ok
|
||||||
@@ -220,12 +335,15 @@ def auto_tune(template_bgr: np.ndarray, mask: np.ndarray | None = None) -> dict:
|
|||||||
else:
|
else:
|
||||||
min_score = 0.45
|
min_score = 0.45
|
||||||
|
|
||||||
# angle step: 5° default; se simmetria, mantengo step ma range ridotto
|
# angle step adattivo (Halcon-style): atan(2/max_side) deg, clampato.
|
||||||
angle_step = 5.0
|
# Template grande → step fine (rotazione minima visibile su perimetro).
|
||||||
|
# Template piccolo → step grosso (over-sampling = sprecato).
|
||||||
|
max_side = max(h, w)
|
||||||
|
angle_step = float(np.clip(np.degrees(np.arctan2(2.0, max_side)), 1.0, 8.0))
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"backend": "line",
|
"backend": "line",
|
||||||
"angle_min": 0.0,
|
"angle_min": angle_min,
|
||||||
"angle_max": angle_max,
|
"angle_max": angle_max,
|
||||||
"angle_step": angle_step,
|
"angle_step": angle_step,
|
||||||
"scale_min": 1.0,
|
"scale_min": 1.0,
|
||||||
@@ -244,7 +362,15 @@ def auto_tune(template_bgr: np.ndarray, mask: np.ndarray | None = None) -> dict:
|
|||||||
"_symmetry_order": sym["order"],
|
"_symmetry_order": sym["order"],
|
||||||
"_symmetry_conf": round(sym["confidence"], 2),
|
"_symmetry_conf": round(sym["confidence"], 2),
|
||||||
"_orient_entropy": round(stats["orient_entropy"], 2),
|
"_orient_entropy": round(stats["orient_entropy"], 2),
|
||||||
|
"_n_strong_pixels": stats["n_strong"],
|
||||||
}
|
}
|
||||||
|
# Halcon-style self-validation: dry-run training+find sul template per
|
||||||
|
# auto-correggere parametri tentativi che non garantirebbero match.
|
||||||
|
if self_validate:
|
||||||
|
result = _self_validate(template_bgr, result, mask=mask)
|
||||||
|
# Round numerici dopo eventuali aggiustamenti
|
||||||
|
result["weak_grad"] = round(result["weak_grad"], 1)
|
||||||
|
result["strong_grad"] = round(result["strong_grad"], 1)
|
||||||
# Store in LRU cache
|
# Store in LRU cache
|
||||||
_TUNE_CACHE[ck] = dict(result)
|
_TUNE_CACHE[ck] = dict(result)
|
||||||
_TUNE_CACHE.move_to_end(ck)
|
_TUNE_CACHE.move_to_end(ck)
|
||||||
|
|||||||
+973
-52
File diff suppressed because it is too large
Load Diff
+104
-4
@@ -48,6 +48,10 @@ IMAGES_DIR = Path(_images_dir_raw)
|
|||||||
if not IMAGES_DIR.is_absolute():
|
if not IMAGES_DIR.is_absolute():
|
||||||
IMAGES_DIR = PROJECT_ROOT / IMAGES_DIR
|
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.line_matcher import LineShapeMatcher, Match
|
||||||
from pm2d.auto_tune import auto_tune
|
from pm2d.auto_tune import auto_tune
|
||||||
|
|
||||||
@@ -249,9 +253,9 @@ PRECISION_ANGLE_STEP = {
|
|||||||
# Un operatore sceglie il livello di rigore, non un numero astratto.
|
# Un operatore sceglie il livello di rigore, non un numero astratto.
|
||||||
FILTRO_FP_MAP = {
|
FILTRO_FP_MAP = {
|
||||||
"off": 0.0, # disabilitato: mantieni tutti i match shape-based
|
"off": 0.0, # disabilitato: mantieni tutti i match shape-based
|
||||||
"leggero": 0.20, # tollera variazioni intensità/illuminazione forti
|
"leggero": 0.30, # tollera variazioni intensità/illuminazione forti
|
||||||
"medio": 0.35, # default bilanciato (consigliato)
|
"medio": 0.50, # default bilanciato (consigliato)
|
||||||
"forte": 0.50, # scarta match con intensità molto diversa dal template
|
"forte": 0.70, # scarta match con intensità molto diversa dal template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -267,6 +271,20 @@ class SimpleMatchParams(BaseModel):
|
|||||||
penalita_scala: float = 0.0 # 0 = score shape invariante, >0 = penalizza scala != 1
|
penalita_scala: float = 0.0 # 0 = score shape invariante, >0 = penalizza scala != 1
|
||||||
min_score: float = 0.65
|
min_score: float = 0.65
|
||||||
max_matches: int = 25
|
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(
|
def _simple_to_technical(
|
||||||
@@ -526,6 +544,9 @@ def match_simple(p: SimpleMatchParams):
|
|||||||
tech = _simple_to_technical(p, roi_img)
|
tech = _simple_to_technical(p, roi_img)
|
||||||
|
|
||||||
key = _matcher_cache_key(roi_img, tech)
|
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)
|
m = _cache_get_matcher(key)
|
||||||
if m is None:
|
if m is None:
|
||||||
m = LineShapeMatcher(
|
m = LineShapeMatcher(
|
||||||
@@ -537,17 +558,30 @@ def match_simple(p: SimpleMatchParams):
|
|||||||
scale_step=tech["scale_step"],
|
scale_step=tech["scale_step"],
|
||||||
spread_radius=tech["spread_radius"],
|
spread_radius=tech["spread_radius"],
|
||||||
pyramid_levels=tech["pyramid_levels"],
|
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
|
t0 = time.time(); n = m.train(roi_img); t_train = time.time() - t0
|
||||||
_cache_put_matcher(key, m)
|
_cache_put_matcher(key, m)
|
||||||
else:
|
else:
|
||||||
n = len(m.variants); t_train = 0.0
|
n = len(m.variants); t_train = 0.0
|
||||||
nms = tech["nms_radius"] if tech["nms_radius"] > 0 else None
|
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()
|
t0 = time.time()
|
||||||
matches = m.find(
|
matches = m.find(
|
||||||
scene, min_score=tech["min_score"], max_matches=tech["max_matches"],
|
scene, min_score=tech["min_score"], max_matches=tech["max_matches"],
|
||||||
nms_radius=nms, verify_threshold=tech["verify_threshold"],
|
nms_radius=nms, verify_threshold=tech["verify_threshold"],
|
||||||
scale_penalty=tech.get("scale_penalty", 0.0),
|
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
|
t_find = time.time() - t0
|
||||||
|
|
||||||
@@ -573,7 +607,73 @@ 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 ---
|
||||||
|
|
||||||
|
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
|
# Mount static
|
||||||
|
|||||||
+130
-4
@@ -52,6 +52,39 @@ function readUserParams() {
|
|||||||
document.getElementById("p-penalita-scala").value),
|
document.getElementById("p-penalita-scala").value),
|
||||||
min_score: parseFloat(document.getElementById("p-min-score").value),
|
min_score: parseFloat(document.getElementById("p-min-score").value),
|
||||||
max_matches: parseInt(document.getElementById("p-max-matches").value, 10),
|
max_matches: parseInt(document.getElementById("p-max-matches").value, 10),
|
||||||
|
...readHalconFlags(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readHalconFlags() {
|
||||||
|
// Halcon-mode toggle: tutti i flag default-off, esposti via "Modalità Halcon"
|
||||||
|
const $cb = (id) => document.getElementById(id)?.checked ?? false;
|
||||||
|
const $num = (id, def) => {
|
||||||
|
const v = parseFloat(document.getElementById(id)?.value);
|
||||||
|
return Number.isFinite(v) ? v : def;
|
||||||
|
};
|
||||||
|
const $int = (id, def) => {
|
||||||
|
const v = parseInt(document.getElementById(id)?.value, 10);
|
||||||
|
return Number.isFinite(v) ? v : def;
|
||||||
|
};
|
||||||
|
const roiStr = document.getElementById("hc-search-roi")?.value.trim() ?? "";
|
||||||
|
let search_roi = null;
|
||||||
|
if (roiStr) {
|
||||||
|
const p = roiStr.split(/[ ,;]+/).map((x) => parseInt(x, 10));
|
||||||
|
if (p.length === 4 && p.every((v) => Number.isFinite(v))) search_roi = p;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
use_polarity: $cb("hc-use-polarity"),
|
||||||
|
use_gpu: $cb("hc-use-gpu"),
|
||||||
|
use_soft_score: $cb("hc-soft-score"),
|
||||||
|
subpixel_lm: $cb("hc-subpixel-lm"),
|
||||||
|
refine_pose_joint: $cb("hc-refine-joint"),
|
||||||
|
pyramid_propagate: $cb("hc-pyr-propagate"),
|
||||||
|
min_recall: $num("hc-min-recall", 0),
|
||||||
|
nms_iou_threshold: $num("hc-nms-iou", 0.3),
|
||||||
|
greediness: $num("hc-greediness", 0),
|
||||||
|
coarse_stride: $int("hc-coarse-stride", 1),
|
||||||
|
search_roi: search_roi,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,12 +327,17 @@ async function doMatch() {
|
|||||||
const SCALE_MAP = {fissa:[1,1,0.1], mini:[0.9,1.1,0.05],
|
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]};
|
medio:[0.75,1.25,0.05], max:[0.5,1.5,0.05]};
|
||||||
const PREC_MAP = {veloce:10, normale:5, preciso:2};
|
const PREC_MAP = {veloce:10, normale:5, preciso:2};
|
||||||
const FP_MAP = {off:0, leggero:0.20, medio:0.35, forte:0.50};
|
// Allineato a FILTRO_FP_MAP server-side (server.py)
|
||||||
|
const FP_MAP = {off:0, leggero:0.30, medio:0.50, forte:0.70};
|
||||||
const [smin, smax, sstep] = SCALE_MAP[user.scala];
|
const [smin, smax, sstep] = SCALE_MAP[user.scala];
|
||||||
|
// NB: SYM_MAP[invariante]=0 e' valido (zero rotazioni). Uso ?? per
|
||||||
|
// distinguere "chiave mancante" da "valore zero": altrimenti 0 || 360
|
||||||
|
// collassa invariante a 360 = bug "simmetria non ha effetto".
|
||||||
|
const angMax = SYM_MAP[user.simmetria] ?? 360;
|
||||||
body = {
|
body = {
|
||||||
model_id: state.model.id, scene_id: state.scene.id, roi: state.roi,
|
model_id: state.model.id, scene_id: state.scene.id, roi: state.roi,
|
||||||
angle_min: 0, angle_max: SYM_MAP[user.simmetria] || 360,
|
angle_min: 0, angle_max: angMax,
|
||||||
angle_step: PREC_MAP[user.precisione] || 5,
|
angle_step: PREC_MAP[user.precisione] ?? 5,
|
||||||
scale_min: smin, scale_max: smax, scale_step: sstep,
|
scale_min: smin, scale_max: smax, scale_step: sstep,
|
||||||
min_score: user.min_score, max_matches: user.max_matches,
|
min_score: user.min_score, max_matches: user.max_matches,
|
||||||
num_features: adv.num_features ?? 96,
|
num_features: adv.num_features ?? 96,
|
||||||
@@ -307,7 +345,7 @@ async function doMatch() {
|
|||||||
strong_grad: adv.strong_grad ?? 60,
|
strong_grad: adv.strong_grad ?? 60,
|
||||||
spread_radius: adv.spread_radius ?? 5,
|
spread_radius: adv.spread_radius ?? 5,
|
||||||
pyramid_levels: adv.pyramid_levels ?? 3,
|
pyramid_levels: adv.pyramid_levels ?? 3,
|
||||||
verify_threshold: adv.verify_threshold ?? (FP_MAP[user.filtro_fp] ?? 0.35),
|
verify_threshold: adv.verify_threshold ?? (FP_MAP[user.filtro_fp] ?? 0.50),
|
||||||
nms_radius: adv.nms_radius ?? 0,
|
nms_radius: adv.nms_radius ?? 0,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -362,6 +400,91 @@ 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 ----------
|
||||||
|
async function saveRecipe() {
|
||||||
|
if (!state.model || !state.roi) {
|
||||||
|
alert("Seleziona modello e disegna ROI prima di salvare la ricetta.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = document.getElementById("hc-recipe-name").value.trim();
|
||||||
|
if (!name) {
|
||||||
|
alert("Inserisci un nome per la ricetta.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const user = readUserParams();
|
||||||
|
const body = {
|
||||||
|
model_id: state.model.id,
|
||||||
|
scene_id: state.scene?.id || state.model.id,
|
||||||
|
roi: state.roi,
|
||||||
|
tipo: user.tipo,
|
||||||
|
simmetria: user.simmetria,
|
||||||
|
scala: user.scala,
|
||||||
|
precisione: user.precisione,
|
||||||
|
use_polarity: user.use_polarity,
|
||||||
|
use_gpu: user.use_gpu,
|
||||||
|
name: name,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const r = await fetch("/recipes", {
|
||||||
|
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();
|
||||||
|
alert(`Ricetta salvata: ${j.name}\n${j.n_variants} varianti, ${j.size} bytes`);
|
||||||
|
} catch (e) {
|
||||||
|
alert(`Errore salvataggio: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", async () => {
|
window.addEventListener("DOMContentLoaded", async () => {
|
||||||
buildAdvancedForm();
|
buildAdvancedForm();
|
||||||
setupROI();
|
setupROI();
|
||||||
@@ -389,6 +512,9 @@ 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",
|
||||||
|
saveRecipe);
|
||||||
const slider = document.getElementById("p-min-score");
|
const slider = document.getElementById("p-min-score");
|
||||||
slider.addEventListener("input", (e) => {
|
slider.addEventListener("input", (e) => {
|
||||||
document.getElementById("v-score").textContent =
|
document.getElementById("v-score").textContent =
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -129,6 +133,67 @@
|
|||||||
<input type="number" id="p-max-matches" value="25" min="1" max="200">
|
<input type="number" id="p-max-matches" value="25" min="1" max="200">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Modalità Halcon</summary>
|
||||||
|
<div class="halcon-grid">
|
||||||
|
<label class="hc-row" title="16-bin orientation polarity-aware (mod 2π)">
|
||||||
|
<input type="checkbox" id="hc-use-polarity">
|
||||||
|
<span>Polarity 16-bin (F)</span>
|
||||||
|
</label>
|
||||||
|
<label class="hc-row" title="Score continuo cos(θ_t-θ_s) invece di bin">
|
||||||
|
<input type="checkbox" id="hc-soft-score">
|
||||||
|
<span>Soft-margin score (Y)</span>
|
||||||
|
</label>
|
||||||
|
<label class="hc-row" title="Sub-pixel refinement gradient field LM">
|
||||||
|
<input type="checkbox" id="hc-subpixel-lm">
|
||||||
|
<span>Sub-pixel LM 0.05 px (Z)</span>
|
||||||
|
</label>
|
||||||
|
<label class="hc-row" title="Refine congiunto Nelder-Mead (cx,cy,θ)">
|
||||||
|
<input type="checkbox" id="hc-refine-joint">
|
||||||
|
<span>Refine pose joint</span>
|
||||||
|
</label>
|
||||||
|
<label class="hc-row" title="Pyramid candidates propagation">
|
||||||
|
<input type="checkbox" id="hc-pyr-propagate">
|
||||||
|
<span>Pyramid propagate</span>
|
||||||
|
</label>
|
||||||
|
<label class="hc-row" title="OpenCL GPU offload (silent fallback CPU)">
|
||||||
|
<input type="checkbox" id="hc-use-gpu">
|
||||||
|
<span>GPU OpenCL (R)</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="hc-row hc-num">
|
||||||
|
<label>Min recall (M)</label>
|
||||||
|
<input type="number" id="hc-min-recall" value="0.0" min="0" max="1" step="0.05">
|
||||||
|
</div>
|
||||||
|
<div class="hc-row hc-num">
|
||||||
|
<label>NMS IoU thr (A)</label>
|
||||||
|
<input type="number" id="hc-nms-iou" value="0.3" min="0" max="1" step="0.05">
|
||||||
|
</div>
|
||||||
|
<div class="hc-row hc-num">
|
||||||
|
<label>Greediness</label>
|
||||||
|
<input type="number" id="hc-greediness" value="0.0" min="0" max="1" step="0.1">
|
||||||
|
</div>
|
||||||
|
<div class="hc-row hc-num">
|
||||||
|
<label>Coarse stride</label>
|
||||||
|
<input type="number" id="hc-coarse-stride" value="1" min="1" max="4" step="1">
|
||||||
|
</div>
|
||||||
|
<div class="hc-row hc-num" style="grid-column:1/-1">
|
||||||
|
<label title="Limita area di ricerca scena: x,y,w,h (vuoto = tutta scena)">
|
||||||
|
Search ROI (x,y,w,h)
|
||||||
|
</label>
|
||||||
|
<input type="text" id="hc-search-roi" placeholder="es. 100,50,800,400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hc-row" style="grid-column:1/-1; border-top:1px solid #444; padding-top:8px">
|
||||||
|
<label>Ricetta pre-trained (V)</label>
|
||||||
|
<div style="display:flex; gap:6px; margin-top:4px">
|
||||||
|
<input type="text" id="hc-recipe-name" placeholder="nome_ricetta" style="flex:1">
|
||||||
|
<button class="btn" id="btn-save-recipe" type="button">💾 Salva</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Avanzate</summary>
|
<summary>Avanzate</summary>
|
||||||
<div id="adv-form"></div>
|
<div id="adv-form"></div>
|
||||||
|
|||||||
@@ -156,3 +156,20 @@ footer h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#col-model, #col-scene { min-width: 0; }
|
#col-model, #col-scene { min-width: 0; }
|
||||||
|
|
||||||
|
/* Halcon-mode panel */
|
||||||
|
.halcon-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 6px 12px;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.hc-row {
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
}
|
||||||
|
.hc-row.hc-num {
|
||||||
|
flex-direction: column; align-items: flex-start;
|
||||||
|
}
|
||||||
|
.hc-row.hc-num label { font-size: 11px; color: #aaa; }
|
||||||
|
.hc-row.hc-num input { width: 100%; }
|
||||||
|
|||||||
Reference in New Issue
Block a user