lazylabel-gui 1.3.0__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.
- lazylabel/core/file_manager.py +33 -1
- lazylabel/ui/main_window.py +308 -80
- lazylabel/ui/right_panel.py +29 -2
- lazylabel/utils/fast_file_manager.py +676 -0
- {lazylabel_gui-1.3.0.dist-info → lazylabel_gui-1.3.2.dist-info}/METADATA +1 -1
- {lazylabel_gui-1.3.0.dist-info → lazylabel_gui-1.3.2.dist-info}/RECORD +10 -10
- lazylabel/ui/test_hover.py +0 -48
- {lazylabel_gui-1.3.0.dist-info → lazylabel_gui-1.3.2.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.3.0.dist-info → lazylabel_gui-1.3.2.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.3.0.dist-info → lazylabel_gui-1.3.2.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.3.0.dist-info → lazylabel_gui-1.3.2.dist-info}/top_level.txt +0 -0
lazylabel/core/file_manager.py
CHANGED
@@ -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
|
-
|
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(
|
lazylabel/ui/main_window.py
CHANGED
@@ -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 -
|
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
|
-
|
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
|
-
|
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
|
-
|
2129
|
-
|
2130
|
-
|
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
|
-
|
2148
|
-
|
2149
|
-
|
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
|
-
|
2812
|
-
self.
|
2813
|
-
|
2814
|
-
|
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
|
-
|
2818
|
-
|
2819
|
-
|
2820
|
-
|
2821
|
-
self
|
2822
|
-
|
2823
|
-
|
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
|
-
|
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
|
3201
|
-
if hasattr(self, "
|
3202
|
-
for
|
3203
|
-
if
|
3204
|
-
|
3205
|
-
self
|
3206
|
-
|
3207
|
-
|
3208
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
3303
|
-
|
3304
|
-
|
3305
|
-
|
3306
|
-
|
3307
|
-
|
3308
|
-
|
3309
|
-
|
3310
|
-
|
3311
|
-
|
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
|
-
#
|
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
|
4951
|
-
self.
|
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
|
-
#
|
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
|
4979
|
-
self.
|
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
|
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()
|
lazylabel/ui/right_panel.py
CHANGED
@@ -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
|
-
|
113
|
-
|
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."""
|