From 3e1650db7587280ab28f5e1f343340b71062a45c Mon Sep 17 00:00:00 2001 From: Ching-yu Lin <60384727+chingyulin@users.noreply.github.com> Date: Wed, 22 Mar 2023 20:27:34 +0000 Subject: [PATCH 1/5] feat: filter segmentation class --- labelCloud/control/controller.py | 4 + labelCloud/control/pcd_manager.py | 60 ++++++++++++- labelCloud/model/point_cloud.py | 47 +++++++++- labelCloud/resources/interfaces/interface.ui | 91 ++++++++++++++++++++ labelCloud/view/color_button.py | 5 +- labelCloud/view/gui.py | 9 +- 6 files changed, 205 insertions(+), 11 deletions(-) diff --git a/labelCloud/control/controller.py b/labelCloud/control/controller.py index 50456e5..9848888 100644 --- a/labelCloud/control/controller.py +++ b/labelCloud/control/controller.py @@ -52,12 +52,16 @@ def startup(self, view: "GUI") -> None: # Read labels from folders self.pcd_manager.read_pointcloud_folder() self.next_pcd(save=False) + if LabelConfig().type == LabelingMode.SEMANTIC_SEGMENTATION: + self.pcd_manager.populate_segmentation_list() def loop_gui(self) -> None: """Function collection called during each event loop iteration.""" self.set_crosshair() self.set_selected_side() self.view.gl_widget.updateGL() + if LabelConfig().type == LabelingMode.SEMANTIC_SEGMENTATION: + self.pcd_manager.loop_seg_list_check_state() # POINT CLOUD METHODS def next_pcd(self, save: bool = True) -> None: diff --git a/labelCloud/control/pcd_manager.py b/labelCloud/control/pcd_manager.py index aed5408..7bdbf0d 100644 --- a/labelCloud/control/pcd_manager.py +++ b/labelCloud/control/pcd_manager.py @@ -8,15 +8,18 @@ from typing import TYPE_CHECKING, List, Optional, Set, Tuple import numpy as np -import pkg_resources - import open3d as o3d +import pkg_resources +from PyQt5 import QtCore +from PyQt5.QtWidgets import QCheckBox, QLabel from ..definitions.types import LabelingMode, Point3D from ..io.labels.config import LabelConfig from ..io.pointclouds import BasePointCloudHandler, Open3DHandler from ..model import BBox, Perspective, PointCloud +from ..utils.color import rgb_to_hex from ..utils.logger import blue, green, print_column +from ..view.color_button import ColorButton from .config_manager import config from .label_manager import LabelManager @@ -264,7 +267,8 @@ def rotate_pointcloud( def assign_point_label_in_box(self, box: BBox) -> None: assert self.pointcloud is not None - points = self.pointcloud.points + points = self.pointcloud.points.copy() + points[~self.pointcloud.visible] = np.finfo(np.float32).max points_inside = box.is_inside(points) # Relabel the points if its inside the box @@ -307,3 +311,53 @@ def update_pcd_infos(self, pointcloud_label: Optional[str] = None) -> None: else: self.view.button_next_pcd.setEnabled(True) self.view.button_prev_pcd.setEnabled(True) + + def populate_segmentation_list(self) -> None: + assert self.pointcloud is not None + assert self.pointcloud.labels is not None + self.seg_list_label: List[QLabel] = [] + self.seg_list_check_box: List[QCheckBox] = [] + self.seg_list_check_state: List[QtCore.Qt.CheckState] = [] + for idx, label_class in enumerate(LabelConfig().classes, start=1): + self.seg_list_label.append(QLabel(label_class.name)) + check_box = QCheckBox() + check_box.setCheckState(QtCore.Qt.Checked) + color_button = ColorButton( + color=rgb_to_hex(label_class.color), changeable=False + ) + self.seg_list_check_box.append(check_box) + self.seg_list_check_state.append(self.seg_list_check_box[-1].checkState()) + self.view.segmentation_list.addWidget(self.seg_list_label[-1], idx, 0) + self.view.segmentation_list.addWidget(color_button, idx, 1) + self.view.segmentation_list.addWidget(self.seg_list_check_box[-1], idx, 2) + + def loop_seg_list_check_state(self): + curr_checked_status = self.seg_list_check_state.copy() + any_changed = False + + move_back = [] + move_away = [] + for idx, (box, prev_status) in enumerate( + zip( + self.seg_list_check_box, + self.seg_list_check_state, + ) + ): + if box.checkState() != prev_status: + any_changed = True + curr_checked_status[idx] = box.checkState() + changed_id = LabelConfig().classes[idx].id + if box.checkState() == QtCore.Qt.Checked: + move_back.append(changed_id) + else: + move_away.append(changed_id) + + if any_changed: + points_move_away = ( + np.isin(self.pointcloud.labels, move_away) if move_away else None + ) + points_move_back = ( + np.isin(self.pointcloud.labels, move_back) if move_back else None + ) + self.pointcloud.update_position_vbo(points_move_away, points_move_back) + self.seg_list_check_state = curr_checked_status diff --git a/labelCloud/model/point_cloud.py b/labelCloud/model/point_cloud.py index 6a19183..297c8dd 100644 --- a/labelCloud/model/point_cloud.py +++ b/labelCloud/model/point_cloud.py @@ -63,6 +63,8 @@ def __init__( self.labels = segmentation_labels self.mix_ratio = config.getfloat("POINTCLOUD", "label_color_mix_ratio") + self.visible = np.ones((self.points.shape[0],), dtype=np.bool_) + self.vbo = None self.center: Point3D = tuple(np.sum(points[:, i]) / len(points) for i in range(3)) # type: ignore self.pcd_mins: npt.NDArray[np.float32] = np.amin(points, axis=0) @@ -201,11 +203,52 @@ def color_with_label(self) -> bool: def has_label(self) -> bool: return self.labels is not None + def update_position_vbo( + self, + points_move_away: Optional[npt.NDArray[np.bool_]], + points_move_back: Optional[npt.NDArray[np.bool_]], + ): + + GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.position_vbo) + # Move points to super far away + if points_move_away is not None and np.any(points_move_away): + move_away_idxs = np.where(points_move_away)[0] + self.visible[move_away_idxs] = False + arrays = consecutive(move_away_idxs) + stride = self.points.shape[1] * SIZE_OF_FLOAT + for arr in arrays: + super_far: npt.NDArray[np.float32] = ( + np.ones((arr.shape[0], 3), dtype=np.float32) + * np.finfo(np.float32).max + ) + # partially update label_vbo from positions arr[0] to arr[-1] + GL.glBufferSubData( + GL.GL_ARRAY_BUFFER, + offset=arr[0] * stride, + size=super_far.nbytes, + data=super_far, + ) + # Move points back + if points_move_back is not None and np.any(points_move_back): + move_back_idxs = np.where(points_move_back)[0] + self.visible[move_back_idxs] = True + arrays = consecutive(move_back_idxs) + stride = self.points.shape[1] * SIZE_OF_FLOAT + + for arr in arrays: + points: npt.NDArray[np.float32] = self.points[arr] + GL.glBufferSubData( + GL.GL_ARRAY_BUFFER, + offset=arr[0] * stride, + size=points.nbytes, + data=points, + ) + def update_selected_points_in_label_vbo( self, points_inside: npt.NDArray[np.bool_] ) -> None: - """Send the selected updated label colors to label vbo. This function - assumes the `self.label_colors[points_inside]` have been altered. + """Send the selected updated label colors to `self.label_vbo`. This + function assumes the `self.label_colors[points_inside]` have been altered. This function only partially updates the label vbo to minimise the data sent to gpu. It leverages `glBufferSubData` method to perform partial update and `consecutive` method to find consecutive indexes diff --git a/labelCloud/resources/interfaces/interface.ui b/labelCloud/resources/interfaces/interface.ui index 3f965c5..d8e181a 100644 --- a/labelCloud/resources/interfaces/interface.ui +++ b/labelCloud/resources/interfaces/interface.ui @@ -1460,6 +1460,97 @@ + + + + + DejaVu Sans,Arial + 75 + true + + + + Segmentation Controls + + + + QLayout::SetDefaultConstraint + + + 0 + + + 6 + + + + + + 0 + 0 + + + + + DejaVu Sans,Arial + 12 + 50 + true + + + + Class Name + + + + + + + + 0 + 0 + + + + + DejaVu Sans,Arial + 12 + 50 + true + + + + Color + + + + + + + + 0 + 0 + + + + + DejaVu Sans,Arial + 12 + 50 + true + + + + Visible + + + + + + + + + diff --git a/labelCloud/view/color_button.py b/labelCloud/view/color_button.py index d1d6bae..9170038 100644 --- a/labelCloud/view/color_button.py +++ b/labelCloud/view/color_button.py @@ -14,12 +14,13 @@ class ColorButton(QtWidgets.QPushButton): colorChanged = pyqtSignal(object) - def __init__(self, *args, color="#FF0000", **kwargs): + def __init__(self, *args, color="#FF0000", changeable: bool = True, **kwargs): super(ColorButton, self).__init__(*args, **kwargs) self._color = None self._default = color - self.pressed.connect(self.onColorPicker) + if changeable: + self.pressed.connect(self.onColorPicker) # Set the initial/default state. self.setColor(self._default) diff --git a/labelCloud/view/gui.py b/labelCloud/view/gui.py index 1debf6f..7903d28 100644 --- a/labelCloud/view/gui.py +++ b/labelCloud/view/gui.py @@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Optional, Set import pkg_resources -from labelCloud.view.startup_dialog import StartupDialog from PyQt5 import QtCore, QtGui, QtWidgets, uic from PyQt5.QtCore import QEvent from PyQt5.QtGui import QPixmap @@ -20,6 +19,8 @@ QMessageBox, ) +from labelCloud.view.startup_dialog import StartupDialog + from ..control.config_manager import config from ..definitions.types import Color3f, LabelingMode from ..io.labels.config import LabelConfig @@ -191,6 +192,8 @@ def __init__(self, control: "Controller") -> None: self.button_save_label: QtWidgets.QPushButton # RIGHT PANEL + self.segmentation_list_group: QtWidgets.QGroupBox + self.segmentation_list: QtWidgets.QGridLayout self.label_list: QtWidgets.QListWidget self.current_class_dropdown: QtWidgets.QComboBox self.button_deselect_label: QtWidgets.QPushButton @@ -247,6 +250,7 @@ def __init__(self, control: "Controller") -> None: if LabelConfig().type == LabelingMode.OBJECT_DETECTION: self.button_assign_label.setVisible(False) self.act_color_with_label.setVisible(False) + self.segmentation_list_group.setVisible(False) # Connect with controller self.controller.startup(self) @@ -508,9 +512,6 @@ def init_progress(self, min_value, max_value): def update_progress(self, value) -> None: self.progressbar_pcds.setValue(value) - def update_current_class_dropdown(self) -> None: - self.controller.pcd_manager.populate_class_dropdown() - def update_bbox_stats(self, bbox) -> None: viewing_precision = config.getint("USER_INTERFACE", "viewing_precision") if bbox and not self.line_edited_activated(): From 7a5297d31a27576f2c0ea0faf459afd149b6591b Mon Sep 17 00:00:00 2001 From: Ching-yu Lin <60384727+chingyulin@users.noreply.github.com> Date: Wed, 22 Mar 2023 20:53:38 +0000 Subject: [PATCH 2/5] update black --- labelCloud.py | 1 - labelCloud/model/bbox.py | 2 -- labelCloud/model/point_cloud.py | 1 - labelCloud/tests/integration/conftest.py | 2 -- .../segmentation_handler/test_numpy_segmentation_handler.py | 1 - labelCloud/view/startup_dialog.py | 2 -- requirements.txt | 2 +- 7 files changed, 1 insertion(+), 10 deletions(-) diff --git a/labelCloud.py b/labelCloud.py index e2cf509..6b73e8a 100644 --- a/labelCloud.py +++ b/labelCloud.py @@ -1,5 +1,4 @@ from labelCloud.__main__ import main if __name__ == "__main__": - main() diff --git a/labelCloud/model/bbox.py b/labelCloud/model/bbox.py index 5fa2727..a35b4df 100644 --- a/labelCloud/model/bbox.py +++ b/labelCloud/model/bbox.py @@ -20,7 +20,6 @@ class BBox(object): - MIN_DIMENSION: float = config.getfloat("LABEL", "MIN_BOUNDINGBOX_DIMENSION") HIGHLIGHTED_COLOR: Color3f = Color3f(0, 1, 0) @@ -255,7 +254,6 @@ def change_side( self.translate_side(0, 4, distance) def is_inside(self, points: npt.NDArray[np.float32]) -> npt.NDArray[np.bool_]: - vertices = self.get_vertices().copy() # .------------. diff --git a/labelCloud/model/point_cloud.py b/labelCloud/model/point_cloud.py index 297c8dd..61aff86 100644 --- a/labelCloud/model/point_cloud.py +++ b/labelCloud/model/point_cloud.py @@ -208,7 +208,6 @@ def update_position_vbo( points_move_away: Optional[npt.NDArray[np.bool_]], points_move_back: Optional[npt.NDArray[np.bool_]], ): - GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.position_vbo) # Move points to super far away if points_move_away is not None and np.any(points_move_away): diff --git a/labelCloud/tests/integration/conftest.py b/labelCloud/tests/integration/conftest.py index 036c0e7..52775dc 100644 --- a/labelCloud/tests/integration/conftest.py +++ b/labelCloud/tests/integration/conftest.py @@ -19,7 +19,6 @@ def pytest_configure(config): @pytest.fixture def startup_pyqt(qtbot, qapp, monkeypatch): - # Setup Model-View-Control structure control = Controller() @@ -39,5 +38,4 @@ def startup_pyqt(qtbot, qapp, monkeypatch): @pytest.fixture def bbox(): - return BBox(cx=0, cy=0, cz=0, length=3, width=2, height=1) diff --git a/labelCloud/tests/unit/segmentation_handler/test_numpy_segmentation_handler.py b/labelCloud/tests/unit/segmentation_handler/test_numpy_segmentation_handler.py index 35b9ea7..70391a7 100644 --- a/labelCloud/tests/unit/segmentation_handler/test_numpy_segmentation_handler.py +++ b/labelCloud/tests/unit/segmentation_handler/test_numpy_segmentation_handler.py @@ -66,7 +66,6 @@ def test_create_labels(handler: NumpySegmentationHandler) -> None: def test_write_labels(handler: NumpySegmentationHandler) -> None: - labels = np.random.randint(low=0, high=4, size=(420,), dtype=np.int8) with tempfile.TemporaryDirectory() as tempdir: label_path = Path(tempdir) / Path("foo.bin") diff --git a/labelCloud/view/startup_dialog.py b/labelCloud/view/startup_dialog.py index f6bffc1..8764823 100644 --- a/labelCloud/view/startup_dialog.py +++ b/labelCloud/view/startup_dialog.py @@ -34,7 +34,6 @@ def validate(self, a0: str, a1: int) -> Tuple["QValidator.State", str, int]: class StartupDialog(QDialog): - NAME_VALIDATOR = LabelNameValidator() def __init__(self, parent=None) -> None: @@ -237,7 +236,6 @@ def delete_label(self, delete_button: QPushButton) -> None: def save_class_labels(self) -> None: classes = [] for i in range(self.nb_of_labels): - row: QHBoxLayout = self.class_labels.itemAt(i) # type: ignore class_id = int(row.itemAt(0).widget().text()) # type: ignore class_name = row.itemAt(1).widget().text() # type: ignore diff --git a/requirements.txt b/requirements.txt index 54d8a4a..e362055 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pytest~=7.1.2 pytest-qt~=4.1.0 # Development -black~=22.3.0 +black~=23.1.0 mypy~=0.971 PyQt5-stubs~=5.15.6 types-setuptools~=57.4.17 From 4c8da7774c47237dfcd159b6c64a4012f583bf8a Mon Sep 17 00:00:00 2001 From: Ching-yu Lin <60384727+chingyulin@users.noreply.github.com> Date: Wed, 22 Mar 2023 20:27:34 +0000 Subject: [PATCH 3/5] feat: filter segmentation class --- labelCloud/control/controller.py | 4 + labelCloud/control/pcd_manager.py | 60 ++++++++++++- labelCloud/model/point_cloud.py | 46 +++++++++- labelCloud/resources/interfaces/interface.ui | 91 ++++++++++++++++++++ labelCloud/view/gui.py | 8 +- labelCloud/view/startup/color_button.py | 5 +- 6 files changed, 205 insertions(+), 9 deletions(-) diff --git a/labelCloud/control/controller.py b/labelCloud/control/controller.py index 8fa96f3..ef6784f 100644 --- a/labelCloud/control/controller.py +++ b/labelCloud/control/controller.py @@ -52,12 +52,16 @@ def startup(self, view: "GUI") -> None: # Read labels from folders self.pcd_manager.read_pointcloud_folder() self.next_pcd(save=False) + if LabelConfig().type == LabelingMode.SEMANTIC_SEGMENTATION: + self.pcd_manager.populate_segmentation_list() def loop_gui(self) -> None: """Function collection called during each event loop iteration.""" self.set_crosshair() self.set_selected_side() self.view.gl_widget.updateGL() + if LabelConfig().type == LabelingMode.SEMANTIC_SEGMENTATION: + self.pcd_manager.loop_seg_list_check_state() # POINT CLOUD METHODS def next_pcd(self, save: bool = True) -> None: diff --git a/labelCloud/control/pcd_manager.py b/labelCloud/control/pcd_manager.py index c0093ed..0e3ddea 100644 --- a/labelCloud/control/pcd_manager.py +++ b/labelCloud/control/pcd_manager.py @@ -10,12 +10,17 @@ import numpy as np import open3d as o3d import pkg_resources +from PyQt5 import QtCore +from PyQt5.QtWidgets import QCheckBox, QLabel -from ..definitions import LabelingMode, Point3D +from ..definitions import LabelingMode +from ..definitions.types import Point3D from ..io.labels.config import LabelConfig from ..io.pointclouds import BasePointCloudHandler, Open3DHandler from ..model import BBox, Perspective, PointCloud +from ..utils.color import rgb_to_hex from ..utils.logger import blue, green, print_column +from ..view.startup.color_button import ColorButton from .config_manager import config from .label_manager import LabelManager @@ -263,7 +268,8 @@ def rotate_pointcloud( def assign_point_label_in_box(self, box: BBox) -> None: assert self.pointcloud is not None - points = self.pointcloud.points + points = self.pointcloud.points.copy() + points[~self.pointcloud.visible] = np.finfo(np.float32).max points_inside = box.is_inside(points) # Relabel the points if its inside the box @@ -306,3 +312,53 @@ def update_pcd_infos(self, pointcloud_label: Optional[str] = None) -> None: else: self.view.button_next_pcd.setEnabled(True) self.view.button_prev_pcd.setEnabled(True) + + def populate_segmentation_list(self) -> None: + assert self.pointcloud is not None + assert self.pointcloud.labels is not None + self.seg_list_label: List[QLabel] = [] + self.seg_list_check_box: List[QCheckBox] = [] + self.seg_list_check_state: List[QtCore.Qt.CheckState] = [] + for idx, label_class in enumerate(LabelConfig().classes, start=1): + self.seg_list_label.append(QLabel(label_class.name)) + check_box = QCheckBox() + check_box.setCheckState(QtCore.Qt.Checked) + color_button = ColorButton( + color=rgb_to_hex(label_class.color), changeable=False + ) + self.seg_list_check_box.append(check_box) + self.seg_list_check_state.append(self.seg_list_check_box[-1].checkState()) + self.view.segmentation_list.addWidget(self.seg_list_label[-1], idx, 0) + self.view.segmentation_list.addWidget(color_button, idx, 1) + self.view.segmentation_list.addWidget(self.seg_list_check_box[-1], idx, 2) + + def loop_seg_list_check_state(self): + curr_checked_status = self.seg_list_check_state.copy() + any_changed = False + + move_back = [] + move_away = [] + for idx, (box, prev_status) in enumerate( + zip( + self.seg_list_check_box, + self.seg_list_check_state, + ) + ): + if box.checkState() != prev_status: + any_changed = True + curr_checked_status[idx] = box.checkState() + changed_id = LabelConfig().classes[idx].id + if box.checkState() == QtCore.Qt.Checked: + move_back.append(changed_id) + else: + move_away.append(changed_id) + + if any_changed: + points_move_away = ( + np.isin(self.pointcloud.labels, move_away) if move_away else None + ) + points_move_back = ( + np.isin(self.pointcloud.labels, move_back) if move_back else None + ) + self.pointcloud.update_position_vbo(points_move_away, points_move_back) + self.seg_list_check_state = curr_checked_status diff --git a/labelCloud/model/point_cloud.py b/labelCloud/model/point_cloud.py index 7f0db60..097d508 100644 --- a/labelCloud/model/point_cloud.py +++ b/labelCloud/model/point_cloud.py @@ -65,6 +65,8 @@ def __init__( self.validate_segmentation_label() self.mix_ratio = config.getfloat("POINTCLOUD", "label_color_mix_ratio") + self.visible = np.ones((self.points.shape[0],), dtype=np.bool_) + self.vbo = None self.center: Point3D = tuple(np.sum(points[:, i]) / len(points) for i in range(3)) # type: ignore self.pcd_mins: npt.NDArray[np.float32] = np.amin(points, axis=0) @@ -233,11 +235,51 @@ def color_with_label(self) -> bool: def has_label(self) -> bool: return self.labels is not None + def update_position_vbo( + self, + points_move_away: Optional[npt.NDArray[np.bool_]], + points_move_back: Optional[npt.NDArray[np.bool_]], + ): + GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.position_vbo) + # Move points to super far away + if points_move_away is not None and np.any(points_move_away): + move_away_idxs = np.where(points_move_away)[0] + self.visible[move_away_idxs] = False + arrays = consecutive(move_away_idxs) + stride = self.points.shape[1] * SIZE_OF_FLOAT + for arr in arrays: + super_far: npt.NDArray[np.float32] = ( + np.ones((arr.shape[0], 3), dtype=np.float32) + * np.finfo(np.float32).max + ) + # partially update label_vbo from positions arr[0] to arr[-1] + GL.glBufferSubData( + GL.GL_ARRAY_BUFFER, + offset=arr[0] * stride, + size=super_far.nbytes, + data=super_far, + ) + # Move points back + if points_move_back is not None and np.any(points_move_back): + move_back_idxs = np.where(points_move_back)[0] + self.visible[move_back_idxs] = True + arrays = consecutive(move_back_idxs) + stride = self.points.shape[1] * SIZE_OF_FLOAT + + for arr in arrays: + points: npt.NDArray[np.float32] = self.points[arr] + GL.glBufferSubData( + GL.GL_ARRAY_BUFFER, + offset=arr[0] * stride, + size=points.nbytes, + data=points, + ) + def update_selected_points_in_label_vbo( self, points_inside: npt.NDArray[np.bool_] ) -> None: - """Send the selected updated label colors to label vbo. This function - assumes the `self.label_colors[points_inside]` have been altered. + """Send the selected updated label colors to `self.label_vbo`. This + function assumes the `self.label_colors[points_inside]` have been altered. This function only partially updates the label vbo to minimise the data sent to gpu. It leverages `glBufferSubData` method to perform partial update and `consecutive` method to find consecutive indexes diff --git a/labelCloud/resources/interfaces/interface.ui b/labelCloud/resources/interfaces/interface.ui index 3f965c5..d8e181a 100644 --- a/labelCloud/resources/interfaces/interface.ui +++ b/labelCloud/resources/interfaces/interface.ui @@ -1460,6 +1460,97 @@ + + + + + DejaVu Sans,Arial + 75 + true + + + + Segmentation Controls + + + + QLayout::SetDefaultConstraint + + + 0 + + + 6 + + + + + + 0 + 0 + + + + + DejaVu Sans,Arial + 12 + 50 + true + + + + Class Name + + + + + + + + 0 + 0 + + + + + DejaVu Sans,Arial + 12 + 50 + true + + + + Color + + + + + + + + 0 + 0 + + + + + DejaVu Sans,Arial + 12 + 50 + true + + + + Visible + + + + + + + + + diff --git a/labelCloud/view/gui.py b/labelCloud/view/gui.py index 631b70a..b5bbb4d 100644 --- a/labelCloud/view/gui.py +++ b/labelCloud/view/gui.py @@ -20,6 +20,8 @@ QMessageBox, ) +from labelCloud.view.startup.dialog import StartupDialog + from ..control.config_manager import config from ..definitions import Color3f, LabelingMode from ..io.labels.config import LabelConfig @@ -194,6 +196,8 @@ def __init__(self, control: "Controller") -> None: self.button_save_label: QtWidgets.QPushButton # RIGHT PANEL + self.segmentation_list_group: QtWidgets.QGroupBox + self.segmentation_list: QtWidgets.QGridLayout self.label_list: QtWidgets.QListWidget self.current_class_dropdown: QtWidgets.QComboBox self.button_deselect_label: QtWidgets.QPushButton @@ -257,6 +261,7 @@ def __init__(self, control: "Controller") -> None: if LabelConfig().type == LabelingMode.OBJECT_DETECTION: self.button_assign_label.setVisible(False) self.act_color_with_label.setVisible(False) + self.segmentation_list_group.setVisible(False) # Connect with controller self.controller.startup(self) @@ -521,9 +526,6 @@ def init_progress(self, min_value, max_value): def update_progress(self, value) -> None: self.progressbar_pcds.setValue(value) - def update_current_class_dropdown(self) -> None: - self.controller.pcd_manager.populate_class_dropdown() - def update_bbox_stats(self, bbox) -> None: viewing_precision = config.getint("USER_INTERFACE", "viewing_precision") if bbox and not self.line_edited_activated(): diff --git a/labelCloud/view/startup/color_button.py b/labelCloud/view/startup/color_button.py index d1d6bae..9170038 100644 --- a/labelCloud/view/startup/color_button.py +++ b/labelCloud/view/startup/color_button.py @@ -14,12 +14,13 @@ class ColorButton(QtWidgets.QPushButton): colorChanged = pyqtSignal(object) - def __init__(self, *args, color="#FF0000", **kwargs): + def __init__(self, *args, color="#FF0000", changeable: bool = True, **kwargs): super(ColorButton, self).__init__(*args, **kwargs) self._color = None self._default = color - self.pressed.connect(self.onColorPicker) + if changeable: + self.pressed.connect(self.onColorPicker) # Set the initial/default state. self.setColor(self._default) From 0e757d8352ceb9c6c6443dd7db8c878670b97b5f Mon Sep 17 00:00:00 2001 From: Ching-yu Lin <60384727+chingyulin@users.noreply.github.com> Date: Wed, 22 Mar 2023 20:53:38 +0000 Subject: [PATCH 4/5] update black --- labelCloud/view/startup_dialog.py | 246 ++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 labelCloud/view/startup_dialog.py diff --git a/labelCloud/view/startup_dialog.py b/labelCloud/view/startup_dialog.py new file mode 100644 index 0000000..8764823 --- /dev/null +++ b/labelCloud/view/startup_dialog.py @@ -0,0 +1,246 @@ +import random +from typing import List, Optional, Tuple + +import pkg_resources +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIcon, QPixmap, QValidator +from PyQt5.QtWidgets import ( + QButtonGroup, + QDesktopWidget, + QDialog, + QDialogButtonBox, + QHBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QScrollArea, + QSizePolicy, + QSpinBox, + QVBoxLayout, + QWidget, +) + +from ..definitions.types import LabelingMode +from ..io.labels.config import ClassConfig, LabelConfig +from ..utils.color import get_distinct_colors, hex_to_rgb, rgb_to_hex +from ..view.color_button import ColorButton + + +class LabelNameValidator(QValidator): + def validate(self, a0: str, a1: int) -> Tuple["QValidator.State", str, int]: + if a0 != "": + return (QValidator.Acceptable, a0, a1) + return (QValidator.Invalid, a0, a1) + + +class StartupDialog(QDialog): + NAME_VALIDATOR = LabelNameValidator() + + def __init__(self, parent=None) -> None: + super().__init__(parent) + self.parent_gui = parent + + self.setWindowTitle("Welcome to labelCloud") + screen_size = QDesktopWidget().availableGeometry(self).size() + self.resize(screen_size * 0.5) + self.setWindowIcon( + QIcon( + pkg_resources.resource_filename( + "labelCloud.resources.icons", "labelCloud.ico" + ) + ) + ) + self.setContentsMargins(50, 10, 50, 10) + + self.colors: List[str] = [] + + main_layout = QVBoxLayout() + main_layout.setSpacing(15) + main_layout.setAlignment(Qt.AlignTop) + self.setLayout(main_layout) + + # 1. Row: Selection of labeling mode via checkable buttons + self.button_semantic_segmentation: QPushButton + self.add_labeling_mode_row(main_layout) + + # 2. Row: Definition of class labels + self.add_class_definition_rows(main_layout) + + # 3. Row: Addition of new class labels + self.button_add_label = QPushButton(text="Add new label") + self.button_add_label.clicked.connect( + lambda: self.add_label(id=self.next_label_id) + ) + self.delete_buttons.buttonClicked.connect(self.delete_label) + main_layout.addWidget(self.button_add_label) + + # 4. Row: Buttons to save or cancel + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Save) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + main_layout.addWidget(self.buttonBox) + + # ---------------------------------------------------------------------------- # + # SETUP # + # ---------------------------------------------------------------------------- # + + def add_labeling_mode_row(self, parent_layout: QVBoxLayout) -> None: + """ + Add a row to the dialog to select the labeling mode with two exclusive buttons. + """ + parent_layout.addWidget(QLabel("Select labeling mode:")) + + row_buttons = QHBoxLayout() + + self.button_object_detection = QPushButton( + text=LabelingMode.OBJECT_DETECTION.title().replace("_", " ") + ) + self.button_object_detection.setCheckable(True) + self.button_object_detection.setToolTip( + "This will result in a label file for each point cloud\n" + "with a bounding box for each annotated object." + ) + row_buttons.addWidget(self.button_object_detection) + + self.button_semantic_segmentation = QPushButton( + text=LabelingMode.SEMANTIC_SEGMENTATION.title().replace("_", " ") + ) + self.button_semantic_segmentation.setCheckable(True) + self.button_semantic_segmentation.setToolTip( + "This will result in a *.bin file for each point cloud\n" + "with a label for each annotated point of an object." + ) + row_buttons.addWidget(self.button_semantic_segmentation) + + parent_layout.addLayout(row_buttons) + + # Click callbacks to switch between the two modes + def select_object_detection(): + self.button_object_detection.setChecked(True) + self.button_semantic_segmentation.setChecked(False) + + self.button_object_detection.clicked.connect(select_object_detection) + + def select_semantic_segmentation(): + self.button_semantic_segmentation.setChecked(True) + self.button_object_detection.setChecked(False) + + self.button_semantic_segmentation.clicked.connect(select_semantic_segmentation) + + def add_class_definition_rows(self, parent_layout: QVBoxLayout) -> None: + scroll_area = QScrollArea() + widget = QWidget() + self.class_labels = QVBoxLayout() + self.class_labels.addStretch() + + widget.setLayout(self.class_labels) + self.delete_buttons = QButtonGroup() + + for class_label in LabelConfig().classes: + self.add_label( + class_label.id, class_label.name, rgb_to_hex(class_label.color) + ) + + parent_layout.addWidget(QLabel("Change class labels:")) + + scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + scroll_area.setWidgetResizable(True) + scroll_area.setWidget(widget) + scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + parent_layout.addWidget(scroll_area) + # Load annotation mode + if LabelConfig().type == LabelingMode.OBJECT_DETECTION: + self.button_object_detection.setChecked(True) + else: + self.button_semantic_segmentation.setChecked(True) + + # ---------------------------------------------------------------------------- # + # PROPERTIES # + # ---------------------------------------------------------------------------- # + + @property + def get_labeling_mode(self) -> LabelingMode: + if self.button_object_detection.isChecked(): + return LabelingMode.OBJECT_DETECTION + return LabelingMode.SEMANTIC_SEGMENTATION + + @property + def nb_of_labels(self) -> int: + return len(self.class_labels.children()) + + @property + def next_label_id(self) -> int: + max_class_id = 0 + for i in range(self.nb_of_labels): + label_id = int(self.class_labels.itemAt(i).itemAt(0).widget().text()) # type: ignore + max_class_id = max(max_class_id, label_id) + return max_class_id + 1 + + @property + def distinct_color(self) -> str: + if not self.colors: + self.colors = get_distinct_colors(25) + random.shuffle(self.colors) + return self.colors.pop() + + # ---------------------------------------------------------------------------- # + # LOGIC # + # ---------------------------------------------------------------------------- # + + def add_label( + self, id: int, name: Optional[str] = None, hex_color: Optional[str] = None + ) -> None: + row_label = QHBoxLayout() + row_label.setSpacing(15) + + label_id = QSpinBox() + label_id.setMinimum(0) + label_id.setMaximum(255) + label_id.setValue(id) + row_label.addWidget(label_id) + + label_name = QLineEdit(name or f"label_{id}") + label_name.setValidator(self.NAME_VALIDATOR) + row_label.addWidget(label_name, stretch=2) + + label_color = ColorButton(color=hex_color or self.distinct_color) + row_label.addWidget(label_color) + + label_delete = QPushButton( + icon=QIcon( + QPixmap( + pkg_resources.resource_filename( + "labelCloud.resources.icons", "delete-outline.svg" + ) + ) + ), + text="", + ) + self.delete_buttons.addButton(label_delete) + row_label.addWidget(label_delete) + + self.class_labels.insertLayout(self.nb_of_labels, row_label) + + def delete_label(self, delete_button: QPushButton) -> None: + row_label: QHBoxLayout + for row_index, row_label in enumerate(self.class_labels.children()): # type: ignore + if row_label.itemAt(3).widget() == delete_button: + for _ in range(row_label.count()): + row_label.removeWidget(row_label.itemAt(0).widget()) + break + + self.class_labels.removeItem(self.class_labels.itemAt(row_index)) # type: ignore + + def save_class_labels(self) -> None: + classes = [] + for i in range(self.nb_of_labels): + row: QHBoxLayout = self.class_labels.itemAt(i) # type: ignore + class_id = int(row.itemAt(0).widget().text()) # type: ignore + class_name = row.itemAt(1).widget().text() # type: ignore + class_color = hex_to_rgb(row.itemAt(2).widget().color()) # type: ignore + classes.append(ClassConfig(id=class_id, name=class_name, color=class_color)) + LabelConfig().classes = classes + LabelConfig().type = self.get_labeling_mode + LabelConfig().save_config() From a88c288505b688824fd087f1b8778170e6ed2fd0 Mon Sep 17 00:00:00 2001 From: Ching-yu Lin <60384727+chingyulin@users.noreply.github.com> Date: Thu, 23 Mar 2023 19:05:17 +0000 Subject: [PATCH 5/5] repath LabelingMode --- labelCloud/view/startup_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labelCloud/view/startup_dialog.py b/labelCloud/view/startup_dialog.py index 8764823..a75bdbc 100644 --- a/labelCloud/view/startup_dialog.py +++ b/labelCloud/view/startup_dialog.py @@ -20,7 +20,7 @@ QWidget, ) -from ..definitions.types import LabelingMode +from ..definitions.labeling_mode import LabelingMode from ..io.labels.config import ClassConfig, LabelConfig from ..utils.color import get_distinct_colors, hex_to_rgb, rgb_to_hex from ..view.color_button import ColorButton