lazylabel-gui 1.1.1__py3-none-any.whl → 1.1.2__py3-none-any.whl
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/__init__.py +8 -8
- lazylabel/config/__init__.py +6 -6
- lazylabel/config/hotkeys.py +168 -168
- lazylabel/config/paths.py +40 -40
- lazylabel/config/settings.py +65 -65
- lazylabel/core/__init__.py +6 -6
- lazylabel/core/file_manager.py +105 -105
- lazylabel/core/model_manager.py +97 -97
- lazylabel/core/segment_manager.py +171 -171
- lazylabel/main.py +36 -36
- lazylabel/models/__init__.py +4 -4
- lazylabel/models/sam_model.py +195 -195
- lazylabel/ui/__init__.py +7 -7
- lazylabel/ui/control_panel.py +241 -237
- lazylabel/ui/editable_vertex.py +64 -51
- lazylabel/ui/hotkey_dialog.py +383 -383
- lazylabel/ui/hoverable_pixelmap_item.py +22 -22
- lazylabel/ui/hoverable_polygon_item.py +39 -39
- lazylabel/ui/main_window.py +1659 -1546
- lazylabel/ui/numeric_table_widget_item.py +9 -9
- lazylabel/ui/photo_viewer.py +54 -54
- lazylabel/ui/reorderable_class_table.py +61 -61
- lazylabel/ui/right_panel.py +315 -315
- lazylabel/ui/widgets/__init__.py +8 -8
- lazylabel/ui/widgets/adjustments_widget.py +108 -107
- lazylabel/ui/widgets/model_selection_widget.py +93 -93
- lazylabel/ui/widgets/settings_widget.py +105 -105
- lazylabel/ui/widgets/status_bar.py +109 -109
- lazylabel/utils/__init__.py +5 -5
- lazylabel/utils/custom_file_system_model.py +132 -132
- lazylabel/utils/utils.py +12 -12
- {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/METADATA +197 -197
- lazylabel_gui-1.1.2.dist-info/RECORD +37 -0
- {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/licenses/LICENSE +21 -21
- lazylabel_gui-1.1.1.dist-info/RECORD +0 -37
- {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/top_level.txt +0 -0
@@ -1,171 +1,171 @@
|
|
1
|
-
"""Segment management functionality."""
|
2
|
-
|
3
|
-
import numpy as np
|
4
|
-
import cv2
|
5
|
-
from typing import List, Dict, Any, Optional, Tuple
|
6
|
-
from PyQt6.QtCore import QPointF
|
7
|
-
|
8
|
-
|
9
|
-
class SegmentManager:
|
10
|
-
"""Manages image segments and classes."""
|
11
|
-
|
12
|
-
def __init__(self):
|
13
|
-
self.segments: List[Dict[str, Any]] = []
|
14
|
-
self.class_aliases: Dict[int, str] = {}
|
15
|
-
self.next_class_id: int = 0
|
16
|
-
self.active_class_id: Optional[int] = None # Currently active/toggled class
|
17
|
-
|
18
|
-
def clear(self) -> None:
|
19
|
-
"""Clear all segments and reset state."""
|
20
|
-
self.segments.clear()
|
21
|
-
self.class_aliases.clear()
|
22
|
-
self.next_class_id = 0
|
23
|
-
self.active_class_id = None
|
24
|
-
|
25
|
-
def add_segment(self, segment_data: Dict[str, Any]) -> None:
|
26
|
-
"""Add a new segment."""
|
27
|
-
if "class_id" not in segment_data:
|
28
|
-
# Use active class if available, otherwise use next class ID
|
29
|
-
if self.active_class_id is not None:
|
30
|
-
segment_data["class_id"] = self.active_class_id
|
31
|
-
else:
|
32
|
-
segment_data["class_id"] = self.next_class_id
|
33
|
-
self.segments.append(segment_data)
|
34
|
-
self._update_next_class_id()
|
35
|
-
|
36
|
-
def delete_segments(self, indices: List[int]) -> None:
|
37
|
-
"""Delete segments by indices."""
|
38
|
-
for i in sorted(indices, reverse=True):
|
39
|
-
if 0 <= i < len(self.segments):
|
40
|
-
del self.segments[i]
|
41
|
-
self._update_next_class_id()
|
42
|
-
|
43
|
-
def assign_segments_to_class(self, indices: List[int]) -> None:
|
44
|
-
"""Assign selected segments to a class."""
|
45
|
-
if not indices:
|
46
|
-
return
|
47
|
-
|
48
|
-
existing_class_ids = [
|
49
|
-
self.segments[i]["class_id"]
|
50
|
-
for i in indices
|
51
|
-
if i < len(self.segments) and self.segments[i].get("class_id") is not None
|
52
|
-
]
|
53
|
-
|
54
|
-
if existing_class_ids:
|
55
|
-
target_class_id = min(existing_class_ids)
|
56
|
-
else:
|
57
|
-
target_class_id = self.next_class_id
|
58
|
-
|
59
|
-
for i in indices:
|
60
|
-
if i < len(self.segments):
|
61
|
-
self.segments[i]["class_id"] = target_class_id
|
62
|
-
|
63
|
-
self._update_next_class_id()
|
64
|
-
|
65
|
-
def get_unique_class_ids(self) -> List[int]:
|
66
|
-
"""Get sorted list of unique class IDs."""
|
67
|
-
return sorted(
|
68
|
-
list(
|
69
|
-
{
|
70
|
-
seg.get("class_id")
|
71
|
-
for seg in self.segments
|
72
|
-
if seg.get("class_id") is not None
|
73
|
-
}
|
74
|
-
)
|
75
|
-
)
|
76
|
-
|
77
|
-
def rasterize_polygon(
|
78
|
-
self, vertices: List[QPointF], image_size: Tuple[int, int]
|
79
|
-
) -> Optional[np.ndarray]:
|
80
|
-
"""Convert polygon vertices to binary mask."""
|
81
|
-
if not vertices:
|
82
|
-
return None
|
83
|
-
|
84
|
-
h, w = image_size
|
85
|
-
points_np = np.array([[p.x(), p.y()] for p in vertices], dtype=np.int32)
|
86
|
-
mask = np.zeros((h, w), dtype=np.uint8)
|
87
|
-
cv2.fillPoly(mask, [points_np], 1)
|
88
|
-
return mask.astype(bool)
|
89
|
-
|
90
|
-
def create_final_mask_tensor(
|
91
|
-
self, image_size: Tuple[int, int], class_order: List[int]
|
92
|
-
) -> np.ndarray:
|
93
|
-
"""Create final mask tensor for saving."""
|
94
|
-
h, w = image_size
|
95
|
-
id_map = {old_id: new_id for new_id, old_id in enumerate(class_order)}
|
96
|
-
num_final_classes = len(class_order)
|
97
|
-
final_mask_tensor = np.zeros((h, w, num_final_classes), dtype=np.uint8)
|
98
|
-
|
99
|
-
for seg in self.segments:
|
100
|
-
class_id = seg.get("class_id")
|
101
|
-
if class_id not in id_map:
|
102
|
-
continue
|
103
|
-
|
104
|
-
new_channel_idx = id_map[class_id]
|
105
|
-
|
106
|
-
if seg["type"] == "Polygon":
|
107
|
-
mask = self.rasterize_polygon(seg["vertices"], image_size)
|
108
|
-
else:
|
109
|
-
mask = seg.get("mask")
|
110
|
-
|
111
|
-
if mask is not None:
|
112
|
-
final_mask_tensor[:, :, new_channel_idx] = np.logical_or(
|
113
|
-
final_mask_tensor[:, :, new_channel_idx], mask
|
114
|
-
)
|
115
|
-
|
116
|
-
return final_mask_tensor
|
117
|
-
|
118
|
-
def reassign_class_ids(self, new_order: List[int]) -> None:
|
119
|
-
"""Reassign class IDs based on new order."""
|
120
|
-
id_map = {old_id: new_id for new_id, old_id in enumerate(new_order)}
|
121
|
-
|
122
|
-
for seg in self.segments:
|
123
|
-
old_id = seg.get("class_id")
|
124
|
-
if old_id in id_map:
|
125
|
-
seg["class_id"] = id_map[old_id]
|
126
|
-
|
127
|
-
# Update aliases
|
128
|
-
new_aliases = {
|
129
|
-
id_map[old_id]: self.class_aliases.get(old_id, str(old_id))
|
130
|
-
for old_id in new_order
|
131
|
-
if old_id in self.class_aliases
|
132
|
-
}
|
133
|
-
self.class_aliases = new_aliases
|
134
|
-
self._update_next_class_id()
|
135
|
-
|
136
|
-
def set_class_alias(self, class_id: int, alias: str) -> None:
|
137
|
-
"""Set alias for a class."""
|
138
|
-
self.class_aliases[class_id] = alias
|
139
|
-
|
140
|
-
def get_class_alias(self, class_id: int) -> str:
|
141
|
-
"""Get alias for a class."""
|
142
|
-
return self.class_aliases.get(class_id, str(class_id))
|
143
|
-
|
144
|
-
def set_active_class(self, class_id: Optional[int]) -> None:
|
145
|
-
"""Set the active class ID."""
|
146
|
-
self.active_class_id = class_id
|
147
|
-
|
148
|
-
def get_active_class(self) -> Optional[int]:
|
149
|
-
"""Get the active class ID."""
|
150
|
-
return self.active_class_id
|
151
|
-
|
152
|
-
def toggle_active_class(self, class_id: int) -> bool:
|
153
|
-
"""Toggle a class as active. Returns True if now active, False if deactivated."""
|
154
|
-
if self.active_class_id == class_id:
|
155
|
-
self.active_class_id = None
|
156
|
-
return False
|
157
|
-
else:
|
158
|
-
self.active_class_id = class_id
|
159
|
-
return True
|
160
|
-
|
161
|
-
def _update_next_class_id(self) -> None:
|
162
|
-
"""Update the next available class ID."""
|
163
|
-
all_ids = {
|
164
|
-
seg.get("class_id")
|
165
|
-
for seg in self.segments
|
166
|
-
if seg.get("class_id") is not None
|
167
|
-
}
|
168
|
-
if not all_ids:
|
169
|
-
self.next_class_id = 0
|
170
|
-
else:
|
171
|
-
self.next_class_id = max(all_ids) + 1
|
1
|
+
"""Segment management functionality."""
|
2
|
+
|
3
|
+
import numpy as np
|
4
|
+
import cv2
|
5
|
+
from typing import List, Dict, Any, Optional, Tuple
|
6
|
+
from PyQt6.QtCore import QPointF
|
7
|
+
|
8
|
+
|
9
|
+
class SegmentManager:
|
10
|
+
"""Manages image segments and classes."""
|
11
|
+
|
12
|
+
def __init__(self):
|
13
|
+
self.segments: List[Dict[str, Any]] = []
|
14
|
+
self.class_aliases: Dict[int, str] = {}
|
15
|
+
self.next_class_id: int = 0
|
16
|
+
self.active_class_id: Optional[int] = None # Currently active/toggled class
|
17
|
+
|
18
|
+
def clear(self) -> None:
|
19
|
+
"""Clear all segments and reset state."""
|
20
|
+
self.segments.clear()
|
21
|
+
self.class_aliases.clear()
|
22
|
+
self.next_class_id = 0
|
23
|
+
self.active_class_id = None
|
24
|
+
|
25
|
+
def add_segment(self, segment_data: Dict[str, Any]) -> None:
|
26
|
+
"""Add a new segment."""
|
27
|
+
if "class_id" not in segment_data:
|
28
|
+
# Use active class if available, otherwise use next class ID
|
29
|
+
if self.active_class_id is not None:
|
30
|
+
segment_data["class_id"] = self.active_class_id
|
31
|
+
else:
|
32
|
+
segment_data["class_id"] = self.next_class_id
|
33
|
+
self.segments.append(segment_data)
|
34
|
+
self._update_next_class_id()
|
35
|
+
|
36
|
+
def delete_segments(self, indices: List[int]) -> None:
|
37
|
+
"""Delete segments by indices."""
|
38
|
+
for i in sorted(indices, reverse=True):
|
39
|
+
if 0 <= i < len(self.segments):
|
40
|
+
del self.segments[i]
|
41
|
+
self._update_next_class_id()
|
42
|
+
|
43
|
+
def assign_segments_to_class(self, indices: List[int]) -> None:
|
44
|
+
"""Assign selected segments to a class."""
|
45
|
+
if not indices:
|
46
|
+
return
|
47
|
+
|
48
|
+
existing_class_ids = [
|
49
|
+
self.segments[i]["class_id"]
|
50
|
+
for i in indices
|
51
|
+
if i < len(self.segments) and self.segments[i].get("class_id") is not None
|
52
|
+
]
|
53
|
+
|
54
|
+
if existing_class_ids:
|
55
|
+
target_class_id = min(existing_class_ids)
|
56
|
+
else:
|
57
|
+
target_class_id = self.next_class_id
|
58
|
+
|
59
|
+
for i in indices:
|
60
|
+
if i < len(self.segments):
|
61
|
+
self.segments[i]["class_id"] = target_class_id
|
62
|
+
|
63
|
+
self._update_next_class_id()
|
64
|
+
|
65
|
+
def get_unique_class_ids(self) -> List[int]:
|
66
|
+
"""Get sorted list of unique class IDs."""
|
67
|
+
return sorted(
|
68
|
+
list(
|
69
|
+
{
|
70
|
+
seg.get("class_id")
|
71
|
+
for seg in self.segments
|
72
|
+
if seg.get("class_id") is not None
|
73
|
+
}
|
74
|
+
)
|
75
|
+
)
|
76
|
+
|
77
|
+
def rasterize_polygon(
|
78
|
+
self, vertices: List[QPointF], image_size: Tuple[int, int]
|
79
|
+
) -> Optional[np.ndarray]:
|
80
|
+
"""Convert polygon vertices to binary mask."""
|
81
|
+
if not vertices:
|
82
|
+
return None
|
83
|
+
|
84
|
+
h, w = image_size
|
85
|
+
points_np = np.array([[p.x(), p.y()] for p in vertices], dtype=np.int32)
|
86
|
+
mask = np.zeros((h, w), dtype=np.uint8)
|
87
|
+
cv2.fillPoly(mask, [points_np], 1)
|
88
|
+
return mask.astype(bool)
|
89
|
+
|
90
|
+
def create_final_mask_tensor(
|
91
|
+
self, image_size: Tuple[int, int], class_order: List[int]
|
92
|
+
) -> np.ndarray:
|
93
|
+
"""Create final mask tensor for saving."""
|
94
|
+
h, w = image_size
|
95
|
+
id_map = {old_id: new_id for new_id, old_id in enumerate(class_order)}
|
96
|
+
num_final_classes = len(class_order)
|
97
|
+
final_mask_tensor = np.zeros((h, w, num_final_classes), dtype=np.uint8)
|
98
|
+
|
99
|
+
for seg in self.segments:
|
100
|
+
class_id = seg.get("class_id")
|
101
|
+
if class_id not in id_map:
|
102
|
+
continue
|
103
|
+
|
104
|
+
new_channel_idx = id_map[class_id]
|
105
|
+
|
106
|
+
if seg["type"] == "Polygon":
|
107
|
+
mask = self.rasterize_polygon(seg["vertices"], image_size)
|
108
|
+
else:
|
109
|
+
mask = seg.get("mask")
|
110
|
+
|
111
|
+
if mask is not None:
|
112
|
+
final_mask_tensor[:, :, new_channel_idx] = np.logical_or(
|
113
|
+
final_mask_tensor[:, :, new_channel_idx], mask
|
114
|
+
)
|
115
|
+
|
116
|
+
return final_mask_tensor
|
117
|
+
|
118
|
+
def reassign_class_ids(self, new_order: List[int]) -> None:
|
119
|
+
"""Reassign class IDs based on new order."""
|
120
|
+
id_map = {old_id: new_id for new_id, old_id in enumerate(new_order)}
|
121
|
+
|
122
|
+
for seg in self.segments:
|
123
|
+
old_id = seg.get("class_id")
|
124
|
+
if old_id in id_map:
|
125
|
+
seg["class_id"] = id_map[old_id]
|
126
|
+
|
127
|
+
# Update aliases
|
128
|
+
new_aliases = {
|
129
|
+
id_map[old_id]: self.class_aliases.get(old_id, str(old_id))
|
130
|
+
for old_id in new_order
|
131
|
+
if old_id in self.class_aliases
|
132
|
+
}
|
133
|
+
self.class_aliases = new_aliases
|
134
|
+
self._update_next_class_id()
|
135
|
+
|
136
|
+
def set_class_alias(self, class_id: int, alias: str) -> None:
|
137
|
+
"""Set alias for a class."""
|
138
|
+
self.class_aliases[class_id] = alias
|
139
|
+
|
140
|
+
def get_class_alias(self, class_id: int) -> str:
|
141
|
+
"""Get alias for a class."""
|
142
|
+
return self.class_aliases.get(class_id, str(class_id))
|
143
|
+
|
144
|
+
def set_active_class(self, class_id: Optional[int]) -> None:
|
145
|
+
"""Set the active class ID."""
|
146
|
+
self.active_class_id = class_id
|
147
|
+
|
148
|
+
def get_active_class(self) -> Optional[int]:
|
149
|
+
"""Get the active class ID."""
|
150
|
+
return self.active_class_id
|
151
|
+
|
152
|
+
def toggle_active_class(self, class_id: int) -> bool:
|
153
|
+
"""Toggle a class as active. Returns True if now active, False if deactivated."""
|
154
|
+
if self.active_class_id == class_id:
|
155
|
+
self.active_class_id = None
|
156
|
+
return False
|
157
|
+
else:
|
158
|
+
self.active_class_id = class_id
|
159
|
+
return True
|
160
|
+
|
161
|
+
def _update_next_class_id(self) -> None:
|
162
|
+
"""Update the next available class ID."""
|
163
|
+
all_ids = {
|
164
|
+
seg.get("class_id")
|
165
|
+
for seg in self.segments
|
166
|
+
if seg.get("class_id") is not None
|
167
|
+
}
|
168
|
+
if not all_ids:
|
169
|
+
self.next_class_id = 0
|
170
|
+
else:
|
171
|
+
self.next_class_id = max(all_ids) + 1
|
lazylabel/main.py
CHANGED
@@ -1,36 +1,36 @@
|
|
1
|
-
"""Main entry point for LazyLabel application."""
|
2
|
-
|
3
|
-
import sys
|
4
|
-
import qdarktheme
|
5
|
-
from PyQt6.QtWidgets import QApplication
|
6
|
-
|
7
|
-
from .ui.main_window import MainWindow
|
8
|
-
|
9
|
-
|
10
|
-
def main():
|
11
|
-
"""Main application entry point."""
|
12
|
-
print("=" * 50)
|
13
|
-
print("LazyLabel - AI-Assisted Image Labeling")
|
14
|
-
print("=" * 50)
|
15
|
-
print()
|
16
|
-
|
17
|
-
print("[1/20] Initializing application...")
|
18
|
-
app = QApplication(sys.argv)
|
19
|
-
|
20
|
-
print("[2/20] Applying dark theme...")
|
21
|
-
qdarktheme.setup_theme()
|
22
|
-
|
23
|
-
main_window = MainWindow()
|
24
|
-
|
25
|
-
print("[19/20] Showing main window...")
|
26
|
-
main_window.show()
|
27
|
-
|
28
|
-
print()
|
29
|
-
print("[20/20] LazyLabel is ready! Happy labeling!")
|
30
|
-
print("=" * 50)
|
31
|
-
|
32
|
-
sys.exit(app.exec())
|
33
|
-
|
34
|
-
|
35
|
-
if __name__ == "__main__":
|
36
|
-
main()
|
1
|
+
"""Main entry point for LazyLabel application."""
|
2
|
+
|
3
|
+
import sys
|
4
|
+
import qdarktheme
|
5
|
+
from PyQt6.QtWidgets import QApplication
|
6
|
+
|
7
|
+
from .ui.main_window import MainWindow
|
8
|
+
|
9
|
+
|
10
|
+
def main():
|
11
|
+
"""Main application entry point."""
|
12
|
+
print("=" * 50)
|
13
|
+
print("LazyLabel - AI-Assisted Image Labeling")
|
14
|
+
print("=" * 50)
|
15
|
+
print()
|
16
|
+
|
17
|
+
print("[1/20] Initializing application...")
|
18
|
+
app = QApplication(sys.argv)
|
19
|
+
|
20
|
+
print("[2/20] Applying dark theme...")
|
21
|
+
qdarktheme.setup_theme()
|
22
|
+
|
23
|
+
main_window = MainWindow()
|
24
|
+
|
25
|
+
print("[19/20] Showing main window...")
|
26
|
+
main_window.show()
|
27
|
+
|
28
|
+
print()
|
29
|
+
print("[20/20] LazyLabel is ready! Happy labeling!")
|
30
|
+
print("=" * 50)
|
31
|
+
|
32
|
+
sys.exit(app.exec())
|
33
|
+
|
34
|
+
|
35
|
+
if __name__ == "__main__":
|
36
|
+
main()
|
lazylabel/models/__init__.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
"""Model-related modules."""
|
2
|
-
|
3
|
-
from .sam_model import SamModel
|
4
|
-
|
1
|
+
"""Model-related modules."""
|
2
|
+
|
3
|
+
from .sam_model import SamModel
|
4
|
+
|
5
5
|
__all__ = ['SamModel']
|