From 0e04693812fda3fad15aa74b30e8f297e9acae3b Mon Sep 17 00:00:00 2001 From: Aglargil <1252223935@qq.com> Date: Fri, 24 Jan 2025 15:08:33 +0800 Subject: [PATCH 1/5] feat: add DistanceMap --- PathPlanning/DistanceMap/distance_map.py | 151 +++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 PathPlanning/DistanceMap/distance_map.py diff --git a/PathPlanning/DistanceMap/distance_map.py b/PathPlanning/DistanceMap/distance_map.py new file mode 100644 index 0000000000..57b8a089ed --- /dev/null +++ b/PathPlanning/DistanceMap/distance_map.py @@ -0,0 +1,151 @@ +""" +Distance Map + +author: Wang Zheng (@Aglargil) + +Ref: + +- [Distance Map] +(https://cs.brown.edu/people/pfelzens/papers/dt-final.pdf) +""" + +import numpy as np +import matplotlib.pyplot as plt + +INF = 1e20 +ENABLE_PLOT = True + + +def compute_sdf(obstacles): + """ + Compute the signed distance field (SDF) from a boolean field. + + Parameters + ---------- + obstacles : array_like + A 2D boolean array where '1' represents obstacles and '0' represents free space. + + Returns + ------- + array_like + A 2D array representing the signed distance field, where positive values indicate distance + to the nearest obstacle, and negative values indicate distance to the nearest free space. + """ + a = compute_udf(obstacles) + b = compute_udf(obstacles == 0) + return a - b + + +def compute_udf(obstacles): + """ + Compute the unsigned distance field (UDF) from a boolean field. + + Parameters + ---------- + obstacles : array_like + A 2D boolean array where '1' represents obstacles and '0' represents free space. + + Returns + ------- + array_like + A 2D array of distances from the nearest obstacle, with the same dimensions as `bool_field`. + """ + edt = obstacles.copy() + if not np.all(np.isin(edt, [0, 1])): + raise ValueError("Input array should only contain 0 and 1") + edt = np.where(edt == 0, INF, edt) + edt = np.where(edt == 1, 0, edt) + for row in range(len(edt)): + dt(edt[row]) + edt = edt.T + for row in range(len(edt)): + dt(edt[row]) + edt = edt.T + return np.sqrt(edt) + + +def dt(d): + """ + Compute 1D distance transform under the squared Euclidean distance + + Parameters + ---------- + d : array_like + Input array containing the distances. + + Returns: + -------- + d : array_like + The transformed array with computed distances. + """ + v = np.zeros(len(d) + 1) + z = np.zeros(len(d) + 1) + k = 0 + v[0] = 0 + z[0] = -INF + z[1] = INF + for q in range(1, len(d)): + s = ((d[q] + q * q) - (d[int(v[k])] + v[k] * v[k])) / (2 * q - 2 * v[k]) + while s <= z[k]: + k = k - 1 + s = ((d[q] + q * q) - (d[int(v[k])] + v[k] * v[k])) / (2 * q - 2 * v[k]) + k = k + 1 + v[k] = q + z[k] = s + z[k + 1] = INF + k = 0 + for q in range(len(d)): + while z[k + 1] < q: + k = k + 1 + dx = q - v[k] + d[q] = dx * dx + d[int(v[k])] + + +def main(): + obstacles = np.array( + [ + [1, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 1, 1, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ] + ) + + # Compute the signed distance field + sdf = compute_sdf(obstacles) + udf = compute_udf(obstacles) + + if ENABLE_PLOT: + fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 5)) + + obstacles_plot = ax1.imshow(obstacles, cmap="binary") + ax1.set_title("Obstacles") + ax1.set_xlabel("x") + ax1.set_ylabel("y") + plt.colorbar(obstacles_plot, ax=ax1) + + udf_plot = ax2.imshow(udf, cmap="viridis") + ax2.set_title("Unsigned Distance Field") + ax2.set_xlabel("x") + ax2.set_ylabel("y") + plt.colorbar(udf_plot, ax=ax2) + + sdf_plot = ax3.imshow(sdf, cmap="RdBu") + ax3.set_title("Signed Distance Field") + ax3.set_xlabel("x") + ax3.set_ylabel("y") + plt.colorbar(sdf_plot, ax=ax3) + + plt.tight_layout() + plt.show() + + +if __name__ == "__main__": + main() From f057b9a944bece069d16ed5df9944cfb0daabec8 Mon Sep 17 00:00:00 2001 From: Aglargil <1252223935@qq.com> Date: Wed, 5 Feb 2025 10:42:39 +0800 Subject: [PATCH 2/5] feat: add DistanceMap test --- tests/test_distance_map.py | 118 +++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 tests/test_distance_map.py diff --git a/tests/test_distance_map.py b/tests/test_distance_map.py new file mode 100644 index 0000000000..9106f5e2ec --- /dev/null +++ b/tests/test_distance_map.py @@ -0,0 +1,118 @@ +import conftest # noqa +import numpy as np +from PathPlanning.DistanceMap import distance_map as m + + +def test_compute_sdf(): + """Test the computation of Signed Distance Field (SDF)""" + # Create a simple obstacle map for testing + obstacles = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]]) + + sdf = m.compute_sdf(obstacles) + + # Verify basic properties of SDF + assert sdf.shape == obstacles.shape, "SDF should have the same shape as input map" + assert np.all(np.isfinite(sdf)), "SDF should not contain infinite values" + + # Verify SDF value is negative at obstacle position + assert sdf[1, 1] < 0, "SDF value should be negative at obstacle position" + + # Verify SDF value is positive in free space + assert sdf[0, 0] > 0, "SDF value should be positive in free space" + + +def test_compute_udf(): + """Test the computation of Unsigned Distance Field (UDF)""" + # Create obstacle map for testing + obstacles = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]]) + + udf = m.compute_udf(obstacles) + + # Verify basic properties of UDF + assert udf.shape == obstacles.shape, "UDF should have the same shape as input map" + assert np.all(np.isfinite(udf)), "UDF should not contain infinite values" + assert np.all(udf >= 0), "All UDF values should be non-negative" + + # Verify UDF value is 0 at obstacle position + assert np.abs(udf[1, 1]) < 1e-10, "UDF value should be 0 at obstacle position" + + # Verify UDF value is 1 for adjacent cells + assert np.abs(udf[0, 1] - 1.0) < 1e-10, ( + "UDF value should be 1 for cells adjacent to obstacle" + ) + assert np.abs(udf[1, 0] - 1.0) < 1e-10, ( + "UDF value should be 1 for cells adjacent to obstacle" + ) + assert np.abs(udf[1, 2] - 1.0) < 1e-10, ( + "UDF value should be 1 for cells adjacent to obstacle" + ) + assert np.abs(udf[2, 1] - 1.0) < 1e-10, ( + "UDF value should be 1 for cells adjacent to obstacle" + ) + + +def test_dt(): + """Test the computation of 1D distance transform""" + # Create test data + d = np.array([m.INF, 0, m.INF]) + m.dt(d) + + # Verify distance transform results + assert np.all(np.isfinite(d)), ( + "Distance transform result should not contain infinite values" + ) + assert d[1] == 0, "Distance at obstacle position should be 0" + assert d[0] == 1, "Distance at adjacent position should be 1" + assert d[2] == 1, "Distance at adjacent position should be 1" + + +def test_compute_sdf_empty(): + """Test SDF computation with empty map""" + # Test with empty map (no obstacles) + empty_map = np.zeros((5, 5)) + sdf = m.compute_sdf(empty_map) + + assert np.all(sdf > 0), "All SDF values should be positive for empty map" + assert sdf.shape == empty_map.shape, "Output shape should match input shape" + + +def test_compute_sdf_full(): + """Test SDF computation with fully occupied map""" + # Test with fully occupied map + full_map = np.ones((5, 5)) + sdf = m.compute_sdf(full_map) + + assert np.all(sdf < 0), "All SDF values should be negative for fully occupied map" + assert sdf.shape == full_map.shape, "Output shape should match input shape" + + +def test_compute_udf_invalid_input(): + """Test UDF computation with invalid input values""" + # Test with invalid values (not 0 or 1) + invalid_map = np.array([[0, 2, 0], [0, -1, 0], [0, 0.5, 0]]) + + try: + m.compute_udf(invalid_map) + assert False, "Should raise ValueError for invalid input values" + except ValueError: + pass + + +def test_compute_udf_empty(): + """Test UDF computation with empty map""" + # Test with empty map + empty_map = np.zeros((5, 5)) + udf = m.compute_udf(empty_map) + + assert np.all(udf > 0), "All UDF values should be positive for empty map" + assert np.all(np.isfinite(udf)), "UDF should not contain infinite values" + + +def test_main(): + """Test the execution of main function""" + m.ENABLE_PLOT = False + m.main() + + +if __name__ == "__main__": + conftest.run_this_test(__file__) From f2e377f213a31ccc51f156e77fdd7fbe5221520e Mon Sep 17 00:00:00 2001 From: Aglargil <1252223935@qq.com> Date: Wed, 5 Feb 2025 12:01:48 +0800 Subject: [PATCH 3/5] feat: add DistanceMap doc --- PathPlanning/DistanceMap/distance_map.py | 2 +- .../distance_map/distance_map.png | Bin 0 -> 32698 bytes .../distance_map/distance_map_main.rst | 27 ++++++++++++++++++ .../5_path_planning/path_planning_main.rst | 1 + 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 docs/modules/5_path_planning/distance_map/distance_map.png create mode 100644 docs/modules/5_path_planning/distance_map/distance_map_main.rst diff --git a/PathPlanning/DistanceMap/distance_map.py b/PathPlanning/DistanceMap/distance_map.py index 57b8a089ed..54c98c6a75 100644 --- a/PathPlanning/DistanceMap/distance_map.py +++ b/PathPlanning/DistanceMap/distance_map.py @@ -123,7 +123,7 @@ def main(): udf = compute_udf(obstacles) if ENABLE_PLOT: - fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 5)) + _, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 5)) obstacles_plot = ax1.imshow(obstacles, cmap="binary") ax1.set_title("Obstacles") diff --git a/docs/modules/5_path_planning/distance_map/distance_map.png b/docs/modules/5_path_planning/distance_map/distance_map.png new file mode 100644 index 0000000000000000000000000000000000000000..2d89252a70358ad1b60025d7bb3d9a217a2cefcc GIT binary patch literal 32698 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 literal 0 HcmV?d00001 diff --git a/docs/modules/5_path_planning/distance_map/distance_map_main.rst b/docs/modules/5_path_planning/distance_map/distance_map_main.rst new file mode 100644 index 0000000000..45273cd3bb --- /dev/null +++ b/docs/modules/5_path_planning/distance_map/distance_map_main.rst @@ -0,0 +1,27 @@ +Distance Map +------------ + +This is an implementation of the Distance Map algorithm for path planning. + +The Distance Map algorithm computes the unsigned distance field (UDF) and signed distance field (SDF) from a boolean field representing obstacles. + +The UDF gives the distance from each point to the nearest obstacle. The SDF gives positive distances for points outside obstacles and negative distances for points inside obstacles. + +Example +~~~~~~~ + +The algorithm is demonstrated on a simple 2D grid with obstacles: + +.. image:: distance_map.png + +API +~~~ + +.. autofunction:: PathPlanning.DistanceMap.distance_map.compute_sdf + +.. autofunction:: PathPlanning.DistanceMap.distance_map.compute_udf + +References +~~~~~~~~~~ + +- `Distance Transforms of Sampled Functions `_ paper by Pedro F. Felzenszwalb and Daniel P. Huttenlocher. \ No newline at end of file diff --git a/docs/modules/5_path_planning/path_planning_main.rst b/docs/modules/5_path_planning/path_planning_main.rst index 4960330b3e..e407787b6b 100644 --- a/docs/modules/5_path_planning/path_planning_main.rst +++ b/docs/modules/5_path_planning/path_planning_main.rst @@ -31,3 +31,4 @@ Path planning is the ability of a robot to search feasible and efficient path to hybridastar/hybridastar frenet_frame_path/frenet_frame_path coverage_path/coverage_path + distance_map/distance_map From 5178dd5404f80dabd569e9820d3b855c4a84e947 Mon Sep 17 00:00:00 2001 From: Aglargil <1252223935@qq.com> Date: Wed, 5 Feb 2025 20:13:23 +0800 Subject: [PATCH 4/5] feat: DistanceMap doc update --- .../distance_map/distance_map.png | Bin .../distance_map/distance_map_main.rst | 0 docs/modules/3_mapping/mapping_main.rst | 1 + docs/modules/5_path_planning/path_planning_main.rst | 1 - 4 files changed, 1 insertion(+), 1 deletion(-) rename docs/modules/{5_path_planning => 3_mapping}/distance_map/distance_map.png (100%) rename docs/modules/{5_path_planning => 3_mapping}/distance_map/distance_map_main.rst (100%) diff --git a/docs/modules/5_path_planning/distance_map/distance_map.png b/docs/modules/3_mapping/distance_map/distance_map.png similarity index 100% rename from docs/modules/5_path_planning/distance_map/distance_map.png rename to docs/modules/3_mapping/distance_map/distance_map.png diff --git a/docs/modules/5_path_planning/distance_map/distance_map_main.rst b/docs/modules/3_mapping/distance_map/distance_map_main.rst similarity index 100% rename from docs/modules/5_path_planning/distance_map/distance_map_main.rst rename to docs/modules/3_mapping/distance_map/distance_map_main.rst diff --git a/docs/modules/3_mapping/mapping_main.rst b/docs/modules/3_mapping/mapping_main.rst index 28e18984d3..825b08d3ec 100644 --- a/docs/modules/3_mapping/mapping_main.rst +++ b/docs/modules/3_mapping/mapping_main.rst @@ -17,3 +17,4 @@ Mapping is the ability of a robot to understand its surroundings with external s circle_fitting/circle_fitting rectangle_fitting/rectangle_fitting normal_vector_estimation/normal_vector_estimation + distance_map/distance_map diff --git a/docs/modules/5_path_planning/path_planning_main.rst b/docs/modules/5_path_planning/path_planning_main.rst index e407787b6b..4960330b3e 100644 --- a/docs/modules/5_path_planning/path_planning_main.rst +++ b/docs/modules/5_path_planning/path_planning_main.rst @@ -31,4 +31,3 @@ Path planning is the ability of a robot to search feasible and efficient path to hybridastar/hybridastar frenet_frame_path/frenet_frame_path coverage_path/coverage_path - distance_map/distance_map From e6f4d2473b35497d4e83a3be309aeea44b87e7e1 Mon Sep 17 00:00:00 2001 From: Aglargil <1252223935@qq.com> Date: Wed, 5 Feb 2025 20:33:34 +0800 Subject: [PATCH 5/5] feat: DistanceMap update --- {PathPlanning => Mapping}/DistanceMap/distance_map.py | 0 tests/test_distance_map.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {PathPlanning => Mapping}/DistanceMap/distance_map.py (100%) diff --git a/PathPlanning/DistanceMap/distance_map.py b/Mapping/DistanceMap/distance_map.py similarity index 100% rename from PathPlanning/DistanceMap/distance_map.py rename to Mapping/DistanceMap/distance_map.py diff --git a/tests/test_distance_map.py b/tests/test_distance_map.py index 9106f5e2ec..df6e394e2c 100644 --- a/tests/test_distance_map.py +++ b/tests/test_distance_map.py @@ -1,6 +1,6 @@ import conftest # noqa import numpy as np -from PathPlanning.DistanceMap import distance_map as m +from Mapping.DistanceMap import distance_map as m def test_compute_sdf():