# 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)

## Deploy VPS con Docker + Traefik

Assume che sulla VPS siano già attivi:
- **Traefik** come reverse proxy su network Docker esterna `traefik`
- Entrypoints `web` (:80) e `websecure` (:443)
- Cert resolver `letsencrypt` configurato

### Build e push al registry

```bash
# Build locale
docker build -t vps-ip:5000/pm2d:latest .
docker push vps-ip:5000/pm2d:latest
```

### Sulla VPS

```bash
# Cartella deploy (immagini persistenti qui)
mkdir -p /opt/docker/pm2d/images
cd /opt/docker/pm2d

# Copia docker-compose.yml
# Imposta REGISTRY / TAG se necessario via .env
echo "REGISTRY=vps-ip:5000" > .env
echo "TAG=latest" >> .env

docker compose pull
docker compose up -d
```

Servizio raggiungibile: **https://pm.tielogic.xyz**

### Note operative

- **Volume `./images`**: persistenza delle immagini caricate tramite UI
  (`IMAGES_DIR=/data/images` nel container). Sopravvive a restart.
- **Upload max 50MB**: middleware Traefik `pm2d-bodysize`. Adattare se serve.
- **Cache matcher in-memory**: si svuota a restart container (no problema,
  viene ri-popolata al primo match).
- **Healthcheck**: HTTP `GET /images` ogni 30s.
- Se nome network Traefik diverso da `traefik`, modifica
  `docker-compose.yml` sezione `networks`.

### Adattamenti config Traefik non-standard

Se la VPS ha convenzioni diverse (es. cert resolver chiamato `le`,
entrypoint `https`), modifica i labels nel `docker-compose.yml`.
