diff --git a/config.ini b/config.ini index f12c872..125f9e1 100644 --- a/config.ini +++ b/config.ini @@ -60,4 +60,3 @@ far_plane = 300 keep_perspective = False ; show button to visualize related images in a separate window show_2d_image = False - diff --git a/labelCloud/__init__.py b/labelCloud/__init__.py index ed9d4d8..ab55bb1 100644 --- a/labelCloud/__init__.py +++ b/labelCloud/__init__.py @@ -1 +1 @@ -__version__ = "0.7.4" +__version__ = "0.7.5" diff --git a/labelCloud/control/label_manager.py b/labelCloud/control/label_manager.py index be10eea..d6f33e2 100644 --- a/labelCloud/control/label_manager.py +++ b/labelCloud/control/label_manager.py @@ -2,8 +2,8 @@ from pathlib import Path from typing import List -from ..label_formats import BaseLabelFormat, CentroidFormat, KittiFormat, VerticesFormat -from ..model.bbox import BBox +from ..io.labels import BaseLabelFormat, CentroidFormat, KittiFormat, VerticesFormat +from ..model import BBox from .config_manager import config diff --git a/labelCloud/control/pcd_manager.py b/labelCloud/control/pcd_manager.py index d1d24cc..a22d190 100644 --- a/labelCloud/control/pcd_manager.py +++ b/labelCloud/control/pcd_manager.py @@ -3,49 +3,30 @@ Sets the point cloud and original point cloud path. Initiate the writing to the virtual object buffer. """ import logging -from dataclasses import dataclass from pathlib import Path from shutil import copyfile from typing import TYPE_CHECKING, List, Optional, Tuple +import pkg_resources + import numpy as np import open3d as o3d -from ..model import BBox, PointCloud -from ..utils.logger import end_section, green, print_column, start_section +from ..io.pointclouds import BasePointCloudHandler, Open3DHandler +from ..model import BBox, Perspective, PointCloud +from ..utils.logger import blue, green, print_column from .config_manager import config from .label_manager import LabelManager if TYPE_CHECKING: from ..view.gui import GUI -import pkg_resources - - -@dataclass -class Perspective(object): - zoom: float - rotation: Tuple[float, float, float] - - -def color_pointcloud(points, z_min, z_max) -> np.ndarray: - palette = np.loadtxt( - pkg_resources.resource_filename("labelCloud.resources", "rocket-palette.txt") - ) - palette_len = len(palette) - 1 - - colors = np.zeros(points.shape) - for ind, height in enumerate(points[:, 2]): - colors[ind] = palette[round((height - z_min) / (z_max - z_min) * palette_len)] - return colors - class PointCloudManger(object): - PCD_EXTENSIONS = [".pcd", ".ply", ".pts", ".xyz", ".xyzn", ".xyzrgb", ".bin"] + PCD_EXTENSIONS = BasePointCloudHandler.get_supported_extensions() ORIGINALS_FOLDER = "original_pointclouds" TRANSLATION_FACTOR = config.getfloat("POINTCLOUD", "STD_TRANSLATION") ZOOM_FACTOR = config.getfloat("POINTCLOUD", "STD_ZOOM") - COLORIZE = config.getboolean("POINTCLOUD", "COLORLESS_COLORIZE") def __init__(self) -> None: # Point cloud management @@ -53,14 +34,13 @@ def __init__(self) -> None: self.pcds: List[Path] = [] self.current_id = -1 - self.current_o3d_pcd = None self.view: Optional[GUI] = None self.label_manager = LabelManager() # Point cloud control self.pointcloud = None self.collected_object_classes = set() - self.saved_perspective: Perspective = None + self.saved_perspective: Optional[Perspective] = None @property def pcd_path(self) -> Path: @@ -95,7 +75,7 @@ def read_pointcloud_folder(self) -> None: self.view.update_status( "Please set the point cloud folder to a location that contains point cloud files." ) - self.pointcloud = self.load_pointcloud( + self.pointcloud = PointCloud.from_file( Path( pkg_resources.resource_filename( "labelCloud.resources", "labelCloud_icon.pcd" @@ -115,7 +95,10 @@ def get_next_pcd(self) -> None: logging.info("Loading next point cloud...") if self.pcds_left(): self.current_id += 1 - self.pointcloud = self.load_pointcloud(self.pcd_path) + self.save_current_perspective() + self.pointcloud = PointCloud.from_file( + self.pcd_path, self.saved_perspective + ) self.update_pcd_infos() else: logging.warning("No point clouds left!") @@ -124,7 +107,10 @@ def get_prev_pcd(self) -> None: logging.info("Loading previous point cloud...") if self.current_id > 0: self.current_id -= 1 - self.pointcloud = self.load_pointcloud(self.pcd_path) + self.save_current_perspective() + self.pointcloud = PointCloud.from_file( + self.pcd_path, self.saved_perspective + ) self.update_pcd_infos() else: raise Exception("No point cloud left for loading!") @@ -150,86 +136,12 @@ def save_labels_into_file(self, bboxes: List[BBox]) -> None: else: logging.warning("No point clouds to save labels for!") - def save_current_perspective(self, active: bool = True) -> None: - if active and self.pointcloud: - self.saved_perspective = Perspective( - zoom=self.pointcloud.trans_z, - rotation=tuple(self.pointcloud.get_rotations()), - ) + def save_current_perspective(self) -> None: + if config.getboolean("USER_INTERFACE", "KEEP_PERSPECTIVE") and self.pointcloud: + self.saved_perspective = Perspective.from_point_cloud(self.pointcloud) logging.info(f"Saved current perspective ({self.saved_perspective}).") - else: - self.saved_perspective = None - logging.info("Reset saved perspective.") # MANIPULATOR - def load_pointcloud(self, path_to_pointcloud: Path) -> PointCloud: - start_section(f"Loading {path_to_pointcloud.name}") - - if config.getboolean("USER_INTERFACE", "keep_perspective"): - self.save_current_perspective() - - if path_to_pointcloud.suffix == ".bin": # Loading binary pcds with numpy - bin_pcd = np.fromfile(path_to_pointcloud, dtype=np.float32) - points = bin_pcd.reshape((-1, 4))[ - :, 0:3 - ] # Reshape and drop reflection values - points = points[~np.isnan(points).any(axis=1)] # drop rows with nan - self.current_o3d_pcd = o3d.geometry.PointCloud( - o3d.utility.Vector3dVector(points) - ) - else: # Load point cloud with open3d - self.current_o3d_pcd = o3d.io.read_point_cloud( - str(path_to_pointcloud), remove_nan_points=True - ) - - tmp_pcd = PointCloud(path_to_pointcloud) - tmp_pcd.points = np.asarray(self.current_o3d_pcd.points).astype( - "float32" - ) # Unpack point cloud - tmp_pcd.colors = np.asarray(self.current_o3d_pcd.colors).astype("float32") - - tmp_pcd.colorless = len(tmp_pcd.colors) == 0 - - print_column(["Number of Points:", f"{len(tmp_pcd.points):n}"]) - # Calculate and set initial translation to view full pointcloud - tmp_pcd.center = self.current_o3d_pcd.get_center() - tmp_pcd.set_mins_maxs() - - if PointCloudManger.COLORIZE and tmp_pcd.colorless: - logging.info("Generating colors for colorless point cloud!") - min_height, max_height = tmp_pcd.get_min_max_height() - tmp_pcd.colors = color_pointcloud(tmp_pcd.points, min_height, max_height) - tmp_pcd.colorless = False - - max_dims = np.subtract(tmp_pcd.pcd_maxs, tmp_pcd.pcd_mins) - diagonal = min( - np.linalg.norm(max_dims), - config.getfloat("USER_INTERFACE", "far_plane") * 0.9, - ) - - tmp_pcd.init_translation = -self.current_o3d_pcd.get_center() - [0, 0, diagonal] - - if self.saved_perspective != None: - tmp_pcd.init_translation = tuple( - list(tmp_pcd.init_translation[:2]) + [self.saved_perspective.zoom] - ) - tmp_pcd.set_rotations(*self.saved_perspective.rotation) - - tmp_pcd.reset_translation() - tmp_pcd.print_details() - if self.pointcloud is not None: # Skip first pcd to intialize OpenGL first - tmp_pcd.write_vbo() - - logging.info( - green(f"Successfully loaded point cloud from {path_to_pointcloud}!") - ) - if tmp_pcd.colorless: - logging.warning( - "Did not find colors for the loaded point cloud, drawing in colorless mode!" - ) - end_section() - return tmp_pcd - def rotate_around_x(self, dangle) -> None: self.pointcloud.set_rot_x(self.pointcloud.rot_x - dangle) @@ -259,7 +171,7 @@ def zoom_into(self, distance) -> None: self.pointcloud.set_trans_z(self.pointcloud.trans_z + zoom_distance) def reset_translation(self) -> None: - self.pointcloud.reset_translation() + self.pointcloud.reset_perspective() def reset_rotation(self) -> None: self.pointcloud.rot_x, self.pointcloud.rot_y, self.pointcloud.rot_z = (0, 0, 0) @@ -278,31 +190,32 @@ def rotate_pointcloud( str(self.pcd_path), str(originals_path.joinpath(self.pcd_name)), ) + logging.info("Copyied the original point cloud to %s.", blue(originals_path)) # Rotate and translate point cloud rotation_matrix = o3d.geometry.get_rotation_matrix_from_axis_angle( np.multiply(axis, angle) ) - self.current_o3d_pcd.rotate(rotation_matrix, center=tuple(rotation_point)) - self.current_o3d_pcd.translate([0, 0, -rotation_point[2]]) + o3d_pointcloud = Open3DHandler().to_open3d_point_cloud(self.pointcloud) + o3d_pointcloud.rotate(rotation_matrix, center=tuple(rotation_point)) + o3d_pointcloud.translate([0, 0, -rotation_point[2]]) + logging.info("Rotating point cloud...") + print_column(["Angle:", np.round(angle, 3)]) + print_column(["Axis:", np.round(axis, 3)]) + print_column(["Point:", np.round(rotation_point, 3)], last=True) # Check if pointcloud is upside-down - pcd_mins = np.amin(self.current_o3d_pcd.points, axis=0) - pcd_maxs = np.amax(self.current_o3d_pcd.points, axis=0) - - if abs(pcd_mins[2]) > pcd_maxs[2]: + if abs(self.pointcloud.pcd_mins[2]) > self.pointcloud.pcd_maxs[2]: logging.warning("Point cloud is upside down, rotating ...") - self.current_o3d_pcd.rotate( + o3d_pointcloud.rotate( o3d.geometry.get_rotation_matrix_from_xyz([np.pi, 0, 0]), center=(0, 0, 0), ) - save_path = self.pcd_path - if save_path.suffix == ".bin": # save .bin point clouds as .pcd - save_path = save_path.parent.joinpath(save_path.stem + ".pcd") - - o3d.io.write_point_cloud(str(save_path), self.current_o3d_pcd) - self.pointcloud = self.load_pointcloud(save_path) + self.pointcloud = PointCloud( + self.pcd_path, *Open3DHandler().to_point_cloud(o3d_pointcloud) + ) + self.pointcloud.to_file() # HELPER diff --git a/labelCloud/io/__init__.py b/labelCloud/io/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/labelCloud/io/labels/__init__.py b/labelCloud/io/labels/__init__.py new file mode 100644 index 0000000..8d3407a --- /dev/null +++ b/labelCloud/io/labels/__init__.py @@ -0,0 +1,8 @@ +from .base import ( + BaseLabelFormat, + abs2rel_rotation, + rel2abs_rotation, +) +from .centroid import CentroidFormat +from .kitti import KittiFormat +from .vertices import VerticesFormat diff --git a/labelCloud/label_formats/base.py b/labelCloud/io/labels/base.py similarity index 99% rename from labelCloud/label_formats/base.py rename to labelCloud/io/labels/base.py index 30a1bd0..8f231f4 100644 --- a/labelCloud/label_formats/base.py +++ b/labelCloud/io/labels/base.py @@ -6,7 +6,7 @@ import numpy as np -from ..model import BBox +from ...model import BBox class BaseLabelFormat(ABC): diff --git a/labelCloud/label_formats/centroid_format.py b/labelCloud/io/labels/centroid.py similarity index 98% rename from labelCloud/label_formats/centroid_format.py rename to labelCloud/io/labels/centroid.py index c5621e7..1e32cb8 100644 --- a/labelCloud/label_formats/centroid_format.py +++ b/labelCloud/io/labels/centroid.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import List -from ..model import BBox +from ...model import BBox from . import BaseLabelFormat, abs2rel_rotation, rel2abs_rotation diff --git a/labelCloud/label_formats/kitti_format.py b/labelCloud/io/labels/kitti.py similarity index 99% rename from labelCloud/label_formats/kitti_format.py rename to labelCloud/io/labels/kitti.py index e9765dc..cd6f49e 100644 --- a/labelCloud/label_formats/kitti_format.py +++ b/labelCloud/io/labels/kitti.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import List -from ..model import BBox +from ...model import BBox from . import BaseLabelFormat, abs2rel_rotation, rel2abs_rotation diff --git a/labelCloud/label_formats/vertices_format.py b/labelCloud/io/labels/vertices.py similarity index 97% rename from labelCloud/label_formats/vertices_format.py rename to labelCloud/io/labels/vertices.py index 253f16e..12bd0e6 100644 --- a/labelCloud/label_formats/vertices_format.py +++ b/labelCloud/io/labels/vertices.py @@ -5,8 +5,8 @@ import numpy as np -from ..model import BBox -from ..utils import math3d +from ...model import BBox +from ...utils import math3d from . import BaseLabelFormat diff --git a/labelCloud/io/pointclouds/__init__.py b/labelCloud/io/pointclouds/__init__.py new file mode 100644 index 0000000..469819c --- /dev/null +++ b/labelCloud/io/pointclouds/__init__.py @@ -0,0 +1,3 @@ +from .base import BasePointCloudHandler +from .numpy import NumpyHandler +from .open3d import Open3DHandler diff --git a/labelCloud/io/pointclouds/base.py b/labelCloud/io/pointclouds/base.py new file mode 100644 index 0000000..5555ac6 --- /dev/null +++ b/labelCloud/io/pointclouds/base.py @@ -0,0 +1,46 @@ +import logging +from abc import abstractmethod +from pathlib import Path +from typing import TYPE_CHECKING, Optional, Set, Tuple + +import numpy as np + +from ...utils.logger import blue +from ...utils.singleton import SingletonABCMeta + +if TYPE_CHECKING: + from ...model import PointCloud + + +class BasePointCloudHandler(object, metaclass=SingletonABCMeta): + EXTENSIONS = set() # should be set in subclasses + + @abstractmethod + def read_point_cloud(self, path: Path) -> Tuple[np.ndarray, Optional[np.ndarray]]: + """Read a point cloud file and return only the points and colors as array.""" + logging.info( + blue("Loading point cloud from %s using %s."), path, self.__class__.__name__ + ) + pass + + @abstractmethod + def write_point_cloud(self, path: Path, pointcloud: "PointCloud") -> None: + logging.info( + blue("Writing point cloud to %s using %s."), path, self.__class__.__name__ + ) + pass + + @classmethod + def get_supported_extensions(cls) -> Set[str]: + return set().union(*[handler.EXTENSIONS for handler in cls.__subclasses__()]) + + @classmethod + def get_handler(cls, file_extension: str) -> "BasePointCloudHandler": + """Return a point cloud handler for the given file extension.""" + for subclass in cls.__subclasses__(): + if file_extension in subclass.EXTENSIONS: + return subclass() + + logging.error( + "No point cloud handler found for file extension %s.", file_extension + ) diff --git a/labelCloud/io/pointclouds/numpy.py b/labelCloud/io/pointclouds/numpy.py new file mode 100644 index 0000000..2603a43 --- /dev/null +++ b/labelCloud/io/pointclouds/numpy.py @@ -0,0 +1,32 @@ +import logging +from pathlib import Path +from typing import TYPE_CHECKING, Tuple + +import numpy as np + +from . import BasePointCloudHandler + +if TYPE_CHECKING: + from ...model import PointCloud + + +class NumpyHandler(BasePointCloudHandler): + EXTENSIONS = {".bin"} + + def __init__(self) -> None: + super().__init__() + + def read_point_cloud(self, path: Path) -> Tuple[np.ndarray, None]: + """Read point cloud file as array and drop reflection and nan values.""" + super().read_point_cloud(path) + points = np.fromfile(path, dtype=np.float32) + points = points.reshape((-1, 4 if len(points) % 4 == 0 else 3))[:, 0:3] + return (points[~np.isnan(points).any(axis=1)], None) + + def write_point_cloud(self, path: Path, pointcloud: "PointCloud") -> None: + """Write point cloud points into binary file.""" + super().write_point_cloud(path, pointcloud) + logging.warning( + "Only writing point coordinates, any previous reflection values will be dropped." + ) + pointcloud.points.tofile(path) diff --git a/labelCloud/io/pointclouds/open3d.py b/labelCloud/io/pointclouds/open3d.py new file mode 100644 index 0000000..1da1a8e --- /dev/null +++ b/labelCloud/io/pointclouds/open3d.py @@ -0,0 +1,44 @@ +from pathlib import Path +from typing import TYPE_CHECKING, Optional, Tuple + +import numpy as np +import open3d as o3d + +from . import BasePointCloudHandler + +if TYPE_CHECKING: + from ...model import PointCloud + + +class Open3DHandler(BasePointCloudHandler): + EXTENSIONS = {".pcd", ".ply", ".pts", ".xyz", ".xyzn", ".xyzrgb"} + + def __init__(self) -> None: + super().__init__() + + def to_point_cloud( + self, pointcloud: o3d.geometry.PointCloud + ) -> Tuple[np.ndarray, Optional[np.ndarray]]: + return ( + np.asarray(pointcloud.points).astype("float32"), + np.asarray(pointcloud.colors).astype("float32"), + ) + + def to_open3d_point_cloud( + self, pointcloud: "PointCloud" + ) -> o3d.geometry.PointCloud: + o3d_pointcloud = o3d.geometry.PointCloud( + o3d.utility.Vector3dVector(pointcloud.points) + ) + o3d_pointcloud.colors = o3d.utility.Vector3dVector(pointcloud.colors) + return o3d_pointcloud + + def read_point_cloud(self, path: Path) -> Tuple[np.ndarray, Optional[np.ndarray]]: + super().read_point_cloud(path) + return self.to_point_cloud( + o3d.io.read_point_cloud(str(path), remove_nan_points=True) + ) + + def write_point_cloud(self, path: Path, pointcloud: "PointCloud") -> None: + super().write_point_cloud(path, pointcloud) + o3d.io.write_point_cloud(str(path), self.to_open3d_point_cloud(pointcloud)) diff --git a/labelCloud/label_formats/__init__.py b/labelCloud/label_formats/__init__.py deleted file mode 100644 index 685525f..0000000 --- a/labelCloud/label_formats/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .base import ( - BaseLabelFormat, - abs2rel_rotation, - rel2abs_rotation, -) -from .centroid_format import CentroidFormat -from .kitti_format import KittiFormat -from .vertices_format import VerticesFormat diff --git a/labelCloud/model/__init__.py b/labelCloud/model/__init__.py index e91cf1e..ae1d0fd 100644 --- a/labelCloud/model/__init__.py +++ b/labelCloud/model/__init__.py @@ -1,2 +1,3 @@ from .bbox import BBox +from .perspective import Perspective from .point_cloud import PointCloud diff --git a/labelCloud/model/perspective.py b/labelCloud/model/perspective.py new file mode 100644 index 0000000..932501d --- /dev/null +++ b/labelCloud/model/perspective.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass +from gettext import translation +from typing import TYPE_CHECKING, Tuple + +if TYPE_CHECKING: + from . import PointCloud + + +@dataclass +class Perspective(object): + translation: Tuple[float, float, float] + rotation: Tuple[float, float, float] + + @classmethod + def from_point_cloud(cls, pointcloud: "PointCloud") -> "Perspective": + return cls( + translation=tuple(pointcloud.get_translations()), + rotation=tuple(pointcloud.get_rotations()), + ) diff --git a/labelCloud/model/point_cloud.py b/labelCloud/model/point_cloud.py index 91c1d3c..51518c6 100644 --- a/labelCloud/model/point_cloud.py +++ b/labelCloud/model/point_cloud.py @@ -1,12 +1,17 @@ import ctypes +import logging from pathlib import Path -from typing import List, Tuple +from typing import List, Optional, Tuple + +import pkg_resources import numpy as np import OpenGL.GL as GL +from . import Perspective from ..control.config_manager import config -from ..utils.logger import print_column +from ..io.pointclouds import BasePointCloudHandler +from ..utils.logger import end_section, green, print_column, red, start_section, yellow # Get size of float (4 bytes) for VBOs SIZE_OF_FLOAT = ctypes.sizeof(ctypes.c_float) @@ -24,25 +29,94 @@ def create_buffer(attributes) -> GL.glGenBuffers: return vbo +def calculate_init_translation( + center: Tuple[float, float, float], mins: np.ndarray, maxs: np.ndarray +) -> np.ndarray: + """Calculates the initial translation (x, y, z) of the point cloud. Considers ... + + - the point cloud center + - the point cloud extents + - the far plane setting (caps zoom) + """ + zoom = min( + np.linalg.norm(maxs - mins), + config.getfloat("USER_INTERFACE", "far_plane") * 0.9, + ) + return -np.add(center, [0, 0, zoom]) + + +def colorize_points(points: np.ndarray, z_min: float, z_max: float) -> np.ndarray: + palette = np.loadtxt( + pkg_resources.resource_filename("labelCloud.resources", "rocket-palette.txt") + ) + palette_len = len(palette) - 1 + + colors = np.zeros(points.shape) + for ind, height in enumerate(points[:, 2]): + colors[ind] = palette[round((height - z_min) / (z_max - z_min) * palette_len)] + return colors + + class PointCloud(object): - def __init__(self, path: Path) -> None: - self.path_to_pointcloud = path - self.points = None - self.colors = None - self.colorless = None + def __init__( + self, + path: Path, + points: np.ndarray, + colors: Optional[np.ndarray] = None, + init_translation: Optional[Tuple[float, float, float]] = None, + init_rotation: Optional[Tuple[float, float, float]] = None, + ) -> None: + start_section(f"Loading {path.name}") + self.path = path + self.points = points + self.colors = colors if type(colors) == np.ndarray and len(colors) > 0 else None self.vbo = None - self.center = (0, 0, 0) - self.pcd_mins = None - self.pcd_maxs = None - self.init_translation = (0, 0, 0) + self.center = tuple(np.sum(points[:, i]) / len(points) for i in range(3)) + self.pcd_mins = np.amin(points, axis=0) + self.pcd_maxs = np.amax(points, axis=0) + self.init_translation = init_translation or calculate_init_translation( + self.center, self.pcd_mins, self.pcd_maxs + ) + self.init_rotation = init_rotation or (0, 0, 0) # Point cloud transformations - self.rot_x = 0.0 - self.rot_y = 0.0 - self.rot_z = 0.0 - self.trans_x = 0.0 - self.trans_y = 0.0 - self.trans_z = 0.0 + self.trans_x, self.trans_y, self.trans_z = self.init_translation + self.rot_x, self.rot_y, self.rot_z = self.init_rotation + + if self.colorless and config.getboolean("POINTCLOUD", "COLORLESS_COLORIZE"): + self.colors = colorize_points( + self.points, self.pcd_mins[2], self.pcd_maxs[2] + ) + logging.info("Generated colors for colorless point cloud based on height.") + + self.write_vbo() + + logging.info(green(f"Successfully loaded point cloud from {path}!")) + self.print_details() + end_section() + + @classmethod + def from_file(cls, path: Path, perspective: Optional[Perspective]) -> "PointCloud": + init_translation, init_rotation = (None, None) + if perspective: + init_translation = perspective.translation + init_rotation = perspective.rotation + + points, colors = BasePointCloudHandler.get_handler( + path.suffix + ).read_point_cloud(path=path) + return cls(path, points, colors, init_translation, init_rotation) + + def to_file(self, path: Optional[Path] = None) -> None: + if not path: + path = self.path + BasePointCloudHandler.get_handler(path.suffix).write_point_cloud( + path=path, pointcloud=self + ) + + @property + def colorless(self): + return self.colors is None # GETTERS AND SETTERS def get_no_of_points(self) -> int: @@ -63,10 +137,6 @@ def get_mins_maxs(self) -> Tuple[float, float]: def get_min_max_height(self) -> Tuple[float, float]: return self.pcd_mins[2], self.pcd_maxs[2] - def set_mins_maxs(self) -> None: - self.pcd_mins = np.amin(self.points, axis=0) - self.pcd_maxs = np.amax(self.points, axis=0) - def set_rot_x(self, angle) -> None: self.rot_x = angle % 360 @@ -107,8 +177,7 @@ def transform_data(self) -> np.ndarray: return attributes.flatten() # flatten to single list def write_vbo(self) -> None: - v_array = self.transform_data() - self.vbo = create_buffer(v_array) + self.vbo = create_buffer(self.transform_data()) def draw_pointcloud(self) -> None: GL.glTranslate( @@ -156,10 +225,29 @@ def draw_pointcloud(self) -> None: GL.glDisableClientState(GL.GL_COLOR_ARRAY) GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0) - def reset_translation(self) -> None: - self.trans_x, self.trans_y, self.trans_z = self.init_translation + def reset_perspective(self) -> None: + self.trans_x, self.trans_y, self.trans_z = self.init_rotation + self.rot_x, self.rot_y, self.rot_z = self.init_rotation def print_details(self) -> None: + print_column( + [ + "Number of Points:", + green(len(self.points)) + if len(self.points) > 0 + else red(len(self.points)), + ] + ) + print_column( + [ + "Number of Colors:", + yellow("None") + if self.colorless + else green(len(self.colors)) + if len(self.colors) == len(self.points) + else red(len(self.colors)), + ] + ) print_column(["Point Cloud Center:", np.round(self.center, 2)]) print_column(["Point Cloud Minimums:", np.round(self.pcd_mins, 2)]) print_column(["Point Cloud Maximums:", np.round(self.pcd_maxs, 2)]) diff --git a/labelCloud/resources/interfaces/interface.ui b/labelCloud/resources/interfaces/interface.ui index 3e7190c..4bd6f81 100644 --- a/labelCloud/resources/interfaces/interface.ui +++ b/labelCloud/resources/interfaces/interface.ui @@ -1621,7 +1621,7 @@ false - Save current Perspective + Keep Perspective diff --git a/labelCloud/utils/logger.py b/labelCloud/utils/logger.py index 2ee4e91..d58b84e 100644 --- a/labelCloud/utils/logger.py +++ b/labelCloud/utils/logger.py @@ -1,18 +1,85 @@ import logging +import re import shutil from enum import Enum from typing import List +# --------------------------------- FORMATTING -------------------------------- # + + +class Format(Enum): + RESET = "\033[0;0m" + RED = "\033[1;31m" + GREEN = "\033[0;32m" + YELLOW = "\33[93m" # "\033[33m" + BLUE = "\033[1;34m" + CYAN = "\033[1;36m" + BOLD = "\033[;1m" + REVERSE = "\033[;7m" + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKCYAN = "\033[96m" + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + ENDC = "\033[0m" + UNDERLINE = "\033[4m" + + GREY = "\33[90m" + + +def format(text: str, color: Format): + return f"{color.value}{text}{Format.ENDC.value}" + + +red = lambda text: format(text, Format.RED) +green = lambda text: format(text, Format.OKGREEN) +yellow = lambda text: format(text, Format.YELLOW) +blue = lambda text: format(text, Format.BLUE) +bold = lambda text: format(text, Format.BOLD) + + +class ColorFormatter(logging.Formatter): + MSG_FORMAT = "%(message)s" + + FORMATS = { + logging.DEBUG: Format.GREY.value + MSG_FORMAT + Format.ENDC.value, + logging.INFO: MSG_FORMAT, + logging.WARNING: Format.YELLOW.value + MSG_FORMAT + Format.ENDC.value, + logging.ERROR: Format.RED.value + MSG_FORMAT + Format.ENDC.value, + logging.CRITICAL: Format.RED.value + + Format.BOLD.value + + MSG_FORMAT + + Format.ENDC.value, + } + + def format(self, record): + log_fmt = self.FORMATS.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + + +class UncolorFormatter(logging.Formatter): + MSG_FORMAT = "%(asctime)s - %(levelname)-8s: %(message)s" + PATTERN = re.compile("|".join(re.escape(c.value) for c in Format)) + + def format(self, record): + record.msg = self.PATTERN.sub("", record.msg) + formatter = logging.Formatter(self.MSG_FORMAT) + return formatter.format(record) + + # ---------------------------------- CONFIG ---------------------------------- # # Create handlers c_handler = logging.StreamHandler() -f_handler = logging.FileHandler(".labelCloud.log", mode="a") +f_handler = logging.FileHandler(".labelCloud.log", mode="w") c_handler.setLevel(logging.INFO) # TODO: Automatic coloring f_handler.setLevel(logging.DEBUG) # TODO: Filter colors # Create formatters and add it to handlers -f_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)-8s: %(message)s")) +c_handler.setFormatter(ColorFormatter()) +f_handler.setFormatter(UncolorFormatter()) logging.basicConfig( @@ -21,6 +88,7 @@ handlers=[c_handler, f_handler], ) + # ---------------------------------- HELPERS --------------------------------- # TERM_SIZE = shutil.get_terminal_size(fallback=(120, 50)) @@ -50,32 +118,3 @@ def print_column(column_values: List[str], last: bool = False): for row in rows: logging.info("".join(str(word).ljust(col_width) for word in row)) rows = [] - - -class Format(Enum): - RESET = "\033[0;0m" - RED = "\033[1;31m" - GREEN = "\033[0;32m" - YELLOW = "\033[33m" - BLUE = "\033[1;34m" - CYAN = "\033[1;36m" - BOLD = "\033[;1m" - REVERSE = "\033[;7m" - HEADER = "\033[95m" - OKBLUE = "\033[94m" - OKCYAN = "\033[96m" - OKGREEN = "\033[92m" - WARNING = "\033[93m" - FAIL = "\033[91m" - ENDC = "\033[0m" - UNDERLINE = "\033[4m" - - -def format(text: str, color: Format): - return f"{color.value}{text}{Format.ENDC.value}" - - -red = lambda text: format(text, Format.RED) -green = lambda text: format(text, Format.OKGREEN) -yellow = lambda text: format(text, Format.YELLOW) -bold = lambda text: format(text, Format.BOLD) diff --git a/labelCloud/utils/singleton.py b/labelCloud/utils/singleton.py new file mode 100644 index 0000000..02c234e --- /dev/null +++ b/labelCloud/utils/singleton.py @@ -0,0 +1,10 @@ +from abc import ABCMeta + + +class SingletonABCMeta(ABCMeta): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(SingletonABCMeta, cls).__call__(*args, **kwargs) + return cls._instances[cls] diff --git a/labelCloud/view/gui.py b/labelCloud/view/gui.py index 10deddd..059c285 100644 --- a/labelCloud/view/gui.py +++ b/labelCloud/view/gui.py @@ -1,7 +1,7 @@ import logging import re from pathlib import Path -from typing import TYPE_CHECKING, List, Set +from typing import TYPE_CHECKING, Set import pkg_resources from PyQt5 import QtCore, QtGui, QtWidgets, uic @@ -53,6 +53,10 @@ def set_zrotation_only(state: bool) -> None: config.set("USER_INTERFACE", "z_rotation_only", str(state)) +def set_keep_perspective(state: bool) -> None: + config.set("USER_INTERFACE", "keep_perspective", str(state)) + + # CSS file paths need to be set dynamically STYLESHEET = """ * {{ @@ -358,9 +362,7 @@ def connect_events(self) -> None: self.action_zrotation.toggled.connect(set_zrotation_only) self.action_showfloor.toggled.connect(set_floor_visibility) self.action_showorientation.toggled.connect(set_orientation_visibility) - self.action_saveperspective.toggled.connect( - lambda state: self.controller.pcd_manager.save_current_perspective(state) - ) + self.action_saveperspective.toggled.connect(set_keep_perspective) self.action_alignpcd.toggled.connect( self.controller.align_mode.change_activation ) @@ -454,7 +456,7 @@ def show_2d_image(self): self.imageLabel.show() def show_no_pointcloud_dialog( - self, pcd_folder: Path, pcd_extensions: List[str] + self, pcd_folder: Path, pcd_extensions: Set[str] ) -> None: msg = QMessageBox(self) msg.setIcon(QMessageBox.Warning) diff --git a/pyproject.toml b/pyproject.toml index b5a3c46..374b58c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,4 +3,4 @@ requires = [ "setuptools>=42", "wheel" ] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt index 7c22fc2..2f3ddbd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ numpy~=1.21.4 open3d~=0.14.1 PyOpenGL~=3.1.5 -PyQt5~=5.14.1 \ No newline at end of file +PyQt5~=5.14.1 diff --git a/setup.cfg b/setup.cfg index 6ddced6..1765c3a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,7 +36,9 @@ packages = labelCloud labelCloud.control labelCloud.definitions - labelCloud.label_formats + labelCloud.io + labelCloud.io.labels + labelCloud.io.pointclouds labelCloud.labeling_strategies labelCloud.model labelCloud.resources