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:
@@ -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"]
|
||||
Reference in New Issue
Block a user