nettracer3d 1.0.8__py3-none-any.whl → 1.0.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nettracer3d might be problematic. Click here for more details.
- nettracer3d/morphology.py +108 -17
- nettracer3d/nettracer.py +63 -13
- nettracer3d/nettracer_gui.py +453 -156
- {nettracer3d-1.0.8.dist-info → nettracer3d-1.0.9.dist-info}/METADATA +6 -4
- {nettracer3d-1.0.8.dist-info → nettracer3d-1.0.9.dist-info}/RECORD +9 -9
- {nettracer3d-1.0.8.dist-info → nettracer3d-1.0.9.dist-info}/WHEEL +0 -0
- {nettracer3d-1.0.8.dist-info → nettracer3d-1.0.9.dist-info}/entry_points.txt +0 -0
- {nettracer3d-1.0.8.dist-info → nettracer3d-1.0.9.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-1.0.8.dist-info → nettracer3d-1.0.9.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -461,8 +461,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
461
461
|
self.last_paint_pos = None
|
|
462
462
|
|
|
463
463
|
self.resume = False
|
|
464
|
-
|
|
465
|
-
self.hold_update = False
|
|
466
464
|
self._first_pan_done = False
|
|
467
465
|
|
|
468
466
|
|
|
@@ -878,23 +876,20 @@ class ImageViewerWindow(QMainWindow):
|
|
|
878
876
|
elif self.scroll_direction > 0 and new_value <= self.slice_slider.maximum():
|
|
879
877
|
self.slice_slider.setValue(new_value)
|
|
880
878
|
|
|
881
|
-
def evaluate_mini(self, mode = 'nodes'):
|
|
882
|
-
if mode == 'nodes':
|
|
883
|
-
if self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2] > self.mini_thresh:
|
|
884
|
-
self.mini_overlay = True
|
|
885
|
-
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
886
|
-
else:
|
|
887
|
-
self.create_highlight_overlay(node_indices=self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
888
|
-
elif mode == 'edges':
|
|
889
879
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
880
|
+
def confirm_mini_thresh(self):
|
|
881
|
+
|
|
882
|
+
if self.shape[0] * self.shape[1] * self.shape[2] > self.mini_thresh:
|
|
883
|
+
self.mini_overlay = True
|
|
884
|
+
return True
|
|
885
|
+
else:
|
|
886
|
+
return False
|
|
887
|
+
|
|
888
|
+
def evaluate_mini(self, mode = 'nodes'):
|
|
889
|
+
if self.confirm_mini_thresh():
|
|
890
|
+
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
891
|
+
else:
|
|
892
|
+
self.create_highlight_overlay(node_indices=self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
898
893
|
|
|
899
894
|
def create_highlight_overlay(self, node_indices=None, edge_indices=None, overlay1_indices = None, overlay2_indices = None, bounds = False):
|
|
900
895
|
"""
|
|
@@ -1021,13 +1016,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1021
1016
|
|
|
1022
1017
|
# Combine results
|
|
1023
1018
|
if node_overlay is not None:
|
|
1024
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, node_overlay)
|
|
1019
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, node_overlay).astype(np.uint8)
|
|
1025
1020
|
if edge_overlay is not None:
|
|
1026
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, edge_overlay)
|
|
1021
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, edge_overlay).astype(np.uint8)
|
|
1027
1022
|
if overlay1_overlay is not None:
|
|
1028
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay1_overlay)
|
|
1023
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay1_overlay).astype(np.uint8)
|
|
1029
1024
|
if overlay2_overlay is not None:
|
|
1030
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay2_overlay)
|
|
1025
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay2_overlay).astype(np.uint8)
|
|
1031
1026
|
|
|
1032
1027
|
# Update display
|
|
1033
1028
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
@@ -1036,6 +1031,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1036
1031
|
|
|
1037
1032
|
"""Highlight overlay generation method specific for the segmenter interactive mode"""
|
|
1038
1033
|
|
|
1034
|
+
self.mini_overlay_data = None
|
|
1035
|
+
self.highlight_overlay = None
|
|
1036
|
+
|
|
1039
1037
|
def process_chunk_bounds(chunk_data, indices_to_check):
|
|
1040
1038
|
"""Process a single chunk of the array to create highlight mask"""
|
|
1041
1039
|
mask = (chunk_data >= indices_to_check[0]) & (chunk_data <= indices_to_check[1])
|
|
@@ -1308,6 +1306,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1308
1306
|
select_nodes = select_all_menu.addAction("Nodes")
|
|
1309
1307
|
select_both = select_all_menu.addAction("Nodes + Edges")
|
|
1310
1308
|
select_edges = select_all_menu.addAction("Edges")
|
|
1309
|
+
select_net_nodes = select_all_menu.addAction("Nodes in Network")
|
|
1310
|
+
select_net_both = select_all_menu.addAction("Nodes + Edges in Network")
|
|
1311
|
+
select_net_edges = select_all_menu.addAction("Edges in Network")
|
|
1311
1312
|
context_menu.addMenu(select_all_menu)
|
|
1312
1313
|
|
|
1313
1314
|
if len(self.clicked_values['nodes']) > 0 or len(self.clicked_values['edges']) > 0:
|
|
@@ -1382,6 +1383,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1382
1383
|
select_nodes.triggered.connect(lambda: self.handle_select_all(edges = False, nodes = True))
|
|
1383
1384
|
select_both.triggered.connect(lambda: self.handle_select_all(edges = True))
|
|
1384
1385
|
select_edges.triggered.connect(lambda: self.handle_select_all(edges = True, nodes = False))
|
|
1386
|
+
select_net_nodes.triggered.connect(lambda: self.handle_select_all(edges = False, nodes = True, network = True))
|
|
1387
|
+
select_net_both.triggered.connect(lambda: self.handle_select_all(edges = True, network = True))
|
|
1388
|
+
select_net_edges.triggered.connect(lambda: self.handle_select_all(edges = True, nodes = False, network = True))
|
|
1385
1389
|
if self.highlight_overlay is not None or self.mini_overlay_data is not None:
|
|
1386
1390
|
highlight_select = context_menu.addAction("Add highlight in network selection")
|
|
1387
1391
|
highlight_select.triggered.connect(self.handle_highlight_select)
|
|
@@ -2066,12 +2070,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2066
2070
|
|
|
2067
2071
|
|
|
2068
2072
|
|
|
2069
|
-
def handle_select_all(self, nodes = True, edges = False):
|
|
2073
|
+
def handle_select_all(self, nodes = True, edges = False, network = False):
|
|
2070
2074
|
|
|
2071
2075
|
try:
|
|
2072
2076
|
|
|
2073
2077
|
if nodes:
|
|
2074
|
-
|
|
2078
|
+
if not network:
|
|
2079
|
+
nodes = list(np.unique(my_network.nodes))
|
|
2080
|
+
else:
|
|
2081
|
+
nodes = list(set(my_network.network_lists[0] + my_network.network_lists[1]))
|
|
2075
2082
|
if nodes[0] == 0:
|
|
2076
2083
|
del nodes[0]
|
|
2077
2084
|
num = (self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2])
|
|
@@ -2079,7 +2086,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2079
2086
|
else:
|
|
2080
2087
|
nodes = []
|
|
2081
2088
|
if edges:
|
|
2082
|
-
|
|
2089
|
+
if not network:
|
|
2090
|
+
edges = list(np.unique(my_network.edges))
|
|
2091
|
+
else:
|
|
2092
|
+
edges = my_network.network_lists[2]
|
|
2083
2093
|
num = (self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2])
|
|
2084
2094
|
if edges[0] == 0:
|
|
2085
2095
|
del edges[0]
|
|
@@ -2202,7 +2212,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2202
2212
|
except:
|
|
2203
2213
|
pass
|
|
2204
2214
|
|
|
2205
|
-
self.format_for_upperright_table(info_dict, title = f'Info on Object')
|
|
2215
|
+
self.format_for_upperright_table(info_dict, title = f'Info on Object', sort = False)
|
|
2206
2216
|
|
|
2207
2217
|
except:
|
|
2208
2218
|
pass
|
|
@@ -3695,17 +3705,23 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3695
3705
|
|
|
3696
3706
|
# Add highlight overlays if they exist (with downsampling)
|
|
3697
3707
|
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3708
|
+
try:
|
|
3709
|
+
display_overlay = crop_and_downsample_image(self.mini_overlay_data, y_min_padded, y_max_padded, x_min_padded, x_max_padded, downsample_factor)
|
|
3710
|
+
highlight_rgba = self.create_highlight_rgba(display_overlay, yellow=True)
|
|
3711
|
+
composite = self.blend_layers(composite, highlight_rgba)
|
|
3712
|
+
except:
|
|
3713
|
+
pass
|
|
3701
3714
|
elif self.highlight_overlay is not None and self.highlight:
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3715
|
+
try:
|
|
3716
|
+
highlight_slice = self.highlight_overlay[self.current_slice]
|
|
3717
|
+
display_highlight = crop_and_downsample_image(highlight_slice, y_min_padded, y_max_padded, x_min_padded, x_max_padded, downsample_factor)
|
|
3718
|
+
if self.machine_window is None:
|
|
3719
|
+
highlight_rgba = self.create_highlight_rgba(display_highlight, yellow=True)
|
|
3720
|
+
else:
|
|
3721
|
+
highlight_rgba = self.create_highlight_rgba(display_highlight, yellow=False)
|
|
3722
|
+
composite = self.blend_layers(composite, highlight_rgba)
|
|
3723
|
+
except:
|
|
3724
|
+
pass
|
|
3709
3725
|
|
|
3710
3726
|
# Convert to 0-255 range for display
|
|
3711
3727
|
return (composite * 255).astype(np.uint8)
|
|
@@ -4517,6 +4533,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4517
4533
|
for i in range(4):
|
|
4518
4534
|
load_action = load_menu.addAction(f"Load {self.channel_names[i]}")
|
|
4519
4535
|
load_action.triggered.connect(lambda checked, ch=i: self.load_channel(ch))
|
|
4536
|
+
load_action = load_menu.addAction("Load Full-Sized Highlight Overlay")
|
|
4537
|
+
load_action.triggered.connect(lambda: self.load_channel(channel_index = 4, load_highlight = True))
|
|
4520
4538
|
load_action = load_menu.addAction("Load Network")
|
|
4521
4539
|
load_action.triggered.connect(self.load_network)
|
|
4522
4540
|
load_action = load_menu.addAction("Load From Excel Helper")
|
|
@@ -4770,7 +4788,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4770
4788
|
# Invalid input - reset to default
|
|
4771
4789
|
self.downsample_factor = 1
|
|
4772
4790
|
|
|
4773
|
-
|
|
4791
|
+
try:
|
|
4792
|
+
self.throttle = self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor
|
|
4793
|
+
except:
|
|
4794
|
+
self.throttle = False
|
|
4774
4795
|
|
|
4775
4796
|
# Optional: Trigger display update if you want immediate effect
|
|
4776
4797
|
if update:
|
|
@@ -4938,7 +4959,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4938
4959
|
|
|
4939
4960
|
|
|
4940
4961
|
|
|
4941
|
-
def format_for_upperright_table(self, data, metric='Metric', value='Value', title=None):
|
|
4962
|
+
def format_for_upperright_table(self, data, metric='Metric', value='Value', title=None, sort = True):
|
|
4942
4963
|
"""
|
|
4943
4964
|
Format dictionary or list data for display in upper right table.
|
|
4944
4965
|
|
|
@@ -5024,11 +5045,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5024
5045
|
table = CustomTableView(self)
|
|
5025
5046
|
table.setModel(PandasModel(df))
|
|
5026
5047
|
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5048
|
+
if sort:
|
|
5049
|
+
try:
|
|
5050
|
+
first_column_name = table.model()._data.columns[0]
|
|
5051
|
+
table.sort_table(first_column_name, ascending=True)
|
|
5052
|
+
except:
|
|
5053
|
+
pass
|
|
5032
5054
|
|
|
5033
5055
|
# Add to tabbed widget
|
|
5034
5056
|
if title is None:
|
|
@@ -5620,6 +5642,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5620
5642
|
self.last_saved = os.path.dirname(directory)
|
|
5621
5643
|
self.last_save_name = directory
|
|
5622
5644
|
|
|
5645
|
+
self.channel_data = [None] * 5
|
|
5623
5646
|
if directory != "":
|
|
5624
5647
|
|
|
5625
5648
|
self.reset(network = True, xy_scale = 1, z_scale = 1, nodes = True, edges = True, network_overlay = True, id_overlay = True, update = False)
|
|
@@ -5700,6 +5723,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5700
5723
|
f"Failed to load Network 3D Object: {str(e)}"
|
|
5701
5724
|
)
|
|
5702
5725
|
|
|
5726
|
+
|
|
5727
|
+
|
|
5703
5728
|
def load_network(self):
|
|
5704
5729
|
"""Load in the network from a .xlsx (need to add .csv support)"""
|
|
5705
5730
|
|
|
@@ -5940,12 +5965,41 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5940
5965
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
5941
5966
|
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
5942
5967
|
|
|
5943
|
-
def
|
|
5968
|
+
def get_scaling_metadata_only(self, filename):
|
|
5969
|
+
# This only reads headers/metadata, not image data
|
|
5970
|
+
with tifffile.TiffFile(filename) as tif:
|
|
5971
|
+
x_scale = y_scale = z_scale = unit = None
|
|
5972
|
+
|
|
5973
|
+
# ImageJ metadata (very lightweight)
|
|
5974
|
+
if hasattr(tif, 'imagej_metadata') and tif.imagej_metadata:
|
|
5975
|
+
metadata = tif.imagej_metadata
|
|
5976
|
+
z_scale = metadata.get('spacing')
|
|
5977
|
+
unit = metadata.get('unit')
|
|
5978
|
+
|
|
5979
|
+
# TIFF tags (also lightweight - just header info)
|
|
5980
|
+
page = tif.pages[0] # This doesn't load image data
|
|
5981
|
+
tags = page.tags
|
|
5982
|
+
|
|
5983
|
+
if 'XResolution' in tags:
|
|
5984
|
+
x_res = tags['XResolution'].value
|
|
5985
|
+
x_scale = x_res[1] / x_res[0] if isinstance(x_res, tuple) else 1.0 / x_res
|
|
5986
|
+
|
|
5987
|
+
if 'YResolution' in tags:
|
|
5988
|
+
y_res = tags['YResolution'].value
|
|
5989
|
+
y_scale = y_res[1] / y_res[0] if isinstance(y_res, tuple) else 1.0 / y_res
|
|
5990
|
+
|
|
5991
|
+
if x_scale is None:
|
|
5992
|
+
x_scale = 1
|
|
5993
|
+
if z_scale is None:
|
|
5994
|
+
z_scale = 1
|
|
5995
|
+
|
|
5996
|
+
return x_scale, z_scale
|
|
5997
|
+
|
|
5998
|
+
def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True, preserve_zoom = None, end_paint = False, begin_paint = False, color = False, load_highlight = False):
|
|
5944
5999
|
"""Load a channel and enable active channel selection if needed."""
|
|
5945
6000
|
|
|
5946
6001
|
try:
|
|
5947
6002
|
|
|
5948
|
-
self.hold_update = True
|
|
5949
6003
|
if not data: # For solo loading
|
|
5950
6004
|
filename, _ = QFileDialog.getOpenFileName(
|
|
5951
6005
|
self,
|
|
@@ -5965,6 +6019,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5965
6019
|
try:
|
|
5966
6020
|
if file_extension in ['tif', 'tiff']:
|
|
5967
6021
|
import tifffile
|
|
6022
|
+
self.channel_data[channel_index] = None
|
|
6023
|
+
if (self.channel_data[0] is None and self.channel_data[1] is None) and (channel_index == 0 or channel_index == 1):
|
|
6024
|
+
try:
|
|
6025
|
+
my_network.xy_scale, my_network.z_scale = self.get_scaling_metadata_only(filename)
|
|
6026
|
+
print(f"xy_scale property set to {my_network.xy_scale}; z_scale property set to {my_network.z_scale}")
|
|
6027
|
+
except:
|
|
6028
|
+
pass
|
|
5968
6029
|
self.channel_data[channel_index] = tifffile.imread(filename)
|
|
5969
6030
|
|
|
5970
6031
|
elif file_extension == 'nii':
|
|
@@ -6070,51 +6131,52 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6070
6131
|
my_network.id_overlay = self.channel_data[channel_index]
|
|
6071
6132
|
|
|
6072
6133
|
# Enable the channel button
|
|
6073
|
-
|
|
6074
|
-
|
|
6134
|
+
if channel_index != 4:
|
|
6135
|
+
self.channel_buttons[channel_index].setEnabled(True)
|
|
6136
|
+
self.delete_buttons[channel_index].setEnabled(True)
|
|
6075
6137
|
|
|
6076
6138
|
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6139
|
+
# Enable active channel selector if this is the first channel loaded
|
|
6140
|
+
if not self.active_channel_combo.isEnabled():
|
|
6141
|
+
self.active_channel_combo.setEnabled(True)
|
|
6080
6142
|
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6143
|
+
# Update slider range if this is the first channel loaded
|
|
6144
|
+
try:
|
|
6145
|
+
if len(self.channel_data[channel_index].shape) == 3 or len(self.channel_data[channel_index].shape) == 4:
|
|
6146
|
+
if not self.slice_slider.isEnabled():
|
|
6147
|
+
self.slice_slider.setEnabled(True)
|
|
6148
|
+
self.slice_slider.setMinimum(0)
|
|
6149
|
+
self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
|
|
6150
|
+
if self.slice_slider.value() < self.channel_data[channel_index].shape[0] - 1:
|
|
6151
|
+
self.current_slice = self.slice_slider.value()
|
|
6152
|
+
else:
|
|
6153
|
+
self.slice_slider.setValue(0)
|
|
6154
|
+
self.current_slice = 0
|
|
6090
6155
|
else:
|
|
6091
|
-
self.slice_slider.
|
|
6092
|
-
self.
|
|
6156
|
+
self.slice_slider.setEnabled(True)
|
|
6157
|
+
self.slice_slider.setMinimum(0)
|
|
6158
|
+
self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
|
|
6159
|
+
if self.slice_slider.value() < self.channel_data[channel_index].shape[0] - 1:
|
|
6160
|
+
self.current_slice = self.slice_slider.value()
|
|
6161
|
+
else:
|
|
6162
|
+
self.current_slice = 0
|
|
6163
|
+
self.slice_slider.setValue(0)
|
|
6093
6164
|
else:
|
|
6094
|
-
self.slice_slider.setEnabled(
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
if self.slice_slider.value() < self.channel_data[channel_index].shape[0] - 1:
|
|
6098
|
-
self.current_slice = self.slice_slider.value()
|
|
6099
|
-
else:
|
|
6100
|
-
self.current_slice = 0
|
|
6101
|
-
self.slice_slider.setValue(0)
|
|
6102
|
-
else:
|
|
6103
|
-
self.slice_slider.setEnabled(False)
|
|
6104
|
-
except:
|
|
6105
|
-
pass
|
|
6165
|
+
self.slice_slider.setEnabled(False)
|
|
6166
|
+
except:
|
|
6167
|
+
pass
|
|
6106
6168
|
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6169
|
+
|
|
6170
|
+
# If this is the first channel loaded, make it active
|
|
6171
|
+
if all(not btn.isEnabled() for btn in self.channel_buttons[:channel_index]):
|
|
6172
|
+
self.set_active_channel(channel_index)
|
|
6111
6173
|
|
|
6112
|
-
|
|
6113
|
-
|
|
6174
|
+
if not self.channel_buttons[channel_index].isChecked():
|
|
6175
|
+
self.channel_buttons[channel_index].click()
|
|
6114
6176
|
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6177
|
+
self.min_max[channel_index][0] = np.min(self.channel_data[channel_index])
|
|
6178
|
+
self.min_max[channel_index][1] = np.max(self.channel_data[channel_index])
|
|
6179
|
+
self.volume_dict[channel_index] = None #reset volumes
|
|
6118
6180
|
|
|
6119
6181
|
try:
|
|
6120
6182
|
if assign_shape: #keep original shape tracked to undo resampling.
|
|
@@ -6138,7 +6200,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6138
6200
|
|
|
6139
6201
|
self.img_height, self.img_width = self.shape[1], self.shape[2]
|
|
6140
6202
|
self.original_ylim, self.original_xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
|
|
6141
|
-
#print(self.original_xlim)
|
|
6142
6203
|
|
|
6143
6204
|
self.completed_paint_strokes = [] #Reset pending paint operations
|
|
6144
6205
|
self.current_stroke_points = []
|
|
@@ -6148,6 +6209,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6148
6209
|
self.current_operation = []
|
|
6149
6210
|
self.current_operation_type = None
|
|
6150
6211
|
|
|
6212
|
+
if load_highlight:
|
|
6213
|
+
self.highlight_overlay = n3d.binarize(self.channel_data[4].astype(np.uint8))
|
|
6214
|
+
self.mini_overlay_data = None
|
|
6215
|
+
self.mini_overlay = False
|
|
6216
|
+
self.channel_data[4] = None
|
|
6217
|
+
|
|
6151
6218
|
if self.pan_mode:
|
|
6152
6219
|
self.pan_button.click()
|
|
6153
6220
|
if self.show_channels:
|
|
@@ -6156,7 +6223,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6156
6223
|
elif not end_paint:
|
|
6157
6224
|
|
|
6158
6225
|
self.update_display(reset_resize = reset_resize, preserve_zoom = preserve_zoom)
|
|
6159
|
-
|
|
6160
6226
|
|
|
6161
6227
|
except Exception as e:
|
|
6162
6228
|
|
|
@@ -6165,7 +6231,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6165
6231
|
QMessageBox.critical(
|
|
6166
6232
|
self,
|
|
6167
6233
|
"Error Loading File",
|
|
6168
|
-
f"Failed to load
|
|
6234
|
+
f"Failed to load file: {str(e)}"
|
|
6169
6235
|
)
|
|
6170
6236
|
|
|
6171
6237
|
def delete_channel(self, channel_index, called = True, update = True):
|
|
@@ -6394,15 +6460,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6394
6460
|
self.pm.convert_virtual_strokes_to_data()
|
|
6395
6461
|
self.current_slice = slice_value
|
|
6396
6462
|
if self.preview:
|
|
6463
|
+
self.highlight_overlay = None
|
|
6464
|
+
self.mini_overlay_data = None
|
|
6465
|
+
self.mini_overlay = False
|
|
6397
6466
|
self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
|
|
6398
6467
|
elif self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
|
|
6399
6468
|
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
6400
|
-
|
|
6401
|
-
self.update_display(preserve_zoom=view_settings)
|
|
6402
|
-
else:
|
|
6403
|
-
self.hold_update = False
|
|
6404
|
-
#if self.machine_window is not None:
|
|
6405
|
-
#self.machine_window.poke_segmenter()
|
|
6469
|
+
self.update_display(preserve_zoom=view_settings)
|
|
6406
6470
|
if self.pan_mode:
|
|
6407
6471
|
self.pan_button.click()
|
|
6408
6472
|
self.pending_slice = None
|
|
@@ -6419,7 +6483,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6419
6483
|
self.channel_brightness[channel_index]['max'] = max_val / 65535
|
|
6420
6484
|
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
6421
6485
|
|
|
6422
|
-
|
|
6423
6486
|
def update_display(self, preserve_zoom=None, dims=None, called=False, reset_resize=False, skip=False):
|
|
6424
6487
|
"""Optimized display update with view-based cropping for performance."""
|
|
6425
6488
|
try:
|
|
@@ -6645,7 +6708,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6645
6708
|
# Handle preview, overlays, and measurements (apply cropping here too)
|
|
6646
6709
|
|
|
6647
6710
|
# Overlay handling (optimized with cropping and downsampling)
|
|
6648
|
-
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
6711
|
+
if self.mini_overlay and self.highlight and self.machine_window is None and not self.preview:
|
|
6649
6712
|
highlight_cmap = LinearSegmentedColormap.from_list('highlight', [(0, 0, 0, 0), (1, 1, 0, 1)])
|
|
6650
6713
|
display_overlay = crop_and_downsample_image(
|
|
6651
6714
|
self.mini_overlay_data, y_min_padded, y_max_padded,
|
|
@@ -6774,8 +6837,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6774
6837
|
#print(traceback.format_exc())
|
|
6775
6838
|
|
|
6776
6839
|
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
6840
|
def get_channel_image(self, channel):
|
|
6780
6841
|
"""Find the matplotlib image object for a specific channel."""
|
|
6781
6842
|
if not hasattr(self.ax, 'images'):
|
|
@@ -10028,6 +10089,22 @@ class InteractionDialog(QDialog):
|
|
|
10028
10089
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
10029
10090
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
10030
10091
|
|
|
10092
|
+
self.length = QPushButton("Return Lengths")
|
|
10093
|
+
self.length.setCheckable(True)
|
|
10094
|
+
self.length.setChecked(False)
|
|
10095
|
+
layout.addRow("(Will Skeletonize the Edge Mirror and use that to calculate adjacent length of edges, as opposed to default volumes):", self.length)
|
|
10096
|
+
|
|
10097
|
+
self.auto = QPushButton("Auto")
|
|
10098
|
+
self.auto.setCheckable(True)
|
|
10099
|
+
try:
|
|
10100
|
+
if self.parent().shape[0] == 1:
|
|
10101
|
+
self.auto.setChecked(False)
|
|
10102
|
+
else:
|
|
10103
|
+
self.auto.setChecked(True)
|
|
10104
|
+
except:
|
|
10105
|
+
self.auto.setChecked(False)
|
|
10106
|
+
layout.addRow("(If Above): Attempt to Auto Correct Skeleton Looping:", self.auto)
|
|
10107
|
+
|
|
10031
10108
|
self.fastdil = QPushButton("Fast Dilate")
|
|
10032
10109
|
self.fastdil.setCheckable(True)
|
|
10033
10110
|
self.fastdil.setChecked(False)
|
|
@@ -10051,10 +10128,16 @@ class InteractionDialog(QDialog):
|
|
|
10051
10128
|
|
|
10052
10129
|
|
|
10053
10130
|
fastdil = self.fastdil.isChecked()
|
|
10131
|
+
length = self.length.isChecked()
|
|
10132
|
+
auto = self.auto.isChecked()
|
|
10054
10133
|
|
|
10055
|
-
result = my_network.interactions(search = node_search, cores = accepted_mode, fastdil = fastdil)
|
|
10134
|
+
result = my_network.interactions(search = node_search, cores = accepted_mode, skele = length, length = length, auto = auto, fastdil = fastdil)
|
|
10135
|
+
|
|
10136
|
+
if not length:
|
|
10137
|
+
self.parent().format_for_upperright_table(result, 'Node ID', ['Volume of Nearby Edge (Scaled)', 'Volume of Search Region (Scaled)'], title = 'Node/Edge Interactions')
|
|
10138
|
+
else:
|
|
10139
|
+
self.parent().format_for_upperright_table(result, 'Node ID', ['~Length of Nearby Edge (Scaled)', 'Volume of Search Region (Scaled)'], title = 'Node/Edge Interactions')
|
|
10056
10140
|
|
|
10057
|
-
self.parent().format_for_upperright_table(result, 'Node ID', ['Volume of Nearby Edge (Scaled)', 'Volume of Search Region'], title = 'Node/Edge Interactions')
|
|
10058
10141
|
|
|
10059
10142
|
self.accept()
|
|
10060
10143
|
|
|
@@ -11306,12 +11389,11 @@ class ThresholdDialog(QDialog):
|
|
|
11306
11389
|
print("Error - please calculate network first")
|
|
11307
11390
|
return
|
|
11308
11391
|
|
|
11309
|
-
if self.parent().mini_overlay_data is not None:
|
|
11310
|
-
self.parent().mini_overlay_data = None
|
|
11311
|
-
|
|
11312
11392
|
thresh_window = ThresholdWindow(self.parent(), accepted_mode)
|
|
11313
11393
|
thresh_window.show() # Non-modal window
|
|
11314
11394
|
self.highlight_overlay = None
|
|
11395
|
+
#self.mini_overlay = False
|
|
11396
|
+
self.mini_overlay_data = None
|
|
11315
11397
|
self.accept()
|
|
11316
11398
|
except:
|
|
11317
11399
|
import traceback
|
|
@@ -12285,16 +12367,23 @@ class ThresholdWindow(QMainWindow):
|
|
|
12285
12367
|
self.parent().bounds = False
|
|
12286
12368
|
|
|
12287
12369
|
elif accepted_mode == 0:
|
|
12288
|
-
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
|
|
12294
|
-
|
|
12295
|
-
self.
|
|
12296
|
-
|
|
12297
|
-
self.histo_list = self.
|
|
12370
|
+
data = self.parent().channel_data[self.parent().active_channel]
|
|
12371
|
+
nonzero_data = data[data != 0]
|
|
12372
|
+
|
|
12373
|
+
if nonzero_data.size > 578009537:
|
|
12374
|
+
# For large arrays, use numpy histogram directly
|
|
12375
|
+
counts, bin_edges = np.histogram(nonzero_data, bins= min(int(np.sqrt(nonzero_data.size)), 500), density=False)
|
|
12376
|
+
# Store min/max separately if needed elsewhere
|
|
12377
|
+
self.data_min = np.min(nonzero_data)
|
|
12378
|
+
self.data_max = np.max(nonzero_data)
|
|
12379
|
+
self.histo_list = [self.data_min, self.data_max]
|
|
12380
|
+
else:
|
|
12381
|
+
# For smaller arrays, can still use histogram method for consistency
|
|
12382
|
+
counts, bin_edges = np.histogram(nonzero_data, bins='auto', density=False)
|
|
12383
|
+
self.data_min = np.min(nonzero_data)
|
|
12384
|
+
self.data_max = np.max(nonzero_data)
|
|
12385
|
+
self.histo_list = [self.data_min, self.data_max]
|
|
12386
|
+
|
|
12298
12387
|
self.bounds = True
|
|
12299
12388
|
self.parent().bounds = True
|
|
12300
12389
|
|
|
@@ -12307,16 +12396,26 @@ class ThresholdWindow(QMainWindow):
|
|
|
12307
12396
|
layout.addWidget(self.canvas)
|
|
12308
12397
|
|
|
12309
12398
|
# Pre-compute histogram with numpy
|
|
12310
|
-
|
|
12311
|
-
|
|
12399
|
+
if accepted_mode != 0:
|
|
12400
|
+
counts, bin_edges = np.histogram(self.histo_list, bins=50)
|
|
12401
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
12402
|
+
# Store histogram bounds
|
|
12403
|
+
if self.bounds:
|
|
12404
|
+
self.data_min = 0
|
|
12405
|
+
else:
|
|
12406
|
+
self.data_min = min(self.histo_list)
|
|
12407
|
+
self.data_max = max(self.histo_list)
|
|
12408
|
+
else:
|
|
12409
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
12410
|
+
bin_width = bin_edges[1] - bin_edges[0]
|
|
12312
12411
|
|
|
12313
12412
|
# Plot pre-computed histogram
|
|
12314
12413
|
self.ax = fig.add_subplot(111)
|
|
12315
12414
|
self.ax.bar(bin_centers, counts, width=bin_edges[1] - bin_edges[0], alpha=0.5)
|
|
12316
12415
|
|
|
12317
12416
|
# Add vertical lines for thresholds
|
|
12318
|
-
self.min_line = self.ax.axvline(
|
|
12319
|
-
self.max_line = self.ax.axvline(
|
|
12417
|
+
self.min_line = self.ax.axvline(self.data_min, color='r')
|
|
12418
|
+
self.max_line = self.ax.axvline(self.data_max, color='b')
|
|
12320
12419
|
|
|
12321
12420
|
# Connect events for dragging
|
|
12322
12421
|
self.canvas.mpl_connect('button_press_event', self.on_press)
|
|
@@ -12324,13 +12423,6 @@ class ThresholdWindow(QMainWindow):
|
|
|
12324
12423
|
self.canvas.mpl_connect('button_release_event', self.on_release)
|
|
12325
12424
|
|
|
12326
12425
|
self.dragging = None
|
|
12327
|
-
|
|
12328
|
-
# Store histogram bounds
|
|
12329
|
-
if self.bounds:
|
|
12330
|
-
self.data_min = 0
|
|
12331
|
-
else:
|
|
12332
|
-
self.data_min = min(self.histo_list)
|
|
12333
|
-
self.data_max = max(self.histo_list)
|
|
12334
12426
|
|
|
12335
12427
|
# Create form layout for inputs
|
|
12336
12428
|
form_layout = QFormLayout()
|
|
@@ -15285,31 +15377,81 @@ class HistogramSelector(QWidget):
|
|
|
15285
15377
|
""")
|
|
15286
15378
|
layout.addWidget(button)
|
|
15287
15379
|
|
|
15380
|
+
|
|
15288
15381
|
def shortest_path_histogram(self):
|
|
15289
15382
|
try:
|
|
15290
|
-
|
|
15291
|
-
|
|
15292
|
-
|
|
15293
|
-
|
|
15294
|
-
|
|
15295
|
-
|
|
15383
|
+
# Check if graph has multiple disconnected components
|
|
15384
|
+
components = list(nx.connected_components(self.G))
|
|
15385
|
+
|
|
15386
|
+
if len(components) > 1:
|
|
15387
|
+
print(f"Warning: Graph has {len(components)} disconnected components. Computing shortest paths within each component separately.")
|
|
15388
|
+
|
|
15389
|
+
# Initialize variables to collect data from all components
|
|
15390
|
+
all_path_lengths = []
|
|
15391
|
+
max_diameter = 0
|
|
15392
|
+
|
|
15393
|
+
# Process each component separately
|
|
15394
|
+
for i, component in enumerate(components):
|
|
15395
|
+
subgraph = self.G.subgraph(component)
|
|
15396
|
+
|
|
15397
|
+
if len(component) < 2:
|
|
15398
|
+
# Skip single-node components (no paths to compute)
|
|
15399
|
+
continue
|
|
15400
|
+
|
|
15401
|
+
# Compute shortest paths for this component
|
|
15402
|
+
shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(subgraph))
|
|
15403
|
+
component_diameter = max(nx.eccentricity(subgraph, sp=shortest_path_lengths).values())
|
|
15404
|
+
max_diameter = max(max_diameter, component_diameter)
|
|
15405
|
+
|
|
15406
|
+
# Collect path lengths from this component
|
|
15407
|
+
for pls in shortest_path_lengths.values():
|
|
15408
|
+
all_path_lengths.extend(list(pls.values()))
|
|
15409
|
+
|
|
15410
|
+
# Remove self-paths (length 0) and create histogram
|
|
15411
|
+
all_path_lengths = [pl for pl in all_path_lengths if pl > 0]
|
|
15412
|
+
|
|
15413
|
+
if not all_path_lengths:
|
|
15414
|
+
print("No paths found across components (only single-node components)")
|
|
15415
|
+
return
|
|
15416
|
+
|
|
15417
|
+
# Create combined histogram
|
|
15418
|
+
path_lengths = np.zeros(max_diameter + 1, dtype=int)
|
|
15419
|
+
pl, cnts = np.unique(all_path_lengths, return_counts=True)
|
|
15296
15420
|
path_lengths[pl] += cnts
|
|
15297
|
-
|
|
15421
|
+
|
|
15422
|
+
title_suffix = f" (across {len(components)} components)"
|
|
15423
|
+
|
|
15424
|
+
else:
|
|
15425
|
+
# Single component
|
|
15426
|
+
shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(self.G))
|
|
15427
|
+
diameter = max(nx.eccentricity(self.G, sp=shortest_path_lengths).values())
|
|
15428
|
+
path_lengths = np.zeros(diameter + 1, dtype=int)
|
|
15429
|
+
for pls in shortest_path_lengths.values():
|
|
15430
|
+
pl, cnts = np.unique(list(pls.values()), return_counts=True)
|
|
15431
|
+
path_lengths[pl] += cnts
|
|
15432
|
+
max_diameter = diameter
|
|
15433
|
+
title_suffix = ""
|
|
15434
|
+
|
|
15435
|
+
# Generate visualization and results (same for both cases)
|
|
15298
15436
|
freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
|
|
15299
|
-
|
|
15300
15437
|
fig, ax = plt.subplots(figsize=(15, 8))
|
|
15301
|
-
ax.bar(np.arange(1,
|
|
15438
|
+
ax.bar(np.arange(1, max_diameter + 1), height=freq_percent)
|
|
15302
15439
|
ax.set_title(
|
|
15303
|
-
"Distribution of shortest path length in G
|
|
15440
|
+
f"Distribution of shortest path length in G{title_suffix}",
|
|
15441
|
+
fontdict={"size": 35}, loc="center"
|
|
15304
15442
|
)
|
|
15305
15443
|
ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
|
|
15306
15444
|
ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
|
|
15307
15445
|
plt.show()
|
|
15308
15446
|
|
|
15309
15447
|
freq_dict = {freq: length for length, freq in enumerate(freq_percent, start=1)}
|
|
15310
|
-
self.network_analysis.format_for_upperright_table(
|
|
15311
|
-
|
|
15312
|
-
|
|
15448
|
+
self.network_analysis.format_for_upperright_table(
|
|
15449
|
+
freq_dict,
|
|
15450
|
+
metric='Frequency (%)',
|
|
15451
|
+
value='Shortest Path Length',
|
|
15452
|
+
title=f"Distribution of shortest path length in G{title_suffix}"
|
|
15453
|
+
)
|
|
15454
|
+
|
|
15313
15455
|
except Exception as e:
|
|
15314
15456
|
print(f"Error generating shortest path histogram: {e}")
|
|
15315
15457
|
|
|
@@ -15328,20 +15470,62 @@ class HistogramSelector(QWidget):
|
|
|
15328
15470
|
title="Degree Centrality Table")
|
|
15329
15471
|
except Exception as e:
|
|
15330
15472
|
print(f"Error generating degree centrality histogram: {e}")
|
|
15331
|
-
|
|
15473
|
+
|
|
15332
15474
|
def betweenness_centrality_histogram(self):
|
|
15333
15475
|
try:
|
|
15334
|
-
|
|
15476
|
+
# Check if graph has multiple disconnected components
|
|
15477
|
+
components = list(nx.connected_components(self.G))
|
|
15478
|
+
|
|
15479
|
+
if len(components) > 1:
|
|
15480
|
+
print(f"Warning: Graph has {len(components)} disconnected components. Computing betweenness centrality within each component separately.")
|
|
15481
|
+
|
|
15482
|
+
# Initialize dictionary to collect betweenness centrality from all components
|
|
15483
|
+
combined_betweenness_centrality = {}
|
|
15484
|
+
|
|
15485
|
+
# Process each component separately
|
|
15486
|
+
for i, component in enumerate(components):
|
|
15487
|
+
if len(component) < 2:
|
|
15488
|
+
# For single-node components, betweenness centrality is 0
|
|
15489
|
+
for node in component:
|
|
15490
|
+
combined_betweenness_centrality[node] = 0.0
|
|
15491
|
+
continue
|
|
15492
|
+
|
|
15493
|
+
# Create subgraph for this component
|
|
15494
|
+
subgraph = self.G.subgraph(component)
|
|
15495
|
+
|
|
15496
|
+
# Compute betweenness centrality for this component
|
|
15497
|
+
component_betweenness = nx.centrality.betweenness_centrality(subgraph)
|
|
15498
|
+
|
|
15499
|
+
# Add to combined results
|
|
15500
|
+
combined_betweenness_centrality.update(component_betweenness)
|
|
15501
|
+
|
|
15502
|
+
betweenness_centrality = combined_betweenness_centrality
|
|
15503
|
+
title_suffix = f" (across {len(components)} components)"
|
|
15504
|
+
|
|
15505
|
+
else:
|
|
15506
|
+
# Single component
|
|
15507
|
+
betweenness_centrality = nx.centrality.betweenness_centrality(self.G)
|
|
15508
|
+
title_suffix = ""
|
|
15509
|
+
|
|
15510
|
+
# Generate visualization and results (same for both cases)
|
|
15335
15511
|
plt.figure(figsize=(15, 8))
|
|
15336
15512
|
plt.hist(betweenness_centrality.values(), bins=100)
|
|
15337
15513
|
plt.xticks(ticks=[0, 0.02, 0.1, 0.2, 0.3, 0.4, 0.5])
|
|
15338
|
-
plt.title(
|
|
15514
|
+
plt.title(
|
|
15515
|
+
f"Betweenness Centrality Histogram{title_suffix}",
|
|
15516
|
+
fontdict={"size": 35}, loc="center"
|
|
15517
|
+
)
|
|
15339
15518
|
plt.xlabel("Betweenness Centrality", fontdict={"size": 20})
|
|
15340
15519
|
plt.ylabel("Counts", fontdict={"size": 20})
|
|
15341
15520
|
plt.show()
|
|
15342
|
-
|
|
15343
|
-
|
|
15344
|
-
|
|
15521
|
+
|
|
15522
|
+
self.network_analysis.format_for_upperright_table(
|
|
15523
|
+
betweenness_centrality,
|
|
15524
|
+
metric='Node',
|
|
15525
|
+
value='Betweenness Centrality',
|
|
15526
|
+
title=f"Betweenness Centrality Table{title_suffix}"
|
|
15527
|
+
)
|
|
15528
|
+
|
|
15345
15529
|
except Exception as e:
|
|
15346
15530
|
print(f"Error generating betweenness centrality histogram: {e}")
|
|
15347
15531
|
|
|
@@ -15394,7 +15578,27 @@ class HistogramSelector(QWidget):
|
|
|
15394
15578
|
def bridges_analysis(self):
|
|
15395
15579
|
try:
|
|
15396
15580
|
bridges = list(nx.bridges(self.G))
|
|
15397
|
-
|
|
15581
|
+
try:
|
|
15582
|
+
# Get the existing DataFrame from the model
|
|
15583
|
+
original_df = self.network_analysis.network_table.model()._data
|
|
15584
|
+
|
|
15585
|
+
# Create boolean mask
|
|
15586
|
+
mask = pd.Series([False] * len(original_df))
|
|
15587
|
+
|
|
15588
|
+
for u, v in bridges:
|
|
15589
|
+
# Check for both (u,v) and (v,u) orientations
|
|
15590
|
+
bridge_mask = (
|
|
15591
|
+
((original_df.iloc[:, 0] == u) & (original_df.iloc[:, 1] == v)) |
|
|
15592
|
+
((original_df.iloc[:, 0] == v) & (original_df.iloc[:, 1] == u))
|
|
15593
|
+
)
|
|
15594
|
+
mask |= bridge_mask
|
|
15595
|
+
# Filter the DataFrame to only include bridge connections
|
|
15596
|
+
filtered_df = original_df[mask].copy()
|
|
15597
|
+
df_dict = {i: row.tolist() for i, row in enumerate(filtered_df.values)}
|
|
15598
|
+
self.network_analysis.format_for_upperright_table(df_dict, metric='Bridge ID', value = ['NodeA', 'NodeB', 'EdgeC'],
|
|
15599
|
+
title="Bridges")
|
|
15600
|
+
except:
|
|
15601
|
+
self.network_analysis.format_for_upperright_table(bridges, metric='Node Pair',
|
|
15398
15602
|
title="Bridges")
|
|
15399
15603
|
except Exception as e:
|
|
15400
15604
|
print(f"Error generating bridges analysis: {e}")
|
|
@@ -15421,7 +15625,7 @@ class HistogramSelector(QWidget):
|
|
|
15421
15625
|
def node_connectivity_histogram(self):
|
|
15422
15626
|
"""Local node connectivity - minimum number of nodes that must be removed to disconnect neighbors"""
|
|
15423
15627
|
try:
|
|
15424
|
-
if self.G.number_of_nodes() > 500:
|
|
15628
|
+
if self.G.number_of_nodes() > 500:
|
|
15425
15629
|
print("Note this analysis may be slow for large network (>500 nodes)")
|
|
15426
15630
|
#return
|
|
15427
15631
|
|
|
@@ -15499,7 +15703,7 @@ class HistogramSelector(QWidget):
|
|
|
15499
15703
|
def load_centrality_histogram(self):
|
|
15500
15704
|
"""Load centrality - fraction of shortest paths passing through each node"""
|
|
15501
15705
|
try:
|
|
15502
|
-
if self.G.number_of_nodes() > 1000:
|
|
15706
|
+
if self.G.number_of_nodes() > 1000:
|
|
15503
15707
|
print("Note this analysis may be slow for large network (>1000 nodes)")
|
|
15504
15708
|
#return
|
|
15505
15709
|
|
|
@@ -15518,21 +15722,67 @@ class HistogramSelector(QWidget):
|
|
|
15518
15722
|
def communicability_centrality_histogram(self):
|
|
15519
15723
|
"""Communicability centrality - based on communicability between nodes"""
|
|
15520
15724
|
try:
|
|
15521
|
-
if self.G.number_of_nodes() > 500:
|
|
15725
|
+
if self.G.number_of_nodes() > 500:
|
|
15522
15726
|
print("Note this analysis may be slow for large network (>500 nodes)")
|
|
15523
15727
|
#return
|
|
15728
|
+
|
|
15729
|
+
# Check if graph has multiple disconnected components
|
|
15730
|
+
components = list(nx.connected_components(self.G))
|
|
15731
|
+
|
|
15732
|
+
if len(components) > 1:
|
|
15733
|
+
print(f"Warning: Graph has {len(components)} disconnected components. Computing communicability centrality within each component separately.")
|
|
15734
|
+
|
|
15735
|
+
# Initialize dictionary to collect communicability centrality from all components
|
|
15736
|
+
combined_comm_centrality = {}
|
|
15737
|
+
|
|
15738
|
+
# Process each component separately
|
|
15739
|
+
for i, component in enumerate(components):
|
|
15740
|
+
if len(component) < 2:
|
|
15741
|
+
# For single-node components, communicability betweenness centrality is 0
|
|
15742
|
+
for node in component:
|
|
15743
|
+
combined_comm_centrality[node] = 0.0
|
|
15744
|
+
continue
|
|
15745
|
+
|
|
15746
|
+
# Create subgraph for this component
|
|
15747
|
+
subgraph = self.G.subgraph(component)
|
|
15748
|
+
|
|
15749
|
+
# Compute communicability betweenness centrality for this component
|
|
15750
|
+
try:
|
|
15751
|
+
component_comm_centrality = nx.communicability_betweenness_centrality(subgraph)
|
|
15752
|
+
# Add to combined results
|
|
15753
|
+
combined_comm_centrality.update(component_comm_centrality)
|
|
15754
|
+
except Exception as comp_e:
|
|
15755
|
+
print(f"Error computing communicability centrality for component {i+1}: {comp_e}")
|
|
15756
|
+
# Set centrality to 0 for nodes in this component if computation fails
|
|
15757
|
+
for node in component:
|
|
15758
|
+
combined_comm_centrality[node] = 0.0
|
|
15759
|
+
|
|
15760
|
+
comm_centrality = combined_comm_centrality
|
|
15761
|
+
title_suffix = f" (across {len(components)} components)"
|
|
15524
15762
|
|
|
15525
|
-
|
|
15526
|
-
|
|
15763
|
+
else:
|
|
15764
|
+
# Single component
|
|
15765
|
+
comm_centrality = nx.communicability_betweenness_centrality(self.G)
|
|
15766
|
+
title_suffix = ""
|
|
15767
|
+
|
|
15768
|
+
# Generate visualization and results (same for both cases)
|
|
15527
15769
|
plt.figure(figsize=(15, 8))
|
|
15528
15770
|
plt.hist(comm_centrality.values(), bins=50, alpha=0.7)
|
|
15529
|
-
plt.title(
|
|
15771
|
+
plt.title(
|
|
15772
|
+
f"Communicability Betweenness Centrality Distribution{title_suffix}",
|
|
15773
|
+
fontdict={"size": 35}, loc="center"
|
|
15774
|
+
)
|
|
15530
15775
|
plt.xlabel("Communicability Betweenness Centrality", fontdict={"size": 20})
|
|
15531
15776
|
plt.ylabel("Frequency", fontdict={"size": 20})
|
|
15532
15777
|
plt.show()
|
|
15533
|
-
|
|
15534
|
-
|
|
15535
|
-
|
|
15778
|
+
|
|
15779
|
+
self.network_analysis.format_for_upperright_table(
|
|
15780
|
+
comm_centrality,
|
|
15781
|
+
metric='Node',
|
|
15782
|
+
value='Communicability Betweenness Centrality',
|
|
15783
|
+
title=f"Communicability Betweenness Centrality Table{title_suffix}"
|
|
15784
|
+
)
|
|
15785
|
+
|
|
15536
15786
|
except Exception as e:
|
|
15537
15787
|
print(f"Error generating communicability betweenness centrality histogram: {e}")
|
|
15538
15788
|
|
|
@@ -15555,20 +15805,67 @@ class HistogramSelector(QWidget):
|
|
|
15555
15805
|
def current_flow_betweenness_histogram(self):
|
|
15556
15806
|
"""Current flow betweenness - models network as electrical circuit"""
|
|
15557
15807
|
try:
|
|
15558
|
-
if self.G.number_of_nodes() > 500:
|
|
15808
|
+
if self.G.number_of_nodes() > 500:
|
|
15559
15809
|
print("Note this analysis may be slow for large network (>500 nodes)")
|
|
15560
15810
|
#return
|
|
15811
|
+
|
|
15812
|
+
# Check if graph has multiple disconnected components
|
|
15813
|
+
components = list(nx.connected_components(self.G))
|
|
15814
|
+
|
|
15815
|
+
if len(components) > 1:
|
|
15816
|
+
print(f"Warning: Graph has {len(components)} disconnected components. Computing current flow betweenness centrality within each component separately.")
|
|
15561
15817
|
|
|
15562
|
-
|
|
15818
|
+
# Initialize dictionary to collect current flow betweenness from all components
|
|
15819
|
+
combined_current_flow = {}
|
|
15820
|
+
|
|
15821
|
+
# Process each component separately
|
|
15822
|
+
for i, component in enumerate(components):
|
|
15823
|
+
if len(component) < 2:
|
|
15824
|
+
# For single-node components, current flow betweenness centrality is 0
|
|
15825
|
+
for node in component:
|
|
15826
|
+
combined_current_flow[node] = 0.0
|
|
15827
|
+
continue
|
|
15828
|
+
|
|
15829
|
+
# Create subgraph for this component
|
|
15830
|
+
subgraph = self.G.subgraph(component)
|
|
15831
|
+
|
|
15832
|
+
# Compute current flow betweenness centrality for this component
|
|
15833
|
+
try:
|
|
15834
|
+
component_current_flow = nx.current_flow_betweenness_centrality(subgraph)
|
|
15835
|
+
# Add to combined results
|
|
15836
|
+
combined_current_flow.update(component_current_flow)
|
|
15837
|
+
except Exception as comp_e:
|
|
15838
|
+
print(f"Error computing current flow betweenness for component {i+1}: {comp_e}")
|
|
15839
|
+
# Set centrality to 0 for nodes in this component if computation fails
|
|
15840
|
+
for node in component:
|
|
15841
|
+
combined_current_flow[node] = 0.0
|
|
15842
|
+
|
|
15843
|
+
current_flow = combined_current_flow
|
|
15844
|
+
title_suffix = f" (across {len(components)} components)"
|
|
15845
|
+
|
|
15846
|
+
else:
|
|
15847
|
+
# Single component
|
|
15848
|
+
current_flow = nx.current_flow_betweenness_centrality(self.G)
|
|
15849
|
+
title_suffix = ""
|
|
15850
|
+
|
|
15851
|
+
# Generate visualization and results (same for both cases)
|
|
15563
15852
|
plt.figure(figsize=(15, 8))
|
|
15564
15853
|
plt.hist(current_flow.values(), bins=50, alpha=0.7)
|
|
15565
|
-
plt.title(
|
|
15854
|
+
plt.title(
|
|
15855
|
+
f"Current Flow Betweenness Centrality Distribution{title_suffix}",
|
|
15856
|
+
fontdict={"size": 35}, loc="center"
|
|
15857
|
+
)
|
|
15566
15858
|
plt.xlabel("Current Flow Betweenness Centrality", fontdict={"size": 20})
|
|
15567
15859
|
plt.ylabel("Frequency", fontdict={"size": 20})
|
|
15568
15860
|
plt.show()
|
|
15569
|
-
|
|
15570
|
-
|
|
15571
|
-
|
|
15861
|
+
|
|
15862
|
+
self.network_analysis.format_for_upperright_table(
|
|
15863
|
+
current_flow,
|
|
15864
|
+
metric='Node',
|
|
15865
|
+
value='Current Flow Betweenness',
|
|
15866
|
+
title=f"Current Flow Betweenness Table{title_suffix}"
|
|
15867
|
+
)
|
|
15868
|
+
|
|
15572
15869
|
except Exception as e:
|
|
15573
15870
|
print(f"Error generating current flow betweenness histogram: {e}")
|
|
15574
15871
|
|