lazylabel-gui 1.1.1__py3-none-any.whl → 1.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lazylabel/__init__.py +1 -1
- lazylabel/config/__init__.py +3 -3
- lazylabel/config/hotkeys.py +96 -58
- lazylabel/config/paths.py +8 -9
- lazylabel/config/settings.py +15 -16
- lazylabel/core/__init__.py +3 -3
- lazylabel/core/file_manager.py +49 -33
- lazylabel/core/model_manager.py +9 -11
- lazylabel/core/segment_manager.py +21 -22
- lazylabel/main.py +1 -0
- lazylabel/models/__init__.py +1 -1
- lazylabel/models/sam_model.py +24 -19
- lazylabel/ui/__init__.py +3 -3
- lazylabel/ui/control_panel.py +21 -19
- lazylabel/ui/editable_vertex.py +16 -3
- lazylabel/ui/hotkey_dialog.py +125 -93
- lazylabel/ui/hoverable_polygon_item.py +1 -2
- lazylabel/ui/main_window.py +290 -49
- lazylabel/ui/photo_viewer.py +4 -7
- lazylabel/ui/reorderable_class_table.py +2 -3
- lazylabel/ui/right_panel.py +15 -16
- lazylabel/ui/widgets/__init__.py +1 -1
- lazylabel/ui/widgets/adjustments_widget.py +22 -21
- lazylabel/ui/widgets/model_selection_widget.py +28 -21
- lazylabel/ui/widgets/settings_widget.py +35 -28
- lazylabel/ui/widgets/status_bar.py +2 -2
- lazylabel/utils/__init__.py +2 -2
- lazylabel/utils/custom_file_system_model.py +3 -2
- {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.3.dist-info}/METADATA +48 -2
- lazylabel_gui-1.1.3.dist-info/RECORD +37 -0
- lazylabel_gui-1.1.1.dist-info/RECORD +0 -37
- {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.3.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.3.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.3.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.3.dist-info}/top_level.txt +0 -0
@@ -1,26 +1,26 @@
|
|
1
1
|
"""Adjustments widget for sliders and controls."""
|
2
2
|
|
3
|
-
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QSlider, QGroupBox
|
4
3
|
from PyQt6.QtCore import Qt, pyqtSignal
|
4
|
+
from PyQt6.QtWidgets import QGroupBox, QLabel, QSlider, QVBoxLayout, QWidget
|
5
5
|
|
6
6
|
|
7
7
|
class AdjustmentsWidget(QWidget):
|
8
8
|
"""Widget for adjustment controls."""
|
9
|
-
|
9
|
+
|
10
10
|
annotation_size_changed = pyqtSignal(int)
|
11
11
|
pan_speed_changed = pyqtSignal(int)
|
12
12
|
join_threshold_changed = pyqtSignal(int)
|
13
|
-
|
13
|
+
|
14
14
|
def __init__(self, parent=None):
|
15
15
|
super().__init__(parent)
|
16
16
|
self._setup_ui()
|
17
17
|
self._connect_signals()
|
18
|
-
|
18
|
+
|
19
19
|
def _setup_ui(self):
|
20
20
|
"""Setup the UI layout."""
|
21
21
|
group = QGroupBox("Adjustments")
|
22
22
|
layout = QVBoxLayout(group)
|
23
|
-
|
23
|
+
|
24
24
|
# Annotation size
|
25
25
|
self.size_label = QLabel("Annotation Size: 1.0x")
|
26
26
|
self.size_slider = QSlider(Qt.Orientation.Horizontal)
|
@@ -29,9 +29,9 @@ class AdjustmentsWidget(QWidget):
|
|
29
29
|
self.size_slider.setToolTip("Adjusts the size of points and lines (Ctrl +/-)")
|
30
30
|
layout.addWidget(self.size_label)
|
31
31
|
layout.addWidget(self.size_slider)
|
32
|
-
|
32
|
+
|
33
33
|
layout.addSpacing(10)
|
34
|
-
|
34
|
+
|
35
35
|
# Pan speed
|
36
36
|
self.pan_label = QLabel("Pan Speed: 1.0x")
|
37
37
|
self.pan_slider = QSlider(Qt.Orientation.Horizontal)
|
@@ -42,9 +42,9 @@ class AdjustmentsWidget(QWidget):
|
|
42
42
|
)
|
43
43
|
layout.addWidget(self.pan_label)
|
44
44
|
layout.addWidget(self.pan_slider)
|
45
|
-
|
45
|
+
|
46
46
|
layout.addSpacing(10)
|
47
|
-
|
47
|
+
|
48
48
|
# Polygon join threshold
|
49
49
|
self.join_label = QLabel("Polygon Join Distance: 2px")
|
50
50
|
self.join_slider = QSlider(Qt.Orientation.Horizontal)
|
@@ -53,55 +53,56 @@ class AdjustmentsWidget(QWidget):
|
|
53
53
|
self.join_slider.setToolTip("The pixel distance to 'snap' a polygon closed.")
|
54
54
|
layout.addWidget(self.join_label)
|
55
55
|
layout.addWidget(self.join_slider)
|
56
|
-
|
56
|
+
|
57
57
|
# Main layout
|
58
58
|
main_layout = QVBoxLayout(self)
|
59
59
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
60
60
|
main_layout.addWidget(group)
|
61
|
-
|
61
|
+
|
62
62
|
def _connect_signals(self):
|
63
63
|
"""Connect internal signals."""
|
64
64
|
self.size_slider.valueChanged.connect(self._on_size_changed)
|
65
65
|
self.pan_slider.valueChanged.connect(self._on_pan_changed)
|
66
66
|
self.join_slider.valueChanged.connect(self._on_join_changed)
|
67
|
-
|
67
|
+
|
68
68
|
def _on_size_changed(self, value):
|
69
69
|
"""Handle annotation size change."""
|
70
70
|
multiplier = value / 10.0
|
71
71
|
self.size_label.setText(f"Annotation Size: {multiplier:.1f}x")
|
72
72
|
self.annotation_size_changed.emit(value)
|
73
|
-
|
73
|
+
|
74
74
|
def _on_pan_changed(self, value):
|
75
75
|
"""Handle pan speed change."""
|
76
76
|
multiplier = value / 10.0
|
77
77
|
self.pan_label.setText(f"Pan Speed: {multiplier:.1f}x")
|
78
78
|
self.pan_speed_changed.emit(value)
|
79
|
-
|
79
|
+
|
80
80
|
def _on_join_changed(self, value):
|
81
81
|
"""Handle join threshold change."""
|
82
82
|
self.join_label.setText(f"Polygon Join Distance: {value}px")
|
83
83
|
self.join_threshold_changed.emit(value)
|
84
|
-
|
84
|
+
|
85
85
|
def get_annotation_size(self):
|
86
86
|
"""Get current annotation size value."""
|
87
87
|
return self.size_slider.value()
|
88
|
-
|
88
|
+
|
89
89
|
def set_annotation_size(self, value):
|
90
90
|
"""Set annotation size value."""
|
91
91
|
self.size_slider.setValue(value)
|
92
|
-
|
92
|
+
|
93
93
|
def get_pan_speed(self):
|
94
94
|
"""Get current pan speed value."""
|
95
95
|
return self.pan_slider.value()
|
96
|
-
|
96
|
+
|
97
97
|
def set_pan_speed(self, value):
|
98
98
|
"""Set pan speed value."""
|
99
99
|
self.pan_slider.setValue(value)
|
100
|
-
|
100
|
+
|
101
101
|
def get_join_threshold(self):
|
102
102
|
"""Get current join threshold value."""
|
103
103
|
return self.join_slider.value()
|
104
|
-
|
104
|
+
|
105
105
|
def set_join_threshold(self, value):
|
106
106
|
"""Set join threshold value."""
|
107
|
-
self.join_slider.setValue(value)
|
107
|
+
self.join_slider.setValue(value)
|
108
|
+
self.join_label.setText(f"Polygon Join Distance: {value}px")
|
@@ -1,94 +1,101 @@
|
|
1
1
|
"""Model selection widget."""
|
2
2
|
|
3
|
-
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QComboBox, QGroupBox
|
4
3
|
from PyQt6.QtCore import pyqtSignal
|
5
|
-
from
|
4
|
+
from PyQt6.QtWidgets import (
|
5
|
+
QComboBox,
|
6
|
+
QGroupBox,
|
7
|
+
QHBoxLayout,
|
8
|
+
QLabel,
|
9
|
+
QPushButton,
|
10
|
+
QVBoxLayout,
|
11
|
+
QWidget,
|
12
|
+
)
|
6
13
|
|
7
14
|
|
8
15
|
class ModelSelectionWidget(QWidget):
|
9
16
|
"""Widget for model selection and management."""
|
10
|
-
|
17
|
+
|
11
18
|
browse_requested = pyqtSignal()
|
12
19
|
refresh_requested = pyqtSignal()
|
13
20
|
model_selected = pyqtSignal(str)
|
14
|
-
|
21
|
+
|
15
22
|
def __init__(self, parent=None):
|
16
23
|
super().__init__(parent)
|
17
24
|
self._setup_ui()
|
18
25
|
self._connect_signals()
|
19
|
-
|
26
|
+
|
20
27
|
def _setup_ui(self):
|
21
28
|
"""Setup the UI layout."""
|
22
29
|
group = QGroupBox("Model Selection")
|
23
30
|
layout = QVBoxLayout(group)
|
24
|
-
|
31
|
+
|
25
32
|
# Buttons
|
26
33
|
button_layout = QHBoxLayout()
|
27
34
|
self.btn_browse = QPushButton("Browse Models")
|
28
35
|
self.btn_browse.setToolTip("Browse for a folder containing .pth model files")
|
29
36
|
self.btn_refresh = QPushButton("Refresh")
|
30
37
|
self.btn_refresh.setToolTip("Refresh the list of available models")
|
31
|
-
|
38
|
+
|
32
39
|
button_layout.addWidget(self.btn_browse)
|
33
40
|
button_layout.addWidget(self.btn_refresh)
|
34
41
|
layout.addLayout(button_layout)
|
35
|
-
|
42
|
+
|
36
43
|
# Model combo
|
37
44
|
layout.addWidget(QLabel("Available Models:"))
|
38
45
|
self.model_combo = QComboBox()
|
39
46
|
self.model_combo.setToolTip("Select a .pth model file to use")
|
40
47
|
self.model_combo.addItem("Default (vit_h)")
|
41
48
|
layout.addWidget(self.model_combo)
|
42
|
-
|
49
|
+
|
43
50
|
# Current model label
|
44
51
|
self.current_model_label = QLabel("Current: Default SAM Model")
|
45
52
|
self.current_model_label.setWordWrap(True)
|
46
53
|
self.current_model_label.setStyleSheet("color: #90EE90; font-style: italic;")
|
47
54
|
layout.addWidget(self.current_model_label)
|
48
|
-
|
55
|
+
|
49
56
|
# Main layout
|
50
57
|
main_layout = QVBoxLayout(self)
|
51
58
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
52
59
|
main_layout.addWidget(group)
|
53
|
-
|
60
|
+
|
54
61
|
def _connect_signals(self):
|
55
62
|
"""Connect internal signals."""
|
56
63
|
self.btn_browse.clicked.connect(self.browse_requested)
|
57
64
|
self.btn_refresh.clicked.connect(self.refresh_requested)
|
58
65
|
self.model_combo.currentTextChanged.connect(self.model_selected)
|
59
|
-
|
60
|
-
def populate_models(self, models:
|
66
|
+
|
67
|
+
def populate_models(self, models: list[tuple[str, str]]):
|
61
68
|
"""Populate the models combo box.
|
62
|
-
|
69
|
+
|
63
70
|
Args:
|
64
71
|
models: List of (display_name, full_path) tuples
|
65
72
|
"""
|
66
73
|
self.model_combo.blockSignals(True)
|
67
74
|
self.model_combo.clear()
|
68
|
-
|
75
|
+
|
69
76
|
# Add default option
|
70
77
|
self.model_combo.addItem("Default (vit_h)")
|
71
|
-
|
78
|
+
|
72
79
|
# Add custom models
|
73
80
|
for display_name, full_path in models:
|
74
81
|
self.model_combo.addItem(display_name, full_path)
|
75
|
-
|
82
|
+
|
76
83
|
self.model_combo.blockSignals(False)
|
77
|
-
|
84
|
+
|
78
85
|
def set_current_model(self, model_name: str):
|
79
86
|
"""Set the current model display."""
|
80
87
|
self.current_model_label.setText(model_name)
|
81
|
-
|
88
|
+
|
82
89
|
def get_selected_model_path(self) -> str:
|
83
90
|
"""Get the path of the currently selected model."""
|
84
91
|
current_index = self.model_combo.currentIndex()
|
85
92
|
if current_index <= 0: # Default option
|
86
93
|
return ""
|
87
94
|
return self.model_combo.itemData(current_index) or ""
|
88
|
-
|
95
|
+
|
89
96
|
def reset_to_default(self):
|
90
97
|
"""Reset selection to default model."""
|
91
98
|
self.model_combo.blockSignals(True)
|
92
99
|
self.model_combo.setCurrentIndex(0)
|
93
100
|
self.model_combo.blockSignals(False)
|
94
|
-
self.set_current_model("Current: Default SAM Model")
|
101
|
+
self.set_current_model("Current: Default SAM Model")
|
@@ -1,24 +1,24 @@
|
|
1
1
|
"""Settings widget for save options."""
|
2
2
|
|
3
|
-
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QCheckBox, QGroupBox
|
4
3
|
from PyQt6.QtCore import pyqtSignal
|
4
|
+
from PyQt6.QtWidgets import QCheckBox, QGroupBox, QVBoxLayout, QWidget
|
5
5
|
|
6
6
|
|
7
7
|
class SettingsWidget(QWidget):
|
8
8
|
"""Widget for application settings."""
|
9
|
-
|
9
|
+
|
10
10
|
settings_changed = pyqtSignal()
|
11
|
-
|
11
|
+
|
12
12
|
def __init__(self, parent=None):
|
13
13
|
super().__init__(parent)
|
14
14
|
self._setup_ui()
|
15
15
|
self._connect_signals()
|
16
|
-
|
16
|
+
|
17
17
|
def _setup_ui(self):
|
18
18
|
"""Setup the UI layout."""
|
19
19
|
group = QGroupBox("Settings")
|
20
20
|
layout = QVBoxLayout(group)
|
21
|
-
|
21
|
+
|
22
22
|
# Auto-save
|
23
23
|
self.chk_auto_save = QCheckBox("Auto-Save on Navigate")
|
24
24
|
self.chk_auto_save.setToolTip(
|
@@ -26,7 +26,7 @@ class SettingsWidget(QWidget):
|
|
26
26
|
)
|
27
27
|
self.chk_auto_save.setChecked(True)
|
28
28
|
layout.addWidget(self.chk_auto_save)
|
29
|
-
|
29
|
+
|
30
30
|
# Save NPZ
|
31
31
|
self.chk_save_npz = QCheckBox("Save .npz")
|
32
32
|
self.chk_save_npz.setChecked(True)
|
@@ -34,7 +34,7 @@ class SettingsWidget(QWidget):
|
|
34
34
|
"Save the final mask as a compressed NumPy NPZ file."
|
35
35
|
)
|
36
36
|
layout.addWidget(self.chk_save_npz)
|
37
|
-
|
37
|
+
|
38
38
|
# Save TXT
|
39
39
|
self.chk_save_txt = QCheckBox("Save .txt")
|
40
40
|
self.chk_save_txt.setChecked(True)
|
@@ -42,7 +42,7 @@ class SettingsWidget(QWidget):
|
|
42
42
|
"Save bounding box annotations in YOLO TXT format."
|
43
43
|
)
|
44
44
|
layout.addWidget(self.chk_save_txt)
|
45
|
-
|
45
|
+
|
46
46
|
# YOLO with aliases
|
47
47
|
self.chk_yolo_use_alias = QCheckBox("Save YOLO with Class Aliases")
|
48
48
|
self.chk_yolo_use_alias.setToolTip(
|
@@ -51,7 +51,7 @@ class SettingsWidget(QWidget):
|
|
51
51
|
)
|
52
52
|
self.chk_yolo_use_alias.setChecked(True)
|
53
53
|
layout.addWidget(self.chk_yolo_use_alias)
|
54
|
-
|
54
|
+
|
55
55
|
# Save class aliases
|
56
56
|
self.chk_save_class_aliases = QCheckBox("Save Class Aliases (.json)")
|
57
57
|
self.chk_save_class_aliases.setToolTip(
|
@@ -59,48 +59,55 @@ class SettingsWidget(QWidget):
|
|
59
59
|
)
|
60
60
|
self.chk_save_class_aliases.setChecked(False)
|
61
61
|
layout.addWidget(self.chk_save_class_aliases)
|
62
|
-
|
62
|
+
|
63
63
|
# Main layout
|
64
64
|
main_layout = QVBoxLayout(self)
|
65
65
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
66
66
|
main_layout.addWidget(group)
|
67
|
-
|
67
|
+
|
68
68
|
def _connect_signals(self):
|
69
69
|
"""Connect internal signals."""
|
70
70
|
self.chk_save_npz.stateChanged.connect(self._handle_save_checkbox_change)
|
71
71
|
self.chk_save_txt.stateChanged.connect(self._handle_save_checkbox_change)
|
72
|
-
|
72
|
+
|
73
73
|
# Connect all checkboxes to settings changed signal
|
74
|
-
for checkbox in [
|
75
|
-
|
74
|
+
for checkbox in [
|
75
|
+
self.chk_auto_save,
|
76
|
+
self.chk_save_npz,
|
77
|
+
self.chk_save_txt,
|
78
|
+
self.chk_yolo_use_alias,
|
79
|
+
self.chk_save_class_aliases,
|
80
|
+
]:
|
76
81
|
checkbox.stateChanged.connect(self.settings_changed)
|
77
|
-
|
82
|
+
|
78
83
|
def _handle_save_checkbox_change(self):
|
79
84
|
"""Ensure at least one save format is selected."""
|
80
85
|
is_npz_checked = self.chk_save_npz.isChecked()
|
81
86
|
is_txt_checked = self.chk_save_txt.isChecked()
|
82
|
-
|
87
|
+
|
83
88
|
if not is_npz_checked and not is_txt_checked:
|
84
89
|
sender = self.sender()
|
85
90
|
if sender == self.chk_save_npz:
|
86
91
|
self.chk_save_txt.setChecked(True)
|
87
92
|
else:
|
88
93
|
self.chk_save_npz.setChecked(True)
|
89
|
-
|
94
|
+
|
90
95
|
def get_settings(self):
|
91
96
|
"""Get current settings as dictionary."""
|
92
97
|
return {
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
+
"auto_save": self.chk_auto_save.isChecked(),
|
99
|
+
"save_npz": self.chk_save_npz.isChecked(),
|
100
|
+
"save_txt": self.chk_save_txt.isChecked(),
|
101
|
+
"yolo_use_alias": self.chk_yolo_use_alias.isChecked(),
|
102
|
+
"save_class_aliases": self.chk_save_class_aliases.isChecked(),
|
98
103
|
}
|
99
|
-
|
104
|
+
|
100
105
|
def set_settings(self, settings):
|
101
106
|
"""Set settings from dictionary."""
|
102
|
-
self.chk_auto_save.setChecked(settings.get(
|
103
|
-
self.chk_save_npz.setChecked(settings.get(
|
104
|
-
self.chk_save_txt.setChecked(settings.get(
|
105
|
-
self.chk_yolo_use_alias.setChecked(settings.get(
|
106
|
-
self.chk_save_class_aliases.setChecked(
|
107
|
+
self.chk_auto_save.setChecked(settings.get("auto_save", True))
|
108
|
+
self.chk_save_npz.setChecked(settings.get("save_npz", True))
|
109
|
+
self.chk_save_txt.setChecked(settings.get("save_txt", True))
|
110
|
+
self.chk_yolo_use_alias.setChecked(settings.get("yolo_use_alias", True))
|
111
|
+
self.chk_save_class_aliases.setChecked(
|
112
|
+
settings.get("save_class_aliases", False)
|
113
|
+
)
|
@@ -1,8 +1,8 @@
|
|
1
1
|
"""Status bar widget for displaying active messages."""
|
2
2
|
|
3
|
-
from PyQt6.
|
4
|
-
from PyQt6.QtCore import QTimer, pyqtSignal, Qt
|
3
|
+
from PyQt6.QtCore import Qt, QTimer
|
5
4
|
from PyQt6.QtGui import QFont
|
5
|
+
from PyQt6.QtWidgets import QLabel, QStatusBar
|
6
6
|
|
7
7
|
|
8
8
|
class StatusBar(QStatusBar):
|
lazylabel/utils/__init__.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Utility modules."""
|
2
2
|
|
3
|
-
from .utils import mask_to_pixmap
|
4
3
|
from .custom_file_system_model import CustomFileSystemModel
|
4
|
+
from .utils import mask_to_pixmap
|
5
5
|
|
6
|
-
__all__ = [
|
6
|
+
__all__ = ["mask_to_pixmap", "CustomFileSystemModel"]
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from pathlib import Path
|
2
|
-
|
3
|
-
from PyQt6.
|
2
|
+
|
3
|
+
from PyQt6.QtCore import QDir, QModelIndex, Qt
|
4
|
+
from PyQt6.QtGui import QBrush, QColor, QFileSystemModel
|
4
5
|
|
5
6
|
|
6
7
|
class CustomFileSystemModel(QFileSystemModel):
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: lazylabel-gui
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.3
|
4
4
|
Summary: An image segmentation GUI for generating ML ready mask tensors and annotations.
|
5
5
|
Author-email: "Deniz N. Cakan" <deniz.n.cakan@gmail.com>
|
6
6
|
License: MIT License
|
@@ -45,6 +45,12 @@ Requires-Dist: opencv-python>=4.11.0.86
|
|
45
45
|
Requires-Dist: scipy>=1.15.3
|
46
46
|
Requires-Dist: requests>=2.32.4
|
47
47
|
Requires-Dist: tqdm>=4.67.1
|
48
|
+
Provides-Extra: dev
|
49
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
50
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
51
|
+
Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
|
52
|
+
Requires-Dist: pytest-qt>=4.2.0; extra == "dev"
|
53
|
+
Requires-Dist: ruff>=0.8.0; extra == "dev"
|
48
54
|
Dynamic: license-file
|
49
55
|
|
50
56
|
# <img src="https://raw.githubusercontent.com/dnzckn/LazyLabel/main/src/lazylabel/demo_pictures/logo2.png" alt="LazyLabel Logo" style="height:60px; vertical-align:middle;" /> <img src="https://raw.githubusercontent.com/dnzckn/LazyLabel/main/src/lazylabel/demo_pictures/logo_black.png" alt="LazyLabel Cursive" style="height:60px; vertical-align:middle;" />
|
@@ -142,7 +148,8 @@ Inspired by [LabelMe](https://github.com/wkentaro/labelme?tab=readme-ov-file#ins
|
|
142
148
|
|---|---|
|
143
149
|
| `L-Click` | Add positive point (Point Mode) or polygon vertex. |
|
144
150
|
| `R-Click` | Add negative point (Point Mode). |
|
145
|
-
| `Ctrl + Z` | Undo last
|
151
|
+
| `Ctrl + Z` | Undo last action. |
|
152
|
+
| `Ctrl + Y` / `Ctrl + Shift + Z` | Redo last action. |
|
146
153
|
| `Spacebar` | Finalize and save current AI segment. |
|
147
154
|
| `Enter` | **Save final mask for the current image to a `.npz` file.** |
|
148
155
|
| `M` | **Merge** selected segments into a single class. |
|
@@ -191,6 +198,45 @@ LazyLabel includes a comprehensive hotkey management system:
|
|
191
198
|
|
192
199
|
For complete hotkey documentation, see [HOTKEY_FEATURE.md](src/lazylabel/HOTKEY_FEATURE.md).
|
193
200
|
|
201
|
+
## Development
|
202
|
+
|
203
|
+
### Code Quality
|
204
|
+
This project uses Ruff for linting and formatting:
|
205
|
+
|
206
|
+
```bash
|
207
|
+
# Activate virtual environment first
|
208
|
+
& e:\venv\lazylabel\Scripts\Activate.ps1
|
209
|
+
|
210
|
+
# Run linter
|
211
|
+
ruff check .
|
212
|
+
|
213
|
+
# Fix auto-fixable issues
|
214
|
+
ruff check --fix .
|
215
|
+
|
216
|
+
# Format code
|
217
|
+
ruff format .
|
218
|
+
|
219
|
+
# Check if code is properly formatted
|
220
|
+
ruff format --check .
|
221
|
+
```
|
222
|
+
|
223
|
+
### Testing
|
224
|
+
|
225
|
+
Run tests using pytest:
|
226
|
+
|
227
|
+
```bash
|
228
|
+
# Run all tests
|
229
|
+
python -m pytest
|
230
|
+
|
231
|
+
# Run tests with coverage
|
232
|
+
python -m pytest --cov=lazylabel --cov-report=html --cov-report=term-missing
|
233
|
+
|
234
|
+
# Run specific test file
|
235
|
+
python -m pytest tests/unit/ui/test_undo_redo.py -v
|
236
|
+
```
|
237
|
+
|
238
|
+
The HTML coverage report will be generated in `htmlcov/` directory.
|
239
|
+
|
194
240
|
---
|
195
241
|
|
196
242
|
## ☕ Support LazyLabel
|
@@ -0,0 +1,37 @@
|
|
1
|
+
lazylabel/__init__.py,sha256=0yitHZYNNuF4Rqj7cqE0TrfDV8zhzKD3A5W1vOymHK4,199
|
2
|
+
lazylabel/main.py,sha256=AQ-SKj8korOa2rKviFaA1kC-c03VPmVN2xU2qk36764,768
|
3
|
+
lazylabel/config/__init__.py,sha256=TnDXH0yx0zy97FfjpiVPHleMgMrM5YwWrUbvevSNJjg,263
|
4
|
+
lazylabel/config/hotkeys.py,sha256=Aq5pH2ED91A0wayQD58Vy0gjmajlV4-ra7Vd-IgZr38,8114
|
5
|
+
lazylabel/config/paths.py,sha256=ZVKbtaNOxmYO4l6JgsY-8DXaE_jaJfDg2RQJJn3-5nw,1275
|
6
|
+
lazylabel/config/settings.py,sha256=Ud27cSadJ7M4l30XN0KrxZ3ixAOPXTUhj-SvvanMjaI,1787
|
7
|
+
lazylabel/core/__init__.py,sha256=FmRjop_uIBSJwKMGhaZ-3Iwu34LkoxTuD-hnq5vbTSY,232
|
8
|
+
lazylabel/core/file_manager.py,sha256=Blo55jXW-uGVLrreGRmlHUtK2yTbyj3jsTKkfjD0z7k,4796
|
9
|
+
lazylabel/core/model_manager.py,sha256=P3IahI0xUk6-F7Y5U4r5RpnUQSelJqX2KBtcmUGNeNY,3368
|
10
|
+
lazylabel/core/segment_manager.py,sha256=h7ikOZXpV-JwutiWK9xYGWctTaNHJKMeWau9ANml1tY,5913
|
11
|
+
lazylabel/models/__init__.py,sha256=fIlk_0DuZfiClcm0XlZdimeHzunQwBmTMI4PcGsaymw,91
|
12
|
+
lazylabel/models/sam_model.py,sha256=89v99hpD0ngAAQKiuRyIib0E-5u9zTDJcubtmxxG-PM,7878
|
13
|
+
lazylabel/ui/__init__.py,sha256=4qDIh9y6tABPmD8MAMGZn_G7oSRyrcHt2HkjoWgbGH4,268
|
14
|
+
lazylabel/ui/control_panel.py,sha256=iyn6v-GvdSabd0Lk7n6S8p_svuTrqsG0jUOZBGhjqAc,8925
|
15
|
+
lazylabel/ui/editable_vertex.py,sha256=xzc5QdFJJXiy035C3HgR4ph-tuJu8245l-Ar7aBvVv8,2399
|
16
|
+
lazylabel/ui/hotkey_dialog.py,sha256=ZlZoPvY822ke8YkbCy22o2RUjxtkXSoEOg8eVETIKpQ,15118
|
17
|
+
lazylabel/ui/hoverable_pixelmap_item.py,sha256=kJFOp7WXiyHpNf7l73TZjiob85jgP30b5MZvu_z5L3c,728
|
18
|
+
lazylabel/ui/hoverable_polygon_item.py,sha256=aclUwd0P8H8xbcep6GwhnfaVs1zSkqeZKAL-xeDyMiU,1222
|
19
|
+
lazylabel/ui/main_window.py,sha256=BQh1OsHxZ_ukjcF4HzcJAAzLqNJENBBHZGpB2bYoXbo,75311
|
20
|
+
lazylabel/ui/numeric_table_widget_item.py,sha256=dQUlIFu9syCxTGAHVIlmbgkI7aJ3f3wmDPBz1AGK9Bg,283
|
21
|
+
lazylabel/ui/photo_viewer.py,sha256=4n2zSpFAnausCoIU-wwWDHDo6vsNp_RGIkhDyhuM2_A,1997
|
22
|
+
lazylabel/ui/reorderable_class_table.py,sha256=sxHhQre5O_MXLDFgKnw43QnvXXoqn5xRKMGitgO7muI,2371
|
23
|
+
lazylabel/ui/right_panel.py,sha256=F407BhQSNTKi_9IIqdZZgjSMiUmxOIcNO-7UtWWbbdk,12279
|
24
|
+
lazylabel/ui/widgets/__init__.py,sha256=bYjLRTqWdi4hcPfSSXmXuT-0c5Aee7HnnQurv_k5bfY,314
|
25
|
+
lazylabel/ui/widgets/adjustments_widget.py,sha256=xM43q1khpvRIHslPfI6bukzjx9J0mPgHUdNKCmp3rtM,3912
|
26
|
+
lazylabel/ui/widgets/model_selection_widget.py,sha256=kMPaBMfdfnEVH-Be1d5OxLEuioO0c04FZT1Hr904axM,3497
|
27
|
+
lazylabel/ui/widgets/settings_widget.py,sha256=2T7A5t1g9oIRx6KsxWk--Reuft9DYeqH1rRI9ME18HQ,4347
|
28
|
+
lazylabel/ui/widgets/status_bar.py,sha256=wTbMQNEOBfmtNj8EVFZS_lxgaemu-CbRXeZzEQDaVz8,4014
|
29
|
+
lazylabel/utils/__init__.py,sha256=V6IR5Gim-39HgM2NyTVT-n8gy3mjilCSFW9y0owN5nc,179
|
30
|
+
lazylabel/utils/custom_file_system_model.py,sha256=-3EimlybvevH6bvqBE0qdFnLADVtayylmkntxPXK0Bk,4869
|
31
|
+
lazylabel/utils/utils.py,sha256=sYSCoXL27OaLgOZaUkCAhgmKZ7YfhR3Cc5F8nDIa3Ig,414
|
32
|
+
lazylabel_gui-1.1.3.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
|
33
|
+
lazylabel_gui-1.1.3.dist-info/METADATA,sha256=i0c3l4ODbKROaIHXpX9y9kttfEbVOU1A3j7NoNONsmo,9506
|
34
|
+
lazylabel_gui-1.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
35
|
+
lazylabel_gui-1.1.3.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
|
36
|
+
lazylabel_gui-1.1.3.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
|
37
|
+
lazylabel_gui-1.1.3.dist-info/RECORD,,
|
@@ -1,37 +0,0 @@
|
|
1
|
-
lazylabel/__init__.py,sha256=XTAPk-88MdkJX5IKg0f51CxNlARQ-SZIiqLneV6B3II,197
|
2
|
-
lazylabel/main.py,sha256=U0_bYNiVmmwdhRgBOryRZE91sunDQYEJTGGMNFubj1M,766
|
3
|
-
lazylabel/config/__init__.py,sha256=ahAF6cneEVmq2zyGwydGgGAOzJKkv_aumq5cWmokOms,261
|
4
|
-
lazylabel/config/hotkeys.py,sha256=LrQ4nmLWVOZvNOe4mEARFU3Wd3bZ7nvvsxGeFY6N9wA,7618
|
5
|
-
lazylabel/config/paths.py,sha256=d6lJ0rwdlnLjrhR5fIzb3Pc9Fhl4lsGIf2IZNmi4vZ8,1320
|
6
|
-
lazylabel/config/settings.py,sha256=IkUGz-FdDapF71k_LRpUOLHScglGeYjdLbbELGJ4hOs,1860
|
7
|
-
lazylabel/core/__init__.py,sha256=YIj3IAI6Er5nN7gSuo9E8eoQKbBbSq2Jy2prtU8Fp50,230
|
8
|
-
lazylabel/core/file_manager.py,sha256=cXaSPSUiqTm57hfpUxkuzgqApOZCK1RAJdfjmKRJKRA,4734
|
9
|
-
lazylabel/core/model_manager.py,sha256=pLTmQlVfambA3y4aypHu_jn_WrW0ZrMu_8j023mr_EM,3435
|
10
|
-
lazylabel/core/segment_manager.py,sha256=kLEj5S-fEmoXIiJ2Em97GJlre8OOpSotX5bIWMrBCvE,6006
|
11
|
-
lazylabel/models/__init__.py,sha256=qH0EvkWsou0boS85yM6DfRhJnrPOLc-QzWI0grAwnRI,89
|
12
|
-
lazylabel/models/sam_model.py,sha256=lMakV3_B-hNJ3BXy1sErkSzH9YvDsColevXLGaeLnP4,7762
|
13
|
-
lazylabel/ui/__init__.py,sha256=i5hgblzrydTkFSJDiFyfRZMNl4Z4J2dumuwx3bWEPvA,266
|
14
|
-
lazylabel/ui/control_panel.py,sha256=Pfv4D3V9oBkTWWzTE944JAUCa0by7jv9h3EruKnJLVM,8894
|
15
|
-
lazylabel/ui/editable_vertex.py,sha256=pMuXvlCkbguUsFisEzj3bC2UZ_xKqfGuM8kyPug5yws,1852
|
16
|
-
lazylabel/ui/hotkey_dialog.py,sha256=3QkthYil_SWYWxRGL_U8cZCsQ4y-6SExQZXJuE5aO8s,15252
|
17
|
-
lazylabel/ui/hoverable_pixelmap_item.py,sha256=kJFOp7WXiyHpNf7l73TZjiob85jgP30b5MZvu_z5L3c,728
|
18
|
-
lazylabel/ui/hoverable_polygon_item.py,sha256=hQKax3vzhtrdB5ThwTINjOCwOYy718zw5YZPLCfLnGM,1251
|
19
|
-
lazylabel/ui/main_window.py,sha256=zjpTH-RqBx5mJ1i1Y0CC8KepgQF28mIJDYc68CUoBIk,64488
|
20
|
-
lazylabel/ui/numeric_table_widget_item.py,sha256=dQUlIFu9syCxTGAHVIlmbgkI7aJ3f3wmDPBz1AGK9Bg,283
|
21
|
-
lazylabel/ui/photo_viewer.py,sha256=PNgm0gU2gnIqvRkrGlQugdobGsKwAi3m3X6ZF487lCo,2055
|
22
|
-
lazylabel/ui/reorderable_class_table.py,sha256=4c-iuSkPcmk5Aey5n2zz49O85x9TQPujKG-JLxtuBCo,2406
|
23
|
-
lazylabel/ui/right_panel.py,sha256=7ksUM7eItHrdXWkuDHohXEShdYYR2FYx-zeSmbfi620,12454
|
24
|
-
lazylabel/ui/widgets/__init__.py,sha256=5wJ09bTPlhqHyBIXGDeXiNJf33YelKKLu0aPrar9AxU,314
|
25
|
-
lazylabel/ui/widgets/adjustments_widget.py,sha256=Az8GZgu4Uvhm6nEnwjiYDyShOqwieinjFnTOiUfxdzo,3940
|
26
|
-
lazylabel/ui/widgets/model_selection_widget.py,sha256=X3qVH90yCaCpLDauj-DLWJgAwAkAVQrzhG9X5mESa-o,3590
|
27
|
-
lazylabel/ui/widgets/settings_widget.py,sha256=8zhjLxUxqFqxqMYDzkXWD1Ye5V-a7HNhPjPJjYBxeZw,4361
|
28
|
-
lazylabel/ui/widgets/status_bar.py,sha256=71paNOXlxXmWbCG6luyciXRmqC7DNFYDi_Eo2HDzjOg,4026
|
29
|
-
lazylabel/utils/__init__.py,sha256=SX_aZvmFojIJ4Lskat9ly0dmFBXgN7leBdmc68aDLpg,177
|
30
|
-
lazylabel/utils/custom_file_system_model.py,sha256=zK0Z4LY2hzYVtycDWsCLEhXZ0zKZz-Qy5XGwHYrqazQ,4867
|
31
|
-
lazylabel/utils/utils.py,sha256=sYSCoXL27OaLgOZaUkCAhgmKZ7YfhR3Cc5F8nDIa3Ig,414
|
32
|
-
lazylabel_gui-1.1.1.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
|
33
|
-
lazylabel_gui-1.1.1.dist-info/METADATA,sha256=RMI2kMGM08gRUJqkKXrMNbkKvmNDawmvjRCutT_Y0bM,8473
|
34
|
-
lazylabel_gui-1.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
35
|
-
lazylabel_gui-1.1.1.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
|
36
|
-
lazylabel_gui-1.1.1.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
|
37
|
-
lazylabel_gui-1.1.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|