8
8
from math import pi , sin , cos , tan , atan2
9
9
import numpy as np
10
10
from scipy import integrate , linalg , interpolate
11
-
11
+ from pathlib import Path
12
12
import matplotlib .pyplot as plt
13
- from matplotlib import patches
13
+ from matplotlib import patches , colors
14
14
import matplotlib .transforms as mtransforms
15
15
16
16
from spatialmath import SE2 , base
17
17
from roboticstoolbox import rtb_load_data
18
18
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::
19
48
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
+ """
21
89
22
90
def __init__ (self ):
23
91
self ._object = None
@@ -40,16 +108,34 @@ def add(self, ax=None, **kwargs):
40
108
41
109
self ._add (** kwargs )
42
110
43
- def update (self , x ):
111
+ def update (self , q ):
44
112
"""
45
113
Update the vehicle animation
46
114
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)
49
117
50
118
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.
51
122
"""
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
53
139
54
140
def __del__ (self ):
55
141
@@ -58,18 +144,20 @@ def __del__(self):
58
144
59
145
# ========================================================================= #
60
146
61
- class VehicleMarker (VehicleAnimation ):
147
+ class VehicleMarker (VehicleAnimationBase ):
62
148
63
149
def __init__ (self , ** kwargs ):
64
150
"""
65
- Create graphical animation of vehicle as a marker
151
+ Create graphical animation of vehicle as a Matplotlib marker
66
152
67
153
:param kwargs: additional arguments passed to matplotlib ``plot``.
68
154
:return: animation object
69
155
:rtype: VehicleAnimation
70
156
71
157
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::
73
161
74
162
a = VehicleMarker(marker='s', markerfacecolor='b')
75
163
veh = Bicycle(driver=RandomPath(10), animation=a)
@@ -101,14 +189,14 @@ def _add(self, x=None, **kwargs):
101
189
102
190
# ========================================================================= #
103
191
104
- class VehiclePolygon (VehicleAnimation ):
192
+ class VehiclePolygon (VehicleAnimationBase ):
105
193
106
194
def __init__ (self , shape = 'car' , scale = 1 , ** kwargs ):
107
195
"""
108
196
Create graphical animation of vehicle as a polygon
109
197
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
112
200
:param scale: Length of the vehicle on the plot, defaults to 1
113
201
:type scale: float
114
202
:param kwargs: additional arguments passed to matplotlib patch such as
@@ -120,18 +208,23 @@ def __init__(self, shape='car', scale=1, **kwargs):
120
208
:rtype: VehicleAnimation
121
209
122
210
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::
124
214
125
215
a = VehiclePolygon('car', color='r')
126
216
veh = Bicycle()
127
217
veh.run(animation=a)
128
218
129
219
``shape`` can be:
130
220
131
- * ``"car"`` a rectangle with a pointy front
221
+ * ``"car"`` a rectangle with chamfered front corners
132
222
* ``"triangle"`` a triangle
133
223
* an Nx2 NumPy array of vertices, does not have to be closed.
134
224
225
+ The polygon is scaled to an image with a length of ``scale`` in the units of
226
+ the plot.
227
+
135
228
:seealso: :func:`~Vehicle`
136
229
"""
137
230
super ().__init__ ()
@@ -142,45 +235,48 @@ def __init__(self, shape='car', scale=1, **kwargs):
142
235
c = 0.5 # centre x coordinate
143
236
w = 1 # width in x direction
144
237
238
+ if isinstance (shape , str ):
145
239
if shape == 'car' :
146
240
self ._coords = np .array ([
147
241
[- c , h ],
148
242
[t - c , h ],
149
243
[w - c , 0 ],
150
244
[t - c , - h ],
151
245
[- c , - h ],
152
- ]).T * scale
246
+ ]).T
153
247
elif shape == 'triangle' :
154
248
self ._coords = np .array ([
155
249
[- c , h ],
156
250
[ w , 0 ],
157
251
[- c , - h ],
158
- ]).T * scale
252
+ ]).T
159
253
else :
160
254
raise ValueError ('unknown vehicle shape name' )
161
-
255
+
162
256
elif isinstance (shape , np .ndarray ) and shape .shape [1 ] == 2 :
163
257
self ._coords = shape
164
258
else :
165
259
raise TypeError ('unknown shape argument' )
166
-
260
+ self . _coords *= scale
167
261
self ._args = kwargs
168
262
169
263
def _add (self , ** kwargs ):
170
264
# color is fillcolor + edgecolor
171
265
# facecolor if None is default
172
266
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 } )
174
268
self ._ax .add_patch (self ._object )
175
269
176
270
def _update (self , x ):
177
271
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 )
180
276
181
277
# ========================================================================= #
182
278
183
- class VehicleIcon (VehicleAnimation ):
279
+ class VehicleIcon (VehicleAnimationBase ):
184
280
185
281
def __init__ (self , filename , origin = None , scale = 1 , rotation = 0 ):
186
282
"""
@@ -199,7 +295,9 @@ def __init__(self, filename, origin=None, scale=1, rotation=0):
199
295
:rtype: VehicleAnimation
200
296
201
297
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::
203
301
204
302
a = VehicleIcon('redcar', scale=2)
205
303
veh = Bicycle()
@@ -209,13 +307,18 @@ def __init__(self, filename, origin=None, scale=1, rotation=0):
209
307
210
308
* ``"greycar"`` a grey and white car (top view)
211
309
* ``"redcar"`` a red car (top view)
310
+ * ``"piano"`` a piano (top view)
212
311
* path to an image file, including extension
213
312
214
- .. image:: ../../roboticstoolbox /data/greycar.png
313
+ .. image:: ../../rtb-data/rtbdata /data/greycar.png
215
314
:width: 200px
216
315
:align: center
217
316
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
219
322
:width: 300px
220
323
:align: center
221
324
@@ -241,7 +344,7 @@ def __init__(self, filename, origin=None, scale=1, rotation=0):
241
344
if '.' not in filename :
242
345
try :
243
346
# 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 )
245
348
except FileNotFoundError :
246
349
raise ValueError (f"{ filename } is not a provided icon" )
247
350
else :
@@ -271,7 +374,7 @@ def __init__(self, filename, origin=None, scale=1, rotation=0):
271
374
def _add (self , ax = None , ** kwargs ):
272
375
273
376
def imshow_affine (ax , z , * args , ** kwargs ):
274
- im = ax .imshow (z , * args , ** kwargs , zorder = 3 )
377
+ im = ax .imshow (z , * args , ** kwargs )
275
378
x1 , x2 , y1 , y2 = im .get_extent ()
276
379
# im._image_skew_coordinate = (x2, y1)
277
380
return im
@@ -283,10 +386,27 @@ def imshow_affine(ax, z, *args, **kwargs):
283
386
(1 - self ._origin [1 ]) * self ._width
284
387
]
285
388
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
+
286
406
self ._object = imshow_affine (self ._ax , self ._image ,
287
407
interpolation = 'none' ,
288
408
extent = extent , clip_on = True ,
289
- alpha = 1 )
409
+ ** { ** kwargs , ** args } )
290
410
291
411
def _update (self , x ):
292
412
@@ -304,6 +424,8 @@ def _update(self, x):
304
424
if __name__ == "__main__" :
305
425
from math import pi
306
426
427
+ from roboticstoolbox import Bicycle , RandomPath
428
+
307
429
V = np .diag (np .r_ [0.02 , 0.5 * pi / 180 ] ** 2 )
308
430
309
431
v = VehiclePolygon ()
0 commit comments