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.
Files changed (38) hide show
  1. lazylabel/__init__.py +8 -8
  2. lazylabel/config/__init__.py +6 -6
  3. lazylabel/config/hotkeys.py +168 -168
  4. lazylabel/config/paths.py +40 -40
  5. lazylabel/config/settings.py +65 -65
  6. lazylabel/core/__init__.py +6 -6
  7. lazylabel/core/file_manager.py +105 -105
  8. lazylabel/core/model_manager.py +97 -97
  9. lazylabel/core/segment_manager.py +171 -171
  10. lazylabel/main.py +36 -36
  11. lazylabel/models/__init__.py +4 -4
  12. lazylabel/models/sam_model.py +195 -195
  13. lazylabel/ui/__init__.py +7 -7
  14. lazylabel/ui/control_panel.py +241 -237
  15. lazylabel/ui/editable_vertex.py +64 -51
  16. lazylabel/ui/hotkey_dialog.py +383 -383
  17. lazylabel/ui/hoverable_pixelmap_item.py +22 -22
  18. lazylabel/ui/hoverable_polygon_item.py +39 -39
  19. lazylabel/ui/main_window.py +1659 -1546
  20. lazylabel/ui/numeric_table_widget_item.py +9 -9
  21. lazylabel/ui/photo_viewer.py +54 -54
  22. lazylabel/ui/reorderable_class_table.py +61 -61
  23. lazylabel/ui/right_panel.py +315 -315
  24. lazylabel/ui/widgets/__init__.py +8 -8
  25. lazylabel/ui/widgets/adjustments_widget.py +108 -107
  26. lazylabel/ui/widgets/model_selection_widget.py +93 -93
  27. lazylabel/ui/widgets/settings_widget.py +105 -105
  28. lazylabel/ui/widgets/status_bar.py +109 -109
  29. lazylabel/utils/__init__.py +5 -5
  30. lazylabel/utils/custom_file_system_model.py +132 -132
  31. lazylabel/utils/utils.py +12 -12
  32. {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/METADATA +197 -197
  33. lazylabel_gui-1.1.2.dist-info/RECORD +37 -0
  34. {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/licenses/LICENSE +21 -21
  35. lazylabel_gui-1.1.1.dist-info/RECORD +0 -37
  36. {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/WHEEL +0 -0
  37. {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/entry_points.txt +0 -0
  38. {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()
@@ -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']