lazylabel-gui 1.3.8__py3-none-any.whl → 1.3.9__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.
@@ -57,6 +57,12 @@ class HotkeyManager:
57
57
  HotkeyAction(
58
58
  "save_segment", "Save Current Segment", "Space", category="Actions"
59
59
  ),
60
+ HotkeyAction(
61
+ "erase_segment",
62
+ "Erase with Current Segment",
63
+ "Shift+Space",
64
+ category="Actions",
65
+ ),
60
66
  HotkeyAction("save_output", "Save Output", "Return", category="Actions"),
61
67
  HotkeyAction(
62
68
  "save_output_alt", "Save Output (Alt)", "Enter", category="Actions"
@@ -68,6 +74,9 @@ class HotkeyManager:
68
74
  HotkeyAction(
69
75
  "escape", "Cancel/Clear Selection", "Escape", category="Actions"
70
76
  ),
77
+ HotkeyAction(
78
+ "toggle_ai_filter", "Toggle AI Filter", "Z", category="Actions"
79
+ ),
71
80
  # Segments
72
81
  HotkeyAction(
73
82
  "merge_segments", "Merge Selected Segments", "M", category="Segments"
@@ -84,6 +93,10 @@ class HotkeyManager:
84
93
  HotkeyAction(
85
94
  "select_all", "Select All Segments", "Ctrl+A", category="Segments"
86
95
  ),
96
+ # Classes
97
+ HotkeyAction(
98
+ "toggle_recent_class", "Toggle Recent Class", "X", category="Classes"
99
+ ),
87
100
  # View
88
101
  HotkeyAction("zoom_in", "Zoom In", "Ctrl+Plus", category="View"),
89
102
  HotkeyAction("zoom_out", "Zoom Out", "Ctrl+Minus", category="View"),
@@ -45,6 +45,10 @@ class Settings:
45
45
  # Multi-view Settings
46
46
  multi_view_grid_mode: str = "2_view" # "2_view" or "4_view"
47
47
 
48
+ # Pixel Priority Settings
49
+ pixel_priority_enabled: bool = False
50
+ pixel_priority_ascending: bool = True
51
+
48
52
  def save_to_file(self, filepath: str) -> None:
49
53
  """Save settings to JSON file."""
50
54
  os.makedirs(os.path.dirname(filepath), exist_ok=True)
@@ -22,6 +22,8 @@ class FileManager:
22
22
  image_size: tuple[int, int],
23
23
  class_order: list[int],
24
24
  crop_coords: tuple[int, int, int, int] | None = None,
25
+ pixel_priority_enabled: bool = False,
26
+ pixel_priority_ascending: bool = True,
25
27
  ) -> str:
26
28
  """Save segments as NPZ file."""
27
29
  logger.debug(f"Saving NPZ for image: {image_path}")
@@ -32,7 +34,7 @@ class FileManager:
32
34
  raise ValueError("No classes defined for saving")
33
35
 
34
36
  final_mask_tensor = self.segment_manager.create_final_mask_tensor(
35
- image_size, class_order
37
+ image_size, class_order, pixel_priority_enabled, pixel_priority_ascending
36
38
  )
37
39
 
38
40
  # Validate mask tensor
@@ -75,10 +77,12 @@ class FileManager:
75
77
  class_order: list[int],
76
78
  class_labels: list[str],
77
79
  crop_coords: tuple[int, int, int, int] | None = None,
80
+ pixel_priority_enabled: bool = False,
81
+ pixel_priority_ascending: bool = True,
78
82
  ) -> str | None:
79
83
  """Save segments as YOLO format TXT file."""
80
84
  final_mask_tensor = self.segment_manager.create_final_mask_tensor(
81
- image_size, class_order
85
+ image_size, class_order, pixel_priority_enabled, pixel_priority_ascending
82
86
  )
83
87
 
84
88
  # Apply crop if coordinates are provided
@@ -15,6 +15,7 @@ class SegmentManager:
15
15
  self.class_aliases: dict[int, str] = {}
16
16
  self.next_class_id: int = 0
17
17
  self.active_class_id: int | None = None # Currently active/toggled class
18
+ self.last_toggled_class_id: int | None = None # Most recently toggled class
18
19
 
19
20
  def clear(self) -> None:
20
21
  """Clear all segments and reset state."""
@@ -22,6 +23,7 @@ class SegmentManager:
22
23
  self.class_aliases.clear()
23
24
  self.next_class_id = 0
24
25
  self.active_class_id = None
26
+ self.last_toggled_class_id = None
25
27
 
26
28
  def add_segment(self, segment_data: dict[str, Any]) -> None:
27
29
  """Add a new segment.
@@ -36,6 +38,9 @@ class SegmentManager:
36
38
  else:
37
39
  segment_data["class_id"] = self.next_class_id
38
40
 
41
+ # Track the class used for this segment as the most recently used
42
+ self.last_toggled_class_id = segment_data["class_id"]
43
+
39
44
  # Convert QPointF to list for storage if it's a polygon and contains QPointF objects
40
45
  if (
41
46
  segment_data.get("type") == "Polygon"
@@ -103,7 +108,11 @@ class SegmentManager:
103
108
  return mask.astype(bool)
104
109
 
105
110
  def create_final_mask_tensor(
106
- self, image_size: tuple[int, int], class_order: list[int]
111
+ self,
112
+ image_size: tuple[int, int],
113
+ class_order: list[int],
114
+ pixel_priority_enabled: bool = False,
115
+ pixel_priority_ascending: bool = True,
107
116
  ) -> np.ndarray:
108
117
  """Create final mask tensor for saving."""
109
118
  h, w = image_size
@@ -130,8 +139,59 @@ class SegmentManager:
130
139
  final_mask_tensor[:, :, new_channel_idx], mask
131
140
  )
132
141
 
142
+ # Apply pixel priority if enabled
143
+ if pixel_priority_enabled:
144
+ final_mask_tensor = self._apply_pixel_priority(
145
+ final_mask_tensor, pixel_priority_ascending
146
+ )
147
+
133
148
  return final_mask_tensor
134
149
 
150
+ def _apply_pixel_priority(
151
+ self, mask_tensor: np.ndarray, ascending: bool
152
+ ) -> np.ndarray:
153
+ """Apply pixel priority to mask tensor so only one class can occupy each pixel.
154
+
155
+ Args:
156
+ mask_tensor: 3D array of shape (height, width, num_classes)
157
+ ascending: If True, lower class indices have priority; if False, higher indices have priority
158
+
159
+ Returns:
160
+ Modified mask tensor with pixel priority applied
161
+ """
162
+ # Create a copy to avoid modifying the original
163
+ prioritized_mask = mask_tensor.copy()
164
+
165
+ # Find pixels with multiple class overlaps (sum > 1 across class dimension)
166
+ overlap_pixels = np.sum(mask_tensor, axis=2) > 1
167
+
168
+ if not np.any(overlap_pixels):
169
+ # No overlapping pixels, return original
170
+ return prioritized_mask
171
+
172
+ # Get coordinates of overlapping pixels
173
+ overlap_coords = np.where(overlap_pixels)
174
+
175
+ for y, x in zip(overlap_coords[0], overlap_coords[1], strict=False):
176
+ # Get all classes present at this pixel
177
+ classes_at_pixel = np.where(mask_tensor[y, x, :] > 0)[0]
178
+
179
+ if len(classes_at_pixel) <= 1:
180
+ continue # No overlap, skip
181
+
182
+ # Determine priority class based on ascending/descending setting
183
+ if ascending:
184
+ priority_class = np.min(classes_at_pixel) # Lowest index has priority
185
+ else:
186
+ priority_class = np.max(classes_at_pixel) # Highest index has priority
187
+
188
+ # Set all classes to 0 at this pixel
189
+ prioritized_mask[y, x, :] = 0
190
+ # Set only the priority class to 1
191
+ prioritized_mask[y, x, priority_class] = 1
192
+
193
+ return prioritized_mask
194
+
135
195
  def reassign_class_ids(self, new_order: list[int]) -> None:
136
196
  """Reassign class IDs based on new order."""
137
197
  id_map = {old_id: new_id for new_id, old_id in enumerate(new_order)}
@@ -168,6 +228,9 @@ class SegmentManager:
168
228
 
169
229
  def toggle_active_class(self, class_id: int) -> bool:
170
230
  """Toggle a class as active. Returns True if now active, False if deactivated."""
231
+ # Track this as the last toggled class
232
+ self.last_toggled_class_id = class_id
233
+
171
234
  if self.active_class_id == class_id:
172
235
  self.active_class_id = None
173
236
  return False
@@ -175,6 +238,175 @@ class SegmentManager:
175
238
  self.active_class_id = class_id
176
239
  return True
177
240
 
241
+ def get_last_toggled_class(self) -> int | None:
242
+ """Get the most recently toggled class ID."""
243
+ return self.last_toggled_class_id
244
+
245
+ def get_class_to_toggle_with_hotkey(self) -> int | None:
246
+ """Get the class ID to toggle when using the hotkey.
247
+
248
+ Returns the most recent class used/toggled, or if no recent class
249
+ was used then returns the last class in the class list.
250
+ """
251
+ # If we have a recently toggled class, use that
252
+ if self.last_toggled_class_id is not None:
253
+ return self.last_toggled_class_id
254
+
255
+ # Otherwise, get the last class in the class list (highest ID)
256
+ unique_class_ids = self.get_unique_class_ids()
257
+ if unique_class_ids:
258
+ return unique_class_ids[-1] # Last (highest) class ID
259
+
260
+ return None
261
+
262
+ def erase_segments_with_shape(
263
+ self, erase_vertices: list[QPointF], image_size: tuple[int, int]
264
+ ) -> list[int]:
265
+ """Erase segments that overlap with the given shape.
266
+
267
+ Args:
268
+ erase_vertices: Vertices of the erase shape
269
+ image_size: Size of the image (height, width)
270
+
271
+ Returns:
272
+ List of indices of segments that were removed
273
+ """
274
+ if not erase_vertices or not self.segments:
275
+ return [], []
276
+
277
+ # Create mask from erase shape
278
+ erase_mask = self.rasterize_polygon(erase_vertices, image_size)
279
+ if erase_mask is None:
280
+ return [], []
281
+
282
+ return self.erase_segments_with_mask(erase_mask, image_size)
283
+
284
+ def erase_segments_with_mask(
285
+ self, erase_mask: np.ndarray, image_size: tuple[int, int]
286
+ ) -> list[int]:
287
+ """Erase overlapping pixels from segments that overlap with the given mask.
288
+
289
+ Args:
290
+ erase_mask: Binary mask indicating pixels to erase
291
+ image_size: Size of the image (height, width)
292
+
293
+ Returns:
294
+ List of indices of segments that were modified or removed
295
+ """
296
+ if erase_mask is None or not self.segments:
297
+ return [], []
298
+
299
+ modified_indices = []
300
+ segments_to_remove = []
301
+ segments_to_add = []
302
+ removed_segments_data = [] # Store segment data for undo
303
+
304
+ # Iterate through all segments to find overlaps
305
+ for i, segment in enumerate(self.segments):
306
+ segment_mask = self._get_segment_mask(segment, image_size)
307
+ if segment_mask is None:
308
+ continue
309
+
310
+ # Check if there's any overlap between erase mask and segment mask
311
+ overlap = np.logical_and(erase_mask, segment_mask)
312
+ overlap_area = np.sum(overlap)
313
+
314
+ if overlap_area > 0:
315
+ modified_indices.append(i)
316
+
317
+ # Store original segment data before modification
318
+ removed_segments_data.append({"index": i, "segment": segment.copy()})
319
+
320
+ # Create new mask by removing erased pixels
321
+ new_mask = np.logical_and(segment_mask, ~erase_mask)
322
+ remaining_area = np.sum(new_mask)
323
+
324
+ if remaining_area == 0:
325
+ # All pixels were erased, remove segment entirely
326
+ segments_to_remove.append(i)
327
+ else:
328
+ # Some pixels remain, check for disconnected components
329
+ # Use connected component analysis to split into separate segments
330
+ connected_segments = self._split_mask_into_components(
331
+ new_mask, segment.get("class_id")
332
+ )
333
+
334
+ # Mark original for removal and add all connected components
335
+ segments_to_remove.append(i)
336
+ segments_to_add.extend(connected_segments)
337
+
338
+ # Remove segments in reverse order to maintain indices
339
+ for i in sorted(segments_to_remove, reverse=True):
340
+ del self.segments[i]
341
+
342
+ # Add new segments (modified versions)
343
+ for new_segment in segments_to_add:
344
+ self.segments.append(new_segment)
345
+
346
+ if modified_indices:
347
+ self._update_next_class_id()
348
+
349
+ return modified_indices, removed_segments_data
350
+
351
+ def _get_segment_mask(
352
+ self, segment: dict[str, Any], image_size: tuple[int, int]
353
+ ) -> np.ndarray | None:
354
+ """Get binary mask for a segment.
355
+
356
+ Args:
357
+ segment: Segment data
358
+ image_size: Size of the image (height, width)
359
+
360
+ Returns:
361
+ Binary mask for the segment or None if unable to create
362
+ """
363
+ if segment["type"] == "Polygon" and "vertices" in segment:
364
+ # Convert stored list of lists back to QPointF objects for rasterization
365
+ qpoints = [QPointF(p[0], p[1]) for p in segment["vertices"]]
366
+ return self.rasterize_polygon(qpoints, image_size)
367
+ elif "mask" in segment and segment["mask"] is not None:
368
+ return segment["mask"]
369
+ return None
370
+
371
+ def _split_mask_into_components(
372
+ self, mask: np.ndarray, class_id: int | None
373
+ ) -> list[dict[str, Any]]:
374
+ """Split a mask into separate segments for each connected component.
375
+
376
+ Args:
377
+ mask: Binary mask to split
378
+ class_id: Class ID to assign to all resulting segments
379
+
380
+ Returns:
381
+ List of segment dictionaries, one for each connected component
382
+ """
383
+ if mask is None or np.sum(mask) == 0:
384
+ return []
385
+
386
+ # Convert boolean mask to uint8 for OpenCV
387
+ mask_uint8 = mask.astype(np.uint8)
388
+
389
+ # Find connected components
390
+ num_labels, labels = cv2.connectedComponents(mask_uint8, connectivity=8)
391
+
392
+ segments = []
393
+
394
+ # Create a segment for each component (skip label 0 which is background)
395
+ for label in range(1, num_labels):
396
+ component_mask = labels == label
397
+
398
+ # Only create segment if component has significant size
399
+ if np.sum(component_mask) > 10: # Minimum 10 pixels
400
+ new_segment = {
401
+ "type": "AI", # Convert to mask-based segment
402
+ "mask": component_mask,
403
+ "vertices": None,
404
+ "class_id": class_id,
405
+ }
406
+ segments.append(new_segment)
407
+
408
+ return segments
409
+
178
410
  def _update_next_class_id(self) -> None:
179
411
  """Update the next available class ID."""
180
412
  all_ids = {
@@ -182,6 +182,9 @@ class MainWindow(QMainWindow):
182
182
  self.pan_multiplier = self.settings.pan_multiplier
183
183
  self.polygon_join_threshold = self.settings.polygon_join_threshold
184
184
  self.fragment_threshold = self.settings.fragment_threshold
185
+ self.last_ai_filter_value = (
186
+ 100 if self.fragment_threshold == 0 else self.fragment_threshold
187
+ )
185
188
 
186
189
  # Image adjustment state
187
190
  self.brightness = self.settings.brightness
@@ -776,7 +779,9 @@ class MainWindow(QMainWindow):
776
779
  "undo": self._undo_last_action,
777
780
  "redo": self._redo_last_action,
778
781
  "select_all": lambda: self.right_panel.select_all_segments(),
782
+ "toggle_recent_class": self._toggle_recent_class,
779
783
  "save_segment": self._handle_space_press,
784
+ "erase_segment": self._handle_shift_space_press,
780
785
  "save_output": self._handle_enter_press,
781
786
  "save_output_alt": self._handle_enter_press,
782
787
  "fit_view": self._handle_fit_view,
@@ -786,6 +791,7 @@ class MainWindow(QMainWindow):
786
791
  "pan_down": lambda: self._handle_pan_key("down"),
787
792
  "pan_left": lambda: self._handle_pan_key("left"),
788
793
  "pan_right": lambda: self._handle_pan_key("right"),
794
+ "toggle_ai_filter": self._toggle_ai_filter,
789
795
  }
790
796
 
791
797
  # Create shortcuts for each action
@@ -1054,6 +1060,8 @@ class MainWindow(QMainWindow):
1054
1060
 
1055
1061
  def _set_fragment_threshold(self, value):
1056
1062
  """Set fragment threshold for AI segment filtering."""
1063
+ if value > 0:
1064
+ self.last_ai_filter_value = value
1057
1065
  self.fragment_threshold = value
1058
1066
  self.settings.fragment_threshold = value
1059
1067
 
@@ -2524,7 +2532,7 @@ class MainWindow(QMainWindow):
2524
2532
 
2525
2533
  if self.mode == "polygon":
2526
2534
  if self.view_mode == "single" and self.polygon_points:
2527
- self._finalize_polygon()
2535
+ self._finalize_polygon(erase_mode=False)
2528
2536
  elif self.view_mode == "multi" and hasattr(
2529
2537
  self, "multi_view_polygon_points"
2530
2538
  ):
@@ -2536,12 +2544,65 @@ class MainWindow(QMainWindow):
2536
2544
  self.multi_view_mode_handler._finalize_multi_view_polygon(i)
2537
2545
  else:
2538
2546
  self._finalize_multi_view_polygon(i)
2547
+ elif self.mode == "ai":
2548
+ # For AI mode, use accept method (normal mode)
2549
+ if self.view_mode == "single":
2550
+ self._accept_ai_segment(erase_mode=False)
2551
+ elif self.view_mode == "multi":
2552
+ # Handle multi-view AI acceptance (normal mode)
2553
+ if (
2554
+ hasattr(self, "multi_view_mode_handler")
2555
+ and self.multi_view_mode_handler
2556
+ ):
2557
+ self.multi_view_mode_handler.save_ai_predictions()
2558
+ self._show_notification("AI segment(s) accepted")
2559
+ else:
2560
+ self._accept_ai_segment(erase_mode=False)
2539
2561
  else:
2540
- # For AI mode, use accept method, otherwise save current segment
2541
- if self.mode == "ai" and self.view_mode == "single":
2542
- self._accept_ai_segment()
2543
- else:
2544
- self._save_current_segment()
2562
+ # For other modes, save current segment
2563
+ self._save_current_segment()
2564
+
2565
+ def _handle_shift_space_press(self):
2566
+ """Handle Shift+Space key press for erase functionality."""
2567
+ logger.debug(
2568
+ f"Shift+Space pressed - mode: {self.mode}, view_mode: {self.view_mode}"
2569
+ )
2570
+
2571
+ if self.mode == "polygon":
2572
+ if self.view_mode == "single" and self.polygon_points:
2573
+ self._finalize_polygon(erase_mode=True)
2574
+ elif self.view_mode == "multi" and hasattr(
2575
+ self, "multi_view_polygon_points"
2576
+ ):
2577
+ # Complete polygons for all viewers that have points
2578
+ for i, points in enumerate(self.multi_view_polygon_points):
2579
+ if points and len(points) >= 3:
2580
+ # Use the multi-view mode handler for proper pairing logic
2581
+ if hasattr(self, "multi_view_mode_handler"):
2582
+ self.multi_view_mode_handler._finalize_multi_view_polygon(i)
2583
+ else:
2584
+ self._finalize_multi_view_polygon(i)
2585
+ elif self.mode == "ai":
2586
+ # For AI mode, use accept method with erase mode
2587
+ if self.view_mode == "single":
2588
+ self._accept_ai_segment(erase_mode=True)
2589
+ elif self.view_mode == "multi":
2590
+ # Handle multi-view AI acceptance with erase mode
2591
+ if (
2592
+ hasattr(self, "multi_view_mode_handler")
2593
+ and self.multi_view_mode_handler
2594
+ ):
2595
+ logger.debug(
2596
+ "Shift+Space pressed in multi-view AI mode - erase not yet implemented"
2597
+ )
2598
+ self._show_notification(
2599
+ "Eraser not yet implemented for multi-view AI mode"
2600
+ )
2601
+ else:
2602
+ self._accept_ai_segment(erase_mode=True)
2603
+ else:
2604
+ # For other modes, show notification that erase isn't available
2605
+ self._show_notification(f"Erase mode not available in {self.mode} mode")
2545
2606
 
2546
2607
  def _handle_enter_press(self):
2547
2608
  """Handle enter key press."""
@@ -2721,6 +2782,13 @@ class MainWindow(QMainWindow):
2721
2782
  "All segments filtered out by fragment threshold"
2722
2783
  )
2723
2784
 
2785
+ def _toggle_ai_filter(self):
2786
+ """Toggle AI filter between 0 and last set value."""
2787
+ new_value = self.last_ai_filter_value if self.fragment_threshold == 0 else 0
2788
+
2789
+ # Update the control panel widget
2790
+ self.control_panel.set_fragment_threshold(new_value)
2791
+
2724
2792
  def _apply_fragment_threshold(self, mask):
2725
2793
  """Apply fragment threshold filtering to remove small segments."""
2726
2794
  if self.fragment_threshold == 0:
@@ -2765,24 +2833,51 @@ class MainWindow(QMainWindow):
2765
2833
  # Convert back to boolean mask
2766
2834
  return (filtered_mask > 0).astype(bool)
2767
2835
 
2768
- def _finalize_polygon(self):
2836
+ def _finalize_polygon(self, erase_mode=False):
2769
2837
  """Finalize polygon drawing."""
2770
2838
  if len(self.polygon_points) < 3:
2771
2839
  return
2772
2840
 
2773
- new_segment = {
2774
- "vertices": [[p.x(), p.y()] for p in self.polygon_points],
2775
- "type": "Polygon",
2776
- "mask": None,
2777
- }
2778
- self.segment_manager.add_segment(new_segment)
2779
- # Record the action for undo
2780
- self.action_history.append(
2781
- {
2782
- "type": "add_segment",
2783
- "segment_index": len(self.segment_manager.segments) - 1,
2841
+ if erase_mode:
2842
+ # Erase overlapping segments using polygon vertices
2843
+ image_height = self.viewer._pixmap_item.pixmap().height()
2844
+ image_width = self.viewer._pixmap_item.pixmap().width()
2845
+ image_size = (image_height, image_width)
2846
+ removed_indices, removed_segments_data = (
2847
+ self.segment_manager.erase_segments_with_shape(
2848
+ self.polygon_points, image_size
2849
+ )
2850
+ )
2851
+
2852
+ if removed_indices:
2853
+ # Record the action for undo
2854
+ self.action_history.append(
2855
+ {
2856
+ "type": "erase_segments",
2857
+ "removed_segments": removed_segments_data,
2858
+ }
2859
+ )
2860
+ self._show_notification(
2861
+ f"Applied eraser to {len(removed_indices)} segment(s)"
2862
+ )
2863
+ else:
2864
+ self._show_notification("No segments to erase")
2865
+ else:
2866
+ # Create new polygon segment (normal mode)
2867
+ new_segment = {
2868
+ "vertices": [[p.x(), p.y()] for p in self.polygon_points],
2869
+ "type": "Polygon",
2870
+ "mask": None,
2784
2871
  }
2785
- )
2872
+ self.segment_manager.add_segment(new_segment)
2873
+ # Record the action for undo
2874
+ self.action_history.append(
2875
+ {
2876
+ "type": "add_segment",
2877
+ "segment_index": len(self.segment_manager.segments) - 1,
2878
+ }
2879
+ )
2880
+
2786
2881
  # Clear redo history when a new action is performed
2787
2882
  self.redo_history.clear()
2788
2883
 
@@ -2881,6 +2976,8 @@ class MainWindow(QMainWindow):
2881
2976
  (h, w),
2882
2977
  class_order,
2883
2978
  crop_coords, # Pass crop coordinates if available
2979
+ settings.get("pixel_priority_enabled", False),
2980
+ settings.get("pixel_priority_ascending", True),
2884
2981
  )
2885
2982
  saved_files.append(os.path.basename(npz_path))
2886
2983
  # Track saved file for highlighting later
@@ -2915,6 +3012,8 @@ class MainWindow(QMainWindow):
2915
3012
  class_order,
2916
3013
  class_labels,
2917
3014
  crop_coords, # Pass crop coordinates if available
3015
+ settings.get("pixel_priority_enabled", False),
3016
+ settings.get("pixel_priority_ascending", True),
2918
3017
  )
2919
3018
  saved_files.append(os.path.basename(txt_path))
2920
3019
  # Track saved file for highlighting later
@@ -3058,6 +3157,8 @@ class MainWindow(QMainWindow):
3058
3157
  (h, w),
3059
3158
  class_order,
3060
3159
  self.current_crop_coords,
3160
+ settings.get("pixel_priority_enabled", False),
3161
+ settings.get("pixel_priority_ascending", True),
3061
3162
  )
3062
3163
  logger.debug(f"NPZ save completed: {npz_path}")
3063
3164
  self._show_success_notification(
@@ -3087,6 +3188,8 @@ class MainWindow(QMainWindow):
3087
3188
  class_order,
3088
3189
  class_labels,
3089
3190
  self.current_crop_coords,
3191
+ settings.get("pixel_priority_enabled", False),
3192
+ settings.get("pixel_priority_ascending", True),
3090
3193
  )
3091
3194
  if txt_path:
3092
3195
  logger.debug(f"TXT save completed: {txt_path}")
@@ -3528,9 +3631,11 @@ class MainWindow(QMainWindow):
3528
3631
  """Clear notification from status bar."""
3529
3632
  self.status_bar.clear_message()
3530
3633
 
3531
- def _accept_ai_segment(self):
3634
+ def _accept_ai_segment(self, erase_mode=False):
3532
3635
  """Accept the current AI segment preview (spacebar handler)."""
3533
- logger.debug(f"_accept_ai_segment called - view_mode: {self.view_mode}")
3636
+ logger.debug(
3637
+ f"_accept_ai_segment called - view_mode: {self.view_mode}, erase_mode: {erase_mode}"
3638
+ )
3534
3639
 
3535
3640
  if self.view_mode == "single":
3536
3641
  # Single view mode - check for preview mask (both point-based and bbox)
@@ -3547,7 +3652,7 @@ class MainWindow(QMainWindow):
3547
3652
  )
3548
3653
 
3549
3654
  logger.debug(
3550
- f"Single view - has_preview_item: {has_preview_item}, has_preview_mask: {has_preview_mask}, has_bbox_preview: {has_bbox_preview}"
3655
+ f"Single view - has_preview_item: {has_preview_item}, has_preview_mask: {has_preview_mask}, has_bbox_preview: {has_bbox_preview}, erase_mode: {erase_mode}"
3551
3656
  )
3552
3657
 
3553
3658
  # Handle bbox preview first
@@ -3556,21 +3661,56 @@ class MainWindow(QMainWindow):
3556
3661
  self.ai_bbox_preview_mask
3557
3662
  )
3558
3663
  if filtered_mask is not None:
3559
- # Create actual segment
3560
- new_segment = {
3561
- "type": "AI",
3562
- "mask": filtered_mask,
3563
- "vertices": None,
3564
- }
3565
- self.segment_manager.add_segment(new_segment)
3664
+ if erase_mode:
3665
+ # Erase overlapping segments
3666
+ logger.debug(
3667
+ "Erase mode active for bbox preview - applying eraser to AI segment"
3668
+ )
3669
+ image_height = self.viewer._pixmap_item.pixmap().height()
3670
+ image_width = self.viewer._pixmap_item.pixmap().width()
3671
+ image_size = (image_height, image_width)
3672
+ removed_indices, removed_segments_data = (
3673
+ self.segment_manager.erase_segments_with_mask(
3674
+ filtered_mask, image_size
3675
+ )
3676
+ )
3677
+ logger.debug(
3678
+ f"Bbox erase operation completed - modified {len(removed_indices)} segments"
3679
+ )
3566
3680
 
3567
- # Record the action for undo
3568
- self.action_history.append(
3569
- {
3570
- "type": "add_segment",
3571
- "segment_index": len(self.segment_manager.segments) - 1,
3681
+ if removed_indices:
3682
+ # Record the action for undo
3683
+ self.action_history.append(
3684
+ {
3685
+ "type": "erase_segments",
3686
+ "removed_segments": removed_segments_data,
3687
+ }
3688
+ )
3689
+ self._show_success_notification(
3690
+ f"Erased {len(removed_indices)} segment(s)!"
3691
+ )
3692
+ else:
3693
+ self._show_notification("No segments to erase")
3694
+ else:
3695
+ # Create actual segment (normal mode)
3696
+ new_segment = {
3697
+ "type": "AI",
3698
+ "mask": filtered_mask,
3699
+ "vertices": None,
3572
3700
  }
3573
- )
3701
+ self.segment_manager.add_segment(new_segment)
3702
+
3703
+ # Record the action for undo
3704
+ self.action_history.append(
3705
+ {
3706
+ "type": "add_segment",
3707
+ "segment_index": len(self.segment_manager.segments) - 1,
3708
+ }
3709
+ )
3710
+ self._show_success_notification(
3711
+ "AI bounding box segment saved!"
3712
+ )
3713
+
3574
3714
  # Clear redo history when a new action is performed
3575
3715
  self.redo_history.clear()
3576
3716
 
@@ -3586,7 +3726,6 @@ class MainWindow(QMainWindow):
3586
3726
  # Clear all points
3587
3727
  self.clear_all_points()
3588
3728
  self._update_all_lists()
3589
- self._show_success_notification("AI bounding box segment saved!")
3590
3729
  else:
3591
3730
  self._show_warning_notification(
3592
3731
  "All segments filtered out by fragment threshold"
@@ -3597,28 +3736,60 @@ class MainWindow(QMainWindow):
3597
3736
  self.current_preview_mask
3598
3737
  )
3599
3738
  if filtered_mask is not None:
3600
- # Create actual segment
3601
- new_segment = {
3602
- "type": "AI",
3603
- "mask": filtered_mask,
3604
- "vertices": None,
3605
- }
3606
- self.segment_manager.add_segment(new_segment)
3739
+ if erase_mode:
3740
+ # Erase overlapping segments
3741
+ logger.debug(
3742
+ "Erase mode active for regular AI preview - applying eraser to AI segment"
3743
+ )
3744
+ image_height = self.viewer._pixmap_item.pixmap().height()
3745
+ image_width = self.viewer._pixmap_item.pixmap().width()
3746
+ image_size = (image_height, image_width)
3747
+ removed_indices, removed_segments_data = (
3748
+ self.segment_manager.erase_segments_with_mask(
3749
+ filtered_mask, image_size
3750
+ )
3751
+ )
3752
+ logger.debug(
3753
+ f"Regular AI erase operation completed - modified {len(removed_indices)} segments"
3754
+ )
3607
3755
 
3608
- # Record the action for undo
3609
- self.action_history.append(
3610
- {
3611
- "type": "add_segment",
3612
- "segment_index": len(self.segment_manager.segments) - 1,
3756
+ if removed_indices:
3757
+ # Record the action for undo
3758
+ self.action_history.append(
3759
+ {
3760
+ "type": "erase_segments",
3761
+ "removed_segments": removed_segments_data,
3762
+ }
3763
+ )
3764
+ self._show_notification(
3765
+ f"Applied eraser to {len(removed_indices)} segment(s)"
3766
+ )
3767
+ else:
3768
+ self._show_notification("No segments to erase")
3769
+ else:
3770
+ # Create actual segment (normal mode)
3771
+ new_segment = {
3772
+ "type": "AI",
3773
+ "mask": filtered_mask,
3774
+ "vertices": None,
3613
3775
  }
3614
- )
3776
+ self.segment_manager.add_segment(new_segment)
3777
+
3778
+ # Record the action for undo
3779
+ self.action_history.append(
3780
+ {
3781
+ "type": "add_segment",
3782
+ "segment_index": len(self.segment_manager.segments) - 1,
3783
+ }
3784
+ )
3785
+ self._show_notification("AI segment accepted")
3786
+
3615
3787
  # Clear redo history when a new action is performed
3616
3788
  self.redo_history.clear()
3617
3789
 
3618
3790
  # Clear all points after accepting
3619
3791
  self.clear_all_points()
3620
3792
  self._update_all_lists()
3621
- self._show_notification("AI segment accepted")
3622
3793
  else:
3623
3794
  self._show_warning_notification(
3624
3795
  "All segments filtered out by fragment threshold"
@@ -3628,6 +3799,18 @@ class MainWindow(QMainWindow):
3628
3799
  self.viewer.scene().removeItem(self.preview_mask_item)
3629
3800
  self.preview_mask_item = None
3630
3801
  self.current_preview_mask = None
3802
+ else:
3803
+ # No AI preview found in single view
3804
+ if erase_mode:
3805
+ logger.debug(
3806
+ "No AI segment preview to erase - no preview mask found in single view"
3807
+ )
3808
+ self._show_notification("No AI segment preview to erase")
3809
+ else:
3810
+ logger.debug(
3811
+ "No AI segment preview to accept - no preview mask found in single view"
3812
+ )
3813
+ self._show_notification("No AI segment preview to accept")
3631
3814
 
3632
3815
  elif self.view_mode == "multi":
3633
3816
  # Multi-view mode - use the multi-view mode handler to save AI predictions
@@ -3707,7 +3890,13 @@ class MainWindow(QMainWindow):
3707
3890
  self._show_success_notification("AI segment(s) accepted")
3708
3891
  self._update_all_lists()
3709
3892
  else:
3710
- self._show_notification("No AI segment preview to accept")
3893
+ if erase_mode:
3894
+ logger.debug(
3895
+ "No AI segment preview to erase - no preview mask found"
3896
+ )
3897
+ self._show_notification("No AI segment preview to erase")
3898
+ else:
3899
+ self._show_notification("No AI segment preview to accept")
3711
3900
 
3712
3901
  def _show_hotkey_dialog(self):
3713
3902
  """Show the hotkey configuration dialog."""
@@ -4068,28 +4257,64 @@ class MainWindow(QMainWindow):
4068
4257
  self.drag_start_pos = None
4069
4258
 
4070
4259
  if rect.width() >= 2 and rect.height() >= 2:
4071
- # Convert QRectF to QPolygonF
4260
+ # Check if shift is pressed for erase functionality
4261
+ modifiers = QApplication.keyboardModifiers()
4262
+ shift_pressed = bool(modifiers & Qt.KeyboardModifier.ShiftModifier)
4263
+
4264
+ if shift_pressed:
4265
+ logger.debug("Shift+bbox release - activating erase mode")
4266
+
4267
+ # Convert QRectF to QPolygonF for vertex representation
4072
4268
  polygon = QPolygonF()
4073
4269
  polygon.append(rect.topLeft())
4074
4270
  polygon.append(rect.topRight())
4075
4271
  polygon.append(rect.bottomRight())
4076
4272
  polygon.append(rect.bottomLeft())
4077
4273
 
4078
- new_segment = {
4079
- "vertices": [[p.x(), p.y()] for p in list(polygon)],
4080
- "type": "Polygon", # Bounding boxes are stored as polygons
4081
- "mask": None,
4082
- }
4274
+ polygon_vertices = [QPointF(p.x(), p.y()) for p in list(polygon)]
4083
4275
 
4084
- self.segment_manager.add_segment(new_segment)
4276
+ if shift_pressed:
4277
+ # Erase overlapping segments using bbox vertices
4278
+ image_height = self.viewer._pixmap_item.pixmap().height()
4279
+ image_width = self.viewer._pixmap_item.pixmap().width()
4280
+ image_size = (image_height, image_width)
4281
+ removed_indices, removed_segments_data = (
4282
+ self.segment_manager.erase_segments_with_shape(
4283
+ polygon_vertices, image_size
4284
+ )
4285
+ )
4085
4286
 
4086
- # Record the action for undo
4087
- self.action_history.append(
4088
- {
4089
- "type": "add_segment",
4090
- "segment_index": len(self.segment_manager.segments) - 1,
4287
+ if removed_indices:
4288
+ # Record the action for undo
4289
+ self.action_history.append(
4290
+ {
4291
+ "type": "erase_segments",
4292
+ "removed_segments": removed_segments_data,
4293
+ }
4294
+ )
4295
+ self._show_notification(
4296
+ f"Applied eraser to {len(removed_indices)} segment(s)"
4297
+ )
4298
+ else:
4299
+ self._show_notification("No segments to erase")
4300
+ else:
4301
+ # Create new bbox segment (normal mode)
4302
+ new_segment = {
4303
+ "vertices": [[p.x(), p.y()] for p in polygon_vertices],
4304
+ "type": "Polygon", # Bounding boxes are stored as polygons
4305
+ "mask": None,
4091
4306
  }
4092
- )
4307
+
4308
+ self.segment_manager.add_segment(new_segment)
4309
+
4310
+ # Record the action for undo
4311
+ self.action_history.append(
4312
+ {
4313
+ "type": "add_segment",
4314
+ "segment_index": len(self.segment_manager.segments) - 1,
4315
+ }
4316
+ )
4317
+
4093
4318
  # Clear redo history when a new action is performed
4094
4319
  self.redo_history.clear()
4095
4320
  self._update_all_lists()
@@ -4313,6 +4538,10 @@ class MainWindow(QMainWindow):
4313
4538
 
4314
4539
  def _handle_polygon_click(self, pos):
4315
4540
  """Handle polygon drawing clicks."""
4541
+ # Check if shift is pressed for erase functionality
4542
+ modifiers = QApplication.keyboardModifiers()
4543
+ shift_pressed = bool(modifiers & Qt.KeyboardModifier.ShiftModifier)
4544
+
4316
4545
  # Check if clicking near the first point to close polygon
4317
4546
  if self.polygon_points and len(self.polygon_points) > 2:
4318
4547
  first_point = self.polygon_points[0]
@@ -4320,7 +4549,11 @@ class MainWindow(QMainWindow):
4320
4549
  pos.y() - first_point.y()
4321
4550
  ) ** 2
4322
4551
  if distance_squared < self.polygon_join_threshold**2:
4323
- self._finalize_polygon()
4552
+ if shift_pressed:
4553
+ logger.debug(
4554
+ "Shift+click polygon completion - activating erase mode"
4555
+ )
4556
+ self._finalize_polygon(erase_mode=shift_pressed)
4324
4557
  return
4325
4558
 
4326
4559
  # Add new point to polygon
@@ -4612,6 +4845,14 @@ class MainWindow(QMainWindow):
4612
4845
  # Update visual display to clear active class
4613
4846
  self.right_panel.update_active_class_display(None)
4614
4847
 
4848
+ def _toggle_recent_class(self):
4849
+ """Toggle the most recent class used/toggled, or the last class in the list."""
4850
+ class_id = self.segment_manager.get_class_to_toggle_with_hotkey()
4851
+ if class_id is not None:
4852
+ self._handle_class_toggle(class_id)
4853
+ else:
4854
+ self._show_notification("No classes available to toggle")
4855
+
4615
4856
  def _pop_out_left_panel(self):
4616
4857
  """Pop out the left control panel into a separate window."""
4617
4858
  if self.left_panel_popout is not None:
@@ -4,6 +4,47 @@ from PyQt6.QtCore import pyqtSignal
4
4
  from PyQt6.QtWidgets import QCheckBox, QGroupBox, QVBoxLayout, QWidget
5
5
 
6
6
 
7
+ class PixelPriorityCheckBox(QCheckBox):
8
+ """Custom tri-state checkbox for pixel priority settings."""
9
+
10
+ def __init__(self, parent=None):
11
+ super().__init__(parent)
12
+ self._state = 0 # 0: disabled, 1: ascending, 2: descending
13
+ self._update_display()
14
+ self.clicked.connect(self._cycle_state)
15
+
16
+ def _cycle_state(self):
17
+ """Cycle through the three states."""
18
+ self._state = (self._state + 1) % 3
19
+ self._update_display()
20
+
21
+ def _update_display(self):
22
+ """Update checkbox text and check state based on current state."""
23
+ if self._state == 0:
24
+ self.setText("Pixel Priority Ascending")
25
+ self.setChecked(False)
26
+ elif self._state == 1:
27
+ self.setText("Pixel Priority Ascending")
28
+ self.setChecked(True)
29
+ else: # state == 2
30
+ self.setText("Pixel Priority Descending")
31
+ self.setChecked(True)
32
+
33
+ def get_pixel_priority_settings(self):
34
+ """Get current pixel priority settings."""
35
+ return {"enabled": self._state != 0, "ascending": self._state == 1}
36
+
37
+ def set_pixel_priority_settings(self, enabled: bool, ascending: bool):
38
+ """Set pixel priority settings."""
39
+ if not enabled:
40
+ self._state = 0
41
+ elif ascending:
42
+ self._state = 1
43
+ else:
44
+ self._state = 2
45
+ self._update_display()
46
+
47
+
7
48
  class SettingsWidget(QWidget):
8
49
  """Widget for application settings."""
9
50
 
@@ -69,6 +110,16 @@ class SettingsWidget(QWidget):
69
110
  self.chk_operate_on_view.setChecked(False)
70
111
  layout.addWidget(self.chk_operate_on_view)
71
112
 
113
+ # Pixel Priority
114
+ self.chk_pixel_priority = PixelPriorityCheckBox()
115
+ self.chk_pixel_priority.setToolTip(
116
+ "Control pixel ownership when multiple classes overlap.\n"
117
+ "Click to cycle through: Off → Ascending → Descending → Off\n"
118
+ "Ascending: Lower class indices take priority\n"
119
+ "Descending: Higher class indices take priority"
120
+ )
121
+ layout.addWidget(self.chk_pixel_priority)
122
+
72
123
  # Main layout
73
124
  main_layout = QVBoxLayout(self)
74
125
  main_layout.setContentsMargins(0, 0, 0, 0)
@@ -90,6 +141,9 @@ class SettingsWidget(QWidget):
90
141
  ]:
91
142
  checkbox.stateChanged.connect(self.settings_changed)
92
143
 
144
+ # Connect pixel priority checkbox (uses clicked signal instead of stateChanged)
145
+ self.chk_pixel_priority.clicked.connect(self.settings_changed)
146
+
93
147
  def _handle_save_checkbox_change(self):
94
148
  """Ensure at least one save format is selected."""
95
149
  is_npz_checked = self.chk_save_npz.isChecked()
@@ -104,6 +158,7 @@ class SettingsWidget(QWidget):
104
158
 
105
159
  def get_settings(self):
106
160
  """Get current settings as dictionary."""
161
+ pixel_priority_settings = self.chk_pixel_priority.get_pixel_priority_settings()
107
162
  return {
108
163
  "auto_save": self.chk_auto_save.isChecked(),
109
164
  "save_npz": self.chk_save_npz.isChecked(),
@@ -111,6 +166,8 @@ class SettingsWidget(QWidget):
111
166
  "yolo_use_alias": self.chk_yolo_use_alias.isChecked(),
112
167
  "save_class_aliases": self.chk_save_class_aliases.isChecked(),
113
168
  "operate_on_view": self.chk_operate_on_view.isChecked(),
169
+ "pixel_priority_enabled": pixel_priority_settings["enabled"],
170
+ "pixel_priority_ascending": pixel_priority_settings["ascending"],
114
171
  }
115
172
 
116
173
  def set_settings(self, settings):
@@ -123,3 +180,7 @@ class SettingsWidget(QWidget):
123
180
  settings.get("save_class_aliases", False)
124
181
  )
125
182
  self.chk_operate_on_view.setChecked(settings.get("operate_on_view", False))
183
+ self.chk_pixel_priority.set_pixel_priority_settings(
184
+ settings.get("pixel_priority_enabled", False),
185
+ settings.get("pixel_priority_ascending", True),
186
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lazylabel-gui
3
- Version: 1.3.8
3
+ Version: 1.3.9
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
@@ -2,13 +2,13 @@ lazylabel/__init__.py,sha256=0yitHZYNNuF4Rqj7cqE0TrfDV8zhzKD3A5W1vOymHK4,199
2
2
  lazylabel/__main__.py,sha256=5IWklQrNkzeMH6rchzZxxoeSnZlkz-HRiPWXqjjj2yA,139
3
3
  lazylabel/main.py,sha256=g_QhEWNhgxp8LLFfxTXDMVTpdaHog_wrs5cv01xRL0Q,904
4
4
  lazylabel/config/__init__.py,sha256=TnDXH0yx0zy97FfjpiVPHleMgMrM5YwWrUbvevSNJjg,263
5
- lazylabel/config/hotkeys.py,sha256=PwxBUy7RvIUU3HCXT74NnexPbg2VFQrbR5FTGek31h8,8008
5
+ lazylabel/config/hotkeys.py,sha256=kHGHbtMTh_VbGQlEnfT8pmhLsu7MOqZtSFnC7shjqLA,8465
6
6
  lazylabel/config/paths.py,sha256=ZVKbtaNOxmYO4l6JgsY-8DXaE_jaJfDg2RQJJn3-5nw,1275
7
- lazylabel/config/settings.py,sha256=myBqkgnYv_P1OomieXt8luCOSNjBiDVN-TybSqHkAgA,1990
7
+ lazylabel/config/settings.py,sha256=FhRF3nja_nEgulO6Qt_ToHhy5qGXwghtTTCLCL-LC48,2104
8
8
  lazylabel/core/__init__.py,sha256=FmRjop_uIBSJwKMGhaZ-3Iwu34LkoxTuD-hnq5vbTSY,232
9
- lazylabel/core/file_manager.py,sha256=Fs4IJIm2hHvYXeDWR7mqEu2EE21o6qwSPpECv8CYQ3o,7283
9
+ lazylabel/core/file_manager.py,sha256=bTsdBx1NzZzS9aG0f5toy9CGxp0Z_LXXBz_C_g3fgvw,7569
10
10
  lazylabel/core/model_manager.py,sha256=NmgnIrw9sb_vSwRVkBr2Z_Mc2kFfdFukFgUhqs2e-L0,6829
11
- lazylabel/core/segment_manager.py,sha256=M6kHcYeiub3WqL01NElCvKOc2GNmf72LUM1W8XwSaxc,6465
11
+ lazylabel/core/segment_manager.py,sha256=43_BJ4CovXAAeuEq9wkuUD54mCFPuwkxkYUXHajZug4,15204
12
12
  lazylabel/models/__init__.py,sha256=fIlk_0DuZfiClcm0XlZdimeHzunQwBmTMI4PcGsaymw,91
13
13
  lazylabel/models/sam2_model.py,sha256=Z0UVs_m9VwUO4znZF5VA4-HQSF-oCT9RbKtK3EU20qU,21747
14
14
  lazylabel/models/sam_model.py,sha256=Q1GAaFG6n5JoZXhClcUZKN-gvA_wmtVuUYzELG7zhPg,8833
@@ -18,7 +18,7 @@ lazylabel/ui/editable_vertex.py,sha256=ofo3r8ZZ3b8oYV40vgzZuS3QnXYBNzE92ArC2wggJ
18
18
  lazylabel/ui/hotkey_dialog.py,sha256=U_B76HLOxWdWkfA4d2XgRUaZTJPAAE_m5fmwf7Rh-5Y,14743
19
19
  lazylabel/ui/hoverable_pixelmap_item.py,sha256=UbWVxpmCTaeae_AeA8gMOHYGUmAw40fZBFTS3sZlw48,1821
20
20
  lazylabel/ui/hoverable_polygon_item.py,sha256=gZalImJ_PJYM7xON0iiSjQ335ZBREOfSscKLVs-MSh8,2314
21
- lazylabel/ui/main_window.py,sha256=udjyVXL6Ms_zjpT6TILdqvaSkX6u_YyaxWF93O93W2s,384664
21
+ lazylabel/ui/main_window.py,sha256=LfbGs41QoUzAj3NKYR_rRtyEF7PltB-sdBWS8ukQjfE,396744
22
22
  lazylabel/ui/numeric_table_widget_item.py,sha256=dQUlIFu9syCxTGAHVIlmbgkI7aJ3f3wmDPBz1AGK9Bg,283
23
23
  lazylabel/ui/photo_viewer.py,sha256=3o7Xldn9kJWvWlbpcHDRMk87dnB5xZKbfyAT3oBYlIo,6670
24
24
  lazylabel/ui/reorderable_class_table.py,sha256=sxHhQre5O_MXLDFgKnw43QnvXXoqn5xRKMGitgO7muI,2371
@@ -34,7 +34,7 @@ lazylabel/ui/widgets/channel_threshold_widget.py,sha256=DSYDOUTMXo02Fn5P4cjh2OqD
34
34
  lazylabel/ui/widgets/fft_threshold_widget.py,sha256=mcbkNoP0aj-Pe57sgIWBnmg4JiEbTikrwG67Vgygnio,20859
35
35
  lazylabel/ui/widgets/fragment_threshold_widget.py,sha256=YtToua1eAUtEuJ3EwdCMvI-39TRrFnshkty4tnR4OMU,3492
36
36
  lazylabel/ui/widgets/model_selection_widget.py,sha256=k4zyhWszi_7e-0TPa0Go1LLPZpTNGtrYSb-yT7uGiVU,8420
37
- lazylabel/ui/widgets/settings_widget.py,sha256=ShTaLJeXxwrSuTV4kmtV2JiWjfREil2D1nvPUIfAgDs,4859
37
+ lazylabel/ui/widgets/settings_widget.py,sha256=-LUIZGSQEf51fIYscJ5dzIqTsE09Rt4wGz7kAS1KnNQ,7311
38
38
  lazylabel/ui/widgets/status_bar.py,sha256=wTbMQNEOBfmtNj8EVFZS_lxgaemu-CbRXeZzEQDaVz8,4014
39
39
  lazylabel/ui/workers/__init__.py,sha256=1pctFFvHiVW3meiaW5_-AUgQ0AKcvCF4J8-QHoWRtwI,529
40
40
  lazylabel/ui/workers/image_discovery_worker.py,sha256=mW4diqPnbyGgyX2DDKm_o9NnAq4V-Jywk_f7risVgS0,2405
@@ -47,9 +47,9 @@ lazylabel/utils/custom_file_system_model.py,sha256=-3EimlybvevH6bvqBE0qdFnLADVta
47
47
  lazylabel/utils/fast_file_manager.py,sha256=kzbWz_xKufG5bP6sjyZV1fmOKRWPPNeL-xLYZEu_8wE,44697
48
48
  lazylabel/utils/logger.py,sha256=R7z6ifgA-NY-9ZbLlNH0i19zzwXndJ_gkG2J1zpVEhg,1306
49
49
  lazylabel/utils/utils.py,sha256=sYSCoXL27OaLgOZaUkCAhgmKZ7YfhR3Cc5F8nDIa3Ig,414
50
- lazylabel_gui-1.3.8.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
51
- lazylabel_gui-1.3.8.dist-info/METADATA,sha256=k6_eCNou42Egim6fW_LuvX3oLd67qTxjVqk2ovHPz0E,7754
52
- lazylabel_gui-1.3.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
- lazylabel_gui-1.3.8.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
54
- lazylabel_gui-1.3.8.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
55
- lazylabel_gui-1.3.8.dist-info/RECORD,,
50
+ lazylabel_gui-1.3.9.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
51
+ lazylabel_gui-1.3.9.dist-info/METADATA,sha256=MO2RNXV4Y62pLyuVezJdVFS-NcxmVfNJD8lpuPJQ20w,7754
52
+ lazylabel_gui-1.3.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ lazylabel_gui-1.3.9.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
54
+ lazylabel_gui-1.3.9.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
55
+ lazylabel_gui-1.3.9.dist-info/RECORD,,