Files
Shape_Model_2D/README.md
T
Adriano 27fa5986bf
CI / test (push) Failing after 18s
docs: README allineato allo stato attuale
Kernel Numba JIT, precisione misurata (0.05 deg / 0.04 px), pipeline
refine (bitmap fine + LSQ pos+angolo), propagate windowed, webapp con
endpoint DXF/roi_poly/ricette, test pytest + CI, Test/ non versionate,
deploy compose build sulla VPS, parametri aggiornati ai default reali.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 14:47:41 +00:00

221 lines
8.8 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
Pattern Matching 2D shape-based (stile Halcon `find_shape_model`), standalone:
libreria Python + GUI desktop + webapp FastAPI.
Due backend algoritmici:
| Backend | Modulo | Algoritmo |
|---|---|---|
| `line` (default) | `pm2d.line_matcher.LineShapeMatcher` | Linemod-style: orientazione gradiente quantizzata + spread bitmap + kernel Numba JIT + refine least-squares |
| `edge` (legacy) | `pm2d.matcher.EdgeShapeMatcher` | Edge Canny + `matchTemplate` multi-rotazione (fallback semplice, lento) |
Porting algoritmico di `meiqua/shape_based_matching/line2Dup`; gli hot-path
sono kernel **Numba JIT** (`pm2d/_jit_kernels.py`, parallelo, ≈ velocità C)
con fallback NumPy automatico se numba non è disponibile.
## Precisione (misurata su ground-truth sintetica, 7 pose note)
| Metrica | Valore |
|---|---|
| Errore angolare mediano | **~0.05°** (step 5°), ~0.03° (step 2°) |
| Errore posizione mediano | **~0.04 px** |
| Recall | 7/7 a min_score 0.5 |
Pipeline di refine: golden-section sull'angolo su bitmap fine (raggio 1,
non satura) → least-squares 3×3 congiunto (dx, dy, dθ) sui gradienti scena
(`subpixel_lm`, ON di default). I test in `tests/` fanno da guardia di
non-regressione su queste soglie.
## Struttura
```
shape_model_2d/
├── pm2d/
│ ├── line_matcher.py # LineShapeMatcher (default)
│ ├── _jit_kernels.py # kernel Numba JIT (score bitmap, windowed, popcount)
│ ├── matcher.py # EdgeShapeMatcher (legacy)
│ ├── dxf.py # rasterizzazione DXF → template (ezdxf)
│ ├── auto_tune.py # stima automatica parametri (simmetria, soglie)
│ ├── gui.py # GUI OpenCV + tk file dialog
│ ├── bench.py, eval.py # CLI benchmark / valutazione
│ └── web/ # webapp FastAPI (server.py + static/)
├── benchmarks/test_suite.py # suite 16 scenari su immagini reali (Test/)
├── tests/ # pytest sintetici (precisione + unit, no Test/)
├── .gitea/workflows/ci.yml # CI: uv sync + ruff + pytest
├── main.py # entry point GUI
├── Test/ # immagini di test LOCALI (non versionate)
├── Dockerfile, docker-compose.yml
└── pyproject.toml
```
GUI/web e algoritmo separati: i matcher sono riusabili da qualsiasi script.
## Setup
```bash
uv sync
```
## Esecuzione
```bash
uv run python main.py # GUI desktop
uv run python -m uvicorn pm2d.web.server:app --port 8080 # webapp
uv run pytest tests/ # test (sintetici, ~1 min)
uv run python benchmarks/test_suite.py # benchmark (richiede Test/)
```
Le immagini in `Test/` **non sono versionate** (vedi `.gitignore`): la suite
benchmark le richiede in locale, i test pytest no.
## API algoritmo (backend `line`)
```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 debole (spread/hysteresis)
strong_grad=60, # soglia gradiente forte (estrazione feature)
angle_range_deg=(0, 360),
angle_step_deg=5.0, # <=0 → step auto dal lato template
scale_range=(0.9, 1.1), # invarianza a scala
scale_step=0.05,
spread_radius=4, # tolleranza posizionale matching coarse
pyramid_levels=3, # clampato auto alla dimensione template
)
m.train(template) # opzionale: train(template, mask=...) per ROI parziale
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) # pose sub-pixel/sub-grado
```
Opzioni `find()` utili: `search_roi=(x, y, w, h)`, `min_recall`,
`scale_penalty`, `use_soft_score`, `debug=True` (diagnostica drop),
`profile=True` (timing per fase via `get_last_profile()`).
Persistenza modello (Halcon write/read_shape_model):
```python
m.save_model("ricetta.npz")
m2 = LineShapeMatcher.load_model("ricetta.npz") # deploy senza re-train
```
## Come funziona il backend `line`
### Training
Per ogni coppia (angolo, scala) del template:
1. Rotazione + scala su canvas con padding diagonale (centro di rotazione
= centro reale del template, coerente in tutta la pipeline)
2. Sobel → magnitude + orientation, quantizzata in **8 bin modulo π**
(16 bin mod 2π con `use_polarity=True`)
3. Edge selection con **hysteresis** weak/strong (Halcon Contrast auto)
4. **N feature sparse** top-magnitude con spacing minimo, salvate come
`(dx, dy, bin)` arrotondate rispetto al centro-modello
5. Piramide feature per livello + dedup varianti identiche (simmetrie)
### Matching
Scena processata una volta per livello (e cachata per scene ripetute):
Sobel → quantizzazione → **spread bitmap** uint8/uint16 (bit b = bin b
presente nel raggio) → kernel JIT:
```
score[y, x] = popcount-AND feature/bitmap / N_features (rescored vs background)
```
1. **Top-level**: valuta 1 variante ogni `cf_auto` (step angolare auto:
al livello top lo spread tollera ~atan(spread/(lato_top/2)) gradi),
pruning per soglia + istogramma orientazioni.
2. **Full-res windowed** (`pyramid_propagate`, default ON): score calcolato
solo in finestre attorno ai massimi locali del top-level — costo
proporzionale ai candidati, non a varianti × W × H. Per template
elongati (>2:1) si torna automaticamente al full-scan esatto.
3. **Refine per candidato**: subpixel 2D sul picco → golden-section
sull'angolo su bitmap fine → least-squares (dx, dy, dθ) sui gradienti.
4. **Verify**: NCC su crop locale (anti falsi-positivi, mediato nello
score) → NMS IoU su bbox poligonali orientati.
## Parametri principali
| Parametro | Default | Significato |
|---|---|---|
| `num_features` | 96 | feature sparse per variante |
| `weak_grad` | 30 | soglia debole (spread + hysteresis) |
| `strong_grad` | 60 | soglia forte (estrazione feature) |
| `spread_radius` | 4 | tolleranza posizionale matching coarse |
| `angle_range_deg` | `(0,360)` | range rotazioni |
| `angle_step_deg` | 5.0 | passo angolare (<=0 = auto) |
| `scale_range` | `(1,1)` | range scale |
| `pyramid_levels` | 3 | livelli piramide (clamp auto su template piccoli) |
| `min_score` | 0.6 | soglia score finale [0..1] |
| `max_matches` | 20 | numero max di match |
| `verify_threshold`| 0.4 | soglia NCC anti falso-positivo |
| `subpixel_lm` | True | least-squares finale pos+angolo |
| `pyramid_propagate` | True | full-res solo in finestre sui picchi top |
## Webapp (pm2d/web)
UI single-page (canvas ROI rettangolare o **poligonale**, slider parametri,
anteprima edge, ricette) + API JSON:
| Endpoint | Funzione |
|---|---|
| `POST /upload` | carica immagine (multipart) |
| `POST /upload_dxf` | carica **DXF** → rasterizzato a template (`?size=128..2048`) |
| `POST /match` | match con parametri tecnici (`roi`, opzionale `roi_poly`) |
| `POST /match_simple` | match con profili semplificati (precisione/filtro_fp/simmetria) |
| `POST /auto_tune` | stima automatica parametri dalla ROI |
| `POST /recipes`, `GET /recipes`, `/match_recipe` | salva/carica/usa ricette `.npz` |
| `GET /image/{id}/annotated` | PNG con overlay match (UCS) |
Dalla UI: bottone **Esporta JSON** per scaricare i risultati completi
(pose, score, bbox, parametri, tempi) per integrazione.
## Test e CI
- `tests/`: pytest **sintetici** (template/scene generati, GT nota) —
precisione angolo/posizione, recall, cache, save/load, mask poligonale.
- CI Gitea Actions (`.gitea/workflows/ci.yml`): ruff + pytest su ogni push.
- `benchmarks/test_suite.py`: 16 scenari su immagini reali per confronto
manuale prestazioni/recall (richiede `Test/` in locale).
## Roadmap
Vedi [ROADMAP.md](ROADMAP.md) — Fase 1 (speed) e Fase 2 (precisione
rotazione + robustezza) completate; prossimo target: latency <50 ms su
1920×1080 (auto step per livello fatto; restano greediness default, GPU,
eventuale SIMD C++).
## Deploy VPS con Docker + Traefik
Assume che sulla VPS siano già attivi:
- **Traefik** come reverse proxy su network Docker esterna `traefik`
- Entrypoint `websecure` (:443) e cert resolver configurato
```bash
cd /opt/docker/visionsuite/shape_model_2d
docker compose build
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 (ri-popolata
al primo match). Le ricette `.npz` invece persistono in `recipes/`.
- **Healthcheck**: HTTP `GET /images` ogni 30s.
- Se nome network Traefik o cert resolver diversi, modifica i labels in
`docker-compose.yml`.