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.
- nettracer3d/cellpose_manager.py +22 -11
- nettracer3d/modularity.py +24 -9
- nettracer3d/neighborhoods.py +153 -30
- nettracer3d/nettracer.py +321 -54
- nettracer3d/nettracer_gui.py +1095 -478
- nettracer3d/proximity.py +101 -48
- nettracer3d/segmenter.py +514 -372
- nettracer3d/segmenter_GPU.py +434 -281
- {nettracer3d-0.8.9.dist-info → nettracer3d-0.9.1.dist-info}/METADATA +7 -4
- nettracer3d-0.9.1.dist-info/RECORD +25 -0
- nettracer3d-0.8.9.dist-info/RECORD +0 -25
- {nettracer3d-0.8.9.dist-info → nettracer3d-0.9.1.dist-info}/WHEEL +0 -0
- {nettracer3d-0.8.9.dist-info → nettracer3d-0.9.1.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.8.9.dist-info → nettracer3d-0.9.1.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.8.9.dist-info → nettracer3d-0.9.1.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -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
|
-
#
|
|
2797
|
-
|
|
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((*
|
|
2817
|
-
rgb_alpha[:, :, :3] =
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
2896
|
+
normalized_image = np.zeros_like(display_image)
|
|
2846
2897
|
else:
|
|
2847
|
-
normalized_image = np.clip((
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
2925
|
+
highlight_rgba = self.create_highlight_rgba(display_highlight, yellow=True)
|
|
2873
2926
|
else:
|
|
2874
|
-
highlight_rgba = self.create_highlight_rgba(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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] <=
|
|
3131
|
-
new_xlim[1] >= self.
|
|
3132
|
-
new_ylim[0] <=
|
|
3133
|
-
new_ylim[1] >= self.
|
|
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.
|
|
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
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
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("
|
|
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
|
|
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
|
-
|
|
3628
|
-
|
|
3824
|
+
|
|
3825
|
+
# Get filename from user
|
|
3629
3826
|
filename, _ = QFileDialog.getSaveFileName(
|
|
3630
3827
|
self,
|
|
3631
3828
|
f"Save Image As",
|
|
3632
|
-
"",
|
|
3633
|
-
"TIFF Files (*.tif *.tiff);;All Files (*)"
|
|
3829
|
+
"",
|
|
3830
|
+
"PNG Files (*.png);;TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)"
|
|
3634
3831
|
)
|
|
3635
3832
|
|
|
3636
|
-
if filename:
|
|
3637
|
-
#
|
|
3638
|
-
if
|
|
3639
|
-
|
|
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
|
-
|
|
3642
|
-
|
|
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
|
-
|
|
3645
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
#
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
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
|
|
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.
|
|
4809
|
-
|
|
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
|
-
|
|
4994
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
#
|
|
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(
|
|
5221
|
-
|
|
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
|
|
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[
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
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
|
-
#
|
|
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
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
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]
|
|
5521
|
+
current_image = self.channel_data[channel][self.current_slice]
|
|
5271
5522
|
else:
|
|
5272
5523
|
current_image = self.channel_data[channel]
|
|
5273
5524
|
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
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
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
self.
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
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),
|
|
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(
|
|
5314
|
-
|
|
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
|
-
|
|
5329
|
-
|
|
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
|
|
5587
|
+
self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
|
|
5338
5588
|
|
|
5339
|
-
#
|
|
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
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
self.ax.imshow(
|
|
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
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5392
|
-
|
|
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
|
-
#
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
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
|
-
|
|
5437
|
-
|
|
5438
|
-
self.ax.
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
self.
|
|
5442
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7140
|
-
|
|
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
|
-
|
|
7414
|
-
self.label
|
|
7415
|
-
self.label.
|
|
7416
|
-
self.label
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
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.
|
|
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
|
|