MatchResp ora include diag dict (CC feature). UI rendering: - Nuovo pannello pieghevole "🔍 Diagnostica" sotto i tempi - Per ogni match mostra: * pipeline pruning (vars total → top_eval → top_pass → full_eval) * candidati (raw → pre_nms → final) * drop reasons (NCC, score, recall, bbox, NMS) con counter * soglie effettive applicate * flag attivi (polarity, soft, subpix-LM) - Quando 0 match → pannello si apre automaticamente + mostra hint contestuale specifico: * "0 candidati top" → suggerisce ↓ min_score / top_thresh * "tutti dropped da NCC" → ↓ verify_threshold (filtro_fp) * "score post-NCC sotto" → ↓ min_score * "recall basso" → ↓ min_recall * "bbox out-of-scene" → check pose / search_roi Risolve il pattern "0 match perche'?" con guida actionable invece del black-box. Tutti e 3 endpoint match (/match, /match_simple, /match_recipe) propagano il diag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
uv sync
Esecuzione
uv run python main.py
Flusso: file dialog modello → ROI → file dialog scena → risultati.
API algoritmo (backend line, raccomandato)
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.):
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:
- Rotazione + scala su canvas con padding diagonale
- Sobel →
magnitude+orientation(atan2) - Quantizzazione orientazione in 8 bin modulo π (edge simmetrici)
- Estrazione N feature sparse: top-magnitude sopra
strong_grad, con spacing minimo per evitare cluster - 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 raggiungemin_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) ewebsecure(:443) - Cert resolver
letsencryptconfigurato
Build e push al registry
# Build locale
docker build -t vps-ip:5000/pm2d:latest .
docker push vps-ip:5000/pm2d:latest
Sulla VPS
# 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/imagesnel 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 /imagesogni 30s. - Se nome network Traefik diverso da
traefik, modificadocker-compose.ymlsezionenetworks.
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.