| 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): |
| |
| |
| 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"} |
|
|
|
|
| def test_npy_drops_nonfinite_points(tmp_path): |
| |
| |
| 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)) |
| 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) |
|
|