fixed commetn from songxiao
This commit is contained in:
@@ -9,6 +9,7 @@ stage owes, and a synthetic one can only prove the code agrees with itself.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import struct
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
@@ -42,8 +43,11 @@ from orisys_app_shared.scene3d.glb import (
|
||||
from orisys_app_shared.scene3d.legacy import (
|
||||
LegacyFingerImporter,
|
||||
build_binding,
|
||||
crease_aware_mesh,
|
||||
estimate_meters_per_source_unit,
|
||||
load_legacy_domain,
|
||||
normals_disagree_with_faces,
|
||||
split_at_crease_edges,
|
||||
split_at_uv_seams,
|
||||
uv_correspondence_residual,
|
||||
validate_rigid_transform,
|
||||
@@ -72,9 +76,11 @@ def _identity() -> np.ndarray:
|
||||
|
||||
|
||||
def _import_run(**kwargs) -> SceneAssets:
|
||||
export_dir = kwargs.pop("export_dir", RUN_DIR)
|
||||
map_path = kwargs.pop("map_path", RUN_MAP)
|
||||
return LegacyFingerImporter().convert(
|
||||
RUN_DIR,
|
||||
RUN_MAP,
|
||||
export_dir,
|
||||
map_path,
|
||||
transform_sensor_from_source=kwargs.pop("transform", _identity()),
|
||||
meters_per_source_unit=kwargs.pop("units", 1e-3),
|
||||
**kwargs,
|
||||
@@ -97,6 +103,79 @@ def test_the_real_export_imports_and_validates():
|
||||
assert assets.source_vertex_of_render.shape == (1032,)
|
||||
|
||||
|
||||
def _write_housing(path: Path) -> Path:
|
||||
"""A tiny tetrahedron standing in for a mounting frame."""
|
||||
vertices = np.array(
|
||||
[[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]],
|
||||
dtype=np.float32,
|
||||
)
|
||||
faces = np.array([[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]], dtype=np.int32)
|
||||
np.savez(path, vertices=vertices, faces=faces)
|
||||
return path
|
||||
|
||||
|
||||
@requires_run
|
||||
def test_a_housing_becomes_its_own_part_without_touching_the_geometry_ids(tmp_path: Path):
|
||||
"""The frame is display-only: it must not enter the identities the device verifies."""
|
||||
plain = _import_run()
|
||||
housing_path = _write_housing(tmp_path / "housing.npz")
|
||||
with_housing = _import_run(housing_path=housing_path)
|
||||
|
||||
assert [part.part_id for part in plain.parts] == ["sensing_surface"]
|
||||
assert [(part.part_id, part.role) for part in with_housing.parts] == [
|
||||
("sensing_surface", "sensing"),
|
||||
("housing", "housing"),
|
||||
]
|
||||
assert with_housing.geometry_id == plain.geometry_id, (
|
||||
"adding a housing changed the sensing geometry id, which the device verifies"
|
||||
)
|
||||
assert with_housing.source_geometry_id == plain.source_geometry_id
|
||||
assert with_housing.asset_hash != plain.asset_hash, (
|
||||
"the package content changed, so the asset hash has to say so"
|
||||
)
|
||||
assert validate_assets(with_housing) == ()
|
||||
# Units and the rigid transform are applied to the housing exactly as to the surface.
|
||||
frame = with_housing.part("housing")
|
||||
assert frame is not None
|
||||
assert np.allclose(np.ptp(np.asarray(frame.positions_sensor_m), axis=0), 0.01, atol=1e-6)
|
||||
|
||||
|
||||
@requires_run
|
||||
def test_a_housing_in_the_export_directory_is_used_automatically(tmp_path: Path, monkeypatch):
|
||||
"""``<legacy-dir>/housing.npz`` is picked up, so a re-export needs no new flag."""
|
||||
export = tmp_path / "export"
|
||||
export.mkdir()
|
||||
shutil.copy(RUN_DIR / "mesh.npz", export / "mesh.npz")
|
||||
|
||||
assert len(_import_run(export_dir=export).parts) == 1
|
||||
_write_housing(export / "housing.npz")
|
||||
assert [part.role for part in _import_run(export_dir=export).parts] == [
|
||||
"sensing",
|
||||
"housing",
|
||||
]
|
||||
|
||||
|
||||
@requires_run
|
||||
def test_a_requested_housing_that_is_missing_is_refused(tmp_path: Path):
|
||||
"""A missing frame must not look like a package that simply has none."""
|
||||
with pytest.raises(ThreeDError) as caught:
|
||||
_import_run(housing_path=tmp_path / "not-there.npz")
|
||||
assert caught.value.diagnostic.code == "geometry_mismatch"
|
||||
|
||||
|
||||
@requires_run
|
||||
def test_a_housing_with_an_out_of_range_triangle_is_refused(tmp_path: Path):
|
||||
path = tmp_path / "housing.npz"
|
||||
np.savez(
|
||||
path,
|
||||
vertices=np.zeros((3, 3), dtype=np.float32),
|
||||
faces=np.array([[0, 1, 9]], dtype=np.int32),
|
||||
)
|
||||
with pytest.raises(ThreeDError) as caught:
|
||||
_import_run(housing_path=path)
|
||||
assert caught.value.diagnostic.code == "geometry_mismatch"
|
||||
|
||||
|
||||
@requires_run
|
||||
def test_the_uv_convention_is_measured_against_the_asset():
|
||||
"""The one fact the importer must not guess: which pixel a UV addresses."""
|
||||
@@ -235,6 +314,149 @@ def test_vertex_normals_are_unit_and_area_weighted():
|
||||
assert np.isfinite(lonely[4]).all()
|
||||
|
||||
|
||||
def _box(half: float = 1.0) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""A closed cube: every corner is a 90° crease, every edge a smooth pair."""
|
||||
positions = np.array(
|
||||
[
|
||||
[-half, -half, -half], [half, -half, -half], [half, half, -half],
|
||||
[-half, half, -half], [-half, -half, half], [half, -half, half],
|
||||
[half, half, half], [-half, half, half],
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
triangles = np.array(
|
||||
[
|
||||
[0, 2, 1], [0, 3, 2], # -z
|
||||
[4, 5, 6], [4, 6, 7], # +z
|
||||
[0, 1, 5], [0, 5, 4], # -y
|
||||
[2, 3, 7], [2, 7, 6], # +y
|
||||
[1, 2, 6], [1, 6, 5], # +x
|
||||
[3, 0, 4], [3, 4, 7], # -x
|
||||
],
|
||||
dtype=np.int64,
|
||||
)
|
||||
return positions, triangles
|
||||
|
||||
|
||||
def test_crease_aware_normals_agree_with_every_face_of_a_box():
|
||||
"""The defect this fixes: an averaged corner normal agrees with *no* face."""
|
||||
positions, triangles = _box()
|
||||
averaged = vertex_normals(positions, triangles)
|
||||
crease = crease_aware_mesh(positions, triangles)
|
||||
split_positions, split_triangles, split_normals = crease
|
||||
|
||||
assert len(split_triangles) == len(triangles), "splitting must not change the faces"
|
||||
assert len(split_positions) == 24, "each of the 8 corners needs 3 normals"
|
||||
|
||||
def worst(normals, faces, verts):
|
||||
a, b, c = verts[faces[:, 0]], verts[faces[:, 1]], verts[faces[:, 2]]
|
||||
face = np.cross(b - a, c - a)
|
||||
unit = face / np.linalg.norm(face, axis=1, keepdims=True)
|
||||
incident = [[] for _ in range(len(verts))]
|
||||
for index, triangle in enumerate(faces):
|
||||
for k in range(3):
|
||||
incident[int(triangle[k])].append(index)
|
||||
return max(
|
||||
float(np.degrees(np.arccos(np.clip(unit[incident[v]] @ normals[v], -1, 1))).min())
|
||||
for v in range(len(verts))
|
||||
if incident[v]
|
||||
)
|
||||
|
||||
assert worst(averaged, triangles, positions) > 30.0, (
|
||||
"the fixture must actually exhibit the defect"
|
||||
)
|
||||
assert worst(split_normals, split_triangles, split_positions) < 1e-6, (
|
||||
"a split corner normal must be exactly a face normal on a cube"
|
||||
)
|
||||
|
||||
|
||||
def test_crease_splitting_keeps_the_smooth_parts_welded():
|
||||
"""A cylinder's curved side must survive whole; only its rim split."""
|
||||
ring = 24
|
||||
angles = np.linspace(0.0, 2.0 * np.pi, ring, endpoint=False)
|
||||
positions = np.vstack(
|
||||
[
|
||||
np.column_stack([np.cos(angles), np.sin(angles), np.zeros(ring)]),
|
||||
np.column_stack([np.cos(angles), np.sin(angles), np.ones(ring)]),
|
||||
]
|
||||
)
|
||||
triangles = []
|
||||
for i in range(ring):
|
||||
j = (i + 1) % ring
|
||||
triangles.append([i, j, ring + i])
|
||||
triangles.append([j, ring + j, ring + i])
|
||||
triangles = np.asarray(triangles, dtype=np.int64)
|
||||
split_positions, split_triangles, _ = split_at_crease_edges(positions, triangles)
|
||||
# The side is a smooth ring (3.7° per step), so nothing on it may be duplicated;
|
||||
# the two rims are boundaries, so the split only separates the *caps'* edges.
|
||||
assert len(split_triangles) == len(triangles)
|
||||
assert len(split_positions) == len(positions), (
|
||||
"a smoothly curved side must stay welded"
|
||||
)
|
||||
|
||||
|
||||
def test_normals_disagree_with_faces_detects_the_defect():
|
||||
positions, triangles = _box()
|
||||
assert normals_disagree_with_faces(
|
||||
positions, triangles, vertex_normals(positions, triangles)
|
||||
)
|
||||
_, _, crease = crease_aware_mesh(positions, triangles)
|
||||
split_positions, split_triangles, _ = split_at_crease_edges(positions, triangles)
|
||||
assert not normals_disagree_with_faces(split_positions, split_triangles, crease)
|
||||
# A flat sheet has no crease at all.
|
||||
flat = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
|
||||
assert not normals_disagree_with_faces(
|
||||
flat, np.array([[0, 1, 2]]), np.array([[0.0, 0.0, 1.0]] * 3)
|
||||
)
|
||||
|
||||
|
||||
@requires_run
|
||||
def test_the_shipped_housing_has_no_stale_shading():
|
||||
"""The real housing: 134 of 2172 edges are creases, so plain averaging bands."""
|
||||
import io
|
||||
import zipfile
|
||||
|
||||
package = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "apps" / "studio" / "config" / "sensors" / "finger" / "scene"
|
||||
/ "finger-display-1.0.0.oryscene"
|
||||
)
|
||||
if not package.is_file():
|
||||
pytest.skip("the compiled finger package is not present")
|
||||
with zipfile.ZipFile(package) as archive:
|
||||
positions = np.load(io.BytesIO(archive.read("housing/positions.npy"))).astype(np.float64)
|
||||
triangles = np.load(io.BytesIO(archive.read("housing/triangles.npy"))).astype(np.int64)
|
||||
normals = np.load(io.BytesIO(archive.read("housing/normals.npy"))).astype(np.float64)
|
||||
# Every stored normal agrees with an incident face, which is only possible
|
||||
# because the source mesh's 726 vertices became 861 (one per side of a crease).
|
||||
assert not normals_disagree_with_faces(positions, triangles, normals), (
|
||||
"the shipped housing still carries normals averaged across its 90° corners"
|
||||
)
|
||||
# ...and the split is not a blanket unweld: the smooth vertices stay shared.
|
||||
unique_positions = len(np.unique(positions, axis=0))
|
||||
assert unique_positions == 726, (
|
||||
f"{unique_positions} unique positions: the smooth surface was unwelded too"
|
||||
)
|
||||
assert len(positions) == 861, (
|
||||
f"{len(positions)} vertices: the creases were not split into their own normals"
|
||||
)
|
||||
# The source mesh this was compiled from is the shape that used to be shipped,
|
||||
# and plain averaging on it is exactly the defect.
|
||||
source = Path(
|
||||
r"D:\orisys_ws\orisys-sensor-design\build\export\finger\r01\compute\housing.npz"
|
||||
)
|
||||
if source.is_file():
|
||||
with np.load(source, allow_pickle=False) as handle:
|
||||
source_positions = np.asarray(handle["vertices"], dtype=np.float64)
|
||||
source_triangles = np.asarray(handle["faces"], dtype=np.int64)
|
||||
assert len(source_positions) == 726, len(source_positions)
|
||||
assert normals_disagree_with_faces(
|
||||
source_positions,
|
||||
source_triangles,
|
||||
vertex_normals(source_positions, source_triangles),
|
||||
), "the detector does not fire on the geometry that actually banded"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- binding
|
||||
def _domain(shape_hw=(4, 5), flip_v=False, world=None):
|
||||
valid = np.ones(shape_hw, dtype=bool)
|
||||
|
||||
Reference in New Issue
Block a user