lazylabel-gui 1.0.2__tar.gz → 1.0.4__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 (21) hide show
  1. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/PKG-INFO +4 -4
  2. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/README.md +4 -4
  3. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/pyproject.toml +1 -1
  4. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/main.py +79 -60
  5. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel_gui.egg-info/PKG-INFO +4 -4
  6. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/LICENSE +0 -0
  7. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/setup.cfg +0 -0
  8. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/controls.py +0 -0
  9. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/custom_file_system_model.py +0 -0
  10. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/editable_vertex.py +0 -0
  11. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/hoverable_polygon_item.py +0 -0
  12. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/numeric_table_widget_item.py +0 -0
  13. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/photo_viewer.py +0 -0
  14. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/reorderable_class_table.py +0 -0
  15. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/sam_model.py +0 -0
  16. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/utils.py +0 -0
  17. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel_gui.egg-info/SOURCES.txt +0 -0
  18. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel_gui.egg-info/dependency_links.txt +0 -0
  19. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel_gui.egg-info/entry_points.txt +0 -0
  20. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel_gui.egg-info/requires.txt +0 -0
  21. {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel_gui.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lazylabel-gui
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: An image segmentation GUI for generating mask tensors.
5
5
  Author-email: "Deniz N. Cakan" <deniz.n.cakan@gmail.com>
6
6
  License: MIT License
@@ -70,11 +70,11 @@ Inspired by [LabelMe](https://github.com/wkentaro/labelme?tab=readme-ov-file#ins
70
70
  ## 🚀 Getting Started
71
71
 
72
72
  ### Prerequisites
73
- Ensure you have **Python 3.10** or newer.
73
+ **Python 3.10**
74
74
 
75
75
  ### Installation
76
76
 
77
- #### For Users (via PyPI)
77
+ #### For Users [via PyPI](https://pypi.org/project/lazylabel-gui/)
78
78
  1. Install LazyLabel directly:
79
79
  ```bash
80
80
  pip install lazylabel-gui
@@ -87,7 +87,7 @@ Ensure you have **Python 3.10** or newer.
87
87
  #### For Developers (from Source)
88
88
  1. Clone the repository:
89
89
  ```bash
90
- git clone [https://github.com/dnzckn/LazyLabel.git](https://github.com/dnzckn/LazyLabel.git)
90
+ git clone https://github.com/dnzckn/LazyLabel.git
91
91
  cd LazyLabel
92
92
  ```
93
93
  2. Install in editable mode, which links the installed package to your source directory:
@@ -21,11 +21,11 @@ Inspired by [LabelMe](https://github.com/wkentaro/labelme?tab=readme-ov-file#ins
21
21
  ## 🚀 Getting Started
22
22
 
23
23
  ### Prerequisites
24
- Ensure you have **Python 3.10** or newer.
24
+ **Python 3.10**
25
25
 
26
26
  ### Installation
27
27
 
28
- #### For Users (via PyPI)
28
+ #### For Users [via PyPI](https://pypi.org/project/lazylabel-gui/)
29
29
  1. Install LazyLabel directly:
30
30
  ```bash
31
31
  pip install lazylabel-gui
@@ -38,7 +38,7 @@ Ensure you have **Python 3.10** or newer.
38
38
  #### For Developers (from Source)
39
39
  1. Clone the repository:
40
40
  ```bash
41
- git clone [https://github.com/dnzckn/LazyLabel.git](https://github.com/dnzckn/LazyLabel.git)
41
+ git clone https://github.com/dnzckn/LazyLabel.git
42
42
  cd LazyLabel
43
43
  ```
44
44
  2. Install in editable mode, which links the installed package to your source directory:
@@ -95,4 +95,4 @@ Each channel is a binary mask for a class, combining all assigned segments into
95
95
  ---
96
96
 
97
97
  ## ☕ Support LazyLabel
98
- [If you found LazyLabel helpful, consider supporting the project!](https://buymeacoffee.com/dnzckn)
98
+ [If you found LazyLabel helpful, consider supporting the project!](https://buymeacoffee.com/dnzckn)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lazylabel-gui"
7
- version = "1.0.2"
7
+ version = "1.0.4"
8
8
  authors = [
9
9
  { name="Deniz N. Cakan", email="deniz.n.cakan@gmail.com" },
10
10
  ]
@@ -19,7 +19,6 @@ from PyQt6.QtWidgets import (
19
19
  from PyQt6.QtGui import QPixmap, QColor, QPen, QBrush, QPolygonF, QIcon
20
20
  from PyQt6.QtCore import Qt, QPointF, QTimer
21
21
 
22
- # Relative imports for package structure
23
22
  from .photo_viewer import PhotoViewer
24
23
  from .sam_model import SamModel
25
24
  from .utils import mask_to_pixmap
@@ -43,7 +42,6 @@ class MainWindow(QMainWindow):
43
42
 
44
43
  self.setGeometry(50, 50, 1600, 900)
45
44
 
46
- # The SamModel instance is now passed in
47
45
  self.sam_model = sam_model
48
46
  self.mode = "sam_points"
49
47
  self.previous_mode = "sam_points"
@@ -117,11 +115,26 @@ class MainWindow(QMainWindow):
117
115
  )
118
116
  self.control_panel.btn_clear_points.clicked.connect(self.clear_all_points)
119
117
 
118
+ def _get_color_for_class(self, class_id, saturation, value):
119
+ if class_id is None:
120
+ return QColor.fromHsv(0, 0, 128)
121
+
122
+ hue = int((class_id * 222.4922359) % 360)
123
+ color = QColor.fromHsv(hue, saturation, value)
124
+
125
+ if not color.isValid():
126
+ return QColor(Qt.GlobalColor.white)
127
+ return color
128
+
120
129
  def set_mode(self, mode_name, is_toggle=False):
130
+ if self.mode == "selection" and mode_name not in ["selection", "edit"]:
131
+ self.right_panel.segment_table.clearSelection()
121
132
  if self.mode == "edit" and mode_name != "edit":
122
133
  self.display_all_segments()
123
- if not is_toggle and self.mode not in ["pan", "selection", "edit"]:
134
+
135
+ if not is_toggle and self.mode not in ["selection", "edit"]:
124
136
  self.previous_mode = self.mode
137
+
125
138
  self.mode = mode_name
126
139
  self.control_panel.mode_label.setText(
127
140
  f"Mode: {mode_name.replace('_', ' ').title()}"
@@ -143,7 +156,7 @@ class MainWindow(QMainWindow):
143
156
  if self.mode == new_mode:
144
157
  self.set_mode(self.previous_mode, is_toggle=True)
145
158
  else:
146
- if self.mode not in ["pan", "selection", "edit"]:
159
+ if self.mode not in ["selection", "edit"]:
147
160
  self.previous_mode = self.mode
148
161
  self.set_mode(new_mode, is_toggle=True)
149
162
 
@@ -179,7 +192,9 @@ class MainWindow(QMainWindow):
179
192
  self.current_file_index = index
180
193
  path = self.file_model.filePath(index)
181
194
 
182
- if os.path.isfile(path) and path.lower().endswith((".png", ".jpg", ".jpeg")):
195
+ if os.path.isfile(path) and path.lower().endswith(
196
+ (".png", ".jpg", ".jpeg", ".tiff")
197
+ ):
183
198
  self.current_image_path = path
184
199
  pixmap = QPixmap(self.current_image_path)
185
200
  if not pixmap.isNull():
@@ -238,12 +253,13 @@ class MainWindow(QMainWindow):
238
253
  self.toggle_pan_mode()
239
254
  elif key == Qt.Key.Key_R:
240
255
  self.toggle_edit_mode()
241
- elif key == Qt.Key.Key_C:
256
+ elif key == Qt.Key.Key_C or key == Qt.Key.Key_Escape:
242
257
  self.clear_all_points()
243
258
  elif key == Qt.Key.Key_V or key == Qt.Key.Key_Backspace:
244
259
  self.delete_selected_segments()
245
260
  elif key == Qt.Key.Key_M:
246
261
  self.assign_selected_to_class()
262
+ self.right_panel.segment_table.clearSelection()
247
263
  elif key == Qt.Key.Key_Z and mods == Qt.KeyboardModifier.ControlModifier:
248
264
  self.undo_last_action()
249
265
  elif key == Qt.Key.Key_A and mods == Qt.KeyboardModifier.ControlModifier:
@@ -387,10 +403,23 @@ class MainWindow(QMainWindow):
387
403
  selected_indices = self.get_selected_segment_indices()
388
404
  if not selected_indices:
389
405
  return
390
- target_class_id = self.segments[selected_indices[0]]["class_id"]
406
+
407
+ existing_class_ids = [
408
+ self.segments[i]["class_id"]
409
+ for i in selected_indices
410
+ if self.segments[i].get("class_id") is not None
411
+ ]
412
+
413
+ if existing_class_ids:
414
+ target_class_id = min(existing_class_ids)
415
+ else:
416
+ target_class_id = self.segments[selected_indices[0]].get("class_id")
417
+
391
418
  for i in selected_indices:
392
419
  self.segments[i]["class_id"] = target_class_id
420
+
393
421
  self.update_all_lists()
422
+ self.right_panel.segment_table.clearSelection()
394
423
  self.viewer.setFocus()
395
424
 
396
425
  def rasterize_polygon(self, vertices):
@@ -412,26 +441,10 @@ class MainWindow(QMainWindow):
412
441
  self.segment_items.clear()
413
442
  selected_indices = self.get_selected_segment_indices()
414
443
 
415
- unique_class_ids = sorted(
416
- list(
417
- {
418
- seg.get("class_id")
419
- for seg in self.segments
420
- if seg.get("class_id") is not None
421
- }
422
- )
423
- )
424
- num_classes = len(unique_class_ids) if unique_class_ids else 1
425
- class_id_to_hue_index = {
426
- class_id: i for i, class_id in enumerate(unique_class_ids)
427
- }
428
-
429
444
  for i, seg_dict in enumerate(self.segments):
430
445
  self.segment_items[i] = []
431
- class_id = seg_dict.get("class_id", 0)
432
- hue_index = class_id_to_hue_index.get(class_id, 0)
433
- hue = int((hue_index * 360 / num_classes)) % 360
434
- base_color = QColor.fromHsv(hue, 220, 220)
446
+ class_id = seg_dict.get("class_id")
447
+ base_color = self._get_color_for_class(class_id, saturation=220, value=220)
435
448
 
436
449
  if seg_dict["type"] == "Polygon":
437
450
  poly_item = HoverablePolygonItem(QPolygonF(seg_dict["vertices"]))
@@ -522,27 +535,14 @@ class MainWindow(QMainWindow):
522
535
 
523
536
  table.setRowCount(len(display_segments))
524
537
 
525
- unique_class_ids = sorted(
526
- list(
527
- {
528
- s.get("class_id")
529
- for s in self.segments
530
- if s.get("class_id") is not None
531
- }
532
- )
533
- )
534
- num_classes = len(unique_class_ids) if unique_class_ids else 1
535
- class_id_to_hue_index = {cid: i for i, cid in enumerate(unique_class_ids)}
536
-
537
538
  for row, (original_index, seg) in enumerate(display_segments):
538
- class_id = seg.get("class_id", 0)
539
- hue_index = class_id_to_hue_index.get(class_id, 0)
540
- hue = int((hue_index * 360 / num_classes)) % 360
541
- color = QColor.fromHsv(hue, 150, 100)
539
+ class_id = seg.get("class_id")
540
+ color = self._get_color_for_class(class_id, saturation=180, value=200)
542
541
 
542
+ class_id_str = str(class_id) if class_id is not None else "N/A"
543
543
  index_item = NumericTableWidgetItem(str(original_index + 1))
544
- class_item = NumericTableWidgetItem(str(class_id))
545
- type_item = QTableWidgetItem(seg["type"])
544
+ class_item = NumericTableWidgetItem(class_id_str)
545
+ type_item = QTableWidgetItem(seg.get("type", "N/A"))
546
546
 
547
547
  index_item.setFlags(index_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
548
548
  type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
@@ -551,12 +551,15 @@ class MainWindow(QMainWindow):
551
551
  table.setItem(row, 0, index_item)
552
552
  table.setItem(row, 1, class_item)
553
553
  table.setItem(row, 2, type_item)
554
+
554
555
  for col in range(3):
555
- table.item(row, col).setBackground(QBrush(color))
556
+ if table.item(row, col):
557
+ table.item(row, col).setBackground(QBrush(color))
556
558
 
557
559
  table.setSortingEnabled(False)
558
560
  for row in range(table.rowCount()):
559
- if table.item(row, 0).data(Qt.ItemDataRole.UserRole) in selected_indices:
561
+ item = table.item(row, 0)
562
+ if item and item.data(Qt.ItemDataRole.UserRole) in selected_indices:
560
563
  table.selectRow(row)
561
564
  table.setSortingEnabled(True)
562
565
 
@@ -566,6 +569,8 @@ class MainWindow(QMainWindow):
566
569
  def update_class_list(self):
567
570
  class_table = self.right_panel.class_table
568
571
  class_table.blockSignals(True)
572
+ class_table.clearContents()
573
+
569
574
  unique_class_ids = sorted(
570
575
  list(
571
576
  {
@@ -576,18 +581,16 @@ class MainWindow(QMainWindow):
576
581
  )
577
582
  )
578
583
  class_table.setRowCount(len(unique_class_ids))
579
- num_classes = len(unique_class_ids) if unique_class_ids else 1
580
- class_id_to_hue_index = {
581
- class_id: i for i, class_id in enumerate(unique_class_ids)
582
- }
584
+
583
585
  for row, cid in enumerate(unique_class_ids):
584
586
  item = QTableWidgetItem(str(cid))
585
587
  item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
586
- hue_index = class_id_to_hue_index.get(cid, 0)
587
- hue = int((hue_index * 360 / num_classes)) % 360
588
- color = QColor.fromHsv(hue, 150, 100)
588
+
589
+ color = self._get_color_for_class(cid, saturation=180, value=200)
590
+
589
591
  item.setBackground(QBrush(color))
590
592
  class_table.setItem(row, 0, item)
593
+
591
594
  class_table.blockSignals(False)
592
595
 
593
596
  def update_class_filter_combo(self):
@@ -632,19 +635,35 @@ class MainWindow(QMainWindow):
632
635
  if item.column() != 1:
633
636
  return
634
637
  table = self.right_panel.segment_table
638
+ index_item = table.item(item.row(), 0)
639
+ if not index_item:
640
+ return
641
+
635
642
  table.blockSignals(True)
636
643
  try:
637
- new_class_id = int(item.text())
638
- original_index = table.item(item.row(), 0).data(Qt.ItemDataRole.UserRole)
644
+ new_class_id_text = item.text()
645
+ if not new_class_id_text.strip():
646
+ raise ValueError("Class ID cannot be empty.")
647
+ new_class_id = int(new_class_id_text)
648
+ original_index = index_item.data(Qt.ItemDataRole.UserRole)
649
+
650
+ if original_index is None or original_index >= len(self.segments):
651
+ raise IndexError("Invalid segment index found in table.")
652
+
639
653
  self.segments[original_index]["class_id"] = new_class_id
640
654
  if new_class_id >= self.next_class_id:
641
655
  self.next_class_id = new_class_id + 1
642
656
  self.update_all_lists()
643
- except (ValueError, TypeError):
644
- original_index = table.item(item.row(), 0).data(Qt.ItemDataRole.UserRole)
645
- item.setText(str(self.segments[original_index]["class_id"]))
646
- table.blockSignals(False)
647
- self.viewer.setFocus()
657
+ except (ValueError, TypeError, AttributeError, IndexError) as e:
658
+ original_index = index_item.data(Qt.ItemDataRole.UserRole)
659
+ if original_index is not None and original_index < len(self.segments):
660
+ original_class_id = self.segments[original_index].get("class_id")
661
+ item.setText(
662
+ str(original_class_id) if original_class_id is not None else "N/A"
663
+ )
664
+ finally:
665
+ table.blockSignals(False)
666
+ self.viewer.setFocus()
648
667
 
649
668
  def get_selected_segment_indices(self):
650
669
  table = self.right_panel.segment_table
@@ -854,7 +873,7 @@ class MainWindow(QMainWindow):
854
873
  def main():
855
874
  app = QApplication(sys.argv)
856
875
  qdarktheme.setup_theme()
857
- sam_model = SamModel(model_type="vit_h") # one-time check/download
876
+ sam_model = SamModel(model_type="vit_h")
858
877
  main_win = MainWindow(sam_model)
859
878
  main_win.show()
860
879
  sys.exit(app.exec())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lazylabel-gui
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: An image segmentation GUI for generating mask tensors.
5
5
  Author-email: "Deniz N. Cakan" <deniz.n.cakan@gmail.com>
6
6
  License: MIT License
@@ -70,11 +70,11 @@ Inspired by [LabelMe](https://github.com/wkentaro/labelme?tab=readme-ov-file#ins
70
70
  ## 🚀 Getting Started
71
71
 
72
72
  ### Prerequisites
73
- Ensure you have **Python 3.10** or newer.
73
+ **Python 3.10**
74
74
 
75
75
  ### Installation
76
76
 
77
- #### For Users (via PyPI)
77
+ #### For Users [via PyPI](https://pypi.org/project/lazylabel-gui/)
78
78
  1. Install LazyLabel directly:
79
79
  ```bash
80
80
  pip install lazylabel-gui
@@ -87,7 +87,7 @@ Ensure you have **Python 3.10** or newer.
87
87
  #### For Developers (from Source)
88
88
  1. Clone the repository:
89
89
  ```bash
90
- git clone [https://github.com/dnzckn/LazyLabel.git](https://github.com/dnzckn/LazyLabel.git)
90
+ git clone https://github.com/dnzckn/LazyLabel.git
91
91
  cd LazyLabel
92
92
  ```
93
93
  2. Install in editable mode, which links the installed package to your source directory:
File without changes
File without changes