nettracer3d 0.8.9__py3-none-any.whl → 0.9.1__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.
@@ -102,7 +102,7 @@ class ImageViewerWindow(QMainWindow):
102
102
  "WHITE": (1, 1, 1),
103
103
  "GRAY": (0.5, 0.5, 0.5),
104
104
  "LIGHT_GRAY": (0.8, 0.8, 0.8),
105
- "DARK_GRAY": (0.2, 0.2, 0.2)
105
+ "DARK_GRAY": (0.2, 0.2, 0.2),
106
106
  }
107
107
 
108
108
  self.base_colors = [ #Channel colors
@@ -466,6 +466,8 @@ class ImageViewerWindow(QMainWindow):
466
466
 
467
467
  self.resume = False
468
468
 
469
+ self.hold_update = False
470
+
469
471
  def start_left_scroll(self):
470
472
  """Start scrolling left when left arrow is pressed."""
471
473
  # Single increment first
@@ -500,7 +502,6 @@ class ImageViewerWindow(QMainWindow):
500
502
  self.slice_slider.setValue(new_value)
501
503
  elif self.scroll_direction > 0 and new_value <= self.slice_slider.maximum():
502
504
  self.slice_slider.setValue(new_value)
503
-
504
505
 
505
506
  def create_highlight_overlay(self, node_indices=None, edge_indices=None, overlay1_indices = None, overlay2_indices = None, bounds = False):
506
507
  """
@@ -514,6 +515,17 @@ class ImageViewerWindow(QMainWindow):
514
515
  self.mini_overlay = False #If this method is ever being called, it means we are rendering the entire overlay so mini overlay needs to reset.
515
516
  self.mini_overlay_data = None
516
517
 
518
+
519
+ if not self.high_button.isChecked():
520
+
521
+ if len(self.clicked_values['edges']) > 0:
522
+ self.format_for_upperright_table(self.clicked_values['edges'], title = 'Selected Edges')
523
+ if len(self.clicked_values['nodes']) > 0:
524
+ self.format_for_upperright_table(self.clicked_values['nodes'], title = 'Selected Nodes')
525
+
526
+ return
527
+
528
+
517
529
  def process_chunk(chunk_data, indices_to_check):
518
530
  """Process a single chunk of the array to create highlight mask"""
519
531
  mask = np.isin(chunk_data, indices_to_check)
@@ -550,7 +562,7 @@ class ImageViewerWindow(QMainWindow):
550
562
  current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
551
563
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
552
564
 
553
- if not node_indices and not edge_indices and not overlay1_indices and not overlay2_indices:
565
+ if not node_indices and not edge_indices and not overlay1_indices and not overlay2_indices and self.machine_window is None:
554
566
  self.highlight_overlay = None
555
567
  self.highlight_bounds = None
556
568
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
@@ -623,7 +635,7 @@ class ImageViewerWindow(QMainWindow):
623
635
  self.highlight_overlay = np.maximum(self.highlight_overlay, overlay1_overlay)
624
636
  if overlay2_overlay is not None:
625
637
  self.highlight_overlay = np.maximum(self.highlight_overlay, overlay2_overlay)
626
-
638
+
627
639
  # Update display
628
640
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
629
641
 
@@ -822,10 +834,6 @@ class ImageViewerWindow(QMainWindow):
822
834
 
823
835
 
824
836
 
825
-
826
-
827
-
828
-
829
837
  #METHODS RELATED TO RIGHT CLICK:
830
838
 
831
839
  def create_context_menu(self, event):
@@ -1991,7 +1999,10 @@ class ImageViewerWindow(QMainWindow):
1991
1999
  del my_network.network_lists[0][i]
1992
2000
  del my_network.network_lists[1][i]
1993
2001
  del my_network.network_lists[2][i]
1994
-
2002
+ for node in self.clicked_values['nodes']:
2003
+ del my_network.node_centroids[node]
2004
+ del my_network.node_identities[node]
2005
+ del my_network.communities[node]
1995
2006
 
1996
2007
 
1997
2008
  if len(self.clicked_values['edges']) > 0:
@@ -2006,6 +2017,8 @@ class ImageViewerWindow(QMainWindow):
2006
2017
  del my_network.network_lists[0][i]
2007
2018
  del my_network.network_lists[1][i]
2008
2019
  del my_network.network_lists[2][i]
2020
+ for node in self.clicked_values['edges']:
2021
+ del my_network.edge_centroids[edge]
2009
2022
 
2010
2023
  my_network.network_lists = my_network.network_lists
2011
2024
 
@@ -2021,7 +2034,7 @@ class ImageViewerWindow(QMainWindow):
2021
2034
  for column in range(model.columnCount(None)):
2022
2035
  self.network_table.resizeColumnToContents(column)
2023
2036
 
2024
- self.show_centroid_dialog()
2037
+ #self.show_centroid_dialog()
2025
2038
  except Exception as e:
2026
2039
  print(f"Error: {e}")
2027
2040
 
@@ -2147,11 +2160,16 @@ class ImageViewerWindow(QMainWindow):
2147
2160
  current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
2148
2161
  current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
2149
2162
 
2150
- if self.high_button.isChecked():
2163
+ if self.high_button.isChecked() and self.machine_window is None:
2151
2164
  if self.highlight_overlay is None and ((len(self.clicked_values['nodes']) + len(self.clicked_values['edges'])) > 0):
2152
2165
  if self.needs_mini:
2153
2166
  self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
2154
2167
  self.needs_mini = False
2168
+ else:
2169
+ self.create_highlight_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
2170
+ else:
2171
+ self.create_highlight_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
2172
+
2155
2173
 
2156
2174
  self.update_display(preserve_zoom=(current_xlim, current_ylim))
2157
2175
 
@@ -2233,6 +2251,11 @@ class ImageViewerWindow(QMainWindow):
2233
2251
 
2234
2252
  # Store current channel visibility state
2235
2253
  self.pre_pan_channel_state = self.channel_visible.copy()
2254
+
2255
+ self.prev_down = self.downsample_factor
2256
+ if self.throttle:
2257
+ if self.downsample_factor < 3:
2258
+ self.validate_downsample_input(text = 3)
2236
2259
 
2237
2260
  # Create static background from currently visible channels
2238
2261
  self.create_pan_background()
@@ -2782,7 +2805,7 @@ class ImageViewerWindow(QMainWindow):
2782
2805
  self.pan_zoom_state = (current_xlim, current_ylim)
2783
2806
 
2784
2807
  def create_composite_for_pan(self):
2785
- """Create a properly rendered composite image for panning"""
2808
+ """Create a properly rendered composite image for panning with downsample support"""
2786
2809
  # Get active channels and dimensions (copied from update_display)
2787
2810
  active_channels = [i for i in range(4) if self.channel_data[i] is not None]
2788
2811
  if active_channels:
@@ -2793,8 +2816,33 @@ class ImageViewerWindow(QMainWindow):
2793
2816
  else:
2794
2817
  return None
2795
2818
 
2796
- # Create a blank RGBA composite to accumulate all channels
2797
- composite = np.zeros((min_height, min_width, 4), dtype=np.float32)
2819
+ # Store original dimensions for coordinate mapping
2820
+ self.original_dims = (min_height, min_width)
2821
+
2822
+ # Get current downsample factor
2823
+ downsample_factor = getattr(self, 'downsample_factor', 1)
2824
+
2825
+ # Calculate display dimensions (downsampled)
2826
+ display_height = min_height // downsample_factor
2827
+ display_width = min_width // downsample_factor
2828
+
2829
+ # Helper function to downsample image (same as in update_display)
2830
+ def downsample_image(image, factor):
2831
+ if factor == 1:
2832
+ return image
2833
+
2834
+ # Handle different image types
2835
+ if len(image.shape) == 2:
2836
+ # Grayscale
2837
+ return image[::factor, ::factor]
2838
+ elif len(image.shape) == 3:
2839
+ # RGB/RGBA
2840
+ return image[::factor, ::factor, :]
2841
+ else:
2842
+ return image
2843
+
2844
+ # Create a blank RGBA composite to accumulate all channels (using display dimensions)
2845
+ composite = np.zeros((display_height, display_width, 4), dtype=np.float32)
2798
2846
 
2799
2847
  # Process each visible channel exactly like update_display does
2800
2848
  for channel in range(4):
@@ -2811,24 +2859,27 @@ class ImageViewerWindow(QMainWindow):
2811
2859
  else:
2812
2860
  current_image = self.channel_data[channel]
2813
2861
 
2862
+ # Downsample the image for rendering
2863
+ display_image = downsample_image(current_image, downsample_factor)
2864
+
2814
2865
  if is_rgb and self.channel_data[channel].shape[-1] == 3:
2815
2866
  # RGB image - convert to RGBA and blend
2816
- rgb_alpha = np.ones((*current_image.shape[:2], 4), dtype=np.float32)
2817
- rgb_alpha[:, :, :3] = current_image.astype(np.float32) / 255.0
2867
+ rgb_alpha = np.ones((*display_image.shape[:2], 4), dtype=np.float32)
2868
+ rgb_alpha[:, :, :3] = display_image.astype(np.float32) / 255.0
2818
2869
  rgb_alpha[:, :, 3] = 0.7 # Same alpha as update_display
2819
2870
  composite = self.blend_layers(composite, rgb_alpha)
2820
2871
 
2821
2872
  elif is_rgb and self.channel_data[channel].shape[-1] == 4:
2822
2873
  # RGBA image - blend directly
2823
- rgba_image = current_image.astype(np.float32) / 255.0
2874
+ rgba_image = display_image.astype(np.float32) / 255.0
2824
2875
  composite = self.blend_layers(composite, rgba_image)
2825
2876
 
2826
2877
  else:
2827
2878
  # Regular channel processing (same logic as update_display)
2828
2879
  if self.min_max[channel][0] == None:
2829
- self.min_max[channel][0] = np.min(current_image)
2880
+ self.min_max[channel][0] = np.min(self.channel_data[channel])
2830
2881
  if self.min_max[channel][1] == None:
2831
- self.min_max[channel][1] = np.max(current_image)
2882
+ self.min_max[channel][1] = np.max(self.channel_data[channel])
2832
2883
 
2833
2884
  img_min = self.min_max[channel][0]
2834
2885
  img_max = self.min_max[channel][1]
@@ -2840,16 +2891,16 @@ class ImageViewerWindow(QMainWindow):
2840
2891
  vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
2841
2892
  vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
2842
2893
 
2843
- # Normalize the image
2894
+ # Normalize the downsampled image
2844
2895
  if vmin == vmax:
2845
- normalized_image = np.zeros_like(current_image)
2896
+ normalized_image = np.zeros_like(display_image)
2846
2897
  else:
2847
- normalized_image = np.clip((current_image - vmin) / (vmax - vmin), 0, 1)
2898
+ normalized_image = np.clip((display_image - vmin) / (vmax - vmin), 0, 1)
2848
2899
 
2849
2900
  # Apply channel color and alpha
2850
2901
  if channel == 2 and self.machine_window is not None:
2851
2902
  # Special case for machine window channel 2
2852
- channel_rgba = self.apply_machine_colormap(current_image)
2903
+ channel_rgba = self.apply_machine_colormap(display_image)
2853
2904
  else:
2854
2905
  # Regular channel with custom color
2855
2906
  color = self.base_colors[channel]
@@ -2862,16 +2913,18 @@ class ImageViewerWindow(QMainWindow):
2862
2913
  # Blend this channel into the composite
2863
2914
  composite = self.blend_layers(composite, channel_rgba)
2864
2915
 
2865
- # Add highlight overlays if they exist (same logic as update_display)
2916
+ # Add highlight overlays if they exist (with downsampling)
2866
2917
  if self.mini_overlay and self.highlight and self.machine_window is None:
2867
- highlight_rgba = self.create_highlight_rgba(self.mini_overlay_data, yellow=True)
2918
+ display_overlay = downsample_image(self.mini_overlay_data, downsample_factor)
2919
+ highlight_rgba = self.create_highlight_rgba(display_overlay, yellow=True)
2868
2920
  composite = self.blend_layers(composite, highlight_rgba)
2869
2921
  elif self.highlight_overlay is not None and self.highlight:
2870
2922
  highlight_slice = self.highlight_overlay[self.current_slice]
2923
+ display_highlight = downsample_image(highlight_slice, downsample_factor)
2871
2924
  if self.machine_window is None:
2872
- highlight_rgba = self.create_highlight_rgba(highlight_slice, yellow=True)
2925
+ highlight_rgba = self.create_highlight_rgba(display_highlight, yellow=True)
2873
2926
  else:
2874
- highlight_rgba = self.create_highlight_rgba(highlight_slice, yellow=False)
2927
+ highlight_rgba = self.create_highlight_rgba(display_highlight, yellow=False)
2875
2928
  composite = self.blend_layers(composite, highlight_rgba)
2876
2929
 
2877
2930
  # Convert to 0-255 range for display
@@ -2902,7 +2955,7 @@ class ImageViewerWindow(QMainWindow):
2902
2955
  if yellow:
2903
2956
  # Yellow highlight
2904
2957
  mask = highlight_data > 0
2905
- rgba[mask] = [1, 1, 0, 0.5] # Yellow with alpha 0.5
2958
+ rgba[mask] = [1, 1, 0, 0.8] # Yellow with alpha 0.5
2906
2959
  else:
2907
2960
  # Multi-color highlight for machine window
2908
2961
  mask_1 = (highlight_data == 1)
@@ -2914,7 +2967,31 @@ class ImageViewerWindow(QMainWindow):
2914
2967
 
2915
2968
  def blend_layers(self, base, overlay):
2916
2969
  """Alpha blend two RGBA layers"""
2917
- # Standard alpha blending formula
2970
+
2971
+ def resize_overlay_to_base(overlay_arr, base_arr):
2972
+ base_height, base_width = base_arr.shape[:2]
2973
+ overlay_height, overlay_width = overlay_arr.shape[:2]
2974
+
2975
+ # First crop if overlay is larger
2976
+ cropped_overlay = overlay_arr[:base_height, :base_width]
2977
+
2978
+ # Then pad if still smaller after cropping
2979
+ current_height, current_width = cropped_overlay.shape[:2]
2980
+ pad_height = base_height - current_height
2981
+ pad_width = base_width - current_width
2982
+
2983
+ if pad_height > 0 or pad_width > 0:
2984
+ cropped_overlay = np.pad(cropped_overlay,
2985
+ ((0, pad_height), (0, pad_width), (0, 0)),
2986
+ mode='constant', constant_values=0)
2987
+
2988
+ return cropped_overlay
2989
+
2990
+ # Resize the ENTIRE overlay array to match base dimensions
2991
+ if overlay.shape[:2] != base.shape[:2]:
2992
+ overlay = resize_overlay_to_base(overlay, base)
2993
+
2994
+ # Now extract alpha channels (they should be the same size)
2918
2995
  alpha_overlay = overlay[:, :, 3:4]
2919
2996
  alpha_base = base[:, :, 3:4]
2920
2997
 
@@ -2936,17 +3013,26 @@ class ImageViewerWindow(QMainWindow):
2936
3013
  return result
2937
3014
 
2938
3015
  def update_display_pan_mode(self):
2939
- """Lightweight display update for pan preview mode"""
3016
+ """Lightweight display update for pan preview mode with downsample support"""
2940
3017
 
2941
3018
  if self.is_pan_preview and self.pan_background_image is not None:
2942
3019
  # Clear and setup axes
2943
3020
  self.ax.clear()
2944
3021
  self.ax.set_facecolor('black')
2945
3022
 
2946
- # Get dimensions
2947
- height, width = self.pan_background_image.shape[:2]
3023
+ # Get original dimensions (before downsampling)
3024
+ if hasattr(self, 'original_dims') and self.original_dims:
3025
+ height, width = self.original_dims
3026
+ else:
3027
+ # Fallback to pan background image dimensions
3028
+ height, width = self.pan_background_image.shape[:2]
3029
+ # If we have downsample factor, scale back up
3030
+ downsample_factor = getattr(self, 'downsample_factor', 1)
3031
+ height *= downsample_factor
3032
+ width *= downsample_factor
2948
3033
 
2949
3034
  # Display the composite background with preserved zoom
3035
+ # Use extent to stretch downsampled image back to original coordinate space
2950
3036
  self.ax.imshow(self.pan_background_image,
2951
3037
  extent=(-0.5, width-0.5, height-0.5, -0.5),
2952
3038
  aspect='equal')
@@ -2956,10 +3042,16 @@ class ImageViewerWindow(QMainWindow):
2956
3042
  self.ax.set_xlim(self.pan_zoom_state[0])
2957
3043
  self.ax.set_ylim(self.pan_zoom_state[1])
2958
3044
 
3045
+ # Get downsample factor for title display
3046
+ downsample_factor = getattr(self, 'downsample_factor', 1)
3047
+
2959
3048
  # Style the axes (same as update_display)
2960
3049
  self.ax.set_xlabel('X')
2961
- self.ax.set_ylabel('Y')
2962
- self.ax.set_title(f'Slice {self.current_slice}')
3050
+ self.ax.set_ylabel('Y')
3051
+ if downsample_factor > 1:
3052
+ self.ax.set_title(f'Slice {self.current_slice} (DS: {downsample_factor}x)')
3053
+ else:
3054
+ self.ax.set_title(f'Slice {self.current_slice}')
2963
3055
  self.ax.xaxis.label.set_color('black')
2964
3056
  self.ax.yaxis.label.set_color('black')
2965
3057
  self.ax.title.set_color('black')
@@ -2967,7 +3059,7 @@ class ImageViewerWindow(QMainWindow):
2967
3059
  for spine in self.ax.spines.values():
2968
3060
  spine.set_color('black')
2969
3061
 
2970
- # Add measurement points if they exist (same as update_display)
3062
+ # Add measurement points if they exist (coordinates remain in original space)
2971
3063
  for point in self.measurement_points:
2972
3064
  x1, y1, z1 = point['point1']
2973
3065
  x2, y2, z2 = point['point2']
@@ -3093,7 +3185,10 @@ class ImageViewerWindow(QMainWindow):
3093
3185
  except:
3094
3186
  pass
3095
3187
  self.selection_rect = None
3096
- self.canvas.draw()
3188
+ current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
3189
+ current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
3190
+ self.update_display(preserve_zoom=(current_xlim, current_ylim))
3191
+ #self.canvas.draw()
3097
3192
 
3098
3193
  elif self.zoom_mode:
3099
3194
  # Handle zoom mode press
@@ -3126,11 +3221,13 @@ class ImageViewerWindow(QMainWindow):
3126
3221
 
3127
3222
  new_xlim = [xdata - x_range, xdata + x_range]
3128
3223
  new_ylim = [ydata - y_range, ydata + y_range]
3224
+
3225
+ shift_pressed = 'shift' in event.modifiers
3129
3226
 
3130
- if (new_xlim[0] <= self.original_xlim[0] or
3131
- new_xlim[1] >= self.original_xlim[1] or
3132
- new_ylim[0] <= self.original_ylim[0] or
3133
- new_ylim[1] >= self.original_ylim[1]):
3227
+ if (new_xlim[0] <= 0 or
3228
+ new_xlim[1] >= self.shape[2] or
3229
+ new_ylim[0] <= 0 or
3230
+ new_ylim[1] >= self.shape[1]) or shift_pressed:
3134
3231
  self.ax.set_xlim(self.original_xlim)
3135
3232
  self.ax.set_ylim(self.original_ylim)
3136
3233
  else:
@@ -3142,7 +3239,10 @@ class ImageViewerWindow(QMainWindow):
3142
3239
  if not hasattr(self, 'zoom_changed'):
3143
3240
  self.zoom_changed = False
3144
3241
 
3145
- self.canvas.draw()
3242
+ current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
3243
+ current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
3244
+ self.update_display(preserve_zoom=(current_xlim, current_ylim))
3245
+ #self.canvas.draw()
3146
3246
 
3147
3247
  # Handle brush mode cleanup with paint session management
3148
3248
  if self.brush_mode and hasattr(self, 'painting') and self.painting:
@@ -3277,7 +3377,7 @@ class ImageViewerWindow(QMainWindow):
3277
3377
 
3278
3378
  if not hasattr(self, 'zoom_changed'):
3279
3379
  self.zoom_changed = False
3280
-
3380
+
3281
3381
  elif event.button == 3: # Right click - zoom out
3282
3382
  x_range = (current_xlim[1] - current_xlim[0])
3283
3383
  y_range = (current_ylim[1] - current_ylim[0])
@@ -3296,10 +3396,11 @@ class ImageViewerWindow(QMainWindow):
3296
3396
  self.ax.set_ylim(new_ylim)
3297
3397
 
3298
3398
 
3299
- self.zoom_changed = False # Flag that zoom has changed
3300
-
3301
-
3302
- self.canvas.draw()
3399
+ current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
3400
+ current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
3401
+ self.update_display(preserve_zoom=(current_xlim, current_ylim))
3402
+
3403
+ #self.canvas.draw()
3303
3404
 
3304
3405
  elif event.button == 3: # Right click
3305
3406
  self.create_context_menu(event)
@@ -3471,12 +3572,10 @@ class ImageViewerWindow(QMainWindow):
3471
3572
  stats_menu = analysis_menu.addMenu("Stats")
3472
3573
  allstats_action = stats_menu.addAction("Calculate Generic Network Stats")
3473
3574
  allstats_action.triggered.connect(self.stats)
3474
- histos_action = stats_menu.addAction("Calculate Generic Network Histograms")
3575
+ histos_action = stats_menu.addAction("Network Statistic Histograms")
3475
3576
  histos_action.triggered.connect(self.histos)
3476
3577
  radial_action = stats_menu.addAction("Radial Distribution Analysis")
3477
3578
  radial_action.triggered.connect(self.show_radial_dialog)
3478
- degree_dist_action = stats_menu.addAction("Degree Distribution Analysis")
3479
- degree_dist_action.triggered.connect(self.show_degree_dist_dialog)
3480
3579
  neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
3481
3580
  neighbor_id_action.triggered.connect(self.show_neighbor_id_dialog)
3482
3581
  ripley_action = stats_menu.addAction("Ripley Clustering Analysis")
@@ -3515,7 +3614,7 @@ class ImageViewerWindow(QMainWindow):
3515
3614
 
3516
3615
  # Process menu
3517
3616
  process_menu = menubar.addMenu("Process")
3518
- calculate_menu = process_menu.addMenu("Calculate")
3617
+ calculate_menu = process_menu.addMenu("Calculate Network")
3519
3618
  calc_all_action = calculate_menu.addAction("Calculate Connectivity Network (Find Node-Edge-Node Network)")
3520
3619
  calc_all_action.triggered.connect(self.show_calc_all_dialog)
3521
3620
  calc_prox_action = calculate_menu.addAction("Calculate Proximity Network (connect nodes by distance)")
@@ -3608,53 +3707,189 @@ class ImageViewerWindow(QMainWindow):
3608
3707
  help_button = menubar.addAction("Help")
3609
3708
  help_button.triggered.connect(self.help_me)
3610
3709
 
3710
+ # Initialize downsample factor
3711
+ self.downsample_factor = 1
3712
+
3713
+ """
3714
+ # Create container widget for corner controls
3715
+ corner_widget = QWidget()
3716
+ corner_layout = QHBoxLayout(corner_widget)
3717
+ corner_layout.setContentsMargins(5, 0, 5, 0)
3718
+
3719
+ # Add downsample control
3720
+ downsample_label = QLabel("Downsample Display:")
3721
+ downsample_label.setStyleSheet("color: black; font-size: 11px;")
3722
+ corner_layout.addWidget(downsample_label)
3723
+
3724
+ self.downsample_input = QLineEdit("1")
3725
+ self.downsample_input.setFixedWidth(40)
3726
+ self.downsample_input.setFixedHeight(25)
3727
+ self.downsample_input.setStyleSheet("""
3728
+ #QLineEdit {
3729
+ #border: 1px solid gray;
3730
+ #border-radius: 2px;
3731
+ #padding: 1px;
3732
+ #font-size: 11px;
3733
+ #}
3734
+ """)
3735
+ self.downsample_input.textChanged.connect(self.on_downsample_changed)
3736
+ self.downsample_input.editingFinished.connect(self.validate_downsample_input)
3737
+ corner_layout.addWidget(self.downsample_input)
3738
+
3739
+ # Add some spacing
3740
+ corner_layout.addSpacing(10)
3741
+
3742
+ # Add camera button
3743
+ cam_button = QPushButton("📷")
3744
+ cam_button.setFixedSize(40, 40)
3745
+ cam_button.setStyleSheet("font-size: 24px;")
3746
+ cam_button.clicked.connect(self.snap)
3747
+ corner_layout.addWidget(cam_button)
3748
+
3749
+ # Set as corner widget
3750
+ menubar.setCornerWidget(corner_widget, Qt.Corner.TopRightCorner)
3751
+ """
3611
3752
  cam_button = QPushButton("📷")
3612
3753
  cam_button.setFixedSize(40, 40)
3613
3754
  cam_button.setStyleSheet("font-size: 24px;") # Makes emoji larger
3614
3755
  cam_button.clicked.connect(self.snap)
3615
3756
  menubar.setCornerWidget(cam_button, Qt.Corner.TopRightCorner)
3616
3757
 
3617
- def snap(self):
3618
-
3758
+ def on_downsample_changed(self, text):
3759
+ """Called whenever the text in the downsample input changes"""
3619
3760
  try:
3761
+ if text.strip() == "":
3762
+ self.downsample_factor = 1
3763
+ else:
3764
+ value = float(text)
3765
+ if value <= 0:
3766
+ self.downsample_factor = 1
3767
+ else:
3768
+ self.downsample_factor = int(value) if value == int(value) else value
3769
+ except (ValueError, TypeError):
3770
+ self.downsample_factor = 1
3771
+
3772
+ def validate_downsample_input(self, text = None, update = True):
3773
+ """Called when user finishes editing (loses focus or presses Enter)"""
3774
+ if text:
3775
+ self.downsample_factor = text
3776
+ else:
3777
+ try: # If enabled for manual display downsampling
3778
+ text = self.downsample_input.text().strip()
3779
+ if text == "":
3780
+ # Empty input - set to default
3781
+ self.downsample_factor = 1
3782
+ self.downsample_input.setText("1")
3783
+ else:
3784
+ value = int(text)
3785
+ if value < 1:
3786
+ # Invalid value - reset to default
3787
+ self.downsample_factor = 1
3788
+ self.downsample_input.setText("1")
3789
+ else:
3790
+ # Valid value - use it (prefer int if possible)
3791
+ if value == int(value):
3792
+ self.downsample_factor = int(value)
3793
+ self.downsample_input.setText(str(int(value)))
3794
+ else:
3795
+ self.downsample_factor = value
3796
+ self.downsample_input.setText(f"{value:.1f}")
3797
+ except:
3798
+ # Invalid input - reset to default
3799
+ self.downsample_factor = 1
3620
3800
 
3801
+ self.throttle = self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor
3802
+ if self.machine_window is not None:
3803
+ if self.throttle: #arbitrary throttle for large arrays.
3804
+ self.machine_window.update_interval = 10
3805
+ else:
3806
+ self.machine_window.update_interval = 1 # Increased to 1s
3807
+
3808
+ # Optional: Trigger display update if you want immediate effect
3809
+ if update:
3810
+ current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
3811
+ current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
3812
+ self.update_display(preserve_zoom=(current_xlim, current_ylim))
3813
+
3814
+ def snap(self):
3815
+ try:
3816
+ # Check if we have any data to save
3817
+ data = False
3621
3818
  for thing in self.channel_data:
3622
3819
  if thing is not None:
3623
3820
  data = True
3821
+ break
3624
3822
  if not data:
3625
3823
  return
3626
-
3627
- snap = self.create_composite_for_pan()
3628
-
3824
+
3825
+ # Get filename from user
3629
3826
  filename, _ = QFileDialog.getSaveFileName(
3630
3827
  self,
3631
3828
  f"Save Image As",
3632
- "", # Default directory
3633
- "TIFF Files (*.tif *.tiff);;All Files (*)" # File type filter
3829
+ "",
3830
+ "PNG Files (*.png);;TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)"
3634
3831
  )
3635
3832
 
3636
- if filename: # Only proceed if user didn't cancel
3637
- # If user didn't type an extension, add .tif
3638
- if not filename.endswith(('.tif', '.tiff')):
3639
- filename += '.tif'
3833
+ if filename:
3834
+ # Determine file extension
3835
+ if filename.lower().endswith(('.tif', '.tiff')):
3836
+ format_type = 'tiff'
3837
+ elif filename.lower().endswith(('.jpg', '.jpeg')):
3838
+ format_type = 'jpeg'
3839
+ elif filename.lower().endswith('.png'):
3840
+ format_type = 'png'
3841
+ else:
3842
+ filename += '.png'
3843
+ format_type = 'png'
3844
+
3845
+ if self.downsample_factor > 1:
3846
+ self.pan_mode = True # Update display will ignore downsamples if this is true so we can just use it here
3847
+ self.downsample_factor = 1
3848
+ current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
3849
+ current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
3850
+ self.update_display(preserve_zoom=(current_xlim, current_ylim))
3640
3851
 
3641
- import tifffile
3642
- tifffile.imwrite(filename, snap)
3852
+ # Save with axes bbox
3853
+ bbox = self.ax.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
3854
+ self.figure.savefig(filename,
3855
+ dpi=300,
3856
+ bbox_inches=bbox,
3857
+ facecolor='black',
3858
+ edgecolor='none',
3859
+ format=format_type,
3860
+ pad_inches=0)
3861
+
3862
+ print(f"Axes snapshot saved: {filename}")
3643
3863
 
3644
- except:
3645
- pass
3864
+ self.toggle_pan_mode() # Assesses pan state since we messed with its vars potentially
3865
+
3866
+ except Exception as e:
3867
+ print(f"Error saving snapshot: {e}")
3646
3868
 
3647
3869
 
3648
3870
  def open_cellpose(self):
3649
3871
 
3872
+ try:
3873
+ if self.shape[0] == 1:
3874
+ use_3d = False
3875
+ print("Launching 2D cellpose GUI")
3876
+ else:
3877
+ use_3d = True
3878
+ print("Launching 3D cellpose GUI")
3879
+ except:
3880
+ use_3d = True
3881
+ print("Launching 3D cellpose GUI")
3882
+
3650
3883
  try:
3651
3884
 
3652
3885
  from . import cellpose_manager
3653
3886
  self.cellpose_launcher = cellpose_manager.CellposeGUILauncher(parent_widget=self)
3654
3887
 
3655
- self.cellpose_launcher.launch_cellpose_gui()
3888
+ self.cellpose_launcher.launch_cellpose_gui(use_3d = use_3d)
3656
3889
 
3657
3890
  except:
3891
+ import traceback
3892
+ print(traceback.format_exc())
3658
3893
  pass
3659
3894
 
3660
3895
 
@@ -3680,106 +3915,26 @@ class ImageViewerWindow(QMainWindow):
3680
3915
  print(f"Error finding stats: {e}")
3681
3916
 
3682
3917
  def histos(self):
3683
-
3684
- """from networkx documentation"""
3685
-
3918
+ """
3919
+ Show a PyQt6 window with buttons to select which histogram to generate.
3920
+ Only calculates the histogram that the user selects.
3921
+ """
3686
3922
  try:
3687
-
3688
- G = my_network.network
3689
-
3690
- shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(G))
3691
- diameter = max(nx.eccentricity(G, sp=shortest_path_lengths).values())
3692
- # We know the maximum shortest path length (the diameter), so create an array
3693
- # to store values from 0 up to (and including) diameter
3694
- path_lengths = np.zeros(diameter + 1, dtype=int)
3695
-
3696
-
3697
-
3698
- # Extract the frequency of shortest path lengths between two nodes
3699
- for pls in shortest_path_lengths.values():
3700
- pl, cnts = np.unique(list(pls.values()), return_counts=True)
3701
- path_lengths[pl] += cnts
3702
-
3703
- # Express frequency distribution as a percentage (ignoring path lengths of 0)
3704
- freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
3705
-
3706
- # Plot the frequency distribution (ignoring path lengths of 0) as a percentage
3707
- fig, ax = plt.subplots(figsize=(15, 8))
3708
- ax.bar(np.arange(1, diameter + 1), height=freq_percent)
3709
- ax.set_title(
3710
- "Distribution of shortest path length in G", fontdict={"size": 35}, loc="center"
3711
- )
3712
- ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
3713
- ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
3714
-
3715
- plt.show()
3716
- freq_dict = {freq: length for length, freq in enumerate(freq_percent, start=1)}
3717
- self.format_for_upperright_table(freq_dict, metric='Frequency (%)', value='Shortest Path Length', title="Distribution of shortest path length in G")
3718
-
3719
- degree_centrality = nx.centrality.degree_centrality(G)
3720
- plt.figure(figsize=(15, 8))
3721
- plt.hist(degree_centrality.values(), bins=25)
3722
- plt.xticks(ticks=[0, 0.025, 0.05, 0.1, 0.15, 0.2]) # set the x axis ticks
3723
- plt.title("Degree Centrality Histogram ", fontdict={"size": 35}, loc="center")
3724
- plt.xlabel("Degree Centrality", fontdict={"size": 20})
3725
- plt.ylabel("Counts", fontdict={"size": 20})
3726
- plt.show()
3727
- self.format_for_upperright_table(degree_centrality, metric='Node', value='Degree Centrality', title="Degree Centrality Table")
3728
-
3729
-
3730
- betweenness_centrality = nx.centrality.betweenness_centrality(
3731
- G
3732
- )
3733
- plt.figure(figsize=(15, 8))
3734
- plt.hist(betweenness_centrality.values(), bins=100)
3735
- plt.xticks(ticks=[0, 0.02, 0.1, 0.2, 0.3, 0.4, 0.5]) # set the x axis ticks
3736
- plt.title("Betweenness Centrality Histogram ", fontdict={"size": 35}, loc="center")
3737
- plt.xlabel("Betweenness Centrality", fontdict={"size": 20})
3738
- plt.ylabel("Counts", fontdict={"size": 20})
3739
- plt.show()
3740
- self.format_for_upperright_table(betweenness_centrality, metric='Node', value='Betweenness Centrality', title="Betweenness Centrality Table")
3741
-
3742
-
3743
- closeness_centrality = nx.centrality.closeness_centrality(
3744
- G
3745
- )
3746
- plt.figure(figsize=(15, 8))
3747
- plt.hist(closeness_centrality.values(), bins=60)
3748
- plt.title("Closeness Centrality Histogram ", fontdict={"size": 35}, loc="center")
3749
- plt.xlabel("Closeness Centrality", fontdict={"size": 20})
3750
- plt.ylabel("Counts", fontdict={"size": 20})
3751
- plt.show()
3752
- self.format_for_upperright_table(closeness_centrality, metric='Node', value='Closeness Centrality', title="Closeness Centrality Table")
3753
-
3754
-
3755
- eigenvector_centrality = nx.centrality.eigenvector_centrality(
3756
- G
3757
- )
3758
- plt.figure(figsize=(15, 8))
3759
- plt.hist(eigenvector_centrality.values(), bins=60)
3760
- plt.xticks(ticks=[0, 0.01, 0.02, 0.04, 0.06, 0.08]) # set the x axis ticks
3761
- plt.title("Eigenvector Centrality Histogram ", fontdict={"size": 35}, loc="center")
3762
- plt.xlabel("Eigenvector Centrality", fontdict={"size": 20})
3763
- plt.ylabel("Counts", fontdict={"size": 20})
3764
- plt.show()
3765
- self.format_for_upperright_table(eigenvector_centrality, metric='Node', value='Eigenvector Centrality', title="Eigenvector Centrality Table")
3766
-
3767
-
3768
-
3769
- clusters = nx.clustering(G)
3770
- plt.figure(figsize=(15, 8))
3771
- plt.hist(clusters.values(), bins=50)
3772
- plt.title("Clustering Coefficient Histogram ", fontdict={"size": 35}, loc="center")
3773
- plt.xlabel("Clustering Coefficient", fontdict={"size": 20})
3774
- plt.ylabel("Counts", fontdict={"size": 20})
3775
- plt.show()
3776
- self.format_for_upperright_table(clusters, metric='Node', value='Clustering Coefficient', title="Clustering Coefficient Table")
3777
-
3778
- bridges = list(nx.bridges(G))
3779
- self.format_for_upperright_table(bridges, metric = 'Node Pair', title="Bridges")
3780
-
3923
+ # Create QApplication if it doesn't exist
3924
+ app = QApplication.instance()
3925
+ if app is None:
3926
+ app = QApplication(sys.argv)
3927
+
3928
+ # Create and show the histogram selector window
3929
+ self.histogram_selector = HistogramSelector(self)
3930
+ self.histogram_selector.show()
3931
+
3932
+ # Keep the window open (you might want to handle this differently based on your application structure)
3933
+ if not app.exec():
3934
+ pass # Window was closed
3935
+
3781
3936
  except Exception as e:
3782
- print(f"Error generating histograms: {e}")
3937
+ print(f"Error creating histogram selector: {e}")
3783
3938
 
3784
3939
  def volumes(self):
3785
3940
 
@@ -4425,7 +4580,7 @@ class ImageViewerWindow(QMainWindow):
4425
4580
 
4426
4581
  if directory != "":
4427
4582
 
4428
- self.reset(network = True, xy_scale = 1, z_scale = 1, edges = True, network_overlay = True, id_overlay = True)
4583
+ self.reset(network = True, xy_scale = 1, z_scale = 1, edges = True, network_overlay = True, id_overlay = True, update = False)
4429
4584
 
4430
4585
  my_network.assemble(directory)
4431
4586
 
@@ -4738,6 +4893,7 @@ class ImageViewerWindow(QMainWindow):
4738
4893
  """Load a channel and enable active channel selection if needed."""
4739
4894
 
4740
4895
  try:
4896
+ self.hold_update = True
4741
4897
  if not data: # For solo loading
4742
4898
  filename, _ = QFileDialog.getOpenFileName(
4743
4899
  self,
@@ -4805,9 +4961,13 @@ class ImageViewerWindow(QMainWindow):
4805
4961
  try:
4806
4962
  if len(self.channel_data[channel_index].shape) == 3: # potentially 2D RGB
4807
4963
  if self.channel_data[channel_index].shape[-1] in (3, 4): # last dim is 3 or 4
4808
- if self.confirm_rgb_dialog():
4809
- # User confirmed it's 2D RGB, expand to 4D
4964
+ if not data and self.shape is None:
4965
+ if self.confirm_rgb_dialog():
4966
+ # User confirmed it's 2D RGB, expand to 4D
4967
+ self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
4968
+ elif self.shape[0] == 1: # this can only be true if the user already loaded in a 2d image
4810
4969
  self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
4970
+
4811
4971
  except:
4812
4972
  pass
4813
4973
 
@@ -4914,6 +5074,11 @@ class ImageViewerWindow(QMainWindow):
4914
5074
  pass
4915
5075
 
4916
5076
  self.shape = self.channel_data[channel_index].shape
5077
+ if self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor:
5078
+ self.throttle = True
5079
+ else:
5080
+ self.throttle = False
5081
+
4917
5082
 
4918
5083
  self.img_height, self.img_width = self.shape[1], self.shape[2]
4919
5084
  self.original_ylim, self.original_xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
@@ -4931,7 +5096,6 @@ class ImageViewerWindow(QMainWindow):
4931
5096
 
4932
5097
  self.update_display(reset_resize = reset_resize, preserve_zoom = preserve_zoom)
4933
5098
 
4934
-
4935
5099
 
4936
5100
  except Exception as e:
4937
5101
 
@@ -4943,7 +5107,7 @@ class ImageViewerWindow(QMainWindow):
4943
5107
  f"Failed to load tiff file: {str(e)}"
4944
5108
  )
4945
5109
 
4946
- def delete_channel(self, channel_index, called = True):
5110
+ def delete_channel(self, channel_index, called = True, update = True):
4947
5111
  """Delete the specified channel and update the display."""
4948
5112
  if called:
4949
5113
  # Confirm deletion
@@ -4989,11 +5153,13 @@ class ImageViewerWindow(QMainWindow):
4989
5153
  else:
4990
5154
  # If no channels are available, disable active channel selector
4991
5155
  self.active_channel_combo.setEnabled(False)
5156
+ self.shape = None # Also there is not an active shape anymore
4992
5157
 
4993
- # Update display
4994
- self.update_display()
5158
+ if update:
5159
+ # Update display
5160
+ self.update_display()
4995
5161
 
4996
- def reset(self, nodes = False, network = False, xy_scale = 1, z_scale = 1, edges = False, search_region = False, network_overlay = False, id_overlay = False):
5162
+ 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):
4997
5163
  """Method to flexibly reset certain fields to free up the RAM as desired"""
4998
5164
 
4999
5165
  # Set scales first before any clearing operations
@@ -5014,10 +5180,10 @@ class ImageViewerWindow(QMainWindow):
5014
5180
  self.selection_table.setModel(PandasModel(empty_df))
5015
5181
 
5016
5182
  if nodes:
5017
- self.delete_channel(0, False)
5183
+ self.delete_channel(0, False, update = update)
5018
5184
 
5019
5185
  if edges:
5020
- self.delete_channel(1, False)
5186
+ self.delete_channel(1, False, update = update)
5021
5187
  try:
5022
5188
  if search_region:
5023
5189
  my_network.search_region = None
@@ -5025,10 +5191,10 @@ class ImageViewerWindow(QMainWindow):
5025
5191
  pass
5026
5192
 
5027
5193
  if network_overlay:
5028
- self.delete_channel(2, False)
5194
+ self.delete_channel(2, False, update = update)
5029
5195
 
5030
5196
  if id_overlay:
5031
- self.delete_channel(3, False)
5197
+ self.delete_channel(3, False, update = update)
5032
5198
 
5033
5199
 
5034
5200
 
@@ -5162,7 +5328,10 @@ class ImageViewerWindow(QMainWindow):
5162
5328
  self.current_slice = slice_value
5163
5329
  if self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
5164
5330
  self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
5165
- self.update_display(preserve_zoom=view_settings)
5331
+ if not self.hold_update:
5332
+ self.update_display(preserve_zoom=view_settings)
5333
+ else:
5334
+ self.hold_update = False
5166
5335
  #if self.machine_window is not None:
5167
5336
  #self.machine_window.poke_segmenter()
5168
5337
  self.pending_slice = None
@@ -5180,51 +5349,59 @@ class ImageViewerWindow(QMainWindow):
5180
5349
  self.update_display(preserve_zoom = (current_xlim, current_ylim))
5181
5350
 
5182
5351
 
5183
-
5184
-
5185
- def update_display(self, preserve_zoom=None, dims = None, called = False, reset_resize = False):
5186
- """Update the display with currently visible channels and highlight overlay."""
5352
+ def update_display(self, preserve_zoom=None, dims=None, called=False, reset_resize=False, skip=False):
5353
+ """Optimized display update with view-based cropping for performance."""
5187
5354
  try:
5188
- self.figure.clear()
5355
+ # Initialize reusable components if they don't exist
5356
+ if not hasattr(self, 'channel_images'):
5357
+ self.channel_images = {}
5358
+ self.highlight_image = None
5359
+ self.measurement_artists = []
5360
+ self.axes_initialized = False
5361
+ self.original_dims = None
5362
+
5363
+ # Handle special states (pan, static background)
5189
5364
  if self.pan_background_image is not None:
5190
- # Restore previously visible channels
5191
5365
  self.channel_visible = self.pre_pan_channel_state.copy()
5192
5366
  self.is_pan_preview = False
5193
5367
  self.pan_background_image = None
5194
5368
  if self.resume:
5195
5369
  self.machine_window.segmentation_worker.resume()
5196
5370
  self.resume = False
5371
+ if self.prev_down != self.downsample_factor:
5372
+ self.validate_downsample_input(text = self.prev_down)
5373
+ return
5374
+
5197
5375
  if self.static_background is not None:
5198
- # NEW: Convert virtual strokes to real data before cleanup
5376
+ # Your existing virtual strokes conversion logic
5199
5377
  if (hasattr(self, 'virtual_draw_operations') and self.virtual_draw_operations) or \
5200
5378
  (hasattr(self, 'virtual_erase_operations') and self.virtual_erase_operations) or \
5201
5379
  (hasattr(self, 'current_operation') and self.current_operation):
5202
- # Finish current operation first
5203
5380
  if hasattr(self, 'current_operation') and self.current_operation:
5204
5381
  self.pm.finish_current_virtual_operation()
5205
- # Now convert to real data
5206
5382
  self.pm.convert_virtual_strokes_to_data()
5207
5383
 
5208
- # Restore hidden channels
5209
5384
  try:
5210
5385
  for i in self.restore_channels:
5211
5386
  self.channel_visible[i] = True
5212
5387
  self.restore_channels = []
5213
5388
  except:
5214
5389
  pass
5215
-
5216
5390
  self.static_background = None
5217
-
5391
+
5392
+ # Your existing machine_window logic
5218
5393
  if self.machine_window is None:
5219
5394
  try:
5220
- self.channel_data[4][self.current_slice, :, :] = n3d.overlay_arrays_simple(self.channel_data[self.temp_chan][self.current_slice, :, :], self.channel_data[4][self.current_slice, :, :])
5221
- self.load_channel(self.temp_chan, self.channel_data[4], data = True, end_paint = True)
5395
+ self.channel_data[4][self.current_slice, :, :] = n3d.overlay_arrays_simple(
5396
+ self.channel_data[self.temp_chan][self.current_slice, :, :],
5397
+ self.channel_data[4][self.current_slice, :, :])
5398
+ self.load_channel(self.temp_chan, self.channel_data[4], data=True, end_paint=True)
5222
5399
  self.channel_data[4] = None
5223
5400
  self.channel_visible[4] = False
5224
5401
  except:
5225
5402
  pass
5226
5403
 
5227
- # Get active channels and their dimensions
5404
+ # Get dimensions
5228
5405
  active_channels = [i for i in range(4) if self.channel_data[i] is not None]
5229
5406
  if dims is None:
5230
5407
  if active_channels:
@@ -5233,230 +5410,258 @@ class ImageViewerWindow(QMainWindow):
5233
5410
  min_height = min(d[0] for d in dims)
5234
5411
  min_width = min(d[1] for d in dims)
5235
5412
  else:
5236
- min_height = 1
5237
- min_width = 1
5413
+ min_height = min_width = 1
5238
5414
  else:
5239
- min_height = dims[0]
5240
- min_width = dims[1]
5241
-
5242
- # Set axes limits before displaying any images
5415
+ min_height, min_width = dims[:2]
5416
+
5417
+ # Store original dimensions for pixel coordinate conversion
5418
+ self.original_dims = (min_height, min_width)
5419
+
5420
+ # Initialize axes only once or when needed
5421
+ if not self.axes_initialized or not hasattr(self, 'ax') or self.ax is None:
5422
+ self.figure.clear()
5423
+ self.figure.patch.set_facecolor('white')
5424
+ self.ax = self.figure.add_subplot(111)
5425
+ self.ax.set_facecolor('black')
5426
+ self.axes_initialized = True
5427
+
5428
+ # Style the axes once
5429
+ self.ax.set_xlabel('X')
5430
+ self.ax.set_ylabel('Y')
5431
+ self.ax.xaxis.label.set_color('black')
5432
+ self.ax.yaxis.label.set_color('black')
5433
+ self.ax.tick_params(colors='black')
5434
+ for spine in self.ax.spines.values():
5435
+ spine.set_color('black')
5436
+ else:
5437
+ # Clear only the image data, keep axes structure
5438
+ for img in list(self.ax.get_images()):
5439
+ img.remove()
5440
+ # Clear measurement points
5441
+ for artist in self.measurement_artists:
5442
+ artist.remove()
5443
+ self.measurement_artists.clear()
5444
+
5445
+ # Determine the current view bounds (either from preserve_zoom or current state)
5446
+ if preserve_zoom:
5447
+ current_xlim, current_ylim = preserve_zoom
5448
+ else:
5449
+ current_xlim = (-0.5, self.shape[2] - 0.5)
5450
+ current_ylim = (self.shape[1] - 0.5, -0.5)
5451
+
5452
+ # Calculate the visible region in pixel coordinates
5453
+ x_min = max(0, int(np.floor(current_xlim[0] + 0.5)))
5454
+ x_max = min(min_width, int(np.ceil(current_xlim[1] + 0.5)))
5455
+ y_min = max(0, int(np.floor(current_ylim[1] + 0.5))) # Note: y is flipped
5456
+ y_max = min(min_height, int(np.ceil(current_ylim[0] + 0.5)))
5457
+
5458
+ if not self.pan_mode: # If using image pyramids
5459
+ size = (x_max - x_min) * (y_max - y_min)
5460
+ if size < (3000 * 3000): # Smaller window
5461
+ val = 1
5462
+ elif size > (3000 * 3000) and size < (6000 * 6000): # Med window
5463
+ val = 2
5464
+ elif size > (6000 * 6000) and size < (9000 * 9000): # Large window
5465
+ val = 3
5466
+ elif size > (9000 * 9000): # Very large window
5467
+ val = 3
5468
+ self.validate_downsample_input(text = val, update = False)
5469
+ downsample_factor = self.downsample_factor
5470
+
5471
+ # Add some padding to avoid edge artifacts during pan/zoom
5472
+ padding = max(10, downsample_factor * 2)
5473
+ x_min_padded = max(0, x_min - padding)
5474
+ x_max_padded = min(min_width, x_max + padding)
5475
+ y_min_padded = max(0, y_min - padding)
5476
+ y_max_padded = min(min_height, y_max + padding)
5477
+
5478
+ # Calculate the extent for the cropped region (in original coordinates)
5479
+ crop_extent = (x_min_padded - 0.5, x_max_padded - 0.5,
5480
+ y_max_padded - 0.5, y_min_padded - 0.5)
5481
+
5482
+ # Set limits to original dimensions (important for pixel queries)
5243
5483
  self.ax.set_xlim(-0.5, min_width - 0.5)
5244
5484
  self.ax.set_ylim(min_height - 0.5, -0.5)
5485
+ self.ax.set_title(f'Slice {self.current_slice}')
5486
+ self.ax.title.set_color('black')
5245
5487
 
5246
- # Create subplot with tight layout and white figure background
5247
- self.figure.patch.set_facecolor('white')
5248
- self.ax = self.figure.add_subplot(111)
5249
-
5250
- # Store current zoom limits if they exist and weren't provided
5251
-
5252
- current_xlim, current_ylim = preserve_zoom if preserve_zoom else (None, None)
5253
-
5254
- # Define base colors for each channel with increased intensity
5255
5488
  base_colors = self.base_colors
5256
- # Set only the axes (image area) background to black
5257
- self.ax.set_facecolor('black')
5258
5489
 
5259
- # Display each visible channel
5490
+ # Helper function to crop and downsample image
5491
+ def crop_and_downsample_image(image, y_start, y_end, x_start, x_end, factor):
5492
+ # Crop first
5493
+ if len(image.shape) == 2:
5494
+ cropped = image[y_start:y_end, x_start:x_end]
5495
+ elif len(image.shape) == 3:
5496
+ cropped = image[y_start:y_end, x_start:x_end, :]
5497
+ else:
5498
+ cropped = image
5499
+
5500
+ # Then downsample if needed
5501
+ if factor == 1:
5502
+ return cropped
5503
+
5504
+ if len(cropped.shape) == 2:
5505
+ return cropped[::factor, ::factor]
5506
+ elif len(cropped.shape) == 3:
5507
+ return cropped[::factor, ::factor, :]
5508
+ else:
5509
+ return cropped
5510
+
5511
+ # Update channel images efficiently with cropping and downsampling
5260
5512
  for channel in range(4):
5261
- if (self.channel_visible[channel] and
5262
- self.channel_data[channel] is not None):
5263
-
5264
- # Check if we're dealing with RGB data
5265
- 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)
5513
+ if self.channel_visible[channel] and self.channel_data[channel] is not None:
5514
+ # Get current image data
5515
+ is_rgb = len(self.channel_data[channel].shape) == 4 and (
5516
+ self.channel_data[channel].shape[-1] in [3, 4])
5266
5517
 
5267
5518
  if len(self.channel_data[channel].shape) == 3 and not is_rgb:
5268
5519
  current_image = self.channel_data[channel][self.current_slice, :, :]
5269
5520
  elif is_rgb:
5270
- current_image = self.channel_data[channel][self.current_slice] # Already has RGB channels
5521
+ current_image = self.channel_data[channel][self.current_slice]
5271
5522
  else:
5272
5523
  current_image = self.channel_data[channel]
5273
5524
 
5274
- if is_rgb and self.channel_data[channel].shape[-1] == 3:
5275
- # For RGB images, just display directly without colormap
5276
- self.ax.imshow(current_image,
5277
- alpha=0.7)
5278
- elif is_rgb and self.channel_data[channel].shape[-1] == 4:
5279
- self.ax.imshow(current_image) #For images that already have an alpha value and RGB, don't update alpha
5280
-
5525
+ # Crop and downsample the image for rendering
5526
+ display_image = crop_and_downsample_image(
5527
+ current_image, y_min_padded, y_max_padded,
5528
+ x_min_padded, x_max_padded, downsample_factor)
5529
+
5530
+ if is_rgb and self.channel_data[channel].shape[-1] in [3, 4]:
5531
+ # RGB handling (keep your existing logic)
5532
+ brightness_min = self.channel_brightness[channel]['min']
5533
+ brightness_max = self.channel_brightness[channel]['max']
5534
+ alpha_range = brightness_max - brightness_min
5535
+ base_alpha = brightness_min
5536
+ final_alpha = np.clip(base_alpha + alpha_range, 0.0, 1.0)
5537
+
5538
+ if display_image.shape[-1] == 4:
5539
+ img_with_alpha = display_image.copy()
5540
+ img_with_alpha[..., 3] = img_with_alpha[..., 3] * final_alpha
5541
+ # Use crop_extent to place in correct location
5542
+ im = self.ax.imshow(img_with_alpha, extent=crop_extent)
5543
+ else:
5544
+ im = self.ax.imshow(display_image, alpha=final_alpha, extent=crop_extent)
5281
5545
  else:
5282
- # Regular channel processing with colormap
5283
- # Calculate brightness/contrast limits from entire volume
5284
- if self.min_max[channel][0] == None:
5285
- self.min_max[channel][0] = np.min(channel)
5286
- if self.min_max[channel][1] == None:
5287
- self.min_max[channel][1] = np.max(channel)
5288
-
5289
- img_min = self.min_max[channel][0]
5290
- img_max = self.min_max[channel][1]
5546
+ # Regular channel processing with optimized normalization
5547
+ if self.min_max[channel][0] is None:
5548
+ # For very large arrays, consider sampling for min/max
5549
+ if self.channel_data[channel].size > 1000000:
5550
+ sample = self.channel_data[channel][::max(1, self.channel_data[channel].shape[0]//100)]
5551
+ self.min_max[channel] = [np.min(sample), np.max(sample)]
5552
+ else:
5553
+ self.min_max[channel] = [np.min(self.channel_data[channel]),
5554
+ np.max(self.channel_data[channel])]
5555
+
5556
+ img_min, img_max = self.min_max[channel]
5291
5557
 
5292
- # Calculate vmin and vmax, ensuring we don't get a zero range
5293
5558
  if img_min == img_max:
5294
- vmin = img_min
5295
- vmax = img_min + 1
5559
+ vmin, vmax = img_min, img_min + 1
5560
+ normalized_image = np.zeros_like(display_image)
5296
5561
  else:
5297
5562
  vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
5298
5563
  vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
5299
-
5300
- # Normalize the image safely
5301
- if vmin == vmax:
5302
- normalized_image = np.zeros_like(current_image)
5303
- else:
5304
- normalized_image = np.clip((current_image - vmin) / (vmax - vmin), 0, 1)
5564
+
5565
+ if vmin == vmax:
5566
+ normalized_image = np.zeros_like(display_image)
5567
+ else:
5568
+ normalized_image = np.clip((display_image - vmin) / (vmax - vmin), 0, 1)
5305
5569
 
5306
5570
  if channel == 2 and self.machine_window is not None:
5307
5571
  custom_cmap = LinearSegmentedColormap.from_list(
5308
5572
  f'custom_{channel}',
5309
- [(0, 0, 0, 0), # transparent for 0
5310
- (0.5, 1, 0.5, 1), # light green for 1
5311
- (1, 0.5, 0.5, 1)] # light red for 2
5573
+ [(0, 0, 0, 0), (0.5, 1, 0.5, 1), (1, 0.5, 0.5, 1)]
5312
5574
  )
5313
- self.ax.imshow(current_image,
5314
- cmap=custom_cmap,
5315
- vmin=0,
5316
- vmax=2,
5317
- alpha=0.7,
5318
- interpolation='nearest',
5319
- extent=(-0.5, min_width-0.5, min_height-0.5, -0.5))
5575
+ im = self.ax.imshow(display_image, cmap=custom_cmap, vmin=0, vmax=2,
5576
+ alpha=0.7, interpolation='nearest', extent=crop_extent)
5320
5577
  else:
5321
- # Create custom colormap with higher intensity
5322
5578
  color = base_colors[channel]
5323
5579
  custom_cmap = LinearSegmentedColormap.from_list(
5324
- f'custom_{channel}',
5325
- [(0,0,0,0), (*color,1)]
5326
- )
5580
+ f'custom_{channel}', [(0,0,0,0), (*color,1)])
5327
5581
 
5328
- # Display the image with slightly higher alpha
5329
- self.ax.imshow(normalized_image,
5330
- alpha=0.7,
5331
- cmap=custom_cmap,
5332
- vmin=0,
5333
- vmax=1,
5334
- extent=(-0.5, min_width-0.5, min_height-0.5, -0.5))
5582
+ im = self.ax.imshow(normalized_image, alpha=0.7, cmap=custom_cmap,
5583
+ vmin=0, vmax=1, extent=crop_extent)
5335
5584
 
5585
+ # Handle preview, overlays, and measurements (apply cropping here too)
5336
5586
  if self.preview and not called:
5337
- self.create_highlight_overlay_slice(self.targs, bounds = self.bounds)
5587
+ self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
5338
5588
 
5339
- # Add highlight overlay if it exists
5589
+ # Overlay handling (optimized with cropping and downsampling)
5340
5590
  if self.mini_overlay and self.highlight and self.machine_window is None:
5341
- highlight_cmap = LinearSegmentedColormap.from_list(
5342
- 'highlight',
5343
- [(0, 0, 0, 0), (1, 1, 0, 1)] # yellow
5344
- )
5345
- self.ax.imshow(self.mini_overlay_data,
5346
- cmap=highlight_cmap,
5347
- alpha=0.5)
5348
- elif self.highlight_overlay is not None and self.highlight and self.machine_window is None:
5349
- highlight_slice = self.highlight_overlay[self.current_slice]
5350
- highlight_cmap = LinearSegmentedColormap.from_list(
5351
- 'highlight',
5352
- [(0, 0, 0, 0), (1, 1, 0, 1)] # yellow
5353
- )
5354
- self.ax.imshow(highlight_slice,
5355
- cmap=highlight_cmap,
5356
- alpha=0.5)
5591
+ highlight_cmap = LinearSegmentedColormap.from_list('highlight', [(0, 0, 0, 0), (1, 1, 0, 1)])
5592
+ display_overlay = crop_and_downsample_image(
5593
+ self.mini_overlay_data, y_min_padded, y_max_padded,
5594
+ x_min_padded, x_max_padded, downsample_factor)
5595
+ self.ax.imshow(display_overlay, cmap=highlight_cmap, alpha=0.8, extent=crop_extent)
5357
5596
  elif self.highlight_overlay is not None and self.highlight:
5358
5597
  highlight_slice = self.highlight_overlay[self.current_slice]
5359
- highlight_cmap = LinearSegmentedColormap.from_list(
5360
- 'highlight',
5361
- [(0, 0, 0, 0), # transparent for 0
5362
- (1, 1, 0, 1), # bright yellow for 1
5363
- (0, 0.7, 1, 1)] # cool blue for 2
5364
- )
5365
- self.ax.imshow(highlight_slice,
5366
- cmap=highlight_cmap,
5367
- vmin=0,
5368
- vmax=2, # Important: set vmax to 2 to accommodate both values
5369
- alpha=0.5)
5370
-
5371
- if self.channel_data[4] is not None:
5372
-
5373
- highlight_slice = self.channel_data[4][self.current_slice]
5374
- img_min = self.min_max[4][0]
5375
- img_max = self.min_max[4][1]
5376
-
5377
- # Calculate vmin and vmax, ensuring we don't get a zero range
5378
- if img_min == img_max:
5379
- vmin = img_min
5380
- vmax = img_min + 1
5381
- else:
5382
- vmin = img_min + (img_max - img_min) * self.channel_brightness[4]['min']
5383
- vmax = img_min + (img_max - img_min) * self.channel_brightness[4]['max']
5384
-
5385
- # Normalize the image safely
5386
- if vmin == vmax:
5387
- normalized_image = np.zeros_like(highlight_slice)
5598
+ display_highlight = crop_and_downsample_image(
5599
+ highlight_slice, y_min_padded, y_max_padded,
5600
+ x_min_padded, x_max_padded, downsample_factor)
5601
+ if self.machine_window is None:
5602
+ highlight_cmap = LinearSegmentedColormap.from_list('highlight', [(0, 0, 0, 0), (1, 1, 0, 1)])
5603
+ self.ax.imshow(display_highlight, cmap=highlight_cmap, alpha=0.8, extent=crop_extent)
5388
5604
  else:
5389
- normalized_image = np.clip((highlight_slice - vmin) / (vmax - vmin), 0, 1)
5605
+ highlight_cmap = LinearSegmentedColormap.from_list('highlight',
5606
+ [(0, 0, 0, 0), (1, 1, 0, 1), (0, 0.7, 1, 1)])
5607
+ self.ax.imshow(display_highlight, cmap=highlight_cmap, vmin=0, vmax=2, alpha=0.3, extent=crop_extent)
5390
5608
 
5391
- color = base_colors[self.temp_chan]
5392
- custom_cmap = LinearSegmentedColormap.from_list(
5393
- f'custom_{4}',
5394
- [(0,0,0,0), (*color,1)]
5395
- )
5396
-
5397
-
5398
- self.ax.imshow(normalized_image,
5399
- alpha=0.7,
5400
- cmap=custom_cmap,
5401
- vmin=0,
5402
- vmax=1)
5403
-
5404
- # Style the axes
5405
- self.ax.set_xlabel('X')
5406
- self.ax.set_ylabel('Y')
5407
- self.ax.set_title(f'Slice {self.current_slice}')
5408
-
5409
- # Make axis labels and ticks black for visibility against white background
5410
- self.ax.xaxis.label.set_color('black')
5411
- self.ax.yaxis.label.set_color('black')
5412
- self.ax.title.set_color('black')
5413
- self.ax.tick_params(colors='black')
5414
- for spine in self.ax.spines.values():
5415
- spine.set_color('black')
5416
-
5417
- # Adjust the layout to ensure the plot fits well in the figure
5418
- self.figure.tight_layout()
5419
-
5420
- # Redraw measurement points and their labels
5609
+ # Redraw measurement points efficiently (no cropping needed - these are vector graphics)
5610
+ # Only draw points that are within the visible region for additional performance
5421
5611
  for point in self.measurement_points:
5422
5612
  x1, y1, z1 = point['point1']
5423
5613
  x2, y2, z2 = point['point2']
5424
5614
  pair_idx = point['pair_index']
5425
5615
 
5426
- # Draw points and labels if they're on current slice
5427
- if z1 == self.current_slice:
5428
- self.ax.plot(x1, y1, 'yo', markersize=8)
5429
- self.ax.text(x1, y1+5, str(pair_idx),
5430
- color='white', ha='center', va='bottom')
5431
- if z2 == self.current_slice:
5432
- self.ax.plot(x2, y2, 'yo', markersize=8)
5433
- self.ax.text(x2, y2+5, str(pair_idx),
5434
- color='white', ha='center', va='bottom')
5616
+ # Check if points are in visible region
5617
+ point1_visible = (z1 == self.current_slice and
5618
+ current_xlim[0] <= x1 <= current_xlim[1] and
5619
+ current_ylim[1] <= y1 <= current_ylim[0])
5620
+ point2_visible = (z2 == self.current_slice and
5621
+ current_xlim[0] <= x2 <= current_xlim[1] and
5622
+ current_ylim[1] <= y2 <= current_ylim[0])
5623
+
5624
+ if point1_visible:
5625
+ pt1 = self.ax.plot(x1, y1, 'yo', markersize=8)[0]
5626
+ txt1 = self.ax.text(x1, y1+5, str(pair_idx), color='white', ha='center', va='bottom')
5627
+ self.measurement_artists.extend([pt1, txt1])
5435
5628
 
5436
- # Draw line if both points are on current slice
5437
- if z1 == z2 == self.current_slice:
5438
- self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
5439
-
5440
- if active_channels:
5441
- self.ax.set_xlim(-0.5, min_width - 0.5)
5442
- self.ax.set_ylim(min_height - 0.5, -0.5)
5629
+ if point2_visible:
5630
+ pt2 = self.ax.plot(x2, y2, 'yo', markersize=8)[0]
5631
+ txt2 = self.ax.text(x2, y2+5, str(pair_idx), color='white', ha='center', va='bottom')
5632
+ self.measurement_artists.extend([pt2, txt2])
5633
+
5634
+ if z1 == z2 == self.current_slice and (point1_visible or point2_visible):
5635
+ line = self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)[0]
5636
+ self.measurement_artists.append(line)
5443
5637
 
5638
+ # Store current view limits for next update
5639
+ self.ax._current_xlim = current_xlim
5640
+ self.ax._current_ylim = current_ylim
5641
+
5642
+ # Handle resizing
5444
5643
  if self.resizing:
5445
5644
  self.original_xlim = self.ax.get_xlim()
5446
5645
  self.original_ylim = self.ax.get_ylim()
5447
- # Restore zoom limits if they existed
5646
+
5647
+ # Restore zoom (this sets the final view, not the data extent)
5448
5648
  if current_xlim is not None and current_ylim is not None:
5449
5649
  self.ax.set_xlim(current_xlim)
5450
5650
  self.ax.set_ylim(current_ylim)
5651
+
5451
5652
  if reset_resize:
5452
5653
  self.resizing = False
5453
5654
 
5454
- self.canvas.draw()
5655
+ # Use draw_idle for better performance
5656
+ self.canvas.draw_idle()
5657
+
5658
+ except Exception as e:
5659
+ pass
5660
+ #import traceback
5661
+ #print(traceback.format_exc())
5662
+
5455
5663
 
5456
5664
 
5457
- except:
5458
- import traceback
5459
- print(traceback.format_exc())
5460
5665
 
5461
5666
  def get_channel_image(self, channel):
5462
5667
  """Find the matplotlib image object for a specific channel."""
@@ -5488,7 +5693,10 @@ class ImageViewerWindow(QMainWindow):
5488
5693
  stats['num_nodes'] = my_network.network.number_of_nodes()
5489
5694
  stats['num_edges'] = my_network.network.number_of_edges()
5490
5695
  except:
5491
- pass
5696
+ try:
5697
+ stats['num_nodes'] = len(np.unique(my_network.nodes)) - 1
5698
+ except:
5699
+ pass
5492
5700
 
5493
5701
  try:
5494
5702
  idens = invert_dict(my_network.node_identities)
@@ -5534,10 +5742,6 @@ class ImageViewerWindow(QMainWindow):
5534
5742
  dialog = RadialDialog(self)
5535
5743
  dialog.exec()
5536
5744
 
5537
- def show_degree_dist_dialog(self):
5538
- dialog = DegreeDistDialog(self)
5539
- dialog.exec()
5540
-
5541
5745
  def show_neighbor_id_dialog(self):
5542
5746
  dialog = NeighborIdentityDialog(self)
5543
5747
  dialog.exec()
@@ -7122,6 +7326,15 @@ class ColorOverlayDialog(QDialog):
7122
7326
 
7123
7327
  layout = QFormLayout(self)
7124
7328
 
7329
+ # Add mode selection dropdown
7330
+ self.mode_selector = QComboBox()
7331
+ self.mode_selector.addItems(["Nodes", "Edges"])
7332
+ if self.parent().active_channel == 0 and self.parent().channel_data[0] is not None:
7333
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
7334
+ else:
7335
+ self.mode_selector.setCurrentIndex(1) # Default to Mode 1
7336
+ layout.addRow("Execution Mode:", self.mode_selector)
7337
+
7125
7338
  self.down_factor = QLineEdit("")
7126
7339
  layout.addRow("down_factor (for speeding up overlay generation - optional):", self.down_factor)
7127
7340
 
@@ -7136,11 +7349,10 @@ class ColorOverlayDialog(QDialog):
7136
7349
 
7137
7350
  down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
7138
7351
 
7139
- if self.parent().active_channel == 0:
7140
- mode = 0
7352
+ mode = self.mode_selector.currentIndex()
7353
+ if mode == 0:
7141
7354
  self.sort = 'Node'
7142
7355
  else:
7143
- mode = 1
7144
7356
  self.sort = 'Edge'
7145
7357
 
7146
7358
 
@@ -7410,11 +7622,10 @@ class ComIdDialog(QDialog):
7410
7622
  self.umap.setChecked(True)
7411
7623
  layout.addRow("Generate UMAP?:", self.umap)
7412
7624
 
7413
- # weighted checkbox (default True)
7414
- self.label = QPushButton("Label")
7415
- self.label.setCheckable(True)
7416
- self.label.setChecked(False)
7417
- layout.addRow("If using above - label UMAP points?:", self.label)
7625
+ self.label = QComboBox()
7626
+ self.label.addItems(["No Label", "By Community", "By Neighborhood (If already calculated via 'Analyze -> Network -> Convert Network Communities...')"])
7627
+ self.label.setCurrentIndex(0)
7628
+ layout.addRow("Label UMAP Points How?:", self.label)
7418
7629
 
7419
7630
  self.limit = QLineEdit("")
7420
7631
  layout.addRow("Min Community Size for UMAP (Smaller communities will be ignored in graph, does not apply if empty)", self.limit)
@@ -7434,6 +7645,12 @@ class ComIdDialog(QDialog):
7434
7645
 
7435
7646
  try:
7436
7647
 
7648
+ if self.parent().prev_coms is not None:
7649
+ temp = my_network.communities
7650
+ my_network.communities = self.parent().prev_coms
7651
+ else:
7652
+ temp = None
7653
+
7437
7654
  if my_network.node_identities is None:
7438
7655
  print("Node identities must be set")
7439
7656
 
@@ -7446,7 +7663,9 @@ class ComIdDialog(QDialog):
7446
7663
  mode = self.mode.currentIndex()
7447
7664
 
7448
7665
  umap = self.umap.isChecked()
7449
- label = self.label.isChecked()
7666
+
7667
+ label = self.label.currentIndex()
7668
+
7450
7669
  proportional = self.proportional.isChecked()
7451
7670
  limit = int(self.limit.text()) if self.limit.text().strip() else 0
7452
7671
 
@@ -7459,10 +7678,13 @@ class ComIdDialog(QDialog):
7459
7678
 
7460
7679
  else:
7461
7680
 
7462
- info, names = my_network.community_id_info_per_com(umap = umap, label = label, limit = limit, proportional = proportional)
7681
+ info, names = my_network.community_id_info_per_com(umap = umap, label = label, limit = limit, proportional = proportional, neighbors = temp)
7463
7682
 
7464
7683
  self.parent().format_for_upperright_table(info, 'Community', names, 'Average of Community Makeup')
7465
7684
 
7685
+ if self.parent().prev_coms is not None:
7686
+ my_network.communities = temp
7687
+
7466
7688
  self.accept()
7467
7689
 
7468
7690
  except Exception as e:
@@ -7658,39 +7880,6 @@ class RadialDialog(QDialog):
7658
7880
  except Exception as e:
7659
7881
  print(f"An error occurred: {e}")
7660
7882
 
7661
- class DegreeDistDialog(QDialog):
7662
-
7663
- def __init__(self, parent=None):
7664
-
7665
- super().__init__(parent)
7666
- self.setWindowTitle("Degree Distribution Parameters")
7667
- self.setModal(True)
7668
-
7669
- layout = QFormLayout(self)
7670
-
7671
- self.directory = QLineEdit("")
7672
- layout.addRow("Output Directory:", self.directory)
7673
-
7674
- # Add Run button
7675
- run_button = QPushButton("Get Degree Distribution")
7676
- run_button.clicked.connect(self.degreedist)
7677
- layout.addWidget(run_button)
7678
-
7679
- def degreedist(self):
7680
-
7681
- try:
7682
-
7683
- directory = str(self.distance.text()) if self.directory.text().strip() else None
7684
-
7685
- degrees = my_network.degree_distribution(directory = directory)
7686
-
7687
-
7688
- self.parent().format_for_upperright_table(degrees, 'Degree (k)', 'Proportion of nodes with degree (p(k))', title = 'Degree Distribution Analysis')
7689
-
7690
- self.accept()
7691
-
7692
- except Exception as e:
7693
- print(f"An error occurred: {e}")
7694
7883
 
7695
7884
  class NearNeighDialog(QDialog):
7696
7885
  def __init__(self, parent=None):
@@ -7725,9 +7914,12 @@ class NearNeighDialog(QDialog):
7725
7914
  self.num = QLineEdit("1")
7726
7915
  identities_layout.addRow("Number of Nearest Neighbors to Evaluate Per Node?:", self.num)
7727
7916
 
7728
-
7729
- main_layout.addWidget(identities_group)
7917
+ self.centroids = QPushButton("Centroids")
7918
+ self.centroids.setCheckable(True)
7919
+ self.centroids.setChecked(True)
7920
+ identities_layout.addRow("Use Centroids? (Recommended for spheroids) Deselecting finds true nearest neighbors for mask but will be slower, and will only support a single nearest neighbor calculation for each root (rather than an avg)", self.centroids)
7730
7921
 
7922
+ main_layout.addWidget(identities_group)
7731
7923
 
7732
7924
  # Optional Heatmap group box
7733
7925
  heatmap_group = QGroupBox("Optional Heatmap")
@@ -7761,39 +7953,71 @@ class NearNeighDialog(QDialog):
7761
7953
 
7762
7954
  main_layout.addWidget(quant_group)
7763
7955
 
7764
- # Get Distribution group box
7956
+ # Get Distribution group box - ENHANCED STYLING
7765
7957
  distribution_group = QGroupBox("Get Distribution")
7766
7958
  distribution_layout = QVBoxLayout(distribution_group)
7767
7959
 
7768
- run_button = QPushButton("Get Average Nearest Neighbor (Plus Distribution)")
7960
+ run_button = QPushButton("🔍 Get Average Nearest Neighbor (Plus Distribution)")
7961
+ # Style for primary action - blue with larger font
7962
+ run_button.setStyleSheet("""
7963
+ QPushButton {
7964
+ background-color: #2196F3;
7965
+ color: white;
7966
+ border: none;
7967
+ padding: 12px 20px;
7968
+ font-size: 14px;
7969
+ font-weight: bold;
7970
+ border-radius: 6px;
7971
+ }
7972
+ QPushButton:hover {
7973
+ background-color: #1976D2;
7974
+ }
7975
+ QPushButton:pressed {
7976
+ background-color: #0D47A1;
7977
+ }
7978
+ """)
7769
7979
  run_button.clicked.connect(self.run)
7770
7980
  distribution_layout.addWidget(run_button)
7771
7981
 
7772
7982
  main_layout.addWidget(distribution_group)
7773
7983
 
7774
- # Get All Averages group box (only if node_identities exists)
7984
+ # Get All Averages group box - ENHANCED STYLING (only if node_identities exists)
7775
7985
  if my_network.node_identities is not None:
7776
7986
  averages_group = QGroupBox("Get All Averages")
7777
7987
  averages_layout = QVBoxLayout(averages_group)
7778
7988
 
7779
- run_button2 = QPushButton("Get Average Nearest All ID Combinations (No Distribution, No Heatmap)")
7989
+ run_button2 = QPushButton("📊 Get Average Nearest All ID Combinations (No Distribution, No Heatmap)")
7990
+ # Style for secondary action - green with different styling
7991
+ run_button2.setStyleSheet("""
7992
+ QPushButton {
7993
+ background-color: #4CAF50;
7994
+ color: white;
7995
+ border: 2px solid #45a049;
7996
+ padding: 10px 16px;
7997
+ font-size: 13px;
7998
+ font-weight: normal;
7999
+ border-radius: 8px;
8000
+ }
8001
+ QPushButton:hover {
8002
+ background-color: #45a049;
8003
+ border-color: #3d8b40;
8004
+ }
8005
+ QPushButton:pressed {
8006
+ background-color: #3d8b40;
8007
+ }
8008
+ """)
7780
8009
  run_button2.clicked.connect(self.run2)
7781
8010
  averages_layout.addWidget(run_button2)
7782
8011
 
7783
8012
  main_layout.addWidget(averages_group)
7784
8013
 
7785
8014
  def toggle_map(self):
7786
-
7787
8015
  if self.numpy.isChecked():
7788
-
7789
8016
  if not self.map.isChecked():
7790
-
7791
8017
  self.map.click()
7792
8018
 
7793
8019
  def run(self):
7794
-
7795
8020
  try:
7796
-
7797
8021
  try:
7798
8022
  root = self.root.currentText()
7799
8023
  except:
@@ -7808,31 +8032,43 @@ class NearNeighDialog(QDialog):
7808
8032
  numpy = self.numpy.isChecked()
7809
8033
  num = int(self.num.text()) if self.num.text().strip() else 1
7810
8034
  quant = self.quant.isChecked()
8035
+ centroids = self.centroids.isChecked()
8036
+ if not centroids:
8037
+ num = 1
7811
8038
 
7812
8039
  if root is not None and targ is not None:
7813
8040
  title = f"Nearest {num} Neighbor(s) Distance of {targ} from {root}"
7814
8041
  header = f"Shortest Distance to Closest {num} {targ}(s)"
7815
8042
  header2 = f"{root} Node ID"
8043
+ header3 = f'Theoretical Uniform Distance to Closest {num} {targ}(s)'
7816
8044
  else:
7817
8045
  title = f"Nearest {num} Neighbor(s) Distance Between Nodes"
7818
8046
  header = f"Shortest Distance to Closest {num} Nodes"
7819
8047
  header2 = "Root Node ID"
8048
+ header3 = f'Simulated Theoretical Uniform Distance to Closest {num} Nodes'
7820
8049
 
7821
- if my_network.node_centroids is None:
8050
+ if centroids and my_network.node_centroids is None:
7822
8051
  self.parent().show_centroid_dialog()
7823
8052
  if my_network.node_centroids is None:
7824
8053
  return
7825
8054
 
7826
8055
  if not numpy:
7827
- avg, output, quant_overlay = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, quant = quant)
8056
+ 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)
7828
8057
  else:
7829
- avg, output, overlay, quant_overlay = 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)
8058
+ 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)
7830
8059
  self.parent().load_channel(3, overlay, data = True)
7831
8060
 
7832
8061
  if quant_overlay is not None:
7833
8062
  self.parent().load_channel(2, quant_overlay, data = True)
8063
+
8064
+ avg = {header:avg}
8065
+
8066
+ if pred is not None:
8067
+
8068
+ avg[header3] = pred
8069
+
7834
8070
 
7835
- self.parent().format_for_upperright_table([avg], metric = f'Avg {title}', title = f'Avg {title}')
8071
+ self.parent().format_for_upperright_table(avg, 'Category', 'Value', title = f'Avg {title}')
7836
8072
  self.parent().format_for_upperright_table(output, header2, header, title = title)
7837
8073
 
7838
8074
  self.accept()
@@ -7840,27 +8076,24 @@ class NearNeighDialog(QDialog):
7840
8076
  except Exception as e:
7841
8077
  import traceback
7842
8078
  print(traceback.format_exc())
7843
-
7844
8079
  print(f"Error: {e}")
7845
8080
 
7846
8081
  def run2(self):
7847
-
7848
8082
  try:
7849
-
7850
8083
  available = list(set(my_network.node_identities.values()))
7851
-
7852
8084
  num = int(self.num.text()) if self.num.text().strip() else 1
7853
8085
 
8086
+ centroids = self.centroids.isChecked()
8087
+ if not centroids:
8088
+ num = 1
8089
+
7854
8090
  output_dict = {}
7855
8091
 
7856
8092
  while len(available) > 1:
7857
-
7858
8093
  root = available[0]
7859
8094
 
7860
8095
  for targ in available:
7861
-
7862
- avg, _, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num)
7863
-
8096
+ avg, _, _, _ = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, centroids = centroids)
7864
8097
  output_dict[f"{root} vs {targ}"] = avg
7865
8098
 
7866
8099
  del available[0]
@@ -7870,7 +8103,6 @@ class NearNeighDialog(QDialog):
7870
8103
  self.accept()
7871
8104
 
7872
8105
  except Exception as e:
7873
-
7874
8106
  print(f"Error: {e}")
7875
8107
 
7876
8108
 
@@ -8773,15 +9005,6 @@ class ResizeDialog(QDialog):
8773
9005
  undo_button.clicked.connect(lambda: self.run_resize(undo = True))
8774
9006
  layout.addRow(undo_button)
8775
9007
 
8776
- if my_network.xy_scale != my_network.z_scale:
8777
- norm_button_upsize = QPushButton(f"Normalize Scaling with Upsample")
8778
- norm_button_upsize.clicked.connect(lambda: self.run_resize(upsize = True, special = True))
8779
- layout.addRow(norm_button_upsize)
8780
-
8781
- norm_button_downsize = QPushButton("Normalize Scaling with Downsample")
8782
- norm_button_downsize.clicked.connect(lambda: self.run_resize(upsize = False, special = True))
8783
- layout.addRow(norm_button_downsize)
8784
-
8785
9008
  run_button = QPushButton("Run Resize")
8786
9009
  run_button.clicked.connect(self.run_resize)
8787
9010
  layout.addRow(run_button)
@@ -8795,7 +9018,7 @@ class ResizeDialog(QDialog):
8795
9018
 
8796
9019
  def run_resize(self, undo = False, upsize = True, special = False):
8797
9020
  try:
8798
- self.parent().resizing = True
9021
+ self.parent().resizing = False
8799
9022
  # Get parameters
8800
9023
  try:
8801
9024
  resize = float(self.resize.text()) if self.resize.text() else None
@@ -8809,6 +9032,12 @@ class ResizeDialog(QDialog):
8809
9032
 
8810
9033
  resize = resize if resize is not None else (zsize, ysize, xsize)
8811
9034
 
9035
+ if (self.parent().shape[1] * resize) < 1 or (self.parent().shape[2] * resize) < 1:
9036
+ print("Incompatible x/y dimensions")
9037
+ return
9038
+ elif (self.parent().shape[0] * resize) < 1:
9039
+ resize = (1, resize, resize)
9040
+
8812
9041
  if special:
8813
9042
  if upsize:
8814
9043
  if (my_network.z_scale > my_network.xy_scale):
@@ -8850,11 +9079,7 @@ class ResizeDialog(QDialog):
8850
9079
  new_shape = tuple(int(dim * resize) for dim in array_shape)
8851
9080
  else:
8852
9081
  new_shape = tuple(int(dim * factor) for dim, factor in zip(array_shape, resize))
8853
-
8854
- #if any(dim < 1 for dim in new_shape):
8855
- #QMessageBox.critical(self, "Error", f"Resize would result in invalid dimensions: {new_shape}")
8856
- #self.reset_fields()
8857
- #return
9082
+
8858
9083
 
8859
9084
  cubic = self.cubic.isChecked()
8860
9085
  order = 3 if cubic else 0
@@ -8868,7 +9093,7 @@ class ResizeDialog(QDialog):
8868
9093
  for channel in range(4):
8869
9094
  if self.parent().channel_data[channel] is not None:
8870
9095
  resized_data = n3d.resize(self.parent().channel_data[channel], resize, order)
8871
- self.parent().load_channel(channel, channel_data=resized_data, data=True, assign_shape = False)
9096
+ self.parent().load_channel(channel, channel_data=resized_data, data=True)
8872
9097
 
8873
9098
 
8874
9099
 
@@ -8889,7 +9114,7 @@ class ResizeDialog(QDialog):
8889
9114
  for channel in range(4):
8890
9115
  if self.parent().channel_data[channel] is not None:
8891
9116
  resized_data = n3d.upsample_with_padding(self.parent().channel_data[channel], original_shape = self.parent().original_shape)
8892
- self.parent().load_channel(channel, channel_data=resized_data, data=True, assign_shape = False)
9117
+ self.parent().load_channel(channel, channel_data=resized_data, data=True)
8893
9118
 
8894
9119
  if self.parent().mini_overlay_data is not None:
8895
9120
 
@@ -10203,7 +10428,7 @@ class SegmentationWorker(QThread):
10203
10428
  self.mem_lock = mem_lock
10204
10429
  self._stop = False
10205
10430
  self._paused = False # Add pause flag
10206
- if self.machine_window.parent().shape[1] * self.machine_window.parent().shape[2] > 3000 * 3000: #arbitrary throttle for large arrays.
10431
+ if self.machine_window.parent().shape[1] * self.machine_window.parent().shape[2] > 3000 * 3000 * self.machine_window.parent().downsample_factor: #arbitrary throttle for large arrays.
10207
10432
  self.update_interval = 10
10208
10433
  else:
10209
10434
  self.update_interval = 1 # Increased to 1s
@@ -10249,11 +10474,14 @@ class SegmentationWorker(QThread):
10249
10474
  if self._stop:
10250
10475
  break
10251
10476
 
10252
- for z,y,x in foreground_coords:
10253
- self.overlay[z,y,x] = 1
10254
- for z,y,x in background_coords:
10255
- self.overlay[z,y,x] = 2
10256
-
10477
+ if foreground_coords:
10478
+ fg_array = np.array(list(foreground_coords))
10479
+ self.overlay[fg_array[:, 0], fg_array[:, 1], fg_array[:, 2]] = 1
10480
+
10481
+ if background_coords:
10482
+ bg_array = np.array(list(background_coords))
10483
+ self.overlay[bg_array[:, 0], bg_array[:, 1], bg_array[:, 2]] = 2
10484
+
10257
10485
  self.chunks_since_update += 1
10258
10486
  current_time = time.time()
10259
10487
  if (self.chunks_since_update >= self.chunks_per_update and
@@ -10264,34 +10492,20 @@ class SegmentationWorker(QThread):
10264
10492
  self.chunks_since_update = 0
10265
10493
  self.last_update = current_time
10266
10494
 
10495
+ current_xlim = self.machine_window.parent().ax.get_xlim()
10496
+
10497
+ current_ylim = self.machine_window.parent().ax.get_ylim()
10498
+
10499
+ self.machine_window.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
10500
+
10267
10501
  self.finished.emit()
10502
+
10268
10503
 
10269
10504
  except Exception as e:
10270
10505
  print(f"Error in segmentation: {e}")
10271
10506
  import traceback
10272
10507
  traceback.print_exc()
10273
10508
 
10274
- def run_batch(self):
10275
- try:
10276
- foreground_coords, _ = self.segmenter.segment_volume()
10277
-
10278
- # Modify the array directly
10279
- self.overlay.fill(False)
10280
- for z,y,x in foreground_coords:
10281
- # Check for pause/stop during batch processing too
10282
- self._check_pause()
10283
- if self._stop:
10284
- break
10285
- self.overlay[z,y,x] = True
10286
-
10287
- self.finished.emit()
10288
-
10289
- except Exception as e:
10290
- print(f"Error in segmentation: {e}")
10291
- raise
10292
-
10293
-
10294
-
10295
10509
 
10296
10510
  class ThresholdWindow(QMainWindow):
10297
10511
  def __init__(self, parent=None, accepted_mode=0):
@@ -11331,7 +11545,7 @@ class SkeletonizeDialog(QDialog):
11331
11545
  )
11332
11546
 
11333
11547
  if remove > 0:
11334
- result = n3d.remove_branches(result, remove)
11548
+ result = n3d.remove_branches_new(result, remove)
11335
11549
 
11336
11550
 
11337
11551
  # Update both the display data and the network object
@@ -13128,6 +13342,409 @@ class ProxDialog(QDialog):
13128
13342
  print(traceback.format_exc())
13129
13343
 
13130
13344
 
13345
+ class HistogramSelector(QWidget):
13346
+ def __init__(self, network_analysis_instance):
13347
+ super().__init__()
13348
+ self.network_analysis = network_analysis_instance
13349
+ self.G = my_network.network
13350
+ self.init_ui()
13351
+
13352
+ def init_ui(self):
13353
+ self.setWindowTitle('Network Analysis - Histogram Selector')
13354
+ self.setGeometry(300, 300, 400, 700) # Increased height for more buttons
13355
+
13356
+ layout = QVBoxLayout()
13357
+
13358
+ # Title label
13359
+ title_label = QLabel('Select Histogram to Generate:')
13360
+ title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
13361
+ title_label.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
13362
+ layout.addWidget(title_label)
13363
+
13364
+ # Create buttons for each histogram type
13365
+ self.create_button(layout, "Shortest Path Length Distribution", self.shortest_path_histogram)
13366
+ self.create_button(layout, "Degree Centrality", self.degree_centrality_histogram)
13367
+ self.create_button(layout, "Betweenness Centrality", self.betweenness_centrality_histogram)
13368
+ self.create_button(layout, "Closeness Centrality", self.closeness_centrality_histogram)
13369
+ self.create_button(layout, "Eigenvector Centrality", self.eigenvector_centrality_histogram)
13370
+ self.create_button(layout, "Clustering Coefficient", self.clustering_coefficient_histogram)
13371
+ self.create_button(layout, "Degree Distribution", self.degree_distribution_histogram)
13372
+ self.create_button(layout, "Node Connectivity", self.node_connectivity_histogram)
13373
+ self.create_button(layout, "Eccentricity", self.eccentricity_histogram)
13374
+ self.create_button(layout, "K-Core Decomposition", self.kcore_histogram)
13375
+ self.create_button(layout, "Triangle Count", self.triangle_count_histogram)
13376
+ self.create_button(layout, "Load Centrality", self.load_centrality_histogram)
13377
+ self.create_button(layout, "Communicability Betweenness Centrality", self.communicability_centrality_histogram)
13378
+ self.create_button(layout, "Harmonic Centrality", self.harmonic_centrality_histogram)
13379
+ self.create_button(layout, "Current Flow Betweenness", self.current_flow_betweenness_histogram)
13380
+ self.create_button(layout, "Dispersion", self.dispersion_histogram)
13381
+ self.create_button(layout, "Network Bridges", self.bridges_analysis)
13382
+
13383
+ # Close button
13384
+ close_button = QPushButton('Close')
13385
+ close_button.clicked.connect(self.close)
13386
+ close_button.setStyleSheet("QPushButton { background-color: #f44336; color: white; font-weight: bold; }")
13387
+ layout.addWidget(close_button)
13388
+
13389
+ self.setLayout(layout)
13390
+
13391
+ def create_button(self, layout, text, callback):
13392
+ button = QPushButton(text)
13393
+ button.clicked.connect(callback)
13394
+ button.setMinimumHeight(40)
13395
+ button.setStyleSheet("""
13396
+ QPushButton {
13397
+ background-color: #4CAF50;
13398
+ color: white;
13399
+ border: none;
13400
+ padding: 10px;
13401
+ font-size: 14px;
13402
+ font-weight: bold;
13403
+ border-radius: 5px;
13404
+ }
13405
+ QPushButton:hover {
13406
+ background-color: #45a049;
13407
+ }
13408
+ QPushButton:pressed {
13409
+ background-color: #3d8b40;
13410
+ }
13411
+ """)
13412
+ layout.addWidget(button)
13413
+
13414
+ def shortest_path_histogram(self):
13415
+ try:
13416
+ shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(self.G))
13417
+ diameter = max(nx.eccentricity(self.G, sp=shortest_path_lengths).values())
13418
+ path_lengths = np.zeros(diameter + 1, dtype=int)
13419
+
13420
+ for pls in shortest_path_lengths.values():
13421
+ pl, cnts = np.unique(list(pls.values()), return_counts=True)
13422
+ path_lengths[pl] += cnts
13423
+
13424
+ freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
13425
+
13426
+ fig, ax = plt.subplots(figsize=(15, 8))
13427
+ ax.bar(np.arange(1, diameter + 1), height=freq_percent)
13428
+ ax.set_title(
13429
+ "Distribution of shortest path length in G", fontdict={"size": 35}, loc="center"
13430
+ )
13431
+ ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
13432
+ ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
13433
+ plt.show()
13434
+
13435
+ freq_dict = {freq: length for length, freq in enumerate(freq_percent, start=1)}
13436
+ self.network_analysis.format_for_upperright_table(freq_dict, metric='Frequency (%)',
13437
+ value='Shortest Path Length',
13438
+ title="Distribution of shortest path length in G")
13439
+ except Exception as e:
13440
+ print(f"Error generating shortest path histogram: {e}")
13441
+
13442
+ def degree_centrality_histogram(self):
13443
+ try:
13444
+ degree_centrality = nx.centrality.degree_centrality(self.G)
13445
+ plt.figure(figsize=(15, 8))
13446
+ plt.hist(degree_centrality.values(), bins=25)
13447
+ plt.xticks(ticks=[0, 0.025, 0.05, 0.1, 0.15, 0.2])
13448
+ plt.title("Degree Centrality Histogram ", fontdict={"size": 35}, loc="center")
13449
+ plt.xlabel("Degree Centrality", fontdict={"size": 20})
13450
+ plt.ylabel("Counts", fontdict={"size": 20})
13451
+ plt.show()
13452
+ self.network_analysis.format_for_upperright_table(degree_centrality, metric='Node',
13453
+ value='Degree Centrality',
13454
+ title="Degree Centrality Table")
13455
+ except Exception as e:
13456
+ print(f"Error generating degree centrality histogram: {e}")
13457
+
13458
+ def betweenness_centrality_histogram(self):
13459
+ try:
13460
+ betweenness_centrality = nx.centrality.betweenness_centrality(self.G)
13461
+ plt.figure(figsize=(15, 8))
13462
+ plt.hist(betweenness_centrality.values(), bins=100)
13463
+ plt.xticks(ticks=[0, 0.02, 0.1, 0.2, 0.3, 0.4, 0.5])
13464
+ plt.title("Betweenness Centrality Histogram ", fontdict={"size": 35}, loc="center")
13465
+ plt.xlabel("Betweenness Centrality", fontdict={"size": 20})
13466
+ plt.ylabel("Counts", fontdict={"size": 20})
13467
+ plt.show()
13468
+ self.network_analysis.format_for_upperright_table(betweenness_centrality, metric='Node',
13469
+ value='Betweenness Centrality',
13470
+ title="Betweenness Centrality Table")
13471
+ except Exception as e:
13472
+ print(f"Error generating betweenness centrality histogram: {e}")
13473
+
13474
+ def closeness_centrality_histogram(self):
13475
+ try:
13476
+ closeness_centrality = nx.centrality.closeness_centrality(self.G)
13477
+ plt.figure(figsize=(15, 8))
13478
+ plt.hist(closeness_centrality.values(), bins=60)
13479
+ plt.title("Closeness Centrality Histogram ", fontdict={"size": 35}, loc="center")
13480
+ plt.xlabel("Closeness Centrality", fontdict={"size": 20})
13481
+ plt.ylabel("Counts", fontdict={"size": 20})
13482
+ plt.show()
13483
+ self.network_analysis.format_for_upperright_table(closeness_centrality, metric='Node',
13484
+ value='Closeness Centrality',
13485
+ title="Closeness Centrality Table")
13486
+ except Exception as e:
13487
+ print(f"Error generating closeness centrality histogram: {e}")
13488
+
13489
+ def eigenvector_centrality_histogram(self):
13490
+ try:
13491
+ eigenvector_centrality = nx.centrality.eigenvector_centrality(self.G)
13492
+ plt.figure(figsize=(15, 8))
13493
+ plt.hist(eigenvector_centrality.values(), bins=60)
13494
+ plt.xticks(ticks=[0, 0.01, 0.02, 0.04, 0.06, 0.08])
13495
+ plt.title("Eigenvector Centrality Histogram ", fontdict={"size": 35}, loc="center")
13496
+ plt.xlabel("Eigenvector Centrality", fontdict={"size": 20})
13497
+ plt.ylabel("Counts", fontdict={"size": 20})
13498
+ plt.show()
13499
+ self.network_analysis.format_for_upperright_table(eigenvector_centrality, metric='Node',
13500
+ value='Eigenvector Centrality',
13501
+ title="Eigenvector Centrality Table")
13502
+ except Exception as e:
13503
+ print(f"Error generating eigenvector centrality histogram: {e}")
13504
+
13505
+ def clustering_coefficient_histogram(self):
13506
+ try:
13507
+ clusters = nx.clustering(self.G)
13508
+ plt.figure(figsize=(15, 8))
13509
+ plt.hist(clusters.values(), bins=50)
13510
+ plt.title("Clustering Coefficient Histogram ", fontdict={"size": 35}, loc="center")
13511
+ plt.xlabel("Clustering Coefficient", fontdict={"size": 20})
13512
+ plt.ylabel("Counts", fontdict={"size": 20})
13513
+ plt.show()
13514
+ self.network_analysis.format_for_upperright_table(clusters, metric='Node',
13515
+ value='Clustering Coefficient',
13516
+ title="Clustering Coefficient Table")
13517
+ except Exception as e:
13518
+ print(f"Error generating clustering coefficient histogram: {e}")
13519
+
13520
+ def bridges_analysis(self):
13521
+ try:
13522
+ bridges = list(nx.bridges(self.G))
13523
+ self.network_analysis.format_for_upperright_table(bridges, metric='Node Pair',
13524
+ title="Bridges")
13525
+ except Exception as e:
13526
+ print(f"Error generating bridges analysis: {e}")
13527
+
13528
+ def degree_distribution_histogram(self):
13529
+ """Raw degree distribution - very useful for understanding network topology"""
13530
+ try:
13531
+ degrees = [self.G.degree(n) for n in self.G.nodes()]
13532
+ plt.figure(figsize=(15, 8))
13533
+ plt.hist(degrees, bins=max(30, int(np.sqrt(len(degrees)))), alpha=0.7)
13534
+ plt.title("Degree Distribution", fontdict={"size": 35}, loc="center")
13535
+ plt.xlabel("Degree", fontdict={"size": 20})
13536
+ plt.ylabel("Frequency", fontdict={"size": 20})
13537
+ plt.yscale('log') # Often useful for degree distributions
13538
+ plt.show()
13539
+
13540
+ degree_dict = {node: deg for node, deg in self.G.degree()}
13541
+ self.network_analysis.format_for_upperright_table(degree_dict, metric='Node',
13542
+ value='Degree', title="Degree Distribution Table")
13543
+ except Exception as e:
13544
+ print(f"Error generating degree distribution histogram: {e}")
13545
+
13546
+
13547
+ def node_connectivity_histogram(self):
13548
+ """Local node connectivity - minimum number of nodes that must be removed to disconnect neighbors"""
13549
+ try:
13550
+ if self.G.number_of_nodes() > 500: # Skip for large networks (computationally expensive)
13551
+ print("Note this analysis may be slow for large network (>500 nodes)")
13552
+ #return
13553
+
13554
+ connectivity = {}
13555
+ for node in self.G.nodes():
13556
+ neighbors = list(self.G.neighbors(node))
13557
+ if len(neighbors) > 1:
13558
+ connectivity[node] = nx.node_connectivity(self.G, neighbors[0], neighbors[1])
13559
+ else:
13560
+ connectivity[node] = 0
13561
+
13562
+ plt.figure(figsize=(15, 8))
13563
+ plt.hist(connectivity.values(), bins=20, alpha=0.7)
13564
+ plt.title("Node Connectivity Distribution", fontdict={"size": 35}, loc="center")
13565
+ plt.xlabel("Node Connectivity", fontdict={"size": 20})
13566
+ plt.ylabel("Frequency", fontdict={"size": 20})
13567
+ plt.show()
13568
+ self.network_analysis.format_for_upperright_table(connectivity, metric='Node',
13569
+ value='Connectivity', title="Node Connectivity Table")
13570
+ except Exception as e:
13571
+ print(f"Error generating node connectivity histogram: {e}")
13572
+
13573
+ def eccentricity_histogram(self):
13574
+ """Eccentricity - maximum distance from a node to any other node"""
13575
+ try:
13576
+ if not nx.is_connected(self.G):
13577
+ print("Graph is not connected. Using largest connected component.")
13578
+ largest_cc = max(nx.connected_components(self.G), key=len)
13579
+ G_cc = self.G.subgraph(largest_cc)
13580
+ eccentricity = nx.eccentricity(G_cc)
13581
+ else:
13582
+ eccentricity = nx.eccentricity(self.G)
13583
+
13584
+ plt.figure(figsize=(15, 8))
13585
+ plt.hist(eccentricity.values(), bins=20, alpha=0.7)
13586
+ plt.title("Eccentricity Distribution", fontdict={"size": 35}, loc="center")
13587
+ plt.xlabel("Eccentricity", fontdict={"size": 20})
13588
+ plt.ylabel("Frequency", fontdict={"size": 20})
13589
+ plt.show()
13590
+ self.network_analysis.format_for_upperright_table(eccentricity, metric='Node',
13591
+ value='Eccentricity', title="Eccentricity Table")
13592
+ except Exception as e:
13593
+ print(f"Error generating eccentricity histogram: {e}")
13594
+
13595
+ def kcore_histogram(self):
13596
+ """K-core decomposition - identifies cohesive subgroups"""
13597
+ try:
13598
+ kcore = nx.core_number(self.G)
13599
+ plt.figure(figsize=(15, 8))
13600
+ plt.hist(kcore.values(), bins=max(5, max(kcore.values())), alpha=0.7)
13601
+ plt.title("K-Core Distribution", fontdict={"size": 35}, loc="center")
13602
+ plt.xlabel("K-Core Number", fontdict={"size": 20})
13603
+ plt.ylabel("Frequency", fontdict={"size": 20})
13604
+ plt.show()
13605
+ self.network_analysis.format_for_upperright_table(kcore, metric='Node',
13606
+ value='K-Core', title="K-Core Table")
13607
+ except Exception as e:
13608
+ print(f"Error generating k-core histogram: {e}")
13609
+
13610
+ def triangle_count_histogram(self):
13611
+ """Number of triangles each node participates in"""
13612
+ try:
13613
+ triangles = nx.triangles(self.G)
13614
+ plt.figure(figsize=(15, 8))
13615
+ plt.hist(triangles.values(), bins=30, alpha=0.7)
13616
+ plt.title("Triangle Count Distribution", fontdict={"size": 35}, loc="center")
13617
+ plt.xlabel("Number of Triangles", fontdict={"size": 20})
13618
+ plt.ylabel("Frequency", fontdict={"size": 20})
13619
+ plt.show()
13620
+ self.network_analysis.format_for_upperright_table(triangles, metric='Node',
13621
+ value='Triangle Count', title="Triangle Count Table")
13622
+ except Exception as e:
13623
+ print(f"Error generating triangle count histogram: {e}")
13624
+
13625
+ def load_centrality_histogram(self):
13626
+ """Load centrality - fraction of shortest paths passing through each node"""
13627
+ try:
13628
+ if self.G.number_of_nodes() > 1000: # Skip for very large networks
13629
+ print("Note this analysis may be slow for large network (>1000 nodes)")
13630
+ #return
13631
+
13632
+ load_centrality = nx.load_centrality(self.G)
13633
+ plt.figure(figsize=(15, 8))
13634
+ plt.hist(load_centrality.values(), bins=50, alpha=0.7)
13635
+ plt.title("Load Centrality Distribution", fontdict={"size": 35}, loc="center")
13636
+ plt.xlabel("Load Centrality", fontdict={"size": 20})
13637
+ plt.ylabel("Frequency", fontdict={"size": 20})
13638
+ plt.show()
13639
+ self.network_analysis.format_for_upperright_table(load_centrality, metric='Node',
13640
+ value='Load Centrality', title="Load Centrality Table")
13641
+ except Exception as e:
13642
+ print(f"Error generating load centrality histogram: {e}")
13643
+
13644
+ def communicability_centrality_histogram(self):
13645
+ """Communicability centrality - based on communicability between nodes"""
13646
+ try:
13647
+ if self.G.number_of_nodes() > 500: # Skip for large networks (memory intensive)
13648
+ print("Note this analysis may be slow for large network (>500 nodes)")
13649
+ #return
13650
+
13651
+ # Use the correct function name - it's in the communicability module
13652
+ comm_centrality = nx.communicability_betweenness_centrality(self.G)
13653
+ plt.figure(figsize=(15, 8))
13654
+ plt.hist(comm_centrality.values(), bins=50, alpha=0.7)
13655
+ plt.title("Communicability Betweenness Centrality Distribution", fontdict={"size": 35}, loc="center")
13656
+ plt.xlabel("Communicability Betweenness Centrality", fontdict={"size": 20})
13657
+ plt.ylabel("Frequency", fontdict={"size": 20})
13658
+ plt.show()
13659
+ self.network_analysis.format_for_upperright_table(comm_centrality, metric='Node',
13660
+ value='Communicability Betweenness Centrality',
13661
+ title="Communicability Betweenness Centrality Table")
13662
+ except Exception as e:
13663
+ print(f"Error generating communicability betweenness centrality histogram: {e}")
13664
+
13665
+ def harmonic_centrality_histogram(self):
13666
+ """Harmonic centrality - better than closeness for disconnected networks"""
13667
+ try:
13668
+ harmonic_centrality = nx.harmonic_centrality(self.G)
13669
+ plt.figure(figsize=(15, 8))
13670
+ plt.hist(harmonic_centrality.values(), bins=50, alpha=0.7)
13671
+ plt.title("Harmonic Centrality Distribution", fontdict={"size": 35}, loc="center")
13672
+ plt.xlabel("Harmonic Centrality", fontdict={"size": 20})
13673
+ plt.ylabel("Frequency", fontdict={"size": 20})
13674
+ plt.show()
13675
+ self.network_analysis.format_for_upperright_table(harmonic_centrality, metric='Node',
13676
+ value='Harmonic Centrality',
13677
+ title="Harmonic Centrality Table")
13678
+ except Exception as e:
13679
+ print(f"Error generating harmonic centrality histogram: {e}")
13680
+
13681
+ def current_flow_betweenness_histogram(self):
13682
+ """Current flow betweenness - models network as electrical circuit"""
13683
+ try:
13684
+ if self.G.number_of_nodes() > 500: # Skip for large networks (computationally expensive)
13685
+ print("Note this analysis may be slow for large network (>500 nodes)")
13686
+ #return
13687
+
13688
+ current_flow = nx.current_flow_betweenness_centrality(self.G)
13689
+ plt.figure(figsize=(15, 8))
13690
+ plt.hist(current_flow.values(), bins=50, alpha=0.7)
13691
+ plt.title("Current Flow Betweenness Centrality Distribution", fontdict={"size": 35}, loc="center")
13692
+ plt.xlabel("Current Flow Betweenness Centrality", fontdict={"size": 20})
13693
+ plt.ylabel("Frequency", fontdict={"size": 20})
13694
+ plt.show()
13695
+ self.network_analysis.format_for_upperright_table(current_flow, metric='Node',
13696
+ value='Current Flow Betweenness',
13697
+ title="Current Flow Betweenness Table")
13698
+ except Exception as e:
13699
+ print(f"Error generating current flow betweenness histogram: {e}")
13700
+
13701
+ def dispersion_histogram(self):
13702
+ """Dispersion - measures how scattered a node's neighbors are"""
13703
+ try:
13704
+ if self.G.number_of_nodes() > 300: # Skip for large networks (very computationally expensive)
13705
+ print("Note this analysis may be slow for large network (>300 nodes)")
13706
+ #return
13707
+
13708
+ # Calculate average dispersion for each node
13709
+ dispersion_values = {}
13710
+ nodes = list(self.G.nodes())
13711
+
13712
+ for u in nodes:
13713
+ if self.G.degree(u) < 2: # Need at least 2 neighbors for dispersion
13714
+ dispersion_values[u] = 0
13715
+ continue
13716
+
13717
+ # Calculate dispersion for node u with all its neighbors
13718
+ neighbors = list(self.G.neighbors(u))
13719
+ if len(neighbors) < 2:
13720
+ dispersion_values[u] = 0
13721
+ continue
13722
+
13723
+ # Get dispersion scores for this node with all neighbors
13724
+ disp_scores = []
13725
+ for v in neighbors:
13726
+ try:
13727
+ disp_score = nx.dispersion(self.G, u, v)
13728
+ disp_scores.append(disp_score)
13729
+ except:
13730
+ continue
13731
+
13732
+ # Average dispersion for this node
13733
+ dispersion_values[u] = sum(disp_scores) / len(disp_scores) if disp_scores else 0
13734
+
13735
+ plt.figure(figsize=(15, 8))
13736
+ plt.hist(dispersion_values.values(), bins=30, alpha=0.7)
13737
+ plt.title("Average Dispersion Distribution", fontdict={"size": 35}, loc="center")
13738
+ plt.xlabel("Average Dispersion", fontdict={"size": 20})
13739
+ plt.ylabel("Frequency", fontdict={"size": 20})
13740
+ plt.show()
13741
+ self.network_analysis.format_for_upperright_table(dispersion_values, metric='Node',
13742
+ value='Average Dispersion',
13743
+ title="Average Dispersion Table")
13744
+ except Exception as e:
13745
+ print(f"Error generating dispersion histogram: {e}")
13746
+
13747
+
13131
13748
 
13132
13749
  # Initiating this program from the script line:
13133
13750