diff --git a/Mapping/point_cloud_sampling/point_cloud_sampling.py b/Mapping/point_cloud_sampling/point_cloud_sampling.py index df7cde41c0..e126346809 100644 --- a/Mapping/point_cloud_sampling/point_cloud_sampling.py +++ b/Mapping/point_cloud_sampling/point_cloud_sampling.py @@ -5,6 +5,8 @@ - Poisson disk sampling """ +import operator + import matplotlib.pyplot as plt import numpy as np import numpy.typing as npt @@ -48,16 +50,28 @@ def farthest_point_sampling(orig_points: npt.NDArray, ---------- orig_points : (M, N) N-dimensional points for sampling. The number of points is M. - n_points : number of points for sampling + n_points : integer number of points for sampling, from 0 to M inclusive. seed : random seed number Returns ------- - sampled points (n_points, N) + sampled points (n_points, N), with the input dtype. + + Raises + ------ + TypeError + If n_points is not an integer. + ValueError + If n_points is negative or exceeds the number of input rows. """ - rng = np.random.default_rng(seed) + n_points = operator.index(n_points) n_orig_points = orig_points.shape[0] + if not 0 <= n_points <= n_orig_points: + raise ValueError("n_points must be between 0 and the number of input rows") + if n_points == 0: + return orig_points[:0].copy() + rng = np.random.default_rng(seed) first_point_id = rng.choice(range(n_orig_points)) min_distances = np.ones(n_orig_points) * float("inf") selected_ids = [first_point_id] diff --git a/docs/modules/3_mapping/point_cloud_sampling/point_cloud_sampling_main.rst b/docs/modules/3_mapping/point_cloud_sampling/point_cloud_sampling_main.rst index 8cb08d4b78..f15df69519 100644 --- a/docs/modules/3_mapping/point_cloud_sampling/point_cloud_sampling_main.rst +++ b/docs/modules/3_mapping/point_cloud_sampling/point_cloud_sampling_main.rst @@ -44,6 +44,12 @@ possible. This method is useful for machine learning and other situations where you want to obtain a specified number of points from point cloud. +The requested count must be an integer between zero and the number of input +rows, inclusive. Each input row is selected at most once, even when multiple +rows have identical coordinates. A zero count returns an empty array with the +same coordinate dimension and dtype as the input. Negative or excessive counts +raise ``ValueError`` instead of starting a sampling loop that cannot finish. + API ===== diff --git a/tests/test_point_cloud_sampling.py b/tests/test_point_cloud_sampling.py index 8f6447c69c..0a9c87925d 100644 --- a/tests/test_point_cloud_sampling.py +++ b/tests/test_point_cloud_sampling.py @@ -1,3 +1,6 @@ +import numpy as np +import pytest + import conftest # Add root path to sys.path from Mapping.point_cloud_sampling import point_cloud_sampling as m @@ -11,5 +14,47 @@ def test_1(capsys): assert "poisson_disk_points.shape=(20, 3)" in captured.out +@pytest.mark.parametrize("points", [np.empty((0, 3)), np.ones((4, 2), dtype=np.int32)]) +def test_farthest_sampling_zero_count(points): + sampled = m.farthest_point_sampling(points, 0, seed=0) + assert sampled.shape == (0, points.shape[1]) + assert sampled.dtype == points.dtype + + +@pytest.mark.parametrize("count", [-1, 3]) +def test_farthest_sampling_rejects_unavailable_count(count): + with pytest.raises(ValueError, match="between 0 and"): + m.farthest_point_sampling(np.zeros((2, 3)), count, seed=0) + + +def test_farthest_sampling_rejects_nonempty_sample_from_empty_cloud(): + with pytest.raises(ValueError, match="between 0 and"): + m.farthest_point_sampling(np.empty((0, 3)), 1, seed=0) + + +@pytest.mark.parametrize("count", [1.5, 2.0, "2", None]) +def test_farthest_sampling_requires_integer_count(count): + with pytest.raises(TypeError): + m.farthest_point_sampling(np.zeros((3, 2)), count, seed=0) + + +@pytest.mark.parametrize("seed, expected", [ + (0, [[16, 0], [0, 0], [9, 0], [4, 0]]), + (42, [[0, 0], [16, 0], [9, 0], [4, 0]]), +]) +def test_farthest_sampling_preserves_seeded_order(seed, expected): + points = np.array([[0, 0], [1, 0], [4, 0], [9, 0], [16, 0]]) + np.testing.assert_array_equal( + m.farthest_point_sampling(points, np.int64(4), seed), expected) + + +def test_farthest_sampling_duplicate_points_use_distinct_rows(): + points = np.array([[0, 0], [0, 0], [1, 0], [1, 0]]) + original = points.copy() + sampled = m.farthest_point_sampling(points, len(points), seed=0) + np.testing.assert_array_equal(np.sort(sampled, axis=0), points) + np.testing.assert_array_equal(points, original) + + if __name__ == '__main__': conftest.run_this_test(__file__)