LineShapeMatcher: - Feature piramidate precomputate al training (_LevelFeatures per livello piramide, dedup risolto una volta) - Refinement angolare: 5 offset ±step/2 + parabolic fit → precisione ~0.5° con angle_step=5° (10x fine rispetto a step training) - Subpixel posizione: parabolic fit 2D sul picco → frazione pixel - Multithreading: n_threads auto=CPU-1, parallelizza top-level pruning e full-res matching tramite ThreadPoolExecutor (numpy/cv2 rilasciano GIL) GUI: - Dialog edit_params con bottone Auto-tune - Legenda numerata match con pallino colore (#i, coords, angle, scala, score) - Hotkey finestra: r=params, o=nuovo ROI, m=nuovo modello, s=nuova scena - Pannello con train/find time + HOTKEY in basso auto_tune.py: - Analisi template: soglie grad da percentili, num_features da densità edge, pyramid_levels da min_side, min_score da entropia orientation, rilevazione simmetria rotazionale (soglia 0.75 NCC su magnitude) Benchmark clip.png (13 istanze, 72 varianti angolari): prima: 5.84s, precisione 5° (step training) ora: 1.67s, precisione ~0.5°, subpixel posizione speed-up: 3.5x, precisione angolare 10x Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shape Model 2D — Standalone PM 2D
Programma standalone Pattern Matching 2D shape-based.
Due backend algoritmici:
| Backend | Modulo | Algoritmo | Tempo clip.png (13 istanze) |
|---|---|---|---|
line (default) |
pm2d.line_matcher.LineShapeMatcher |
Linemod-style: gradient orient quantizzata + spread + response map + feature sparse | 3.5 s, 12/13 score 1.0 |
edge |
pm2d.matcher.EdgeShapeMatcher |
Edge Canny + matchTemplate multi-rotazione |
84 s, 6/13 score ~0.3 |
Porting algoritmico (non SIMD) di meiqua/shape_based_matching/line2Dup. MIPP (wrapper SIMD C++) non ha senso in Python — la vettorizzazione la fa già NumPy.
Struttura
Shape_model_2d/
├── pm2d/
│ ├── __init__.py
│ ├── matcher.py # EdgeShapeMatcher (fallback, semplice)
│ ├── line_matcher.py # LineShapeMatcher (default, ottimizzato)
│ └── gui.py # GUI OpenCV + tk file dialog
├── main.py # entry point
├── Test/ # immagini di test
├── pyproject.toml
└── README.md
GUI e algoritmo separati: i matcher sono riusabili da qualsiasi script/backend.
Setup
uv sync
Esecuzione
uv run python main.py
Flusso: file dialog modello → ROI → file dialog scena → risultati.
API algoritmo (backend line, raccomandato)
import cv2
from pm2d import LineShapeMatcher
template = cv2.imread("model.png")
scene = cv2.imread("scene.png")
m = LineShapeMatcher(
num_features=96, # feature sparse per variante
weak_grad=30, # soglia gradiente per spread
strong_grad=60, # soglia gradiente per estrazione feature
angle_range_deg=(0, 360),
angle_step_deg=5.0,
scale_range=(0.9, 1.1), # invarianza a scala
scale_step=0.05,
spread_radius=5, # raggio dilate per robustezza
pyramid_levels=3, # velocità via pruning top-level
top_score_factor=0.5, # soglia top = min_score * factor
)
m.train(template) # ~0.2 s
matches = m.find(scene, min_score=0.55, max_matches=25)
for x in matches:
print(x.cx, x.cy, x.angle_deg, x.scale, x.score)
Modello su regione parziale (non blob distinto)
train() accetta una maschera binaria opzionale per limitare le feature
a una porzione della ROI (es. parte interna di un oggetto complesso, dettaglio
distintivo, ecc.):
mask = np.zeros_like(template[:, :, 0])
cv2.fillPoly(mask, [poligono_utente], 255)
m.train(template, mask=mask)
Solo i gradienti dentro la maschera contribuiscono alle feature.
Come funziona il backend line
Training (costoso, ~0.2 s / 72 varianti)
Per ogni coppia (angolo, scala) del template:
- Rotazione + scala su canvas con padding diagonale
- Sobel →
magnitude+orientation(atan2) - Quantizzazione orientazione in 8 bin modulo π (edge simmetrici)
- Estrazione N feature sparse: top-magnitude sopra
strong_grad, con spacing minimo per evitare cluster - Feature salvate come
(dx, dy, bin)relative al centro-modello
Matching (veloce)
Scena processata una volta per livello piramide:
- Sobel → mag → orient quantizzato → bin invalidato dove
mag < weak_grad - Spread: dilate morfologica per bin (tolleranza localizzazione)
- Response map
(8, H, W): response[b][y,x] = 1 dove orient b è presente
Per ogni variante:
score[y, x] = Σ_i resp[bin_i][y + dy_i, x + dx_i] / N_features
Implementato con shift+add vettorizzato NumPy (O(N_features · H · W) invece di O(kh·kw·H·W) come matchTemplate).
Piramide multi-risoluzione
- Top-level (risoluzione /4 di default con
pyramid_levels=3): score ridotto per pruning varianti. Se nessun pixel raggiungemin_score * top_score_factor, la variante è scartata. - Full-res: calcolato solo per le varianti sopravvissute → nel benchmark clip: ~5-10 varianti su 72 = 7-14× speed-up rispetto a full-res per tutte.
Parametri principali
| Parametro | Default | Significato |
|---|---|---|
num_features |
96 | feature sparse per variante |
weak_grad |
30 | threshold debole (per spread) |
strong_grad |
60 | threshold forte (per estrazione feature) |
spread_radius |
5 | raggio dilate spread (tolleranza posizionale) |
min_feature_spacing |
3 | spacing minimo tra feature per evitare cluster |
angle_range_deg |
(0,360) |
range rotazioni |
angle_step_deg |
5.0 | passo angolare |
scale_range |
(1,1) |
range scale |
scale_step |
0.1 | passo scala |
pyramid_levels |
3 | livelli piramide (più = pruning più aggressivo) |
top_score_factor |
0.5 | soglia top-level = min_score * factor |
min_score |
0.55 | soglia score finale [0..1] |
max_matches |
25 | numero max di match |
nms_radius |
min(w,h)/2 |
raggio NMS baricentri |
Roadmap
- Subpixel refinement (interpolazione parabolic sui picchi)
- ICP locale per raffinamento pose
- Vincoli di orientamento: clustering delle pose per eliminare duplicati cross-variante
- Numba JIT per il ciclo shift+add (eventuale 3-5× su scene grandi)