lazylabel-gui 1.3.4__py3-none-any.whl → 1.3.5__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.
@@ -2,11 +2,12 @@
2
2
 
3
3
  import hashlib
4
4
  import os
5
+ import re
5
6
  from pathlib import Path
6
7
 
7
8
  import cv2
8
9
  import numpy as np
9
- from PyQt6.QtCore import QModelIndex, QPointF, QRectF, Qt, QThread, QTimer, pyqtSignal
10
+ from PyQt6.QtCore import QModelIndex, QPointF, QRectF, Qt, QTimer, pyqtSignal
10
11
  from PyQt6.QtGui import (
11
12
  QBrush,
12
13
  QColor,
@@ -53,530 +54,13 @@ from .numeric_table_widget_item import NumericTableWidgetItem
53
54
  from .photo_viewer import PhotoViewer
54
55
  from .right_panel import RightPanel
55
56
  from .widgets import StatusBar
56
-
57
-
58
- class SAMUpdateWorker(QThread):
59
- """Worker thread for updating SAM model in background."""
60
-
61
- finished = pyqtSignal()
62
- error = pyqtSignal(str)
63
-
64
- def __init__(
65
- self,
66
- model_manager,
67
- image_path,
68
- operate_on_view,
69
- current_image=None,
70
- parent=None,
71
- ):
72
- super().__init__(parent)
73
- self.model_manager = model_manager
74
- self.image_path = image_path
75
- self.operate_on_view = operate_on_view
76
- self.current_image = current_image # Numpy array of current modified image
77
- self._should_stop = False
78
- self.scale_factor = 1.0 # Track scaling factor for coordinate transformation
79
-
80
- def stop(self):
81
- """Request the worker to stop."""
82
- self._should_stop = True
83
-
84
- def get_scale_factor(self):
85
- """Get the scale factor used for image resizing."""
86
- return self.scale_factor
87
-
88
- def run(self):
89
- """Run SAM update in background thread."""
90
- try:
91
- if self._should_stop:
92
- return
93
-
94
- if self.operate_on_view and self.current_image is not None:
95
- # Use the provided modified image
96
- if self._should_stop:
97
- return
98
-
99
- # Optimize image size for faster SAM processing
100
- image = self.current_image
101
- original_height, original_width = image.shape[:2]
102
- max_size = 1024
103
-
104
- if original_height > max_size or original_width > max_size:
105
- # Calculate scaling factor
106
- self.scale_factor = min(
107
- max_size / original_width, max_size / original_height
108
- )
109
- new_width = int(original_width * self.scale_factor)
110
- new_height = int(original_height * self.scale_factor)
111
-
112
- # Resize using OpenCV for speed
113
- image = cv2.resize(
114
- image, (new_width, new_height), interpolation=cv2.INTER_AREA
115
- )
116
- else:
117
- self.scale_factor = 1.0
118
-
119
- if self._should_stop:
120
- return
121
-
122
- # Set image from numpy array (FIXED: use resized image, not original)
123
- self.model_manager.set_image_from_array(image)
124
- else:
125
- # Load original image
126
- pixmap = QPixmap(self.image_path)
127
- if pixmap.isNull():
128
- self.error.emit("Failed to load image")
129
- return
130
-
131
- if self._should_stop:
132
- return
133
-
134
- original_width = pixmap.width()
135
- original_height = pixmap.height()
136
-
137
- # Optimize image size for faster SAM processing
138
- max_size = 1024
139
- if original_width > max_size or original_height > max_size:
140
- # Calculate scaling factor
141
- self.scale_factor = min(
142
- max_size / original_width, max_size / original_height
143
- )
144
-
145
- # Scale down while maintaining aspect ratio
146
- scaled_pixmap = pixmap.scaled(
147
- max_size,
148
- max_size,
149
- Qt.AspectRatioMode.KeepAspectRatio,
150
- Qt.TransformationMode.SmoothTransformation,
151
- )
152
-
153
- # Convert to numpy array for SAM
154
- qimage = scaled_pixmap.toImage()
155
- width = qimage.width()
156
- height = qimage.height()
157
- ptr = qimage.bits()
158
- ptr.setsize(height * width * 4)
159
- arr = np.array(ptr).reshape(height, width, 4)
160
- # Convert RGBA to RGB
161
- image_array = arr[:, :, :3]
162
-
163
- if self._should_stop:
164
- return
165
-
166
- # FIXED: Use the resized image array, not original path
167
- self.model_manager.set_image_from_array(image_array)
168
- else:
169
- self.scale_factor = 1.0
170
- # For images that don't need resizing, use original path
171
- self.model_manager.set_image_from_path(self.image_path)
172
-
173
- if not self._should_stop:
174
- self.finished.emit()
175
-
176
- except Exception as e:
177
- if not self._should_stop:
178
- self.error.emit(str(e))
179
-
180
-
181
- class SingleViewSAMInitWorker(QThread):
182
- """Worker thread for initializing single-view SAM model in background."""
183
-
184
- model_initialized = pyqtSignal(object) # model_instance
185
- finished = pyqtSignal()
186
- error = pyqtSignal(str)
187
- progress = pyqtSignal(str) # status message
188
-
189
- def __init__(self, model_manager, default_model_type, custom_model_path=None):
190
- super().__init__()
191
- self.model_manager = model_manager
192
- self.default_model_type = default_model_type
193
- self.custom_model_path = custom_model_path
194
- self._should_stop = False
195
-
196
- def stop(self):
197
- """Stop the worker gracefully."""
198
- self._should_stop = True
199
-
200
- def run(self):
201
- """Initialize SAM model in background."""
202
- try:
203
- if self._should_stop:
204
- return
205
-
206
- if self.custom_model_path:
207
- # Load custom model
208
- model_name = os.path.basename(self.custom_model_path)
209
- self.progress.emit(f"Loading {model_name}...")
210
-
211
- success = self.model_manager.load_custom_model(self.custom_model_path)
212
- if not success:
213
- raise Exception(f"Failed to load custom model: {model_name}")
214
-
215
- sam_model = self.model_manager.sam_model
216
- else:
217
- # Initialize the default model
218
- self.progress.emit("Initializing AI model...")
219
- sam_model = self.model_manager.initialize_default_model(
220
- self.default_model_type
221
- )
222
-
223
- if self._should_stop:
224
- return
225
-
226
- if sam_model and sam_model.is_loaded:
227
- self.model_initialized.emit(sam_model)
228
- self.progress.emit("AI model initialized")
229
- else:
230
- self.error.emit("Model failed to load")
231
-
232
- except Exception as e:
233
- if not self._should_stop:
234
- self.error.emit(f"Failed to load AI model: {str(e)}")
235
-
236
-
237
- class MultiViewSAMInitWorker(QThread):
238
- """Worker thread for initializing multi-view SAM models in background."""
239
-
240
- model_initialized = pyqtSignal(int, object) # viewer_index, model_instance
241
- all_models_initialized = pyqtSignal(int) # total_models_count
242
- error = pyqtSignal(str)
243
- progress = pyqtSignal(int, int) # current, total
244
-
245
- def __init__(self, model_manager, parent=None):
246
- super().__init__(parent)
247
- self.model_manager = model_manager
248
- self._should_stop = False
249
- self.models_created = []
250
-
251
- def stop(self):
252
- """Request the worker to stop."""
253
- self._should_stop = True
254
-
255
- def run(self):
256
- """Initialize multi-view SAM models in background thread."""
257
- try:
258
- if self._should_stop:
259
- return
260
-
261
- # Import the required model classes
262
- from ..models.sam_model import SamModel
263
-
264
- try:
265
- from ..models.sam2_model import Sam2Model
266
-
267
- SAM2_AVAILABLE = True
268
- except ImportError:
269
- Sam2Model = None
270
- SAM2_AVAILABLE = False
271
-
272
- # Determine which type of model to create
273
- # Get the currently selected model from the GUI
274
- parent = self.parent()
275
- custom_model_path = None
276
- default_model_type = "vit_h" # fallback
277
-
278
- if parent and hasattr(parent, "control_panel"):
279
- # Get the selected model path from the model selection widget
280
- model_path = parent.control_panel.model_widget.get_selected_model_path()
281
- if model_path:
282
- # User has selected a custom model
283
- custom_model_path = model_path
284
- # Detect model type from filename
285
- default_model_type = self.model_manager.detect_model_type(
286
- model_path
287
- )
288
- else:
289
- # Using default model
290
- default_model_type = (
291
- parent.settings.default_model_type
292
- if hasattr(parent, "settings")
293
- else "vit_h"
294
- )
295
-
296
- is_sam2 = default_model_type.startswith("sam2")
297
-
298
- # Create model instances for all viewers - but optimize memory usage
299
- config = parent._get_multi_view_config()
300
- num_viewers = config["num_viewers"]
301
-
302
- # Warn about performance implications for VIT_H in multi-view
303
- if num_viewers > 2 and default_model_type == "vit_h":
304
- logger.warning(
305
- f"Using vit_h model with {num_viewers} viewers may cause performance issues. Consider using vit_b for better performance."
306
- )
307
- for i in range(num_viewers):
308
- if self._should_stop:
309
- return
310
-
311
- self.progress.emit(i + 1, num_viewers)
312
-
313
- try:
314
- if is_sam2 and SAM2_AVAILABLE:
315
- # Create SAM2 model instance
316
- if custom_model_path:
317
- model_instance = Sam2Model(custom_model_path)
318
- else:
319
- model_instance = Sam2Model(model_type=default_model_type)
320
- else:
321
- # Create SAM1 model instance
322
- if custom_model_path:
323
- model_instance = SamModel(
324
- model_type=default_model_type,
325
- custom_model_path=custom_model_path,
326
- )
327
- else:
328
- model_instance = SamModel(model_type=default_model_type)
329
-
330
- if self._should_stop:
331
- return
332
-
333
- if model_instance and getattr(model_instance, "is_loaded", False):
334
- self.models_created.append(model_instance)
335
- self.model_initialized.emit(i, model_instance)
336
-
337
- # Synchronize and clear GPU cache after each model for stability
338
- try:
339
- import torch
340
-
341
- if torch.cuda.is_available():
342
- torch.cuda.synchronize() # Ensure model is fully loaded
343
- torch.cuda.empty_cache()
344
- except ImportError:
345
- pass # PyTorch not available
346
- else:
347
- raise Exception(f"Model instance {i + 1} failed to load")
348
-
349
- except Exception as model_error:
350
- logger.error(
351
- f"Error creating model instance {i + 1}: {model_error}"
352
- )
353
- if not self._should_stop:
354
- self.error.emit(
355
- f"Failed to create model instance {i + 1}: {model_error}"
356
- )
357
- return
358
-
359
- if not self._should_stop:
360
- self.all_models_initialized.emit(len(self.models_created))
361
-
362
- except Exception as e:
363
- if not self._should_stop:
364
- self.error.emit(str(e))
365
-
366
-
367
- class ImageDiscoveryWorker(QThread):
368
- """Worker thread for discovering all image file paths in background."""
369
-
370
- images_discovered = pyqtSignal(list) # List of all image file paths
371
- progress = pyqtSignal(str) # Progress message
372
- error = pyqtSignal(str)
373
-
374
- def __init__(self, file_model, file_manager, parent=None):
375
- super().__init__(parent)
376
- self.file_model = file_model
377
- self.file_manager = file_manager
378
- self._should_stop = False
379
-
380
- def stop(self):
381
- """Request the worker to stop."""
382
- self._should_stop = True
383
-
384
- def run(self):
385
- """Discover all image file paths in background."""
386
- try:
387
- if self._should_stop:
388
- return
389
-
390
- self.progress.emit("Scanning for images...")
391
-
392
- if (
393
- not hasattr(self.file_model, "rootPath")
394
- or not self.file_model.rootPath()
395
- ):
396
- self.images_discovered.emit([])
397
- return
398
-
399
- all_image_paths = []
400
- root_index = self.file_model.index(self.file_model.rootPath())
401
-
402
- def scan_directory(parent_index):
403
- if self._should_stop:
404
- return
405
-
406
- for row in range(self.file_model.rowCount(parent_index)):
407
- if self._should_stop:
408
- return
409
-
410
- index = self.file_model.index(row, 0, parent_index)
411
- if self.file_model.isDir(index):
412
- scan_directory(index) # Recursively scan subdirectories
413
- else:
414
- path = self.file_model.filePath(index)
415
- if self.file_manager.is_image_file(path):
416
- # Simply add all image file paths without checking for NPZ
417
- all_image_paths.append(path)
418
-
419
- scan_directory(root_index)
420
-
421
- if not self._should_stop:
422
- self.progress.emit(f"Found {len(all_image_paths)} images")
423
- self.images_discovered.emit(sorted(all_image_paths))
424
-
425
- except Exception as e:
426
- if not self._should_stop:
427
- self.error.emit(f"Error discovering images: {str(e)}")
428
-
429
-
430
- class MultiViewSAMUpdateWorker(QThread):
431
- """Worker thread for updating SAM model image in multi-view mode."""
432
-
433
- finished = pyqtSignal(int) # viewer_index
434
- error = pyqtSignal(int, str) # viewer_index, error_message
435
-
436
- def __init__(
437
- self,
438
- viewer_index,
439
- model,
440
- image_path,
441
- operate_on_view=False,
442
- current_image=None,
443
- parent=None,
444
- ):
445
- super().__init__(parent)
446
- self.viewer_index = viewer_index
447
- self.model = model
448
- self.image_path = image_path
449
- self.operate_on_view = operate_on_view
450
- self.current_image = current_image
451
- self._should_stop = False
452
- self.scale_factor = 1.0
453
-
454
- def stop(self):
455
- """Request the worker to stop."""
456
- self._should_stop = True
457
-
458
- def get_scale_factor(self):
459
- """Get the scale factor used for image resizing."""
460
- return self.scale_factor
461
-
462
- def run(self):
463
- """Update SAM model image in background thread."""
464
- try:
465
- if self._should_stop:
466
- return
467
-
468
- # Clear GPU cache to reduce memory pressure in multi-view mode
469
- try:
470
- import torch
471
-
472
- if torch.cuda.is_available():
473
- torch.cuda.empty_cache()
474
- except ImportError:
475
- pass # PyTorch not available
476
-
477
- if self.operate_on_view and self.current_image is not None:
478
- # Use the provided modified image
479
- if self._should_stop:
480
- return
481
-
482
- # Optimize image size for faster SAM processing
483
- image = self.current_image
484
- original_height, original_width = image.shape[:2]
485
- max_size = 1024
486
-
487
- if original_height > max_size or original_width > max_size:
488
- # Calculate scaling factor
489
- self.scale_factor = min(
490
- max_size / original_width, max_size / original_height
491
- )
492
- new_width = int(original_width * self.scale_factor)
493
- new_height = int(original_height * self.scale_factor)
494
-
495
- # Resize using OpenCV for speed
496
- image = cv2.resize(
497
- image, (new_width, new_height), interpolation=cv2.INTER_AREA
498
- )
499
- else:
500
- self.scale_factor = 1.0
501
-
502
- if self._should_stop:
503
- return
504
-
505
- # Set image from numpy array
506
- self.model.set_image_from_array(image)
507
- else:
508
- # Load original image
509
- if self._should_stop:
510
- return
511
-
512
- # Optimize image size for faster SAM processing
513
- pixmap = QPixmap(self.image_path)
514
- if pixmap.isNull():
515
- if not self._should_stop:
516
- self.error.emit(self.viewer_index, "Failed to load image")
517
- return
518
-
519
- original_width = pixmap.width()
520
- original_height = pixmap.height()
521
- max_size = 1024
522
-
523
- if original_width > max_size or original_height > max_size:
524
- # Calculate scaling factor
525
- self.scale_factor = min(
526
- max_size / original_width, max_size / original_height
527
- )
528
-
529
- # Scale down while maintaining aspect ratio
530
- scaled_pixmap = pixmap.scaled(
531
- max_size,
532
- max_size,
533
- Qt.AspectRatioMode.KeepAspectRatio,
534
- Qt.TransformationMode.SmoothTransformation,
535
- )
536
-
537
- # Convert to numpy array for SAM
538
- qimage = scaled_pixmap.toImage()
539
- width = qimage.width()
540
- height = qimage.height()
541
- ptr = qimage.bits()
542
- ptr.setsize(height * width * 4)
543
- arr = np.array(ptr).reshape(height, width, 4)
544
- # Convert RGBA to RGB
545
- image_array = arr[:, :, :3]
546
-
547
- if self._should_stop:
548
- return
549
-
550
- # Add CUDA synchronization for multi-model scenarios
551
- try:
552
- import torch
553
-
554
- if torch.cuda.is_available():
555
- torch.cuda.synchronize()
556
- except ImportError:
557
- pass
558
-
559
- self.model.set_image_from_array(image_array)
560
- else:
561
- self.scale_factor = 1.0
562
-
563
- # Add CUDA synchronization for multi-model scenarios
564
- try:
565
- import torch
566
-
567
- if torch.cuda.is_available():
568
- torch.cuda.synchronize()
569
- except ImportError:
570
- pass
571
-
572
- self.model.set_image_from_path(self.image_path)
573
-
574
- if not self._should_stop:
575
- self.finished.emit(self.viewer_index)
576
-
577
- except Exception as e:
578
- if not self._should_stop:
579
- self.error.emit(self.viewer_index, str(e))
57
+ from .workers import (
58
+ ImageDiscoveryWorker,
59
+ MultiViewSAMInitWorker,
60
+ MultiViewSAMUpdateWorker,
61
+ SAMUpdateWorker,
62
+ SingleViewSAMInitWorker,
63
+ )
580
64
 
581
65
 
582
66
  class PanelPopoutWindow(QDialog):
@@ -778,9 +262,28 @@ class MainWindow(QMainWindow):
778
262
  self._setup_shortcuts()
779
263
  self._load_settings()
780
264
 
265
+ def _get_version(self) -> str:
266
+ """Get version from pyproject.toml."""
267
+ try:
268
+ # Get the project root directory (3 levels up from main_window.py)
269
+ project_root = Path(__file__).parent.parent.parent.parent
270
+ pyproject_path = project_root / "pyproject.toml"
271
+
272
+ if pyproject_path.exists():
273
+ with open(pyproject_path, encoding="utf-8") as f:
274
+ content = f.read()
275
+ # Use regex to find version line
276
+ match = re.search(r'version\s*=\s*"([^"]+)"', content)
277
+ if match:
278
+ return match.group(1)
279
+ except Exception:
280
+ pass
281
+ return "unknown"
282
+
781
283
  def _setup_ui(self):
782
284
  """Setup the main user interface."""
783
- self.setWindowTitle("LazyLabel by DNC")
285
+ version = self._get_version()
286
+ self.setWindowTitle(f"LazyLabel by DNC (version {version})")
784
287
  self.setGeometry(
785
288
  50, 50, self.settings.window_width, self.settings.window_height
786
289
  )
@@ -1510,7 +1013,7 @@ class MainWindow(QMainWindow):
1510
1013
  if not model_text or model_text == "Default (vit_h)":
1511
1014
  # Clear any pending custom model and use default
1512
1015
  self.pending_custom_model_path = None
1513
- self.control_panel.set_current_model("Current: Default SAM Model")
1016
+ self.control_panel.set_current_model("Selected: Default SAM Model")
1514
1017
  # Clear existing model to free memory until needed
1515
1018
  self._reset_sam_state_for_model_switch()
1516
1019
  return
@@ -1703,7 +1206,17 @@ class MainWindow(QMainWindow):
1703
1206
  self._save_output_to_npz()
1704
1207
 
1705
1208
  self.current_image_path = path
1706
- pixmap = QPixmap(self.current_image_path)
1209
+ # Load image with explicit transparency support
1210
+ qimage = QImage(self.current_image_path)
1211
+ if qimage.isNull():
1212
+ return
1213
+ # For PNG files, always ensure proper alpha format handling
1214
+ if self.current_image_path.lower().endswith(".png"):
1215
+ # PNG files can have alpha channels, use ARGB32_Premultiplied for proper handling
1216
+ qimage = qimage.convertToFormat(
1217
+ QImage.Format.Format_ARGB32_Premultiplied
1218
+ )
1219
+ pixmap = QPixmap.fromImage(qimage)
1707
1220
  if not pixmap.isNull():
1708
1221
  self._reset_state()
1709
1222
  self.viewer.set_photo(pixmap)
@@ -1757,6 +1270,11 @@ class MainWindow(QMainWindow):
1757
1270
  ):
1758
1271
  self._save_output_to_npz()
1759
1272
 
1273
+ self.current_image_path = path
1274
+
1275
+ # CRITICAL: Reset state and mark SAM as dirty when loading new image
1276
+ self._reset_state()
1277
+
1760
1278
  self.segment_manager.clear()
1761
1279
  # Remove all scene items except the pixmap
1762
1280
  items_to_remove = [
@@ -1766,7 +1284,6 @@ class MainWindow(QMainWindow):
1766
1284
  ]
1767
1285
  for item in items_to_remove:
1768
1286
  self.viewer.scene().removeItem(item)
1769
- self.current_image_path = path
1770
1287
 
1771
1288
  # Load the image
1772
1289
  original_image = cv2.imread(path)
@@ -1818,6 +1335,9 @@ class MainWindow(QMainWindow):
1818
1335
  # Update file selection in the file manager
1819
1336
  self.right_panel.select_file(Path(path))
1820
1337
 
1338
+ # CRITICAL: Update SAM model with new image
1339
+ self._update_sam_model_image()
1340
+
1821
1341
  # Update threshold widgets for new image (this was missing!)
1822
1342
  self._update_channel_threshold_for_image(pixmap)
1823
1343
 
@@ -1944,7 +1464,21 @@ class MainWindow(QMainWindow):
1944
1464
  image_path = self.multi_view_images[i]
1945
1465
 
1946
1466
  if image_path:
1947
- pixmap = QPixmap(image_path)
1467
+ # Load image with explicit transparency support
1468
+ qimage = QImage(image_path)
1469
+ if qimage.isNull():
1470
+ self.multi_view_viewers[i].set_photo(QPixmap())
1471
+ self.multi_view_info_labels[i].setText(
1472
+ f"Image {i + 1}: Failed to load"
1473
+ )
1474
+ continue
1475
+ # For PNG files, always ensure proper alpha format handling
1476
+ if image_path.lower().endswith(".png"):
1477
+ # PNG files can have alpha channels, use ARGB32_Premultiplied for proper handling
1478
+ qimage = qimage.convertToFormat(
1479
+ QImage.Format.Format_ARGB32_Premultiplied
1480
+ )
1481
+ pixmap = QPixmap.fromImage(qimage)
1948
1482
  if not pixmap.isNull():
1949
1483
  self.multi_view_viewers[i].set_photo(pixmap)
1950
1484
  # Apply current image adjustments to the newly loaded image
@@ -2033,6 +1567,21 @@ class MainWindow(QMainWindow):
2033
1567
  self.action_history.clear()
2034
1568
  self.redo_history.clear()
2035
1569
 
1570
+ # Reset SAM model state - force reload for new images (same as single view)
1571
+ self.current_sam_hash = None # Invalidate SAM cache
1572
+ self.sam_is_dirty = True # Mark SAM as needing update
1573
+
1574
+ # Clear cached image data to prevent using previous images
1575
+ self._cached_original_image = None
1576
+ if hasattr(self, "_cached_multi_view_original_images"):
1577
+ self._cached_multi_view_original_images = None
1578
+
1579
+ # Clear SAM embedding cache to ensure fresh processing
1580
+ self.sam_embedding_cache.clear()
1581
+
1582
+ # Reset AI mode state
1583
+ self.ai_click_start_pos = None
1584
+
2036
1585
  # Update UI lists to reflect cleared state
2037
1586
  self._update_all_lists()
2038
1587
 
@@ -2297,9 +1846,18 @@ class MainWindow(QMainWindow):
2297
1846
  # Convert from BGRA to RGB for SAM
2298
1847
  image_rgb = cv2.cvtColor(image_np, cv2.COLOR_BGRA2RGB)
2299
1848
  self.model_manager.sam_model.set_image_from_array(image_rgb)
1849
+ # Update hash to prevent worker thread from re-updating
1850
+ image_hash = self._get_image_hash(image_rgb)
1851
+ self.current_sam_hash = image_hash
2300
1852
  else:
2301
1853
  # Pass the original image path to SAM model
2302
1854
  self.model_manager.sam_model.set_image_from_path(self.current_image_path)
1855
+ # Update hash to prevent worker thread from re-updating
1856
+ image_hash = hashlib.md5(self.current_image_path.encode()).hexdigest()
1857
+ self.current_sam_hash = image_hash
1858
+
1859
+ # Mark SAM as clean since we just updated it
1860
+ self.sam_is_dirty = False
2303
1861
 
2304
1862
  def _load_next_image(self):
2305
1863
  """Load next image in the file list."""
@@ -4236,6 +3794,18 @@ class MainWindow(QMainWindow):
4236
3794
  self.crop_start_pos = None
4237
3795
  self.current_crop_coords = None
4238
3796
 
3797
+ # Reset SAM model state - force reload for new image
3798
+ self.current_sam_hash = None # Invalidate SAM cache
3799
+ self.sam_is_dirty = True # Mark SAM as needing update
3800
+
3801
+ # Clear cached image data to prevent using previous image
3802
+ self._cached_original_image = None
3803
+ if hasattr(self, "_cached_multi_view_original_images"):
3804
+ self._cached_multi_view_original_images = None
3805
+
3806
+ # Clear SAM embedding cache to ensure fresh processing
3807
+ self.sam_embedding_cache.clear()
3808
+
4239
3809
  # Reset AI mode state
4240
3810
  self.ai_click_start_pos = None
4241
3811
  self.ai_click_time = 0