lazylabel-gui 1.0.8__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.
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/PKG-INFO +2 -2
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/pyproject.toml +2 -2
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/controls.py +7 -3
- lazylabel_gui-1.0.9/src/lazylabel/custom_file_system_model.py +126 -0
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/main.py +24 -4
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/PKG-INFO +2 -2
- lazylabel_gui-1.0.8/src/lazylabel/custom_file_system_model.py +0 -72
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/LICENSE +0 -0
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/README.md +0 -0
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/setup.cfg +0 -0
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/editable_vertex.py +0 -0
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/hoverable_pixelmap_item.py +0 -0
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/hoverable_polygon_item.py +0 -0
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/numeric_table_widget_item.py +0 -0
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/photo_viewer.py +0 -0
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/reorderable_class_table.py +0 -0
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/sam_model.py +0 -0
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/utils.py +0 -0
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/SOURCES.txt +0 -0
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/dependency_links.txt +0 -0
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/entry_points.txt +0 -0
- {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/requires.txt +0 -0
- {lazylabel_gui-1.0.8 → 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.
|
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
|
+
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"
|
@@ -92,6 +92,13 @@ class ControlPanel(QWidget):
|
|
92
92
|
)
|
93
93
|
settings_layout.addWidget(self.chk_save_txt)
|
94
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
|
+
|
95
102
|
self.chk_save_class_aliases = QCheckBox("Save Class Aliases (.json)")
|
96
103
|
self.chk_save_class_aliases.setToolTip(
|
97
104
|
"Save class aliases to a companion JSON file."
|
@@ -173,7 +180,6 @@ class RightPanel(QWidget):
|
|
173
180
|
|
174
181
|
v_splitter = QSplitter(Qt.Orientation.Vertical)
|
175
182
|
|
176
|
-
# --- File Explorer Widget ---
|
177
183
|
file_explorer_widget = QWidget()
|
178
184
|
file_explorer_layout = QVBoxLayout(file_explorer_widget)
|
179
185
|
file_explorer_layout.setContentsMargins(0, 0, 0, 0)
|
@@ -184,7 +190,6 @@ class RightPanel(QWidget):
|
|
184
190
|
file_explorer_layout.addWidget(self.file_tree)
|
185
191
|
v_splitter.addWidget(file_explorer_widget)
|
186
192
|
|
187
|
-
# --- Segment List Widget ---
|
188
193
|
segment_widget = QWidget()
|
189
194
|
segment_layout = QVBoxLayout(segment_widget)
|
190
195
|
segment_layout.setContentsMargins(0, 0, 0, 0)
|
@@ -225,7 +230,6 @@ class RightPanel(QWidget):
|
|
225
230
|
segment_layout.addLayout(segment_action_layout)
|
226
231
|
v_splitter.addWidget(segment_widget)
|
227
232
|
|
228
|
-
# --- Class Table Widget ---
|
229
233
|
class_widget = QWidget()
|
230
234
|
class_layout = QVBoxLayout(class_widget)
|
231
235
|
class_layout.setContentsMargins(0, 0, 0, 0)
|
@@ -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)
|
@@ -1016,13 +1016,27 @@ class MainWindow(QMainWindow):
|
|
1016
1016
|
np.savez_compressed(
|
1017
1017
|
npz_path, mask=final_mask_tensor.astype(np.uint8)
|
1018
1018
|
)
|
1019
|
+
self.file_model.update_cache_for_path(npz_path)
|
1020
|
+
|
1019
1021
|
self.file_model.set_highlighted_path(npz_path)
|
1020
1022
|
QTimer.singleShot(
|
1021
1023
|
1500, lambda: self.file_model.set_highlighted_path(None)
|
1022
1024
|
)
|
1023
1025
|
saved_something = True
|
1024
1026
|
if save_txt:
|
1025
|
-
self.
|
1027
|
+
if self.control_panel.chk_yolo_use_alias.isChecked():
|
1028
|
+
class_labels = [
|
1029
|
+
class_table.item(row, 0).text()
|
1030
|
+
for row in range(class_table.rowCount())
|
1031
|
+
]
|
1032
|
+
else:
|
1033
|
+
class_labels = list(range(num_final_classes))
|
1034
|
+
|
1035
|
+
txt_path = self.generate_yolo_annotations(
|
1036
|
+
final_mask_tensor, class_labels
|
1037
|
+
)
|
1038
|
+
if txt_path:
|
1039
|
+
self.file_model.update_cache_for_path(txt_path)
|
1026
1040
|
saved_something = True
|
1027
1041
|
|
1028
1042
|
if save_aliases:
|
@@ -1039,7 +1053,7 @@ class MainWindow(QMainWindow):
|
|
1039
1053
|
|
1040
1054
|
QTimer.singleShot(3000, lambda: self.right_panel.status_label.clear())
|
1041
1055
|
|
1042
|
-
def generate_yolo_annotations(self, mask_tensor):
|
1056
|
+
def generate_yolo_annotations(self, mask_tensor, class_labels):
|
1043
1057
|
output_path = os.path.splitext(self.current_image_path)[0] + ".txt"
|
1044
1058
|
h, w, num_channels = mask_tensor.shape
|
1045
1059
|
|
@@ -1055,20 +1069,26 @@ class MainWindow(QMainWindow):
|
|
1055
1069
|
contours, _ = cv2.findContours(
|
1056
1070
|
single_channel_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
1057
1071
|
)
|
1058
|
-
|
1072
|
+
|
1073
|
+
class_label = class_labels[channel]
|
1059
1074
|
for contour in contours:
|
1060
1075
|
x, y, width, height = cv2.boundingRect(contour)
|
1061
1076
|
center_x = (x + width / 2) / w
|
1062
1077
|
center_y = (y + height / 2) / h
|
1063
1078
|
normalized_width = width / w
|
1064
1079
|
normalized_height = height / h
|
1065
|
-
yolo_entry = f"{
|
1080
|
+
yolo_entry = f"{class_label} {center_x} {center_y} {normalized_width} {normalized_height}"
|
1066
1081
|
yolo_annotations.append(yolo_entry)
|
1067
1082
|
|
1083
|
+
if not yolo_annotations:
|
1084
|
+
return None
|
1085
|
+
|
1068
1086
|
with open(output_path, "w") as file:
|
1069
1087
|
for annotation in yolo_annotations:
|
1070
1088
|
file.write(annotation + "\n")
|
1071
1089
|
|
1090
|
+
return output_path
|
1091
|
+
|
1072
1092
|
def save_current_segment(self):
|
1073
1093
|
if (
|
1074
1094
|
self.mode != "sam_points"
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: lazylabel-gui
|
3
|
-
Version: 1.0.
|
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
|
|
@@ -1,72 +0,0 @@
|
|
1
|
-
import os
|
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
|
-
def set_highlighted_path(self, path):
|
15
|
-
self.highlighted_path = os.path.normpath(path) if path else None
|
16
|
-
self.layoutChanged.emit()
|
17
|
-
|
18
|
-
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
19
|
-
return 3
|
20
|
-
|
21
|
-
def headerData(
|
22
|
-
self,
|
23
|
-
section: int,
|
24
|
-
orientation: Qt.Orientation,
|
25
|
-
role: int = Qt.ItemDataRole.DisplayRole,
|
26
|
-
):
|
27
|
-
if (
|
28
|
-
orientation == Qt.Orientation.Horizontal
|
29
|
-
and role == Qt.ItemDataRole.DisplayRole
|
30
|
-
):
|
31
|
-
if section == 0:
|
32
|
-
return "File Name"
|
33
|
-
if section == 1:
|
34
|
-
return ".npz"
|
35
|
-
if section == 2:
|
36
|
-
return ".txt"
|
37
|
-
return super().headerData(section, orientation, role)
|
38
|
-
|
39
|
-
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
|
40
|
-
if not index.isValid():
|
41
|
-
return None
|
42
|
-
|
43
|
-
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))
|
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 ""
|
71
|
-
|
72
|
-
return super().data(index, role)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|