lazylabel-gui 1.1.1__py3-none-any.whl → 1.1.2__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.
Files changed (38) hide show
  1. lazylabel/__init__.py +8 -8
  2. lazylabel/config/__init__.py +6 -6
  3. lazylabel/config/hotkeys.py +168 -168
  4. lazylabel/config/paths.py +40 -40
  5. lazylabel/config/settings.py +65 -65
  6. lazylabel/core/__init__.py +6 -6
  7. lazylabel/core/file_manager.py +105 -105
  8. lazylabel/core/model_manager.py +97 -97
  9. lazylabel/core/segment_manager.py +171 -171
  10. lazylabel/main.py +36 -36
  11. lazylabel/models/__init__.py +4 -4
  12. lazylabel/models/sam_model.py +195 -195
  13. lazylabel/ui/__init__.py +7 -7
  14. lazylabel/ui/control_panel.py +241 -237
  15. lazylabel/ui/editable_vertex.py +64 -51
  16. lazylabel/ui/hotkey_dialog.py +383 -383
  17. lazylabel/ui/hoverable_pixelmap_item.py +22 -22
  18. lazylabel/ui/hoverable_polygon_item.py +39 -39
  19. lazylabel/ui/main_window.py +1659 -1546
  20. lazylabel/ui/numeric_table_widget_item.py +9 -9
  21. lazylabel/ui/photo_viewer.py +54 -54
  22. lazylabel/ui/reorderable_class_table.py +61 -61
  23. lazylabel/ui/right_panel.py +315 -315
  24. lazylabel/ui/widgets/__init__.py +8 -8
  25. lazylabel/ui/widgets/adjustments_widget.py +108 -107
  26. lazylabel/ui/widgets/model_selection_widget.py +93 -93
  27. lazylabel/ui/widgets/settings_widget.py +105 -105
  28. lazylabel/ui/widgets/status_bar.py +109 -109
  29. lazylabel/utils/__init__.py +5 -5
  30. lazylabel/utils/custom_file_system_model.py +132 -132
  31. lazylabel/utils/utils.py +12 -12
  32. {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/METADATA +197 -197
  33. lazylabel_gui-1.1.2.dist-info/RECORD +37 -0
  34. {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/licenses/LICENSE +21 -21
  35. lazylabel_gui-1.1.1.dist-info/RECORD +0 -37
  36. {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/WHEEL +0 -0
  37. {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/entry_points.txt +0 -0
  38. {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/top_level.txt +0 -0
@@ -1,109 +1,109 @@
1
- """Status bar widget for displaying active messages."""
2
-
3
- from PyQt6.QtWidgets import QStatusBar, QLabel
4
- from PyQt6.QtCore import QTimer, pyqtSignal, Qt
5
- from PyQt6.QtGui import QFont
6
-
7
-
8
- class StatusBar(QStatusBar):
9
- """Custom status bar for displaying messages and app status."""
10
-
11
- def __init__(self, parent=None):
12
- super().__init__(parent)
13
- self._message_timer = QTimer()
14
- self._message_timer.timeout.connect(self._clear_temporary_message)
15
- self._setup_ui()
16
-
17
- def _setup_ui(self):
18
- """Setup the status bar UI."""
19
- # Set a reasonable height for the status bar
20
- self.setFixedHeight(25)
21
-
22
- # Main message label (centered)
23
- self.message_label = QLabel()
24
- self.message_label.setStyleSheet("color: #ffa500; padding: 2px 5px;")
25
- self.message_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
26
- font = QFont()
27
- font.setPointSize(9)
28
- self.message_label.setFont(font)
29
-
30
- # Add the message label as the main widget
31
- self.addWidget(self.message_label, 1) # stretch factor 1
32
-
33
- # Permanent status label (right side)
34
- self.permanent_label = QLabel()
35
- self.permanent_label.setStyleSheet("color: #888; padding: 2px 5px;")
36
- font = QFont()
37
- font.setPointSize(9)
38
- self.permanent_label.setFont(font)
39
- self.addPermanentWidget(self.permanent_label)
40
-
41
- # Default state
42
- self.set_ready_message()
43
-
44
- def show_message(self, message: str, duration: int = 5000):
45
- """Show a temporary message for specified duration."""
46
- self.message_label.setText(message)
47
- self.message_label.setStyleSheet("color: #ffa500; padding: 2px 5px;")
48
-
49
- # Stop any existing timer
50
- self._message_timer.stop()
51
-
52
- # Start new timer if duration > 0
53
- if duration > 0:
54
- self._message_timer.start(duration)
55
-
56
- def show_error_message(self, message: str, duration: int = 8000):
57
- """Show an error message with red color."""
58
- self.message_label.setText(f"Error: {message}")
59
- self.message_label.setStyleSheet("color: #ff6b6b; padding: 2px 5px;")
60
-
61
- # Stop any existing timer
62
- self._message_timer.stop()
63
-
64
- # Start new timer if duration > 0
65
- if duration > 0:
66
- self._message_timer.start(duration)
67
-
68
- def show_success_message(self, message: str, duration: int = 3000):
69
- """Show a success message with green color."""
70
- self.message_label.setText(message)
71
- self.message_label.setStyleSheet("color: #51cf66; padding: 2px 5px;")
72
-
73
- # Stop any existing timer
74
- self._message_timer.stop()
75
-
76
- # Start new timer if duration > 0
77
- if duration > 0:
78
- self._message_timer.start(duration)
79
-
80
- def show_warning_message(self, message: str, duration: int = 5000):
81
- """Show a warning message with yellow color."""
82
- self.message_label.setText(f"Warning: {message}")
83
- self.message_label.setStyleSheet("color: #ffd43b; padding: 2px 5px;")
84
-
85
- # Stop any existing timer
86
- self._message_timer.stop()
87
-
88
- # Start new timer if duration > 0
89
- if duration > 0:
90
- self._message_timer.start(duration)
91
-
92
- def set_permanent_message(self, message: str):
93
- """Set a permanent message (usually for status info)."""
94
- self.permanent_label.setText(message)
95
-
96
- def set_ready_message(self):
97
- """Set the default ready message."""
98
- self.message_label.setText("") # Blank instead of "Ready"
99
- self.message_label.setStyleSheet("color: #888; padding: 2px 5px;")
100
- self._message_timer.stop()
101
-
102
- def _clear_temporary_message(self):
103
- """Clear temporary message and return to ready state."""
104
- self.set_ready_message()
105
- self._message_timer.stop()
106
-
107
- def clear_message(self):
108
- """Immediately clear any message."""
109
- self.set_ready_message()
1
+ """Status bar widget for displaying active messages."""
2
+
3
+ from PyQt6.QtWidgets import QStatusBar, QLabel
4
+ from PyQt6.QtCore import QTimer, pyqtSignal, Qt
5
+ from PyQt6.QtGui import QFont
6
+
7
+
8
+ class StatusBar(QStatusBar):
9
+ """Custom status bar for displaying messages and app status."""
10
+
11
+ def __init__(self, parent=None):
12
+ super().__init__(parent)
13
+ self._message_timer = QTimer()
14
+ self._message_timer.timeout.connect(self._clear_temporary_message)
15
+ self._setup_ui()
16
+
17
+ def _setup_ui(self):
18
+ """Setup the status bar UI."""
19
+ # Set a reasonable height for the status bar
20
+ self.setFixedHeight(25)
21
+
22
+ # Main message label (centered)
23
+ self.message_label = QLabel()
24
+ self.message_label.setStyleSheet("color: #ffa500; padding: 2px 5px;")
25
+ self.message_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
26
+ font = QFont()
27
+ font.setPointSize(9)
28
+ self.message_label.setFont(font)
29
+
30
+ # Add the message label as the main widget
31
+ self.addWidget(self.message_label, 1) # stretch factor 1
32
+
33
+ # Permanent status label (right side)
34
+ self.permanent_label = QLabel()
35
+ self.permanent_label.setStyleSheet("color: #888; padding: 2px 5px;")
36
+ font = QFont()
37
+ font.setPointSize(9)
38
+ self.permanent_label.setFont(font)
39
+ self.addPermanentWidget(self.permanent_label)
40
+
41
+ # Default state
42
+ self.set_ready_message()
43
+
44
+ def show_message(self, message: str, duration: int = 5000):
45
+ """Show a temporary message for specified duration."""
46
+ self.message_label.setText(message)
47
+ self.message_label.setStyleSheet("color: #ffa500; padding: 2px 5px;")
48
+
49
+ # Stop any existing timer
50
+ self._message_timer.stop()
51
+
52
+ # Start new timer if duration > 0
53
+ if duration > 0:
54
+ self._message_timer.start(duration)
55
+
56
+ def show_error_message(self, message: str, duration: int = 8000):
57
+ """Show an error message with red color."""
58
+ self.message_label.setText(f"Error: {message}")
59
+ self.message_label.setStyleSheet("color: #ff6b6b; padding: 2px 5px;")
60
+
61
+ # Stop any existing timer
62
+ self._message_timer.stop()
63
+
64
+ # Start new timer if duration > 0
65
+ if duration > 0:
66
+ self._message_timer.start(duration)
67
+
68
+ def show_success_message(self, message: str, duration: int = 3000):
69
+ """Show a success message with green color."""
70
+ self.message_label.setText(message)
71
+ self.message_label.setStyleSheet("color: #51cf66; padding: 2px 5px;")
72
+
73
+ # Stop any existing timer
74
+ self._message_timer.stop()
75
+
76
+ # Start new timer if duration > 0
77
+ if duration > 0:
78
+ self._message_timer.start(duration)
79
+
80
+ def show_warning_message(self, message: str, duration: int = 5000):
81
+ """Show a warning message with yellow color."""
82
+ self.message_label.setText(f"Warning: {message}")
83
+ self.message_label.setStyleSheet("color: #ffd43b; padding: 2px 5px;")
84
+
85
+ # Stop any existing timer
86
+ self._message_timer.stop()
87
+
88
+ # Start new timer if duration > 0
89
+ if duration > 0:
90
+ self._message_timer.start(duration)
91
+
92
+ def set_permanent_message(self, message: str):
93
+ """Set a permanent message (usually for status info)."""
94
+ self.permanent_label.setText(message)
95
+
96
+ def set_ready_message(self):
97
+ """Set the default ready message."""
98
+ self.message_label.setText("") # Blank instead of "Ready"
99
+ self.message_label.setStyleSheet("color: #888; padding: 2px 5px;")
100
+ self._message_timer.stop()
101
+
102
+ def _clear_temporary_message(self):
103
+ """Clear temporary message and return to ready state."""
104
+ self.set_ready_message()
105
+ self._message_timer.stop()
106
+
107
+ def clear_message(self):
108
+ """Immediately clear any message."""
109
+ self.set_ready_message()
@@ -1,6 +1,6 @@
1
- """Utility modules."""
2
-
3
- from .utils import mask_to_pixmap
4
- from .custom_file_system_model import CustomFileSystemModel
5
-
1
+ """Utility modules."""
2
+
3
+ from .utils import mask_to_pixmap
4
+ from .custom_file_system_model import CustomFileSystemModel
5
+
6
6
  __all__ = ['mask_to_pixmap', 'CustomFileSystemModel']
@@ -1,132 +1,132 @@
1
- from pathlib import Path
2
- from PyQt6.QtCore import Qt, QModelIndex, QDir
3
- from PyQt6.QtGui import QFileSystemModel, QBrush, QColor
4
-
5
-
6
- class CustomFileSystemModel(QFileSystemModel):
7
- def __init__(self, parent=None):
8
- super().__init__(parent)
9
- self.setFilter(QDir.Filter.NoDotAndDotDot | QDir.Filter.Files)
10
- self.setNameFilterDisables(False)
11
- self.setNameFilters(["*.png", "*.jpg", "*.jpeg", "*.tiff", "*.tif"])
12
- self.highlighted_path = None
13
-
14
- self.npz_files = set()
15
- self.txt_files = set()
16
-
17
- def setRootPath(self, path: str) -> QModelIndex:
18
- self._scan_directory(path)
19
- return super().setRootPath(path)
20
-
21
- def _scan_directory(self, path: str):
22
- """Scans the directory once and caches the basenames of .npz and .txt files."""
23
- self.npz_files.clear()
24
- self.txt_files.clear()
25
- if not path:
26
- return
27
-
28
- directory = Path(path)
29
- if not directory.is_dir():
30
- return
31
-
32
- try:
33
- for file_path in directory.iterdir():
34
- if file_path.suffix == ".npz":
35
- self.npz_files.add(file_path.stem)
36
- elif file_path.suffix == ".txt":
37
- self.txt_files.add(file_path.stem)
38
- except OSError:
39
- pass
40
-
41
- def update_cache_for_path(self, saved_file_path: str):
42
- """Incrementally updates the cache and the view for a newly saved or deleted file."""
43
- if not saved_file_path:
44
- return
45
-
46
- p = Path(saved_file_path)
47
- base_name = p.stem
48
-
49
- if p.suffix == ".npz":
50
- if p.exists():
51
- self.npz_files.add(base_name)
52
- else:
53
- self.npz_files.discard(base_name)
54
- elif p.suffix == ".txt":
55
- if p.exists():
56
- self.txt_files.add(base_name)
57
- else:
58
- self.txt_files.discard(base_name)
59
- else:
60
- return
61
-
62
- # Find the model index for the corresponding image file to refresh its row
63
- # This assumes the image file is in the same directory (the root path)
64
- root_path = Path(self.rootPath())
65
- for image_ext in self.nameFilters(): # e.g., '*.png', '*.jpg'
66
- # Construct full path to the potential image file
67
- image_file = root_path / (base_name + image_ext.replace("*", ""))
68
- index = self.index(str(image_file))
69
-
70
- if index.isValid() and index.row() != -1:
71
- # Found the corresponding image file, emit signal to refresh its checkmarks
72
- index_col1 = self.index(index.row(), 1, index.parent())
73
- index_col2 = self.index(index.row(), 2, index.parent())
74
- self.dataChanged.emit(
75
- index_col1, index_col2, [Qt.ItemDataRole.CheckStateRole]
76
- )
77
- break
78
-
79
- def set_highlighted_path(self, path):
80
- self.highlighted_path = str(Path(path)) if path else None
81
- self.layoutChanged.emit()
82
-
83
- def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
84
- return 3
85
-
86
- def headerData(
87
- self,
88
- section: int,
89
- orientation: Qt.Orientation,
90
- role: int = Qt.ItemDataRole.DisplayRole,
91
- ):
92
- if (
93
- orientation == Qt.Orientation.Horizontal
94
- and role == Qt.ItemDataRole.DisplayRole
95
- ):
96
- if section == 0:
97
- return "File Name"
98
- if section == 1:
99
- return ".npz"
100
- if section == 2:
101
- return ".txt"
102
- return super().headerData(section, orientation, role)
103
-
104
- def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
105
- if not index.isValid():
106
- return None
107
-
108
- if role == Qt.ItemDataRole.BackgroundRole:
109
- filePath = self.filePath(index)
110
- if self.highlighted_path:
111
- p_file = Path(filePath)
112
- p_highlight = Path(self.highlighted_path)
113
- if p_file.with_suffix("") == p_highlight.with_suffix(""):
114
- return QBrush(QColor(40, 80, 40))
115
-
116
- if index.column() > 0 and role == Qt.ItemDataRole.CheckStateRole:
117
- fileName = self.fileName(index.siblingAtColumn(0))
118
- base_name = Path(fileName).stem
119
-
120
- if index.column() == 1:
121
- exists = base_name in self.npz_files
122
- elif index.column() == 2:
123
- exists = base_name in self.txt_files
124
- else:
125
- return None
126
-
127
- return Qt.CheckState.Checked if exists else Qt.CheckState.Unchecked
128
-
129
- if index.column() > 0 and role == Qt.ItemDataRole.DisplayRole:
130
- return ""
131
-
132
- return super().data(index, role)
1
+ from pathlib import Path
2
+ from PyQt6.QtCore import Qt, QModelIndex, QDir
3
+ from PyQt6.QtGui import QFileSystemModel, QBrush, QColor
4
+
5
+
6
+ class CustomFileSystemModel(QFileSystemModel):
7
+ def __init__(self, parent=None):
8
+ super().__init__(parent)
9
+ self.setFilter(QDir.Filter.NoDotAndDotDot | QDir.Filter.Files)
10
+ self.setNameFilterDisables(False)
11
+ self.setNameFilters(["*.png", "*.jpg", "*.jpeg", "*.tiff", "*.tif"])
12
+ self.highlighted_path = None
13
+
14
+ self.npz_files = set()
15
+ self.txt_files = set()
16
+
17
+ def setRootPath(self, path: str) -> QModelIndex:
18
+ self._scan_directory(path)
19
+ return super().setRootPath(path)
20
+
21
+ def _scan_directory(self, path: str):
22
+ """Scans the directory once and caches the basenames of .npz and .txt files."""
23
+ self.npz_files.clear()
24
+ self.txt_files.clear()
25
+ if not path:
26
+ return
27
+
28
+ directory = Path(path)
29
+ if not directory.is_dir():
30
+ return
31
+
32
+ try:
33
+ for file_path in directory.iterdir():
34
+ if file_path.suffix == ".npz":
35
+ self.npz_files.add(file_path.stem)
36
+ elif file_path.suffix == ".txt":
37
+ self.txt_files.add(file_path.stem)
38
+ except OSError:
39
+ pass
40
+
41
+ def update_cache_for_path(self, saved_file_path: str):
42
+ """Incrementally updates the cache and the view for a newly saved or deleted file."""
43
+ if not saved_file_path:
44
+ return
45
+
46
+ p = Path(saved_file_path)
47
+ base_name = p.stem
48
+
49
+ if p.suffix == ".npz":
50
+ if p.exists():
51
+ self.npz_files.add(base_name)
52
+ else:
53
+ self.npz_files.discard(base_name)
54
+ elif p.suffix == ".txt":
55
+ if p.exists():
56
+ self.txt_files.add(base_name)
57
+ else:
58
+ self.txt_files.discard(base_name)
59
+ else:
60
+ return
61
+
62
+ # Find the model index for the corresponding image file to refresh its row
63
+ # This assumes the image file is in the same directory (the root path)
64
+ root_path = Path(self.rootPath())
65
+ for image_ext in self.nameFilters(): # e.g., '*.png', '*.jpg'
66
+ # Construct full path to the potential image file
67
+ image_file = root_path / (base_name + image_ext.replace("*", ""))
68
+ index = self.index(str(image_file))
69
+
70
+ if index.isValid() and index.row() != -1:
71
+ # Found the corresponding image file, emit signal to refresh its checkmarks
72
+ index_col1 = self.index(index.row(), 1, index.parent())
73
+ index_col2 = self.index(index.row(), 2, index.parent())
74
+ self.dataChanged.emit(
75
+ index_col1, index_col2, [Qt.ItemDataRole.CheckStateRole]
76
+ )
77
+ break
78
+
79
+ def set_highlighted_path(self, path):
80
+ self.highlighted_path = str(Path(path)) if path else None
81
+ self.layoutChanged.emit()
82
+
83
+ def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
84
+ return 3
85
+
86
+ def headerData(
87
+ self,
88
+ section: int,
89
+ orientation: Qt.Orientation,
90
+ role: int = Qt.ItemDataRole.DisplayRole,
91
+ ):
92
+ if (
93
+ orientation == Qt.Orientation.Horizontal
94
+ and role == Qt.ItemDataRole.DisplayRole
95
+ ):
96
+ if section == 0:
97
+ return "File Name"
98
+ if section == 1:
99
+ return ".npz"
100
+ if section == 2:
101
+ return ".txt"
102
+ return super().headerData(section, orientation, role)
103
+
104
+ def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
105
+ if not index.isValid():
106
+ return None
107
+
108
+ if role == Qt.ItemDataRole.BackgroundRole:
109
+ filePath = self.filePath(index)
110
+ if self.highlighted_path:
111
+ p_file = Path(filePath)
112
+ p_highlight = Path(self.highlighted_path)
113
+ if p_file.with_suffix("") == p_highlight.with_suffix(""):
114
+ return QBrush(QColor(40, 80, 40))
115
+
116
+ if index.column() > 0 and role == Qt.ItemDataRole.CheckStateRole:
117
+ fileName = self.fileName(index.siblingAtColumn(0))
118
+ base_name = Path(fileName).stem
119
+
120
+ if index.column() == 1:
121
+ exists = base_name in self.npz_files
122
+ elif index.column() == 2:
123
+ exists = base_name in self.txt_files
124
+ else:
125
+ return None
126
+
127
+ return Qt.CheckState.Checked if exists else Qt.CheckState.Unchecked
128
+
129
+ if index.column() > 0 and role == Qt.ItemDataRole.DisplayRole:
130
+ return ""
131
+
132
+ return super().data(index, role)
lazylabel/utils/utils.py CHANGED
@@ -1,12 +1,12 @@
1
- import numpy as np
2
- from PyQt6.QtGui import QImage, QPixmap
3
-
4
-
5
- def mask_to_pixmap(mask, color, alpha=150):
6
- colored_mask = np.zeros((mask.shape[0], mask.shape[1], 4), dtype=np.uint8)
7
- colored_mask[mask, :3] = color
8
- colored_mask[mask, 3] = alpha
9
- image = QImage(
10
- colored_mask.data, mask.shape[1], mask.shape[0], QImage.Format.Format_RGBA8888
11
- )
12
- return QPixmap.fromImage(image)
1
+ import numpy as np
2
+ from PyQt6.QtGui import QImage, QPixmap
3
+
4
+
5
+ def mask_to_pixmap(mask, color, alpha=150):
6
+ colored_mask = np.zeros((mask.shape[0], mask.shape[1], 4), dtype=np.uint8)
7
+ colored_mask[mask, :3] = color
8
+ colored_mask[mask, 3] = alpha
9
+ image = QImage(
10
+ colored_mask.data, mask.shape[1], mask.shape[0], QImage.Format.Format_RGBA8888
11
+ )
12
+ return QPixmap.fromImage(image)