lazylabel-gui 1.3.0__tar.gz → 1.3.2__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 (55) hide show
  1. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/PKG-INFO +1 -1
  2. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/pyproject.toml +1 -1
  3. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/core/file_manager.py +33 -1
  4. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/main_window.py +308 -80
  5. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/right_panel.py +29 -2
  6. lazylabel_gui-1.3.2/src/lazylabel/utils/fast_file_manager.py +676 -0
  7. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel_gui.egg-info/PKG-INFO +1 -1
  8. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel_gui.egg-info/SOURCES.txt +1 -1
  9. lazylabel_gui-1.3.0/src/lazylabel/ui/test_hover.py +0 -48
  10. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/LICENSE +0 -0
  11. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/README.md +0 -0
  12. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/setup.cfg +0 -0
  13. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/__init__.py +0 -0
  14. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/__main__.py +0 -0
  15. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/config/__init__.py +0 -0
  16. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/config/hotkeys.py +0 -0
  17. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/config/paths.py +0 -0
  18. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/config/settings.py +0 -0
  19. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/core/__init__.py +0 -0
  20. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/core/model_manager.py +0 -0
  21. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/core/segment_manager.py +0 -0
  22. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/main.py +0 -0
  23. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/models/__init__.py +0 -0
  24. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/models/sam2_model.py +0 -0
  25. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/models/sam_model.py +0 -0
  26. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/__init__.py +0 -0
  27. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/control_panel.py +0 -0
  28. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/editable_vertex.py +0 -0
  29. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/hotkey_dialog.py +0 -0
  30. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/hoverable_pixelmap_item.py +0 -0
  31. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/hoverable_polygon_item.py +0 -0
  32. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/modes/__init__.py +0 -0
  33. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/modes/base_mode.py +0 -0
  34. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/modes/multi_view_mode.py +0 -0
  35. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/modes/single_view_mode.py +0 -0
  36. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/numeric_table_widget_item.py +0 -0
  37. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/photo_viewer.py +0 -0
  38. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/reorderable_class_table.py +0 -0
  39. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/widgets/__init__.py +0 -0
  40. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/widgets/adjustments_widget.py +0 -0
  41. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/widgets/border_crop_widget.py +0 -0
  42. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/widgets/channel_threshold_widget.py +0 -0
  43. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/widgets/fft_threshold_widget.py +0 -0
  44. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/widgets/fragment_threshold_widget.py +0 -0
  45. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/widgets/model_selection_widget.py +0 -0
  46. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/widgets/settings_widget.py +0 -0
  47. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/ui/widgets/status_bar.py +0 -0
  48. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/utils/__init__.py +0 -0
  49. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/utils/custom_file_system_model.py +0 -0
  50. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/utils/logger.py +0 -0
  51. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel/utils/utils.py +0 -0
  52. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel_gui.egg-info/dependency_links.txt +0 -0
  53. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel_gui.egg-info/entry_points.txt +0 -0
  54. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/src/lazylabel_gui.egg-info/requires.txt +0 -0
  55. {lazylabel_gui-1.3.0 → lazylabel_gui-1.3.2}/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.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lazylabel-gui"
7
- version = "1.3.0"
7
+ version = "1.3.2"
8
8
  authors = [
9
9
  { name="Deniz N. Cakan", email="deniz.n.cakan@gmail.com" },
10
10
  ]
@@ -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
  )
@@ -1601,14 +1603,19 @@ class MainWindow(QMainWindow):
1601
1603
  if self.current_image_path:
1602
1604
  self._mark_sam_dirty()
1603
1605
 
1604
- # Handle multi view mode - mark models as dirty so they get updated with adjusted images
1606
+ # Handle multi view mode - use fast updates for adjusted images instead of marking dirty
1605
1607
  elif self.view_mode == "multi" and hasattr(self, "multi_view_models"):
1608
+ changed_indices = []
1606
1609
  for i in range(len(self.multi_view_models)):
1607
1610
  if (
1608
1611
  self.multi_view_images[i]
1609
1612
  and self.multi_view_models[i] is not None
1610
1613
  ):
1611
- self._mark_multi_view_sam_dirty(i)
1614
+ changed_indices.append(i)
1615
+
1616
+ # Use fast updates instead of marking all models dirty
1617
+ if changed_indices:
1618
+ self._fast_update_multi_view_images(changed_indices)
1612
1619
 
1613
1620
  # File management methods
1614
1621
  def _open_folder_dialog(self):
@@ -1620,6 +1627,13 @@ class MainWindow(QMainWindow):
1620
1627
  self._start_background_image_discovery()
1621
1628
  self.viewer.setFocus()
1622
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
+
1623
1637
  def _load_selected_image(self, index):
1624
1638
  """Load the selected image. Auto-saves previous work if enabled."""
1625
1639
 
@@ -1681,6 +1695,131 @@ class MainWindow(QMainWindow):
1681
1695
 
1682
1696
  self._show_success_notification(f"Loaded: {Path(self.current_image_path).name}")
1683
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
+
1684
1823
  def _load_selected_image_multi_view(self, index, path):
1685
1824
  """Load selected image in multi-view mode starting from the selected file."""
1686
1825
  # Auto-save if enabled and we have current images (not the first load)
@@ -1805,17 +1944,28 @@ class MainWindow(QMainWindow):
1805
1944
  while len(self._last_multi_view_images) < num_viewers:
1806
1945
  self._last_multi_view_images.append(None)
1807
1946
 
1808
- # Check and update SAM models for all viewers
1947
+ # Check and update SAM models for all viewers - USE FAST UPDATES for existing models
1948
+ changed_indices = []
1809
1949
  for i in range(num_viewers):
1810
1950
  image_path = (
1811
1951
  self.multi_view_images[i] if i < len(self.multi_view_images) else None
1812
1952
  )
1813
- if (
1814
- i < len(self.multi_view_models)
1815
- and self._last_multi_view_images[i] != image_path
1816
- ):
1953
+ if self._last_multi_view_images[i] != image_path:
1817
1954
  self._last_multi_view_images[i] = image_path
1818
- self._mark_multi_view_sam_dirty(i)
1955
+
1956
+ # Only mark dirty if model doesn't exist yet (needs initialization)
1957
+ if (
1958
+ i >= len(self.multi_view_models)
1959
+ or self.multi_view_models[i] is None
1960
+ ):
1961
+ self._mark_multi_view_sam_dirty(i)
1962
+ else:
1963
+ # Model exists - use fast image update instead of recreation
1964
+ changed_indices.append(i)
1965
+
1966
+ # Perform fast batch updates for existing models
1967
+ if changed_indices:
1968
+ self._fast_update_multi_view_images(changed_indices)
1819
1969
 
1820
1970
  # Load existing segments for all loaded images
1821
1971
  valid_image_paths = [path for path in image_paths if path is not None]
@@ -2125,17 +2275,9 @@ class MainWindow(QMainWindow):
2125
2275
  self._load_next_multi_batch()
2126
2276
  return
2127
2277
 
2128
- if not self.current_file_index.isValid():
2129
- return
2130
- parent = self.current_file_index.parent()
2131
- row = self.current_file_index.row()
2132
- # Find next valid image file
2133
- for next_row in range(row + 1, self.file_model.rowCount(parent)):
2134
- next_index = self.file_model.index(next_row, 0, parent)
2135
- path = self.file_model.filePath(next_index)
2136
- if os.path.isfile(path) and self.file_manager.is_image_file(path):
2137
- self._load_selected_image(next_index)
2138
- return
2278
+ # Use new file manager navigation
2279
+ self.right_panel.navigate_next_image()
2280
+ return
2139
2281
 
2140
2282
  def _load_previous_image(self):
2141
2283
  """Load previous image in the file list."""
@@ -2144,17 +2286,9 @@ class MainWindow(QMainWindow):
2144
2286
  self._load_previous_multi_batch()
2145
2287
  return
2146
2288
 
2147
- if not self.current_file_index.isValid():
2148
- return
2149
- parent = self.current_file_index.parent()
2150
- row = self.current_file_index.row()
2151
- # Find previous valid image file
2152
- for prev_row in range(row - 1, -1, -1):
2153
- prev_index = self.file_model.index(prev_row, 0, parent)
2154
- path = self.file_model.filePath(prev_index)
2155
- if os.path.isfile(path) and self.file_manager.is_image_file(path):
2156
- self._load_selected_image(prev_index)
2157
- return
2289
+ # Use new file manager navigation
2290
+ self.right_panel.navigate_previous_image()
2291
+ return
2158
2292
 
2159
2293
  # Segment management methods
2160
2294
  def _assign_selected_to_class(self):
@@ -2807,20 +2941,34 @@ class MainWindow(QMainWindow):
2807
2941
 
2808
2942
  def _handle_enter_press(self):
2809
2943
  """Handle enter key press."""
2944
+ logger.debug(f"Enter pressed - mode: {self.mode}, view_mode: {self.view_mode}")
2945
+
2810
2946
  if self.mode == "polygon":
2811
- if self.view_mode == "single" and self.polygon_points:
2812
- self._finalize_polygon()
2813
- elif self.view_mode == "multi" and hasattr(
2814
- self, "multi_view_polygon_points"
2815
- ):
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":
2816
2959
  # Complete polygons for all viewers that have points
2817
- for i, points in enumerate(self.multi_view_polygon_points):
2818
- if points and len(points) >= 3:
2819
- # Use the multi-view mode handler for proper pairing logic
2820
- if hasattr(self, "multi_view_mode_handler"):
2821
- self.multi_view_mode_handler._finalize_multi_view_polygon(i)
2822
- else:
2823
- 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()
2824
2972
  else:
2825
2973
  # First accept any AI segments (same as spacebar), then save
2826
2974
  self._accept_ai_segment()
@@ -3182,7 +3330,13 @@ class MainWindow(QMainWindow):
3182
3330
  try:
3183
3331
  os.remove(file_path)
3184
3332
  deleted_files.append(os.path.basename(file_path))
3185
- 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
+ )
3186
3340
  except Exception as e:
3187
3341
  self._show_error_notification(
3188
3342
  f"Error deleting {file_path}: {e}"
@@ -3197,20 +3351,15 @@ class MainWindow(QMainWindow):
3197
3351
  f"Error saving {os.path.basename(image_path)}: {str(e)}"
3198
3352
  )
3199
3353
 
3200
- # Update file list with green flash and tick marks (like single view mode)
3201
- if hasattr(self, "_saved_file_paths") and self._saved_file_paths:
3202
- for path in self._saved_file_paths:
3203
- if path:
3204
- self.file_model.update_cache_for_path(path)
3205
- self.file_model.set_highlighted_path(path)
3206
- QTimer.singleShot(
3207
- 1500,
3208
- lambda p=path: (
3209
- self.file_model.set_highlighted_path(None)
3210
- if self.file_model.highlighted_path == p
3211
- else None
3212
- ),
3213
- )
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))
3214
3363
  # Clear the tracking list for next save
3215
3364
  self._saved_file_paths = []
3216
3365
 
@@ -3242,7 +3391,13 @@ class MainWindow(QMainWindow):
3242
3391
  try:
3243
3392
  os.remove(file_path)
3244
3393
  deleted_files.append(file_path)
3245
- 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
+ )
3246
3401
  except Exception as e:
3247
3402
  self._show_error_notification(
3248
3403
  f"Error deleting {file_path}: {e}"
@@ -3260,24 +3415,35 @@ class MainWindow(QMainWindow):
3260
3415
  try:
3261
3416
  settings = self.control_panel.get_settings()
3262
3417
  npz_path = None
3263
- 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
+
3264
3423
  if settings.get("save_npz", True):
3265
3424
  h, w = (
3266
3425
  self.viewer._pixmap_item.pixmap().height(),
3267
3426
  self.viewer._pixmap_item.pixmap().width(),
3268
3427
  )
3269
3428
  class_order = self.segment_manager.get_unique_class_ids()
3429
+ logger.debug(f"Class order for saving: {class_order}")
3430
+
3270
3431
  if class_order:
3432
+ logger.debug(
3433
+ f"Attempting to save NPZ to: {os.path.splitext(self.current_image_path)[0]}.npz"
3434
+ )
3271
3435
  npz_path = self.file_manager.save_npz(
3272
3436
  self.current_image_path,
3273
3437
  (h, w),
3274
3438
  class_order,
3275
3439
  self.current_crop_coords,
3276
3440
  )
3441
+ logger.debug(f"NPZ save completed: {npz_path}")
3277
3442
  self._show_success_notification(
3278
3443
  f"Saved: {os.path.basename(npz_path)}"
3279
3444
  )
3280
3445
  else:
3446
+ logger.warning("No classes defined for saving")
3281
3447
  self._show_warning_notification("No classes defined for saving.")
3282
3448
  if settings.get("save_txt", True):
3283
3449
  h, w = (
@@ -3292,27 +3458,25 @@ class MainWindow(QMainWindow):
3292
3458
  else:
3293
3459
  class_labels = [str(cid) for cid in class_order]
3294
3460
  if class_order:
3295
- txt_path = self.file_manager.save_yolo_txt(
3461
+ self.file_manager.save_yolo_txt(
3296
3462
  self.current_image_path,
3297
3463
  (h, w),
3298
3464
  class_order,
3299
3465
  class_labels,
3300
3466
  self.current_crop_coords,
3301
3467
  )
3302
- # Efficiently update file list tickboxes and highlight
3303
- for path in [npz_path, txt_path]:
3304
- if path:
3305
- self.file_model.update_cache_for_path(path)
3306
- self.file_model.set_highlighted_path(path)
3307
- QTimer.singleShot(
3308
- 1500,
3309
- lambda p=path: (
3310
- self.file_model.set_highlighted_path(None)
3311
- if self.file_model.highlighted_path == p
3312
- else None
3313
- ),
3314
- )
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
+ )
3315
3478
  except Exception as e:
3479
+ logger.error(f"Error saving file: {str(e)}", exc_info=True)
3316
3480
  self._show_error_notification(f"Error saving: {str(e)}")
3317
3481
 
3318
3482
  def _handle_merge_press(self):
@@ -4944,11 +5108,19 @@ class MainWindow(QMainWindow):
4944
5108
  if hasattr(self, "multi_view_images") and any(self.multi_view_images):
4945
5109
  self._apply_multi_view_image_processing_fast()
4946
5110
 
4947
- # Mark multi-view SAM models as dirty if needed
5111
+ # Use fast updates for multi-view SAM models instead of marking dirty
4948
5112
  if self.settings.operate_on_view:
5113
+ changed_indices = []
4949
5114
  for i in range(len(self.multi_view_images)):
4950
- if self.multi_view_images[i]:
4951
- self._mark_multi_view_sam_dirty(i)
5115
+ if (
5116
+ self.multi_view_images[i]
5117
+ and i < len(self.multi_view_models)
5118
+ and self.multi_view_models[i] is not None
5119
+ ):
5120
+ changed_indices.append(i)
5121
+
5122
+ if changed_indices:
5123
+ self._fast_update_multi_view_images(changed_indices)
4952
5124
  return
4953
5125
 
4954
5126
  # Handle single-view mode
@@ -4972,11 +5144,19 @@ class MainWindow(QMainWindow):
4972
5144
  if hasattr(self, "multi_view_images") and any(self.multi_view_images):
4973
5145
  self._apply_multi_view_image_processing_fast()
4974
5146
 
4975
- # Mark multi-view SAM models as dirty if needed
5147
+ # Use fast updates for multi-view SAM models instead of marking dirty
4976
5148
  if self.settings.operate_on_view:
5149
+ changed_indices = []
4977
5150
  for i in range(len(self.multi_view_images)):
4978
- if self.multi_view_images[i]:
4979
- self._mark_multi_view_sam_dirty(i)
5151
+ if (
5152
+ self.multi_view_images[i]
5153
+ and i < len(self.multi_view_models)
5154
+ and self.multi_view_models[i] is not None
5155
+ ):
5156
+ changed_indices.append(i)
5157
+
5158
+ if changed_indices:
5159
+ self._fast_update_multi_view_images(changed_indices)
4980
5160
  return
4981
5161
 
4982
5162
  # Handle single-view mode
@@ -6511,10 +6691,58 @@ class MainWindow(QMainWindow):
6511
6691
  )
6512
6692
 
6513
6693
  def _mark_multi_view_sam_dirty(self, viewer_index):
6514
- """Mark multi-view SAM model as dirty (needs image update)."""
6694
+ """Mark multi-view SAM model as dirty (needs model recreation)."""
6515
6695
  if 0 <= viewer_index < len(self.multi_view_models_dirty):
6516
6696
  self.multi_view_models_dirty[viewer_index] = True
6517
6697
 
6698
+ def _update_multi_view_model_image(self, viewer_index, image_path):
6699
+ """Fast update: Set new image in existing model without recreating model."""
6700
+ if (
6701
+ viewer_index >= len(self.multi_view_models)
6702
+ or self.multi_view_models[viewer_index] is None
6703
+ or not image_path
6704
+ ):
6705
+ return False
6706
+
6707
+ model = self.multi_view_models[viewer_index]
6708
+
6709
+ try:
6710
+ # Get current modified image if operate_on_view is enabled
6711
+ if self.settings.operate_on_view:
6712
+ current_image = self._get_multi_view_modified_image(viewer_index)
6713
+ if current_image is not None:
6714
+ return model.set_image_from_array(current_image)
6715
+
6716
+ # Use original image path
6717
+ return model.set_image_from_path(image_path)
6718
+
6719
+ except Exception as e:
6720
+ logger.error(f"Failed to update image for model {viewer_index}: {e}")
6721
+ return False
6722
+
6723
+ def _fast_update_multi_view_images(self, changed_indices):
6724
+ """Fast batch update of images in existing models without recreation."""
6725
+ if not changed_indices:
6726
+ return
6727
+
6728
+ logger.debug(f"Fast updating images for viewers: {changed_indices}")
6729
+
6730
+ for viewer_index in changed_indices:
6731
+ if viewer_index >= len(self.multi_view_images):
6732
+ continue
6733
+
6734
+ image_path = self.multi_view_images[viewer_index]
6735
+ if image_path:
6736
+ success = self._update_multi_view_model_image(viewer_index, image_path)
6737
+ if success:
6738
+ logger.debug(f"Fast updated model {viewer_index} with new image")
6739
+ else:
6740
+ # Fall back to full model update if fast update fails
6741
+ logger.warning(
6742
+ f"Fast update failed for viewer {viewer_index}, marking dirty"
6743
+ )
6744
+ self._mark_multi_view_sam_dirty(viewer_index)
6745
+
6518
6746
  def _ensure_multi_view_sam_updated(self, viewer_index):
6519
6747
  """Ensure multi-view SAM model is updated for the given viewer."""
6520
6748
  config = self._get_multi_view_config()