# 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`.