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>
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
uv sync
Esecuzione
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)
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):
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:
- Rotazione + scala su canvas con padding diagonale (centro di rotazione = centro reale del template, coerente in tutta la pipeline)
- Sobel → magnitude + orientation, quantizzata in 8 bin modulo π
(16 bin mod 2π con
use_polarity=True) - Edge selection con hysteresis weak/strong (Halcon Contrast auto)
- N feature sparse top-magnitude con spacing minimo, salvate come
(dx, dy, bin)arrotondate rispetto al centro-modello - 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)
- 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. - 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. - Refine per candidato: subpixel 2D sul picco → golden-section sull'angolo su bitmap fine → least-squares (dx, dy, dθ) sui gradienti.
- 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 (richiedeTest/in locale).
Roadmap
Vedi 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
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/imagesnel 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
.npzinvece persistono inrecipes/. - Healthcheck: HTTP
GET /imagesogni 30s. - Se nome network Traefik o cert resolver diversi, modifica i labels in
docker-compose.yml.