lazylabel-gui 1.0.7__py3-none-any.whl → 1.0.9__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/controls.py CHANGED
@@ -11,6 +11,9 @@ from PyQt6.QtWidgets import (
11
11
  QComboBox,
12
12
  QHeaderView,
13
13
  QCheckBox,
14
+ QSlider,
15
+ QGroupBox,
16
+ QSplitter,
14
17
  )
15
18
  from PyQt6.QtCore import Qt
16
19
  from .reorderable_class_table import ReorderableClassTable
@@ -21,88 +24,176 @@ class ControlPanel(QWidget):
21
24
  super().__init__(parent)
22
25
  layout = QVBoxLayout(self)
23
26
  layout.setAlignment(Qt.AlignmentFlag.AlignTop)
27
+
28
+ toggle_layout = QHBoxLayout()
29
+ self.btn_toggle_visibility = QPushButton("< Hide")
30
+ self.btn_toggle_visibility.setToolTip("Hide this panel")
31
+ toggle_layout.addWidget(self.btn_toggle_visibility)
32
+ toggle_layout.addStretch()
33
+ layout.addLayout(toggle_layout)
34
+
35
+ self.main_controls_widget = QWidget()
36
+ main_layout = QVBoxLayout(self.main_controls_widget)
37
+ main_layout.setContentsMargins(0, 0, 0, 0)
38
+
24
39
  self.mode_label = QLabel("Mode: Points")
25
40
  font = self.mode_label.font()
26
41
  font.setPointSize(14)
27
42
  font.setBold(True)
28
43
  self.mode_label.setFont(font)
29
- layout.addWidget(self.mode_label)
44
+ main_layout.addWidget(self.mode_label)
30
45
 
31
- # Mode Buttons
32
46
  self.btn_sam_mode = QPushButton("Point Mode (1)")
33
47
  self.btn_sam_mode.setToolTip("Switch to Point Mode for AI segmentation (1)")
34
48
  self.btn_polygon_mode = QPushButton("Polygon Mode (2)")
35
49
  self.btn_polygon_mode.setToolTip("Switch to Polygon Drawing Mode (2)")
36
50
  self.btn_selection_mode = QPushButton("Selection Mode (E)")
37
51
  self.btn_selection_mode.setToolTip("Toggle segment selection (E)")
38
- layout.addWidget(self.btn_sam_mode)
39
- layout.addWidget(self.btn_polygon_mode)
40
- layout.addWidget(self.btn_selection_mode)
52
+ main_layout.addWidget(self.btn_sam_mode)
53
+ main_layout.addWidget(self.btn_polygon_mode)
54
+ main_layout.addWidget(self.btn_selection_mode)
41
55
 
42
- layout.addSpacing(20)
56
+ main_layout.addSpacing(20)
43
57
  line1 = QFrame()
44
58
  line1.setFrameShape(QFrame.Shape.HLine)
45
- layout.addWidget(line1)
46
- layout.addSpacing(10)
59
+ main_layout.addWidget(line1)
60
+ main_layout.addSpacing(10)
47
61
 
48
- # Action Buttons
49
62
  self.btn_fit_view = QPushButton("Fit View (.)")
50
63
  self.btn_fit_view.setToolTip("Reset image zoom and pan to fit the view (.)")
51
64
  self.btn_clear_points = QPushButton("Clear Clicks (C)")
52
65
  self.btn_clear_points.setToolTip("Clear current temporary points/vertices (C)")
53
- layout.addWidget(self.btn_fit_view)
54
- layout.addWidget(self.btn_clear_points)
66
+ main_layout.addWidget(self.btn_fit_view)
67
+ main_layout.addWidget(self.btn_clear_points)
55
68
 
56
- layout.addSpacing(10)
69
+ main_layout.addSpacing(10)
70
+
71
+ settings_group = QGroupBox("Settings")
72
+ settings_layout = QVBoxLayout()
57
73
 
58
- # Settings
59
74
  self.chk_auto_save = QCheckBox("Auto-Save on Navigate")
60
75
  self.chk_auto_save.setToolTip(
61
76
  "Automatically save work when using arrow keys to change images."
62
77
  )
63
78
  self.chk_auto_save.setChecked(True)
64
- layout.addWidget(self.chk_auto_save)
79
+ settings_layout.addWidget(self.chk_auto_save)
80
+
81
+ self.chk_save_npz = QCheckBox("Save .npz")
82
+ self.chk_save_npz.setChecked(True)
83
+ self.chk_save_npz.setToolTip(
84
+ "Save the final mask as a compressed NumPy NPZ file."
85
+ )
86
+ settings_layout.addWidget(self.chk_save_npz)
87
+
88
+ self.chk_save_txt = QCheckBox("Save .txt")
89
+ self.chk_save_txt.setChecked(True)
90
+ self.chk_save_txt.setToolTip(
91
+ "Save bounding box annotations in YOLO TXT format."
92
+ )
93
+ settings_layout.addWidget(self.chk_save_txt)
94
+
95
+ self.chk_yolo_use_alias = QCheckBox("Save YOLO with Class Aliases")
96
+ self.chk_yolo_use_alias.setToolTip(
97
+ "If checked, saves YOLO .txt files using class alias names instead of numeric IDs.\nThis is useful when a separate .yaml or .names file defines the classes."
98
+ )
99
+ self.chk_yolo_use_alias.setChecked(True)
100
+ settings_layout.addWidget(self.chk_yolo_use_alias)
101
+
102
+ self.chk_save_class_aliases = QCheckBox("Save Class Aliases (.json)")
103
+ self.chk_save_class_aliases.setToolTip(
104
+ "Save class aliases to a companion JSON file."
105
+ )
106
+ self.chk_save_class_aliases.setChecked(False)
107
+ settings_layout.addWidget(self.chk_save_class_aliases)
108
+
109
+ settings_group.setLayout(settings_layout)
110
+ main_layout.addWidget(settings_group)
111
+
112
+ sliders_group = QGroupBox("Adjustments")
113
+ sliders_layout = QVBoxLayout()
114
+
115
+ self.size_label = QLabel("Annotation Size: 1.0x")
116
+ self.size_slider = QSlider(Qt.Orientation.Horizontal)
117
+ self.size_slider.setRange(1, 50)
118
+ self.size_slider.setValue(10)
119
+ self.size_slider.setToolTip("Adjusts the size of points and lines (Ctrl +/-)")
120
+ sliders_layout.addWidget(self.size_label)
121
+ sliders_layout.addWidget(self.size_slider)
122
+
123
+ sliders_layout.addSpacing(10)
124
+
125
+ self.pan_label = QLabel("Pan Speed: 1.0x")
126
+ self.pan_slider = QSlider(Qt.Orientation.Horizontal)
127
+ self.pan_slider.setRange(1, 100)
128
+ self.pan_slider.setValue(10)
129
+ self.pan_slider.setToolTip(
130
+ "Adjusts the speed of WASD panning. Hold Shift for 5x boost."
131
+ )
132
+ sliders_layout.addWidget(self.pan_label)
133
+ sliders_layout.addWidget(self.pan_slider)
134
+
135
+ sliders_layout.addSpacing(10)
136
+
137
+ self.join_label = QLabel("Polygon Join Distance: 2px")
138
+ self.join_slider = QSlider(Qt.Orientation.Horizontal)
139
+ self.join_slider.setRange(1, 10)
140
+ self.join_slider.setValue(2)
141
+ self.join_slider.setToolTip("The pixel distance to 'snap' a polygon closed.")
142
+ sliders_layout.addWidget(self.join_label)
143
+ sliders_layout.addWidget(self.join_slider)
65
144
 
66
- layout.addStretch()
145
+ sliders_group.setLayout(sliders_layout)
146
+ main_layout.addWidget(sliders_group)
147
+
148
+ main_layout.addStretch()
67
149
 
68
- # Notification Label
69
150
  self.notification_label = QLabel("")
70
151
  font = self.notification_label.font()
71
152
  font.setItalic(True)
72
153
  self.notification_label.setFont(font)
73
- self.notification_label.setStyleSheet(
74
- "color: #ffa500;"
75
- ) # Orange color for visibility
154
+ self.notification_label.setStyleSheet("color: #ffa500;")
76
155
  self.notification_label.setWordWrap(True)
77
- layout.addWidget(self.notification_label)
156
+ main_layout.addWidget(self.notification_label)
78
157
 
79
- # Device Label
80
158
  self.device_label = QLabel("Device: Unknown")
81
- layout.addWidget(self.device_label)
159
+ main_layout.addWidget(self.device_label)
160
+
161
+ layout.addWidget(self.main_controls_widget)
82
162
  self.setFixedWidth(250)
83
163
 
84
164
 
85
165
  class RightPanel(QWidget):
86
166
  def __init__(self, parent=None):
87
167
  super().__init__(parent)
88
- layout = QVBoxLayout(self)
168
+ self.v_layout = QVBoxLayout(self)
169
+
170
+ toggle_layout = QHBoxLayout()
171
+ toggle_layout.addStretch()
172
+ self.btn_toggle_visibility = QPushButton("Hide >")
173
+ self.btn_toggle_visibility.setToolTip("Hide this panel")
174
+ toggle_layout.addWidget(self.btn_toggle_visibility)
175
+ self.v_layout.addLayout(toggle_layout)
176
+
177
+ self.main_controls_widget = QWidget()
178
+ main_layout = QVBoxLayout(self.main_controls_widget)
179
+ main_layout.setContentsMargins(0, 0, 0, 0)
180
+
181
+ v_splitter = QSplitter(Qt.Orientation.Vertical)
89
182
 
90
- # File Explorer
91
- file_explorer_layout = QVBoxLayout()
183
+ file_explorer_widget = QWidget()
184
+ file_explorer_layout = QVBoxLayout(file_explorer_widget)
185
+ file_explorer_layout.setContentsMargins(0, 0, 0, 0)
92
186
  self.btn_open_folder = QPushButton("Open Image Folder")
93
187
  self.btn_open_folder.setToolTip("Open a directory of images")
94
188
  self.file_tree = QTreeView()
95
189
  file_explorer_layout.addWidget(self.btn_open_folder)
96
190
  file_explorer_layout.addWidget(self.file_tree)
97
- layout.addLayout(file_explorer_layout)
191
+ v_splitter.addWidget(file_explorer_widget)
98
192
 
99
- # Status Label
100
- self.status_label = QLabel("")
101
- self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
102
- layout.addWidget(self.status_label)
193
+ segment_widget = QWidget()
194
+ segment_layout = QVBoxLayout(segment_widget)
195
+ segment_layout.setContentsMargins(0, 0, 0, 0)
103
196
 
104
- # Segment Table
105
- segment_layout = QVBoxLayout()
106
197
  class_filter_layout = QHBoxLayout()
107
198
  class_filter_layout.addWidget(QLabel("Filter Class:"))
108
199
  self.class_filter_combo = QComboBox()
@@ -112,7 +203,9 @@ class RightPanel(QWidget):
112
203
 
113
204
  self.segment_table = QTableWidget()
114
205
  self.segment_table.setColumnCount(3)
115
- self.segment_table.setHorizontalHeaderLabels(["Index", "Class ID", "Type"])
206
+ self.segment_table.setHorizontalHeaderLabels(
207
+ ["Segment ID", "Class ID", "Alias"]
208
+ )
116
209
  self.segment_table.horizontalHeader().setSectionResizeMode(
117
210
  QHeaderView.ResizeMode.Stretch
118
211
  )
@@ -120,6 +213,7 @@ class RightPanel(QWidget):
120
213
  QAbstractItemView.SelectionBehavior.SelectRows
121
214
  )
122
215
  self.segment_table.setSortingEnabled(True)
216
+ self.segment_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
123
217
  segment_layout.addWidget(self.segment_table)
124
218
 
125
219
  segment_action_layout = QHBoxLayout()
@@ -134,30 +228,38 @@ class RightPanel(QWidget):
134
228
  segment_action_layout.addWidget(self.btn_merge_selection)
135
229
  segment_action_layout.addWidget(self.btn_delete_selection)
136
230
  segment_layout.addLayout(segment_action_layout)
137
- layout.addLayout(segment_layout, 2)
231
+ v_splitter.addWidget(segment_widget)
138
232
 
139
- # Class Table
140
- class_layout = QVBoxLayout()
233
+ class_widget = QWidget()
234
+ class_layout = QVBoxLayout(class_widget)
235
+ class_layout.setContentsMargins(0, 0, 0, 0)
141
236
  class_layout.addWidget(QLabel("Class Order:"))
142
237
  self.class_table = ReorderableClassTable()
143
238
  self.class_table.setToolTip(
144
- "Set class aliases and drag to reorder channels for saving."
239
+ "Double-click to set class aliases and drag to reorder channels for saving."
145
240
  )
146
241
  self.class_table.setColumnCount(2)
147
- self.class_table.setHorizontalHeaderLabels(["Alias", "Channel Index"])
242
+ self.class_table.setHorizontalHeaderLabels(["Alias", "Class ID"])
148
243
  self.class_table.horizontalHeader().setSectionResizeMode(
149
244
  0, QHeaderView.ResizeMode.Stretch
150
245
  )
151
246
  self.class_table.horizontalHeader().setSectionResizeMode(
152
247
  1, QHeaderView.ResizeMode.ResizeToContents
153
248
  )
154
-
249
+ self.class_table.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked)
155
250
  class_layout.addWidget(self.class_table)
156
251
  self.btn_reassign_classes = QPushButton("Reassign Class IDs")
157
252
  self.btn_reassign_classes.setToolTip(
158
253
  "Re-index class channels based on the current order in this table"
159
254
  )
160
255
  class_layout.addWidget(self.btn_reassign_classes)
161
- layout.addLayout(class_layout, 1)
256
+ v_splitter.addWidget(class_widget)
257
+
258
+ main_layout.addWidget(v_splitter)
259
+
260
+ self.status_label = QLabel("")
261
+ self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
262
+ main_layout.addWidget(self.status_label)
162
263
 
264
+ self.v_layout.addWidget(self.main_controls_widget)
163
265
  self.setFixedWidth(350)
@@ -1,4 +1,4 @@
1
- import os
1
+ from pathlib import Path
2
2
  from PyQt6.QtCore import Qt, QModelIndex, QDir
3
3
  from PyQt6.QtGui import QFileSystemModel, QBrush, QColor
4
4
 
@@ -11,13 +11,71 @@ class CustomFileSystemModel(QFileSystemModel):
11
11
  self.setNameFilters(["*.png", "*.jpg", "*.jpeg", "*.tiff", "*.tif"])
12
12
  self.highlighted_path = None
13
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 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
+ self.npz_files.add(base_name)
51
+ elif p.suffix == ".txt":
52
+ self.txt_files.add(base_name)
53
+ else:
54
+ return
55
+
56
+ # Find the model index for the corresponding image file to refresh its row
57
+ # This assumes the image file is in the same directory (the root path)
58
+ root_path = Path(self.rootPath())
59
+ for image_ext in self.nameFilters(): # e.g., '*.png', '*.jpg'
60
+ # Construct full path to the potential image file
61
+ image_file = root_path / (base_name + image_ext.replace("*", ""))
62
+ index = self.index(str(image_file))
63
+
64
+ if index.isValid() and index.row() != -1:
65
+ # Found the corresponding image file, emit signal to refresh its checkmarks
66
+ index_col1 = self.index(index.row(), 1, index.parent())
67
+ index_col2 = self.index(index.row(), 2, index.parent())
68
+ self.dataChanged.emit(
69
+ index_col1, index_col2, [Qt.ItemDataRole.CheckStateRole]
70
+ )
71
+ break
72
+
14
73
  def set_highlighted_path(self, path):
15
- self.highlighted_path = os.path.normpath(path) if path else None
16
- # Trigger repaint of the entire view
74
+ self.highlighted_path = str(Path(path)) if path else None
17
75
  self.layoutChanged.emit()
18
76
 
19
77
  def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
20
- return 2
78
+ return 3
21
79
 
22
80
  def headerData(
23
81
  self,
@@ -32,32 +90,37 @@ class CustomFileSystemModel(QFileSystemModel):
32
90
  if section == 0:
33
91
  return "File Name"
34
92
  if section == 1:
35
- return "Mask"
93
+ return ".npz"
94
+ if section == 2:
95
+ return ".txt"
36
96
  return super().headerData(section, orientation, role)
37
97
 
38
98
  def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
39
99
  if not index.isValid():
40
100
  return None
41
101
 
42
- # Handle the temporary highlight for saving
43
102
  if role == Qt.ItemDataRole.BackgroundRole:
44
- filePath = os.path.normpath(self.filePath(index))
45
- if (
46
- self.highlighted_path
47
- and os.path.splitext(filePath)[0]
48
- == os.path.splitext(self.highlighted_path)[0]
49
- ):
50
- return QBrush(QColor(40, 80, 40)) # Dark green highlight
51
-
52
- if index.column() == 1:
53
- if role == Qt.ItemDataRole.CheckStateRole:
54
- filePath = self.filePath(index.siblingAtColumn(0))
55
- mask_path = os.path.splitext(filePath)[0] + ".npz"
56
- return (
57
- Qt.CheckState.Checked
58
- if os.path.exists(mask_path)
59
- else Qt.CheckState.Unchecked
60
- )
61
- return None
103
+ filePath = self.filePath(index)
104
+ if self.highlighted_path:
105
+ p_file = Path(filePath)
106
+ p_highlight = Path(self.highlighted_path)
107
+ if p_file.with_suffix("") == p_highlight.with_suffix(""):
108
+ return QBrush(QColor(40, 80, 40))
109
+
110
+ if index.column() > 0 and role == Qt.ItemDataRole.CheckStateRole:
111
+ fileName = self.fileName(index.siblingAtColumn(0))
112
+ base_name = Path(fileName).stem
113
+
114
+ if index.column() == 1:
115
+ exists = base_name in self.npz_files
116
+ elif index.column() == 2:
117
+ exists = base_name in self.txt_files
118
+ else:
119
+ return None
120
+
121
+ return Qt.CheckState.Checked if exists else Qt.CheckState.Unchecked
122
+
123
+ if index.column() > 0 and role == Qt.ItemDataRole.DisplayRole:
124
+ return ""
62
125
 
63
126
  return super().data(index, role)