lazylabel-gui 1.1.4__py3-none-any.whl → 1.1.6__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.
@@ -1,183 +1,188 @@
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
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 and contains QPointF objects
40
+ if (
41
+ segment_data.get("type") == "Polygon"
42
+ and segment_data.get("vertices")
43
+ and segment_data["vertices"]
44
+ and isinstance(segment_data["vertices"][0], QPointF)
45
+ ):
46
+ segment_data["vertices"] = [
47
+ [p.x(), p.y()] for p in segment_data["vertices"]
48
+ ]
49
+
50
+ self.segments.append(segment_data)
51
+ self._update_next_class_id()
52
+
53
+ def delete_segments(self, indices: list[int]) -> None:
54
+ """Delete segments by indices."""
55
+ for i in sorted(indices, reverse=True):
56
+ if 0 <= i < len(self.segments):
57
+ del self.segments[i]
58
+ self._update_next_class_id()
59
+
60
+ def assign_segments_to_class(self, indices: list[int]) -> None:
61
+ """Assign selected segments to a class."""
62
+ if not indices:
63
+ return
64
+
65
+ existing_class_ids = [
66
+ self.segments[i]["class_id"]
67
+ for i in indices
68
+ if i < len(self.segments) and self.segments[i].get("class_id") is not None
69
+ ]
70
+
71
+ if existing_class_ids:
72
+ target_class_id = min(existing_class_ids)
73
+ else:
74
+ target_class_id = self.next_class_id
75
+
76
+ for i in indices:
77
+ if i < len(self.segments):
78
+ self.segments[i]["class_id"] = target_class_id
79
+
80
+ self._update_next_class_id()
81
+
82
+ def get_unique_class_ids(self) -> list[int]:
83
+ """Get sorted list of unique class IDs."""
84
+ return sorted(
85
+ {
86
+ seg.get("class_id")
87
+ for seg in self.segments
88
+ if seg.get("class_id") is not None
89
+ }
90
+ )
91
+
92
+ def rasterize_polygon(
93
+ self, vertices: list[QPointF], image_size: tuple[int, int]
94
+ ) -> np.ndarray | None:
95
+ """Convert polygon vertices to binary mask."""
96
+ if not vertices:
97
+ return None
98
+
99
+ h, w = image_size
100
+ points_np = np.array([[p.x(), p.y()] for p in vertices], dtype=np.int32)
101
+ mask = np.zeros((h, w), dtype=np.uint8)
102
+ cv2.fillPoly(mask, [points_np], 1)
103
+ return mask.astype(bool)
104
+
105
+ def create_final_mask_tensor(
106
+ self, image_size: tuple[int, int], class_order: list[int]
107
+ ) -> np.ndarray:
108
+ """Create final mask tensor for saving."""
109
+ h, w = image_size
110
+ id_map = {old_id: new_id for new_id, old_id in enumerate(class_order)}
111
+ num_final_classes = len(class_order)
112
+ final_mask_tensor = np.zeros((h, w, num_final_classes), dtype=np.uint8)
113
+
114
+ for seg in self.segments:
115
+ class_id = seg.get("class_id")
116
+ if class_id not in id_map:
117
+ continue
118
+
119
+ new_channel_idx = id_map[class_id]
120
+
121
+ if seg["type"] == "Polygon":
122
+ # Convert stored list of lists back to QPointF objects for rasterization
123
+ qpoints = [QPointF(p[0], p[1]) for p in seg["vertices"]]
124
+ mask = self.rasterize_polygon(qpoints, image_size)
125
+ else:
126
+ mask = seg.get("mask")
127
+
128
+ if mask is not None:
129
+ final_mask_tensor[:, :, new_channel_idx] = np.logical_or(
130
+ final_mask_tensor[:, :, new_channel_idx], mask
131
+ )
132
+
133
+ return final_mask_tensor
134
+
135
+ def reassign_class_ids(self, new_order: list[int]) -> None:
136
+ """Reassign class IDs based on new order."""
137
+ id_map = {old_id: new_id for new_id, old_id in enumerate(new_order)}
138
+
139
+ for seg in self.segments:
140
+ old_id = seg.get("class_id")
141
+ if old_id in id_map:
142
+ seg["class_id"] = id_map[old_id]
143
+
144
+ # Update aliases
145
+ new_aliases = {
146
+ id_map[old_id]: self.class_aliases.get(old_id, str(old_id))
147
+ for old_id in new_order
148
+ if old_id in self.class_aliases
149
+ }
150
+ self.class_aliases = new_aliases
151
+ self._update_next_class_id()
152
+
153
+ def set_class_alias(self, class_id: int, alias: str) -> None:
154
+ """Set alias for a class."""
155
+ self.class_aliases[class_id] = alias
156
+
157
+ def get_class_alias(self, class_id: int) -> str:
158
+ """Get alias for a class."""
159
+ return self.class_aliases.get(class_id, str(class_id))
160
+
161
+ def set_active_class(self, class_id: int | None) -> None:
162
+ """Set the active class ID."""
163
+ self.active_class_id = class_id
164
+
165
+ def get_active_class(self) -> int | None:
166
+ """Get the active class ID."""
167
+ return self.active_class_id
168
+
169
+ def toggle_active_class(self, class_id: int) -> bool:
170
+ """Toggle a class as active. Returns True if now active, False if deactivated."""
171
+ if self.active_class_id == class_id:
172
+ self.active_class_id = None
173
+ return False
174
+ else:
175
+ self.active_class_id = class_id
176
+ return True
177
+
178
+ def _update_next_class_id(self) -> None:
179
+ """Update the next available class ID."""
180
+ all_ids = {
181
+ seg.get("class_id")
182
+ for seg in self.segments
183
+ if seg.get("class_id") is not None
184
+ }
185
+ if not all_ids:
186
+ self.next_class_id = 0
187
+ else:
188
+ self.next_class_id = max(all_ids) + 1
lazylabel/main.py CHANGED
@@ -1,37 +1,39 @@
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
+ """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
+ from .utils.logger import logger
10
+
11
+
12
+ def main():
13
+ """Main application entry point."""
14
+
15
+ logger.info("=" * 50)
16
+ logger.info("Step 1/8: LazyLabel - AI-Assisted Image Labeling")
17
+ logger.info("=" * 50)
18
+ logger.info("")
19
+
20
+ logger.info("Step 2/8: Initializing application...")
21
+ app = QApplication(sys.argv)
22
+
23
+ logger.info("Step 3/8: Applying dark theme...")
24
+ qdarktheme.setup_theme()
25
+
26
+ main_window = MainWindow()
27
+
28
+ logger.info("Step 7/8: Showing main window...")
29
+ main_window.show()
30
+
31
+ logger.info("")
32
+ logger.info("Step 8/8: LazyLabel is ready! Happy labeling!")
33
+ logger.info("=" * 50)
34
+
35
+ sys.exit(app.exec())
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()