nettracer3d 0.9.4__py3-none-any.whl → 0.9.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.
- nettracer3d/community_extractor.py +88 -25
- nettracer3d/neighborhoods.py +17 -213
- nettracer3d/nettracer.py +221 -228
- nettracer3d/nettracer_gui.py +812 -285
- nettracer3d/proximity.py +91 -1
- {nettracer3d-0.9.4.dist-info → nettracer3d-0.9.6.dist-info}/METADATA +7 -3
- {nettracer3d-0.9.4.dist-info → nettracer3d-0.9.6.dist-info}/RECORD +11 -11
- {nettracer3d-0.9.4.dist-info → nettracer3d-0.9.6.dist-info}/WHEEL +0 -0
- {nettracer3d-0.9.4.dist-info → nettracer3d-0.9.6.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.9.4.dist-info → nettracer3d-0.9.6.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.9.4.dist-info → nettracer3d-0.9.6.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -5,7 +5,7 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QG
|
|
|
5
5
|
QFormLayout, QLineEdit, QPushButton, QFileDialog,
|
|
6
6
|
QLabel, QComboBox, QMessageBox, QTableView, QInputDialog,
|
|
7
7
|
QMenu, QTabWidget, QGroupBox)
|
|
8
|
-
from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal, QObject, QCoreApplication, QEvent)
|
|
8
|
+
from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal, QObject, QCoreApplication, QEvent, QEventLoop)
|
|
9
9
|
import numpy as np
|
|
10
10
|
import time
|
|
11
11
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
@@ -210,6 +210,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
210
210
|
buttons_widget = QWidget()
|
|
211
211
|
buttons_layout = QHBoxLayout(buttons_widget)
|
|
212
212
|
|
|
213
|
+
self.reset_view = QPushButton("🏠")
|
|
214
|
+
self.reset_view.setFixedSize(20, 20)
|
|
215
|
+
self.reset_view.clicked.connect(self.home)
|
|
216
|
+
control_layout.addWidget(self.reset_view)
|
|
217
|
+
|
|
213
218
|
# "Create" zoom button
|
|
214
219
|
self.zoom_button = QPushButton("🔍")
|
|
215
220
|
self.zoom_button.setCheckable(True)
|
|
@@ -293,14 +298,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
293
298
|
|
|
294
299
|
control_layout.addWidget(channel_container)
|
|
295
300
|
|
|
296
|
-
self.show_channels = QPushButton("✓")
|
|
297
|
-
self.show_channels.setCheckable(True)
|
|
298
|
-
self.show_channels.setChecked(True)
|
|
299
|
-
self.show_channels.setFixedSize(20, 20)
|
|
300
|
-
self.show_channels.clicked.connect(self.toggle_chan_load)
|
|
301
|
-
control_layout.addWidget(self.show_channels)
|
|
302
|
-
self.chan_load = True
|
|
303
|
-
|
|
304
301
|
# Create the main widget and layout
|
|
305
302
|
main_widget = QWidget()
|
|
306
303
|
self.setCentralWidget(main_widget)
|
|
@@ -1005,7 +1002,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1005
1002
|
pass
|
|
1006
1003
|
|
|
1007
1004
|
# Update display
|
|
1008
|
-
self.update_display(preserve_zoom=(current_xlim, current_ylim)
|
|
1005
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
1006
|
+
|
|
1007
|
+
if self.pan_mode:
|
|
1008
|
+
self.create_pan_background()
|
|
1009
|
+
current_xlim = self.ax.get_xlim()
|
|
1010
|
+
current_ylim = self.ax.get_ylim()
|
|
1011
|
+
self.update_display_pan_mode(current_xlim, current_ylim)
|
|
1012
|
+
|
|
1013
|
+
|
|
1009
1014
|
|
|
1010
1015
|
def create_mini_overlay(self, node_indices = None, edge_indices = None):
|
|
1011
1016
|
|
|
@@ -1979,6 +1984,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1979
1984
|
except:
|
|
1980
1985
|
pass
|
|
1981
1986
|
|
|
1987
|
+
|
|
1988
|
+
if my_network.network is not None:
|
|
1989
|
+
try:
|
|
1990
|
+
info_dict['Neighbors'] = list(my_network.network.neighbors(label))
|
|
1991
|
+
except:
|
|
1992
|
+
pass
|
|
1993
|
+
|
|
1982
1994
|
if my_network.communities is not None:
|
|
1983
1995
|
try:
|
|
1984
1996
|
info_dict['Community'] = my_network.communities[label]
|
|
@@ -2012,6 +2024,24 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2012
2024
|
|
|
2013
2025
|
info_dict['Object Class'] = 'Edge'
|
|
2014
2026
|
|
|
2027
|
+
try:
|
|
2028
|
+
# Get the existing DataFrame from the model
|
|
2029
|
+
original_df = self.network_table.model()._data
|
|
2030
|
+
|
|
2031
|
+
# Create mask for rows where one column is any original node AND the other column is any neighbor
|
|
2032
|
+
mask = (
|
|
2033
|
+
(original_df.iloc[:, 0].isin(self.clicked_values['nodes'])) |
|
|
2034
|
+
(original_df.iloc[:, 1].isin(self.clicked_values['nodes'])) |
|
|
2035
|
+
(original_df.iloc[:, 2].isin(self.clicked_values['edges']))
|
|
2036
|
+
)
|
|
2037
|
+
|
|
2038
|
+
filtered_df = original_df[mask].copy()
|
|
2039
|
+
node_list = list(set(filtered_df.iloc[:, 0].to_list() + filtered_df.iloc[:, 1].to_list()))
|
|
2040
|
+
info_dict["Num Nodes"] = len(node_list)
|
|
2041
|
+
info_dict['Nodes'] = node_list
|
|
2042
|
+
except:
|
|
2043
|
+
pass
|
|
2044
|
+
|
|
2015
2045
|
if my_network.edge_centroids is not None:
|
|
2016
2046
|
try:
|
|
2017
2047
|
info_dict['Centroid'] = my_network.edge_centroids[label]
|
|
@@ -2440,19 +2470,18 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2440
2470
|
print(f"Error: {e}")
|
|
2441
2471
|
|
|
2442
2472
|
|
|
2443
|
-
def
|
|
2473
|
+
def home(self):
|
|
2474
|
+
|
|
2475
|
+
self.update_display()
|
|
2476
|
+
|
|
2444
2477
|
|
|
2445
|
-
if self.show_channels.isChecked():
|
|
2446
|
-
self.chan_load = True
|
|
2447
|
-
else:
|
|
2448
|
-
self.chan_load = False
|
|
2449
2478
|
|
|
2450
2479
|
def toggle_highlight(self):
|
|
2451
2480
|
self.highlight = self.high_button.isChecked()
|
|
2452
2481
|
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2453
2482
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2454
2483
|
|
|
2455
|
-
if self.high_button.isChecked() and self.machine_window is None:
|
|
2484
|
+
if self.high_button.isChecked() and self.machine_window is None and not self.preview:
|
|
2456
2485
|
if self.highlight_overlay is None and ((len(self.clicked_values['nodes']) + len(self.clicked_values['edges'])) > 0):
|
|
2457
2486
|
if self.needs_mini:
|
|
2458
2487
|
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
@@ -2471,7 +2500,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2471
2500
|
self.zoom_mode = self.zoom_button.isChecked()
|
|
2472
2501
|
|
|
2473
2502
|
if self.zoom_mode:
|
|
2474
|
-
self.
|
|
2503
|
+
if self.pan_mode:
|
|
2504
|
+
self.pan_button.click()
|
|
2475
2505
|
|
|
2476
2506
|
self.pen_button.setChecked(False)
|
|
2477
2507
|
self.brush_mode = False
|
|
@@ -2547,33 +2577,23 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2547
2577
|
current_xlim = self.ax.get_xlim()
|
|
2548
2578
|
current_ylim = self.ax.get_ylim()
|
|
2549
2579
|
|
|
2550
|
-
|
|
2580
|
+
# Create static background from currently visible channels
|
|
2581
|
+
self.create_pan_background()
|
|
2582
|
+
|
|
2583
|
+
# Hide all channels and show only the background
|
|
2584
|
+
self.channel_visible = [False] * 4
|
|
2585
|
+
self.is_pan_preview = True
|
|
2586
|
+
|
|
2587
|
+
# Get current downsample factor
|
|
2588
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2589
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2590
|
+
# Update display to show only background
|
|
2591
|
+
self.update_display_pan_mode(current_xlim, current_ylim)
|
|
2551
2592
|
|
|
2552
|
-
# Create static background from currently visible channels
|
|
2553
|
-
self.create_pan_background()
|
|
2554
|
-
|
|
2555
|
-
# Hide all channels and show only the background
|
|
2556
|
-
self.channel_visible = [False] * 4
|
|
2557
|
-
self.is_pan_preview = True
|
|
2558
|
-
|
|
2559
|
-
# Get current downsample factor
|
|
2560
|
-
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2561
|
-
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2562
|
-
# Update display to show only background
|
|
2563
|
-
self.update_display_pan_mode(current_xlim, current_ylim)
|
|
2564
|
-
self.needs_update = False
|
|
2565
|
-
else:
|
|
2566
|
-
self.needs_update = True
|
|
2567
|
-
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
2568
|
-
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
2569
|
-
# Update display to show only background
|
|
2570
|
-
self._first_pan_done = False
|
|
2571
|
-
self.update_display(current_xlim, current_ylim)
|
|
2572
2593
|
|
|
2573
2594
|
else:
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
2595
|
+
self.setEnabled(True)
|
|
2596
|
+
self.update_display(preserve_zoom=(self.ax.get_xlim(), self.ax.get_ylim()))
|
|
2577
2597
|
if self.machine_window is None:
|
|
2578
2598
|
self.canvas.setCursor(Qt.CursorShape.ArrowCursor)
|
|
2579
2599
|
else:
|
|
@@ -2584,6 +2604,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2584
2604
|
self.brush_mode = self.pen_button.isChecked()
|
|
2585
2605
|
if self.brush_mode:
|
|
2586
2606
|
|
|
2607
|
+
if self.pan_mode:
|
|
2608
|
+
self.pan_button.click()
|
|
2609
|
+
|
|
2587
2610
|
self.pm = painting.PaintManager(parent = self)
|
|
2588
2611
|
|
|
2589
2612
|
# Start virtual paint session
|
|
@@ -3069,14 +3092,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3069
3092
|
self.ax.set_xlim(new_xlim)
|
|
3070
3093
|
self.ax.set_ylim(new_ylim)
|
|
3071
3094
|
|
|
3072
|
-
|
|
3073
|
-
if self.pan_background_image is not None or self._first_pan_done == True:
|
|
3074
|
-
self.canvas.draw_idle()
|
|
3075
|
-
else:
|
|
3076
|
-
# For the first pan without background, mark that we've done the first pan
|
|
3077
|
-
self._first_pan_done = True
|
|
3078
|
-
# Force a proper display update instead of draw_idle
|
|
3079
|
-
self.update_display(preserve_zoom=(new_xlim, new_ylim))
|
|
3095
|
+
self.canvas.draw_idle()
|
|
3080
3096
|
|
|
3081
3097
|
# Update pan start position
|
|
3082
3098
|
self.pan_start = (event.xdata, event.ydata)
|
|
@@ -3105,18 +3121,36 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3105
3121
|
|
|
3106
3122
|
|
|
3107
3123
|
def create_pan_background(self):
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3124
|
+
"""Create a static background image from currently visible channels with proper rendering"""
|
|
3125
|
+
# Store current zoom state
|
|
3126
|
+
current_xlim = self.ax.get_xlim()
|
|
3127
|
+
current_ylim = self.ax.get_ylim()
|
|
3128
|
+
|
|
3129
|
+
# Try GPU acceleration first, fallback to CPU
|
|
3130
|
+
try:
|
|
3131
|
+
import cupy as cp
|
|
3132
|
+
self.use_gpu = True
|
|
3133
|
+
except ImportError:
|
|
3134
|
+
self.use_gpu = False
|
|
3135
|
+
|
|
3136
|
+
# Render all visible channels with proper colors/brightness into a single composite
|
|
3137
|
+
self.channel_visible = self.pre_pan_channel_state.copy()
|
|
3138
|
+
try:
|
|
3139
|
+
if self.use_gpu:
|
|
3140
|
+
self.pan_background_image = self.create_composite_for_pan_gpu()
|
|
3141
|
+
else:
|
|
3142
|
+
self.pan_background_image = self.create_composite_for_pan()
|
|
3143
|
+
except Exception as e:
|
|
3144
|
+
print(f'GPU implementation failed: {e}, falling back to CPU')
|
|
3145
|
+
self.use_gpu = False
|
|
3146
|
+
self.pan_background_image = self.create_composite_for_pan()
|
|
3147
|
+
|
|
3148
|
+
self.pan_zoom_state = (current_xlim, current_ylim)
|
|
3117
3149
|
|
|
3118
|
-
def
|
|
3119
|
-
"""Create a properly rendered composite image for panning with
|
|
3150
|
+
def create_composite_for_pan_gpu(self):
|
|
3151
|
+
"""Create a properly rendered composite image for panning with GPU acceleration"""
|
|
3152
|
+
import cupy as cp
|
|
3153
|
+
|
|
3120
3154
|
# Get active channels and dimensions (copied from update_display)
|
|
3121
3155
|
active_channels = [i for i in range(4) if self.channel_data[i] is not None]
|
|
3122
3156
|
if active_channels:
|
|
@@ -3134,6 +3168,28 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3134
3168
|
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
3135
3169
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
3136
3170
|
|
|
3171
|
+
# Helper function to crop and downsample image on GPU
|
|
3172
|
+
def crop_and_downsample_image_gpu(image_cp, y_start, y_end, x_start, x_end, factor):
|
|
3173
|
+
# Crop first
|
|
3174
|
+
if len(image_cp.shape) == 2:
|
|
3175
|
+
cropped = image_cp[y_start:y_end, x_start:x_end]
|
|
3176
|
+
elif len(image_cp.shape) == 3:
|
|
3177
|
+
cropped = image_cp[y_start:y_end, x_start:x_end, :]
|
|
3178
|
+
else:
|
|
3179
|
+
cropped = image_cp
|
|
3180
|
+
|
|
3181
|
+
# Then downsample if needed
|
|
3182
|
+
if factor == 1:
|
|
3183
|
+
return cropped
|
|
3184
|
+
|
|
3185
|
+
if len(cropped.shape) == 2:
|
|
3186
|
+
return cropped[::factor, ::factor]
|
|
3187
|
+
elif len(cropped.shape) == 3:
|
|
3188
|
+
return cropped[::factor, ::factor, :]
|
|
3189
|
+
else:
|
|
3190
|
+
return cropped
|
|
3191
|
+
|
|
3192
|
+
min_height, min_width = self.original_dims
|
|
3137
3193
|
|
|
3138
3194
|
# Calculate the visible region in pixel coordinates
|
|
3139
3195
|
x_min = max(0, int(np.floor(current_xlim[0] + 0.5)))
|
|
@@ -3141,41 +3197,281 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3141
3197
|
y_min = max(0, int(np.floor(current_ylim[1] + 0.5))) # Note: y is flipped
|
|
3142
3198
|
y_max = min(min_height, int(np.ceil(current_ylim[0] + 0.5)))
|
|
3143
3199
|
|
|
3144
|
-
box_len = x_max - x_min
|
|
3145
|
-
box_height = y_max - y_min
|
|
3200
|
+
box_len = int((x_max - x_min)/2)
|
|
3201
|
+
box_height = int((y_max - y_min)/2)
|
|
3146
3202
|
x_min = max(0, x_min - box_len)
|
|
3147
3203
|
x_max = min(self.shape[2], x_max + box_len)
|
|
3148
3204
|
y_min = max(0, y_min - box_height)
|
|
3149
3205
|
y_max = min(self.shape[1], y_max + box_height)
|
|
3150
3206
|
|
|
3151
|
-
# If using image pyramids
|
|
3152
3207
|
size = (x_max - x_min) * (y_max - y_min)
|
|
3153
|
-
val = int(np.ceil(size/(
|
|
3154
|
-
if self.shape[1] * self.shape[2] > 3000 * 3000 * val:
|
|
3155
|
-
val = 3
|
|
3156
|
-
|
|
3208
|
+
val = int(np.ceil(size/(2000 * 2000)))
|
|
3157
3209
|
self.validate_downsample_input(text = val, update = False)
|
|
3158
3210
|
|
|
3159
3211
|
downsample_factor = self.downsample_factor
|
|
3160
3212
|
|
|
3161
|
-
#
|
|
3162
|
-
|
|
3163
|
-
|
|
3213
|
+
# Add some padding to avoid edge artifacts during pan/zoom
|
|
3214
|
+
padding = max(10, downsample_factor * 2)
|
|
3215
|
+
x_min_padded = max(0, x_min - padding)
|
|
3216
|
+
x_max_padded = min(min_width, x_max + padding)
|
|
3217
|
+
y_min_padded = max(0, y_min - padding)
|
|
3218
|
+
y_max_padded = min(min_height, y_max + padding)
|
|
3219
|
+
|
|
3220
|
+
display_height = (y_max_padded - y_min_padded) // downsample_factor
|
|
3221
|
+
display_width = (x_max_padded - x_min_padded) // downsample_factor
|
|
3164
3222
|
|
|
3165
|
-
#
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3223
|
+
# Create a blank RGBA composite to accumulate all channels (using display dimensions) - on GPU
|
|
3224
|
+
composite = cp.zeros((display_height, display_width, 4), dtype=cp.float32)
|
|
3225
|
+
|
|
3226
|
+
# Process each visible channel exactly like update_display does
|
|
3227
|
+
for channel in range(4):
|
|
3228
|
+
if (self.channel_visible[channel] and
|
|
3229
|
+
self.channel_data[channel] is not None):
|
|
3230
|
+
|
|
3231
|
+
# Get current slice data (same logic as update_display)
|
|
3232
|
+
is_rgb = len(self.channel_data[channel].shape) == 4 and (self.channel_data[channel].shape[-1] == 3 or self.channel_data[channel].shape[-1] == 4)
|
|
3233
|
+
|
|
3234
|
+
if len(self.channel_data[channel].shape) == 3 and not is_rgb:
|
|
3235
|
+
current_image = self.channel_data[channel][self.current_slice, :, :]
|
|
3236
|
+
elif is_rgb:
|
|
3237
|
+
current_image = self.channel_data[channel][self.current_slice]
|
|
3238
|
+
else:
|
|
3239
|
+
current_image = self.channel_data[channel]
|
|
3240
|
+
|
|
3241
|
+
# Convert to CuPy array and crop/downsample on GPU
|
|
3242
|
+
current_image_cp = cp.asarray(current_image)
|
|
3243
|
+
display_image_cp = crop_and_downsample_image_gpu(
|
|
3244
|
+
current_image_cp, y_min_padded, y_max_padded,
|
|
3245
|
+
x_min_padded, x_max_padded, downsample_factor)
|
|
3246
|
+
|
|
3247
|
+
if is_rgb and self.channel_data[channel].shape[-1] == 3:
|
|
3248
|
+
# RGB image - convert to RGBA and blend
|
|
3249
|
+
rgb_alpha = cp.ones((*display_image_cp.shape[:2], 4), dtype=cp.float32)
|
|
3250
|
+
rgb_alpha[:, :, :3] = display_image_cp.astype(cp.float32) / 255.0
|
|
3251
|
+
rgb_alpha[:, :, 3] = 0.7 # Same alpha as update_display
|
|
3252
|
+
composite = self.blend_layers_gpu(composite, rgb_alpha)
|
|
3253
|
+
|
|
3254
|
+
elif is_rgb and self.channel_data[channel].shape[-1] == 4:
|
|
3255
|
+
# RGBA image - blend directly
|
|
3256
|
+
rgba_image = display_image_cp.astype(cp.float32) / 255.0
|
|
3257
|
+
composite = self.blend_layers_gpu(composite, rgba_image)
|
|
3258
|
+
|
|
3259
|
+
else:
|
|
3260
|
+
# Regular channel processing (same logic as update_display)
|
|
3261
|
+
if self.min_max[channel][0] == None:
|
|
3262
|
+
self.min_max[channel][0] = cp.asnumpy(cp.min(current_image_cp))
|
|
3263
|
+
if self.min_max[channel][1] == None:
|
|
3264
|
+
self.min_max[channel][1] = cp.asnumpy(cp.max(current_image_cp))
|
|
3265
|
+
|
|
3266
|
+
img_min = self.min_max[channel][0]
|
|
3267
|
+
img_max = self.min_max[channel][1]
|
|
3268
|
+
|
|
3269
|
+
if img_min == img_max:
|
|
3270
|
+
vmin = img_min
|
|
3271
|
+
vmax = img_min + 1
|
|
3272
|
+
else:
|
|
3273
|
+
vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
|
|
3274
|
+
vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
|
|
3275
|
+
|
|
3276
|
+
# Normalize the downsampled image on GPU
|
|
3277
|
+
if vmin == vmax:
|
|
3278
|
+
normalized_image = cp.zeros_like(display_image_cp)
|
|
3279
|
+
else:
|
|
3280
|
+
normalized_image = cp.clip((display_image_cp - vmin) / (vmax - vmin), 0, 1)
|
|
3281
|
+
|
|
3282
|
+
# Apply channel color and alpha
|
|
3283
|
+
if channel == 2 and self.machine_window is not None:
|
|
3284
|
+
# Special case for machine window channel 2
|
|
3285
|
+
channel_rgba = self.apply_machine_colormap_gpu(display_image_cp)
|
|
3286
|
+
else:
|
|
3287
|
+
# Regular channel with custom color
|
|
3288
|
+
color = self.base_colors[channel]
|
|
3289
|
+
channel_rgba = cp.zeros((*normalized_image.shape, 4), dtype=cp.float32)
|
|
3290
|
+
channel_rgba[:, :, 0] = normalized_image * color[0] # R
|
|
3291
|
+
channel_rgba[:, :, 1] = normalized_image * color[1] # G
|
|
3292
|
+
channel_rgba[:, :, 2] = normalized_image * color[2] # B
|
|
3293
|
+
channel_rgba[:, :, 3] = normalized_image * 0.7 # A (same alpha as update_display)
|
|
3294
|
+
|
|
3295
|
+
# Blend this channel into the composite
|
|
3296
|
+
composite = self.blend_layers_gpu(composite, channel_rgba)
|
|
3297
|
+
|
|
3298
|
+
# Add highlight overlays if they exist (with downsampling)
|
|
3299
|
+
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
3300
|
+
mini_overlay_cp = cp.asarray(self.mini_overlay_data)
|
|
3301
|
+
display_overlay = crop_and_downsample_image_gpu(mini_overlay_cp, y_min_padded, y_max_padded, x_min_padded, x_max_padded, downsample_factor)
|
|
3302
|
+
highlight_rgba = self.create_highlight_rgba_gpu(display_overlay, yellow=True)
|
|
3303
|
+
composite = self.blend_layers_gpu(composite, highlight_rgba)
|
|
3304
|
+
elif self.highlight_overlay is not None and self.highlight:
|
|
3305
|
+
highlight_slice = self.highlight_overlay[self.current_slice]
|
|
3306
|
+
highlight_slice_cp = cp.asarray(highlight_slice)
|
|
3307
|
+
display_highlight = crop_and_downsample_image_gpu(highlight_slice_cp, y_min_padded, y_max_padded, x_min_padded, x_max_padded, downsample_factor)
|
|
3308
|
+
if self.machine_window is None:
|
|
3309
|
+
highlight_rgba = self.create_highlight_rgba_gpu(display_highlight, yellow=True)
|
|
3310
|
+
else:
|
|
3311
|
+
highlight_rgba = self.create_highlight_rgba_gpu(display_highlight, yellow=False)
|
|
3312
|
+
composite = self.blend_layers_gpu(composite, highlight_rgba)
|
|
3313
|
+
|
|
3314
|
+
# Convert back to CPU and to 0-255 range for display
|
|
3315
|
+
composite_cpu = cp.asnumpy(composite)
|
|
3316
|
+
return (composite_cpu * 255).astype(np.uint8)
|
|
3317
|
+
|
|
3318
|
+
def apply_machine_colormap_gpu(self, image_cp):
|
|
3319
|
+
"""Apply the special machine window colormap for channel 2 - GPU version"""
|
|
3320
|
+
import cupy as cp
|
|
3321
|
+
|
|
3322
|
+
rgba = cp.zeros((*image_cp.shape, 4), dtype=cp.float32)
|
|
3323
|
+
|
|
3324
|
+
# Transparent for 0
|
|
3325
|
+
mask_0 = (image_cp == 0)
|
|
3326
|
+
rgba[mask_0] = cp.array([0, 0, 0, 0])
|
|
3327
|
+
|
|
3328
|
+
# Light green for 1
|
|
3329
|
+
mask_1 = (image_cp == 1)
|
|
3330
|
+
rgba[mask_1] = cp.array([0.5, 1, 0.5, 0.7])
|
|
3331
|
+
|
|
3332
|
+
# Light red for 2
|
|
3333
|
+
mask_2 = (image_cp == 2)
|
|
3334
|
+
rgba[mask_2] = cp.array([1, 0.5, 0.5, 0.7])
|
|
3335
|
+
|
|
3336
|
+
return rgba
|
|
3337
|
+
|
|
3338
|
+
def create_highlight_rgba_gpu(self, highlight_data_cp, yellow=True):
|
|
3339
|
+
"""Create RGBA highlight overlay - GPU version"""
|
|
3340
|
+
import cupy as cp
|
|
3341
|
+
|
|
3342
|
+
rgba = cp.zeros((*highlight_data_cp.shape, 4), dtype=cp.float32)
|
|
3343
|
+
|
|
3344
|
+
if yellow:
|
|
3345
|
+
# Yellow highlight
|
|
3346
|
+
mask = highlight_data_cp > 0
|
|
3347
|
+
rgba[mask] = cp.array([1, 1, 0, 0.8]) # Yellow with alpha 0.8
|
|
3348
|
+
else:
|
|
3349
|
+
# Multi-color highlight for machine window
|
|
3350
|
+
mask_1 = (highlight_data_cp == 1)
|
|
3351
|
+
mask_2 = (highlight_data_cp == 2)
|
|
3352
|
+
rgba[mask_1] = cp.array([1, 1, 0, 0.5]) # Yellow for 1
|
|
3353
|
+
rgba[mask_2] = cp.array([0, 0.7, 1, 0.5]) # Blue for 2
|
|
3354
|
+
|
|
3355
|
+
return rgba
|
|
3356
|
+
|
|
3357
|
+
def blend_layers_gpu(self, base_cp, overlay_cp):
|
|
3358
|
+
"""Alpha blend two RGBA layers - GPU version"""
|
|
3359
|
+
import cupy as cp
|
|
3360
|
+
|
|
3361
|
+
def resize_overlay_to_base_gpu(overlay_arr_cp, base_arr_cp):
|
|
3362
|
+
base_height, base_width = base_arr_cp.shape[:2]
|
|
3363
|
+
overlay_height, overlay_width = overlay_arr_cp.shape[:2]
|
|
3364
|
+
|
|
3365
|
+
# First crop if overlay is larger
|
|
3366
|
+
cropped_overlay = overlay_arr_cp[:base_height, :base_width]
|
|
3367
|
+
|
|
3368
|
+
# Then pad if still smaller after cropping
|
|
3369
|
+
current_height, current_width = cropped_overlay.shape[:2]
|
|
3370
|
+
pad_height = base_height - current_height
|
|
3371
|
+
pad_width = base_width - current_width
|
|
3372
|
+
|
|
3373
|
+
if pad_height > 0 or pad_width > 0:
|
|
3374
|
+
cropped_overlay = cp.pad(cropped_overlay,
|
|
3375
|
+
((0, pad_height), (0, pad_width), (0, 0)),
|
|
3376
|
+
mode='constant', constant_values=0)
|
|
3169
3377
|
|
|
3170
|
-
|
|
3378
|
+
return cropped_overlay
|
|
3379
|
+
|
|
3380
|
+
# Resize the ENTIRE overlay array to match base dimensions
|
|
3381
|
+
if overlay_cp.shape[:2] != base_cp.shape[:2]:
|
|
3382
|
+
overlay_cp = resize_overlay_to_base_gpu(overlay_cp, base_cp)
|
|
3383
|
+
|
|
3384
|
+
# Now extract alpha channels (they should be the same size)
|
|
3385
|
+
alpha_overlay = overlay_cp[:, :, 3:4]
|
|
3386
|
+
alpha_base = base_cp[:, :, 3:4]
|
|
3387
|
+
|
|
3388
|
+
# Calculate output alpha
|
|
3389
|
+
alpha_out = alpha_overlay + alpha_base * (1 - alpha_overlay)
|
|
3390
|
+
|
|
3391
|
+
# Calculate output RGB
|
|
3392
|
+
rgb_out = cp.zeros_like(base_cp[:, :, :3])
|
|
3393
|
+
mask = alpha_out[:, :, 0] > 0
|
|
3394
|
+
|
|
3395
|
+
rgb_out[mask] = (overlay_cp[mask, :3] * alpha_overlay[mask] +
|
|
3396
|
+
base_cp[mask, :3] * alpha_base[mask] * (1 - alpha_overlay[mask])) / alpha_out[mask]
|
|
3397
|
+
|
|
3398
|
+
# Combine RGB and alpha
|
|
3399
|
+
result = cp.zeros_like(base_cp)
|
|
3400
|
+
result[:, :, :3] = rgb_out
|
|
3401
|
+
result[:, :, 3:4] = alpha_out
|
|
3402
|
+
|
|
3403
|
+
return result
|
|
3404
|
+
|
|
3405
|
+
def create_composite_for_pan(self):
|
|
3406
|
+
"""Create a properly rendered composite image for panning with downsample support"""
|
|
3407
|
+
# Get active channels and dimensions (copied from update_display)
|
|
3408
|
+
active_channels = [i for i in range(4) if self.channel_data[i] is not None]
|
|
3409
|
+
if active_channels:
|
|
3410
|
+
dims = [(self.channel_data[i].shape[1:3] if len(self.channel_data[i].shape) >= 3 else
|
|
3411
|
+
self.channel_data[i].shape) for i in active_channels]
|
|
3412
|
+
min_height = min(d[0] for d in dims)
|
|
3413
|
+
min_width = min(d[1] for d in dims)
|
|
3414
|
+
else:
|
|
3415
|
+
return None
|
|
3416
|
+
|
|
3417
|
+
# Store original dimensions for coordinate mapping
|
|
3418
|
+
self.original_dims = (min_height, min_width)
|
|
3419
|
+
|
|
3420
|
+
# Get current downsample factor
|
|
3421
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
3422
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
3423
|
+
|
|
3424
|
+
# Helper function to crop and downsample image
|
|
3425
|
+
def crop_and_downsample_image(image, y_start, y_end, x_start, x_end, factor):
|
|
3426
|
+
# Crop first
|
|
3171
3427
|
if len(image.shape) == 2:
|
|
3172
|
-
|
|
3173
|
-
return image[::factor, ::factor]
|
|
3428
|
+
cropped = image[y_start:y_end, x_start:x_end]
|
|
3174
3429
|
elif len(image.shape) == 3:
|
|
3175
|
-
|
|
3176
|
-
return image[::factor, ::factor, :]
|
|
3430
|
+
cropped = image[y_start:y_end, x_start:x_end, :]
|
|
3177
3431
|
else:
|
|
3178
|
-
|
|
3432
|
+
cropped = image
|
|
3433
|
+
|
|
3434
|
+
# Then downsample if needed
|
|
3435
|
+
if factor == 1:
|
|
3436
|
+
return cropped
|
|
3437
|
+
|
|
3438
|
+
if len(cropped.shape) == 2:
|
|
3439
|
+
return cropped[::factor, ::factor]
|
|
3440
|
+
elif len(cropped.shape) == 3:
|
|
3441
|
+
return cropped[::factor, ::factor, :]
|
|
3442
|
+
else:
|
|
3443
|
+
return cropped
|
|
3444
|
+
|
|
3445
|
+
min_height, min_width = self.original_dims
|
|
3446
|
+
|
|
3447
|
+
# Calculate the visible region in pixel coordinates
|
|
3448
|
+
x_min = max(0, int(np.floor(current_xlim[0] + 0.5)))
|
|
3449
|
+
x_max = min(min_width, int(np.ceil(current_xlim[1] + 0.5)))
|
|
3450
|
+
y_min = max(0, int(np.floor(current_ylim[1] + 0.5))) # Note: y is flipped
|
|
3451
|
+
y_max = min(min_height, int(np.ceil(current_ylim[0] + 0.5)))
|
|
3452
|
+
|
|
3453
|
+
box_len = int((x_max - x_min)/2)
|
|
3454
|
+
box_height = int((y_max - y_min)/2)
|
|
3455
|
+
x_min = max(0, x_min - box_len)
|
|
3456
|
+
x_max = min(self.shape[2], x_max + box_len)
|
|
3457
|
+
y_min = max(0, y_min - box_height)
|
|
3458
|
+
y_max = min(self.shape[1], y_max + box_height)
|
|
3459
|
+
|
|
3460
|
+
size = (x_max - x_min) * (y_max - y_min)
|
|
3461
|
+
val = max(2, int(np.ceil(size/(2000 * 2000))))
|
|
3462
|
+
self.validate_downsample_input(text = val, update = False)
|
|
3463
|
+
|
|
3464
|
+
downsample_factor = self.downsample_factor
|
|
3465
|
+
|
|
3466
|
+
# Add some padding to avoid edge artifacts during pan/zoom
|
|
3467
|
+
padding = max(10, downsample_factor * 2)
|
|
3468
|
+
x_min_padded = max(0, x_min - padding)
|
|
3469
|
+
x_max_padded = min(min_width, x_max + padding)
|
|
3470
|
+
y_min_padded = max(0, y_min - padding)
|
|
3471
|
+
y_max_padded = min(min_height, y_max + padding)
|
|
3472
|
+
|
|
3473
|
+
display_height = (y_max_padded - y_min_padded) // downsample_factor
|
|
3474
|
+
display_width = (x_max_padded - x_min_padded) // downsample_factor
|
|
3179
3475
|
|
|
3180
3476
|
# Create a blank RGBA composite to accumulate all channels (using display dimensions)
|
|
3181
3477
|
composite = np.zeros((display_height, display_width, 4), dtype=np.float32)
|
|
@@ -3195,8 +3491,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3195
3491
|
else:
|
|
3196
3492
|
current_image = self.channel_data[channel]
|
|
3197
3493
|
|
|
3198
|
-
#
|
|
3199
|
-
display_image =
|
|
3494
|
+
# Crop and downsample the image for rendering
|
|
3495
|
+
display_image = crop_and_downsample_image(
|
|
3496
|
+
current_image, y_min_padded, y_max_padded,
|
|
3497
|
+
x_min_padded, x_max_padded, downsample_factor)
|
|
3200
3498
|
|
|
3201
3499
|
if is_rgb and self.channel_data[channel].shape[-1] == 3:
|
|
3202
3500
|
# RGB image - convert to RGBA and blend
|
|
@@ -3251,12 +3549,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3251
3549
|
|
|
3252
3550
|
# Add highlight overlays if they exist (with downsampling)
|
|
3253
3551
|
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
3254
|
-
display_overlay =
|
|
3552
|
+
display_overlay = crop_and_downsample_image(self.mini_overlay_data, y_min_padded, y_max_padded, x_min_padded, x_max_padded, downsample_factor)
|
|
3255
3553
|
highlight_rgba = self.create_highlight_rgba(display_overlay, yellow=True)
|
|
3256
3554
|
composite = self.blend_layers(composite, highlight_rgba)
|
|
3257
3555
|
elif self.highlight_overlay is not None and self.highlight:
|
|
3258
3556
|
highlight_slice = self.highlight_overlay[self.current_slice]
|
|
3259
|
-
display_highlight =
|
|
3557
|
+
display_highlight = crop_and_downsample_image(highlight_slice, y_min_padded, y_max_padded, x_min_padded, x_max_padded, downsample_factor)
|
|
3260
3558
|
if self.machine_window is None:
|
|
3261
3559
|
highlight_rgba = self.create_highlight_rgba(display_highlight, yellow=True)
|
|
3262
3560
|
else:
|
|
@@ -3266,7 +3564,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3266
3564
|
# Convert to 0-255 range for display
|
|
3267
3565
|
return (composite * 255).astype(np.uint8)
|
|
3268
3566
|
|
|
3269
|
-
|
|
3270
3567
|
def apply_machine_colormap(self, image):
|
|
3271
3568
|
"""Apply the special machine window colormap for channel 2"""
|
|
3272
3569
|
rgba = np.zeros((*image.shape, 4), dtype=np.float32)
|
|
@@ -3368,18 +3665,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3368
3665
|
height *= downsample_factor
|
|
3369
3666
|
width *= downsample_factor
|
|
3370
3667
|
|
|
3371
|
-
def crop_image(image, y_start, y_end, x_start, x_end):
|
|
3372
|
-
# Crop
|
|
3373
|
-
if len(image.shape) == 2:
|
|
3374
|
-
cropped = image[y_start:y_end, x_start:x_end]
|
|
3375
|
-
elif len(image.shape) == 3:
|
|
3376
|
-
cropped = image[y_start:y_end, x_start:x_end, :]
|
|
3377
|
-
else:
|
|
3378
|
-
cropped = image
|
|
3379
|
-
|
|
3380
|
-
return cropped
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
3668
|
downsample_factor = self.downsample_factor
|
|
3384
3669
|
min_height, min_width = self.original_dims
|
|
3385
3670
|
|
|
@@ -3389,8 +3674,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3389
3674
|
y_min = max(0, int(np.floor(current_ylim[1] + 0.5))) # Note: y is flipped
|
|
3390
3675
|
y_max = min(min_height, int(np.ceil(current_ylim[0] + 0.5)))
|
|
3391
3676
|
|
|
3392
|
-
box_len = x_max - x_min
|
|
3393
|
-
box_height = y_max - y_min
|
|
3677
|
+
box_len = int((x_max - x_min)/2)
|
|
3678
|
+
box_height = int((y_max - y_min)/2)
|
|
3394
3679
|
x_min = max(0, x_min - box_len)
|
|
3395
3680
|
x_max = min(self.shape[2], x_max + box_len)
|
|
3396
3681
|
y_min = max(0, y_min - box_height)
|
|
@@ -3418,11 +3703,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3418
3703
|
x_max_padded_ds = min(self.pan_background_image.shape[1], x_max_ds + padding_ds)
|
|
3419
3704
|
y_min_padded_ds = max(0, y_min_ds - padding_ds)
|
|
3420
3705
|
y_max_padded_ds = min(self.pan_background_image.shape[0], y_max_ds + padding_ds)
|
|
3421
|
-
|
|
3422
|
-
# Crop using downsampled coordinates
|
|
3423
|
-
display_image = crop_image(
|
|
3424
|
-
self.pan_background_image, y_min_padded_ds, y_max_padded_ds,
|
|
3425
|
-
x_min_padded_ds, x_max_padded_ds)
|
|
3426
3706
|
|
|
3427
3707
|
# Calculate the extent for the cropped region (in original coordinates)
|
|
3428
3708
|
crop_extent = (x_min_padded - 0.5, x_max_padded - 0.5,
|
|
@@ -3430,7 +3710,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3430
3710
|
|
|
3431
3711
|
# Display the composite background with preserved zoom
|
|
3432
3712
|
# Use extent to stretch downsampled image back to original coordinate space
|
|
3433
|
-
self.ax.imshow(
|
|
3713
|
+
self.ax.imshow(self.pan_background_image,
|
|
3434
3714
|
extent=crop_extent,
|
|
3435
3715
|
aspect='equal')
|
|
3436
3716
|
|
|
@@ -3490,11 +3770,17 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3490
3770
|
# Update display to show only background
|
|
3491
3771
|
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
3492
3772
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
3493
|
-
#
|
|
3494
|
-
|
|
3773
|
+
#self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
3774
|
+
self.setEnabled(False)
|
|
3775
|
+
|
|
3776
|
+
try:
|
|
3777
|
+
self.create_pan_background()
|
|
3778
|
+
current_xlim = self.ax.get_xlim()
|
|
3779
|
+
current_ylim = self.ax.get_ylim()
|
|
3495
3780
|
self.update_display_pan_mode(current_xlim, current_ylim)
|
|
3496
|
-
|
|
3497
|
-
|
|
3781
|
+
finally:
|
|
3782
|
+
# Re-enable the widget when done
|
|
3783
|
+
self.setEnabled(True)
|
|
3498
3784
|
|
|
3499
3785
|
self.panning = False
|
|
3500
3786
|
self.pan_start = None
|
|
@@ -3561,8 +3847,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3561
3847
|
self.create_highlight_overlay(node_indices=self.clicked_values['nodes'])
|
|
3562
3848
|
|
|
3563
3849
|
# Try to highlight the last selected value in tables
|
|
3564
|
-
if self.clicked_values['nodes']:
|
|
3850
|
+
if len(self.clicked_values['nodes']) == 1:
|
|
3565
3851
|
self.highlight_value_in_tables(self.clicked_values['nodes'][-1])
|
|
3852
|
+
self.handle_info('node')
|
|
3566
3853
|
|
|
3567
3854
|
elif self.active_channel == 1: # Edges
|
|
3568
3855
|
if not ctrl_pressed:
|
|
@@ -3577,8 +3864,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3577
3864
|
self.create_highlight_overlay(edge_indices=self.clicked_values['edges'])
|
|
3578
3865
|
|
|
3579
3866
|
# Try to highlight the last selected value in tables
|
|
3580
|
-
if self.clicked_values['edges']:
|
|
3867
|
+
if len(self.clicked_values['edges']):
|
|
3581
3868
|
self.highlight_value_in_tables(self.clicked_values['edges'][-1])
|
|
3869
|
+
self.handle_info('edge')
|
|
3582
3870
|
|
|
3583
3871
|
elif not self.selecting and self.selection_start: # If we had a click but never started selection
|
|
3584
3872
|
# Handle as a normal click
|
|
@@ -3957,9 +4245,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3957
4245
|
load_action.triggered.connect(lambda: self.load_misc('Edge Centroids'))
|
|
3958
4246
|
load_action = misc_menu.addAction("Load Node Communities")
|
|
3959
4247
|
load_action.triggered.connect(lambda: self.load_misc('Communities'))
|
|
3960
|
-
|
|
4248
|
+
node_identities = file_menu.addMenu('Images -> Node Identities')
|
|
4249
|
+
load_action = node_identities.addAction("Merge Labeled Images Into Nodes")
|
|
3961
4250
|
load_action.triggered.connect(lambda: self.load_misc('Merge Nodes'))
|
|
3962
|
-
load_action =
|
|
4251
|
+
load_action = node_identities.addAction("Assign Node Identities From Overlap With Other Images")
|
|
3963
4252
|
load_action.triggered.connect(self.show_merge_node_id_dialog)
|
|
3964
4253
|
|
|
3965
4254
|
|
|
@@ -4012,8 +4301,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4012
4301
|
id_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Identity'))
|
|
4013
4302
|
umap_action = overlay_menu.addAction("Centroid UMAP")
|
|
4014
4303
|
umap_action.triggered.connect(self.handle_centroid_umap)
|
|
4015
|
-
iden_umap_action = overlay_menu.addAction("Identity UMAP (If any nodes were assigned multiple identities)")
|
|
4016
|
-
iden_umap_action.triggered.connect(self.handle_iden_umap)
|
|
4017
4304
|
|
|
4018
4305
|
rand_menu = analysis_menu.addMenu("Randomize")
|
|
4019
4306
|
random_action = rand_menu.addAction("Generate Equivalent Random Network")
|
|
@@ -4080,10 +4367,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4080
4367
|
gennodes_action.triggered.connect(self.show_gennodes_dialog)
|
|
4081
4368
|
branch_action = generate_menu.addAction("Label Branches")
|
|
4082
4369
|
branch_action.triggered.connect(lambda: self.show_branch_dialog())
|
|
4083
|
-
genvor_action = generate_menu.addAction("Generate Voronoi Diagram
|
|
4370
|
+
genvor_action = generate_menu.addAction("Generate Voronoi Diagram - goes in Overlay2")
|
|
4084
4371
|
genvor_action.triggered.connect(self.voronoi)
|
|
4085
4372
|
|
|
4086
|
-
modify_action = process_menu.addAction("Modify Network")
|
|
4373
|
+
modify_action = process_menu.addAction("Modify Network/Properties")
|
|
4087
4374
|
modify_action.triggered.connect(self.show_modify_dialog)
|
|
4088
4375
|
|
|
4089
4376
|
|
|
@@ -4102,8 +4389,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4102
4389
|
idoverlay_action.triggered.connect(self.show_idoverlay_dialog)
|
|
4103
4390
|
coloroverlay_action = overlay_menu.addAction("Color Nodes (or Edges)")
|
|
4104
4391
|
coloroverlay_action.triggered.connect(self.show_coloroverlay_dialog)
|
|
4105
|
-
#searchoverlay_action = overlay_menu.addAction("Show Search Regions")
|
|
4106
|
-
#searchoverlay_action.triggered.connect(self.show_search_dialog)
|
|
4107
4392
|
shuffle_action = overlay_menu.addAction("Shuffle")
|
|
4108
4393
|
shuffle_action.triggered.connect(self.show_shuffle_dialog)
|
|
4109
4394
|
arbitrary_action = image_menu.addAction("Select Objects")
|
|
@@ -4433,6 +4718,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4433
4718
|
# Create new table
|
|
4434
4719
|
table = CustomTableView(self)
|
|
4435
4720
|
table.setModel(PandasModel(df))
|
|
4721
|
+
|
|
4722
|
+
try:
|
|
4723
|
+
first_column_name = table.model()._data.columns[0]
|
|
4724
|
+
table.sort_table(first_column_name, ascending=True)
|
|
4725
|
+
except:
|
|
4726
|
+
pass
|
|
4436
4727
|
|
|
4437
4728
|
# Add to tabbed widget
|
|
4438
4729
|
if title is None:
|
|
@@ -4440,17 +4731,27 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4440
4731
|
else:
|
|
4441
4732
|
self.tabbed_data.add_table(f"{title}", table)
|
|
4442
4733
|
|
|
4734
|
+
|
|
4735
|
+
|
|
4443
4736
|
# Adjust column widths to content
|
|
4444
4737
|
for column in range(table.model().columnCount(None)):
|
|
4445
4738
|
table.resizeColumnToContents(column)
|
|
4446
4739
|
|
|
4447
4740
|
except:
|
|
4448
|
-
|
|
4741
|
+
pass
|
|
4449
4742
|
|
|
4450
4743
|
def show_merge_node_id_dialog(self):
|
|
4451
4744
|
|
|
4452
|
-
|
|
4453
|
-
|
|
4745
|
+
if my_network.nodes is None:
|
|
4746
|
+
QMessageBox.critical(
|
|
4747
|
+
self,
|
|
4748
|
+
"Error",
|
|
4749
|
+
"Please load your segmented cells into 'Nodes' channel first"
|
|
4750
|
+
)
|
|
4751
|
+
return
|
|
4752
|
+
else:
|
|
4753
|
+
dialog = MergeNodeIdDialog(self)
|
|
4754
|
+
dialog.exec()
|
|
4454
4755
|
|
|
4455
4756
|
def show_gray_water_dialog(self):
|
|
4456
4757
|
"""Show the gray watershed parameter dialog."""
|
|
@@ -4686,18 +4987,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4686
4987
|
|
|
4687
4988
|
try:
|
|
4688
4989
|
|
|
4689
|
-
|
|
4690
|
-
shape = my_network.nodes.shape
|
|
4691
|
-
else:
|
|
4692
|
-
shape = None
|
|
4693
|
-
|
|
4694
|
-
if my_network.node_centroids is None:
|
|
4695
|
-
self.show_centroid_dialog()
|
|
4696
|
-
if my_network.node_centroids is None:
|
|
4697
|
-
print("Node centroids must be set")
|
|
4698
|
-
return
|
|
4699
|
-
|
|
4700
|
-
array = pxt.create_voronoi_3d_kdtree(my_network.node_centroids, shape)
|
|
4990
|
+
array = sdl.smart_dilate(self.channel_data[self.active_channel], dilate_xy = np.max(self.shape), dilate_z = np.max(self.shape), use_dt_dil_amount = np.max(self.shape), fast_dil = False)
|
|
4701
4991
|
self.load_channel(3, array, True)
|
|
4702
4992
|
|
|
4703
4993
|
except Exception as e:
|
|
@@ -4753,10 +5043,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4753
5043
|
dialog = ColorOverlayDialog(self)
|
|
4754
5044
|
dialog.exec()
|
|
4755
5045
|
|
|
4756
|
-
def show_search_dialog(self):
|
|
4757
|
-
"""Show the search dialog"""
|
|
4758
|
-
dialog = SearchOverlayDialog(self)
|
|
4759
|
-
dialog.exec()
|
|
4760
5046
|
|
|
4761
5047
|
def show_shuffle_dialog(self):
|
|
4762
5048
|
"""Show the shuffle dialog"""
|
|
@@ -4832,29 +5118,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4832
5118
|
if sort == 'Node Identities':
|
|
4833
5119
|
my_network.load_node_identities(file_path = filename)
|
|
4834
5120
|
|
|
4835
|
-
"""
|
|
4836
|
-
first_value = list(my_network.node_identities.values())[0] # Check that there are not multiple IDs
|
|
4837
|
-
if isinstance(first_value, (list, tuple)):
|
|
4838
|
-
trump_value, ok = QInputDialog.getText(
|
|
4839
|
-
self,
|
|
4840
|
-
'Multiple IDs Detected',
|
|
4841
|
-
'The node identities appear to contain multiple ids per node in a list.\n'
|
|
4842
|
-
'If you desire one node ID to trump all others, enter it here.\n'
|
|
4843
|
-
'(Enter "-" to have the first IDs trump all others)\n'
|
|
4844
|
-
'(Enter "/" to have multi-ID nodes be split into many nodes sharing a centroid)\n'
|
|
4845
|
-
'(Close this window to continue with multi-ID nodes)'
|
|
4846
|
-
)
|
|
4847
|
-
if not ok or trump_value.strip() == '':
|
|
4848
|
-
trump_value = None
|
|
4849
|
-
elif trump_value.upper() == '-':
|
|
4850
|
-
trump_value = '-'
|
|
4851
|
-
elif trump_value.upper() == "/":
|
|
4852
|
-
trump_value = '/'
|
|
4853
|
-
my_network.node_identities = uncork(my_network.node_identities, trump_value)
|
|
4854
|
-
else:
|
|
4855
|
-
trump_value = None
|
|
4856
|
-
my_network.node_identities = uncork(my_network.node_identities, trump_value)
|
|
4857
|
-
"""
|
|
4858
5121
|
|
|
4859
5122
|
if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
|
|
4860
5123
|
try:
|
|
@@ -4903,6 +5166,14 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4903
5166
|
elif sort == 'Merge Nodes':
|
|
4904
5167
|
try:
|
|
4905
5168
|
|
|
5169
|
+
if my_network.nodes is None:
|
|
5170
|
+
QMessageBox.critical(
|
|
5171
|
+
self,
|
|
5172
|
+
"Error",
|
|
5173
|
+
"Please load your first set of nodes into the 'Nodes' channel first"
|
|
5174
|
+
)
|
|
5175
|
+
return
|
|
5176
|
+
|
|
4906
5177
|
if len(np.unique(my_network.nodes)) < 3:
|
|
4907
5178
|
self.show_label_dialog()
|
|
4908
5179
|
|
|
@@ -4916,6 +5187,21 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4916
5187
|
|
|
4917
5188
|
msg.exec()
|
|
4918
5189
|
|
|
5190
|
+
# Also if they want centroids:
|
|
5191
|
+
msg2 = QMessageBox()
|
|
5192
|
+
msg2.setWindowTitle("Selection Type")
|
|
5193
|
+
msg2.setText("Would you like to compute node centroids for each image prior to merging?")
|
|
5194
|
+
yes_button = msg2.addButton("Yes", QMessageBox.ButtonRole.AcceptRole)
|
|
5195
|
+
no_button = msg2.addButton("No", QMessageBox.ButtonRole.AcceptRole)
|
|
5196
|
+
msg2.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
|
|
5197
|
+
|
|
5198
|
+
msg2.exec()
|
|
5199
|
+
|
|
5200
|
+
if msg2.clickedButton() == yes_button:
|
|
5201
|
+
centroids = True
|
|
5202
|
+
else:
|
|
5203
|
+
centroids = False
|
|
5204
|
+
|
|
4919
5205
|
if msg.clickedButton() == tiff_button:
|
|
4920
5206
|
# Code for selecting TIFF files
|
|
4921
5207
|
filename, _ = QFileDialog.getOpenFileName(
|
|
@@ -4938,7 +5224,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4938
5224
|
if dialog.exec() == QFileDialog.DialogCode.Accepted:
|
|
4939
5225
|
selected_path = dialog.directory().absolutePath()
|
|
4940
5226
|
|
|
4941
|
-
my_network.merge_nodes(selected_path, root_id = self.node_name)
|
|
5227
|
+
my_network.merge_nodes(selected_path, root_id = self.node_name, centroids = centroids)
|
|
4942
5228
|
self.load_channel(0, my_network.nodes, True)
|
|
4943
5229
|
|
|
4944
5230
|
|
|
@@ -4947,8 +5233,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4947
5233
|
self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
|
|
4948
5234
|
except Exception as e:
|
|
4949
5235
|
print(f"Error loading node identity table: {e}")
|
|
5236
|
+
if centroids:
|
|
5237
|
+
self.format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
|
|
5238
|
+
|
|
4950
5239
|
|
|
4951
5240
|
except Exception as e:
|
|
5241
|
+
|
|
4952
5242
|
QMessageBox.critical(
|
|
4953
5243
|
self,
|
|
4954
5244
|
"Error Merging",
|
|
@@ -5448,12 +5738,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5448
5738
|
if all(not btn.isEnabled() for btn in self.channel_buttons[:channel_index]):
|
|
5449
5739
|
self.set_active_channel(channel_index)
|
|
5450
5740
|
|
|
5451
|
-
if self.
|
|
5452
|
-
|
|
5453
|
-
self.channel_buttons[channel_index].click()
|
|
5454
|
-
else:
|
|
5455
|
-
if self.channel_buttons[channel_index].isChecked():
|
|
5456
|
-
self.channel_buttons[channel_index].click()
|
|
5741
|
+
if not self.channel_buttons[channel_index].isChecked():
|
|
5742
|
+
self.channel_buttons[channel_index].click()
|
|
5457
5743
|
|
|
5458
5744
|
self.min_max[channel_index][0] = np.min(self.channel_data[channel_index])
|
|
5459
5745
|
self.min_max[channel_index][1] = np.max(self.channel_data[channel_index])
|
|
@@ -5470,6 +5756,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5470
5756
|
except:
|
|
5471
5757
|
pass
|
|
5472
5758
|
|
|
5759
|
+
if self.shape == self.channel_data[channel_index].shape:
|
|
5760
|
+
preserve_zoom = (self.ax.get_xlim(), self.ax.get_ylim())
|
|
5473
5761
|
self.shape = self.channel_data[channel_index].shape
|
|
5474
5762
|
if self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor:
|
|
5475
5763
|
self.throttle = True
|
|
@@ -5559,9 +5847,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5559
5847
|
|
|
5560
5848
|
if update:
|
|
5561
5849
|
# Update display
|
|
5562
|
-
self.update_display()
|
|
5850
|
+
self.update_display(preserve_zoom = (self.ax.get_xlim(), self.ax.get_ylim()))
|
|
5563
5851
|
|
|
5564
|
-
def reset(self, nodes = False, network = False, xy_scale = 1, z_scale = 1, edges = False, search_region = False, network_overlay = False, id_overlay = False, update = True):
|
|
5852
|
+
def reset(self, nodes = False, network = False, xy_scale = 1, z_scale = 1, edges = False, search_region = False, network_overlay = False, id_overlay = False, update = True, node_identities = False):
|
|
5565
5853
|
"""Method to flexibly reset certain fields to free up the RAM as desired"""
|
|
5566
5854
|
|
|
5567
5855
|
# Set scales first before any clearing operations
|
|
@@ -5581,6 +5869,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5581
5869
|
# Clear selection table
|
|
5582
5870
|
self.selection_table.setModel(PandasModel(empty_df))
|
|
5583
5871
|
|
|
5872
|
+
if node_identities:
|
|
5873
|
+
my_network.node_identities = None
|
|
5874
|
+
|
|
5584
5875
|
if nodes:
|
|
5585
5876
|
self.delete_channel(0, False, update = update)
|
|
5586
5877
|
|
|
@@ -5691,6 +5982,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5691
5982
|
def toggle_channel(self, channel_index):
|
|
5692
5983
|
"""Toggle visibility of a channel."""
|
|
5693
5984
|
# Store current zoom settings before toggling
|
|
5985
|
+
if self.pan_mode:
|
|
5986
|
+
self.pan_button.click()
|
|
5987
|
+
|
|
5694
5988
|
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
5695
5989
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
5696
5990
|
|
|
@@ -5728,7 +6022,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5728
6022
|
# Now convert to real data
|
|
5729
6023
|
self.pm.convert_virtual_strokes_to_data()
|
|
5730
6024
|
self.current_slice = slice_value
|
|
5731
|
-
if self.
|
|
6025
|
+
if self.preview:
|
|
6026
|
+
self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
|
|
6027
|
+
elif self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
|
|
5732
6028
|
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
5733
6029
|
if not self.hold_update:
|
|
5734
6030
|
self.update_display(preserve_zoom=view_settings)
|
|
@@ -5763,9 +6059,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5763
6059
|
self.measurement_artists = []
|
|
5764
6060
|
self.axes_initialized = False
|
|
5765
6061
|
self.original_dims = None
|
|
5766
|
-
|
|
6062
|
+
|
|
5767
6063
|
# Handle special states (pan, static background)
|
|
5768
|
-
if self.pan_background_image is not None:
|
|
6064
|
+
if self.pan_background_image is not None and not self.pan_mode:
|
|
5769
6065
|
self.channel_visible = self.pre_pan_channel_state.copy()
|
|
5770
6066
|
self.is_pan_preview = False
|
|
5771
6067
|
self.pan_background_image = None
|
|
@@ -5774,7 +6070,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5774
6070
|
self.resume = False
|
|
5775
6071
|
if self.prev_down != self.downsample_factor:
|
|
5776
6072
|
self.validate_downsample_input(text = self.prev_down)
|
|
5777
|
-
return
|
|
5778
6073
|
|
|
5779
6074
|
if self.static_background is not None:
|
|
5780
6075
|
# Your existing virtual strokes conversion logic
|
|
@@ -5989,8 +6284,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5989
6284
|
vmin=0, vmax=1, extent=crop_extent)
|
|
5990
6285
|
|
|
5991
6286
|
# Handle preview, overlays, and measurements (apply cropping here too)
|
|
5992
|
-
if self.preview and not called:
|
|
5993
|
-
|
|
6287
|
+
#if self.preview and not called:
|
|
6288
|
+
# self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
|
|
5994
6289
|
|
|
5995
6290
|
# Overlay handling (optimized with cropping and downsampling)
|
|
5996
6291
|
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
@@ -6204,12 +6499,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6204
6499
|
|
|
6205
6500
|
my_network.centroid_umap()
|
|
6206
6501
|
|
|
6207
|
-
def handle_iden_umap(self):
|
|
6208
|
-
|
|
6209
|
-
if my_network.node_identities is None:
|
|
6210
|
-
return
|
|
6211
|
-
|
|
6212
|
-
my_network.identity_umap()
|
|
6213
6502
|
|
|
6214
6503
|
def closeEvent(self, event):
|
|
6215
6504
|
"""Override closeEvent to close all windows when main window closes"""
|
|
@@ -6226,6 +6515,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6226
6515
|
# Force quit the application
|
|
6227
6516
|
QCoreApplication.quit()
|
|
6228
6517
|
|
|
6518
|
+
exit()
|
|
6519
|
+
|
|
6229
6520
|
|
|
6230
6521
|
|
|
6231
6522
|
#TABLE RELATED:
|
|
@@ -6468,11 +6759,7 @@ class CustomTableView(QTableView):
|
|
|
6468
6759
|
self.resizeColumnToContents(col)
|
|
6469
6760
|
|
|
6470
6761
|
except Exception as e:
|
|
6471
|
-
|
|
6472
|
-
self,
|
|
6473
|
-
"Error",
|
|
6474
|
-
f"Error sorting table: {str(e)}"
|
|
6475
|
-
)
|
|
6762
|
+
pass
|
|
6476
6763
|
|
|
6477
6764
|
def save_table_as(self, file_type):
|
|
6478
6765
|
"""Save the table data as either CSV or Excel file."""
|
|
@@ -6992,8 +7279,13 @@ class PropertiesDialog(QDialog):
|
|
|
6992
7279
|
self.network.setChecked(self.check_checked(my_network.network))
|
|
6993
7280
|
layout.addRow("Network Status", self.network)
|
|
6994
7281
|
|
|
7282
|
+
self.node_identities = QPushButton("Node Identities")
|
|
7283
|
+
self.node_identities.setCheckable(True)
|
|
7284
|
+
self.node_identities.setChecked(self.check_checked(my_network.node_identities))
|
|
7285
|
+
layout.addRow("Identities Status", self.node_identities)
|
|
7286
|
+
|
|
6995
7287
|
# Add Run button
|
|
6996
|
-
run_button = QPushButton("Enter")
|
|
7288
|
+
run_button = QPushButton("Enter (Erases Unchecked Properties)")
|
|
6997
7289
|
run_button.clicked.connect(self.run_properties)
|
|
6998
7290
|
layout.addWidget(run_button)
|
|
6999
7291
|
|
|
@@ -7030,8 +7322,9 @@ class PropertiesDialog(QDialog):
|
|
|
7030
7322
|
id_overlay = not self.id_overlay.isChecked()
|
|
7031
7323
|
search_region = not self.search_region.isChecked()
|
|
7032
7324
|
network = not self.network.isChecked()
|
|
7325
|
+
node_identities = not self.node_identities.isChecked()
|
|
7033
7326
|
|
|
7034
|
-
self.parent().reset(nodes = nodes, edges = edges, search_region = search_region, network_overlay = network_overlay, id_overlay = id_overlay, network = network, xy_scale = xy_scale, z_scale = z_scale)
|
|
7327
|
+
self.parent().reset(nodes = nodes, edges = edges, search_region = search_region, network_overlay = network_overlay, id_overlay = id_overlay, network = network, xy_scale = xy_scale, z_scale = z_scale, node_identities = node_identities)
|
|
7035
7328
|
|
|
7036
7329
|
self.accept()
|
|
7037
7330
|
|
|
@@ -7239,7 +7532,7 @@ class ColorDialog(QDialog):
|
|
|
7239
7532
|
self.parent().base_colors[i] = new_color
|
|
7240
7533
|
|
|
7241
7534
|
# Update the display
|
|
7242
|
-
self.parent().update_display()
|
|
7535
|
+
self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
7243
7536
|
self.accept()
|
|
7244
7537
|
|
|
7245
7538
|
class ArbitraryDialog(QDialog):
|
|
@@ -7459,78 +7752,252 @@ class ArbitraryDialog(QDialog):
|
|
|
7459
7752
|
except Exception as e:
|
|
7460
7753
|
QMessageBox.critical(self, "Error", f"Error processing selections: {str(e)}")
|
|
7461
7754
|
|
|
7462
|
-
class MergeNodeIdDialog(QDialog):
|
|
7463
7755
|
|
|
7756
|
+
class MergeNodeIdDialog(QDialog):
|
|
7464
7757
|
def __init__(self, parent=None):
|
|
7758
|
+
|
|
7465
7759
|
super().__init__(parent)
|
|
7760
|
+
|
|
7466
7761
|
self.setWindowTitle("Merging Node Identities From Folder Dialog.\nNote that you should prelabel or prewatershed your current node objects before doing this. (See Process -> Image) It does not label them for you.")
|
|
7467
7762
|
self.setModal(True)
|
|
7468
7763
|
|
|
7469
7764
|
layout = QFormLayout(self)
|
|
7470
|
-
|
|
7471
7765
|
self.search = QLineEdit("")
|
|
7472
7766
|
layout.addRow("Step-out distance (from current nodes image - ignore if you dilated them previously or don't want):", self.search)
|
|
7473
|
-
|
|
7474
7767
|
self.xy_scale = QLineEdit(f"{my_network.xy_scale}")
|
|
7475
7768
|
layout.addRow("xy_scale:", self.xy_scale)
|
|
7476
|
-
|
|
7477
7769
|
self.z_scale = QLineEdit(f"{my_network.z_scale}")
|
|
7478
7770
|
layout.addRow("z_scale:", self.z_scale)
|
|
7771
|
+
self.mode_selector = QComboBox()
|
|
7772
|
+
self.mode_selector.addItems(["Auto-Binarize(Otsu)/Presegmented", "Manual (Interactive Thresholder)"])
|
|
7773
|
+
self.mode_selector.setCurrentIndex(1) # Default to Mode 1
|
|
7774
|
+
layout.addRow("Binarization Strategy:", self.mode_selector)
|
|
7479
7775
|
|
|
7480
|
-
|
|
7481
|
-
self.
|
|
7776
|
+
self.umap = QPushButton("If using manual threshold: Also generate identity UMAP?")
|
|
7777
|
+
self.umap.setCheckable(True)
|
|
7778
|
+
self.umap.setChecked(True)
|
|
7779
|
+
layout.addWidget(self.umap)
|
|
7780
|
+
|
|
7781
|
+
self.include = QPushButton("Include When a Node is Negative for an ID?")
|
|
7482
7782
|
self.include.setCheckable(True)
|
|
7483
|
-
self.include.setChecked(
|
|
7783
|
+
self.include.setChecked(False)
|
|
7484
7784
|
layout.addWidget(self.include)
|
|
7485
|
-
|
|
7486
|
-
# Add Run button
|
|
7785
|
+
|
|
7487
7786
|
run_button = QPushButton("Get Directory")
|
|
7488
7787
|
run_button.clicked.connect(self.run)
|
|
7489
7788
|
layout.addWidget(run_button)
|
|
7490
7789
|
|
|
7491
|
-
def
|
|
7790
|
+
def wait_for_threshold_processing(self):
|
|
7791
|
+
"""
|
|
7792
|
+
Opens ThresholdWindow and waits for user to process the image.
|
|
7793
|
+
Returns True if completed, False if cancelled.
|
|
7794
|
+
The thresholded image will be available in the main window after completion.
|
|
7795
|
+
"""
|
|
7796
|
+
# Create event loop to wait for user
|
|
7797
|
+
loop = QEventLoop()
|
|
7798
|
+
result = {'completed': False}
|
|
7799
|
+
|
|
7800
|
+
# Create the threshold window
|
|
7801
|
+
thresh_window = ThresholdWindow(self.parent(), 4)
|
|
7802
|
+
|
|
7803
|
+
# Connect signals
|
|
7804
|
+
def on_processing_complete():
|
|
7805
|
+
result['completed'] = True
|
|
7806
|
+
loop.quit()
|
|
7807
|
+
|
|
7808
|
+
def on_processing_cancelled():
|
|
7809
|
+
result['completed'] = False
|
|
7810
|
+
loop.quit()
|
|
7811
|
+
|
|
7812
|
+
thresh_window.processing_complete.connect(on_processing_complete)
|
|
7813
|
+
thresh_window.processing_cancelled.connect(on_processing_cancelled)
|
|
7814
|
+
|
|
7815
|
+
# Show window and wait
|
|
7816
|
+
thresh_window.show()
|
|
7817
|
+
thresh_window.raise_()
|
|
7818
|
+
thresh_window.activateWindow()
|
|
7819
|
+
|
|
7820
|
+
# Block until user clicks "Apply Threshold & Continue" or "Cancel"
|
|
7821
|
+
loop.exec()
|
|
7822
|
+
|
|
7823
|
+
# Clean up
|
|
7824
|
+
thresh_window.deleteLater()
|
|
7825
|
+
|
|
7826
|
+
return result['completed']
|
|
7492
7827
|
|
|
7828
|
+
def run(self):
|
|
7493
7829
|
try:
|
|
7494
7830
|
|
|
7495
7831
|
search = float(self.search.text()) if self.search.text().strip() else 0
|
|
7496
7832
|
xy_scale = float(self.xy_scale.text()) if self.xy_scale.text().strip() else 1
|
|
7497
7833
|
z_scale = float(self.z_scale.text()) if self.z_scale.text().strip() else 1
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
7834
|
data = self.parent().channel_data[0]
|
|
7501
7835
|
include = self.include.isChecked()
|
|
7502
|
-
|
|
7836
|
+
umap = self.umap.isChecked()
|
|
7837
|
+
|
|
7503
7838
|
if data is None:
|
|
7504
7839
|
return
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7840
|
+
|
|
7508
7841
|
dialog = QFileDialog(self)
|
|
7509
7842
|
dialog.setOption(QFileDialog.Option.DontUseNativeDialog)
|
|
7510
7843
|
dialog.setOption(QFileDialog.Option.ReadOnly)
|
|
7511
7844
|
dialog.setFileMode(QFileDialog.FileMode.Directory)
|
|
7512
7845
|
dialog.setViewMode(QFileDialog.ViewMode.Detail)
|
|
7513
|
-
|
|
7846
|
+
|
|
7514
7847
|
if dialog.exec() == QFileDialog.DialogCode.Accepted:
|
|
7515
7848
|
selected_path = dialog.directory().absolutePath()
|
|
7516
|
-
|
|
7849
|
+
else:
|
|
7850
|
+
return # User cancelled directory selection
|
|
7851
|
+
|
|
7517
7852
|
if search > 0:
|
|
7518
|
-
data = sdl.smart_dilate(data, 1, 1, GPU
|
|
7853
|
+
data = sdl.smart_dilate(data, 1, 1, GPU=False, fast_dil=False,
|
|
7854
|
+
use_dt_dil_amount=search, xy_scale=xy_scale, z_scale=z_scale)
|
|
7855
|
+
|
|
7856
|
+
# Check if manual mode is selected
|
|
7857
|
+
if self.mode_selector.currentIndex() == 1: # Manual mode
|
|
7519
7858
|
|
|
7520
|
-
|
|
7859
|
+
if my_network.node_identities is None: # Prepare modular dict
|
|
7521
7860
|
|
|
7522
|
-
|
|
7861
|
+
my_network.node_identities = {}
|
|
7523
7862
|
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7863
|
+
nodes = list(np.unique(data))
|
|
7864
|
+
if 0 in nodes:
|
|
7865
|
+
del nodes[0]
|
|
7866
|
+
for node in nodes:
|
|
7867
|
+
|
|
7868
|
+
my_network.node_identities[node] = [] # Assign to lists at first
|
|
7869
|
+
else:
|
|
7870
|
+
for node, iden in my_network.node_identities.items():
|
|
7871
|
+
try:
|
|
7872
|
+
my_network.node_identities[node] = ast.literal_eval(iden)
|
|
7873
|
+
except:
|
|
7874
|
+
my_network.node_identities[node] = [iden]
|
|
7875
|
+
|
|
7876
|
+
id_dicts = my_network.get_merge_node_dictionaries(selected_path, data)
|
|
7877
|
+
|
|
7878
|
+
# For loop example - get threshold for multiple images/data
|
|
7879
|
+
results = []
|
|
7880
|
+
|
|
7881
|
+
img_list = n3d.directory_info(selected_path)
|
|
7882
|
+
data_backup = copy.deepcopy(data)
|
|
7883
|
+
self.parent().load_channel(0, data, data = True)
|
|
7884
|
+
self.hide()
|
|
7885
|
+
self.parent().highlight_overlay = None
|
|
7886
|
+
|
|
7887
|
+
good_list = []
|
|
7888
|
+
|
|
7889
|
+
for i, img in enumerate(img_list):
|
|
7890
|
+
|
|
7891
|
+
if img.endswith('.tiff') or img.endswith('.tif'):
|
|
7892
|
+
|
|
7893
|
+
print(f"Please threshold {img}")
|
|
7894
|
+
|
|
7895
|
+
|
|
7896
|
+
mask = tifffile.imread(f'{selected_path}/{img}')
|
|
7897
|
+
self.parent().load_channel(2, mask, data = True)
|
|
7898
|
+
|
|
7899
|
+
# Wait for user to threshold this data
|
|
7900
|
+
self.parent().special_dict = id_dicts[i]
|
|
7901
|
+
processing_completed = self.wait_for_threshold_processing()
|
|
7902
|
+
|
|
7903
|
+
if not processing_completed:
|
|
7904
|
+
# User cancelled, ask if they want to continue
|
|
7905
|
+
reply = QMessageBox.question(self, 'Continue?',
|
|
7906
|
+
f'Threshold cancelled for item {i+1}. Continue with remaining items?',
|
|
7907
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
7908
|
+
if reply == QMessageBox.StandardButton.No:
|
|
7909
|
+
break
|
|
7910
|
+
continue
|
|
7911
|
+
|
|
7912
|
+
# At this point, the thresholded image is in the main window's memory
|
|
7913
|
+
# Get the processed/thresholded data from wherever ThresholdWindow stored it
|
|
7914
|
+
thresholded_vals = list(np.unique(self.parent().channel_data[0]))
|
|
7915
|
+
if 0 in thresholded_vals:
|
|
7916
|
+
del thresholded_vals[0]
|
|
7917
|
+
|
|
7918
|
+
if img.endswith('.tiff'):
|
|
7919
|
+
base_name = img[:-5]
|
|
7920
|
+
elif img.endswith('.tif'):
|
|
7921
|
+
base_name = img[:-4]
|
|
7922
|
+
else:
|
|
7923
|
+
base_name = img
|
|
7924
|
+
|
|
7925
|
+
assigned = {}
|
|
7926
|
+
|
|
7927
|
+
for node in my_network.node_identities.keys():
|
|
7928
|
+
|
|
7929
|
+
try:
|
|
7930
|
+
|
|
7931
|
+
if int(node) in thresholded_vals:
|
|
7932
|
+
|
|
7933
|
+
my_network.node_identities[node].append(f'{base_name}+')
|
|
7934
|
+
|
|
7935
|
+
elif include:
|
|
7936
|
+
|
|
7937
|
+
my_network.node_identities[node].append(f'{base_name}-')
|
|
7938
|
+
|
|
7939
|
+
except:
|
|
7940
|
+
pass
|
|
7941
|
+
|
|
7942
|
+
# Process the thresholded data
|
|
7943
|
+
self.parent().highlight_overlay = None
|
|
7944
|
+
self.parent().load_channel(0, data_backup, data = True)
|
|
7945
|
+
good_list.append(base_name)
|
|
7946
|
+
|
|
7947
|
+
modify_dict = copy.deepcopy(my_network.node_identities)
|
|
7948
|
+
|
|
7949
|
+
for node, iden in my_network.node_identities.items():
|
|
7950
|
+
|
|
7951
|
+
try:
|
|
7952
|
+
|
|
7953
|
+
if len(iden) == 1:
|
|
7954
|
+
|
|
7955
|
+
modify_dict[node] = str(iden[0]) # Singleton lists become bare strings
|
|
7956
|
+
elif len(iden) == 0:
|
|
7957
|
+
del modify_dict[node]
|
|
7958
|
+
else:
|
|
7959
|
+
modify_dict[node] = str(iden) # We hold multi element lists as strings for compatibility
|
|
7960
|
+
|
|
7961
|
+
except:
|
|
7962
|
+
pass
|
|
7963
|
+
|
|
7964
|
+
my_network.node_identities = modify_dict
|
|
7965
|
+
|
|
7966
|
+
self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity')
|
|
7967
|
+
|
|
7968
|
+
all_keys = id_dicts[0].keys()
|
|
7969
|
+
result = {key: np.array([d[key] for d in id_dicts]) for key in all_keys}
|
|
7970
|
+
|
|
7971
|
+
|
|
7972
|
+
self.parent().format_for_upperright_table(result, 'NodeID', good_list, 'Mean Intensity')
|
|
7973
|
+
if umap:
|
|
7974
|
+
my_network.identity_umap(result)
|
|
7529
7975
|
|
|
7530
|
-
|
|
7976
|
+
|
|
7977
|
+
QMessageBox.information(
|
|
7978
|
+
self,
|
|
7979
|
+
"Success",
|
|
7980
|
+
"Node Identities Merged. New IDs represent presence of corresponding img foreground with +, absence with -. Please save your new identities as csv, then use File -> Load -> Load From Excel Helper to bulk search and rename desired combinations. (Press Help [above] for more info)"
|
|
7981
|
+
)
|
|
7982
|
+
|
|
7983
|
+
self.accept()
|
|
7984
|
+
else:
|
|
7985
|
+
my_network.merge_node_ids(selected_path, data, include)
|
|
7986
|
+
|
|
7987
|
+
self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity')
|
|
7988
|
+
|
|
7989
|
+
QMessageBox.information(
|
|
7990
|
+
self,
|
|
7991
|
+
"Success",
|
|
7992
|
+
"Node Identities Merged. New IDs represent presence of corresponding img foreground with +, absence with -. Please save your new identities as csv, then use File -> Load -> Load From Excel Helper to bulk search and rename desired combinations. (Press Help [above] for more info)"
|
|
7993
|
+
)
|
|
7994
|
+
|
|
7995
|
+
self.accept()
|
|
7531
7996
|
|
|
7532
7997
|
except Exception as e:
|
|
7533
|
-
|
|
7998
|
+
import traceback
|
|
7999
|
+
print(traceback.format_exc())
|
|
8000
|
+
#print(f"Error: {e}")
|
|
7534
8001
|
|
|
7535
8002
|
|
|
7536
8003
|
class Show3dDialog(QDialog):
|
|
@@ -7643,7 +8110,7 @@ class NetOverlayDialog(QDialog):
|
|
|
7643
8110
|
|
|
7644
8111
|
my_network.network_overlay = my_network.draw_network()
|
|
7645
8112
|
|
|
7646
|
-
self.parent().load_channel(2, channel_data = my_network.network_overlay, data = True)
|
|
8113
|
+
self.parent().load_channel(2, channel_data = my_network.network_overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
7647
8114
|
|
|
7648
8115
|
self.accept()
|
|
7649
8116
|
|
|
@@ -7651,34 +8118,6 @@ class NetOverlayDialog(QDialog):
|
|
|
7651
8118
|
|
|
7652
8119
|
print(f"Error with Overlay Generation: {e}")
|
|
7653
8120
|
|
|
7654
|
-
class SearchOverlayDialog(QDialog):
|
|
7655
|
-
|
|
7656
|
-
def __init__(self, parent=None):
|
|
7657
|
-
|
|
7658
|
-
super().__init__(parent)
|
|
7659
|
-
self.setWindowTitle("Generate Search Region Overlay?")
|
|
7660
|
-
self.setModal(True)
|
|
7661
|
-
|
|
7662
|
-
layout = QFormLayout(self)
|
|
7663
|
-
|
|
7664
|
-
# Add Run button
|
|
7665
|
-
run_button = QPushButton("Generate (Will go to Overlay 2)")
|
|
7666
|
-
run_button.clicked.connect(self.searchoverlay)
|
|
7667
|
-
layout.addWidget(run_button)
|
|
7668
|
-
|
|
7669
|
-
def searchoverlay(self):
|
|
7670
|
-
|
|
7671
|
-
try:
|
|
7672
|
-
|
|
7673
|
-
my_network.id_overlay = my_network.search_region
|
|
7674
|
-
|
|
7675
|
-
self.parent().load_channel(3, channel_data = my_network.search_region, data = True)
|
|
7676
|
-
|
|
7677
|
-
self.accept()
|
|
7678
|
-
|
|
7679
|
-
except Exception as e:
|
|
7680
|
-
|
|
7681
|
-
print(f"Error with Overlay Generation: {e}")
|
|
7682
8121
|
|
|
7683
8122
|
class IdOverlayDialog(QDialog):
|
|
7684
8123
|
|
|
@@ -7732,7 +8171,7 @@ class IdOverlayDialog(QDialog):
|
|
|
7732
8171
|
my_network.id_overlay = my_network.draw_edge_indices()
|
|
7733
8172
|
|
|
7734
8173
|
|
|
7735
|
-
self.parent().load_channel(3, channel_data = my_network.id_overlay, data = True)
|
|
8174
|
+
self.parent().load_channel(3, channel_data = my_network.id_overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
7736
8175
|
|
|
7737
8176
|
self.accept()
|
|
7738
8177
|
|
|
@@ -7781,7 +8220,7 @@ class ColorOverlayDialog(QDialog):
|
|
|
7781
8220
|
self.parent().format_for_upperright_table(legend, f'{self.sort} Id', f'Encoding Val: {self.sort}', 'Legend')
|
|
7782
8221
|
|
|
7783
8222
|
|
|
7784
|
-
self.parent().load_channel(3, channel_data = result, data = True)
|
|
8223
|
+
self.parent().load_channel(3, channel_data = result, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
7785
8224
|
|
|
7786
8225
|
self.accept()
|
|
7787
8226
|
|
|
@@ -7849,7 +8288,7 @@ class ShuffleDialog(QDialog):
|
|
|
7849
8288
|
except:
|
|
7850
8289
|
self.parent().highlight_overay = None
|
|
7851
8290
|
else:
|
|
7852
|
-
self.parent().load_channel(accepted_mode, channel_data = target_data, data = True)
|
|
8291
|
+
self.parent().load_channel(accepted_mode, channel_data = target_data, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
7853
8292
|
except:
|
|
7854
8293
|
pass
|
|
7855
8294
|
|
|
@@ -7861,14 +8300,14 @@ class ShuffleDialog(QDialog):
|
|
|
7861
8300
|
except:
|
|
7862
8301
|
self.parent().highlight_overlay = None
|
|
7863
8302
|
else:
|
|
7864
|
-
self.parent().load_channel(accepted_target, channel_data = active_data, data = True)
|
|
8303
|
+
self.parent().load_channel(accepted_target, channel_data = active_data, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
7865
8304
|
except:
|
|
7866
8305
|
pass
|
|
7867
8306
|
|
|
7868
8307
|
|
|
7869
8308
|
|
|
7870
8309
|
|
|
7871
|
-
self.parent().update_display()
|
|
8310
|
+
self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
7872
8311
|
|
|
7873
8312
|
self.accept()
|
|
7874
8313
|
|
|
@@ -8476,10 +8915,10 @@ class NearNeighDialog(QDialog):
|
|
|
8476
8915
|
avg, output, quant_overlay, pred = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, quant = quant, centroids = centroids)
|
|
8477
8916
|
else:
|
|
8478
8917
|
avg, output, overlay, quant_overlay, pred = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, numpy = True, quant = quant, centroids = centroids)
|
|
8479
|
-
self.parent().load_channel(3, overlay, data = True)
|
|
8918
|
+
self.parent().load_channel(3, overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
8480
8919
|
|
|
8481
8920
|
if quant_overlay is not None:
|
|
8482
|
-
self.parent().load_channel(2, quant_overlay, data = True)
|
|
8921
|
+
self.parent().load_channel(2, quant_overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
8483
8922
|
|
|
8484
8923
|
avg = {header:avg}
|
|
8485
8924
|
|
|
@@ -8595,6 +9034,8 @@ class NeighborIdentityDialog(QDialog):
|
|
|
8595
9034
|
|
|
8596
9035
|
self.accept()
|
|
8597
9036
|
except Exception as e:
|
|
9037
|
+
import traceback
|
|
9038
|
+
print(traceback.format_exc())
|
|
8598
9039
|
print(f"Error: {e}")
|
|
8599
9040
|
|
|
8600
9041
|
|
|
@@ -8826,7 +9267,7 @@ class HeatmapDialog(QDialog):
|
|
|
8826
9267
|
else:
|
|
8827
9268
|
|
|
8828
9269
|
heat_dict, overlay = my_network.community_heatmap(num_nodes = nodecount, is3d = is3d, numpy = True)
|
|
8829
|
-
self.parent().load_channel(3, overlay, data = True)
|
|
9270
|
+
self.parent().load_channel(3, overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
8830
9271
|
|
|
8831
9272
|
|
|
8832
9273
|
self.parent().format_for_upperright_table(heat_dict, metric='Community', value='ln(Predicted Community Nodecount/Actual)', title="Community Heatmap")
|
|
@@ -9183,7 +9624,7 @@ class DegreeDialog(QDialog):
|
|
|
9183
9624
|
nodes = n3d.upsample_with_padding(nodes, down_factor, original_shape)
|
|
9184
9625
|
|
|
9185
9626
|
if accepted_mode > 0:
|
|
9186
|
-
self.parent().load_channel(3, channel_data = nodes, data = True)
|
|
9627
|
+
self.parent().load_channel(3, channel_data = nodes, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
9187
9628
|
|
|
9188
9629
|
|
|
9189
9630
|
self.accept()
|
|
@@ -9246,7 +9687,7 @@ class HubDialog(QDialog):
|
|
|
9246
9687
|
|
|
9247
9688
|
if img is not None:
|
|
9248
9689
|
|
|
9249
|
-
self.parent().load_channel(3, channel_data = img, data = True)
|
|
9690
|
+
self.parent().load_channel(3, channel_data = img, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
9250
9691
|
|
|
9251
9692
|
|
|
9252
9693
|
self.accept()
|
|
@@ -9305,7 +9746,7 @@ class MotherDialog(QDialog):
|
|
|
9305
9746
|
G = my_network.isolate_mothers(self, ret_nodes = True, called = True)
|
|
9306
9747
|
else:
|
|
9307
9748
|
G, result = my_network.isolate_mothers(self, ret_nodes = False, called = True)
|
|
9308
|
-
self.parent().load_channel(2, channel_data = result, data = True)
|
|
9749
|
+
self.parent().load_channel(2, channel_data = result, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
9309
9750
|
|
|
9310
9751
|
degree_dict = {}
|
|
9311
9752
|
|
|
@@ -9380,8 +9821,7 @@ class CodeDialog(QDialog):
|
|
|
9380
9821
|
|
|
9381
9822
|
self.parent().format_for_upperright_table(output, f'{self.sort} Id', f'Encoding Val: {self.sort}', 'Legend')
|
|
9382
9823
|
|
|
9383
|
-
|
|
9384
|
-
self.parent().load_channel(3, image, True)
|
|
9824
|
+
self.parent().load_channel(3, image, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
9385
9825
|
self.accept()
|
|
9386
9826
|
|
|
9387
9827
|
except Exception as e:
|
|
@@ -9794,7 +10234,7 @@ class BinarizeDialog(QDialog):
|
|
|
9794
10234
|
# Update the corresponding property in my_network
|
|
9795
10235
|
setattr(my_network, network_properties[self.parent().active_channel], result)
|
|
9796
10236
|
|
|
9797
|
-
self.parent().update_display()
|
|
10237
|
+
self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
9798
10238
|
self.accept()
|
|
9799
10239
|
|
|
9800
10240
|
except Exception as e:
|
|
@@ -9846,7 +10286,7 @@ class LabelDialog(QDialog):
|
|
|
9846
10286
|
# Update the corresponding property in my_network
|
|
9847
10287
|
setattr(my_network, network_properties[self.parent().active_channel], result)
|
|
9848
10288
|
|
|
9849
|
-
self.parent().update_display()
|
|
10289
|
+
self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
9850
10290
|
self.accept()
|
|
9851
10291
|
|
|
9852
10292
|
except Exception as e:
|
|
@@ -9929,7 +10369,7 @@ class SLabelDialog(QDialog):
|
|
|
9929
10369
|
|
|
9930
10370
|
binary_array = binary_array * label_array
|
|
9931
10371
|
|
|
9932
|
-
self.parent().load_channel(accepted_target, binary_array, True)
|
|
10372
|
+
self.parent().load_channel(accepted_target, binary_array, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
9933
10373
|
|
|
9934
10374
|
self.accept()
|
|
9935
10375
|
|
|
@@ -10613,6 +11053,10 @@ class MachineWindow(QMainWindow):
|
|
|
10613
11053
|
|
|
10614
11054
|
def start_segmentation(self):
|
|
10615
11055
|
|
|
11056
|
+
if self.parent().pan_mode:
|
|
11057
|
+
self.parent().pan_button.click()
|
|
11058
|
+
|
|
11059
|
+
|
|
10616
11060
|
self.parent().static_background = None
|
|
10617
11061
|
|
|
10618
11062
|
self.kill_segmentation()
|
|
@@ -10934,11 +11378,15 @@ class SegmentationWorker(QThread):
|
|
|
10934
11378
|
|
|
10935
11379
|
|
|
10936
11380
|
class ThresholdWindow(QMainWindow):
|
|
11381
|
+
processing_complete = pyqtSignal() # Emitted when user finishes and images are modified
|
|
11382
|
+
processing_cancelled = pyqtSignal() # Emitted when user cancels
|
|
11383
|
+
|
|
10937
11384
|
def __init__(self, parent=None, accepted_mode=0):
|
|
10938
11385
|
super().__init__(parent)
|
|
10939
11386
|
self.setWindowTitle("Threshold")
|
|
10940
11387
|
|
|
10941
11388
|
self.accepted_mode = accepted_mode
|
|
11389
|
+
self.preview = True
|
|
10942
11390
|
|
|
10943
11391
|
# Create central widget and layout
|
|
10944
11392
|
central_widget = QWidget()
|
|
@@ -10970,6 +11418,10 @@ class ThresholdWindow(QMainWindow):
|
|
|
10970
11418
|
self.histo_list = list(self.parent().degree_dict.values())
|
|
10971
11419
|
self.bounds = False
|
|
10972
11420
|
self.parent().bounds = False
|
|
11421
|
+
elif accepted_mode == 4:
|
|
11422
|
+
self.histo_list = list(self.parent().special_dict.values())
|
|
11423
|
+
self.bounds = False
|
|
11424
|
+
self.parent().bounds = False
|
|
10973
11425
|
|
|
10974
11426
|
elif accepted_mode == 0:
|
|
10975
11427
|
targ_shape = self.parent().channel_data[self.parent().active_channel].shape
|
|
@@ -11041,16 +11493,39 @@ class ThresholdWindow(QMainWindow):
|
|
|
11041
11493
|
self.preview.clicked.connect(self.preview_mode)
|
|
11042
11494
|
form_layout.addRow("Show Preview:", self.preview)
|
|
11043
11495
|
|
|
11044
|
-
|
|
11045
|
-
|
|
11046
|
-
|
|
11496
|
+
button_layout = QHBoxLayout()
|
|
11497
|
+
|
|
11498
|
+
|
|
11499
|
+
# Keep your existing Apply Threshold button, but modify its behavior
|
|
11500
|
+
run_button = QPushButton("Apply Threshold/Continue")
|
|
11501
|
+
run_button.clicked.connect(self.apply_and_continue) # New method
|
|
11502
|
+
button_layout.addWidget(run_button)
|
|
11047
11503
|
|
|
11048
|
-
|
|
11504
|
+
# Add Cancel button for external dialog use
|
|
11505
|
+
cancel_button = QPushButton("Cancel/Skip")
|
|
11506
|
+
cancel_button.clicked.connect(self.cancel_processing)
|
|
11507
|
+
button_layout.addWidget(cancel_button)
|
|
11049
11508
|
|
|
11509
|
+
form_layout.addRow(button_layout)
|
|
11510
|
+
layout.addLayout(form_layout)
|
|
11511
|
+
|
|
11050
11512
|
# Set a reasonable default size
|
|
11051
11513
|
self.setMinimumWidth(400)
|
|
11052
11514
|
self.setMinimumHeight(400)
|
|
11053
11515
|
|
|
11516
|
+
def apply_and_continue(self):
|
|
11517
|
+
"""Apply threshold, modify main window images, then signal completion"""
|
|
11518
|
+
self.thresh() # This should modify the main window images
|
|
11519
|
+
|
|
11520
|
+
# Signal that processing is complete
|
|
11521
|
+
self.processing_complete.emit()
|
|
11522
|
+
self.close()
|
|
11523
|
+
|
|
11524
|
+
def cancel_processing(self):
|
|
11525
|
+
"""Cancel without applying changes"""
|
|
11526
|
+
self.processing_cancelled.emit()
|
|
11527
|
+
self.close()
|
|
11528
|
+
|
|
11054
11529
|
def closeEvent(self, event):
|
|
11055
11530
|
self.parent().preview = False
|
|
11056
11531
|
self.parent().targs = None
|
|
@@ -11102,6 +11577,10 @@ class ThresholdWindow(QMainWindow):
|
|
|
11102
11577
|
for node, vol in self.parent().degree_dict.items():
|
|
11103
11578
|
if min_val <= vol <= max_val:
|
|
11104
11579
|
output.append(node)
|
|
11580
|
+
elif self.accepted_mode == 4:
|
|
11581
|
+
for node, vol in self.parent().special_dict.items():
|
|
11582
|
+
if min_val <= vol <= max_val:
|
|
11583
|
+
output.append(node)
|
|
11105
11584
|
return output
|
|
11106
11585
|
|
|
11107
11586
|
def get_values_in_range(self, lst, min_val, max_val):
|
|
@@ -11119,11 +11598,18 @@ class ThresholdWindow(QMainWindow):
|
|
|
11119
11598
|
for item in self.parent().degree_dict:
|
|
11120
11599
|
if self.parent().degree_dict[item] in values:
|
|
11121
11600
|
output.append(item)
|
|
11601
|
+
elif self.accepted_mode == 4:
|
|
11602
|
+
for item in self.parent().special_dict:
|
|
11603
|
+
if self.parent().special_dict[item] in values:
|
|
11604
|
+
output.append(item)
|
|
11605
|
+
|
|
11122
11606
|
return output
|
|
11123
11607
|
|
|
11124
11608
|
|
|
11125
11609
|
def min_value_changed(self):
|
|
11126
11610
|
try:
|
|
11611
|
+
if not self.preview.isChecked():
|
|
11612
|
+
self.preview.click()
|
|
11127
11613
|
text = self.min.text()
|
|
11128
11614
|
if not text: # If empty, ignore
|
|
11129
11615
|
return
|
|
@@ -11171,6 +11657,8 @@ class ThresholdWindow(QMainWindow):
|
|
|
11171
11657
|
|
|
11172
11658
|
def max_value_changed(self):
|
|
11173
11659
|
try:
|
|
11660
|
+
if not self.preview.isChecked():
|
|
11661
|
+
self.preview.click()
|
|
11174
11662
|
text = self.max.text()
|
|
11175
11663
|
if not text: # If empty, ignore
|
|
11176
11664
|
return
|
|
@@ -11294,7 +11782,6 @@ class ThresholdWindow(QMainWindow):
|
|
|
11294
11782
|
f"Error running threshold: {str(e)}"
|
|
11295
11783
|
)
|
|
11296
11784
|
|
|
11297
|
-
|
|
11298
11785
|
class SmartDilateDialog(QDialog):
|
|
11299
11786
|
def __init__(self, parent, params):
|
|
11300
11787
|
super().__init__(parent)
|
|
@@ -11336,7 +11823,7 @@ class SmartDilateDialog(QDialog):
|
|
|
11336
11823
|
|
|
11337
11824
|
result = sdl.smart_dilate(active_data, dilate_xy, dilate_z, GPU = GPU, predownsample = down_factor, fast_dil = predt, use_dt_dil_amount = amount, xy_scale = xy_scale, z_scale = z_scale)
|
|
11338
11825
|
|
|
11339
|
-
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
11826
|
+
self.parent().load_channel(self.parent().active_channel, result, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
11340
11827
|
self.accept()
|
|
11341
11828
|
|
|
11342
11829
|
|
|
@@ -11477,7 +11964,7 @@ class ErodeDialog(QDialog):
|
|
|
11477
11964
|
|
|
11478
11965
|
# Add mode selection dropdown
|
|
11479
11966
|
self.mode_selector = QComboBox()
|
|
11480
|
-
self.mode_selector.addItems(["Pseudo3D Binary Kernels (For Fast, small erosions)", "Distance Transform-Based (Slower but more accurate at larger dilations)"])
|
|
11967
|
+
self.mode_selector.addItems(["Pseudo3D Binary Kernels (For Fast, small erosions)", "Distance Transform-Based (Slower but more accurate at larger dilations)", "Preserve Labels (Slower)"])
|
|
11481
11968
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
11482
11969
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
11483
11970
|
|
|
@@ -11513,6 +12000,12 @@ class ErodeDialog(QDialog):
|
|
|
11513
12000
|
z_scale = 1
|
|
11514
12001
|
|
|
11515
12002
|
mode = self.mode_selector.currentIndex()
|
|
12003
|
+
|
|
12004
|
+
if mode == 2:
|
|
12005
|
+
mode = 1
|
|
12006
|
+
preserve_labels = True
|
|
12007
|
+
else:
|
|
12008
|
+
preserve_labels = False
|
|
11516
12009
|
|
|
11517
12010
|
# Get the active channel data from parent
|
|
11518
12011
|
active_data = self.parent().channel_data[self.parent().active_channel]
|
|
@@ -11525,14 +12018,13 @@ class ErodeDialog(QDialog):
|
|
|
11525
12018
|
amount,
|
|
11526
12019
|
xy_scale = xy_scale,
|
|
11527
12020
|
z_scale = z_scale,
|
|
11528
|
-
mode = mode
|
|
12021
|
+
mode = mode,
|
|
12022
|
+
preserve_labels = preserve_labels
|
|
11529
12023
|
)
|
|
11530
12024
|
|
|
11531
12025
|
|
|
11532
|
-
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
11533
|
-
|
|
12026
|
+
self.parent().load_channel(self.parent().active_channel, result, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
11534
12027
|
|
|
11535
|
-
self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
11536
12028
|
self.accept()
|
|
11537
12029
|
|
|
11538
12030
|
except Exception as e:
|
|
@@ -11598,7 +12090,7 @@ class HoleDialog(QDialog):
|
|
|
11598
12090
|
self.parent().load_channel(3, active_data - result, True)
|
|
11599
12091
|
|
|
11600
12092
|
|
|
11601
|
-
self.parent().update_display()
|
|
12093
|
+
self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
11602
12094
|
self.accept()
|
|
11603
12095
|
|
|
11604
12096
|
except Exception as e:
|
|
@@ -11676,9 +12168,9 @@ class MaskDialog(QDialog):
|
|
|
11676
12168
|
|
|
11677
12169
|
|
|
11678
12170
|
# Update both the display data and the network object
|
|
11679
|
-
self.parent().load_channel(output_target, channel_data = result, data = True)
|
|
12171
|
+
self.parent().load_channel(output_target, channel_data = result, data = True,)
|
|
11680
12172
|
|
|
11681
|
-
self.parent().update_display()
|
|
12173
|
+
self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
11682
12174
|
|
|
11683
12175
|
self.accept()
|
|
11684
12176
|
|
|
@@ -11911,7 +12403,7 @@ class TypeDialog(QDialog):
|
|
|
11911
12403
|
|
|
11912
12404
|
active_data = active_data.astype(np.float64)
|
|
11913
12405
|
|
|
11914
|
-
self.parent().load_channel(self.active_chan, active_data, True)
|
|
12406
|
+
self.parent().load_channel(self.active_chan, active_data, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
11915
12407
|
|
|
11916
12408
|
|
|
11917
12409
|
print(f"Channel {self.active_chan}) dtype now: {self.parent().channel_data[self.active_chan].dtype}")
|
|
@@ -11972,6 +12464,8 @@ class SkeletonizeDialog(QDialog):
|
|
|
11972
12464
|
|
|
11973
12465
|
if remove > 0:
|
|
11974
12466
|
result = n3d.remove_branches_new(result, remove)
|
|
12467
|
+
result = n3d.dilate_3D(result, 3, 3, 3)
|
|
12468
|
+
result = n3d.skeletonize(result)
|
|
11975
12469
|
|
|
11976
12470
|
|
|
11977
12471
|
# Update both the display data and the network object
|
|
@@ -11981,7 +12475,7 @@ class SkeletonizeDialog(QDialog):
|
|
|
11981
12475
|
# Update the corresponding property in my_network
|
|
11982
12476
|
setattr(my_network, network_properties[self.parent().active_channel], result)
|
|
11983
12477
|
|
|
11984
|
-
self.parent().update_display()
|
|
12478
|
+
self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
11985
12479
|
self.accept()
|
|
11986
12480
|
|
|
11987
12481
|
except Exception as e:
|
|
@@ -12012,7 +12506,7 @@ class DistanceDialog(QDialog):
|
|
|
12012
12506
|
|
|
12013
12507
|
data = sdl.compute_distance_transform_distance(data, sampling = [my_network.z_scale, my_network.xy_scale, my_network.xy_scale])
|
|
12014
12508
|
|
|
12015
|
-
self.parent().load_channel(self.parent().active_channel, data, data = True)
|
|
12509
|
+
self.parent().load_channel(self.parent().active_channel, data, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
12016
12510
|
|
|
12017
12511
|
except Exception as e:
|
|
12018
12512
|
|
|
@@ -12050,7 +12544,7 @@ class GrayWaterDialog(QDialog):
|
|
|
12050
12544
|
|
|
12051
12545
|
data = n3d.gray_watershed(data, min_peak_distance, min_intensity)
|
|
12052
12546
|
|
|
12053
|
-
self.parent().load_channel(self.parent().active_channel, data, data = True)
|
|
12547
|
+
self.parent().load_channel(self.parent().active_channel, data, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
12054
12548
|
|
|
12055
12549
|
self.accept()
|
|
12056
12550
|
|
|
@@ -12175,7 +12669,7 @@ class WatershedDialog(QDialog):
|
|
|
12175
12669
|
# Update the corresponding property in my_network
|
|
12176
12670
|
setattr(my_network, network_properties[self.parent().active_channel], result)
|
|
12177
12671
|
|
|
12178
|
-
self.parent().update_display()
|
|
12672
|
+
self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
12179
12673
|
self.accept()
|
|
12180
12674
|
|
|
12181
12675
|
except Exception as e:
|
|
@@ -12228,7 +12722,7 @@ class InvertDialog(QDialog):
|
|
|
12228
12722
|
# Update the corresponding property in my_network
|
|
12229
12723
|
setattr(my_network, network_properties[self.parent().active_channel], result)
|
|
12230
12724
|
|
|
12231
|
-
self.parent().update_display()
|
|
12725
|
+
self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
12232
12726
|
self.accept()
|
|
12233
12727
|
|
|
12234
12728
|
except Exception as e:
|
|
@@ -12272,7 +12766,7 @@ class ZDialog(QDialog):
|
|
|
12272
12766
|
for i in range(len(self.parent().channel_data)):
|
|
12273
12767
|
try:
|
|
12274
12768
|
self.parent().channel_data[i] = n3d.z_project(self.parent().channel_data[i], mode)
|
|
12275
|
-
self.parent().load_channel(i, self.parent().channel_data[i], True)
|
|
12769
|
+
self.parent().load_channel(i, self.parent().channel_data[i], True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
12276
12770
|
except:
|
|
12277
12771
|
pass
|
|
12278
12772
|
|
|
@@ -12775,7 +13269,7 @@ class BranchDialog(QDialog):
|
|
|
12775
13269
|
|
|
12776
13270
|
self.parent().load_channel(1, channel_data = output, data = True)
|
|
12777
13271
|
|
|
12778
|
-
self.parent().update_display()
|
|
13272
|
+
self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
12779
13273
|
self.accept()
|
|
12780
13274
|
|
|
12781
13275
|
except Exception as e:
|
|
@@ -12946,6 +13440,12 @@ class ModifyDialog(QDialog):
|
|
|
12946
13440
|
self.revid.setChecked(False)
|
|
12947
13441
|
layout.addRow("Remove Unassigned IDs from Centroid List?:", self.revid)
|
|
12948
13442
|
|
|
13443
|
+
self.revdupeid = QPushButton("Make Singleton IDs")
|
|
13444
|
+
self.revdupeid.setCheckable(True)
|
|
13445
|
+
self.revdupeid.setChecked(False)
|
|
13446
|
+
layout.addRow("Force Any Multiple IDs to Pick a Random Single ID?:", self.revdupeid)
|
|
13447
|
+
|
|
13448
|
+
|
|
12949
13449
|
self.remove = QPushButton("Remove Missing")
|
|
12950
13450
|
self.remove.setCheckable(True)
|
|
12951
13451
|
self.remove.setChecked(False)
|
|
@@ -13024,6 +13524,7 @@ class ModifyDialog(QDialog):
|
|
|
13024
13524
|
try:
|
|
13025
13525
|
|
|
13026
13526
|
revid = self.revid.isChecked()
|
|
13527
|
+
revdupeid = self.revdupeid.isChecked()
|
|
13027
13528
|
trunk = self.trunk.isChecked()
|
|
13028
13529
|
if not trunk:
|
|
13029
13530
|
trunknode = self.trunknode.isChecked()
|
|
@@ -13048,6 +13549,20 @@ class ModifyDialog(QDialog):
|
|
|
13048
13549
|
except:
|
|
13049
13550
|
pass
|
|
13050
13551
|
|
|
13552
|
+
if revdupeid:
|
|
13553
|
+
try:
|
|
13554
|
+
for node, iden in my_network.node_identities.items():
|
|
13555
|
+
try:
|
|
13556
|
+
import ast
|
|
13557
|
+
import random
|
|
13558
|
+
iden = ast.literal_eval(iden)
|
|
13559
|
+
my_network.node_identities[node] = random.choice(iden)
|
|
13560
|
+
except:
|
|
13561
|
+
pass
|
|
13562
|
+
self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
|
|
13563
|
+
except:
|
|
13564
|
+
pass
|
|
13565
|
+
|
|
13051
13566
|
|
|
13052
13567
|
if remove:
|
|
13053
13568
|
my_network.purge_properties()
|
|
@@ -13155,6 +13670,11 @@ class CentroidDialog(QDialog):
|
|
|
13155
13670
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
13156
13671
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
13157
13672
|
|
|
13673
|
+
self.ignore_empty = QPushButton("Skip ID-less?")
|
|
13674
|
+
self.ignore_empty.setCheckable(True)
|
|
13675
|
+
self.ignore_empty.setChecked(False)
|
|
13676
|
+
layout.addRow("Skip Node Centroids Without Identity Property?:", self.ignore_empty)
|
|
13677
|
+
|
|
13158
13678
|
# Add Run button
|
|
13159
13679
|
run_button = QPushButton("Run Calculate Centroids")
|
|
13160
13680
|
run_button.clicked.connect(self.run_centroids)
|
|
@@ -13167,6 +13687,7 @@ class CentroidDialog(QDialog):
|
|
|
13167
13687
|
print("Calculating centroids...")
|
|
13168
13688
|
|
|
13169
13689
|
chan = self.mode_selector.currentIndex()
|
|
13690
|
+
ignore_empty = self.ignore_empty.isChecked()
|
|
13170
13691
|
|
|
13171
13692
|
# Get directory (None if empty)
|
|
13172
13693
|
directory = self.directory.text() if self.directory.text() else None
|
|
@@ -13227,6 +13748,12 @@ class CentroidDialog(QDialog):
|
|
|
13227
13748
|
except Exception as e:
|
|
13228
13749
|
print(f"Error loading edge centroid table: {e}")
|
|
13229
13750
|
|
|
13751
|
+
if ignore_empty:
|
|
13752
|
+
try:
|
|
13753
|
+
my_network.remove_ids()
|
|
13754
|
+
self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
|
|
13755
|
+
except:
|
|
13756
|
+
pass
|
|
13230
13757
|
|
|
13231
13758
|
self.parent().update_display()
|
|
13232
13759
|
self.accept()
|