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 +142 -40
- lazylabel/custom_file_system_model.py +87 -24
- lazylabel/main.py +307 -219
- lazylabel/numeric_table_widget_item.py +0 -1
- lazylabel/utils.py +1 -1
- {lazylabel_gui-1.0.7.dist-info → lazylabel_gui-1.0.9.dist-info}/METADATA +2 -2
- lazylabel_gui-1.0.9.dist-info/RECORD +17 -0
- lazylabel_gui-1.0.7.dist-info/RECORD +0 -17
- {lazylabel_gui-1.0.7.dist-info → lazylabel_gui-1.0.9.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.0.7.dist-info → lazylabel_gui-1.0.9.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.0.7.dist-info → lazylabel_gui-1.0.9.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.0.7.dist-info → lazylabel_gui-1.0.9.dist-info}/top_level.txt +0 -0
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
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
56
|
+
main_layout.addSpacing(20)
|
43
57
|
line1 = QFrame()
|
44
58
|
line1.setFrameShape(QFrame.Shape.HLine)
|
45
|
-
|
46
|
-
|
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
|
-
|
54
|
-
|
66
|
+
main_layout.addWidget(self.btn_fit_view)
|
67
|
+
main_layout.addWidget(self.btn_clear_points)
|
55
68
|
|
56
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
156
|
+
main_layout.addWidget(self.notification_label)
|
78
157
|
|
79
|
-
# Device Label
|
80
158
|
self.device_label = QLabel("Device: Unknown")
|
81
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
191
|
+
v_splitter.addWidget(file_explorer_widget)
|
98
192
|
|
99
|
-
|
100
|
-
|
101
|
-
|
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(
|
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
|
-
|
231
|
+
v_splitter.addWidget(segment_widget)
|
138
232
|
|
139
|
-
|
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
|
-
"
|
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", "
|
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
|
-
|
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
|
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 =
|
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
|
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 "
|
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 =
|
45
|
-
if
|
46
|
-
|
47
|
-
|
48
|
-
==
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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)
|