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.
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/PKG-INFO +4 -4
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/README.md +4 -4
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/pyproject.toml +1 -1
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/main.py +79 -60
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel_gui.egg-info/PKG-INFO +4 -4
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/LICENSE +0 -0
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/setup.cfg +0 -0
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/controls.py +0 -0
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/custom_file_system_model.py +0 -0
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/editable_vertex.py +0 -0
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/hoverable_polygon_item.py +0 -0
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/numeric_table_widget_item.py +0 -0
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/photo_viewer.py +0 -0
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/reorderable_class_table.py +0 -0
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/sam_model.py +0 -0
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel/utils.py +0 -0
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel_gui.egg-info/SOURCES.txt +0 -0
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel_gui.egg-info/dependency_links.txt +0 -0
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel_gui.egg-info/entry_points.txt +0 -0
- {lazylabel_gui-1.0.2 → lazylabel_gui-1.0.4}/src/lazylabel_gui.egg-info/requires.txt +0 -0
- {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.
|
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
|
-
|
73
|
+
**Python 3.10**
|
74
74
|
|
75
75
|
### Installation
|
76
76
|
|
77
|
-
#### For Users
|
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
|
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
|
-
|
24
|
+
**Python 3.10**
|
25
25
|
|
26
26
|
### Installation
|
27
27
|
|
28
|
-
#### For Users
|
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
|
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)
|
@@ -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
|
-
|
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 ["
|
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(
|
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
|
-
|
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"
|
432
|
-
|
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"
|
539
|
-
|
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(
|
545
|
-
type_item = QTableWidgetItem(seg
|
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)
|
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
|
-
|
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
|
-
|
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
|
-
|
587
|
-
|
588
|
-
|
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
|
-
|
638
|
-
|
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 =
|
645
|
-
|
646
|
-
|
647
|
-
|
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")
|
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.
|
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
|
-
|
73
|
+
**Python 3.10**
|
74
74
|
|
75
75
|
### Installation
|
76
76
|
|
77
|
-
#### For Users
|
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
|
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
|
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
|