Files
Shape_Model_2D/pm2d/dxf.py
T
Adriano e3114d6255
CI / test (push) Failing after 32s
feat: input DXF + ROI poligonale + export JSON + CI Gitea
- pm2d/dxf.py: rasterizzazione DXF -> template (ezdxf, flattening
  entita', scala/centratura, render edge antialiased)
- POST /upload_dxf: carica CAD come modello (size 128..2048)
- roi_poly su /match, /match_simple e POST /recipes: train con mask
  cv2.fillPoly (validazione 400 su poligoni degeneri), cache key inclusa
- UI: upload .dxf, modalita' ROI poligonale su canvas (click=vertice,
  dblclick=chiudi, reset), bottone Esporta JSON dei risultati
- .gitea/workflows/ci.yml: uv sync + ruff + pytest su push/PR

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:30:46 +00:00

120 lines
4.2 KiB
Python

"""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