Source code for vis4d.data.transforms.points

"""Pointwise transformations."""

from __future__ import annotations

from typing import TypedDict

import numpy as np

from vis4d.common.typing import NDArrayFloat
from vis4d.data.const import CommonKeys as K

from .base import Transform


[docs] @Transform(in_keys=K.points3d, out_keys="transforms.pc_bounds") class GenPcBounds: """Extracts the max and min values of the loaded points."""
[docs] def __call__( self, coordinates_list: list[NDArrayFloat] ) -> list[NDArrayFloat]: """Extracts the max and min values of the pointcloud.""" coordinates = coordinates_list[0] pc_bounds = [np.stack([coordinates.min(0), coordinates.max(0)])] * len( coordinates_list ) return pc_bounds
[docs] @Transform(in_keys=(K.points3d, "trasforms.pc_bounds"), out_keys=K.points3d) class NormalizeByMaxBounds: """Normalizes the pointcloud by the max bounds.""" def __init__(self, axes: tuple[int, int, int] = (0, 1, 2)) -> None: """Creates an instance of the class. Args: axes (tuple[int, int, int]): Over which axes to apply normalization. """ self.axes = axes
[docs] def __call__( self, coords_list: list[NDArrayFloat], pc_bounds_list: list[NDArrayFloat], ) -> list[NDArrayFloat]: """Applies the normalization.""" for i, (coords, pc_bounds) in enumerate( zip(coords_list, pc_bounds_list) ): max_bound = np.max(np.abs(pc_bounds), axis=0) for ax in self.axes: coords[:, ax] = coords[:, ax] / max_bound[ax] coords_list[i] = coords return coords_list
[docs] @Transform(in_keys=K.points3d, out_keys=K.points3d) class CenterAndNormalize: """Centers and normalizes the pointcloud.""" def __init__(self, centering: bool = True, normalize: bool = True) -> None: """Creates an instance of the class. Args: centering (bool): Whether to center the pointcloud normalize (bool): Whether to normalize the pointcloud """ self.centering = centering self.normalize = normalize
[docs] def __call__(self, coords_list: list[NDArrayFloat]) -> list[NDArrayFloat]: """Applies the Center and Normalization operations.""" for i, coords in enumerate(coords_list): if self.centering: coords = coords - np.mean(coords, axis=0) if self.normalize: coords = coords / np.max(np.sqrt(np.sum(coords**2, axis=-1))) coords_list[i] = coords return coords_list
[docs] @Transform(in_keys=K.points3d, out_keys=K.points3d) class AddGaussianNoise: """Adds random normal distributed noise with given std to the data. Args: std (float): Standard Deviation of the noise """ def __init__(self, noise_level: float = 0.01): """Creates an instance of the class. Args: noise_level (float): The noise level. Standard deviation for the gaussian noise. """ self.noise_level = noise_level
[docs] def __call__( self, coordinates_list: list[NDArrayFloat] ) -> list[NDArrayFloat]: """Adds gaussian noise to the coordiantes.""" for i, coordinates in enumerate(coordinates_list): coordinates[i] = ( coordinates + np.random.randn(*coordinates.shape) * self.noise_level ) return coordinates_list
[docs] @Transform(in_keys=K.points3d, out_keys=K.points3d) class AddUniformNoise: """Adds random normal distributed noise with given std to the data. Args: std (float): Standard Deviation of the noise """ def __init__(self, noise_level: float = 0.01): """Creates an instance of the class. Args: noise_level (float): The noise level. Half the range of the uniform noise. The noise is sampled from [-noise_level, noise_level]. """ self.noise_level = noise_level
[docs] def __call__( self, coordinates_list: list[NDArrayFloat] ) -> list[NDArrayFloat]: """Adds uniform noise to the coordinates.""" for i, coordinates in enumerate(coordinates_list): coordinates_list[i] = coordinates + np.random.uniform( -self.noise_level, self.noise_level, coordinates.shape ) return coordinates_list
[docs] class SE3Transform(TypedDict): """Parameters for Resize.""" translation: NDArrayFloat rotation: NDArrayFloat
def _gen_random_se3_transform( translation_min: NDArrayFloat, translation_max: NDArrayFloat, rotation_min: NDArrayFloat, rotation_max: NDArrayFloat, ) -> SE3Transform: """Creates a random SE3 Transforms. The transform is generated by sampling a random translation and rotation from a uniform distribution. """ angle = np.random.uniform(rotation_min, rotation_max) translation = np.random.uniform(translation_min, translation_max) cos_x, sin_x = np.cos(angle[0]), np.sin(angle[0]) cos_y, sin_y = np.cos(angle[1]), np.sin(angle[1]) cos_z, sin_z = np.cos(angle[2]), np.sin(angle[2]) rotx = np.array([[1, 0, 0], [0, cos_x, -sin_x], [0, sin_x, cos_x]]) roty = np.array([[cos_y, 0, sin_y], [0, 1, 0], [-sin_y, 0, cos_y]]) rotz = np.array([[cos_z, -sin_z, 0], [sin_z, cos_z, 0], [0, 0, 1]]) rot = np.dot(rotz, np.dot(roty, rotx)) return SE3Transform(translation=translation, rotation=rot)
[docs] @Transform(in_keys=K.points3d, out_keys=K.points3d) class ApplySE3Transform: """Applies a given SE3 Transform to the data.""" def __init__( self, translation_min: tuple[float, float, float] = (0.0, 0.0, 0.0), translation_max: tuple[float, float, float] = (0.0, 0.0, 0.0), rotation_min: tuple[float, float, float] = (0.0, 0.0, 0.0), rotation_max: tuple[float, float, float] = (0.0, 0.0, 0.0), ) -> None: """Creates an instance of the class. Args: translation_min (tuple[float, float, float]): Minimum translation. translation_max (tuple[float, float, float]): Maximum translation. rotation_min (tuple[float, float, float]): Minimum euler rotation angles [rad]. Applied in the order rot_x -> rot_y -> rot_z. rotation_max (tuple[float, float, float]): Maximum euler rotation angles [rad]. Applied in the order rot_x -> rot_y -> rot_z. """ self.translation_min = np.asarray(translation_min) self.translation_max = np.asarray(translation_max) self.rotation_min = np.asarray(rotation_min) self.rotation_max = np.asarray(rotation_max)
[docs] def __call__( self, coordinates_list: list[NDArrayFloat] ) -> list[NDArrayFloat]: """Applies a SE3 Transform.""" for i, coordinates in enumerate(coordinates_list): transform = _gen_random_se3_transform( self.translation_min, self.translation_max, self.rotation_min, self.rotation_max, ) if coordinates.shape[-1] == 3: coordinates_list[i] = ( transform["rotation"] @ coordinates.T ).T + transform["translation"] elif coordinates.shape[-2] == 3: coordinates_list[i] = ( transform["rotation"] @ coordinates ).T + transform["translation"] else: raise ValueError( f"Invalid shape for coordinates: {coordinates.shape}" ) return coordinates_list
[docs] class ApplySO3Transform(ApplySE3Transform): """Applies a given SO3 Transform to the data."""
[docs] def __call__( self, coordinates_list: list[NDArrayFloat] ) -> list[NDArrayFloat]: """Applies a given SO3 Transform to the data.""" for i, coordinates in enumerate(coordinates_list): transform = _gen_random_se3_transform( self.translation_min, self.translation_max, self.rotation_min, self.rotation_max, )["rotation"] if coordinates.shape[-1] == 3: coordinates_list[i] = (transform @ coordinates.T).T elif coordinates.shape[-2] == 3: coordinates_list[i] = (transform @ coordinates).T else: raise ValueError( f"Invalid shape for coordinates: {coordinates.shape}" ) return coordinates_list
[docs] @Transform(in_keys=K.points3d, out_keys=K.points3d) class TransposeChannels: """Transposes some predifined channels.""" def __init__(self, channels: tuple[int, int] = (-1, -2)): """Creates an instance of the class. Args: channels (tuple[int, int]): Tuple of channels to transpose """ self.channels = channels
[docs] def __call__( self, coordinates_list: list[NDArrayFloat] ) -> list[NDArrayFloat]: """Transposes some predifined channels.""" for i, coordinates in enumerate(coordinates_list): coordinates_list[i] = coordinates.transpose(*self.channels) return coordinates_list