Skip to content

Commit 9927da2

Browse files
committed
feat: ElasticBands update
1 parent 082bf53 commit 9927da2

File tree

2 files changed

+71
-9
lines changed

2 files changed

+71
-9
lines changed

Mapping/DistanceMap/distance_map.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,50 @@
1111

1212
import numpy as np
1313
import matplotlib.pyplot as plt
14+
import scipy
1415

1516
INF = 1e20
1617
ENABLE_PLOT = True
1718

1819

20+
def compute_sdf_scipy(obstacles):
21+
"""
22+
Compute the signed distance field (SDF) from a boolean field using scipy.
23+
24+
Parameters
25+
----------
26+
obstacles : array_like
27+
A 2D boolean array where '1' represents obstacles and '0' represents free space.
28+
29+
Returns
30+
-------
31+
array_like
32+
A 2D array representing the signed distance field, where positive values indicate distance
33+
to the nearest obstacle, and negative values indicate distance to the nearest free space.
34+
"""
35+
# distance_transform_edt use '0' as obstacles, so we need to convert the obstacles to '0'
36+
a = scipy.ndimage.distance_transform_edt(obstacles == 0)
37+
b = scipy.ndimage.distance_transform_edt(obstacles == 1)
38+
return a - b
39+
40+
41+
def compute_udf_scipy(obstacles):
42+
"""
43+
Compute the unsigned distance field (UDF) from a boolean field using scipy.
44+
45+
Parameters
46+
----------
47+
obstacles : array_like
48+
A 2D boolean array where '1' represents obstacles and '0' represents free space.
49+
50+
Returns
51+
-------
52+
array_like
53+
A 2D array of distances from the nearest obstacle, with the same dimensions as `bool_field`.
54+
"""
55+
return scipy.ndimage.distance_transform_edt(obstacles == 0)
56+
57+
1958
def compute_sdf(obstacles):
2059
"""
2160
Compute the signed distance field (SDF) from a boolean field.

PathPlanning/ElasticBands/elastic_bands.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
sys.path.append(str(pathlib.Path(__file__).parent.parent.parent))
1919

20-
from Mapping.DistanceMap.distance_map import compute_sdf
20+
from Mapping.DistanceMap.distance_map import compute_sdf_scipy
2121

2222
# Elastic Bands Params
2323
MAX_BUBBLE_RADIUS = 100
@@ -45,8 +45,13 @@ def __init__(self, position, radius):
4545

4646

4747
class ElasticBands:
48-
def __init__(self, initial_path, obstacles, rho0=RHO0, kc=0.05, kr=-0.1):
49-
self.distance_map = compute_sdf(obstacles)
48+
def __init__(self, initial_path, obstacles, rho0=RHO0, kc=KC, kr=KR):
49+
import time
50+
51+
t1 = time.time()
52+
self.distance_map = compute_sdf_scipy(obstacles)
53+
t2 = time.time()
54+
print(f"Time taken to compute distance map: {t2 - t1} seconds")
5055
self.bubbles = [
5156
Bubble(p, self.compute_rho(p)) for p in initial_path
5257
] # Initialize bubble chain
@@ -73,7 +78,7 @@ def contraction_force(self, i):
7378
dir_next = (next_ - current) / (np.linalg.norm(next_ - current) + 1e-6)
7479
return self.kc * (dir_prev + dir_next)
7580

76-
def external_force(self, i):
81+
def repulsive_force(self, i):
7782
"""Calculate external repulsive force for the i-th bubble"""
7883
h = STEP_SIZE # Step size
7984
b = self.bubbles[i].pos
@@ -99,7 +104,7 @@ def update_bubbles(self):
99104
new_bubbles.append(self.bubbles[i]) # Fixed start and end points
100105
continue
101106

102-
f_total = self.contraction_force(i) + self.external_force(i)
107+
f_total = self.contraction_force(i) + self.repulsive_force(i)
103108
alpha = self.bubbles[i].radius # Adaptive step size
104109
new_pos = self.bubbles[i].pos + alpha * f_total
105110
new_pos = np.clip(new_pos, 0, 499)
@@ -146,10 +151,11 @@ def __init__(self):
146151
self.obstacles = np.zeros((500, 500))
147152
self.path_points = []
148153
self.elastic_band = None
154+
self.running = True
149155

150156
if ENABLE_PLOT:
151157
self.fig, self.ax = plt.subplots(figsize=(8, 8))
152-
# Set the display range of the graph
158+
self.fig.canvas.mpl_connect("close_event", self.on_close)
153159
self.ax.set_xlim(0, 500)
154160
self.ax.set_ylim(0, 500)
155161

@@ -164,15 +170,32 @@ def __init__(self):
164170

165171
self.plot_background()
166172

173+
def on_close(self, event):
174+
"""Handle window close event"""
175+
self.running = False
176+
plt.close("all") # Close all figure windows
177+
167178
def plot_background(self):
168179
"""Plot the background grid"""
169-
if not ENABLE_PLOT:
180+
if not ENABLE_PLOT or not self.running:
170181
return
171182

172183
self.ax.cla()
173184
self.ax.set_xlim(0, 500)
174185
self.ax.set_ylim(0, 500)
175186
self.ax.grid(True)
187+
188+
if ENABLE_INTERACTIVE:
189+
self.ax.set_title(
190+
"Elastic Bands Path Planning\n"
191+
"Left click: Add obstacles\n"
192+
"Right click: Add path points\n"
193+
"Middle click: Start planning",
194+
pad=20,
195+
)
196+
else:
197+
self.ax.set_title("Elastic Bands Path Planning", pad=20)
198+
176199
if self.path_points:
177200
self.ax.plot(
178201
[p[0] for p in self.path_points],
@@ -240,6 +263,6 @@ def plan_path(self):
240263

241264

242265
if __name__ == "__main__":
243-
_ = ElasticBandsVisualizer()
266+
visualizer = ElasticBandsVisualizer()
244267
if ENABLE_PLOT:
245-
plt.show()
268+
plt.show(block=True)

0 commit comments

Comments
 (0)