71a364a1fd
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>
192 lines
6.5 KiB
Markdown
192 lines
6.5 KiB
Markdown
# 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`.
|