Adriano 1cc7881a51 feat: pm2d.eval - validation harness CLI per LineShapeMatcher
Tool da CLI per misurare oggettivamente la qualita' del matcher
su dataset etichettato. Halcon ha questo solo nell'IDE (HDevelop),
qui esposto come modulo Python testabile in CI.

Format dataset JSON:
  - template + mask
  - params init matcher (override)
  - find_params (override per find())
  - scenes con ground_truth: lista pose attese (cx, cy, angle, scale,
    tolerance_px, tolerance_deg)

Metriche per scena: TP/FP/FN, precision, recall, IoU medio bbox,
tempo find. Aggregato: precision globale, recall, F1.

Match-to-GT criterio: distanza centro <= tolerance_px AND
|angle| <= tolerance_deg, oppure IoU bbox >= 0.3.

Use case:
- regressione: confronto config A vs B oggettivo
- tuning: trovare param ottimi via grid-search guidato da F1
- validazione pre-deploy: report TP/FP/FN su dataset prod

Esposto come entry-point pm2d-eval (pyproject.toml).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 10:09:45 +02:00

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:

  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

# 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/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.

S
Description
No description provided
Readme 11 MiB
Languages
Python 82.3%
JavaScript 11.3%
HTML 4%
CSS 2%
Dockerfile 0.4%