# Engine `shape_model_2d` — Documento tecnico operativo

**Progetto di appartenenza**: Tielogic Vision Suite
**Scope**: implementazione del primo matching engine della suite
**Tipo di matching**: shape-based 2D invariante a rotazione, scala, illuminazione
**Equivalente Halcon**: `create_shape_model`, `find_shape_model`, varianti `scaled`, `aniso`
**Stato**: documento operativo — integra e approfondisce la sezione 4 del documento di progetto Vision Suite
**Data**: 23 aprile 2026

---

## 1. Cosa deve fare questo engine

Il motore `shape_model_2d` risolve il problema classico del vision industriale 2D:

> Dato un template di riferimento (un'immagine esempio di un oggetto, oppure un DXF del suo profilo), trovare tutte le istanze di quel template in immagini di scena, restituendo per ciascuna istanza posizione (x, y), angolo di rotazione, scala e score di confidenza, con precisione subpixel.

Caratteristiche richieste:

- **Invariante a illuminazione**: lavora su gradienti/edge, non su intensità assolute
- **Invariante a rotazione**: range angolare configurabile (tipicamente 0-360°)
- **Invariante a scala**: range di scala configurabile (tipicamente ±5-20%)
- **Multi-istanza**: trova N istanze indipendenti dello stesso template
- **Robusto a occlusione**: fino al 40-50% dell'oggetto coperto
- **Performance real-time**: >10 Hz su CPU moderna per immagini 1920x1080
- **Subpixel accuracy**: <0.5 px di precisione posizionale, <0.5° angolare

Applicazioni industriali tipiche:

- Ispezione presenza/assenza di pezzi su nastro trasportatore
- Allineamento preciso prima di pick-and-place robotico
- Verifica orientamento componenti
- Controllo qualità di sagome tagliate (laser, fresatura, tranciatura)
- Conteggio istanze multiple dello stesso pezzo

---

## 2. Stato dell'open-source e scelte tecnologiche

### 2.1 Opzioni disponibili

Il panorama open-source per shape-based matching include diverse alternative, con diversa maturità e diverse trade-off.

**Opzione 1 — `meiqua/shape_based_matching`** (https://github.com/meiqua/shape_based_matching)

Reimplementazione open-source del shape-based matching di Halcon, basata su linemod di OpenCV con miglioramenti significativi. 1.4k star, 512 fork. Autore dell'SJTU robotics institute.

Caratteristiche:
- Scritto in C++ con OpenCV
- Accelerazione SIMD via MIPP (x86 SSE/AVX, ARM Neon)
- Branch dedicati per pose refinement con ICP, subpixel, gestione scale
- Può processare ~1000 template in ~20ms

Stato manutenzione:
- Sviluppo principale 2018-2020
- Attualmente non attivamente mantenuto ma stabile
- Molti fork derivati attivi

Problemi pratici:
- No Python binding ufficiali (solo C++)
- Build CMake da sistemare a mano
- Documentazione essenziale, esempi solo in `test.cpp`

**Opzione 2 — `mwwzbinf/mwwz-shape-match`** (https://github.com/mwwzbinf/mwwz-shape-match)

Implementazione più recente con focus esplicito sulle varianti Halcon (scaled, aniso).

Caratteristiche:
- Supporto esplicito a `find_scaled_shape_model` e `find_aniso_shape_model`
- Creazione modello da cerchio o rettangolo
- Subpixel nativo

Stato:
- Più recente del repo meiqua
- Base di utenti più piccola, meno testato in produzione
- Documentazione in cinese predominante

**Opzione 3 — OpenCV `cv::linemod` (opencv_contrib)**

Modulo base da cui deriva l'algoritmo di meiqua. Incluso in `opencv-contrib-python`.

Caratteristiche:
- Installazione banale (`pip install opencv-contrib-python`)
- Manutenzione OpenCV garantita
- Documentazione ufficiale

Limiti:
- Algoritmo "raw" senza raffinamenti
- No subpixel nativo
- Performance peggiori (no SIMD custom)
- Precisione angolare di ~1-2°, non sotto come i raffinamenti di meiqua

**Opzione 4 — OpenCV `matchTemplate`**

Correlation matching classico.

Limiti stringenti:
- No rotation invariance nativa (va wrappata manualmente con rotazioni multiple del template)
- No scale invariance nativa
- Non robusto a illuminazione

Non adatto come engine production. Utile solo come baseline di confronto.

### 2.2 Scelta raccomandata

**Strategia a tre fasi implementative**, ciascuna con un motore diverso:

**Fase Alpha** (2-3 settimane): prototipo con **OpenCV linemod**. L'obiettivo non è la precisione finale ma validare l'architettura Vision Suite, i contratti API, il workflow web UI. Precisione target: "funziona ed è utilizzabile".

**Fase Beta** (3-4 settimane): upgrade a **fork Tielogic di `meiqua/shape_based_matching`**. Precisione production, performance ottimizzate. Questa è la versione destinata ai clienti.

**Fase Gamma** (opzionale, futuro): se emergono requisiti specifici non coperti (deformazioni, matching planari prospettici, ecc.) si valuta integrazione di motori aggiuntivi complementari senza sostituire Beta.

### 2.3 Perché forkare il repo meiqua invece di usarlo direttamente

Per un prodotto commerciale destinato a clienti industriali, dipendere da un repo non attivamente mantenuto è un rischio operativo inaccettabile. Il fork Tielogic garantisce:

- **Continuità**: se l'autore rimuovesse il repo domani, il fork resta indipendente
- **Controllo qualità**: bug fix applicabili in autonomia
- **Adattamenti specifici**: aggiunte di feature richieste dai clienti Tielogic
- **Python bindings ufficiali**: il repo originale non ha binding Python; il fork li aggiunge come first-class citizen
- **CI/CD**: build riproducibili in container Docker verificati
- **Supporto commerciale**: possibilità di offrire SLA ai clienti paganti

Il costo iniziale del fork è ~1-2 settimane per sistemare build, aggiungere binding Python, test di non-regressione rispetto al repo originale. Da confrontare con il rischio di trovarsi bloccati in futuro.

**Strategia di fork pragmatica**:

1. Fork del repo originale sotto l'organization Tielogic (es. `tielogic/shape_based_matching`)
2. Branch `tielogic/main` che traccia `upstream/master`
3. Branch `tielogic/production` dove applichi patch Tielogic (binding Python, CMake multi-platform, bug fix)
4. Aggiornamenti periodici da upstream via merge controllato

---

## 3. Specifica funzionale dell'engine

### 3.1 Ingressi

L'engine accetta due tipi di asset sorgente per la creazione di un modello:

**Tipo 1 — Immagine di riferimento**

- Formato: PNG, JPG, BMP, TIFF
- Colore: accetta RGB/BGR o grayscale (conversione automatica a grayscale internamente)
- Risoluzione minima: 50x50 px (sotto questa soglia il modello è troppo povero)
- Risoluzione massima: 2000x2000 px (sopra rallenta creazione senza benefici)
- ROI opzionale: l'operatore può ritagliare una regione di interesse dall'immagine fornita

**Tipo 2 — DXF (Drawing Exchange Format)**

- Versioni supportate: DXF da R12 a AutoCAD 2022
- Entità supportate: LINE, POLYLINE, LWPOLYLINE, ARC, CIRCLE, SPLINE, ELLIPSE
- Entità ignorate: TEXT, MTEXT, DIMENSION, HATCH, INSERT (blocchi), LAYER invisibili
- Filtering: selezione esplicita dei layer da includere
- Unità: detection automatica da `$INSUNITS` header, override manuale disponibile

### 3.2 Parametri di creazione modello

```json
{
  "pyramid_levels": 4,
  "angle_range_deg": [0, 360],
  "angle_step_deg": 1.0,
  "scale_range": [1.0, 1.0],
  "scale_step": 0.05,
  "min_contrast": 30,
  "greediness": 0.9,
  "num_features": 150,
  
  "dxf_params": {
    "resolution_px_per_mm": 5.0,
    "layers_included": ["CONTORNO", "0"],
    "tessellation_tolerance_mm": 0.1,
    "line_thickness_px": 2
  }
}
```

**Significato dei parametri principali**:

- `pyramid_levels`: livelli della piramide multi-risoluzione. Più livelli = ricerca più rapida ma meno sensibile a feature piccole. Default 4 è buono per oggetti 100-800 px.
- `angle_range_deg`: range di rotazioni da cercare. Se sai che l'oggetto è sempre ±30° dalla posizione nominale, limitare il range velocizza 10x.
- `angle_step_deg`: risoluzione angolare dei template precomputati. 1° è standard; 0.5° per precisione superiore ma doppio tempo/memoria.
- `scale_range` + `scale_step`: range e risoluzione di scala. Lasciare `[1.0, 1.0]` se la scala è fissa (setup camera fisso) per massime performance.
- `min_contrast`: soglia gradiente minimo per considerare un pixel come feature. Basso (20-30) per immagini a basso contrasto, alto (60-80) per immagini pulite.
- `greediness`: trade-off tra velocità e accuracy. 0.9 è default, 0.7 più accurato ma più lento.
- `num_features`: numero di feature estratte dal template. Più feature = più robusto ma più lento.

### 3.3 Uscite del matching

Per ogni istanza trovata:

```json
{
  "x_px": 452.37,
  "y_px": 301.84,
  "angle_deg": 45.2,
  "scale": 1.01,
  "score": 0.94,
  "template_id": 0,
  "contour_polygon": [[x1,y1], [x2,y2], ...]
}
```

Dove:

- `(x_px, y_px)`: coordinate subpixel del punto origine del template, nell'immagine di scena
- `angle_deg`: rotazione dell'istanza rispetto al template canonico (0° = template non ruotato)
- `scale`: fattore di scala (1.0 = scala del template nominale)
- `score`: similarità normalizzata 0-1, dove 1.0 = match perfetto
- `template_id`: identificatore del sub-template usato (utile se un modello contiene varianti)
- `contour_polygon`: poligono di contorno del template trasformato con la pose trovata, utile per visualizzazione e per IoU downstream

---

## 4. Pipeline tecnica end-to-end

### 4.1 Pipeline di creazione modello

```
[Asset sorgente]
      │
      ▼
[Pre-processing]
  - Se DXF: parsing + tassellazione + rasterizzazione
  - Se immagine: conversione grayscale + eventuale ROI crop
      │
      ▼
[Edge extraction]
  - Calcolo gradiente (Sobel o Scharr)
  - Binning dell'orientamento su 8 direzioni (linemod)
  - Soglia su magnitude
      │
      ▼
[Feature selection]
  - Sampling di N feature spaziate sul template
  - Ordinamento per magnitude gradient
      │
      ▼
[Pyramid building]
  - Downscaling 2x ricorsivo
  - Ri-estrazione feature a ogni livello
      │
      ▼
[Template database]
  - Per ogni angolo nel range: ruota + ricampiona
  - Per ogni scala nel range: ridimensiona + ricampiona
  - Per ogni (angolo, scala): salva feature set
      │
      ▼
[Serializzazione]
  - Salvataggio binary su disco (model_cache.bin)
  - Metadata JSON
```

### 4.2 Pipeline di matching runtime

```
[Scena input]
      │
      ▼
[Pre-processing scena]
  - Conversione grayscale
  - Eventuale ROI crop
      │
      ▼
[Edge extraction]
  - Stesso processo del template
  - Response map costruito
      │
      ▼
[Matching piramidale]
  - Livello più alto piramide:
    - Correla ogni template ruotato/scalato contro la scena
    - Trova candidati con score > threshold
  - Livelli successivi:
    - Raffinamento locale intorno ai candidati
    - Trasferimento pose dal livello superiore
      │
      ▼
[Non-Maximum Suppression]
  - Elimina candidati duplicati spazialmente
  - Mantiene solo il best score per area
      │
      ▼
[Subpixel refinement]
  - Interpolazione parabolic sui peak di score
  - Ottimizzazione least-squares su pose
      │
      ▼
[Output: lista di match]
```

### 4.3 Pipeline DXF → PNG dettagliata

Questo sub-sistema è critico e merita specifica puntuale.

**Step 1 — Parsing**

```python
import ezdxf

doc = ezdxf.readfile(dxf_path)
msp = doc.modelspace()

# Filtra layer se richiesto
if layers_included:
    entities = [e for e in msp if e.dxf.layer in layers_included]
else:
    entities = list(msp)
```

**Step 2 — Detection unità**

```python
# $INSUNITS: 0=unitless, 1=inch, 4=mm
insunits_map = {1: 25.4, 2: 304.8, 4: 1.0, 5: 10.0, 6: 1000.0}
header_insunits = doc.header.get('$INSUNITS', 0)
unit_to_mm = insunits_map.get(header_insunits, 1.0)

# Override manuale se specificato
if force_unit == 'mm':
    unit_to_mm = 1.0
elif force_unit == 'inch':
    unit_to_mm = 25.4
```

**Step 3 — Tassellazione entità**

Ogni tipo di entità va convertita in polyline di punti:

```python
def entity_to_polyline(entity, tessellation_tol_mm):
    if entity.dxftype() == 'LINE':
        return [entity.dxf.start[:2], entity.dxf.end[:2]]
    
    elif entity.dxftype() == 'LWPOLYLINE':
        return [(v[0], v[1]) for v in entity.vertices_in_wcs()]
    
    elif entity.dxftype() == 'CIRCLE':
        # Tassellazione con step angolare calibrato su tolleranza
        from math import cos, sin, pi, acos
        cx, cy = entity.dxf.center[:2]
        r = entity.dxf.radius
        # Step che garantisce chord error < tolerance
        max_step = 2 * acos(1 - tessellation_tol_mm / r)
        n_segments = max(16, int(2 * pi / max_step))
        return [(cx + r*cos(2*pi*i/n_segments),
                 cy + r*sin(2*pi*i/n_segments)) 
                for i in range(n_segments + 1)]
    
    elif entity.dxftype() == 'ARC':
        # Analogo al cerchio ma con start_angle e end_angle
        ...
    
    elif entity.dxftype() == 'SPLINE':
        # ezdxf ha metodo flattening con tolerance
        return [(p[0], p[1]) for p in entity.flattening(tessellation_tol_mm)]
    
    elif entity.dxftype() == 'ELLIPSE':
        return [(p[0], p[1]) for p in entity.flattening(tessellation_tol_mm)]
    
    # Altri tipi: skippa
    return []
```

**Step 4 — Calcolo bounding box geometria**

```python
all_points = [p for polyline in polylines for p in polyline]
xs, ys = zip(*all_points)
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)

# Aggiungi margine (5% per lato)
margin = 0.05 * max(max_x - min_x, max_y - min_y)
min_x -= margin; max_x += margin
min_y -= margin; max_y += margin
```

**Step 5 — Rasterizzazione**

```python
import numpy as np
import cv2

# Dimensioni canvas in pixel
width_mm = max_x - min_x
height_mm = max_y - min_y
canvas_w_px = int(width_mm * resolution_px_per_mm)
canvas_h_px = int(height_mm * resolution_px_per_mm)

# Crea canvas bianco (sfondo)
canvas = np.ones((canvas_h_px, canvas_w_px), dtype=np.uint8) * 255

# Trasforma polyline in coordinate pixel (flip Y per convention image)
for polyline in polylines:
    pixel_points = [
        (int((x - min_x) * resolution_px_per_mm),
         int((max_y - y) * resolution_px_per_mm))  # flip Y
        for x, y in polyline
    ]
    # Disegna con antialiasing
    pts = np.array(pixel_points, dtype=np.int32).reshape((-1, 1, 2))
    cv2.polylines(canvas, [pts], isClosed=False, 
                  color=0, thickness=line_thickness_px,
                  lineType=cv2.LINE_AA)

return canvas
```

**Step 6 — Validazione finale**

```python
# Verifica che ci siano edge sufficienti per il matching
edges = cv2.Canny(canvas, 50, 150)
edge_pixel_count = np.count_nonzero(edges)
if edge_pixel_count < 100:
    raise ValueError("DXF produces too few edges for reliable matching")
```

---

## 5. Implementazione Fase Alpha: OpenCV linemod

Primo stadio implementativo con obiettivo di validare l'architettura. Precisione limitata, ma zero complessità di build.

### 5.1 Dipendenze

```
opencv-contrib-python>=4.8.0
ezdxf>=1.1.0
numpy>=1.24
pillow>=10.0
```

Nessuna build C++, tutto via pip.

### 5.2 Wrapper `ShapeModel2DMatcher`

```python
import cv2
import numpy as np
import pickle


class ShapeModel2DMatcher:
    """Wrapper attorno cv2.linemod per Fase Alpha.
    
    Espone API simile a Halcon: add_template, match, save, load.
    Sacrifica precisione subpixel per semplicità implementativa.
    """
    
    def __init__(self, pyramid_levels=4):
        self.detector = cv2.linemod.getDefaultLINE()
        self.templates = {}  # template_id -> metadata
        self.pyramid_levels = pyramid_levels
    
    def add_template(self, template_image, class_id="default",
                     angle_range=(0, 360), angle_step=1.0,
                     scale_range=(1.0, 1.0), scale_step=0.1):
        """Aggiunge template con rotazioni e scale precomputate."""
        
        # Prepara mask (tutti i pixel = foreground)
        mask = np.ones_like(template_image, dtype=np.uint8) * 255
        
        angles = np.arange(angle_range[0], angle_range[1] + angle_step, angle_step)
        scales = np.arange(scale_range[0], scale_range[1] + scale_step, scale_step)
        
        for scale in scales:
            for angle in angles:
                # Applica rotazione + scala
                h, w = template_image.shape
                M = cv2.getRotationMatrix2D((w/2, h/2), angle, scale)
                rotated = cv2.warpAffine(template_image, M, (w, h),
                                          borderValue=255)
                rotated_mask = cv2.warpAffine(mask, M, (w, h),
                                               borderValue=0)
                
                # Estrai feature e aggiungi template a linemod
                sources = [rotated]
                tid = self.detector.addTemplate(sources, class_id, rotated_mask)
                
                # Salva metadata per recupero
                self.templates[tid] = {
                    "angle": angle,
                    "scale": scale,
                    "class_id": class_id,
                    "template_size": (w, h),
                }
        
        return len(self.templates)
    
    def match(self, scene_image, min_score=0.7, max_matches=10):
        """Esegue matching sulla scena."""
        
        sources = [scene_image]
        matches = self.detector.match(sources, min_score * 100)
        
        # Converti format: linemod usa 0-100, noi 0-1
        results = []
        for m in matches[:max_matches]:
            tid = m.template_id
            meta = self.templates.get(tid, {})
            results.append({
                "x_px": float(m.x),
                "y_px": float(m.y),
                "angle_deg": meta.get("angle", 0.0),
                "scale": meta.get("scale", 1.0),
                "score": float(m.similarity) / 100.0,
                "template_id": tid,
            })
        
        return results
    
    def save(self, path):
        """Serializza matcher su disco."""
        # linemod non ha native serialization completa, usiamo hack
        # con FileStorage + pickle per metadata
        fs = cv2.FileStorage(path + ".xml", cv2.FILE_STORAGE_WRITE)
        self.detector.write(fs)
        fs.release()
        
        with open(path + ".meta", 'wb') as f:
            pickle.dump(self.templates, f)
    
    def load(self, path):
        """Carica matcher da disco."""
        fs = cv2.FileStorage(path + ".xml", cv2.FILE_STORAGE_READ)
        self.detector.read(fs.root())
        fs.release()
        
        with open(path + ".meta", 'rb') as f:
            self.templates = pickle.load(f)
```

### 5.3 Integrazione nell'architettura Vision Suite

La classe `ShapeModel2DEngine` (scheletro in Appendice B del documento Vision Suite) usa `ShapeModel2DMatcher` internamente. Zero cambiamenti all'interfaccia pubblica quando si passa da Alpha a Beta.

### 5.4 Prestazioni attese Fase Alpha

Su CPU moderna (Intel i7-12xxx o AMD Ryzen 7 5xxx, 8 core):

- Creazione modello: 5-15 secondi per template 300x300 px con range angolare 0-360° step 2°
- Memory modello: 50-200 MB per modello tipico
- Matching runtime: 50-200 ms per immagine 1920x1080
- Precisione posizionale: 1-3 px (no subpixel)
- Precisione angolare: 2° (risoluzione angolare step)

Queste performance sono sufficienti per validare l'architettura, non per produzione finale.

### 5.5 Limitazioni note della Fase Alpha

- No subpixel refinement
- No SIMD acceleration specifica
- Scale invariance implementata "a mano" (multiplicazione template precomputati) — poco efficiente memoria
- Angular precision limitata dal step scelto
- Non gestisce bene template con molta simmetria

Queste limitazioni sono il motivo per cui è uno **stadio di prototipo**, non il prodotto finale.

---

## 6. Implementazione Fase Beta: fork `meiqua/shape_based_matching`

### 6.1 Strategia di fork

**Preparazione repository**:

```bash
# Fork su organization Tielogic
# GitHub: Settings → Fork → Create as tielogic/shape_based_matching

git clone https://github.com/tielogic/shape_based_matching.git
cd shape_based_matching

# Traccia upstream
git remote add upstream https://github.com/meiqua/shape_based_matching.git

# Branch Tielogic
git checkout -b tielogic/production
```

**Patch Tielogic da applicare**:

1. **CMakeLists.txt multi-platform**:
   - Rimuovere hardcoded paths ROS
   - Supporto Windows + Linux + macOS
   - Detection automatica OpenCV version

2. **Python bindings via pybind11**:
   - Creazione `python_bindings/` sub-directory
   - Wrapping API C++ principali
   - Setup.py per pip-installable package

3. **API helpers addizionali**:
   - Export `save_to_json` / `load_from_json` per metadata
   - Helper per subpixel refinement con ICP integrato
   - Batch matching API per scenari multi-template

4. **Testing**:
   - Dataset di test industriali (pezzi Tielogic reali)
   - Benchmark automatici di precisione vs Halcon (se disponibile per riferimento)
   - Continuous integration GitHub Actions

5. **Documentazione**:
   - README completo con esempi Python e C++
   - Guide migration da Halcon
   - Performance tuning guide

### 6.2 Struttura del fork

```
tielogic/shape_based_matching/
├── CMakeLists.txt                    # aggiornato multi-platform
├── line2Dup.h / .cpp                 # codice originale (minimamente toccato)
├── MIPP/                              # SIMD library (originale)
├── tielogic_extensions/               # codice Tielogic aggiuntivo
│   ├── subpixel_refiner.h / .cpp
│   ├── batch_matcher.h / .cpp
│   └── json_serializer.h / .cpp
├── python_bindings/                   # NEW: pybind11 wrapping
│   ├── CMakeLists.txt
│   ├── bindings.cpp
│   └── setup.py
├── tests/
│   ├── cpp/                           # test C++ originali + estensioni
│   └── python/                        # test Python del wrapper
├── benchmarks/
│   ├── datasets/                      # pezzi reali Tielogic
│   └── scripts/
├── docs/
│   ├── README.md
│   ├── python_api.md
│   ├── cpp_api.md
│   └── migration_from_halcon.md
└── .github/workflows/
    ├── ci.yml                         # build e test automatici
    └── release.yml                    # packaging e release
```

### 6.3 Python bindings

Scheletro del wrapping pybind11 in `python_bindings/bindings.cpp`:

```cpp
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>
#include "line2Dup.h"
#include "tielogic_extensions/subpixel_refiner.h"

namespace py = pybind11;

py::array_t<uint8_t> cv_mat_to_numpy(const cv::Mat& mat) {
    // Conversione cv::Mat -> numpy array
    // ...
}

cv::Mat numpy_to_cv_mat(py::array_t<uint8_t> arr) {
    // Conversione numpy array -> cv::Mat
    // ...
}

PYBIND11_MODULE(shape_based_matching, m) {
    m.doc() = "Tielogic shape-based matching (Halcon-equivalent)";
    
    py::class_<line2Dup::Detector>(m, "Detector")
        .def(py::init<int, std::vector<int>, float, float>(),
             py::arg("num_features") = 128,
             py::arg("T") = std::vector<int>{4, 8},
             py::arg("weak_thresh") = 30.0f,
             py::arg("strong_thresh") = 60.0f)
        .def("add_template", [](line2Dup::Detector& self,
                                py::array_t<uint8_t> image,
                                const std::string& class_id,
                                py::array_t<uint8_t> mask) {
            cv::Mat img = numpy_to_cv_mat(image);
            cv::Mat msk = numpy_to_cv_mat(mask);
            std::vector<cv::Mat> sources = {img};
            return self.addTemplate(sources, class_id, msk);
        })
        .def("match", [](line2Dup::Detector& self,
                         py::array_t<uint8_t> image,
                         float threshold,
                         const std::vector<std::string>& class_ids) {
            cv::Mat img = numpy_to_cv_mat(image);
            std::vector<cv::Mat> sources = {img};
            auto matches = self.match(sources, threshold, class_ids);
            
            // Converti risultato in dizionari Python
            py::list result;
            for (const auto& m : matches) {
                py::dict d;
                d["x"] = m.x;
                d["y"] = m.y;
                d["similarity"] = m.similarity;
                d["class_id"] = m.class_id;
                d["template_id"] = m.template_id;
                result.append(d);
            }
            return result;
        }, py::arg("image"),
           py::arg("threshold") = 80.0f,
           py::arg("class_ids") = std::vector<std::string>{})
        .def("save", &line2Dup::Detector::writeClasses)
        .def("load", &line2Dup::Detector::readClasses);
    
    // Subpixel refiner Tielogic extension
    py::class_<tielogic::SubpixelRefiner>(m, "SubpixelRefiner")
        .def(py::init<>())
        .def("refine", [](tielogic::SubpixelRefiner& self,
                          py::array_t<uint8_t> scene,
                          py::array_t<uint8_t> template_img,
                          float x, float y, float angle) {
            cv::Mat scn = numpy_to_cv_mat(scene);
            cv::Mat tpl = numpy_to_cv_mat(template_img);
            auto refined = self.refine(scn, tpl, x, y, angle);
            py::dict d;
            d["x"] = refined.x;
            d["y"] = refined.y;
            d["angle"] = refined.angle;
            return d;
        });
}
```

Build via `setup.py`:

```python
from setuptools import setup, Extension
from pybind11.setup_helpers import Pybind11Extension, build_ext
import sys

ext_modules = [
    Pybind11Extension(
        "shape_based_matching",
        sources=[
            "python_bindings/bindings.cpp",
            "line2Dup.cpp",
            "tielogic_extensions/subpixel_refiner.cpp",
        ],
        include_dirs=["./", "MIPP/"],
        libraries=["opencv_core", "opencv_imgproc"],
        language="c++",
        cxx_std=17,
    ),
]

setup(
    name="tielogic-shape-matching",
    version="0.1.0",
    ext_modules=ext_modules,
    cmdclass={"build_ext": build_ext},
    zip_safe=False,
)
```

### 6.4 Usage Python dopo build

```python
import shape_based_matching as sbm
import cv2

# Creazione detector
detector = sbm.Detector(num_features=150, T=[4, 8])

# Add template
template = cv2.imread("flangia_ref.png", cv2.IMREAD_GRAYSCALE)
mask = (template < 200).astype('uint8') * 255
template_id = detector.add_template(template, "flangia", mask)

# Generate rotation variants (Tielogic helper)
variants = sbm.generate_rotation_variants(
    template=template,
    angle_range=(0, 360),
    angle_step=1.0,
)
for v in variants:
    detector.add_template(v.image, f"flangia_rot_{v.angle}", v.mask)

# Match on scene
scene = cv2.imread("production_scene.png", cv2.IMREAD_GRAYSCALE)
matches = detector.match(scene, threshold=80.0)

# Subpixel refinement
refiner = sbm.SubpixelRefiner()
for m in matches:
    refined = refiner.refine(scene, template, m["x"], m["y"], 0.0)
    print(f"Match at ({refined['x']:.3f}, {refined['y']:.3f})")
```

### 6.5 Integrazione nel Dockerfile Vision Suite

```dockerfile
FROM python:3.11-slim-bookworm

# Dipendenze build C++
RUN apt-get update && apt-get install -y \
    build-essential \
    cmake \
    libopencv-dev \
    libopencv-contrib-dev \
    git \
    && rm -rf /var/lib/apt/lists/*

# Installa pybind11 per Python bindings
RUN pip install --no-cache-dir pybind11

# Clone fork Tielogic
RUN git clone --depth 1 --branch tielogic/production \
    https://github.com/tielogic/shape_based_matching.git /opt/sbm

# Build e installa modulo Python
WORKDIR /opt/sbm/python_bindings
RUN python setup.py build_ext --inplace && \
    pip install --no-cache-dir .

# Verifica import
RUN python -c "import shape_based_matching; print(shape_based_matching.__version__)"

# ... resto del Vision Suite Dockerfile
```

### 6.6 Prestazioni attese Fase Beta

Sullo stesso hardware di riferimento:

- Creazione modello: 2-5 secondi per template 300x300 px con range angolare 0-360° step 1°
- Memory modello: 10-50 MB (più efficiente di linemod puro)
- Matching runtime: 5-30 ms per immagine 1920x1080
- Precisione posizionale: 0.1-0.3 px (subpixel)
- Precisione angolare: 0.1-0.5° (dopo refinement)

Guadagno 10x in velocità runtime, 3-5x in precisione rispetto a Fase Alpha.

---

## 7. Analisi di distintività automatica

Feature che distingue questa implementazione da un semplice wrapper: un'analisi offline che segnala problemi nel template prima che diventino problemi in produzione.

### 7.1 Metriche calcolate

**Simmetrie rotazionali**

Calcolate via auto-correlation del template rotato su sé stesso:

```python
def detect_rotational_symmetries(template, angle_step=5.0):
    """Rileva simmetrie rotazionali nel template.
    
    Ritorna lista di tuple (n, tolerance_deg) dove n è l'ordine della simmetria
    (es. 4 = simmetria quadrangolare 90°, 2 = simmetria bilaterale 180°).
    """
    import cv2
    import numpy as np
    
    h, w = template.shape
    center = (w // 2, h // 2)
    
    correlations = []
    for angle in np.arange(0, 360, angle_step):
        M = cv2.getRotationMatrix2D(center, angle, 1.0)
        rotated = cv2.warpAffine(template, M, (w, h))
        # NCC tra originale e ruotato
        result = cv2.matchTemplate(
            template, rotated, cv2.TM_CCOEFF_NORMED
        )
        correlations.append((angle, float(result.max())))
    
    # Trova picchi (>0.85 correlation con ±tolerance)
    peaks = []
    for angle, corr in correlations:
        if corr > 0.85 and angle > 0:
            peaks.append((angle, corr))
    
    # Deduci ordine di simmetria dai picchi
    symmetries = []
    for angle, corr in peaks:
        if abs(angle - 180) < 10:
            symmetries.append({"type": "bilateral", "n": 2})
        elif abs(angle - 90) < 10 or abs(angle - 270) < 10:
            if not any(s["n"] == 4 for s in symmetries):
                symmetries.append({"type": "rotational", "n": 4})
        elif abs(angle - 120) < 10 or abs(angle - 240) < 10:
            symmetries.append({"type": "rotational", "n": 3})
    
    return symmetries
```

**Entropia degli orientamenti edge**

Basso valore → template ripetitivo/ambiguo:

```python
def edge_orientation_entropy(template, num_bins=16):
    """Entropia dell'istogramma orientamenti edge.
    
    Alto valore (vicino a log(num_bins)) = edge in tutte le direzioni, distintivo.
    Basso valore = edge concentrati in poche direzioni, template ambiguo.
    """
    import cv2
    import numpy as np
    
    gx = cv2.Sobel(template, cv2.CV_32F, 1, 0, ksize=3)
    gy = cv2.Sobel(template, cv2.CV_32F, 0, 1, ksize=3)
    
    magnitude = np.sqrt(gx**2 + gy**2)
    angle = np.arctan2(gy, gx) * 180 / np.pi
    
    # Solo pixel con edge forte
    mask = magnitude > np.percentile(magnitude, 80)
    angles_valid = angle[mask]
    
    hist, _ = np.histogram(angles_valid, bins=num_bins, range=(-180, 180))
    hist_normalized = hist / (hist.sum() + 1e-9)
    
    # Entropia di Shannon
    entropy = -np.sum(
        hist_normalized * np.log(hist_normalized + 1e-9)
    )
    max_entropy = np.log(num_bins)
    
    return entropy / max_entropy  # Normalizzata 0-1
```

**Self-similarity score**

Indica quanto il template è simile a sé stesso in posizioni diverse:

```python
def self_similarity_score(template, offset_range=None):
    """Quanto il template assomiglia a versioni traslate di sé stesso.
    
    Alto valore = template ripetitivo (checkerboard, griglia).
    Basso valore = template distintivo.
    """
    import cv2
    import numpy as np
    
    h, w = template.shape
    if offset_range is None:
        offset_range = (w // 20, w // 4)  # 5-25% della dimensione
    
    max_similarity = 0.0
    for dx in range(offset_range[0], offset_range[1], 5):
        shifted = np.roll(template, dx, axis=1)
        result = cv2.matchTemplate(template, shifted, cv2.TM_CCOEFF_NORMED)
        max_similarity = max(max_similarity, float(result.max()))
    
    return max_similarity
```

### 7.2 Output dell'analisi

```json
{
  "distinctiveness_score": 0.74,
  "edge_orientation_entropy": 0.88,
  "self_similarity": 0.23,
  "num_features_extracted": 147,
  "symmetries_detected": [
    {"type": "bilateral", "n": 2, "tolerance_deg": 3.0}
  ],
  "warnings": [
    "Il modello mostra simmetria bilaterale. L'angolo stimato potrebbe avere ambiguità di ±180°.",
    "Considera di includere feature asimmetriche (marcature, fori, incisioni) per eliminare l'ambiguità."
  ],
  "expected_accuracy": {
    "positional_px": 0.25,
    "angular_deg": 0.5
  },
  "recommended_matching_options": {
    "min_score": 0.75,
    "overlap_threshold": 0.3
  }
}
```

### 7.3 Esposizione all'utente nella web UI

Questa analisi viene mostrata durante la creazione della ricetta, prima del salvataggio finale:

```
┌─ Analisi qualità modello ─────────────────────────────┐
│                                                        │
│   Score distintività: ██████████░░  74%              │
│                                                        │
│   ⚠ Attenzione: rilevata simmetria bilaterale         │
│                                                        │
│   Suggerimenti:                                        │
│   • Aggiungi feature asimmetriche per ridurre         │
│     ambiguità a ±180°                                  │
│                                                        │
│   Accuracy attesa in produzione:                       │
│   • Posizione: ±0.25 px                               │
│   • Angolo: ±0.5°                                      │
│                                                        │
│   [Prosegui comunque] [Torna indietro e modifica]     │
└────────────────────────────────────────────────────────┘
```

---

## 8. Benchmark e validazione

### 8.1 Dataset di test

Per validare l'engine servono dataset rappresentativi. Suggerimenti:

**Dataset sintetico**:
- 20-30 template rappresentativi di pezzi industriali (flange, staffe, cinghie, PCB)
- Per ogni template generazione di 100+ scene con:
  - Pose GT note
  - Rumore gaussiano variabile (5%, 10%, 20%)
  - Occlusioni parziali (0%, 20%, 40%)
  - Variazioni di illuminazione (± 30%)
  - Sfondi variabili

**Dataset reale Tielogic**:
- 10-20 pezzi cliente reali fotografati in condizioni di produzione
- Ground truth via misurazione manuale o altro sistema di riferimento
- Condivisione con clienti come beneficio (loro ottengono audit qualità, Tielogic arricchisce dataset)

### 8.2 Metriche di valutazione

Per ogni match ritornato dall'engine:

- **Posizionale**: `|pose_stimata - pose_GT|` in pixel
- **Angolare**: differenza angolo modulo 360° (considerando simmetrie)
- **Precision**: percentuale di match ritornati che sono true positive
- **Recall**: percentuale di istanze GT correttamente rilevate
- **F1 score**: media armonica di precision e recall
- **Latency**: tempo medio di matching per immagine

### 8.3 Target di qualità

Per essere accettabile in produzione, l'engine Beta deve raggiungere:

- F1 score >0.95 su dataset sintetico senza occlusione
- F1 score >0.85 su dataset sintetico con 30% occlusione
- F1 score >0.90 su dataset reale Tielogic
- Precisione posizionale mediana <0.5 px
- Precisione angolare mediana <1.0°
- Latency mediana <50 ms su immagini 1920x1080

Se questi target non sono raggiunti dopo la Fase Beta, si attiva la Fase Gamma con motori complementari.

### 8.4 Continuous validation

Una volta in produzione, ogni release candidate deve passare suite di benchmark automatici:

```
CI Pipeline:
1. Build Vision Suite container
2. Scarica dataset di test dal registry interno
3. Esegui suite di match su tutti i casi
4. Confronta con risultati golden (tolleranza di degrado <5%)
5. Pubblica report dettagliato
6. Se regression: block merge
```

---

## 9. Casi d'uso validazione con clienti Tielogic

### 9.1 Scenari tipici di deployment

**Scenario A — Ispezione linea pezzi stampati**:
- Camera industriale fissa sopra nastro trasportatore
- Lighting LED uniforme, sfondo contrastato
- Pezzi ~50-200 mm, velocità nastro 0.5 m/s
- Esigenza: localizzare pose per pick-and-place robot
- Target: 95% pick success rate, latency <100 ms

**Scenario B — Controllo qualità sagoma taglio laser**:
- Camera calibrata metrologicamente su banco misura
- Backlight per contorno pulito
- Confronto sagoma tagliata vs DXF progetto
- Esigenza: verifica tolleranze dimensionali ±0.1 mm
- Target: ripetibilità <0.05 mm, discriminazione scarti >99%

**Scenario C — Assemblaggio componenti elettronici**:
- Camera su robot SCARA
- Illuminazione frontale con diffusore
- PCB con componenti multipli da riconoscere
- Esigenza: localizzare e orientare 10-20 componenti per istanza
- Target: 1 Hz di throughput, accuracy ±0.1 mm

### 9.2 Iteration con primi clienti

Il processo di validation con cliente reale:

1. **Acquisizione requisiti dettagliata** — hardware disponibile, precisione richiesta, throughput, ambiente
2. **Fornitura dataset di test** — il cliente cattura 50-100 immagini rappresentative con GT
3. **Creazione ricette iniziali** — Tielogic crea prime ricette via web UI
4. **Validation loop** — iterazione su parametri fino a raggiungere target
5. **Pilot on-site** — deployment iniziale con monitoring intensivo (1-4 settimane)
6. **Go-live** — passaggio a produzione con SLA

Ogni pilot diventa **case study** e dataset di riferimento per la suite. Con 3-5 pilot il sistema ha solidità statistica per confronto con Halcon.

---

## 10. Roadmap implementativa specifica

### Settimana 1-2: Setup Fase Alpha

- [ ] Struttura progetto engine dentro Vision Suite
- [ ] Integrazione OpenCV contrib nel Dockerfile
- [ ] Implementazione `ShapeModel2DMatcher` wrapper su linemod
- [ ] Unit test della classe wrapper
- [ ] Integrazione con `MatchingEngine` ABC

### Settimana 3: Pipeline DXF → PNG

- [ ] Installazione ezdxf nel container
- [ ] Implementazione `dxf_rasterizer.py` completo
- [ ] Test con 10+ DXF campione di diverse provenienze
- [ ] Gestione edge case (layer invisibili, unità miste, entità non supportate)

### Settimana 4: Web UI Recipe Wizard

- [ ] Pagina upload asset con detection automatica tipo
- [ ] Editor ROI per immagini
- [ ] Preview live rasterizzazione DXF con parametri tunabili
- [ ] Form parametri matching con valori default
- [ ] Submission a API backend

### Settimana 5: Analisi distintività

- [ ] Implementazione metriche (simmetrie, entropia, self-similarity)
- [ ] Integrazione in flusso creazione ricetta
- [ ] UI di visualizzazione warning
- [ ] Test su template noti ambigui per calibrare soglie

### Settimana 6-7: Endpoint API matching completo

- [ ] Validazione completa input runtime
- [ ] Routing `/api/match` a `ShapeModel2DEngine.match()`
- [ ] Serializzazione risultati JSON
- [ ] Debug store snapshot
- [ ] Playground web UI funzionante

### Settimana 8-9: Setup Fase Beta

- [ ] Fork `meiqua/shape_based_matching` su org Tielogic
- [ ] Aggiornamento CMakeLists.txt multi-platform
- [ ] Implementazione Python bindings base
- [ ] Build test Linux (CI Ubuntu) e Windows
- [ ] Pacchetto Python installabile via pip

### Settimana 10-11: Migration Alpha → Beta

- [ ] Integrazione pacchetto fork nel Dockerfile
- [ ] Aggiornamento `ShapeModel2DMatcher` per usare fork
- [ ] Compatibilità: ricette Alpha devono essere riutilizzabili in Beta
- [ ] Benchmark: confronto precisione Alpha vs Beta
- [ ] Documentazione upgrade path

### Settimana 12-14: Subpixel, ottimizzazioni, documentazione

- [ ] Integrazione subpixel refiner (estensione Tielogic)
- [ ] Ottimizzazioni performance specifiche
- [ ] Test di ripetibilità su hardware diverso
- [ ] Documentazione API Python completa
- [ ] Guida migration da Halcon per programmatori

**Totale: 12-14 settimane per Fase Alpha + Beta complete.**

---

## 11. Criteri di uscita / done

L'engine `shape_model_2d` è considerato pronto per il rilascio quando:

- [ ] Tutte le funzionalità Fase Beta implementate e testate
- [ ] Target di qualità (sezione 8.3) raggiunti su dataset sintetico e reale
- [ ] Suite di test automatici verdi (>95% code coverage componenti core)
- [ ] Documentazione API Python completa con esempi eseguibili
- [ ] Guida Docker deployment per cliente verificata da persona esterna al team sviluppo
- [ ] 3 case study completi su pezzi industriali reali
- [ ] Fork `tielogic/shape_based_matching` pubblicato con release v1.0 taggata
- [ ] Benchmark comparativo con almeno un altro strumento (Halcon se accessibile, altrimenti OpenCV linemod baseline)
- [ ] Web UI Recipe Wizard utilizzabile da operatore non-programmatore (validato con test utente)
- [ ] Issue tracking attivo per regression e feedback clienti

---

## 12. Riferimenti tecnici

**Repository principale di riferimento**:
- `meiqua/shape_based_matching`: https://github.com/meiqua/shape_based_matching
- Fork Tielogic (da creare): https://github.com/tielogic/shape_based_matching

**Alternative valutate**:
- `mwwzbinf/mwwz-shape-match`: https://github.com/mwwzbinf/mwwz-shape-match
- `daxiaHuang/shape_based_matching_subpixel`: https://github.com/daxiaHuang/shape_based_matching_subpixel
- OpenCV linemod: https://docs.opencv.org/4.x/d7/d00/tutorial_line_descriptor_main.html

**Librerie Python supporto**:
- ezdxf: https://ezdxf.readthedocs.io/
- opencv-contrib-python: https://pypi.org/project/opencv-contrib-python/
- pybind11: https://pybind11.readthedocs.io/

**Riferimento scientifico originale**:
- Linemod paper: Hinterstoisser et al., "Gradient Response Maps for Real-Time Detection of Textureless Objects", TPAMI 2012
- "Machine Vision Algorithms and Applications" (libro di riferimento Halcon engineers)

**Riferimento commerciale (per confronto)**:
- MVTec Halcon shape matching: https://www.mvtec.com/products/halcon

---

## Appendice A — Esempio completo workflow utente

Dalla prospettiva di un operatore Tielogic che configura un nuovo task di ispezione.

### A.1 Creazione della ricetta via web UI

Operatore apre browser su `http://vision-service.tielogic.local:8080/ui`:

1. Click su **"Crea nuova ricetta"**
2. Selezione tipo: **"Shape Model 2D"**
3. Upload asset:
   - Opzione scelta: caricamento DXF `flangia_80mm.dxf`
   - Sistema rileva unità: mm (confermato)
   - Preview rasterizzazione mostrata con edge in verde
4. Configurazione layer DXF:
   - Layer `CONTORNO` selezionato (contiene il profilo esterno)
   - Layer `QUOTE`, `TESTI`, `CENTRINI` deselezionati
5. Parametri matching:
   - Range angolare: 0-360° (default)
   - Scale: fissa a 1.0 (camera fissa calibrata)
   - Pyramid levels: 4 (default)
6. Click **"Analizza"**:
   - Sistema genera modello in 3 secondi
   - Mostra preview con feature in rosso
   - Score distintività: 91% (buono)
   - Nessun warning di simmetria
   - Accuracy attesa: ±0.2 px, ±0.3°
7. Compilazione metadata:
   - recipe_id: `flangia_80mm_v1`
   - Display name: `Flangia 80mm Cliente Acme`
   - Tags: `cliente_acme`, `linea_produzione_3`
8. Click **"Salva"**. Ricetta disponibile in lista.

### A.2 Test in Playground

Operatore va su pagina **"Playground"**:

1. Selezione ricetta: `flangia_80mm_v1`
2. Upload immagine di test: `sample_production_scene.png`
3. Parametri runtime:
   - Min score: 0.7
   - Max matches: 10
4. Click **"Match"**:
   - Risultato in 18 ms
   - 3 istanze trovate con score 0.94, 0.91, 0.87
   - Visualizzazione con overlay rettangoli colorati sui match
   - Tabella sottostante con dati numerici

Se risultati soddisfacenti, la ricetta è pronta per il deployment.

### A.3 Uso da applicazione client

Codice Python che l'integratore installa sulla macchina di produzione:

```python
from tielogic_vision import VisionClient
import cv2
import pyrealsense2 as rs  # o qualsiasi SDK camera

# Connessione al servizio vision
client = VisionClient(service_url="http://vision-service.tielogic.local:8080")

# Setup acquisizione camera (esempio generico)
camera = setup_camera()

while True:
    # Acquisisci frame
    frame = camera.capture()
    
    # Matching
    matches = client.match(
        recipe_id="flangia_80mm_v1",
        image=frame,
        options={"min_score": 0.8, "max_matches": 5}
    )
    
    # Invia posizioni al robot
    for m in matches:
        robot.queue_pick(
            x_px=m.x_px,
            y_px=m.y_px,
            angle_deg=m.angle_deg
        )
    
    # Log telemetria
    log_metrics(num_matches=len(matches), 
                latency_ms=matches[0].inference_time_ms)
```

---

## Appendice B — Esempio di test industriale

Test di validazione su pezzo cliente reale (esempio fittizio ma realistico).

### B.1 Contesto

Cliente: stamperia metalli di precisione.
Pezzo: flangia rotonda 80mm con 6 fori M8 su cerchio di 60mm.
Esigenza: localizzare 10-20 flange su nastro 500x300mm per pick-and-place.

### B.2 Setup test

- Camera: Basler ace acA2440-75um (5MP monocromatica)
- Ottica: 12mm, WD 500mm
- Illuminazione: ring light LED bianco
- Nastro fermo durante acquisizione (ciclo stop-capture-pick)

### B.3 Dataset

- 100 immagini con flange distribuite casualmente
- GT tramite misurazione manuale con marker ArUco temporanei
- Condizioni di illuminazione variabili (±15%)
- Alcuni casi con occlusione parziale (altre flange sovrapposte)

### B.4 Risultati attesi

Target da raggiungere per considerare deploy riuscito:

- Recall: >99% (nessuna flangia persa)
- Precision: >99% (nessun falso positivo)
- Precisione posizionale mediana: <0.3 px
- Precisione angolare mediana: <0.5°
- Latency mediana: <30 ms
- Robustezza a occlusioni fino al 30%

### B.5 Protocollo di test

1. Creazione ricetta da DXF CAD del pezzo
2. Analisi distintività deve dare score >80%
3. Run batch su 100 immagini
4. Calcolo metriche aggregate
5. Review manuale di tutti i match con score <0.8
6. Iterazione parametri se target non raggiunti
7. Test di ripetibilità: stesso stream 10 volte, verifica deviation standard pose <0.1 px

Test passato → ricetta approvata per produzione.
