lazylabel-gui 1.3.4__py3-none-any.whl → 1.3.6__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/models/sam2_model.py +253 -134
- lazylabel/ui/main_window.py +100 -530
- lazylabel/ui/photo_viewer.py +35 -11
- lazylabel/ui/widgets/channel_threshold_widget.py +18 -4
- lazylabel/ui/workers/__init__.py +15 -0
- lazylabel/ui/workers/image_discovery_worker.py +66 -0
- lazylabel/ui/workers/multi_view_sam_init_worker.py +135 -0
- lazylabel/ui/workers/multi_view_sam_update_worker.py +158 -0
- lazylabel/ui/workers/sam_update_worker.py +129 -0
- lazylabel/ui/workers/single_view_sam_init_worker.py +61 -0
- {lazylabel_gui-1.3.4.dist-info → lazylabel_gui-1.3.6.dist-info}/METADATA +52 -49
- {lazylabel_gui-1.3.4.dist-info → lazylabel_gui-1.3.6.dist-info}/RECORD +16 -10
- {lazylabel_gui-1.3.4.dist-info → lazylabel_gui-1.3.6.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.3.4.dist-info → lazylabel_gui-1.3.6.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.3.4.dist-info → lazylabel_gui-1.3.6.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.3.4.dist-info → lazylabel_gui-1.3.6.dist-info}/top_level.txt +0 -0
lazylabel/ui/main_window.py
CHANGED
@@ -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,
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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.
|
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("
|
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
|
-
|
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
|
-
|
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
|