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.
@@ -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)
@@ -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
- mask = self.rasterize_polygon(seg["vertices"], image_size)
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
 
@@ -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)
@@ -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
  )
@@ -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
- QPointF(v.x(), v.y()) for v in vertices
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) for p in self.segment_manager.segments[i]["vertices"]
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
- QPointF(v) + delta for v in initial_verts
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
- points_np = np.array(
1526
- [[p.x(), p.y()] for p in seg["vertices"]], dtype=np.int32
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, pt in enumerate(seg["vertices"]):
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 # new_pos is already the correct scene coordinate
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
- item.setPolygon(
1631
- QPolygonF(self.segment_manager.segments[segment_index]["vertices"])
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):
@@ -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.4
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=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=iyn6v-GvdSabd0Lk7n6S8p_svuTrqsG0jUOZBGhjqAc,8925
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=BQh1OsHxZ_ukjcF4HzcJAAzLqNJENBBHZGpB2bYoXbo,75311
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=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.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,,