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