lazylabel-gui 1.3.1__py3-none-any.whl → 1.3.2__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.
@@ -24,16 +24,48 @@ class FileManager:
24
24
  crop_coords: tuple[int, int, int, int] | None = None,
25
25
  ) -> str:
26
26
  """Save segments as NPZ file."""
27
+ logger.debug(f"Saving NPZ for image: {image_path}")
28
+ logger.debug(f"Image size: {image_size}, Class order: {class_order}")
29
+
30
+ # Validate inputs
31
+ if not class_order:
32
+ raise ValueError("No classes defined for saving")
33
+
27
34
  final_mask_tensor = self.segment_manager.create_final_mask_tensor(
28
35
  image_size, class_order
29
36
  )
30
37
 
38
+ # Validate mask tensor
39
+ if final_mask_tensor.size == 0:
40
+ raise ValueError("Empty mask tensor generated")
41
+
42
+ logger.debug(f"Final mask tensor shape: {final_mask_tensor.shape}")
43
+
31
44
  # Apply crop if coordinates are provided
32
45
  if crop_coords:
33
46
  final_mask_tensor = self._apply_crop_to_mask(final_mask_tensor, crop_coords)
47
+ logger.debug(f"Applied crop: {crop_coords}")
34
48
 
35
49
  npz_path = os.path.splitext(image_path)[0] + ".npz"
36
- np.savez_compressed(npz_path, mask=final_mask_tensor.astype(np.uint8))
50
+
51
+ # Create parent directory if it doesn't exist
52
+ parent_dir = os.path.dirname(npz_path)
53
+ if parent_dir: # Only create if there's actually a parent directory
54
+ os.makedirs(parent_dir, exist_ok=True)
55
+ logger.debug(f"Ensured directory exists: {parent_dir}")
56
+
57
+ # Save the NPZ file
58
+ try:
59
+ np.savez_compressed(npz_path, mask=final_mask_tensor.astype(np.uint8))
60
+ logger.debug(f"Saved NPZ file: {npz_path}")
61
+ except Exception as e:
62
+ raise OSError(f"Failed to save NPZ file {npz_path}: {str(e)}") from e
63
+
64
+ # Verify the file was actually created
65
+ if not os.path.exists(npz_path):
66
+ raise OSError(f"NPZ file was not created: {npz_path}")
67
+
68
+ logger.info(f"Successfully saved NPZ: {os.path.basename(npz_path)}")
37
69
  return npz_path
38
70
 
39
71
  def save_yolo_txt(
@@ -1176,6 +1176,8 @@ class MainWindow(QMainWindow):
1176
1176
  # Right panel connections
1177
1177
  self.right_panel.open_folder_requested.connect(self._open_folder_dialog)
1178
1178
  self.right_panel.image_selected.connect(self._load_selected_image)
1179
+ # Connect new path-based signal from FastFileManager
1180
+ self.right_panel.image_path_selected.connect(self._load_image_from_path)
1179
1181
  self.right_panel.merge_selection_requested.connect(
1180
1182
  self._assign_selected_to_class
1181
1183
  )
@@ -1625,6 +1627,13 @@ class MainWindow(QMainWindow):
1625
1627
  self._start_background_image_discovery()
1626
1628
  self.viewer.setFocus()
1627
1629
 
1630
+ def _load_image_from_path(self, file_path: Path):
1631
+ """Load image from a Path object (used by FastFileManager)."""
1632
+ if file_path.is_file() and self.file_manager.is_image_file(str(file_path)):
1633
+ # Convert Path to QModelIndex for compatibility
1634
+ # This allows existing code to work while using the new file manager
1635
+ self._load_image_by_path(str(file_path))
1636
+
1628
1637
  def _load_selected_image(self, index):
1629
1638
  """Load the selected image. Auto-saves previous work if enabled."""
1630
1639
 
@@ -1686,6 +1695,131 @@ class MainWindow(QMainWindow):
1686
1695
 
1687
1696
  self._show_success_notification(f"Loaded: {Path(self.current_image_path).name}")
1688
1697
 
1698
+ def _load_image_by_path(self, path: str):
1699
+ """Load image by file path directly (for FastFileManager)."""
1700
+ # Check if we're in multi-view mode
1701
+ if hasattr(self, "view_mode") and self.view_mode == "multi":
1702
+ # For multi-view, we need to handle differently
1703
+ # Load the selected image and consecutive ones
1704
+ self._load_multi_view_from_path(path)
1705
+ return
1706
+
1707
+ if path == self.current_image_path: # Only reset if loading a new image
1708
+ return
1709
+
1710
+ # Auto-save if enabled and we have a current image (not the first load)
1711
+ if self.current_image_path and self.control_panel.get_settings().get(
1712
+ "auto_save", True
1713
+ ):
1714
+ self._save_output_to_npz()
1715
+
1716
+ self.segment_manager.clear()
1717
+ # Remove all scene items except the pixmap
1718
+ items_to_remove = [
1719
+ item
1720
+ for item in self.viewer.scene().items()
1721
+ if item is not self.viewer._pixmap_item
1722
+ ]
1723
+ for item in items_to_remove:
1724
+ self.viewer.scene().removeItem(item)
1725
+ self.current_image_path = path
1726
+
1727
+ # Load the image
1728
+ original_image = cv2.imread(path)
1729
+ if original_image is None:
1730
+ logger.error(f"Failed to load image: {path}")
1731
+ return
1732
+
1733
+ # Convert BGR to RGB
1734
+ if len(original_image.shape) == 3:
1735
+ original_image = cv2.cvtColor(original_image, cv2.COLOR_BGR2RGB)
1736
+ self.original_image = original_image
1737
+
1738
+ # Convert to QImage and display
1739
+ height, width = original_image.shape[:2]
1740
+ bytes_per_line = 3 * width if len(original_image.shape) == 3 else width
1741
+
1742
+ if len(original_image.shape) == 3:
1743
+ q_image = QImage(
1744
+ original_image.data.tobytes(),
1745
+ width,
1746
+ height,
1747
+ bytes_per_line,
1748
+ QImage.Format.Format_RGB888,
1749
+ )
1750
+ else:
1751
+ q_image = QImage(
1752
+ original_image.data.tobytes(),
1753
+ width,
1754
+ height,
1755
+ bytes_per_line,
1756
+ QImage.Format.Format_Grayscale8,
1757
+ )
1758
+
1759
+ pixmap = QPixmap.fromImage(q_image)
1760
+ self.viewer.set_photo(pixmap)
1761
+
1762
+ # Load existing segments and class aliases
1763
+ self.file_manager.load_class_aliases(path)
1764
+ self.file_manager.load_existing_mask(path)
1765
+
1766
+ # Update UI lists to reflect loaded segments
1767
+ self._update_all_lists()
1768
+
1769
+ # Update display if we have an update method
1770
+ if hasattr(self, "_update_display"):
1771
+ self._update_display()
1772
+ self._show_success_notification(f"Loaded: {Path(path).name}")
1773
+
1774
+ # Update file selection in the file manager
1775
+ self.right_panel.select_file(Path(path))
1776
+
1777
+ def _load_multi_view_from_path(self, path: str):
1778
+ """Load multi-view starting from a specific path using FastFileManager."""
1779
+ config = self._get_multi_view_config()
1780
+ num_viewers = config["num_viewers"]
1781
+
1782
+ # Auto-save if enabled
1783
+ if (
1784
+ hasattr(self, "multi_view_images")
1785
+ and self.multi_view_images
1786
+ and self.multi_view_images[0]
1787
+ and self.control_panel.get_settings().get("auto_save", True)
1788
+ ):
1789
+ self._save_multi_view_output()
1790
+
1791
+ # Get all files from the FastFileManager model
1792
+ file_manager_model = self.right_panel.file_manager._model
1793
+ all_files = []
1794
+ current_idx = -1
1795
+
1796
+ # Collect all files from the model
1797
+ for i in range(file_manager_model.rowCount()):
1798
+ file_info = file_manager_model.getFileInfo(i)
1799
+ if file_info:
1800
+ all_files.append(str(file_info.path))
1801
+ if str(file_info.path) == path:
1802
+ current_idx = i
1803
+
1804
+ if current_idx == -1:
1805
+ # File not found in current list
1806
+ return
1807
+
1808
+ # Load consecutive images
1809
+ images_to_load = []
1810
+ for i in range(num_viewers):
1811
+ idx = current_idx + i
1812
+ if idx < len(all_files):
1813
+ images_to_load.append(all_files[idx])
1814
+ else:
1815
+ images_to_load.append(None)
1816
+
1817
+ # Load the images
1818
+ self._load_multi_view_images(images_to_load)
1819
+
1820
+ # Update file manager selection
1821
+ self.right_panel.select_file(Path(path))
1822
+
1689
1823
  def _load_selected_image_multi_view(self, index, path):
1690
1824
  """Load selected image in multi-view mode starting from the selected file."""
1691
1825
  # Auto-save if enabled and we have current images (not the first load)
@@ -2141,17 +2275,9 @@ class MainWindow(QMainWindow):
2141
2275
  self._load_next_multi_batch()
2142
2276
  return
2143
2277
 
2144
- if not self.current_file_index.isValid():
2145
- return
2146
- parent = self.current_file_index.parent()
2147
- row = self.current_file_index.row()
2148
- # Find next valid image file
2149
- for next_row in range(row + 1, self.file_model.rowCount(parent)):
2150
- next_index = self.file_model.index(next_row, 0, parent)
2151
- path = self.file_model.filePath(next_index)
2152
- if os.path.isfile(path) and self.file_manager.is_image_file(path):
2153
- self._load_selected_image(next_index)
2154
- return
2278
+ # Use new file manager navigation
2279
+ self.right_panel.navigate_next_image()
2280
+ return
2155
2281
 
2156
2282
  def _load_previous_image(self):
2157
2283
  """Load previous image in the file list."""
@@ -2160,17 +2286,9 @@ class MainWindow(QMainWindow):
2160
2286
  self._load_previous_multi_batch()
2161
2287
  return
2162
2288
 
2163
- if not self.current_file_index.isValid():
2164
- return
2165
- parent = self.current_file_index.parent()
2166
- row = self.current_file_index.row()
2167
- # Find previous valid image file
2168
- for prev_row in range(row - 1, -1, -1):
2169
- prev_index = self.file_model.index(prev_row, 0, parent)
2170
- path = self.file_model.filePath(prev_index)
2171
- if os.path.isfile(path) and self.file_manager.is_image_file(path):
2172
- self._load_selected_image(prev_index)
2173
- return
2289
+ # Use new file manager navigation
2290
+ self.right_panel.navigate_previous_image()
2291
+ return
2174
2292
 
2175
2293
  # Segment management methods
2176
2294
  def _assign_selected_to_class(self):
@@ -2823,20 +2941,34 @@ class MainWindow(QMainWindow):
2823
2941
 
2824
2942
  def _handle_enter_press(self):
2825
2943
  """Handle enter key press."""
2944
+ logger.debug(f"Enter pressed - mode: {self.mode}, view_mode: {self.view_mode}")
2945
+
2826
2946
  if self.mode == "polygon":
2827
- if self.view_mode == "single" and self.polygon_points:
2828
- self._finalize_polygon()
2829
- elif self.view_mode == "multi" and hasattr(
2830
- self, "multi_view_polygon_points"
2831
- ):
2947
+ logger.debug(
2948
+ f"Polygon mode - polygon_points: {len(self.polygon_points) if hasattr(self, 'polygon_points') and self.polygon_points else 0}"
2949
+ )
2950
+ if self.view_mode == "single":
2951
+ # If there are pending polygon points, finalize them first
2952
+ if self.polygon_points:
2953
+ logger.debug("Finalizing pending polygon")
2954
+ self._finalize_polygon()
2955
+ # Always save after handling pending work to show checkmarks and notifications
2956
+ logger.debug("Saving polygon segments to NPZ")
2957
+ self._save_output_to_npz()
2958
+ elif self.view_mode == "multi":
2832
2959
  # Complete polygons for all viewers that have points
2833
- for i, points in enumerate(self.multi_view_polygon_points):
2834
- if points and len(points) >= 3:
2835
- # Use the multi-view mode handler for proper pairing logic
2836
- if hasattr(self, "multi_view_mode_handler"):
2837
- self.multi_view_mode_handler._finalize_multi_view_polygon(i)
2838
- else:
2839
- self._finalize_multi_view_polygon(i)
2960
+ if hasattr(self, "multi_view_polygon_points"):
2961
+ for i, points in enumerate(self.multi_view_polygon_points):
2962
+ if points and len(points) >= 3:
2963
+ # Use the multi-view mode handler for proper pairing logic
2964
+ if hasattr(self, "multi_view_mode_handler"):
2965
+ self.multi_view_mode_handler._finalize_multi_view_polygon(
2966
+ i
2967
+ )
2968
+ else:
2969
+ self._finalize_multi_view_polygon(i)
2970
+ # Always save after handling pending work to show checkmarks and notifications
2971
+ self._save_output_to_npz()
2840
2972
  else:
2841
2973
  # First accept any AI segments (same as spacebar), then save
2842
2974
  self._accept_ai_segment()
@@ -3198,7 +3330,13 @@ class MainWindow(QMainWindow):
3198
3330
  try:
3199
3331
  os.remove(file_path)
3200
3332
  deleted_files.append(os.path.basename(file_path))
3201
- self.file_model.update_cache_for_path(file_path)
3333
+ # Update FastFileManager after file deletion
3334
+ if hasattr(self, "right_panel") and hasattr(
3335
+ self.right_panel, "file_manager"
3336
+ ):
3337
+ self.right_panel.file_manager.updateNpzStatus(
3338
+ Path(image_path)
3339
+ )
3202
3340
  except Exception as e:
3203
3341
  self._show_error_notification(
3204
3342
  f"Error deleting {file_path}: {e}"
@@ -3213,20 +3351,15 @@ class MainWindow(QMainWindow):
3213
3351
  f"Error saving {os.path.basename(image_path)}: {str(e)}"
3214
3352
  )
3215
3353
 
3216
- # Update file list with green flash and tick marks (like single view mode)
3217
- if hasattr(self, "_saved_file_paths") and self._saved_file_paths:
3218
- for path in self._saved_file_paths:
3219
- if path:
3220
- self.file_model.update_cache_for_path(path)
3221
- self.file_model.set_highlighted_path(path)
3222
- QTimer.singleShot(
3223
- 1500,
3224
- lambda p=path: (
3225
- self.file_model.set_highlighted_path(None)
3226
- if self.file_model.highlighted_path == p
3227
- else None
3228
- ),
3229
- )
3354
+ # Update FastFileManager to show NPZ checkmarks for multi-view
3355
+ if hasattr(self, "multi_view_images") and self.multi_view_images:
3356
+ for image_path in self.multi_view_images:
3357
+ if (
3358
+ image_path
3359
+ and hasattr(self, "right_panel")
3360
+ and hasattr(self.right_panel, "file_manager")
3361
+ ):
3362
+ self.right_panel.file_manager.updateNpzStatus(Path(image_path))
3230
3363
  # Clear the tracking list for next save
3231
3364
  self._saved_file_paths = []
3232
3365
 
@@ -3258,7 +3391,13 @@ class MainWindow(QMainWindow):
3258
3391
  try:
3259
3392
  os.remove(file_path)
3260
3393
  deleted_files.append(file_path)
3261
- self.file_model.update_cache_for_path(file_path)
3394
+ # Update FastFileManager after file deletion
3395
+ if hasattr(self, "right_panel") and hasattr(
3396
+ self.right_panel, "file_manager"
3397
+ ):
3398
+ self.right_panel.file_manager.updateNpzStatus(
3399
+ Path(self.current_image_path)
3400
+ )
3262
3401
  except Exception as e:
3263
3402
  self._show_error_notification(
3264
3403
  f"Error deleting {file_path}: {e}"
@@ -3276,24 +3415,35 @@ class MainWindow(QMainWindow):
3276
3415
  try:
3277
3416
  settings = self.control_panel.get_settings()
3278
3417
  npz_path = None
3279
- txt_path = None
3418
+
3419
+ # Debug logging
3420
+ logger.debug(f"Starting save process for: {self.current_image_path}")
3421
+ logger.debug(f"Number of segments: {len(self.segment_manager.segments)}")
3422
+
3280
3423
  if settings.get("save_npz", True):
3281
3424
  h, w = (
3282
3425
  self.viewer._pixmap_item.pixmap().height(),
3283
3426
  self.viewer._pixmap_item.pixmap().width(),
3284
3427
  )
3285
3428
  class_order = self.segment_manager.get_unique_class_ids()
3429
+ logger.debug(f"Class order for saving: {class_order}")
3430
+
3286
3431
  if class_order:
3432
+ logger.debug(
3433
+ f"Attempting to save NPZ to: {os.path.splitext(self.current_image_path)[0]}.npz"
3434
+ )
3287
3435
  npz_path = self.file_manager.save_npz(
3288
3436
  self.current_image_path,
3289
3437
  (h, w),
3290
3438
  class_order,
3291
3439
  self.current_crop_coords,
3292
3440
  )
3441
+ logger.debug(f"NPZ save completed: {npz_path}")
3293
3442
  self._show_success_notification(
3294
3443
  f"Saved: {os.path.basename(npz_path)}"
3295
3444
  )
3296
3445
  else:
3446
+ logger.warning("No classes defined for saving")
3297
3447
  self._show_warning_notification("No classes defined for saving.")
3298
3448
  if settings.get("save_txt", True):
3299
3449
  h, w = (
@@ -3308,27 +3458,25 @@ class MainWindow(QMainWindow):
3308
3458
  else:
3309
3459
  class_labels = [str(cid) for cid in class_order]
3310
3460
  if class_order:
3311
- txt_path = self.file_manager.save_yolo_txt(
3461
+ self.file_manager.save_yolo_txt(
3312
3462
  self.current_image_path,
3313
3463
  (h, w),
3314
3464
  class_order,
3315
3465
  class_labels,
3316
3466
  self.current_crop_coords,
3317
3467
  )
3318
- # Efficiently update file list tickboxes and highlight
3319
- for path in [npz_path, txt_path]:
3320
- if path:
3321
- self.file_model.update_cache_for_path(path)
3322
- self.file_model.set_highlighted_path(path)
3323
- QTimer.singleShot(
3324
- 1500,
3325
- lambda p=path: (
3326
- self.file_model.set_highlighted_path(None)
3327
- if self.file_model.highlighted_path == p
3328
- else None
3329
- ),
3330
- )
3468
+ # Update FastFileManager to show NPZ checkmarks
3469
+ if (
3470
+ npz_path
3471
+ and hasattr(self, "right_panel")
3472
+ and hasattr(self.right_panel, "file_manager")
3473
+ ):
3474
+ # Update the NPZ status in the FastFileManager
3475
+ self.right_panel.file_manager.updateNpzStatus(
3476
+ Path(self.current_image_path)
3477
+ )
3331
3478
  except Exception as e:
3479
+ logger.error(f"Error saving file: {str(e)}", exc_info=True)
3332
3480
  self._show_error_notification(f"Error saving: {str(e)}")
3333
3481
 
3334
3482
  def _handle_merge_press(self):
@@ -1,5 +1,7 @@
1
1
  """Right panel with file explorer and segment management."""
2
2
 
3
+ from pathlib import Path
4
+
3
5
  from PyQt6.QtCore import Qt, pyqtSignal
4
6
  from PyQt6.QtWidgets import (
5
7
  QComboBox,
@@ -14,6 +16,7 @@ from PyQt6.QtWidgets import (
14
16
  QWidget,
15
17
  )
16
18
 
19
+ from ..utils.fast_file_manager import FastFileManager
17
20
  from .reorderable_class_table import ReorderableClassTable
18
21
 
19
22
 
@@ -23,6 +26,7 @@ class RightPanel(QWidget):
23
26
  # Signals
24
27
  open_folder_requested = pyqtSignal()
25
28
  image_selected = pyqtSignal("QModelIndex")
29
+ image_path_selected = pyqtSignal(Path) # New signal for path-based selection
26
30
  merge_selection_requested = pyqtSignal()
27
31
  delete_selection_requested = pyqtSignal()
28
32
  segments_selection_changed = pyqtSignal()
@@ -109,8 +113,13 @@ class RightPanel(QWidget):
109
113
  self.btn_open_folder.setToolTip("Open a directory of images")
110
114
  layout.addWidget(self.btn_open_folder)
111
115
 
112
- self.file_tree = QTreeView()
113
- layout.addWidget(self.file_tree)
116
+ # Use new FastFileManager instead of QTreeView
117
+ self.file_manager = FastFileManager()
118
+ layout.addWidget(self.file_manager)
119
+
120
+ # Keep file_tree reference for compatibility
121
+ self.file_tree = QTreeView() # Hidden, for backward compatibility
122
+ self.file_tree.hide()
114
123
 
115
124
  splitter.addWidget(file_explorer_widget)
116
125
 
@@ -195,6 +204,8 @@ class RightPanel(QWidget):
195
204
  """Connect internal signals."""
196
205
  self.btn_open_folder.clicked.connect(self.open_folder_requested)
197
206
  self.file_tree.doubleClicked.connect(self.image_selected)
207
+ # Connect new file manager signal
208
+ self.file_manager.fileSelected.connect(self.image_path_selected)
198
209
  self.btn_merge_selection.clicked.connect(self.merge_selection_requested)
199
210
  self.btn_delete_selection.clicked.connect(self.delete_selection_requested)
200
211
  self.segment_table.itemSelectionChanged.connect(self.segments_selection_changed)
@@ -276,12 +287,28 @@ class RightPanel(QWidget):
276
287
 
277
288
  def setup_file_model(self, file_model):
278
289
  """Setup the file model for the tree view."""
290
+ # Keep for backward compatibility
279
291
  self.file_tree.setModel(file_model)
280
292
  self.file_tree.setColumnWidth(0, 200)
281
293
 
282
294
  def set_folder(self, folder_path, file_model):
283
295
  """Set the folder for file browsing."""
296
+ # Keep old tree view for compatibility
284
297
  self.file_tree.setRootIndex(file_model.setRootPath(folder_path))
298
+ # Use new file manager
299
+ self.file_manager.setDirectory(Path(folder_path))
300
+
301
+ def navigate_next_image(self):
302
+ """Navigate to next image in the file manager."""
303
+ self.file_manager.navigateNext()
304
+
305
+ def navigate_previous_image(self):
306
+ """Navigate to previous image in the file manager."""
307
+ self.file_manager.navigatePrevious()
308
+
309
+ def select_file(self, file_path: Path):
310
+ """Select a specific file in the file manager."""
311
+ self.file_manager.selectFile(file_path)
285
312
 
286
313
  def get_selected_segment_indices(self):
287
314
  """Get indices of selected segments."""
@@ -0,0 +1,676 @@
1
+ """
2
+ Fast file manager with lazy loading, sorting, and efficient navigation
3
+ """
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ from PyQt6.QtCore import (
11
+ QAbstractTableModel,
12
+ QModelIndex,
13
+ QSortFilterProxyModel,
14
+ Qt,
15
+ QThread,
16
+ pyqtSignal,
17
+ )
18
+ from PyQt6.QtGui import QPixmap
19
+ from PyQt6.QtWidgets import (
20
+ QComboBox,
21
+ QHBoxLayout,
22
+ QHeaderView,
23
+ QLabel,
24
+ QLineEdit,
25
+ QPushButton,
26
+ QTableView,
27
+ QVBoxLayout,
28
+ QWidget,
29
+ )
30
+
31
+ # Image extensions supported
32
+ IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".tiff", ".tif"}
33
+
34
+
35
+ @dataclass
36
+ class FileInfo:
37
+ """Information about a file"""
38
+
39
+ path: Path
40
+ name: str
41
+ size: int = 0 # Lazy load for speed
42
+ modified: float = 0.0 # Lazy load for speed
43
+ has_npz: bool = False
44
+ has_txt: bool = False
45
+ thumbnail: QPixmap | None = None
46
+
47
+
48
+ class FileScanner(QThread):
49
+ """Background thread for scanning files"""
50
+
51
+ filesFound = pyqtSignal(list) # Emits batches of FileInfo
52
+ scanComplete = pyqtSignal(int) # Total file count
53
+ progress = pyqtSignal(int, int) # Current, total
54
+
55
+ def __init__(self, directory: Path):
56
+ super().__init__()
57
+ self.directory = directory
58
+ self._stop_flag = False
59
+
60
+ def run(self):
61
+ """Scan directory in background - OPTIMIZED FOR SPEED"""
62
+ batch_size = 1000 # Larger batches = fewer UI updates = faster
63
+ batch = []
64
+ total_files = 0
65
+
66
+ try:
67
+ # Use os.scandir() - MUCH faster than Path.iterdir()
68
+ # Single pass to collect all file info
69
+ npz_stems = set()
70
+ txt_stems = set()
71
+ image_entries = []
72
+
73
+ with os.scandir(self.directory) as entries:
74
+ for entry in entries:
75
+ if self._stop_flag:
76
+ break
77
+
78
+ # Check file extension
79
+ name = entry.name
80
+ ext = os.path.splitext(name)[1].lower()
81
+
82
+ if ext == ".npz":
83
+ npz_stems.add(os.path.splitext(name)[0])
84
+ elif ext == ".txt":
85
+ txt_stems.add(os.path.splitext(name)[0])
86
+ elif ext in IMAGE_EXTENSIONS:
87
+ image_entries.append((entry.path, name))
88
+
89
+ # Process images in batches
90
+ total_count = len(image_entries)
91
+
92
+ for i, (path, name) in enumerate(image_entries):
93
+ if self._stop_flag:
94
+ break
95
+
96
+ stem = os.path.splitext(name)[0]
97
+
98
+ # Create FileInfo - NO STAT CALLS for speed!
99
+ file_info = FileInfo(
100
+ path=Path(path),
101
+ name=name,
102
+ has_npz=stem in npz_stems,
103
+ has_txt=stem in txt_stems,
104
+ )
105
+
106
+ batch.append(file_info)
107
+ total_files += 1
108
+
109
+ if len(batch) >= batch_size:
110
+ self.filesFound.emit(batch)
111
+ batch = []
112
+
113
+ # Progress updates less frequently
114
+ if i % 1000 == 0 and i > 0:
115
+ self.progress.emit(i, total_count)
116
+
117
+ # Emit remaining files
118
+ if batch:
119
+ self.filesFound.emit(batch)
120
+
121
+ self.scanComplete.emit(total_files)
122
+
123
+ except Exception as e:
124
+ print(f"Error scanning directory: {e}")
125
+
126
+ def stop(self):
127
+ """Stop the scanning thread"""
128
+ self._stop_flag = True
129
+
130
+
131
+ class FastFileModel(QAbstractTableModel):
132
+ """High-performance file model with background loading"""
133
+
134
+ fileSelected = pyqtSignal(Path)
135
+
136
+ def __init__(self):
137
+ super().__init__()
138
+ self._files: list[FileInfo] = []
139
+ self._scanner: FileScanner | None = None
140
+
141
+ def rowCount(self, parent=QModelIndex()):
142
+ return len(self._files)
143
+
144
+ def columnCount(self, parent=QModelIndex()):
145
+ return 5 # Name, Size, Modified, NPZ, TXT
146
+
147
+ def data(self, index: QModelIndex, role=Qt.ItemDataRole.DisplayRole):
148
+ if not index.isValid():
149
+ return None
150
+
151
+ file_info = self._files[index.row()]
152
+ col = index.column()
153
+
154
+ if role == Qt.ItemDataRole.DisplayRole:
155
+ if col == 0: # Name
156
+ return file_info.name
157
+ elif col == 1: # Size
158
+ # Lazy load size only when displayed
159
+ if file_info.size == 0:
160
+ try:
161
+ file_info.size = file_info.path.stat().st_size
162
+ except OSError:
163
+ file_info.size = -1 # Mark as error
164
+ return self._format_size(file_info.size) if file_info.size >= 0 else "-"
165
+ elif col == 2: # Modified
166
+ # Lazy load modified time only when displayed
167
+ if file_info.modified == 0.0:
168
+ try:
169
+ file_info.modified = file_info.path.stat().st_mtime
170
+ except OSError:
171
+ file_info.modified = -1 # Mark as error
172
+ return (
173
+ datetime.fromtimestamp(file_info.modified).strftime(
174
+ "%Y-%m-%d %H:%M"
175
+ )
176
+ if file_info.modified > 0
177
+ else "-"
178
+ )
179
+ elif col == 3: # NPZ
180
+ return "✓" if file_info.has_npz else ""
181
+ elif col == 4: # TXT
182
+ return "✓" if file_info.has_txt else ""
183
+ elif role == Qt.ItemDataRole.UserRole:
184
+ # Return the FileInfo object for custom access
185
+ return file_info
186
+ elif role == Qt.ItemDataRole.TextAlignmentRole and col in [
187
+ 3,
188
+ 4,
189
+ ]: # Center checkmarks
190
+ return Qt.AlignmentFlag.AlignCenter
191
+
192
+ return None
193
+
194
+ def headerData(self, section, orientation, role):
195
+ if (
196
+ orientation == Qt.Orientation.Horizontal
197
+ and role == Qt.ItemDataRole.DisplayRole
198
+ ):
199
+ headers = ["Name", "Size", "Modified", "NPZ", "TXT"]
200
+ return headers[section]
201
+ return None
202
+
203
+ def _format_size(self, size: int) -> str:
204
+ """Format file size in human readable format"""
205
+ for unit in ["B", "KB", "MB", "GB"]:
206
+ if size < 1024:
207
+ return f"{size:.1f} {unit}"
208
+ size /= 1024
209
+ return f"{size:.1f} TB"
210
+
211
+ def setDirectory(self, directory: Path):
212
+ """Set directory to scan"""
213
+ # Stop previous scanner if running
214
+ if self._scanner and self._scanner.isRunning():
215
+ self._scanner.stop()
216
+ self._scanner.wait()
217
+
218
+ # Clear current files
219
+ self.beginResetModel()
220
+ self._files.clear()
221
+ self.endResetModel()
222
+
223
+ # Start new scan
224
+ self._scanner = FileScanner(directory)
225
+ self._scanner.filesFound.connect(self._on_files_found)
226
+ self._scanner.scanComplete.connect(self._on_scan_complete)
227
+ self._scanner.start()
228
+
229
+ def _on_files_found(self, files: list[FileInfo]):
230
+ """Handle batch of files found"""
231
+ start_row = len(self._files)
232
+ end_row = start_row + len(files) - 1
233
+ self.beginInsertRows(QModelIndex(), start_row, end_row)
234
+ self._files.extend(files)
235
+ self.endInsertRows()
236
+
237
+ def _on_scan_complete(self, total: int):
238
+ """Handle scan completion"""
239
+ print(f"Scan complete: {total} files found")
240
+
241
+ def getFileInfo(self, index: int) -> FileInfo | None:
242
+ """Get file info at index"""
243
+ if 0 <= index < len(self._files):
244
+ return self._files[index]
245
+ return None
246
+
247
+ def updateNpzStatus(self, image_path: Path):
248
+ """Update NPZ status for a specific image file"""
249
+ image_path_str = str(image_path)
250
+ npz_path = image_path.with_suffix(".npz")
251
+ has_npz = npz_path.exists()
252
+
253
+ # Find and update the file info
254
+ for i, file_info in enumerate(self._files):
255
+ if str(file_info.path) == image_path_str:
256
+ old_has_npz = file_info.has_npz
257
+ file_info.has_npz = has_npz
258
+
259
+ # Only emit dataChanged if status actually changed
260
+ if old_has_npz != has_npz:
261
+ index = self.index(i, 3) # NPZ column
262
+ self.dataChanged.emit(index, index)
263
+ break
264
+
265
+ def getFileIndex(self, path: Path) -> int:
266
+ """Get index of file by path"""
267
+ for i, file_info in enumerate(self._files):
268
+ if file_info.path == path:
269
+ return i
270
+ return -1
271
+
272
+
273
+ class FileSortProxyModel(QSortFilterProxyModel):
274
+ """Custom proxy model for proper sorting of file data."""
275
+
276
+ def lessThan(self, left: QModelIndex, right: QModelIndex) -> bool:
277
+ """Custom sorting comparison."""
278
+ # Get the file info objects
279
+ left_info = self.sourceModel().getFileInfo(left.row())
280
+ right_info = self.sourceModel().getFileInfo(right.row())
281
+
282
+ if not left_info or not right_info:
283
+ return False
284
+
285
+ col = left.column()
286
+
287
+ # Sort based on column
288
+ if col == 0: # Name
289
+ return left_info.name.lower() < right_info.name.lower()
290
+ elif col == 1: # Size
291
+ # Lazy load size if needed for sorting
292
+ if left_info.size == 0:
293
+ try:
294
+ left_info.size = left_info.path.stat().st_size
295
+ except OSError:
296
+ left_info.size = -1
297
+ if right_info.size == 0:
298
+ try:
299
+ right_info.size = right_info.path.stat().st_size
300
+ except OSError:
301
+ right_info.size = -1
302
+ return left_info.size < right_info.size
303
+ elif col == 2: # Modified
304
+ # Lazy load modified time if needed for sorting
305
+ if left_info.modified == 0.0:
306
+ try:
307
+ left_info.modified = left_info.path.stat().st_mtime
308
+ except OSError:
309
+ left_info.modified = -1
310
+ if right_info.modified == 0.0:
311
+ try:
312
+ right_info.modified = right_info.path.stat().st_mtime
313
+ except OSError:
314
+ right_info.modified = -1
315
+ return left_info.modified < right_info.modified
316
+ elif col == 3: # NPZ
317
+ return left_info.has_npz < right_info.has_npz
318
+ elif col == 4: # TXT
319
+ return left_info.has_txt < right_info.has_txt
320
+
321
+ return False
322
+
323
+
324
+ class FastFileManager(QWidget):
325
+ """Main file manager widget with improved performance"""
326
+
327
+ fileSelected = pyqtSignal(Path)
328
+
329
+ def __init__(self):
330
+ super().__init__()
331
+ self._current_directory = None
332
+ self._init_ui()
333
+
334
+ def _init_ui(self):
335
+ """Initialize the UI"""
336
+ layout = QVBoxLayout(self)
337
+ layout.setContentsMargins(0, 0, 0, 0)
338
+
339
+ # Header with controls
340
+ header = self._create_header()
341
+ layout.addWidget(header)
342
+
343
+ # File table view
344
+ self._table_view = QTableView()
345
+ self._table_view.setAlternatingRowColors(True)
346
+ self._table_view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
347
+ self._table_view.setSelectionMode(QTableView.SelectionMode.SingleSelection)
348
+ self._table_view.setSortingEnabled(False) # We'll handle sorting manually
349
+ self._table_view.setEditTriggers(QTableView.EditTrigger.NoEditTriggers)
350
+
351
+ # Set up model and proxy
352
+ self._model = FastFileModel()
353
+ self._model.fileSelected.connect(self.fileSelected)
354
+
355
+ # Set up custom sorting proxy
356
+ self._proxy_model = FileSortProxyModel()
357
+ self._proxy_model.setSourceModel(self._model)
358
+ self._table_view.setModel(self._proxy_model)
359
+
360
+ # Enable sorting
361
+ self._table_view.setSortingEnabled(True)
362
+ self._table_view.sortByColumn(0, Qt.SortOrder.AscendingOrder)
363
+
364
+ # Configure headers
365
+ header = self._table_view.horizontalHeader()
366
+ header.setSectionResizeMode(
367
+ 0, QHeaderView.ResizeMode.Stretch
368
+ ) # Name column stretches
369
+ header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Size
370
+ header.setSectionResizeMode(
371
+ 2, QHeaderView.ResizeMode.ResizeToContents
372
+ ) # Modified
373
+ header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) # NPZ
374
+ header.setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed) # TXT
375
+ header.resizeSection(3, 50)
376
+ header.resizeSection(4, 50)
377
+
378
+ # Style the table to match the existing UI
379
+ self._table_view.setStyleSheet("""
380
+ QTableView {
381
+ background-color: transparent;
382
+ alternate-background-color: rgba(255, 255, 255, 0.03);
383
+ gridline-color: rgba(255, 255, 255, 0.1);
384
+ color: #E0E0E0;
385
+ }
386
+ QTableView::item {
387
+ padding: 2px;
388
+ color: #E0E0E0;
389
+ }
390
+ QTableView::item:selected {
391
+ background-color: rgba(100, 100, 200, 0.5);
392
+ }
393
+ QHeaderView::section {
394
+ background-color: rgba(60, 60, 60, 0.5);
395
+ color: #E0E0E0;
396
+ padding: 4px;
397
+ border: 1px solid rgba(80, 80, 80, 0.4);
398
+ font-weight: bold;
399
+ }
400
+ """)
401
+
402
+ # Connect selection
403
+ self._table_view.clicked.connect(self._on_item_clicked)
404
+ self._table_view.doubleClicked.connect(self._on_item_double_clicked)
405
+
406
+ layout.addWidget(self._table_view)
407
+
408
+ # Status bar
409
+ self._status_label = QLabel("No folder selected")
410
+ self._status_label.setStyleSheet(
411
+ "padding: 5px; background: rgba(60, 60, 60, 0.3); color: #E0E0E0;"
412
+ )
413
+ layout.addWidget(self._status_label)
414
+
415
+ def _create_header(self) -> QWidget:
416
+ """Create header with controls"""
417
+ header = QWidget()
418
+ header.setStyleSheet("background: rgba(60, 60, 60, 0.3); padding: 5px;")
419
+ layout = QHBoxLayout(header)
420
+ layout.setContentsMargins(5, 5, 5, 5)
421
+
422
+ # Search box
423
+ self._search_box = QLineEdit()
424
+ self._search_box.setPlaceholderText("Search files...")
425
+ self._search_box.textChanged.connect(self._on_search_changed)
426
+ self._search_box.setStyleSheet("""
427
+ QLineEdit {
428
+ background-color: rgba(50, 50, 50, 0.5);
429
+ border: 1px solid rgba(80, 80, 80, 0.4);
430
+ color: #E0E0E0;
431
+ padding: 4px;
432
+ border-radius: 3px;
433
+ }
434
+ """)
435
+ layout.addWidget(self._search_box)
436
+
437
+ # Sort dropdown
438
+ sort_label = QLabel("Sort:")
439
+ sort_label.setStyleSheet("color: #E0E0E0;")
440
+ layout.addWidget(sort_label)
441
+
442
+ self._sort_combo = QComboBox()
443
+ self._sort_combo.addItems(
444
+ [
445
+ "Name (A-Z)",
446
+ "Name (Z-A)",
447
+ "Date (Oldest)",
448
+ "Date (Newest)",
449
+ "Size (Smallest)",
450
+ "Size (Largest)",
451
+ ]
452
+ )
453
+ self._sort_combo.currentIndexChanged.connect(self._on_sort_changed)
454
+ self._sort_combo.setStyleSheet("""
455
+ QComboBox {
456
+ background-color: rgba(50, 50, 50, 0.5);
457
+ border: 1px solid rgba(80, 80, 80, 0.4);
458
+ color: #E0E0E0;
459
+ padding: 4px;
460
+ border-radius: 3px;
461
+ }
462
+ QComboBox::drop-down {
463
+ border: none;
464
+ }
465
+ QComboBox::down-arrow {
466
+ width: 12px;
467
+ height: 12px;
468
+ }
469
+ QComboBox QAbstractItemView {
470
+ background-color: rgba(50, 50, 50, 0.9);
471
+ border: 1px solid rgba(80, 80, 80, 0.4);
472
+ color: #E0E0E0;
473
+ selection-background-color: rgba(100, 100, 200, 0.5);
474
+ }
475
+ """)
476
+ layout.addWidget(self._sort_combo)
477
+
478
+ # Refresh button
479
+ refresh_btn = QPushButton("Refresh")
480
+ refresh_btn.clicked.connect(self._refresh)
481
+ refresh_btn.setStyleSheet("""
482
+ QPushButton {
483
+ background-color: rgba(70, 70, 70, 0.6);
484
+ border: 1px solid rgba(80, 80, 80, 0.4);
485
+ color: #E0E0E0;
486
+ padding: 4px 8px;
487
+ border-radius: 3px;
488
+ }
489
+ QPushButton:hover {
490
+ background-color: rgba(90, 90, 90, 0.8);
491
+ }
492
+ QPushButton:pressed {
493
+ background-color: rgba(50, 50, 50, 0.8);
494
+ }
495
+ """)
496
+ layout.addWidget(refresh_btn)
497
+
498
+ layout.addStretch()
499
+
500
+ return header
501
+
502
+ def setDirectory(self, directory: Path):
503
+ """Set the directory to display"""
504
+ self._current_directory = directory
505
+ self._model.setDirectory(directory)
506
+ self._update_status(f"Loading: {directory.name}")
507
+
508
+ # Connect to scan complete signal
509
+ if self._model._scanner:
510
+ self._model._scanner.scanComplete.connect(
511
+ lambda count: self._update_status(f"{count} files in {directory.name}")
512
+ )
513
+
514
+ def _on_search_changed(self, text: str):
515
+ """Handle search text change"""
516
+ self._proxy_model.setFilterFixedString(text)
517
+ self._proxy_model.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
518
+ self._proxy_model.setFilterKeyColumn(0) # Filter on name column
519
+
520
+ def _on_sort_changed(self, index: int):
521
+ """Handle sort order change"""
522
+ # Map combo index to column and order
523
+ column_map = {
524
+ 0: 0,
525
+ 1: 0,
526
+ 2: 2,
527
+ 3: 2,
528
+ 4: 1,
529
+ 5: 1,
530
+ } # Name, Name, Date, Date, Size, Size
531
+ order_map = {
532
+ 0: Qt.SortOrder.AscendingOrder,
533
+ 1: Qt.SortOrder.DescendingOrder,
534
+ 2: Qt.SortOrder.AscendingOrder,
535
+ 3: Qt.SortOrder.DescendingOrder,
536
+ 4: Qt.SortOrder.AscendingOrder,
537
+ 5: Qt.SortOrder.DescendingOrder,
538
+ }
539
+
540
+ column = column_map.get(index, 0)
541
+ order = order_map.get(index, Qt.SortOrder.AscendingOrder)
542
+
543
+ self._table_view.sortByColumn(column, order)
544
+
545
+ def _refresh(self):
546
+ """Refresh current directory"""
547
+ if self._current_directory:
548
+ self.setDirectory(self._current_directory)
549
+
550
+ def updateNpzStatus(self, image_path: Path):
551
+ """Update NPZ status for a specific image file"""
552
+ self._model.updateNpzStatus(image_path)
553
+
554
+ def refreshFile(self, image_path: Path):
555
+ """Refresh status for a specific file (alias for updateNpzStatus)"""
556
+ self.updateNpzStatus(image_path)
557
+
558
+ def _on_item_clicked(self, index: QModelIndex):
559
+ """Handle item click"""
560
+ # Map proxy index to source index
561
+ source_index = self._proxy_model.mapToSource(index)
562
+ file_info = self._model.getFileInfo(source_index.row())
563
+ if file_info:
564
+ self.fileSelected.emit(file_info.path)
565
+
566
+ def _on_item_double_clicked(self, index: QModelIndex):
567
+ """Handle item double click"""
568
+ # Map proxy index to source index
569
+ source_index = self._proxy_model.mapToSource(index)
570
+ file_info = self._model.getFileInfo(source_index.row())
571
+ if file_info:
572
+ self.fileSelected.emit(file_info.path)
573
+
574
+ def _update_status(self, text: str):
575
+ """Update status label"""
576
+ self._status_label.setText(text)
577
+
578
+ def selectFile(self, path: Path):
579
+ """Select a specific file in the view"""
580
+ index = self._model.getFileIndex(path)
581
+ if index >= 0:
582
+ source_index = self._model.index(index, 0)
583
+ proxy_index = self._proxy_model.mapFromSource(source_index)
584
+ self._table_view.setCurrentIndex(proxy_index)
585
+ self._table_view.scrollTo(proxy_index)
586
+ # Select the entire row
587
+ selection_model = self._table_view.selectionModel()
588
+ selection_model.select(
589
+ proxy_index,
590
+ selection_model.SelectionFlag.ClearAndSelect
591
+ | selection_model.SelectionFlag.Rows,
592
+ )
593
+
594
+ def getSelectedFile(self) -> Path | None:
595
+ """Get currently selected file"""
596
+ index = self._table_view.currentIndex()
597
+ if index.isValid():
598
+ source_index = self._proxy_model.mapToSource(index)
599
+ file_info = self._model.getFileInfo(source_index.row())
600
+ if file_info:
601
+ return file_info.path
602
+ return None
603
+
604
+ def navigateNext(self):
605
+ """Navigate to next file"""
606
+ current = self._table_view.currentIndex()
607
+
608
+ # If no current selection and we have files, select first
609
+ if not current.isValid() and self._proxy_model.rowCount() > 0:
610
+ first_index = self._proxy_model.index(0, 0)
611
+ self._table_view.setCurrentIndex(first_index)
612
+ selection_model = self._table_view.selectionModel()
613
+ selection_model.select(
614
+ first_index,
615
+ selection_model.SelectionFlag.ClearAndSelect
616
+ | selection_model.SelectionFlag.Rows,
617
+ )
618
+ # Emit file selection
619
+ source_index = self._proxy_model.mapToSource(first_index)
620
+ file_info = self._model.getFileInfo(source_index.row())
621
+ if file_info:
622
+ self.fileSelected.emit(file_info.path)
623
+ return
624
+
625
+ if current.isValid() and current.row() < self._proxy_model.rowCount() - 1:
626
+ next_index = self._proxy_model.index(current.row() + 1, 0)
627
+ self._table_view.setCurrentIndex(next_index)
628
+ # Select entire row
629
+ selection_model = self._table_view.selectionModel()
630
+ selection_model.select(
631
+ next_index,
632
+ selection_model.SelectionFlag.ClearAndSelect
633
+ | selection_model.SelectionFlag.Rows,
634
+ )
635
+ # Emit file selection
636
+ source_index = self._proxy_model.mapToSource(next_index)
637
+ file_info = self._model.getFileInfo(source_index.row())
638
+ if file_info:
639
+ self.fileSelected.emit(file_info.path)
640
+
641
+ def navigatePrevious(self):
642
+ """Navigate to previous file"""
643
+ current = self._table_view.currentIndex()
644
+
645
+ # If no current selection and we have files, select last
646
+ if not current.isValid() and self._proxy_model.rowCount() > 0:
647
+ last_index = self._proxy_model.index(self._proxy_model.rowCount() - 1, 0)
648
+ self._table_view.setCurrentIndex(last_index)
649
+ selection_model = self._table_view.selectionModel()
650
+ selection_model.select(
651
+ last_index,
652
+ selection_model.SelectionFlag.ClearAndSelect
653
+ | selection_model.SelectionFlag.Rows,
654
+ )
655
+ # Emit file selection
656
+ source_index = self._proxy_model.mapToSource(last_index)
657
+ file_info = self._model.getFileInfo(source_index.row())
658
+ if file_info:
659
+ self.fileSelected.emit(file_info.path)
660
+ return
661
+
662
+ if current.isValid() and current.row() > 0:
663
+ prev_index = self._proxy_model.index(current.row() - 1, 0)
664
+ self._table_view.setCurrentIndex(prev_index)
665
+ # Select entire row
666
+ selection_model = self._table_view.selectionModel()
667
+ selection_model.select(
668
+ prev_index,
669
+ selection_model.SelectionFlag.ClearAndSelect
670
+ | selection_model.SelectionFlag.Rows,
671
+ )
672
+ # Emit file selection
673
+ source_index = self._proxy_model.mapToSource(prev_index)
674
+ file_info = self._model.getFileInfo(source_index.row())
675
+ if file_info:
676
+ self.fileSelected.emit(file_info.path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lazylabel-gui
3
- Version: 1.3.1
3
+ Version: 1.3.2
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
@@ -6,7 +6,7 @@ lazylabel/config/hotkeys.py,sha256=PwxBUy7RvIUU3HCXT74NnexPbg2VFQrbR5FTGek31h8,8
6
6
  lazylabel/config/paths.py,sha256=ZVKbtaNOxmYO4l6JgsY-8DXaE_jaJfDg2RQJJn3-5nw,1275
7
7
  lazylabel/config/settings.py,sha256=myBqkgnYv_P1OomieXt8luCOSNjBiDVN-TybSqHkAgA,1990
8
8
  lazylabel/core/__init__.py,sha256=FmRjop_uIBSJwKMGhaZ-3Iwu34LkoxTuD-hnq5vbTSY,232
9
- lazylabel/core/file_manager.py,sha256=CmRLd3FtOa64mt9Wz-eYErDerFOm1gt3roGlibviqwo,6013
9
+ lazylabel/core/file_manager.py,sha256=qVPCJdo9KEu9ESujHaWPmlpfVLjyaVXmDe9TmM9c8k4,7282
10
10
  lazylabel/core/model_manager.py,sha256=NmgnIrw9sb_vSwRVkBr2Z_Mc2kFfdFukFgUhqs2e-L0,6829
11
11
  lazylabel/core/segment_manager.py,sha256=M6kHcYeiub3WqL01NElCvKOc2GNmf72LUM1W8XwSaxc,6465
12
12
  lazylabel/models/__init__.py,sha256=fIlk_0DuZfiClcm0XlZdimeHzunQwBmTMI4PcGsaymw,91
@@ -18,12 +18,11 @@ 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=r1Vk3aY346O3T1H8-4xmha0KpeWxANV27JthFQNX7ss,381615
21
+ lazylabel/ui/main_window.py,sha256=b30amNwHkSXDi2_WvXFYlmzd6TTlRnjNRgShGIoatiQ,387531
22
22
  lazylabel/ui/numeric_table_widget_item.py,sha256=dQUlIFu9syCxTGAHVIlmbgkI7aJ3f3wmDPBz1AGK9Bg,283
23
23
  lazylabel/ui/photo_viewer.py,sha256=D_t2DS0AYckzlXjotTVC5ZxCsMYC-0xjBjeVQCrhqws,5631
24
24
  lazylabel/ui/reorderable_class_table.py,sha256=sxHhQre5O_MXLDFgKnw43QnvXXoqn5xRKMGitgO7muI,2371
25
- lazylabel/ui/right_panel.py,sha256=-PeXcu7Lr-xhZniBMvWLDPiFb_RAHYAcILyw8fPJs6I,13139
26
- lazylabel/ui/test_hover.py,sha256=5H7J2jhMlhDYwbvGJXgfTupwO9VRylK30ESAG85Ju7I,1521
25
+ lazylabel/ui/right_panel.py,sha256=D69XgPXpLleflsIl8xCtoBZzFleLqx0SezdwfcEJhUg,14280
27
26
  lazylabel/ui/modes/__init__.py,sha256=ikg47aeexLQavSda_3tYn79xGJW38jKoUCLXRe2w8ok,219
28
27
  lazylabel/ui/modes/base_mode.py,sha256=0R3AkjN_WpYwetF3uuOvkTxb6Q1HB-Z1NQPvLh9eUTY,1315
29
28
  lazylabel/ui/modes/multi_view_mode.py,sha256=TKdqTHokIk267aFVvwXikAtAcuxpG6ezIOlQwoque9c,50606
@@ -39,11 +38,12 @@ lazylabel/ui/widgets/settings_widget.py,sha256=ShTaLJeXxwrSuTV4kmtV2JiWjfREil2D1
39
38
  lazylabel/ui/widgets/status_bar.py,sha256=wTbMQNEOBfmtNj8EVFZS_lxgaemu-CbRXeZzEQDaVz8,4014
40
39
  lazylabel/utils/__init__.py,sha256=V6IR5Gim-39HgM2NyTVT-n8gy3mjilCSFW9y0owN5nc,179
41
40
  lazylabel/utils/custom_file_system_model.py,sha256=-3EimlybvevH6bvqBE0qdFnLADVtayylmkntxPXK0Bk,4869
41
+ lazylabel/utils/fast_file_manager.py,sha256=lOc7Lu7lcOFME-5EO3K6PiEE_oW6I3LhxpeizwgkhsM,24628
42
42
  lazylabel/utils/logger.py,sha256=R7z6ifgA-NY-9ZbLlNH0i19zzwXndJ_gkG2J1zpVEhg,1306
43
43
  lazylabel/utils/utils.py,sha256=sYSCoXL27OaLgOZaUkCAhgmKZ7YfhR3Cc5F8nDIa3Ig,414
44
- lazylabel_gui-1.3.1.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
45
- lazylabel_gui-1.3.1.dist-info/METADATA,sha256=8Q4xKBHglQ2KIOuIzCM1W84pUQ4h_k-omx6uuzO_naw,6898
46
- lazylabel_gui-1.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
- lazylabel_gui-1.3.1.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
48
- lazylabel_gui-1.3.1.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
49
- lazylabel_gui-1.3.1.dist-info/RECORD,,
44
+ lazylabel_gui-1.3.2.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
45
+ lazylabel_gui-1.3.2.dist-info/METADATA,sha256=LflKLfur3j-0agOkaMDITRzavzagyFrKI_JzZJIBCk0,6898
46
+ lazylabel_gui-1.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
+ lazylabel_gui-1.3.2.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
48
+ lazylabel_gui-1.3.2.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
49
+ lazylabel_gui-1.3.2.dist-info/RECORD,,
@@ -1,48 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Test script to check hover functionality.
4
- Run this before starting LazyLabel to enable debug logging.
5
- """
6
-
7
- # First, enable debug logging
8
- import logging
9
- import os
10
- import sys
11
-
12
- # Add the src directory to path so we can import LazyLabel modules
13
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
14
-
15
- from lazylabel.utils.logger import logger
16
-
17
- # Set logger to DEBUG level
18
- logger.setLevel(logging.DEBUG)
19
-
20
- # Add console handler if not already present
21
- if not logger.handlers:
22
- console_handler = logging.StreamHandler()
23
- console_handler.setLevel(logging.DEBUG)
24
- formatter = logging.Formatter(
25
- "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
26
- )
27
- console_handler.setFormatter(formatter)
28
- logger.addHandler(console_handler)
29
-
30
- print("=" * 50)
31
- print("HOVER DEBUG MODE ENABLED")
32
- print("=" * 50)
33
- print("Logger level:", logger.level)
34
- print("Logger handlers:", len(logger.handlers))
35
- print()
36
- print("Now run LazyLabel and test hover functionality:")
37
- print("1. Load images in multi-view mode")
38
- print("2. Create some segments (AI or polygon)")
39
- print("3. Try hovering over segments")
40
- print("4. Watch the console for debug messages")
41
- print()
42
- print("Expected debug messages:")
43
- print("- HoverablePolygonItem.set_segment_info")
44
- print("- HoverablePixmapItem.set_segment_info")
45
- print("- HoverablePolygonItem.hoverEnterEvent")
46
- print("- HoverablePixmapItem.hoverEnterEvent")
47
- print("- _trigger_segment_hover called")
48
- print("=" * 50)