Files
Shape_Model_2D/README.md
T
Adriano 71a364a1fd deploy: Dockerfile + docker-compose Traefik per VPS pm.tielogic.xyz
Dockerfile (multi-arch, python 3.13-slim):
- uv copiato da ghcr.io/astral-sh/uv per install deps
- System deps: libgl1 libglib2.0-0 (cv2) + libgomp1 (numba)
- uv sync --frozen --no-dev da uv.lock
- ENV: IMAGES_DIR=/data/images, HOST=0.0.0.0, PORT=8080
- HEALTHCHECK su GET /images ogni 30s

docker-compose.yml:
- Service pm2d con image ${REGISTRY}/pm2d:${TAG}
- Volume ./images:/data/images (persistenza upload/UI)
- Network esterna 'traefik' (adattare se diverso)
- Labels Traefik:
  - Router HTTPS Host(pm.tielogic.xyz) entrypoint websecure TLS letsencrypt
  - Middleware bodysize 50MB (upload multipart)
  - Redirect HTTP->HTTPS automatico

main.py: HOST/PORT da env (default 127.0.0.1:8080 per dev locale).

README: sezione Deploy con build/push/run su VPS.

.dockerignore: esclude .venv, Test/, benchmarks/, md files.

Build + smoke test container: OK su port 18080.

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

192 lines
6.5 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)
## 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`.