-
Notifications
You must be signed in to change notification settings - Fork 561
Expand file tree
/
Copy pathDistanceTransformPlanner.py
More file actions
392 lines (312 loc) · 11.9 KB
/
DistanceTransformPlanner.py
File metadata and controls
392 lines (312 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
"""
@Author: Peter Corke, original MATLAB code and Python version
@Author: Kristian Gibson, initial MATLAB port
"""
from scipy import integrate
from spatialmath.base.transforms2d import *
from spatialmath.base.vectors import *
from spatialmath.pose2d import SE2
from spatialmath import base
from scipy.ndimage import *
import matplotlib.pyplot as plt
from matplotlib import cm
from roboticstoolbox.mobile.PlannerBase import PlannerBase
class DistanceTransformPlanner(PlannerBase):
r"""
Distance transform path planner
:param occgrid: occupancy grid
:type occgrid: :class:`BinaryOccGrid` or ndarray(h,w)
:param metric: distane metric, one of: "euclidean" [default], "manhattan"
:type metric: str optional
:param kwargs: common planner options, see :class:`PlannerBase`
================== ========================
Feature Capability
================== ========================
Plan :math:`\mathbb{R}^2`, discrete
Obstacle avoidance Yes, occupancy grid
Curvature Discontinuous
Motion Omnidirectional
================== ========================
Creates a planner that finds the path between two points in the 2D grid
using omnidirectional motion and avoiding occupied cells. The path
comprises a set of 4- or -way connected points in adjacent cells. Also
known as the wavefront, grassfire or brushfire planning algorithm.
The map is described by a 2D occupancy ``occgrid`` whose elements are zero
if traversable of nonzero if untraversable, ie. an obstacle.
The cells are assumed to be unit squares. Crossing the cell horizontally or
vertically is a travel distance of 1, and for the Euclidean distance
measure, the crossing the cell diagonally is a distance of :math:`\sqrt{2}`.
Example:
.. runblock:: pycon
>>> from roboticstoolbox import DistanceTransformPlanner
>>> import numpy as np
>>> simplegrid = np.zeros((6, 6));
>>> simplegrid[2:5, 3:5] = 1
>>> dx = DistanceTransformPlanner(simplegrid, goal=(1, 1), distance="manhattan");
>>> dx.plan()
>>> path = dx.query(start=(5, 4))
>>> print(path.T)
.. warning:: The distance planner is iterative and implemented in Python, will
be slow for very large occupancy grids.
:author: Peter Corke
:seealso: :meth:`plan` :meth:`query` :class:`PlannerBase`
"""
def __init__(self, occgrid=None, metric="euclidean", **kwargs):
super().__init__(occgrid=occgrid, ndims=2, **kwargs)
self._metric = metric
self._distancemap = None
@property
def metric(self):
"""
Get the distance metric
:return: Get the metric, either "euclidean" or "manhattan"
:rtype: str
"""
return self._metric
@property
def distancemap(self):
"""
Get the distance map
:return: distance map
:rtype: ndarray(h,w)
The 2D array, the same size as the passed occupancy grid, has elements
equal to nan if they contain an obstacle, otherwise the minimum
obstacle-free distance to the goal using the particular distance metric.
"""
return self._distancemap
def __str__(self):
s = super().__str__()
s += f"\n Distance metric: {self._metric}"
if self.distancemap is not None:
s += ", Distance map: computed "
else:
s += ", Distance map: empty "
return s
def plan(self, goal=None, animate=False, summary=False):
r"""
Plan path using distance transform
:param goal: goal position :math:`(x, y)`, defaults to previously set value
:type goal: array_like(2), optional
Compute the distance transform for all non-obstacle cells, which is the
minimum obstacle-free distance to the goal using the particular distance
metric.
:seealso: :meth:`query`
"""
# show = None
# if animate:
# show = 0.05
# else:
# show = 0
if goal is not None:
self.goal = goal
if self._goal is None:
raise ValueError("No goal specified here or in constructor")
self._distancemap = distancexform(
self.occgrid.grid,
goal=self._goal,
metric=self._metric,
animate=animate,
summary=summary,
)
def next(self, position):
"""
Find next point on the path
:param position: current robot position
:type position: array_like(2)
:raises RuntimeError: no plan has been computed
:return: next robot position
:rtype: ndarray(2)
Return the robot position that is one step closer to the goal. Called
by :meth:`query` to find a path from start to goal.
:seealso: :meth:`plan` :meth:`query`
"""
if self.distancemap is None:
raise ValueError("No distance map computed, you need to plan.")
directions = np.array(
[
# dy dx
[-1, -1],
[0, -1],
[1, -1],
[-1, 0],
[0, 0],
[1, 0],
[0, 1],
[1, 1],
],
dtype=int,
)
x = int(position[0])
y = int(position[1])
min_dist = np.inf
for d in directions:
try:
if self._distancemap[y + d[0], x + d[1]] < min_dist:
min_dir = d
min_dist = self._distancemap[y + d[0], x + d[1]]
except:
# come here if the neighbouring cell is outside the map bounds
# raise RuntimeError(f"Unexpected error finding next min dist at {d}")
pass
if np.isinf(min_dist):
raise RuntimeError("no minimum found, shouldn't happen")
x = x + min_dir[1]
y = y + min_dir[0]
next = np.r_[x, y]
if all(next == self._goal):
return None
else:
return next
def plot_3d(self, path=None, ls=None):
"""
Plot path on 3D cost surface
:param path: robot path, defaults to None
:type path: ndarray(N,2), optional
:param ls: dictionary of Matplotlib linestyle options, defaults to None
:type ls: dict, optional
:return: Matplotlib 3D axes
:rtype: Axes
Creates a 3D plot showing distance from the goal as a cost surface.
Overlays the path if given.
.. warning:: The visualization is poor because of Matplotlib's poor hidden
line/surface handling.
"""
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
distance = self._distancemap
X, Y = np.meshgrid(np.arange(distance.shape[1]), np.arange(distance.shape[0]))
surf = ax.plot_surface(
X, Y, distance, linewidth=1, antialiased=False # cmap='gray',
)
if path is not None:
# k = sub2ind(np.shape(self._distancemap), p[:, 1], p[:, 0])
height = distance[path[:, 1], path[:, 0]]
ax.plot(path[:, 0], path[:, 1], height, **ls)
plt.xlabel("x")
plt.ylabel("y")
ax.set_zlabel("z")
plt.show()
return ax
import numpy as np
def distancexform(occgrid, goal, metric="cityblock", animate=False, summary=False):
"""
Distance transform for path planning
:param occgrid: Occupancy grid, 0 is free, >0 is occupied/obstacle
:type occgrid: NumPy ndarray
:param goal: Goal position (x,y)
:type goal: 2-element array-like
:param metric: distance metric, defaults to 'cityblock'
:type metric: str, optional
:param animate: animate the iterations of the algorithm
:return: Distance transform matrix
:rtype: NumPy ndarray
Implements the grass/brush fire algorithm to compute, for every reachable
cell in the occupancy grid, its distance from the goal.
The result is an array, the same size as the occupancy grid ``occgrid``,
where each cell contains the distance to the goal according to the chosen
``metric``. In addition:
- Obstacle cells will be set to ``nan``
- Unreachable cells, ie. free cells _inside obstacles_ will be set
to ``inf``
The cells of the passed occupancy grid are:
- zero, cell is free or driveable
- one, cell is an obstacle, or not driveable
"""
# build the matrix for performing distance transform:
# - obstacles are nan
# - other cells are inf
# - goal is zero
goal = base.getvector(goal, 2, dtype=int)
distance = occgrid.astype(np.float32)
distance[occgrid > 0] = np.nan # assign nan to obstacle cells
distance[occgrid == 0] = np.inf # assign inf to other cells
distance[goal[1], goal[0]] = 0 # assign zero to goal
# create the appropriate distance matrix D
if metric.lower() in ("manhattan", "cityblock"):
# fmt: off
D = np.array([
[ np.inf, 1, np.inf],
[ 1, 0, 1],
[ np.inf, 1, np.inf]
])
# fmt: on
elif metric.lower() == "euclidean":
r2 = np.sqrt(2)
# fmt: off
D = np.array([
[ r2, 1, r2],
[ 1, 0, 1],
[ r2, 1, r2]
])
# fmt: on
# get ready to iterate
count = 0
ninf = np.inf # number of infinities in the map
h = None
while True:
distance = grassfire_step(distance, D)
distance[occgrid > 0] = np.nan # reinsert nans for obstacles
count += 1
if animate:
# TODO, needs work to update colorbar and be faster
display = distance.copy()
display[np.isinf(display)] = 0
if h is None:
plt.figure()
plt.xlabel("x")
plt.ylabel("y")
ax = plt.gca()
plt.pause(0.001)
cmap = cm.get_cmap("gray")
cmap.set_bad("red")
cmap.set_over("white")
h = plt.imshow(display, cmap=cmap)
plt.colorbar(label="distance")
else:
h.remove()
h = plt.imshow(display, cmap=cmap)
plt.pause(0.001)
ninfnow = np.isinf(distance).sum() # current number of Infs
if ninfnow == ninf:
# stop if the number of Infs left in the map had stopped reducing
# it may never get to zero if there are unreachable cells in the map
break
ninf = ninfnow
if summary:
print(f"{count:d} iterations, {ninf:d} unreachable cells")
return distance
def grassfire_step(G, D):
# pad with inf
H = np.pad(G, max(D.shape) // 2, "constant", constant_values=np.inf)
rows, columns = G.shape
# inspired by https://landscapearchaeology.org/2018/numpy-loops/
minimum = np.full(G.shape, np.inf)
for y in range(3):
for x in range(3):
v = H[y : rows + y, x : columns + x] + D[y, x]
# we use fmin() because it ignores NaNs, behaves like MATLAB min()
minimum = np.fmin(minimum, v)
return minimum
if __name__ == "__main__":
pass
# # make a simple map, as per the MOOCs
# occgrid = np.zeros((10,10))
# occgrid[3:6,2:7] = 1
# occgrid[4,3:5] = 0 # hole in the obstacle
# # occgrid[7:8,7] = 1 # extra bit
# print(occgrid)
# print()
# goal=[5,7]
# np.set_printoptions(precision=1)
# dx = distancexform(occgrid, goal, metric='Euclidean')
# print(dx)
# from roboticstoolbox import DistanceTransformPlanner, rtb_loadmat
# house = rtb_loadmat('data/house.mat')
# floorplan = house['floorplan']
# places = house['places']
# dx = DistanceTransformPlanner(floorplan)
# print(dx)
# dx.goal = [1,2]
# dx.plan(places.kitchen)
# path = dx.query(places.br3)
# dx.plot(path, block=True)