lazylabel-gui 1.0.7__tar.gz → 1.0.9__tar.gz

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 (24) hide show
  1. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/PKG-INFO +2 -2
  2. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/pyproject.toml +2 -2
  3. lazylabel_gui-1.0.9/src/lazylabel/controls.py +265 -0
  4. lazylabel_gui-1.0.9/src/lazylabel/custom_file_system_model.py +126 -0
  5. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/src/lazylabel/main.py +307 -219
  6. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/src/lazylabel/numeric_table_widget_item.py +0 -1
  7. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/src/lazylabel/utils.py +1 -1
  8. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/PKG-INFO +2 -2
  9. lazylabel_gui-1.0.7/src/lazylabel/controls.py +0 -163
  10. lazylabel_gui-1.0.7/src/lazylabel/custom_file_system_model.py +0 -63
  11. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/LICENSE +0 -0
  12. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/README.md +0 -0
  13. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/setup.cfg +0 -0
  14. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/src/lazylabel/editable_vertex.py +0 -0
  15. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/src/lazylabel/hoverable_pixelmap_item.py +0 -0
  16. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/src/lazylabel/hoverable_polygon_item.py +0 -0
  17. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/src/lazylabel/photo_viewer.py +0 -0
  18. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/src/lazylabel/reorderable_class_table.py +0 -0
  19. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/src/lazylabel/sam_model.py +0 -0
  20. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/SOURCES.txt +0 -0
  21. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/dependency_links.txt +0 -0
  22. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/entry_points.txt +0 -0
  23. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/requires.txt +0 -0
  24. {lazylabel_gui-1.0.7 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lazylabel-gui
3
- Version: 1.0.7
4
- Summary: An image segmentation GUI for generating mask tensors.
3
+ Version: 1.0.9
4
+ Summary: An image segmentation GUI for generating ML ready mask tensors and annotations.
5
5
  Author-email: "Deniz N. Cakan" <deniz.n.cakan@gmail.com>
6
6
  License: MIT License
7
7
 
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lazylabel-gui"
7
- version = "1.0.7"
7
+ version = "1.0.9"
8
8
  authors = [
9
9
  { name="Deniz N. Cakan", email="deniz.n.cakan@gmail.com" },
10
10
  ]
11
- description = "An image segmentation GUI for generating mask tensors."
11
+ description = "An image segmentation GUI for generating ML ready mask tensors and annotations."
12
12
  readme = "README.md"
13
13
  license = { file="LICENSE" }
14
14
  requires-python = ">=3.10"
@@ -0,0 +1,265 @@
1
+ from PyQt6.QtWidgets import (
2
+ QWidget,
3
+ QVBoxLayout,
4
+ QPushButton,
5
+ QLabel,
6
+ QFrame,
7
+ QTableWidget,
8
+ QTreeView,
9
+ QAbstractItemView,
10
+ QHBoxLayout,
11
+ QComboBox,
12
+ QHeaderView,
13
+ QCheckBox,
14
+ QSlider,
15
+ QGroupBox,
16
+ QSplitter,
17
+ )
18
+ from PyQt6.QtCore import Qt
19
+ from .reorderable_class_table import ReorderableClassTable
20
+
21
+
22
+ class ControlPanel(QWidget):
23
+ def __init__(self, parent=None):
24
+ super().__init__(parent)
25
+ layout = QVBoxLayout(self)
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
+
39
+ self.mode_label = QLabel("Mode: Points")
40
+ font = self.mode_label.font()
41
+ font.setPointSize(14)
42
+ font.setBold(True)
43
+ self.mode_label.setFont(font)
44
+ main_layout.addWidget(self.mode_label)
45
+
46
+ self.btn_sam_mode = QPushButton("Point Mode (1)")
47
+ self.btn_sam_mode.setToolTip("Switch to Point Mode for AI segmentation (1)")
48
+ self.btn_polygon_mode = QPushButton("Polygon Mode (2)")
49
+ self.btn_polygon_mode.setToolTip("Switch to Polygon Drawing Mode (2)")
50
+ self.btn_selection_mode = QPushButton("Selection Mode (E)")
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)
57
+ line1 = QFrame()
58
+ line1.setFrameShape(QFrame.Shape.HLine)
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 (.)")
64
+ self.btn_clear_points = QPushButton("Clear Clicks (C)")
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_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)
144
+
145
+ sliders_group.setLayout(sliders_layout)
146
+ main_layout.addWidget(sliders_group)
147
+
148
+ main_layout.addStretch()
149
+
150
+ self.notification_label = QLabel("")
151
+ font = self.notification_label.font()
152
+ font.setItalic(True)
153
+ self.notification_label.setFont(font)
154
+ self.notification_label.setStyleSheet("color: #ffa500;")
155
+ self.notification_label.setWordWrap(True)
156
+ main_layout.addWidget(self.notification_label)
157
+
158
+ self.device_label = QLabel("Device: Unknown")
159
+ main_layout.addWidget(self.device_label)
160
+
161
+ layout.addWidget(self.main_controls_widget)
162
+ self.setFixedWidth(250)
163
+
164
+
165
+ class RightPanel(QWidget):
166
+ def __init__(self, parent=None):
167
+ super().__init__(parent)
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)
182
+
183
+ file_explorer_widget = QWidget()
184
+ file_explorer_layout = QVBoxLayout(file_explorer_widget)
185
+ file_explorer_layout.setContentsMargins(0, 0, 0, 0)
186
+ self.btn_open_folder = QPushButton("Open Image Folder")
187
+ self.btn_open_folder.setToolTip("Open a directory of images")
188
+ self.file_tree = QTreeView()
189
+ file_explorer_layout.addWidget(self.btn_open_folder)
190
+ file_explorer_layout.addWidget(self.file_tree)
191
+ v_splitter.addWidget(file_explorer_widget)
192
+
193
+ segment_widget = QWidget()
194
+ segment_layout = QVBoxLayout(segment_widget)
195
+ segment_layout.setContentsMargins(0, 0, 0, 0)
196
+
197
+ class_filter_layout = QHBoxLayout()
198
+ class_filter_layout.addWidget(QLabel("Filter Class:"))
199
+ self.class_filter_combo = QComboBox()
200
+ self.class_filter_combo.setToolTip("Filter segments list by class")
201
+ class_filter_layout.addWidget(self.class_filter_combo)
202
+ segment_layout.addLayout(class_filter_layout)
203
+
204
+ self.segment_table = QTableWidget()
205
+ self.segment_table.setColumnCount(3)
206
+ self.segment_table.setHorizontalHeaderLabels(
207
+ ["Segment ID", "Class ID", "Alias"]
208
+ )
209
+ self.segment_table.horizontalHeader().setSectionResizeMode(
210
+ QHeaderView.ResizeMode.Stretch
211
+ )
212
+ self.segment_table.setSelectionBehavior(
213
+ QAbstractItemView.SelectionBehavior.SelectRows
214
+ )
215
+ self.segment_table.setSortingEnabled(True)
216
+ self.segment_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
217
+ segment_layout.addWidget(self.segment_table)
218
+
219
+ segment_action_layout = QHBoxLayout()
220
+ self.btn_merge_selection = QPushButton("Merge to Class")
221
+ self.btn_merge_selection.setToolTip(
222
+ "Merge selected segments into a single class (M)"
223
+ )
224
+ self.btn_delete_selection = QPushButton("Delete")
225
+ self.btn_delete_selection.setToolTip(
226
+ "Delete selected segments (Delete/Backspace)"
227
+ )
228
+ segment_action_layout.addWidget(self.btn_merge_selection)
229
+ segment_action_layout.addWidget(self.btn_delete_selection)
230
+ segment_layout.addLayout(segment_action_layout)
231
+ v_splitter.addWidget(segment_widget)
232
+
233
+ class_widget = QWidget()
234
+ class_layout = QVBoxLayout(class_widget)
235
+ class_layout.setContentsMargins(0, 0, 0, 0)
236
+ class_layout.addWidget(QLabel("Class Order:"))
237
+ self.class_table = ReorderableClassTable()
238
+ self.class_table.setToolTip(
239
+ "Double-click to set class aliases and drag to reorder channels for saving."
240
+ )
241
+ self.class_table.setColumnCount(2)
242
+ self.class_table.setHorizontalHeaderLabels(["Alias", "Class ID"])
243
+ self.class_table.horizontalHeader().setSectionResizeMode(
244
+ 0, QHeaderView.ResizeMode.Stretch
245
+ )
246
+ self.class_table.horizontalHeader().setSectionResizeMode(
247
+ 1, QHeaderView.ResizeMode.ResizeToContents
248
+ )
249
+ self.class_table.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked)
250
+ class_layout.addWidget(self.class_table)
251
+ self.btn_reassign_classes = QPushButton("Reassign Class IDs")
252
+ self.btn_reassign_classes.setToolTip(
253
+ "Re-index class channels based on the current order in this table"
254
+ )
255
+ class_layout.addWidget(self.btn_reassign_classes)
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)
263
+
264
+ self.v_layout.addWidget(self.main_controls_widget)
265
+ self.setFixedWidth(350)
@@ -0,0 +1,126 @@
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 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
+
73
+ def set_highlighted_path(self, path):
74
+ self.highlighted_path = str(Path(path)) if path else None
75
+ self.layoutChanged.emit()
76
+
77
+ def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
78
+ return 3
79
+
80
+ def headerData(
81
+ self,
82
+ section: int,
83
+ orientation: Qt.Orientation,
84
+ role: int = Qt.ItemDataRole.DisplayRole,
85
+ ):
86
+ if (
87
+ orientation == Qt.Orientation.Horizontal
88
+ and role == Qt.ItemDataRole.DisplayRole
89
+ ):
90
+ if section == 0:
91
+ return "File Name"
92
+ if section == 1:
93
+ return ".npz"
94
+ if section == 2:
95
+ return ".txt"
96
+ return super().headerData(section, orientation, role)
97
+
98
+ def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
99
+ if not index.isValid():
100
+ return None
101
+
102
+ if role == Qt.ItemDataRole.BackgroundRole:
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 ""
125
+
126
+ return super().data(index, role)