"""Rasterizzazione DXF → immagine template per il matcher shape-based. Il matcher lavora sui gradienti degli edge: un line-drawing pulito (sfondo grigio scuro, tratti chiari) è un template perfettamente valido. Questo modulo converte un file DXF (CAD 2D) in una bitmap grayscale centrata e scalata, pronta per train(). """ from __future__ import annotations import io import cv2 import numpy as np # Valori di rendering: sfondo scuro / tratto chiaro → gradiente netto BG_GRAY = 60 LINE_GRAY = 220 def _read_doc(data: bytes): """Parse DXF da bytes con gestione encoding. Prima prova ezdxf.read su StringIO (DXF ASCII utf-8 / cp1252), poi fallback su ezdxf.recover che auto-rileva encoding e tollera file malformati. """ import ezdxf from ezdxf import recover for enc in ("utf-8", "cp1252"): try: text = data.decode(enc) return ezdxf.read(io.StringIO(text)) except Exception: # UnicodeDecodeError, DXFStructureError e simili: prossimo tentativo continue # Ultimo tentativo: recover lavora direttamente sui bytes try: doc, _auditor = recover.read(io.BytesIO(data)) return doc except Exception as e: raise ValueError(f"DXF illeggibile o corrotto: {e}") from e def _extract_polylines(doc, flatten_dist: float = 0.05) -> tuple[list[np.ndarray], int]: """Converte le entità del modelspace in polilinee (liste di punti XY). Entità non convertibili (non supportate da make_path) vengono saltate silenziosamente ma conteggiate. Ritorna (polilinee, n_saltate). """ from ezdxf import path as ezpath polylines: list[np.ndarray] = [] skipped = 0 for entity in doc.modelspace(): try: p = ezpath.make_path(entity) pts = np.array( [(v.x, v.y) for v in p.flattening(distance=flatten_dist)], dtype=np.float64, ) if len(pts) >= 2: polylines.append(pts) except Exception: skipped += 1 return polylines, skipped def dxf_to_image(data: bytes, target_size: int = 512, line_thickness: int = 2, margin: int = 16) -> np.ndarray: """Rasterizza un DXF in immagine grayscale (H, W) uint8. - Scala uniforme: il lato lungo del disegno = target_size - 2*margin. - Disegno centrato, asse Y CAD (su) ribaltato in convenzione immagine. - Sfondo grigio scuro (60), tratti chiari (220), antialiased. Solleva ValueError se il DXF è vuoto o illeggibile. """ doc = _read_doc(data) # Distanza di flattening provvisoria in unità CAD: raffinata sotto # una volta nota la scala (qui serve solo per il bounding box). polylines, skipped = _extract_polylines(doc) if not polylines: raise ValueError( "DXF vuoto: nessuna entità convertibile in polilinea nel " f"modelspace ({skipped} entità non supportate saltate)") all_pts = np.vstack(polylines) min_xy = all_pts.min(axis=0) max_xy = all_pts.max(axis=0) extent = max_xy - min_xy long_side = float(extent.max()) if long_side <= 0: raise ValueError("DXF degenere: bounding box con estensione nulla") # Ri-flattening con distanza adattiva: ~0.25 px di errore alla scala # finale (il primo pass usava una tolleranza in unità CAD arbitraria). avail = max(1, target_size - 2 * margin) scale = avail / long_side polylines, _ = _extract_polylines(doc, flatten_dist=max(1e-9, 0.25 / scale)) canvas = np.full((target_size, target_size), BG_GRAY, dtype=np.uint8) # Offset per centrare il disegno (anche sul lato corto) draw_w = extent[0] * scale draw_h = extent[1] * scale off_x = (target_size - draw_w) / 2.0 off_y = (target_size - draw_h) / 2.0 for pts in polylines: px = (pts[:, 0] - min_xy[0]) * scale + off_x # Y CAD verso l'alto → Y immagine verso il basso py = (max_xy[1] - pts[:, 1]) * scale + off_y ipts = np.stack([px, py], axis=1).round().astype(np.int32) cv2.polylines(canvas, [ipts], isClosed=False, color=LINE_GRAY, thickness=line_thickness, lineType=cv2.LINE_AA) return canvas