lazylabel-gui 1.0.6__py3-none-any.whl → 1.0.8__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
@@ -10,6 +10,10 @@ from PyQt6.QtWidgets import (
10
10
  QHBoxLayout,
11
11
  QComboBox,
12
12
  QHeaderView,
13
+ QCheckBox,
14
+ QSlider,
15
+ QGroupBox,
16
+ QSplitter,
13
17
  )
14
18
  from PyQt6.QtCore import Qt
15
19
  from .reorderable_class_table import ReorderableClassTable
@@ -20,60 +24,183 @@ class ControlPanel(QWidget):
20
24
  super().__init__(parent)
21
25
  layout = QVBoxLayout(self)
22
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
+
23
39
  self.mode_label = QLabel("Mode: Points")
24
40
  font = self.mode_label.font()
25
41
  font.setPointSize(14)
26
42
  font.setBold(True)
27
43
  self.mode_label.setFont(font)
28
- layout.addWidget(self.mode_label)
44
+ main_layout.addWidget(self.mode_label)
45
+
29
46
  self.btn_sam_mode = QPushButton("Point Mode (1)")
47
+ self.btn_sam_mode.setToolTip("Switch to Point Mode for AI segmentation (1)")
30
48
  self.btn_polygon_mode = QPushButton("Polygon Mode (2)")
49
+ self.btn_polygon_mode.setToolTip("Switch to Polygon Drawing Mode (2)")
31
50
  self.btn_selection_mode = QPushButton("Selection Mode (E)")
32
- layout.addWidget(self.btn_sam_mode)
33
- layout.addWidget(self.btn_polygon_mode)
34
- layout.addWidget(self.btn_selection_mode)
35
- layout.addSpacing(20)
51
+ self.btn_selection_mode.setToolTip("Toggle segment selection (E)")
52
+ main_layout.addWidget(self.btn_sam_mode)
53
+ main_layout.addWidget(self.btn_polygon_mode)
54
+ main_layout.addWidget(self.btn_selection_mode)
55
+
56
+ main_layout.addSpacing(20)
36
57
  line1 = QFrame()
37
58
  line1.setFrameShape(QFrame.Shape.HLine)
38
- layout.addWidget(line1)
39
- layout.addSpacing(10)
59
+ main_layout.addWidget(line1)
60
+ main_layout.addSpacing(10)
61
+
62
+ self.btn_fit_view = QPushButton("Fit View (.)")
63
+ self.btn_fit_view.setToolTip("Reset image zoom and pan to fit the view (.)")
40
64
  self.btn_clear_points = QPushButton("Clear Clicks (C)")
41
- layout.addWidget(self.btn_clear_points)
42
- layout.addStretch()
65
+ self.btn_clear_points.setToolTip("Clear current temporary points/vertices (C)")
66
+ main_layout.addWidget(self.btn_fit_view)
67
+ main_layout.addWidget(self.btn_clear_points)
68
+
69
+ main_layout.addSpacing(10)
70
+
71
+ settings_group = QGroupBox("Settings")
72
+ settings_layout = QVBoxLayout()
73
+
74
+ self.chk_auto_save = QCheckBox("Auto-Save on Navigate")
75
+ self.chk_auto_save.setToolTip(
76
+ "Automatically save work when using arrow keys to change images."
77
+ )
78
+ self.chk_auto_save.setChecked(True)
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_save_class_aliases = QCheckBox("Save Class Aliases (.json)")
96
+ self.chk_save_class_aliases.setToolTip(
97
+ "Save class aliases to a companion JSON file."
98
+ )
99
+ self.chk_save_class_aliases.setChecked(False)
100
+ settings_layout.addWidget(self.chk_save_class_aliases)
101
+
102
+ settings_group.setLayout(settings_layout)
103
+ main_layout.addWidget(settings_group)
104
+
105
+ sliders_group = QGroupBox("Adjustments")
106
+ sliders_layout = QVBoxLayout()
107
+
108
+ self.size_label = QLabel("Annotation Size: 1.0x")
109
+ self.size_slider = QSlider(Qt.Orientation.Horizontal)
110
+ self.size_slider.setRange(1, 50)
111
+ self.size_slider.setValue(10)
112
+ self.size_slider.setToolTip("Adjusts the size of points and lines (Ctrl +/-)")
113
+ sliders_layout.addWidget(self.size_label)
114
+ sliders_layout.addWidget(self.size_slider)
115
+
116
+ sliders_layout.addSpacing(10)
117
+
118
+ self.pan_label = QLabel("Pan Speed: 1.0x")
119
+ self.pan_slider = QSlider(Qt.Orientation.Horizontal)
120
+ self.pan_slider.setRange(1, 100)
121
+ self.pan_slider.setValue(10)
122
+ self.pan_slider.setToolTip(
123
+ "Adjusts the speed of WASD panning. Hold Shift for 5x boost."
124
+ )
125
+ sliders_layout.addWidget(self.pan_label)
126
+ sliders_layout.addWidget(self.pan_slider)
127
+
128
+ sliders_layout.addSpacing(10)
129
+
130
+ self.join_label = QLabel("Polygon Join Distance: 2px")
131
+ self.join_slider = QSlider(Qt.Orientation.Horizontal)
132
+ self.join_slider.setRange(1, 10)
133
+ self.join_slider.setValue(2)
134
+ self.join_slider.setToolTip("The pixel distance to 'snap' a polygon closed.")
135
+ sliders_layout.addWidget(self.join_label)
136
+ sliders_layout.addWidget(self.join_slider)
137
+
138
+ sliders_group.setLayout(sliders_layout)
139
+ main_layout.addWidget(sliders_group)
140
+
141
+ main_layout.addStretch()
142
+
143
+ self.notification_label = QLabel("")
144
+ font = self.notification_label.font()
145
+ font.setItalic(True)
146
+ self.notification_label.setFont(font)
147
+ self.notification_label.setStyleSheet("color: #ffa500;")
148
+ self.notification_label.setWordWrap(True)
149
+ main_layout.addWidget(self.notification_label)
150
+
43
151
  self.device_label = QLabel("Device: Unknown")
44
- layout.addWidget(self.device_label)
152
+ main_layout.addWidget(self.device_label)
153
+
154
+ layout.addWidget(self.main_controls_widget)
45
155
  self.setFixedWidth(250)
46
156
 
47
157
 
48
158
  class RightPanel(QWidget):
49
159
  def __init__(self, parent=None):
50
160
  super().__init__(parent)
51
- layout = QVBoxLayout(self)
161
+ self.v_layout = QVBoxLayout(self)
52
162
 
53
- # File Explorer
54
- file_explorer_layout = QVBoxLayout()
163
+ toggle_layout = QHBoxLayout()
164
+ toggle_layout.addStretch()
165
+ self.btn_toggle_visibility = QPushButton("Hide >")
166
+ self.btn_toggle_visibility.setToolTip("Hide this panel")
167
+ toggle_layout.addWidget(self.btn_toggle_visibility)
168
+ self.v_layout.addLayout(toggle_layout)
169
+
170
+ self.main_controls_widget = QWidget()
171
+ main_layout = QVBoxLayout(self.main_controls_widget)
172
+ main_layout.setContentsMargins(0, 0, 0, 0)
173
+
174
+ v_splitter = QSplitter(Qt.Orientation.Vertical)
175
+
176
+ # --- File Explorer Widget ---
177
+ file_explorer_widget = QWidget()
178
+ file_explorer_layout = QVBoxLayout(file_explorer_widget)
179
+ file_explorer_layout.setContentsMargins(0, 0, 0, 0)
55
180
  self.btn_open_folder = QPushButton("Open Image Folder")
181
+ self.btn_open_folder.setToolTip("Open a directory of images")
56
182
  self.file_tree = QTreeView()
57
183
  file_explorer_layout.addWidget(self.btn_open_folder)
58
184
  file_explorer_layout.addWidget(self.file_tree)
59
- layout.addLayout(file_explorer_layout)
185
+ v_splitter.addWidget(file_explorer_widget)
60
186
 
61
- # Status Label
62
- self.status_label = QLabel("")
63
- self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
64
- layout.addWidget(self.status_label)
187
+ # --- Segment List Widget ---
188
+ segment_widget = QWidget()
189
+ segment_layout = QVBoxLayout(segment_widget)
190
+ segment_layout.setContentsMargins(0, 0, 0, 0)
65
191
 
66
- # Segment Table
67
- segment_layout = QVBoxLayout()
68
192
  class_filter_layout = QHBoxLayout()
69
193
  class_filter_layout.addWidget(QLabel("Filter Class:"))
70
194
  self.class_filter_combo = QComboBox()
195
+ self.class_filter_combo.setToolTip("Filter segments list by class")
71
196
  class_filter_layout.addWidget(self.class_filter_combo)
72
197
  segment_layout.addLayout(class_filter_layout)
73
198
 
74
199
  self.segment_table = QTableWidget()
75
200
  self.segment_table.setColumnCount(3)
76
- self.segment_table.setHorizontalHeaderLabels(["Index", "Class", "Type"])
201
+ self.segment_table.setHorizontalHeaderLabels(
202
+ ["Segment ID", "Class ID", "Alias"]
203
+ )
77
204
  self.segment_table.horizontalHeader().setSectionResizeMode(
78
205
  QHeaderView.ResizeMode.Stretch
79
206
  )
@@ -81,28 +208,54 @@ class RightPanel(QWidget):
81
208
  QAbstractItemView.SelectionBehavior.SelectRows
82
209
  )
83
210
  self.segment_table.setSortingEnabled(True)
211
+ self.segment_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
84
212
  segment_layout.addWidget(self.segment_table)
85
213
 
86
214
  segment_action_layout = QHBoxLayout()
87
215
  self.btn_merge_selection = QPushButton("Merge to Class")
216
+ self.btn_merge_selection.setToolTip(
217
+ "Merge selected segments into a single class (M)"
218
+ )
88
219
  self.btn_delete_selection = QPushButton("Delete")
220
+ self.btn_delete_selection.setToolTip(
221
+ "Delete selected segments (Delete/Backspace)"
222
+ )
89
223
  segment_action_layout.addWidget(self.btn_merge_selection)
90
224
  segment_action_layout.addWidget(self.btn_delete_selection)
91
225
  segment_layout.addLayout(segment_action_layout)
92
- layout.addLayout(segment_layout, 2)
226
+ v_splitter.addWidget(segment_widget)
93
227
 
94
- # Class Table
95
- class_layout = QVBoxLayout()
228
+ # --- Class Table Widget ---
229
+ class_widget = QWidget()
230
+ class_layout = QVBoxLayout(class_widget)
231
+ class_layout.setContentsMargins(0, 0, 0, 0)
96
232
  class_layout.addWidget(QLabel("Class Order:"))
97
233
  self.class_table = ReorderableClassTable()
98
- self.class_table.setColumnCount(1)
99
- self.class_table.setHorizontalHeaderLabels(["Class ID"])
234
+ self.class_table.setToolTip(
235
+ "Double-click to set class aliases and drag to reorder channels for saving."
236
+ )
237
+ self.class_table.setColumnCount(2)
238
+ self.class_table.setHorizontalHeaderLabels(["Alias", "Class ID"])
100
239
  self.class_table.horizontalHeader().setSectionResizeMode(
101
- QHeaderView.ResizeMode.Stretch
240
+ 0, QHeaderView.ResizeMode.Stretch
241
+ )
242
+ self.class_table.horizontalHeader().setSectionResizeMode(
243
+ 1, QHeaderView.ResizeMode.ResizeToContents
102
244
  )
245
+ self.class_table.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked)
103
246
  class_layout.addWidget(self.class_table)
104
247
  self.btn_reassign_classes = QPushButton("Reassign Class IDs")
248
+ self.btn_reassign_classes.setToolTip(
249
+ "Re-index class channels based on the current order in this table"
250
+ )
105
251
  class_layout.addWidget(self.btn_reassign_classes)
106
- layout.addLayout(class_layout, 1)
252
+ v_splitter.addWidget(class_widget)
253
+
254
+ main_layout.addWidget(v_splitter)
255
+
256
+ self.status_label = QLabel("")
257
+ self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
258
+ main_layout.addWidget(self.status_label)
107
259
 
260
+ self.v_layout.addWidget(self.main_controls_widget)
108
261
  self.setFixedWidth(350)
@@ -12,10 +12,11 @@ class CustomFileSystemModel(QFileSystemModel):
12
12
  self.highlighted_path = None
13
13
 
14
14
  def set_highlighted_path(self, path):
15
- self.highlighted_path = path
15
+ self.highlighted_path = os.path.normpath(path) if path else None
16
+ self.layoutChanged.emit()
16
17
 
17
18
  def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
18
- return 2
19
+ return 3
19
20
 
20
21
  def headerData(
21
22
  self,
@@ -30,28 +31,42 @@ class CustomFileSystemModel(QFileSystemModel):
30
31
  if section == 0:
31
32
  return "File Name"
32
33
  if section == 1:
33
- return "Mask"
34
+ return ".npz"
35
+ if section == 2:
36
+ return ".txt"
34
37
  return super().headerData(section, orientation, role)
35
38
 
36
39
  def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
37
40
  if not index.isValid():
38
41
  return None
39
42
 
40
- # Handle the temporary highlight for saving
41
43
  if role == Qt.ItemDataRole.BackgroundRole:
42
- filePath = self.filePath(index)
43
- if filePath == self.highlighted_path:
44
- return QBrush(QColor("yellow"))
45
-
46
- if index.column() == 1:
47
- if role == Qt.ItemDataRole.CheckStateRole:
48
- filePath = self.filePath(index.siblingAtColumn(0))
49
- mask_path = os.path.splitext(filePath)[0] + ".npz"
50
- return (
51
- Qt.CheckState.Checked
52
- if os.path.exists(mask_path)
53
- else Qt.CheckState.Unchecked
54
- )
55
- return None
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))
51
+
52
+ if index.column() > 0 and role == Qt.ItemDataRole.CheckStateRole:
53
+ filePath = self.filePath(index.siblingAtColumn(0))
54
+ base_path = os.path.splitext(filePath)[0]
55
+
56
+ if index.column() == 1:
57
+ check_path = base_path + ".npz"
58
+ elif index.column() == 2:
59
+ check_path = base_path + ".txt"
60
+ else:
61
+ return None
62
+
63
+ return (
64
+ Qt.CheckState.Checked
65
+ if os.path.exists(check_path)
66
+ else Qt.CheckState.Unchecked
67
+ )
68
+
69
+ if index.column() > 0 and role == Qt.ItemDataRole.DisplayRole:
70
+ return ""
56
71
 
57
72
  return super().data(index, role)
@@ -1,6 +1,6 @@
1
1
  from PyQt6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem
2
2
  from PyQt6.QtCore import Qt
3
- from PyQt6.QtGui import QBrush, QPen
3
+ from PyQt6.QtGui import QBrush, QPen, QColor
4
4
 
5
5
 
6
6
  class EditableVertexItem(QGraphicsEllipseItem):
@@ -11,8 +11,12 @@ class EditableVertexItem(QGraphicsEllipseItem):
11
11
  self.vertex_index = vertex_index
12
12
 
13
13
  self.setZValue(200)
14
- self.setBrush(QBrush(Qt.GlobalColor.cyan))
15
- self.setPen(QPen(Qt.GlobalColor.white, 1))
14
+
15
+ color = QColor(Qt.GlobalColor.cyan)
16
+ color.setAlpha(180)
17
+ self.setBrush(QBrush(color))
18
+
19
+ self.setPen(QPen(Qt.GlobalColor.transparent))
16
20
  self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
17
21
  self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
18
22
 
@@ -0,0 +1,22 @@
1
+ from PyQt6.QtWidgets import QGraphicsPixmapItem
2
+
3
+
4
+ class HoverablePixmapItem(QGraphicsPixmapItem):
5
+ def __init__(self, parent=None):
6
+ super().__init__(parent)
7
+ self.setAcceptHoverEvents(True)
8
+ self.default_pixmap = None
9
+ self.hover_pixmap = None
10
+
11
+ def set_pixmaps(self, default_pixmap, hover_pixmap):
12
+ self.default_pixmap = default_pixmap
13
+ self.hover_pixmap = hover_pixmap
14
+ self.setPixmap(self.default_pixmap)
15
+
16
+ def hoverEnterEvent(self, event):
17
+ self.setPixmap(self.hover_pixmap)
18
+ super().hoverEnterEvent(event)
19
+
20
+ def hoverLeaveEvent(self, event):
21
+ self.setPixmap(self.default_pixmap)
22
+ super().hoverLeaveEvent(event)