fix: precisione rotazione sub-0.1° + refine least-squares + propagate windowed
Root cause rotazione imprecisa: score saturo sulla spread bitmap dilatata (raggio 4-5) -> refine senza gradiente (angolo restava quantizzato allo step) e minMaxLoc sul plateau spostava il centro sull'angolo finestra (errore sistematico 3*sqrt(2) px). - _refine_angle: ottimizza su bitmap fine raggio 1 (spread_fine, in cache scena), picco sub-pixel con centroide plateau, score finale ricalcolato su spread coarse (semantica soglie invariata) - _subpixel_refine_lm riscritto: snap edge sub-pixel lungo la normale + LSQ 3x3 (dx, dy, dtheta), ON di default, Sobel scena precomputato - _prepare_padded_template: centro rotazione coerente col padding - round invece di truncation sugli offset feature (bias 0.25px) - _angle_list include estremo superiore del range - _refine_pose_joint rimosso (NM su funzione a gradini, terminava subito) - pyramid_propagate default ON con kernel windowed (le feature campionano l'intera scena: il crop precedente le troncava -> score 0), picchi = massimi locali, auto-off per template elongati >2:1 - piramide 3 livelli default con clamp su dimensione template - cache scena: hash dell'intera immagine (64KB collidevano) GT sintetica 7 pose: errore angolo 2.3->0.05 deg, posizione 4.2->0.04 px. Suite 16 scenari: match >= baseline, totale find -13%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -271,6 +271,108 @@ if HAS_NUMBA:
|
||||
acc[y, x] = 0.0
|
||||
return acc
|
||||
|
||||
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||
def _jit_score_bitmap_rescored_window(
|
||||
spread: np.ndarray, # uint8 (H, W) - scena INTERA
|
||||
dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
||||
bit_active: np.uint8,
|
||||
bg: np.ndarray, # float32 (H, W) - scena intera
|
||||
y0: nb.int64, x0: nb.int64,
|
||||
wh: nb.int64, ww: nb.int64,
|
||||
) -> np.ndarray:
|
||||
"""Score rescored valutato SOLO nella finestra (y0, x0, wh, ww).
|
||||
|
||||
Le feature campionano lo spread dell'intera scena (bounds-checked
|
||||
sui bordi scena): a differenza di chiamare il kernel su un crop,
|
||||
le feature che escono dalla finestra NON contano come miss.
|
||||
Usato dal path pyramid_propagate: costo ∝ area finestra.
|
||||
"""
|
||||
H, W = spread.shape
|
||||
N = dx.shape[0]
|
||||
acc = np.zeros((wh, ww), dtype=np.float32)
|
||||
for yi in nb.prange(wh):
|
||||
y = y0 + yi
|
||||
for i in range(N):
|
||||
b = bins[i]
|
||||
mask = np.uint8(1) << b
|
||||
if (bit_active & mask) == 0:
|
||||
continue
|
||||
yy = y + dy[i]
|
||||
if yy < 0 or yy >= H:
|
||||
continue
|
||||
ddx = dx[i]
|
||||
xi_lo = 0
|
||||
xi_hi = ww
|
||||
lo = -(x0 + ddx)
|
||||
if lo > xi_lo:
|
||||
xi_lo = lo
|
||||
hi = W - (x0 + ddx)
|
||||
if hi < xi_hi:
|
||||
xi_hi = hi
|
||||
for xi in range(xi_lo, xi_hi):
|
||||
if spread[yy, x0 + xi + ddx] & mask:
|
||||
acc[yi, xi] += 1.0
|
||||
if N > 0:
|
||||
inv = 1.0 / N
|
||||
for yi in nb.prange(wh):
|
||||
for xi in range(ww):
|
||||
v = acc[yi, xi] * inv
|
||||
bgv = bg[y0 + yi, x0 + xi]
|
||||
if bgv < 1.0:
|
||||
r = (v - bgv) / (1.0 - bgv + 1e-6)
|
||||
acc[yi, xi] = r if r > 0.0 else 0.0
|
||||
else:
|
||||
acc[yi, xi] = 0.0
|
||||
return acc
|
||||
|
||||
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||
def _jit_score_bitmap_rescored_window_u16(
|
||||
spread: np.ndarray, # uint16 (H, W)
|
||||
dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
||||
bit_active: np.uint16,
|
||||
bg: np.ndarray,
|
||||
y0: nb.int64, x0: nb.int64,
|
||||
wh: nb.int64, ww: nb.int64,
|
||||
) -> np.ndarray:
|
||||
"""Versione uint16 (polarity 16-bin) del kernel windowed."""
|
||||
H, W = spread.shape
|
||||
N = dx.shape[0]
|
||||
acc = np.zeros((wh, ww), dtype=np.float32)
|
||||
for yi in nb.prange(wh):
|
||||
y = y0 + yi
|
||||
for i in range(N):
|
||||
b = bins[i]
|
||||
mask = np.uint16(1) << b
|
||||
if (bit_active & mask) == 0:
|
||||
continue
|
||||
yy = y + dy[i]
|
||||
if yy < 0 or yy >= H:
|
||||
continue
|
||||
ddx = dx[i]
|
||||
xi_lo = 0
|
||||
xi_hi = ww
|
||||
lo = -(x0 + ddx)
|
||||
if lo > xi_lo:
|
||||
xi_lo = lo
|
||||
hi = W - (x0 + ddx)
|
||||
if hi < xi_hi:
|
||||
xi_hi = hi
|
||||
for xi in range(xi_lo, xi_hi):
|
||||
if spread[yy, x0 + xi + ddx] & mask:
|
||||
acc[yi, xi] += 1.0
|
||||
if N > 0:
|
||||
inv = 1.0 / N
|
||||
for yi in nb.prange(wh):
|
||||
for xi in range(ww):
|
||||
v = acc[yi, xi] * inv
|
||||
bgv = bg[y0 + yi, x0 + xi]
|
||||
if bgv < 1.0:
|
||||
r = (v - bgv) / (1.0 - bgv + 1e-6)
|
||||
acc[yi, xi] = r if r > 0.0 else 0.0
|
||||
else:
|
||||
acc[yi, xi] = 0.0
|
||||
return acc
|
||||
|
||||
@nb.njit(cache=True, parallel=True, fastmath=True, boundscheck=False)
|
||||
def _jit_top_max_per_variant(
|
||||
spread: np.ndarray, # uint8 (H, W)
|
||||
@@ -426,6 +528,9 @@ if HAS_NUMBA:
|
||||
_jit_top_max_per_variant(
|
||||
spread, dx, dy, b, offsets, np.uint8(0xFF), bg_pv, scale_idx,
|
||||
)
|
||||
_jit_score_bitmap_rescored_window(
|
||||
spread, dx, dy, b, np.uint8(0xFF), bg, 4, 4, 8, 8,
|
||||
)
|
||||
_jit_popcount_density(spread)
|
||||
spread16 = np.zeros((32, 32), dtype=np.uint16)
|
||||
_jit_score_bitmap_rescored_u16(
|
||||
@@ -447,6 +552,12 @@ else: # pragma: no cover
|
||||
def _jit_score_bitmap_rescored_strided(spread, dx, dy, bins, bit_active, bg, stride):
|
||||
raise RuntimeError("numba non disponibile")
|
||||
|
||||
def _jit_score_bitmap_rescored_window(spread, dx, dy, bins, bit_active, bg, y0, x0, wh, ww):
|
||||
raise RuntimeError("numba non disponibile")
|
||||
|
||||
def _jit_score_bitmap_rescored_window_u16(spread, dx, dy, bins, bit_active, bg, y0, x0, wh, ww):
|
||||
raise RuntimeError("numba non disponibile")
|
||||
|
||||
def _jit_score_bitmap_greedy(spread, dx, dy, bins, bit_active, min_score, greediness):
|
||||
raise RuntimeError("numba non disponibile")
|
||||
|
||||
@@ -524,6 +635,39 @@ def score_bitmap_rescored(
|
||||
return np.maximum(0.0, out).astype(np.float32)
|
||||
|
||||
|
||||
def score_bitmap_rescored_window(
|
||||
spread: np.ndarray, dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
||||
bit_active: int, bg: np.ndarray,
|
||||
y0: int, x0: int, wh: int, ww: int,
|
||||
) -> np.ndarray:
|
||||
"""Score rescored solo nella finestra (y0, x0, wh, ww) della scena.
|
||||
|
||||
Le feature campionano l'INTERA scena: feature fuori finestra ma dentro
|
||||
scena contano correttamente (chiamare il kernel su un crop le tratta
|
||||
come miss e azzera lo score — il bug che rendeva inutilizzabile il
|
||||
path pyramid_propagate). Fallback no-numba: kernel pieno + slice.
|
||||
"""
|
||||
if HAS_NUMBA and len(dx) > 0:
|
||||
dx_c = np.ascontiguousarray(dx, dtype=np.int32)
|
||||
dy_c = np.ascontiguousarray(dy, dtype=np.int32)
|
||||
bins_c = np.ascontiguousarray(bins, dtype=np.int8)
|
||||
bg_c = np.ascontiguousarray(bg, dtype=np.float32)
|
||||
if spread.dtype == np.uint16:
|
||||
return _jit_score_bitmap_rescored_window_u16(
|
||||
np.ascontiguousarray(spread, dtype=np.uint16),
|
||||
dx_c, dy_c, bins_c, np.uint16(bit_active), bg_c,
|
||||
int(y0), int(x0), int(wh), int(ww),
|
||||
)
|
||||
return _jit_score_bitmap_rescored_window(
|
||||
np.ascontiguousarray(spread, dtype=np.uint8),
|
||||
dx_c, dy_c, bins_c, np.uint8(bit_active), bg_c,
|
||||
int(y0), int(x0), int(wh), int(ww),
|
||||
)
|
||||
# Fallback (lento, solo senza numba): score full-frame + slice finestra
|
||||
full = score_bitmap_rescored(spread, dx, dy, bins, bit_active, bg)
|
||||
return full[y0:y0 + wh, x0:x0 + ww]
|
||||
|
||||
|
||||
def score_bitmap_greedy(
|
||||
spread: np.ndarray, dx: np.ndarray, dy: np.ndarray, bins: np.ndarray,
|
||||
bit_active: int, min_score: float, greediness: float,
|
||||
|
||||
Reference in New Issue
Block a user