plot.py
Go to the documentation of this file.
1 """Various plotting utlities."""
2 
3 # pylint: disable=no-member, invalid-name
4 
5 from typing import Iterable, Optional, Tuple
6 
7 import matplotlib.pyplot as plt
8 import numpy as np
9 from matplotlib import patches
10 from mpl_toolkits.mplot3d import Axes3D # pylint: disable=unused-import
11 
12 import gtsam
13 from gtsam import Marginals, Point2, Point3, Pose2, Pose3, Values
14 
15 # For translation between a scaling of the uncertainty ellipse and the
16 # percentage of inliers see discussion in
17 # [PR 1067](https://github.com/borglab/gtsam/pull/1067)
18 # and the notebook python/gtsam/notebooks/ellipses.ipynb (needs scipy).
19 #
20 # In the following, the default scaling is chosen for 95% inliers, which
21 # translates to the following sigma values:
22 # 1D: 1.959963984540
23 # 2D: 2.447746830681
24 # 3D: 2.795483482915
25 #
26 # Further references are Stochastic Models, Estimation, and Control Vol 1 by Maybeck,
27 # page 366 and https://www.xarg.org/2018/04/how-to-plot-a-covariance-error-ellipse/
28 #
29 # For reference, here are the inlier percentages for some sigma values:
30 # 1 2 3 4 5
31 # 1D 68.26895 95.44997 99.73002 99.99367 99.99994
32 # 2D 39.34693 86.46647 98.88910 99.96645 99.99963
33 # 3D 19.87480 73.85359 97.07091 99.88660 99.99846
34 
35 def set_axes_equal(fignum: int) -> None:
36  """
37  Make axes of 3D plot have equal scale so that spheres appear as spheres,
38  cubes as cubes, etc.. This is one possible solution to Matplotlib's
39  ax.set_aspect('equal') and ax.axis('equal') not working for 3D.
40 
41  Args:
42  fignum: An integer representing the figure number for Matplotlib.
43  """
44  fig = plt.figure(fignum)
45  if not fig.axes:
46  ax = fig.add_subplot(projection='3d')
47  else:
48  ax = fig.axes[0]
49 
50  limits = np.array([
51  ax.get_xlim3d(),
52  ax.get_ylim3d(),
53  ax.get_zlim3d(),
54  ])
55 
56  origin = np.mean(limits, axis=1)
57  radius = 0.5 * np.max(np.abs(limits[:, 1] - limits[:, 0]))
58 
59  ax.set_xlim3d([origin[0] - radius, origin[0] + radius])
60  ax.set_ylim3d([origin[1] - radius, origin[1] + radius])
61  ax.set_zlim3d([origin[2] - radius, origin[2] + radius])
62 
63 
64 def ellipsoid(rx: float, ry: float, rz: float,
65  n: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
66  """
67  Numpy equivalent of Matlab's ellipsoid function.
68 
69  Args:
70  rx: Radius of ellipsoid in X-axis.
71  ry: Radius of ellipsoid in Y-axis.
72  rz: Radius of ellipsoid in Z-axis.
73  n: The granularity of the ellipsoid plotted.
74 
75  Returns:
76  The points in the x, y and z axes to use for the surface plot.
77  """
78  u = np.linspace(0, 2 * np.pi, n + 1)
79  v = np.linspace(0, np.pi, n + 1)
80  x = -rx * np.outer(np.cos(u), np.sin(v)).T
81  y = -ry * np.outer(np.sin(u), np.sin(v)).T
82  z = -rz * np.outer(np.ones_like(u), np.cos(v)).T
83 
84  return x, y, z
85 
86 
88  origin: Point3,
89  P: np.ndarray,
90  scale: float = 1,
91  n: int = 8,
92  alpha: float = 0.5) -> None:
93  """
94  Plots a Gaussian as an uncertainty ellipse
95 
96  The ellipse is scaled in such a way that 95% of drawn samples are inliers.
97  Derivation of the scaling factor is explained at the beginning of this file.
98 
99  Args:
100  axes (matplotlib.axes.Axes): Matplotlib axes.
101  origin: The origin in the world frame.
102  P: The marginal covariance matrix of the 3D point
103  which will be represented as an ellipse.
104  scale: Scaling factor of the radii of the covariance ellipse.
105  n: Defines the granularity of the ellipse. Higher values indicate finer ellipses.
106  alpha: Transparency value for the plotted surface in the range [0, 1].
107  """
108  # this corresponds to 95%, see note above
109  k = 2.795483482915
110  U, S, _ = np.linalg.svd(P)
111 
112  radii = k * np.sqrt(S)
113  radii = radii * scale
114  rx, ry, rz = radii
115 
116  # generate data for "unrotated" ellipsoid
117  xc, yc, zc = ellipsoid(rx, ry, rz, n)
118 
119  # rotate data with orientation matrix U and center c
120  data = np.kron(U[:, 0:1], xc) + np.kron(U[:, 1:2], yc) + \
121  np.kron(U[:, 2:3], zc)
122  n = data.shape[1]
123  x = data[0:n, :] + origin[0]
124  y = data[n:2 * n, :] + origin[1]
125  z = data[2 * n:, :] + origin[2]
126 
127  axes.plot_surface(x, y, z, alpha=alpha, cmap='hot')
128 
129 
131  origin: Point2,
132  covariance: np.ndarray) -> None:
133  """
134  Plots a Gaussian as an uncertainty ellipse
135 
136  The ellipse is scaled in such a way that 95% of drawn samples are inliers.
137  Derivation of the scaling factor is explained at the beginning of this file.
138 
139  Args:
140  axes (matplotlib.axes.Axes): Matplotlib axes.
141  origin: The origin in the world frame.
142  covariance: The marginal covariance matrix of the 2D point
143  which will be represented as an ellipse.
144  """
145 
146  w, v = np.linalg.eigh(covariance)
147 
148  # this corresponds to 95%, see note above
149  k = 2.447746830681
150 
151  angle = np.arctan2(v[1, 0], v[0, 0])
152  # We multiply k by 2 since k corresponds to the radius but Ellipse uses
153  # the diameter.
154  e1 = patches.Ellipse(origin,
155  np.sqrt(w[0]) * 2 * k,
156  np.sqrt(w[1]) * 2 * k,
157  np.rad2deg(angle),
158  fill=False)
159  axes.add_patch(e1)
160 
161 
162 def plot_point2_on_axes(axes,
163  point: Point2,
164  linespec: str,
165  P: Optional[np.ndarray] = None) -> None:
166  """
167  Plot a 2D point and its corresponding uncertainty ellipse on given axis
168  `axes` with given `linespec`.
169 
170  The uncertainty ellipse (if covariance is given) is scaled in such a way
171  that 95% of drawn samples are inliers, see `plot_covariance_ellipse_2d`.
172 
173  Args:
174  axes (matplotlib.axes.Axes): Matplotlib axes.
175  point: The point to be plotted.
176  linespec: String representing formatting options for Matplotlib.
177  P: Marginal covariance matrix to plot the uncertainty of the estimation.
178  """
179  axes.plot([point[0]], [point[1]], linespec, marker='.', markersize=10)
180  if P is not None:
181  plot_covariance_ellipse_2d(axes, point, P)
182 
183 def plot_point2(
184  fignum: int,
185  point: Point2,
186  linespec: str,
187  P: np.ndarray = None,
188  axis_labels: Iterable[str] = ("X axis", "Y axis"),
189 ) -> plt.Figure:
190  """
191  Plot a 2D point on given figure with given `linespec`.
192 
193  The uncertainty ellipse (if covariance is given) is scaled in such a way
194  that 95% of drawn samples are inliers, see `plot_covariance_ellipse_2d`.
195 
196  Args:
197  fignum: Integer representing the figure number to use for plotting.
198  point: The point to be plotted.
199  linespec: String representing formatting options for Matplotlib.
200  P: Marginal covariance matrix to plot the uncertainty of the estimation.
201  axis_labels: List of axis labels to set.
202 
203  Returns:
204  fig: The matplotlib figure.
205 
206  """
207  fig = plt.figure(fignum)
208  axes = fig.gca()
209  plot_point2_on_axes(axes, point, linespec, P)
210 
211  axes.set_xlabel(axis_labels[0])
212  axes.set_ylabel(axis_labels[1])
213 
214  return fig
215 
216 
217 def plot_pose2_on_axes(axes,
218  pose: Pose2,
219  axis_length: float = 0.1,
220  covariance: np.ndarray = None) -> None:
221  """
222  Plot a 2D pose on given axis `axes` with given `axis_length`.
223 
224  The ellipse is scaled in such a way that 95% of drawn samples are inliers,
225  see `plot_covariance_ellipse_2d`.
226 
227  Args:
228  axes (matplotlib.axes.Axes): Matplotlib axes.
229  pose: The pose to be plotted.
230  axis_length: The length of the camera axes.
231  covariance (numpy.ndarray): Marginal covariance matrix to plot
232  the uncertainty of the estimation.
233  """
234  # get rotation and translation (center)
235  gRp = pose.rotation().matrix() # rotation from pose to global
236  t = pose.translation()
237  origin = t
238 
239  # draw the camera axes
240  x_axis = origin + gRp[:, 0] * axis_length
241  line = np.append(origin[np.newaxis], x_axis[np.newaxis], axis=0)
242  axes.plot(line[:, 0], line[:, 1], 'r-')
243 
244  y_axis = origin + gRp[:, 1] * axis_length
245  line = np.append(origin[np.newaxis], y_axis[np.newaxis], axis=0)
246  axes.plot(line[:, 0], line[:, 1], 'g-')
247 
248  if covariance is not None:
249  pPp = covariance[0:2, 0:2]
250  gPp = np.matmul(np.matmul(gRp, pPp), gRp.T)
251  plot_covariance_ellipse_2d(axes, origin, gPp)
252 
253 
254 def plot_pose2(
255  fignum: int,
256  pose: Pose2,
257  axis_length: float = 0.1,
258  covariance: np.ndarray = None,
259  axis_labels=("X axis", "Y axis", "Z axis"),
260 ) -> plt.Figure:
261  """
262  Plot a 2D pose on given figure with given `axis_length`.
263 
264  The uncertainty ellipse (if covariance is given) is scaled in such a way
265  that 95% of drawn samples are inliers, see `plot_covariance_ellipse_2d`.
266 
267  Args:
268  fignum: Integer representing the figure number to use for plotting.
269  pose: The pose to be plotted.
270  axis_length: The length of the camera axes.
271  covariance: Marginal covariance matrix to plot
272  the uncertainty of the estimation.
273  axis_labels (iterable[string]): List of axis labels to set.
274  """
275  # get figure object
276  fig = plt.figure(fignum)
277  axes = fig.gca()
278  plot_pose2_on_axes(axes,
279  pose,
280  axis_length=axis_length,
281  covariance=covariance)
282 
283  axes.set_xlabel(axis_labels[0])
284  axes.set_ylabel(axis_labels[1])
285 
286  return fig
287 
288 
289 def plot_point3_on_axes(axes,
290  point: Point3,
291  linespec: str,
292  P: Optional[np.ndarray] = None) -> None:
293  """
294  Plot a 3D point on given axis `axes` with given `linespec`.
295 
296  The uncertainty ellipse (if covariance is given) is scaled in such a way
297  that 95% of drawn samples are inliers, see `plot_covariance_ellipse_3d`.
298 
299  Args:
300  axes (matplotlib.axes.Axes): Matplotlib axes.
301  point: The point to be plotted.
302  linespec: String representing formatting options for Matplotlib.
303  P: Marginal covariance matrix to plot the uncertainty of the estimation.
304  """
305  axes.plot([point[0]], [point[1]], [point[2]], linespec)
306  if P is not None:
307  plot_covariance_ellipse_3d(axes, point, P)
308 
309 
310 def plot_point3(
311  fignum: int,
312  point: Point3,
313  linespec: str,
314  P: np.ndarray = None,
315  axis_labels: Iterable[str] = ("X axis", "Y axis", "Z axis"),
316 ) -> plt.Figure:
317  """
318  Plot a 3D point on given figure with given `linespec`.
319 
320  The uncertainty ellipse (if covariance is given) is scaled in such a way
321  that 95% of drawn samples are inliers, see `plot_covariance_ellipse_3d`.
322 
323  Args:
324  fignum: Integer representing the figure number to use for plotting.
325  point: The point to be plotted.
326  linespec: String representing formatting options for Matplotlib.
327  P: Marginal covariance matrix to plot the uncertainty of the estimation.
328  axis_labels: List of axis labels to set.
329 
330  Returns:
331  fig: The matplotlib figure.
332 
333  """
334  fig = plt.figure(fignum)
335  if not fig.axes:
336  axes = fig.add_subplot(projection='3d')
337  else:
338  axes = fig.axes[0]
339  plot_point3_on_axes(axes, point, linespec, P)
340 
341  axes.set_xlabel(axis_labels[0])
342  axes.set_ylabel(axis_labels[1])
343  axes.set_zlabel(axis_labels[2])
344 
345  return fig
346 
347 
348 def plot_3d_points(fignum,
349  values,
350  linespec="g*",
351  marginals=None,
352  title="3D Points",
353  axis_labels=('X axis', 'Y axis', 'Z axis')):
354  """
355  Plots the Point3s in `values`, with optional covariances.
356  Finds all the Point3 objects in the given Values object and plots them.
357  If a Marginals object is given, this function will also plot marginal
358  covariance ellipses for each point.
359 
360  Args:
361  fignum (int): Integer representing the figure number to use for plotting.
362  values (gtsam.Values): Values dictionary consisting of points to be plotted.
363  linespec (string): String representing formatting options for Matplotlib.
364  marginals (numpy.ndarray): Marginal covariance matrix to plot the
365  uncertainty of the estimation.
366  title (string): The title of the plot.
367  axis_labels (iterable[string]): List of axis labels to set.
368  """
369 
370  keys = values.keys()
371 
372  # Plot points and covariance matrices
373  for key in keys:
374  try:
375  point = values.atPoint3(key)
376  if marginals is not None:
377  covariance = marginals.marginalCovariance(key)
378  else:
379  covariance = None
380 
381  fig = plot_point3(fignum,
382  point,
383  linespec,
384  covariance,
385  axis_labels=axis_labels)
386 
387  except RuntimeError:
388  continue
389  # I guess it's not a Point3
390 
391  fig = plt.figure(fignum)
392  fig.suptitle(title)
393  fig.canvas.manager.set_window_title(title.lower())
394 
395 
396 def plot_pose3_on_axes(axes, pose, axis_length=0.1, P=None, scale=1):
397  """
398  Plot a 3D pose on given axis `axes` with given `axis_length`.
399 
400  The uncertainty ellipse (if covariance is given) is scaled in such a way
401  that 95% of drawn samples are inliers, see `plot_covariance_ellipse_3d`.
402 
403  Args:
404  axes (matplotlib.axes.Axes): Matplotlib axes.
405  point (gtsam.Point3): The point to be plotted.
406  linespec (string): String representing formatting options for Matplotlib.
407  P (numpy.ndarray): Marginal covariance matrix to plot the uncertainty of the estimation.
408  """
409  # get rotation and translation (center)
410  gRp = pose.rotation().matrix() # rotation from pose to global
411  origin = pose.translation()
412 
413  # draw the camera axes
414  x_axis = origin + gRp[:, 0] * axis_length
415  line = np.append(origin[np.newaxis], x_axis[np.newaxis], axis=0)
416  axes.plot(line[:, 0], line[:, 1], line[:, 2], 'r-')
417 
418  y_axis = origin + gRp[:, 1] * axis_length
419  line = np.append(origin[np.newaxis], y_axis[np.newaxis], axis=0)
420  axes.plot(line[:, 0], line[:, 1], line[:, 2], 'g-')
421 
422  z_axis = origin + gRp[:, 2] * axis_length
423  line = np.append(origin[np.newaxis], z_axis[np.newaxis], axis=0)
424  axes.plot(line[:, 0], line[:, 1], line[:, 2], 'b-')
425 
426  # plot the covariance
427  if P is not None:
428  # covariance matrix in pose coordinate frame
429  pPp = P[3:6, 3:6]
430  # convert the covariance matrix to global coordinate frame
431  gPp = gRp @ pPp @ gRp.T
432  plot_covariance_ellipse_3d(axes, origin, gPp)
433 
434 
435 def plot_pose3(
436  fignum: int,
437  pose: Pose3,
438  axis_length: float = 0.1,
439  P: np.ndarray = None,
440  axis_labels: Iterable[str] = ("X axis", "Y axis", "Z axis"),
441 ) -> plt.Figure:
442  """
443  Plot a 3D pose on given figure with given `axis_length`.
444 
445  The uncertainty ellipse (if covariance is given) is scaled in such a way
446  that 95% of drawn samples are inliers, see `plot_covariance_ellipse_3d`.
447 
448  Args:
449  fignum: Integer representing the figure number to use for plotting.
450  pose (gtsam.Pose3): 3D pose to be plotted.
451  axis_length: The length of the camera axes.
452  P: Marginal covariance matrix to plot the uncertainty of the estimation.
453  axis_labels: List of axis labels to set.
454 
455  Returns:
456  fig: The matplotlib figure.
457  """
458  # get figure object
459  fig = plt.figure(fignum)
460  if not fig.axes:
461  axes = fig.add_subplot(projection='3d')
462  else:
463  axes = fig.axes[0]
464 
465  plot_pose3_on_axes(axes, pose, P=P, axis_length=axis_length)
466 
467  axes.set_xlabel(axis_labels[0])
468  axes.set_ylabel(axis_labels[1])
469  axes.set_zlabel(axis_labels[2])
470 
471  return fig
472 
473 
474 def plot_trajectory(
475  fignum: int,
476  values: Values,
477  scale: float = 1,
478  marginals: Marginals = None,
479  title: str = "Plot Trajectory",
480  axis_labels: Iterable[str] = ("X axis", "Y axis", "Z axis"),
481 ) -> None:
482  """
483  Plot a complete 2D/3D trajectory using poses in `values`.
484 
485  Args:
486  fignum: Integer representing the figure number to use for plotting.
487  values: Values containing some Pose2 and/or Pose3 values.
488  scale: Value to scale the poses by.
489  marginals: Marginalized probability values of the estimation.
490  Used to plot uncertainty bounds.
491  title: The title of the plot.
492  axis_labels (iterable[string]): List of axis labels to set.
493  """
494  fig = plt.figure(fignum)
495  if not fig.axes:
496  axes = fig.add_subplot(projection='3d')
497  else:
498  axes = fig.axes[0]
499 
500  axes.set_xlabel(axis_labels[0])
501  axes.set_ylabel(axis_labels[1])
502  axes.set_zlabel(axis_labels[2])
503 
504  # Plot 2D poses, if any
505  poses = gtsam.utilities.allPose2s(values)
506  for key in poses.keys():
507  pose = poses.atPose2(key)
508  if marginals:
509  covariance = marginals.marginalCovariance(key)
510  else:
511  covariance = None
512 
513  plot_pose2_on_axes(axes,
514  pose,
515  covariance=covariance,
516  axis_length=scale)
517 
518  # Then 3D poses, if any
519  poses = gtsam.utilities.allPose3s(values)
520  for key in poses.keys():
521  pose = poses.atPose3(key)
522  if marginals:
523  covariance = marginals.marginalCovariance(key)
524  else:
525  covariance = None
526 
527  plot_pose3_on_axes(axes, pose, P=covariance, axis_length=scale)
528 
529  fig.suptitle(title)
530  fig.canvas.manager.set_window_title(title.lower())
531 
532 
534  values: Values,
535  start: int = 0,
536  scale: float = 1,
537  marginals: Optional[Marginals] = None,
538  time_interval: float = 0.0) -> None:
539  """
540  Incrementally plot a complete 3D trajectory using poses in `values`.
541 
542  Args:
543  fignum: Integer representing the figure number to use for plotting.
544  values: Values dict containing the poses.
545  start: Starting index to start plotting from.
546  scale: Value to scale the poses by.
547  marginals: Marginalized probability values of the estimation.
548  Used to plot uncertainty bounds.
549  time_interval: Time in seconds to pause between each rendering.
550  Used to create animation effect.
551  """
552  fig = plt.figure(fignum)
553  if not fig.axes:
554  axes = fig.add_subplot(projection='3d')
555  else:
556  axes = fig.axes[0]
557 
558  poses = gtsam.utilities.allPose3s(values)
559  keys = poses.keys()
560 
561  for key in keys[start:]:
562  if values.exists(key):
563  pose_i = values.atPose3(key)
564  plot_pose3(fignum, pose_i, scale)
565 
566  # Update the plot space to encompass all plotted points
567  axes.autoscale()
568 
569  # Set the 3 axes equal
570  set_axes_equal(fignum)
571 
572  # Pause for a fixed amount of seconds
573  plt.pause(time_interval)
def plot_pose2
Definition: plot.py:255
def plot_3d_points(fignum, values, linespec="g*", marginals=None, title="3D Points", axis_labels=('X axis', 'Y axis', 'Z axis'))
Definition: plot.py:353
def plot_pose3
Definition: plot.py:436
def plot_pose3_on_axes(axes, pose, axis_length=0.1, P=None, scale=1)
Definition: plot.py:396
def ellipsoid
Definition: plot.py:64
def plot_pose2_on_axes
Definition: plot.py:218
def plot_incremental_trajectory
Definition: plot.py:533
def set_axes_equal
Definition: plot.py:35
def plot_trajectory
Definition: plot.py:475
def plot_point3
Definition: plot.py:311
Values allPose3s(const Values &values)
Extract all Pose3 values.
def plot_point2
Definition: plot.py:184
def plot_point3_on_axes
Definition: plot.py:290
Values allPose2s(const Values &values)
Extract all Pose3 values.
def plot_point2_on_axes
Definition: plot.py:163
Map< Matrix< T, Dynamic, Dynamic, ColMajor >, 0, OuterStride<> > matrix(T *data, int rows, int cols, int stride)
def plot_covariance_ellipse_3d
Definition: plot.py:88
def plot_covariance_ellipse_2d
Definition: plot.py:131


gtsam
Author(s):
autogenerated on Tue Jul 4 2023 02:35:14