fix: allineamento preciso match (skip refine saturo + plateau centroid)
Bug: modello == scena non sovrapponeva perfettamente. 1. refine_angle trovava angoli spurious -2.5 deg con score saturo 1.0 perche' parabolic fit su picco saturo estrapola rumore. Fix: skip refine quando original_score >= 0.99 2. Subpixel peak su plateau (spread_radius=5 satura picco su area) sceglieva pixel random via cv2.minMaxLoc. Fix: se >1 pixel a score >= max-0.01 nel raggio 10 usa CENTROIDE del plateau invece del parabolic fit. Test self-match tooth_rim foro piccolo: prima: pos=(355, 111.50) delta=(0, -3.50) ang=-2.5 deg dopo: pos=(355, 115.00) delta=(0, +0.00) ang=+0.0 deg Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+26
-2
@@ -333,9 +333,26 @@ class LineShapeMatcher:
|
||||
return _jit_score_by_shift(resp, dx, dy, bins, bin_has_data)
|
||||
|
||||
@staticmethod
|
||||
def _subpixel_peak(acc: np.ndarray, x: int, y: int) -> tuple[float, float]:
|
||||
"""Fit parabolico 2D attorno al picco per offset subpixel (±0.5 px)."""
|
||||
def _subpixel_peak(
|
||||
acc: np.ndarray, x: int, y: int, plateau_radius: int = 10,
|
||||
) -> tuple[float, float]:
|
||||
"""Posizione sub-pixel del picco.
|
||||
|
||||
Se c'è un plateau di valori ~massimi (spread_radius satura il peak
|
||||
su un'area) ritorna il CENTROIDE del plateau. Altrimenti fit
|
||||
parabolico 2D ±0.5 px.
|
||||
"""
|
||||
H, W = acc.shape
|
||||
val = float(acc[y, x])
|
||||
# Plateau detection: valori >= val - 0.01 entro raggio limitato
|
||||
y0 = max(0, y - plateau_radius); y1 = min(H, y + plateau_radius + 1)
|
||||
x0 = max(0, x - plateau_radius); x1 = min(W, x + plateau_radius + 1)
|
||||
patch = acc[y0:y1, x0:x1]
|
||||
plateau = patch >= val - 0.01
|
||||
if plateau.sum() > 1:
|
||||
ys_m, xs_m = np.where(plateau)
|
||||
return float(x0 + xs_m.mean()), float(y0 + ys_m.mean())
|
||||
# Fallback parabolico
|
||||
if x <= 0 or x >= W - 1 or y <= 0 or y >= H - 1:
|
||||
return float(x), float(y)
|
||||
c = acc[y, x]
|
||||
@@ -359,6 +376,7 @@ class LineShapeMatcher:
|
||||
mask_full: np.ndarray,
|
||||
angle_fine_step: float = 0.5,
|
||||
search_radius: float | None = None,
|
||||
original_score: float | None = None,
|
||||
) -> tuple[float, float, float, float]:
|
||||
"""Ricerca angolare fine (sub-step) attorno al match grezzo.
|
||||
|
||||
@@ -366,6 +384,11 @@ class LineShapeMatcher:
|
||||
l'angolo con score massimo (parabolic fit sulle 3 score centrali).
|
||||
Ritorna (angle_refined, score, cx_refined, cy_refined).
|
||||
"""
|
||||
# Se il match grezzo è già quasi perfetto, NON refinare: il parabolic
|
||||
# fit su picco saturo produce spostamenti spurious di posizione e
|
||||
# angolo (esempio: modello==scena deve dare ang=0, pos=centro ROI)
|
||||
if original_score is not None and original_score >= 0.99:
|
||||
return (angle_deg, original_score, cx, cy)
|
||||
if search_radius is None:
|
||||
search_radius = self.angle_step_deg / 2.0
|
||||
offsets = np.linspace(-search_radius, search_radius, 5)
|
||||
@@ -652,6 +675,7 @@ class LineShapeMatcher:
|
||||
spread0, bit_active_full, self.template_gray, cx_f, cy_f,
|
||||
var.angle_deg, var.scale, mask_full,
|
||||
search_radius=self.angle_step_deg / 2.0,
|
||||
original_score=score,
|
||||
)
|
||||
if verify_ncc:
|
||||
ncc = self._verify_ncc(gray0, cx_f, cy_f, ang_f, var.scale)
|
||||
|
||||
Reference in New Issue
Block a user