lazylabel-gui 1.3.3__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.
- lazylabel/core/file_manager.py +1 -1
- lazylabel/models/sam2_model.py +253 -134
- lazylabel/ui/control_panel.py +7 -2
- lazylabel/ui/main_window.py +264 -593
- lazylabel/ui/photo_viewer.py +35 -11
- lazylabel/ui/widgets/channel_threshold_widget.py +8 -9
- lazylabel/ui/widgets/fft_threshold_widget.py +4 -0
- lazylabel/ui/widgets/model_selection_widget.py +9 -0
- 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/utils/fast_file_manager.py +422 -78
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.5.dist-info}/METADATA +1 -1
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.5.dist-info}/RECORD +21 -15
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.5.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.5.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.5.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.3.3.dist-info → lazylabel_gui-1.3.5.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,
|
@@ -19,12 +20,14 @@ from PyQt6.QtGui import (
|
|
19
20
|
QShortcut,
|
20
21
|
)
|
21
22
|
from PyQt6.QtWidgets import (
|
23
|
+
QApplication,
|
22
24
|
QDialog,
|
23
25
|
QFileDialog,
|
24
26
|
QGraphicsEllipseItem,
|
25
27
|
QGraphicsLineItem,
|
26
28
|
QGraphicsPolygonItem,
|
27
29
|
QGraphicsRectItem,
|
30
|
+
QGridLayout,
|
28
31
|
QHBoxLayout,
|
29
32
|
QLabel,
|
30
33
|
QMainWindow,
|
@@ -51,530 +54,13 @@ from .numeric_table_widget_item import NumericTableWidgetItem
|
|
51
54
|
from .photo_viewer import PhotoViewer
|
52
55
|
from .right_panel import RightPanel
|
53
56
|
from .widgets import StatusBar
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
def __init__(
|
63
|
-
self,
|
64
|
-
model_manager,
|
65
|
-
image_path,
|
66
|
-
operate_on_view,
|
67
|
-
current_image=None,
|
68
|
-
parent=None,
|
69
|
-
):
|
70
|
-
super().__init__(parent)
|
71
|
-
self.model_manager = model_manager
|
72
|
-
self.image_path = image_path
|
73
|
-
self.operate_on_view = operate_on_view
|
74
|
-
self.current_image = current_image # Numpy array of current modified image
|
75
|
-
self._should_stop = False
|
76
|
-
self.scale_factor = 1.0 # Track scaling factor for coordinate transformation
|
77
|
-
|
78
|
-
def stop(self):
|
79
|
-
"""Request the worker to stop."""
|
80
|
-
self._should_stop = True
|
81
|
-
|
82
|
-
def get_scale_factor(self):
|
83
|
-
"""Get the scale factor used for image resizing."""
|
84
|
-
return self.scale_factor
|
85
|
-
|
86
|
-
def run(self):
|
87
|
-
"""Run SAM update in background thread."""
|
88
|
-
try:
|
89
|
-
if self._should_stop:
|
90
|
-
return
|
91
|
-
|
92
|
-
if self.operate_on_view and self.current_image is not None:
|
93
|
-
# Use the provided modified image
|
94
|
-
if self._should_stop:
|
95
|
-
return
|
96
|
-
|
97
|
-
# Optimize image size for faster SAM processing
|
98
|
-
image = self.current_image
|
99
|
-
original_height, original_width = image.shape[:2]
|
100
|
-
max_size = 1024
|
101
|
-
|
102
|
-
if original_height > max_size or original_width > max_size:
|
103
|
-
# Calculate scaling factor
|
104
|
-
self.scale_factor = min(
|
105
|
-
max_size / original_width, max_size / original_height
|
106
|
-
)
|
107
|
-
new_width = int(original_width * self.scale_factor)
|
108
|
-
new_height = int(original_height * self.scale_factor)
|
109
|
-
|
110
|
-
# Resize using OpenCV for speed
|
111
|
-
image = cv2.resize(
|
112
|
-
image, (new_width, new_height), interpolation=cv2.INTER_AREA
|
113
|
-
)
|
114
|
-
else:
|
115
|
-
self.scale_factor = 1.0
|
116
|
-
|
117
|
-
if self._should_stop:
|
118
|
-
return
|
119
|
-
|
120
|
-
# Set image from numpy array (FIXED: use resized image, not original)
|
121
|
-
self.model_manager.set_image_from_array(image)
|
122
|
-
else:
|
123
|
-
# Load original image
|
124
|
-
pixmap = QPixmap(self.image_path)
|
125
|
-
if pixmap.isNull():
|
126
|
-
self.error.emit("Failed to load image")
|
127
|
-
return
|
128
|
-
|
129
|
-
if self._should_stop:
|
130
|
-
return
|
131
|
-
|
132
|
-
original_width = pixmap.width()
|
133
|
-
original_height = pixmap.height()
|
134
|
-
|
135
|
-
# Optimize image size for faster SAM processing
|
136
|
-
max_size = 1024
|
137
|
-
if original_width > max_size or original_height > max_size:
|
138
|
-
# Calculate scaling factor
|
139
|
-
self.scale_factor = min(
|
140
|
-
max_size / original_width, max_size / original_height
|
141
|
-
)
|
142
|
-
|
143
|
-
# Scale down while maintaining aspect ratio
|
144
|
-
scaled_pixmap = pixmap.scaled(
|
145
|
-
max_size,
|
146
|
-
max_size,
|
147
|
-
Qt.AspectRatioMode.KeepAspectRatio,
|
148
|
-
Qt.TransformationMode.SmoothTransformation,
|
149
|
-
)
|
150
|
-
|
151
|
-
# Convert to numpy array for SAM
|
152
|
-
qimage = scaled_pixmap.toImage()
|
153
|
-
width = qimage.width()
|
154
|
-
height = qimage.height()
|
155
|
-
ptr = qimage.bits()
|
156
|
-
ptr.setsize(height * width * 4)
|
157
|
-
arr = np.array(ptr).reshape(height, width, 4)
|
158
|
-
# Convert RGBA to RGB
|
159
|
-
image_array = arr[:, :, :3]
|
160
|
-
|
161
|
-
if self._should_stop:
|
162
|
-
return
|
163
|
-
|
164
|
-
# FIXED: Use the resized image array, not original path
|
165
|
-
self.model_manager.set_image_from_array(image_array)
|
166
|
-
else:
|
167
|
-
self.scale_factor = 1.0
|
168
|
-
# For images that don't need resizing, use original path
|
169
|
-
self.model_manager.set_image_from_path(self.image_path)
|
170
|
-
|
171
|
-
if not self._should_stop:
|
172
|
-
self.finished.emit()
|
173
|
-
|
174
|
-
except Exception as e:
|
175
|
-
if not self._should_stop:
|
176
|
-
self.error.emit(str(e))
|
177
|
-
|
178
|
-
|
179
|
-
class SingleViewSAMInitWorker(QThread):
|
180
|
-
"""Worker thread for initializing single-view SAM model in background."""
|
181
|
-
|
182
|
-
model_initialized = pyqtSignal(object) # model_instance
|
183
|
-
finished = pyqtSignal()
|
184
|
-
error = pyqtSignal(str)
|
185
|
-
progress = pyqtSignal(str) # status message
|
186
|
-
|
187
|
-
def __init__(self, model_manager, default_model_type, custom_model_path=None):
|
188
|
-
super().__init__()
|
189
|
-
self.model_manager = model_manager
|
190
|
-
self.default_model_type = default_model_type
|
191
|
-
self.custom_model_path = custom_model_path
|
192
|
-
self._should_stop = False
|
193
|
-
|
194
|
-
def stop(self):
|
195
|
-
"""Stop the worker gracefully."""
|
196
|
-
self._should_stop = True
|
197
|
-
|
198
|
-
def run(self):
|
199
|
-
"""Initialize SAM model in background."""
|
200
|
-
try:
|
201
|
-
if self._should_stop:
|
202
|
-
return
|
203
|
-
|
204
|
-
if self.custom_model_path:
|
205
|
-
# Load custom model
|
206
|
-
model_name = os.path.basename(self.custom_model_path)
|
207
|
-
self.progress.emit(f"Loading {model_name}...")
|
208
|
-
|
209
|
-
success = self.model_manager.load_custom_model(self.custom_model_path)
|
210
|
-
if not success:
|
211
|
-
raise Exception(f"Failed to load custom model: {model_name}")
|
212
|
-
|
213
|
-
sam_model = self.model_manager.sam_model
|
214
|
-
else:
|
215
|
-
# Initialize the default model
|
216
|
-
self.progress.emit("Initializing AI model...")
|
217
|
-
sam_model = self.model_manager.initialize_default_model(
|
218
|
-
self.default_model_type
|
219
|
-
)
|
220
|
-
|
221
|
-
if self._should_stop:
|
222
|
-
return
|
223
|
-
|
224
|
-
if sam_model and sam_model.is_loaded:
|
225
|
-
self.model_initialized.emit(sam_model)
|
226
|
-
self.progress.emit("AI model initialized")
|
227
|
-
else:
|
228
|
-
self.error.emit("Model failed to load")
|
229
|
-
|
230
|
-
except Exception as e:
|
231
|
-
if not self._should_stop:
|
232
|
-
self.error.emit(f"Failed to load AI model: {str(e)}")
|
233
|
-
|
234
|
-
|
235
|
-
class MultiViewSAMInitWorker(QThread):
|
236
|
-
"""Worker thread for initializing multi-view SAM models in background."""
|
237
|
-
|
238
|
-
model_initialized = pyqtSignal(int, object) # viewer_index, model_instance
|
239
|
-
all_models_initialized = pyqtSignal(int) # total_models_count
|
240
|
-
error = pyqtSignal(str)
|
241
|
-
progress = pyqtSignal(int, int) # current, total
|
242
|
-
|
243
|
-
def __init__(self, model_manager, parent=None):
|
244
|
-
super().__init__(parent)
|
245
|
-
self.model_manager = model_manager
|
246
|
-
self._should_stop = False
|
247
|
-
self.models_created = []
|
248
|
-
|
249
|
-
def stop(self):
|
250
|
-
"""Request the worker to stop."""
|
251
|
-
self._should_stop = True
|
252
|
-
|
253
|
-
def run(self):
|
254
|
-
"""Initialize multi-view SAM models in background thread."""
|
255
|
-
try:
|
256
|
-
if self._should_stop:
|
257
|
-
return
|
258
|
-
|
259
|
-
# Import the required model classes
|
260
|
-
from ..models.sam_model import SamModel
|
261
|
-
|
262
|
-
try:
|
263
|
-
from ..models.sam2_model import Sam2Model
|
264
|
-
|
265
|
-
SAM2_AVAILABLE = True
|
266
|
-
except ImportError:
|
267
|
-
Sam2Model = None
|
268
|
-
SAM2_AVAILABLE = False
|
269
|
-
|
270
|
-
# Determine which type of model to create
|
271
|
-
# Get the currently selected model from the GUI
|
272
|
-
parent = self.parent()
|
273
|
-
custom_model_path = None
|
274
|
-
default_model_type = "vit_h" # fallback
|
275
|
-
|
276
|
-
if parent and hasattr(parent, "control_panel"):
|
277
|
-
# Get the selected model path from the model selection widget
|
278
|
-
model_path = parent.control_panel.model_widget.get_selected_model_path()
|
279
|
-
if model_path:
|
280
|
-
# User has selected a custom model
|
281
|
-
custom_model_path = model_path
|
282
|
-
# Detect model type from filename
|
283
|
-
default_model_type = self.model_manager.detect_model_type(
|
284
|
-
model_path
|
285
|
-
)
|
286
|
-
else:
|
287
|
-
# Using default model
|
288
|
-
default_model_type = (
|
289
|
-
parent.settings.default_model_type
|
290
|
-
if hasattr(parent, "settings")
|
291
|
-
else "vit_h"
|
292
|
-
)
|
293
|
-
|
294
|
-
is_sam2 = default_model_type.startswith("sam2")
|
295
|
-
|
296
|
-
# Create model instances for all viewers - but optimize memory usage
|
297
|
-
config = parent._get_multi_view_config()
|
298
|
-
num_viewers = config["num_viewers"]
|
299
|
-
|
300
|
-
# Warn about performance implications for VIT_H in multi-view
|
301
|
-
if num_viewers > 2 and default_model_type == "vit_h":
|
302
|
-
logger.warning(
|
303
|
-
f"Using vit_h model with {num_viewers} viewers may cause performance issues. Consider using vit_b for better performance."
|
304
|
-
)
|
305
|
-
for i in range(num_viewers):
|
306
|
-
if self._should_stop:
|
307
|
-
return
|
308
|
-
|
309
|
-
self.progress.emit(i + 1, num_viewers)
|
310
|
-
|
311
|
-
try:
|
312
|
-
if is_sam2 and SAM2_AVAILABLE:
|
313
|
-
# Create SAM2 model instance
|
314
|
-
if custom_model_path:
|
315
|
-
model_instance = Sam2Model(custom_model_path)
|
316
|
-
else:
|
317
|
-
model_instance = Sam2Model(model_type=default_model_type)
|
318
|
-
else:
|
319
|
-
# Create SAM1 model instance
|
320
|
-
if custom_model_path:
|
321
|
-
model_instance = SamModel(
|
322
|
-
model_type=default_model_type,
|
323
|
-
custom_model_path=custom_model_path,
|
324
|
-
)
|
325
|
-
else:
|
326
|
-
model_instance = SamModel(model_type=default_model_type)
|
327
|
-
|
328
|
-
if self._should_stop:
|
329
|
-
return
|
330
|
-
|
331
|
-
if model_instance and getattr(model_instance, "is_loaded", False):
|
332
|
-
self.models_created.append(model_instance)
|
333
|
-
self.model_initialized.emit(i, model_instance)
|
334
|
-
|
335
|
-
# Synchronize and clear GPU cache after each model for stability
|
336
|
-
try:
|
337
|
-
import torch
|
338
|
-
|
339
|
-
if torch.cuda.is_available():
|
340
|
-
torch.cuda.synchronize() # Ensure model is fully loaded
|
341
|
-
torch.cuda.empty_cache()
|
342
|
-
except ImportError:
|
343
|
-
pass # PyTorch not available
|
344
|
-
else:
|
345
|
-
raise Exception(f"Model instance {i + 1} failed to load")
|
346
|
-
|
347
|
-
except Exception as model_error:
|
348
|
-
logger.error(
|
349
|
-
f"Error creating model instance {i + 1}: {model_error}"
|
350
|
-
)
|
351
|
-
if not self._should_stop:
|
352
|
-
self.error.emit(
|
353
|
-
f"Failed to create model instance {i + 1}: {model_error}"
|
354
|
-
)
|
355
|
-
return
|
356
|
-
|
357
|
-
if not self._should_stop:
|
358
|
-
self.all_models_initialized.emit(len(self.models_created))
|
359
|
-
|
360
|
-
except Exception as e:
|
361
|
-
if not self._should_stop:
|
362
|
-
self.error.emit(str(e))
|
363
|
-
|
364
|
-
|
365
|
-
class ImageDiscoveryWorker(QThread):
|
366
|
-
"""Worker thread for discovering all image file paths in background."""
|
367
|
-
|
368
|
-
images_discovered = pyqtSignal(list) # List of all image file paths
|
369
|
-
progress = pyqtSignal(str) # Progress message
|
370
|
-
error = pyqtSignal(str)
|
371
|
-
|
372
|
-
def __init__(self, file_model, file_manager, parent=None):
|
373
|
-
super().__init__(parent)
|
374
|
-
self.file_model = file_model
|
375
|
-
self.file_manager = file_manager
|
376
|
-
self._should_stop = False
|
377
|
-
|
378
|
-
def stop(self):
|
379
|
-
"""Request the worker to stop."""
|
380
|
-
self._should_stop = True
|
381
|
-
|
382
|
-
def run(self):
|
383
|
-
"""Discover all image file paths in background."""
|
384
|
-
try:
|
385
|
-
if self._should_stop:
|
386
|
-
return
|
387
|
-
|
388
|
-
self.progress.emit("Scanning for images...")
|
389
|
-
|
390
|
-
if (
|
391
|
-
not hasattr(self.file_model, "rootPath")
|
392
|
-
or not self.file_model.rootPath()
|
393
|
-
):
|
394
|
-
self.images_discovered.emit([])
|
395
|
-
return
|
396
|
-
|
397
|
-
all_image_paths = []
|
398
|
-
root_index = self.file_model.index(self.file_model.rootPath())
|
399
|
-
|
400
|
-
def scan_directory(parent_index):
|
401
|
-
if self._should_stop:
|
402
|
-
return
|
403
|
-
|
404
|
-
for row in range(self.file_model.rowCount(parent_index)):
|
405
|
-
if self._should_stop:
|
406
|
-
return
|
407
|
-
|
408
|
-
index = self.file_model.index(row, 0, parent_index)
|
409
|
-
if self.file_model.isDir(index):
|
410
|
-
scan_directory(index) # Recursively scan subdirectories
|
411
|
-
else:
|
412
|
-
path = self.file_model.filePath(index)
|
413
|
-
if self.file_manager.is_image_file(path):
|
414
|
-
# Simply add all image file paths without checking for NPZ
|
415
|
-
all_image_paths.append(path)
|
416
|
-
|
417
|
-
scan_directory(root_index)
|
418
|
-
|
419
|
-
if not self._should_stop:
|
420
|
-
self.progress.emit(f"Found {len(all_image_paths)} images")
|
421
|
-
self.images_discovered.emit(sorted(all_image_paths))
|
422
|
-
|
423
|
-
except Exception as e:
|
424
|
-
if not self._should_stop:
|
425
|
-
self.error.emit(f"Error discovering images: {str(e)}")
|
426
|
-
|
427
|
-
|
428
|
-
class MultiViewSAMUpdateWorker(QThread):
|
429
|
-
"""Worker thread for updating SAM model image in multi-view mode."""
|
430
|
-
|
431
|
-
finished = pyqtSignal(int) # viewer_index
|
432
|
-
error = pyqtSignal(int, str) # viewer_index, error_message
|
433
|
-
|
434
|
-
def __init__(
|
435
|
-
self,
|
436
|
-
viewer_index,
|
437
|
-
model,
|
438
|
-
image_path,
|
439
|
-
operate_on_view=False,
|
440
|
-
current_image=None,
|
441
|
-
parent=None,
|
442
|
-
):
|
443
|
-
super().__init__(parent)
|
444
|
-
self.viewer_index = viewer_index
|
445
|
-
self.model = model
|
446
|
-
self.image_path = image_path
|
447
|
-
self.operate_on_view = operate_on_view
|
448
|
-
self.current_image = current_image
|
449
|
-
self._should_stop = False
|
450
|
-
self.scale_factor = 1.0
|
451
|
-
|
452
|
-
def stop(self):
|
453
|
-
"""Request the worker to stop."""
|
454
|
-
self._should_stop = True
|
455
|
-
|
456
|
-
def get_scale_factor(self):
|
457
|
-
"""Get the scale factor used for image resizing."""
|
458
|
-
return self.scale_factor
|
459
|
-
|
460
|
-
def run(self):
|
461
|
-
"""Update SAM model image in background thread."""
|
462
|
-
try:
|
463
|
-
if self._should_stop:
|
464
|
-
return
|
465
|
-
|
466
|
-
# Clear GPU cache to reduce memory pressure in multi-view mode
|
467
|
-
try:
|
468
|
-
import torch
|
469
|
-
|
470
|
-
if torch.cuda.is_available():
|
471
|
-
torch.cuda.empty_cache()
|
472
|
-
except ImportError:
|
473
|
-
pass # PyTorch not available
|
474
|
-
|
475
|
-
if self.operate_on_view and self.current_image is not None:
|
476
|
-
# Use the provided modified image
|
477
|
-
if self._should_stop:
|
478
|
-
return
|
479
|
-
|
480
|
-
# Optimize image size for faster SAM processing
|
481
|
-
image = self.current_image
|
482
|
-
original_height, original_width = image.shape[:2]
|
483
|
-
max_size = 1024
|
484
|
-
|
485
|
-
if original_height > max_size or original_width > max_size:
|
486
|
-
# Calculate scaling factor
|
487
|
-
self.scale_factor = min(
|
488
|
-
max_size / original_width, max_size / original_height
|
489
|
-
)
|
490
|
-
new_width = int(original_width * self.scale_factor)
|
491
|
-
new_height = int(original_height * self.scale_factor)
|
492
|
-
|
493
|
-
# Resize using OpenCV for speed
|
494
|
-
image = cv2.resize(
|
495
|
-
image, (new_width, new_height), interpolation=cv2.INTER_AREA
|
496
|
-
)
|
497
|
-
else:
|
498
|
-
self.scale_factor = 1.0
|
499
|
-
|
500
|
-
if self._should_stop:
|
501
|
-
return
|
502
|
-
|
503
|
-
# Set image from numpy array
|
504
|
-
self.model.set_image_from_array(image)
|
505
|
-
else:
|
506
|
-
# Load original image
|
507
|
-
if self._should_stop:
|
508
|
-
return
|
509
|
-
|
510
|
-
# Optimize image size for faster SAM processing
|
511
|
-
pixmap = QPixmap(self.image_path)
|
512
|
-
if pixmap.isNull():
|
513
|
-
if not self._should_stop:
|
514
|
-
self.error.emit(self.viewer_index, "Failed to load image")
|
515
|
-
return
|
516
|
-
|
517
|
-
original_width = pixmap.width()
|
518
|
-
original_height = pixmap.height()
|
519
|
-
max_size = 1024
|
520
|
-
|
521
|
-
if original_width > max_size or original_height > max_size:
|
522
|
-
# Calculate scaling factor
|
523
|
-
self.scale_factor = min(
|
524
|
-
max_size / original_width, max_size / original_height
|
525
|
-
)
|
526
|
-
|
527
|
-
# Scale down while maintaining aspect ratio
|
528
|
-
scaled_pixmap = pixmap.scaled(
|
529
|
-
max_size,
|
530
|
-
max_size,
|
531
|
-
Qt.AspectRatioMode.KeepAspectRatio,
|
532
|
-
Qt.TransformationMode.SmoothTransformation,
|
533
|
-
)
|
534
|
-
|
535
|
-
# Convert to numpy array for SAM
|
536
|
-
qimage = scaled_pixmap.toImage()
|
537
|
-
width = qimage.width()
|
538
|
-
height = qimage.height()
|
539
|
-
ptr = qimage.bits()
|
540
|
-
ptr.setsize(height * width * 4)
|
541
|
-
arr = np.array(ptr).reshape(height, width, 4)
|
542
|
-
# Convert RGBA to RGB
|
543
|
-
image_array = arr[:, :, :3]
|
544
|
-
|
545
|
-
if self._should_stop:
|
546
|
-
return
|
547
|
-
|
548
|
-
# Add CUDA synchronization for multi-model scenarios
|
549
|
-
try:
|
550
|
-
import torch
|
551
|
-
|
552
|
-
if torch.cuda.is_available():
|
553
|
-
torch.cuda.synchronize()
|
554
|
-
except ImportError:
|
555
|
-
pass
|
556
|
-
|
557
|
-
self.model.set_image_from_array(image_array)
|
558
|
-
else:
|
559
|
-
self.scale_factor = 1.0
|
560
|
-
|
561
|
-
# Add CUDA synchronization for multi-model scenarios
|
562
|
-
try:
|
563
|
-
import torch
|
564
|
-
|
565
|
-
if torch.cuda.is_available():
|
566
|
-
torch.cuda.synchronize()
|
567
|
-
except ImportError:
|
568
|
-
pass
|
569
|
-
|
570
|
-
self.model.set_image_from_path(self.image_path)
|
571
|
-
|
572
|
-
if not self._should_stop:
|
573
|
-
self.finished.emit(self.viewer_index)
|
574
|
-
|
575
|
-
except Exception as e:
|
576
|
-
if not self._should_stop:
|
577
|
-
self.error.emit(self.viewer_index, str(e))
|
57
|
+
from .workers import (
|
58
|
+
ImageDiscoveryWorker,
|
59
|
+
MultiViewSAMInitWorker,
|
60
|
+
MultiViewSAMUpdateWorker,
|
61
|
+
SAMUpdateWorker,
|
62
|
+
SingleViewSAMInitWorker,
|
63
|
+
)
|
578
64
|
|
579
65
|
|
580
66
|
class PanelPopoutWindow(QDialog):
|
@@ -772,12 +258,32 @@ class MainWindow(QMainWindow):
|
|
772
258
|
logger.info("Step 5/8: Discovering available models...")
|
773
259
|
self._setup_model_manager() # Just setup manager, don't load model
|
774
260
|
self._setup_connections()
|
261
|
+
self._fix_fft_connection() # Workaround for FFT signal connection issue
|
775
262
|
self._setup_shortcuts()
|
776
263
|
self._load_settings()
|
777
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
|
+
|
778
283
|
def _setup_ui(self):
|
779
284
|
"""Setup the main user interface."""
|
780
|
-
self.
|
285
|
+
version = self._get_version()
|
286
|
+
self.setWindowTitle(f"LazyLabel by DNC (version {version})")
|
781
287
|
self.setGeometry(
|
782
288
|
50, 50, self.settings.window_width, self.settings.window_height
|
783
289
|
)
|
@@ -877,12 +383,7 @@ class MainWindow(QMainWindow):
|
|
877
383
|
grid_widget = QWidget()
|
878
384
|
|
879
385
|
# Use grid layout for 4-view, horizontal layout for 2-view
|
880
|
-
if use_grid
|
881
|
-
from PyQt6.QtWidgets import QGridLayout
|
882
|
-
|
883
|
-
grid_layout = QGridLayout(grid_widget)
|
884
|
-
else:
|
885
|
-
grid_layout = QHBoxLayout(grid_widget)
|
386
|
+
grid_layout = QGridLayout(grid_widget) if use_grid else QHBoxLayout(grid_widget)
|
886
387
|
grid_layout.setSpacing(5)
|
887
388
|
|
888
389
|
self.multi_view_viewers = []
|
@@ -951,29 +452,30 @@ class MainWindow(QMainWindow):
|
|
951
452
|
grid_mode_label = QLabel("View Mode:")
|
952
453
|
controls_layout.addWidget(grid_mode_label)
|
953
454
|
|
954
|
-
from
|
455
|
+
from lazylabel.ui.widgets.model_selection_widget import CustomDropdown
|
955
456
|
|
956
|
-
self.grid_mode_combo =
|
457
|
+
self.grid_mode_combo = CustomDropdown()
|
458
|
+
self.grid_mode_combo.setText("View Mode") # Default text
|
957
459
|
self.grid_mode_combo.addItem("2 Views (1x2)", "2_view")
|
958
460
|
self.grid_mode_combo.addItem("4 Views (2x2)", "4_view")
|
959
461
|
|
960
462
|
# Set current selection based on settings
|
961
463
|
current_mode = self.settings.multi_view_grid_mode
|
962
|
-
for i in range(self.grid_mode_combo.
|
464
|
+
for i in range(len(self.grid_mode_combo.items)):
|
963
465
|
if self.grid_mode_combo.itemData(i) == current_mode:
|
964
466
|
self.grid_mode_combo.setCurrentIndex(i)
|
965
467
|
break
|
966
468
|
|
967
|
-
self.grid_mode_combo.
|
469
|
+
self.grid_mode_combo.activated.connect(self._on_grid_mode_changed)
|
968
470
|
controls_layout.addWidget(self.grid_mode_combo)
|
969
471
|
|
970
472
|
controls_layout.addStretch()
|
971
473
|
|
972
474
|
layout.addWidget(controls_widget)
|
973
475
|
|
974
|
-
def _on_grid_mode_changed(self):
|
476
|
+
def _on_grid_mode_changed(self, index):
|
975
477
|
"""Handle grid mode change from combo box."""
|
976
|
-
current_data = self.grid_mode_combo.
|
478
|
+
current_data = self.grid_mode_combo.itemData(index)
|
977
479
|
if current_data and current_data != self.settings.multi_view_grid_mode:
|
978
480
|
# Update settings
|
979
481
|
self.settings.multi_view_grid_mode = current_data
|
@@ -1062,8 +564,7 @@ class MainWindow(QMainWindow):
|
|
1062
564
|
layout.deleteLater()
|
1063
565
|
except Exception as e:
|
1064
566
|
# If layout clearing fails, just reset the viewer lists
|
1065
|
-
|
1066
|
-
pass
|
567
|
+
logger.error(f"Layout clearing failed: {e}")
|
1067
568
|
|
1068
569
|
def _clear_multi_view_layout(self):
|
1069
570
|
"""Clear the existing multi-view layout."""
|
@@ -1121,6 +622,48 @@ class MainWindow(QMainWindow):
|
|
1121
622
|
# Switch to polygon mode if SAM is disabled and we're in SAM/AI mode
|
1122
623
|
self.set_polygon_mode()
|
1123
624
|
|
625
|
+
def _fix_fft_connection(self):
|
626
|
+
"""Fix FFT signal connection issue - workaround for connection timing problem."""
|
627
|
+
try:
|
628
|
+
# Get the FFT widget directly and connect to its signal
|
629
|
+
fft_widget = self.control_panel.get_fft_threshold_widget()
|
630
|
+
if fft_widget:
|
631
|
+
# Direct connection bypass - connect FFT widget directly to main window handler
|
632
|
+
# This bypasses the control panel signal forwarding which has timing issues
|
633
|
+
# Use a wrapper to ensure the connection works reliably
|
634
|
+
def fft_signal_wrapper():
|
635
|
+
self._handle_fft_threshold_changed()
|
636
|
+
|
637
|
+
fft_widget.fft_threshold_changed.connect(fft_signal_wrapper)
|
638
|
+
|
639
|
+
logger.info("FFT signal connection bypass established successfully")
|
640
|
+
else:
|
641
|
+
logger.warning("FFT widget not found during connection fix")
|
642
|
+
except Exception as e:
|
643
|
+
logger.warning(f"Failed to establish FFT connection bypass: {e}")
|
644
|
+
|
645
|
+
# Also fix channel threshold connection for RGB images
|
646
|
+
try:
|
647
|
+
channel_widget = self.control_panel.get_channel_threshold_widget()
|
648
|
+
if channel_widget:
|
649
|
+
# Direct connection bypass for channel threshold widget too
|
650
|
+
def channel_signal_wrapper():
|
651
|
+
self._handle_channel_threshold_changed()
|
652
|
+
|
653
|
+
channel_widget.thresholdChanged.connect(channel_signal_wrapper)
|
654
|
+
|
655
|
+
logger.info(
|
656
|
+
"Channel threshold signal connection bypass established successfully"
|
657
|
+
)
|
658
|
+
else:
|
659
|
+
logger.warning(
|
660
|
+
"Channel threshold widget not found during connection fix"
|
661
|
+
)
|
662
|
+
except Exception as e:
|
663
|
+
logger.warning(
|
664
|
+
f"Failed to establish channel threshold connection bypass: {e}"
|
665
|
+
)
|
666
|
+
|
1124
667
|
def _setup_connections(self):
|
1125
668
|
"""Setup signal connections."""
|
1126
669
|
# Control panel connections
|
@@ -1169,9 +712,13 @@ class MainWindow(QMainWindow):
|
|
1169
712
|
)
|
1170
713
|
|
1171
714
|
# FFT threshold connections
|
1172
|
-
|
1173
|
-
self.
|
1174
|
-
|
715
|
+
try:
|
716
|
+
self.control_panel.fft_threshold_changed.connect(
|
717
|
+
self._handle_fft_threshold_changed
|
718
|
+
)
|
719
|
+
logger.debug("FFT threshold connection established in _setup_connections")
|
720
|
+
except Exception as e:
|
721
|
+
logger.error(f"Failed to establish FFT threshold connection: {e}")
|
1175
722
|
|
1176
723
|
# Right panel connections
|
1177
724
|
self.right_panel.open_folder_requested.connect(self._open_folder_dialog)
|
@@ -1466,7 +1013,7 @@ class MainWindow(QMainWindow):
|
|
1466
1013
|
if not model_text or model_text == "Default (vit_h)":
|
1467
1014
|
# Clear any pending custom model and use default
|
1468
1015
|
self.pending_custom_model_path = None
|
1469
|
-
self.control_panel.set_current_model("
|
1016
|
+
self.control_panel.set_current_model("Selected: Default SAM Model")
|
1470
1017
|
# Clear existing model to free memory until needed
|
1471
1018
|
self._reset_sam_state_for_model_switch()
|
1472
1019
|
return
|
@@ -1659,7 +1206,17 @@ class MainWindow(QMainWindow):
|
|
1659
1206
|
self._save_output_to_npz()
|
1660
1207
|
|
1661
1208
|
self.current_image_path = path
|
1662
|
-
|
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)
|
1663
1220
|
if not pixmap.isNull():
|
1664
1221
|
self._reset_state()
|
1665
1222
|
self.viewer.set_photo(pixmap)
|
@@ -1713,6 +1270,11 @@ class MainWindow(QMainWindow):
|
|
1713
1270
|
):
|
1714
1271
|
self._save_output_to_npz()
|
1715
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
|
+
|
1716
1278
|
self.segment_manager.clear()
|
1717
1279
|
# Remove all scene items except the pixmap
|
1718
1280
|
items_to_remove = [
|
@@ -1722,7 +1284,6 @@ class MainWindow(QMainWindow):
|
|
1722
1284
|
]
|
1723
1285
|
for item in items_to_remove:
|
1724
1286
|
self.viewer.scene().removeItem(item)
|
1725
|
-
self.current_image_path = path
|
1726
1287
|
|
1727
1288
|
# Load the image
|
1728
1289
|
original_image = cv2.imread(path)
|
@@ -1774,6 +1335,12 @@ class MainWindow(QMainWindow):
|
|
1774
1335
|
# Update file selection in the file manager
|
1775
1336
|
self.right_panel.select_file(Path(path))
|
1776
1337
|
|
1338
|
+
# CRITICAL: Update SAM model with new image
|
1339
|
+
self._update_sam_model_image()
|
1340
|
+
|
1341
|
+
# Update threshold widgets for new image (this was missing!)
|
1342
|
+
self._update_channel_threshold_for_image(pixmap)
|
1343
|
+
|
1777
1344
|
def _load_multi_view_from_path(self, path: str):
|
1778
1345
|
"""Load multi-view starting from a specific path using FastFileManager."""
|
1779
1346
|
config = self._get_multi_view_config()
|
@@ -1897,7 +1464,21 @@ class MainWindow(QMainWindow):
|
|
1897
1464
|
image_path = self.multi_view_images[i]
|
1898
1465
|
|
1899
1466
|
if image_path:
|
1900
|
-
|
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)
|
1901
1482
|
if not pixmap.isNull():
|
1902
1483
|
self.multi_view_viewers[i].set_photo(pixmap)
|
1903
1484
|
# Apply current image adjustments to the newly loaded image
|
@@ -1948,6 +1529,9 @@ class MainWindow(QMainWindow):
|
|
1948
1529
|
if changed_indices:
|
1949
1530
|
self._fast_update_multi_view_images(changed_indices)
|
1950
1531
|
|
1532
|
+
# Update threshold widgets for the loaded images
|
1533
|
+
self._update_multi_view_channel_threshold_for_images()
|
1534
|
+
|
1951
1535
|
# Load existing segments for all loaded images
|
1952
1536
|
valid_image_paths = [path for path in image_paths if path is not None]
|
1953
1537
|
if valid_image_paths:
|
@@ -1983,6 +1567,21 @@ class MainWindow(QMainWindow):
|
|
1983
1567
|
self.action_history.clear()
|
1984
1568
|
self.redo_history.clear()
|
1985
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
|
+
|
1986
1585
|
# Update UI lists to reflect cleared state
|
1987
1586
|
self._update_all_lists()
|
1988
1587
|
|
@@ -2005,7 +1604,9 @@ class MainWindow(QMainWindow):
|
|
2005
1604
|
all_segments.extend(viewer_segments)
|
2006
1605
|
self.segment_manager.segments.clear()
|
2007
1606
|
except Exception as e:
|
2008
|
-
|
1607
|
+
logger.error(
|
1608
|
+
f"Error loading segments for viewer {viewer_index}: {e}"
|
1609
|
+
)
|
2009
1610
|
|
2010
1611
|
# Set all segments at once
|
2011
1612
|
self.segment_manager.segments = all_segments
|
@@ -2025,7 +1626,7 @@ class MainWindow(QMainWindow):
|
|
2025
1626
|
self._update_all_lists()
|
2026
1627
|
|
2027
1628
|
except Exception as e:
|
2028
|
-
|
1629
|
+
logger.error(f"Error in _load_multi_view_segments: {e}")
|
2029
1630
|
self.segment_manager.segments.clear()
|
2030
1631
|
|
2031
1632
|
def _cancel_multi_view_sam_loading(self):
|
@@ -2245,9 +1846,18 @@ class MainWindow(QMainWindow):
|
|
2245
1846
|
# Convert from BGRA to RGB for SAM
|
2246
1847
|
image_rgb = cv2.cvtColor(image_np, cv2.COLOR_BGRA2RGB)
|
2247
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
|
2248
1852
|
else:
|
2249
1853
|
# Pass the original image path to SAM model
|
2250
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
|
2251
1861
|
|
2252
1862
|
def _load_next_image(self):
|
2253
1863
|
"""Load next image in the file list."""
|
@@ -3353,6 +2963,8 @@ class MainWindow(QMainWindow):
|
|
3353
2963
|
valid_paths = [Path(img) for img in self.multi_view_images if img]
|
3354
2964
|
if valid_paths:
|
3355
2965
|
self.right_panel.file_manager.batchUpdateFileStatus(valid_paths)
|
2966
|
+
# Force immediate GUI update
|
2967
|
+
QApplication.processEvents()
|
3356
2968
|
# Clear the tracking list for next save
|
3357
2969
|
self._saved_file_paths = []
|
3358
2970
|
|
@@ -3401,6 +3013,8 @@ class MainWindow(QMainWindow):
|
|
3401
3013
|
)
|
3402
3014
|
# Update UI immediately when files are deleted
|
3403
3015
|
self._update_all_lists()
|
3016
|
+
# Force immediate GUI update
|
3017
|
+
QApplication.processEvents()
|
3404
3018
|
else:
|
3405
3019
|
self._show_warning_notification("No segments to save.")
|
3406
3020
|
return
|
@@ -3478,15 +3092,16 @@ class MainWindow(QMainWindow):
|
|
3478
3092
|
)
|
3479
3093
|
|
3480
3094
|
# Update FastFileManager to show NPZ/TXT checkmarks
|
3481
|
-
|
3482
|
-
|
3483
|
-
|
3484
|
-
and hasattr(self.right_panel, "file_manager")
|
3095
|
+
# Always update file status after save attempt (regardless of what was saved)
|
3096
|
+
if hasattr(self, "right_panel") and hasattr(
|
3097
|
+
self.right_panel, "file_manager"
|
3485
3098
|
):
|
3486
3099
|
# Update the file status in the FastFileManager
|
3487
3100
|
self.right_panel.file_manager.updateFileStatus(
|
3488
3101
|
Path(self.current_image_path)
|
3489
3102
|
)
|
3103
|
+
# Force immediate GUI update
|
3104
|
+
QApplication.processEvents()
|
3490
3105
|
except Exception as e:
|
3491
3106
|
logger.error(f"Error saving file: {str(e)}", exc_info=True)
|
3492
3107
|
self._show_error_notification(f"Error saving: {str(e)}")
|
@@ -4179,6 +3794,18 @@ class MainWindow(QMainWindow):
|
|
4179
3794
|
self.crop_start_pos = None
|
4180
3795
|
self.current_crop_coords = None
|
4181
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
|
+
|
4182
3809
|
# Reset AI mode state
|
4183
3810
|
self.ai_click_start_pos = None
|
4184
3811
|
self.ai_click_time = 0
|
@@ -5714,27 +5341,50 @@ class MainWindow(QMainWindow):
|
|
5714
5341
|
|
5715
5342
|
def _update_channel_threshold_for_image(self, pixmap):
|
5716
5343
|
"""Update channel threshold widget for the given image pixmap."""
|
5717
|
-
if pixmap.isNull():
|
5344
|
+
if pixmap.isNull() or not self.current_image_path:
|
5718
5345
|
self.control_panel.update_channel_threshold_for_image(None)
|
5719
5346
|
return
|
5720
5347
|
|
5721
|
-
#
|
5722
|
-
|
5723
|
-
|
5724
|
-
ptr.setsize(qimage.bytesPerLine() * qimage.height())
|
5725
|
-
image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
|
5726
|
-
# Convert from BGRA to RGB, ignore alpha
|
5727
|
-
image_rgb = image_np[:, :, [2, 1, 0]]
|
5348
|
+
# Use cv2.imread for more robust loading instead of QPixmap conversion
|
5349
|
+
try:
|
5350
|
+
import cv2
|
5728
5351
|
|
5729
|
-
|
5730
|
-
|
5731
|
-
|
5732
|
-
|
5733
|
-
|
5734
|
-
|
5735
|
-
|
5736
|
-
|
5737
|
-
|
5352
|
+
image_array = cv2.imread(self.current_image_path)
|
5353
|
+
if image_array is None:
|
5354
|
+
self.control_panel.update_channel_threshold_for_image(None)
|
5355
|
+
return
|
5356
|
+
|
5357
|
+
# Convert from BGR to RGB
|
5358
|
+
if len(image_array.shape) == 3 and image_array.shape[2] == 3:
|
5359
|
+
image_array = cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB)
|
5360
|
+
|
5361
|
+
# Check if image is grayscale (all channels are the same)
|
5362
|
+
if (
|
5363
|
+
len(image_array.shape) == 3
|
5364
|
+
and np.array_equal(image_array[:, :, 0], image_array[:, :, 1])
|
5365
|
+
and np.array_equal(image_array[:, :, 1], image_array[:, :, 2])
|
5366
|
+
):
|
5367
|
+
# Convert to single channel grayscale
|
5368
|
+
image_array = image_array[:, :, 0]
|
5369
|
+
|
5370
|
+
except Exception:
|
5371
|
+
# Fallback to QPixmap conversion if cv2 fails
|
5372
|
+
qimage = pixmap.toImage()
|
5373
|
+
ptr = qimage.constBits()
|
5374
|
+
ptr.setsize(qimage.bytesPerLine() * qimage.height())
|
5375
|
+
image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
|
5376
|
+
# Convert from BGRA to RGB, ignore alpha
|
5377
|
+
image_rgb = image_np[:, :, [2, 1, 0]]
|
5378
|
+
|
5379
|
+
# Check if image is grayscale (all channels are the same)
|
5380
|
+
if np.array_equal(
|
5381
|
+
image_rgb[:, :, 0], image_rgb[:, :, 1]
|
5382
|
+
) and np.array_equal(image_rgb[:, :, 1], image_rgb[:, :, 2]):
|
5383
|
+
# Convert to single channel grayscale
|
5384
|
+
image_array = image_rgb[:, :, 0]
|
5385
|
+
else:
|
5386
|
+
# Keep as RGB
|
5387
|
+
image_array = image_rgb
|
5738
5388
|
|
5739
5389
|
# Update the channel threshold widget
|
5740
5390
|
self.control_panel.update_channel_threshold_for_image(image_array)
|
@@ -5762,29 +5412,51 @@ class MainWindow(QMainWindow):
|
|
5762
5412
|
self.control_panel.update_channel_threshold_for_image(None)
|
5763
5413
|
return
|
5764
5414
|
|
5765
|
-
#
|
5766
|
-
|
5767
|
-
|
5768
|
-
self.control_panel.update_channel_threshold_for_image(None)
|
5769
|
-
return
|
5415
|
+
# Use cv2.imread for more robust loading instead of QPixmap conversion
|
5416
|
+
try:
|
5417
|
+
import cv2
|
5770
5418
|
|
5771
|
-
|
5772
|
-
|
5773
|
-
|
5774
|
-
|
5775
|
-
image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
|
5776
|
-
# Convert from BGRA to RGB, ignore alpha
|
5777
|
-
image_rgb = image_np[:, :, [2, 1, 0]]
|
5419
|
+
image_array = cv2.imread(first_image_path)
|
5420
|
+
if image_array is None:
|
5421
|
+
self.control_panel.update_channel_threshold_for_image(None)
|
5422
|
+
return
|
5778
5423
|
|
5779
|
-
|
5780
|
-
|
5781
|
-
|
5782
|
-
|
5783
|
-
#
|
5784
|
-
|
5785
|
-
|
5786
|
-
|
5787
|
-
|
5424
|
+
# Convert from BGR to RGB
|
5425
|
+
if len(image_array.shape) == 3 and image_array.shape[2] == 3:
|
5426
|
+
image_array = cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB)
|
5427
|
+
|
5428
|
+
# Check if image is grayscale (all channels are the same)
|
5429
|
+
if (
|
5430
|
+
len(image_array.shape) == 3
|
5431
|
+
and np.array_equal(image_array[:, :, 0], image_array[:, :, 1])
|
5432
|
+
and np.array_equal(image_array[:, :, 1], image_array[:, :, 2])
|
5433
|
+
):
|
5434
|
+
# Convert to single channel grayscale
|
5435
|
+
image_array = image_array[:, :, 0]
|
5436
|
+
|
5437
|
+
except Exception:
|
5438
|
+
# Fallback to QPixmap conversion if cv2 fails
|
5439
|
+
pixmap = QPixmap(first_image_path)
|
5440
|
+
if pixmap.isNull():
|
5441
|
+
self.control_panel.update_channel_threshold_for_image(None)
|
5442
|
+
return
|
5443
|
+
|
5444
|
+
qimage = pixmap.toImage()
|
5445
|
+
ptr = qimage.constBits()
|
5446
|
+
ptr.setsize(qimage.bytesPerLine() * qimage.height())
|
5447
|
+
image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
|
5448
|
+
# Convert from BGRA to RGB, ignore alpha
|
5449
|
+
image_rgb = image_np[:, :, [2, 1, 0]]
|
5450
|
+
|
5451
|
+
# Check if image is grayscale (all channels are the same)
|
5452
|
+
if np.array_equal(
|
5453
|
+
image_rgb[:, :, 0], image_rgb[:, :, 1]
|
5454
|
+
) and np.array_equal(image_rgb[:, :, 1], image_rgb[:, :, 2]):
|
5455
|
+
# Convert to single channel grayscale
|
5456
|
+
image_array = image_rgb[:, :, 0]
|
5457
|
+
else:
|
5458
|
+
# Keep as RGB
|
5459
|
+
image_array = image_rgb
|
5788
5460
|
|
5789
5461
|
# Update the channel threshold widget
|
5790
5462
|
self.control_panel.update_channel_threshold_for_image(image_array)
|
@@ -7097,7 +6769,6 @@ class MainWindow(QMainWindow):
|
|
7097
6769
|
)
|
7098
6770
|
|
7099
6771
|
# Load existing segments for the current image
|
7100
|
-
print(f"Loading segments for single-view: {self.current_image_path}")
|
7101
6772
|
try:
|
7102
6773
|
# Clear any leftover multi-view segments first
|
7103
6774
|
self.segment_manager.clear()
|
@@ -7114,7 +6785,7 @@ class MainWindow(QMainWindow):
|
|
7114
6785
|
self._update_all_lists()
|
7115
6786
|
|
7116
6787
|
except Exception as e:
|
7117
|
-
|
6788
|
+
logger.error(f"Error loading segments for single-view: {e}")
|
7118
6789
|
|
7119
6790
|
# Redisplay segments for single view
|
7120
6791
|
if hasattr(self, "single_view_mode_handler"):
|