lazylabel-gui 1.3.2__tar.gz → 1.3.3__tar.gz

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.
Files changed (54) hide show
  1. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/PKG-INFO +1 -1
  2. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/pyproject.toml +1 -1
  3. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/main_window.py +207 -94
  4. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/utils/fast_file_manager.py +162 -7
  5. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel_gui.egg-info/PKG-INFO +1 -1
  6. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/LICENSE +0 -0
  7. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/README.md +0 -0
  8. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/setup.cfg +0 -0
  9. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/__init__.py +0 -0
  10. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/__main__.py +0 -0
  11. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/config/__init__.py +0 -0
  12. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/config/hotkeys.py +0 -0
  13. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/config/paths.py +0 -0
  14. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/config/settings.py +0 -0
  15. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/core/__init__.py +0 -0
  16. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/core/file_manager.py +0 -0
  17. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/core/model_manager.py +0 -0
  18. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/core/segment_manager.py +0 -0
  19. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/main.py +0 -0
  20. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/models/__init__.py +0 -0
  21. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/models/sam2_model.py +0 -0
  22. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/models/sam_model.py +0 -0
  23. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/__init__.py +0 -0
  24. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/control_panel.py +0 -0
  25. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/editable_vertex.py +0 -0
  26. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/hotkey_dialog.py +0 -0
  27. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/hoverable_pixelmap_item.py +0 -0
  28. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/hoverable_polygon_item.py +0 -0
  29. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/modes/__init__.py +0 -0
  30. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/modes/base_mode.py +0 -0
  31. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/modes/multi_view_mode.py +0 -0
  32. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/modes/single_view_mode.py +0 -0
  33. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/numeric_table_widget_item.py +0 -0
  34. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/photo_viewer.py +0 -0
  35. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/reorderable_class_table.py +0 -0
  36. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/right_panel.py +0 -0
  37. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/widgets/__init__.py +0 -0
  38. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/widgets/adjustments_widget.py +0 -0
  39. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/widgets/border_crop_widget.py +0 -0
  40. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/widgets/channel_threshold_widget.py +0 -0
  41. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/widgets/fft_threshold_widget.py +0 -0
  42. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/widgets/fragment_threshold_widget.py +0 -0
  43. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/widgets/model_selection_widget.py +0 -0
  44. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/widgets/settings_widget.py +0 -0
  45. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/ui/widgets/status_bar.py +0 -0
  46. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/utils/__init__.py +0 -0
  47. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/utils/custom_file_system_model.py +0 -0
  48. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/utils/logger.py +0 -0
  49. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel/utils/utils.py +0 -0
  50. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel_gui.egg-info/SOURCES.txt +0 -0
  51. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel_gui.egg-info/dependency_links.txt +0 -0
  52. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel_gui.egg-info/entry_points.txt +0 -0
  53. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel_gui.egg-info/requires.txt +0 -0
  54. {lazylabel_gui-1.3.2 → lazylabel_gui-1.3.3}/src/lazylabel_gui.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lazylabel-gui
3
- Version: 1.3.2
3
+ Version: 1.3.3
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lazylabel-gui"
7
- version = "1.3.2"
7
+ version = "1.3.3"
8
8
  authors = [
9
9
  { name="Deniz N. Cakan", email="deniz.n.cakan@gmail.com" },
10
10
  ]
@@ -1788,31 +1788,12 @@ class MainWindow(QMainWindow):
1788
1788
  ):
1789
1789
  self._save_multi_view_output()
1790
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
1791
+ # Get surrounding files in current sorted/filtered order
1792
+ file_manager = self.right_panel.file_manager
1793
+ surrounding_files = file_manager.getSurroundingFiles(Path(path), num_viewers)
1795
1794
 
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)
1795
+ # Convert to strings for loading
1796
+ images_to_load = [str(p) if p else None for p in surrounding_files]
1816
1797
 
1817
1798
  # Load the images
1818
1799
  self._load_multi_view_images(images_to_load)
@@ -3320,6 +3301,18 @@ class MainWindow(QMainWindow):
3320
3301
  # Restore original segments
3321
3302
  self.segment_manager.segments = original_segments
3322
3303
 
3304
+ # Save class aliases if enabled
3305
+ if settings.get("save_class_aliases", False) and viewer_segments:
3306
+ # Temporarily set segments for this viewer to save correct aliases
3307
+ original_segments = self.segment_manager.segments
3308
+ self.segment_manager.segments = viewer_segments
3309
+
3310
+ aliases_path = self.file_manager.save_class_aliases(image_path)
3311
+ saved_files.append(os.path.basename(aliases_path))
3312
+
3313
+ # Restore original segments
3314
+ self.segment_manager.segments = original_segments
3315
+
3323
3316
  # If no segments for this viewer, delete associated files
3324
3317
  if not viewer_segments:
3325
3318
  base, _ = os.path.splitext(image_path)
@@ -3334,7 +3327,7 @@ class MainWindow(QMainWindow):
3334
3327
  if hasattr(self, "right_panel") and hasattr(
3335
3328
  self.right_panel, "file_manager"
3336
3329
  ):
3337
- self.right_panel.file_manager.updateNpzStatus(
3330
+ self.right_panel.file_manager.updateFileStatus(
3338
3331
  Path(image_path)
3339
3332
  )
3340
3333
  except Exception as e:
@@ -3353,13 +3346,13 @@ class MainWindow(QMainWindow):
3353
3346
 
3354
3347
  # Update FastFileManager to show NPZ checkmarks for multi-view
3355
3348
  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))
3349
+ if hasattr(self, "right_panel") and hasattr(
3350
+ self.right_panel, "file_manager"
3351
+ ):
3352
+ # Batch update for better performance
3353
+ valid_paths = [Path(img) for img in self.multi_view_images if img]
3354
+ if valid_paths:
3355
+ self.right_panel.file_manager.batchUpdateFileStatus(valid_paths)
3363
3356
  # Clear the tracking list for next save
3364
3357
  self._saved_file_paths = []
3365
3358
 
@@ -3395,7 +3388,7 @@ class MainWindow(QMainWindow):
3395
3388
  if hasattr(self, "right_panel") and hasattr(
3396
3389
  self.right_panel, "file_manager"
3397
3390
  ):
3398
- self.right_panel.file_manager.updateNpzStatus(
3391
+ self.right_panel.file_manager.updateFileStatus(
3399
3392
  Path(self.current_image_path)
3400
3393
  )
3401
3394
  except Exception as e:
@@ -3445,6 +3438,8 @@ class MainWindow(QMainWindow):
3445
3438
  else:
3446
3439
  logger.warning("No classes defined for saving")
3447
3440
  self._show_warning_notification("No classes defined for saving.")
3441
+ # Save TXT file if enabled
3442
+ txt_path = None
3448
3443
  if settings.get("save_txt", True):
3449
3444
  h, w = (
3450
3445
  self.viewer._pixmap_item.pixmap().height(),
@@ -3458,21 +3453,38 @@ class MainWindow(QMainWindow):
3458
3453
  else:
3459
3454
  class_labels = [str(cid) for cid in class_order]
3460
3455
  if class_order:
3461
- self.file_manager.save_yolo_txt(
3456
+ txt_path = self.file_manager.save_yolo_txt(
3462
3457
  self.current_image_path,
3463
3458
  (h, w),
3464
3459
  class_order,
3465
3460
  class_labels,
3466
3461
  self.current_crop_coords,
3467
3462
  )
3468
- # Update FastFileManager to show NPZ checkmarks
3463
+ if txt_path:
3464
+ logger.debug(f"TXT save completed: {txt_path}")
3465
+ self._show_success_notification(
3466
+ f"Saved: {os.path.basename(txt_path)}"
3467
+ )
3468
+
3469
+ # Save class aliases if enabled
3470
+ if settings.get("save_class_aliases", False):
3471
+ aliases_path = self.file_manager.save_class_aliases(
3472
+ self.current_image_path
3473
+ )
3474
+ if aliases_path:
3475
+ logger.debug(f"Class aliases saved: {aliases_path}")
3476
+ self._show_success_notification(
3477
+ f"Saved: {os.path.basename(aliases_path)}"
3478
+ )
3479
+
3480
+ # Update FastFileManager to show NPZ/TXT checkmarks
3469
3481
  if (
3470
- npz_path
3482
+ (npz_path or txt_path)
3471
3483
  and hasattr(self, "right_panel")
3472
3484
  and hasattr(self.right_panel, "file_manager")
3473
3485
  ):
3474
- # Update the NPZ status in the FastFileManager
3475
- self.right_panel.file_manager.updateNpzStatus(
3486
+ # Update the file status in the FastFileManager
3487
+ self.right_panel.file_manager.updateFileStatus(
3476
3488
  Path(self.current_image_path)
3477
3489
  )
3478
3490
  except Exception as e:
@@ -7236,24 +7248,73 @@ class MainWindow(QMainWindow):
7236
7248
  self._load_multi_view_images(image_paths)
7237
7249
 
7238
7250
  def _load_next_multi_batch(self):
7239
- """Load the next batch of images using cached list."""
7240
- if not self.current_file_index.isValid():
7241
- return
7242
-
7243
- # Auto-save if enabled and we have current images (not the first load)
7244
- if self.multi_view_images[0] and self.control_panel.get_settings().get(
7245
- "auto_save", True
7251
+ """Load the next batch of images using fast file manager or cached list."""
7252
+ # Auto-save if enabled and we have current images
7253
+ if (
7254
+ hasattr(self, "multi_view_images")
7255
+ and self.multi_view_images
7256
+ and self.multi_view_images[0]
7257
+ and self.control_panel.get_settings().get("auto_save", True)
7246
7258
  ):
7247
7259
  self._save_multi_view_output()
7248
7260
 
7249
- # If cached list isn't ready, fall back to file model navigation
7261
+ # Get current image path - look for any valid image in multi-view state
7262
+ current_path = None
7263
+ if hasattr(self, "multi_view_images") and self.multi_view_images:
7264
+ # Find the first valid image path in the current multi-view state
7265
+ for img_path in self.multi_view_images:
7266
+ if img_path:
7267
+ current_path = img_path
7268
+ break
7269
+
7270
+ # Fallback to current_image_path if no valid multi-view images found
7271
+ if (
7272
+ not current_path
7273
+ and hasattr(self, "current_image_path")
7274
+ and self.current_image_path
7275
+ ):
7276
+ current_path = self.current_image_path
7277
+
7278
+ # If no valid path found, can't navigate forward
7279
+ if not current_path:
7280
+ return
7281
+
7282
+ # Get the number of viewers for multi-view mode
7283
+ config = self._get_multi_view_config()
7284
+ num_viewers = config["num_viewers"]
7285
+
7286
+ # Try to use fast file manager first (respects sorting/filtering)
7287
+ try:
7288
+ if (
7289
+ hasattr(self, "right_panel")
7290
+ and hasattr(self.right_panel, "file_manager")
7291
+ and hasattr(self.right_panel.file_manager, "getSurroundingFiles")
7292
+ ):
7293
+ file_manager = self.right_panel.file_manager
7294
+ surrounding_files = file_manager.getSurroundingFiles(
7295
+ Path(current_path), num_viewers * 2
7296
+ )
7297
+
7298
+ if len(surrounding_files) > num_viewers:
7299
+ # Skip ahead by num_viewers to get the next batch
7300
+ next_batch = surrounding_files[num_viewers : num_viewers * 2]
7301
+
7302
+ # Convert to strings and load
7303
+ images_to_load = [str(p) if p else None for p in next_batch]
7304
+ self._load_multi_view_images(images_to_load)
7305
+
7306
+ # Update file manager selection to first image of new batch
7307
+ if next_batch and next_batch[0]:
7308
+ self.right_panel.select_file(next_batch[0])
7309
+ return
7310
+ except Exception:
7311
+ pass # Fall back to cached list approach
7312
+
7313
+ # Fall back to cached list approach (for backward compatibility / tests)
7250
7314
  if not self.cached_image_paths:
7251
7315
  self._load_next_multi_batch_fallback()
7252
7316
  return
7253
7317
 
7254
- # Get current image path
7255
- current_path = self.file_model.filePath(self.current_file_index)
7256
-
7257
7318
  # Find current position in cached list
7258
7319
  try:
7259
7320
  current_index = self.cached_image_paths.index(current_path)
@@ -7262,13 +7323,14 @@ class MainWindow(QMainWindow):
7262
7323
  self._load_next_multi_batch_fallback()
7263
7324
  return
7264
7325
 
7265
- # Get the number of viewers for multi-view mode
7266
- config = self._get_multi_view_config()
7267
- num_viewers = config["num_viewers"]
7268
-
7269
7326
  # Skip num_viewers positions ahead in cached list
7270
7327
  target_index = current_index + num_viewers
7271
7328
 
7329
+ # Check if we can navigate forward (at least one valid image at target position)
7330
+ if target_index >= len(self.cached_image_paths):
7331
+ return # Can't navigate forward - at or past the end
7332
+
7333
+ # Check if we have at least one valid image at the target position
7272
7334
  if target_index < len(self.cached_image_paths):
7273
7335
  # Collect consecutive images
7274
7336
  image_paths = []
@@ -7278,39 +7340,88 @@ class MainWindow(QMainWindow):
7278
7340
  else:
7279
7341
  image_paths.append(None)
7280
7342
 
7281
- # Load all images
7282
- self._load_multi_view_images(image_paths)
7283
-
7284
- # Update current file index to the first image of the new batch
7285
- # Find the file model index for this path
7286
- if image_paths and image_paths[0]:
7287
- parent_index = self.current_file_index.parent()
7288
- for row in range(self.file_model.rowCount(parent_index)):
7289
- index = self.file_model.index(row, 0, parent_index)
7290
- if self.file_model.filePath(index) == image_paths[0]:
7291
- self.current_file_index = index
7292
- self.right_panel.file_tree.setCurrentIndex(index)
7293
- break
7343
+ # Only proceed if we have at least one valid image (prevent all-None batches)
7344
+ if any(path is not None for path in image_paths):
7345
+ # Load all images
7346
+ self._load_multi_view_images(image_paths)
7347
+
7348
+ # Update current file index to the first image of the new batch
7349
+ # Find the file model index for this path
7350
+ if image_paths and image_paths[0]:
7351
+ parent_index = self.current_file_index.parent()
7352
+ for row in range(self.file_model.rowCount(parent_index)):
7353
+ index = self.file_model.index(row, 0, parent_index)
7354
+ if self.file_model.filePath(index) == image_paths[0]:
7355
+ self.current_file_index = index
7356
+ self.right_panel.file_tree.setCurrentIndex(index)
7357
+ break
7358
+ # If all would be None, don't navigate (stay at current position)
7294
7359
 
7295
7360
  def _load_previous_multi_batch(self):
7296
- """Load the previous batch of images using cached list."""
7297
- if not self.current_file_index.isValid():
7298
- return
7299
-
7300
- # Auto-save if enabled and we have current images (not the first load)
7301
- if self.multi_view_images[0] and self.control_panel.get_settings().get(
7302
- "auto_save", True
7361
+ """Load the previous batch of images using fast file manager or cached list."""
7362
+ # Auto-save if enabled and we have current images
7363
+ if (
7364
+ hasattr(self, "multi_view_images")
7365
+ and self.multi_view_images
7366
+ and self.multi_view_images[0]
7367
+ and self.control_panel.get_settings().get("auto_save", True)
7303
7368
  ):
7304
7369
  self._save_multi_view_output()
7305
7370
 
7306
- # If cached list isn't ready, fall back to file model navigation
7371
+ # Get current image path - look for any valid image in multi-view state
7372
+ current_path = None
7373
+ if hasattr(self, "multi_view_images") and self.multi_view_images:
7374
+ # Find the first valid image path in the current multi-view state
7375
+ for img_path in self.multi_view_images:
7376
+ if img_path:
7377
+ current_path = img_path
7378
+ break
7379
+
7380
+ # Fallback to current_image_path if no valid multi-view images found
7381
+ if (
7382
+ not current_path
7383
+ and hasattr(self, "current_image_path")
7384
+ and self.current_image_path
7385
+ ):
7386
+ current_path = self.current_image_path
7387
+
7388
+ # If no valid path found, can't navigate backward
7389
+ if not current_path:
7390
+ return
7391
+
7392
+ # Get the number of viewers for multi-view mode
7393
+ config = self._get_multi_view_config()
7394
+ num_viewers = config["num_viewers"]
7395
+
7396
+ # Try to use fast file manager first (respects sorting/filtering)
7397
+ try:
7398
+ if (
7399
+ hasattr(self, "right_panel")
7400
+ and hasattr(self.right_panel, "file_manager")
7401
+ and hasattr(self.right_panel.file_manager, "getPreviousFiles")
7402
+ ):
7403
+ file_manager = self.right_panel.file_manager
7404
+ previous_batch = file_manager.getPreviousFiles(
7405
+ Path(current_path), num_viewers
7406
+ )
7407
+
7408
+ if previous_batch and any(previous_batch):
7409
+ # Convert to strings and load
7410
+ images_to_load = [str(p) if p else None for p in previous_batch]
7411
+ self._load_multi_view_images(images_to_load)
7412
+
7413
+ # Update file manager selection to first image of new batch
7414
+ if previous_batch and previous_batch[0]:
7415
+ self.right_panel.select_file(previous_batch[0])
7416
+ return
7417
+ except Exception:
7418
+ pass # Fall back to cached list approach
7419
+
7420
+ # Fall back to cached list approach (for backward compatibility / tests)
7307
7421
  if not self.cached_image_paths:
7308
7422
  self._load_previous_multi_batch_fallback()
7309
7423
  return
7310
7424
 
7311
- # Get current image path
7312
- current_path = self.file_model.filePath(self.current_file_index)
7313
-
7314
7425
  # Find current position in cached list
7315
7426
  try:
7316
7427
  current_index = self.cached_image_paths.index(current_path)
@@ -7319,10 +7430,6 @@ class MainWindow(QMainWindow):
7319
7430
  self._load_previous_multi_batch_fallback()
7320
7431
  return
7321
7432
 
7322
- # Get the number of viewers for multi-view mode
7323
- config = self._get_multi_view_config()
7324
- num_viewers = config["num_viewers"]
7325
-
7326
7433
  # Skip num_viewers positions back in cached list
7327
7434
  target_index = current_index - num_viewers
7328
7435
  if target_index < 0:
@@ -7331,24 +7438,30 @@ class MainWindow(QMainWindow):
7331
7438
  # Collect consecutive images
7332
7439
  image_paths = []
7333
7440
  for i in range(num_viewers):
7334
- if target_index + i < len(self.cached_image_paths):
7441
+ if (
7442
+ target_index + i < len(self.cached_image_paths)
7443
+ and target_index + i >= 0
7444
+ ):
7335
7445
  image_paths.append(self.cached_image_paths[target_index + i])
7336
7446
  else:
7337
7447
  image_paths.append(None)
7338
7448
 
7339
- # Load all images
7340
- self._load_multi_view_images(image_paths)
7449
+ # Only proceed if we have at least one valid image (prevent all-None batches)
7450
+ if any(path is not None for path in image_paths):
7451
+ # Load all images
7452
+ self._load_multi_view_images(image_paths)
7341
7453
 
7342
- # Update current file index to the first image of the new batch
7343
- # Find the file model index for this path
7344
- if image_paths and image_paths[0]:
7345
- parent_index = self.current_file_index.parent()
7346
- for row in range(self.file_model.rowCount(parent_index)):
7347
- index = self.file_model.index(row, 0, parent_index)
7348
- if self.file_model.filePath(index) == image_paths[0]:
7349
- self.current_file_index = index
7350
- self.right_panel.file_tree.setCurrentIndex(index)
7351
- break
7454
+ # Update current file index to the first image of the new batch
7455
+ # Find the file model index for this path
7456
+ if image_paths and image_paths[0]:
7457
+ parent_index = self.current_file_index.parent()
7458
+ for row in range(self.file_model.rowCount(parent_index)):
7459
+ index = self.file_model.index(row, 0, parent_index)
7460
+ if self.file_model.filePath(index) == image_paths[0]:
7461
+ self.current_file_index = index
7462
+ self.right_panel.file_tree.setCurrentIndex(index)
7463
+ break
7464
+ # If all would be None, don't navigate (stay at current position)
7352
7465
 
7353
7466
  def _load_next_multi_batch_fallback(self):
7354
7467
  """Fallback navigation using file model when cached list isn't available."""
@@ -136,6 +136,7 @@ class FastFileModel(QAbstractTableModel):
136
136
  def __init__(self):
137
137
  super().__init__()
138
138
  self._files: list[FileInfo] = []
139
+ self._path_to_index: dict[str, int] = {} # For O(1) lookups
139
140
  self._scanner: FileScanner | None = None
140
141
 
141
142
  def rowCount(self, parent=QModelIndex()):
@@ -218,6 +219,7 @@ class FastFileModel(QAbstractTableModel):
218
219
  # Clear current files
219
220
  self.beginResetModel()
220
221
  self._files.clear()
222
+ self._path_to_index.clear()
221
223
  self.endResetModel()
222
224
 
223
225
  # Start new scan
@@ -231,7 +233,13 @@ class FastFileModel(QAbstractTableModel):
231
233
  start_row = len(self._files)
232
234
  end_row = start_row + len(files) - 1
233
235
  self.beginInsertRows(QModelIndex(), start_row, end_row)
234
- self._files.extend(files)
236
+
237
+ # Add files and update path-to-index mapping
238
+ for i, file_info in enumerate(files):
239
+ idx = start_row + i
240
+ self._files.append(file_info)
241
+ self._path_to_index[str(file_info.path)] = idx
242
+
235
243
  self.endInsertRows()
236
244
 
237
245
  def _on_scan_complete(self, total: int):
@@ -262,12 +270,80 @@ class FastFileModel(QAbstractTableModel):
262
270
  self.dataChanged.emit(index, index)
263
271
  break
264
272
 
273
+ def updateFileStatus(self, image_path: Path):
274
+ """Update both NPZ and TXT status for a specific image file"""
275
+ image_path_str = str(image_path)
276
+ npz_path = image_path.with_suffix(".npz")
277
+ txt_path = image_path.with_suffix(".txt")
278
+ has_npz = npz_path.exists()
279
+ has_txt = txt_path.exists()
280
+
281
+ # O(1) lookup using path-to-index mapping
282
+ if image_path_str not in self._path_to_index:
283
+ return # File not in current view
284
+
285
+ i = self._path_to_index[image_path_str]
286
+ file_info = self._files[i]
287
+
288
+ # Update status and emit changes only if needed
289
+ old_has_npz = file_info.has_npz
290
+ old_has_txt = file_info.has_txt
291
+ file_info.has_npz = has_npz
292
+ file_info.has_txt = has_txt
293
+
294
+ # Emit dataChanged for NPZ column if status changed
295
+ if old_has_npz != has_npz:
296
+ index = self.index(i, 3) # NPZ column
297
+ self.dataChanged.emit(index, index)
298
+
299
+ # Emit dataChanged for TXT column if status changed
300
+ if old_has_txt != has_txt:
301
+ index = self.index(i, 4) # TXT column
302
+ self.dataChanged.emit(index, index)
303
+
265
304
  def getFileIndex(self, path: Path) -> int:
266
305
  """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
306
+ return self._path_to_index.get(str(path), -1)
307
+
308
+ def batchUpdateFileStatus(self, image_paths: list[Path]):
309
+ """Batch update file status for multiple files"""
310
+ if not image_paths:
311
+ return
312
+
313
+ changed_indices = []
314
+
315
+ for image_path in image_paths:
316
+ image_path_str = str(image_path)
317
+
318
+ # O(1) lookup using path-to-index mapping
319
+ if image_path_str not in self._path_to_index:
320
+ continue # File not in current view
321
+
322
+ i = self._path_to_index[image_path_str]
323
+ file_info = self._files[i]
324
+
325
+ # Check file existence
326
+ npz_path = image_path.with_suffix(".npz")
327
+ txt_path = image_path.with_suffix(".txt")
328
+ has_npz = npz_path.exists()
329
+ has_txt = txt_path.exists()
330
+
331
+ # Update status and track changes
332
+ old_has_npz = file_info.has_npz
333
+ old_has_txt = file_info.has_txt
334
+ file_info.has_npz = has_npz
335
+ file_info.has_txt = has_txt
336
+
337
+ # Track changed indices for batch emission
338
+ if old_has_npz != has_npz:
339
+ changed_indices.append((i, 3)) # NPZ column
340
+ if old_has_txt != has_txt:
341
+ changed_indices.append((i, 4)) # TXT column
342
+
343
+ # Batch emit dataChanged signals
344
+ for i, col in changed_indices:
345
+ index = self.index(i, col)
346
+ self.dataChanged.emit(index, index)
271
347
 
272
348
 
273
349
  class FileSortProxyModel(QSortFilterProxyModel):
@@ -551,9 +627,88 @@ class FastFileManager(QWidget):
551
627
  """Update NPZ status for a specific image file"""
552
628
  self._model.updateNpzStatus(image_path)
553
629
 
630
+ def updateFileStatus(self, image_path: Path):
631
+ """Update both NPZ and TXT status for a specific image file"""
632
+ self._model.updateFileStatus(image_path)
633
+
554
634
  def refreshFile(self, image_path: Path):
555
- """Refresh status for a specific file (alias for updateNpzStatus)"""
556
- self.updateNpzStatus(image_path)
635
+ """Refresh status for a specific file"""
636
+ self.updateFileStatus(image_path)
637
+
638
+ def batchUpdateFileStatus(self, image_paths: list[Path]):
639
+ """Batch update file status for multiple files"""
640
+ self._model.batchUpdateFileStatus(image_paths)
641
+
642
+ def getSurroundingFiles(self, current_path: Path, count: int) -> list[Path]:
643
+ """Get files in current sorted/filtered order surrounding the given path"""
644
+ files = []
645
+
646
+ # Find current file in proxy model order
647
+ current_index = -1
648
+ for row in range(self._proxy_model.rowCount()):
649
+ proxy_index = self._proxy_model.index(row, 0)
650
+ source_index = self._proxy_model.mapToSource(proxy_index)
651
+ file_info = self._model.getFileInfo(source_index.row())
652
+ if file_info and file_info.path == current_path:
653
+ current_index = row
654
+ break
655
+
656
+ if current_index == -1:
657
+ return [] # File not found in current view
658
+
659
+ # Get surrounding files in proxy order
660
+ for i in range(count):
661
+ row = current_index + i
662
+ if row < self._proxy_model.rowCount():
663
+ proxy_index = self._proxy_model.index(row, 0)
664
+ source_index = self._proxy_model.mapToSource(proxy_index)
665
+ file_info = self._model.getFileInfo(source_index.row())
666
+ if file_info:
667
+ files.append(file_info.path)
668
+ else:
669
+ files.append(None)
670
+ else:
671
+ files.append(None)
672
+
673
+ return files
674
+
675
+ def getPreviousFiles(self, current_path: Path, count: int) -> list[Path]:
676
+ """Get previous files in current sorted/filtered order before the given path"""
677
+ files = []
678
+
679
+ # Find current file in proxy model order
680
+ current_index = -1
681
+ for row in range(self._proxy_model.rowCount()):
682
+ proxy_index = self._proxy_model.index(row, 0)
683
+ source_index = self._proxy_model.mapToSource(proxy_index)
684
+ file_info = self._model.getFileInfo(source_index.row())
685
+ if file_info and file_info.path == current_path:
686
+ current_index = row
687
+ break
688
+
689
+ if current_index == -1:
690
+ return [] # File not found in current view
691
+
692
+ # Get previous files going backward from current position
693
+ start_row = current_index - count
694
+ if start_row < 0:
695
+ start_row = 0
696
+
697
+ # Get consecutive files starting from start_row
698
+ for i in range(count):
699
+ row = start_row + i
700
+ if row < current_index and row >= 0:
701
+ proxy_index = self._proxy_model.index(row, 0)
702
+ source_index = self._proxy_model.mapToSource(proxy_index)
703
+ file_info = self._model.getFileInfo(source_index.row())
704
+ if file_info:
705
+ files.append(file_info.path)
706
+ else:
707
+ files.append(None)
708
+ else:
709
+ files.append(None)
710
+
711
+ return files
557
712
 
558
713
  def _on_item_clicked(self, index: QModelIndex):
559
714
  """Handle item click"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lazylabel-gui
3
- Version: 1.3.2
3
+ Version: 1.3.3
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
File without changes
File without changes
File without changes