Module vcd.draw

Module to handle drawing functions using VCD data.

This module helps to draw TopView images, VCD info and other functionalities.

Expand source code
"""
Module to handle drawing functions using VCD data.

This module helps to draw TopView images, VCD info and other functionalities.
"""
# VCD (Video Content Description) library.
#
# Project website: http://vcd.vicomtech.org
#
# Copyright (C) 2023, Vicomtech (http://www.vicomtech.es/),
# (Spain) all rights reserved.
#
# VCD is a Python library to create and manage OpenLABEL content.
# VCD is distributed under MIT License. See LICENSE.


from __future__ import annotations

import copy
import warnings
from secrets import randbelow
from typing import Any

import cv2 as cv
import matplotlib.figure as fig
import matplotlib.pyplot as plt
import numpy as np
import numpy.typing as npt

from vcd import core, scl, utils


class SetupViewer:
    """This class offers Matplotlib routines to display the coordinate systems of the Scene."""

    def __init__(self, scene: scl.Scene, coordinate_system: str):
        if not isinstance(scene, scl.Scene):
            raise TypeError("Argument 'scene' must be of type 'vcd.scl.Scene'")
        self.scene = scene
        self.fig = plt.figure(figsize=(8, 8))
        self.ax = self.fig.add_subplot(projection="3d")
        self.coordinate_system = coordinate_system
        if not self.scene.vcd.has_coordinate_system(coordinate_system):
            raise ValueError(
                "The provided scene does not have the specified coordinate system"
            )

    def __plot_cs(self, pose_wrt_ref: npt.NDArray, name: str, la: float = 1):
        # Explore the coordinate systems defined for this scene
        axis = np.array(
            [
                [0, la, 0, 0, 0, 0],
                [0, 0, 0, la, 0, 0],
                [0, 0, 0, 0, 0, la],
                [1, 1, 1, 1, 1, 1],
            ]
        )  # matrix with several 4x1 points
        pose_wrt_ref = np.array(pose_wrt_ref).reshape(4, 4)
        axis_ref = pose_wrt_ref.dot(axis)
        origin = axis_ref[:, 0]
        x_axis_end = axis_ref[:, 1]
        y_axis_end = axis_ref[:, 3]
        z_axis_end = axis_ref[:, 5]
        self.ax.plot(
            [origin[0], x_axis_end[0]],
            [origin[1], x_axis_end[1]],
            [origin[2], x_axis_end[2]],
            "r-",
        )
        self.ax.plot(
            [origin[0], y_axis_end[0]],
            [origin[1], y_axis_end[1]],
            [origin[2], y_axis_end[2]],
            "g-",
        )
        self.ax.plot(
            [origin[0], z_axis_end[0]],
            [origin[1], z_axis_end[1]],
            [origin[2], z_axis_end[2]],
            "b-",
        )

        self.ax.text(origin[0], origin[1], origin[2], rf"{name}")
        self.ax.text(x_axis_end[0], x_axis_end[1], x_axis_end[2], "X")
        self.ax.text(y_axis_end[0], y_axis_end[1], y_axis_end[2], "Y")
        self.ax.text(z_axis_end[0], z_axis_end[1], z_axis_end[2], "Z")

    def plot_cuboid(self, cuboid_cs: str, cuboid_vals: tuple | list, color: Any):
        t, static = self.scene.get_transform(cuboid_cs, self.coordinate_system)
        cuboid_vals_transformed = utils.transform_cuboid(cuboid_vals, t)

        p = utils.generate_cuboid_points_ref_4x8(cuboid_vals_transformed)

        pairs = (
            [0, 1],
            [1, 2],
            [2, 3],
            [3, 0],
            [0, 4],
            [1, 5],
            [2, 6],
            [3, 7],
            [4, 5],
            [5, 6],
            [6, 7],
            [7, 4],
        )
        for pair in pairs:
            self.ax.plot(
                [p[0, pair[0]], p[0, pair[1]]],
                [p[1, pair[0]], p[1, pair[1]]],
                [p[2, pair[0]], p[2, pair[1]]],
                c=color,
            )

    def plot_setup(self, axes: list[list] | None = None) -> fig.Figure:
        for cs_name, cs in self.scene.vcd.get_root()["coordinate_systems"].items():
            transform, _static = self.scene.get_transform(cs_name, self.coordinate_system)
            la = 2.0
            if cs["type"] == "sensor_cs":
                la = 0.5
            self.__plot_cs(transform, cs_name, la)

        if "objects" in self.scene.vcd.get_root():
            for _object_id, obj in self.scene.vcd.get_root()["objects"].items():
                if obj["name"] == "Ego-car":
                    cuboid = obj["object_data"]["cuboid"][0]
                    cuboid_cs = cuboid["coordinate_system"]
                    cuboid_vals = cuboid["val"]
                    self.plot_cuboid(cuboid_cs, cuboid_vals, "k")

                else:
                    if "object_data" in obj:
                        if "cuboid" in obj["object_data"]:
                            for cuboid in obj["object_data"]["cuboid"]:
                                self.plot_cuboid(
                                    cuboid["coordinate_system"], cuboid["val"], "k"
                                )

        if axes is None:
            self.ax.set_xlim(-1.25, 4.25)
            self.ax.set_ylim(-2.75, 2.75)
            self.ax.set_zlim(0, 5.5)
        else:
            self.ax.set_xlim(axes[0][0], axes[0][1])
            self.ax.set_ylim(axes[1][0], axes[1][1])
            self.ax.set_zlim(axes[2][0], axes[2][1])

        self.ax.set_xlabel("X")
        self.ax.set_ylabel("Y")
        self.ax.set_zlabel("Z")

        return self.fig


class TopView:
    """
    Define functions to draw topview representations.

    This class draws a top view of the scene, assuming Z=0 is the ground plane (i.e. the
    topview sees the XY plane) Range and scale can be used to select a certain part of the XY
    plane.
    """

    class Params:  # pylint: disable=too-many-instance-attributes
        """
        Define the parameters needed to draw topview representations.

        Assuming cuboids are drawn top view, so Z coordinate is ignored RZ is the rotation in
        Z-axis, it assumes/enforces SY>SX, thus keeping RZ between pi/2 and -pi/2.

        Z, RX, RY, and SZ are ignored

        For Vehicle cases, we adopt ISO8855: origin at rear axle at ground, x-to-front, y-to-
        left
        """

        def __init__(
            self,
            step_x: float | None = None,
            step_y: float | None = None,
            background_color: int | None = None,
            topview_size: tuple[int, int] | None = None,
            range_x: tuple[float, float] | None = None,
            range_y: tuple[float, float] | None = None,
            color_map: dict | None = None,
            ignore_classes: dict | None = None,
            draw_grid: bool = True,
            draw_only_current_image: bool = True,
        ):
            self.topview_size = (1920, 1080)  # width, height
            if topview_size is not None:
                if not isinstance(topview_size, tuple):
                    raise TypeError("Argument 'topview_size' must be of type 'tuple'")
                self.topview_size = topview_size

            self.ar = self.topview_size[0] / self.topview_size[1]

            self.range_x = (-80.0, 80.0)
            if range_x is not None:
                if not isinstance(range_x, tuple):
                    raise TypeError("Argument 'range_x' must be of type 'tuple'")
                self.range_x = range_x

            self.range_y = (self.range_x[0] / self.ar, self.range_x[1] / self.ar)
            if range_y is not None:
                if not isinstance(range_y, tuple):
                    raise TypeError("Argument 'range_y' must be of type 'tuple'")
                self.range_y = range_y

            self.scale_x = self.topview_size[0] / (self.range_x[1] - self.range_x[0])
            self.scale_y = -self.topview_size[1] / (
                self.range_y[1] - self.range_y[0]
            )  # Negative?

            self.offset_x = round(-self.range_x[0] * self.scale_x)
            self.offset_y = round(
                -self.range_y[1] * self.scale_y
            )  # TODO: shouldn't it be -self.rangeY[0]?

            self.S = np.array(
                [
                    [self.scale_x, 0, self.offset_x],
                    [0, self.scale_y, self.offset_y],
                    [0, 0, 1],
                ]
            )

            self.step_x = 1.0
            if step_x is not None:
                self.step_x = step_x
            self.step_y = 1.0
            if step_y is not None:
                self.step_y = step_y

            self.grid_lines_thickness = 1
            self.background_color = 255
            if background_color is not None:
                self.background_color = background_color

            self.grid_text_color = (0, 0, 0)

            if color_map is None:
                self.color_map = {}
            else:
                if not isinstance(color_map, dict):
                    raise TypeError("Argument 'color_map' must be of type 'dict'")
                self.color_map = color_map

            if ignore_classes is None:
                self.ignore_classes = {}
            else:
                self.ignore_classes = ignore_classes

            self.draw_grid = True
            if draw_grid is not None:
                self.draw_grid = draw_grid

            self.draw_only_current_image = draw_only_current_image

    def __init__(
        self,
        scene: scl.Scene,
        coordinate_system: str,
        params: TopView.Params | None = None,
    ):
        # scene contains the VCD and helper functions for transforms and projections
        if not isinstance(scene, scl.Scene):
            raise TypeError("Argument 'scene' must be of type 'vcd.scl.Scene'")
        self.scene = scene
        # This value specifies which coordinate system is fixed in the
        # center of the TopView, e.g. "odom" or "vehicle-iso8855"
        if not scene.vcd.has_coordinate_system(coordinate_system):
            raise ValueError(
                f"The provided scene does not have coordinate system {coordinate_system}"
            )
        self.coordinate_system = coordinate_system
        if params is not None:
            self.params = params
        else:
            self.params = TopView.Params()

        # Start topView base with a background color
        self.topView = np.zeros(
            (self.params.topview_size[1], self.params.topview_size[0], 3), np.uint8
        )  # Needs to be here
        self.topView.fill(self.params.background_color)
        self.images: dict = {}

    def add_images(self, imgs: dict, frame_num: int):
        """
        Add images to the TopView representation.

        By specifying the frame num and the camera name, several images can be loaded in one
        single call. Images should be provided as dictionary:

        {"CAM_FRONT": img_front, "CAM_REAR": img_rear}

        The function pre-computes all the necessary variables to create the TopView, such as
        the homography from image plane to world plane, or the camera region of interest, which
        is stored in scene.cameras dictionary.

        :param imgs: dictionary of images
        :param frame_num: frame number
        :return: nothing
        """
        # Base images
        # should be {"CAM_FRONT": img_front, "CAM_REAR": img_rear}
        if not isinstance(imgs, dict):
            raise TypeError("Argument 'imgs' must be of type 'dict'")

        # This option creates 1 remap for the entire topview, and not 1 per camera
        # The key idea is to weight the contribution of each camera depending on the
        # distance between point and cam
        # Instead of storing the result in self.images[cam_name] and then paint them
        # in drawBEV, we can store in self.images[frameNum] directly
        h = self.params.topview_size[1]
        w = self.params.topview_size[0]
        num_cams = len(imgs)
        cams: dict[str, scl.Camera | None] = {}
        need_to_recompute_weights_acc = False
        need_to_recompute_maps = {}
        need_to_recompute_weights = {}
        for cam_name, img in imgs.items():
            if not self.scene.vcd.has_coordinate_system(cam_name):
                raise ValueError(
                    f"The provided scene does not have coordinate system {cam_name}"
                )
            # this call creates an entry inside scene
            cam = self.scene.get_camera(cam_name, frame_num, compute_remaps=False)
            cams[cam_name] = cam
            self.images.setdefault(cam_name, {})
            self.images[cam_name]["img"] = img
            t_ref_to_cam_4x4, static = self.scene.get_transform(
                self.coordinate_system, cam_name, frame_num
            )

            # Compute distances to this camera and add to weight map
            need_to_recompute_maps[cam_name] = False
            need_to_recompute_weights[cam_name] = False

            if (num_cams > 1 and not static) or (
                num_cams > 1 and static and "weights" not in self.images[cam_name]
            ):
                need_to_recompute_weights[cam_name] = True
                need_to_recompute_weights_acc = True

            if (not static) or (static and "mapX" not in self.images[cam_name]):
                need_to_recompute_maps[cam_name] = True

            if need_to_recompute_maps[cam_name]:
                print(cam_name + " top view remap computation...")
                self.images[cam_name]["mapX"] = np.zeros((h, w), dtype=np.float32)
                self.images[cam_name]["mapY"] = np.zeros((h, w), dtype=np.float32)

            if need_to_recompute_weights[cam_name]:
                print(cam_name + " top view weights computation...")
                self.images[cam_name].setdefault(
                    "weights", np.zeros((h, w, 3), dtype=np.float32)
                )

        # Loop over top view domain
        for i in range(0, h):
            # Read all pixels pos of this row
            points2d_z0_3xN = np.array(
                [np.linspace(0, w - 1, num=w), i * np.ones(w), np.ones(w)]
            )
            # from pixels to points 3d
            temp = utils.inv(self.params.S).dot(points2d_z0_3xN)
            # hom. coords.
            points3d_z0_4xN = np.vstack((temp[0, :], temp[1, :], np.zeros(w), temp[2, :]))

            # Loop over cameras
            for _idx, (cam_name, cam) in enumerate(cams.items()):
                # Convert into camera coordinate system for all M cameras
                t_ref_to_cam_4x4, static = self.scene.get_transform(
                    self.coordinate_system, cam_name, frame_num
                )
                points3d_cam_4xN = t_ref_to_cam_4x4.dot(points3d_z0_4xN)

                if need_to_recompute_weights[cam_name]:
                    self.images[cam_name]["weights"][i, :, 0] = 1.0 / np.linalg.norm(
                        points3d_cam_4xN, axis=0
                    )
                    self.images[cam_name]["weights"][i, :, 1] = self.images[cam_name][
                        "weights"
                    ][i, :, 0]
                    self.images[cam_name]["weights"][i, :, 2] = self.images[cam_name][
                        "weights"
                    ][i, :, 0]

                if need_to_recompute_maps[cam_name]:
                    if cam is not None:
                        # Project into image
                        points2d_dist_3xN, idx_valid = cam.project_points3d(
                            points3d_cam_4xN, remove_outside=True
                        )

                        # Assign into map
                        self.images[cam_name]["mapX"][i, :] = points2d_dist_3xN[0, :]
                        self.images[cam_name]["mapY"][i, :] = points2d_dist_3xN[1, :]

        # Compute accumulated weights if more than 1 camera
        if need_to_recompute_weights_acc:
            self.images["weights_acc"] = np.zeros((h, w, 3), dtype=np.float32)
            for _idx, (cam_name, _cam) in enumerate(cams.items()):
                self.images["weights_acc"] = cv.add(
                    self.images[cam_name]["weights"], self.images["weights_acc"]
                )

    def draw(
        self,
        frame_num: int | None = None,
        uid: int | str | None = None,
        _draw_trajectory: bool = True,
    ) -> cv.Mat:
        """
        Define the main drawing function for the TopView drawer.

        It explores the provided params to select different options.

        :param frameNum: frame number
        :param uid: unique identifier of object to be drawn (if None, all are drawn)
        :param _drawTrajectory: boolean to draw the trajectory of objects
        :param _params: additional parameters
        :return: the TopView image
        """
        # Base top view is used from previous iteration
        if self.params.draw_only_current_image:
            # Needs to be here
            self.topView = np.zeros(
                (self.params.topview_size[1], self.params.topview_size[0], 3), np.uint8
            )
            self.topView.fill(self.params.background_color)

            # Draw BEW
        self.draw_bevs(frame_num)

        # Base grids
        self.draw_topview_base()

        # Draw objects
        topview_with_objects = copy.deepcopy(self.topView)
        self.draw_objects_at_frame(topview_with_objects, uid, frame_num, _draw_trajectory)

        # Draw frame info
        self.draw_info(topview_with_objects, frame_num)

        return topview_with_objects

    def draw_info(self, topview: cv.Mat, frame_num: int | None = None):
        h = topview.shape[0]
        w = topview.shape[1]
        w_margin = 250
        h_margin = 140
        h_step = 20
        font_size = 0.8
        cv.putText(
            topview,
            "Img. Size(px): " + str(w) + " x " + str(h),
            (w - w_margin, h - h_margin),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        if frame_num is None:
            frame_num = -1
        cv.putText(
            topview,
            "Frame: " + str(frame_num),
            (w - w_margin, h - h_margin + h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        cv.putText(
            topview,
            "CS: " + str(self.coordinate_system),
            (w - w_margin, h - h_margin + 2 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )

        cv.putText(
            topview,
            "RangeX (m): ("
            + str(self.params.range_x[0])
            + ", "
            + str(self.params.range_x[1])
            + ")",
            (w - w_margin, h - h_margin + 3 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        cv.putText(
            topview,
            "RangeY (m): ("
            + str(self.params.range_y[0])
            + ", "
            + str(self.params.range_y[1])
            + ")",
            (w - w_margin, h - h_margin + 4 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )

        cv.putText(
            topview,
            "OffsetX (px): ("
            + str(self.params.offset_x)
            + ", "
            + str(self.params.offset_x)
            + ")",
            (w - w_margin, h - h_margin + 5 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        cv.putText(
            topview,
            "OffsetY (px): ("
            + str(self.params.offset_y)
            + ", "
            + str(self.params.offset_y)
            + ")",
            (w - w_margin, h - h_margin + 6 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )

    def draw_topview_base(self):
        # self.topView.fill(self.params.backgroundColor)

        if self.params.draw_grid:
            # Grid x (1/2)
            for x in np.arange(
                self.params.range_x[0],
                self.params.range_x[1] + self.params.step_x,
                self.params.step_x,
            ):
                x_round = round(x)
                pt_img1 = self.point2pixel((x_round, self.params.range_y[0]))
                pt_img2 = self.point2pixel((x_round, self.params.range_y[1]))
                cv.line(
                    self.topView,
                    pt_img1,
                    pt_img2,
                    (127, 127, 127),
                    self.params.grid_lines_thickness,
                )

            # Grid y (1/2)
            for y in np.arange(
                self.params.range_y[0],
                self.params.range_y[1] + self.params.step_y,
                self.params.step_y,
            ):
                y_round = round(y)
                pt_img1 = self.point2pixel((self.params.range_x[0], y_round))
                pt_img2 = self.point2pixel((self.params.range_x[1], y_round))
                cv.line(
                    self.topView,
                    pt_img1,
                    pt_img2,
                    (127, 127, 127),
                    self.params.grid_lines_thickness,
                )

            # Grid x (2/2)
            for x in np.arange(
                self.params.range_x[0],
                self.params.range_x[1] + self.params.step_x,
                self.params.step_x,
            ):
                x_round = round(x)
                pt_img1 = self.point2pixel((x_round, self.params.range_y[0]))
                cv.putText(
                    self.topView,
                    str(round(x)) + " m",
                    (pt_img1[0] + 5, 15),
                    cv.FONT_HERSHEY_PLAIN,
                    0.6,
                    self.params.grid_text_color,
                    1,
                    cv.LINE_AA,
                )
            # Grid y (2/2)
            for y in np.arange(
                self.params.range_y[0],
                self.params.range_y[1] + self.params.step_y,
                self.params.step_y,
            ):
                y_round = round(y)
                pt_img1 = self.point2pixel((self.params.range_x[0], y_round))
                cv.putText(
                    self.topView,
                    str(round(y)) + " m",
                    (5, pt_img1[1] - 5),
                    cv.FONT_HERSHEY_PLAIN,
                    0.6,
                    self.params.grid_text_color,
                    1,
                    cv.LINE_AA,
                )

        # World origin
        cv.circle(self.topView, self.point2pixel((0.0, 0.0)), 4, (255, 255, 255), -1)
        cv.line(
            self.topView,
            self.point2pixel((0.0, 0.0)),
            self.point2pixel((5.0, 0.0)),
            (0, 0, 255),
            2,
        )
        cv.line(
            self.topView,
            self.point2pixel((0.0, 0.0)),
            self.point2pixel((0.0, 5.0)),
            (0, 255, 0),
            2,
        )

        cv.putText(
            self.topView,
            "X",
            self.point2pixel((5.0, -0.5)),
            cv.FONT_HERSHEY_PLAIN,
            1.0,
            (0, 0, 255),
            1,
            cv.LINE_AA,
        )
        cv.putText(
            self.topView,
            "Y",
            self.point2pixel((-1.0, 5.0)),
            cv.FONT_HERSHEY_PLAIN,
            1.0,
            (0, 255, 0),
            1,
            cv.LINE_AA,
        )

    def draw_points3d(self, _img: cv.Mat, points3d_4xN: npt.NDArray, _color: tuple):
        rows, cols = points3d_4xN.shape
        for i in range(0, cols):
            # thus ignoring z component
            pt = self.point2pixel((points3d_4xN[0, i], points3d_4xN[1, i]))
            cv.circle(_img, pt, 2, _color, -1)

    def draw_cuboid_topview(
        self,
        _img: cv.Mat,
        _cuboid: list,
        _class: str,
        _color: tuple[int, int, int],
        _thick: int,
        _id: int | str = "",
    ):
        if not isinstance(_cuboid, list):
            raise TypeError("Argument '_cuboid' must be of type 'list'")

        # (X, Y, Z, RX, RY, RZ, SX, SY, SZ)
        if len(_cuboid) != 9 or len(_cuboid) != 10:
            raise ValueError("Invalid argument '_cuboid' size")

        points_4x8 = utils.generate_cuboid_points_ref_4x8(_cuboid)
        # Project into topview
        points_4x8[2, :] = 0

        pairs = (
            [0, 1],
            [1, 2],
            [2, 3],
            [3, 0],
            [0, 4],
            [1, 5],
            [2, 6],
            [3, 7],
            [4, 5],
            [5, 6],
            [6, 7],
            [7, 4],
        )
        for pair in pairs:
            p_a = (points_4x8[0, pair[0]], points_4x8[1, pair[0]])
            p_b = (points_4x8[0, pair[1]], points_4x8[1, pair[1]])
            cv.line(_img, self.point2pixel(p_a), self.point2pixel(p_b), _color, _thick)

    def draw_mesh_topview(self, img: cv.Mat, mesh: dict, points3d_4xN: npt.NDArray):
        mesh_point_dict = mesh["point3d"]
        mesh_line_refs = mesh["line_reference"]
        mesh_area_refs = mesh["area_reference"]

        # Convert points into pixels
        points2d = []
        rows, cols = points3d_4xN.shape
        for i in range(0, cols):
            pt = self.point2pixel(
                (points3d_4xN[0, i], points3d_4xN[1, i])
            )  # thus ignoring z component
            points2d.append(pt)

        # Draw areas first
        for _area_id, area in mesh_area_refs.items():
            line_refs = area["val"]
            points_area = []
            # Loop over lines and create a list of points
            for line_ref in line_refs:
                line = mesh_line_refs[str(line_ref)]

                point_refs = line["val"]
                point_a_ref = point_refs[0]
                point_b_ref = point_refs[1]
                point_a = points2d[list(mesh_point_dict).index(str(point_a_ref))]
                point_b = points2d[list(mesh_point_dict).index(str(point_b_ref))]

                points_area.append(point_a)
                points_area.append(point_b)

            cv.fillConvexPoly(img, np.array(points_area), (0, 255, 0))

        # Draw lines
        for _line_id, line in mesh_line_refs.items():
            point_refs = line["val"]
            point_a_ref = point_refs[0]
            point_b_ref = point_refs[1]

            point_a = points2d[list(mesh_point_dict).index(str(point_a_ref))]
            point_b = points2d[list(mesh_point_dict).index(str(point_b_ref))]

            cv.line(img, point_a, point_b, (255, 0, 0), 2)

        # Draw points
        for pt in points2d:
            cv.circle(img, pt, 5, (0, 0, 0), -1)
            cv.circle(img, pt, 3, (0, 0, 255), -1)

    def draw_object_data(
        self,
        object_: dict,
        object_class: str,
        _img: cv.Mat,
        uid: int | str,
        _frame_num: int | None,
        _draw_trajectory: bool,
    ):  # pylint: disable=too-many-locals
        # Reads cuboids
        if "object_data" not in object_:
            return

        for object_data_key in object_["object_data"].keys():
            for object_data_item in object_["object_data"][object_data_key]:
                ########################################
                # CUBOIDS
                ########################################
                if object_data_key == "cuboid":
                    cuboid_vals = object_data_item["val"]
                    cuboid_name = object_data_item["name"]
                    if "coordinate_system" in object_data_item:
                        cs_data = object_data_item["coordinate_system"]
                    else:
                        warnings.warn(
                            "WARNING: The cuboids of this VCD don't have a "
                            "coordinate_system.",
                            Warning,
                            2,
                        )
                        # For simplicity, let's assume they are already expressed in
                        # the target cs
                        cs_data = self.coordinate_system

                    # Convert from data coordinate system (e.g. "CAM_LEFT")
                    #  into reference coordinate system (e.g. "VEHICLE-ISO8855")
                    cuboid_vals_transformed = cuboid_vals
                    if cs_data != self.coordinate_system:
                        cuboid_vals_transformed = self.scene.transform_cuboid(
                            cuboid_vals, cs_data, self.coordinate_system, _frame_num
                        )
                    # Draw
                    self.draw_cuboid_topview(
                        _img,
                        cuboid_vals_transformed,
                        object_class,
                        self.params.color_map[object_class],
                        2,
                        uid,
                    )

                    if _draw_trajectory and _frame_num is not None:
                        fis = self.__get_object_data_fis(uid, cuboid_name)

                        for fi in fis:
                            prev_center: dict = {}
                            for f in range(fi["frame_start"], _frame_num + 1):
                                object_data_item = self.scene.vcd.get_object_data(
                                    uid, cuboid_name, f
                                )

                                cuboid_vals = object_data_item["val"]
                                cuboid_vals_transformed = cuboid_vals
                                if cs_data != self.coordinate_system:
                                    src_cs = cs_data
                                    dst_cs = self.coordinate_system
                                    (
                                        transform_src_dst,
                                        _,
                                    ) = self.scene.get_transform(src_cs, dst_cs, f)
                                    cuboid_vals_transformed = utils.transform_cuboid(
                                        cuboid_vals, transform_src_dst
                                    )

                                name = object_data_item["name"]

                                center = (
                                    cuboid_vals_transformed[0],
                                    cuboid_vals_transformed[1],
                                )
                                center_pix = self.point2pixel(center)

                                # this is a dict to allow multiple trajectories
                                # (e.g. several cuboids per object)
                                if prev_center.get(name) is not None:
                                    cv.line(
                                        _img,
                                        prev_center[name],
                                        center_pix,
                                        (0, 0, 0),
                                        1,
                                        cv.LINE_AA,
                                    )

                                cv.circle(
                                    _img,
                                    center_pix,
                                    2,
                                    self.params.color_map[object_class],
                                    -1,
                                )

                                prev_center[name] = center_pix
                ########################################
                # mat - points3d_4xN
                ########################################
                elif object_data_key == "mat":
                    width = object_data_item["width"]
                    height = object_data_item["height"]

                    if height == 4:
                        # These are points 4xN
                        color = self.params.color_map[object_class]
                        points3d_4xN = np.array(object_data_item["val"]).reshape(
                            height, width
                        )
                        points_cs = object_data_item["coordinate_system"]

                        # First convert from the src coordinate system into the camera
                        # coordinate system
                        points3d_4xN_transformed = self.scene.transform_points3d_4xN(
                            points3d_4xN, points_cs, self.coordinate_system
                        )

                        if "attributes" in object_data_item:
                            for attr_type, attr_list in object_data_item[
                                "attributes"
                            ].items():
                                if attr_type == "vec":
                                    for attr in attr_list:
                                        if attr["name"] == "color":
                                            color = attr["val"]
                        if points3d_4xN_transformed is not None:
                            self.draw_points3d(_img, points3d_4xN_transformed, color)
                ########################################
                # point3d - Single point in 3D
                ########################################
                elif object_data_key == "point3d":
                    color = self.params.color_map[object_class]
                    point_name = object_data_item["name"]

                    if "coordinate_system" in object_data_item:
                        cs_data = object_data_item["coordinate_system"]
                    else:
                        warnings.warn(
                            "WARNING: The point3d of this VCD don't have a "
                            "coordinate_system.",
                            Warning,
                            2,
                        )
                        # For simplicity, let's assume they are already expressed in
                        # the target cs
                        cs_data = self.coordinate_system

                    x = object_data_item["val"][0]
                    y = object_data_item["val"][1]
                    z = object_data_item["val"][2]
                    points3d_4xN = np.array([x, y, z, 1]).reshape(4, 1)
                    points_cs = object_data_item["coordinate_system"]

                    # First convert from the src coordinate system into the camera
                    # coordinate system
                    points3d_4xN_transformed = self.scene.transform_points3d_4xN(
                        points3d_4xN, points_cs, self.coordinate_system
                    )

                    if "attributes" in object_data_item:
                        for attr_type, attr_list in object_data_item[
                            "attributes"
                        ].items():
                            if attr_type == "vec":
                                for attr in attr_list:
                                    if attr["name"] == "color":
                                        color = attr["val"]

                    if points3d_4xN_transformed is not None:
                        self.draw_points3d(_img, points3d_4xN_transformed, color)

                    if _draw_trajectory and _frame_num is not None:
                        fis = self.__get_object_data_fis(uid, point_name)

                        for fi in fis:
                            prev_center = {}
                            for f in range(fi["frame_start"], _frame_num + 1):
                                object_data_item = self.scene.vcd.get_object_data(
                                    uid, point_name, f
                                )

                                x = object_data_item["val"][0]
                                y = object_data_item["val"][1]
                                z = object_data_item["val"][2]
                                points3d_4xN = np.array([x, y, z, 1]).reshape(4, 1)
                                points3d_4xN_transformed = points3d_4xN

                                if cs_data != self.coordinate_system:
                                    src_cs = cs_data
                                    dst_cs = self.coordinate_system
                                    (
                                        transform_src_dst,
                                        _,
                                    ) = self.scene.get_transform(src_cs, dst_cs, f)
                                    points3d_4xN_transformed = (
                                        self.scene.transform_points3d_4xN(
                                            points3d_4xN,
                                            points_cs,
                                            self.coordinate_system,
                                        )
                                    )

                                name = object_data_item["name"]

                                if points3d_4xN_transformed is not None:
                                    center = (
                                        points3d_4xN_transformed[0, 0],
                                        points3d_4xN_transformed[1, 0],
                                    )
                                    center_pix = self.point2pixel(center)

                                    # this is a dict to allow multiple trajectories
                                    # (e.g. several cuboids per object)
                                    if prev_center.get(name) is not None:
                                        cv.line(
                                            _img,
                                            prev_center[name],
                                            center_pix,
                                            (0, 0, 0),
                                            1,
                                            cv.LINE_AA,
                                        )

                                    cv.circle(
                                        _img,
                                        center_pix,
                                        2,
                                        self.params.color_map[object_class],
                                        -1,
                                    )

                                    prev_center[name] = center_pix
                ########################################
                # mesh - Point-line-area structure
                ########################################
                elif object_data_key == "mesh":
                    if "coordinate_system" in object_data_item:
                        cs_data = object_data_item["coordinate_system"]
                    else:
                        warnings.warn(
                            "WARNING: The mesh of this VCD don't have a coordinate_system.",
                            Warning,
                            2,
                        )
                        # For simplicity, let's assume they are already expressed in
                        # the target cs
                        cs_data = self.coordinate_system

                    # Let's convert mesh points into 4xN array
                    points = object_data_item["point3d"]
                    points3d_4xN = np.ones((4, len(points)))
                    for point_count, (_point_id, point) in enumerate(points.items()):
                        points3d_4xN[0, point_count] = point["val"][0]
                        points3d_4xN[1, point_count] = point["val"][1]
                        points3d_4xN[2, point_count] = point["val"][2]

                    points3d_4xN_transformed = self.scene.transform_points3d_4xN(
                        points3d_4xN, cs_data, self.coordinate_system
                    )

                    if points3d_4xN_transformed is not None:
                        # Let's send the data and the possible transform info to the
                        # drawing function
                        self.draw_mesh_topview(
                            img=_img,
                            mesh=object_data_item,
                            points3d_4xN=points3d_4xN_transformed,
                        )

    def draw_objects_at_frame(
        self,
        top_view: cv.Mat,
        uid: int | str | None,
        _frame_num: int | None,
        _draw_trajectory: bool,
    ):
        img = top_view

        # Select static or dynamic objects depending on the provided input _frameNum
        objects = {}
        if _frame_num is not None:
            vcd_frame = self.scene.vcd.get_frame(_frame_num)
            if "objects" in vcd_frame:
                objects = vcd_frame["objects"]
        else:
            if self.scene.vcd.has_objects():
                objects = self.scene.vcd.get_objects()

        # Explore objects at this VCD frame
        for object_id, object_ in objects.items():
            if uid is not None:
                if core.UID(object_id).as_str() != core.UID(uid).as_str():
                    continue

            object_element = self.scene.vcd.get_object(object_id)
            if object_element is not None:
                # Get object static info
                object_class = object_element["type"]

            # Ignore classes
            if object_class in self.params.ignore_classes:
                continue

            # Colors
            if self.params.color_map.get(object_class) is None:
                # Let's create a new entry for this class
                self.params.color_map[object_class] = (
                    randbelow(255),
                    randbelow(255),
                    randbelow(255),
                )

            # Check if the object has specific info at this frame, or if we need to consult the
            # static object info
            if len(object_) == 0:
                # So this is a pointer to a static object
                static_object = self.scene.vcd.get_root()["objects"][object_id]
                self.draw_object_data(
                    static_object,
                    object_class,
                    img,
                    object_id,
                    _frame_num,
                    _draw_trajectory,
                )
            else:
                # Let's use the dynamic info of this object
                self.draw_object_data(
                    object_, object_class, img, object_id, _frame_num, _draw_trajectory
                )

    def draw_bev(self, cam_name: str) -> npt.NDArray[np.float32]:
        img = self.images[cam_name]["img"]

        map_x = self.images[cam_name]["mapX"]
        map_y = self.images[cam_name]["mapY"]
        bev = cv.remap(
            img,
            map_x,
            map_y,
            interpolation=cv.INTER_LINEAR,
            borderMode=cv.BORDER_CONSTANT,
        )

        bev32 = np.array(bev, np.float32)
        if "weights" in self.images[cam_name]:
            cv.multiply(self.images[cam_name]["weights"], bev32, bev32)

        # cv.imshow('bev' + cam_name, bev)
        # cv.waitKey(1)

        # bev832 = np.uint8(bev32)
        # cv.imshow('bev8' + cam_name, bev832)
        # cv.waitKey(1)

        return bev32

    def draw_bevs(self, _frame_num: int | None = None):
        """
        Draw BEVs into the topview.

        :param _frameNum:
        :return:
        """
        num_cams = len(self.images)
        if num_cams == 0:
            return

        h = self.params.topview_size[1]
        w = self.params.topview_size[0]
        # Prepare image with drawing for this call
        # black background
        acc32: npt.NDArray[np.float32] = np.zeros((h, w, 3), dtype=np.float32)

        for cam_name in self.images:
            if self.scene.get_camera(cam_name, _frame_num) is not None:
                temp32 = self.draw_bev(cam_name=cam_name)
                # mask = np.zeros((h, w), dtype=np.uint8)
                # mask[temp32 > 0] = 255
                # mask = (temp32 > 0)
                if num_cams > 1:
                    acc32 = cv.add(temp32, acc32)
        if num_cams > 1:
            acc32 /= self.images["weights_acc"]
        else:
            acc32 = temp32
        acc8 = acc32.astype(dtype=np.uint8)
        # cv.imshow('acc', acc8)
        # cv.waitKey(1)

        # Copy into topView only new pixels
        nonzero = acc8 > 0
        self.topView[nonzero] = acc8[nonzero]

    def size2pixel(self, _size: tuple[int, int]) -> tuple[int, int]:
        return (
            int(round(_size[0] * abs(self.params.scale_x))),
            int(round(_size[1] * abs(self.params.scale_y))),
        )

    def point2pixel(self, _point: tuple[int, int]) -> tuple[int, int]:
        pixel = (
            int(round(_point[0] * self.params.scale_x + self.params.offset_x)),
            int(round(_point[1] * self.params.scale_y + self.params.offset_y)),
        )
        return pixel

    def __get_object_data_fis(self, uid: int | str, name: str) -> list[dict]:
        fis_object = self.scene.vcd.get_object_data_frame_intervals(uid, name)
        if fis_object is None:
            fis: list[dict] = [{}]
        elif fis_object.empty():
            # So this object is static, let's project its cuboid into
            # the current transform
            fis = self.scene.vcd.get_frame_intervals().get_dict()
        else:
            fis = fis_object.get_dict()
        return fis


class Image:
    """
    Draw 2D elements in the Image.

    Devised to draw bboxes, it can also project 3D entities (e.g. cuboids) using the
    calibration parameters
    """

    class Params:
        def __init__(
            self,
            _draw_trajectory: bool = False,
            _color_map: dict | None = None,
            _ignore_classes: dict | None = None,
            _draw_types: set[str] | None = None,
            _barrel: bool | None = None,
            _thickness: int | None = None,
        ):
            if _color_map is None:
                self.color_map = {}
            else:
                if not isinstance(_color_map, dict):
                    raise TypeError("Argument '_color_map' must be of type 'dict'")
                self.color_map = _color_map
            self.draw_trajectory = _draw_trajectory
            if _ignore_classes is None:
                self.ignore_classes = {}
            else:
                self.ignore_classes = _ignore_classes

            if _draw_types is not None:
                self.draw_types = _draw_types
            else:
                self.draw_types = {"bbox"}

            if _barrel is not None:
                self.draw_barrel = _barrel
            else:
                self.draw_barrel = False

            if _thickness is not None:
                self.thickness = _thickness
            else:
                self.thickness = 1

    def __init__(self, scene: scl.Scene, camera_coordinate_system: str | None = None):
        if not isinstance(scene, scl.Scene):
            raise TypeError("Argument 'scene' must be of type 'vcd.scl.Scene'")
        self.scene = scene
        if camera_coordinate_system is not None:
            if not scene.vcd.has_coordinate_system(camera_coordinate_system):
                raise ValueError(
                    "The provided scene does not have the specified coordinate system"
                )
            self.camera_coordinate_system = camera_coordinate_system
            self.camera = self.scene.get_camera(
                self.camera_coordinate_system, compute_remaps=False
            )

        self.params = Image.Params()

    def reset_image(self) -> cv.Mat:
        img = np.array([], np.int8)
        if self.camera is not None:
            img = np.zeros((self.camera.height, self.camera.width, 3), np.uint8)
            img.fill(255)
        return img

    def draw_points3d(
        self, _img: cv.Mat, points3d_4xN: npt.NDArray, _color: tuple[int, int, int]
    ):
        if self.camera is None:
            return
        # this function may return LESS than N points IF 3D points are BEHIND the camera
        points2d_3xN, idx_valid = self.camera.project_points3d(
            points3d_4xN, remove_outside=True
        )
        if points2d_3xN is None:
            return
        rows, cols = points2d_3xN.shape
        img_rows, img_cols, img_channels = _img.shape
        for i in range(0, cols):
            if idx_valid[i]:
                if np.isnan(points2d_3xN[0, i]) or np.isnan(points2d_3xN[1, i]):
                    continue
                center = (
                    utils.round(points2d_3xN[0, i]),
                    utils.round(points2d_3xN[1, i]),
                )
                if not utils.is_inside_image(img_cols, img_rows, center[0], center[1]):
                    continue
                cv.circle(_img, (int(center[0]), int(center[1])), 2, _color, -1)

    def draw_cuboid(
        self,
        _img: cv.Mat,
        _cuboid_vals: list[float],
        _class: str,
        _color: tuple[int, int, int],
        _thickness: int = 1,
    ):
        if not isinstance(_cuboid_vals, list):
            raise TypeError("Argument '_cuboid' must be of type 'list'")

        # (X, Y, Z, RX, RY, RZ, SX, SY, SZ)
        if len(_cuboid_vals) != 9:
            raise ValueError("Invalid argument '_cuboid' size")
        # TODO cuboids with quaternions

        # Generate object coordinates
        points3d_4x8 = utils.generate_cuboid_points_ref_4x8(_cuboid_vals)

        # this function may return LESS than 8 points IF 3D points are BEHIND the camera
        if self.camera is None:
            return
        points2d_4x8, idx_valid = self.camera.project_points3d(points3d_4x8, True)

        if points2d_4x8 is None:
            return
        img_rows, img_cols, img_channels = _img.shape

        pairs = (
            [0, 1],
            [1, 2],
            [2, 3],
            [3, 0],
            [0, 4],
            [1, 5],
            [2, 6],
            [3, 7],
            [4, 5],
            [5, 6],
            [6, 7],
            [7, 4],
        )
        for _count, pair in enumerate(pairs):
            if idx_valid[pair[0]] and idx_valid[pair[1]]:
                # if pair[0] >= num_points_projected or pair[1] >= num_points_projected:
                #    continue
                p_a = (
                    utils.round(points2d_4x8[0, pair[0]]),
                    utils.round(points2d_4x8[1, pair[0]]),
                )
                p_b = (
                    utils.round(points2d_4x8[0, pair[1]]),
                    utils.round(points2d_4x8[1, pair[1]]),
                )

                if not utils.is_inside_image(
                    img_cols, img_rows, p_a[0], p_b[1]
                ) or not utils.is_inside_image(img_cols, img_rows, p_b[0], p_b[1]):
                    continue

                cv.line(_img, p_a, p_b, _color, _thickness)

    def draw_bbox(
        self,
        _img: cv.Mat,
        _bbox: tuple[int, int, int, int],
        _object_class: str,
        _color: tuple[int, int, int],
        add_border: bool = False,
    ):
        pt1 = (int(round(_bbox[0] - _bbox[2] / 2)), int(round(_bbox[1] - _bbox[3] / 2)))
        pt2 = (int(round(_bbox[0] + _bbox[2] / 2)), int(round(_bbox[1] + _bbox[3] / 2)))

        pta = (pt1[0], pt1[1] - 15)
        ptb = (pt2[0], pt1[1])
        img_rows, img_cols, img_channels = _img.shape

        if add_border:
            cv.rectangle(_img, pta, ptb, _color, 2)
            cv.rectangle(_img, pta, ptb, _color, -1)

        cv.putText(
            _img,
            _object_class,
            (pta[0], pta[1] + 10),
            cv.FONT_HERSHEY_PLAIN,
            0.6,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )

        if utils.is_inside_image(
            img_cols, img_rows, pt1[0], pt1[1]
        ) and utils.is_inside_image(img_cols, img_rows, pt2[0], pt2[1]):
            cv.rectangle(_img, pt1, pt2, _color, 2)

    def draw_line(
        self,
        _img: cv.Mat,
        _pt1: tuple[int, int],
        _pt2: tuple[int, int],
        _color: tuple[int, int, int],
        _thickness: int = 1,
    ):
        cv.line(_img, _pt1, _pt2, _color, _thickness)

    def draw_trajectory(
        self, _img: cv.Mat, _object_id: str, _frame_num: int, _params: Image.Params | None
    ):
        # object_class = self.scene.vcd.get_object(_object_id)["type"]
        fis = (
            self.scene.vcd.get_element_frame_intervals(
                core.ElementType.object, _object_id
            )
        ).get_dict()

        for fi in fis:
            prev_center: dict = {}
            for f in range(fi["frame_start"], _frame_num + 1):
                vcd_other_frame = self.scene.vcd.get_frame(f)
                if "objects" in vcd_other_frame:
                    for object_id_this, obj in vcd_other_frame["objects"].items():
                        if object_id_this is not _object_id:
                            continue

                        # Get value at this frame
                        if "object_data" in obj:
                            for object_data_key in obj["object_data"].keys():
                                for object_data_item in obj["object_data"][
                                    object_data_key
                                ]:
                                    if object_data_key == "bbox":
                                        bbox = object_data_item["val"]
                                        name = object_data_item["name"]

                                        center = (
                                            int(round(bbox[0])),
                                            int(round(bbox[1])),
                                        )

                                        # this is a dict to allow multiple trajectories
                                        # (e.g. several bbox per object)
                                        if prev_center.get(name) is not None:
                                            cv.line(
                                                _img,
                                                prev_center[name],
                                                center,
                                                (0, 0, 0),
                                                1,
                                                cv.LINE_AA,
                                            )

                                        # if _param is not None
                                        # cv.circle(_img, center, 2,
                                        #          _params.color_map[object_class], -1)

                                        prev_center[name] = center

    def draw_barrel_distortion_grid(
        self,
        img: cv.Mat,
        color: tuple[int, int, int],
        only_outer: bool = True,
        extended: bool = False,
    ):
        if self.camera is None:
            return
        if not isinstance(self.camera, (scl.CameraPinhole, scl.CameraFisheye)):
            return

        # Define grid in undistorted space and then apply distortPoint
        height, width = img.shape[:2]

        # Debug, see where the points fall if undistorted
        num_steps = 50
        x_start = 0
        x_end = width
        y_start = 0
        y_end = height

        if extended:
            factor = 1
            x_start = int(-factor * width)
            x_end = int(width + factor * width)
            y_start = int(-factor * height)
            y_end = int(height + factor * height)

        step_x = (x_end - x_start) / num_steps
        step_y = (y_end - y_start) / num_steps

        # Lines in X
        for y in np.linspace(y_start, y_end, num_steps + 1):
            for x in np.linspace(x_start, x_end, num_steps + 1):
                if only_outer:
                    if 0 < y < height:
                        continue

                p_a = (x, y, 1)  # (i * stepX, j * stepY)
                p_b = (x + step_x, y, 1)  # ((i+1) * stepX, j * stepY)
                if not extended:
                    if x + step_x > width:
                        continue
                p_da = self.camera.distort_points2d(np.array(p_a).reshape(3, 1))
                p_db = self.camera.distort_points2d(np.array(p_b).reshape(3, 1))

                # cv2.circle(imgDist, pointDistA, 3, bgr, -1)
                if (
                    0 <= p_da[0, 0] < width
                    and 0 <= p_da[1, 0] < height
                    and 0 <= p_db[0, 0] < width
                    and 0 <= p_db[1, 0] < height
                ):
                    color_to_use = color
                    if y in (0, height):
                        color_to_use = (255, 0, 0)
                    cv.line(
                        img,
                        (utils.round(p_da[0, 0]), utils.round(p_da[1, 0])),
                        (utils.round(p_db[0, 0]), utils.round(p_db[1, 0])),
                        color_to_use,
                        2,
                    )

        # Lines in Y
        for y in np.linspace(y_start, y_end, num_steps + 1):
            for x in np.linspace(x_start, x_end, num_steps + 1):
                if only_outer:
                    if 0 < x < width:
                        continue
                p_a = (x, y, 1)  # (i * stepX, j * stepY)
                p_b = (x, y + step_y, 1)  # (i * stepX, (j + 1) * stepY)
                if not extended:
                    if y + step_y > height:
                        continue
                p_da = self.camera.distort_points2d(np.array(p_a).reshape(3, 1))
                p_db = self.camera.distort_points2d(np.array(p_b).reshape(3, 1))

                # cv2.circle(imgDist, pointDistA, 3, bgr, -1)
                if (
                    0 <= p_da[0, 0] < width
                    and 0 <= p_da[1, 0] < height
                    and 0 <= p_db[0, 0] < width
                    and 0 <= p_db[1, 0] < height
                ):
                    color_to_use = color
                    if x in (0, width):
                        color_to_use = (255, 0, 0)
                    cv.line(
                        img,
                        (utils.round(p_da[0, 0]), utils.round(p_da[1, 0])),
                        (utils.round(p_db[0, 0]), utils.round(p_db[1, 0])),
                        color_to_use,
                        2,
                    )

        # r_limit
        if isinstance(self.camera, scl.CameraPinhole) and self.camera.r_limit is not None:
            # r_limit is a radius limit in calibrated coordinates
            # It might be possible to draw it by sampling points of a circle r in the
            # undistorted domain and apply distortPoints to them
            num_points = 100
            points2d_und_3xN = np.ones((3, num_points), dtype=np.float64)
            count = 0
            for angle in np.linspace(0, 2 * np.pi, num_points, endpoint=False):
                x = np.sin(angle) * self.camera.r_limit
                y = np.cos(angle) * self.camera.r_limit
                points2d_und_3xN[0, count] = x
                points2d_und_3xN[1, count] = y
                count += 1
            points2d_und_3xN = self.camera.K_3x3.dot(points2d_und_3xN)
            points2d_dist_3xN = self.camera.distort_points2d(points2d_und_3xN)
            point2d_prev = None
            for point2d in points2d_dist_3xN.transpose():
                x = utils.round(point2d[0])
                y = utils.round(point2d[1])
                if point2d_prev is not None:
                    cv.line(img, point2d_prev, (x, y), (0, 255, 255), 3)
                point2d_prev = (x, y)

    def draw_cs(
        self, _img: cv.Mat, cs_name: str, length: float = 1.0, thickness: int = 1
    ):
        """
        Draw a coordinate system.

        This function draws a coordinate system, as 3 lines of 1 meter length (Red, Green,
        Blue) corresponding to the X-axis, Y-axis, and Z-axis of the coordinate system.
        """
        if not self.scene.vcd.has_coordinate_system(cs_name):
            warnings.warn(
                "WARNING: Trying to draw coordinate system"
                + cs_name
                + " not existing in VCD.",
                Warning,
                2,
            )

        x_axis_as_points3d_4x2 = np.array(
            [[0.0, length], [0.0, 0.0], [0.0, 0.0], [1.0, 1.0]]
        )
        y_axis_as_points3d_4x2 = np.array(
            [[0.0, 0.0], [0.0, length], [0.0, 0.0], [1.0, 1.0]]
        )
        z_axis_as_points3d_4x2 = np.array(
            [[0.0, 0.0], [0.0, 0.0], [0.0, length], [1.0, 1.0]]
        )

        x_axis, _ = self.scene.project_points3d_4xN(
            x_axis_as_points3d_4x2, cs_name, cs_cam=self.camera_coordinate_system
        )
        y_axis, _ = self.scene.project_points3d_4xN(
            y_axis_as_points3d_4x2, cs_name, cs_cam=self.camera_coordinate_system
        )
        z_axis, _ = self.scene.project_points3d_4xN(
            z_axis_as_points3d_4x2, cs_name, cs_cam=self.camera_coordinate_system
        )

        self.draw_line(
            _img,
            (int(x_axis[0, 0]), int(x_axis[1, 0])),
            (int(x_axis[0, 1]), int(x_axis[1, 1])),
            (0, 0, 255),
            thickness,
        )
        self.draw_line(
            _img,
            (int(y_axis[0, 0]), int(y_axis[1, 0])),
            (int(y_axis[0, 1]), int(y_axis[1, 1])),
            (0, 255, 0),
            thickness,
        )
        self.draw_line(
            _img,
            (int(z_axis[0, 0]), int(z_axis[1, 0])),
            (int(z_axis[0, 1]), int(z_axis[1, 1])),
            (255, 0, 0),
            thickness,
        )

        cv.putText(
            _img,
            "X",
            (int(x_axis[0, 1]), int(x_axis[1, 1])),
            cv.FONT_HERSHEY_DUPLEX,
            0.8,
            (0, 0, 255),
            thickness,
            cv.LINE_AA,
        )
        cv.putText(
            _img,
            "Y",
            (int(y_axis[0, 1]), int(y_axis[1, 1])),
            cv.FONT_HERSHEY_DUPLEX,
            0.8,
            (0, 255, 0),
            thickness,
            cv.LINE_AA,
        )
        cv.putText(
            _img,
            "Z",
            (int(z_axis[0, 1]), int(z_axis[1, 1])),
            cv.FONT_HERSHEY_DUPLEX,
            0.8,
            (255, 0, 0),
            thickness,
            cv.LINE_AA,
        )

    def draw(
        self,
        _img: cv.Mat = None,
        _frame_num: int | None = None,
        _params: Image.Params | None = None,
        **kwargs: list[str] | dict,
    ) -> cv.Mat:
        _ = kwargs
        if _params is not None:
            if not isinstance(_params, Image.Params):
                raise TypeError(
                    "Argument '_params' must be of type 'vcd.draw.Image.Params'"
                )

            self.params = _params

        if _img is not None:
            img = _img
        else:
            img = self.reset_image()

        # Explore objects at VCD
        objects = None
        if _frame_num is not None:
            vcd_frame = self.scene.vcd.get_frame(_frame_num)
            if "objects" in vcd_frame:
                objects = vcd_frame["objects"]
        else:
            if self.scene.vcd.has_objects():
                objects = self.scene.vcd.get_objects()

        if not objects:
            return img

        for object_id, obj in objects.items():
            # Get object static info
            # name = self.scene.vcd.get_object(object_id)["name"]
            object_ = self.scene.vcd.get_object(object_id)
            if object_ is not None:
                object_class = object_["type"]
                if object_class in self.params.ignore_classes:
                    continue

                # Colors
                if self.params.color_map.get(object_class) is None:
                    # Let's create a new entry for this class
                    self.params.color_map[object_class] = (
                        randbelow(255),
                        randbelow(255),
                        randbelow(255),
                    )

            # Get current value at this frame
            if "object_data" in obj:
                object_data = obj["object_data"]
            else:
                # Check if the object has an object_data in root
                object_ = self.scene.vcd.get_object(object_id)
                if object_ is not None and "object_data" in object_:
                    object_data = object_["object_data"]

            # Loop over object data
            for object_data_key in object_data.keys():
                for object_data_item in object_data[object_data_key]:
                    ############################################
                    # bbox
                    ############################################
                    if object_data_key == "bbox":
                        bbox = object_data_item["val"]
                        bbox_name = object_data_item["name"]
                        if (
                            "coordinate_system" in object_data_item
                        ):  # Check if this bbox corresponds to this camera
                            if (
                                object_data_item["coordinate_system"]
                                != self.camera_coordinate_system
                            ):
                                continue

                        if len(object_data[object_data_key]) == 1:
                            # Only one bbox, let's write the class name
                            text = object_id + " " + object_class
                        else:
                            # If several bounding boxes, let's write the bounding box name
                            # text = "(" + object_id + "," + name +")-(" + object_class +
                            # ")-(" + bbox_name +")"
                            text = object_id + " " + bbox_name
                        self.draw_bbox(
                            img, bbox, text, self.params.color_map[object_class], True
                        )
                        if _frame_num is not None and self.params.draw_trajectory:
                            self.draw_trajectory(img, object_id, _frame_num, _params)
                    ############################################
                    # cuboid
                    ############################################
                    elif object_data_key == "cuboid":
                        # Read coordinate system of this cuboid, and transform into
                        # camera coordinate system
                        cuboid_cs = object_data_item["coordinate_system"]
                        cuboid_vals = object_data_item["val"]
                        cuboid_vals_transformed = self.scene.transform_cuboid(
                            cuboid_vals, cuboid_cs, self.camera_coordinate_system
                        )
                        self.draw_cuboid(
                            img,
                            cuboid_vals_transformed,
                            "",
                            self.params.color_map[object_class],
                            self.params.thickness,
                        )
                    ############################################
                    # mat as points3d_4xN
                    ############################################
                    elif object_data_key == "mat":
                        width = object_data_item["width"]
                        height = object_data_item["height"]

                        if height == 4:
                            # These are points 4xN
                            color = self.params.color_map[object_class]
                            points3d_4xN = np.array(object_data_item["val"]).reshape(
                                height, width
                            )
                            points_cs = object_data_item["coordinate_system"]

                            # First convert from the src coordinate system into the
                            # camera coordinate system
                            points3d_4xN_transformed = self.scene.transform_points3d_4xN(
                                points3d_4xN,
                                points_cs,
                                self.camera_coordinate_system,
                            )

                            if "attributes" in object_data_item:
                                for attr_type, attr_list in object_data_item[
                                    "attributes"
                                ].items():
                                    if attr_type == "vec":
                                        for attr in attr_list:
                                            if attr["name"] == "color":
                                                color = attr["val"]

                            if points3d_4xN_transformed is not None:
                                self.draw_points3d(img, points3d_4xN_transformed, color)

        # Draw info
        if self.camera_coordinate_system is not None:
            text = self.camera_coordinate_system
            margin = 20
            cv.putText(
                img,
                text,
                (margin, margin),
                cv.FONT_HERSHEY_DUPLEX,
                0.8,
                (0, 0, 0),
                2,
                cv.LINE_AA,
            )
            cv.putText(
                img,
                text,
                (margin, margin),
                cv.FONT_HERSHEY_DUPLEX,
                0.8,
                (255, 255, 255),
                1,
                cv.LINE_AA,
            )

        # Draw barrel
        # if self.params.draw_barrel:
        #    self.draw_barrel_distortion_grid(_img, (0, 255, 0), False, False)

        return img


class TopViewOrtho(Image):
    def __init__(
        self,
        scene: scl.Scene,
        camera_coordinate_system: str | None = None,
        step_x: float = 1.0,
        step_y: float = 1.0,
    ):
        super().__init__(scene, camera_coordinate_system=camera_coordinate_system)

        # Initialize image
        self.images: dict = {}

        # Grid config
        self.stepX = step_x  # meters
        self.stepY = step_y
        self.gridTextColor = (0, 0, 0)

    ##################################
    # Public functions
    ##################################
    def draw(
        self,
        _img: cv.Mat = None,
        _frame_num: int | None = None,
        _params: Image.Params | None = None,
        **kwargs: list[str] | dict,
    ) -> cv.Mat:
        # Create image
        if _img is not None:
            img = _img
        else:
            img = super().reset_image()

        # Compute and draw warped images
        for key, value in kwargs.items():
            if key == "add_images" and isinstance(value, dict):
                self.__add_images(value, _frame_num)

        # Draw BEW
        self.__draw_bevs(img, _frame_num)

        # Draw base grid
        self.__draw_topview_base(img)

        # Draw coordinate systems
        for key, value in kwargs.items():
            if key == "cs_names_to_draw":
                for cs_name in value:
                    self.draw_cs(img, cs_name, 2, 2)

        # Draw objects
        super().draw(img, _frame_num, _params)

        # Draw frame info
        self.__draw_info(img, _frame_num)

        return img

    ##################################
    # Internal functions
    ##################################
    def __add_images(self, imgs: dict, frame_num: int | None):
        if self.camera is not None and imgs is not None:
            h = self.camera.height  # this is the orthographic camera
            w = self.camera.width
            if not isinstance(imgs, dict):
                raise TypeError("Argument 'imgs' must be of type 'dict'")
            num_cams = len(imgs)
            cams = {}

            need_to_recompute_weights_acc = False
            need_to_recompute_maps = {}
            need_to_recompute_weights = {}

            for cam_name, img in imgs.items():
                if not self.scene.vcd.has_coordinate_system(cam_name):
                    raise ValueError(
                        "The provided scene does not have the specified coordinate system"
                    )
                # this call creates an entry inside scene
                cam = self.scene.get_camera(cam_name, frame_num, compute_remaps=False)
                if cam is not None:
                    cams[cam_name] = cam
                self.images.setdefault(cam_name, {})
                self.images[cam_name]["img"] = img
                _, static = self.scene.get_transform(
                    self.camera_coordinate_system, cam_name, frame_num
                )

                # Compute distances to this camera and add to weight map
                need_to_recompute_maps[cam_name] = False
                need_to_recompute_weights[cam_name] = False

                if (num_cams > 1 and not static) or (
                    num_cams > 1 and static and "weights" not in self.images[cam_name]
                ):
                    need_to_recompute_weights[cam_name] = True
                    need_to_recompute_weights_acc = True

                if (not static) or (static and "mapX" not in self.images[cam_name]):
                    need_to_recompute_maps[cam_name] = True

                # For each camera, compute the remaps and weights
                if need_to_recompute_maps:
                    map_x, map_y = self.scene.create_img_projection_maps(
                        cam_src_name=cam_name,
                        cam_dst_name=self.camera.name,
                        frame_num=frame_num,
                    )
                    self.images[cam_name]["mapX"] = map_x
                    self.images[cam_name]["mapY"] = map_y

                if need_to_recompute_weights[cam_name]:
                    print(cam_name + " top view weights computation...")
                    # self.images[cam_name].setdefault('weights', np.zeros((h, w, 3),
                    #  dtype=np.float32))
                    # self.images[cam_name].setdefault('weights',
                    #  (1.0/num_cams)*np.ones((h, w, 3),
                    #  dtype=np.float32))

                    # Weight according to distance to center point in image
                    # Might be good for fisheye
                    r_max_2 = (w / 2) * (w / 2) + (h / 2) * (h / 2)
                    x = map_x[:, :, 0]
                    y = map_x[:, :, 1]
                    r_2 = (x - w / 2) ** 2 + (y - h / 2) ** 2  # this is a matrix
                    weights = 1 - r_2 / r_max_2
                    temp = np.zeros((h, w, 3), dtype=np.float32)
                    temp[:, :, 0] = weights
                    temp[:, :, 1] = weights
                    temp[:, :, 2] = weights
                    self.images[cam_name].setdefault("weights", temp)

            # Compute accumulated weights if more than 1 camera
            if need_to_recompute_weights_acc:
                self.images["weights_acc"] = np.ones((h, w, 3), dtype=np.float32)
                # for idx, (cam_name, cam) in enumerate(cams.items()):
                #    self.images['weights_acc'] = cv.add(self.images[cam_name]['weights'],
                #  self.images['weights_acc'])

    def __draw_topview_base(self, _img: cv.Mat):
        if self.camera is None:
            warnings.warn(
                "__draw_topview_base: Camera is not  set",
                Warning,
                2,
            )
            return
        if not isinstance(self.camera, scl.CameraOrthographic):
            warnings.warn(
                "__draw_topview_base: Camera is not orthographic",
                Warning,
                2,
            )
            return

        # Grid x (1/2)
        for x in np.arange(self.camera.xmin, self.camera.xmax + self.stepX, self.stepX):
            x_0 = round(x)
            y_0 = self.camera.ymin
            y_1 = self.camera.ymax
            # points3d_4x2 = np.array([[x_0, y_0, 0.0, 1.0], [x_0, y_1, 0.0, 1.0]])
            points3d_4x2 = np.array([[x_0, x_0], [y_0, y_1], [0.0, 0.0], [1.0, 1.0]])
            points2d_3x2, _ = self.camera.project_points3d(points3d_4xN=points3d_4x2)
            self.draw_line(
                _img,
                (int(points2d_3x2[0, 0]), int(points2d_3x2[1, 0])),
                (int(points2d_3x2[0, 1]), int(points2d_3x2[1, 1])),
                (127, 127, 127),
            )
        # Grid y (1/2)
        for y in np.arange(self.camera.ymin, self.camera.ymax + self.stepY, self.stepY):
            y_0 = round(y)
            x_0 = self.camera.xmin
            x_1 = self.camera.xmax
            # points3d_4x2 = np.array([[x_0, y_0, 0.0, 1.0], [x_1, y_0, 0.0, 1.0]])
            points3d_4x2 = np.array([[x_0, x_1], [y_0, y_0], [0.0, 0.0], [1.0, 1.0]])
            points2d_3x2, _ = self.camera.project_points3d(points3d_4xN=points3d_4x2)
            self.draw_line(
                _img,
                (int(points2d_3x2[0, 0]), int(points2d_3x2[1, 0])),
                (int(points2d_3x2[0, 1]), int(points2d_3x2[1, 1])),
                (127, 127, 127),
            )
        # Grid x (2/2)
        for x in np.arange(self.camera.xmin, self.camera.xmax + self.stepX, self.stepX):
            x_0 = round(x)
            y_0 = self.camera.ymin
            points3d_4x1 = np.array([[x_0], [y_0], [0.0], [1.0]])
            points2d_3x1, _ = self.camera.project_points3d(points3d_4xN=points3d_4x1)
            cv.putText(
                _img,
                str(round(x_0)) + " m",
                (int(points2d_3x1[0, 0]) + 5, 15),
                cv.FONT_HERSHEY_PLAIN,
                0.6,
                self.gridTextColor,
                1,
                cv.LINE_AA,
            )
        # Grid y (2/2)
        for y in np.arange(self.camera.ymin, self.camera.ymax + self.stepY, self.stepY):
            y_0 = round(y)
            x_0 = self.camera.xmin
            points3d_4x1 = np.array([[x_0], [y_0], [0.0], [1.0]])
            points2d_3x1, _ = self.camera.project_points3d(points3d_4xN=points3d_4x1)
            cv.putText(
                _img,
                str(round(y_0)) + " m",
                (5, int(points2d_3x1[1, 0]) - 5),
                cv.FONT_HERSHEY_PLAIN,
                0.6,
                self.gridTextColor,
                1,
                cv.LINE_AA,
            )

        # World origin
        # cv.circle(self.topView, self.point2Pixel((0.0, 0.0)), 4, (255, 255, 255), -1)
        # cv.line(self.topView, self.point2Pixel((0.0, 0.0)), self.point2Pixel((5.0, 0.0)),
        #  (0, 0, 255), 2)
        # cv.line(self.topView, self.point2Pixel((0.0, 0.0)), self.point2Pixel((0.0, 5.0)),
        #  (0, 255, 0), 2)
        # cv.putText(self.topView, "X", self.point2Pixel((5.0, -0.5)), cv.FONT_HERSHEY_PLAIN,
        #  1.0, (0, 0, 255), 1, cv.LINE_AA)
        # cv.putText(self.topView, "Y", self.point2Pixel((-1.0, 5.0)), cv.FONT_HERSHEY_PLAIN,
        #  1.0, (0, 255, 0), 1, cv.LINE_AA)

    def __draw_bev(self, cam_name: str) -> npt.NDArray[np.float32]:
        img = self.images[cam_name]["img"]

        map_x = self.images[cam_name]["mapX"]
        map_y = self.images[cam_name]["mapY"]
        bev = cv.remap(
            img,
            map_x,
            map_y,
            interpolation=cv.INTER_LINEAR,
            borderMode=cv.BORDER_CONSTANT,
        )

        bev32 = np.array(bev, np.float32)
        if "weights" in self.images[cam_name]:
            cv.multiply(self.images[cam_name]["weights"], bev32, bev32)

        # cv.imshow('bev' + cam_name, bev)
        # cv.waitKey(1)

        # bev832 = np.uint8(bev32)
        # cv.imshow('bev8' + cam_name, bev832)
        # cv.waitKey(1)

        return bev32

    def __draw_bevs(self, _img: cv.Mat, _frame_num: int | None = None):
        """
        Draw BEVs into the topview.

        :param _frameNum:
        :return:
        """
        if self.camera is None:
            return

        num_cams = len(self.images)
        if num_cams == 0:
            return

        h = self.camera.height
        w = self.camera.width
        # Prepare image with drawing for this call
        # black background
        acc32: npt.NDArray[np.float32] = np.zeros((h, w, 3), dtype=np.float32)

        for cam_name in self.images:
            if self.scene.get_camera(cam_name, _frame_num) is not None:
                temp32 = self.__draw_bev(cam_name=cam_name)
                # mask = np.zeros((h, w), dtype=np.uint8)
                # mask[temp32 > 0] = 255
                # mask = (temp32 > 0)
                if num_cams > 1:
                    acc32 = cv.add(temp32, acc32)
        if num_cams > 1:
            acc32 /= self.images["weights_acc"]
        else:
            acc32 = temp32
        acc8 = np.uint8(acc32)
        # cv.imshow('acc', acc8)
        # cv.waitKey(1)

        # Copy into topView only new pixels
        nonzero = acc8 > 0
        _img[nonzero] = acc8

    def __draw_info(self, topview: npt.NDArray, frame_num: int | None = None):
        if not isinstance(self.camera, scl.CameraOrthographic):
            warnings.warn("__draw_info: Camera is not orthographic", Warning, 2)
            return

        h = topview.shape[0]
        w = topview.shape[1]
        w_margin = 250
        h_margin = 140
        h_step = 20
        font_size = 0.8
        cv.putText(
            topview,
            "Img. Size(px): " + str(w) + " x " + str(h),
            (w - w_margin, h - h_margin),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        if frame_num is None:
            frame_num = -1
        cv.putText(
            topview,
            "Frame: " + str(frame_num),
            (w - w_margin, h - h_margin + h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        cv.putText(
            topview,
            "CS: " + str(self.camera_coordinate_system),
            (w - w_margin, h - h_margin + 2 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )

        cv.putText(
            topview,
            "RangeX (m): (" + str(self.camera.xmin) + ", " + str(self.camera.xmax) + ")",
            (w - w_margin, h - h_margin + 3 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        cv.putText(
            topview,
            "RangeY (m): (" + str(self.camera.ymin) + ", " + str(self.camera.ymax) + ")",
            (w - w_margin, h - h_margin + 4 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )

        # cv.putText(topView, "OffsetX (px): (" + str(self.params.offsetX) + ", " +
        #           str(self.params.offsetX) + ")",
        #           (w - w_margin, h - h_margin + 5*h_step),
        #           cv.FONT_HERSHEY_PLAIN, font_size, (0, 0, 0), 1, cv.LINE_AA)
        # cv.putText(topView, "OffsetY (px): (" + str(self.params.offsetY) + ", " +
        #           str(self.params.offsetY) + ")",
        #           (w - w_margin, h - h_margin + 6*h_step),
        #           cv.FONT_HERSHEY_PLAIN, font_size, (0, 0, 0), 1, cv.LINE_AA)


class FrameInfoDrawer:
    # This class draws Element information in a window
    class Params:
        def __init__(self, _color_map: dict | None = None):
            if _color_map is None:
                self.color_map = {}
            else:
                if not isinstance(_color_map, dict):
                    raise TypeError("Argument '_color_map' must be of type 'dict'")
                self.color_map = _color_map

    def __init__(self, vcd: core.VCD):
        if not isinstance(vcd, core.VCD):
            raise TypeError("Argument 'vcd' must be of type 'vcd.core.VCD'")
        self.vcd = vcd
        self.params = FrameInfoDrawer.Params()

    def draw_base(self, _img: cv.Mat, _frame_num: int):
        if _frame_num is not None:
            last_frame = self.vcd.get_frame_intervals().get()[-1][1]
            text = "Frame: " + str(_frame_num) + " / " + str(last_frame)
        else:
            text = "Static image"

        margin = 20
        cv.putText(
            _img,
            text,
            (margin, margin),
            cv.FONT_HERSHEY_DUPLEX,
            0.8,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        rows, cols, channels = _img.shape
        cv.line(_img, (0, margin + 10), (cols, margin + 10), (0, 0, 0), 1)

    def draw(
        self,
        _frame_num: int,
        cols: int = 600,
        rows: int = 1200,
        _params: FrameInfoDrawer.Params | None = None,
    ) -> cv.Mat:
        img = 255 * np.ones((rows, cols, 3), np.uint8)
        if _params is not None:
            if not isinstance(_params, FrameInfoDrawer.Params):
                raise TypeError(
                    "Argument '_params' must be of type 'vcd.draw.FrameInfoDrawer.Params'"
                )
            self.params = _params

        self.draw_base(img, _frame_num)
        rows, cols, channels = img.shape

        # Explore objects at VCD
        count = 0
        margin = 50
        jump = 30

        # Explore objects at VCD
        if _frame_num is not None:
            vcd_frame = self.vcd.get_frame(_frame_num)
            if "objects" in vcd_frame:
                objects = vcd_frame["objects"]
        else:
            if self.vcd.has_objects():
                objects = self.vcd.get_objects()

        if len(objects) > 0:
            num_objects = len(objects.keys())
            text = "Objects: " + str(num_objects)
            cv.putText(
                img,
                text,
                (margin, margin),
                cv.FONT_HERSHEY_DUPLEX,
                0.8,
                (0, 0, 0),
                1,
                cv.LINE_AA,
            )
            cv.line(img, (0, margin + 10), (cols, margin + 10), (0, 0, 0), 1)
            count += 1
            for object_id, _object in objects.items():
                # Get object static info
                # name = self.vcd.get_object(object_id)["name"]
                fis = self.vcd.get_element_frame_intervals(
                    core.ElementType.object, object_id
                )

                # Colors
                _object = self.vcd.get_object(object_id)
                if _object is None:
                    continue
                object_class = _object["type"]
                if self.params.color_map.get(object_class) is None:
                    # Let's create a new entry for this class
                    self.params.color_map[object_class] = (
                        randbelow(255),
                        randbelow(255),
                        randbelow(255),
                    )

                # text = object_id + " " + object_class + " \"" + name + "\" " + fis.to_str()
                text = object_id + " " + object_class + " " + fis.to_str()
                cv.putText(
                    img,
                    text,
                    (margin, margin + count * jump),
                    cv.FONT_HERSHEY_DUPLEX,
                    0.6,
                    self.params.color_map[object_class],
                    1,
                    cv.LINE_AA,
                )
                count += 1

        return img


class TextDrawer:
    def __init__(self):
        pass

    def draw(self, _str: str, cols: int = 600, rows: int = 1200) -> cv.Mat:
        img = np.zeros((rows, cols, 3), np.uint8)
        count = 0

        # Split into pieces
        chars_per_line = cols // 8  # fits well with 0.4 fontsize
        text_rows = [
            _str[i : i + chars_per_line] for i in range(0, len(_str), chars_per_line)
        ]

        margin = 20
        jump = 20
        for text_row in text_rows:
            cv.putText(
                img,
                text_row,
                (margin, margin + count * jump),
                cv.FONT_HERSHEY_DUPLEX,
                0.4,
                (255, 255, 255),
                1,
                cv.LINE_AA,
            )
            count += 1

        return img

Classes

class FrameInfoDrawer (vcd: core.VCD)
Expand source code
class FrameInfoDrawer:
    # This class draws Element information in a window
    class Params:
        def __init__(self, _color_map: dict | None = None):
            if _color_map is None:
                self.color_map = {}
            else:
                if not isinstance(_color_map, dict):
                    raise TypeError("Argument '_color_map' must be of type 'dict'")
                self.color_map = _color_map

    def __init__(self, vcd: core.VCD):
        if not isinstance(vcd, core.VCD):
            raise TypeError("Argument 'vcd' must be of type 'vcd.core.VCD'")
        self.vcd = vcd
        self.params = FrameInfoDrawer.Params()

    def draw_base(self, _img: cv.Mat, _frame_num: int):
        if _frame_num is not None:
            last_frame = self.vcd.get_frame_intervals().get()[-1][1]
            text = "Frame: " + str(_frame_num) + " / " + str(last_frame)
        else:
            text = "Static image"

        margin = 20
        cv.putText(
            _img,
            text,
            (margin, margin),
            cv.FONT_HERSHEY_DUPLEX,
            0.8,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        rows, cols, channels = _img.shape
        cv.line(_img, (0, margin + 10), (cols, margin + 10), (0, 0, 0), 1)

    def draw(
        self,
        _frame_num: int,
        cols: int = 600,
        rows: int = 1200,
        _params: FrameInfoDrawer.Params | None = None,
    ) -> cv.Mat:
        img = 255 * np.ones((rows, cols, 3), np.uint8)
        if _params is not None:
            if not isinstance(_params, FrameInfoDrawer.Params):
                raise TypeError(
                    "Argument '_params' must be of type 'vcd.draw.FrameInfoDrawer.Params'"
                )
            self.params = _params

        self.draw_base(img, _frame_num)
        rows, cols, channels = img.shape

        # Explore objects at VCD
        count = 0
        margin = 50
        jump = 30

        # Explore objects at VCD
        if _frame_num is not None:
            vcd_frame = self.vcd.get_frame(_frame_num)
            if "objects" in vcd_frame:
                objects = vcd_frame["objects"]
        else:
            if self.vcd.has_objects():
                objects = self.vcd.get_objects()

        if len(objects) > 0:
            num_objects = len(objects.keys())
            text = "Objects: " + str(num_objects)
            cv.putText(
                img,
                text,
                (margin, margin),
                cv.FONT_HERSHEY_DUPLEX,
                0.8,
                (0, 0, 0),
                1,
                cv.LINE_AA,
            )
            cv.line(img, (0, margin + 10), (cols, margin + 10), (0, 0, 0), 1)
            count += 1
            for object_id, _object in objects.items():
                # Get object static info
                # name = self.vcd.get_object(object_id)["name"]
                fis = self.vcd.get_element_frame_intervals(
                    core.ElementType.object, object_id
                )

                # Colors
                _object = self.vcd.get_object(object_id)
                if _object is None:
                    continue
                object_class = _object["type"]
                if self.params.color_map.get(object_class) is None:
                    # Let's create a new entry for this class
                    self.params.color_map[object_class] = (
                        randbelow(255),
                        randbelow(255),
                        randbelow(255),
                    )

                # text = object_id + " " + object_class + " \"" + name + "\" " + fis.to_str()
                text = object_id + " " + object_class + " " + fis.to_str()
                cv.putText(
                    img,
                    text,
                    (margin, margin + count * jump),
                    cv.FONT_HERSHEY_DUPLEX,
                    0.6,
                    self.params.color_map[object_class],
                    1,
                    cv.LINE_AA,
                )
                count += 1

        return img

Class variables

var Params

Methods

def draw(self, _frame_num: int, cols: int = 600, rows: int = 1200) ‑> cv2.Mat
Expand source code
def draw(
    self,
    _frame_num: int,
    cols: int = 600,
    rows: int = 1200,
    _params: FrameInfoDrawer.Params | None = None,
) -> cv.Mat:
    img = 255 * np.ones((rows, cols, 3), np.uint8)
    if _params is not None:
        if not isinstance(_params, FrameInfoDrawer.Params):
            raise TypeError(
                "Argument '_params' must be of type 'vcd.draw.FrameInfoDrawer.Params'"
            )
        self.params = _params

    self.draw_base(img, _frame_num)
    rows, cols, channels = img.shape

    # Explore objects at VCD
    count = 0
    margin = 50
    jump = 30

    # Explore objects at VCD
    if _frame_num is not None:
        vcd_frame = self.vcd.get_frame(_frame_num)
        if "objects" in vcd_frame:
            objects = vcd_frame["objects"]
    else:
        if self.vcd.has_objects():
            objects = self.vcd.get_objects()

    if len(objects) > 0:
        num_objects = len(objects.keys())
        text = "Objects: " + str(num_objects)
        cv.putText(
            img,
            text,
            (margin, margin),
            cv.FONT_HERSHEY_DUPLEX,
            0.8,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        cv.line(img, (0, margin + 10), (cols, margin + 10), (0, 0, 0), 1)
        count += 1
        for object_id, _object in objects.items():
            # Get object static info
            # name = self.vcd.get_object(object_id)["name"]
            fis = self.vcd.get_element_frame_intervals(
                core.ElementType.object, object_id
            )

            # Colors
            _object = self.vcd.get_object(object_id)
            if _object is None:
                continue
            object_class = _object["type"]
            if self.params.color_map.get(object_class) is None:
                # Let's create a new entry for this class
                self.params.color_map[object_class] = (
                    randbelow(255),
                    randbelow(255),
                    randbelow(255),
                )

            # text = object_id + " " + object_class + " \"" + name + "\" " + fis.to_str()
            text = object_id + " " + object_class + " " + fis.to_str()
            cv.putText(
                img,
                text,
                (margin, margin + count * jump),
                cv.FONT_HERSHEY_DUPLEX,
                0.6,
                self.params.color_map[object_class],
                1,
                cv.LINE_AA,
            )
            count += 1

    return img
def draw_base(self, _img: cv.Mat, _frame_num: int)
Expand source code
def draw_base(self, _img: cv.Mat, _frame_num: int):
    if _frame_num is not None:
        last_frame = self.vcd.get_frame_intervals().get()[-1][1]
        text = "Frame: " + str(_frame_num) + " / " + str(last_frame)
    else:
        text = "Static image"

    margin = 20
    cv.putText(
        _img,
        text,
        (margin, margin),
        cv.FONT_HERSHEY_DUPLEX,
        0.8,
        (0, 0, 0),
        1,
        cv.LINE_AA,
    )
    rows, cols, channels = _img.shape
    cv.line(_img, (0, margin + 10), (cols, margin + 10), (0, 0, 0), 1)
class Image (scene: scl.Scene, camera_coordinate_system: str | None = None)

Draw 2D elements in the Image.

Devised to draw bboxes, it can also project 3D entities (e.g. cuboids) using the calibration parameters

Expand source code
class Image:
    """
    Draw 2D elements in the Image.

    Devised to draw bboxes, it can also project 3D entities (e.g. cuboids) using the
    calibration parameters
    """

    class Params:
        def __init__(
            self,
            _draw_trajectory: bool = False,
            _color_map: dict | None = None,
            _ignore_classes: dict | None = None,
            _draw_types: set[str] | None = None,
            _barrel: bool | None = None,
            _thickness: int | None = None,
        ):
            if _color_map is None:
                self.color_map = {}
            else:
                if not isinstance(_color_map, dict):
                    raise TypeError("Argument '_color_map' must be of type 'dict'")
                self.color_map = _color_map
            self.draw_trajectory = _draw_trajectory
            if _ignore_classes is None:
                self.ignore_classes = {}
            else:
                self.ignore_classes = _ignore_classes

            if _draw_types is not None:
                self.draw_types = _draw_types
            else:
                self.draw_types = {"bbox"}

            if _barrel is not None:
                self.draw_barrel = _barrel
            else:
                self.draw_barrel = False

            if _thickness is not None:
                self.thickness = _thickness
            else:
                self.thickness = 1

    def __init__(self, scene: scl.Scene, camera_coordinate_system: str | None = None):
        if not isinstance(scene, scl.Scene):
            raise TypeError("Argument 'scene' must be of type 'vcd.scl.Scene'")
        self.scene = scene
        if camera_coordinate_system is not None:
            if not scene.vcd.has_coordinate_system(camera_coordinate_system):
                raise ValueError(
                    "The provided scene does not have the specified coordinate system"
                )
            self.camera_coordinate_system = camera_coordinate_system
            self.camera = self.scene.get_camera(
                self.camera_coordinate_system, compute_remaps=False
            )

        self.params = Image.Params()

    def reset_image(self) -> cv.Mat:
        img = np.array([], np.int8)
        if self.camera is not None:
            img = np.zeros((self.camera.height, self.camera.width, 3), np.uint8)
            img.fill(255)
        return img

    def draw_points3d(
        self, _img: cv.Mat, points3d_4xN: npt.NDArray, _color: tuple[int, int, int]
    ):
        if self.camera is None:
            return
        # this function may return LESS than N points IF 3D points are BEHIND the camera
        points2d_3xN, idx_valid = self.camera.project_points3d(
            points3d_4xN, remove_outside=True
        )
        if points2d_3xN is None:
            return
        rows, cols = points2d_3xN.shape
        img_rows, img_cols, img_channels = _img.shape
        for i in range(0, cols):
            if idx_valid[i]:
                if np.isnan(points2d_3xN[0, i]) or np.isnan(points2d_3xN[1, i]):
                    continue
                center = (
                    utils.round(points2d_3xN[0, i]),
                    utils.round(points2d_3xN[1, i]),
                )
                if not utils.is_inside_image(img_cols, img_rows, center[0], center[1]):
                    continue
                cv.circle(_img, (int(center[0]), int(center[1])), 2, _color, -1)

    def draw_cuboid(
        self,
        _img: cv.Mat,
        _cuboid_vals: list[float],
        _class: str,
        _color: tuple[int, int, int],
        _thickness: int = 1,
    ):
        if not isinstance(_cuboid_vals, list):
            raise TypeError("Argument '_cuboid' must be of type 'list'")

        # (X, Y, Z, RX, RY, RZ, SX, SY, SZ)
        if len(_cuboid_vals) != 9:
            raise ValueError("Invalid argument '_cuboid' size")
        # TODO cuboids with quaternions

        # Generate object coordinates
        points3d_4x8 = utils.generate_cuboid_points_ref_4x8(_cuboid_vals)

        # this function may return LESS than 8 points IF 3D points are BEHIND the camera
        if self.camera is None:
            return
        points2d_4x8, idx_valid = self.camera.project_points3d(points3d_4x8, True)

        if points2d_4x8 is None:
            return
        img_rows, img_cols, img_channels = _img.shape

        pairs = (
            [0, 1],
            [1, 2],
            [2, 3],
            [3, 0],
            [0, 4],
            [1, 5],
            [2, 6],
            [3, 7],
            [4, 5],
            [5, 6],
            [6, 7],
            [7, 4],
        )
        for _count, pair in enumerate(pairs):
            if idx_valid[pair[0]] and idx_valid[pair[1]]:
                # if pair[0] >= num_points_projected or pair[1] >= num_points_projected:
                #    continue
                p_a = (
                    utils.round(points2d_4x8[0, pair[0]]),
                    utils.round(points2d_4x8[1, pair[0]]),
                )
                p_b = (
                    utils.round(points2d_4x8[0, pair[1]]),
                    utils.round(points2d_4x8[1, pair[1]]),
                )

                if not utils.is_inside_image(
                    img_cols, img_rows, p_a[0], p_b[1]
                ) or not utils.is_inside_image(img_cols, img_rows, p_b[0], p_b[1]):
                    continue

                cv.line(_img, p_a, p_b, _color, _thickness)

    def draw_bbox(
        self,
        _img: cv.Mat,
        _bbox: tuple[int, int, int, int],
        _object_class: str,
        _color: tuple[int, int, int],
        add_border: bool = False,
    ):
        pt1 = (int(round(_bbox[0] - _bbox[2] / 2)), int(round(_bbox[1] - _bbox[3] / 2)))
        pt2 = (int(round(_bbox[0] + _bbox[2] / 2)), int(round(_bbox[1] + _bbox[3] / 2)))

        pta = (pt1[0], pt1[1] - 15)
        ptb = (pt2[0], pt1[1])
        img_rows, img_cols, img_channels = _img.shape

        if add_border:
            cv.rectangle(_img, pta, ptb, _color, 2)
            cv.rectangle(_img, pta, ptb, _color, -1)

        cv.putText(
            _img,
            _object_class,
            (pta[0], pta[1] + 10),
            cv.FONT_HERSHEY_PLAIN,
            0.6,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )

        if utils.is_inside_image(
            img_cols, img_rows, pt1[0], pt1[1]
        ) and utils.is_inside_image(img_cols, img_rows, pt2[0], pt2[1]):
            cv.rectangle(_img, pt1, pt2, _color, 2)

    def draw_line(
        self,
        _img: cv.Mat,
        _pt1: tuple[int, int],
        _pt2: tuple[int, int],
        _color: tuple[int, int, int],
        _thickness: int = 1,
    ):
        cv.line(_img, _pt1, _pt2, _color, _thickness)

    def draw_trajectory(
        self, _img: cv.Mat, _object_id: str, _frame_num: int, _params: Image.Params | None
    ):
        # object_class = self.scene.vcd.get_object(_object_id)["type"]
        fis = (
            self.scene.vcd.get_element_frame_intervals(
                core.ElementType.object, _object_id
            )
        ).get_dict()

        for fi in fis:
            prev_center: dict = {}
            for f in range(fi["frame_start"], _frame_num + 1):
                vcd_other_frame = self.scene.vcd.get_frame(f)
                if "objects" in vcd_other_frame:
                    for object_id_this, obj in vcd_other_frame["objects"].items():
                        if object_id_this is not _object_id:
                            continue

                        # Get value at this frame
                        if "object_data" in obj:
                            for object_data_key in obj["object_data"].keys():
                                for object_data_item in obj["object_data"][
                                    object_data_key
                                ]:
                                    if object_data_key == "bbox":
                                        bbox = object_data_item["val"]
                                        name = object_data_item["name"]

                                        center = (
                                            int(round(bbox[0])),
                                            int(round(bbox[1])),
                                        )

                                        # this is a dict to allow multiple trajectories
                                        # (e.g. several bbox per object)
                                        if prev_center.get(name) is not None:
                                            cv.line(
                                                _img,
                                                prev_center[name],
                                                center,
                                                (0, 0, 0),
                                                1,
                                                cv.LINE_AA,
                                            )

                                        # if _param is not None
                                        # cv.circle(_img, center, 2,
                                        #          _params.color_map[object_class], -1)

                                        prev_center[name] = center

    def draw_barrel_distortion_grid(
        self,
        img: cv.Mat,
        color: tuple[int, int, int],
        only_outer: bool = True,
        extended: bool = False,
    ):
        if self.camera is None:
            return
        if not isinstance(self.camera, (scl.CameraPinhole, scl.CameraFisheye)):
            return

        # Define grid in undistorted space and then apply distortPoint
        height, width = img.shape[:2]

        # Debug, see where the points fall if undistorted
        num_steps = 50
        x_start = 0
        x_end = width
        y_start = 0
        y_end = height

        if extended:
            factor = 1
            x_start = int(-factor * width)
            x_end = int(width + factor * width)
            y_start = int(-factor * height)
            y_end = int(height + factor * height)

        step_x = (x_end - x_start) / num_steps
        step_y = (y_end - y_start) / num_steps

        # Lines in X
        for y in np.linspace(y_start, y_end, num_steps + 1):
            for x in np.linspace(x_start, x_end, num_steps + 1):
                if only_outer:
                    if 0 < y < height:
                        continue

                p_a = (x, y, 1)  # (i * stepX, j * stepY)
                p_b = (x + step_x, y, 1)  # ((i+1) * stepX, j * stepY)
                if not extended:
                    if x + step_x > width:
                        continue
                p_da = self.camera.distort_points2d(np.array(p_a).reshape(3, 1))
                p_db = self.camera.distort_points2d(np.array(p_b).reshape(3, 1))

                # cv2.circle(imgDist, pointDistA, 3, bgr, -1)
                if (
                    0 <= p_da[0, 0] < width
                    and 0 <= p_da[1, 0] < height
                    and 0 <= p_db[0, 0] < width
                    and 0 <= p_db[1, 0] < height
                ):
                    color_to_use = color
                    if y in (0, height):
                        color_to_use = (255, 0, 0)
                    cv.line(
                        img,
                        (utils.round(p_da[0, 0]), utils.round(p_da[1, 0])),
                        (utils.round(p_db[0, 0]), utils.round(p_db[1, 0])),
                        color_to_use,
                        2,
                    )

        # Lines in Y
        for y in np.linspace(y_start, y_end, num_steps + 1):
            for x in np.linspace(x_start, x_end, num_steps + 1):
                if only_outer:
                    if 0 < x < width:
                        continue
                p_a = (x, y, 1)  # (i * stepX, j * stepY)
                p_b = (x, y + step_y, 1)  # (i * stepX, (j + 1) * stepY)
                if not extended:
                    if y + step_y > height:
                        continue
                p_da = self.camera.distort_points2d(np.array(p_a).reshape(3, 1))
                p_db = self.camera.distort_points2d(np.array(p_b).reshape(3, 1))

                # cv2.circle(imgDist, pointDistA, 3, bgr, -1)
                if (
                    0 <= p_da[0, 0] < width
                    and 0 <= p_da[1, 0] < height
                    and 0 <= p_db[0, 0] < width
                    and 0 <= p_db[1, 0] < height
                ):
                    color_to_use = color
                    if x in (0, width):
                        color_to_use = (255, 0, 0)
                    cv.line(
                        img,
                        (utils.round(p_da[0, 0]), utils.round(p_da[1, 0])),
                        (utils.round(p_db[0, 0]), utils.round(p_db[1, 0])),
                        color_to_use,
                        2,
                    )

        # r_limit
        if isinstance(self.camera, scl.CameraPinhole) and self.camera.r_limit is not None:
            # r_limit is a radius limit in calibrated coordinates
            # It might be possible to draw it by sampling points of a circle r in the
            # undistorted domain and apply distortPoints to them
            num_points = 100
            points2d_und_3xN = np.ones((3, num_points), dtype=np.float64)
            count = 0
            for angle in np.linspace(0, 2 * np.pi, num_points, endpoint=False):
                x = np.sin(angle) * self.camera.r_limit
                y = np.cos(angle) * self.camera.r_limit
                points2d_und_3xN[0, count] = x
                points2d_und_3xN[1, count] = y
                count += 1
            points2d_und_3xN = self.camera.K_3x3.dot(points2d_und_3xN)
            points2d_dist_3xN = self.camera.distort_points2d(points2d_und_3xN)
            point2d_prev = None
            for point2d in points2d_dist_3xN.transpose():
                x = utils.round(point2d[0])
                y = utils.round(point2d[1])
                if point2d_prev is not None:
                    cv.line(img, point2d_prev, (x, y), (0, 255, 255), 3)
                point2d_prev = (x, y)

    def draw_cs(
        self, _img: cv.Mat, cs_name: str, length: float = 1.0, thickness: int = 1
    ):
        """
        Draw a coordinate system.

        This function draws a coordinate system, as 3 lines of 1 meter length (Red, Green,
        Blue) corresponding to the X-axis, Y-axis, and Z-axis of the coordinate system.
        """
        if not self.scene.vcd.has_coordinate_system(cs_name):
            warnings.warn(
                "WARNING: Trying to draw coordinate system"
                + cs_name
                + " not existing in VCD.",
                Warning,
                2,
            )

        x_axis_as_points3d_4x2 = np.array(
            [[0.0, length], [0.0, 0.0], [0.0, 0.0], [1.0, 1.0]]
        )
        y_axis_as_points3d_4x2 = np.array(
            [[0.0, 0.0], [0.0, length], [0.0, 0.0], [1.0, 1.0]]
        )
        z_axis_as_points3d_4x2 = np.array(
            [[0.0, 0.0], [0.0, 0.0], [0.0, length], [1.0, 1.0]]
        )

        x_axis, _ = self.scene.project_points3d_4xN(
            x_axis_as_points3d_4x2, cs_name, cs_cam=self.camera_coordinate_system
        )
        y_axis, _ = self.scene.project_points3d_4xN(
            y_axis_as_points3d_4x2, cs_name, cs_cam=self.camera_coordinate_system
        )
        z_axis, _ = self.scene.project_points3d_4xN(
            z_axis_as_points3d_4x2, cs_name, cs_cam=self.camera_coordinate_system
        )

        self.draw_line(
            _img,
            (int(x_axis[0, 0]), int(x_axis[1, 0])),
            (int(x_axis[0, 1]), int(x_axis[1, 1])),
            (0, 0, 255),
            thickness,
        )
        self.draw_line(
            _img,
            (int(y_axis[0, 0]), int(y_axis[1, 0])),
            (int(y_axis[0, 1]), int(y_axis[1, 1])),
            (0, 255, 0),
            thickness,
        )
        self.draw_line(
            _img,
            (int(z_axis[0, 0]), int(z_axis[1, 0])),
            (int(z_axis[0, 1]), int(z_axis[1, 1])),
            (255, 0, 0),
            thickness,
        )

        cv.putText(
            _img,
            "X",
            (int(x_axis[0, 1]), int(x_axis[1, 1])),
            cv.FONT_HERSHEY_DUPLEX,
            0.8,
            (0, 0, 255),
            thickness,
            cv.LINE_AA,
        )
        cv.putText(
            _img,
            "Y",
            (int(y_axis[0, 1]), int(y_axis[1, 1])),
            cv.FONT_HERSHEY_DUPLEX,
            0.8,
            (0, 255, 0),
            thickness,
            cv.LINE_AA,
        )
        cv.putText(
            _img,
            "Z",
            (int(z_axis[0, 1]), int(z_axis[1, 1])),
            cv.FONT_HERSHEY_DUPLEX,
            0.8,
            (255, 0, 0),
            thickness,
            cv.LINE_AA,
        )

    def draw(
        self,
        _img: cv.Mat = None,
        _frame_num: int | None = None,
        _params: Image.Params | None = None,
        **kwargs: list[str] | dict,
    ) -> cv.Mat:
        _ = kwargs
        if _params is not None:
            if not isinstance(_params, Image.Params):
                raise TypeError(
                    "Argument '_params' must be of type 'vcd.draw.Image.Params'"
                )

            self.params = _params

        if _img is not None:
            img = _img
        else:
            img = self.reset_image()

        # Explore objects at VCD
        objects = None
        if _frame_num is not None:
            vcd_frame = self.scene.vcd.get_frame(_frame_num)
            if "objects" in vcd_frame:
                objects = vcd_frame["objects"]
        else:
            if self.scene.vcd.has_objects():
                objects = self.scene.vcd.get_objects()

        if not objects:
            return img

        for object_id, obj in objects.items():
            # Get object static info
            # name = self.scene.vcd.get_object(object_id)["name"]
            object_ = self.scene.vcd.get_object(object_id)
            if object_ is not None:
                object_class = object_["type"]
                if object_class in self.params.ignore_classes:
                    continue

                # Colors
                if self.params.color_map.get(object_class) is None:
                    # Let's create a new entry for this class
                    self.params.color_map[object_class] = (
                        randbelow(255),
                        randbelow(255),
                        randbelow(255),
                    )

            # Get current value at this frame
            if "object_data" in obj:
                object_data = obj["object_data"]
            else:
                # Check if the object has an object_data in root
                object_ = self.scene.vcd.get_object(object_id)
                if object_ is not None and "object_data" in object_:
                    object_data = object_["object_data"]

            # Loop over object data
            for object_data_key in object_data.keys():
                for object_data_item in object_data[object_data_key]:
                    ############################################
                    # bbox
                    ############################################
                    if object_data_key == "bbox":
                        bbox = object_data_item["val"]
                        bbox_name = object_data_item["name"]
                        if (
                            "coordinate_system" in object_data_item
                        ):  # Check if this bbox corresponds to this camera
                            if (
                                object_data_item["coordinate_system"]
                                != self.camera_coordinate_system
                            ):
                                continue

                        if len(object_data[object_data_key]) == 1:
                            # Only one bbox, let's write the class name
                            text = object_id + " " + object_class
                        else:
                            # If several bounding boxes, let's write the bounding box name
                            # text = "(" + object_id + "," + name +")-(" + object_class +
                            # ")-(" + bbox_name +")"
                            text = object_id + " " + bbox_name
                        self.draw_bbox(
                            img, bbox, text, self.params.color_map[object_class], True
                        )
                        if _frame_num is not None and self.params.draw_trajectory:
                            self.draw_trajectory(img, object_id, _frame_num, _params)
                    ############################################
                    # cuboid
                    ############################################
                    elif object_data_key == "cuboid":
                        # Read coordinate system of this cuboid, and transform into
                        # camera coordinate system
                        cuboid_cs = object_data_item["coordinate_system"]
                        cuboid_vals = object_data_item["val"]
                        cuboid_vals_transformed = self.scene.transform_cuboid(
                            cuboid_vals, cuboid_cs, self.camera_coordinate_system
                        )
                        self.draw_cuboid(
                            img,
                            cuboid_vals_transformed,
                            "",
                            self.params.color_map[object_class],
                            self.params.thickness,
                        )
                    ############################################
                    # mat as points3d_4xN
                    ############################################
                    elif object_data_key == "mat":
                        width = object_data_item["width"]
                        height = object_data_item["height"]

                        if height == 4:
                            # These are points 4xN
                            color = self.params.color_map[object_class]
                            points3d_4xN = np.array(object_data_item["val"]).reshape(
                                height, width
                            )
                            points_cs = object_data_item["coordinate_system"]

                            # First convert from the src coordinate system into the
                            # camera coordinate system
                            points3d_4xN_transformed = self.scene.transform_points3d_4xN(
                                points3d_4xN,
                                points_cs,
                                self.camera_coordinate_system,
                            )

                            if "attributes" in object_data_item:
                                for attr_type, attr_list in object_data_item[
                                    "attributes"
                                ].items():
                                    if attr_type == "vec":
                                        for attr in attr_list:
                                            if attr["name"] == "color":
                                                color = attr["val"]

                            if points3d_4xN_transformed is not None:
                                self.draw_points3d(img, points3d_4xN_transformed, color)

        # Draw info
        if self.camera_coordinate_system is not None:
            text = self.camera_coordinate_system
            margin = 20
            cv.putText(
                img,
                text,
                (margin, margin),
                cv.FONT_HERSHEY_DUPLEX,
                0.8,
                (0, 0, 0),
                2,
                cv.LINE_AA,
            )
            cv.putText(
                img,
                text,
                (margin, margin),
                cv.FONT_HERSHEY_DUPLEX,
                0.8,
                (255, 255, 255),
                1,
                cv.LINE_AA,
            )

        # Draw barrel
        # if self.params.draw_barrel:
        #    self.draw_barrel_distortion_grid(_img, (0, 255, 0), False, False)

        return img

Subclasses

Class variables

var Params

Methods

def draw(self, **kwargs: list[str] | dict) ‑> cv2.Mat
Expand source code
def draw(
    self,
    _img: cv.Mat = None,
    _frame_num: int | None = None,
    _params: Image.Params | None = None,
    **kwargs: list[str] | dict,
) -> cv.Mat:
    _ = kwargs
    if _params is not None:
        if not isinstance(_params, Image.Params):
            raise TypeError(
                "Argument '_params' must be of type 'vcd.draw.Image.Params'"
            )

        self.params = _params

    if _img is not None:
        img = _img
    else:
        img = self.reset_image()

    # Explore objects at VCD
    objects = None
    if _frame_num is not None:
        vcd_frame = self.scene.vcd.get_frame(_frame_num)
        if "objects" in vcd_frame:
            objects = vcd_frame["objects"]
    else:
        if self.scene.vcd.has_objects():
            objects = self.scene.vcd.get_objects()

    if not objects:
        return img

    for object_id, obj in objects.items():
        # Get object static info
        # name = self.scene.vcd.get_object(object_id)["name"]
        object_ = self.scene.vcd.get_object(object_id)
        if object_ is not None:
            object_class = object_["type"]
            if object_class in self.params.ignore_classes:
                continue

            # Colors
            if self.params.color_map.get(object_class) is None:
                # Let's create a new entry for this class
                self.params.color_map[object_class] = (
                    randbelow(255),
                    randbelow(255),
                    randbelow(255),
                )

        # Get current value at this frame
        if "object_data" in obj:
            object_data = obj["object_data"]
        else:
            # Check if the object has an object_data in root
            object_ = self.scene.vcd.get_object(object_id)
            if object_ is not None and "object_data" in object_:
                object_data = object_["object_data"]

        # Loop over object data
        for object_data_key in object_data.keys():
            for object_data_item in object_data[object_data_key]:
                ############################################
                # bbox
                ############################################
                if object_data_key == "bbox":
                    bbox = object_data_item["val"]
                    bbox_name = object_data_item["name"]
                    if (
                        "coordinate_system" in object_data_item
                    ):  # Check if this bbox corresponds to this camera
                        if (
                            object_data_item["coordinate_system"]
                            != self.camera_coordinate_system
                        ):
                            continue

                    if len(object_data[object_data_key]) == 1:
                        # Only one bbox, let's write the class name
                        text = object_id + " " + object_class
                    else:
                        # If several bounding boxes, let's write the bounding box name
                        # text = "(" + object_id + "," + name +")-(" + object_class +
                        # ")-(" + bbox_name +")"
                        text = object_id + " " + bbox_name
                    self.draw_bbox(
                        img, bbox, text, self.params.color_map[object_class], True
                    )
                    if _frame_num is not None and self.params.draw_trajectory:
                        self.draw_trajectory(img, object_id, _frame_num, _params)
                ############################################
                # cuboid
                ############################################
                elif object_data_key == "cuboid":
                    # Read coordinate system of this cuboid, and transform into
                    # camera coordinate system
                    cuboid_cs = object_data_item["coordinate_system"]
                    cuboid_vals = object_data_item["val"]
                    cuboid_vals_transformed = self.scene.transform_cuboid(
                        cuboid_vals, cuboid_cs, self.camera_coordinate_system
                    )
                    self.draw_cuboid(
                        img,
                        cuboid_vals_transformed,
                        "",
                        self.params.color_map[object_class],
                        self.params.thickness,
                    )
                ############################################
                # mat as points3d_4xN
                ############################################
                elif object_data_key == "mat":
                    width = object_data_item["width"]
                    height = object_data_item["height"]

                    if height == 4:
                        # These are points 4xN
                        color = self.params.color_map[object_class]
                        points3d_4xN = np.array(object_data_item["val"]).reshape(
                            height, width
                        )
                        points_cs = object_data_item["coordinate_system"]

                        # First convert from the src coordinate system into the
                        # camera coordinate system
                        points3d_4xN_transformed = self.scene.transform_points3d_4xN(
                            points3d_4xN,
                            points_cs,
                            self.camera_coordinate_system,
                        )

                        if "attributes" in object_data_item:
                            for attr_type, attr_list in object_data_item[
                                "attributes"
                            ].items():
                                if attr_type == "vec":
                                    for attr in attr_list:
                                        if attr["name"] == "color":
                                            color = attr["val"]

                        if points3d_4xN_transformed is not None:
                            self.draw_points3d(img, points3d_4xN_transformed, color)

    # Draw info
    if self.camera_coordinate_system is not None:
        text = self.camera_coordinate_system
        margin = 20
        cv.putText(
            img,
            text,
            (margin, margin),
            cv.FONT_HERSHEY_DUPLEX,
            0.8,
            (0, 0, 0),
            2,
            cv.LINE_AA,
        )
        cv.putText(
            img,
            text,
            (margin, margin),
            cv.FONT_HERSHEY_DUPLEX,
            0.8,
            (255, 255, 255),
            1,
            cv.LINE_AA,
        )

    # Draw barrel
    # if self.params.draw_barrel:
    #    self.draw_barrel_distortion_grid(_img, (0, 255, 0), False, False)

    return img
def draw_barrel_distortion_grid(self, img: cv.Mat, color: tuple[int, int, int], only_outer: bool = True, extended: bool = False)
Expand source code
def draw_barrel_distortion_grid(
    self,
    img: cv.Mat,
    color: tuple[int, int, int],
    only_outer: bool = True,
    extended: bool = False,
):
    if self.camera is None:
        return
    if not isinstance(self.camera, (scl.CameraPinhole, scl.CameraFisheye)):
        return

    # Define grid in undistorted space and then apply distortPoint
    height, width = img.shape[:2]

    # Debug, see where the points fall if undistorted
    num_steps = 50
    x_start = 0
    x_end = width
    y_start = 0
    y_end = height

    if extended:
        factor = 1
        x_start = int(-factor * width)
        x_end = int(width + factor * width)
        y_start = int(-factor * height)
        y_end = int(height + factor * height)

    step_x = (x_end - x_start) / num_steps
    step_y = (y_end - y_start) / num_steps

    # Lines in X
    for y in np.linspace(y_start, y_end, num_steps + 1):
        for x in np.linspace(x_start, x_end, num_steps + 1):
            if only_outer:
                if 0 < y < height:
                    continue

            p_a = (x, y, 1)  # (i * stepX, j * stepY)
            p_b = (x + step_x, y, 1)  # ((i+1) * stepX, j * stepY)
            if not extended:
                if x + step_x > width:
                    continue
            p_da = self.camera.distort_points2d(np.array(p_a).reshape(3, 1))
            p_db = self.camera.distort_points2d(np.array(p_b).reshape(3, 1))

            # cv2.circle(imgDist, pointDistA, 3, bgr, -1)
            if (
                0 <= p_da[0, 0] < width
                and 0 <= p_da[1, 0] < height
                and 0 <= p_db[0, 0] < width
                and 0 <= p_db[1, 0] < height
            ):
                color_to_use = color
                if y in (0, height):
                    color_to_use = (255, 0, 0)
                cv.line(
                    img,
                    (utils.round(p_da[0, 0]), utils.round(p_da[1, 0])),
                    (utils.round(p_db[0, 0]), utils.round(p_db[1, 0])),
                    color_to_use,
                    2,
                )

    # Lines in Y
    for y in np.linspace(y_start, y_end, num_steps + 1):
        for x in np.linspace(x_start, x_end, num_steps + 1):
            if only_outer:
                if 0 < x < width:
                    continue
            p_a = (x, y, 1)  # (i * stepX, j * stepY)
            p_b = (x, y + step_y, 1)  # (i * stepX, (j + 1) * stepY)
            if not extended:
                if y + step_y > height:
                    continue
            p_da = self.camera.distort_points2d(np.array(p_a).reshape(3, 1))
            p_db = self.camera.distort_points2d(np.array(p_b).reshape(3, 1))

            # cv2.circle(imgDist, pointDistA, 3, bgr, -1)
            if (
                0 <= p_da[0, 0] < width
                and 0 <= p_da[1, 0] < height
                and 0 <= p_db[0, 0] < width
                and 0 <= p_db[1, 0] < height
            ):
                color_to_use = color
                if x in (0, width):
                    color_to_use = (255, 0, 0)
                cv.line(
                    img,
                    (utils.round(p_da[0, 0]), utils.round(p_da[1, 0])),
                    (utils.round(p_db[0, 0]), utils.round(p_db[1, 0])),
                    color_to_use,
                    2,
                )

    # r_limit
    if isinstance(self.camera, scl.CameraPinhole) and self.camera.r_limit is not None:
        # r_limit is a radius limit in calibrated coordinates
        # It might be possible to draw it by sampling points of a circle r in the
        # undistorted domain and apply distortPoints to them
        num_points = 100
        points2d_und_3xN = np.ones((3, num_points), dtype=np.float64)
        count = 0
        for angle in np.linspace(0, 2 * np.pi, num_points, endpoint=False):
            x = np.sin(angle) * self.camera.r_limit
            y = np.cos(angle) * self.camera.r_limit
            points2d_und_3xN[0, count] = x
            points2d_und_3xN[1, count] = y
            count += 1
        points2d_und_3xN = self.camera.K_3x3.dot(points2d_und_3xN)
        points2d_dist_3xN = self.camera.distort_points2d(points2d_und_3xN)
        point2d_prev = None
        for point2d in points2d_dist_3xN.transpose():
            x = utils.round(point2d[0])
            y = utils.round(point2d[1])
            if point2d_prev is not None:
                cv.line(img, point2d_prev, (x, y), (0, 255, 255), 3)
            point2d_prev = (x, y)
def draw_bbox(self, _img: cv.Mat, _bbox: tuple[int, int, int, int], _object_class: str, _color: tuple[int, int, int], add_border: bool = False)
Expand source code
def draw_bbox(
    self,
    _img: cv.Mat,
    _bbox: tuple[int, int, int, int],
    _object_class: str,
    _color: tuple[int, int, int],
    add_border: bool = False,
):
    pt1 = (int(round(_bbox[0] - _bbox[2] / 2)), int(round(_bbox[1] - _bbox[3] / 2)))
    pt2 = (int(round(_bbox[0] + _bbox[2] / 2)), int(round(_bbox[1] + _bbox[3] / 2)))

    pta = (pt1[0], pt1[1] - 15)
    ptb = (pt2[0], pt1[1])
    img_rows, img_cols, img_channels = _img.shape

    if add_border:
        cv.rectangle(_img, pta, ptb, _color, 2)
        cv.rectangle(_img, pta, ptb, _color, -1)

    cv.putText(
        _img,
        _object_class,
        (pta[0], pta[1] + 10),
        cv.FONT_HERSHEY_PLAIN,
        0.6,
        (0, 0, 0),
        1,
        cv.LINE_AA,
    )

    if utils.is_inside_image(
        img_cols, img_rows, pt1[0], pt1[1]
    ) and utils.is_inside_image(img_cols, img_rows, pt2[0], pt2[1]):
        cv.rectangle(_img, pt1, pt2, _color, 2)
def draw_cs(self, _img: cv.Mat, cs_name: str, length: float = 1.0, thickness: int = 1)

Draw a coordinate system.

This function draws a coordinate system, as 3 lines of 1 meter length (Red, Green, Blue) corresponding to the X-axis, Y-axis, and Z-axis of the coordinate system.

Expand source code
def draw_cs(
    self, _img: cv.Mat, cs_name: str, length: float = 1.0, thickness: int = 1
):
    """
    Draw a coordinate system.

    This function draws a coordinate system, as 3 lines of 1 meter length (Red, Green,
    Blue) corresponding to the X-axis, Y-axis, and Z-axis of the coordinate system.
    """
    if not self.scene.vcd.has_coordinate_system(cs_name):
        warnings.warn(
            "WARNING: Trying to draw coordinate system"
            + cs_name
            + " not existing in VCD.",
            Warning,
            2,
        )

    x_axis_as_points3d_4x2 = np.array(
        [[0.0, length], [0.0, 0.0], [0.0, 0.0], [1.0, 1.0]]
    )
    y_axis_as_points3d_4x2 = np.array(
        [[0.0, 0.0], [0.0, length], [0.0, 0.0], [1.0, 1.0]]
    )
    z_axis_as_points3d_4x2 = np.array(
        [[0.0, 0.0], [0.0, 0.0], [0.0, length], [1.0, 1.0]]
    )

    x_axis, _ = self.scene.project_points3d_4xN(
        x_axis_as_points3d_4x2, cs_name, cs_cam=self.camera_coordinate_system
    )
    y_axis, _ = self.scene.project_points3d_4xN(
        y_axis_as_points3d_4x2, cs_name, cs_cam=self.camera_coordinate_system
    )
    z_axis, _ = self.scene.project_points3d_4xN(
        z_axis_as_points3d_4x2, cs_name, cs_cam=self.camera_coordinate_system
    )

    self.draw_line(
        _img,
        (int(x_axis[0, 0]), int(x_axis[1, 0])),
        (int(x_axis[0, 1]), int(x_axis[1, 1])),
        (0, 0, 255),
        thickness,
    )
    self.draw_line(
        _img,
        (int(y_axis[0, 0]), int(y_axis[1, 0])),
        (int(y_axis[0, 1]), int(y_axis[1, 1])),
        (0, 255, 0),
        thickness,
    )
    self.draw_line(
        _img,
        (int(z_axis[0, 0]), int(z_axis[1, 0])),
        (int(z_axis[0, 1]), int(z_axis[1, 1])),
        (255, 0, 0),
        thickness,
    )

    cv.putText(
        _img,
        "X",
        (int(x_axis[0, 1]), int(x_axis[1, 1])),
        cv.FONT_HERSHEY_DUPLEX,
        0.8,
        (0, 0, 255),
        thickness,
        cv.LINE_AA,
    )
    cv.putText(
        _img,
        "Y",
        (int(y_axis[0, 1]), int(y_axis[1, 1])),
        cv.FONT_HERSHEY_DUPLEX,
        0.8,
        (0, 255, 0),
        thickness,
        cv.LINE_AA,
    )
    cv.putText(
        _img,
        "Z",
        (int(z_axis[0, 1]), int(z_axis[1, 1])),
        cv.FONT_HERSHEY_DUPLEX,
        0.8,
        (255, 0, 0),
        thickness,
        cv.LINE_AA,
    )
def draw_cuboid(self, _img: cv.Mat, _cuboid_vals: list[float], _class: str, _color: tuple[int, int, int])
Expand source code
def draw_cuboid(
    self,
    _img: cv.Mat,
    _cuboid_vals: list[float],
    _class: str,
    _color: tuple[int, int, int],
    _thickness: int = 1,
):
    if not isinstance(_cuboid_vals, list):
        raise TypeError("Argument '_cuboid' must be of type 'list'")

    # (X, Y, Z, RX, RY, RZ, SX, SY, SZ)
    if len(_cuboid_vals) != 9:
        raise ValueError("Invalid argument '_cuboid' size")
    # TODO cuboids with quaternions

    # Generate object coordinates
    points3d_4x8 = utils.generate_cuboid_points_ref_4x8(_cuboid_vals)

    # this function may return LESS than 8 points IF 3D points are BEHIND the camera
    if self.camera is None:
        return
    points2d_4x8, idx_valid = self.camera.project_points3d(points3d_4x8, True)

    if points2d_4x8 is None:
        return
    img_rows, img_cols, img_channels = _img.shape

    pairs = (
        [0, 1],
        [1, 2],
        [2, 3],
        [3, 0],
        [0, 4],
        [1, 5],
        [2, 6],
        [3, 7],
        [4, 5],
        [5, 6],
        [6, 7],
        [7, 4],
    )
    for _count, pair in enumerate(pairs):
        if idx_valid[pair[0]] and idx_valid[pair[1]]:
            # if pair[0] >= num_points_projected or pair[1] >= num_points_projected:
            #    continue
            p_a = (
                utils.round(points2d_4x8[0, pair[0]]),
                utils.round(points2d_4x8[1, pair[0]]),
            )
            p_b = (
                utils.round(points2d_4x8[0, pair[1]]),
                utils.round(points2d_4x8[1, pair[1]]),
            )

            if not utils.is_inside_image(
                img_cols, img_rows, p_a[0], p_b[1]
            ) or not utils.is_inside_image(img_cols, img_rows, p_b[0], p_b[1]):
                continue

            cv.line(_img, p_a, p_b, _color, _thickness)
def draw_line(self, _img: cv.Mat, _pt1: tuple[int, int], _pt2: tuple[int, int], _color: tuple[int, int, int])
Expand source code
def draw_line(
    self,
    _img: cv.Mat,
    _pt1: tuple[int, int],
    _pt2: tuple[int, int],
    _color: tuple[int, int, int],
    _thickness: int = 1,
):
    cv.line(_img, _pt1, _pt2, _color, _thickness)
def draw_points3d(self, _img: cv.Mat, points3d_4xN: npt.NDArray, _color: tuple[int, int, int])
Expand source code
def draw_points3d(
    self, _img: cv.Mat, points3d_4xN: npt.NDArray, _color: tuple[int, int, int]
):
    if self.camera is None:
        return
    # this function may return LESS than N points IF 3D points are BEHIND the camera
    points2d_3xN, idx_valid = self.camera.project_points3d(
        points3d_4xN, remove_outside=True
    )
    if points2d_3xN is None:
        return
    rows, cols = points2d_3xN.shape
    img_rows, img_cols, img_channels = _img.shape
    for i in range(0, cols):
        if idx_valid[i]:
            if np.isnan(points2d_3xN[0, i]) or np.isnan(points2d_3xN[1, i]):
                continue
            center = (
                utils.round(points2d_3xN[0, i]),
                utils.round(points2d_3xN[1, i]),
            )
            if not utils.is_inside_image(img_cols, img_rows, center[0], center[1]):
                continue
            cv.circle(_img, (int(center[0]), int(center[1])), 2, _color, -1)
def draw_trajectory(self, _img: cv.Mat, _object_id: str, _frame_num: int, _params: Image.Params | None)
Expand source code
def draw_trajectory(
    self, _img: cv.Mat, _object_id: str, _frame_num: int, _params: Image.Params | None
):
    # object_class = self.scene.vcd.get_object(_object_id)["type"]
    fis = (
        self.scene.vcd.get_element_frame_intervals(
            core.ElementType.object, _object_id
        )
    ).get_dict()

    for fi in fis:
        prev_center: dict = {}
        for f in range(fi["frame_start"], _frame_num + 1):
            vcd_other_frame = self.scene.vcd.get_frame(f)
            if "objects" in vcd_other_frame:
                for object_id_this, obj in vcd_other_frame["objects"].items():
                    if object_id_this is not _object_id:
                        continue

                    # Get value at this frame
                    if "object_data" in obj:
                        for object_data_key in obj["object_data"].keys():
                            for object_data_item in obj["object_data"][
                                object_data_key
                            ]:
                                if object_data_key == "bbox":
                                    bbox = object_data_item["val"]
                                    name = object_data_item["name"]

                                    center = (
                                        int(round(bbox[0])),
                                        int(round(bbox[1])),
                                    )

                                    # this is a dict to allow multiple trajectories
                                    # (e.g. several bbox per object)
                                    if prev_center.get(name) is not None:
                                        cv.line(
                                            _img,
                                            prev_center[name],
                                            center,
                                            (0, 0, 0),
                                            1,
                                            cv.LINE_AA,
                                        )

                                    # if _param is not None
                                    # cv.circle(_img, center, 2,
                                    #          _params.color_map[object_class], -1)

                                    prev_center[name] = center
def reset_image(self) ‑> cv2.Mat
Expand source code
def reset_image(self) -> cv.Mat:
    img = np.array([], np.int8)
    if self.camera is not None:
        img = np.zeros((self.camera.height, self.camera.width, 3), np.uint8)
        img.fill(255)
    return img
class SetupViewer (scene: scl.Scene, coordinate_system: str)

This class offers Matplotlib routines to display the coordinate systems of the Scene.

Expand source code
class SetupViewer:
    """This class offers Matplotlib routines to display the coordinate systems of the Scene."""

    def __init__(self, scene: scl.Scene, coordinate_system: str):
        if not isinstance(scene, scl.Scene):
            raise TypeError("Argument 'scene' must be of type 'vcd.scl.Scene'")
        self.scene = scene
        self.fig = plt.figure(figsize=(8, 8))
        self.ax = self.fig.add_subplot(projection="3d")
        self.coordinate_system = coordinate_system
        if not self.scene.vcd.has_coordinate_system(coordinate_system):
            raise ValueError(
                "The provided scene does not have the specified coordinate system"
            )

    def __plot_cs(self, pose_wrt_ref: npt.NDArray, name: str, la: float = 1):
        # Explore the coordinate systems defined for this scene
        axis = np.array(
            [
                [0, la, 0, 0, 0, 0],
                [0, 0, 0, la, 0, 0],
                [0, 0, 0, 0, 0, la],
                [1, 1, 1, 1, 1, 1],
            ]
        )  # matrix with several 4x1 points
        pose_wrt_ref = np.array(pose_wrt_ref).reshape(4, 4)
        axis_ref = pose_wrt_ref.dot(axis)
        origin = axis_ref[:, 0]
        x_axis_end = axis_ref[:, 1]
        y_axis_end = axis_ref[:, 3]
        z_axis_end = axis_ref[:, 5]
        self.ax.plot(
            [origin[0], x_axis_end[0]],
            [origin[1], x_axis_end[1]],
            [origin[2], x_axis_end[2]],
            "r-",
        )
        self.ax.plot(
            [origin[0], y_axis_end[0]],
            [origin[1], y_axis_end[1]],
            [origin[2], y_axis_end[2]],
            "g-",
        )
        self.ax.plot(
            [origin[0], z_axis_end[0]],
            [origin[1], z_axis_end[1]],
            [origin[2], z_axis_end[2]],
            "b-",
        )

        self.ax.text(origin[0], origin[1], origin[2], rf"{name}")
        self.ax.text(x_axis_end[0], x_axis_end[1], x_axis_end[2], "X")
        self.ax.text(y_axis_end[0], y_axis_end[1], y_axis_end[2], "Y")
        self.ax.text(z_axis_end[0], z_axis_end[1], z_axis_end[2], "Z")

    def plot_cuboid(self, cuboid_cs: str, cuboid_vals: tuple | list, color: Any):
        t, static = self.scene.get_transform(cuboid_cs, self.coordinate_system)
        cuboid_vals_transformed = utils.transform_cuboid(cuboid_vals, t)

        p = utils.generate_cuboid_points_ref_4x8(cuboid_vals_transformed)

        pairs = (
            [0, 1],
            [1, 2],
            [2, 3],
            [3, 0],
            [0, 4],
            [1, 5],
            [2, 6],
            [3, 7],
            [4, 5],
            [5, 6],
            [6, 7],
            [7, 4],
        )
        for pair in pairs:
            self.ax.plot(
                [p[0, pair[0]], p[0, pair[1]]],
                [p[1, pair[0]], p[1, pair[1]]],
                [p[2, pair[0]], p[2, pair[1]]],
                c=color,
            )

    def plot_setup(self, axes: list[list] | None = None) -> fig.Figure:
        for cs_name, cs in self.scene.vcd.get_root()["coordinate_systems"].items():
            transform, _static = self.scene.get_transform(cs_name, self.coordinate_system)
            la = 2.0
            if cs["type"] == "sensor_cs":
                la = 0.5
            self.__plot_cs(transform, cs_name, la)

        if "objects" in self.scene.vcd.get_root():
            for _object_id, obj in self.scene.vcd.get_root()["objects"].items():
                if obj["name"] == "Ego-car":
                    cuboid = obj["object_data"]["cuboid"][0]
                    cuboid_cs = cuboid["coordinate_system"]
                    cuboid_vals = cuboid["val"]
                    self.plot_cuboid(cuboid_cs, cuboid_vals, "k")

                else:
                    if "object_data" in obj:
                        if "cuboid" in obj["object_data"]:
                            for cuboid in obj["object_data"]["cuboid"]:
                                self.plot_cuboid(
                                    cuboid["coordinate_system"], cuboid["val"], "k"
                                )

        if axes is None:
            self.ax.set_xlim(-1.25, 4.25)
            self.ax.set_ylim(-2.75, 2.75)
            self.ax.set_zlim(0, 5.5)
        else:
            self.ax.set_xlim(axes[0][0], axes[0][1])
            self.ax.set_ylim(axes[1][0], axes[1][1])
            self.ax.set_zlim(axes[2][0], axes[2][1])

        self.ax.set_xlabel("X")
        self.ax.set_ylabel("Y")
        self.ax.set_zlabel("Z")

        return self.fig

Methods

def plot_cuboid(self, cuboid_cs: str, cuboid_vals: tuple | list, color: Any)
Expand source code
def plot_cuboid(self, cuboid_cs: str, cuboid_vals: tuple | list, color: Any):
    t, static = self.scene.get_transform(cuboid_cs, self.coordinate_system)
    cuboid_vals_transformed = utils.transform_cuboid(cuboid_vals, t)

    p = utils.generate_cuboid_points_ref_4x8(cuboid_vals_transformed)

    pairs = (
        [0, 1],
        [1, 2],
        [2, 3],
        [3, 0],
        [0, 4],
        [1, 5],
        [2, 6],
        [3, 7],
        [4, 5],
        [5, 6],
        [6, 7],
        [7, 4],
    )
    for pair in pairs:
        self.ax.plot(
            [p[0, pair[0]], p[0, pair[1]]],
            [p[1, pair[0]], p[1, pair[1]]],
            [p[2, pair[0]], p[2, pair[1]]],
            c=color,
        )
def plot_setup(self, axes: list[list] | None = None) ‑> matplotlib.figure.Figure
Expand source code
def plot_setup(self, axes: list[list] | None = None) -> fig.Figure:
    for cs_name, cs in self.scene.vcd.get_root()["coordinate_systems"].items():
        transform, _static = self.scene.get_transform(cs_name, self.coordinate_system)
        la = 2.0
        if cs["type"] == "sensor_cs":
            la = 0.5
        self.__plot_cs(transform, cs_name, la)

    if "objects" in self.scene.vcd.get_root():
        for _object_id, obj in self.scene.vcd.get_root()["objects"].items():
            if obj["name"] == "Ego-car":
                cuboid = obj["object_data"]["cuboid"][0]
                cuboid_cs = cuboid["coordinate_system"]
                cuboid_vals = cuboid["val"]
                self.plot_cuboid(cuboid_cs, cuboid_vals, "k")

            else:
                if "object_data" in obj:
                    if "cuboid" in obj["object_data"]:
                        for cuboid in obj["object_data"]["cuboid"]:
                            self.plot_cuboid(
                                cuboid["coordinate_system"], cuboid["val"], "k"
                            )

    if axes is None:
        self.ax.set_xlim(-1.25, 4.25)
        self.ax.set_ylim(-2.75, 2.75)
        self.ax.set_zlim(0, 5.5)
    else:
        self.ax.set_xlim(axes[0][0], axes[0][1])
        self.ax.set_ylim(axes[1][0], axes[1][1])
        self.ax.set_zlim(axes[2][0], axes[2][1])

    self.ax.set_xlabel("X")
    self.ax.set_ylabel("Y")
    self.ax.set_zlabel("Z")

    return self.fig
class TextDrawer
Expand source code
class TextDrawer:
    def __init__(self):
        pass

    def draw(self, _str: str, cols: int = 600, rows: int = 1200) -> cv.Mat:
        img = np.zeros((rows, cols, 3), np.uint8)
        count = 0

        # Split into pieces
        chars_per_line = cols // 8  # fits well with 0.4 fontsize
        text_rows = [
            _str[i : i + chars_per_line] for i in range(0, len(_str), chars_per_line)
        ]

        margin = 20
        jump = 20
        for text_row in text_rows:
            cv.putText(
                img,
                text_row,
                (margin, margin + count * jump),
                cv.FONT_HERSHEY_DUPLEX,
                0.4,
                (255, 255, 255),
                1,
                cv.LINE_AA,
            )
            count += 1

        return img

Methods

def draw(self, _str: str, cols: int = 600, rows: int = 1200) ‑> cv2.Mat
Expand source code
def draw(self, _str: str, cols: int = 600, rows: int = 1200) -> cv.Mat:
    img = np.zeros((rows, cols, 3), np.uint8)
    count = 0

    # Split into pieces
    chars_per_line = cols // 8  # fits well with 0.4 fontsize
    text_rows = [
        _str[i : i + chars_per_line] for i in range(0, len(_str), chars_per_line)
    ]

    margin = 20
    jump = 20
    for text_row in text_rows:
        cv.putText(
            img,
            text_row,
            (margin, margin + count * jump),
            cv.FONT_HERSHEY_DUPLEX,
            0.4,
            (255, 255, 255),
            1,
            cv.LINE_AA,
        )
        count += 1

    return img
class TopView (scene: scl.Scene, coordinate_system: str, params: TopView.Params | None = None)

Define functions to draw topview representations.

This class draws a top view of the scene, assuming Z=0 is the ground plane (i.e. the topview sees the XY plane) Range and scale can be used to select a certain part of the XY plane.

Expand source code
class TopView:
    """
    Define functions to draw topview representations.

    This class draws a top view of the scene, assuming Z=0 is the ground plane (i.e. the
    topview sees the XY plane) Range and scale can be used to select a certain part of the XY
    plane.
    """

    class Params:  # pylint: disable=too-many-instance-attributes
        """
        Define the parameters needed to draw topview representations.

        Assuming cuboids are drawn top view, so Z coordinate is ignored RZ is the rotation in
        Z-axis, it assumes/enforces SY>SX, thus keeping RZ between pi/2 and -pi/2.

        Z, RX, RY, and SZ are ignored

        For Vehicle cases, we adopt ISO8855: origin at rear axle at ground, x-to-front, y-to-
        left
        """

        def __init__(
            self,
            step_x: float | None = None,
            step_y: float | None = None,
            background_color: int | None = None,
            topview_size: tuple[int, int] | None = None,
            range_x: tuple[float, float] | None = None,
            range_y: tuple[float, float] | None = None,
            color_map: dict | None = None,
            ignore_classes: dict | None = None,
            draw_grid: bool = True,
            draw_only_current_image: bool = True,
        ):
            self.topview_size = (1920, 1080)  # width, height
            if topview_size is not None:
                if not isinstance(topview_size, tuple):
                    raise TypeError("Argument 'topview_size' must be of type 'tuple'")
                self.topview_size = topview_size

            self.ar = self.topview_size[0] / self.topview_size[1]

            self.range_x = (-80.0, 80.0)
            if range_x is not None:
                if not isinstance(range_x, tuple):
                    raise TypeError("Argument 'range_x' must be of type 'tuple'")
                self.range_x = range_x

            self.range_y = (self.range_x[0] / self.ar, self.range_x[1] / self.ar)
            if range_y is not None:
                if not isinstance(range_y, tuple):
                    raise TypeError("Argument 'range_y' must be of type 'tuple'")
                self.range_y = range_y

            self.scale_x = self.topview_size[0] / (self.range_x[1] - self.range_x[0])
            self.scale_y = -self.topview_size[1] / (
                self.range_y[1] - self.range_y[0]
            )  # Negative?

            self.offset_x = round(-self.range_x[0] * self.scale_x)
            self.offset_y = round(
                -self.range_y[1] * self.scale_y
            )  # TODO: shouldn't it be -self.rangeY[0]?

            self.S = np.array(
                [
                    [self.scale_x, 0, self.offset_x],
                    [0, self.scale_y, self.offset_y],
                    [0, 0, 1],
                ]
            )

            self.step_x = 1.0
            if step_x is not None:
                self.step_x = step_x
            self.step_y = 1.0
            if step_y is not None:
                self.step_y = step_y

            self.grid_lines_thickness = 1
            self.background_color = 255
            if background_color is not None:
                self.background_color = background_color

            self.grid_text_color = (0, 0, 0)

            if color_map is None:
                self.color_map = {}
            else:
                if not isinstance(color_map, dict):
                    raise TypeError("Argument 'color_map' must be of type 'dict'")
                self.color_map = color_map

            if ignore_classes is None:
                self.ignore_classes = {}
            else:
                self.ignore_classes = ignore_classes

            self.draw_grid = True
            if draw_grid is not None:
                self.draw_grid = draw_grid

            self.draw_only_current_image = draw_only_current_image

    def __init__(
        self,
        scene: scl.Scene,
        coordinate_system: str,
        params: TopView.Params | None = None,
    ):
        # scene contains the VCD and helper functions for transforms and projections
        if not isinstance(scene, scl.Scene):
            raise TypeError("Argument 'scene' must be of type 'vcd.scl.Scene'")
        self.scene = scene
        # This value specifies which coordinate system is fixed in the
        # center of the TopView, e.g. "odom" or "vehicle-iso8855"
        if not scene.vcd.has_coordinate_system(coordinate_system):
            raise ValueError(
                f"The provided scene does not have coordinate system {coordinate_system}"
            )
        self.coordinate_system = coordinate_system
        if params is not None:
            self.params = params
        else:
            self.params = TopView.Params()

        # Start topView base with a background color
        self.topView = np.zeros(
            (self.params.topview_size[1], self.params.topview_size[0], 3), np.uint8
        )  # Needs to be here
        self.topView.fill(self.params.background_color)
        self.images: dict = {}

    def add_images(self, imgs: dict, frame_num: int):
        """
        Add images to the TopView representation.

        By specifying the frame num and the camera name, several images can be loaded in one
        single call. Images should be provided as dictionary:

        {"CAM_FRONT": img_front, "CAM_REAR": img_rear}

        The function pre-computes all the necessary variables to create the TopView, such as
        the homography from image plane to world plane, or the camera region of interest, which
        is stored in scene.cameras dictionary.

        :param imgs: dictionary of images
        :param frame_num: frame number
        :return: nothing
        """
        # Base images
        # should be {"CAM_FRONT": img_front, "CAM_REAR": img_rear}
        if not isinstance(imgs, dict):
            raise TypeError("Argument 'imgs' must be of type 'dict'")

        # This option creates 1 remap for the entire topview, and not 1 per camera
        # The key idea is to weight the contribution of each camera depending on the
        # distance between point and cam
        # Instead of storing the result in self.images[cam_name] and then paint them
        # in drawBEV, we can store in self.images[frameNum] directly
        h = self.params.topview_size[1]
        w = self.params.topview_size[0]
        num_cams = len(imgs)
        cams: dict[str, scl.Camera | None] = {}
        need_to_recompute_weights_acc = False
        need_to_recompute_maps = {}
        need_to_recompute_weights = {}
        for cam_name, img in imgs.items():
            if not self.scene.vcd.has_coordinate_system(cam_name):
                raise ValueError(
                    f"The provided scene does not have coordinate system {cam_name}"
                )
            # this call creates an entry inside scene
            cam = self.scene.get_camera(cam_name, frame_num, compute_remaps=False)
            cams[cam_name] = cam
            self.images.setdefault(cam_name, {})
            self.images[cam_name]["img"] = img
            t_ref_to_cam_4x4, static = self.scene.get_transform(
                self.coordinate_system, cam_name, frame_num
            )

            # Compute distances to this camera and add to weight map
            need_to_recompute_maps[cam_name] = False
            need_to_recompute_weights[cam_name] = False

            if (num_cams > 1 and not static) or (
                num_cams > 1 and static and "weights" not in self.images[cam_name]
            ):
                need_to_recompute_weights[cam_name] = True
                need_to_recompute_weights_acc = True

            if (not static) or (static and "mapX" not in self.images[cam_name]):
                need_to_recompute_maps[cam_name] = True

            if need_to_recompute_maps[cam_name]:
                print(cam_name + " top view remap computation...")
                self.images[cam_name]["mapX"] = np.zeros((h, w), dtype=np.float32)
                self.images[cam_name]["mapY"] = np.zeros((h, w), dtype=np.float32)

            if need_to_recompute_weights[cam_name]:
                print(cam_name + " top view weights computation...")
                self.images[cam_name].setdefault(
                    "weights", np.zeros((h, w, 3), dtype=np.float32)
                )

        # Loop over top view domain
        for i in range(0, h):
            # Read all pixels pos of this row
            points2d_z0_3xN = np.array(
                [np.linspace(0, w - 1, num=w), i * np.ones(w), np.ones(w)]
            )
            # from pixels to points 3d
            temp = utils.inv(self.params.S).dot(points2d_z0_3xN)
            # hom. coords.
            points3d_z0_4xN = np.vstack((temp[0, :], temp[1, :], np.zeros(w), temp[2, :]))

            # Loop over cameras
            for _idx, (cam_name, cam) in enumerate(cams.items()):
                # Convert into camera coordinate system for all M cameras
                t_ref_to_cam_4x4, static = self.scene.get_transform(
                    self.coordinate_system, cam_name, frame_num
                )
                points3d_cam_4xN = t_ref_to_cam_4x4.dot(points3d_z0_4xN)

                if need_to_recompute_weights[cam_name]:
                    self.images[cam_name]["weights"][i, :, 0] = 1.0 / np.linalg.norm(
                        points3d_cam_4xN, axis=0
                    )
                    self.images[cam_name]["weights"][i, :, 1] = self.images[cam_name][
                        "weights"
                    ][i, :, 0]
                    self.images[cam_name]["weights"][i, :, 2] = self.images[cam_name][
                        "weights"
                    ][i, :, 0]

                if need_to_recompute_maps[cam_name]:
                    if cam is not None:
                        # Project into image
                        points2d_dist_3xN, idx_valid = cam.project_points3d(
                            points3d_cam_4xN, remove_outside=True
                        )

                        # Assign into map
                        self.images[cam_name]["mapX"][i, :] = points2d_dist_3xN[0, :]
                        self.images[cam_name]["mapY"][i, :] = points2d_dist_3xN[1, :]

        # Compute accumulated weights if more than 1 camera
        if need_to_recompute_weights_acc:
            self.images["weights_acc"] = np.zeros((h, w, 3), dtype=np.float32)
            for _idx, (cam_name, _cam) in enumerate(cams.items()):
                self.images["weights_acc"] = cv.add(
                    self.images[cam_name]["weights"], self.images["weights_acc"]
                )

    def draw(
        self,
        frame_num: int | None = None,
        uid: int | str | None = None,
        _draw_trajectory: bool = True,
    ) -> cv.Mat:
        """
        Define the main drawing function for the TopView drawer.

        It explores the provided params to select different options.

        :param frameNum: frame number
        :param uid: unique identifier of object to be drawn (if None, all are drawn)
        :param _drawTrajectory: boolean to draw the trajectory of objects
        :param _params: additional parameters
        :return: the TopView image
        """
        # Base top view is used from previous iteration
        if self.params.draw_only_current_image:
            # Needs to be here
            self.topView = np.zeros(
                (self.params.topview_size[1], self.params.topview_size[0], 3), np.uint8
            )
            self.topView.fill(self.params.background_color)

            # Draw BEW
        self.draw_bevs(frame_num)

        # Base grids
        self.draw_topview_base()

        # Draw objects
        topview_with_objects = copy.deepcopy(self.topView)
        self.draw_objects_at_frame(topview_with_objects, uid, frame_num, _draw_trajectory)

        # Draw frame info
        self.draw_info(topview_with_objects, frame_num)

        return topview_with_objects

    def draw_info(self, topview: cv.Mat, frame_num: int | None = None):
        h = topview.shape[0]
        w = topview.shape[1]
        w_margin = 250
        h_margin = 140
        h_step = 20
        font_size = 0.8
        cv.putText(
            topview,
            "Img. Size(px): " + str(w) + " x " + str(h),
            (w - w_margin, h - h_margin),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        if frame_num is None:
            frame_num = -1
        cv.putText(
            topview,
            "Frame: " + str(frame_num),
            (w - w_margin, h - h_margin + h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        cv.putText(
            topview,
            "CS: " + str(self.coordinate_system),
            (w - w_margin, h - h_margin + 2 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )

        cv.putText(
            topview,
            "RangeX (m): ("
            + str(self.params.range_x[0])
            + ", "
            + str(self.params.range_x[1])
            + ")",
            (w - w_margin, h - h_margin + 3 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        cv.putText(
            topview,
            "RangeY (m): ("
            + str(self.params.range_y[0])
            + ", "
            + str(self.params.range_y[1])
            + ")",
            (w - w_margin, h - h_margin + 4 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )

        cv.putText(
            topview,
            "OffsetX (px): ("
            + str(self.params.offset_x)
            + ", "
            + str(self.params.offset_x)
            + ")",
            (w - w_margin, h - h_margin + 5 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        cv.putText(
            topview,
            "OffsetY (px): ("
            + str(self.params.offset_y)
            + ", "
            + str(self.params.offset_y)
            + ")",
            (w - w_margin, h - h_margin + 6 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )

    def draw_topview_base(self):
        # self.topView.fill(self.params.backgroundColor)

        if self.params.draw_grid:
            # Grid x (1/2)
            for x in np.arange(
                self.params.range_x[0],
                self.params.range_x[1] + self.params.step_x,
                self.params.step_x,
            ):
                x_round = round(x)
                pt_img1 = self.point2pixel((x_round, self.params.range_y[0]))
                pt_img2 = self.point2pixel((x_round, self.params.range_y[1]))
                cv.line(
                    self.topView,
                    pt_img1,
                    pt_img2,
                    (127, 127, 127),
                    self.params.grid_lines_thickness,
                )

            # Grid y (1/2)
            for y in np.arange(
                self.params.range_y[0],
                self.params.range_y[1] + self.params.step_y,
                self.params.step_y,
            ):
                y_round = round(y)
                pt_img1 = self.point2pixel((self.params.range_x[0], y_round))
                pt_img2 = self.point2pixel((self.params.range_x[1], y_round))
                cv.line(
                    self.topView,
                    pt_img1,
                    pt_img2,
                    (127, 127, 127),
                    self.params.grid_lines_thickness,
                )

            # Grid x (2/2)
            for x in np.arange(
                self.params.range_x[0],
                self.params.range_x[1] + self.params.step_x,
                self.params.step_x,
            ):
                x_round = round(x)
                pt_img1 = self.point2pixel((x_round, self.params.range_y[0]))
                cv.putText(
                    self.topView,
                    str(round(x)) + " m",
                    (pt_img1[0] + 5, 15),
                    cv.FONT_HERSHEY_PLAIN,
                    0.6,
                    self.params.grid_text_color,
                    1,
                    cv.LINE_AA,
                )
            # Grid y (2/2)
            for y in np.arange(
                self.params.range_y[0],
                self.params.range_y[1] + self.params.step_y,
                self.params.step_y,
            ):
                y_round = round(y)
                pt_img1 = self.point2pixel((self.params.range_x[0], y_round))
                cv.putText(
                    self.topView,
                    str(round(y)) + " m",
                    (5, pt_img1[1] - 5),
                    cv.FONT_HERSHEY_PLAIN,
                    0.6,
                    self.params.grid_text_color,
                    1,
                    cv.LINE_AA,
                )

        # World origin
        cv.circle(self.topView, self.point2pixel((0.0, 0.0)), 4, (255, 255, 255), -1)
        cv.line(
            self.topView,
            self.point2pixel((0.0, 0.0)),
            self.point2pixel((5.0, 0.0)),
            (0, 0, 255),
            2,
        )
        cv.line(
            self.topView,
            self.point2pixel((0.0, 0.0)),
            self.point2pixel((0.0, 5.0)),
            (0, 255, 0),
            2,
        )

        cv.putText(
            self.topView,
            "X",
            self.point2pixel((5.0, -0.5)),
            cv.FONT_HERSHEY_PLAIN,
            1.0,
            (0, 0, 255),
            1,
            cv.LINE_AA,
        )
        cv.putText(
            self.topView,
            "Y",
            self.point2pixel((-1.0, 5.0)),
            cv.FONT_HERSHEY_PLAIN,
            1.0,
            (0, 255, 0),
            1,
            cv.LINE_AA,
        )

    def draw_points3d(self, _img: cv.Mat, points3d_4xN: npt.NDArray, _color: tuple):
        rows, cols = points3d_4xN.shape
        for i in range(0, cols):
            # thus ignoring z component
            pt = self.point2pixel((points3d_4xN[0, i], points3d_4xN[1, i]))
            cv.circle(_img, pt, 2, _color, -1)

    def draw_cuboid_topview(
        self,
        _img: cv.Mat,
        _cuboid: list,
        _class: str,
        _color: tuple[int, int, int],
        _thick: int,
        _id: int | str = "",
    ):
        if not isinstance(_cuboid, list):
            raise TypeError("Argument '_cuboid' must be of type 'list'")

        # (X, Y, Z, RX, RY, RZ, SX, SY, SZ)
        if len(_cuboid) != 9 or len(_cuboid) != 10:
            raise ValueError("Invalid argument '_cuboid' size")

        points_4x8 = utils.generate_cuboid_points_ref_4x8(_cuboid)
        # Project into topview
        points_4x8[2, :] = 0

        pairs = (
            [0, 1],
            [1, 2],
            [2, 3],
            [3, 0],
            [0, 4],
            [1, 5],
            [2, 6],
            [3, 7],
            [4, 5],
            [5, 6],
            [6, 7],
            [7, 4],
        )
        for pair in pairs:
            p_a = (points_4x8[0, pair[0]], points_4x8[1, pair[0]])
            p_b = (points_4x8[0, pair[1]], points_4x8[1, pair[1]])
            cv.line(_img, self.point2pixel(p_a), self.point2pixel(p_b), _color, _thick)

    def draw_mesh_topview(self, img: cv.Mat, mesh: dict, points3d_4xN: npt.NDArray):
        mesh_point_dict = mesh["point3d"]
        mesh_line_refs = mesh["line_reference"]
        mesh_area_refs = mesh["area_reference"]

        # Convert points into pixels
        points2d = []
        rows, cols = points3d_4xN.shape
        for i in range(0, cols):
            pt = self.point2pixel(
                (points3d_4xN[0, i], points3d_4xN[1, i])
            )  # thus ignoring z component
            points2d.append(pt)

        # Draw areas first
        for _area_id, area in mesh_area_refs.items():
            line_refs = area["val"]
            points_area = []
            # Loop over lines and create a list of points
            for line_ref in line_refs:
                line = mesh_line_refs[str(line_ref)]

                point_refs = line["val"]
                point_a_ref = point_refs[0]
                point_b_ref = point_refs[1]
                point_a = points2d[list(mesh_point_dict).index(str(point_a_ref))]
                point_b = points2d[list(mesh_point_dict).index(str(point_b_ref))]

                points_area.append(point_a)
                points_area.append(point_b)

            cv.fillConvexPoly(img, np.array(points_area), (0, 255, 0))

        # Draw lines
        for _line_id, line in mesh_line_refs.items():
            point_refs = line["val"]
            point_a_ref = point_refs[0]
            point_b_ref = point_refs[1]

            point_a = points2d[list(mesh_point_dict).index(str(point_a_ref))]
            point_b = points2d[list(mesh_point_dict).index(str(point_b_ref))]

            cv.line(img, point_a, point_b, (255, 0, 0), 2)

        # Draw points
        for pt in points2d:
            cv.circle(img, pt, 5, (0, 0, 0), -1)
            cv.circle(img, pt, 3, (0, 0, 255), -1)

    def draw_object_data(
        self,
        object_: dict,
        object_class: str,
        _img: cv.Mat,
        uid: int | str,
        _frame_num: int | None,
        _draw_trajectory: bool,
    ):  # pylint: disable=too-many-locals
        # Reads cuboids
        if "object_data" not in object_:
            return

        for object_data_key in object_["object_data"].keys():
            for object_data_item in object_["object_data"][object_data_key]:
                ########################################
                # CUBOIDS
                ########################################
                if object_data_key == "cuboid":
                    cuboid_vals = object_data_item["val"]
                    cuboid_name = object_data_item["name"]
                    if "coordinate_system" in object_data_item:
                        cs_data = object_data_item["coordinate_system"]
                    else:
                        warnings.warn(
                            "WARNING: The cuboids of this VCD don't have a "
                            "coordinate_system.",
                            Warning,
                            2,
                        )
                        # For simplicity, let's assume they are already expressed in
                        # the target cs
                        cs_data = self.coordinate_system

                    # Convert from data coordinate system (e.g. "CAM_LEFT")
                    #  into reference coordinate system (e.g. "VEHICLE-ISO8855")
                    cuboid_vals_transformed = cuboid_vals
                    if cs_data != self.coordinate_system:
                        cuboid_vals_transformed = self.scene.transform_cuboid(
                            cuboid_vals, cs_data, self.coordinate_system, _frame_num
                        )
                    # Draw
                    self.draw_cuboid_topview(
                        _img,
                        cuboid_vals_transformed,
                        object_class,
                        self.params.color_map[object_class],
                        2,
                        uid,
                    )

                    if _draw_trajectory and _frame_num is not None:
                        fis = self.__get_object_data_fis(uid, cuboid_name)

                        for fi in fis:
                            prev_center: dict = {}
                            for f in range(fi["frame_start"], _frame_num + 1):
                                object_data_item = self.scene.vcd.get_object_data(
                                    uid, cuboid_name, f
                                )

                                cuboid_vals = object_data_item["val"]
                                cuboid_vals_transformed = cuboid_vals
                                if cs_data != self.coordinate_system:
                                    src_cs = cs_data
                                    dst_cs = self.coordinate_system
                                    (
                                        transform_src_dst,
                                        _,
                                    ) = self.scene.get_transform(src_cs, dst_cs, f)
                                    cuboid_vals_transformed = utils.transform_cuboid(
                                        cuboid_vals, transform_src_dst
                                    )

                                name = object_data_item["name"]

                                center = (
                                    cuboid_vals_transformed[0],
                                    cuboid_vals_transformed[1],
                                )
                                center_pix = self.point2pixel(center)

                                # this is a dict to allow multiple trajectories
                                # (e.g. several cuboids per object)
                                if prev_center.get(name) is not None:
                                    cv.line(
                                        _img,
                                        prev_center[name],
                                        center_pix,
                                        (0, 0, 0),
                                        1,
                                        cv.LINE_AA,
                                    )

                                cv.circle(
                                    _img,
                                    center_pix,
                                    2,
                                    self.params.color_map[object_class],
                                    -1,
                                )

                                prev_center[name] = center_pix
                ########################################
                # mat - points3d_4xN
                ########################################
                elif object_data_key == "mat":
                    width = object_data_item["width"]
                    height = object_data_item["height"]

                    if height == 4:
                        # These are points 4xN
                        color = self.params.color_map[object_class]
                        points3d_4xN = np.array(object_data_item["val"]).reshape(
                            height, width
                        )
                        points_cs = object_data_item["coordinate_system"]

                        # First convert from the src coordinate system into the camera
                        # coordinate system
                        points3d_4xN_transformed = self.scene.transform_points3d_4xN(
                            points3d_4xN, points_cs, self.coordinate_system
                        )

                        if "attributes" in object_data_item:
                            for attr_type, attr_list in object_data_item[
                                "attributes"
                            ].items():
                                if attr_type == "vec":
                                    for attr in attr_list:
                                        if attr["name"] == "color":
                                            color = attr["val"]
                        if points3d_4xN_transformed is not None:
                            self.draw_points3d(_img, points3d_4xN_transformed, color)
                ########################################
                # point3d - Single point in 3D
                ########################################
                elif object_data_key == "point3d":
                    color = self.params.color_map[object_class]
                    point_name = object_data_item["name"]

                    if "coordinate_system" in object_data_item:
                        cs_data = object_data_item["coordinate_system"]
                    else:
                        warnings.warn(
                            "WARNING: The point3d of this VCD don't have a "
                            "coordinate_system.",
                            Warning,
                            2,
                        )
                        # For simplicity, let's assume they are already expressed in
                        # the target cs
                        cs_data = self.coordinate_system

                    x = object_data_item["val"][0]
                    y = object_data_item["val"][1]
                    z = object_data_item["val"][2]
                    points3d_4xN = np.array([x, y, z, 1]).reshape(4, 1)
                    points_cs = object_data_item["coordinate_system"]

                    # First convert from the src coordinate system into the camera
                    # coordinate system
                    points3d_4xN_transformed = self.scene.transform_points3d_4xN(
                        points3d_4xN, points_cs, self.coordinate_system
                    )

                    if "attributes" in object_data_item:
                        for attr_type, attr_list in object_data_item[
                            "attributes"
                        ].items():
                            if attr_type == "vec":
                                for attr in attr_list:
                                    if attr["name"] == "color":
                                        color = attr["val"]

                    if points3d_4xN_transformed is not None:
                        self.draw_points3d(_img, points3d_4xN_transformed, color)

                    if _draw_trajectory and _frame_num is not None:
                        fis = self.__get_object_data_fis(uid, point_name)

                        for fi in fis:
                            prev_center = {}
                            for f in range(fi["frame_start"], _frame_num + 1):
                                object_data_item = self.scene.vcd.get_object_data(
                                    uid, point_name, f
                                )

                                x = object_data_item["val"][0]
                                y = object_data_item["val"][1]
                                z = object_data_item["val"][2]
                                points3d_4xN = np.array([x, y, z, 1]).reshape(4, 1)
                                points3d_4xN_transformed = points3d_4xN

                                if cs_data != self.coordinate_system:
                                    src_cs = cs_data
                                    dst_cs = self.coordinate_system
                                    (
                                        transform_src_dst,
                                        _,
                                    ) = self.scene.get_transform(src_cs, dst_cs, f)
                                    points3d_4xN_transformed = (
                                        self.scene.transform_points3d_4xN(
                                            points3d_4xN,
                                            points_cs,
                                            self.coordinate_system,
                                        )
                                    )

                                name = object_data_item["name"]

                                if points3d_4xN_transformed is not None:
                                    center = (
                                        points3d_4xN_transformed[0, 0],
                                        points3d_4xN_transformed[1, 0],
                                    )
                                    center_pix = self.point2pixel(center)

                                    # this is a dict to allow multiple trajectories
                                    # (e.g. several cuboids per object)
                                    if prev_center.get(name) is not None:
                                        cv.line(
                                            _img,
                                            prev_center[name],
                                            center_pix,
                                            (0, 0, 0),
                                            1,
                                            cv.LINE_AA,
                                        )

                                    cv.circle(
                                        _img,
                                        center_pix,
                                        2,
                                        self.params.color_map[object_class],
                                        -1,
                                    )

                                    prev_center[name] = center_pix
                ########################################
                # mesh - Point-line-area structure
                ########################################
                elif object_data_key == "mesh":
                    if "coordinate_system" in object_data_item:
                        cs_data = object_data_item["coordinate_system"]
                    else:
                        warnings.warn(
                            "WARNING: The mesh of this VCD don't have a coordinate_system.",
                            Warning,
                            2,
                        )
                        # For simplicity, let's assume they are already expressed in
                        # the target cs
                        cs_data = self.coordinate_system

                    # Let's convert mesh points into 4xN array
                    points = object_data_item["point3d"]
                    points3d_4xN = np.ones((4, len(points)))
                    for point_count, (_point_id, point) in enumerate(points.items()):
                        points3d_4xN[0, point_count] = point["val"][0]
                        points3d_4xN[1, point_count] = point["val"][1]
                        points3d_4xN[2, point_count] = point["val"][2]

                    points3d_4xN_transformed = self.scene.transform_points3d_4xN(
                        points3d_4xN, cs_data, self.coordinate_system
                    )

                    if points3d_4xN_transformed is not None:
                        # Let's send the data and the possible transform info to the
                        # drawing function
                        self.draw_mesh_topview(
                            img=_img,
                            mesh=object_data_item,
                            points3d_4xN=points3d_4xN_transformed,
                        )

    def draw_objects_at_frame(
        self,
        top_view: cv.Mat,
        uid: int | str | None,
        _frame_num: int | None,
        _draw_trajectory: bool,
    ):
        img = top_view

        # Select static or dynamic objects depending on the provided input _frameNum
        objects = {}
        if _frame_num is not None:
            vcd_frame = self.scene.vcd.get_frame(_frame_num)
            if "objects" in vcd_frame:
                objects = vcd_frame["objects"]
        else:
            if self.scene.vcd.has_objects():
                objects = self.scene.vcd.get_objects()

        # Explore objects at this VCD frame
        for object_id, object_ in objects.items():
            if uid is not None:
                if core.UID(object_id).as_str() != core.UID(uid).as_str():
                    continue

            object_element = self.scene.vcd.get_object(object_id)
            if object_element is not None:
                # Get object static info
                object_class = object_element["type"]

            # Ignore classes
            if object_class in self.params.ignore_classes:
                continue

            # Colors
            if self.params.color_map.get(object_class) is None:
                # Let's create a new entry for this class
                self.params.color_map[object_class] = (
                    randbelow(255),
                    randbelow(255),
                    randbelow(255),
                )

            # Check if the object has specific info at this frame, or if we need to consult the
            # static object info
            if len(object_) == 0:
                # So this is a pointer to a static object
                static_object = self.scene.vcd.get_root()["objects"][object_id]
                self.draw_object_data(
                    static_object,
                    object_class,
                    img,
                    object_id,
                    _frame_num,
                    _draw_trajectory,
                )
            else:
                # Let's use the dynamic info of this object
                self.draw_object_data(
                    object_, object_class, img, object_id, _frame_num, _draw_trajectory
                )

    def draw_bev(self, cam_name: str) -> npt.NDArray[np.float32]:
        img = self.images[cam_name]["img"]

        map_x = self.images[cam_name]["mapX"]
        map_y = self.images[cam_name]["mapY"]
        bev = cv.remap(
            img,
            map_x,
            map_y,
            interpolation=cv.INTER_LINEAR,
            borderMode=cv.BORDER_CONSTANT,
        )

        bev32 = np.array(bev, np.float32)
        if "weights" in self.images[cam_name]:
            cv.multiply(self.images[cam_name]["weights"], bev32, bev32)

        # cv.imshow('bev' + cam_name, bev)
        # cv.waitKey(1)

        # bev832 = np.uint8(bev32)
        # cv.imshow('bev8' + cam_name, bev832)
        # cv.waitKey(1)

        return bev32

    def draw_bevs(self, _frame_num: int | None = None):
        """
        Draw BEVs into the topview.

        :param _frameNum:
        :return:
        """
        num_cams = len(self.images)
        if num_cams == 0:
            return

        h = self.params.topview_size[1]
        w = self.params.topview_size[0]
        # Prepare image with drawing for this call
        # black background
        acc32: npt.NDArray[np.float32] = np.zeros((h, w, 3), dtype=np.float32)

        for cam_name in self.images:
            if self.scene.get_camera(cam_name, _frame_num) is not None:
                temp32 = self.draw_bev(cam_name=cam_name)
                # mask = np.zeros((h, w), dtype=np.uint8)
                # mask[temp32 > 0] = 255
                # mask = (temp32 > 0)
                if num_cams > 1:
                    acc32 = cv.add(temp32, acc32)
        if num_cams > 1:
            acc32 /= self.images["weights_acc"]
        else:
            acc32 = temp32
        acc8 = acc32.astype(dtype=np.uint8)
        # cv.imshow('acc', acc8)
        # cv.waitKey(1)

        # Copy into topView only new pixels
        nonzero = acc8 > 0
        self.topView[nonzero] = acc8[nonzero]

    def size2pixel(self, _size: tuple[int, int]) -> tuple[int, int]:
        return (
            int(round(_size[0] * abs(self.params.scale_x))),
            int(round(_size[1] * abs(self.params.scale_y))),
        )

    def point2pixel(self, _point: tuple[int, int]) -> tuple[int, int]:
        pixel = (
            int(round(_point[0] * self.params.scale_x + self.params.offset_x)),
            int(round(_point[1] * self.params.scale_y + self.params.offset_y)),
        )
        return pixel

    def __get_object_data_fis(self, uid: int | str, name: str) -> list[dict]:
        fis_object = self.scene.vcd.get_object_data_frame_intervals(uid, name)
        if fis_object is None:
            fis: list[dict] = [{}]
        elif fis_object.empty():
            # So this object is static, let's project its cuboid into
            # the current transform
            fis = self.scene.vcd.get_frame_intervals().get_dict()
        else:
            fis = fis_object.get_dict()
        return fis

Class variables

var Params

Define the parameters needed to draw topview representations.

Assuming cuboids are drawn top view, so Z coordinate is ignored RZ is the rotation in Z-axis, it assumes/enforces SY>SX, thus keeping RZ between pi/2 and -pi/2.

Z, RX, RY, and SZ are ignored

For Vehicle cases, we adopt ISO8855: origin at rear axle at ground, x-to-front, y-to- left

Methods

def add_images(self, imgs: dict, frame_num: int)

Add images to the TopView representation.

By specifying the frame num and the camera name, several images can be loaded in one single call. Images should be provided as dictionary:

{"CAM_FRONT": img_front, "CAM_REAR": img_rear}

The function pre-computes all the necessary variables to create the TopView, such as the homography from image plane to world plane, or the camera region of interest, which is stored in scene.cameras dictionary.

:param imgs: dictionary of images :param frame_num: frame number :return: nothing

Expand source code
def add_images(self, imgs: dict, frame_num: int):
    """
    Add images to the TopView representation.

    By specifying the frame num and the camera name, several images can be loaded in one
    single call. Images should be provided as dictionary:

    {"CAM_FRONT": img_front, "CAM_REAR": img_rear}

    The function pre-computes all the necessary variables to create the TopView, such as
    the homography from image plane to world plane, or the camera region of interest, which
    is stored in scene.cameras dictionary.

    :param imgs: dictionary of images
    :param frame_num: frame number
    :return: nothing
    """
    # Base images
    # should be {"CAM_FRONT": img_front, "CAM_REAR": img_rear}
    if not isinstance(imgs, dict):
        raise TypeError("Argument 'imgs' must be of type 'dict'")

    # This option creates 1 remap for the entire topview, and not 1 per camera
    # The key idea is to weight the contribution of each camera depending on the
    # distance between point and cam
    # Instead of storing the result in self.images[cam_name] and then paint them
    # in drawBEV, we can store in self.images[frameNum] directly
    h = self.params.topview_size[1]
    w = self.params.topview_size[0]
    num_cams = len(imgs)
    cams: dict[str, scl.Camera | None] = {}
    need_to_recompute_weights_acc = False
    need_to_recompute_maps = {}
    need_to_recompute_weights = {}
    for cam_name, img in imgs.items():
        if not self.scene.vcd.has_coordinate_system(cam_name):
            raise ValueError(
                f"The provided scene does not have coordinate system {cam_name}"
            )
        # this call creates an entry inside scene
        cam = self.scene.get_camera(cam_name, frame_num, compute_remaps=False)
        cams[cam_name] = cam
        self.images.setdefault(cam_name, {})
        self.images[cam_name]["img"] = img
        t_ref_to_cam_4x4, static = self.scene.get_transform(
            self.coordinate_system, cam_name, frame_num
        )

        # Compute distances to this camera and add to weight map
        need_to_recompute_maps[cam_name] = False
        need_to_recompute_weights[cam_name] = False

        if (num_cams > 1 and not static) or (
            num_cams > 1 and static and "weights" not in self.images[cam_name]
        ):
            need_to_recompute_weights[cam_name] = True
            need_to_recompute_weights_acc = True

        if (not static) or (static and "mapX" not in self.images[cam_name]):
            need_to_recompute_maps[cam_name] = True

        if need_to_recompute_maps[cam_name]:
            print(cam_name + " top view remap computation...")
            self.images[cam_name]["mapX"] = np.zeros((h, w), dtype=np.float32)
            self.images[cam_name]["mapY"] = np.zeros((h, w), dtype=np.float32)

        if need_to_recompute_weights[cam_name]:
            print(cam_name + " top view weights computation...")
            self.images[cam_name].setdefault(
                "weights", np.zeros((h, w, 3), dtype=np.float32)
            )

    # Loop over top view domain
    for i in range(0, h):
        # Read all pixels pos of this row
        points2d_z0_3xN = np.array(
            [np.linspace(0, w - 1, num=w), i * np.ones(w), np.ones(w)]
        )
        # from pixels to points 3d
        temp = utils.inv(self.params.S).dot(points2d_z0_3xN)
        # hom. coords.
        points3d_z0_4xN = np.vstack((temp[0, :], temp[1, :], np.zeros(w), temp[2, :]))

        # Loop over cameras
        for _idx, (cam_name, cam) in enumerate(cams.items()):
            # Convert into camera coordinate system for all M cameras
            t_ref_to_cam_4x4, static = self.scene.get_transform(
                self.coordinate_system, cam_name, frame_num
            )
            points3d_cam_4xN = t_ref_to_cam_4x4.dot(points3d_z0_4xN)

            if need_to_recompute_weights[cam_name]:
                self.images[cam_name]["weights"][i, :, 0] = 1.0 / np.linalg.norm(
                    points3d_cam_4xN, axis=0
                )
                self.images[cam_name]["weights"][i, :, 1] = self.images[cam_name][
                    "weights"
                ][i, :, 0]
                self.images[cam_name]["weights"][i, :, 2] = self.images[cam_name][
                    "weights"
                ][i, :, 0]

            if need_to_recompute_maps[cam_name]:
                if cam is not None:
                    # Project into image
                    points2d_dist_3xN, idx_valid = cam.project_points3d(
                        points3d_cam_4xN, remove_outside=True
                    )

                    # Assign into map
                    self.images[cam_name]["mapX"][i, :] = points2d_dist_3xN[0, :]
                    self.images[cam_name]["mapY"][i, :] = points2d_dist_3xN[1, :]

    # Compute accumulated weights if more than 1 camera
    if need_to_recompute_weights_acc:
        self.images["weights_acc"] = np.zeros((h, w, 3), dtype=np.float32)
        for _idx, (cam_name, _cam) in enumerate(cams.items()):
            self.images["weights_acc"] = cv.add(
                self.images[cam_name]["weights"], self.images["weights_acc"]
            )
def draw(self, frame_num: int | None = None, uid: int | str | None = None) ‑> cv2.Mat

Define the main drawing function for the TopView drawer.

It explores the provided params to select different options.

:param frameNum: frame number :param uid: unique identifier of object to be drawn (if None, all are drawn) :param _drawTrajectory: boolean to draw the trajectory of objects :param _params: additional parameters :return: the TopView image

Expand source code
def draw(
    self,
    frame_num: int | None = None,
    uid: int | str | None = None,
    _draw_trajectory: bool = True,
) -> cv.Mat:
    """
    Define the main drawing function for the TopView drawer.

    It explores the provided params to select different options.

    :param frameNum: frame number
    :param uid: unique identifier of object to be drawn (if None, all are drawn)
    :param _drawTrajectory: boolean to draw the trajectory of objects
    :param _params: additional parameters
    :return: the TopView image
    """
    # Base top view is used from previous iteration
    if self.params.draw_only_current_image:
        # Needs to be here
        self.topView = np.zeros(
            (self.params.topview_size[1], self.params.topview_size[0], 3), np.uint8
        )
        self.topView.fill(self.params.background_color)

        # Draw BEW
    self.draw_bevs(frame_num)

    # Base grids
    self.draw_topview_base()

    # Draw objects
    topview_with_objects = copy.deepcopy(self.topView)
    self.draw_objects_at_frame(topview_with_objects, uid, frame_num, _draw_trajectory)

    # Draw frame info
    self.draw_info(topview_with_objects, frame_num)

    return topview_with_objects
def draw_bev(self, cam_name: str) ‑> numpy.ndarray[typing.Any, numpy.dtype[numpy.float32]]
Expand source code
def draw_bev(self, cam_name: str) -> npt.NDArray[np.float32]:
    img = self.images[cam_name]["img"]

    map_x = self.images[cam_name]["mapX"]
    map_y = self.images[cam_name]["mapY"]
    bev = cv.remap(
        img,
        map_x,
        map_y,
        interpolation=cv.INTER_LINEAR,
        borderMode=cv.BORDER_CONSTANT,
    )

    bev32 = np.array(bev, np.float32)
    if "weights" in self.images[cam_name]:
        cv.multiply(self.images[cam_name]["weights"], bev32, bev32)

    # cv.imshow('bev' + cam_name, bev)
    # cv.waitKey(1)

    # bev832 = np.uint8(bev32)
    # cv.imshow('bev8' + cam_name, bev832)
    # cv.waitKey(1)

    return bev32
def draw_bevs(self)

Draw BEVs into the topview.

:param _frameNum: :return:

Expand source code
def draw_bevs(self, _frame_num: int | None = None):
    """
    Draw BEVs into the topview.

    :param _frameNum:
    :return:
    """
    num_cams = len(self.images)
    if num_cams == 0:
        return

    h = self.params.topview_size[1]
    w = self.params.topview_size[0]
    # Prepare image with drawing for this call
    # black background
    acc32: npt.NDArray[np.float32] = np.zeros((h, w, 3), dtype=np.float32)

    for cam_name in self.images:
        if self.scene.get_camera(cam_name, _frame_num) is not None:
            temp32 = self.draw_bev(cam_name=cam_name)
            # mask = np.zeros((h, w), dtype=np.uint8)
            # mask[temp32 > 0] = 255
            # mask = (temp32 > 0)
            if num_cams > 1:
                acc32 = cv.add(temp32, acc32)
    if num_cams > 1:
        acc32 /= self.images["weights_acc"]
    else:
        acc32 = temp32
    acc8 = acc32.astype(dtype=np.uint8)
    # cv.imshow('acc', acc8)
    # cv.waitKey(1)

    # Copy into topView only new pixels
    nonzero = acc8 > 0
    self.topView[nonzero] = acc8[nonzero]
def draw_cuboid_topview(self, _img: cv.Mat, _cuboid: list, _class: str, _color: tuple[int, int, int], _thick: int)
Expand source code
def draw_cuboid_topview(
    self,
    _img: cv.Mat,
    _cuboid: list,
    _class: str,
    _color: tuple[int, int, int],
    _thick: int,
    _id: int | str = "",
):
    if not isinstance(_cuboid, list):
        raise TypeError("Argument '_cuboid' must be of type 'list'")

    # (X, Y, Z, RX, RY, RZ, SX, SY, SZ)
    if len(_cuboid) != 9 or len(_cuboid) != 10:
        raise ValueError("Invalid argument '_cuboid' size")

    points_4x8 = utils.generate_cuboid_points_ref_4x8(_cuboid)
    # Project into topview
    points_4x8[2, :] = 0

    pairs = (
        [0, 1],
        [1, 2],
        [2, 3],
        [3, 0],
        [0, 4],
        [1, 5],
        [2, 6],
        [3, 7],
        [4, 5],
        [5, 6],
        [6, 7],
        [7, 4],
    )
    for pair in pairs:
        p_a = (points_4x8[0, pair[0]], points_4x8[1, pair[0]])
        p_b = (points_4x8[0, pair[1]], points_4x8[1, pair[1]])
        cv.line(_img, self.point2pixel(p_a), self.point2pixel(p_b), _color, _thick)
def draw_info(self, topview: cv.Mat, frame_num: int | None = None)
Expand source code
def draw_info(self, topview: cv.Mat, frame_num: int | None = None):
    h = topview.shape[0]
    w = topview.shape[1]
    w_margin = 250
    h_margin = 140
    h_step = 20
    font_size = 0.8
    cv.putText(
        topview,
        "Img. Size(px): " + str(w) + " x " + str(h),
        (w - w_margin, h - h_margin),
        cv.FONT_HERSHEY_PLAIN,
        font_size,
        (0, 0, 0),
        1,
        cv.LINE_AA,
    )
    if frame_num is None:
        frame_num = -1
    cv.putText(
        topview,
        "Frame: " + str(frame_num),
        (w - w_margin, h - h_margin + h_step),
        cv.FONT_HERSHEY_PLAIN,
        font_size,
        (0, 0, 0),
        1,
        cv.LINE_AA,
    )
    cv.putText(
        topview,
        "CS: " + str(self.coordinate_system),
        (w - w_margin, h - h_margin + 2 * h_step),
        cv.FONT_HERSHEY_PLAIN,
        font_size,
        (0, 0, 0),
        1,
        cv.LINE_AA,
    )

    cv.putText(
        topview,
        "RangeX (m): ("
        + str(self.params.range_x[0])
        + ", "
        + str(self.params.range_x[1])
        + ")",
        (w - w_margin, h - h_margin + 3 * h_step),
        cv.FONT_HERSHEY_PLAIN,
        font_size,
        (0, 0, 0),
        1,
        cv.LINE_AA,
    )
    cv.putText(
        topview,
        "RangeY (m): ("
        + str(self.params.range_y[0])
        + ", "
        + str(self.params.range_y[1])
        + ")",
        (w - w_margin, h - h_margin + 4 * h_step),
        cv.FONT_HERSHEY_PLAIN,
        font_size,
        (0, 0, 0),
        1,
        cv.LINE_AA,
    )

    cv.putText(
        topview,
        "OffsetX (px): ("
        + str(self.params.offset_x)
        + ", "
        + str(self.params.offset_x)
        + ")",
        (w - w_margin, h - h_margin + 5 * h_step),
        cv.FONT_HERSHEY_PLAIN,
        font_size,
        (0, 0, 0),
        1,
        cv.LINE_AA,
    )
    cv.putText(
        topview,
        "OffsetY (px): ("
        + str(self.params.offset_y)
        + ", "
        + str(self.params.offset_y)
        + ")",
        (w - w_margin, h - h_margin + 6 * h_step),
        cv.FONT_HERSHEY_PLAIN,
        font_size,
        (0, 0, 0),
        1,
        cv.LINE_AA,
    )
def draw_mesh_topview(self, img: cv.Mat, mesh: dict, points3d_4xN: npt.NDArray)
Expand source code
def draw_mesh_topview(self, img: cv.Mat, mesh: dict, points3d_4xN: npt.NDArray):
    mesh_point_dict = mesh["point3d"]
    mesh_line_refs = mesh["line_reference"]
    mesh_area_refs = mesh["area_reference"]

    # Convert points into pixels
    points2d = []
    rows, cols = points3d_4xN.shape
    for i in range(0, cols):
        pt = self.point2pixel(
            (points3d_4xN[0, i], points3d_4xN[1, i])
        )  # thus ignoring z component
        points2d.append(pt)

    # Draw areas first
    for _area_id, area in mesh_area_refs.items():
        line_refs = area["val"]
        points_area = []
        # Loop over lines and create a list of points
        for line_ref in line_refs:
            line = mesh_line_refs[str(line_ref)]

            point_refs = line["val"]
            point_a_ref = point_refs[0]
            point_b_ref = point_refs[1]
            point_a = points2d[list(mesh_point_dict).index(str(point_a_ref))]
            point_b = points2d[list(mesh_point_dict).index(str(point_b_ref))]

            points_area.append(point_a)
            points_area.append(point_b)

        cv.fillConvexPoly(img, np.array(points_area), (0, 255, 0))

    # Draw lines
    for _line_id, line in mesh_line_refs.items():
        point_refs = line["val"]
        point_a_ref = point_refs[0]
        point_b_ref = point_refs[1]

        point_a = points2d[list(mesh_point_dict).index(str(point_a_ref))]
        point_b = points2d[list(mesh_point_dict).index(str(point_b_ref))]

        cv.line(img, point_a, point_b, (255, 0, 0), 2)

    # Draw points
    for pt in points2d:
        cv.circle(img, pt, 5, (0, 0, 0), -1)
        cv.circle(img, pt, 3, (0, 0, 255), -1)
def draw_object_data(self, object_: dict, object_class: str, _img: cv.Mat, uid: int | str, _frame_num: int | None, _draw_trajectory: bool)
Expand source code
def draw_object_data(
    self,
    object_: dict,
    object_class: str,
    _img: cv.Mat,
    uid: int | str,
    _frame_num: int | None,
    _draw_trajectory: bool,
):  # pylint: disable=too-many-locals
    # Reads cuboids
    if "object_data" not in object_:
        return

    for object_data_key in object_["object_data"].keys():
        for object_data_item in object_["object_data"][object_data_key]:
            ########################################
            # CUBOIDS
            ########################################
            if object_data_key == "cuboid":
                cuboid_vals = object_data_item["val"]
                cuboid_name = object_data_item["name"]
                if "coordinate_system" in object_data_item:
                    cs_data = object_data_item["coordinate_system"]
                else:
                    warnings.warn(
                        "WARNING: The cuboids of this VCD don't have a "
                        "coordinate_system.",
                        Warning,
                        2,
                    )
                    # For simplicity, let's assume they are already expressed in
                    # the target cs
                    cs_data = self.coordinate_system

                # Convert from data coordinate system (e.g. "CAM_LEFT")
                #  into reference coordinate system (e.g. "VEHICLE-ISO8855")
                cuboid_vals_transformed = cuboid_vals
                if cs_data != self.coordinate_system:
                    cuboid_vals_transformed = self.scene.transform_cuboid(
                        cuboid_vals, cs_data, self.coordinate_system, _frame_num
                    )
                # Draw
                self.draw_cuboid_topview(
                    _img,
                    cuboid_vals_transformed,
                    object_class,
                    self.params.color_map[object_class],
                    2,
                    uid,
                )

                if _draw_trajectory and _frame_num is not None:
                    fis = self.__get_object_data_fis(uid, cuboid_name)

                    for fi in fis:
                        prev_center: dict = {}
                        for f in range(fi["frame_start"], _frame_num + 1):
                            object_data_item = self.scene.vcd.get_object_data(
                                uid, cuboid_name, f
                            )

                            cuboid_vals = object_data_item["val"]
                            cuboid_vals_transformed = cuboid_vals
                            if cs_data != self.coordinate_system:
                                src_cs = cs_data
                                dst_cs = self.coordinate_system
                                (
                                    transform_src_dst,
                                    _,
                                ) = self.scene.get_transform(src_cs, dst_cs, f)
                                cuboid_vals_transformed = utils.transform_cuboid(
                                    cuboid_vals, transform_src_dst
                                )

                            name = object_data_item["name"]

                            center = (
                                cuboid_vals_transformed[0],
                                cuboid_vals_transformed[1],
                            )
                            center_pix = self.point2pixel(center)

                            # this is a dict to allow multiple trajectories
                            # (e.g. several cuboids per object)
                            if prev_center.get(name) is not None:
                                cv.line(
                                    _img,
                                    prev_center[name],
                                    center_pix,
                                    (0, 0, 0),
                                    1,
                                    cv.LINE_AA,
                                )

                            cv.circle(
                                _img,
                                center_pix,
                                2,
                                self.params.color_map[object_class],
                                -1,
                            )

                            prev_center[name] = center_pix
            ########################################
            # mat - points3d_4xN
            ########################################
            elif object_data_key == "mat":
                width = object_data_item["width"]
                height = object_data_item["height"]

                if height == 4:
                    # These are points 4xN
                    color = self.params.color_map[object_class]
                    points3d_4xN = np.array(object_data_item["val"]).reshape(
                        height, width
                    )
                    points_cs = object_data_item["coordinate_system"]

                    # First convert from the src coordinate system into the camera
                    # coordinate system
                    points3d_4xN_transformed = self.scene.transform_points3d_4xN(
                        points3d_4xN, points_cs, self.coordinate_system
                    )

                    if "attributes" in object_data_item:
                        for attr_type, attr_list in object_data_item[
                            "attributes"
                        ].items():
                            if attr_type == "vec":
                                for attr in attr_list:
                                    if attr["name"] == "color":
                                        color = attr["val"]
                    if points3d_4xN_transformed is not None:
                        self.draw_points3d(_img, points3d_4xN_transformed, color)
            ########################################
            # point3d - Single point in 3D
            ########################################
            elif object_data_key == "point3d":
                color = self.params.color_map[object_class]
                point_name = object_data_item["name"]

                if "coordinate_system" in object_data_item:
                    cs_data = object_data_item["coordinate_system"]
                else:
                    warnings.warn(
                        "WARNING: The point3d of this VCD don't have a "
                        "coordinate_system.",
                        Warning,
                        2,
                    )
                    # For simplicity, let's assume they are already expressed in
                    # the target cs
                    cs_data = self.coordinate_system

                x = object_data_item["val"][0]
                y = object_data_item["val"][1]
                z = object_data_item["val"][2]
                points3d_4xN = np.array([x, y, z, 1]).reshape(4, 1)
                points_cs = object_data_item["coordinate_system"]

                # First convert from the src coordinate system into the camera
                # coordinate system
                points3d_4xN_transformed = self.scene.transform_points3d_4xN(
                    points3d_4xN, points_cs, self.coordinate_system
                )

                if "attributes" in object_data_item:
                    for attr_type, attr_list in object_data_item[
                        "attributes"
                    ].items():
                        if attr_type == "vec":
                            for attr in attr_list:
                                if attr["name"] == "color":
                                    color = attr["val"]

                if points3d_4xN_transformed is not None:
                    self.draw_points3d(_img, points3d_4xN_transformed, color)

                if _draw_trajectory and _frame_num is not None:
                    fis = self.__get_object_data_fis(uid, point_name)

                    for fi in fis:
                        prev_center = {}
                        for f in range(fi["frame_start"], _frame_num + 1):
                            object_data_item = self.scene.vcd.get_object_data(
                                uid, point_name, f
                            )

                            x = object_data_item["val"][0]
                            y = object_data_item["val"][1]
                            z = object_data_item["val"][2]
                            points3d_4xN = np.array([x, y, z, 1]).reshape(4, 1)
                            points3d_4xN_transformed = points3d_4xN

                            if cs_data != self.coordinate_system:
                                src_cs = cs_data
                                dst_cs = self.coordinate_system
                                (
                                    transform_src_dst,
                                    _,
                                ) = self.scene.get_transform(src_cs, dst_cs, f)
                                points3d_4xN_transformed = (
                                    self.scene.transform_points3d_4xN(
                                        points3d_4xN,
                                        points_cs,
                                        self.coordinate_system,
                                    )
                                )

                            name = object_data_item["name"]

                            if points3d_4xN_transformed is not None:
                                center = (
                                    points3d_4xN_transformed[0, 0],
                                    points3d_4xN_transformed[1, 0],
                                )
                                center_pix = self.point2pixel(center)

                                # this is a dict to allow multiple trajectories
                                # (e.g. several cuboids per object)
                                if prev_center.get(name) is not None:
                                    cv.line(
                                        _img,
                                        prev_center[name],
                                        center_pix,
                                        (0, 0, 0),
                                        1,
                                        cv.LINE_AA,
                                    )

                                cv.circle(
                                    _img,
                                    center_pix,
                                    2,
                                    self.params.color_map[object_class],
                                    -1,
                                )

                                prev_center[name] = center_pix
            ########################################
            # mesh - Point-line-area structure
            ########################################
            elif object_data_key == "mesh":
                if "coordinate_system" in object_data_item:
                    cs_data = object_data_item["coordinate_system"]
                else:
                    warnings.warn(
                        "WARNING: The mesh of this VCD don't have a coordinate_system.",
                        Warning,
                        2,
                    )
                    # For simplicity, let's assume they are already expressed in
                    # the target cs
                    cs_data = self.coordinate_system

                # Let's convert mesh points into 4xN array
                points = object_data_item["point3d"]
                points3d_4xN = np.ones((4, len(points)))
                for point_count, (_point_id, point) in enumerate(points.items()):
                    points3d_4xN[0, point_count] = point["val"][0]
                    points3d_4xN[1, point_count] = point["val"][1]
                    points3d_4xN[2, point_count] = point["val"][2]

                points3d_4xN_transformed = self.scene.transform_points3d_4xN(
                    points3d_4xN, cs_data, self.coordinate_system
                )

                if points3d_4xN_transformed is not None:
                    # Let's send the data and the possible transform info to the
                    # drawing function
                    self.draw_mesh_topview(
                        img=_img,
                        mesh=object_data_item,
                        points3d_4xN=points3d_4xN_transformed,
                    )
def draw_objects_at_frame(self, top_view: cv.Mat, uid: int | str | None, _frame_num: int | None, _draw_trajectory: bool)
Expand source code
def draw_objects_at_frame(
    self,
    top_view: cv.Mat,
    uid: int | str | None,
    _frame_num: int | None,
    _draw_trajectory: bool,
):
    img = top_view

    # Select static or dynamic objects depending on the provided input _frameNum
    objects = {}
    if _frame_num is not None:
        vcd_frame = self.scene.vcd.get_frame(_frame_num)
        if "objects" in vcd_frame:
            objects = vcd_frame["objects"]
    else:
        if self.scene.vcd.has_objects():
            objects = self.scene.vcd.get_objects()

    # Explore objects at this VCD frame
    for object_id, object_ in objects.items():
        if uid is not None:
            if core.UID(object_id).as_str() != core.UID(uid).as_str():
                continue

        object_element = self.scene.vcd.get_object(object_id)
        if object_element is not None:
            # Get object static info
            object_class = object_element["type"]

        # Ignore classes
        if object_class in self.params.ignore_classes:
            continue

        # Colors
        if self.params.color_map.get(object_class) is None:
            # Let's create a new entry for this class
            self.params.color_map[object_class] = (
                randbelow(255),
                randbelow(255),
                randbelow(255),
            )

        # Check if the object has specific info at this frame, or if we need to consult the
        # static object info
        if len(object_) == 0:
            # So this is a pointer to a static object
            static_object = self.scene.vcd.get_root()["objects"][object_id]
            self.draw_object_data(
                static_object,
                object_class,
                img,
                object_id,
                _frame_num,
                _draw_trajectory,
            )
        else:
            # Let's use the dynamic info of this object
            self.draw_object_data(
                object_, object_class, img, object_id, _frame_num, _draw_trajectory
            )
def draw_points3d(self, _img: cv.Mat, points3d_4xN: npt.NDArray, _color: tuple)
Expand source code
def draw_points3d(self, _img: cv.Mat, points3d_4xN: npt.NDArray, _color: tuple):
    rows, cols = points3d_4xN.shape
    for i in range(0, cols):
        # thus ignoring z component
        pt = self.point2pixel((points3d_4xN[0, i], points3d_4xN[1, i]))
        cv.circle(_img, pt, 2, _color, -1)
def draw_topview_base(self)
Expand source code
def draw_topview_base(self):
    # self.topView.fill(self.params.backgroundColor)

    if self.params.draw_grid:
        # Grid x (1/2)
        for x in np.arange(
            self.params.range_x[0],
            self.params.range_x[1] + self.params.step_x,
            self.params.step_x,
        ):
            x_round = round(x)
            pt_img1 = self.point2pixel((x_round, self.params.range_y[0]))
            pt_img2 = self.point2pixel((x_round, self.params.range_y[1]))
            cv.line(
                self.topView,
                pt_img1,
                pt_img2,
                (127, 127, 127),
                self.params.grid_lines_thickness,
            )

        # Grid y (1/2)
        for y in np.arange(
            self.params.range_y[0],
            self.params.range_y[1] + self.params.step_y,
            self.params.step_y,
        ):
            y_round = round(y)
            pt_img1 = self.point2pixel((self.params.range_x[0], y_round))
            pt_img2 = self.point2pixel((self.params.range_x[1], y_round))
            cv.line(
                self.topView,
                pt_img1,
                pt_img2,
                (127, 127, 127),
                self.params.grid_lines_thickness,
            )

        # Grid x (2/2)
        for x in np.arange(
            self.params.range_x[0],
            self.params.range_x[1] + self.params.step_x,
            self.params.step_x,
        ):
            x_round = round(x)
            pt_img1 = self.point2pixel((x_round, self.params.range_y[0]))
            cv.putText(
                self.topView,
                str(round(x)) + " m",
                (pt_img1[0] + 5, 15),
                cv.FONT_HERSHEY_PLAIN,
                0.6,
                self.params.grid_text_color,
                1,
                cv.LINE_AA,
            )
        # Grid y (2/2)
        for y in np.arange(
            self.params.range_y[0],
            self.params.range_y[1] + self.params.step_y,
            self.params.step_y,
        ):
            y_round = round(y)
            pt_img1 = self.point2pixel((self.params.range_x[0], y_round))
            cv.putText(
                self.topView,
                str(round(y)) + " m",
                (5, pt_img1[1] - 5),
                cv.FONT_HERSHEY_PLAIN,
                0.6,
                self.params.grid_text_color,
                1,
                cv.LINE_AA,
            )

    # World origin
    cv.circle(self.topView, self.point2pixel((0.0, 0.0)), 4, (255, 255, 255), -1)
    cv.line(
        self.topView,
        self.point2pixel((0.0, 0.0)),
        self.point2pixel((5.0, 0.0)),
        (0, 0, 255),
        2,
    )
    cv.line(
        self.topView,
        self.point2pixel((0.0, 0.0)),
        self.point2pixel((0.0, 5.0)),
        (0, 255, 0),
        2,
    )

    cv.putText(
        self.topView,
        "X",
        self.point2pixel((5.0, -0.5)),
        cv.FONT_HERSHEY_PLAIN,
        1.0,
        (0, 0, 255),
        1,
        cv.LINE_AA,
    )
    cv.putText(
        self.topView,
        "Y",
        self.point2pixel((-1.0, 5.0)),
        cv.FONT_HERSHEY_PLAIN,
        1.0,
        (0, 255, 0),
        1,
        cv.LINE_AA,
    )
def point2pixel(self, _point: tuple[int, int]) ‑> tuple[int, int]
Expand source code
def point2pixel(self, _point: tuple[int, int]) -> tuple[int, int]:
    pixel = (
        int(round(_point[0] * self.params.scale_x + self.params.offset_x)),
        int(round(_point[1] * self.params.scale_y + self.params.offset_y)),
    )
    return pixel
def size2pixel(self, _size: tuple[int, int]) ‑> tuple[int, int]
Expand source code
def size2pixel(self, _size: tuple[int, int]) -> tuple[int, int]:
    return (
        int(round(_size[0] * abs(self.params.scale_x))),
        int(round(_size[1] * abs(self.params.scale_y))),
    )
class TopViewOrtho (scene: scl.Scene, camera_coordinate_system: str | None = None, step_x: float = 1.0, step_y: float = 1.0)

Draw 2D elements in the Image.

Devised to draw bboxes, it can also project 3D entities (e.g. cuboids) using the calibration parameters

Expand source code
class TopViewOrtho(Image):
    def __init__(
        self,
        scene: scl.Scene,
        camera_coordinate_system: str | None = None,
        step_x: float = 1.0,
        step_y: float = 1.0,
    ):
        super().__init__(scene, camera_coordinate_system=camera_coordinate_system)

        # Initialize image
        self.images: dict = {}

        # Grid config
        self.stepX = step_x  # meters
        self.stepY = step_y
        self.gridTextColor = (0, 0, 0)

    ##################################
    # Public functions
    ##################################
    def draw(
        self,
        _img: cv.Mat = None,
        _frame_num: int | None = None,
        _params: Image.Params | None = None,
        **kwargs: list[str] | dict,
    ) -> cv.Mat:
        # Create image
        if _img is not None:
            img = _img
        else:
            img = super().reset_image()

        # Compute and draw warped images
        for key, value in kwargs.items():
            if key == "add_images" and isinstance(value, dict):
                self.__add_images(value, _frame_num)

        # Draw BEW
        self.__draw_bevs(img, _frame_num)

        # Draw base grid
        self.__draw_topview_base(img)

        # Draw coordinate systems
        for key, value in kwargs.items():
            if key == "cs_names_to_draw":
                for cs_name in value:
                    self.draw_cs(img, cs_name, 2, 2)

        # Draw objects
        super().draw(img, _frame_num, _params)

        # Draw frame info
        self.__draw_info(img, _frame_num)

        return img

    ##################################
    # Internal functions
    ##################################
    def __add_images(self, imgs: dict, frame_num: int | None):
        if self.camera is not None and imgs is not None:
            h = self.camera.height  # this is the orthographic camera
            w = self.camera.width
            if not isinstance(imgs, dict):
                raise TypeError("Argument 'imgs' must be of type 'dict'")
            num_cams = len(imgs)
            cams = {}

            need_to_recompute_weights_acc = False
            need_to_recompute_maps = {}
            need_to_recompute_weights = {}

            for cam_name, img in imgs.items():
                if not self.scene.vcd.has_coordinate_system(cam_name):
                    raise ValueError(
                        "The provided scene does not have the specified coordinate system"
                    )
                # this call creates an entry inside scene
                cam = self.scene.get_camera(cam_name, frame_num, compute_remaps=False)
                if cam is not None:
                    cams[cam_name] = cam
                self.images.setdefault(cam_name, {})
                self.images[cam_name]["img"] = img
                _, static = self.scene.get_transform(
                    self.camera_coordinate_system, cam_name, frame_num
                )

                # Compute distances to this camera and add to weight map
                need_to_recompute_maps[cam_name] = False
                need_to_recompute_weights[cam_name] = False

                if (num_cams > 1 and not static) or (
                    num_cams > 1 and static and "weights" not in self.images[cam_name]
                ):
                    need_to_recompute_weights[cam_name] = True
                    need_to_recompute_weights_acc = True

                if (not static) or (static and "mapX" not in self.images[cam_name]):
                    need_to_recompute_maps[cam_name] = True

                # For each camera, compute the remaps and weights
                if need_to_recompute_maps:
                    map_x, map_y = self.scene.create_img_projection_maps(
                        cam_src_name=cam_name,
                        cam_dst_name=self.camera.name,
                        frame_num=frame_num,
                    )
                    self.images[cam_name]["mapX"] = map_x
                    self.images[cam_name]["mapY"] = map_y

                if need_to_recompute_weights[cam_name]:
                    print(cam_name + " top view weights computation...")
                    # self.images[cam_name].setdefault('weights', np.zeros((h, w, 3),
                    #  dtype=np.float32))
                    # self.images[cam_name].setdefault('weights',
                    #  (1.0/num_cams)*np.ones((h, w, 3),
                    #  dtype=np.float32))

                    # Weight according to distance to center point in image
                    # Might be good for fisheye
                    r_max_2 = (w / 2) * (w / 2) + (h / 2) * (h / 2)
                    x = map_x[:, :, 0]
                    y = map_x[:, :, 1]
                    r_2 = (x - w / 2) ** 2 + (y - h / 2) ** 2  # this is a matrix
                    weights = 1 - r_2 / r_max_2
                    temp = np.zeros((h, w, 3), dtype=np.float32)
                    temp[:, :, 0] = weights
                    temp[:, :, 1] = weights
                    temp[:, :, 2] = weights
                    self.images[cam_name].setdefault("weights", temp)

            # Compute accumulated weights if more than 1 camera
            if need_to_recompute_weights_acc:
                self.images["weights_acc"] = np.ones((h, w, 3), dtype=np.float32)
                # for idx, (cam_name, cam) in enumerate(cams.items()):
                #    self.images['weights_acc'] = cv.add(self.images[cam_name]['weights'],
                #  self.images['weights_acc'])

    def __draw_topview_base(self, _img: cv.Mat):
        if self.camera is None:
            warnings.warn(
                "__draw_topview_base: Camera is not  set",
                Warning,
                2,
            )
            return
        if not isinstance(self.camera, scl.CameraOrthographic):
            warnings.warn(
                "__draw_topview_base: Camera is not orthographic",
                Warning,
                2,
            )
            return

        # Grid x (1/2)
        for x in np.arange(self.camera.xmin, self.camera.xmax + self.stepX, self.stepX):
            x_0 = round(x)
            y_0 = self.camera.ymin
            y_1 = self.camera.ymax
            # points3d_4x2 = np.array([[x_0, y_0, 0.0, 1.0], [x_0, y_1, 0.0, 1.0]])
            points3d_4x2 = np.array([[x_0, x_0], [y_0, y_1], [0.0, 0.0], [1.0, 1.0]])
            points2d_3x2, _ = self.camera.project_points3d(points3d_4xN=points3d_4x2)
            self.draw_line(
                _img,
                (int(points2d_3x2[0, 0]), int(points2d_3x2[1, 0])),
                (int(points2d_3x2[0, 1]), int(points2d_3x2[1, 1])),
                (127, 127, 127),
            )
        # Grid y (1/2)
        for y in np.arange(self.camera.ymin, self.camera.ymax + self.stepY, self.stepY):
            y_0 = round(y)
            x_0 = self.camera.xmin
            x_1 = self.camera.xmax
            # points3d_4x2 = np.array([[x_0, y_0, 0.0, 1.0], [x_1, y_0, 0.0, 1.0]])
            points3d_4x2 = np.array([[x_0, x_1], [y_0, y_0], [0.0, 0.0], [1.0, 1.0]])
            points2d_3x2, _ = self.camera.project_points3d(points3d_4xN=points3d_4x2)
            self.draw_line(
                _img,
                (int(points2d_3x2[0, 0]), int(points2d_3x2[1, 0])),
                (int(points2d_3x2[0, 1]), int(points2d_3x2[1, 1])),
                (127, 127, 127),
            )
        # Grid x (2/2)
        for x in np.arange(self.camera.xmin, self.camera.xmax + self.stepX, self.stepX):
            x_0 = round(x)
            y_0 = self.camera.ymin
            points3d_4x1 = np.array([[x_0], [y_0], [0.0], [1.0]])
            points2d_3x1, _ = self.camera.project_points3d(points3d_4xN=points3d_4x1)
            cv.putText(
                _img,
                str(round(x_0)) + " m",
                (int(points2d_3x1[0, 0]) + 5, 15),
                cv.FONT_HERSHEY_PLAIN,
                0.6,
                self.gridTextColor,
                1,
                cv.LINE_AA,
            )
        # Grid y (2/2)
        for y in np.arange(self.camera.ymin, self.camera.ymax + self.stepY, self.stepY):
            y_0 = round(y)
            x_0 = self.camera.xmin
            points3d_4x1 = np.array([[x_0], [y_0], [0.0], [1.0]])
            points2d_3x1, _ = self.camera.project_points3d(points3d_4xN=points3d_4x1)
            cv.putText(
                _img,
                str(round(y_0)) + " m",
                (5, int(points2d_3x1[1, 0]) - 5),
                cv.FONT_HERSHEY_PLAIN,
                0.6,
                self.gridTextColor,
                1,
                cv.LINE_AA,
            )

        # World origin
        # cv.circle(self.topView, self.point2Pixel((0.0, 0.0)), 4, (255, 255, 255), -1)
        # cv.line(self.topView, self.point2Pixel((0.0, 0.0)), self.point2Pixel((5.0, 0.0)),
        #  (0, 0, 255), 2)
        # cv.line(self.topView, self.point2Pixel((0.0, 0.0)), self.point2Pixel((0.0, 5.0)),
        #  (0, 255, 0), 2)
        # cv.putText(self.topView, "X", self.point2Pixel((5.0, -0.5)), cv.FONT_HERSHEY_PLAIN,
        #  1.0, (0, 0, 255), 1, cv.LINE_AA)
        # cv.putText(self.topView, "Y", self.point2Pixel((-1.0, 5.0)), cv.FONT_HERSHEY_PLAIN,
        #  1.0, (0, 255, 0), 1, cv.LINE_AA)

    def __draw_bev(self, cam_name: str) -> npt.NDArray[np.float32]:
        img = self.images[cam_name]["img"]

        map_x = self.images[cam_name]["mapX"]
        map_y = self.images[cam_name]["mapY"]
        bev = cv.remap(
            img,
            map_x,
            map_y,
            interpolation=cv.INTER_LINEAR,
            borderMode=cv.BORDER_CONSTANT,
        )

        bev32 = np.array(bev, np.float32)
        if "weights" in self.images[cam_name]:
            cv.multiply(self.images[cam_name]["weights"], bev32, bev32)

        # cv.imshow('bev' + cam_name, bev)
        # cv.waitKey(1)

        # bev832 = np.uint8(bev32)
        # cv.imshow('bev8' + cam_name, bev832)
        # cv.waitKey(1)

        return bev32

    def __draw_bevs(self, _img: cv.Mat, _frame_num: int | None = None):
        """
        Draw BEVs into the topview.

        :param _frameNum:
        :return:
        """
        if self.camera is None:
            return

        num_cams = len(self.images)
        if num_cams == 0:
            return

        h = self.camera.height
        w = self.camera.width
        # Prepare image with drawing for this call
        # black background
        acc32: npt.NDArray[np.float32] = np.zeros((h, w, 3), dtype=np.float32)

        for cam_name in self.images:
            if self.scene.get_camera(cam_name, _frame_num) is not None:
                temp32 = self.__draw_bev(cam_name=cam_name)
                # mask = np.zeros((h, w), dtype=np.uint8)
                # mask[temp32 > 0] = 255
                # mask = (temp32 > 0)
                if num_cams > 1:
                    acc32 = cv.add(temp32, acc32)
        if num_cams > 1:
            acc32 /= self.images["weights_acc"]
        else:
            acc32 = temp32
        acc8 = np.uint8(acc32)
        # cv.imshow('acc', acc8)
        # cv.waitKey(1)

        # Copy into topView only new pixels
        nonzero = acc8 > 0
        _img[nonzero] = acc8

    def __draw_info(self, topview: npt.NDArray, frame_num: int | None = None):
        if not isinstance(self.camera, scl.CameraOrthographic):
            warnings.warn("__draw_info: Camera is not orthographic", Warning, 2)
            return

        h = topview.shape[0]
        w = topview.shape[1]
        w_margin = 250
        h_margin = 140
        h_step = 20
        font_size = 0.8
        cv.putText(
            topview,
            "Img. Size(px): " + str(w) + " x " + str(h),
            (w - w_margin, h - h_margin),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        if frame_num is None:
            frame_num = -1
        cv.putText(
            topview,
            "Frame: " + str(frame_num),
            (w - w_margin, h - h_margin + h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        cv.putText(
            topview,
            "CS: " + str(self.camera_coordinate_system),
            (w - w_margin, h - h_margin + 2 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )

        cv.putText(
            topview,
            "RangeX (m): (" + str(self.camera.xmin) + ", " + str(self.camera.xmax) + ")",
            (w - w_margin, h - h_margin + 3 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )
        cv.putText(
            topview,
            "RangeY (m): (" + str(self.camera.ymin) + ", " + str(self.camera.ymax) + ")",
            (w - w_margin, h - h_margin + 4 * h_step),
            cv.FONT_HERSHEY_PLAIN,
            font_size,
            (0, 0, 0),
            1,
            cv.LINE_AA,
        )

        # cv.putText(topView, "OffsetX (px): (" + str(self.params.offsetX) + ", " +
        #           str(self.params.offsetX) + ")",
        #           (w - w_margin, h - h_margin + 5*h_step),
        #           cv.FONT_HERSHEY_PLAIN, font_size, (0, 0, 0), 1, cv.LINE_AA)
        # cv.putText(topView, "OffsetY (px): (" + str(self.params.offsetY) + ", " +
        #           str(self.params.offsetY) + ")",
        #           (w - w_margin, h - h_margin + 6*h_step),
        #           cv.FONT_HERSHEY_PLAIN, font_size, (0, 0, 0), 1, cv.LINE_AA)

Ancestors

Methods

def draw(self, **kwargs: list[str] | dict) ‑> cv2.Mat
Expand source code
def draw(
    self,
    _img: cv.Mat = None,
    _frame_num: int | None = None,
    _params: Image.Params | None = None,
    **kwargs: list[str] | dict,
) -> cv.Mat:
    # Create image
    if _img is not None:
        img = _img
    else:
        img = super().reset_image()

    # Compute and draw warped images
    for key, value in kwargs.items():
        if key == "add_images" and isinstance(value, dict):
            self.__add_images(value, _frame_num)

    # Draw BEW
    self.__draw_bevs(img, _frame_num)

    # Draw base grid
    self.__draw_topview_base(img)

    # Draw coordinate systems
    for key, value in kwargs.items():
        if key == "cs_names_to_draw":
            for cs_name in value:
                self.draw_cs(img, cs_name, 2, 2)

    # Draw objects
    super().draw(img, _frame_num, _params)

    # Draw frame info
    self.__draw_info(img, _frame_num)

    return img

Inherited members