Skip to content

Commit 6d2e075

Browse files
committed
feat: ElasticBands update
1 parent 082bf53 commit 6d2e075

File tree

2 files changed

+65
-8
lines changed

2 files changed

+65
-8
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: 26 additions & 8 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,8 @@ 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+
self.distance_map = compute_sdf_scipy(obstacles)
5050
self.bubbles = [
5151
Bubble(p, self.compute_rho(p)) for p in initial_path
5252
] # Initialize bubble chain
@@ -73,7 +73,7 @@ def contraction_force(self, i):
7373
dir_next = (next_ - current) / (np.linalg.norm(next_ - current) + 1e-6)
7474
return self.kc * (dir_prev + dir_next)
7575

76-
def external_force(self, i):
76+
def repulsive_force(self, i):
7777
"""Calculate external repulsive force for the i-th bubble"""
7878
h = STEP_SIZE # Step size
7979
b = self.bubbles[i].pos
@@ -99,7 +99,7 @@ def update_bubbles(self):
9999
new_bubbles.append(self.bubbles[i]) # Fixed start and end points
100100
continue
101101

102-
f_total = self.contraction_force(i) + self.external_force(i)
102+
f_total = self.contraction_force(i) + self.repulsive_force(i)
103103
alpha = self.bubbles[i].radius # Adaptive step size
104104
new_pos = self.bubbles[i].pos + alpha * f_total
105105
new_pos = np.clip(new_pos, 0, 499)
@@ -146,10 +146,11 @@ def __init__(self):
146146
self.obstacles = np.zeros((500, 500))
147147
self.path_points = []
148148
self.elastic_band = None
149+
self.running = True
149150

150151
if ENABLE_PLOT:
151152
self.fig, self.ax = plt.subplots(figsize=(8, 8))
152-
# Set the display range of the graph
153+
self.fig.canvas.mpl_connect("close_event", self.on_close)
153154
self.ax.set_xlim(0, 500)
154155
self.ax.set_ylim(0, 500)
155156

@@ -164,15 +165,32 @@ def __init__(self):
164165

165166
self.plot_background()
166167

168+
def on_close(self, event):
169+
"""Handle window close event"""
170+
self.running = False
171+
plt.close("all") # Close all figure windows
172+
167173
def plot_background(self):
168174
"""Plot the background grid"""
169-
if not ENABLE_PLOT:
175+
if not ENABLE_PLOT or not self.running:
170176
return
171177

172178
self.ax.cla()
173179
self.ax.set_xlim(0, 500)
174180
self.ax.set_ylim(0, 500)
175181
self.ax.grid(True)
182+
183+
if ENABLE_INTERACTIVE:
184+
self.ax.set_title(
185+
"Elastic Bands Path Planning\n"
186+
"Left click: Add obstacles\n"
187+
"Right click: Add path points\n"
188+
"Middle click: Start planning",
189+
pad=20,
190+
)
191+
else:
192+
self.ax.set_title("Elastic Bands Path Planning", pad=20)
193+
176194
if self.path_points:
177195
self.ax.plot(
178196
[p[0] for p in self.path_points],
@@ -242,4 +260,4 @@ def plan_path(self):
242260
if __name__ == "__main__":
243261
_ = ElasticBandsVisualizer()
244262
if ENABLE_PLOT:
245-
plt.show()
263+
plt.show(block=True)

0 commit comments

Comments
 (0)