Skip to content

Figures

plots

The figures trajectopy offers for trajectories, evaluation results and alignment parameters.

The functions in this module only describe figures (see :class:~trajectopy.visualization.figure.Figure); nothing is drawn until a figure is shown or exported. The collections :func:trajectory_figures, :func:deviation_figures and :func:alignment_figures are what the GUI displays.

positions_figure

positions_figure(
    trajectories: Sequence[Trajectory],
    settings: PlotSettings | None = None,
) -> Figure

Top view (x/y) of the trajectories.

Source code in trajectopy\visualization\plots.py
def positions_figure(trajectories: Sequence[Trajectory], settings: PlotSettings | None = None) -> Figure:
    """Top view (x/y) of the trajectories."""
    settings = _settings(settings)

    def render(layout: pg.GraphicsLayout) -> None:
        view = _top_view(trajectories)
        plot = _top_view_plot(layout, settings, view, legend=True)
        for index, (trajectory, xy) in enumerate(zip(trajectories, view.xy)):
            xy = _thin(xy, MAX_PATH_POINTS)
            _line(plot, xy[:, 0], xy[:, 1], index, trajectory.name, settings)

    return Figure("Positions", render)

xyz_figure

xyz_figure(
    trajectories: Sequence[Trajectory],
    settings: PlotSettings | None = None,
) -> Figure

Coordinates of the trajectories over time or path length.

Source code in trajectopy\visualization\plots.py
def xyz_figure(trajectories: Sequence[Trajectory], settings: PlotSettings | None = None) -> Figure:
    """Coordinates of the trajectories over time or path length."""
    settings = _settings(settings)
    axis = index_axis(trajectories, settings.use_relative_timestamps)
    labels = get_axis_label(trajectories)

    def render(layout: pg.GraphicsLayout) -> None:
        plots = []
        for dim, label in enumerate(labels):
            plot = _add_plot(
                layout,
                dim,
                settings,
                x_label=axis.label if dim == 2 else "",
                y_label=label,
                datetime_axis=axis.is_datetime,
                legend=dim == 0,
            )
            for index, trajectory in enumerate(trajectories):
                _line(plot, axis.values(trajectory), trajectory.xyz[:, dim], index, trajectory.name, settings)
            plots.append(plot)
        _link_x(plots)
        _link_visibility(plots)

    return Figure("XYZ", render)

rpy_figure

rpy_figure(
    trajectories: Sequence[Trajectory],
    settings: PlotSettings | None = None,
) -> Figure | None

Roll, pitch and yaw of the trajectories over time or path length (None without orientations).

Source code in trajectopy\visualization\plots.py
def rpy_figure(trajectories: Sequence[Trajectory], settings: PlotSettings | None = None) -> Figure | None:
    """Roll, pitch and yaw of the trajectories over time or path length (None without orientations)."""
    settings = _settings(settings)
    oriented = [trajectory for trajectory in trajectories if trajectory.has_orientation]
    if not oriented:
        return None
    axis = index_axis(oriented, settings.use_relative_timestamps)

    def render(layout: pg.GraphicsLayout) -> None:
        rpy = {id(trajectory): np.rad2deg(trajectory.rpy) for trajectory in oriented}
        plots = []
        for dim, label in enumerate(("roll [°]", "pitch [°]", "yaw [°]")):
            plot = _add_plot(
                layout,
                dim,
                settings,
                x_label=axis.label if dim == 2 else "",
                y_label=label,
                datetime_axis=axis.is_datetime,
                legend=dim == 0,
            )
            for index, trajectory in enumerate(trajectories):
                if id(trajectory) in rpy:
                    _line(plot, axis.values(trajectory), rpy[id(trajectory)][:, dim], index, trajectory.name, settings)
            plots.append(plot)
        _link_x(plots)
        _link_visibility(plots)

    return Figure("RPY", render)

trajectory_map_figure

trajectory_map_figure(
    trajectories: Sequence[Trajectory],
    settings: PlotSettings | None = None,
) -> Figure

Top view of each trajectory colored by velocity or height.

Source code in trajectopy\visualization\plots.py
def trajectory_map_figure(trajectories: Sequence[Trajectory], settings: PlotSettings | None = None) -> Figure:
    """Top view of each trajectory colored by velocity or height."""
    settings = _settings(settings)
    z_label = get_axis_label(trajectories)[2]
    multiple = len(trajectories) > 1
    variants: dict[str, RenderFunction] = {}
    for trajectory in trajectories:
        quantities = {"Velocity": (trajectory.absolute_velocity, "velocity [m/s]")}
        quantities["Height"] = (trajectory.xyz[:, 2], z_label)
        for quantity, (values, label) in quantities.items():

            def render(layout, trajectory=trajectory, values=values, label=label) -> None:
                _value_map(layout, trajectory, values, label, settings)

            variants[_variant_name(trajectory.name, quantity, multiple)] = render
    return Figure("Map", variants=variants)

trajectory_figures

trajectory_figures(
    trajectories: Sequence[Trajectory],
    settings: PlotSettings | None = None,
) -> list[Figure]

All figures describing the given trajectories.

Source code in trajectopy\visualization\plots.py
def trajectory_figures(trajectories: Sequence[Trajectory], settings: PlotSettings | None = None) -> list[Figure]:
    """All figures describing the given trajectories."""
    if not trajectories:
        return []
    figures = [
        positions_figure(trajectories, settings),
        xyz_figure(trajectories, settings),
        rpy_figure(trajectories, settings),
        trajectory_map_figure(trajectories, settings),
    ]
    return [figure for figure in figures if figure is not None]

ate_figure

ate_figure(
    ate_results: Sequence[ATEResult],
    settings: PlotSettings | None = None,
) -> Figure

Position (and rotation) deviations over time or path length.

Source code in trajectopy\visualization\plots.py
def ate_figure(ate_results: Sequence[ATEResult], settings: PlotSettings | None = None) -> Figure:
    """Position (and rotation) deviations over time or path length."""
    settings = _settings(settings)
    axis = index_axis([result.trajectory for result in ate_results], settings.use_relative_timestamps)
    with_rotation = any(result.has_orientation for result in ate_results)

    def render(layout: pg.GraphicsLayout) -> None:
        position_plot = _add_plot(
            layout,
            0,
            settings,
            x_label="" if with_rotation else axis.label,
            y_label=f"position deviation [{settings.unit}]",
            datetime_axis=axis.is_datetime,
            legend=True,
        )
        plots = [position_plot]
        if with_rotation:
            plots.append(
                _add_plot(
                    layout,
                    1,
                    settings,
                    x_label=axis.label,
                    y_label="rotation deviation [°]",
                    datetime_axis=axis.is_datetime,
                )
            )
        for index, result in enumerate(ate_results):
            x = axis.values(result.trajectory)
            _line(position_plot, x, result.pos_dev_comb * settings.unit_multiplier, index, result.name, settings)
            if with_rotation and result.has_orientation:
                _line(plots[1], x, np.rad2deg(result.rot_dev_comb), index, result.name, settings)
        _link_x(plots)
        _link_visibility(plots)

    return Figure("ATE", render)

ate_components_figure

ate_components_figure(
    ate_results: Sequence[ATEResult],
    settings: PlotSettings | None = None,
) -> Figure

Deviations of the single degrees of freedom over time or path length.

Source code in trajectopy\visualization\plots.py
def ate_components_figure(ate_results: Sequence[ATEResult], settings: PlotSettings | None = None) -> Figure:
    """Deviations of the single degrees of freedom over time or path length."""
    settings = _settings(settings)
    variants: dict[str, RenderFunction] = {}
    for result in ate_results:
        axis = index_axis([result.trajectory], settings.use_relative_timestamps)

        def render(layout: pg.GraphicsLayout, result=result, axis=axis) -> None:
            groups = [(_position_components(settings), f"position deviation [{settings.unit}]")]
            if result.has_orientation:
                groups.append((ROTATION_COMPONENTS, "rotation deviation [°]"))
            plots = []
            for row, (components, y_label) in enumerate(groups):
                plot = _add_plot(
                    layout,
                    row,
                    settings,
                    x_label=axis.label if row == len(groups) - 1 else "",
                    y_label=y_label,
                    datetime_axis=axis.is_datetime,
                    legend=True,
                )
                x = axis.values(result.trajectory)
                for dim, component in enumerate(components):
                    plot.plot(
                        x,
                        _component_values(component, result, settings),
                        pen=style.pen(style.AXIS_COLORS[dim], settings.line_width),
                        name=component.name,
                        antialias=style.antialias(len(x)),
                    )
                plots.append(plot)
            _link_x(plots)

        variants[result.name] = render
    return Figure("ATE Components", variants=variants)

ate_histogram_figure

ate_histogram_figure(
    ate_results: Sequence[ATEResult],
    settings: PlotSettings | None = None,
) -> Figure

Histograms of the deviations of the single degrees of freedom.

Source code in trajectopy\visualization\plots.py
def ate_histogram_figure(ate_results: Sequence[ATEResult], settings: PlotSettings | None = None) -> Figure:
    """Histograms of the deviations of the single degrees of freedom."""
    settings = _settings(settings)
    variants: dict[str, RenderFunction] = {}
    for result in ate_results:

        def render(layout: pg.GraphicsLayout, result=result) -> None:
            groups = [(_position_components(settings), f"position deviation [{settings.unit}]")]
            if result.has_orientation:
                groups.append((ROTATION_COMPONENTS, "rotation deviation [°]"))
            for row, (components, x_label) in enumerate(groups):
                plot = _add_plot(layout, row, settings, x_label=x_label, y_label="count", legend=True)
                plot.setClipToView(False)
                plot.setDownsampling(auto=False)
                for dim, component in enumerate(components):
                    values = _component_values(component, result, settings)
                    _histogram(plot, values, dim, component.name, settings.hist_percentile)

        variants[result.name] = render
    return Figure("ATE Histograms", variants=variants)

ate_statistics_figure

ate_statistics_figure(
    ate_results: Sequence[ATEResult],
    settings: PlotSettings | None = None,
) -> Figure

Min, max, mean, median, RMS and standard deviation of the deviations as bar charts.

Source code in trajectopy\visualization\plots.py
def ate_statistics_figure(ate_results: Sequence[ATEResult], settings: PlotSettings | None = None) -> Figure:
    """Min, max, mean, median, RMS and standard deviation of the deviations as bar charts."""
    settings = _settings(settings)
    with_rotation = any(result.has_orientation for result in ate_results)

    def statistics(result: ATEResult, rotation: bool) -> list[float]:
        # same definitions as the ATEResult properties, but each combined deviation is computed only once
        values = np.rad2deg(result.rot_dev_comb) if rotation else result.pos_dev_comb * settings.unit_multiplier
        return [
            float(np.min(values)),
            float(np.max(values)),
            float(np.mean(values)),
            float(np.median(values)),
            rms(values),
            float(np.std(values)),
        ]

    def render(layout: pg.GraphicsLayout) -> None:
        groups = [(False, f"position deviation [{settings.unit}]")]
        if with_rotation:
            groups.append((True, "rotation deviation [°]"))
        width = 0.8 / len(ate_results)
        for row, (rotation, y_label) in enumerate(groups):
            plot = _add_plot(layout, row, settings, y_label=y_label, legend=True)
            plot.setClipToView(False)
            plot.setDownsampling(auto=False)
            plot.getAxis("bottom").setTicks([list(enumerate(STATISTICS))])
            for index, result in enumerate(ate_results):
                if rotation and not result.has_orientation:
                    continue
                offset = (index - (len(ate_results) - 1) / 2) * width
                bars = pg.BarGraphItem(
                    x=np.arange(len(STATISTICS)) + offset,
                    height=statistics(result, rotation),
                    width=width,
                    brush=style.brush(index),
                    pen=None,
                )
                plot.addItem(bars)
                if plot.legend is not None:
                    plot.legend.addItem(bars, result.name)

    return Figure("ATE Statistics", render)

ate_correlation_figure

ate_correlation_figure(
    ate_results: Sequence[ATEResult],
    settings: PlotSettings | None = None,
) -> Figure

Temporal correlation of the deviations: autocorrelation over the time lag (with the level that defines the correlation time of each component, 1/e of its correlated share, and the correlation time as a point) and the standard deviation in consecutive time windows.

Source code in trajectopy\visualization\plots.py
def ate_correlation_figure(ate_results: Sequence[ATEResult], settings: PlotSettings | None = None) -> Figure:
    """Temporal correlation of the deviations: autocorrelation over the time lag (with the level that defines the
    correlation time of each component, 1/e of its correlated share, and the correlation time as a point) and the
    standard deviation in consecutive time windows."""
    settings = _settings(settings)
    variants: dict[str, RenderFunction] = {}
    for result in ate_results:

        def render(layout: pg.GraphicsLayout, result=result) -> None:
            correlation = result.correlation
            names = ("along", "cross_h", "cross_v") if settings.directed_ate else ("x", "y", "z")
            groups = [(names, "position")]
            rotations = ("roll", "pitch", "yaw")
            # only with rotation deviations (without any, there is no autocorrelation)
            if result.has_orientation and any(
                np.isfinite(correlation.components[name].autocorrelation[0]) for name in rotations
            ):
                groups.append((rotations, "rotation"))
            plots = []
            for row, (components, quantity) in enumerate(groups):
                plot = _add_plot(
                    layout, row, settings, x_label="time lag [s]", y_label=f"autocorrelation ({quantity})", legend=True
                )
                for index, name in enumerate(components):
                    component = correlation.components[name]
                    tau = component.correlation_time
                    tau_text = "> max. lag" if np.isinf(tau) else f"{tau:.2f} s"
                    white = f"{100 * component.white_noise_share:.0f} % white noise"
                    label = f"{COMPONENT_LABELS[name]} ({tau_text}, {white})"
                    valid = np.isfinite(component.autocorrelation)
                    curve = _line(
                        plot, component.lags[valid], component.autocorrelation[valid], index, label, settings
                    )
                    _mark_correlation_time(plot, curve, component, index)
                plots.append(plot)
            _link_x(plots)

            plot = _add_plot(
                layout,
                len(groups),
                settings,
                x_label="time [s]",
                y_label=f"std. dev. per window [{settings.unit}]",
                legend=True,
            )
            start = float(np.min(result.trajectory.timestamps)) if len(result.trajectory) else 0.0
            for index, name in enumerate(names):
                component = correlation.components[name]
                plot.plot(
                    component.window_centers - start,
                    component.window_std * settings.unit_multiplier,
                    pen=style.pen(index, settings.line_width),
                    symbol="o",
                    symbolSize=5,
                    symbolBrush=style.brush(index),
                    symbolPen=None,
                    name=COMPONENT_LABELS[name],
                )

        variants[result.name] = render
    return Figure("ATE Correlation", variants=variants)

ate_distribution_figure

ate_distribution_figure(
    ate_results: Sequence[ATEResult],
    settings: PlotSettings | None = None,
) -> Figure

Empirical cumulative distribution of the position (and rotation) deviations.

Source code in trajectopy\visualization\plots.py
def ate_distribution_figure(ate_results: Sequence[ATEResult], settings: PlotSettings | None = None) -> Figure:
    """Empirical cumulative distribution of the position (and rotation) deviations."""
    settings = _settings(settings)
    with_rotation = any(result.has_orientation for result in ate_results)

    def edf(values: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
        values = np.sort(values[np.isfinite(values)])
        return values, np.arange(1, len(values) + 1) / max(len(values), 1)

    def render(layout: pg.GraphicsLayout) -> None:
        position_plot = _add_plot(
            layout,
            0,
            settings,
            x_label=f"position deviation [{settings.unit}]",
            y_label="cumulative probability",
            legend=True,
        )
        rotation_plot = (
            _add_plot(layout, 1, settings, x_label="rotation deviation [°]", y_label="cumulative probability")
            if with_rotation
            else None
        )
        for index, result in enumerate(ate_results):
            _line(position_plot, *edf(result.pos_dev_comb * settings.unit_multiplier), index, result.name, settings)
            if rotation_plot is not None and result.has_orientation:
                _line(rotation_plot, *edf(np.rad2deg(result.rot_dev_comb)), index, None, settings)

    return Figure("ATE Distribution", render)

ate_map_figure

ate_map_figure(
    ate_results: Sequence[ATEResult],
    settings: PlotSettings | None = None,
) -> Figure

Top view of the evaluated trajectories colored by their deviations.

Source code in trajectopy\visualization\plots.py
def ate_map_figure(ate_results: Sequence[ATEResult], settings: PlotSettings | None = None) -> Figure:
    """Top view of the evaluated trajectories colored by their deviations."""
    settings = _settings(settings)
    multiple = len(ate_results) > 1
    variants: dict[str, RenderFunction] = {}
    for result in ate_results:
        quantities: list[tuple[str, np.ndarray, str]] = [
            ("Position", result.pos_dev_comb * settings.unit_multiplier, f"deviation [{settings.unit}]")
        ]
        if result.has_orientation:
            quantities.append(("Rotation", np.rad2deg(result.rot_dev_comb), "deviation [°]"))
        components = list(_position_components(settings))
        if result.has_orientation:
            components += ROTATION_COMPONENTS
        for component in components:
            unit = "°" if component.is_rotation else settings.unit
            values = _component_values(component, result, settings)
            quantities.append((component.name, values, f"deviation [{unit}]"))

        for quantity, values, label in quantities:

            def render(layout, result=result, values=values, label=label) -> None:
                _value_map(layout, result.trajectory, values, label, settings)

            variants[_variant_name(result.name, quantity, multiple)] = render
    return Figure("ATE Map", variants=variants)

rpe_figures

rpe_figures(
    rpe_results: Sequence[RPEResult],
    settings: PlotSettings | None = None,
) -> list[Figure]

Relative pose errors over the pair distance, one figure per pair distance unit.

Source code in trajectopy\visualization\plots.py
def rpe_figures(rpe_results: Sequence[RPEResult], settings: PlotSettings | None = None) -> list[Figure]:
    """Relative pose errors over the pair distance, one figure per pair distance unit."""
    settings = _settings(settings)
    figures = []
    for unit, title in ((PairDistanceUnit.METER, "RPE (per meter)"), (PairDistanceUnit.SECOND, "RPE (per second)")):
        results = [result for result in rpe_results if result.rpe_dev.pair_distance_unit == unit]
        if not results:
            continue

        def render(layout: pg.GraphicsLayout, results=results) -> None:
            first = results[0]
            with_rotation = any(result.has_rot_dev for result in results)
            position_plot = _add_plot(
                layout,
                0,
                settings,
                x_label="" if with_rotation else f"pair distance [{first.pair_distance_unit}]",
                y_label=f"position drift [{first.pos_drift_unit}]",
                legend=True,
            )
            plots = [position_plot]
            if with_rotation:
                plots.append(
                    _add_plot(
                        layout,
                        1,
                        settings,
                        x_label=f"pair distance [{first.pair_distance_unit}]",
                        y_label=f"rotation drift [{first.rot_drift_unit}]",
                    )
                )
            for index, result in enumerate(results):
                x = np.asarray(result.mean_pair_distances)
                series: list[tuple[pg.PlotItem, np.ndarray, np.ndarray, str | None]] = [
                    (position_plot, np.asarray(result.pos_dev_mean), np.asarray(result.pos_std), result.name)
                ]
                if with_rotation and result.has_rot_dev:
                    series.append((plots[1], np.rad2deg(result.rot_dev_mean), np.rad2deg(result.rot_std), result.name))
                for plot, mean, std, name in series:
                    error_bars = pg.ErrorBarItem(x=x, y=mean, height=2 * std, beam=0, pen=style.pen(index, 1.0))
                    plot.addItem(error_bars)
                    curve = plot.plot(
                        x,
                        mean,
                        pen=style.pen(index, settings.line_width),
                        symbol="o",
                        symbolSize=6,
                        symbolBrush=style.brush(index),
                        symbolPen=None,
                        name=name,
                        antialias=True,
                    )
                    _follow_visibility(error_bars, curve)
            for plot in plots:
                plot.setClipToView(False)
            _link_x(plots)
            _link_visibility(plots)

        figures.append(Figure(title, render))
    return figures

deviation_figures

deviation_figures(
    ate_results: Sequence[ATEResult] = (),
    rpe_results: Sequence[RPEResult] = (),
    settings: PlotSettings | None = None,
) -> list[Figure]

All figures describing absolute (ATE) and relative (RPE) evaluation results.

The same figures are used for one and for several results; figures that show one result at a time offer the results as variants.

Source code in trajectopy\visualization\plots.py
def deviation_figures(
    ate_results: Sequence[ATEResult] = (),
    rpe_results: Sequence[RPEResult] = (),
    settings: PlotSettings | None = None,
) -> list[Figure]:
    """All figures describing absolute (ATE) and relative (RPE) evaluation results.

    The same figures are used for one and for several results; figures that show one result at a time
    offer the results as variants.
    """
    settings = _settings(settings)
    ate_results = _plottable_ate_results(ate_results, settings.ate_remove_above)
    figures: list[Figure] = []
    if ate_results:
        figures += [
            ate_figure(ate_results, settings),
            ate_components_figure(ate_results, settings),
            ate_histogram_figure(ate_results, settings),
            ate_statistics_figure(ate_results, settings),
            ate_distribution_figure(ate_results, settings),
            ate_correlation_figure(ate_results, settings),
            ate_map_figure(ate_results, settings),
        ]
    figures += rpe_figures(rpe_results, settings)
    return figures

parameter_matrix_figure

parameter_matrix_figure(
    parameters: AlignmentParameters,
    correlation: bool = False,
    settings: PlotSettings | None = None,
) -> Figure

Covariance or correlation matrix of estimated alignment parameters.

Source code in trajectopy\visualization\plots.py
def parameter_matrix_figure(
    parameters: AlignmentParameters, correlation: bool = False, settings: PlotSettings | None = None
) -> Figure:
    """Covariance or correlation matrix of estimated alignment parameters."""
    settings = _settings(settings)
    labels = parameters.params_labels(enabled_only=True, lower_case=True)
    matrix = np.array(parameters.get_covariance_matrix(enabled_only=True), dtype=float)
    if correlation:
        std = np.sqrt(np.diag(matrix))
        with np.errstate(divide="ignore", invalid="ignore"):
            matrix = matrix / np.outer(std, std)
        np.fill_diagonal(matrix, np.nan)
        limit, value_format = 1.0, "{:.2f}"
    else:
        finite = np.abs(matrix[np.isfinite(matrix)])
        limit, value_format = (float(finite.max()) if finite.size else 0.0) or 1.0, "{:.2e}"

    def render(layout: pg.GraphicsLayout) -> None:
        plot = _add_plot(layout, 0, settings, tight_x=False)
        plot.showGrid(x=False, y=False)
        plot.setClipToView(False)
        plot.setMouseEnabled(x=False, y=False)
        colormap = style.diverging_colormap()
        image = pg.ImageItem(np.nan_to_num(matrix), axisOrder="row-major")
        image.setColorMap(colormap)
        image.setLevels((-limit, limit))
        plot.addItem(image)
        plot.invertY(True)
        plot.setAspectLocked(True)
        ticks = [[(i + 0.5, label) for i, label in enumerate(labels)]]
        plot.getAxis("bottom").setTicks(ticks)
        plot.getAxis("left").setTicks(ticks)
        for (row, col), value in np.ndenumerate(matrix):
            if np.isfinite(value):
                cell_color = colormap.map((np.clip(value, -limit, limit) + limit) / (2 * limit), mode="qcolor")
                text = pg.TextItem(
                    value_format.format(value), color=style.readable_text_color(cell_color), anchor=(0.5, 0.5)
                )
                text.setPos(col + 0.5, row + 0.5)
                plot.addItem(text)
        colorbar = pg.ColorBarItem(
            values=(-limit, limit),
            colorMap=colormap,
            label="correlation" if correlation else "covariance",
            interactive=False,
            width=15,
        )
        layout.addItem(colorbar, row=0, col=1)

    return Figure("Parameter Correlation" if correlation else "Parameter Covariance", render)

alignment_figures

alignment_figures(
    parameters: AlignmentParameters,
    settings: PlotSettings | None = None,
) -> list[Figure]

Covariance and correlation of estimated alignment parameters.

Source code in trajectopy\visualization\plots.py
def alignment_figures(parameters: AlignmentParameters, settings: PlotSettings | None = None) -> list[Figure]:
    """Covariance and correlation of estimated alignment parameters."""
    return [
        parameter_matrix_figure(parameters, correlation=False, settings=settings),
        parameter_matrix_figure(parameters, correlation=True, settings=settings),
    ]