nettracer3d 0.9.3__py3-none-any.whl → 0.9.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nettracer3d might be problematic. Click here for more details.

@@ -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)
@@ -1979,6 +1976,13 @@ class ImageViewerWindow(QMainWindow):
1979
1976
  except:
1980
1977
  pass
1981
1978
 
1979
+
1980
+ if my_network.network is not None:
1981
+ try:
1982
+ info_dict['Neighbors'] = list(my_network.network.neighbors(label))
1983
+ except:
1984
+ pass
1985
+
1982
1986
  if my_network.communities is not None:
1983
1987
  try:
1984
1988
  info_dict['Community'] = my_network.communities[label]
@@ -2012,6 +2016,24 @@ class ImageViewerWindow(QMainWindow):
2012
2016
 
2013
2017
  info_dict['Object Class'] = 'Edge'
2014
2018
 
2019
+ try:
2020
+ # Get the existing DataFrame from the model
2021
+ original_df = self.network_table.model()._data
2022
+
2023
+ # Create mask for rows where one column is any original node AND the other column is any neighbor
2024
+ mask = (
2025
+ (original_df.iloc[:, 0].isin(self.clicked_values['nodes'])) |
2026
+ (original_df.iloc[:, 1].isin(self.clicked_values['nodes'])) |
2027
+ (original_df.iloc[:, 2].isin(self.clicked_values['edges']))
2028
+ )
2029
+
2030
+ filtered_df = original_df[mask].copy()
2031
+ node_list = list(set(filtered_df.iloc[:, 0].to_list() + filtered_df.iloc[:, 1].to_list()))
2032
+ info_dict["Num Nodes"] = len(node_list)
2033
+ info_dict['Nodes'] = node_list
2034
+ except:
2035
+ pass
2036
+
2015
2037
  if my_network.edge_centroids is not None:
2016
2038
  try:
2017
2039
  info_dict['Centroid'] = my_network.edge_centroids[label]
@@ -2440,12 +2462,11 @@ class ImageViewerWindow(QMainWindow):
2440
2462
  print(f"Error: {e}")
2441
2463
 
2442
2464
 
2443
- def toggle_chan_load(self):
2465
+ def home(self):
2466
+
2467
+ self.update_display()
2468
+
2444
2469
 
2445
- if self.show_channels.isChecked():
2446
- self.chan_load = True
2447
- else:
2448
- self.chan_load = False
2449
2470
 
2450
2471
  def toggle_highlight(self):
2451
2472
  self.highlight = self.high_button.isChecked()
@@ -2471,7 +2492,8 @@ class ImageViewerWindow(QMainWindow):
2471
2492
  self.zoom_mode = self.zoom_button.isChecked()
2472
2493
 
2473
2494
  if self.zoom_mode:
2474
- self.pan_button.setChecked(False)
2495
+ if self.pan_mode:
2496
+ self.pan_button.click()
2475
2497
 
2476
2498
  self.pen_button.setChecked(False)
2477
2499
  self.brush_mode = False
@@ -2547,33 +2569,23 @@ class ImageViewerWindow(QMainWindow):
2547
2569
  current_xlim = self.ax.get_xlim()
2548
2570
  current_ylim = self.ax.get_ylim()
2549
2571
 
2550
- if (abs(current_xlim[1] - current_xlim[0]) * abs(current_ylim[0] - current_ylim[1]) > 400 * 400 and not self.shape[2] * self.shape[1] > 9000 * 9000 * 6) or self.shape[2] * self.shape[1] < 3000 * 3000:
2572
+ # Create static background from currently visible channels
2573
+ self.create_pan_background()
2574
+
2575
+ # Hide all channels and show only the background
2576
+ self.channel_visible = [False] * 4
2577
+ self.is_pan_preview = True
2578
+
2579
+ # Get current downsample factor
2580
+ current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
2581
+ current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
2582
+ # Update display to show only background
2583
+ self.update_display_pan_mode(current_xlim, current_ylim)
2551
2584
 
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
2585
 
2573
2586
  else:
2574
- current_xlim = self.ax.get_xlim()
2575
- current_ylim = self.ax.get_ylim()
2576
- self.update_display(preserve_zoom=(current_xlim, current_ylim))
2587
+ self.setEnabled(True)
2588
+ self.update_display(preserve_zoom=(self.ax.get_xlim(), self.ax.get_ylim()))
2577
2589
  if self.machine_window is None:
2578
2590
  self.canvas.setCursor(Qt.CursorShape.ArrowCursor)
2579
2591
  else:
@@ -2584,6 +2596,9 @@ class ImageViewerWindow(QMainWindow):
2584
2596
  self.brush_mode = self.pen_button.isChecked()
2585
2597
  if self.brush_mode:
2586
2598
 
2599
+ if self.pan_mode:
2600
+ self.pan_button.click()
2601
+
2587
2602
  self.pm = painting.PaintManager(parent = self)
2588
2603
 
2589
2604
  # Start virtual paint session
@@ -3069,14 +3084,7 @@ class ImageViewerWindow(QMainWindow):
3069
3084
  self.ax.set_xlim(new_xlim)
3070
3085
  self.ax.set_ylim(new_ylim)
3071
3086
 
3072
- # Only call draw_idle if we have a pan background OR if this isn't the first pan
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))
3087
+ self.canvas.draw_idle()
3080
3088
 
3081
3089
  # Update pan start position
3082
3090
  self.pan_start = (event.xdata, event.ydata)
@@ -3105,18 +3113,36 @@ class ImageViewerWindow(QMainWindow):
3105
3113
 
3106
3114
 
3107
3115
  def create_pan_background(self):
3108
- """Create a static background image from currently visible channels with proper rendering"""
3109
- # Store current zoom state
3110
- current_xlim = self.ax.get_xlim()
3111
- current_ylim = self.ax.get_ylim()
3112
-
3113
- # Render all visible channels with proper colors/brightness into a single composite
3114
- self.pan_background_image = self.create_composite_for_pan()
3115
- self.pan_zoom_state = (current_xlim, current_ylim)
3116
-
3116
+ """Create a static background image from currently visible channels with proper rendering"""
3117
+ # Store current zoom state
3118
+ current_xlim = self.ax.get_xlim()
3119
+ current_ylim = self.ax.get_ylim()
3120
+
3121
+ # Try GPU acceleration first, fallback to CPU
3122
+ try:
3123
+ import cupy as cp
3124
+ self.use_gpu = True
3125
+ except ImportError:
3126
+ self.use_gpu = False
3127
+
3128
+ # Render all visible channels with proper colors/brightness into a single composite
3129
+ self.channel_visible = self.pre_pan_channel_state.copy()
3130
+ try:
3131
+ if self.use_gpu:
3132
+ self.pan_background_image = self.create_composite_for_pan_gpu()
3133
+ else:
3134
+ self.pan_background_image = self.create_composite_for_pan()
3135
+ except Exception as e:
3136
+ print(f'GPU implementation failed: {e}, falling back to CPU')
3137
+ self.use_gpu = False
3138
+ self.pan_background_image = self.create_composite_for_pan()
3139
+
3140
+ self.pan_zoom_state = (current_xlim, current_ylim)
3117
3141
 
3118
- def create_composite_for_pan(self):
3119
- """Create a properly rendered composite image for panning with downsample support"""
3142
+ def create_composite_for_pan_gpu(self):
3143
+ """Create a properly rendered composite image for panning with GPU acceleration"""
3144
+ import cupy as cp
3145
+
3120
3146
  # Get active channels and dimensions (copied from update_display)
3121
3147
  active_channels = [i for i in range(4) if self.channel_data[i] is not None]
3122
3148
  if active_channels:
@@ -3134,6 +3160,28 @@ class ImageViewerWindow(QMainWindow):
3134
3160
  current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
3135
3161
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
3136
3162
 
3163
+ # Helper function to crop and downsample image on GPU
3164
+ def crop_and_downsample_image_gpu(image_cp, y_start, y_end, x_start, x_end, factor):
3165
+ # Crop first
3166
+ if len(image_cp.shape) == 2:
3167
+ cropped = image_cp[y_start:y_end, x_start:x_end]
3168
+ elif len(image_cp.shape) == 3:
3169
+ cropped = image_cp[y_start:y_end, x_start:x_end, :]
3170
+ else:
3171
+ cropped = image_cp
3172
+
3173
+ # Then downsample if needed
3174
+ if factor == 1:
3175
+ return cropped
3176
+
3177
+ if len(cropped.shape) == 2:
3178
+ return cropped[::factor, ::factor]
3179
+ elif len(cropped.shape) == 3:
3180
+ return cropped[::factor, ::factor, :]
3181
+ else:
3182
+ return cropped
3183
+
3184
+ min_height, min_width = self.original_dims
3137
3185
 
3138
3186
  # Calculate the visible region in pixel coordinates
3139
3187
  x_min = max(0, int(np.floor(current_xlim[0] + 0.5)))
@@ -3141,41 +3189,281 @@ class ImageViewerWindow(QMainWindow):
3141
3189
  y_min = max(0, int(np.floor(current_ylim[1] + 0.5))) # Note: y is flipped
3142
3190
  y_max = min(min_height, int(np.ceil(current_ylim[0] + 0.5)))
3143
3191
 
3144
- box_len = x_max - x_min
3145
- box_height = y_max - y_min
3192
+ box_len = int((x_max - x_min)/2)
3193
+ box_height = int((y_max - y_min)/2)
3146
3194
  x_min = max(0, x_min - box_len)
3147
3195
  x_max = min(self.shape[2], x_max + box_len)
3148
3196
  y_min = max(0, y_min - box_height)
3149
3197
  y_max = min(self.shape[1], y_max + box_height)
3150
3198
 
3151
- # If using image pyramids
3152
3199
  size = (x_max - x_min) * (y_max - y_min)
3153
- val = int(np.ceil(size/(3000 * 3000)))
3154
- if self.shape[1] * self.shape[2] > 3000 * 3000 * val:
3155
- val = 3
3156
-
3200
+ val = int(np.ceil(size/(2000 * 2000)))
3157
3201
  self.validate_downsample_input(text = val, update = False)
3158
3202
 
3159
3203
  downsample_factor = self.downsample_factor
3160
3204
 
3161
- # Calculate display dimensions (downsampled)
3162
- display_height = min_height // downsample_factor
3163
- display_width = min_width // downsample_factor
3205
+ # Add some padding to avoid edge artifacts during pan/zoom
3206
+ padding = max(10, downsample_factor * 2)
3207
+ x_min_padded = max(0, x_min - padding)
3208
+ x_max_padded = min(min_width, x_max + padding)
3209
+ y_min_padded = max(0, y_min - padding)
3210
+ y_max_padded = min(min_height, y_max + padding)
3211
+
3212
+ display_height = (y_max_padded - y_min_padded) // downsample_factor
3213
+ display_width = (x_max_padded - x_min_padded) // downsample_factor
3164
3214
 
3165
- # Helper function to downsample image (same as in update_display)
3166
- def downsample_image(image, factor):
3167
- if factor == 1:
3168
- return image
3215
+ # Create a blank RGBA composite to accumulate all channels (using display dimensions) - on GPU
3216
+ composite = cp.zeros((display_height, display_width, 4), dtype=cp.float32)
3217
+
3218
+ # Process each visible channel exactly like update_display does
3219
+ for channel in range(4):
3220
+ if (self.channel_visible[channel] and
3221
+ self.channel_data[channel] is not None):
3222
+
3223
+ # Get current slice data (same logic as update_display)
3224
+ 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)
3225
+
3226
+ if len(self.channel_data[channel].shape) == 3 and not is_rgb:
3227
+ current_image = self.channel_data[channel][self.current_slice, :, :]
3228
+ elif is_rgb:
3229
+ current_image = self.channel_data[channel][self.current_slice]
3230
+ else:
3231
+ current_image = self.channel_data[channel]
3232
+
3233
+ # Convert to CuPy array and crop/downsample on GPU
3234
+ current_image_cp = cp.asarray(current_image)
3235
+ display_image_cp = crop_and_downsample_image_gpu(
3236
+ current_image_cp, y_min_padded, y_max_padded,
3237
+ x_min_padded, x_max_padded, downsample_factor)
3238
+
3239
+ if is_rgb and self.channel_data[channel].shape[-1] == 3:
3240
+ # RGB image - convert to RGBA and blend
3241
+ rgb_alpha = cp.ones((*display_image_cp.shape[:2], 4), dtype=cp.float32)
3242
+ rgb_alpha[:, :, :3] = display_image_cp.astype(cp.float32) / 255.0
3243
+ rgb_alpha[:, :, 3] = 0.7 # Same alpha as update_display
3244
+ composite = self.blend_layers_gpu(composite, rgb_alpha)
3245
+
3246
+ elif is_rgb and self.channel_data[channel].shape[-1] == 4:
3247
+ # RGBA image - blend directly
3248
+ rgba_image = display_image_cp.astype(cp.float32) / 255.0
3249
+ composite = self.blend_layers_gpu(composite, rgba_image)
3250
+
3251
+ else:
3252
+ # Regular channel processing (same logic as update_display)
3253
+ if self.min_max[channel][0] == None:
3254
+ self.min_max[channel][0] = cp.asnumpy(cp.min(current_image_cp))
3255
+ if self.min_max[channel][1] == None:
3256
+ self.min_max[channel][1] = cp.asnumpy(cp.max(current_image_cp))
3257
+
3258
+ img_min = self.min_max[channel][0]
3259
+ img_max = self.min_max[channel][1]
3260
+
3261
+ if img_min == img_max:
3262
+ vmin = img_min
3263
+ vmax = img_min + 1
3264
+ else:
3265
+ vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
3266
+ vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
3267
+
3268
+ # Normalize the downsampled image on GPU
3269
+ if vmin == vmax:
3270
+ normalized_image = cp.zeros_like(display_image_cp)
3271
+ else:
3272
+ normalized_image = cp.clip((display_image_cp - vmin) / (vmax - vmin), 0, 1)
3273
+
3274
+ # Apply channel color and alpha
3275
+ if channel == 2 and self.machine_window is not None:
3276
+ # Special case for machine window channel 2
3277
+ channel_rgba = self.apply_machine_colormap_gpu(display_image_cp)
3278
+ else:
3279
+ # Regular channel with custom color
3280
+ color = self.base_colors[channel]
3281
+ channel_rgba = cp.zeros((*normalized_image.shape, 4), dtype=cp.float32)
3282
+ channel_rgba[:, :, 0] = normalized_image * color[0] # R
3283
+ channel_rgba[:, :, 1] = normalized_image * color[1] # G
3284
+ channel_rgba[:, :, 2] = normalized_image * color[2] # B
3285
+ channel_rgba[:, :, 3] = normalized_image * 0.7 # A (same alpha as update_display)
3286
+
3287
+ # Blend this channel into the composite
3288
+ composite = self.blend_layers_gpu(composite, channel_rgba)
3289
+
3290
+ # Add highlight overlays if they exist (with downsampling)
3291
+ if self.mini_overlay and self.highlight and self.machine_window is None:
3292
+ mini_overlay_cp = cp.asarray(self.mini_overlay_data)
3293
+ display_overlay = crop_and_downsample_image_gpu(mini_overlay_cp, y_min_padded, y_max_padded, x_min_padded, x_max_padded, downsample_factor)
3294
+ highlight_rgba = self.create_highlight_rgba_gpu(display_overlay, yellow=True)
3295
+ composite = self.blend_layers_gpu(composite, highlight_rgba)
3296
+ elif self.highlight_overlay is not None and self.highlight:
3297
+ highlight_slice = self.highlight_overlay[self.current_slice]
3298
+ highlight_slice_cp = cp.asarray(highlight_slice)
3299
+ display_highlight = crop_and_downsample_image_gpu(highlight_slice_cp, y_min_padded, y_max_padded, x_min_padded, x_max_padded, downsample_factor)
3300
+ if self.machine_window is None:
3301
+ highlight_rgba = self.create_highlight_rgba_gpu(display_highlight, yellow=True)
3302
+ else:
3303
+ highlight_rgba = self.create_highlight_rgba_gpu(display_highlight, yellow=False)
3304
+ composite = self.blend_layers_gpu(composite, highlight_rgba)
3305
+
3306
+ # Convert back to CPU and to 0-255 range for display
3307
+ composite_cpu = cp.asnumpy(composite)
3308
+ return (composite_cpu * 255).astype(np.uint8)
3309
+
3310
+ def apply_machine_colormap_gpu(self, image_cp):
3311
+ """Apply the special machine window colormap for channel 2 - GPU version"""
3312
+ import cupy as cp
3313
+
3314
+ rgba = cp.zeros((*image_cp.shape, 4), dtype=cp.float32)
3315
+
3316
+ # Transparent for 0
3317
+ mask_0 = (image_cp == 0)
3318
+ rgba[mask_0] = cp.array([0, 0, 0, 0])
3319
+
3320
+ # Light green for 1
3321
+ mask_1 = (image_cp == 1)
3322
+ rgba[mask_1] = cp.array([0.5, 1, 0.5, 0.7])
3323
+
3324
+ # Light red for 2
3325
+ mask_2 = (image_cp == 2)
3326
+ rgba[mask_2] = cp.array([1, 0.5, 0.5, 0.7])
3327
+
3328
+ return rgba
3329
+
3330
+ def create_highlight_rgba_gpu(self, highlight_data_cp, yellow=True):
3331
+ """Create RGBA highlight overlay - GPU version"""
3332
+ import cupy as cp
3333
+
3334
+ rgba = cp.zeros((*highlight_data_cp.shape, 4), dtype=cp.float32)
3335
+
3336
+ if yellow:
3337
+ # Yellow highlight
3338
+ mask = highlight_data_cp > 0
3339
+ rgba[mask] = cp.array([1, 1, 0, 0.8]) # Yellow with alpha 0.8
3340
+ else:
3341
+ # Multi-color highlight for machine window
3342
+ mask_1 = (highlight_data_cp == 1)
3343
+ mask_2 = (highlight_data_cp == 2)
3344
+ rgba[mask_1] = cp.array([1, 1, 0, 0.5]) # Yellow for 1
3345
+ rgba[mask_2] = cp.array([0, 0.7, 1, 0.5]) # Blue for 2
3346
+
3347
+ return rgba
3348
+
3349
+ def blend_layers_gpu(self, base_cp, overlay_cp):
3350
+ """Alpha blend two RGBA layers - GPU version"""
3351
+ import cupy as cp
3352
+
3353
+ def resize_overlay_to_base_gpu(overlay_arr_cp, base_arr_cp):
3354
+ base_height, base_width = base_arr_cp.shape[:2]
3355
+ overlay_height, overlay_width = overlay_arr_cp.shape[:2]
3356
+
3357
+ # First crop if overlay is larger
3358
+ cropped_overlay = overlay_arr_cp[:base_height, :base_width]
3169
3359
 
3170
- # Handle different image types
3360
+ # Then pad if still smaller after cropping
3361
+ current_height, current_width = cropped_overlay.shape[:2]
3362
+ pad_height = base_height - current_height
3363
+ pad_width = base_width - current_width
3364
+
3365
+ if pad_height > 0 or pad_width > 0:
3366
+ cropped_overlay = cp.pad(cropped_overlay,
3367
+ ((0, pad_height), (0, pad_width), (0, 0)),
3368
+ mode='constant', constant_values=0)
3369
+
3370
+ return cropped_overlay
3371
+
3372
+ # Resize the ENTIRE overlay array to match base dimensions
3373
+ if overlay_cp.shape[:2] != base_cp.shape[:2]:
3374
+ overlay_cp = resize_overlay_to_base_gpu(overlay_cp, base_cp)
3375
+
3376
+ # Now extract alpha channels (they should be the same size)
3377
+ alpha_overlay = overlay_cp[:, :, 3:4]
3378
+ alpha_base = base_cp[:, :, 3:4]
3379
+
3380
+ # Calculate output alpha
3381
+ alpha_out = alpha_overlay + alpha_base * (1 - alpha_overlay)
3382
+
3383
+ # Calculate output RGB
3384
+ rgb_out = cp.zeros_like(base_cp[:, :, :3])
3385
+ mask = alpha_out[:, :, 0] > 0
3386
+
3387
+ rgb_out[mask] = (overlay_cp[mask, :3] * alpha_overlay[mask] +
3388
+ base_cp[mask, :3] * alpha_base[mask] * (1 - alpha_overlay[mask])) / alpha_out[mask]
3389
+
3390
+ # Combine RGB and alpha
3391
+ result = cp.zeros_like(base_cp)
3392
+ result[:, :, :3] = rgb_out
3393
+ result[:, :, 3:4] = alpha_out
3394
+
3395
+ return result
3396
+
3397
+ def create_composite_for_pan(self):
3398
+ """Create a properly rendered composite image for panning with downsample support"""
3399
+ # Get active channels and dimensions (copied from update_display)
3400
+ active_channels = [i for i in range(4) if self.channel_data[i] is not None]
3401
+ if active_channels:
3402
+ dims = [(self.channel_data[i].shape[1:3] if len(self.channel_data[i].shape) >= 3 else
3403
+ self.channel_data[i].shape) for i in active_channels]
3404
+ min_height = min(d[0] for d in dims)
3405
+ min_width = min(d[1] for d in dims)
3406
+ else:
3407
+ return None
3408
+
3409
+ # Store original dimensions for coordinate mapping
3410
+ self.original_dims = (min_height, min_width)
3411
+
3412
+ # Get current downsample factor
3413
+ current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
3414
+ current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
3415
+
3416
+ # Helper function to crop and downsample image
3417
+ def crop_and_downsample_image(image, y_start, y_end, x_start, x_end, factor):
3418
+ # Crop first
3171
3419
  if len(image.shape) == 2:
3172
- # Grayscale
3173
- return image[::factor, ::factor]
3420
+ cropped = image[y_start:y_end, x_start:x_end]
3174
3421
  elif len(image.shape) == 3:
3175
- # RGB/RGBA
3176
- return image[::factor, ::factor, :]
3422
+ cropped = image[y_start:y_end, x_start:x_end, :]
3423
+ else:
3424
+ cropped = image
3425
+
3426
+ # Then downsample if needed
3427
+ if factor == 1:
3428
+ return cropped
3429
+
3430
+ if len(cropped.shape) == 2:
3431
+ return cropped[::factor, ::factor]
3432
+ elif len(cropped.shape) == 3:
3433
+ return cropped[::factor, ::factor, :]
3177
3434
  else:
3178
- return image
3435
+ return cropped
3436
+
3437
+ min_height, min_width = self.original_dims
3438
+
3439
+ # Calculate the visible region in pixel coordinates
3440
+ x_min = max(0, int(np.floor(current_xlim[0] + 0.5)))
3441
+ x_max = min(min_width, int(np.ceil(current_xlim[1] + 0.5)))
3442
+ y_min = max(0, int(np.floor(current_ylim[1] + 0.5))) # Note: y is flipped
3443
+ y_max = min(min_height, int(np.ceil(current_ylim[0] + 0.5)))
3444
+
3445
+ box_len = int((x_max - x_min)/2)
3446
+ box_height = int((y_max - y_min)/2)
3447
+ x_min = max(0, x_min - box_len)
3448
+ x_max = min(self.shape[2], x_max + box_len)
3449
+ y_min = max(0, y_min - box_height)
3450
+ y_max = min(self.shape[1], y_max + box_height)
3451
+
3452
+ size = (x_max - x_min) * (y_max - y_min)
3453
+ val = max(2, int(np.ceil(size/(2000 * 2000))))
3454
+ self.validate_downsample_input(text = val, update = False)
3455
+
3456
+ downsample_factor = self.downsample_factor
3457
+
3458
+ # Add some padding to avoid edge artifacts during pan/zoom
3459
+ padding = max(10, downsample_factor * 2)
3460
+ x_min_padded = max(0, x_min - padding)
3461
+ x_max_padded = min(min_width, x_max + padding)
3462
+ y_min_padded = max(0, y_min - padding)
3463
+ y_max_padded = min(min_height, y_max + padding)
3464
+
3465
+ display_height = (y_max_padded - y_min_padded) // downsample_factor
3466
+ display_width = (x_max_padded - x_min_padded) // downsample_factor
3179
3467
 
3180
3468
  # Create a blank RGBA composite to accumulate all channels (using display dimensions)
3181
3469
  composite = np.zeros((display_height, display_width, 4), dtype=np.float32)
@@ -3195,8 +3483,10 @@ class ImageViewerWindow(QMainWindow):
3195
3483
  else:
3196
3484
  current_image = self.channel_data[channel]
3197
3485
 
3198
- # Downsample the image for rendering
3199
- display_image = downsample_image(current_image, downsample_factor)
3486
+ # Crop and downsample the image for rendering
3487
+ display_image = crop_and_downsample_image(
3488
+ current_image, y_min_padded, y_max_padded,
3489
+ x_min_padded, x_max_padded, downsample_factor)
3200
3490
 
3201
3491
  if is_rgb and self.channel_data[channel].shape[-1] == 3:
3202
3492
  # RGB image - convert to RGBA and blend
@@ -3251,12 +3541,12 @@ class ImageViewerWindow(QMainWindow):
3251
3541
 
3252
3542
  # Add highlight overlays if they exist (with downsampling)
3253
3543
  if self.mini_overlay and self.highlight and self.machine_window is None:
3254
- display_overlay = downsample_image(self.mini_overlay_data, downsample_factor)
3544
+ 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
3545
  highlight_rgba = self.create_highlight_rgba(display_overlay, yellow=True)
3256
3546
  composite = self.blend_layers(composite, highlight_rgba)
3257
3547
  elif self.highlight_overlay is not None and self.highlight:
3258
3548
  highlight_slice = self.highlight_overlay[self.current_slice]
3259
- display_highlight = downsample_image(highlight_slice, downsample_factor)
3549
+ display_highlight = crop_and_downsample_image(highlight_slice, y_min_padded, y_max_padded, x_min_padded, x_max_padded, downsample_factor)
3260
3550
  if self.machine_window is None:
3261
3551
  highlight_rgba = self.create_highlight_rgba(display_highlight, yellow=True)
3262
3552
  else:
@@ -3266,7 +3556,6 @@ class ImageViewerWindow(QMainWindow):
3266
3556
  # Convert to 0-255 range for display
3267
3557
  return (composite * 255).astype(np.uint8)
3268
3558
 
3269
-
3270
3559
  def apply_machine_colormap(self, image):
3271
3560
  """Apply the special machine window colormap for channel 2"""
3272
3561
  rgba = np.zeros((*image.shape, 4), dtype=np.float32)
@@ -3368,18 +3657,6 @@ class ImageViewerWindow(QMainWindow):
3368
3657
  height *= downsample_factor
3369
3658
  width *= downsample_factor
3370
3659
 
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
3660
  downsample_factor = self.downsample_factor
3384
3661
  min_height, min_width = self.original_dims
3385
3662
 
@@ -3389,8 +3666,8 @@ class ImageViewerWindow(QMainWindow):
3389
3666
  y_min = max(0, int(np.floor(current_ylim[1] + 0.5))) # Note: y is flipped
3390
3667
  y_max = min(min_height, int(np.ceil(current_ylim[0] + 0.5)))
3391
3668
 
3392
- box_len = x_max - x_min
3393
- box_height = y_max - y_min
3669
+ box_len = int((x_max - x_min)/2)
3670
+ box_height = int((y_max - y_min)/2)
3394
3671
  x_min = max(0, x_min - box_len)
3395
3672
  x_max = min(self.shape[2], x_max + box_len)
3396
3673
  y_min = max(0, y_min - box_height)
@@ -3418,11 +3695,6 @@ class ImageViewerWindow(QMainWindow):
3418
3695
  x_max_padded_ds = min(self.pan_background_image.shape[1], x_max_ds + padding_ds)
3419
3696
  y_min_padded_ds = max(0, y_min_ds - padding_ds)
3420
3697
  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
3698
 
3427
3699
  # Calculate the extent for the cropped region (in original coordinates)
3428
3700
  crop_extent = (x_min_padded - 0.5, x_max_padded - 0.5,
@@ -3430,7 +3702,7 @@ class ImageViewerWindow(QMainWindow):
3430
3702
 
3431
3703
  # Display the composite background with preserved zoom
3432
3704
  # Use extent to stretch downsampled image back to original coordinate space
3433
- self.ax.imshow(display_image,
3705
+ self.ax.imshow(self.pan_background_image,
3434
3706
  extent=crop_extent,
3435
3707
  aspect='equal')
3436
3708
 
@@ -3490,11 +3762,17 @@ class ImageViewerWindow(QMainWindow):
3490
3762
  # Update display to show only background
3491
3763
  current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
3492
3764
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
3493
- # Update display to show only background
3494
- if self.pan_background_image is not None:
3765
+ #self.update_display(preserve_zoom = (current_xlim, current_ylim))
3766
+ self.setEnabled(False)
3767
+
3768
+ try:
3769
+ self.create_pan_background()
3770
+ current_xlim = self.ax.get_xlim()
3771
+ current_ylim = self.ax.get_ylim()
3495
3772
  self.update_display_pan_mode(current_xlim, current_ylim)
3496
- else:
3497
- self.update_display(preserve_zoom=(current_xlim, current_ylim))
3773
+ finally:
3774
+ # Re-enable the widget when done
3775
+ self.setEnabled(True)
3498
3776
 
3499
3777
  self.panning = False
3500
3778
  self.pan_start = None
@@ -3561,8 +3839,9 @@ class ImageViewerWindow(QMainWindow):
3561
3839
  self.create_highlight_overlay(node_indices=self.clicked_values['nodes'])
3562
3840
 
3563
3841
  # Try to highlight the last selected value in tables
3564
- if self.clicked_values['nodes']:
3842
+ if len(self.clicked_values['nodes']) == 1:
3565
3843
  self.highlight_value_in_tables(self.clicked_values['nodes'][-1])
3844
+ self.handle_info('node')
3566
3845
 
3567
3846
  elif self.active_channel == 1: # Edges
3568
3847
  if not ctrl_pressed:
@@ -3577,8 +3856,9 @@ class ImageViewerWindow(QMainWindow):
3577
3856
  self.create_highlight_overlay(edge_indices=self.clicked_values['edges'])
3578
3857
 
3579
3858
  # Try to highlight the last selected value in tables
3580
- if self.clicked_values['edges']:
3859
+ if len(self.clicked_values['edges']):
3581
3860
  self.highlight_value_in_tables(self.clicked_values['edges'][-1])
3861
+ self.handle_info('edge')
3582
3862
 
3583
3863
  elif not self.selecting and self.selection_start: # If we had a click but never started selection
3584
3864
  # Handle as a normal click
@@ -4011,7 +4291,9 @@ class ImageViewerWindow(QMainWindow):
4011
4291
  id_code_action = overlay_menu.addAction("Code Identities")
4012
4292
  id_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Identity'))
4013
4293
  umap_action = overlay_menu.addAction("Centroid UMAP")
4014
- umap_action.triggered.connect(self.handle_umap)
4294
+ umap_action.triggered.connect(self.handle_centroid_umap)
4295
+ iden_umap_action = overlay_menu.addAction("Identity UMAP (If any nodes were assigned multiple identities)")
4296
+ iden_umap_action.triggered.connect(self.handle_iden_umap)
4015
4297
 
4016
4298
  rand_menu = analysis_menu.addMenu("Randomize")
4017
4299
  random_action = rand_menu.addAction("Generate Equivalent Random Network")
@@ -4078,7 +4360,7 @@ class ImageViewerWindow(QMainWindow):
4078
4360
  gennodes_action.triggered.connect(self.show_gennodes_dialog)
4079
4361
  branch_action = generate_menu.addAction("Label Branches")
4080
4362
  branch_action.triggered.connect(lambda: self.show_branch_dialog())
4081
- genvor_action = generate_menu.addAction("Generate Voronoi Diagram (From Node Centroids) - goes in Overlay2")
4363
+ genvor_action = generate_menu.addAction("Generate Voronoi Diagram - goes in Overlay2")
4082
4364
  genvor_action.triggered.connect(self.voronoi)
4083
4365
 
4084
4366
  modify_action = process_menu.addAction("Modify Network")
@@ -4100,8 +4382,6 @@ class ImageViewerWindow(QMainWindow):
4100
4382
  idoverlay_action.triggered.connect(self.show_idoverlay_dialog)
4101
4383
  coloroverlay_action = overlay_menu.addAction("Color Nodes (or Edges)")
4102
4384
  coloroverlay_action.triggered.connect(self.show_coloroverlay_dialog)
4103
- #searchoverlay_action = overlay_menu.addAction("Show Search Regions")
4104
- #searchoverlay_action.triggered.connect(self.show_search_dialog)
4105
4385
  shuffle_action = overlay_menu.addAction("Shuffle")
4106
4386
  shuffle_action.triggered.connect(self.show_shuffle_dialog)
4107
4387
  arbitrary_action = image_menu.addAction("Select Objects")
@@ -4684,18 +4964,7 @@ class ImageViewerWindow(QMainWindow):
4684
4964
 
4685
4965
  try:
4686
4966
 
4687
- if my_network.nodes is not None:
4688
- shape = my_network.nodes.shape
4689
- else:
4690
- shape = None
4691
-
4692
- if my_network.node_centroids is None:
4693
- self.show_centroid_dialog()
4694
- if my_network.node_centroids is None:
4695
- print("Node centroids must be set")
4696
- return
4697
-
4698
- array = pxt.create_voronoi_3d_kdtree(my_network.node_centroids, shape)
4967
+ 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)
4699
4968
  self.load_channel(3, array, True)
4700
4969
 
4701
4970
  except Exception as e:
@@ -4751,10 +5020,6 @@ class ImageViewerWindow(QMainWindow):
4751
5020
  dialog = ColorOverlayDialog(self)
4752
5021
  dialog.exec()
4753
5022
 
4754
- def show_search_dialog(self):
4755
- """Show the search dialog"""
4756
- dialog = SearchOverlayDialog(self)
4757
- dialog.exec()
4758
5023
 
4759
5024
  def show_shuffle_dialog(self):
4760
5025
  """Show the shuffle dialog"""
@@ -5446,12 +5711,8 @@ class ImageViewerWindow(QMainWindow):
5446
5711
  if all(not btn.isEnabled() for btn in self.channel_buttons[:channel_index]):
5447
5712
  self.set_active_channel(channel_index)
5448
5713
 
5449
- if self.chan_load:
5450
- if not self.channel_buttons[channel_index].isChecked():
5451
- self.channel_buttons[channel_index].click()
5452
- else:
5453
- if self.channel_buttons[channel_index].isChecked():
5454
- self.channel_buttons[channel_index].click()
5714
+ if not self.channel_buttons[channel_index].isChecked():
5715
+ self.channel_buttons[channel_index].click()
5455
5716
 
5456
5717
  self.min_max[channel_index][0] = np.min(self.channel_data[channel_index])
5457
5718
  self.min_max[channel_index][1] = np.max(self.channel_data[channel_index])
@@ -5557,7 +5818,7 @@ class ImageViewerWindow(QMainWindow):
5557
5818
 
5558
5819
  if update:
5559
5820
  # Update display
5560
- self.update_display()
5821
+ self.update_display(preserve_zoom = (self.ax.get_xlim(), self.ax.get_ylim()))
5561
5822
 
5562
5823
  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):
5563
5824
  """Method to flexibly reset certain fields to free up the RAM as desired"""
@@ -5689,6 +5950,9 @@ class ImageViewerWindow(QMainWindow):
5689
5950
  def toggle_channel(self, channel_index):
5690
5951
  """Toggle visibility of a channel."""
5691
5952
  # Store current zoom settings before toggling
5953
+ if self.pan_mode:
5954
+ self.pan_button.click()
5955
+
5692
5956
  current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
5693
5957
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
5694
5958
 
@@ -5761,9 +6025,9 @@ class ImageViewerWindow(QMainWindow):
5761
6025
  self.measurement_artists = []
5762
6026
  self.axes_initialized = False
5763
6027
  self.original_dims = None
5764
-
6028
+
5765
6029
  # Handle special states (pan, static background)
5766
- if self.pan_background_image is not None:
6030
+ if self.pan_background_image is not None and not self.pan_mode:
5767
6031
  self.channel_visible = self.pre_pan_channel_state.copy()
5768
6032
  self.is_pan_preview = False
5769
6033
  self.pan_background_image = None
@@ -5772,7 +6036,6 @@ class ImageViewerWindow(QMainWindow):
5772
6036
  self.resume = False
5773
6037
  if self.prev_down != self.downsample_factor:
5774
6038
  self.validate_downsample_input(text = self.prev_down)
5775
- return
5776
6039
 
5777
6040
  if self.static_background is not None:
5778
6041
  # Your existing virtual strokes conversion logic
@@ -5858,8 +6121,8 @@ class ImageViewerWindow(QMainWindow):
5858
6121
  y_max = min(min_height, int(np.ceil(current_ylim[0] + 0.5)))
5859
6122
 
5860
6123
  if self.pan_mode:
5861
- box_len = (x_max - x_min)
5862
- box_height = (y_max - y_min)
6124
+ box_len = int((x_max - x_min)/2)
6125
+ box_height = int((y_max - y_min)/2)
5863
6126
  x_min = max(0, x_min - box_len)
5864
6127
  x_max = min(self.shape[2], x_max + box_len)
5865
6128
  y_min = max(0, y_min - box_height)
@@ -6195,13 +6458,20 @@ class ImageViewerWindow(QMainWindow):
6195
6458
  dialog = CodeDialog(self, sort = sort)
6196
6459
  dialog.exec()
6197
6460
 
6198
- def handle_umap(self):
6461
+ def handle_centroid_umap(self):
6199
6462
 
6200
6463
  if my_network.node_centroids is None:
6201
6464
  self.show_centroid_dialog()
6202
6465
 
6203
6466
  my_network.centroid_umap()
6204
6467
 
6468
+ def handle_iden_umap(self):
6469
+
6470
+ if my_network.node_identities is None:
6471
+ return
6472
+
6473
+ my_network.identity_umap()
6474
+
6205
6475
  def closeEvent(self, event):
6206
6476
  """Override closeEvent to close all windows when main window closes"""
6207
6477
 
@@ -7230,7 +7500,7 @@ class ColorDialog(QDialog):
7230
7500
  self.parent().base_colors[i] = new_color
7231
7501
 
7232
7502
  # Update the display
7233
- self.parent().update_display()
7503
+ self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
7234
7504
  self.accept()
7235
7505
 
7236
7506
  class ArbitraryDialog(QDialog):
@@ -7468,6 +7738,12 @@ class MergeNodeIdDialog(QDialog):
7468
7738
  self.z_scale = QLineEdit(f"{my_network.z_scale}")
7469
7739
  layout.addRow("z_scale:", self.z_scale)
7470
7740
 
7741
+ # Add Run button
7742
+ self.include = QPushButton("Include Negative Gates?")
7743
+ self.include.setCheckable(True)
7744
+ self.include.setChecked(True)
7745
+ layout.addWidget(self.include)
7746
+
7471
7747
  # Add Run button
7472
7748
  run_button = QPushButton("Get Directory")
7473
7749
  run_button.clicked.connect(self.run)
@@ -7483,6 +7759,7 @@ class MergeNodeIdDialog(QDialog):
7483
7759
 
7484
7760
 
7485
7761
  data = self.parent().channel_data[0]
7762
+ include = self.include.isChecked()
7486
7763
 
7487
7764
  if data is None:
7488
7765
  return
@@ -7501,7 +7778,7 @@ class MergeNodeIdDialog(QDialog):
7501
7778
  if search > 0:
7502
7779
  data = sdl.smart_dilate(data, 1, 1, GPU = False, fast_dil = False, use_dt_dil_amount = search, xy_scale = xy_scale, z_scale = z_scale)
7503
7780
 
7504
- my_network.merge_node_ids(selected_path, data)
7781
+ my_network.merge_node_ids(selected_path, data, include)
7505
7782
 
7506
7783
  self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity')
7507
7784
 
@@ -7627,7 +7904,7 @@ class NetOverlayDialog(QDialog):
7627
7904
 
7628
7905
  my_network.network_overlay = my_network.draw_network()
7629
7906
 
7630
- self.parent().load_channel(2, channel_data = my_network.network_overlay, data = True)
7907
+ 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()))
7631
7908
 
7632
7909
  self.accept()
7633
7910
 
@@ -7635,34 +7912,6 @@ class NetOverlayDialog(QDialog):
7635
7912
 
7636
7913
  print(f"Error with Overlay Generation: {e}")
7637
7914
 
7638
- class SearchOverlayDialog(QDialog):
7639
-
7640
- def __init__(self, parent=None):
7641
-
7642
- super().__init__(parent)
7643
- self.setWindowTitle("Generate Search Region Overlay?")
7644
- self.setModal(True)
7645
-
7646
- layout = QFormLayout(self)
7647
-
7648
- # Add Run button
7649
- run_button = QPushButton("Generate (Will go to Overlay 2)")
7650
- run_button.clicked.connect(self.searchoverlay)
7651
- layout.addWidget(run_button)
7652
-
7653
- def searchoverlay(self):
7654
-
7655
- try:
7656
-
7657
- my_network.id_overlay = my_network.search_region
7658
-
7659
- self.parent().load_channel(3, channel_data = my_network.search_region, data = True)
7660
-
7661
- self.accept()
7662
-
7663
- except Exception as e:
7664
-
7665
- print(f"Error with Overlay Generation: {e}")
7666
7915
 
7667
7916
  class IdOverlayDialog(QDialog):
7668
7917
 
@@ -7716,7 +7965,7 @@ class IdOverlayDialog(QDialog):
7716
7965
  my_network.id_overlay = my_network.draw_edge_indices()
7717
7966
 
7718
7967
 
7719
- self.parent().load_channel(3, channel_data = my_network.id_overlay, data = True)
7968
+ 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()))
7720
7969
 
7721
7970
  self.accept()
7722
7971
 
@@ -7765,7 +8014,7 @@ class ColorOverlayDialog(QDialog):
7765
8014
  self.parent().format_for_upperright_table(legend, f'{self.sort} Id', f'Encoding Val: {self.sort}', 'Legend')
7766
8015
 
7767
8016
 
7768
- self.parent().load_channel(3, channel_data = result, data = True)
8017
+ self.parent().load_channel(3, channel_data = result, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
7769
8018
 
7770
8019
  self.accept()
7771
8020
 
@@ -7833,7 +8082,7 @@ class ShuffleDialog(QDialog):
7833
8082
  except:
7834
8083
  self.parent().highlight_overay = None
7835
8084
  else:
7836
- self.parent().load_channel(accepted_mode, channel_data = target_data, data = True)
8085
+ self.parent().load_channel(accepted_mode, channel_data = target_data, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
7837
8086
  except:
7838
8087
  pass
7839
8088
 
@@ -7845,14 +8094,14 @@ class ShuffleDialog(QDialog):
7845
8094
  except:
7846
8095
  self.parent().highlight_overlay = None
7847
8096
  else:
7848
- self.parent().load_channel(accepted_target, channel_data = active_data, data = True)
8097
+ self.parent().load_channel(accepted_target, channel_data = active_data, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
7849
8098
  except:
7850
8099
  pass
7851
8100
 
7852
8101
 
7853
8102
 
7854
8103
 
7855
- self.parent().update_display()
8104
+ self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
7856
8105
 
7857
8106
  self.accept()
7858
8107
 
@@ -8460,10 +8709,10 @@ class NearNeighDialog(QDialog):
8460
8709
  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)
8461
8710
  else:
8462
8711
  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)
8463
- self.parent().load_channel(3, overlay, data = True)
8712
+ self.parent().load_channel(3, overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
8464
8713
 
8465
8714
  if quant_overlay is not None:
8466
- self.parent().load_channel(2, quant_overlay, data = True)
8715
+ self.parent().load_channel(2, quant_overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
8467
8716
 
8468
8717
  avg = {header:avg}
8469
8718
 
@@ -8579,6 +8828,8 @@ class NeighborIdentityDialog(QDialog):
8579
8828
 
8580
8829
  self.accept()
8581
8830
  except Exception as e:
8831
+ import traceback
8832
+ print(traceback.format_exc())
8582
8833
  print(f"Error: {e}")
8583
8834
 
8584
8835
 
@@ -8810,7 +9061,7 @@ class HeatmapDialog(QDialog):
8810
9061
  else:
8811
9062
 
8812
9063
  heat_dict, overlay = my_network.community_heatmap(num_nodes = nodecount, is3d = is3d, numpy = True)
8813
- self.parent().load_channel(3, overlay, data = True)
9064
+ self.parent().load_channel(3, overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
8814
9065
 
8815
9066
 
8816
9067
  self.parent().format_for_upperright_table(heat_dict, metric='Community', value='ln(Predicted Community Nodecount/Actual)', title="Community Heatmap")
@@ -9167,7 +9418,7 @@ class DegreeDialog(QDialog):
9167
9418
  nodes = n3d.upsample_with_padding(nodes, down_factor, original_shape)
9168
9419
 
9169
9420
  if accepted_mode > 0:
9170
- self.parent().load_channel(3, channel_data = nodes, data = True)
9421
+ self.parent().load_channel(3, channel_data = nodes, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
9171
9422
 
9172
9423
 
9173
9424
  self.accept()
@@ -9230,7 +9481,7 @@ class HubDialog(QDialog):
9230
9481
 
9231
9482
  if img is not None:
9232
9483
 
9233
- self.parent().load_channel(3, channel_data = img, data = True)
9484
+ self.parent().load_channel(3, channel_data = img, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
9234
9485
 
9235
9486
 
9236
9487
  self.accept()
@@ -9289,7 +9540,7 @@ class MotherDialog(QDialog):
9289
9540
  G = my_network.isolate_mothers(self, ret_nodes = True, called = True)
9290
9541
  else:
9291
9542
  G, result = my_network.isolate_mothers(self, ret_nodes = False, called = True)
9292
- self.parent().load_channel(2, channel_data = result, data = True)
9543
+ self.parent().load_channel(2, channel_data = result, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
9293
9544
 
9294
9545
  degree_dict = {}
9295
9546
 
@@ -9364,8 +9615,7 @@ class CodeDialog(QDialog):
9364
9615
 
9365
9616
  self.parent().format_for_upperright_table(output, f'{self.sort} Id', f'Encoding Val: {self.sort}', 'Legend')
9366
9617
 
9367
-
9368
- self.parent().load_channel(3, image, True)
9618
+ self.parent().load_channel(3, image, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
9369
9619
  self.accept()
9370
9620
 
9371
9621
  except Exception as e:
@@ -9778,7 +10028,7 @@ class BinarizeDialog(QDialog):
9778
10028
  # Update the corresponding property in my_network
9779
10029
  setattr(my_network, network_properties[self.parent().active_channel], result)
9780
10030
 
9781
- self.parent().update_display()
10031
+ self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
9782
10032
  self.accept()
9783
10033
 
9784
10034
  except Exception as e:
@@ -9830,7 +10080,7 @@ class LabelDialog(QDialog):
9830
10080
  # Update the corresponding property in my_network
9831
10081
  setattr(my_network, network_properties[self.parent().active_channel], result)
9832
10082
 
9833
- self.parent().update_display()
10083
+ self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
9834
10084
  self.accept()
9835
10085
 
9836
10086
  except Exception as e:
@@ -9913,7 +10163,7 @@ class SLabelDialog(QDialog):
9913
10163
 
9914
10164
  binary_array = binary_array * label_array
9915
10165
 
9916
- self.parent().load_channel(accepted_target, binary_array, True)
10166
+ self.parent().load_channel(accepted_target, binary_array, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
9917
10167
 
9918
10168
  self.accept()
9919
10169
 
@@ -10597,6 +10847,10 @@ class MachineWindow(QMainWindow):
10597
10847
 
10598
10848
  def start_segmentation(self):
10599
10849
 
10850
+ if self.parent().pan_mode:
10851
+ self.parent().pan_button.click()
10852
+
10853
+
10600
10854
  self.parent().static_background = None
10601
10855
 
10602
10856
  self.kill_segmentation()
@@ -11320,7 +11574,7 @@ class SmartDilateDialog(QDialog):
11320
11574
 
11321
11575
  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)
11322
11576
 
11323
- self.parent().load_channel(self.parent().active_channel, result, True)
11577
+ self.parent().load_channel(self.parent().active_channel, result, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
11324
11578
  self.accept()
11325
11579
 
11326
11580
 
@@ -11461,7 +11715,7 @@ class ErodeDialog(QDialog):
11461
11715
 
11462
11716
  # Add mode selection dropdown
11463
11717
  self.mode_selector = QComboBox()
11464
- self.mode_selector.addItems(["Pseudo3D Binary Kernels (For Fast, small erosions)", "Distance Transform-Based (Slower but more accurate at larger dilations)"])
11718
+ self.mode_selector.addItems(["Pseudo3D Binary Kernels (For Fast, small erosions)", "Distance Transform-Based (Slower but more accurate at larger dilations)", "Preserve Labels (Slower)"])
11465
11719
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
11466
11720
  layout.addRow("Execution Mode:", self.mode_selector)
11467
11721
 
@@ -11497,6 +11751,12 @@ class ErodeDialog(QDialog):
11497
11751
  z_scale = 1
11498
11752
 
11499
11753
  mode = self.mode_selector.currentIndex()
11754
+
11755
+ if mode == 2:
11756
+ mode = 1
11757
+ preserve_labels = True
11758
+ else:
11759
+ preserve_labels = False
11500
11760
 
11501
11761
  # Get the active channel data from parent
11502
11762
  active_data = self.parent().channel_data[self.parent().active_channel]
@@ -11509,14 +11769,13 @@ class ErodeDialog(QDialog):
11509
11769
  amount,
11510
11770
  xy_scale = xy_scale,
11511
11771
  z_scale = z_scale,
11512
- mode = mode
11772
+ mode = mode,
11773
+ preserve_labels = preserve_labels
11513
11774
  )
11514
11775
 
11515
11776
 
11516
- self.parent().load_channel(self.parent().active_channel, result, True)
11517
-
11777
+ self.parent().load_channel(self.parent().active_channel, result, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
11518
11778
 
11519
- self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
11520
11779
  self.accept()
11521
11780
 
11522
11781
  except Exception as e:
@@ -11582,7 +11841,7 @@ class HoleDialog(QDialog):
11582
11841
  self.parent().load_channel(3, active_data - result, True)
11583
11842
 
11584
11843
 
11585
- self.parent().update_display()
11844
+ self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
11586
11845
  self.accept()
11587
11846
 
11588
11847
  except Exception as e:
@@ -11660,9 +11919,9 @@ class MaskDialog(QDialog):
11660
11919
 
11661
11920
 
11662
11921
  # Update both the display data and the network object
11663
- self.parent().load_channel(output_target, channel_data = result, data = True)
11922
+ self.parent().load_channel(output_target, channel_data = result, data = True,)
11664
11923
 
11665
- self.parent().update_display()
11924
+ self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
11666
11925
 
11667
11926
  self.accept()
11668
11927
 
@@ -11895,7 +12154,7 @@ class TypeDialog(QDialog):
11895
12154
 
11896
12155
  active_data = active_data.astype(np.float64)
11897
12156
 
11898
- self.parent().load_channel(self.active_chan, active_data, True)
12157
+ self.parent().load_channel(self.active_chan, active_data, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
11899
12158
 
11900
12159
 
11901
12160
  print(f"Channel {self.active_chan}) dtype now: {self.parent().channel_data[self.active_chan].dtype}")
@@ -11956,6 +12215,8 @@ class SkeletonizeDialog(QDialog):
11956
12215
 
11957
12216
  if remove > 0:
11958
12217
  result = n3d.remove_branches_new(result, remove)
12218
+ result = n3d.dilate_3D(result, 3, 3, 3)
12219
+ result = n3d.skeletonize(result)
11959
12220
 
11960
12221
 
11961
12222
  # Update both the display data and the network object
@@ -11965,7 +12226,7 @@ class SkeletonizeDialog(QDialog):
11965
12226
  # Update the corresponding property in my_network
11966
12227
  setattr(my_network, network_properties[self.parent().active_channel], result)
11967
12228
 
11968
- self.parent().update_display()
12229
+ self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
11969
12230
  self.accept()
11970
12231
 
11971
12232
  except Exception as e:
@@ -11996,7 +12257,7 @@ class DistanceDialog(QDialog):
11996
12257
 
11997
12258
  data = sdl.compute_distance_transform_distance(data, sampling = [my_network.z_scale, my_network.xy_scale, my_network.xy_scale])
11998
12259
 
11999
- self.parent().load_channel(self.parent().active_channel, data, data = True)
12260
+ self.parent().load_channel(self.parent().active_channel, data, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
12000
12261
 
12001
12262
  except Exception as e:
12002
12263
 
@@ -12034,7 +12295,7 @@ class GrayWaterDialog(QDialog):
12034
12295
 
12035
12296
  data = n3d.gray_watershed(data, min_peak_distance, min_intensity)
12036
12297
 
12037
- self.parent().load_channel(self.parent().active_channel, data, data = True)
12298
+ self.parent().load_channel(self.parent().active_channel, data, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
12038
12299
 
12039
12300
  self.accept()
12040
12301
 
@@ -12159,7 +12420,7 @@ class WatershedDialog(QDialog):
12159
12420
  # Update the corresponding property in my_network
12160
12421
  setattr(my_network, network_properties[self.parent().active_channel], result)
12161
12422
 
12162
- self.parent().update_display()
12423
+ self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
12163
12424
  self.accept()
12164
12425
 
12165
12426
  except Exception as e:
@@ -12212,7 +12473,7 @@ class InvertDialog(QDialog):
12212
12473
  # Update the corresponding property in my_network
12213
12474
  setattr(my_network, network_properties[self.parent().active_channel], result)
12214
12475
 
12215
- self.parent().update_display()
12476
+ self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
12216
12477
  self.accept()
12217
12478
 
12218
12479
  except Exception as e:
@@ -12256,7 +12517,7 @@ class ZDialog(QDialog):
12256
12517
  for i in range(len(self.parent().channel_data)):
12257
12518
  try:
12258
12519
  self.parent().channel_data[i] = n3d.z_project(self.parent().channel_data[i], mode)
12259
- self.parent().load_channel(i, self.parent().channel_data[i], True)
12520
+ self.parent().load_channel(i, self.parent().channel_data[i], True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
12260
12521
  except:
12261
12522
  pass
12262
12523
 
@@ -12759,7 +13020,7 @@ class BranchDialog(QDialog):
12759
13020
 
12760
13021
  self.parent().load_channel(1, channel_data = output, data = True)
12761
13022
 
12762
- self.parent().update_display()
13023
+ self.parent().update_display(preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
12763
13024
  self.accept()
12764
13025
 
12765
13026
  except Exception as e:
@@ -13139,6 +13400,11 @@ class CentroidDialog(QDialog):
13139
13400
  self.mode_selector.setCurrentIndex(0) # Default to Mode 1
13140
13401
  layout.addRow("Execution Mode:", self.mode_selector)
13141
13402
 
13403
+ self.ignore_empty = QPushButton("Skip ID-less?")
13404
+ self.ignore_empty.setCheckable(True)
13405
+ self.ignore_empty.setChecked(False)
13406
+ layout.addRow("Skip Node Centroids Without Identity Property?:", self.ignore_empty)
13407
+
13142
13408
  # Add Run button
13143
13409
  run_button = QPushButton("Run Calculate Centroids")
13144
13410
  run_button.clicked.connect(self.run_centroids)
@@ -13151,6 +13417,7 @@ class CentroidDialog(QDialog):
13151
13417
  print("Calculating centroids...")
13152
13418
 
13153
13419
  chan = self.mode_selector.currentIndex()
13420
+ ignore_empty = self.ignore_empty.isChecked()
13154
13421
 
13155
13422
  # Get directory (None if empty)
13156
13423
  directory = self.directory.text() if self.directory.text() else None
@@ -13211,6 +13478,12 @@ class CentroidDialog(QDialog):
13211
13478
  except Exception as e:
13212
13479
  print(f"Error loading edge centroid table: {e}")
13213
13480
 
13481
+ if ignore_empty:
13482
+ try:
13483
+ my_network.remove_ids()
13484
+ self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
13485
+ except:
13486
+ pass
13214
13487
 
13215
13488
  self.parent().update_display()
13216
13489
  self.accept()