88from math import pi , sin , cos , tan , atan2
99import numpy as np
1010from scipy import integrate , linalg , interpolate
11-
11+ from pathlib import Path
1212import matplotlib .pyplot as plt
13- from matplotlib import patches
13+ from matplotlib import patches , colors
1414import matplotlib .transforms as mtransforms
1515
1616from spatialmath import SE2 , base
1717from 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):
304424if __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