lazylabel-gui 1.1.2__py3-none-any.whl → 1.1.3__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 +207 -169
  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 +170 -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 +239 -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 +1787 -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 +314 -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.3.dist-info}/METADATA +243 -197
  33. lazylabel_gui-1.1.3.dist-info/RECORD +37 -0
  34. {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.3.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.3.dist-info}/WHEEL +0 -0
  37. {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.3.dist-info}/entry_points.txt +0 -0
  38. {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.3.dist-info}/top_level.txt +0 -0
@@ -1,171 +1,170 @@
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
+ if "class_id" not in segment_data:
29
+ # Use active class if available, otherwise use next class ID
30
+ if self.active_class_id is not None:
31
+ segment_data["class_id"] = self.active_class_id
32
+ else:
33
+ segment_data["class_id"] = self.next_class_id
34
+ self.segments.append(segment_data)
35
+ self._update_next_class_id()
36
+
37
+ def delete_segments(self, indices: list[int]) -> None:
38
+ """Delete segments by indices."""
39
+ for i in sorted(indices, reverse=True):
40
+ if 0 <= i < len(self.segments):
41
+ del self.segments[i]
42
+ self._update_next_class_id()
43
+
44
+ def assign_segments_to_class(self, indices: list[int]) -> None:
45
+ """Assign selected segments to a class."""
46
+ if not indices:
47
+ return
48
+
49
+ existing_class_ids = [
50
+ self.segments[i]["class_id"]
51
+ for i in indices
52
+ if i < len(self.segments) and self.segments[i].get("class_id") is not None
53
+ ]
54
+
55
+ if existing_class_ids:
56
+ target_class_id = min(existing_class_ids)
57
+ else:
58
+ target_class_id = self.next_class_id
59
+
60
+ for i in indices:
61
+ if i < len(self.segments):
62
+ self.segments[i]["class_id"] = target_class_id
63
+
64
+ self._update_next_class_id()
65
+
66
+ def get_unique_class_ids(self) -> list[int]:
67
+ """Get sorted list of unique class IDs."""
68
+ return sorted(
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
+ def rasterize_polygon(
77
+ self, vertices: list[QPointF], image_size: tuple[int, int]
78
+ ) -> np.ndarray | None:
79
+ """Convert polygon vertices to binary mask."""
80
+ if not vertices:
81
+ return None
82
+
83
+ h, w = image_size
84
+ points_np = np.array([[p.x(), p.y()] for p in vertices], dtype=np.int32)
85
+ mask = np.zeros((h, w), dtype=np.uint8)
86
+ cv2.fillPoly(mask, [points_np], 1)
87
+ return mask.astype(bool)
88
+
89
+ def create_final_mask_tensor(
90
+ self, image_size: tuple[int, int], class_order: list[int]
91
+ ) -> np.ndarray:
92
+ """Create final mask tensor for saving."""
93
+ h, w = image_size
94
+ id_map = {old_id: new_id for new_id, old_id in enumerate(class_order)}
95
+ num_final_classes = len(class_order)
96
+ final_mask_tensor = np.zeros((h, w, num_final_classes), dtype=np.uint8)
97
+
98
+ for seg in self.segments:
99
+ class_id = seg.get("class_id")
100
+ if class_id not in id_map:
101
+ continue
102
+
103
+ new_channel_idx = id_map[class_id]
104
+
105
+ if seg["type"] == "Polygon":
106
+ mask = self.rasterize_polygon(seg["vertices"], image_size)
107
+ else:
108
+ mask = seg.get("mask")
109
+
110
+ if mask is not None:
111
+ final_mask_tensor[:, :, new_channel_idx] = np.logical_or(
112
+ final_mask_tensor[:, :, new_channel_idx], mask
113
+ )
114
+
115
+ return final_mask_tensor
116
+
117
+ def reassign_class_ids(self, new_order: list[int]) -> None:
118
+ """Reassign class IDs based on new order."""
119
+ id_map = {old_id: new_id for new_id, old_id in enumerate(new_order)}
120
+
121
+ for seg in self.segments:
122
+ old_id = seg.get("class_id")
123
+ if old_id in id_map:
124
+ seg["class_id"] = id_map[old_id]
125
+
126
+ # Update aliases
127
+ new_aliases = {
128
+ id_map[old_id]: self.class_aliases.get(old_id, str(old_id))
129
+ for old_id in new_order
130
+ if old_id in self.class_aliases
131
+ }
132
+ self.class_aliases = new_aliases
133
+ self._update_next_class_id()
134
+
135
+ def set_class_alias(self, class_id: int, alias: str) -> None:
136
+ """Set alias for a class."""
137
+ self.class_aliases[class_id] = alias
138
+
139
+ def get_class_alias(self, class_id: int) -> str:
140
+ """Get alias for a class."""
141
+ return self.class_aliases.get(class_id, str(class_id))
142
+
143
+ def set_active_class(self, class_id: int | None) -> None:
144
+ """Set the active class ID."""
145
+ self.active_class_id = class_id
146
+
147
+ def get_active_class(self) -> int | None:
148
+ """Get the active class ID."""
149
+ return self.active_class_id
150
+
151
+ def toggle_active_class(self, class_id: int) -> bool:
152
+ """Toggle a class as active. Returns True if now active, False if deactivated."""
153
+ if self.active_class_id == class_id:
154
+ self.active_class_id = None
155
+ return False
156
+ else:
157
+ self.active_class_id = class_id
158
+ return True
159
+
160
+ def _update_next_class_id(self) -> None:
161
+ """Update the next available class ID."""
162
+ all_ids = {
163
+ seg.get("class_id")
164
+ for seg in self.segments
165
+ if seg.get("class_id") is not None
166
+ }
167
+ if not all_ids:
168
+ self.next_class_id = 0
169
+ else:
170
+ 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"]