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>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user