Skip to content

Commit c42d01c

Browse files
committed
vehicle animation classes
added plot method, added some doco
1 parent b512246 commit c42d01c

File tree

1 file changed

+151
-29
lines changed

1 file changed

+151
-29
lines changed

roboticstoolbox/mobile/Animations.py

Lines changed: 151 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,84 @@
88
from math import pi, sin, cos, tan, atan2
99
import numpy as np
1010
from scipy import integrate, linalg, interpolate
11-
11+
from pathlib import Path
1212
import matplotlib.pyplot as plt
13-
from matplotlib import patches
13+
from matplotlib import patches, colors
1414
import matplotlib.transforms as mtransforms
1515

1616
from spatialmath import SE2, base
1717
from roboticstoolbox import rtb_load_data
1818

19+
"""
20+
Class to support animation of a vehicle on Matplotlib plot
21+
22+
There are three concrete subclasses:
23+
24+
- ``VehicleMarker`` animates a Matplotlib marker
25+
- ``VehiclePolygon`` animates a polygon, including predefined shapes
26+
- ``VehicleIcon`` animates an image
27+
28+
29+
30+
These can be used in two different ways, firstly::
31+
32+
a.add()
33+
a.update(q)
34+
35+
adds an instance of the animation shape to the plot and subsequent calls
36+
to ``update`` will animate it.
37+
38+
Secondly::
39+
40+
a.plot(q)
41+
42+
adds an instance of the animation shape to the plot with the specified
43+
configuration. It cannot be moved, but the method does return a reference
44+
to the Matplotlib object added to the plot.
45+
46+
Any of these can be passed to a Vehicle subclass object to make an animation
47+
during simulation::
1948
20-
class VehicleAnimation(ABC):
49+
a = VehiclePolygon('car', color='r')
50+
veh = Bicycle()
51+
veh.run(animation=a)
52+
53+
"""
54+
55+
class VehicleAnimationBase(ABC):
56+
"""
57+
Class to support animation of a vehicle on Matplotlib plot
58+
59+
There are three concrete subclasses:
60+
61+
- ``VehicleMarker`` animates a Matplotlib marker
62+
- ``VehiclePolygon`` animates a polygon, including predefined shapes
63+
- ``VehicleIcon`` animates an image
64+
65+
These can be used in two different ways, firstly::
66+
67+
a.add()
68+
a.update(q)
69+
70+
adds an instance of the animation shape to the plot and subsequent calls
71+
to ``update`` will animate it.
72+
73+
Secondly::
74+
75+
a.plot(q)
76+
77+
adds an instance of the animation shape to the plot with the specified
78+
configuration. It cannot be moved, but the method does return a reference
79+
to the Matplotlib object added to the plot.
80+
81+
Any of these can be passed to a Vehicle subclass object to make an animation
82+
during simulation::
83+
84+
a = VehiclePolygon('car', color='r')
85+
veh = Bicycle()
86+
veh.run(animation=a)
87+
88+
"""
2189

2290
def __init__(self):
2391
self._object = None
@@ -40,16 +108,34 @@ def add(self, ax=None, **kwargs):
40108

41109
self._add(**kwargs)
42110

43-
def update(self, x):
111+
def update(self, q):
44112
"""
45113
Update the vehicle animation
46114
47-
:param x: vehicle state
48-
:type x: ndarray(2) or ndarray(3)
115+
:param q: vehicle configuration
116+
:type q: ndarray(2) or ndarray(3)
49117
50118
The graphical depiction of the vehicle position or pose is updated.
119+
120+
For ``AnimationMarker`` only position can be depicted so the third element
121+
of ``q``, if given, is ignored.
51122
"""
52-
self._update(x)
123+
self._update(q)
124+
125+
def plot(self, q, **kwargs):
126+
"""
127+
Add vehicle to the current plot
128+
129+
:param q: vehicle configuration
130+
:type q: ndarray(2) or ndarray(3)
131+
:return: reference to Matplotlib object
132+
133+
A reference to the animation object is kept, and it will be deleted
134+
from the plot when the ``VehicleAnimation`` object is garbage collected.
135+
"""
136+
self.add(**kwargs)
137+
self.update(q)
138+
return self._object
53139

54140
def __del__(self):
55141

@@ -58,18 +144,20 @@ def __del__(self):
58144

59145
# ========================================================================= #
60146

61-
class VehicleMarker(VehicleAnimation):
147+
class VehicleMarker(VehicleAnimationBase):
62148

63149
def __init__(self, **kwargs):
64150
"""
65-
Create graphical animation of vehicle as a marker
151+
Create graphical animation of vehicle as a Matplotlib marker
66152
67153
:param kwargs: additional arguments passed to matplotlib ``plot``.
68154
:return: animation object
69155
:rtype: VehicleAnimation
70156
71157
Creates an object that can be passed to a ``Vehicle`` subclass to depict
72-
the moving robot as a simple matplotlib marker during simulation::
158+
the moving robot as a simple matplotlib marker during simulation.
159+
160+
For example, a blue filled square, is::
73161
74162
a = VehicleMarker(marker='s', markerfacecolor='b')
75163
veh = Bicycle(driver=RandomPath(10), animation=a)
@@ -101,14 +189,14 @@ def _add(self, x=None, **kwargs):
101189

102190
# ========================================================================= #
103191

104-
class VehiclePolygon(VehicleAnimation):
192+
class VehiclePolygon(VehicleAnimationBase):
105193

106194
def __init__(self, shape='car', scale=1, **kwargs):
107195
"""
108196
Create graphical animation of vehicle as a polygon
109197
110-
:param shape: polygon shape, defaults to 'car'
111-
:type shape: str
198+
:param shape: polygon shape as vertices or a predefined shape, defaults to 'car'
199+
:type shape: ndarray(2,n) or str
112200
:param scale: Length of the vehicle on the plot, defaults to 1
113201
:type scale: float
114202
:param kwargs: additional arguments passed to matplotlib patch such as
@@ -120,18 +208,23 @@ def __init__(self, shape='car', scale=1, **kwargs):
120208
:rtype: VehicleAnimation
121209
122210
Creates an object that can be passed to a ``Vehicle`` subclass to
123-
depict the moving robot as a polygon during simulation::
211+
depict the moving robot as a polygon during simulation.
212+
213+
For example, a red filled car-shaped polygon is::
124214
125215
a = VehiclePolygon('car', color='r')
126216
veh = Bicycle()
127217
veh.run(animation=a)
128218
129219
``shape`` can be:
130220
131-
* ``"car"`` a rectangle with a pointy front
221+
* ``"car"`` a rectangle with chamfered front corners
132222
* ``"triangle"`` a triangle
133223
* an Nx2 NumPy array of vertices, does not have to be closed.
134224
225+
The polygon is scaled to an image with a length of ``scale`` in the units of
226+
the plot.
227+
135228
:seealso: :func:`~Vehicle`
136229
"""
137230
super().__init__()
@@ -142,45 +235,48 @@ def __init__(self, shape='car', scale=1, **kwargs):
142235
c = 0.5 # centre x coordinate
143236
w = 1 # width in x direction
144237

238+
if isinstance(shape, str):
145239
if shape == 'car':
146240
self._coords = np.array([
147241
[-c, h],
148242
[t - c, h],
149243
[w - c, 0],
150244
[t - c, -h],
151245
[-c, -h],
152-
]).T * scale
246+
]).T
153247
elif shape == 'triangle':
154248
self._coords = np.array([
155249
[-c, h],
156250
[ w, 0],
157251
[-c, -h],
158-
]).T * scale
252+
]).T
159253
else:
160254
raise ValueError('unknown vehicle shape name')
161-
255+
162256
elif isinstance(shape, np.ndarray) and shape.shape[1] == 2:
163257
self._coords = shape
164258
else:
165259
raise TypeError('unknown shape argument')
166-
260+
self._coords *= scale
167261
self._args = kwargs
168262

169263
def _add(self, **kwargs):
170264
# color is fillcolor + edgecolor
171265
# facecolor if None is default
172266
self._ax = plt.gca()
173-
self._object = patches.Polygon(self._coords.T, **self._args)
267+
self._object = patches.Polygon(self._coords.T, **{**self._args, **kwargs})
174268
self._ax.add_patch(self._object)
175269

176270
def _update(self, x):
177271

178-
xy = SE2(x) * self._coords
179-
self._object.set_xy(xy.T)
272+
if self._object is not None:
273+
# if animation is initialized
274+
xy = SE2(x) * self._coords
275+
self._object.set_xy(xy.T)
180276

181277
# ========================================================================= #
182278

183-
class VehicleIcon(VehicleAnimation):
279+
class VehicleIcon(VehicleAnimationBase):
184280

185281
def __init__(self, filename, origin=None, scale=1, rotation=0):
186282
"""
@@ -199,7 +295,9 @@ def __init__(self, filename, origin=None, scale=1, rotation=0):
199295
:rtype: VehicleAnimation
200296
201297
Creates an object that can be passed to a ``Vehicle`` subclass to
202-
depict the moving robot as a polygon during simulation::
298+
depict the moving robot as a polygon during simulation.
299+
300+
For example, the image of a red car is::
203301
204302
a = VehicleIcon('redcar', scale=2)
205303
veh = Bicycle()
@@ -209,13 +307,18 @@ def __init__(self, filename, origin=None, scale=1, rotation=0):
209307
210308
* ``"greycar"`` a grey and white car (top view)
211309
* ``"redcar"`` a red car (top view)
310+
* ``"piano"`` a piano (top view)
212311
* path to an image file, including extension
213312
214-
.. image:: ../../roboticstoolbox/data/greycar.png
313+
.. image:: ../../rtb-data/rtbdata/data/greycar.png
215314
:width: 200px
216315
:align: center
217316
218-
.. image:: ../../roboticstoolbox/data/redcar.png
317+
.. image:: ../../rtb-data/rtbdata/data/redcar.png
318+
:width: 300px
319+
:align: center
320+
321+
.. image:: ../../rtb-data/rtbdata/data/piano.png
219322
:width: 300px
220323
:align: center
221324
@@ -241,7 +344,7 @@ def __init__(self, filename, origin=None, scale=1, rotation=0):
241344
if '.' not in filename:
242345
try:
243346
# try the default folder first
244-
image = rtb_loaddata(filename + ".png", plt.imread)
347+
image = rtb_load_data(Path("data") / Path(filename + ".png"), plt.imread)
245348
except FileNotFoundError:
246349
raise ValueError(f"{filename} is not a provided icon")
247350
else:
@@ -271,7 +374,7 @@ def __init__(self, filename, origin=None, scale=1, rotation=0):
271374
def _add(self, ax=None, **kwargs):
272375

273376
def imshow_affine(ax, z, *args, **kwargs):
274-
im = ax.imshow(z, *args, **kwargs, zorder=3)
377+
im = ax.imshow(z, *args, **kwargs)
275378
x1, x2, y1, y2 = im.get_extent()
276379
# im._image_skew_coordinate = (x2, y1)
277380
return im
@@ -283,10 +386,27 @@ def imshow_affine(ax, z, *args, **kwargs):
283386
(1 - self._origin[1]) * self._width
284387
]
285388
self._ax = plt.gca()
389+
390+
args = {}
391+
if 'color' in kwargs and self._image.ndim == 2:
392+
color = kwargs['color']
393+
del kwargs['color']
394+
rgb = colors.to_rgb(color)
395+
cmapdata = {'red': [(0.0, 0.0, 0.0), (1.0, rgb[0], 0.0)],
396+
'green': [(0.0, 0.0, 0.0), (1.0, rgb[1], 0.0)],
397+
'blue': [(0.0, 0.0, 0.0), (1.0, rgb[2], 0.0)]
398+
}
399+
cmap = colors.LinearSegmentedColormap('linear', segmentdata=cmapdata, N=256)
400+
args = {'cmap': cmap}
401+
elif self._image.ndim == 2:
402+
args = {'cmap': 'gray'}
403+
if 'zorder' not in kwargs:
404+
args['zorder'] = 3
405+
286406
self._object = imshow_affine(self._ax, self._image,
287407
interpolation='none',
288408
extent=extent, clip_on=True,
289-
alpha=1)
409+
**{**kwargs, **args})
290410

291411
def _update(self, x):
292412

@@ -304,6 +424,8 @@ def _update(self, x):
304424
if __name__ == "__main__":
305425
from math import pi
306426

427+
from roboticstoolbox import Bicycle, RandomPath
428+
307429
V = np.diag(np.r_[0.02, 0.5 * pi / 180] ** 2)
308430

309431
v = VehiclePolygon()

0 commit comments

Comments
 (0)