perf: Numba JIT kernel per score_by_shift (2.1x speedup)

- Nuovo modulo pm2d/_jit_kernels.py con _jit_score_by_shift Numba njit
  parallel + fastmath + boundscheck=False
- Parallelizzazione per riga output (no race condition su acc)
- Fallback automatico numpy se numba non installato
- Warmup automatico al module import (evita JIT lag al 1 match)

Benchmark clip.png (13 istanze):
  prima (numpy + threads): 1.55s
  dopo (numba + threads):  0.72s
  speedup: 2.1x

Pipeline totale full (refine+subpix): 0.80s

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 01:30:31 +02:00
parent 51ed53cedd
commit b20b11c029
5 changed files with 169 additions and 21 deletions
+4 -20
View File
@@ -33,6 +33,8 @@ from dataclasses import dataclass
import cv2
import numpy as np
from pm2d._jit_kernels import score_by_shift as _jit_score_by_shift, HAS_NUMBA
N_BINS = 8 # orientamenti quantizzati modulo π
@@ -303,27 +305,9 @@ class LineShapeMatcher:
) -> np.ndarray:
"""score[y,x] = Σ_i resp[bin_i][y+dy_i, x+dx_i] / len(dx).
Ottimizzazione: se `bin_has_data` è fornito, skippa feature il cui
bin non ha pixel attivi nella scena (contribuzione = 0).
Dispatch a kernel Numba JIT se disponibile, altrimenti fallback numpy.
"""
_, H, W = resp.shape
acc = np.zeros((H, W), dtype=np.float32)
n = len(dx)
for i in range(n):
b = int(bins[i])
if bin_has_data is not None and not bin_has_data[b]:
continue
ddx = int(dx[i]); ddy = int(dy[i])
y0s = max(0, -ddy); y1s = min(H, H - ddy)
x0s = max(0, -ddx); x1s = min(W, W - ddx)
if y0s >= y1s or x0s >= x1s:
continue
y0r = y0s + ddy; y1r = y1s + ddy
x0r = x0s + ddx; x1r = x1s + ddx
acc[y0s:y1s, x0s:x1s] += resp[b, y0r:y1r, x0r:x1r]
if n > 0:
acc /= n
return acc
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]: