Files
Presa-Mano/tests/test_stl_sampler.py
2026-05-13 10:01:25 +02:00

89 lines
3.3 KiB
Python

"""Unit tests for stl_sampler."""
from __future__ import annotations
import io
import struct
import numpy as np
import pytest
from presa_mano.perception.stl_sampler import parse_stl_bytes, sample_surface
def _make_cube_stl_bytes(side_mm: float = 10.0) -> bytes:
"""Synthesize a binary STL of an axis-aligned cube centered at origin,
from -side/2 to +side/2 on each axis. 12 triangles, no normals needed."""
s = side_mm / 2.0
v = [
(-s, -s, -s), ( s, -s, -s), ( s, s, -s), (-s, s, -s),
(-s, -s, s), ( s, -s, s), ( s, s, s), (-s, s, s),
]
# 6 faces x 2 tris (vertex order: any orientation works for our use)
tris = [
(0, 1, 2), (0, 2, 3), # z=-s
(4, 6, 5), (4, 7, 6), # z=+s
(0, 4, 5), (0, 5, 1), # y=-s
(3, 2, 6), (3, 6, 7), # y=+s
(0, 3, 7), (0, 7, 4), # x=-s
(1, 5, 6), (1, 6, 2), # x=+s
]
buf = io.BytesIO()
buf.write(b"\x00" * 80) # header
buf.write(struct.pack("<I", len(tris))) # n_triangles
for (i, j, k) in tris:
buf.write(struct.pack("<fff", 0.0, 0.0, 0.0)) # normal (unused)
for idx in (i, j, k):
buf.write(struct.pack("<fff", *v[idx]))
buf.write(struct.pack("<H", 0)) # attribute byte count
return buf.getvalue()
def test_parse_stl_cube_bbox():
stl_bytes = _make_cube_stl_bytes(side_mm=10.0)
verts, tris = parse_stl_bytes(stl_bytes)
assert verts.ndim == 2 and verts.shape[1] == 3
assert tris.ndim == 2 and tris.shape[1] == 3
assert tris.dtype.kind == "i"
# BB del cubo: [-5, +5] su ogni asse
np.testing.assert_allclose(verts.min(axis=0), [-5.0, -5.0, -5.0], atol=1e-6)
np.testing.assert_allclose(verts.max(axis=0), [ 5.0, 5.0, 5.0], atol=1e-6)
def test_sample_surface_in_bbox():
stl_bytes = _make_cube_stl_bytes(side_mm=10.0)
verts, tris = parse_stl_bytes(stl_bytes)
pts = sample_surface(verts, tris, n=1000, seed=0)
assert pts.shape == (1000, 3)
# tutti i punti devono cadere dentro la BB del cubo (più epsilon
# per il rumore di floating point bary)
np.testing.assert_array_less(pts.max(axis=0), 5.0 + 1e-4)
np.testing.assert_array_less(-pts.min(axis=0), 5.0 + 1e-4)
def test_sample_surface_distribution_uniform():
"""8 octanti del cubo, ognuno >= 8% dei punti (perfetto = 100/6 ~= 16.7%
per faccia, distribuita su 4 octanti adiacenti)."""
stl_bytes = _make_cube_stl_bytes(side_mm=10.0)
verts, tris = parse_stl_bytes(stl_bytes)
pts = sample_surface(verts, tris, n=2000, seed=1)
counts = np.zeros(8, dtype=np.int64)
for p in pts:
# bin (x>0, y>0, z>0) -> 0..7
idx = (int(p[0] > 0) << 2) | (int(p[1] > 0) << 1) | int(p[2] > 0)
counts[idx] += 1
frac = counts / pts.shape[0]
assert (frac > 0.08).all(), f"octant fractions too uneven: {frac}"
def test_sample_surface_seed_deterministic():
stl_bytes = _make_cube_stl_bytes(side_mm=10.0)
verts, tris = parse_stl_bytes(stl_bytes)
a = sample_surface(verts, tris, n=100, seed=42)
b = sample_surface(verts, tris, n=100, seed=42)
np.testing.assert_array_equal(a, b)
def test_parse_stl_bad_payload_raises():
with pytest.raises(Exception): # numpy-stl raises various; we just want it to not silently pass
parse_stl_bytes(b"not a real STL")