lazylabel-gui 1.1.3__py3-none-any.whl → 1.1.5__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,207 +1,208 @@
1
- """Hotkey management system."""
2
-
3
- import json
4
- import os
5
- from dataclasses import dataclass
6
-
7
- from PyQt6.QtGui import QKeySequence
8
-
9
-
10
- @dataclass
11
- class HotkeyAction:
12
- """Represents a hotkey action with primary and secondary keys."""
13
-
14
- name: str
15
- description: str
16
- primary_key: str
17
- secondary_key: str | None = None
18
- category: str = "General"
19
- mouse_related: bool = False # Cannot be reassigned if True
20
-
21
-
22
- class HotkeyManager:
23
- """Manages application hotkeys with persistence."""
24
-
25
- def __init__(self, config_dir: str):
26
- self.config_dir = config_dir
27
- self.hotkeys_file = os.path.join(config_dir, "hotkeys.json")
28
- self.actions: dict[str, HotkeyAction] = {}
29
- self._initialize_default_hotkeys()
30
- self.load_hotkeys()
31
-
32
- def _initialize_default_hotkeys(self):
33
- """Initialize default hotkey mappings."""
34
- default_hotkeys = [
35
- # Navigation
36
- HotkeyAction(
37
- "load_next_image", "Load Next Image", "Right", category="Navigation"
38
- ),
39
- HotkeyAction(
40
- "load_previous_image",
41
- "Load Previous Image",
42
- "Left",
43
- category="Navigation",
44
- ),
45
- HotkeyAction("fit_view", "Fit View", ".", category="Navigation"),
46
- # Modes
47
- HotkeyAction("sam_mode", "Point Mode (SAM)", "1", category="Modes"),
48
- HotkeyAction("polygon_mode", "Polygon Mode", "2", category="Modes"),
49
- HotkeyAction("selection_mode", "Selection Mode", "E", category="Modes"),
50
- HotkeyAction("pan_mode", "Pan Mode", "Q", category="Modes"),
51
- HotkeyAction("edit_mode", "Edit Mode", "R", category="Modes"),
52
- # Actions
53
- HotkeyAction(
54
- "clear_points", "Clear Points/Vertices", "C", category="Actions"
55
- ),
56
- HotkeyAction(
57
- "save_segment", "Save Current Segment", "Space", category="Actions"
58
- ),
59
- HotkeyAction("save_output", "Save Output", "Return", category="Actions"),
60
- HotkeyAction(
61
- "save_output_alt", "Save Output (Alt)", "Enter", category="Actions"
62
- ),
63
- HotkeyAction("undo", "Undo Last Action", "Ctrl+Z", category="Actions"),
64
- HotkeyAction(
65
- "redo", "Redo Last Action", "Ctrl+Y", "Ctrl+Shift+Z", category="Actions"
66
- ),
67
- HotkeyAction(
68
- "escape", "Cancel/Clear Selection", "Escape", category="Actions"
69
- ),
70
- # Segments
71
- HotkeyAction(
72
- "merge_segments", "Merge Selected Segments", "M", category="Segments"
73
- ),
74
- HotkeyAction(
75
- "delete_segments", "Delete Selected Segments", "V", category="Segments"
76
- ),
77
- HotkeyAction(
78
- "delete_segments_alt",
79
- "Delete Selected Segments (Alt)",
80
- "Backspace",
81
- category="Segments",
82
- ),
83
- HotkeyAction(
84
- "select_all", "Select All Segments", "Ctrl+A", category="Segments"
85
- ),
86
- # View
87
- HotkeyAction("zoom_in", "Zoom In", "Ctrl+Plus", category="View"),
88
- HotkeyAction("zoom_out", "Zoom Out", "Ctrl+Minus", category="View"),
89
- # Movement (WASD)
90
- HotkeyAction("pan_up", "Pan Up", "W", category="Movement"),
91
- HotkeyAction("pan_down", "Pan Down", "S", category="Movement"),
92
- HotkeyAction("pan_left", "Pan Left", "A", category="Movement"),
93
- HotkeyAction("pan_right", "Pan Right", "D", category="Movement"),
94
- # Mouse-related (cannot be reassigned)
95
- HotkeyAction(
96
- "left_click",
97
- "Add Positive Point / Select",
98
- "Left Click",
99
- category="Mouse",
100
- mouse_related=True,
101
- ),
102
- HotkeyAction(
103
- "right_click",
104
- "Add Negative Point",
105
- "Right Click",
106
- category="Mouse",
107
- mouse_related=True,
108
- ),
109
- HotkeyAction(
110
- "mouse_drag",
111
- "Drag/Pan",
112
- "Mouse Drag",
113
- category="Mouse",
114
- mouse_related=True,
115
- ),
116
- ]
117
-
118
- for action in default_hotkeys:
119
- self.actions[action.name] = action
120
-
121
- def get_action(self, action_name: str) -> HotkeyAction | None:
122
- """Get hotkey action by name."""
123
- return self.actions.get(action_name)
124
-
125
- def get_actions_by_category(self) -> dict[str, list[HotkeyAction]]:
126
- """Get actions grouped by category."""
127
- categories = {}
128
- for action in self.actions.values():
129
- if action.category not in categories:
130
- categories[action.category] = []
131
- categories[action.category].append(action)
132
- return categories
133
-
134
- def set_primary_key(self, action_name: str, key: str) -> bool:
135
- """Set primary key for an action."""
136
- if action_name in self.actions and not self.actions[action_name].mouse_related:
137
- self.actions[action_name].primary_key = key
138
- return True
139
- return False
140
-
141
- def set_secondary_key(self, action_name: str, key: str | None) -> bool:
142
- """Set secondary key for an action."""
143
- if action_name in self.actions and not self.actions[action_name].mouse_related:
144
- self.actions[action_name].secondary_key = key
145
- return True
146
- return False
147
-
148
- def get_key_for_action(self, action_name: str) -> tuple[str | None, str | None]:
149
- """Get primary and secondary keys for an action."""
150
- action = self.actions.get(action_name)
151
- if action:
152
- return action.primary_key, action.secondary_key
153
- return None, None
154
-
155
- def is_key_in_use(self, key: str, exclude_action: str = None) -> str | None:
156
- """Check if a key is already in use by another action."""
157
- for name, action in self.actions.items():
158
- if name == exclude_action:
159
- continue
160
- if action.primary_key == key or action.secondary_key == key:
161
- return name
162
- return None
163
-
164
- def reset_to_defaults(self):
165
- """Reset all hotkeys to default values."""
166
- self._initialize_default_hotkeys()
167
-
168
- def save_hotkeys(self):
169
- """Save hotkeys to file."""
170
- os.makedirs(self.config_dir, exist_ok=True)
171
-
172
- # Convert to serializable format
173
- data = {}
174
- for name, action in self.actions.items():
175
- if not action.mouse_related: # Don't save mouse-related actions
176
- data[name] = {
177
- "primary_key": action.primary_key,
178
- "secondary_key": action.secondary_key,
179
- }
180
-
181
- with open(self.hotkeys_file, "w") as f:
182
- json.dump(data, f, indent=4)
183
-
184
- def load_hotkeys(self):
185
- """Load hotkeys from file."""
186
- if not os.path.exists(self.hotkeys_file):
187
- return
188
-
189
- try:
190
- with open(self.hotkeys_file) as f:
191
- data = json.load(f)
192
-
193
- for name, keys in data.items():
194
- if name in self.actions and not self.actions[name].mouse_related:
195
- self.actions[name].primary_key = keys.get("primary_key", "")
196
- self.actions[name].secondary_key = keys.get("secondary_key")
197
- except (json.JSONDecodeError, KeyError, FileNotFoundError):
198
- # If loading fails, keep defaults
199
- pass
200
-
201
- def key_sequence_to_string(self, key_sequence: QKeySequence) -> str:
202
- """Convert QKeySequence to string representation."""
203
- return key_sequence.toString()
204
-
205
- def string_to_key_sequence(self, key_string: str) -> QKeySequence:
206
- """Convert string to QKeySequence."""
207
- return QKeySequence(key_string)
1
+ """Hotkey management system."""
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass
6
+
7
+ from PyQt6.QtGui import QKeySequence
8
+
9
+
10
+ @dataclass
11
+ class HotkeyAction:
12
+ """Represents a hotkey action with primary and secondary keys."""
13
+
14
+ name: str
15
+ description: str
16
+ primary_key: str
17
+ secondary_key: str | None = None
18
+ category: str = "General"
19
+ mouse_related: bool = False # Cannot be reassigned if True
20
+
21
+
22
+ class HotkeyManager:
23
+ """Manages application hotkeys with persistence."""
24
+
25
+ def __init__(self, config_dir: str):
26
+ self.config_dir = config_dir
27
+ self.hotkeys_file = os.path.join(config_dir, "hotkeys.json")
28
+ self.actions: dict[str, HotkeyAction] = {}
29
+ self._initialize_default_hotkeys()
30
+ self.load_hotkeys()
31
+
32
+ def _initialize_default_hotkeys(self):
33
+ """Initialize default hotkey mappings."""
34
+ default_hotkeys = [
35
+ # Navigation
36
+ HotkeyAction(
37
+ "load_next_image", "Load Next Image", "Right", category="Navigation"
38
+ ),
39
+ HotkeyAction(
40
+ "load_previous_image",
41
+ "Load Previous Image",
42
+ "Left",
43
+ category="Navigation",
44
+ ),
45
+ HotkeyAction("fit_view", "Fit View", ".", category="Navigation"),
46
+ # Modes
47
+ HotkeyAction("sam_mode", "Point Mode (SAM)", "1", category="Modes"),
48
+ HotkeyAction("polygon_mode", "Polygon Mode", "2", category="Modes"),
49
+ HotkeyAction("bbox_mode", "Bounding Box Mode", "3", category="Modes"),
50
+ HotkeyAction("selection_mode", "Selection Mode", "E", category="Modes"),
51
+ HotkeyAction("pan_mode", "Pan Mode", "Q", category="Modes"),
52
+ HotkeyAction("edit_mode", "Edit Mode", "R", category="Modes"),
53
+ # Actions
54
+ HotkeyAction(
55
+ "clear_points", "Clear Points/Vertices", "C", category="Actions"
56
+ ),
57
+ HotkeyAction(
58
+ "save_segment", "Save Current Segment", "Space", category="Actions"
59
+ ),
60
+ HotkeyAction("save_output", "Save Output", "Return", category="Actions"),
61
+ HotkeyAction(
62
+ "save_output_alt", "Save Output (Alt)", "Enter", category="Actions"
63
+ ),
64
+ HotkeyAction("undo", "Undo Last Action", "Ctrl+Z", category="Actions"),
65
+ HotkeyAction(
66
+ "redo", "Redo Last Action", "Ctrl+Y", "Ctrl+Shift+Z", category="Actions"
67
+ ),
68
+ HotkeyAction(
69
+ "escape", "Cancel/Clear Selection", "Escape", category="Actions"
70
+ ),
71
+ # Segments
72
+ HotkeyAction(
73
+ "merge_segments", "Merge Selected Segments", "M", category="Segments"
74
+ ),
75
+ HotkeyAction(
76
+ "delete_segments", "Delete Selected Segments", "V", category="Segments"
77
+ ),
78
+ HotkeyAction(
79
+ "delete_segments_alt",
80
+ "Delete Selected Segments (Alt)",
81
+ "Backspace",
82
+ category="Segments",
83
+ ),
84
+ HotkeyAction(
85
+ "select_all", "Select All Segments", "Ctrl+A", category="Segments"
86
+ ),
87
+ # View
88
+ HotkeyAction("zoom_in", "Zoom In", "Ctrl+Plus", category="View"),
89
+ HotkeyAction("zoom_out", "Zoom Out", "Ctrl+Minus", category="View"),
90
+ # Movement (WASD)
91
+ HotkeyAction("pan_up", "Pan Up", "W", category="Movement"),
92
+ HotkeyAction("pan_down", "Pan Down", "S", category="Movement"),
93
+ HotkeyAction("pan_left", "Pan Left", "A", category="Movement"),
94
+ HotkeyAction("pan_right", "Pan Right", "D", category="Movement"),
95
+ # Mouse-related (cannot be reassigned)
96
+ HotkeyAction(
97
+ "left_click",
98
+ "Add Positive Point / Select",
99
+ "Left Click",
100
+ category="Mouse",
101
+ mouse_related=True,
102
+ ),
103
+ HotkeyAction(
104
+ "right_click",
105
+ "Add Negative Point",
106
+ "Right Click",
107
+ category="Mouse",
108
+ mouse_related=True,
109
+ ),
110
+ HotkeyAction(
111
+ "mouse_drag",
112
+ "Drag/Pan",
113
+ "Mouse Drag",
114
+ category="Mouse",
115
+ mouse_related=True,
116
+ ),
117
+ ]
118
+
119
+ for action in default_hotkeys:
120
+ self.actions[action.name] = action
121
+
122
+ def get_action(self, action_name: str) -> HotkeyAction | None:
123
+ """Get hotkey action by name."""
124
+ return self.actions.get(action_name)
125
+
126
+ def get_actions_by_category(self) -> dict[str, list[HotkeyAction]]:
127
+ """Get actions grouped by category."""
128
+ categories = {}
129
+ for action in self.actions.values():
130
+ if action.category not in categories:
131
+ categories[action.category] = []
132
+ categories[action.category].append(action)
133
+ return categories
134
+
135
+ def set_primary_key(self, action_name: str, key: str) -> bool:
136
+ """Set primary key for an action."""
137
+ if action_name in self.actions and not self.actions[action_name].mouse_related:
138
+ self.actions[action_name].primary_key = key
139
+ return True
140
+ return False
141
+
142
+ def set_secondary_key(self, action_name: str, key: str | None) -> bool:
143
+ """Set secondary key for an action."""
144
+ if action_name in self.actions and not self.actions[action_name].mouse_related:
145
+ self.actions[action_name].secondary_key = key
146
+ return True
147
+ return False
148
+
149
+ def get_key_for_action(self, action_name: str) -> tuple[str | None, str | None]:
150
+ """Get primary and secondary keys for an action."""
151
+ action = self.actions.get(action_name)
152
+ if action:
153
+ return action.primary_key, action.secondary_key
154
+ return None, None
155
+
156
+ def is_key_in_use(self, key: str, exclude_action: str = None) -> str | None:
157
+ """Check if a key is already in use by another action."""
158
+ for name, action in self.actions.items():
159
+ if name == exclude_action:
160
+ continue
161
+ if action.primary_key == key or action.secondary_key == key:
162
+ return name
163
+ return None
164
+
165
+ def reset_to_defaults(self):
166
+ """Reset all hotkeys to default values."""
167
+ self._initialize_default_hotkeys()
168
+
169
+ def save_hotkeys(self):
170
+ """Save hotkeys to file."""
171
+ os.makedirs(self.config_dir, exist_ok=True)
172
+
173
+ # Convert to serializable format
174
+ data = {}
175
+ for name, action in self.actions.items():
176
+ if not action.mouse_related: # Don't save mouse-related actions
177
+ data[name] = {
178
+ "primary_key": action.primary_key,
179
+ "secondary_key": action.secondary_key,
180
+ }
181
+
182
+ with open(self.hotkeys_file, "w") as f:
183
+ json.dump(data, f, indent=4)
184
+
185
+ def load_hotkeys(self):
186
+ """Load hotkeys from file."""
187
+ if not os.path.exists(self.hotkeys_file):
188
+ return
189
+
190
+ try:
191
+ with open(self.hotkeys_file) as f:
192
+ data = json.load(f)
193
+
194
+ for name, keys in data.items():
195
+ if name in self.actions and not self.actions[name].mouse_related:
196
+ self.actions[name].primary_key = keys.get("primary_key", "")
197
+ self.actions[name].secondary_key = keys.get("secondary_key")
198
+ except (json.JSONDecodeError, KeyError, FileNotFoundError):
199
+ # If loading fails, keep defaults
200
+ pass
201
+
202
+ def key_sequence_to_string(self, key_sequence: QKeySequence) -> str:
203
+ """Convert QKeySequence to string representation."""
204
+ return key_sequence.toString()
205
+
206
+ def string_to_key_sequence(self, key_string: str) -> QKeySequence:
207
+ """Convert string to QKeySequence."""
208
+ return QKeySequence(key_string)
@@ -1,170 +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
- 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
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
@@ -19,6 +19,7 @@ class ControlPanel(QWidget):
19
19
  # Signals
20
20
  sam_mode_requested = pyqtSignal()
21
21
  polygon_mode_requested = pyqtSignal()
22
+ bbox_mode_requested = pyqtSignal() # New signal for bounding box mode
22
23
  selection_mode_requested = pyqtSignal()
23
24
  clear_points_requested = pyqtSignal()
24
25
  fit_view_requested = pyqtSignal()
@@ -117,11 +118,15 @@ class ControlPanel(QWidget):
117
118
  self.btn_polygon_mode = QPushButton("Polygon Mode (2)")
118
119
  self.btn_polygon_mode.setToolTip("Switch to Polygon Drawing Mode (2)")
119
120
 
121
+ self.btn_bbox_mode = QPushButton("BBox Mode (3)")
122
+ self.btn_bbox_mode.setToolTip("Switch to Bounding Box Drawing Mode (3)")
123
+
120
124
  self.btn_selection_mode = QPushButton("Selection Mode (E)")
121
125
  self.btn_selection_mode.setToolTip("Toggle segment selection (E)")
122
126
 
123
127
  layout.addWidget(self.btn_sam_mode)
124
128
  layout.addWidget(self.btn_polygon_mode)
129
+ layout.addWidget(self.btn_bbox_mode)
125
130
  layout.addWidget(self.btn_selection_mode)
126
131
 
127
132
  def _add_action_buttons(self, layout):
@@ -149,6 +154,7 @@ class ControlPanel(QWidget):
149
154
  """Connect internal signals."""
150
155
  self.btn_sam_mode.clicked.connect(self.sam_mode_requested)
151
156
  self.btn_polygon_mode.clicked.connect(self.polygon_mode_requested)
157
+ self.btn_bbox_mode.clicked.connect(self.bbox_mode_requested)
152
158
  self.btn_selection_mode.clicked.connect(self.selection_mode_requested)
153
159
  self.btn_clear_points.clicked.connect(self.clear_points_requested)
154
160
  self.btn_fit_view.clicked.connect(self.fit_view_requested)
@@ -55,8 +55,8 @@ class EditableVertexItem(QGraphicsEllipseItem):
55
55
  "type": "move_vertex",
56
56
  "segment_index": self.segment_index,
57
57
  "vertex_index": self.vertex_index,
58
- "old_pos": self.initial_pos,
59
- "new_pos": self.pos(),
58
+ "old_pos": [self.initial_pos.x(), self.initial_pos.y()],
59
+ "new_pos": [self.pos().x(), self.pos().y()],
60
60
  }
61
61
  )
62
62
  self.initial_pos = None
@@ -4,7 +4,7 @@ import os
4
4
 
5
5
  import cv2
6
6
  import numpy as np
7
- from PyQt6.QtCore import QModelIndex, QPointF, Qt, QTimer, pyqtSignal
7
+ from PyQt6.QtCore import QModelIndex, QPointF, QRectF, Qt, QTimer, pyqtSignal
8
8
  from PyQt6.QtGui import (
9
9
  QBrush,
10
10
  QColor,
@@ -22,6 +22,7 @@ from PyQt6.QtWidgets import (
22
22
  QGraphicsEllipseItem,
23
23
  QGraphicsLineItem,
24
24
  QGraphicsPolygonItem,
25
+ QGraphicsRectItem,
25
26
  QMainWindow,
26
27
  QSplitter,
27
28
  QTableWidgetItem,
@@ -116,6 +117,7 @@ class MainWindow(QMainWindow):
116
117
  self.point_items, self.positive_points, self.negative_points = [], [], []
117
118
  self.polygon_points, self.polygon_preview_items = [], []
118
119
  self.rubber_band_line = None
120
+ self.rubber_band_rect = None # New attribute for bounding box
119
121
  self.preview_mask_item = None
120
122
  self.segments, self.segment_items, self.highlight_items = [], {}, []
121
123
  self.edit_handles = []
@@ -245,6 +247,7 @@ class MainWindow(QMainWindow):
245
247
  # Control panel connections
246
248
  self.control_panel.sam_mode_requested.connect(self.set_sam_mode)
247
249
  self.control_panel.polygon_mode_requested.connect(self.set_polygon_mode)
250
+ self.control_panel.bbox_mode_requested.connect(self.set_bbox_mode)
248
251
  self.control_panel.selection_mode_requested.connect(self.toggle_selection_mode)
249
252
  self.control_panel.clear_points_requested.connect(self.clear_all_points)
250
253
  self.control_panel.fit_view_requested.connect(self.viewer.fitInView)
@@ -302,6 +305,7 @@ class MainWindow(QMainWindow):
302
305
  "load_previous_image": self._load_previous_image,
303
306
  "sam_mode": self.set_sam_mode,
304
307
  "polygon_mode": self.set_polygon_mode,
308
+ "bbox_mode": self.set_bbox_mode,
305
309
  "selection_mode": self.toggle_selection_mode,
306
310
  "pan_mode": self.toggle_pan_mode,
307
311
  "edit_mode": self.toggle_edit_mode,
@@ -376,6 +380,10 @@ class MainWindow(QMainWindow):
376
380
  """Set polygon drawing mode."""
377
381
  self._set_mode("polygon")
378
382
 
383
+ def set_bbox_mode(self):
384
+ """Set bounding box drawing mode."""
385
+ self._set_mode("bbox")
386
+
379
387
  def toggle_selection_mode(self):
380
388
  """Toggle selection mode."""
381
389
  self._toggle_mode("selection")
@@ -401,6 +409,7 @@ class MainWindow(QMainWindow):
401
409
  cursor_map = {
402
410
  "sam_points": Qt.CursorShape.CrossCursor,
403
411
  "polygon": Qt.CursorShape.CrossCursor,
412
+ "bbox": Qt.CursorShape.CrossCursor,
404
413
  "selection": Qt.CursorShape.ArrowCursor,
405
414
  "edit": Qt.CursorShape.SizeAllCursor,
406
415
  "pan": Qt.CursorShape.OpenHandCursor,
@@ -521,6 +530,9 @@ class MainWindow(QMainWindow):
521
530
  path = self.file_model.filePath(index)
522
531
 
523
532
  if os.path.isfile(path) and self.file_manager.is_image_file(path):
533
+ if path == self.current_image_path: # Only reset if loading a new image
534
+ return
535
+
524
536
  self.current_image_path = path
525
537
  pixmap = QPixmap(self.current_image_path)
526
538
  if not pixmap.isNull():
@@ -613,7 +625,9 @@ class MainWindow(QMainWindow):
613
625
  highlight_brush = QBrush(QColor(255, 255, 0, 180))
614
626
 
615
627
  if seg["type"] == "Polygon" and seg.get("vertices"):
616
- poly_item = QGraphicsPolygonItem(QPolygonF(seg["vertices"]))
628
+ # Convert stored list of lists back to QPointF objects
629
+ qpoints = [QPointF(p[0], p[1]) for p in seg["vertices"]]
630
+ poly_item = QGraphicsPolygonItem(QPolygonF(qpoints))
617
631
  poly_item.setBrush(highlight_brush)
618
632
  poly_item.setPen(QPen(Qt.GlobalColor.transparent))
619
633
  poly_item.setZValue(99)
@@ -805,13 +819,17 @@ class MainWindow(QMainWindow):
805
819
  self._clear_edit_handles()
806
820
 
807
821
  # Display segments from segment manager
822
+
808
823
  for i, segment in enumerate(self.segment_manager.segments):
809
824
  self.segment_items[i] = []
810
825
  class_id = segment.get("class_id")
811
826
  base_color = self._get_color_for_class(class_id)
812
827
 
813
828
  if segment["type"] == "Polygon" and segment.get("vertices"):
814
- poly_item = HoverablePolygonItem(QPolygonF(segment["vertices"]))
829
+ # Convert stored list of lists back to QPointF objects
830
+ qpoints = [QPointF(p[0], p[1]) for p in segment["vertices"]]
831
+
832
+ poly_item = HoverablePolygonItem(QPolygonF(qpoints))
815
833
  default_brush = QBrush(
816
834
  QColor(base_color.red(), base_color.green(), base_color.blue(), 70)
817
835
  )
@@ -894,7 +912,7 @@ class MainWindow(QMainWindow):
894
912
  return
895
913
 
896
914
  new_segment = {
897
- "vertices": list(self.polygon_points),
915
+ "vertices": [[p.x(), p.y()] for p in self.polygon_points],
898
916
  "type": "Polygon",
899
917
  "mask": None,
900
918
  }
@@ -1052,7 +1070,9 @@ class MainWindow(QMainWindow):
1052
1070
  elif action_type == "move_polygon":
1053
1071
  initial_vertices = last_action.get("initial_vertices")
1054
1072
  for i, vertices in initial_vertices.items():
1055
- self.segment_manager.segments[i]["vertices"] = vertices
1073
+ self.segment_manager.segments[i]["vertices"] = [
1074
+ [p[0], p[1]] for p in vertices
1075
+ ]
1056
1076
  self._update_polygon_item(i)
1057
1077
  self._display_edit_handles()
1058
1078
  self._highlight_selected_segments()
@@ -1061,13 +1081,27 @@ class MainWindow(QMainWindow):
1061
1081
  segment_index = last_action.get("segment_index")
1062
1082
  vertex_index = last_action.get("vertex_index")
1063
1083
  old_pos = last_action.get("old_pos")
1064
- self.segment_manager.segments[segment_index]["vertices"][vertex_index] = (
1065
- old_pos
1066
- )
1067
- self._update_polygon_item(segment_index)
1068
- self._display_edit_handles()
1069
- self._highlight_selected_segments()
1070
- self._show_notification("Undid: Move Vertex")
1084
+ if (
1085
+ segment_index is not None
1086
+ and vertex_index is not None
1087
+ and old_pos is not None
1088
+ ):
1089
+ if segment_index < len(self.segment_manager.segments):
1090
+ self.segment_manager.segments[segment_index]["vertices"][
1091
+ vertex_index
1092
+ ] = old_pos
1093
+ self._update_polygon_item(segment_index)
1094
+ self._display_edit_handles()
1095
+ self._highlight_selected_segments()
1096
+ self._show_notification("Undid: Move Vertex")
1097
+ else:
1098
+ self._show_warning_notification(
1099
+ "Cannot undo: Segment no longer exists"
1100
+ )
1101
+ self.redo_history.pop() # Remove from redo history if segment is gone
1102
+ else:
1103
+ self._show_warning_notification("Cannot undo: Missing vertex data")
1104
+ self.redo_history.pop() # Remove from redo history if data is incomplete
1071
1105
 
1072
1106
  # Add more undo logic for other action types here in the future
1073
1107
  else:
@@ -1129,7 +1163,7 @@ class MainWindow(QMainWindow):
1129
1163
  for i, vertices in final_vertices.items():
1130
1164
  if i < len(self.segment_manager.segments):
1131
1165
  self.segment_manager.segments[i]["vertices"] = [
1132
- QPointF(v.x(), v.y()) for v in vertices
1166
+ [p[0], p[1]] for p in vertices
1133
1167
  ]
1134
1168
  self._update_polygon_item(i)
1135
1169
  self._display_edit_handles()
@@ -1159,10 +1193,10 @@ class MainWindow(QMainWindow):
1159
1193
  self._show_warning_notification(
1160
1194
  "Cannot redo: Segment no longer exists"
1161
1195
  )
1162
- self.action_history.pop()
1196
+ self.action_history.pop() # Remove from action history if segment is gone
1163
1197
  else:
1164
1198
  self._show_warning_notification("Cannot redo: Missing vertex data")
1165
- self.action_history.pop()
1199
+ self.action_history.pop() # Remove from action history if data is incomplete
1166
1200
  else:
1167
1201
  self._show_warning_notification(
1168
1202
  f"Redo for action '{action_type}' not implemented."
@@ -1302,7 +1336,8 @@ class MainWindow(QMainWindow):
1302
1336
  selected_indices = self.right_panel.get_selected_segment_indices()
1303
1337
  self.drag_initial_vertices = {
1304
1338
  i: [
1305
- QPointF(p) for p in self.segment_manager.segments[i]["vertices"]
1339
+ [p.x(), p.y()] if isinstance(p, QPointF) else p
1340
+ for p in self.segment_manager.segments[i]["vertices"]
1306
1341
  ]
1307
1342
  for i in selected_indices
1308
1343
  if self.segment_manager.segments[i].get("type") == "Polygon"
@@ -1312,9 +1347,6 @@ class MainWindow(QMainWindow):
1312
1347
 
1313
1348
  # Call the original scene handler.
1314
1349
  self._original_mouse_press(event)
1315
- # Skip further processing unless we're in selection mode.
1316
- if event.isAccepted() and self.mode != "selection":
1317
- return
1318
1350
 
1319
1351
  if self.is_dragging_polygon:
1320
1352
  return
@@ -1338,6 +1370,14 @@ class MainWindow(QMainWindow):
1338
1370
  elif self.mode == "polygon":
1339
1371
  if event.button() == Qt.MouseButton.LeftButton:
1340
1372
  self._handle_polygon_click(pos)
1373
+ elif self.mode == "bbox":
1374
+ if event.button() == Qt.MouseButton.LeftButton:
1375
+ self.drag_start_pos = pos
1376
+ self.rubber_band_rect = QGraphicsRectItem()
1377
+ self.rubber_band_rect.setPen(
1378
+ QPen(Qt.GlobalColor.red, self.line_thickness, Qt.PenStyle.DashLine)
1379
+ )
1380
+ self.viewer.scene().addItem(self.rubber_band_rect)
1341
1381
  elif self.mode == "selection" and event.button() == Qt.MouseButton.LeftButton:
1342
1382
  self._handle_segment_selection_click(pos)
1343
1383
 
@@ -1346,8 +1386,13 @@ class MainWindow(QMainWindow):
1346
1386
  if self.mode == "edit" and self.is_dragging_polygon:
1347
1387
  delta = event.scenePos() - self.drag_start_pos
1348
1388
  for i, initial_verts in self.drag_initial_vertices.items():
1389
+ # initial_verts are lists, convert to QPointF for addition with delta
1349
1390
  self.segment_manager.segments[i]["vertices"] = [
1350
- QPointF(v) + delta for v in initial_verts
1391
+ [
1392
+ (QPointF(p[0], p[1]) + delta).x(),
1393
+ (QPointF(p[0], p[1]) + delta).y(),
1394
+ ]
1395
+ for p in initial_verts
1351
1396
  ]
1352
1397
  self._update_polygon_item(i)
1353
1398
  self._display_edit_handles() # Redraw handles at new positions
@@ -1357,12 +1402,22 @@ class MainWindow(QMainWindow):
1357
1402
 
1358
1403
  self._original_mouse_move(event)
1359
1404
 
1405
+ if self.mode == "bbox" and self.rubber_band_rect and self.drag_start_pos:
1406
+ current_pos = event.scenePos()
1407
+ rect = QRectF(self.drag_start_pos, current_pos).normalized()
1408
+ self.rubber_band_rect.setRect(rect)
1409
+ event.accept()
1410
+ return
1411
+
1360
1412
  def _scene_mouse_release(self, event):
1361
1413
  """Handle mouse release events in the scene."""
1362
1414
  if self.mode == "edit" and self.is_dragging_polygon:
1363
1415
  # Record the action for undo
1364
1416
  final_vertices = {
1365
- i: list(self.segment_manager.segments[i]["vertices"])
1417
+ i: [
1418
+ [p.x(), p.y()] if isinstance(p, QPointF) else p
1419
+ for p in self.segment_manager.segments[i]["vertices"]
1420
+ ]
1366
1421
  for i in self.drag_initial_vertices
1367
1422
  }
1368
1423
  self.action_history.append(
@@ -1383,6 +1438,41 @@ class MainWindow(QMainWindow):
1383
1438
 
1384
1439
  if self.mode == "pan":
1385
1440
  self.viewer.set_cursor(Qt.CursorShape.OpenHandCursor)
1441
+ elif self.mode == "bbox" and self.rubber_band_rect:
1442
+ self.viewer.scene().removeItem(self.rubber_band_rect)
1443
+ rect = self.rubber_band_rect.rect()
1444
+ self.rubber_band_rect = None
1445
+ self.drag_start_pos = None
1446
+
1447
+ if rect.width() > 0 and rect.height() > 0:
1448
+ # Convert QRectF to QPolygonF
1449
+ polygon = QPolygonF()
1450
+ polygon.append(rect.topLeft())
1451
+ polygon.append(rect.topRight())
1452
+ polygon.append(rect.bottomRight())
1453
+ polygon.append(rect.bottomLeft())
1454
+
1455
+ new_segment = {
1456
+ "vertices": [[p.x(), p.y()] for p in list(polygon)],
1457
+ "type": "Polygon", # Bounding boxes are stored as polygons
1458
+ "mask": None,
1459
+ }
1460
+
1461
+ self.segment_manager.add_segment(new_segment)
1462
+
1463
+ # Record the action for undo
1464
+ self.action_history.append(
1465
+ {
1466
+ "type": "add_segment",
1467
+ "segment_index": len(self.segment_manager.segments) - 1,
1468
+ }
1469
+ )
1470
+ # Clear redo history when a new action is performed
1471
+ self.redo_history.clear()
1472
+ self._update_all_lists()
1473
+ event.accept()
1474
+ return
1475
+
1386
1476
  self._original_mouse_release(event)
1387
1477
 
1388
1478
  def _add_point(self, pos, positive):
@@ -1522,9 +1612,9 @@ class MainWindow(QMainWindow):
1522
1612
  continue
1523
1613
  h = self.viewer._pixmap_item.pixmap().height()
1524
1614
  w = self.viewer._pixmap_item.pixmap().width()
1525
- points_np = np.array(
1526
- [[p.x(), p.y()] for p in seg["vertices"]], dtype=np.int32
1527
- )
1615
+ # Convert stored list of lists back to QPointF objects for rasterization
1616
+ qpoints = [QPointF(p[0], p[1]) for p in seg["vertices"]]
1617
+ points_np = np.array([[p.x(), p.y()] for p in qpoints], dtype=np.int32)
1528
1618
  # Ensure points are within bounds
1529
1619
  points_np = np.clip(points_np, 0, [w - 1, h - 1])
1530
1620
  mask = np.zeros((h, w), dtype=np.uint8)
@@ -1574,7 +1664,8 @@ class MainWindow(QMainWindow):
1574
1664
  for seg_idx in selected_indices:
1575
1665
  seg = self.segment_manager.segments[seg_idx]
1576
1666
  if seg["type"] == "Polygon" and seg.get("vertices"):
1577
- for v_idx, pt in enumerate(seg["vertices"]):
1667
+ for v_idx, pt_list in enumerate(seg["vertices"]):
1668
+ pt = QPointF(pt_list[0], pt_list[1]) # Convert list to QPointF
1578
1669
  handle = EditableVertexItem(
1579
1670
  self,
1580
1671
  seg_idx,
@@ -1610,15 +1701,16 @@ class MainWindow(QMainWindow):
1610
1701
  "type": "move_vertex",
1611
1702
  "segment_index": segment_index,
1612
1703
  "vertex_index": vertex_index,
1613
- "old_pos": old_pos,
1614
- "new_pos": new_pos,
1704
+ "old_pos": [old_pos[0], old_pos[1]], # Store as list
1705
+ "new_pos": [new_pos.x(), new_pos.y()], # Store as list
1615
1706
  }
1616
1707
  )
1617
1708
  # Clear redo history when a new action is performed
1618
1709
  self.redo_history.clear()
1619
- seg["vertices"][vertex_index] = (
1620
- new_pos # new_pos is already the correct scene coordinate
1621
- )
1710
+ seg["vertices"][vertex_index] = [
1711
+ new_pos.x(),
1712
+ new_pos.y(), # Store as list
1713
+ ]
1622
1714
  self._update_polygon_item(segment_index)
1623
1715
  self._highlight_selected_segments() # Keep the highlight in sync with the new shape
1624
1716
 
@@ -1627,9 +1719,12 @@ class MainWindow(QMainWindow):
1627
1719
  items = self.segment_items.get(segment_index, [])
1628
1720
  for item in items:
1629
1721
  if isinstance(item, HoverablePolygonItem):
1630
- item.setPolygon(
1631
- QPolygonF(self.segment_manager.segments[segment_index]["vertices"])
1632
- )
1722
+ # Convert stored list of lists back to QPointF objects
1723
+ qpoints = [
1724
+ QPointF(p[0], p[1])
1725
+ for p in self.segment_manager.segments[segment_index]["vertices"]
1726
+ ]
1727
+ item.setPolygon(QPolygonF(qpoints))
1633
1728
  return
1634
1729
 
1635
1730
  def _handle_class_toggle(self, class_id):
@@ -72,6 +72,12 @@ class RightPanel(QWidget):
72
72
  # Class management section
73
73
  self._setup_class_management(v_splitter)
74
74
 
75
+ # Action history section (new)
76
+ self.action_pane_widget = QWidget() # Placeholder for ActionPane
77
+ action_pane_layout = QVBoxLayout(self.action_pane_widget)
78
+ action_pane_layout.setContentsMargins(0, 0, 0, 0)
79
+ v_splitter.addWidget(self.action_pane_widget)
80
+
75
81
  main_layout.addWidget(v_splitter)
76
82
 
77
83
  # Status label
@@ -81,6 +87,18 @@ class RightPanel(QWidget):
81
87
 
82
88
  self.v_layout.addWidget(self.main_controls_widget)
83
89
 
90
+ def set_action_pane(self, action_pane_widget):
91
+ """Sets the ActionPane widget into the right panel."""
92
+ # Clear existing layout in the placeholder widget
93
+ while self.action_pane_widget.layout().count():
94
+ item = self.action_pane_widget.layout().takeAt(0)
95
+ if item.widget():
96
+ item.widget().setParent(None)
97
+ self.action_pane_widget.layout().addWidget(action_pane_widget)
98
+ action_pane_widget.setParent(
99
+ self.action_pane_widget
100
+ ) # Ensure correct parentage
101
+
84
102
  def _setup_file_explorer(self, splitter):
85
103
  """Setup file explorer section."""
86
104
  file_explorer_widget = QWidget()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lazylabel-gui
3
- Version: 1.1.3
3
+ Version: 1.1.5
4
4
  Summary: An image segmentation GUI for generating ML ready mask tensors and annotations.
5
5
  Author-email: "Deniz N. Cakan" <deniz.n.cakan@gmail.com>
6
6
  License: MIT License
@@ -1,26 +1,26 @@
1
1
  lazylabel/__init__.py,sha256=0yitHZYNNuF4Rqj7cqE0TrfDV8zhzKD3A5W1vOymHK4,199
2
2
  lazylabel/main.py,sha256=AQ-SKj8korOa2rKviFaA1kC-c03VPmVN2xU2qk36764,768
3
3
  lazylabel/config/__init__.py,sha256=TnDXH0yx0zy97FfjpiVPHleMgMrM5YwWrUbvevSNJjg,263
4
- lazylabel/config/hotkeys.py,sha256=Aq5pH2ED91A0wayQD58Vy0gjmajlV4-ra7Vd-IgZr38,8114
4
+ lazylabel/config/hotkeys.py,sha256=bbHPpCNokwgH0EX74vGcNdn3RoyVZNVr-is6us-7J3M,7990
5
5
  lazylabel/config/paths.py,sha256=ZVKbtaNOxmYO4l6JgsY-8DXaE_jaJfDg2RQJJn3-5nw,1275
6
6
  lazylabel/config/settings.py,sha256=Ud27cSadJ7M4l30XN0KrxZ3ixAOPXTUhj-SvvanMjaI,1787
7
7
  lazylabel/core/__init__.py,sha256=FmRjop_uIBSJwKMGhaZ-3Iwu34LkoxTuD-hnq5vbTSY,232
8
8
  lazylabel/core/file_manager.py,sha256=Blo55jXW-uGVLrreGRmlHUtK2yTbyj3jsTKkfjD0z7k,4796
9
9
  lazylabel/core/model_manager.py,sha256=P3IahI0xUk6-F7Y5U4r5RpnUQSelJqX2KBtcmUGNeNY,3368
10
- lazylabel/core/segment_manager.py,sha256=h7ikOZXpV-JwutiWK9xYGWctTaNHJKMeWau9ANml1tY,5913
10
+ lazylabel/core/segment_manager.py,sha256=M6kHcYeiub3WqL01NElCvKOc2GNmf72LUM1W8XwSaxc,6465
11
11
  lazylabel/models/__init__.py,sha256=fIlk_0DuZfiClcm0XlZdimeHzunQwBmTMI4PcGsaymw,91
12
12
  lazylabel/models/sam_model.py,sha256=89v99hpD0ngAAQKiuRyIib0E-5u9zTDJcubtmxxG-PM,7878
13
13
  lazylabel/ui/__init__.py,sha256=4qDIh9y6tABPmD8MAMGZn_G7oSRyrcHt2HkjoWgbGH4,268
14
- lazylabel/ui/control_panel.py,sha256=iyn6v-GvdSabd0Lk7n6S8p_svuTrqsG0jUOZBGhjqAc,8925
15
- lazylabel/ui/editable_vertex.py,sha256=xzc5QdFJJXiy035C3HgR4ph-tuJu8245l-Ar7aBvVv8,2399
14
+ lazylabel/ui/control_panel.py,sha256=UlETN2IZv1l2xiaWPQvdalaK_f9YQf9iiB1Jb_EEgfs,9260
15
+ lazylabel/ui/editable_vertex.py,sha256=nAFC2UuFfbvMbGBbAiLWA77cS5-Hn3a08xe1_QLz2yk,2449
16
16
  lazylabel/ui/hotkey_dialog.py,sha256=ZlZoPvY822ke8YkbCy22o2RUjxtkXSoEOg8eVETIKpQ,15118
17
17
  lazylabel/ui/hoverable_pixelmap_item.py,sha256=kJFOp7WXiyHpNf7l73TZjiob85jgP30b5MZvu_z5L3c,728
18
18
  lazylabel/ui/hoverable_polygon_item.py,sha256=aclUwd0P8H8xbcep6GwhnfaVs1zSkqeZKAL-xeDyMiU,1222
19
- lazylabel/ui/main_window.py,sha256=BQh1OsHxZ_ukjcF4HzcJAAzLqNJENBBHZGpB2bYoXbo,75311
19
+ lazylabel/ui/main_window.py,sha256=h0wfVjQRskiTIq6X-Wd7OvM-PfOdKEKdFcwgx0iRIj0,79831
20
20
  lazylabel/ui/numeric_table_widget_item.py,sha256=dQUlIFu9syCxTGAHVIlmbgkI7aJ3f3wmDPBz1AGK9Bg,283
21
21
  lazylabel/ui/photo_viewer.py,sha256=4n2zSpFAnausCoIU-wwWDHDo6vsNp_RGIkhDyhuM2_A,1997
22
22
  lazylabel/ui/reorderable_class_table.py,sha256=sxHhQre5O_MXLDFgKnw43QnvXXoqn5xRKMGitgO7muI,2371
23
- lazylabel/ui/right_panel.py,sha256=F407BhQSNTKi_9IIqdZZgjSMiUmxOIcNO-7UtWWbbdk,12279
23
+ lazylabel/ui/right_panel.py,sha256=-PeXcu7Lr-xhZniBMvWLDPiFb_RAHYAcILyw8fPJs6I,13139
24
24
  lazylabel/ui/widgets/__init__.py,sha256=bYjLRTqWdi4hcPfSSXmXuT-0c5Aee7HnnQurv_k5bfY,314
25
25
  lazylabel/ui/widgets/adjustments_widget.py,sha256=xM43q1khpvRIHslPfI6bukzjx9J0mPgHUdNKCmp3rtM,3912
26
26
  lazylabel/ui/widgets/model_selection_widget.py,sha256=kMPaBMfdfnEVH-Be1d5OxLEuioO0c04FZT1Hr904axM,3497
@@ -29,9 +29,9 @@ lazylabel/ui/widgets/status_bar.py,sha256=wTbMQNEOBfmtNj8EVFZS_lxgaemu-CbRXeZzEQ
29
29
  lazylabel/utils/__init__.py,sha256=V6IR5Gim-39HgM2NyTVT-n8gy3mjilCSFW9y0owN5nc,179
30
30
  lazylabel/utils/custom_file_system_model.py,sha256=-3EimlybvevH6bvqBE0qdFnLADVtayylmkntxPXK0Bk,4869
31
31
  lazylabel/utils/utils.py,sha256=sYSCoXL27OaLgOZaUkCAhgmKZ7YfhR3Cc5F8nDIa3Ig,414
32
- lazylabel_gui-1.1.3.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
33
- lazylabel_gui-1.1.3.dist-info/METADATA,sha256=i0c3l4ODbKROaIHXpX9y9kttfEbVOU1A3j7NoNONsmo,9506
34
- lazylabel_gui-1.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
- lazylabel_gui-1.1.3.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
36
- lazylabel_gui-1.1.3.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
37
- lazylabel_gui-1.1.3.dist-info/RECORD,,
32
+ lazylabel_gui-1.1.5.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
33
+ lazylabel_gui-1.1.5.dist-info/METADATA,sha256=2MNsqA48WN9e7YJMg864WZYGMZixitm3XtMnD2uZin4,9506
34
+ lazylabel_gui-1.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
+ lazylabel_gui-1.1.5.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
36
+ lazylabel_gui-1.1.5.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
37
+ lazylabel_gui-1.1.5.dist-info/RECORD,,