DXF compare: ancore ICP sul solo pezzo staccato dal bordo, errore ICP con motivo (#86)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jbf7qKuxsnAVjxthRy7dnB
This commit is contained in:
@@ -17,6 +17,7 @@ import math
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import cv2 # #86: soglia/componenti connesse per la maschera del pezzo
|
||||
import numpy as np
|
||||
from visionsuite.core.image import Image
|
||||
|
||||
@@ -113,12 +114,41 @@ def _locate_reference(gray: np.ndarray, polarity: str) -> dict:
|
||||
return pezzo.found_poses[0].to_dict()
|
||||
|
||||
|
||||
def _candidate_anchors(gray: np.ndarray, dxf_path: Path, px_per_mm: float) -> list[tuple[float, float, float]]:
|
||||
def _maschera_pezzo(gray: np.ndarray, polarity: str, px_per_mm: float = 71.0) -> np.ndarray:
|
||||
"""Immagine con solo il pezzo: fuori dal blob staccato dal bordo e' sfondo (#86).
|
||||
|
||||
Otsu (INV se dark), componenti connesse, la piu' grande che NON tocca il
|
||||
bordo immagine (piastre del prisma, ombre e angoli neri della foto
|
||||
raddrizzata ci sono attaccati), dilatata di ~0,3 mm (minimo 15 px).
|
||||
"""
|
||||
flag = cv2.THRESH_BINARY_INV if polarity == "dark" else cv2.THRESH_BINARY
|
||||
_, binary = cv2.threshold(gray, 0, 255, flag | cv2.THRESH_OTSU)
|
||||
n, labels, stats, _ = cv2.connectedComponentsWithStats(binary, connectivity=8)
|
||||
h, w = gray.shape[:2]
|
||||
best, best_area = 0, 0
|
||||
for i in range(1, n): # 0 = sfondo
|
||||
x, y, cw, ch, area = (int(v) for v in stats[i])
|
||||
if x == 0 or y == 0 or x + cw >= w or y + ch >= h:
|
||||
continue # tocca il bordo immagine: non e' il pezzo
|
||||
if area > best_area:
|
||||
best, best_area = i, area
|
||||
if best == 0:
|
||||
raise CostruttoreError("pezzo non trovato: nessun blob staccato dal bordo")
|
||||
raggio = max(15, int(round(0.3 * px_per_mm))) # margine in funzione della scala
|
||||
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2 * raggio + 1, 2 * raggio + 1))
|
||||
zona = cv2.dilate((labels == best).astype(np.uint8), kernel) > 0
|
||||
sfondo = 255 if polarity == "dark" else 0
|
||||
return np.where(zona, gray, np.uint8(sfondo)).astype(np.uint8)
|
||||
|
||||
|
||||
def _candidate_anchors(gray: np.ndarray, dxf_path: Path, px_per_mm: float,
|
||||
polarity: str = "dark") -> list[tuple[float, float, float]]:
|
||||
"""Pose (riga, col, angolo) del disegno sull'immagine: ICP da 4 orientamenti.
|
||||
|
||||
Il disegno e' in px (origine DXF, y capovolta); si parte da ognuno dei 4
|
||||
angoli con i centroidi sovrapposti e l'ICP rifinisce. Ritorna le posa trovate
|
||||
(anche per orientamenti che non convergono: le giudica la ricetta eseguita).
|
||||
Centroide e ICP lavorano sull'immagine mascherata sul solo pezzo (#86).
|
||||
"""
|
||||
from visionsuite.calibration import ScaleCalibration
|
||||
from visionsuite.dxf.compare import DXFCompare, _stack
|
||||
@@ -126,6 +156,7 @@ def _candidate_anchors(gray: np.ndarray, dxf_path: Path, px_per_mm: float) -> li
|
||||
|
||||
cmp_ = DXFCompare(dxf_path, ScaleCalibration(px_per_mm=px_per_mm))
|
||||
drawing = _stack(cmp_.drawing_contours())
|
||||
gray = _maschera_pezzo(gray, polarity, px_per_mm) # #86: via piastre, ombre, angoli neri
|
||||
measured = _stack(cmp_.measured_contours(Image(gray)))
|
||||
if len(drawing) < 3:
|
||||
raise CostruttoreError("DXF senza geometria utilizzabile")
|
||||
@@ -134,18 +165,22 @@ def _candidate_anchors(gray: np.ndarray, dxf_path: Path, px_per_mm: float) -> li
|
||||
dc, mc = drawing.mean(axis=0), measured.mean(axis=0)
|
||||
|
||||
anchors = []
|
||||
errori = [] # #86: motivo di ogni orientamento caduto, per la diagnosi in campo
|
||||
for theta in _START_ANGLES:
|
||||
# traslazione che porta il centroide del disegno ruotato sul misurato
|
||||
rr, cc = Pose(0.0, 0.0, theta).apply(dc[0], dc[1])
|
||||
start = Pose(float(mc[0] - rr), float(mc[1] - cc), theta)
|
||||
try:
|
||||
result = cmp_.compare(Image(gray), initial_pose=start)
|
||||
except Exception: # noqa: BLE001 - un orientamento che cade non ferma gli altri
|
||||
except Exception as exc: # noqa: BLE001 - un orientamento che cade non ferma gli altri
|
||||
errori.append(f"{theta:g}gradi: {type(exc).__name__}: {exc}") # #86
|
||||
continue
|
||||
# transform: misurato -> disegno; la posa del disegno e' l'inversa
|
||||
m = np.linalg.inv(result.transform)
|
||||
angle = math.degrees(math.atan2(m[1, 0], m[0, 0]))
|
||||
anchors.append((float(m[0, 2]), float(m[1, 2]), angle))
|
||||
if not anchors and errori: # #86: nessuna ancora -> il motivo risale al chiamante
|
||||
raise CostruttoreError("disegno non sovrapponibile al pezzo (ICP fallito): " + "; ".join(errori))
|
||||
return anchors
|
||||
|
||||
|
||||
@@ -199,7 +234,7 @@ def build_dxf_compare(
|
||||
raise CostruttoreError("DXF illeggibile: nessuna entita' geometrica")
|
||||
|
||||
reference_pose = _locate_reference(frame, polarity)
|
||||
anchors = _candidate_anchors(frame, dxf_path, px_per_mm)
|
||||
anchors = _candidate_anchors(frame, dxf_path, px_per_mm, polarity) # #86
|
||||
if not anchors:
|
||||
raise CostruttoreError("disegno non sovrapponibile al pezzo (ICP fallito)")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user