split example visualizaiton pipeline
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import orisys
|
||||
from orisys.finger import TactileProcessor, load_finger_export_bundle, resolve_export_paths
|
||||
from orisys.finger.exports import DEFAULT_FINGER_MAP_CACHE
|
||||
from orisys.finger.runtime import resolve_finger_geometry
|
||||
|
||||
|
||||
def _touch_bundle(root: Path):
|
||||
for name in ("mesh.npz", "camera_intrinsics.npz", "world_to_cam.npy"):
|
||||
(root / name).write_bytes(b"")
|
||||
|
||||
|
||||
def test_resolve_export_paths_returns_expected_bundle(tmp_path):
|
||||
_touch_bundle(tmp_path)
|
||||
|
||||
bundle = resolve_export_paths(tmp_path)
|
||||
|
||||
assert bundle.export_dir == tmp_path.resolve()
|
||||
assert bundle.mesh.name == "mesh.npz"
|
||||
assert bundle.intrinsics.name == "camera_intrinsics.npz"
|
||||
assert bundle.pose.name == "world_to_cam.npy"
|
||||
|
||||
|
||||
def test_load_finger_export_bundle_rehomes_default_map_cache(tmp_path, monkeypatch):
|
||||
_touch_bundle(tmp_path)
|
||||
|
||||
import orisys.finger.exports as exports_mod
|
||||
|
||||
monkeypatch.setattr(
|
||||
exports_mod,
|
||||
"load_exported_data",
|
||||
lambda *args: {"K": np.eye(3), "width": 1280, "height": 720},
|
||||
)
|
||||
monkeypatch.setattr(exports_mod, "compute_uv_distortion_metrics", lambda data: {"ok": True})
|
||||
monkeypatch.setattr(exports_mod, "recommend_edge_expand", lambda _: 1.2)
|
||||
|
||||
bundle = load_finger_export_bundle(
|
||||
tmp_path,
|
||||
map_cache=DEFAULT_FINGER_MAP_CACHE,
|
||||
source_scale=1.0,
|
||||
)
|
||||
|
||||
assert Path(bundle.map_cache) == (tmp_path / "unroll_map_uv.npz").resolve()
|
||||
assert bundle.edge_expand == 1.2
|
||||
|
||||
|
||||
def test_resolve_finger_geometry_uses_config_defaults(tmp_path, monkeypatch):
|
||||
_touch_bundle(tmp_path)
|
||||
|
||||
import orisys.finger.exports as exports_mod
|
||||
import orisys.finger.runtime as runtime_mod
|
||||
|
||||
monkeypatch.setattr(
|
||||
runtime_mod,
|
||||
"load_config",
|
||||
lambda _: {"camera": {"resolution_stitch": [280, 300], "source_scale": 0.4}},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
exports_mod,
|
||||
"load_exported_data",
|
||||
lambda *args: {"K": np.eye(3), "width": 1280, "height": 720},
|
||||
)
|
||||
monkeypatch.setattr(exports_mod, "compute_uv_distortion_metrics", lambda data: {"ok": True})
|
||||
monkeypatch.setattr(exports_mod, "recommend_edge_expand", lambda _: 1.2)
|
||||
|
||||
geometry = resolve_finger_geometry(
|
||||
export_dir=tmp_path,
|
||||
config="dd02-ov",
|
||||
)
|
||||
|
||||
assert geometry.out_width == 280
|
||||
assert geometry.out_height == 300
|
||||
assert geometry.source_scale == 0.4
|
||||
assert geometry.source_width == 512
|
||||
assert geometry.source_height == 288
|
||||
assert Path(geometry.map_cache) == (tmp_path / "unroll_map_uv_s0.4.npz").resolve()
|
||||
|
||||
|
||||
def test_default_map_cache_rehomes_to_resolved_export_dir(tmp_path, monkeypatch):
|
||||
_touch_bundle(tmp_path)
|
||||
|
||||
import orisys.finger.exports as exports_mod
|
||||
import orisys.finger.runtime as runtime_mod
|
||||
|
||||
monkeypatch.setattr(
|
||||
runtime_mod,
|
||||
"load_config",
|
||||
lambda _: {"camera": {"resolution_stitch": [280, 300], "source_scale": 1.0}},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
exports_mod,
|
||||
"load_exported_data",
|
||||
lambda *args: {"K": np.eye(3), "width": 1280, "height": 720},
|
||||
)
|
||||
monkeypatch.setattr(exports_mod, "compute_uv_distortion_metrics", lambda data: {"ok": True})
|
||||
monkeypatch.setattr(exports_mod, "recommend_edge_expand", lambda _: 1.2)
|
||||
|
||||
geometry = resolve_finger_geometry(
|
||||
export_dir=tmp_path,
|
||||
config="dd02-ov",
|
||||
map_cache=DEFAULT_FINGER_MAP_CACHE,
|
||||
)
|
||||
|
||||
assert Path(geometry.map_cache) == (tmp_path / "unroll_map_uv.npz").resolve()
|
||||
|
||||
|
||||
def test_orisys_root_exports_finger_module():
|
||||
assert hasattr(orisys, "finger")
|
||||
|
||||
|
||||
def test_tactile_processor_push_frame_uses_default_valid_mask():
|
||||
class DummySensor:
|
||||
info = object()
|
||||
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
self.frame = None
|
||||
self.img = None
|
||||
self.flow = None
|
||||
self.vnormal = None
|
||||
self.vshear = None
|
||||
self.depth_map = None
|
||||
self.mask_stats = {}
|
||||
self.valid_mask = None
|
||||
|
||||
def get_img(self, **kwargs):
|
||||
self.calls.append(kwargs)
|
||||
self.frame = kwargs["frame"]
|
||||
self.valid_mask = kwargs["valid_mask"]
|
||||
return "ok"
|
||||
|
||||
sensor = DummySensor()
|
||||
default_mask = np.array([[True, False], [False, True]])
|
||||
tactile = TactileProcessor(sensor, default_valid_mask=default_mask)
|
||||
frame = np.zeros((2, 2), dtype=np.uint8)
|
||||
|
||||
result = tactile.push_frame(frame, already_gray=True)
|
||||
|
||||
assert result == "ok"
|
||||
assert sensor.calls[0]["frame"] is frame
|
||||
assert sensor.calls[0]["already_processed"] is True
|
||||
assert sensor.calls[0]["already_gray"] is True
|
||||
assert sensor.calls[0]["valid_mask"] is default_mask
|
||||
|
||||
|
||||
def test_finger_runtime_get_img_returns_none_on_capture_failure():
|
||||
from orisys.finger.runtime import FingerRuntime
|
||||
|
||||
runtime = object.__new__(FingerRuntime)
|
||||
runtime.grab_and_process = lambda *, color=False: (False, None)
|
||||
assert runtime.get_img() is None
|
||||
|
||||
|
||||
def test_finger_runtime_get_img_returns_unrolled_frame_on_success():
|
||||
from orisys.finger.runtime import FingerRuntime
|
||||
|
||||
frame = np.zeros((2, 2), dtype=np.uint8)
|
||||
runtime = object.__new__(FingerRuntime)
|
||||
runtime.grab_and_process = lambda *, color=False: (True, frame)
|
||||
assert runtime.get_img(color=True) is frame
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"video,expected",
|
||||
[
|
||||
("0", 0),
|
||||
(" 1 ", 1),
|
||||
(r".\data\dd02\0607_1.mp4" + "\\", r".\data\dd02\0607_1.mp4"),
|
||||
(0, 0),
|
||||
("./clip.mp4", "./clip.mp4"),
|
||||
],
|
||||
)
|
||||
def test_normalize_video_source(video, expected):
|
||||
from orisys.finger.runtime import normalize_video_source
|
||||
|
||||
assert normalize_video_source(video) == expected
|
||||
@@ -1,7 +1,9 @@
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from orisys.backend_availability import CUPY_AVAILABLE
|
||||
from orisys.hhd import PoissonSolver, nHHD
|
||||
|
||||
|
||||
@@ -124,6 +126,46 @@ def test_capfft_hhd_forces_track_sparse_reference():
|
||||
assert np.allclose(vshear[~mask], 0.0, atol=1e-5)
|
||||
|
||||
|
||||
def test_sparse_hhd_normal_sign_tracks_capfft_on_masked_uv_flow():
|
||||
shape = (36, 44)
|
||||
mask = _circle_mask(shape)
|
||||
flow = _radial_flow(shape, scale=0.03)
|
||||
flow[~mask] = 0.0
|
||||
|
||||
sparse_hhd = nHHD(
|
||||
cuda=False,
|
||||
grid=shape,
|
||||
spacings=(1.0, 1.0),
|
||||
poisson_backend="cpu",
|
||||
masked_poisson_solver="sparse",
|
||||
)
|
||||
capfft_hhd = nHHD(
|
||||
cuda=False,
|
||||
grid=shape,
|
||||
spacings=(1.0, 1.0),
|
||||
poisson_backend="cpu",
|
||||
masked_poisson_solver="capfft",
|
||||
masked_poisson_max_boundary=40,
|
||||
masked_poisson_max_iters=3,
|
||||
)
|
||||
|
||||
sparse_hhd.get_force(flow, valid_mask=mask)
|
||||
capfft_hhd.get_force(flow, valid_mask=mask)
|
||||
|
||||
sparse_vnormal = sparse_hhd.get_normal()
|
||||
capfft_vnormal = capfft_hhd.get_normal()
|
||||
|
||||
# The masked sparse solver should use the same sign convention as capfft/FFT.
|
||||
sparse_flat = sparse_vnormal[mask].reshape(-1, 2)
|
||||
capfft_flat = capfft_vnormal[mask].reshape(-1, 2)
|
||||
dot = float(np.sum(sparse_flat * capfft_flat))
|
||||
assert dot > 0.0
|
||||
|
||||
denom = max(1.0, float(np.linalg.norm(capfft_flat)))
|
||||
rel = float(np.linalg.norm(sparse_flat - capfft_flat) / denom)
|
||||
assert rel < 0.35
|
||||
|
||||
|
||||
def test_capfft_uv_mask_fixture_improves_over_raw_fft():
|
||||
uv_path = REPO_ROOT / "unroll_map_uv.npz"
|
||||
if not uv_path.exists():
|
||||
@@ -160,3 +202,91 @@ def test_capfft_uv_mask_fixture_improves_over_raw_fft():
|
||||
err_cap = float(np.linalg.norm((capfft - sparse)[mask]))
|
||||
err_raw = float(np.linalg.norm((raw_fft - sparse)[mask]))
|
||||
assert err_cap < err_raw
|
||||
|
||||
|
||||
@pytest.mark.skipif(not CUPY_AVAILABLE, reason="CuPy not available")
|
||||
def test_capfft_solver_cupy_backend_uses_gpu_bulk_solve_and_tracks_cpu():
|
||||
shape = (32, 40)
|
||||
mask = _circle_mask(shape)
|
||||
rhs = _smooth_rhs(shape)
|
||||
rhs_masked = np.where(mask, rhs, 0).astype(np.float32)
|
||||
|
||||
cpu_solver = PoissonSolver(
|
||||
grid=shape,
|
||||
spacings=(1.0, 1.0),
|
||||
poisson_backend="cpu",
|
||||
masked_poisson_solver="capfft",
|
||||
masked_poisson_max_boundary=40,
|
||||
masked_poisson_max_iters=3,
|
||||
)
|
||||
cupy_solver = PoissonSolver(
|
||||
grid=shape,
|
||||
spacings=(1.0, 1.0),
|
||||
poisson_backend="cupy",
|
||||
masked_poisson_solver="capfft",
|
||||
masked_poisson_max_boundary=40,
|
||||
masked_poisson_max_iters=3,
|
||||
)
|
||||
|
||||
cpu_capfft = cpu_solver.solve(rhs_masked, mask=mask)
|
||||
raw_fft = cpu_solver.solve(rhs_masked, mask=None)
|
||||
|
||||
original_gpu_solve = cupy_solver._rect_solver._solve_gpu_internal
|
||||
calls = {"count": 0}
|
||||
|
||||
def counted_gpu_solve(f_gpu):
|
||||
calls["count"] += 1
|
||||
return original_gpu_solve(f_gpu)
|
||||
|
||||
cupy_solver._rect_solver._solve_gpu_internal = counted_gpu_solve
|
||||
cupy_capfft = cupy_solver.solve(rhs_masked, mask=mask)
|
||||
|
||||
assert calls["count"] > 0
|
||||
assert np.allclose(cupy_capfft[~mask], 0.0, atol=1e-6)
|
||||
|
||||
err_gpu = float(np.linalg.norm((cupy_capfft - cpu_capfft)[mask]))
|
||||
err_raw = float(np.linalg.norm((raw_fft - cpu_capfft)[mask]))
|
||||
assert err_gpu < err_raw
|
||||
|
||||
denom = max(1.0, float(np.linalg.norm(cpu_capfft[mask])))
|
||||
rel = err_gpu / denom
|
||||
assert rel < 1e-2
|
||||
|
||||
|
||||
@pytest.mark.skipif(not CUPY_AVAILABLE, reason="CuPy not available")
|
||||
def test_capfft_hhd_cupy_backend_tracks_cpu_capfft_reference():
|
||||
shape = (36, 44)
|
||||
mask = _circle_mask(shape)
|
||||
flow = _radial_flow(shape, scale=0.03)
|
||||
flow[~mask] = 0.0
|
||||
|
||||
cpu_hhd = nHHD(
|
||||
cuda=False,
|
||||
grid=shape,
|
||||
spacings=(1.0, 1.0),
|
||||
poisson_backend="cpu",
|
||||
masked_poisson_solver="capfft",
|
||||
masked_poisson_max_boundary=40,
|
||||
masked_poisson_max_iters=3,
|
||||
)
|
||||
cupy_hhd = nHHD(
|
||||
cuda=False,
|
||||
grid=shape,
|
||||
spacings=(1.0, 1.0),
|
||||
poisson_backend="cupy",
|
||||
masked_poisson_solver="capfft",
|
||||
masked_poisson_max_boundary=40,
|
||||
masked_poisson_max_iters=3,
|
||||
)
|
||||
|
||||
_, cpu_forces = cpu_hhd.get_force(flow, valid_mask=mask)
|
||||
_, cupy_forces = cupy_hhd.get_force(flow, valid_mask=mask)
|
||||
|
||||
denom = max(1.0, float(np.linalg.norm(cpu_forces)))
|
||||
rel = float(np.linalg.norm(cupy_forces - cpu_forces) / denom)
|
||||
assert rel < 1e-2
|
||||
|
||||
vnormal = cupy_hhd.get_normal()
|
||||
vshear = cupy_hhd.get_shear()
|
||||
assert np.allclose(vnormal[~mask], 0.0, atol=1e-5)
|
||||
assert np.allclose(vshear[~mask], 0.0, atol=1e-5)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def packaging_utils():
|
||||
import sys
|
||||
|
||||
build_dir = REPO_ROOT / "build"
|
||||
if str(build_dir) not in sys.path:
|
||||
sys.path.insert(0, str(build_dir))
|
||||
from pyproject_utils import extras_require, install_requires, project_metadata
|
||||
|
||||
return install_requires, extras_require, project_metadata
|
||||
|
||||
|
||||
def test_pyproject_dependencies(packaging_utils):
|
||||
install_requires, extras_require, project_metadata = packaging_utils
|
||||
metadata = project_metadata(REPO_ROOT)
|
||||
|
||||
assert metadata["name"] == "orisys"
|
||||
assert "numpy>=" in install_requires(REPO_ROOT)[0]
|
||||
assert "cuda12" in extras_require(REPO_ROOT)
|
||||
assert extras_require(REPO_ROOT)["cuda"] == extras_require(REPO_ROOT)["cuda12"]
|
||||
|
||||
|
||||
def test_pyproject_dynamic_version(packaging_utils):
|
||||
import sys
|
||||
|
||||
build_dir = REPO_ROOT / "build"
|
||||
if str(build_dir) not in sys.path:
|
||||
sys.path.insert(0, str(build_dir))
|
||||
from pyproject_utils import get_version
|
||||
|
||||
assert get_version(REPO_ROOT) == "0.4.0"
|
||||
|
||||
|
||||
def test_pyproject_bundle_manifest(packaging_utils):
|
||||
import sys
|
||||
|
||||
build_dir = REPO_ROOT / "build"
|
||||
if str(build_dir) not in sys.path:
|
||||
sys.path.insert(0, str(build_dir))
|
||||
from pyproject_utils import bundle_data_files, bundle_items
|
||||
|
||||
items = bundle_items(REPO_ROOT)
|
||||
names = {item["name"] for item in items}
|
||||
assert names == {"configs", "examples", "docs", "assets"}
|
||||
|
||||
data_files = dict(bundle_data_files(REPO_ROOT))
|
||||
assert "orisys/configs" in data_files
|
||||
config_files = data_files["orisys/configs"]
|
||||
assert config_files
|
||||
assert all("/" in path and "*" not in path for path in config_files)
|
||||
assert any(path.endswith(".json") for path in config_files)
|
||||
|
||||
asset_dirs = [dest for dest in data_files if dest.startswith("orisys/assets/")]
|
||||
assert asset_dirs
|
||||
assert all("*" not in path for files in data_files.values() for path in files)
|
||||
assert "examples" not in data_files
|
||||
|
||||
|
||||
def test_bundle_explicit_files():
|
||||
import sys
|
||||
|
||||
build_dir = REPO_ROOT / "build"
|
||||
if str(build_dir) not in sys.path:
|
||||
sys.path.insert(0, str(build_dir))
|
||||
from wheel_layout import data_file_entries_for_item, resolve_bundle_source_files
|
||||
|
||||
item = {
|
||||
"name": "configs",
|
||||
"source": "config",
|
||||
"target": "configs",
|
||||
"files": ["dd02-ov.json"],
|
||||
}
|
||||
paths = resolve_bundle_source_files(REPO_ROOT, item)
|
||||
assert [p.name for p in paths] == ["dd02-ov.json"]
|
||||
assert data_file_entries_for_item(item) == ["config/dd02-ov.json"]
|
||||
@@ -0,0 +1,34 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from orisys.unroll.geometry import (
|
||||
resolve_uv_runtime_sizes,
|
||||
scale_camera_geometry,
|
||||
scaled_map_cache_path,
|
||||
)
|
||||
|
||||
|
||||
def test_scale_camera_geometry_halves_intrinsics_and_size():
|
||||
k = np.array([[1000.0, 0.0, 640.0], [0.0, 1000.0, 360.0], [0.0, 0.0, 1.0]])
|
||||
k_scaled, w, h = scale_camera_geometry(k, 1280, 720, 0.5)
|
||||
assert w == 640
|
||||
assert h == 360
|
||||
assert k_scaled[0, 0] == pytest.approx(500.0)
|
||||
assert k_scaled[1, 1] == pytest.approx(500.0)
|
||||
assert k_scaled[0, 2] == pytest.approx(320.0)
|
||||
assert k_scaled[1, 2] == pytest.approx(180.0)
|
||||
|
||||
|
||||
def test_resolve_uv_runtime_sizes_from_work4():
|
||||
out_w, out_h, scale = resolve_uv_runtime_sizes(
|
||||
{
|
||||
"resolution_stitch": [280, 300],
|
||||
"work4": [280, 300, 4, 0.4],
|
||||
}
|
||||
)
|
||||
assert (out_w, out_h, scale) == (280, 300, 0.4)
|
||||
|
||||
|
||||
def test_scaled_map_cache_path_suffix():
|
||||
assert scaled_map_cache_path("unroll_map_uv.npz", 0.4) == "unroll_map_uv_s0.4.npz"
|
||||
assert scaled_map_cache_path("unroll_map_uv.npz", 1.0) == "unroll_map_uv.npz"
|
||||
@@ -0,0 +1,80 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from orisys.finger.mesh import (
|
||||
build_uv_surface_mesh,
|
||||
compute_uv_surface_frames,
|
||||
flow_magnitude_vertex_colors,
|
||||
lift_uv_flow_to_3d,
|
||||
mesh_uv_to_world_scale,
|
||||
scalar_map_vertex_colors,
|
||||
)
|
||||
|
||||
|
||||
def test_build_uv_surface_mesh_from_grid():
|
||||
h, w = 4, 5
|
||||
yy, xx = np.mgrid[0:h, 0:w]
|
||||
world_xyz = np.stack([xx, yy, np.zeros((h, w))], axis=2).astype(np.float64)
|
||||
valid_mask = np.ones((h, w), dtype=bool)
|
||||
valid_mask[0, 0] = False
|
||||
|
||||
mesh = build_uv_surface_mesh(world_xyz, valid_mask)
|
||||
assert mesh.rest_vertices.shape[0] == int(valid_mask.sum())
|
||||
assert mesh.triangles.shape[1] == 3
|
||||
assert len(mesh.triangles) > 0
|
||||
assert mesh.base_colors.shape == (mesh.rest_vertices.shape[0], 3)
|
||||
assert np.allclose(mesh.base_colors[0], mesh.base_colors[-1])
|
||||
|
||||
|
||||
def test_lift_uv_flow_to_3d_tangent_only():
|
||||
flow = np.zeros((3, 3, 2), dtype=np.float64)
|
||||
flow[1, 1] = (1.0, 0.0)
|
||||
world_xyz = np.zeros((3, 3, 3), dtype=np.float64)
|
||||
world_xyz[:, :, 0] = np.arange(3)[None, :]
|
||||
world_xyz[:, :, 1] = np.arange(3)[:, None]
|
||||
valid = np.ones((3, 3), dtype=bool)
|
||||
tx, ty, normals = compute_uv_surface_frames(world_xyz, valid)
|
||||
lifted = lift_uv_flow_to_3d(flow, tx, ty, normals)
|
||||
assert lifted.shape == (3, 3, 3)
|
||||
assert np.linalg.norm(lifted[1, 1]) > 0.0
|
||||
|
||||
|
||||
def test_flow_magnitude_vertex_colors_respects_threshold():
|
||||
h, w = 3, 3
|
||||
world_xyz = np.zeros((h, w, 3), dtype=np.float64)
|
||||
valid_mask = np.ones((h, w), dtype=bool)
|
||||
mesh = build_uv_surface_mesh(world_xyz, valid_mask)
|
||||
|
||||
flow = np.zeros((h, w, 2), dtype=np.float64)
|
||||
flow[1, 1] = (0.1, 0.0) # mag = 2 * 0.1 = 0.2
|
||||
flow[2, 2] = (1.0, 0.0) # mag = 2.0
|
||||
|
||||
colors = flow_magnitude_vertex_colors(flow, mesh, threshold=0.4)
|
||||
low_idx = np.where((mesh.vertex_ys == 1) & (mesh.vertex_xs == 1))[0][0]
|
||||
high_idx = np.where((mesh.vertex_ys == 2) & (mesh.vertex_xs == 2))[0][0]
|
||||
assert np.allclose(colors[low_idx], mesh.base_colors[low_idx])
|
||||
assert not np.allclose(colors[high_idx], mesh.base_colors[high_idx])
|
||||
|
||||
|
||||
def test_scalar_map_vertex_colors_respects_threshold():
|
||||
h, w = 3, 3
|
||||
world_xyz = np.zeros((h, w, 3), dtype=np.float64)
|
||||
valid_mask = np.ones((h, w), dtype=bool)
|
||||
mesh = build_uv_surface_mesh(world_xyz, valid_mask)
|
||||
|
||||
depth_map = np.zeros((h, w), dtype=np.float64)
|
||||
depth_map[1, 1] = 0.05
|
||||
depth_map[2, 2] = 1.0
|
||||
|
||||
colors = scalar_map_vertex_colors(depth_map, mesh, threshold=0.1)
|
||||
low_idx = np.where((mesh.vertex_ys == 1) & (mesh.vertex_xs == 1))[0][0]
|
||||
high_idx = np.where((mesh.vertex_ys == 2) & (mesh.vertex_xs == 2))[0][0]
|
||||
assert np.allclose(colors[low_idx], mesh.base_colors[low_idx])
|
||||
assert not np.allclose(colors[high_idx], mesh.base_colors[high_idx])
|
||||
|
||||
|
||||
def test_mesh_uv_to_world_scale_positive():
|
||||
world_xyz = np.random.randn(8, 8, 3).astype(np.float64)
|
||||
valid = np.ones((8, 8), dtype=bool)
|
||||
scale = mesh_uv_to_world_scale(world_xyz, valid)
|
||||
assert scale > 0.0
|
||||
Reference in New Issue
Block a user