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