- 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>
This commit is contained in:
+119
@@ -0,0 +1,119 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user