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

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:

  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 — 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/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.
S
Description
No description provided
Readme 11 MiB
Languages
Python 82.3%
JavaScript 11.9%
HTML 3.7%
CSS 1.7%
Dockerfile 0.4%