e3114d6255
CI / test (push) Failing after 32s
- 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>
120 lines
4.2 KiB
Python
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
|