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 +181 -28
- lazylabel/custom_file_system_model.py +33 -18
- lazylabel/editable_vertex.py +7 -3
- lazylabel/hoverable_pixelmap_item.py +22 -0
- lazylabel/main.py +660 -196
- lazylabel/numeric_table_widget_item.py +0 -1
- lazylabel/photo_viewer.py +6 -3
- lazylabel/reorderable_class_table.py +10 -7
- lazylabel/utils.py +2 -2
- {lazylabel_gui-1.0.6.dist-info → lazylabel_gui-1.0.8.dist-info}/METADATA +2 -2
- lazylabel_gui-1.0.8.dist-info/RECORD +17 -0
- lazylabel_gui-1.0.6.dist-info/RECORD +0 -16
- {lazylabel_gui-1.0.6.dist-info → lazylabel_gui-1.0.8.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.0.6.dist-info → lazylabel_gui-1.0.8.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.0.6.dist-info → lazylabel_gui-1.0.8.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.0.6.dist-info → lazylabel_gui-1.0.8.dist-info}/top_level.txt +0 -0
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
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
39
|
-
|
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
|
-
|
42
|
-
|
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
|
-
|
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
|
-
|
161
|
+
self.v_layout = QVBoxLayout(self)
|
52
162
|
|
53
|
-
|
54
|
-
|
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
|
-
|
185
|
+
v_splitter.addWidget(file_explorer_widget)
|
60
186
|
|
61
|
-
#
|
62
|
-
|
63
|
-
|
64
|
-
|
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(
|
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
|
-
|
226
|
+
v_splitter.addWidget(segment_widget)
|
93
227
|
|
94
|
-
# Class Table
|
95
|
-
|
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.
|
99
|
-
|
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
|
-
|
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
|
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 "
|
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
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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)
|
lazylabel/editable_vertex.py
CHANGED
@@ -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
|
-
|
15
|
-
|
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)
|