Files
Shape_Model_2D/README.md
T
Adriano b9a4d51fac feat: PM2D standalone shape-based matcher
Programma standalone Pattern Matching 2D con GUI cv2/tk + algoritmo
puro riusabile. Due backend:

- LineShapeMatcher (default): porting Python di line2Dup (linemod-style)
  - Gradient orientation quantized 8-bin modulo π + spreading
  - Feature sparse top-magnitude con spacing minimo
  - Score via shift-add vettorizzato numpy (O(N_features·H·W))
  - Piramide multi-risoluzione con pruning varianti al top-level
  - Supporto mask binaria per modello non-rettangolare

- EdgeShapeMatcher (fallback): Canny + matchTemplate multi-rotazione

GUI separata da algoritmo. Benchmark clip.png (13 istanze):
  - Edge backend:  84s, 6/13 score ~0.3
  - Line backend:  4.1s, 13/13 score 0.98-1.00

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 00:46:59 +02:00

143 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```bash
uv sync
```
## Esecuzione
```bash
uv run python main.py
```
Flusso: file dialog modello → ROI → file dialog scena → risultati.
## API algoritmo (backend `line`, raccomandato)
```python
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.):
```python
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:
1. Rotazione + scala su canvas con padding diagonale
2. Sobel → `magnitude` + `orientation` (atan2)
3. Quantizzazione orientazione in **8 bin modulo π** (edge simmetrici)
4. Estrazione **N feature sparse**: top-magnitude sopra `strong_grad`, con spacing minimo per evitare cluster
5. 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 raggiunge `min_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)