lazylabel-gui 1.3.2__py3-none-any.whl → 1.3.4__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/ui/control_panel.py +7 -2
- lazylabel/ui/main_window.py +370 -156
- 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/utils/fast_file_manager.py +584 -85
- {lazylabel_gui-1.3.2.dist-info → lazylabel_gui-1.3.4.dist-info}/METADATA +1 -1
- {lazylabel_gui-1.3.2.dist-info → lazylabel_gui-1.3.4.dist-info}/RECORD +13 -13
- {lazylabel_gui-1.3.2.dist-info → lazylabel_gui-1.3.4.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.3.2.dist-info → lazylabel_gui-1.3.4.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.3.2.dist-info → lazylabel_gui-1.3.4.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.3.2.dist-info → lazylabel_gui-1.3.4.dist-info}/top_level.txt +0 -0
lazylabel/ui/main_window.py
CHANGED
@@ -19,12 +19,14 @@ from PyQt6.QtGui import (
|
|
19
19
|
QShortcut,
|
20
20
|
)
|
21
21
|
from PyQt6.QtWidgets import (
|
22
|
+
QApplication,
|
22
23
|
QDialog,
|
23
24
|
QFileDialog,
|
24
25
|
QGraphicsEllipseItem,
|
25
26
|
QGraphicsLineItem,
|
26
27
|
QGraphicsPolygonItem,
|
27
28
|
QGraphicsRectItem,
|
29
|
+
QGridLayout,
|
28
30
|
QHBoxLayout,
|
29
31
|
QLabel,
|
30
32
|
QMainWindow,
|
@@ -772,6 +774,7 @@ class MainWindow(QMainWindow):
|
|
772
774
|
logger.info("Step 5/8: Discovering available models...")
|
773
775
|
self._setup_model_manager() # Just setup manager, don't load model
|
774
776
|
self._setup_connections()
|
777
|
+
self._fix_fft_connection() # Workaround for FFT signal connection issue
|
775
778
|
self._setup_shortcuts()
|
776
779
|
self._load_settings()
|
777
780
|
|
@@ -877,12 +880,7 @@ class MainWindow(QMainWindow):
|
|
877
880
|
grid_widget = QWidget()
|
878
881
|
|
879
882
|
# 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)
|
883
|
+
grid_layout = QGridLayout(grid_widget) if use_grid else QHBoxLayout(grid_widget)
|
886
884
|
grid_layout.setSpacing(5)
|
887
885
|
|
888
886
|
self.multi_view_viewers = []
|
@@ -951,29 +949,30 @@ class MainWindow(QMainWindow):
|
|
951
949
|
grid_mode_label = QLabel("View Mode:")
|
952
950
|
controls_layout.addWidget(grid_mode_label)
|
953
951
|
|
954
|
-
from
|
952
|
+
from lazylabel.ui.widgets.model_selection_widget import CustomDropdown
|
955
953
|
|
956
|
-
self.grid_mode_combo =
|
954
|
+
self.grid_mode_combo = CustomDropdown()
|
955
|
+
self.grid_mode_combo.setText("View Mode") # Default text
|
957
956
|
self.grid_mode_combo.addItem("2 Views (1x2)", "2_view")
|
958
957
|
self.grid_mode_combo.addItem("4 Views (2x2)", "4_view")
|
959
958
|
|
960
959
|
# Set current selection based on settings
|
961
960
|
current_mode = self.settings.multi_view_grid_mode
|
962
|
-
for i in range(self.grid_mode_combo.
|
961
|
+
for i in range(len(self.grid_mode_combo.items)):
|
963
962
|
if self.grid_mode_combo.itemData(i) == current_mode:
|
964
963
|
self.grid_mode_combo.setCurrentIndex(i)
|
965
964
|
break
|
966
965
|
|
967
|
-
self.grid_mode_combo.
|
966
|
+
self.grid_mode_combo.activated.connect(self._on_grid_mode_changed)
|
968
967
|
controls_layout.addWidget(self.grid_mode_combo)
|
969
968
|
|
970
969
|
controls_layout.addStretch()
|
971
970
|
|
972
971
|
layout.addWidget(controls_widget)
|
973
972
|
|
974
|
-
def _on_grid_mode_changed(self):
|
973
|
+
def _on_grid_mode_changed(self, index):
|
975
974
|
"""Handle grid mode change from combo box."""
|
976
|
-
current_data = self.grid_mode_combo.
|
975
|
+
current_data = self.grid_mode_combo.itemData(index)
|
977
976
|
if current_data and current_data != self.settings.multi_view_grid_mode:
|
978
977
|
# Update settings
|
979
978
|
self.settings.multi_view_grid_mode = current_data
|
@@ -1062,8 +1061,7 @@ class MainWindow(QMainWindow):
|
|
1062
1061
|
layout.deleteLater()
|
1063
1062
|
except Exception as e:
|
1064
1063
|
# If layout clearing fails, just reset the viewer lists
|
1065
|
-
|
1066
|
-
pass
|
1064
|
+
logger.error(f"Layout clearing failed: {e}")
|
1067
1065
|
|
1068
1066
|
def _clear_multi_view_layout(self):
|
1069
1067
|
"""Clear the existing multi-view layout."""
|
@@ -1121,6 +1119,48 @@ class MainWindow(QMainWindow):
|
|
1121
1119
|
# Switch to polygon mode if SAM is disabled and we're in SAM/AI mode
|
1122
1120
|
self.set_polygon_mode()
|
1123
1121
|
|
1122
|
+
def _fix_fft_connection(self):
|
1123
|
+
"""Fix FFT signal connection issue - workaround for connection timing problem."""
|
1124
|
+
try:
|
1125
|
+
# Get the FFT widget directly and connect to its signal
|
1126
|
+
fft_widget = self.control_panel.get_fft_threshold_widget()
|
1127
|
+
if fft_widget:
|
1128
|
+
# Direct connection bypass - connect FFT widget directly to main window handler
|
1129
|
+
# This bypasses the control panel signal forwarding which has timing issues
|
1130
|
+
# Use a wrapper to ensure the connection works reliably
|
1131
|
+
def fft_signal_wrapper():
|
1132
|
+
self._handle_fft_threshold_changed()
|
1133
|
+
|
1134
|
+
fft_widget.fft_threshold_changed.connect(fft_signal_wrapper)
|
1135
|
+
|
1136
|
+
logger.info("FFT signal connection bypass established successfully")
|
1137
|
+
else:
|
1138
|
+
logger.warning("FFT widget not found during connection fix")
|
1139
|
+
except Exception as e:
|
1140
|
+
logger.warning(f"Failed to establish FFT connection bypass: {e}")
|
1141
|
+
|
1142
|
+
# Also fix channel threshold connection for RGB images
|
1143
|
+
try:
|
1144
|
+
channel_widget = self.control_panel.get_channel_threshold_widget()
|
1145
|
+
if channel_widget:
|
1146
|
+
# Direct connection bypass for channel threshold widget too
|
1147
|
+
def channel_signal_wrapper():
|
1148
|
+
self._handle_channel_threshold_changed()
|
1149
|
+
|
1150
|
+
channel_widget.thresholdChanged.connect(channel_signal_wrapper)
|
1151
|
+
|
1152
|
+
logger.info(
|
1153
|
+
"Channel threshold signal connection bypass established successfully"
|
1154
|
+
)
|
1155
|
+
else:
|
1156
|
+
logger.warning(
|
1157
|
+
"Channel threshold widget not found during connection fix"
|
1158
|
+
)
|
1159
|
+
except Exception as e:
|
1160
|
+
logger.warning(
|
1161
|
+
f"Failed to establish channel threshold connection bypass: {e}"
|
1162
|
+
)
|
1163
|
+
|
1124
1164
|
def _setup_connections(self):
|
1125
1165
|
"""Setup signal connections."""
|
1126
1166
|
# Control panel connections
|
@@ -1169,9 +1209,13 @@ class MainWindow(QMainWindow):
|
|
1169
1209
|
)
|
1170
1210
|
|
1171
1211
|
# FFT threshold connections
|
1172
|
-
|
1173
|
-
self.
|
1174
|
-
|
1212
|
+
try:
|
1213
|
+
self.control_panel.fft_threshold_changed.connect(
|
1214
|
+
self._handle_fft_threshold_changed
|
1215
|
+
)
|
1216
|
+
logger.debug("FFT threshold connection established in _setup_connections")
|
1217
|
+
except Exception as e:
|
1218
|
+
logger.error(f"Failed to establish FFT threshold connection: {e}")
|
1175
1219
|
|
1176
1220
|
# Right panel connections
|
1177
1221
|
self.right_panel.open_folder_requested.connect(self._open_folder_dialog)
|
@@ -1774,6 +1818,9 @@ class MainWindow(QMainWindow):
|
|
1774
1818
|
# Update file selection in the file manager
|
1775
1819
|
self.right_panel.select_file(Path(path))
|
1776
1820
|
|
1821
|
+
# Update threshold widgets for new image (this was missing!)
|
1822
|
+
self._update_channel_threshold_for_image(pixmap)
|
1823
|
+
|
1777
1824
|
def _load_multi_view_from_path(self, path: str):
|
1778
1825
|
"""Load multi-view starting from a specific path using FastFileManager."""
|
1779
1826
|
config = self._get_multi_view_config()
|
@@ -1788,31 +1835,12 @@ class MainWindow(QMainWindow):
|
|
1788
1835
|
):
|
1789
1836
|
self._save_multi_view_output()
|
1790
1837
|
|
1791
|
-
# Get
|
1792
|
-
|
1793
|
-
|
1794
|
-
current_idx = -1
|
1838
|
+
# Get surrounding files in current sorted/filtered order
|
1839
|
+
file_manager = self.right_panel.file_manager
|
1840
|
+
surrounding_files = file_manager.getSurroundingFiles(Path(path), num_viewers)
|
1795
1841
|
|
1796
|
-
#
|
1797
|
-
for
|
1798
|
-
file_info = file_manager_model.getFileInfo(i)
|
1799
|
-
if file_info:
|
1800
|
-
all_files.append(str(file_info.path))
|
1801
|
-
if str(file_info.path) == path:
|
1802
|
-
current_idx = i
|
1803
|
-
|
1804
|
-
if current_idx == -1:
|
1805
|
-
# File not found in current list
|
1806
|
-
return
|
1807
|
-
|
1808
|
-
# Load consecutive images
|
1809
|
-
images_to_load = []
|
1810
|
-
for i in range(num_viewers):
|
1811
|
-
idx = current_idx + i
|
1812
|
-
if idx < len(all_files):
|
1813
|
-
images_to_load.append(all_files[idx])
|
1814
|
-
else:
|
1815
|
-
images_to_load.append(None)
|
1842
|
+
# Convert to strings for loading
|
1843
|
+
images_to_load = [str(p) if p else None for p in surrounding_files]
|
1816
1844
|
|
1817
1845
|
# Load the images
|
1818
1846
|
self._load_multi_view_images(images_to_load)
|
@@ -1967,6 +1995,9 @@ class MainWindow(QMainWindow):
|
|
1967
1995
|
if changed_indices:
|
1968
1996
|
self._fast_update_multi_view_images(changed_indices)
|
1969
1997
|
|
1998
|
+
# Update threshold widgets for the loaded images
|
1999
|
+
self._update_multi_view_channel_threshold_for_images()
|
2000
|
+
|
1970
2001
|
# Load existing segments for all loaded images
|
1971
2002
|
valid_image_paths = [path for path in image_paths if path is not None]
|
1972
2003
|
if valid_image_paths:
|
@@ -2024,7 +2055,9 @@ class MainWindow(QMainWindow):
|
|
2024
2055
|
all_segments.extend(viewer_segments)
|
2025
2056
|
self.segment_manager.segments.clear()
|
2026
2057
|
except Exception as e:
|
2027
|
-
|
2058
|
+
logger.error(
|
2059
|
+
f"Error loading segments for viewer {viewer_index}: {e}"
|
2060
|
+
)
|
2028
2061
|
|
2029
2062
|
# Set all segments at once
|
2030
2063
|
self.segment_manager.segments = all_segments
|
@@ -2044,7 +2077,7 @@ class MainWindow(QMainWindow):
|
|
2044
2077
|
self._update_all_lists()
|
2045
2078
|
|
2046
2079
|
except Exception as e:
|
2047
|
-
|
2080
|
+
logger.error(f"Error in _load_multi_view_segments: {e}")
|
2048
2081
|
self.segment_manager.segments.clear()
|
2049
2082
|
|
2050
2083
|
def _cancel_multi_view_sam_loading(self):
|
@@ -3320,6 +3353,18 @@ class MainWindow(QMainWindow):
|
|
3320
3353
|
# Restore original segments
|
3321
3354
|
self.segment_manager.segments = original_segments
|
3322
3355
|
|
3356
|
+
# Save class aliases if enabled
|
3357
|
+
if settings.get("save_class_aliases", False) and viewer_segments:
|
3358
|
+
# Temporarily set segments for this viewer to save correct aliases
|
3359
|
+
original_segments = self.segment_manager.segments
|
3360
|
+
self.segment_manager.segments = viewer_segments
|
3361
|
+
|
3362
|
+
aliases_path = self.file_manager.save_class_aliases(image_path)
|
3363
|
+
saved_files.append(os.path.basename(aliases_path))
|
3364
|
+
|
3365
|
+
# Restore original segments
|
3366
|
+
self.segment_manager.segments = original_segments
|
3367
|
+
|
3323
3368
|
# If no segments for this viewer, delete associated files
|
3324
3369
|
if not viewer_segments:
|
3325
3370
|
base, _ = os.path.splitext(image_path)
|
@@ -3334,7 +3379,7 @@ class MainWindow(QMainWindow):
|
|
3334
3379
|
if hasattr(self, "right_panel") and hasattr(
|
3335
3380
|
self.right_panel, "file_manager"
|
3336
3381
|
):
|
3337
|
-
self.right_panel.file_manager.
|
3382
|
+
self.right_panel.file_manager.updateFileStatus(
|
3338
3383
|
Path(image_path)
|
3339
3384
|
)
|
3340
3385
|
except Exception as e:
|
@@ -3353,13 +3398,15 @@ class MainWindow(QMainWindow):
|
|
3353
3398
|
|
3354
3399
|
# Update FastFileManager to show NPZ checkmarks for multi-view
|
3355
3400
|
if hasattr(self, "multi_view_images") and self.multi_view_images:
|
3356
|
-
|
3357
|
-
|
3358
|
-
|
3359
|
-
|
3360
|
-
|
3361
|
-
|
3362
|
-
self.right_panel.file_manager.
|
3401
|
+
if hasattr(self, "right_panel") and hasattr(
|
3402
|
+
self.right_panel, "file_manager"
|
3403
|
+
):
|
3404
|
+
# Batch update for better performance
|
3405
|
+
valid_paths = [Path(img) for img in self.multi_view_images if img]
|
3406
|
+
if valid_paths:
|
3407
|
+
self.right_panel.file_manager.batchUpdateFileStatus(valid_paths)
|
3408
|
+
# Force immediate GUI update
|
3409
|
+
QApplication.processEvents()
|
3363
3410
|
# Clear the tracking list for next save
|
3364
3411
|
self._saved_file_paths = []
|
3365
3412
|
|
@@ -3395,7 +3442,7 @@ class MainWindow(QMainWindow):
|
|
3395
3442
|
if hasattr(self, "right_panel") and hasattr(
|
3396
3443
|
self.right_panel, "file_manager"
|
3397
3444
|
):
|
3398
|
-
self.right_panel.file_manager.
|
3445
|
+
self.right_panel.file_manager.updateFileStatus(
|
3399
3446
|
Path(self.current_image_path)
|
3400
3447
|
)
|
3401
3448
|
except Exception as e:
|
@@ -3408,6 +3455,8 @@ class MainWindow(QMainWindow):
|
|
3408
3455
|
)
|
3409
3456
|
# Update UI immediately when files are deleted
|
3410
3457
|
self._update_all_lists()
|
3458
|
+
# Force immediate GUI update
|
3459
|
+
QApplication.processEvents()
|
3411
3460
|
else:
|
3412
3461
|
self._show_warning_notification("No segments to save.")
|
3413
3462
|
return
|
@@ -3445,6 +3494,8 @@ class MainWindow(QMainWindow):
|
|
3445
3494
|
else:
|
3446
3495
|
logger.warning("No classes defined for saving")
|
3447
3496
|
self._show_warning_notification("No classes defined for saving.")
|
3497
|
+
# Save TXT file if enabled
|
3498
|
+
txt_path = None
|
3448
3499
|
if settings.get("save_txt", True):
|
3449
3500
|
h, w = (
|
3450
3501
|
self.viewer._pixmap_item.pixmap().height(),
|
@@ -3458,23 +3509,41 @@ class MainWindow(QMainWindow):
|
|
3458
3509
|
else:
|
3459
3510
|
class_labels = [str(cid) for cid in class_order]
|
3460
3511
|
if class_order:
|
3461
|
-
self.file_manager.save_yolo_txt(
|
3512
|
+
txt_path = self.file_manager.save_yolo_txt(
|
3462
3513
|
self.current_image_path,
|
3463
3514
|
(h, w),
|
3464
3515
|
class_order,
|
3465
3516
|
class_labels,
|
3466
3517
|
self.current_crop_coords,
|
3467
3518
|
)
|
3468
|
-
|
3469
|
-
|
3470
|
-
|
3471
|
-
|
3472
|
-
|
3519
|
+
if txt_path:
|
3520
|
+
logger.debug(f"TXT save completed: {txt_path}")
|
3521
|
+
self._show_success_notification(
|
3522
|
+
f"Saved: {os.path.basename(txt_path)}"
|
3523
|
+
)
|
3524
|
+
|
3525
|
+
# Save class aliases if enabled
|
3526
|
+
if settings.get("save_class_aliases", False):
|
3527
|
+
aliases_path = self.file_manager.save_class_aliases(
|
3528
|
+
self.current_image_path
|
3529
|
+
)
|
3530
|
+
if aliases_path:
|
3531
|
+
logger.debug(f"Class aliases saved: {aliases_path}")
|
3532
|
+
self._show_success_notification(
|
3533
|
+
f"Saved: {os.path.basename(aliases_path)}"
|
3534
|
+
)
|
3535
|
+
|
3536
|
+
# Update FastFileManager to show NPZ/TXT checkmarks
|
3537
|
+
# Always update file status after save attempt (regardless of what was saved)
|
3538
|
+
if hasattr(self, "right_panel") and hasattr(
|
3539
|
+
self.right_panel, "file_manager"
|
3473
3540
|
):
|
3474
|
-
# Update the
|
3475
|
-
self.right_panel.file_manager.
|
3541
|
+
# Update the file status in the FastFileManager
|
3542
|
+
self.right_panel.file_manager.updateFileStatus(
|
3476
3543
|
Path(self.current_image_path)
|
3477
3544
|
)
|
3545
|
+
# Force immediate GUI update
|
3546
|
+
QApplication.processEvents()
|
3478
3547
|
except Exception as e:
|
3479
3548
|
logger.error(f"Error saving file: {str(e)}", exc_info=True)
|
3480
3549
|
self._show_error_notification(f"Error saving: {str(e)}")
|
@@ -5702,27 +5771,50 @@ class MainWindow(QMainWindow):
|
|
5702
5771
|
|
5703
5772
|
def _update_channel_threshold_for_image(self, pixmap):
|
5704
5773
|
"""Update channel threshold widget for the given image pixmap."""
|
5705
|
-
if pixmap.isNull():
|
5774
|
+
if pixmap.isNull() or not self.current_image_path:
|
5706
5775
|
self.control_panel.update_channel_threshold_for_image(None)
|
5707
5776
|
return
|
5708
5777
|
|
5709
|
-
#
|
5710
|
-
|
5711
|
-
|
5712
|
-
ptr.setsize(qimage.bytesPerLine() * qimage.height())
|
5713
|
-
image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
|
5714
|
-
# Convert from BGRA to RGB, ignore alpha
|
5715
|
-
image_rgb = image_np[:, :, [2, 1, 0]]
|
5778
|
+
# Use cv2.imread for more robust loading instead of QPixmap conversion
|
5779
|
+
try:
|
5780
|
+
import cv2
|
5716
5781
|
|
5717
|
-
|
5718
|
-
|
5719
|
-
|
5720
|
-
|
5721
|
-
|
5722
|
-
|
5723
|
-
|
5724
|
-
|
5725
|
-
|
5782
|
+
image_array = cv2.imread(self.current_image_path)
|
5783
|
+
if image_array is None:
|
5784
|
+
self.control_panel.update_channel_threshold_for_image(None)
|
5785
|
+
return
|
5786
|
+
|
5787
|
+
# Convert from BGR to RGB
|
5788
|
+
if len(image_array.shape) == 3 and image_array.shape[2] == 3:
|
5789
|
+
image_array = cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB)
|
5790
|
+
|
5791
|
+
# Check if image is grayscale (all channels are the same)
|
5792
|
+
if (
|
5793
|
+
len(image_array.shape) == 3
|
5794
|
+
and np.array_equal(image_array[:, :, 0], image_array[:, :, 1])
|
5795
|
+
and np.array_equal(image_array[:, :, 1], image_array[:, :, 2])
|
5796
|
+
):
|
5797
|
+
# Convert to single channel grayscale
|
5798
|
+
image_array = image_array[:, :, 0]
|
5799
|
+
|
5800
|
+
except Exception:
|
5801
|
+
# Fallback to QPixmap conversion if cv2 fails
|
5802
|
+
qimage = pixmap.toImage()
|
5803
|
+
ptr = qimage.constBits()
|
5804
|
+
ptr.setsize(qimage.bytesPerLine() * qimage.height())
|
5805
|
+
image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
|
5806
|
+
# Convert from BGRA to RGB, ignore alpha
|
5807
|
+
image_rgb = image_np[:, :, [2, 1, 0]]
|
5808
|
+
|
5809
|
+
# Check if image is grayscale (all channels are the same)
|
5810
|
+
if np.array_equal(
|
5811
|
+
image_rgb[:, :, 0], image_rgb[:, :, 1]
|
5812
|
+
) and np.array_equal(image_rgb[:, :, 1], image_rgb[:, :, 2]):
|
5813
|
+
# Convert to single channel grayscale
|
5814
|
+
image_array = image_rgb[:, :, 0]
|
5815
|
+
else:
|
5816
|
+
# Keep as RGB
|
5817
|
+
image_array = image_rgb
|
5726
5818
|
|
5727
5819
|
# Update the channel threshold widget
|
5728
5820
|
self.control_panel.update_channel_threshold_for_image(image_array)
|
@@ -5750,29 +5842,51 @@ class MainWindow(QMainWindow):
|
|
5750
5842
|
self.control_panel.update_channel_threshold_for_image(None)
|
5751
5843
|
return
|
5752
5844
|
|
5753
|
-
#
|
5754
|
-
|
5755
|
-
|
5756
|
-
self.control_panel.update_channel_threshold_for_image(None)
|
5757
|
-
return
|
5845
|
+
# Use cv2.imread for more robust loading instead of QPixmap conversion
|
5846
|
+
try:
|
5847
|
+
import cv2
|
5758
5848
|
|
5759
|
-
|
5760
|
-
|
5761
|
-
|
5762
|
-
|
5763
|
-
image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
|
5764
|
-
# Convert from BGRA to RGB, ignore alpha
|
5765
|
-
image_rgb = image_np[:, :, [2, 1, 0]]
|
5849
|
+
image_array = cv2.imread(first_image_path)
|
5850
|
+
if image_array is None:
|
5851
|
+
self.control_panel.update_channel_threshold_for_image(None)
|
5852
|
+
return
|
5766
5853
|
|
5767
|
-
|
5768
|
-
|
5769
|
-
|
5770
|
-
|
5771
|
-
#
|
5772
|
-
|
5773
|
-
|
5774
|
-
|
5775
|
-
|
5854
|
+
# Convert from BGR to RGB
|
5855
|
+
if len(image_array.shape) == 3 and image_array.shape[2] == 3:
|
5856
|
+
image_array = cv2.cvtColor(image_array, cv2.COLOR_BGR2RGB)
|
5857
|
+
|
5858
|
+
# Check if image is grayscale (all channels are the same)
|
5859
|
+
if (
|
5860
|
+
len(image_array.shape) == 3
|
5861
|
+
and np.array_equal(image_array[:, :, 0], image_array[:, :, 1])
|
5862
|
+
and np.array_equal(image_array[:, :, 1], image_array[:, :, 2])
|
5863
|
+
):
|
5864
|
+
# Convert to single channel grayscale
|
5865
|
+
image_array = image_array[:, :, 0]
|
5866
|
+
|
5867
|
+
except Exception:
|
5868
|
+
# Fallback to QPixmap conversion if cv2 fails
|
5869
|
+
pixmap = QPixmap(first_image_path)
|
5870
|
+
if pixmap.isNull():
|
5871
|
+
self.control_panel.update_channel_threshold_for_image(None)
|
5872
|
+
return
|
5873
|
+
|
5874
|
+
qimage = pixmap.toImage()
|
5875
|
+
ptr = qimage.constBits()
|
5876
|
+
ptr.setsize(qimage.bytesPerLine() * qimage.height())
|
5877
|
+
image_np = np.array(ptr).reshape(qimage.height(), qimage.width(), 4)
|
5878
|
+
# Convert from BGRA to RGB, ignore alpha
|
5879
|
+
image_rgb = image_np[:, :, [2, 1, 0]]
|
5880
|
+
|
5881
|
+
# Check if image is grayscale (all channels are the same)
|
5882
|
+
if np.array_equal(
|
5883
|
+
image_rgb[:, :, 0], image_rgb[:, :, 1]
|
5884
|
+
) and np.array_equal(image_rgb[:, :, 1], image_rgb[:, :, 2]):
|
5885
|
+
# Convert to single channel grayscale
|
5886
|
+
image_array = image_rgb[:, :, 0]
|
5887
|
+
else:
|
5888
|
+
# Keep as RGB
|
5889
|
+
image_array = image_rgb
|
5776
5890
|
|
5777
5891
|
# Update the channel threshold widget
|
5778
5892
|
self.control_panel.update_channel_threshold_for_image(image_array)
|
@@ -7085,7 +7199,6 @@ class MainWindow(QMainWindow):
|
|
7085
7199
|
)
|
7086
7200
|
|
7087
7201
|
# Load existing segments for the current image
|
7088
|
-
print(f"Loading segments for single-view: {self.current_image_path}")
|
7089
7202
|
try:
|
7090
7203
|
# Clear any leftover multi-view segments first
|
7091
7204
|
self.segment_manager.clear()
|
@@ -7102,7 +7215,7 @@ class MainWindow(QMainWindow):
|
|
7102
7215
|
self._update_all_lists()
|
7103
7216
|
|
7104
7217
|
except Exception as e:
|
7105
|
-
|
7218
|
+
logger.error(f"Error loading segments for single-view: {e}")
|
7106
7219
|
|
7107
7220
|
# Redisplay segments for single view
|
7108
7221
|
if hasattr(self, "single_view_mode_handler"):
|
@@ -7236,24 +7349,73 @@ class MainWindow(QMainWindow):
|
|
7236
7349
|
self._load_multi_view_images(image_paths)
|
7237
7350
|
|
7238
7351
|
def _load_next_multi_batch(self):
|
7239
|
-
"""Load the next batch of images using cached list."""
|
7240
|
-
if
|
7241
|
-
|
7242
|
-
|
7243
|
-
|
7244
|
-
|
7245
|
-
"auto_save", True
|
7352
|
+
"""Load the next batch of images using fast file manager or cached list."""
|
7353
|
+
# Auto-save if enabled and we have current images
|
7354
|
+
if (
|
7355
|
+
hasattr(self, "multi_view_images")
|
7356
|
+
and self.multi_view_images
|
7357
|
+
and self.multi_view_images[0]
|
7358
|
+
and self.control_panel.get_settings().get("auto_save", True)
|
7246
7359
|
):
|
7247
7360
|
self._save_multi_view_output()
|
7248
7361
|
|
7249
|
-
#
|
7362
|
+
# Get current image path - look for any valid image in multi-view state
|
7363
|
+
current_path = None
|
7364
|
+
if hasattr(self, "multi_view_images") and self.multi_view_images:
|
7365
|
+
# Find the first valid image path in the current multi-view state
|
7366
|
+
for img_path in self.multi_view_images:
|
7367
|
+
if img_path:
|
7368
|
+
current_path = img_path
|
7369
|
+
break
|
7370
|
+
|
7371
|
+
# Fallback to current_image_path if no valid multi-view images found
|
7372
|
+
if (
|
7373
|
+
not current_path
|
7374
|
+
and hasattr(self, "current_image_path")
|
7375
|
+
and self.current_image_path
|
7376
|
+
):
|
7377
|
+
current_path = self.current_image_path
|
7378
|
+
|
7379
|
+
# If no valid path found, can't navigate forward
|
7380
|
+
if not current_path:
|
7381
|
+
return
|
7382
|
+
|
7383
|
+
# Get the number of viewers for multi-view mode
|
7384
|
+
config = self._get_multi_view_config()
|
7385
|
+
num_viewers = config["num_viewers"]
|
7386
|
+
|
7387
|
+
# Try to use fast file manager first (respects sorting/filtering)
|
7388
|
+
try:
|
7389
|
+
if (
|
7390
|
+
hasattr(self, "right_panel")
|
7391
|
+
and hasattr(self.right_panel, "file_manager")
|
7392
|
+
and hasattr(self.right_panel.file_manager, "getSurroundingFiles")
|
7393
|
+
):
|
7394
|
+
file_manager = self.right_panel.file_manager
|
7395
|
+
surrounding_files = file_manager.getSurroundingFiles(
|
7396
|
+
Path(current_path), num_viewers * 2
|
7397
|
+
)
|
7398
|
+
|
7399
|
+
if len(surrounding_files) > num_viewers:
|
7400
|
+
# Skip ahead by num_viewers to get the next batch
|
7401
|
+
next_batch = surrounding_files[num_viewers : num_viewers * 2]
|
7402
|
+
|
7403
|
+
# Convert to strings and load
|
7404
|
+
images_to_load = [str(p) if p else None for p in next_batch]
|
7405
|
+
self._load_multi_view_images(images_to_load)
|
7406
|
+
|
7407
|
+
# Update file manager selection to first image of new batch
|
7408
|
+
if next_batch and next_batch[0]:
|
7409
|
+
self.right_panel.select_file(next_batch[0])
|
7410
|
+
return
|
7411
|
+
except Exception:
|
7412
|
+
pass # Fall back to cached list approach
|
7413
|
+
|
7414
|
+
# Fall back to cached list approach (for backward compatibility / tests)
|
7250
7415
|
if not self.cached_image_paths:
|
7251
7416
|
self._load_next_multi_batch_fallback()
|
7252
7417
|
return
|
7253
7418
|
|
7254
|
-
# Get current image path
|
7255
|
-
current_path = self.file_model.filePath(self.current_file_index)
|
7256
|
-
|
7257
7419
|
# Find current position in cached list
|
7258
7420
|
try:
|
7259
7421
|
current_index = self.cached_image_paths.index(current_path)
|
@@ -7262,13 +7424,14 @@ class MainWindow(QMainWindow):
|
|
7262
7424
|
self._load_next_multi_batch_fallback()
|
7263
7425
|
return
|
7264
7426
|
|
7265
|
-
# Get the number of viewers for multi-view mode
|
7266
|
-
config = self._get_multi_view_config()
|
7267
|
-
num_viewers = config["num_viewers"]
|
7268
|
-
|
7269
7427
|
# Skip num_viewers positions ahead in cached list
|
7270
7428
|
target_index = current_index + num_viewers
|
7271
7429
|
|
7430
|
+
# Check if we can navigate forward (at least one valid image at target position)
|
7431
|
+
if target_index >= len(self.cached_image_paths):
|
7432
|
+
return # Can't navigate forward - at or past the end
|
7433
|
+
|
7434
|
+
# Check if we have at least one valid image at the target position
|
7272
7435
|
if target_index < len(self.cached_image_paths):
|
7273
7436
|
# Collect consecutive images
|
7274
7437
|
image_paths = []
|
@@ -7278,39 +7441,88 @@ class MainWindow(QMainWindow):
|
|
7278
7441
|
else:
|
7279
7442
|
image_paths.append(None)
|
7280
7443
|
|
7281
|
-
#
|
7282
|
-
|
7283
|
-
|
7284
|
-
|
7285
|
-
|
7286
|
-
|
7287
|
-
|
7288
|
-
|
7289
|
-
|
7290
|
-
|
7291
|
-
self.
|
7292
|
-
self.
|
7293
|
-
|
7444
|
+
# Only proceed if we have at least one valid image (prevent all-None batches)
|
7445
|
+
if any(path is not None for path in image_paths):
|
7446
|
+
# Load all images
|
7447
|
+
self._load_multi_view_images(image_paths)
|
7448
|
+
|
7449
|
+
# Update current file index to the first image of the new batch
|
7450
|
+
# Find the file model index for this path
|
7451
|
+
if image_paths and image_paths[0]:
|
7452
|
+
parent_index = self.current_file_index.parent()
|
7453
|
+
for row in range(self.file_model.rowCount(parent_index)):
|
7454
|
+
index = self.file_model.index(row, 0, parent_index)
|
7455
|
+
if self.file_model.filePath(index) == image_paths[0]:
|
7456
|
+
self.current_file_index = index
|
7457
|
+
self.right_panel.file_tree.setCurrentIndex(index)
|
7458
|
+
break
|
7459
|
+
# If all would be None, don't navigate (stay at current position)
|
7294
7460
|
|
7295
7461
|
def _load_previous_multi_batch(self):
|
7296
|
-
"""Load the previous batch of images using cached list."""
|
7297
|
-
if
|
7298
|
-
|
7299
|
-
|
7300
|
-
|
7301
|
-
|
7302
|
-
"auto_save", True
|
7462
|
+
"""Load the previous batch of images using fast file manager or cached list."""
|
7463
|
+
# Auto-save if enabled and we have current images
|
7464
|
+
if (
|
7465
|
+
hasattr(self, "multi_view_images")
|
7466
|
+
and self.multi_view_images
|
7467
|
+
and self.multi_view_images[0]
|
7468
|
+
and self.control_panel.get_settings().get("auto_save", True)
|
7303
7469
|
):
|
7304
7470
|
self._save_multi_view_output()
|
7305
7471
|
|
7306
|
-
#
|
7472
|
+
# Get current image path - look for any valid image in multi-view state
|
7473
|
+
current_path = None
|
7474
|
+
if hasattr(self, "multi_view_images") and self.multi_view_images:
|
7475
|
+
# Find the first valid image path in the current multi-view state
|
7476
|
+
for img_path in self.multi_view_images:
|
7477
|
+
if img_path:
|
7478
|
+
current_path = img_path
|
7479
|
+
break
|
7480
|
+
|
7481
|
+
# Fallback to current_image_path if no valid multi-view images found
|
7482
|
+
if (
|
7483
|
+
not current_path
|
7484
|
+
and hasattr(self, "current_image_path")
|
7485
|
+
and self.current_image_path
|
7486
|
+
):
|
7487
|
+
current_path = self.current_image_path
|
7488
|
+
|
7489
|
+
# If no valid path found, can't navigate backward
|
7490
|
+
if not current_path:
|
7491
|
+
return
|
7492
|
+
|
7493
|
+
# Get the number of viewers for multi-view mode
|
7494
|
+
config = self._get_multi_view_config()
|
7495
|
+
num_viewers = config["num_viewers"]
|
7496
|
+
|
7497
|
+
# Try to use fast file manager first (respects sorting/filtering)
|
7498
|
+
try:
|
7499
|
+
if (
|
7500
|
+
hasattr(self, "right_panel")
|
7501
|
+
and hasattr(self.right_panel, "file_manager")
|
7502
|
+
and hasattr(self.right_panel.file_manager, "getPreviousFiles")
|
7503
|
+
):
|
7504
|
+
file_manager = self.right_panel.file_manager
|
7505
|
+
previous_batch = file_manager.getPreviousFiles(
|
7506
|
+
Path(current_path), num_viewers
|
7507
|
+
)
|
7508
|
+
|
7509
|
+
if previous_batch and any(previous_batch):
|
7510
|
+
# Convert to strings and load
|
7511
|
+
images_to_load = [str(p) if p else None for p in previous_batch]
|
7512
|
+
self._load_multi_view_images(images_to_load)
|
7513
|
+
|
7514
|
+
# Update file manager selection to first image of new batch
|
7515
|
+
if previous_batch and previous_batch[0]:
|
7516
|
+
self.right_panel.select_file(previous_batch[0])
|
7517
|
+
return
|
7518
|
+
except Exception:
|
7519
|
+
pass # Fall back to cached list approach
|
7520
|
+
|
7521
|
+
# Fall back to cached list approach (for backward compatibility / tests)
|
7307
7522
|
if not self.cached_image_paths:
|
7308
7523
|
self._load_previous_multi_batch_fallback()
|
7309
7524
|
return
|
7310
7525
|
|
7311
|
-
# Get current image path
|
7312
|
-
current_path = self.file_model.filePath(self.current_file_index)
|
7313
|
-
|
7314
7526
|
# Find current position in cached list
|
7315
7527
|
try:
|
7316
7528
|
current_index = self.cached_image_paths.index(current_path)
|
@@ -7319,10 +7531,6 @@ class MainWindow(QMainWindow):
|
|
7319
7531
|
self._load_previous_multi_batch_fallback()
|
7320
7532
|
return
|
7321
7533
|
|
7322
|
-
# Get the number of viewers for multi-view mode
|
7323
|
-
config = self._get_multi_view_config()
|
7324
|
-
num_viewers = config["num_viewers"]
|
7325
|
-
|
7326
7534
|
# Skip num_viewers positions back in cached list
|
7327
7535
|
target_index = current_index - num_viewers
|
7328
7536
|
if target_index < 0:
|
@@ -7331,24 +7539,30 @@ class MainWindow(QMainWindow):
|
|
7331
7539
|
# Collect consecutive images
|
7332
7540
|
image_paths = []
|
7333
7541
|
for i in range(num_viewers):
|
7334
|
-
if
|
7542
|
+
if (
|
7543
|
+
target_index + i < len(self.cached_image_paths)
|
7544
|
+
and target_index + i >= 0
|
7545
|
+
):
|
7335
7546
|
image_paths.append(self.cached_image_paths[target_index + i])
|
7336
7547
|
else:
|
7337
7548
|
image_paths.append(None)
|
7338
7549
|
|
7339
|
-
#
|
7340
|
-
|
7550
|
+
# Only proceed if we have at least one valid image (prevent all-None batches)
|
7551
|
+
if any(path is not None for path in image_paths):
|
7552
|
+
# Load all images
|
7553
|
+
self._load_multi_view_images(image_paths)
|
7341
7554
|
|
7342
|
-
|
7343
|
-
|
7344
|
-
|
7345
|
-
|
7346
|
-
|
7347
|
-
|
7348
|
-
|
7349
|
-
|
7350
|
-
|
7351
|
-
|
7555
|
+
# Update current file index to the first image of the new batch
|
7556
|
+
# Find the file model index for this path
|
7557
|
+
if image_paths and image_paths[0]:
|
7558
|
+
parent_index = self.current_file_index.parent()
|
7559
|
+
for row in range(self.file_model.rowCount(parent_index)):
|
7560
|
+
index = self.file_model.index(row, 0, parent_index)
|
7561
|
+
if self.file_model.filePath(index) == image_paths[0]:
|
7562
|
+
self.current_file_index = index
|
7563
|
+
self.right_panel.file_tree.setCurrentIndex(index)
|
7564
|
+
break
|
7565
|
+
# If all would be None, don't navigate (stay at current position)
|
7352
7566
|
|
7353
7567
|
def _load_next_multi_batch_fallback(self):
|
7354
7568
|
"""Fallback navigation using file model when cached list isn't available."""
|