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.
Files changed (23) hide show
  1. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/PKG-INFO +2 -2
  2. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/pyproject.toml +2 -2
  3. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/controls.py +7 -3
  4. lazylabel_gui-1.0.9/src/lazylabel/custom_file_system_model.py +126 -0
  5. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/main.py +24 -4
  6. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/PKG-INFO +2 -2
  7. lazylabel_gui-1.0.8/src/lazylabel/custom_file_system_model.py +0 -72
  8. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/LICENSE +0 -0
  9. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/README.md +0 -0
  10. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/setup.cfg +0 -0
  11. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/editable_vertex.py +0 -0
  12. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/hoverable_pixelmap_item.py +0 -0
  13. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/hoverable_polygon_item.py +0 -0
  14. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/numeric_table_widget_item.py +0 -0
  15. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/photo_viewer.py +0 -0
  16. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/reorderable_class_table.py +0 -0
  17. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/sam_model.py +0 -0
  18. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel/utils.py +0 -0
  19. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/SOURCES.txt +0 -0
  20. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/dependency_links.txt +0 -0
  21. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/entry_points.txt +0 -0
  22. {lazylabel_gui-1.0.8 → lazylabel_gui-1.0.9}/src/lazylabel_gui.egg-info/requires.txt +0 -0
  23. {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.8
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.8"
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.generate_yolo_annotations(final_mask_tensor)
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
- class_id = channel
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"{class_id} {center_x} {center_y} {normalized_width} {normalized_height}"
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.8
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