From 308b98434a7b2a37c1c3807febfcb3689d9ca9d3 Mon Sep 17 00:00:00 2001 From: ch-sa Date: Sat, 5 Feb 2022 13:26:36 +0100 Subject: [PATCH 1/4] Create io module - package will all import export logic - moving label formats there --- labelCloud/control/label_manager.py | 2 +- labelCloud/io/__init__.py | 0 labelCloud/io/labels/__init__.py | 8 ++++++++ labelCloud/{label_formats => io/labels}/base.py | 2 +- .../centroid_format.py => io/labels/centroid.py} | 2 +- .../{label_formats/kitti_format.py => io/labels/kitti.py} | 2 +- .../vertices_format.py => io/labels/vertices.py} | 4 ++-- labelCloud/io/pointclouds/__init__.py | 0 labelCloud/label_formats/__init__.py | 8 -------- setup.cfg | 4 +++- 10 files changed, 17 insertions(+), 15 deletions(-) create mode 100644 labelCloud/io/__init__.py create mode 100644 labelCloud/io/labels/__init__.py rename labelCloud/{label_formats => io/labels}/base.py (99%) rename labelCloud/{label_formats/centroid_format.py => io/labels/centroid.py} (98%) rename labelCloud/{label_formats/kitti_format.py => io/labels/kitti.py} (99%) rename labelCloud/{label_formats/vertices_format.py => io/labels/vertices.py} (97%) create mode 100644 labelCloud/io/pointclouds/__init__.py delete mode 100644 labelCloud/label_formats/__init__.py diff --git a/labelCloud/control/label_manager.py b/labelCloud/control/label_manager.py index be10eea..eb4d7b6 100644 --- a/labelCloud/control/label_manager.py +++ b/labelCloud/control/label_manager.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import List -from ..label_formats import BaseLabelFormat, CentroidFormat, KittiFormat, VerticesFormat +from ..io.labels import BaseLabelFormat, CentroidFormat, KittiFormat, VerticesFormat from ..model.bbox import BBox from .config_manager import config 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..e69de29 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/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 From 06cc2d535fa36061bbc61ec5271430b5f6b3c7b0 Mon Sep 17 00:00:00 2001 From: ch-sa Date: Sat, 5 Feb 2022 13:55:53 +0100 Subject: [PATCH 2/4] Add point cloud handlers - abstract point cloud handlers - add Open3D and numpy handlers - support read and write --- labelCloud/__init__.py | 2 +- labelCloud/control/pcd_manager.py | 127 +++++------------------- labelCloud/io/pointclouds/__init__.py | 2 + labelCloud/io/pointclouds/base.py | 46 +++++++++ labelCloud/io/pointclouds/numpy.py | 32 ++++++ labelCloud/io/pointclouds/open3d.py | 29 ++++++ labelCloud/model/__init__.py | 1 + labelCloud/model/perspective.py | 9 ++ labelCloud/model/point_cloud.py | 136 +++++++++++++++++++++----- labelCloud/utils/logger.py | 1 + labelCloud/utils/singleton.py | 10 ++ labelCloud/view/gui.py | 2 +- 12 files changed, 269 insertions(+), 128 deletions(-) create mode 100644 labelCloud/io/pointclouds/base.py create mode 100644 labelCloud/io/pointclouds/numpy.py create mode 100644 labelCloud/io/pointclouds/open3d.py create mode 100644 labelCloud/model/perspective.py create mode 100644 labelCloud/utils/singleton.py 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/pcd_manager.py b/labelCloud/control/pcd_manager.py index d1d24cc..a7c5c7b 100644 --- a/labelCloud/control/pcd_manager.py +++ b/labelCloud/control/pcd_manager.py @@ -2,50 +2,32 @@ Module to manage the point clouds (loading, navigation, floor alignment). Sets the point cloud and original point cloud path. Initiate the writing to the virtual object buffer. """ +from gettext import translation 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 green 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 @@ -95,7 +77,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 +97,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 +109,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!") @@ -151,9 +139,12 @@ def save_labels_into_file(self, bboxes: List[BBox]) -> None: logging.warning("No point clouds to save labels for!") def save_current_perspective(self, active: bool = True) -> None: - if active and self.pointcloud: + if not config.getboolean("USER_INTERFACE", "KEEP_PERSPECTIVE") or active: + return + + if self.pointcloud and active: self.saved_perspective = Perspective( - zoom=self.pointcloud.trans_z, + translation=tuple(self.pointcloud.get_translations()), rotation=tuple(self.pointcloud.get_rotations()), ) logging.info(f"Saved current perspective ({self.saved_perspective}).") @@ -162,74 +153,6 @@ def save_current_perspective(self, active: bool = True) -> 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 +182,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) @@ -298,11 +221,11 @@ def rotate_pointcloud( ) 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") + # 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.from_file(save_path, self.saved_perspective) # HELPER diff --git a/labelCloud/io/pointclouds/__init__.py b/labelCloud/io/pointclouds/__init__.py index e69de29..15cccc5 100644 --- a/labelCloud/io/pointclouds/__init__.py +++ b/labelCloud/io/pointclouds/__init__.py @@ -0,0 +1,2 @@ +from .open3d import Open3DHandler +from .numpy import NumpyHandler 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..265e9e9 --- /dev/null +++ b/labelCloud/io/pointclouds/open3d.py @@ -0,0 +1,29 @@ +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 read_point_cloud(self, path: Path) -> Tuple[np.ndarray, Optional[np.ndarray]]: + super().read_point_cloud(path) + o3d_pcd = o3d.io.read_point_cloud(str(path), remove_nan_points=True) + return ( + np.asarray(o3d_pcd.points).astype("float32"), + np.asarray(o3d_pcd.colors).astype("float32"), + ) + + def write_point_cloud(self, path: Path, point_cloud: "PointCloud") -> None: + # TODO: Implement + return super().write_point_cloud(path, point_cloud) 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..2f271fe --- /dev/null +++ b/labelCloud/model/perspective.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass +from typing import Tuple + + +@dataclass +class Perspective(object): + translation: Tuple[float, float, float] + rotation: Tuple[float, float, float] + diff --git a/labelCloud/model/point_cloud.py b/labelCloud/model/point_cloud.py index 91c1d3c..94e60c0 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 NumpyHandler, Open3DHandler +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: + 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_to_pointcloud = path - self.points = None - self.colors = None - self.colorless = None + 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/utils/logger.py b/labelCloud/utils/logger.py index 2ee4e91..b5c43bf 100644 --- a/labelCloud/utils/logger.py +++ b/labelCloud/utils/logger.py @@ -78,4 +78,5 @@ def format(text: str, color: Format): 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) 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..ef1251c 100644 --- a/labelCloud/view/gui.py +++ b/labelCloud/view/gui.py @@ -454,7 +454,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) From f86d48dc902341443461eba6121c19cd3421b310 Mon Sep 17 00:00:00 2001 From: ch-sa Date: Sat, 5 Feb 2022 22:33:07 +0100 Subject: [PATCH 3/4] Move Perspective to model - fix align point cloud to also write *.bin point clouds --- config.ini | 1 - labelCloud/control/pcd_manager.py | 38 ++++++++------------ labelCloud/io/pointclouds/__init__.py | 3 +- labelCloud/io/pointclouds/open3d.py | 29 +++++++++++---- labelCloud/model/perspective.py | 12 ++++++- labelCloud/model/point_cloud.py | 6 ++-- labelCloud/resources/interfaces/interface.ui | 2 +- labelCloud/view/gui.py | 8 +++-- pyproject.toml | 2 +- requirements.txt | 2 +- 10 files changed, 60 insertions(+), 43 deletions(-) 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/control/pcd_manager.py b/labelCloud/control/pcd_manager.py index a7c5c7b..6a52899 100644 --- a/labelCloud/control/pcd_manager.py +++ b/labelCloud/control/pcd_manager.py @@ -2,7 +2,6 @@ Module to manage the point clouds (loading, navigation, floor alignment). Sets the point cloud and original point cloud path. Initiate the writing to the virtual object buffer. """ -from gettext import translation import logging from pathlib import Path from shutil import copyfile @@ -12,6 +11,7 @@ import numpy as np import open3d as o3d +from labelCloud.io.pointclouds.open3d import Open3DHandler from ..io.pointclouds import BasePointCloudHandler, Open3DHandler from ..model import BBox, Perspective, PointCloud @@ -35,14 +35,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: @@ -138,19 +137,10 @@ 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 not config.getboolean("USER_INTERFACE", "KEEP_PERSPECTIVE") or active: - return - - if self.pointcloud and active: - self.saved_perspective = Perspective( - translation=tuple(self.pointcloud.get_translations()), - 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 rotate_around_x(self, dangle) -> None: @@ -206,16 +196,14 @@ def rotate_pointcloud( 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]]) # 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), ) @@ -224,8 +212,10 @@ def rotate_pointcloud( # 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 = PointCloud.from_file(save_path, self.saved_perspective) + self.pointcloud = PointCloud( + save_path, *Open3DHandler().to_point_cloud(o3d_pointcloud) + ) + self.pointcloud.to_file() # HELPER diff --git a/labelCloud/io/pointclouds/__init__.py b/labelCloud/io/pointclouds/__init__.py index 15cccc5..469819c 100644 --- a/labelCloud/io/pointclouds/__init__.py +++ b/labelCloud/io/pointclouds/__init__.py @@ -1,2 +1,3 @@ -from .open3d import Open3DHandler +from .base import BasePointCloudHandler from .numpy import NumpyHandler +from .open3d import Open3DHandler diff --git a/labelCloud/io/pointclouds/open3d.py b/labelCloud/io/pointclouds/open3d.py index 265e9e9..1da1a8e 100644 --- a/labelCloud/io/pointclouds/open3d.py +++ b/labelCloud/io/pointclouds/open3d.py @@ -16,14 +16,29 @@ class Open3DHandler(BasePointCloudHandler): 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) - o3d_pcd = o3d.io.read_point_cloud(str(path), remove_nan_points=True) - return ( - np.asarray(o3d_pcd.points).astype("float32"), - np.asarray(o3d_pcd.colors).astype("float32"), + return self.to_point_cloud( + o3d.io.read_point_cloud(str(path), remove_nan_points=True) ) - def write_point_cloud(self, path: Path, point_cloud: "PointCloud") -> None: - # TODO: Implement - return super().write_point_cloud(path, point_cloud) + 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/model/perspective.py b/labelCloud/model/perspective.py index 2f271fe..932501d 100644 --- a/labelCloud/model/perspective.py +++ b/labelCloud/model/perspective.py @@ -1,5 +1,9 @@ from dataclasses import dataclass -from typing import Tuple +from gettext import translation +from typing import TYPE_CHECKING, Tuple + +if TYPE_CHECKING: + from . import PointCloud @dataclass @@ -7,3 +11,9 @@ 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 94e60c0..7bfa8bb 100644 --- a/labelCloud/model/point_cloud.py +++ b/labelCloud/model/point_cloud.py @@ -1,7 +1,7 @@ import ctypes import logging from pathlib import Path -from typing import List, Optional, Tuple +from typing import Dict, List, Optional, Tuple import pkg_resources @@ -10,7 +10,7 @@ from . import Perspective from ..control.config_manager import config -from ..io.pointclouds import NumpyHandler, Open3DHandler +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 @@ -67,7 +67,7 @@ def __init__( init_rotation: Optional[Tuple[float, float, float]] = None, ) -> None: start_section(f"Loading {path.name}") - self.path_to_pointcloud = path + self.path = path self.points = points self.colors = colors if type(colors) == np.ndarray and len(colors) > 0 else None self.vbo = None 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/view/gui.py b/labelCloud/view/gui.py index ef1251c..8562906 100644 --- a/labelCloud/view/gui.py +++ b/labelCloud/view/gui.py @@ -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 ) 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 From 48af69c288a1e22751e68e99e5f179281d00158f Mon Sep 17 00:00:00 2001 From: ch-sa Date: Sun, 6 Feb 2022 16:59:53 +0100 Subject: [PATCH 4/4] Cleaning logger - color cli logs according to level - clear file logs from color tags - overwrite log file each session --- labelCloud/control/label_manager.py | 2 +- labelCloud/control/pcd_manager.py | 14 ++-- labelCloud/model/point_cloud.py | 2 +- labelCloud/utils/logger.py | 102 +++++++++++++++++++--------- labelCloud/view/gui.py | 2 +- 5 files changed, 80 insertions(+), 42 deletions(-) diff --git a/labelCloud/control/label_manager.py b/labelCloud/control/label_manager.py index eb4d7b6..d6f33e2 100644 --- a/labelCloud/control/label_manager.py +++ b/labelCloud/control/label_manager.py @@ -3,7 +3,7 @@ from typing import List from ..io.labels import BaseLabelFormat, CentroidFormat, KittiFormat, VerticesFormat -from ..model.bbox import BBox +from ..model import BBox from .config_manager import config diff --git a/labelCloud/control/pcd_manager.py b/labelCloud/control/pcd_manager.py index 6a52899..a22d190 100644 --- a/labelCloud/control/pcd_manager.py +++ b/labelCloud/control/pcd_manager.py @@ -11,11 +11,10 @@ import numpy as np import open3d as o3d -from labelCloud.io.pointclouds.open3d import Open3DHandler from ..io.pointclouds import BasePointCloudHandler, Open3DHandler from ..model import BBox, Perspective, PointCloud -from ..utils.logger import green +from ..utils.logger import blue, green, print_column from .config_manager import config from .label_manager import LabelManager @@ -191,6 +190,7 @@ 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( @@ -199,6 +199,10 @@ def rotate_pointcloud( 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 if abs(self.pointcloud.pcd_mins[2]) > self.pointcloud.pcd_maxs[2]: @@ -208,12 +212,8 @@ def rotate_pointcloud( 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") - self.pointcloud = PointCloud( - save_path, *Open3DHandler().to_point_cloud(o3d_pointcloud) + self.pcd_path, *Open3DHandler().to_point_cloud(o3d_pointcloud) ) self.pointcloud.to_file() diff --git a/labelCloud/model/point_cloud.py b/labelCloud/model/point_cloud.py index 7bfa8bb..51518c6 100644 --- a/labelCloud/model/point_cloud.py +++ b/labelCloud/model/point_cloud.py @@ -1,7 +1,7 @@ import ctypes import logging from pathlib import Path -from typing import Dict, List, Optional, Tuple +from typing import List, Optional, Tuple import pkg_resources diff --git a/labelCloud/utils/logger.py b/labelCloud/utils/logger.py index b5c43bf..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,33 +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) -blue = lambda text: format(text, Format.BLUE) -bold = lambda text: format(text, Format.BOLD) diff --git a/labelCloud/view/gui.py b/labelCloud/view/gui.py index 8562906..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