This module contains a library to be used from C++ that generates YAML files for data logs.
A Python script is then used to plot the files with various options.
- Required: C++ 17
- Required: Python3 Matplotlib (with Axes3D support)
- Optional:
pyvista
to use meshes in the plots
Version 3 has heavily changed the way to add fixed or moving objects to 3D plots. In particular, there is no notion of desired pose when plotting a moving object associated with a 3D pose. Just plot it as a fixed object at the desired pose.
As of version 2, the module is ROS-agnostic and is a classical CMake package:
mkdir build
cd build
cmake ..
make
orsudo make install
to use it from another project
The library can then be found through CMake find_package.
The plotting script will be placed at /usr/local/bin/log2plot
and can be used just typing log2plot <yaml data file>
.
The logger can also plot 2D or 3D graphs at runtime, by using -DENABLE_DYNAMIC_PLOT=ON with Cmake. This requires Python3 and Matplotlib.
In this case a new logger, namely LogPlotter
can be used instead of Logger
. This class requires a target rate and will try to update the plot within this rate, despite the possibly slow Python interpreter.
Examples can be found in the examples
folder. The main class is log2plot::Logger
and should be instanciated with the desired data file path and prefix: log2plot::Logger logger(fileprefix)
. If no fileprefix is given then the files will be created in the /tmp
folder. Shipped examples use the examples
path at compile time.
The logged variables have to be containers of some sort, as long as the following member functions are available:
operator[]
to get the value at a given indexsize()
to get the length of the logged container
Besides these two points, all kind of data can be saved, but of course they will not be plottable if not numerical.
Four types of data may be logged:
- Iteration-based data will use the index as the X-axis for the plots.
logger.save(v, name, legend, ylabel)
legend
should be a YAML-style list and may be using Latex:"[v_x, \\omega_z]"
- Time-based data has to be given a time and will use it as the X-axis.
logger.setTime(t, "s");
wheret
is adouble
logger.saveTimed(v, name, legend, ylabel)
legend
should be a YAML-style list and may be using Latex:"[v_x, \\omega_z]"
- XY-based data are defined as {x1, y1, x2, y2, ...}
logger.saveXY(v, legend, x-label, y-label)
legend
should be a YAML-style list with half the dimension of v`
- 3D pose data has to be given a 6-components pose vector (as in translation + angle-axis representation).
logger.save3Dpose(v, name, trajectory_name, invert_pose)
trajectory_name
should be a single stringinvert_pose
(default false) allows to log a pose whom inverse will be actually plotted. This can be useful typically when working with a world-to-camera pose but we still want to display the camera-to-world pose afterwards.
- Timed-XY-based data are defined as {x1, y1, x2, y2, ...}
logger.saveTimedXY(v, legend, x-label, y-label)
legend
should be a YAML-style list with one element- This data type is used to visualize a changing XY curve. Only the video option would be relevant in this case
This will log data into the file: fileprefix + name + .yaml
Log is actually done when calling logger.update();
, typically from inside a loop. Two parameters can be changed:
- Subsampling to log only once every n updates:
logger.setSubSampling(n)
(default 1) - Buffer size before writing to the file:
logger.setBuffer(b)
(default 10) - The plot can be done directly from C++ if needed:
logget.plot(script_path)
, wherescript_path
is the path to the Python script. The default value is the path at library compile time.
Options should be given before calling the first update()
.
The following commands will be applied to the last added variable:
- Units:
logger.setUnits("[unit1, unit2, unit2]");
will save the units for the 3 first components - Line styles:
logger.setLineType(["b, g, r--]");
, line styles have to be defined in Matplotlib styles (color + line style) - particular time steps:
logger.setSteps({});
, will display dashed vertical lines at those instant - Steps can also be added while recording with
logger.writeStep();
The log2plot::Shape
defines an arbitrary shape from a set of nodes (vector<SomePoints>
) and graph (vector<vector<size_t>>
).
The default graph is empty, leading to point clouds.
Such a shape can be applied to the last added variable:
logger.showMovingShape(log2plot::Shape)
for 2D graphslogger.showMovingShape(log2plot::Shape, log2plot::Surface = log2plot::PointCloud)
for 3D graphs- Using surfaces other than
log2plot::PointCloud
requires the Python modulepyvista
log2plot::PointCloud
(default): does not reconstruct any surface from the nodeslog2plot::ConvexHull
: computes the convex hull of the given point cloudlog2plot::Surface
: reconstructs a smooth surface out of the point cloudlog2plot::AlphaShape
: reconstructs an alpha-shape from the point cloudlog2plot::Faces
: plots the surface according to the vertex index in the graph
- if the color of the
Shape
includes any marker (such asbD
) then the points will also be displayed along the surface
- Using surfaces other than
A few builtin shapes are defines for 3D plots:
log2plot::Camera
: a camera that will scale to the axis sizelog2plot::Box
: a box defined by its lower and upper cornerslog2plot::Frame
: a 3D RGB frame that will scale to the axis size
Once defined, a Shape
can be modified through Shape.transform(pose, color, legend)
. This is useful when displaying several times the same shape at various places with different colors and legends.
If some (double) logged data is non defined or irrelevant at some point, it is possible to keep logging but write Not a Number so that it will not be plotted. The syntax is:
v[0] = log2plot::nan;
to erase only one component.log2plot::setNaN(v, 0, 2);
to erase components 0 and 1 from the v vector or array.
The log2plot/loader.h
header defines a log2plot::Log
class that takes a file path in the constructor.
The resulting Log
will get data from the file, allowing to re-process or re-populate it if needed
A basic Python wrapper (only for the Logger
class) is proposed and installed if PYTHON_BINDINGS
is set to True
(default).
It relies on cppyy
, install it with: pip3 install cppyy
.
Please look at the from_python.py
example. Note that the logged variables have to be underlying C++ objects. The way to do it is:
- Initialize the logger:
logger = log2plot.Logger(<base path>)
- initialize object:
v = log2plot.Vec(5)
for a vector of dimension 5 - save it through your logger :
logger.save(v, path and legends)
- when logging your data e.g.
my_array
, copy it tov
:log2plot.copy(my_array, v)
logger.update()
The Python script used to plot the files is in the src
folder and requires matplotlib
, YAML
, and argparse
. It may be useful to re-plot a file with different options. The script can be called from the command line:
python3 path/to/log2plot/src/plot <file.yaml>
(if not installed)log2plot <file.yaml>
(if installed)
Many (probably too many) options are available from the command line, call plot -h
to have a list. Several files can be plotted at the same time, in this case if they have the same y-label their y-axis will be at the same scale. By default they will be plotted in different subplots, but can be plotted in the same plot with the -g
option.
Videos can be created using the -v <subsampling>
option. ffmpeg or avconv will be used to create a mp4 file showing the plot evolution. Similarly, passing --gif
uses pillow or imagemagick to create an animated gif.
In the examples
folder are shipped 4 use cases:
std_container
uses std::vectors and shows iteration-based, time-based and 3D pose plots. It also shows how to use Not a Number for iterations where some logged values are not defined.visp_containers
uses containers from the ViSP library (vpColVector and vpPoseVector) and logs an inverted 3D poseeigen_containers
uses containers from the Eigen library (Eigen::Vector3d)animation
shows how to perform a plot during runtimetimed_xy
is an example of time-varying XY trajectory, only useful if saved as a videostatic_3d
shows a 3D plot with only static 3D objects, some of which are displayed with a meshfrom_python.py
shows how to use the logger from Python
If the option BUILD_PARSER
is set to True (default) then a log2plot::ConfigManager
class is also available. It allows easy loading of a configuration file written in Yaml through the templated read
method.
The configuration manager can also generate dynamically suitable names for experimental files through the following methods:
setDirName(std::string s)
addNameElement(std::string str)
addNameElement(std::string pref, T val)
addConditionalNameElement(std::string strTrue, bool condition, std::string strFalse)
fullName()
: outputs the resulting file name from all above informationsaveConfig()
: saves underfullName() + _config.yaml
See the corresponding example in examples/parser
YAML files that comply with the log2plot
representation can be loaded into PlotJuggler through the plugin available in plotjuggler
folder.
See the corresponding readme to compile it.