split example visualizaiton pipeline

This commit is contained in:
ykcheungab
2026-06-18 13:47:16 +08:00
parent fbee1c37ed
commit f19f092b64
42 changed files with 3067 additions and 702 deletions
+180
View File
@@ -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
+130
View File
@@ -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)
+82
View File
@@ -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"]
+34
View File
@@ -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"
+80
View File
@@ -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