Programma standalone Pattern Matching 2D con GUI cv2/tk + algoritmo puro riusabile. Due backend: - LineShapeMatcher (default): porting Python di line2Dup (linemod-style) - Gradient orientation quantized 8-bin modulo π + spreading - Feature sparse top-magnitude con spacing minimo - Score via shift-add vettorizzato numpy (O(N_features·H·W)) - Piramide multi-risoluzione con pruning varianti al top-level - Supporto mask binaria per modello non-rettangolare - EdgeShapeMatcher (fallback): Canny + matchTemplate multi-rotazione GUI separata da algoritmo. Benchmark clip.png (13 istanze): - Edge backend: 84s, 6/13 score ~0.3 - Line backend: 4.1s, 13/13 score 0.98-1.00 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
45 KiB
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_modelefind_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:
- Fork del repo originale sotto l'organization Tielogic (es.
tielogic/shape_based_matching) - Branch
tielogic/mainche tracciaupstream/master - Branch
tielogic/productiondove applichi patch Tielogic (binding Python, CMake multi-platform, bug fix) - 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
$INSUNITSheader, override manuale disponibile
3.2 Parametri di creazione modello
{
"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:
{
"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 scenaangle_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 perfettotemplate_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
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à
# $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:
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
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
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
# 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
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:
# 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:
-
CMakeLists.txt multi-platform:
- Rimuovere hardcoded paths ROS
- Supporto Windows + Linux + macOS
- Detection automatica OpenCV version
-
Python bindings via pybind11:
- Creazione
python_bindings/sub-directory - Wrapping API C++ principali
- Setup.py per pip-installable package
- Creazione
-
API helpers addizionali:
- Export
save_to_json/load_from_jsonper metadata - Helper per subpixel refinement con ICP integrato
- Batch matching API per scenari multi-template
- Export
-
Testing:
- Dataset di test industriali (pezzi Tielogic reali)
- Benchmark automatici di precisione vs Halcon (se disponibile per riferimento)
- Continuous integration GitHub Actions
-
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:
#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:
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
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
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:
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:
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:
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
{
"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:
- Acquisizione requisiti dettagliata — hardware disponibile, precisione richiesta, throughput, ambiente
- Fornitura dataset di test — il cliente cattura 50-100 immagini rappresentative con GT
- Creazione ricette iniziali — Tielogic crea prime ricette via web UI
- Validation loop — iterazione su parametri fino a raggiungere target
- Pilot on-site — deployment iniziale con monitoring intensivo (1-4 settimane)
- 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
ShapeModel2DMatcherwrapper su linemod - Unit test della classe wrapper
- Integrazione con
MatchingEngineABC
Settimana 3: Pipeline DXF → PNG
- Installazione ezdxf nel container
- Implementazione
dxf_rasterizer.pycompleto - 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/matchaShapeModel2DEngine.match() - Serializzazione risultati JSON
- Debug store snapshot
- Playground web UI funzionante
Settimana 8-9: Setup Fase Beta
- Fork
meiqua/shape_based_matchingsu 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
ShapeModel2DMatcherper 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_matchingpubblicato 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-matchdaxiaHuang/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:
- Click su "Crea nuova ricetta"
- Selezione tipo: "Shape Model 2D"
- Upload asset:
- Opzione scelta: caricamento DXF
flangia_80mm.dxf - Sistema rileva unità: mm (confermato)
- Preview rasterizzazione mostrata con edge in verde
- Opzione scelta: caricamento DXF
- Configurazione layer DXF:
- Layer
CONTORNOselezionato (contiene il profilo esterno) - Layer
QUOTE,TESTI,CENTRINIdeselezionati
- Layer
- Parametri matching:
- Range angolare: 0-360° (default)
- Scale: fissa a 1.0 (camera fissa calibrata)
- Pyramid levels: 4 (default)
- 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°
- Compilazione metadata:
- recipe_id:
flangia_80mm_v1 - Display name:
Flangia 80mm Cliente Acme - Tags:
cliente_acme,linea_produzione_3
- recipe_id:
- Click "Salva". Ricetta disponibile in lista.
A.2 Test in Playground
Operatore va su pagina "Playground":
- Selezione ricetta:
flangia_80mm_v1 - Upload immagine di test:
sample_production_scene.png - Parametri runtime:
- Min score: 0.7
- Max matches: 10
- 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:
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
- Creazione ricetta da DXF CAD del pezzo
- Analisi distintività deve dare score >80%
- Run batch su 100 immagini
- Calcolo metriche aggregate
- Review manuale di tutti i match con score <0.8
- Iterazione parametri se target non raggiunti
- Test di ripetibilità: stesso stream 10 volte, verifica deviation standard pose <0.1 px
Test passato → ricetta approvata per produzione.