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.
- lazylabel/config/hotkeys.py +208 -207
- lazylabel/core/segment_manager.py +188 -170
- lazylabel/ui/control_panel.py +6 -0
- lazylabel/ui/editable_vertex.py +2 -2
- lazylabel/ui/main_window.py +128 -33
- lazylabel/ui/right_panel.py +18 -0
- {lazylabel_gui-1.1.3.dist-info → lazylabel_gui-1.1.5.dist-info}/METADATA +1 -1
- {lazylabel_gui-1.1.3.dist-info → lazylabel_gui-1.1.5.dist-info}/RECORD +12 -12
- {lazylabel_gui-1.1.3.dist-info → lazylabel_gui-1.1.5.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.1.3.dist-info → lazylabel_gui-1.1.5.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.1.3.dist-info → lazylabel_gui-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.1.3.dist-info → lazylabel_gui-1.1.5.dist-info}/top_level.txt +0 -0
lazylabel/config/hotkeys.py
CHANGED
@@ -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("
|
50
|
-
HotkeyAction("
|
51
|
-
HotkeyAction("
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
HotkeyAction(
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
HotkeyAction(
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
"
|
80
|
-
"
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
HotkeyAction("
|
89
|
-
|
90
|
-
|
91
|
-
HotkeyAction("
|
92
|
-
HotkeyAction("
|
93
|
-
HotkeyAction("
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
"
|
98
|
-
"
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
"
|
105
|
-
"
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
"
|
112
|
-
"
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
"
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
self.actions[name].
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
for
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
def
|
136
|
-
"""
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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/ui/control_panel.py
CHANGED
@@ -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)
|
lazylabel/ui/editable_vertex.py
CHANGED
@@ -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
|
lazylabel/ui/main_window.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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":
|
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"] =
|
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
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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:
|
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
|
-
|
1526
|
-
|
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,
|
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
|
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
|
-
|
1631
|
-
|
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):
|
lazylabel/ui/right_panel.py
CHANGED
@@ -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,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=
|
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=
|
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=
|
15
|
-
lazylabel/ui/editable_vertex.py,sha256=
|
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=
|
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
|
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.
|
33
|
-
lazylabel_gui-1.1.
|
34
|
-
lazylabel_gui-1.1.
|
35
|
-
lazylabel_gui-1.1.
|
36
|
-
lazylabel_gui-1.1.
|
37
|
-
lazylabel_gui-1.1.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|