toaster / tests /test_io.py
SmaugC137's picture
Deploy Toaster demo to HF Space — procedural natural-terrain sample
b1390b1
Raw
History Blame Contribute Delete
5.52 kB
from __future__ import annotations
import numpy as np
import pytest
from toaster.io import load_cloud, supported_extensions
def test_supported_extensions_include_builtins():
exts = supported_extensions()
assert {".ply", ".bin", ".las", ".laz", ".pcd", ".npy"} <= set(exts)
def test_bin_roundtrip(tmp_path):
pts = np.array([[0, 0, 0, 0.1], [1, 2, 3, 0.5]], dtype=np.float32)
path = tmp_path / "scan.bin"
pts.tofile(path)
cloud = load_cloud(path)
assert cloud.n == 2
assert np.allclose(cloud.xyz, pts[:, :3])
assert np.allclose(cloud.features["intensity"], pts[:, 3])
assert cloud.source == path
def test_ply_roundtrip(tmp_path):
plyfile = pytest.importorskip("plyfile")
verts = np.array(
[(0.0, 0.0, 0.0, 0.2), (1.0, 1.0, 1.0, 0.8)],
dtype=[("x", "f4"), ("y", "f4"), ("z", "f4"), ("intensity", "f4")],
)
el = plyfile.PlyElement.describe(verts, "vertex")
path = tmp_path / "scan.ply"
plyfile.PlyData([el], text=True).write(str(path))
cloud = load_cloud(path)
assert cloud.n == 2
assert np.allclose(cloud.xyz[1], [1, 1, 1])
assert np.allclose(cloud.features["intensity"], [0.2, 0.8])
def test_pcd_ascii_roundtrip(tmp_path):
path = tmp_path / "scan.pcd"
path.write_text(
"# .PCD v0.7\n"
"VERSION 0.7\n"
"FIELDS x y z intensity\n"
"SIZE 4 4 4 4\n"
"TYPE F F F F\n"
"COUNT 1 1 1 1\n"
"WIDTH 3\nHEIGHT 1\n"
"VIEWPOINT 0 0 0 1 0 0 0\n"
"POINTS 3\nDATA ascii\n"
"0 0 0 0.1\n1 1 1 0.2\n2 2 2 0.3\n"
)
cloud = load_cloud(path)
assert cloud.n == 3
assert np.allclose(cloud.xyz[2], [2, 2, 2])
assert np.allclose(cloud.features["intensity"], [0.1, 0.2, 0.3])
def test_las_roundtrip(tmp_path):
laspy = pytest.importorskip("laspy")
las = laspy.create(point_format=3)
las.x = np.array([0.0, 1.0, 2.0])
las.y = np.array([0.0, 1.0, 2.0])
las.z = np.array([0.0, 0.5, 1.0])
las.intensity = np.array([10, 20, 30])
path = tmp_path / "scan.las"
las.write(str(path))
cloud = load_cloud(path)
assert cloud.n == 3
assert np.allclose(cloud.xyz[1], [1.0, 1.0, 0.5], atol=1e-3)
def test_npy_xyz(tmp_path):
pts = np.array([[0, 0, 0], [1, 2, 3]], dtype=np.float32)
path = tmp_path / "scan.npy"
np.save(path, pts)
cloud = load_cloud(path)
assert cloud.n == 2
assert np.allclose(cloud.xyz, pts)
assert cloud.features == {}
assert cloud.source == path
def test_npy_xyz_intensity(tmp_path):
pts = np.array([[0, 0, 0, 0.1], [1, 2, 3, 0.5]], dtype=np.float32)
path = tmp_path / "scan.npy"
np.save(path, pts)
cloud = load_cloud(path)
assert np.allclose(cloud.xyz, pts[:, :3])
assert np.allclose(cloud.features["intensity"], pts[:, 3])
def test_npy_rgb_0_255(tmp_path):
pts = np.array([[0, 0, 0, 255, 0, 0], [1, 1, 1, 0, 128, 255]], dtype=np.float32)
path = tmp_path / "scan.npy"
np.save(path, pts)
cloud = load_cloud(path)
rgb = cloud.features["rgb"]
assert rgb.dtype == np.uint8
assert rgb.tolist() == [[255, 0, 0], [0, 128, 255]]
def test_npy_rgb_0_1_scaled_to_uint8(tmp_path):
pts = np.array([[0, 0, 0, 1.0, 0.0, 0.0], [1, 1, 1, 0.0, 0.5, 1.0]], dtype=np.float32)
path = tmp_path / "scan.npy"
np.save(path, pts)
cloud = load_cloud(path)
rgb = cloud.features["rgb"]
assert rgb.dtype == np.uint8
assert rgb[0].tolist() == [255, 0, 0]
def test_npy_normals_detected_by_negative_values(tmp_path):
pts = np.array([[0, 0, 0, 0.0, 0.0, -1.0], [1, 1, 1, 1.0, 0.0, 0.0]], dtype=np.float32)
path = tmp_path / "scan.npy"
np.save(path, pts)
cloud = load_cloud(path)
assert "normals" in cloud.features
assert cloud.features["normals"].dtype == np.float32
def test_npy_ouster_9col_keeps_xyz_intensity(tmp_path):
# An Ouster scan dumps 9 fields: x, y, z, intensity, t, reflectivity, ring,
# ambient, range. Keep xyz + intensity (column 3); drop the sensor metadata.
pts = np.arange(2 * 9, dtype=np.float32).reshape(2, 9)
path = tmp_path / "scan.npy"
np.save(path, pts)
cloud = load_cloud(path)
assert np.allclose(cloud.xyz, pts[:, :3])
assert np.allclose(cloud.features["intensity"], pts[:, 3])
assert set(cloud.features) == {"intensity"} # trailing columns are dropped
def test_npy_drops_nonfinite_points(tmp_path):
# Ouster encodes "no return" as NaN xyz; such rows are dropped, and their
# feature values go with them so xyz and intensity stay aligned.
pts = np.array(
[[0, 0, 0, 0.1], [np.nan, np.nan, np.nan, 0.2], [1, 2, 3, 0.3]],
dtype=np.float32,
)
path = tmp_path / "scan.npy"
np.save(path, pts)
cloud = load_cloud(path)
assert cloud.n == 2
assert np.allclose(cloud.xyz, [[0, 0, 0], [1, 2, 3]])
assert np.allclose(cloud.features["intensity"], [0.1, 0.3])
def test_npy_bad_shape_raises(tmp_path):
path = tmp_path / "scan.npy"
np.save(path, np.zeros((4, 2), dtype=np.float32)) # fewer than 3 columns
with pytest.raises(ValueError):
load_cloud(path)
def test_npy_structured_array_raises(tmp_path):
path = tmp_path / "scan.npy"
np.save(path, np.zeros(3, dtype=[("x", "f4"), ("y", "f4"), ("z", "f4")]))
with pytest.raises(ValueError):
load_cloud(path)
def test_unknown_extension_raises(tmp_path):
path = tmp_path / "x.foo"
path.write_text("nope")
with pytest.raises(ValueError):
load_cloud(path)