- max_vars_full = max(max_matches*8, n_variants // 2): protegge perf con molte scale mantenendo metà delle varianti al full-res (vs intero senza cap che dava 22s su 864 varianti, vs 80s screenshot utente) - Dialog tkinter: resizable, minsize 360x420, Entry col peso 2 espandibile - Finestra risultati cv2: WINDOW_NORMAL con resizeWindow iniziale 1600x900 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)