lazylabel-gui 1.1.3__py3-none-any.whl → 1.1.4__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 +15 -2
- lazylabel/ui/control_panel.py +6 -0
- lazylabel/ui/main_window.py +94 -21
- lazylabel/ui/right_panel.py +18 -0
- {lazylabel_gui-1.1.3.dist-info → lazylabel_gui-1.1.4.dist-info}/METADATA +1 -1
- {lazylabel_gui-1.1.3.dist-info → lazylabel_gui-1.1.4.dist-info}/RECORD +11 -11
- {lazylabel_gui-1.1.3.dist-info → lazylabel_gui-1.1.4.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.1.3.dist-info → lazylabel_gui-1.1.4.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.1.3.dist-info → lazylabel_gui-1.1.4.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.1.3.dist-info → lazylabel_gui-1.1.4.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)
|
@@ -24,13 +24,24 @@ class SegmentManager:
|
|
24
24
|
self.active_class_id = None
|
25
25
|
|
26
26
|
def add_segment(self, segment_data: dict[str, Any]) -> None:
|
27
|
-
"""Add a new segment.
|
27
|
+
"""Add a new segment.
|
28
|
+
|
29
|
+
If the segment is a polygon, convert QPointF objects to simple lists
|
30
|
+
for serialization compatibility.
|
31
|
+
"""
|
28
32
|
if "class_id" not in segment_data:
|
29
33
|
# Use active class if available, otherwise use next class ID
|
30
34
|
if self.active_class_id is not None:
|
31
35
|
segment_data["class_id"] = self.active_class_id
|
32
36
|
else:
|
33
37
|
segment_data["class_id"] = self.next_class_id
|
38
|
+
|
39
|
+
# Convert QPointF to list for storage if it's a polygon
|
40
|
+
if segment_data.get("type") == "Polygon" and segment_data.get("vertices"):
|
41
|
+
segment_data["vertices"] = [
|
42
|
+
[p.x(), p.y()] for p in segment_data["vertices"]
|
43
|
+
]
|
44
|
+
|
34
45
|
self.segments.append(segment_data)
|
35
46
|
self._update_next_class_id()
|
36
47
|
|
@@ -103,7 +114,9 @@ class SegmentManager:
|
|
103
114
|
new_channel_idx = id_map[class_id]
|
104
115
|
|
105
116
|
if seg["type"] == "Polygon":
|
106
|
-
|
117
|
+
# Convert stored list of lists back to QPointF objects for rasterization
|
118
|
+
qpoints = [QPointF(p[0], p[1]) for p in seg["vertices"]]
|
119
|
+
mask = self.rasterize_polygon(qpoints, image_size)
|
107
120
|
else:
|
108
121
|
mask = seg.get("mask")
|
109
122
|
|
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/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
|
)
|
@@ -1129,7 +1147,8 @@ class MainWindow(QMainWindow):
|
|
1129
1147
|
for i, vertices in final_vertices.items():
|
1130
1148
|
if i < len(self.segment_manager.segments):
|
1131
1149
|
self.segment_manager.segments[i]["vertices"] = [
|
1132
|
-
|
1150
|
+
[v.x(), v.y()]
|
1151
|
+
for v in [QPointF(p[0], p[1]) for p in vertices]
|
1133
1152
|
]
|
1134
1153
|
self._update_polygon_item(i)
|
1135
1154
|
self._display_edit_handles()
|
@@ -1302,7 +1321,8 @@ class MainWindow(QMainWindow):
|
|
1302
1321
|
selected_indices = self.right_panel.get_selected_segment_indices()
|
1303
1322
|
self.drag_initial_vertices = {
|
1304
1323
|
i: [
|
1305
|
-
QPointF(p
|
1324
|
+
QPointF(p[0], p[1])
|
1325
|
+
for p in self.segment_manager.segments[i]["vertices"]
|
1306
1326
|
]
|
1307
1327
|
for i in selected_indices
|
1308
1328
|
if self.segment_manager.segments[i].get("type") == "Polygon"
|
@@ -1312,9 +1332,6 @@ class MainWindow(QMainWindow):
|
|
1312
1332
|
|
1313
1333
|
# Call the original scene handler.
|
1314
1334
|
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
1335
|
|
1319
1336
|
if self.is_dragging_polygon:
|
1320
1337
|
return
|
@@ -1338,6 +1355,14 @@ class MainWindow(QMainWindow):
|
|
1338
1355
|
elif self.mode == "polygon":
|
1339
1356
|
if event.button() == Qt.MouseButton.LeftButton:
|
1340
1357
|
self._handle_polygon_click(pos)
|
1358
|
+
elif self.mode == "bbox":
|
1359
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
1360
|
+
self.drag_start_pos = pos
|
1361
|
+
self.rubber_band_rect = QGraphicsRectItem()
|
1362
|
+
self.rubber_band_rect.setPen(
|
1363
|
+
QPen(Qt.GlobalColor.red, 2, Qt.PenStyle.DashLine)
|
1364
|
+
)
|
1365
|
+
self.viewer.scene().addItem(self.rubber_band_rect)
|
1341
1366
|
elif self.mode == "selection" and event.button() == Qt.MouseButton.LeftButton:
|
1342
1367
|
self._handle_segment_selection_click(pos)
|
1343
1368
|
|
@@ -1346,8 +1371,9 @@ class MainWindow(QMainWindow):
|
|
1346
1371
|
if self.mode == "edit" and self.is_dragging_polygon:
|
1347
1372
|
delta = event.scenePos() - self.drag_start_pos
|
1348
1373
|
for i, initial_verts in self.drag_initial_vertices.items():
|
1374
|
+
# initial_verts are already QPointF objects
|
1349
1375
|
self.segment_manager.segments[i]["vertices"] = [
|
1350
|
-
|
1376
|
+
[(p + delta).x(), (p + delta).y()] for p in initial_verts
|
1351
1377
|
]
|
1352
1378
|
self._update_polygon_item(i)
|
1353
1379
|
self._display_edit_handles() # Redraw handles at new positions
|
@@ -1357,6 +1383,13 @@ class MainWindow(QMainWindow):
|
|
1357
1383
|
|
1358
1384
|
self._original_mouse_move(event)
|
1359
1385
|
|
1386
|
+
if self.mode == "bbox" and self.rubber_band_rect and self.drag_start_pos:
|
1387
|
+
current_pos = event.scenePos()
|
1388
|
+
rect = QRectF(self.drag_start_pos, current_pos).normalized()
|
1389
|
+
self.rubber_band_rect.setRect(rect)
|
1390
|
+
event.accept()
|
1391
|
+
return
|
1392
|
+
|
1360
1393
|
def _scene_mouse_release(self, event):
|
1361
1394
|
"""Handle mouse release events in the scene."""
|
1362
1395
|
if self.mode == "edit" and self.is_dragging_polygon:
|
@@ -1383,6 +1416,41 @@ class MainWindow(QMainWindow):
|
|
1383
1416
|
|
1384
1417
|
if self.mode == "pan":
|
1385
1418
|
self.viewer.set_cursor(Qt.CursorShape.OpenHandCursor)
|
1419
|
+
elif self.mode == "bbox" and self.rubber_band_rect:
|
1420
|
+
self.viewer.scene().removeItem(self.rubber_band_rect)
|
1421
|
+
rect = self.rubber_band_rect.rect()
|
1422
|
+
self.rubber_band_rect = None
|
1423
|
+
self.drag_start_pos = None
|
1424
|
+
|
1425
|
+
if rect.width() > 0 and rect.height() > 0:
|
1426
|
+
# Convert QRectF to QPolygonF
|
1427
|
+
polygon = QPolygonF()
|
1428
|
+
polygon.append(rect.topLeft())
|
1429
|
+
polygon.append(rect.topRight())
|
1430
|
+
polygon.append(rect.bottomRight())
|
1431
|
+
polygon.append(rect.bottomLeft())
|
1432
|
+
|
1433
|
+
new_segment = {
|
1434
|
+
"vertices": list(polygon),
|
1435
|
+
"type": "Polygon", # Bounding boxes are stored as polygons
|
1436
|
+
"mask": None,
|
1437
|
+
}
|
1438
|
+
|
1439
|
+
self.segment_manager.add_segment(new_segment)
|
1440
|
+
|
1441
|
+
# Record the action for undo
|
1442
|
+
self.action_history.append(
|
1443
|
+
{
|
1444
|
+
"type": "add_segment",
|
1445
|
+
"segment_index": len(self.segment_manager.segments) - 1,
|
1446
|
+
}
|
1447
|
+
)
|
1448
|
+
# Clear redo history when a new action is performed
|
1449
|
+
self.redo_history.clear()
|
1450
|
+
self._update_all_lists()
|
1451
|
+
event.accept()
|
1452
|
+
return
|
1453
|
+
|
1386
1454
|
self._original_mouse_release(event)
|
1387
1455
|
|
1388
1456
|
def _add_point(self, pos, positive):
|
@@ -1522,9 +1590,9 @@ class MainWindow(QMainWindow):
|
|
1522
1590
|
continue
|
1523
1591
|
h = self.viewer._pixmap_item.pixmap().height()
|
1524
1592
|
w = self.viewer._pixmap_item.pixmap().width()
|
1525
|
-
|
1526
|
-
|
1527
|
-
)
|
1593
|
+
# Convert stored list of lists back to QPointF objects for rasterization
|
1594
|
+
qpoints = [QPointF(p[0], p[1]) for p in seg["vertices"]]
|
1595
|
+
points_np = np.array([[p.x(), p.y()] for p in qpoints], dtype=np.int32)
|
1528
1596
|
# Ensure points are within bounds
|
1529
1597
|
points_np = np.clip(points_np, 0, [w - 1, h - 1])
|
1530
1598
|
mask = np.zeros((h, w), dtype=np.uint8)
|
@@ -1574,7 +1642,8 @@ class MainWindow(QMainWindow):
|
|
1574
1642
|
for seg_idx in selected_indices:
|
1575
1643
|
seg = self.segment_manager.segments[seg_idx]
|
1576
1644
|
if seg["type"] == "Polygon" and seg.get("vertices"):
|
1577
|
-
for v_idx,
|
1645
|
+
for v_idx, pt_list in enumerate(seg["vertices"]):
|
1646
|
+
pt = QPointF(pt_list[0], pt_list[1]) # Convert list to QPointF
|
1578
1647
|
handle = EditableVertexItem(
|
1579
1648
|
self,
|
1580
1649
|
seg_idx,
|
@@ -1610,15 +1679,16 @@ class MainWindow(QMainWindow):
|
|
1610
1679
|
"type": "move_vertex",
|
1611
1680
|
"segment_index": segment_index,
|
1612
1681
|
"vertex_index": vertex_index,
|
1613
|
-
"old_pos": old_pos,
|
1614
|
-
"new_pos": new_pos,
|
1682
|
+
"old_pos": [old_pos[0], old_pos[1]], # Store as list
|
1683
|
+
"new_pos": [new_pos.x(), new_pos.y()], # Store as list
|
1615
1684
|
}
|
1616
1685
|
)
|
1617
1686
|
# Clear redo history when a new action is performed
|
1618
1687
|
self.redo_history.clear()
|
1619
|
-
seg["vertices"][vertex_index] =
|
1620
|
-
new_pos
|
1621
|
-
|
1688
|
+
seg["vertices"][vertex_index] = [
|
1689
|
+
new_pos.x(),
|
1690
|
+
new_pos.y(), # Store as list
|
1691
|
+
]
|
1622
1692
|
self._update_polygon_item(segment_index)
|
1623
1693
|
self._highlight_selected_segments() # Keep the highlight in sync with the new shape
|
1624
1694
|
|
@@ -1627,9 +1697,12 @@ class MainWindow(QMainWindow):
|
|
1627
1697
|
items = self.segment_items.get(segment_index, [])
|
1628
1698
|
for item in items:
|
1629
1699
|
if isinstance(item, HoverablePolygonItem):
|
1630
|
-
|
1631
|
-
|
1632
|
-
|
1700
|
+
# Convert stored list of lists back to QPointF objects
|
1701
|
+
qpoints = [
|
1702
|
+
QPointF(p[0], p[1])
|
1703
|
+
for p in self.segment_manager.segments[segment_index]["vertices"]
|
1704
|
+
]
|
1705
|
+
item.setPolygon(QPolygonF(qpoints))
|
1633
1706
|
return
|
1634
1707
|
|
1635
1708
|
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=1lABdVdaa69_RPKx6ftYKJh7lNWCanodTsh7Utrpn2Q,6477
|
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=
|
14
|
+
lazylabel/ui/control_panel.py,sha256=UlETN2IZv1l2xiaWPQvdalaK_f9YQf9iiB1Jb_EEgfs,9260
|
15
15
|
lazylabel/ui/editable_vertex.py,sha256=xzc5QdFJJXiy035C3HgR4ph-tuJu8245l-Ar7aBvVv8,2399
|
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=ta3nzb6MWG-eLexlGKBc3oKtlsF9HpI-q1SvlrA-W2o,78623
|
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.4.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
|
33
|
+
lazylabel_gui-1.1.4.dist-info/METADATA,sha256=hYVIAtM7vZNaJ8-0QOnFtNyaK3UpScgswNyJuaiJaFE,9506
|
34
|
+
lazylabel_gui-1.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
35
|
+
lazylabel_gui-1.1.4.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
|
36
|
+
lazylabel_gui-1.1.4.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
|
37
|
+
lazylabel_gui-1.1.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|