vision: il DXF di confronto unisce le entita' aperte in contorni chiusi (Refs #103)

chiudi_contorni unisce LINE/ARC aperti in LWPOLYLINE chiuse prima di
costruire la ricetta e per l'anteprima: con banda larga cut_open_ends
non lasciava piu' punti in banda.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 16:11:18 +01:00
parent 4791c34205
commit 77419f9ebd
3 changed files with 167 additions and 1 deletions
+3 -1
View File
@@ -18,7 +18,7 @@ import numpy as np
from fastapi import FastAPI, File, Form, HTTPException, UploadFile, status
from PIL import Image, UnidentifiedImageError
from src.vision.costruttori import CostruttoreError, build_dxf_compare # #83
from src.vision.costruttori import CostruttoreError, build_dxf_compare, chiudi_contorni # #83 #103
from src.vision.runner import (
GraphBuildError,
RunOutcome,
@@ -315,6 +315,8 @@ def _dxf_preview(dxf_bytes: bytes) -> dict:
if not dxf_bytes:
raise CostruttoreError("DXF illeggibile: file vuoto")
from visionsuite.io.dxf import read_xld_dxf
# #103: l'anteprima mostra cio' che il confronto usera' (contorni chiusi)
dxf_bytes, _ = chiudi_contorni(dxf_bytes)
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "disegno.dxf"
path.write_bytes(dxf_bytes)
@@ -0,0 +1,101 @@
"""#103: chiudi_contorni unisce LINE/ARC aperti in contorni chiusi (test sincroni, no pytest-asyncio)."""
import hashlib
import io
import tempfile
from pathlib import Path
import cv2
import ezdxf
import numpy as np
import pytest
from src.vision.costruttori import CostruttoreError, build_dxf_compare, chiudi_contorni
from src.vision_worker.main import _dxf_preview
def _dxf(riempi) -> bytes:
doc = ezdxf.new("R2010")
riempi(doc.modelspace())
buf = io.StringIO()
doc.write(buf)
return buf.getvalue().encode("utf-8")
def _quattro_line(msp, w=20.0, h=10.0):
pts = [(0, 0), (w, 0), (w, h), (0, h)]
for i in range(4):
msp.add_line(pts[i], pts[(i + 1) % 4])
def _entita(dxf: bytes):
with tempfile.TemporaryDirectory() as tmp: # readfile gestisce da solo codifica e CRLF
path = Path(tmp) / "o.dxf"
path.write_bytes(dxf)
return list(ezdxf.readfile(str(path)).modelspace())
def test_chiudi_rettangolo_di_4_line_in_una_polilinea_chiusa():
out, info = chiudi_contorni(_dxf(_quattro_line))
assert info == {"anelli": 1, "entita_unite": 4, "entita_libere": 0}
ents = _entita(out)
assert [e.dxftype() for e in ents] == ["LWPOLYLINE"] and ents[0].closed
xs = [p[0] for p in ents[0].get_points()]
ys = [p[1] for p in ents[0].get_points()]
assert (min(xs), max(xs), min(ys), max(ys)) == (0, 20, 0, 10) # bbox invariato
def test_chiudi_asola_line_e_arc_ha_bulge():
def asola(msp): # asola 20x10: due LINE orizzontali + due semicerchi r=5
msp.add_line((5, 0), (15, 0))
msp.add_arc((15, 5), 5, -90, 90)
msp.add_line((15, 10), (5, 10))
msp.add_arc((5, 5), 5, 90, 270)
out, info = chiudi_contorni(_dxf(asola))
assert info["anelli"] == 1 and info["entita_unite"] == 4
ents = _entita(out)
assert len(ents) == 1 and ents[0].closed
assert any(abs(p[4]) > 1e-6 for p in ents[0].get_points()) # gli archi diventano bulge
def test_dxf_gia_chiuso_resta_identico():
def chiuso(msp):
msp.add_lwpolyline([(0, 0), (20, 0), (20, 10), (0, 10)], close=True)
msp.add_circle((10, 5), 2)
dxf = _dxf(chiuso)
out, info = chiudi_contorni(dxf)
assert out == dxf and info["anelli"] == 0
def test_line_isolata_resta_e_byte_identici():
dxf = _dxf(lambda msp: msp.add_line((0, 0), (10, 0)))
out, info = chiudi_contorni(dxf)
assert out == dxf and info["anelli"] == 0 and info["entita_libere"] == 1
def test_byte_non_dxf_ritornati_identici():
brutto = b"questo non e' un dxf"
out, info = chiudi_contorni(brutto)
assert out == brutto and info["anelli"] == 0
def test_preview_di_quattro_line_e_una_sola_entita():
resp = _dxf_preview(_dxf(_quattro_line))
assert resp["entita"] == 1 and resp["larghezza_mm"] == 20 and resp["altezza_mm"] == 10
@pytest.mark.parametrize("band", [0.1, 0.3])
def test_chiudi_build_dxf_compare_banda_larga_con_4_line(band):
# rettangolo scuro 20x10 mm su bianco, 20 px/mm; disegno = 4 LINE aperte
ppm = 20.0
img = np.full((int(40 * ppm), int(60 * ppm)), 255, np.uint8)
cx, cy = img.shape[1] // 2, img.shape[0] // 2
cv2.rectangle(img, (cx - int(10 * ppm), cy - int(5 * ppm)),
(cx + int(10 * ppm), cy + int(5 * ppm)), 0, -1)
dxf = _dxf(_quattro_line)
ris = build_dxf_compare(img, dxf, px_per_mm=ppm, band_mm=band)
assert ris["outputs"]["dxf.points_in_pct"] > 90.0
# il disegno nella ricetta e' quello chiuso, non l'originale
import base64
salvato = base64.b64decode(ris["files"]["disegno.dxf"])
assert salvato != dxf and hashlib.sha256(salvato).hexdigest() == \
ris["graph"]["meta"]["dxf"]["disegno"]["sha256"]