nettracer3d 1.0.7__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 +64 -14
- nettracer3d/nettracer_gui.py +480 -180
- nettracer3d/segmenter.py +67 -25
- nettracer3d/segmenter_GPU.py +67 -29
- nettracer3d/stats.py +861 -0
- {nettracer3d-1.0.7.dist-info → nettracer3d-1.0.9.dist-info}/METADATA +6 -4
- {nettracer3d-1.0.7.dist-info → nettracer3d-1.0.9.dist-info}/RECORD +12 -11
- {nettracer3d-1.0.7.dist-info → nettracer3d-1.0.9.dist-info}/WHEEL +0 -0
- {nettracer3d-1.0.7.dist-info → nettracer3d-1.0.9.dist-info}/entry_points.txt +0 -0
- {nettracer3d-1.0.7.dist-info → nettracer3d-1.0.9.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-1.0.7.dist-info → nettracer3d-1.0.9.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -36,6 +36,7 @@ from threading import Lock
|
|
|
36
36
|
from scipy import ndimage
|
|
37
37
|
import os
|
|
38
38
|
from . import painting
|
|
39
|
+
from . import stats as net_stats
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
|
|
@@ -425,8 +426,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
425
426
|
self.canvas.mpl_connect('button_release_event', self.on_mouse_release)
|
|
426
427
|
self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
|
|
427
428
|
|
|
428
|
-
#self.canvas.mpl_connect('button_press_event', self.on_mouse_click)
|
|
429
|
-
|
|
430
429
|
# Initialize measurement tracking
|
|
431
430
|
self.measurement_points = [] # List to store point pairs
|
|
432
431
|
self.angle_measurements = [] # NEW: List to store angle trios
|
|
@@ -462,8 +461,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
462
461
|
self.last_paint_pos = None
|
|
463
462
|
|
|
464
463
|
self.resume = False
|
|
465
|
-
|
|
466
|
-
self.hold_update = False
|
|
467
464
|
self._first_pan_done = False
|
|
468
465
|
|
|
469
466
|
|
|
@@ -879,6 +876,21 @@ class ImageViewerWindow(QMainWindow):
|
|
|
879
876
|
elif self.scroll_direction > 0 and new_value <= self.slice_slider.maximum():
|
|
880
877
|
self.slice_slider.setValue(new_value)
|
|
881
878
|
|
|
879
|
+
|
|
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'])
|
|
893
|
+
|
|
882
894
|
def create_highlight_overlay(self, node_indices=None, edge_indices=None, overlay1_indices = None, overlay2_indices = None, bounds = False):
|
|
883
895
|
"""
|
|
884
896
|
Create a binary overlay highlighting specific nodes and/or edges using parallel processing.
|
|
@@ -1004,13 +1016,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1004
1016
|
|
|
1005
1017
|
# Combine results
|
|
1006
1018
|
if node_overlay is not None:
|
|
1007
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, node_overlay)
|
|
1019
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, node_overlay).astype(np.uint8)
|
|
1008
1020
|
if edge_overlay is not None:
|
|
1009
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, edge_overlay)
|
|
1021
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, edge_overlay).astype(np.uint8)
|
|
1010
1022
|
if overlay1_overlay is not None:
|
|
1011
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay1_overlay)
|
|
1023
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay1_overlay).astype(np.uint8)
|
|
1012
1024
|
if overlay2_overlay is not None:
|
|
1013
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay2_overlay)
|
|
1025
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay2_overlay).astype(np.uint8)
|
|
1014
1026
|
|
|
1015
1027
|
# Update display
|
|
1016
1028
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
@@ -1019,6 +1031,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1019
1031
|
|
|
1020
1032
|
"""Highlight overlay generation method specific for the segmenter interactive mode"""
|
|
1021
1033
|
|
|
1034
|
+
self.mini_overlay_data = None
|
|
1035
|
+
self.highlight_overlay = None
|
|
1036
|
+
|
|
1022
1037
|
def process_chunk_bounds(chunk_data, indices_to_check):
|
|
1023
1038
|
"""Process a single chunk of the array to create highlight mask"""
|
|
1024
1039
|
mask = (chunk_data >= indices_to_check[0]) & (chunk_data <= indices_to_check[1])
|
|
@@ -1291,6 +1306,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1291
1306
|
select_nodes = select_all_menu.addAction("Nodes")
|
|
1292
1307
|
select_both = select_all_menu.addAction("Nodes + Edges")
|
|
1293
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")
|
|
1294
1312
|
context_menu.addMenu(select_all_menu)
|
|
1295
1313
|
|
|
1296
1314
|
if len(self.clicked_values['nodes']) > 0 or len(self.clicked_values['edges']) > 0:
|
|
@@ -1365,6 +1383,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1365
1383
|
select_nodes.triggered.connect(lambda: self.handle_select_all(edges = False, nodes = True))
|
|
1366
1384
|
select_both.triggered.connect(lambda: self.handle_select_all(edges = True))
|
|
1367
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))
|
|
1368
1389
|
if self.highlight_overlay is not None or self.mini_overlay_data is not None:
|
|
1369
1390
|
highlight_select = context_menu.addAction("Add highlight in network selection")
|
|
1370
1391
|
highlight_select.triggered.connect(self.handle_highlight_select)
|
|
@@ -1705,28 +1726,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1705
1726
|
if edges:
|
|
1706
1727
|
edge_indices = filtered_df.iloc[:, 2].unique().tolist()
|
|
1707
1728
|
self.clicked_values['edges'] = edge_indices
|
|
1708
|
-
|
|
1709
|
-
if self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2] > self.mini_thresh:
|
|
1710
|
-
self.mini_overlay = True
|
|
1711
|
-
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1712
|
-
else:
|
|
1713
|
-
self.create_highlight_overlay(
|
|
1714
|
-
node_indices=self.clicked_values['nodes'],
|
|
1715
|
-
edge_indices=self.clicked_values['edges']
|
|
1716
|
-
)
|
|
1729
|
+
self.evaluate_mini(mode = 'edges')
|
|
1717
1730
|
else:
|
|
1718
|
-
|
|
1719
|
-
self.mini_overlay = True
|
|
1720
|
-
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1721
|
-
else:
|
|
1722
|
-
self.create_highlight_overlay(
|
|
1723
|
-
node_indices=self.clicked_values['nodes'],
|
|
1724
|
-
edge_indices = self.clicked_values['edges']
|
|
1725
|
-
)
|
|
1726
|
-
|
|
1731
|
+
self.evaluate_mini()
|
|
1727
1732
|
|
|
1728
1733
|
except Exception as e:
|
|
1729
|
-
print(f"Error
|
|
1734
|
+
print(f"Error showing neighbors: {e}")
|
|
1730
1735
|
|
|
1731
1736
|
|
|
1732
1737
|
def handle_show_component(self, edges = False, nodes = True):
|
|
@@ -1797,23 +1802,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1797
1802
|
if edges:
|
|
1798
1803
|
edge_indices = filtered_df.iloc[:, 2].unique().tolist()
|
|
1799
1804
|
self.clicked_values['edges'] = edge_indices
|
|
1800
|
-
|
|
1801
|
-
self.mini_overlay = True
|
|
1802
|
-
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1803
|
-
else:
|
|
1804
|
-
self.create_highlight_overlay(
|
|
1805
|
-
node_indices=self.clicked_values['nodes'],
|
|
1806
|
-
edge_indices=edge_indices
|
|
1807
|
-
)
|
|
1805
|
+
self.evaluate_mini(mode = 'edges')
|
|
1808
1806
|
else:
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1812
|
-
else:
|
|
1813
|
-
self.create_highlight_overlay(
|
|
1814
|
-
node_indices = self.clicked_values['nodes'],
|
|
1815
|
-
edge_indices = self.clicked_values['edges']
|
|
1816
|
-
)
|
|
1807
|
+
self.evaluate_mini()
|
|
1808
|
+
|
|
1817
1809
|
|
|
1818
1810
|
except Exception as e:
|
|
1819
1811
|
|
|
@@ -2078,12 +2070,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2078
2070
|
|
|
2079
2071
|
|
|
2080
2072
|
|
|
2081
|
-
def handle_select_all(self, nodes = True, edges = False):
|
|
2073
|
+
def handle_select_all(self, nodes = True, edges = False, network = False):
|
|
2082
2074
|
|
|
2083
2075
|
try:
|
|
2084
2076
|
|
|
2085
2077
|
if nodes:
|
|
2086
|
-
|
|
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]))
|
|
2087
2082
|
if nodes[0] == 0:
|
|
2088
2083
|
del nodes[0]
|
|
2089
2084
|
num = (self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2])
|
|
@@ -2091,7 +2086,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2091
2086
|
else:
|
|
2092
2087
|
nodes = []
|
|
2093
2088
|
if edges:
|
|
2094
|
-
|
|
2089
|
+
if not network:
|
|
2090
|
+
edges = list(np.unique(my_network.edges))
|
|
2091
|
+
else:
|
|
2092
|
+
edges = my_network.network_lists[2]
|
|
2095
2093
|
num = (self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2])
|
|
2096
2094
|
if edges[0] == 0:
|
|
2097
2095
|
del edges[0]
|
|
@@ -2214,7 +2212,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2214
2212
|
except:
|
|
2215
2213
|
pass
|
|
2216
2214
|
|
|
2217
|
-
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)
|
|
2218
2216
|
|
|
2219
2217
|
except:
|
|
2220
2218
|
pass
|
|
@@ -2640,9 +2638,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2640
2638
|
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
2641
2639
|
self.needs_mini = False
|
|
2642
2640
|
else:
|
|
2643
|
-
self.
|
|
2641
|
+
self.evaluate_mini()
|
|
2644
2642
|
else:
|
|
2645
|
-
self.
|
|
2643
|
+
self.evaluate_mini()
|
|
2646
2644
|
|
|
2647
2645
|
|
|
2648
2646
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
@@ -3707,17 +3705,23 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3707
3705
|
|
|
3708
3706
|
# Add highlight overlays if they exist (with downsampling)
|
|
3709
3707
|
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
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
|
|
3713
3714
|
elif self.highlight_overlay is not None and self.highlight:
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
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
|
|
3721
3725
|
|
|
3722
3726
|
# Convert to 0-255 range for display
|
|
3723
3727
|
return (composite * 255).astype(np.uint8)
|
|
@@ -4529,6 +4533,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4529
4533
|
for i in range(4):
|
|
4530
4534
|
load_action = load_menu.addAction(f"Load {self.channel_names[i]}")
|
|
4531
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))
|
|
4532
4538
|
load_action = load_menu.addAction("Load Network")
|
|
4533
4539
|
load_action.triggered.connect(self.load_network)
|
|
4534
4540
|
load_action = load_menu.addAction("Load From Excel Helper")
|
|
@@ -4569,6 +4575,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4569
4575
|
allstats_action.triggered.connect(self.stats)
|
|
4570
4576
|
histos_action = stats_menu.addAction("Network Statistic Histograms")
|
|
4571
4577
|
histos_action.triggered.connect(self.histos)
|
|
4578
|
+
sig_action = stats_menu.addAction("Significance Testing")
|
|
4579
|
+
sig_action.triggered.connect(self.sig_test)
|
|
4572
4580
|
radial_action = stats_menu.addAction("Radial Distribution Analysis")
|
|
4573
4581
|
radial_action.triggered.connect(self.show_radial_dialog)
|
|
4574
4582
|
neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
|
|
@@ -4780,7 +4788,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4780
4788
|
# Invalid input - reset to default
|
|
4781
4789
|
self.downsample_factor = 1
|
|
4782
4790
|
|
|
4783
|
-
|
|
4791
|
+
try:
|
|
4792
|
+
self.throttle = self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor
|
|
4793
|
+
except:
|
|
4794
|
+
self.throttle = False
|
|
4784
4795
|
|
|
4785
4796
|
# Optional: Trigger display update if you want immediate effect
|
|
4786
4797
|
if update:
|
|
@@ -4913,6 +4924,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4913
4924
|
except Exception as e:
|
|
4914
4925
|
print(f"Error creating histogram selector: {e}")
|
|
4915
4926
|
|
|
4927
|
+
def sig_test(self):
|
|
4928
|
+
# Get the existing QApplication instance
|
|
4929
|
+
app = QApplication.instance()
|
|
4930
|
+
|
|
4931
|
+
# Create the statistical GUI window without starting a new event loop
|
|
4932
|
+
stats_window = net_stats.main(app)
|
|
4933
|
+
|
|
4934
|
+
# Keep a reference so it doesn't get garbage collected
|
|
4935
|
+
self.stats_window = stats_window
|
|
4936
|
+
|
|
4916
4937
|
def volumes(self):
|
|
4917
4938
|
|
|
4918
4939
|
|
|
@@ -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
|
|
|
@@ -10616,6 +10699,9 @@ class MotherDialog(QDialog):
|
|
|
10616
10699
|
|
|
10617
10700
|
except Exception as e:
|
|
10618
10701
|
|
|
10702
|
+
import traceback
|
|
10703
|
+
print(traceback.format_exc())
|
|
10704
|
+
|
|
10619
10705
|
print(f"Error finding mothers: {e}")
|
|
10620
10706
|
|
|
10621
10707
|
|
|
@@ -11303,12 +11389,11 @@ class ThresholdDialog(QDialog):
|
|
|
11303
11389
|
print("Error - please calculate network first")
|
|
11304
11390
|
return
|
|
11305
11391
|
|
|
11306
|
-
if self.parent().mini_overlay_data is not None:
|
|
11307
|
-
self.parent().mini_overlay_data = None
|
|
11308
|
-
|
|
11309
11392
|
thresh_window = ThresholdWindow(self.parent(), accepted_mode)
|
|
11310
11393
|
thresh_window.show() # Non-modal window
|
|
11311
11394
|
self.highlight_overlay = None
|
|
11395
|
+
#self.mini_overlay = False
|
|
11396
|
+
self.mini_overlay_data = None
|
|
11312
11397
|
self.accept()
|
|
11313
11398
|
except:
|
|
11314
11399
|
import traceback
|
|
@@ -12282,16 +12367,23 @@ class ThresholdWindow(QMainWindow):
|
|
|
12282
12367
|
self.parent().bounds = False
|
|
12283
12368
|
|
|
12284
12369
|
elif accepted_mode == 0:
|
|
12285
|
-
|
|
12286
|
-
|
|
12287
|
-
|
|
12288
|
-
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
|
|
12292
|
-
self.
|
|
12293
|
-
|
|
12294
|
-
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
|
+
|
|
12295
12387
|
self.bounds = True
|
|
12296
12388
|
self.parent().bounds = True
|
|
12297
12389
|
|
|
@@ -12304,16 +12396,26 @@ class ThresholdWindow(QMainWindow):
|
|
|
12304
12396
|
layout.addWidget(self.canvas)
|
|
12305
12397
|
|
|
12306
12398
|
# Pre-compute histogram with numpy
|
|
12307
|
-
|
|
12308
|
-
|
|
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]
|
|
12309
12411
|
|
|
12310
12412
|
# Plot pre-computed histogram
|
|
12311
12413
|
self.ax = fig.add_subplot(111)
|
|
12312
12414
|
self.ax.bar(bin_centers, counts, width=bin_edges[1] - bin_edges[0], alpha=0.5)
|
|
12313
12415
|
|
|
12314
12416
|
# Add vertical lines for thresholds
|
|
12315
|
-
self.min_line = self.ax.axvline(
|
|
12316
|
-
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')
|
|
12317
12419
|
|
|
12318
12420
|
# Connect events for dragging
|
|
12319
12421
|
self.canvas.mpl_connect('button_press_event', self.on_press)
|
|
@@ -12321,13 +12423,6 @@ class ThresholdWindow(QMainWindow):
|
|
|
12321
12423
|
self.canvas.mpl_connect('button_release_event', self.on_release)
|
|
12322
12424
|
|
|
12323
12425
|
self.dragging = None
|
|
12324
|
-
|
|
12325
|
-
# Store histogram bounds
|
|
12326
|
-
if self.bounds:
|
|
12327
|
-
self.data_min = 0
|
|
12328
|
-
else:
|
|
12329
|
-
self.data_min = min(self.histo_list)
|
|
12330
|
-
self.data_max = max(self.histo_list)
|
|
12331
12426
|
|
|
12332
12427
|
# Create form layout for inputs
|
|
12333
12428
|
form_layout = QFormLayout()
|
|
@@ -12360,7 +12455,7 @@ class ThresholdWindow(QMainWindow):
|
|
|
12360
12455
|
button_layout.addWidget(run_button)
|
|
12361
12456
|
|
|
12362
12457
|
# Add Cancel button for external dialog use
|
|
12363
|
-
cancel_button = QPushButton("Cancel/Skip")
|
|
12458
|
+
cancel_button = QPushButton("Cancel/Skip (Retains Selection)")
|
|
12364
12459
|
cancel_button.clicked.connect(self.cancel_processing)
|
|
12365
12460
|
button_layout.addWidget(cancel_button)
|
|
12366
12461
|
|
|
@@ -15282,31 +15377,81 @@ class HistogramSelector(QWidget):
|
|
|
15282
15377
|
""")
|
|
15283
15378
|
layout.addWidget(button)
|
|
15284
15379
|
|
|
15380
|
+
|
|
15285
15381
|
def shortest_path_histogram(self):
|
|
15286
15382
|
try:
|
|
15287
|
-
|
|
15288
|
-
|
|
15289
|
-
|
|
15290
|
-
|
|
15291
|
-
|
|
15292
|
-
|
|
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)
|
|
15293
15420
|
path_lengths[pl] += cnts
|
|
15294
|
-
|
|
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)
|
|
15295
15436
|
freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
|
|
15296
|
-
|
|
15297
15437
|
fig, ax = plt.subplots(figsize=(15, 8))
|
|
15298
|
-
ax.bar(np.arange(1,
|
|
15438
|
+
ax.bar(np.arange(1, max_diameter + 1), height=freq_percent)
|
|
15299
15439
|
ax.set_title(
|
|
15300
|
-
"Distribution of shortest path length in G
|
|
15440
|
+
f"Distribution of shortest path length in G{title_suffix}",
|
|
15441
|
+
fontdict={"size": 35}, loc="center"
|
|
15301
15442
|
)
|
|
15302
15443
|
ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
|
|
15303
15444
|
ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
|
|
15304
15445
|
plt.show()
|
|
15305
15446
|
|
|
15306
15447
|
freq_dict = {freq: length for length, freq in enumerate(freq_percent, start=1)}
|
|
15307
|
-
self.network_analysis.format_for_upperright_table(
|
|
15308
|
-
|
|
15309
|
-
|
|
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
|
+
|
|
15310
15455
|
except Exception as e:
|
|
15311
15456
|
print(f"Error generating shortest path histogram: {e}")
|
|
15312
15457
|
|
|
@@ -15325,20 +15470,62 @@ class HistogramSelector(QWidget):
|
|
|
15325
15470
|
title="Degree Centrality Table")
|
|
15326
15471
|
except Exception as e:
|
|
15327
15472
|
print(f"Error generating degree centrality histogram: {e}")
|
|
15328
|
-
|
|
15473
|
+
|
|
15329
15474
|
def betweenness_centrality_histogram(self):
|
|
15330
15475
|
try:
|
|
15331
|
-
|
|
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)
|
|
15332
15511
|
plt.figure(figsize=(15, 8))
|
|
15333
15512
|
plt.hist(betweenness_centrality.values(), bins=100)
|
|
15334
15513
|
plt.xticks(ticks=[0, 0.02, 0.1, 0.2, 0.3, 0.4, 0.5])
|
|
15335
|
-
plt.title(
|
|
15514
|
+
plt.title(
|
|
15515
|
+
f"Betweenness Centrality Histogram{title_suffix}",
|
|
15516
|
+
fontdict={"size": 35}, loc="center"
|
|
15517
|
+
)
|
|
15336
15518
|
plt.xlabel("Betweenness Centrality", fontdict={"size": 20})
|
|
15337
15519
|
plt.ylabel("Counts", fontdict={"size": 20})
|
|
15338
15520
|
plt.show()
|
|
15339
|
-
|
|
15340
|
-
|
|
15341
|
-
|
|
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
|
+
|
|
15342
15529
|
except Exception as e:
|
|
15343
15530
|
print(f"Error generating betweenness centrality histogram: {e}")
|
|
15344
15531
|
|
|
@@ -15391,7 +15578,27 @@ class HistogramSelector(QWidget):
|
|
|
15391
15578
|
def bridges_analysis(self):
|
|
15392
15579
|
try:
|
|
15393
15580
|
bridges = list(nx.bridges(self.G))
|
|
15394
|
-
|
|
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',
|
|
15395
15602
|
title="Bridges")
|
|
15396
15603
|
except Exception as e:
|
|
15397
15604
|
print(f"Error generating bridges analysis: {e}")
|
|
@@ -15418,7 +15625,7 @@ class HistogramSelector(QWidget):
|
|
|
15418
15625
|
def node_connectivity_histogram(self):
|
|
15419
15626
|
"""Local node connectivity - minimum number of nodes that must be removed to disconnect neighbors"""
|
|
15420
15627
|
try:
|
|
15421
|
-
if self.G.number_of_nodes() > 500:
|
|
15628
|
+
if self.G.number_of_nodes() > 500:
|
|
15422
15629
|
print("Note this analysis may be slow for large network (>500 nodes)")
|
|
15423
15630
|
#return
|
|
15424
15631
|
|
|
@@ -15496,7 +15703,7 @@ class HistogramSelector(QWidget):
|
|
|
15496
15703
|
def load_centrality_histogram(self):
|
|
15497
15704
|
"""Load centrality - fraction of shortest paths passing through each node"""
|
|
15498
15705
|
try:
|
|
15499
|
-
if self.G.number_of_nodes() > 1000:
|
|
15706
|
+
if self.G.number_of_nodes() > 1000:
|
|
15500
15707
|
print("Note this analysis may be slow for large network (>1000 nodes)")
|
|
15501
15708
|
#return
|
|
15502
15709
|
|
|
@@ -15515,21 +15722,67 @@ class HistogramSelector(QWidget):
|
|
|
15515
15722
|
def communicability_centrality_histogram(self):
|
|
15516
15723
|
"""Communicability centrality - based on communicability between nodes"""
|
|
15517
15724
|
try:
|
|
15518
|
-
if self.G.number_of_nodes() > 500:
|
|
15725
|
+
if self.G.number_of_nodes() > 500:
|
|
15519
15726
|
print("Note this analysis may be slow for large network (>500 nodes)")
|
|
15520
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)"
|
|
15521
15762
|
|
|
15522
|
-
|
|
15523
|
-
|
|
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)
|
|
15524
15769
|
plt.figure(figsize=(15, 8))
|
|
15525
15770
|
plt.hist(comm_centrality.values(), bins=50, alpha=0.7)
|
|
15526
|
-
plt.title(
|
|
15771
|
+
plt.title(
|
|
15772
|
+
f"Communicability Betweenness Centrality Distribution{title_suffix}",
|
|
15773
|
+
fontdict={"size": 35}, loc="center"
|
|
15774
|
+
)
|
|
15527
15775
|
plt.xlabel("Communicability Betweenness Centrality", fontdict={"size": 20})
|
|
15528
15776
|
plt.ylabel("Frequency", fontdict={"size": 20})
|
|
15529
15777
|
plt.show()
|
|
15530
|
-
|
|
15531
|
-
|
|
15532
|
-
|
|
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
|
+
|
|
15533
15786
|
except Exception as e:
|
|
15534
15787
|
print(f"Error generating communicability betweenness centrality histogram: {e}")
|
|
15535
15788
|
|
|
@@ -15552,20 +15805,67 @@ class HistogramSelector(QWidget):
|
|
|
15552
15805
|
def current_flow_betweenness_histogram(self):
|
|
15553
15806
|
"""Current flow betweenness - models network as electrical circuit"""
|
|
15554
15807
|
try:
|
|
15555
|
-
if self.G.number_of_nodes() > 500:
|
|
15808
|
+
if self.G.number_of_nodes() > 500:
|
|
15556
15809
|
print("Note this analysis may be slow for large network (>500 nodes)")
|
|
15557
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.")
|
|
15817
|
+
|
|
15818
|
+
# Initialize dictionary to collect current flow betweenness from all components
|
|
15819
|
+
combined_current_flow = {}
|
|
15558
15820
|
|
|
15559
|
-
|
|
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)
|
|
15560
15852
|
plt.figure(figsize=(15, 8))
|
|
15561
15853
|
plt.hist(current_flow.values(), bins=50, alpha=0.7)
|
|
15562
|
-
plt.title(
|
|
15854
|
+
plt.title(
|
|
15855
|
+
f"Current Flow Betweenness Centrality Distribution{title_suffix}",
|
|
15856
|
+
fontdict={"size": 35}, loc="center"
|
|
15857
|
+
)
|
|
15563
15858
|
plt.xlabel("Current Flow Betweenness Centrality", fontdict={"size": 20})
|
|
15564
15859
|
plt.ylabel("Frequency", fontdict={"size": 20})
|
|
15565
15860
|
plt.show()
|
|
15566
|
-
|
|
15567
|
-
|
|
15568
|
-
|
|
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
|
+
|
|
15569
15869
|
except Exception as e:
|
|
15570
15870
|
print(f"Error generating current flow betweenness histogram: {e}")
|
|
15571
15871
|
|