nettracer3d 0.2.6__py3-none-any.whl → 0.2.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nettracer3d/community_extractor.py +237 -0
- nettracer3d/hub_getter.py +1 -1
- nettracer3d/modularity.py +263 -19
- nettracer3d/morphology.py +149 -7
- nettracer3d/nettracer.py +573 -52
- nettracer3d/nettracer_gui.py +1850 -210
- nettracer3d/network_analysis.py +155 -30
- nettracer3d/node_draw.py +3 -1
- nettracer3d/proximity.py +66 -6
- nettracer3d/simple_network.py +82 -72
- {nettracer3d-0.2.6.dist-info → nettracer3d-0.2.8.dist-info}/METADATA +1 -1
- nettracer3d-0.2.8.dist-info/RECORD +18 -0
- nettracer3d-0.2.6.dist-info/RECORD +0 -18
- {nettracer3d-0.2.6.dist-info → nettracer3d-0.2.8.dist-info}/LICENSE +0 -0
- {nettracer3d-0.2.6.dist-info → nettracer3d-0.2.8.dist-info}/WHEEL +0 -0
- {nettracer3d-0.2.6.dist-info → nettracer3d-0.2.8.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -13,11 +13,17 @@ from matplotlib.figure import Figure
|
|
|
13
13
|
import matplotlib.pyplot as plt
|
|
14
14
|
from qtrangeslider import QRangeSlider
|
|
15
15
|
from nettracer3d import nettracer as n3d
|
|
16
|
+
from nettracer3d import smart_dilate as sdl
|
|
17
|
+
from nettracer3d import proximity as pxt
|
|
16
18
|
from matplotlib.colors import LinearSegmentedColormap
|
|
17
19
|
import pandas as pd
|
|
18
20
|
from PyQt6.QtGui import (QFont, QCursor, QColor)
|
|
19
21
|
import tifffile
|
|
20
22
|
import copy
|
|
23
|
+
import multiprocessing as mp
|
|
24
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
25
|
+
from functools import partial
|
|
26
|
+
|
|
21
27
|
|
|
22
28
|
class ImageViewerWindow(QMainWindow):
|
|
23
29
|
def __init__(self):
|
|
@@ -120,6 +126,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
120
126
|
2: [0,0],
|
|
121
127
|
3: [0,0]
|
|
122
128
|
}
|
|
129
|
+
|
|
130
|
+
self.volume_dict = {
|
|
131
|
+
0: None,
|
|
132
|
+
1: None,
|
|
133
|
+
2: None,
|
|
134
|
+
3: None
|
|
135
|
+
} #For storing thresholding information
|
|
136
|
+
|
|
137
|
+
self.original_shape = None #For undoing resamples
|
|
123
138
|
|
|
124
139
|
# Create control panel
|
|
125
140
|
control_panel = QWidget()
|
|
@@ -379,23 +394,44 @@ class ImageViewerWindow(QMainWindow):
|
|
|
379
394
|
elif self.scroll_direction > 0 and new_value <= self.slice_slider.maximum():
|
|
380
395
|
self.slice_slider.setValue(new_value)
|
|
381
396
|
|
|
382
|
-
|
|
397
|
+
|
|
398
|
+
def create_highlight_overlay(self, node_indices=None, edge_indices=None, overlay1_indices = None, overlay2_indices = None):
|
|
383
399
|
"""
|
|
384
|
-
Create a binary overlay highlighting specific nodes and/or edges using
|
|
400
|
+
Create a binary overlay highlighting specific nodes and/or edges using parallel processing.
|
|
385
401
|
|
|
386
402
|
Args:
|
|
387
403
|
node_indices (list): List of node indices to highlight
|
|
388
404
|
edge_indices (list): List of edge indices to highlight
|
|
389
405
|
"""
|
|
406
|
+
|
|
407
|
+
def process_chunk(chunk_data, indices_to_check):
|
|
408
|
+
"""Process a single chunk of the array to create highlight mask"""
|
|
409
|
+
mask = np.isin(chunk_data, indices_to_check)
|
|
410
|
+
return mask * 255
|
|
411
|
+
|
|
412
|
+
if node_indices is not None:
|
|
413
|
+
if 0 in node_indices:
|
|
414
|
+
node_indices.remove(0)
|
|
415
|
+
if edge_indices is not None:
|
|
416
|
+
if 0 in edge_indices:
|
|
417
|
+
edge_indices.remove(0)
|
|
418
|
+
if overlay1_indices is not None:
|
|
419
|
+
if 0 in overlay1_indices:
|
|
420
|
+
overlay1_indices.remove(0)
|
|
421
|
+
|
|
390
422
|
if node_indices is None:
|
|
391
423
|
node_indices = []
|
|
392
424
|
if edge_indices is None:
|
|
393
425
|
edge_indices = []
|
|
426
|
+
if overlay1_indices is None:
|
|
427
|
+
overlay1_indices = []
|
|
428
|
+
if overlay2_indices is None:
|
|
429
|
+
overlay2_indices = []
|
|
394
430
|
|
|
395
|
-
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
431
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
396
432
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
397
|
-
|
|
398
|
-
if not node_indices and not edge_indices:
|
|
433
|
+
|
|
434
|
+
if not node_indices and not edge_indices and not overlay1_indices and not overlay2_indices:
|
|
399
435
|
self.highlight_overlay = None
|
|
400
436
|
self.highlight_bounds = None
|
|
401
437
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
@@ -412,22 +448,62 @@ class ImageViewerWindow(QMainWindow):
|
|
|
412
448
|
# Initialize full-size overlay
|
|
413
449
|
self.highlight_overlay = np.zeros(full_shape, dtype=np.uint8)
|
|
414
450
|
|
|
415
|
-
#
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
451
|
+
# Get number of CPU cores
|
|
452
|
+
num_cores = mp.cpu_count()
|
|
453
|
+
|
|
454
|
+
# Calculate chunk size along y-axis
|
|
455
|
+
chunk_size = full_shape[0] // num_cores
|
|
456
|
+
if chunk_size < 1:
|
|
457
|
+
chunk_size = 1
|
|
458
|
+
|
|
459
|
+
def process_channel(channel_data, indices, array_shape):
|
|
460
|
+
if channel_data is None or not indices:
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
# Create chunks
|
|
464
|
+
chunks = []
|
|
465
|
+
for i in range(0, array_shape[0], chunk_size):
|
|
466
|
+
end = min(i + chunk_size, array_shape[0])
|
|
467
|
+
chunks.append(channel_data[i:end])
|
|
419
468
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
mask = np.isin(self.channel_data[1], edge_indices)
|
|
423
|
-
self.highlight_overlay[mask] = 255
|
|
469
|
+
# Process chunks in parallel using ThreadPoolExecutor
|
|
470
|
+
process_func = partial(process_chunk, indices_to_check=indices)
|
|
424
471
|
|
|
425
|
-
|
|
472
|
+
with ThreadPoolExecutor(max_workers=num_cores) as executor:
|
|
473
|
+
chunk_results = list(executor.map(process_func, chunks))
|
|
474
|
+
|
|
475
|
+
# Reassemble the chunks
|
|
476
|
+
return np.vstack(chunk_results)
|
|
477
|
+
|
|
478
|
+
# Process nodes and edges in parallel using multiprocessing
|
|
479
|
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
480
|
+
future_nodes = executor.submit(process_channel, self.channel_data[0], node_indices, full_shape)
|
|
481
|
+
future_edges = executor.submit(process_channel, self.channel_data[1], edge_indices, full_shape)
|
|
482
|
+
future_overlay1 = executor.submit(process_channel, self.channel_data[2], overlay1_indices, full_shape)
|
|
483
|
+
future_overlay2 = executor.submit(process_channel, self.channel_data[3], overlay2_indices, full_shape)
|
|
484
|
+
|
|
485
|
+
# Get results
|
|
486
|
+
node_overlay = future_nodes.result()
|
|
487
|
+
edge_overlay = future_edges.result()
|
|
488
|
+
overlay1_overlay = future_overlay1.result()
|
|
489
|
+
overlay2_overlay = future_overlay2.result()
|
|
490
|
+
|
|
491
|
+
# Combine results
|
|
492
|
+
if node_overlay is not None:
|
|
493
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, node_overlay)
|
|
494
|
+
if edge_overlay is not None:
|
|
495
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, edge_overlay)
|
|
496
|
+
if overlay1_overlay is not None:
|
|
497
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay1_overlay)
|
|
498
|
+
if overlay2_overlay is not None:
|
|
499
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay2_overlay)
|
|
500
|
+
|
|
426
501
|
# Update display
|
|
427
502
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
428
503
|
|
|
429
504
|
|
|
430
505
|
|
|
506
|
+
|
|
431
507
|
#METHODS RELATED TO RIGHT CLICK:
|
|
432
508
|
|
|
433
509
|
def create_context_menu(self, event):
|
|
@@ -474,6 +550,22 @@ class ImageViewerWindow(QMainWindow):
|
|
|
474
550
|
select_edges = select_all_menu.addAction("Edges")
|
|
475
551
|
context_menu.addMenu(select_all_menu)
|
|
476
552
|
|
|
553
|
+
if len(self.clicked_values['nodes']) > 0 or len(self.clicked_values['edges']) > 0:
|
|
554
|
+
highlight_menu = QMenu("Selection", self)
|
|
555
|
+
if len(self.clicked_values['nodes']) > 1 or len(self.clicked_values['edges']) > 1:
|
|
556
|
+
combine_obj = highlight_menu.addAction("Combine Object Labels")
|
|
557
|
+
combine_obj.triggered.connect(self.handle_combine)
|
|
558
|
+
split_obj = highlight_menu.addAction("Split Non-Touching Labels")
|
|
559
|
+
split_obj.triggered.connect(self.handle_seperate)
|
|
560
|
+
delete_obj = highlight_menu.addAction("Delete Selection")
|
|
561
|
+
delete_obj.triggered.connect(self.handle_delete)
|
|
562
|
+
if len(self.clicked_values['nodes']) > 1:
|
|
563
|
+
link_nodes = highlight_menu.addAction("Link Nodes")
|
|
564
|
+
link_nodes.triggered.connect(self.handle_link)
|
|
565
|
+
delink_nodes = highlight_menu.addAction("Split Nodes")
|
|
566
|
+
delink_nodes.triggered.connect(self.handle_split)
|
|
567
|
+
context_menu.addMenu(highlight_menu)
|
|
568
|
+
|
|
477
569
|
# Create measure menu
|
|
478
570
|
measure_menu = QMenu("Measure", self)
|
|
479
571
|
|
|
@@ -901,6 +993,299 @@ class ImageViewerWindow(QMainWindow):
|
|
|
901
993
|
except Exception as e:
|
|
902
994
|
print(f"Error: {e}")
|
|
903
995
|
|
|
996
|
+
def handle_info(self, sort = 'node'):
|
|
997
|
+
|
|
998
|
+
try:
|
|
999
|
+
|
|
1000
|
+
info_dict = {}
|
|
1001
|
+
|
|
1002
|
+
if sort == 'node':
|
|
1003
|
+
|
|
1004
|
+
label = self.clicked_values['nodes'][-1]
|
|
1005
|
+
|
|
1006
|
+
info_dict['Label'] = label
|
|
1007
|
+
|
|
1008
|
+
info_dict['Object Class'] = 'Node'
|
|
1009
|
+
|
|
1010
|
+
if my_network.node_identities is not None:
|
|
1011
|
+
info_dict['ID'] = my_network.node_identities[label]
|
|
1012
|
+
|
|
1013
|
+
if my_network.network is not None:
|
|
1014
|
+
info_dict['Degree'] = my_network.network.degree(label)
|
|
1015
|
+
|
|
1016
|
+
if my_network.communities is not None:
|
|
1017
|
+
info_dict['Community'] = my_network.communities[label]
|
|
1018
|
+
|
|
1019
|
+
if my_network.node_centroids is not None:
|
|
1020
|
+
info_dict['Centroid'] = my_network.node_centroids[label]
|
|
1021
|
+
|
|
1022
|
+
if self.volume_dict[0] is not None:
|
|
1023
|
+
info_dict['Volume'] = self.volume_dict[0][label]
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
elif sort == 'edge':
|
|
1027
|
+
|
|
1028
|
+
label = self.clicked_values['edges'][-1]
|
|
1029
|
+
|
|
1030
|
+
info_dict['Label'] = label
|
|
1031
|
+
|
|
1032
|
+
info_dict['Object Class'] = 'Edge'
|
|
1033
|
+
|
|
1034
|
+
if my_network.edge_centroids is not None:
|
|
1035
|
+
info_dict['Centroid'] = my_network.edge_centroids[label]
|
|
1036
|
+
|
|
1037
|
+
if self.volume_dict[1] is not None:
|
|
1038
|
+
info_dict['Volume'] = self.volume_dict[1][label]
|
|
1039
|
+
|
|
1040
|
+
self.format_for_upperright_table(info_dict, title = f'Info on Object')
|
|
1041
|
+
|
|
1042
|
+
except:
|
|
1043
|
+
pass
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def handle_combine(self):
|
|
1049
|
+
|
|
1050
|
+
try:
|
|
1051
|
+
|
|
1052
|
+
self.clicked_values['nodes'].sort()
|
|
1053
|
+
nodes = copy.deepcopy(self.clicked_values['nodes'])
|
|
1054
|
+
self.clicked_values['edges'].sort()
|
|
1055
|
+
edges = copy.deepcopy(self.clicked_values['edges'])
|
|
1056
|
+
|
|
1057
|
+
if len(nodes) > 1:
|
|
1058
|
+
new_nodes = nodes[0]
|
|
1059
|
+
|
|
1060
|
+
mask = np.isin(self.channel_data[0], nodes)
|
|
1061
|
+
my_network.nodes[mask] = new_nodes
|
|
1062
|
+
self.load_channel(0, my_network.nodes, True)
|
|
1063
|
+
self.clicked_values['nodes'] = new_nodes
|
|
1064
|
+
|
|
1065
|
+
if len(edges) > 1:
|
|
1066
|
+
new_edges = edges[0]
|
|
1067
|
+
|
|
1068
|
+
mask = np.isin(self.channel_data[1], edges)
|
|
1069
|
+
my_network.edges[mask] = new_edges
|
|
1070
|
+
self.load_channel(1, my_network.edges, True)
|
|
1071
|
+
self.clicked_values['edges'] = new_edges
|
|
1072
|
+
|
|
1073
|
+
try:
|
|
1074
|
+
|
|
1075
|
+
for i in range(len(my_network.network_lists[0])):
|
|
1076
|
+
if my_network.network_lists[0][i] in nodes and len(nodes) > 1:
|
|
1077
|
+
my_network.network_lists[0][i] = new_nodes
|
|
1078
|
+
if my_network.network_lists[1][i] in nodes and len(nodes) > 1:
|
|
1079
|
+
my_network.network_lists[1][i] = new_nodes
|
|
1080
|
+
if my_network.network_lists[2][i] in edges and len(edges) > 1:
|
|
1081
|
+
my_network.network_lists[2][i] = new_edges
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
my_network.network_lists = my_network.network_lists
|
|
1085
|
+
|
|
1086
|
+
if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
|
|
1087
|
+
empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
|
|
1088
|
+
model = PandasModel(empty_df)
|
|
1089
|
+
self.network_table.setModel(model)
|
|
1090
|
+
else:
|
|
1091
|
+
model = PandasModel(my_network.network_lists)
|
|
1092
|
+
self.network_table.setModel(model)
|
|
1093
|
+
# Adjust column widths to content
|
|
1094
|
+
for column in range(model.columnCount(None)):
|
|
1095
|
+
self.network_table.resizeColumnToContents(column)
|
|
1096
|
+
|
|
1097
|
+
self.highlight_overlay = None
|
|
1098
|
+
self.update_display()
|
|
1099
|
+
|
|
1100
|
+
self.show_centroid_dialog()
|
|
1101
|
+
|
|
1102
|
+
except Exception as e:
|
|
1103
|
+
print(f"Error, could not update network: {e}")
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
except Exception as e:
|
|
1107
|
+
print(f"An error has occured: {e}")
|
|
1108
|
+
|
|
1109
|
+
def handle_seperate(self):
|
|
1110
|
+
|
|
1111
|
+
try:
|
|
1112
|
+
|
|
1113
|
+
if len(self.clicked_values['nodes']) > 0:
|
|
1114
|
+
self.create_highlight_overlay(node_indices = self.clicked_values['nodes'])
|
|
1115
|
+
max_val = np.max(my_network.nodes)
|
|
1116
|
+
self.highlight_overlay, num = n3d.label_objects(self.highlight_overlay)
|
|
1117
|
+
|
|
1118
|
+
node_bools = self.highlight_overlay != 0
|
|
1119
|
+
new_max = num + max_val
|
|
1120
|
+
self.highlight_overlay = self.highlight_overlay + max_val
|
|
1121
|
+
self.highlight_overlay = self.highlight_overlay * node_bools
|
|
1122
|
+
if new_max < 256:
|
|
1123
|
+
dtype = np.uint8
|
|
1124
|
+
elif new_max < 65536:
|
|
1125
|
+
dtype = np.uint16
|
|
1126
|
+
else:
|
|
1127
|
+
dtype = np.uint32
|
|
1128
|
+
|
|
1129
|
+
self.highlight_overlay = self.highlight_overlay.astype(dtype)
|
|
1130
|
+
my_network.nodes = my_network.nodes + self.highlight_overlay
|
|
1131
|
+
self.load_channel(0, my_network.nodes, True)
|
|
1132
|
+
|
|
1133
|
+
if len(self.clicked_values['edges']) > 0:
|
|
1134
|
+
self.create_highlight_overlay(edge_indices = self.clicked_values['edges'])
|
|
1135
|
+
max_val = np.max(my_network.edges)
|
|
1136
|
+
self.highlight_overlay, num = n3d.label_objects(self.highlight_overlay)
|
|
1137
|
+
node_bools = self.highlight_overlay != 0
|
|
1138
|
+
new_max = num + max_val
|
|
1139
|
+
|
|
1140
|
+
self.highlight_overlay = self.highlight_overlay + max_val
|
|
1141
|
+
self.highlight_overlay = self.highlight_overlay * node_bools
|
|
1142
|
+
if new_max < 256:
|
|
1143
|
+
dtype = np.uint8
|
|
1144
|
+
elif new_max < 65536:
|
|
1145
|
+
dtype = np.uint16
|
|
1146
|
+
else:
|
|
1147
|
+
dtype = np.uint32
|
|
1148
|
+
|
|
1149
|
+
self.highlight_overlay = self.highlight_overlay.astype(dtype)
|
|
1150
|
+
my_network.edges = my_network.edges + self.highlight_overlay
|
|
1151
|
+
self.load_channel(1, my_network.edges, True)
|
|
1152
|
+
self.highlight_overlay = None
|
|
1153
|
+
self.update_display()
|
|
1154
|
+
print("Network is not updated automatically, please recompute if necesarry. Identities are not automatically updated.")
|
|
1155
|
+
self.show_centroid_dialog()
|
|
1156
|
+
|
|
1157
|
+
except Exception as e:
|
|
1158
|
+
print(f"Error seperating: {e}")
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
def handle_delete(self):
|
|
1165
|
+
|
|
1166
|
+
try:
|
|
1167
|
+
if len(self.clicked_values['nodes']) > 0:
|
|
1168
|
+
self.create_highlight_overlay(node_indices = self.clicked_values['nodes'])
|
|
1169
|
+
mask = self.highlight_overlay == 0
|
|
1170
|
+
my_network.nodes = my_network.nodes * mask
|
|
1171
|
+
self.load_channel(0, my_network.nodes, True)
|
|
1172
|
+
|
|
1173
|
+
for i in range(len(my_network.network_lists[0]) - 1, -1, -1):
|
|
1174
|
+
if my_network.network_lists[0][i] in self.clicked_values['nodes'] or my_network.network_lists[0][i] in self.clicked_values['nodes']:
|
|
1175
|
+
del my_network.network_lists[0][i]
|
|
1176
|
+
del my_network.network_lists[1][i]
|
|
1177
|
+
del my_network.network_lists[2][i]
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
if len(self.clicked_values['edges']) > 0:
|
|
1182
|
+
self.create_highlight_overlay(node_indices = self.clicked_values['edges'])
|
|
1183
|
+
mask = self.highlight_overlay == 0
|
|
1184
|
+
my_network.edges = my_network.edges * mask
|
|
1185
|
+
self.load_channel(1, my_network.edges, True)
|
|
1186
|
+
|
|
1187
|
+
for i in range(len(my_network.network_lists[1]) - 1, -1, -1):
|
|
1188
|
+
if my_network.network_lists[2][i] in self.clicked_values['edges']:
|
|
1189
|
+
del my_network.network_lists[0][i]
|
|
1190
|
+
del my_network.network_lists[1][i]
|
|
1191
|
+
del my_network.network_lists[2][i]
|
|
1192
|
+
|
|
1193
|
+
my_network.network_lists = my_network.network_lists
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
|
|
1197
|
+
empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
|
|
1198
|
+
model = PandasModel(empty_df)
|
|
1199
|
+
self.network_table.setModel(model)
|
|
1200
|
+
else:
|
|
1201
|
+
model = PandasModel(my_network.network_lists)
|
|
1202
|
+
self.network_table.setModel(model)
|
|
1203
|
+
# Adjust column widths to content
|
|
1204
|
+
for column in range(model.columnCount(None)):
|
|
1205
|
+
self.network_table.resizeColumnToContents(column)
|
|
1206
|
+
|
|
1207
|
+
self.show_centroid_dialog()
|
|
1208
|
+
except Exception as e:
|
|
1209
|
+
print(f"Error: {e}")
|
|
1210
|
+
|
|
1211
|
+
def handle_link(self):
|
|
1212
|
+
|
|
1213
|
+
try:
|
|
1214
|
+
nodes = self.clicked_values['nodes']
|
|
1215
|
+
from itertools import combinations
|
|
1216
|
+
pairs = list(combinations(nodes, 2))
|
|
1217
|
+
|
|
1218
|
+
# Convert existing connections to a set of tuples for efficient lookup
|
|
1219
|
+
existing_connections = set()
|
|
1220
|
+
for n1, n2 in zip(my_network.network_lists[0], my_network.network_lists[1]):
|
|
1221
|
+
existing_connections.add((n1, n2))
|
|
1222
|
+
existing_connections.add((n2, n1)) # Add reverse pair too
|
|
1223
|
+
|
|
1224
|
+
# Filter out existing connections
|
|
1225
|
+
new_pairs = []
|
|
1226
|
+
for pair in pairs:
|
|
1227
|
+
if pair not in existing_connections:
|
|
1228
|
+
new_pairs.append(pair)
|
|
1229
|
+
|
|
1230
|
+
# Add new connections
|
|
1231
|
+
for pair in new_pairs:
|
|
1232
|
+
my_network.network_lists[0].append(pair[0])
|
|
1233
|
+
my_network.network_lists[1].append(pair[1])
|
|
1234
|
+
my_network.network_lists[2].append(0)
|
|
1235
|
+
|
|
1236
|
+
# Update the table
|
|
1237
|
+
if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
|
|
1238
|
+
empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
|
|
1239
|
+
model = PandasModel(empty_df)
|
|
1240
|
+
self.network_table.setModel(model)
|
|
1241
|
+
else:
|
|
1242
|
+
model = PandasModel(my_network.network_lists)
|
|
1243
|
+
self.network_table.setModel(model)
|
|
1244
|
+
# Adjust column widths to content
|
|
1245
|
+
for column in range(model.columnCount(None)):
|
|
1246
|
+
self.network_table.resizeColumnToContents(column)
|
|
1247
|
+
except Exception as e:
|
|
1248
|
+
print(f"An error has occurred: {e}")
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def handle_split(self):
|
|
1252
|
+
try:
|
|
1253
|
+
nodes = self.clicked_values['nodes']
|
|
1254
|
+
|
|
1255
|
+
from itertools import combinations
|
|
1256
|
+
|
|
1257
|
+
pairs = list(combinations(nodes, 2))
|
|
1258
|
+
|
|
1259
|
+
print(pairs)
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
for i in range(len(my_network.network_lists[0]) - 1, -1, -1):
|
|
1263
|
+
print((my_network.network_lists[0][i], my_network.network_lists[1][i]))
|
|
1264
|
+
if (my_network.network_lists[0][i], my_network.network_lists[1][i]) in pairs or (my_network.network_lists[1][i], my_network.network_lists[0][i]) in pairs:
|
|
1265
|
+
del my_network.network_lists[0][i]
|
|
1266
|
+
del my_network.network_lists[1][i]
|
|
1267
|
+
del my_network.network_lists[2][i]
|
|
1268
|
+
|
|
1269
|
+
my_network.network_lists = my_network.network_lists
|
|
1270
|
+
|
|
1271
|
+
if not hasattr(my_network, 'network_lists') or my_network.network_lists is None:
|
|
1272
|
+
empty_df = pd.DataFrame(columns=['Node 1A', 'Node 1B', 'Edge 1C'])
|
|
1273
|
+
model = PandasModel(empty_df)
|
|
1274
|
+
self.network_table.setModel(model)
|
|
1275
|
+
else:
|
|
1276
|
+
model = PandasModel(my_network.network_lists)
|
|
1277
|
+
self.network_table.setModel(model)
|
|
1278
|
+
# Adjust column widths to content
|
|
1279
|
+
for column in range(model.columnCount(None)):
|
|
1280
|
+
self.network_table.resizeColumnToContents(column)
|
|
1281
|
+
except Exception as e:
|
|
1282
|
+
print(f"An error has occurred: {e}")
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
|
|
904
1289
|
|
|
905
1290
|
def handle_highlight_select(self):
|
|
906
1291
|
|
|
@@ -1129,6 +1514,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1129
1514
|
# Try to highlight the last selected value in tables
|
|
1130
1515
|
if self.clicked_values['edges']:
|
|
1131
1516
|
self.highlight_value_in_tables(self.clicked_values['edges'][-1])
|
|
1517
|
+
|
|
1132
1518
|
|
|
1133
1519
|
elif not self.selecting and self.selection_start: # If we had a click but never started selection
|
|
1134
1520
|
# Handle as a normal click
|
|
@@ -1264,18 +1650,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1264
1650
|
# Get clicked value
|
|
1265
1651
|
x_idx = int(round(event.xdata))
|
|
1266
1652
|
y_idx = int(round(event.ydata))
|
|
1653
|
+
# Check if Ctrl key is pressed (using matplotlib's key_press system)
|
|
1654
|
+
ctrl_pressed = 'ctrl' in event.modifiers # Note: changed from 'control' to 'ctrl'
|
|
1267
1655
|
if self.channel_data[self.active_channel][self.current_slice, y_idx, x_idx] != 0:
|
|
1268
1656
|
clicked_value = self.channel_data[self.active_channel][self.current_slice, y_idx, x_idx]
|
|
1269
1657
|
else:
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1658
|
+
if not ctrl_pressed:
|
|
1659
|
+
self.clicked_values = {
|
|
1660
|
+
'nodes': [],
|
|
1661
|
+
'edges': []
|
|
1662
|
+
}
|
|
1663
|
+
self.create_highlight_overlay()
|
|
1275
1664
|
return
|
|
1276
1665
|
|
|
1277
|
-
# Check if Ctrl key is pressed (using matplotlib's key_press system)
|
|
1278
|
-
ctrl_pressed = 'ctrl' in event.modifiers # Note: changed from 'control' to 'ctrl'
|
|
1279
1666
|
|
|
1280
1667
|
starting_vals = copy.deepcopy(self.clicked_values)
|
|
1281
1668
|
|
|
@@ -1293,6 +1680,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1293
1680
|
self.clicked_values = {'nodes': [clicked_value], 'edges': []}
|
|
1294
1681
|
# Get latest value (or the last remaining one if we just removed an item)
|
|
1295
1682
|
latest_value = self.clicked_values['nodes'][-1] if self.clicked_values['nodes'] else None
|
|
1683
|
+
self.handle_info('node')
|
|
1296
1684
|
elif self.active_channel == 1:
|
|
1297
1685
|
if ctrl_pressed:
|
|
1298
1686
|
if clicked_value in self.clicked_values['edges']:
|
|
@@ -1306,6 +1694,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1306
1694
|
self.clicked_values = {'nodes': [], 'edges': [clicked_value]}
|
|
1307
1695
|
# Get latest value (or the last remaining one if we just removed an item)
|
|
1308
1696
|
latest_value = self.clicked_values['edges'][-1] if self.clicked_values['edges'] else None
|
|
1697
|
+
self.handle_info('edge')
|
|
1698
|
+
|
|
1309
1699
|
|
|
1310
1700
|
# Try to find and highlight the latest value in the current table
|
|
1311
1701
|
try:
|
|
@@ -1384,6 +1774,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1384
1774
|
load_action.triggered.connect(lambda: self.load_misc('Node Centroids'))
|
|
1385
1775
|
load_action = misc_menu.addAction("Load Edge Centroids")
|
|
1386
1776
|
load_action.triggered.connect(lambda: self.load_misc('Edge Centroids'))
|
|
1777
|
+
load_action = misc_menu.addAction("Merge Nodes")
|
|
1778
|
+
load_action.triggered.connect(lambda: self.load_misc('Merge Nodes'))
|
|
1387
1779
|
|
|
1388
1780
|
|
|
1389
1781
|
# Analysis menu
|
|
@@ -1391,11 +1783,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1391
1783
|
network_menu = analysis_menu.addMenu("Network")
|
|
1392
1784
|
netshow_action = network_menu.addAction("Show Network")
|
|
1393
1785
|
netshow_action.triggered.connect(self.show_netshow_dialog)
|
|
1394
|
-
partition_action = network_menu.addAction("Community Partition")
|
|
1786
|
+
partition_action = network_menu.addAction("Community Partition + Community Stats")
|
|
1395
1787
|
partition_action.triggered.connect(self.show_partition_dialog)
|
|
1396
1788
|
stats_menu = analysis_menu.addMenu("Stats")
|
|
1397
1789
|
allstats_action = stats_menu.addAction("Calculate Generic Network Stats")
|
|
1398
1790
|
allstats_action.triggered.connect(self.stats)
|
|
1791
|
+
radial_action = stats_menu.addAction("Radial Distribution Analysis")
|
|
1792
|
+
radial_action.triggered.connect(self.show_radial_dialog)
|
|
1793
|
+
degree_dist_action = stats_menu.addAction("Degree Distribution Analysis")
|
|
1794
|
+
degree_dist_action.triggered.connect(self.show_degree_dist_dialog)
|
|
1795
|
+
neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
|
|
1796
|
+
neighbor_id_action.triggered.connect(self.show_neighbor_id_dialog)
|
|
1797
|
+
random_action = stats_menu.addAction("Generate Equivalent Random Network")
|
|
1798
|
+
random_action.triggered.connect(self.show_random_dialog)
|
|
1399
1799
|
vol_action = stats_menu.addAction("Calculate Volumes")
|
|
1400
1800
|
vol_action.triggered.connect(self.volumes)
|
|
1401
1801
|
inter_action = stats_menu.addAction("Calculate Node < > Edge Interaction")
|
|
@@ -1403,8 +1803,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1403
1803
|
overlay_menu = analysis_menu.addMenu("Data/Overlays")
|
|
1404
1804
|
degree_action = overlay_menu.addAction("Get Degree Information")
|
|
1405
1805
|
degree_action.triggered.connect(self.show_degree_dialog)
|
|
1806
|
+
hub_action = overlay_menu.addAction("Get Hub Information")
|
|
1807
|
+
hub_action.triggered.connect(self.show_hub_dialog)
|
|
1406
1808
|
mother_action = overlay_menu.addAction("Get Mother Nodes")
|
|
1407
1809
|
mother_action.triggered.connect(self.show_mother_dialog)
|
|
1810
|
+
community_code_action = overlay_menu.addAction("Code Communities")
|
|
1811
|
+
community_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Community'))
|
|
1812
|
+
id_code_action = overlay_menu.addAction("Code Identities")
|
|
1813
|
+
id_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Identity'))
|
|
1814
|
+
|
|
1408
1815
|
|
|
1409
1816
|
# Process menu
|
|
1410
1817
|
process_menu = menubar.addMenu("Process")
|
|
@@ -1421,25 +1828,37 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1421
1828
|
resize_action.triggered.connect(self.show_resize_dialog)
|
|
1422
1829
|
dilate_action = image_menu.addAction("Dilate")
|
|
1423
1830
|
dilate_action.triggered.connect(self.show_dilate_dialog)
|
|
1831
|
+
erode_action = image_menu.addAction("Erode")
|
|
1832
|
+
erode_action.triggered.connect(self.show_erode_dialog)
|
|
1833
|
+
hole_action = image_menu.addAction("Fill Holes")
|
|
1834
|
+
hole_action.triggered.connect(self.show_hole_dialog)
|
|
1424
1835
|
binarize_action = image_menu.addAction("Binarize")
|
|
1425
1836
|
binarize_action.triggered.connect(self.show_binarize_dialog)
|
|
1426
1837
|
label_action = image_menu.addAction("Label Objects")
|
|
1427
1838
|
label_action.triggered.connect(self.show_label_dialog)
|
|
1839
|
+
thresh_action = image_menu.addAction("Threshold/Segment")
|
|
1840
|
+
thresh_action.triggered.connect(self.show_thresh_dialog)
|
|
1428
1841
|
mask_action = image_menu.addAction("Mask Channel")
|
|
1429
1842
|
mask_action.triggered.connect(self.show_mask_dialog)
|
|
1430
1843
|
skeletonize_action = image_menu.addAction("Skeletonize")
|
|
1431
1844
|
skeletonize_action.triggered.connect(self.show_skeletonize_dialog)
|
|
1432
1845
|
watershed_action = image_menu.addAction("Watershed")
|
|
1433
1846
|
watershed_action.triggered.connect(self.show_watershed_dialog)
|
|
1847
|
+
z_proj_action = image_menu.addAction("Z Project")
|
|
1848
|
+
z_proj_action.triggered.connect(self.show_z_dialog)
|
|
1434
1849
|
|
|
1435
|
-
|
|
1850
|
+
generate_menu = process_menu.addMenu("Generate")
|
|
1851
|
+
centroid_node_action = generate_menu.addAction("Generate Nodes (From Node Centroids)")
|
|
1436
1852
|
centroid_node_action.triggered.connect(self.show_centroid_node_dialog)
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
gennodes_action = process_menu.addAction("Generate Nodes (From 'Edge' Vertices)")
|
|
1853
|
+
gennodes_action = generate_menu.addAction("Generate Nodes (From 'Edge' Vertices)")
|
|
1440
1854
|
gennodes_action.triggered.connect(self.show_gennodes_dialog)
|
|
1441
|
-
branch_action =
|
|
1855
|
+
branch_action = generate_menu.addAction("Label Branches")
|
|
1442
1856
|
branch_action.triggered.connect(self.show_branch_dialog)
|
|
1857
|
+
genvor_action = generate_menu.addAction("Generate Voronoi Diagram (From Node Centroids) - goes in Overlay2")
|
|
1858
|
+
genvor_action.triggered.connect(self.voronoi)
|
|
1859
|
+
|
|
1860
|
+
modify_action = process_menu.addAction("Modify Network")
|
|
1861
|
+
modify_action.triggered.connect(self.show_modify_dialog)
|
|
1443
1862
|
|
|
1444
1863
|
|
|
1445
1864
|
# Image menu
|
|
@@ -1477,17 +1896,26 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1477
1896
|
|
|
1478
1897
|
def volumes(self):
|
|
1479
1898
|
|
|
1480
|
-
print(self.active_channel)
|
|
1481
1899
|
|
|
1482
1900
|
if self.active_channel == 1:
|
|
1483
1901
|
output = my_network.volumes('edges')
|
|
1484
1902
|
self.format_for_upperright_table(output, metric='Edge ID', value = 'Voxel Volume (Scaled)', title = 'Edge Volumes')
|
|
1903
|
+
self.volume_dict[1] = output
|
|
1485
1904
|
|
|
1486
|
-
|
|
1905
|
+
elif self.active_channel == 0:
|
|
1487
1906
|
output = my_network.volumes('nodes')
|
|
1488
1907
|
self.format_for_upperright_table(output, metric='Node ID', value = 'Voxel Volume (Scaled)', title = 'Node Volumes')
|
|
1908
|
+
self.volume_dict[0] = output
|
|
1489
1909
|
|
|
1910
|
+
elif self.active_channel == 2:
|
|
1911
|
+
output = my_network.volumes('network_overlay')
|
|
1912
|
+
self.format_for_upperright_table(output, metric='Object ID', value = 'Voxel Volume (Scaled)', title = 'Overlay 1 Volumes')
|
|
1913
|
+
self.volume_dict[2] = output
|
|
1490
1914
|
|
|
1915
|
+
elif self.active_channel == 3:
|
|
1916
|
+
output = my_network.volumes('id_overlay')
|
|
1917
|
+
self.format_for_upperright_table(output, metric='Object ID', value = 'Voxel Volume (Scaled)', title = 'Overlay 2 Volumes')
|
|
1918
|
+
self.volume_dict[3] = output
|
|
1491
1919
|
|
|
1492
1920
|
|
|
1493
1921
|
|
|
@@ -1582,6 +2010,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1582
2010
|
dialog = WatershedDialog(self)
|
|
1583
2011
|
dialog.exec()
|
|
1584
2012
|
|
|
2013
|
+
def show_z_dialog(self):
|
|
2014
|
+
"""Show the z-proj dialog."""
|
|
2015
|
+
dialog = ZDialog(self)
|
|
2016
|
+
dialog.exec()
|
|
2017
|
+
|
|
1585
2018
|
def show_calc_all_dialog(self):
|
|
1586
2019
|
"""Show the calculate all parameter dialog."""
|
|
1587
2020
|
dialog = CalcAllDialog(self)
|
|
@@ -1602,11 +2035,26 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1602
2035
|
dialog = DilateDialog(self)
|
|
1603
2036
|
dialog.exec()
|
|
1604
2037
|
|
|
2038
|
+
def show_erode_dialog(self):
|
|
2039
|
+
"""show the erode dialog"""
|
|
2040
|
+
dialog = ErodeDialog(self)
|
|
2041
|
+
dialog.exec()
|
|
2042
|
+
|
|
2043
|
+
def show_hole_dialog(self):
|
|
2044
|
+
"""show the hole dialog"""
|
|
2045
|
+
dialog = HoleDialog(self)
|
|
2046
|
+
dialog.exec()
|
|
2047
|
+
|
|
1605
2048
|
def show_label_dialog(self):
|
|
1606
2049
|
"""Show the label dialog"""
|
|
1607
2050
|
dialog = LabelDialog(self)
|
|
1608
2051
|
dialog.exec()
|
|
1609
2052
|
|
|
2053
|
+
def show_thresh_dialog(self):
|
|
2054
|
+
"""Show threshold dialog"""
|
|
2055
|
+
thresh_window = ThresholdWindow(self)
|
|
2056
|
+
thresh_window.show() # Non-modal window
|
|
2057
|
+
|
|
1610
2058
|
def show_mask_dialog(self):
|
|
1611
2059
|
"""Show the mask dialog"""
|
|
1612
2060
|
dialog = MaskDialog(self)
|
|
@@ -1633,6 +2081,33 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1633
2081
|
dialog = BranchDialog(self)
|
|
1634
2082
|
dialog.exec()
|
|
1635
2083
|
|
|
2084
|
+
def voronoi(self):
|
|
2085
|
+
|
|
2086
|
+
try:
|
|
2087
|
+
|
|
2088
|
+
if my_network.nodes is not None:
|
|
2089
|
+
shape = my_network.nodes.shape
|
|
2090
|
+
else:
|
|
2091
|
+
shape = None
|
|
2092
|
+
|
|
2093
|
+
if my_network.node_centroids is None:
|
|
2094
|
+
self.show_centroid_dialog()
|
|
2095
|
+
if my_network.node_centroids is None:
|
|
2096
|
+
print("Node centroids must be set")
|
|
2097
|
+
return
|
|
2098
|
+
|
|
2099
|
+
array = pxt.create_voronoi_3d_kdtree(my_network.node_centroids, shape)
|
|
2100
|
+
self.load_channel(3, array, True)
|
|
2101
|
+
|
|
2102
|
+
except Exception as e:
|
|
2103
|
+
print(f"Error generating voronoi: {e}")
|
|
2104
|
+
|
|
2105
|
+
|
|
2106
|
+
def show_modify_dialog(self):
|
|
2107
|
+
"""Show the network modify dialog"""
|
|
2108
|
+
dialog = ModifyDialog(self)
|
|
2109
|
+
dialog.exec()
|
|
2110
|
+
|
|
1636
2111
|
|
|
1637
2112
|
def show_binarize_dialog(self):
|
|
1638
2113
|
"""show the binarize dialog"""
|
|
@@ -1696,53 +2171,164 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1696
2171
|
def load_misc(self, sort):
|
|
1697
2172
|
"""Loads various things"""
|
|
1698
2173
|
|
|
1699
|
-
|
|
2174
|
+
def uncork(my_dict, trumper = None):
|
|
1700
2175
|
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
2176
|
+
if trumper is None:
|
|
2177
|
+
for thing in my_dict:
|
|
2178
|
+
val = my_dict[thing]
|
|
2179
|
+
new_val = val[0]
|
|
2180
|
+
for i in range(1, len(val)):
|
|
2181
|
+
try:
|
|
2182
|
+
new_val += f" AND {val[i]}"
|
|
2183
|
+
except:
|
|
2184
|
+
break
|
|
2185
|
+
my_dict[thing] = new_val
|
|
2186
|
+
elif trumper == '-':
|
|
2187
|
+
for key, value in my_dict.items():
|
|
2188
|
+
my_dict[key] = value[0]
|
|
2189
|
+
else:
|
|
2190
|
+
for thing in my_dict:
|
|
2191
|
+
val = my_dict[thing]
|
|
2192
|
+
if trumper in val:
|
|
2193
|
+
my_dict[thing] = trumper
|
|
2194
|
+
else:
|
|
2195
|
+
new_val = val[0]
|
|
2196
|
+
for i in range(1, len(val)):
|
|
2197
|
+
try:
|
|
2198
|
+
new_val += f" AND {val[i]}"
|
|
2199
|
+
except:
|
|
2200
|
+
break
|
|
2201
|
+
my_dict[thing] = new_val
|
|
2202
|
+
|
|
2203
|
+
return my_dict
|
|
2204
|
+
|
|
2205
|
+
if sort != 'Merge Nodes':
|
|
1707
2206
|
|
|
1708
2207
|
try:
|
|
1709
|
-
if sort == 'Node Identities':
|
|
1710
|
-
my_network.load_node_identities(file_path = filename)
|
|
1711
2208
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
2209
|
+
filename, _ = QFileDialog.getOpenFileName(
|
|
2210
|
+
self,
|
|
2211
|
+
f"Load {sort}",
|
|
2212
|
+
"",
|
|
2213
|
+
"Spreadsheets (*.xlsx *.csv *.json)"
|
|
2214
|
+
)
|
|
1717
2215
|
|
|
1718
|
-
|
|
1719
|
-
|
|
2216
|
+
try:
|
|
2217
|
+
if sort == 'Node Identities':
|
|
2218
|
+
my_network.load_node_identities(file_path = filename)
|
|
2219
|
+
|
|
2220
|
+
first_value = list(my_network.node_identities.values())[0] # Check that there are not multiple IDs
|
|
2221
|
+
if isinstance(first_value, (list, tuple)):
|
|
2222
|
+
trump_value, ok = QInputDialog.getText(
|
|
2223
|
+
self,
|
|
2224
|
+
'Multiple IDs Detected',
|
|
2225
|
+
'The node identities appear to contain multiple ids per node in a list.\n'
|
|
2226
|
+
'If you desire one node ID to trump all others, enter it here.\n'
|
|
2227
|
+
'(Enter "-" to have the first IDs trump all others or press x to skip)'
|
|
2228
|
+
)
|
|
2229
|
+
if not ok or trump_value.strip() == '':
|
|
2230
|
+
trump_value = None
|
|
2231
|
+
elif trump_value.upper() == '-':
|
|
2232
|
+
trump_value = '-'
|
|
2233
|
+
my_network.node_identities = uncork(my_network.node_identities, trump_value)
|
|
2234
|
+
else:
|
|
2235
|
+
trump_value = None
|
|
2236
|
+
my_network.node_identities = uncork(my_network.node_identities, trump_value)
|
|
1720
2237
|
|
|
1721
|
-
if hasattr(my_network, 'node_centroids') and my_network.node_centroids is not None:
|
|
1722
|
-
try:
|
|
1723
|
-
self.format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
|
|
1724
|
-
except Exception as e:
|
|
1725
|
-
print(f"Error loading node centroid table: {e}")
|
|
1726
2238
|
|
|
1727
|
-
|
|
1728
|
-
|
|
2239
|
+
if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
|
|
2240
|
+
try:
|
|
2241
|
+
self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
|
|
2242
|
+
except Exception as e:
|
|
2243
|
+
print(f"Error loading node identity table: {e}")
|
|
1729
2244
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
self.format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
|
|
1733
|
-
except Exception as e:
|
|
1734
|
-
print(f"Error loading edge centroid table: {e}")
|
|
2245
|
+
elif sort == 'Node Centroids':
|
|
2246
|
+
my_network.load_node_centroids(file_path = filename)
|
|
1735
2247
|
|
|
2248
|
+
if hasattr(my_network, 'node_centroids') and my_network.node_centroids is not None:
|
|
2249
|
+
try:
|
|
2250
|
+
self.format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
|
|
2251
|
+
except Exception as e:
|
|
2252
|
+
print(f"Error loading node centroid table: {e}")
|
|
2253
|
+
|
|
2254
|
+
elif sort == 'Edge Centroids':
|
|
2255
|
+
my_network.load_edge_centroids(file_path = filename)
|
|
2256
|
+
|
|
2257
|
+
if hasattr(my_network, 'edge_centroids') and my_network.edge_centroids is not None:
|
|
2258
|
+
try:
|
|
2259
|
+
self.format_for_upperright_table(my_network.edge_centroids, 'EdgeID', ['Z', 'Y', 'X'], 'Edge Centroids')
|
|
2260
|
+
except Exception as e:
|
|
2261
|
+
print(f"Error loading edge centroid table: {e}")
|
|
2262
|
+
|
|
2263
|
+
|
|
2264
|
+
except Exception as e:
|
|
2265
|
+
import traceback
|
|
2266
|
+
print(traceback.format_exc())
|
|
2267
|
+
print(f"An error has occured: {e}")
|
|
1736
2268
|
|
|
1737
2269
|
except Exception as e:
|
|
1738
|
-
|
|
2270
|
+
import traceback
|
|
2271
|
+
print(traceback.format_exc())
|
|
2272
|
+
QMessageBox.critical(
|
|
2273
|
+
self,
|
|
2274
|
+
"Error Loading",
|
|
2275
|
+
f"Failed to load {sort}: {str(e)}"
|
|
2276
|
+
)
|
|
1739
2277
|
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
2278
|
+
else:
|
|
2279
|
+
try:
|
|
2280
|
+
|
|
2281
|
+
if len(np.unique(my_network.nodes)) < 3:
|
|
2282
|
+
self.show_label_dialog()
|
|
2283
|
+
|
|
2284
|
+
# First ask user what they want to select
|
|
2285
|
+
msg = QMessageBox()
|
|
2286
|
+
msg.setWindowTitle("Selection Type")
|
|
2287
|
+
msg.setText("Would you like to select a TIFF file or a directory?")
|
|
2288
|
+
tiff_button = msg.addButton("TIFF File", QMessageBox.ButtonRole.AcceptRole)
|
|
2289
|
+
dir_button = msg.addButton("Directory", QMessageBox.ButtonRole.AcceptRole)
|
|
2290
|
+
msg.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
|
|
2291
|
+
|
|
2292
|
+
msg.exec()
|
|
2293
|
+
|
|
2294
|
+
if msg.clickedButton() == tiff_button:
|
|
2295
|
+
# Code for selecting TIFF files
|
|
2296
|
+
filename, _ = QFileDialog.getOpenFileName(
|
|
2297
|
+
self,
|
|
2298
|
+
"Select TIFF file",
|
|
2299
|
+
"",
|
|
2300
|
+
"TIFF files (*.tiff *.tif)"
|
|
2301
|
+
)
|
|
2302
|
+
if filename:
|
|
2303
|
+
selected_path = filename
|
|
2304
|
+
|
|
2305
|
+
elif msg.clickedButton() == dir_button:
|
|
2306
|
+
# Code for selecting directories
|
|
2307
|
+
dialog = QFileDialog(self)
|
|
2308
|
+
dialog.setOption(QFileDialog.Option.DontUseNativeDialog)
|
|
2309
|
+
dialog.setOption(QFileDialog.Option.ReadOnly)
|
|
2310
|
+
dialog.setFileMode(QFileDialog.FileMode.Directory)
|
|
2311
|
+
dialog.setViewMode(QFileDialog.ViewMode.Detail)
|
|
2312
|
+
|
|
2313
|
+
if dialog.exec() == QFileDialog.DialogCode.Accepted:
|
|
2314
|
+
selected_path = dialog.directory().absolutePath()
|
|
2315
|
+
|
|
2316
|
+
my_network.merge_nodes(selected_path)
|
|
2317
|
+
self.load_channel(0, my_network.nodes, True)
|
|
2318
|
+
|
|
2319
|
+
|
|
2320
|
+
if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
|
|
2321
|
+
try:
|
|
2322
|
+
self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
|
|
2323
|
+
except Exception as e:
|
|
2324
|
+
print(f"Error loading node identity table: {e}")
|
|
2325
|
+
|
|
2326
|
+
except Exception as e:
|
|
2327
|
+
QMessageBox.critical(
|
|
2328
|
+
self,
|
|
2329
|
+
"Error Merging",
|
|
2330
|
+
f"Failed to load {sort}: {str(e)}"
|
|
2331
|
+
)
|
|
1746
2332
|
|
|
1747
2333
|
|
|
1748
2334
|
# Modify load_from_network_obj method
|
|
@@ -1834,7 +2420,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1834
2420
|
self,
|
|
1835
2421
|
f"Load Network",
|
|
1836
2422
|
"",
|
|
1837
|
-
"Spreadsheets (*.xlsx *.csv)"
|
|
2423
|
+
"Spreadsheets (*.xlsx *.csv *.json)"
|
|
1838
2424
|
)
|
|
1839
2425
|
|
|
1840
2426
|
my_network.load_network(file_path = filename)
|
|
@@ -1870,7 +2456,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1870
2456
|
else:
|
|
1871
2457
|
btn.setStyleSheet("")
|
|
1872
2458
|
|
|
1873
|
-
def load_channel(self, channel_index, channel_data=None, data=False):
|
|
2459
|
+
def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True):
|
|
1874
2460
|
"""Load a channel and enable active channel selection if needed."""
|
|
1875
2461
|
|
|
1876
2462
|
try:
|
|
@@ -1884,7 +2470,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1884
2470
|
)
|
|
1885
2471
|
self.channel_data[channel_index] = tifffile.imread(filename)
|
|
1886
2472
|
if len(self.channel_data[channel_index].shape) == 2:
|
|
1887
|
-
self.channel_data[channel_index] = np.stack((self.channel_data[channel_index], self.channel_data[channel_index]), axis = 0) #currently handle 2d arrays by just making them 3d
|
|
2473
|
+
#self.channel_data[channel_index] = np.stack((self.channel_data[channel_index], self.channel_data[channel_index]), axis = 0) #currently handle 2d arrays by just making them 3d
|
|
2474
|
+
self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
|
|
1888
2475
|
|
|
1889
2476
|
|
|
1890
2477
|
else:
|
|
@@ -1910,18 +2497,21 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1910
2497
|
self.active_channel_combo.setEnabled(True)
|
|
1911
2498
|
|
|
1912
2499
|
# Update slider range if this is the first channel loaded
|
|
1913
|
-
if
|
|
1914
|
-
self.slice_slider.
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2500
|
+
if len(self.channel_data[channel_index].shape) == 3:
|
|
2501
|
+
if not self.slice_slider.isEnabled():
|
|
2502
|
+
self.slice_slider.setEnabled(True)
|
|
2503
|
+
self.slice_slider.setMinimum(0)
|
|
2504
|
+
self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
|
|
2505
|
+
self.slice_slider.setValue(0)
|
|
2506
|
+
self.current_slice = 0
|
|
2507
|
+
else:
|
|
2508
|
+
self.slice_slider.setEnabled(True)
|
|
2509
|
+
self.slice_slider.setMinimum(0)
|
|
2510
|
+
self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
|
|
2511
|
+
self.slice_slider.setValue(0)
|
|
2512
|
+
self.current_slice = 0
|
|
2513
|
+
else:
|
|
2514
|
+
self.slice_slider.setEnabled(False)
|
|
1925
2515
|
|
|
1926
2516
|
|
|
1927
2517
|
# If this is the first channel loaded, make it active
|
|
@@ -1932,6 +2522,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1932
2522
|
self.channel_buttons[channel_index].click()
|
|
1933
2523
|
self.min_max[channel_index][0] = np.min(self.channel_data[channel_index])
|
|
1934
2524
|
self.min_max[channel_index][1] = np.max(self.channel_data[channel_index])
|
|
2525
|
+
self.volume_dict[channel_index] = None #reset volumes
|
|
2526
|
+
|
|
2527
|
+
if assign_shape: #keep original shape tracked to undo resampling.
|
|
2528
|
+
self.original_shape = self.channel_data[channel_index].shape
|
|
1935
2529
|
|
|
1936
2530
|
self.update_display()
|
|
1937
2531
|
|
|
@@ -2157,117 +2751,132 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2157
2751
|
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
2158
2752
|
|
|
2159
2753
|
def update_display(self, preserve_zoom=None):
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2754
|
+
"""Update the display with currently visible channels and highlight overlay."""
|
|
2755
|
+
self.figure.clear()
|
|
2756
|
+
|
|
2757
|
+
# Create subplot with tight layout and white figure background
|
|
2758
|
+
self.figure.patch.set_facecolor('white')
|
|
2759
|
+
self.ax = self.figure.add_subplot(111)
|
|
2760
|
+
|
|
2761
|
+
# Store current zoom limits if they exist and weren't provided
|
|
2762
|
+
if preserve_zoom is None and hasattr(self, 'ax'):
|
|
2763
|
+
current_xlim = self.ax.get_xlim() if self.ax.get_xlim() != (0, 1) else None
|
|
2764
|
+
current_ylim = self.ax.get_ylim() if self.ax.get_ylim() != (0, 1) else None
|
|
2765
|
+
else:
|
|
2766
|
+
current_xlim, current_ylim = preserve_zoom if preserve_zoom else (None, None)
|
|
2767
|
+
|
|
2768
|
+
# Define base colors for each channel with increased intensity
|
|
2769
|
+
base_colors = self.base_colors
|
|
2770
|
+
# Set only the axes (image area) background to black
|
|
2771
|
+
self.ax.set_facecolor('black')
|
|
2772
|
+
|
|
2773
|
+
# Display each visible channel
|
|
2774
|
+
for channel in range(4):
|
|
2775
|
+
if (self.channel_visible[channel] and
|
|
2776
|
+
self.channel_data[channel] is not None):
|
|
2777
|
+
|
|
2778
|
+
# Check if we're dealing with RGB data
|
|
2779
|
+
is_rgb = len(self.channel_data[channel].shape) == 4 and self.channel_data[channel].shape[-1] == 3
|
|
2780
|
+
|
|
2781
|
+
if len(self.channel_data[channel].shape) == 3 and not is_rgb:
|
|
2782
|
+
current_image = self.channel_data[channel][self.current_slice, :, :]
|
|
2783
|
+
elif is_rgb:
|
|
2784
|
+
current_image = self.channel_data[channel][self.current_slice] # Already has RGB channels
|
|
2785
|
+
else:
|
|
2786
|
+
current_image = self.channel_data[channel]
|
|
2787
|
+
|
|
2788
|
+
if is_rgb:
|
|
2789
|
+
# For RGB images, just display directly without colormap
|
|
2790
|
+
self.ax.imshow(current_image,
|
|
2791
|
+
alpha=0.7)
|
|
2792
|
+
else:
|
|
2793
|
+
# Regular channel processing with colormap
|
|
2794
|
+
# Calculate brightness/contrast limits from entire volume
|
|
2795
|
+
img_min = self.min_max[channel][0]
|
|
2796
|
+
img_max = self.min_max[channel][1]
|
|
2797
|
+
|
|
2798
|
+
# Calculate vmin and vmax, ensuring we don't get a zero range
|
|
2799
|
+
if img_min == img_max:
|
|
2800
|
+
vmin = img_min
|
|
2801
|
+
vmax = img_min + 1
|
|
2802
|
+
else:
|
|
2803
|
+
vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
|
|
2804
|
+
vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
|
|
2805
|
+
|
|
2806
|
+
# Normalize the image safely
|
|
2807
|
+
if vmin == vmax:
|
|
2808
|
+
normalized_image = np.zeros_like(current_image)
|
|
2809
|
+
else:
|
|
2810
|
+
normalized_image = np.clip((current_image - vmin) / (vmax - vmin), 0, 1)
|
|
2811
|
+
|
|
2812
|
+
# Create custom colormap with higher intensity
|
|
2813
|
+
color = base_colors[channel]
|
|
2814
|
+
custom_cmap = LinearSegmentedColormap.from_list(
|
|
2815
|
+
f'custom_{channel}',
|
|
2816
|
+
[(0,0,0,0), (*color,1)]
|
|
2817
|
+
)
|
|
2818
|
+
|
|
2819
|
+
# Display the image with slightly higher alpha
|
|
2820
|
+
self.ax.imshow(normalized_image,
|
|
2821
|
+
alpha=0.7,
|
|
2822
|
+
cmap=custom_cmap,
|
|
2823
|
+
vmin=0,
|
|
2824
|
+
vmax=1)
|
|
2825
|
+
|
|
2826
|
+
# Rest of the code remains the same...
|
|
2827
|
+
# Add highlight overlay if it exists
|
|
2828
|
+
if self.highlight_overlay is not None:
|
|
2829
|
+
highlight_slice = self.highlight_overlay[self.current_slice]
|
|
2830
|
+
highlight_cmap = LinearSegmentedColormap.from_list(
|
|
2831
|
+
'highlight',
|
|
2832
|
+
[(0, 0, 0, 0), (1, 1, 0, 1)] # yellow
|
|
2208
2833
|
)
|
|
2834
|
+
self.ax.imshow(highlight_slice,
|
|
2835
|
+
cmap=highlight_cmap,
|
|
2836
|
+
alpha=0.5)
|
|
2837
|
+
|
|
2838
|
+
# Restore zoom limits if they existed
|
|
2839
|
+
if current_xlim is not None and current_ylim is not None:
|
|
2840
|
+
self.ax.set_xlim(current_xlim)
|
|
2841
|
+
self.ax.set_ylim(current_ylim)
|
|
2842
|
+
|
|
2843
|
+
# Style the axes
|
|
2844
|
+
self.ax.set_xlabel('X')
|
|
2845
|
+
self.ax.set_ylabel('Y')
|
|
2846
|
+
self.ax.set_title(f'Slice {self.current_slice}')
|
|
2847
|
+
|
|
2848
|
+
# Make axis labels and ticks black for visibility against white background
|
|
2849
|
+
self.ax.xaxis.label.set_color('black')
|
|
2850
|
+
self.ax.yaxis.label.set_color('black')
|
|
2851
|
+
self.ax.title.set_color('black')
|
|
2852
|
+
self.ax.tick_params(colors='black')
|
|
2853
|
+
for spine in self.ax.spines.values():
|
|
2854
|
+
spine.set_color('black')
|
|
2855
|
+
|
|
2856
|
+
# Adjust the layout to ensure the plot fits well in the figure
|
|
2857
|
+
self.figure.tight_layout()
|
|
2858
|
+
|
|
2859
|
+
# Redraw measurement points and their labels
|
|
2860
|
+
for point in self.measurement_points:
|
|
2861
|
+
x1, y1, z1 = point['point1']
|
|
2862
|
+
x2, y2, z2 = point['point2']
|
|
2863
|
+
pair_idx = point['pair_index']
|
|
2209
2864
|
|
|
2210
|
-
#
|
|
2211
|
-
self.
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
)
|
|
2224
|
-
self.ax.imshow(highlight_slice,
|
|
2225
|
-
cmap=highlight_cmap,
|
|
2226
|
-
alpha=0.5)
|
|
2227
|
-
|
|
2228
|
-
# Restore zoom limits if they existed
|
|
2229
|
-
if current_xlim is not None and current_ylim is not None:
|
|
2230
|
-
self.ax.set_xlim(current_xlim)
|
|
2231
|
-
self.ax.set_ylim(current_ylim)
|
|
2232
|
-
|
|
2233
|
-
# Style the axes
|
|
2234
|
-
self.ax.set_xlabel('X')
|
|
2235
|
-
self.ax.set_ylabel('Y')
|
|
2236
|
-
self.ax.set_title(f'Slice {self.current_slice}')
|
|
2237
|
-
|
|
2238
|
-
# Make axis labels and ticks black for visibility against white background
|
|
2239
|
-
self.ax.xaxis.label.set_color('black')
|
|
2240
|
-
self.ax.yaxis.label.set_color('black')
|
|
2241
|
-
self.ax.title.set_color('black')
|
|
2242
|
-
self.ax.tick_params(colors='black')
|
|
2243
|
-
for spine in self.ax.spines.values():
|
|
2244
|
-
spine.set_color('black')
|
|
2245
|
-
|
|
2246
|
-
# Adjust the layout to ensure the plot fits well in the figure
|
|
2247
|
-
self.figure.tight_layout()
|
|
2248
|
-
|
|
2249
|
-
# Redraw measurement points and their labels
|
|
2250
|
-
for point in self.measurement_points:
|
|
2251
|
-
x1, y1, z1 = point['point1']
|
|
2252
|
-
x2, y2, z2 = point['point2']
|
|
2253
|
-
pair_idx = point['pair_index']
|
|
2254
|
-
|
|
2255
|
-
# Draw points and labels if they're on current slice
|
|
2256
|
-
if z1 == self.current_slice:
|
|
2257
|
-
self.ax.plot(x1, y1, 'yo', markersize=8)
|
|
2258
|
-
self.ax.text(x1, y1+5, str(pair_idx),
|
|
2259
|
-
color='white', ha='center', va='bottom')
|
|
2260
|
-
if z2 == self.current_slice:
|
|
2261
|
-
self.ax.plot(x2, y2, 'yo', markersize=8)
|
|
2262
|
-
self.ax.text(x2, y2+5, str(pair_idx),
|
|
2263
|
-
color='white', ha='center', va='bottom')
|
|
2264
|
-
|
|
2265
|
-
# Draw line if both points are on current slice
|
|
2266
|
-
if z1 == z2 == self.current_slice:
|
|
2267
|
-
self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
|
|
2268
|
-
|
|
2865
|
+
# Draw points and labels if they're on current slice
|
|
2866
|
+
if z1 == self.current_slice:
|
|
2867
|
+
self.ax.plot(x1, y1, 'yo', markersize=8)
|
|
2868
|
+
self.ax.text(x1, y1+5, str(pair_idx),
|
|
2869
|
+
color='white', ha='center', va='bottom')
|
|
2870
|
+
if z2 == self.current_slice:
|
|
2871
|
+
self.ax.plot(x2, y2, 'yo', markersize=8)
|
|
2872
|
+
self.ax.text(x2, y2+5, str(pair_idx),
|
|
2873
|
+
color='white', ha='center', va='bottom')
|
|
2874
|
+
|
|
2875
|
+
# Draw line if both points are on current slice
|
|
2876
|
+
if z1 == z2 == self.current_slice:
|
|
2877
|
+
self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
|
|
2269
2878
|
|
|
2270
|
-
|
|
2879
|
+
self.canvas.draw()
|
|
2271
2880
|
|
|
2272
2881
|
def show_netshow_dialog(self):
|
|
2273
2882
|
dialog = NetShowDialog(self)
|
|
@@ -2277,6 +2886,23 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2277
2886
|
dialog = PartitionDialog(self)
|
|
2278
2887
|
dialog.exec()
|
|
2279
2888
|
|
|
2889
|
+
def show_radial_dialog(self):
|
|
2890
|
+
dialog = RadialDialog(self)
|
|
2891
|
+
dialog.exec()
|
|
2892
|
+
|
|
2893
|
+
def show_degree_dist_dialog(self):
|
|
2894
|
+
dialog = DegreeDistDialog(self)
|
|
2895
|
+
dialog.exec()
|
|
2896
|
+
|
|
2897
|
+
def show_neighbor_id_dialog(self):
|
|
2898
|
+
dialog = NeighborIdentityDialog(self)
|
|
2899
|
+
dialog.exec()
|
|
2900
|
+
|
|
2901
|
+
def show_random_dialog(self):
|
|
2902
|
+
dialog = RandomDialog(self)
|
|
2903
|
+
dialog.exec()
|
|
2904
|
+
|
|
2905
|
+
|
|
2280
2906
|
def show_interaction_dialog(self):
|
|
2281
2907
|
dialog = InteractionDialog(self)
|
|
2282
2908
|
dialog.exec()
|
|
@@ -2285,10 +2911,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2285
2911
|
dialog = DegreeDialog(self)
|
|
2286
2912
|
dialog.exec()
|
|
2287
2913
|
|
|
2914
|
+
|
|
2915
|
+
def show_hub_dialog(self):
|
|
2916
|
+
dialog = HubDialog(self)
|
|
2917
|
+
dialog.exec()
|
|
2918
|
+
|
|
2288
2919
|
def show_mother_dialog(self):
|
|
2289
2920
|
dialog = MotherDialog(self)
|
|
2290
2921
|
dialog.exec()
|
|
2291
2922
|
|
|
2923
|
+
def show_code_dialog(self, sort = 'Community'):
|
|
2924
|
+
dialog = CodeDialog(self, sort = sort)
|
|
2925
|
+
dialog.exec()
|
|
2926
|
+
|
|
2292
2927
|
|
|
2293
2928
|
|
|
2294
2929
|
#TABLE RELATED:
|
|
@@ -3509,6 +4144,9 @@ class NetShowDialog(QDialog):
|
|
|
3509
4144
|
def show_network(self):
|
|
3510
4145
|
# Get parameters and run analysis
|
|
3511
4146
|
geo = self.geo_layout.isChecked()
|
|
4147
|
+
if geo:
|
|
4148
|
+
if my_network.node_centroids is None:
|
|
4149
|
+
self.parent().show_centroid_dialog()
|
|
3512
4150
|
accepted_mode = self.mode_selector.currentIndex() # Convert to 1-based index
|
|
3513
4151
|
# Get directory (None if empty)
|
|
3514
4152
|
directory = self.directory.text() if self.directory.text() else None
|
|
@@ -3533,6 +4171,8 @@ class NetShowDialog(QDialog):
|
|
|
3533
4171
|
self.accept()
|
|
3534
4172
|
except Exception as e:
|
|
3535
4173
|
print(f"Error showing network: {e}")
|
|
4174
|
+
import traceback
|
|
4175
|
+
print(traceback.format_exc())
|
|
3536
4176
|
|
|
3537
4177
|
class PartitionDialog(QDialog):
|
|
3538
4178
|
def __init__(self, parent=None):
|
|
@@ -3555,6 +4195,12 @@ class PartitionDialog(QDialog):
|
|
|
3555
4195
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
3556
4196
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
3557
4197
|
|
|
4198
|
+
# stats checkbox (default True)
|
|
4199
|
+
self.stats = QPushButton("Stats")
|
|
4200
|
+
self.stats.setCheckable(True)
|
|
4201
|
+
self.stats.setChecked(True)
|
|
4202
|
+
layout.addRow("Community Stats:", self.stats)
|
|
4203
|
+
|
|
3558
4204
|
# Add Run button
|
|
3559
4205
|
run_button = QPushButton("Partition")
|
|
3560
4206
|
run_button.clicked.connect(self.partition)
|
|
@@ -3564,18 +4210,204 @@ class PartitionDialog(QDialog):
|
|
|
3564
4210
|
|
|
3565
4211
|
accepted_mode = self.mode_selector.currentIndex()
|
|
3566
4212
|
weighted = self.weighted.isChecked()
|
|
4213
|
+
dostats = self.stats.isChecked()
|
|
4214
|
+
|
|
4215
|
+
my_network.communities = None
|
|
3567
4216
|
|
|
3568
4217
|
try:
|
|
3569
|
-
my_network.community_partition(weighted = weighted, style = accepted_mode)
|
|
4218
|
+
stats = my_network.community_partition(weighted = weighted, style = accepted_mode, dostats = dostats)
|
|
3570
4219
|
print(f"Discovered communities: {my_network.communities}")
|
|
3571
4220
|
|
|
3572
|
-
self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID')
|
|
4221
|
+
self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'CommunityID', title = 'Community Partition')
|
|
4222
|
+
|
|
4223
|
+
if len(stats.keys()) > 0:
|
|
4224
|
+
self.parent().format_for_upperright_table(stats, title = 'Community Stats')
|
|
3573
4225
|
|
|
3574
4226
|
self.accept()
|
|
3575
4227
|
|
|
3576
4228
|
except Exception as e:
|
|
3577
4229
|
print(f"Error creating communities: {e}")
|
|
3578
4230
|
|
|
4231
|
+
class RadialDialog(QDialog):
|
|
4232
|
+
|
|
4233
|
+
def __init__(self, parent=None):
|
|
4234
|
+
|
|
4235
|
+
super().__init__(parent)
|
|
4236
|
+
self.setWindowTitle("Radial Parameters")
|
|
4237
|
+
self.setModal(True)
|
|
4238
|
+
|
|
4239
|
+
layout = QFormLayout(self)
|
|
4240
|
+
|
|
4241
|
+
self.distance = QLineEdit("50")
|
|
4242
|
+
layout.addRow("Bucket Distance for Searching For Node Neighbors (automatically scaled by xy and z scales):", self.distance)
|
|
4243
|
+
|
|
4244
|
+
self.directory = QLineEdit("")
|
|
4245
|
+
layout.addRow("Output Directory:", self.directory)
|
|
4246
|
+
|
|
4247
|
+
# Add Run button
|
|
4248
|
+
run_button = QPushButton("Get Radial Distribution")
|
|
4249
|
+
run_button.clicked.connect(self.radial)
|
|
4250
|
+
layout.addWidget(run_button)
|
|
4251
|
+
|
|
4252
|
+
def radial(self):
|
|
4253
|
+
|
|
4254
|
+
distance = float(self.distance.text()) if self.distance.text().strip() else 50
|
|
4255
|
+
|
|
4256
|
+
directory = str(self.distance.text()) if self.directory.text().strip() else None
|
|
4257
|
+
|
|
4258
|
+
if my_network.node_centroids is None:
|
|
4259
|
+
self.parent().show_centroid_dialog()
|
|
4260
|
+
|
|
4261
|
+
radial = my_network.radial_distribution(distance, directory = directory)
|
|
4262
|
+
|
|
4263
|
+
self.parent().format_for_upperright_table(radial, 'Radial Distance From Any Node', 'Average Number of Neighboring Nodes', title = 'Radial Distribution Analysis')
|
|
4264
|
+
|
|
4265
|
+
self.accept()
|
|
4266
|
+
|
|
4267
|
+
class DegreeDistDialog(QDialog):
|
|
4268
|
+
|
|
4269
|
+
def __init__(self, parent=None):
|
|
4270
|
+
|
|
4271
|
+
super().__init__(parent)
|
|
4272
|
+
self.setWindowTitle("Degree Distribution Parameters")
|
|
4273
|
+
self.setModal(True)
|
|
4274
|
+
|
|
4275
|
+
layout = QFormLayout(self)
|
|
4276
|
+
|
|
4277
|
+
self.directory = QLineEdit("")
|
|
4278
|
+
layout.addRow("Output Directory:", self.directory)
|
|
4279
|
+
|
|
4280
|
+
# Add Run button
|
|
4281
|
+
run_button = QPushButton("Get Degree Distribution")
|
|
4282
|
+
run_button.clicked.connect(self.degreedist)
|
|
4283
|
+
layout.addWidget(run_button)
|
|
4284
|
+
|
|
4285
|
+
def degreedist(self):
|
|
4286
|
+
|
|
4287
|
+
try:
|
|
4288
|
+
|
|
4289
|
+
directory = str(self.distance.text()) if self.directory.text().strip() else None
|
|
4290
|
+
|
|
4291
|
+
degrees = my_network.degree_distribution(directory = directory)
|
|
4292
|
+
|
|
4293
|
+
|
|
4294
|
+
self.parent().format_for_upperright_table(degrees, 'Degree (k)', 'Proportion of nodes with degree (p(k))', title = 'Degree Distribution Analysis')
|
|
4295
|
+
|
|
4296
|
+
self.accept()
|
|
4297
|
+
|
|
4298
|
+
except Excpetion as e:
|
|
4299
|
+
print(f"An error occurred: {e}")
|
|
4300
|
+
|
|
4301
|
+
class NeighborIdentityDialog(QDialog):
|
|
4302
|
+
|
|
4303
|
+
def __init__(self, parent=None):
|
|
4304
|
+
|
|
4305
|
+
super().__init__(parent)
|
|
4306
|
+
self.setWindowTitle(f"Neighborhood Identity Distribution Parameters \n(Note - the same node is not included more than once as a neighbor even if it borders multiple nodes of the root ID)")
|
|
4307
|
+
self.setModal(True)
|
|
4308
|
+
|
|
4309
|
+
layout = QFormLayout(self)
|
|
4310
|
+
|
|
4311
|
+
if my_network.node_identities is not None:
|
|
4312
|
+
self.root = QComboBox()
|
|
4313
|
+
self.root.addItems(list(set(my_network.node_identities.values())))
|
|
4314
|
+
self.root.setCurrentIndex(0)
|
|
4315
|
+
layout.addRow("Root Identity to Search for Neighbor's IDs (search uses nodes of this ID, finds what IDs they connect to", self.root)
|
|
4316
|
+
else:
|
|
4317
|
+
self.root = None
|
|
4318
|
+
|
|
4319
|
+
self.directory = QLineEdit("")
|
|
4320
|
+
layout.addRow("Output Directory:", self.directory)
|
|
4321
|
+
|
|
4322
|
+
self.mode = QComboBox()
|
|
4323
|
+
self.mode.addItems(["From Network - Based on Absolute Connectivity", "Use Labeled Nodes - Based on Morphological Neighborhood Densities"])
|
|
4324
|
+
self.mode.setCurrentIndex(0)
|
|
4325
|
+
layout.addRow("Mode", self.mode)
|
|
4326
|
+
|
|
4327
|
+
self.search = QLineEdit("")
|
|
4328
|
+
layout.addRow("Search Radius (Ignore if using network):", self.search)
|
|
4329
|
+
|
|
4330
|
+
# Add Run button
|
|
4331
|
+
run_button = QPushButton("Get Neighborhood Identity Distribution")
|
|
4332
|
+
run_button.clicked.connect(self.neighborids)
|
|
4333
|
+
layout.addWidget(run_button)
|
|
4334
|
+
|
|
4335
|
+
def neighborids(self):
|
|
4336
|
+
|
|
4337
|
+
try:
|
|
4338
|
+
|
|
4339
|
+
try:
|
|
4340
|
+
root = self.root.currentText()
|
|
4341
|
+
except:
|
|
4342
|
+
pass
|
|
4343
|
+
|
|
4344
|
+
directory = self.directory.text() if self.directory.text().strip() else None
|
|
4345
|
+
|
|
4346
|
+
mode = self.mode.currentIndex()
|
|
4347
|
+
|
|
4348
|
+
search = float(self.search.text()) if self.search.text().strip() else 0
|
|
4349
|
+
|
|
4350
|
+
|
|
4351
|
+
result, result2, title1, title2, densities = my_network.neighborhood_identities(root = root, directory = directory, mode = mode, search = search)
|
|
4352
|
+
|
|
4353
|
+
self.parent().format_for_upperright_table(result, 'Node Identity', 'Amount', title = title1)
|
|
4354
|
+
self.parent().format_for_upperright_table(result2, 'Node Identity', 'Proportion', title = title2)
|
|
4355
|
+
|
|
4356
|
+
if mode == 1:
|
|
4357
|
+
|
|
4358
|
+
self.parent().format_for_upperright_table(densities, 'Node Identity', 'Density in search/density total', title = f'Clustering Factor of Node Identities with {search} from nodes {root}')
|
|
4359
|
+
|
|
4360
|
+
|
|
4361
|
+
self.accept()
|
|
4362
|
+
except Exception as e:
|
|
4363
|
+
print(f"Error: {e}")
|
|
4364
|
+
|
|
4365
|
+
|
|
4366
|
+
|
|
4367
|
+
|
|
4368
|
+
|
|
4369
|
+
|
|
4370
|
+
|
|
4371
|
+
|
|
4372
|
+
class RandomDialog(QDialog):
|
|
4373
|
+
|
|
4374
|
+
def __init__(self, parent=None):
|
|
4375
|
+
|
|
4376
|
+
super().__init__(parent)
|
|
4377
|
+
self.setWindowTitle("Degree Distribution Parameters")
|
|
4378
|
+
self.setModal(True)
|
|
4379
|
+
|
|
4380
|
+
layout = QFormLayout(self)
|
|
4381
|
+
|
|
4382
|
+
|
|
4383
|
+
# stats checkbox (default True)
|
|
4384
|
+
self.weighted = QPushButton("weighted")
|
|
4385
|
+
self.weighted.setCheckable(True)
|
|
4386
|
+
self.weighted.setChecked(True)
|
|
4387
|
+
layout.addRow("Allow Random Network to be weighted? (Whether or not edges can be repeatedly assigned between the same set of nodes to increase their weights, or if they must always find a new partner):", self.weighted)
|
|
4388
|
+
|
|
4389
|
+
|
|
4390
|
+
# Add Run button
|
|
4391
|
+
run_button = QPushButton("Get Random Network (Will go in Selection Table)")
|
|
4392
|
+
run_button.clicked.connect(self.random)
|
|
4393
|
+
layout.addWidget(run_button)
|
|
4394
|
+
|
|
4395
|
+
def random(self):
|
|
4396
|
+
|
|
4397
|
+
weighted = self.weighted.isChecked()
|
|
4398
|
+
|
|
4399
|
+
_, df = my_network.assign_random(weighted = weighted)
|
|
4400
|
+
|
|
4401
|
+
# Create new model with filtered DataFrame and update selection table
|
|
4402
|
+
new_model = PandasModel(df)
|
|
4403
|
+
self.parent().selection_table.setModel(new_model)
|
|
4404
|
+
|
|
4405
|
+
# Switch to selection table
|
|
4406
|
+
self.parent().selection_button.click()
|
|
4407
|
+
|
|
4408
|
+
self.accept()
|
|
4409
|
+
|
|
4410
|
+
|
|
3579
4411
|
|
|
3580
4412
|
class InteractionDialog(QDialog):
|
|
3581
4413
|
|
|
@@ -3645,6 +4477,9 @@ class DegreeDialog(QDialog):
|
|
|
3645
4477
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
3646
4478
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
3647
4479
|
|
|
4480
|
+
self.mask_limiter = QLineEdit("1")
|
|
4481
|
+
layout.addRow("Masks smaller high degree proportion of nodes (ignore if only returning degrees)", self.mask_limiter)
|
|
4482
|
+
|
|
3648
4483
|
self.down_factor = QLineEdit("1")
|
|
3649
4484
|
layout.addRow("down_factor (for speeding up overlay generation - ignore if only returning degrees:", self.down_factor)
|
|
3650
4485
|
|
|
@@ -3664,6 +4499,11 @@ class DegreeDialog(QDialog):
|
|
|
3664
4499
|
except ValueError:
|
|
3665
4500
|
down_factor = 1
|
|
3666
4501
|
|
|
4502
|
+
try:
|
|
4503
|
+
mask_limiter = float(self.mask_limiter.text()) if self.mask_limiter.text() else 1
|
|
4504
|
+
except ValueError:
|
|
4505
|
+
mask_limiter = 1
|
|
4506
|
+
|
|
3667
4507
|
if self.parent().active_channel == 1:
|
|
3668
4508
|
active_data = self.parent().channel_data[0]
|
|
3669
4509
|
else:
|
|
@@ -3680,9 +4520,47 @@ class DegreeDialog(QDialog):
|
|
|
3680
4520
|
|
|
3681
4521
|
original_shape = copy.deepcopy(active_data.shape)
|
|
3682
4522
|
|
|
3683
|
-
temp_network = n3d.Network_3D(nodes = active_data, node_centroids = my_network.node_centroids, network = my_network.network, network_lists = my_network.network_lists)
|
|
3684
4523
|
|
|
3685
|
-
|
|
4524
|
+
if mask_limiter < 1 and accepted_mode != 0:
|
|
4525
|
+
|
|
4526
|
+
if len(np.unique(active_data)) < 3:
|
|
4527
|
+
active_data, _ = n3d.label_objects(active_data)
|
|
4528
|
+
|
|
4529
|
+
node_list = list(my_network.network.nodes)
|
|
4530
|
+
node_dict = {}
|
|
4531
|
+
|
|
4532
|
+
for node in node_list:
|
|
4533
|
+
node_dict[node] = (my_network.network.degree(node))
|
|
4534
|
+
|
|
4535
|
+
# Calculate the number of top proportion% entries
|
|
4536
|
+
num_items = len(node_dict)
|
|
4537
|
+
num_top_10_percent = max(1, int(num_items * mask_limiter)) # Ensure at least one item
|
|
4538
|
+
|
|
4539
|
+
# Sort the dictionary by values in descending order and get the top 10%
|
|
4540
|
+
sorted_items = sorted(node_dict.items(), key=lambda item: item[1], reverse=True)
|
|
4541
|
+
top_10_percent_items = sorted_items[:num_top_10_percent]
|
|
4542
|
+
|
|
4543
|
+
# Extract the keys from the top proportion% items
|
|
4544
|
+
top_10_percent_keys = [key for key, value in top_10_percent_items]
|
|
4545
|
+
|
|
4546
|
+
mask = np.isin(active_data, top_10_percent_keys)
|
|
4547
|
+
nodes = mask * active_data
|
|
4548
|
+
new_centroids = {}
|
|
4549
|
+
for node in my_network.node_centroids:
|
|
4550
|
+
if node in top_10_percent_keys:
|
|
4551
|
+
new_centroids[node] = my_network.node_centroids[node]
|
|
4552
|
+
del mask
|
|
4553
|
+
|
|
4554
|
+
temp_network = n3d.Network_3D(nodes = nodes, node_centroids = new_centroids, network = my_network.network, network_lists = my_network.network_lists)
|
|
4555
|
+
|
|
4556
|
+
result, nodes = temp_network.get_degrees(called = True, no_img = accepted_mode, down_factor = down_factor)
|
|
4557
|
+
|
|
4558
|
+
else:
|
|
4559
|
+
temp_network = n3d.Network_3D(nodes = active_data, node_centroids = my_network.node_centroids, network = my_network.network, network_lists = my_network.network_lists)
|
|
4560
|
+
|
|
4561
|
+
result, nodes = temp_network.get_degrees(called = True, no_img = accepted_mode, down_factor = down_factor)
|
|
4562
|
+
|
|
4563
|
+
|
|
3686
4564
|
|
|
3687
4565
|
self.parent().format_for_upperright_table(result, 'Node ID', 'Degree', title = 'Degrees of nodes')
|
|
3688
4566
|
|
|
@@ -3698,9 +4576,75 @@ class DegreeDialog(QDialog):
|
|
|
3698
4576
|
|
|
3699
4577
|
except Exception as e:
|
|
3700
4578
|
|
|
4579
|
+
import traceback
|
|
4580
|
+
print(traceback.format_exc())
|
|
4581
|
+
|
|
3701
4582
|
print(f"Error finding degrees: {e}")
|
|
3702
4583
|
|
|
3703
4584
|
|
|
4585
|
+
class HubDialog(QDialog):
|
|
4586
|
+
|
|
4587
|
+
def __init__(self, parent=None):
|
|
4588
|
+
|
|
4589
|
+
super().__init__(parent)
|
|
4590
|
+
self.setWindowTitle("Hub Parameters")
|
|
4591
|
+
self.setModal(True)
|
|
4592
|
+
|
|
4593
|
+
layout = QFormLayout(self)
|
|
4594
|
+
|
|
4595
|
+
layout.addRow("Note:", QLabel(f"Finds hubs, which are nodes in the network that have the shortest number of steps to the other nodes\nWe can draw optional overlays to Overlay 2 as described below:"))
|
|
4596
|
+
|
|
4597
|
+
# Overlay checkbox (default True)
|
|
4598
|
+
self.overlay = QPushButton("Overlay")
|
|
4599
|
+
self.overlay.setCheckable(True)
|
|
4600
|
+
self.overlay.setChecked(True)
|
|
4601
|
+
layout.addRow("Make Overlay?:", self.overlay)
|
|
4602
|
+
|
|
4603
|
+
|
|
4604
|
+
self.proportion = QLineEdit("0.15")
|
|
4605
|
+
layout.addRow("Proportion of most connected hubs to keep (1 would imply returning entire network)", self.proportion)
|
|
4606
|
+
|
|
4607
|
+
|
|
4608
|
+
# Add Run button
|
|
4609
|
+
run_button = QPushButton("Get hubs")
|
|
4610
|
+
run_button.clicked.connect(self.hubs)
|
|
4611
|
+
layout.addWidget(run_button)
|
|
4612
|
+
|
|
4613
|
+
def hubs(self):
|
|
4614
|
+
|
|
4615
|
+
try:
|
|
4616
|
+
|
|
4617
|
+
try:
|
|
4618
|
+
proportion = float(self.proportion.text()) if self.proportion.text() else 1
|
|
4619
|
+
except ValueError:
|
|
4620
|
+
proportion = 1
|
|
4621
|
+
|
|
4622
|
+
overlay = self.overlay.isChecked()
|
|
4623
|
+
|
|
4624
|
+
result, img = my_network.isolate_hubs(proportion = proportion, retimg = overlay)
|
|
4625
|
+
|
|
4626
|
+
hub_dict = {}
|
|
4627
|
+
|
|
4628
|
+
for node in result:
|
|
4629
|
+
hub_dict[node] = my_network.network.degree(node)
|
|
4630
|
+
|
|
4631
|
+
self.parent().format_for_upperright_table(hub_dict, 'NodeID', 'Degree', title = f'Upper {proportion} Hub Nodes')
|
|
4632
|
+
|
|
4633
|
+
if img is not None:
|
|
4634
|
+
|
|
4635
|
+
self.parent().load_channel(3, channel_data = img, data = True)
|
|
4636
|
+
|
|
4637
|
+
|
|
4638
|
+
self.accept()
|
|
4639
|
+
|
|
4640
|
+
except Exception as e:
|
|
4641
|
+
|
|
4642
|
+
import traceback
|
|
4643
|
+
print(traceback.format_exc())
|
|
4644
|
+
|
|
4645
|
+
print(f"Error finding hubs: {e}")
|
|
4646
|
+
|
|
4647
|
+
|
|
3704
4648
|
|
|
3705
4649
|
class MotherDialog(QDialog):
|
|
3706
4650
|
|
|
@@ -3749,7 +4693,12 @@ class MotherDialog(QDialog):
|
|
|
3749
4693
|
G, result = my_network.isolate_mothers(self, louvain = my_network.communities, ret_nodes = False, called = True)
|
|
3750
4694
|
self.parent().load_channel(2, channel_data = result, data = True)
|
|
3751
4695
|
|
|
3752
|
-
|
|
4696
|
+
degree_dict = {}
|
|
4697
|
+
|
|
4698
|
+
for node in G.nodes():
|
|
4699
|
+
degree_dict[node] = my_network.network.degree(node)
|
|
4700
|
+
|
|
4701
|
+
self.parent().format_for_upperright_table(degree_dict, 'Mother ID', 'Degree', title = 'Mother Nodes')
|
|
3753
4702
|
|
|
3754
4703
|
|
|
3755
4704
|
self.accept()
|
|
@@ -3759,11 +4708,78 @@ class MotherDialog(QDialog):
|
|
|
3759
4708
|
print(f"Error finding mothers: {e}")
|
|
3760
4709
|
|
|
3761
4710
|
|
|
4711
|
+
class CodeDialog(QDialog):
|
|
3762
4712
|
|
|
4713
|
+
def __init__(self, parent=None, sort = 'Community'):
|
|
3763
4714
|
|
|
4715
|
+
super().__init__(parent)
|
|
4716
|
+
self.setWindowTitle(f"{sort} Code Parameters (Will go to Overlay2)")
|
|
4717
|
+
self.setModal(True)
|
|
3764
4718
|
|
|
4719
|
+
layout = QFormLayout(self)
|
|
3765
4720
|
|
|
3766
|
-
|
|
4721
|
+
self.sort = sort
|
|
4722
|
+
|
|
4723
|
+
self.down_factor = QLineEdit("")
|
|
4724
|
+
layout.addRow("down_factor (for speeding up overlay generation - optional):", self.down_factor)
|
|
4725
|
+
|
|
4726
|
+
# Add mode selection dropdown
|
|
4727
|
+
self.mode_selector = QComboBox()
|
|
4728
|
+
self.mode_selector.addItems(["Color Coded", "Grayscale Coded"])
|
|
4729
|
+
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
4730
|
+
layout.addRow("Execution Mode:", self.mode_selector)
|
|
4731
|
+
|
|
4732
|
+
|
|
4733
|
+
# Add Run button
|
|
4734
|
+
run_button = QPushButton(f"{sort} Code")
|
|
4735
|
+
run_button.clicked.connect(self.code)
|
|
4736
|
+
layout.addWidget(run_button)
|
|
4737
|
+
|
|
4738
|
+
def code(self):
|
|
4739
|
+
|
|
4740
|
+
try:
|
|
4741
|
+
|
|
4742
|
+
mode = self.mode_selector.currentIndex()
|
|
4743
|
+
|
|
4744
|
+
down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
|
|
4745
|
+
|
|
4746
|
+
|
|
4747
|
+
if self.sort == 'Community':
|
|
4748
|
+
if my_network.communities is None:
|
|
4749
|
+
self.parent().show_partition_dialog()
|
|
4750
|
+
if my_network.communities is None:
|
|
4751
|
+
return
|
|
4752
|
+
elif my_network.node_identities is None:
|
|
4753
|
+
print("Node identities are not set")
|
|
4754
|
+
return
|
|
4755
|
+
|
|
4756
|
+
if self.sort == 'Community':
|
|
4757
|
+
if mode == 0:
|
|
4758
|
+
image, output = my_network.extract_communities(down_factor = down_factor)
|
|
4759
|
+
elif mode == 1:
|
|
4760
|
+
image, output = my_network.extract_communities(color_code = False, down_factor = down_factor)
|
|
4761
|
+
else:
|
|
4762
|
+
if mode == 0:
|
|
4763
|
+
image, output = my_network.extract_communities(down_factor = down_factor, identities = True)
|
|
4764
|
+
elif mode == 1:
|
|
4765
|
+
image, output = my_network.extract_communities(color_code = False, down_factor = down_factor, identities = True)
|
|
4766
|
+
|
|
4767
|
+
self.parent().format_for_upperright_table(output, f'{self.sort} Id', f'Encoding Val: {self.sort}', 'Legend')
|
|
4768
|
+
|
|
4769
|
+
|
|
4770
|
+
self.parent().load_channel(3, image, True)
|
|
4771
|
+
self.accept()
|
|
4772
|
+
|
|
4773
|
+
except Exception as e:
|
|
4774
|
+
print(f"An error has occurred: {e}")
|
|
4775
|
+
import traceback
|
|
4776
|
+
print(traceback.format_exc())
|
|
4777
|
+
|
|
4778
|
+
|
|
4779
|
+
|
|
4780
|
+
|
|
4781
|
+
|
|
4782
|
+
# PROCESS MENU RELATED:
|
|
3767
4783
|
|
|
3768
4784
|
|
|
3769
4785
|
class ResizeDialog(QDialog):
|
|
@@ -3790,7 +4806,11 @@ class ResizeDialog(QDialog):
|
|
|
3790
4806
|
self.cubic.setChecked(False)
|
|
3791
4807
|
layout.addRow("Use cubic algorithm:", self.cubic)
|
|
3792
4808
|
|
|
3793
|
-
|
|
4809
|
+
if self.parent().original_shape is not None:
|
|
4810
|
+
undo_button = QPushButton(f"Resample to original shape: {self.parent().original_shape}")
|
|
4811
|
+
undo_button.clicked.connect(lambda: self.run_resize(undo = True))
|
|
4812
|
+
layout.addRow(undo_button)
|
|
4813
|
+
|
|
3794
4814
|
run_button = QPushButton("Run Resize")
|
|
3795
4815
|
run_button.clicked.connect(self.run_resize)
|
|
3796
4816
|
layout.addRow(run_button)
|
|
@@ -3800,9 +4820,9 @@ class ResizeDialog(QDialog):
|
|
|
3800
4820
|
self.resize.clear()
|
|
3801
4821
|
self.zsize.setText("1")
|
|
3802
4822
|
self.xsize.setText("1")
|
|
3803
|
-
self.ysize.setText("1")
|
|
4823
|
+
self.ysize.setText("1")
|
|
3804
4824
|
|
|
3805
|
-
def run_resize(self):
|
|
4825
|
+
def run_resize(self, undo = False):
|
|
3806
4826
|
try:
|
|
3807
4827
|
# Get parameters
|
|
3808
4828
|
try:
|
|
@@ -3851,16 +4871,31 @@ class ResizeDialog(QDialog):
|
|
|
3851
4871
|
self.parent().slice_slider.setValue(0)
|
|
3852
4872
|
self.parent().current_slice = 0
|
|
3853
4873
|
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
4874
|
+
if not undo:
|
|
4875
|
+
# Process each channel
|
|
4876
|
+
for channel in range(4):
|
|
4877
|
+
if self.parent().channel_data[channel] is not None:
|
|
4878
|
+
resized_data = n3d.resize(self.parent().channel_data[channel], resize, order)
|
|
4879
|
+
self.parent().load_channel(channel, channel_data=resized_data, data=True, assign_shape = False)
|
|
4880
|
+
|
|
4881
|
+
|
|
4882
|
+
# Process highlight overlay if it exists
|
|
4883
|
+
if self.parent().highlight_overlay is not None:
|
|
4884
|
+
self.parent().highlight_overlay = n3d.resize(self.parent().highlight_overlay, resize, order)
|
|
4885
|
+
else:
|
|
4886
|
+
# Process each channel
|
|
4887
|
+
if array_shape == self.parent().original_shape:
|
|
4888
|
+
return
|
|
4889
|
+
for channel in range(4):
|
|
4890
|
+
if self.parent().channel_data[channel] is not None:
|
|
4891
|
+
resized_data = n3d.upsample_with_padding(self.parent().channel_data[channel], original_shape = self.parent().original_shape)
|
|
4892
|
+
self.parent().load_channel(channel, channel_data=resized_data, data=True, assign_shape = False)
|
|
4893
|
+
|
|
4894
|
+
|
|
4895
|
+
# Process highlight overlay if it exists
|
|
4896
|
+
if self.parent().highlight_overlay is not None:
|
|
4897
|
+
self.parent().highlight_overlay = n3d.upsample_with_padding(self.parent().highlight_overlay, original_shape = self.parent().original_shape)
|
|
3859
4898
|
|
|
3860
|
-
|
|
3861
|
-
# Process highlight overlay if it exists
|
|
3862
|
-
if self.parent().highlight_overlay is not None:
|
|
3863
|
-
self.parent().highlight_overlay = n3d.resize(self.parent().highlight_overlay, resize, order)
|
|
3864
4899
|
|
|
3865
4900
|
# Update slider range based on new z-dimension
|
|
3866
4901
|
for channel in self.parent().channel_data:
|
|
@@ -4043,6 +5078,152 @@ class LabelDialog(QDialog):
|
|
|
4043
5078
|
f"Error running label: {str(e)}"
|
|
4044
5079
|
)
|
|
4045
5080
|
|
|
5081
|
+
class ThresholdWindow(QMainWindow):
|
|
5082
|
+
def __init__(self, parent=None):
|
|
5083
|
+
super().__init__(parent)
|
|
5084
|
+
self.setWindowTitle("Threshold Params (Active Image)")
|
|
5085
|
+
|
|
5086
|
+
# Create central widget and layout
|
|
5087
|
+
central_widget = QWidget()
|
|
5088
|
+
self.setCentralWidget(central_widget)
|
|
5089
|
+
layout = QFormLayout(central_widget)
|
|
5090
|
+
|
|
5091
|
+
self.min = QLineEdit("")
|
|
5092
|
+
layout.addRow("Minimum Value to retain:", self.min)
|
|
5093
|
+
|
|
5094
|
+
# Create widgets
|
|
5095
|
+
self.max = QLineEdit("")
|
|
5096
|
+
layout.addRow("Maximum Value to retain:", self.max)
|
|
5097
|
+
|
|
5098
|
+
# Add mode selection dropdown
|
|
5099
|
+
self.mode_selector = QComboBox()
|
|
5100
|
+
self.mode_selector.addItems(["Using Volumes", "Using Label/Brightness"])
|
|
5101
|
+
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
5102
|
+
layout.addRow("Execution Mode:", self.mode_selector)
|
|
5103
|
+
|
|
5104
|
+
# Add Run button
|
|
5105
|
+
prev_button = QPushButton("Preview")
|
|
5106
|
+
prev_button.clicked.connect(self.run_preview)
|
|
5107
|
+
layout.addRow(prev_button)
|
|
5108
|
+
|
|
5109
|
+
# Add Run button
|
|
5110
|
+
run_button = QPushButton("Apply Threshold")
|
|
5111
|
+
run_button.clicked.connect(self.thresh)
|
|
5112
|
+
layout.addRow(run_button)
|
|
5113
|
+
|
|
5114
|
+
# Set a reasonable default size
|
|
5115
|
+
self.setMinimumWidth(300)
|
|
5116
|
+
|
|
5117
|
+
def run_preview(self):
|
|
5118
|
+
|
|
5119
|
+
def get_valid_float(text, default_value):
|
|
5120
|
+
try:
|
|
5121
|
+
return float(text) if text.strip() else default_value
|
|
5122
|
+
except ValueError:
|
|
5123
|
+
print(f"Invalid input: {text}")
|
|
5124
|
+
return default_value
|
|
5125
|
+
|
|
5126
|
+
try:
|
|
5127
|
+
channel = self.parent().active_channel
|
|
5128
|
+
accepted_mode = self.mode_selector.currentIndex()
|
|
5129
|
+
|
|
5130
|
+
if accepted_mode == 0:
|
|
5131
|
+
if len(np.unique(self.parent().channel_data[self.parent().active_channel])) < 3:
|
|
5132
|
+
self.parent().show_label_dialog()
|
|
5133
|
+
|
|
5134
|
+
if self.parent().volume_dict[channel] is None:
|
|
5135
|
+
self.parent().volumes()
|
|
5136
|
+
|
|
5137
|
+
volumes = self.parent().volume_dict[channel]
|
|
5138
|
+
default_max = max(volumes.values())
|
|
5139
|
+
default_min = min(volumes.values())
|
|
5140
|
+
|
|
5141
|
+
max_val = get_valid_float(self.max.text(), default_max)
|
|
5142
|
+
min_val = get_valid_float(self.min.text(), default_min)
|
|
5143
|
+
|
|
5144
|
+
valid_indices = [item for item in volumes
|
|
5145
|
+
if min_val <= volumes[item] <= max_val]
|
|
5146
|
+
|
|
5147
|
+
elif accepted_mode == 1:
|
|
5148
|
+
channel_data = self.parent().channel_data[self.parent().active_channel]
|
|
5149
|
+
default_max = np.max(channel_data)
|
|
5150
|
+
default_min = np.min(channel_data)
|
|
5151
|
+
|
|
5152
|
+
max_val = int(get_valid_float(self.max.text(), default_max))
|
|
5153
|
+
min_val = int(get_valid_float(self.min.text(), default_min))
|
|
5154
|
+
|
|
5155
|
+
if min_val > max_val:
|
|
5156
|
+
min_val, max_val = max_val, min_val
|
|
5157
|
+
|
|
5158
|
+
valid_indices = list(range(min_val, max_val + 1))
|
|
5159
|
+
|
|
5160
|
+
if channel == 0:
|
|
5161
|
+
self.parent().create_highlight_overlay(node_indices = valid_indices)
|
|
5162
|
+
elif channel == 1:
|
|
5163
|
+
self.parent().create_highlight_overlay(edge_indices = valid_indices)
|
|
5164
|
+
elif channel == 2:
|
|
5165
|
+
self.parent().create_highlight_overlay(overlay1_indices = valid_indices)
|
|
5166
|
+
elif channel == 3:
|
|
5167
|
+
self.parent().create_highlight_overlay(overlay2_indices = valid_indices)
|
|
5168
|
+
|
|
5169
|
+
except Exception as e:
|
|
5170
|
+
print(f"Error showing preview: {e}")
|
|
5171
|
+
|
|
5172
|
+
def thresh(self):
|
|
5173
|
+
try:
|
|
5174
|
+
|
|
5175
|
+
self.run_preview()
|
|
5176
|
+
channel_data = self.parent().channel_data[self.parent().active_channel]
|
|
5177
|
+
mask = self.parent().highlight_overlay > 0
|
|
5178
|
+
channel_data = channel_data * mask
|
|
5179
|
+
self.parent().load_channel(self.parent().active_channel, channel_data, True)
|
|
5180
|
+
self.parent().update_display()
|
|
5181
|
+
self.close()
|
|
5182
|
+
|
|
5183
|
+
except Exception as e:
|
|
5184
|
+
QMessageBox.critical(
|
|
5185
|
+
self,
|
|
5186
|
+
"Error",
|
|
5187
|
+
f"Error running threshold: {str(e)}"
|
|
5188
|
+
)
|
|
5189
|
+
|
|
5190
|
+
|
|
5191
|
+
class SmartDilateDialog(QDialog):
|
|
5192
|
+
def __init__(self, parent, params):
|
|
5193
|
+
super().__init__(parent)
|
|
5194
|
+
self.setWindowTitle("Additional Smart Dilate Parameters")
|
|
5195
|
+
self.setModal(True)
|
|
5196
|
+
|
|
5197
|
+
layout = QFormLayout(self)
|
|
5198
|
+
|
|
5199
|
+
# GPU checkbox (default True)
|
|
5200
|
+
self.GPU = QPushButton("GPU")
|
|
5201
|
+
self.GPU.setCheckable(True)
|
|
5202
|
+
self.GPU.setChecked(True)
|
|
5203
|
+
layout.addRow("Use GPU:", self.GPU)
|
|
5204
|
+
|
|
5205
|
+
self.down_factor = QLineEdit("")
|
|
5206
|
+
layout.addRow("Internal Downsample for GPU (if needed):", self.down_factor)
|
|
5207
|
+
|
|
5208
|
+
self.params = params
|
|
5209
|
+
|
|
5210
|
+
# Add Run button
|
|
5211
|
+
run_button = QPushButton("Dilate")
|
|
5212
|
+
run_button.clicked.connect(self.smart_dilate)
|
|
5213
|
+
layout.addRow(run_button)
|
|
5214
|
+
|
|
5215
|
+
def smart_dilate(self):
|
|
5216
|
+
|
|
5217
|
+
GPU = self.GPU.isChecked()
|
|
5218
|
+
down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
|
|
5219
|
+
active_data, amount, xy_scale, z_scale = self.params
|
|
5220
|
+
|
|
5221
|
+
dilate_xy, dilate_z = n3d.dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
|
|
5222
|
+
|
|
5223
|
+
result = sdl.smart_dilate(active_data, dilate_xy, dilate_z, GPU = GPU, predownsample = down_factor)
|
|
5224
|
+
|
|
5225
|
+
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
5226
|
+
self.accept()
|
|
4046
5227
|
|
|
4047
5228
|
|
|
4048
5229
|
|
|
@@ -4073,6 +5254,12 @@ class DilateDialog(QDialog):
|
|
|
4073
5254
|
self.z_scale = QLineEdit(z_scale)
|
|
4074
5255
|
layout.addRow("z_scale:", self.z_scale)
|
|
4075
5256
|
|
|
5257
|
+
# Add mode selection dropdown
|
|
5258
|
+
self.mode_selector = QComboBox()
|
|
5259
|
+
self.mode_selector.addItems(["Binary Dilation", "Preserve Labels (slower)", "Recursive Binary Dilation (Use if the dilation radius is much larger than your objects)"])
|
|
5260
|
+
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
5261
|
+
layout.addRow("Execution Mode:", self.mode_selector)
|
|
5262
|
+
|
|
4076
5263
|
# Add Run button
|
|
4077
5264
|
run_button = QPushButton("Run Dilate")
|
|
4078
5265
|
run_button.clicked.connect(self.run_dilate)
|
|
@@ -4080,6 +5267,8 @@ class DilateDialog(QDialog):
|
|
|
4080
5267
|
|
|
4081
5268
|
def run_dilate(self):
|
|
4082
5269
|
try:
|
|
5270
|
+
|
|
5271
|
+
accepted_mode = self.mode_selector.currentIndex()
|
|
4083
5272
|
|
|
4084
5273
|
# Get amount
|
|
4085
5274
|
try:
|
|
@@ -4101,13 +5290,104 @@ class DilateDialog(QDialog):
|
|
|
4101
5290
|
active_data = self.parent().channel_data[self.parent().active_channel]
|
|
4102
5291
|
if active_data is None:
|
|
4103
5292
|
raise ValueError("No active image selected")
|
|
4104
|
-
|
|
5293
|
+
|
|
5294
|
+
if accepted_mode == 1:
|
|
5295
|
+
dialog = SmartDilateDialog(self.parent(), [active_data, amount, xy_scale, z_scale])
|
|
5296
|
+
dialog.exec()
|
|
5297
|
+
self.accept()
|
|
5298
|
+
return
|
|
5299
|
+
|
|
5300
|
+
if accepted_mode == 2:
|
|
5301
|
+
recursive = True
|
|
5302
|
+
else:
|
|
5303
|
+
recursive = False
|
|
5304
|
+
|
|
4105
5305
|
# Call dilate method with parameters
|
|
4106
5306
|
result = n3d.dilate(
|
|
4107
5307
|
active_data,
|
|
4108
5308
|
amount,
|
|
4109
5309
|
xy_scale = xy_scale,
|
|
4110
5310
|
z_scale = z_scale,
|
|
5311
|
+
recursive = recursive
|
|
5312
|
+
)
|
|
5313
|
+
|
|
5314
|
+
# Update both the display data and the network object
|
|
5315
|
+
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
5316
|
+
|
|
5317
|
+
self.parent().update_display()
|
|
5318
|
+
self.accept()
|
|
5319
|
+
|
|
5320
|
+
except Exception as e:
|
|
5321
|
+
import traceback
|
|
5322
|
+
print(traceback.format_exc())
|
|
5323
|
+
QMessageBox.critical(
|
|
5324
|
+
self,
|
|
5325
|
+
"Error",
|
|
5326
|
+
f"Error running dilate: {str(e)}"
|
|
5327
|
+
)
|
|
5328
|
+
|
|
5329
|
+
class ErodeDialog(QDialog):
|
|
5330
|
+
def __init__(self, parent=None):
|
|
5331
|
+
super().__init__(parent)
|
|
5332
|
+
self.setWindowTitle("Erosion Parameters")
|
|
5333
|
+
self.setModal(True)
|
|
5334
|
+
|
|
5335
|
+
layout = QFormLayout(self)
|
|
5336
|
+
|
|
5337
|
+
self.amount = QLineEdit("1")
|
|
5338
|
+
layout.addRow("Erosion Radius:", self.amount)
|
|
5339
|
+
|
|
5340
|
+
if my_network.xy_scale is not None:
|
|
5341
|
+
xy_scale = f"{my_network.xy_scale}"
|
|
5342
|
+
else:
|
|
5343
|
+
xy_scale = "1"
|
|
5344
|
+
|
|
5345
|
+
self.xy_scale = QLineEdit(xy_scale)
|
|
5346
|
+
layout.addRow("xy_scale:", self.xy_scale)
|
|
5347
|
+
|
|
5348
|
+
if my_network.z_scale is not None:
|
|
5349
|
+
z_scale = f"{my_network.z_scale}"
|
|
5350
|
+
else:
|
|
5351
|
+
z_scale = "1"
|
|
5352
|
+
|
|
5353
|
+
self.z_scale = QLineEdit(z_scale)
|
|
5354
|
+
layout.addRow("z_scale:", self.z_scale)
|
|
5355
|
+
|
|
5356
|
+
# Add Run button
|
|
5357
|
+
run_button = QPushButton("Run Erode")
|
|
5358
|
+
run_button.clicked.connect(self.run_erode)
|
|
5359
|
+
layout.addRow(run_button)
|
|
5360
|
+
|
|
5361
|
+
def run_erode(self):
|
|
5362
|
+
try:
|
|
5363
|
+
|
|
5364
|
+
# Get amount
|
|
5365
|
+
try:
|
|
5366
|
+
amount = float(self.amount.text()) if self.amount.text() else 1
|
|
5367
|
+
except ValueError:
|
|
5368
|
+
amount = 1
|
|
5369
|
+
|
|
5370
|
+
try:
|
|
5371
|
+
xy_scale = float(self.xy_scale.text()) if self.xy_scale.text() else 1
|
|
5372
|
+
except ValueError:
|
|
5373
|
+
xy_scale = 1
|
|
5374
|
+
|
|
5375
|
+
try:
|
|
5376
|
+
z_scale = float(self.z_scale.text()) if self.z_scale.text() else 1
|
|
5377
|
+
except ValueError:
|
|
5378
|
+
z_scale = 1
|
|
5379
|
+
|
|
5380
|
+
# Get the active channel data from parent
|
|
5381
|
+
active_data = self.parent().channel_data[self.parent().active_channel]
|
|
5382
|
+
if active_data is None:
|
|
5383
|
+
raise ValueError("No active image selected")
|
|
5384
|
+
|
|
5385
|
+
# Call dilate method with parameters
|
|
5386
|
+
result = n3d.erode(
|
|
5387
|
+
active_data,
|
|
5388
|
+
amount,
|
|
5389
|
+
xy_scale = xy_scale,
|
|
5390
|
+
z_scale = z_scale,
|
|
4111
5391
|
)
|
|
4112
5392
|
|
|
4113
5393
|
# Update both the display data and the network object
|
|
@@ -4124,7 +5404,46 @@ class DilateDialog(QDialog):
|
|
|
4124
5404
|
QMessageBox.critical(
|
|
4125
5405
|
self,
|
|
4126
5406
|
"Error",
|
|
4127
|
-
f"Error running
|
|
5407
|
+
f"Error running erode: {str(e)}"
|
|
5408
|
+
)
|
|
5409
|
+
|
|
5410
|
+
class HoleDialog(QDialog):
|
|
5411
|
+
def __init__(self, parent=None):
|
|
5412
|
+
super().__init__(parent)
|
|
5413
|
+
self.setWindowTitle("Fill Holes? (Active Image)")
|
|
5414
|
+
self.setModal(True)
|
|
5415
|
+
|
|
5416
|
+
layout = QFormLayout(self)
|
|
5417
|
+
|
|
5418
|
+
# Add Run button
|
|
5419
|
+
run_button = QPushButton("Run Fill Holes")
|
|
5420
|
+
run_button.clicked.connect(self.run_holes)
|
|
5421
|
+
layout.addRow(run_button)
|
|
5422
|
+
|
|
5423
|
+
def run_holes(self):
|
|
5424
|
+
try:
|
|
5425
|
+
|
|
5426
|
+
|
|
5427
|
+
# Get the active channel data from parent
|
|
5428
|
+
active_data = self.parent().channel_data[self.parent().active_channel]
|
|
5429
|
+
if active_data is None:
|
|
5430
|
+
raise ValueError("No active image selected")
|
|
5431
|
+
|
|
5432
|
+
# Call dilate method with parameters
|
|
5433
|
+
result = n3d.fill_holes_3d(
|
|
5434
|
+
active_data
|
|
5435
|
+
)
|
|
5436
|
+
|
|
5437
|
+
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
5438
|
+
|
|
5439
|
+
self.parent().update_display()
|
|
5440
|
+
self.accept()
|
|
5441
|
+
|
|
5442
|
+
except Exception as e:
|
|
5443
|
+
QMessageBox.critical(
|
|
5444
|
+
self,
|
|
5445
|
+
"Error",
|
|
5446
|
+
f"Error running fill holes: {str(e)}"
|
|
4128
5447
|
)
|
|
4129
5448
|
|
|
4130
5449
|
class MaskDialog(QDialog):
|
|
@@ -4372,6 +5691,40 @@ class WatershedDialog(QDialog):
|
|
|
4372
5691
|
f"Error running watershed: {str(e)}"
|
|
4373
5692
|
)
|
|
4374
5693
|
|
|
5694
|
+
class ZDialog(QDialog):
|
|
5695
|
+
|
|
5696
|
+
def __init__(self, parent=None):
|
|
5697
|
+
super().__init__(parent)
|
|
5698
|
+
self.setWindowTitle("Z Parameters (Save your network first - this will alter all channels into 2D versions)")
|
|
5699
|
+
self.setModal(True)
|
|
5700
|
+
|
|
5701
|
+
layout = QFormLayout(self)
|
|
5702
|
+
|
|
5703
|
+
# Add mode selection dropdown
|
|
5704
|
+
self.mode_selector = QComboBox()
|
|
5705
|
+
self.mode_selector.addItems(["max", "mean", "min", "sum", "std"])
|
|
5706
|
+
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
5707
|
+
layout.addRow("Execution Mode:", self.mode_selector)
|
|
5708
|
+
|
|
5709
|
+
# Add Run button
|
|
5710
|
+
run_button = QPushButton("Run Z Project")
|
|
5711
|
+
run_button.clicked.connect(self.run_z)
|
|
5712
|
+
layout.addRow(run_button)
|
|
5713
|
+
|
|
5714
|
+
def run_z(self):
|
|
5715
|
+
|
|
5716
|
+
mode = self.mode_selector.currentText()
|
|
5717
|
+
|
|
5718
|
+
for i in range(len(self.parent().channel_data)):
|
|
5719
|
+
try:
|
|
5720
|
+
self.parent().channel_data[i] = n3d.z_project(self.parent().channel_data[i], mode)
|
|
5721
|
+
self.parent().load_channel(i, self.parent().channel_data[i], True)
|
|
5722
|
+
except:
|
|
5723
|
+
pass
|
|
5724
|
+
|
|
5725
|
+
self.accept()
|
|
5726
|
+
|
|
5727
|
+
|
|
4375
5728
|
class CentroidNodeDialog(QDialog):
|
|
4376
5729
|
def __init__(self, parent=None):
|
|
4377
5730
|
super().__init__(parent)
|
|
@@ -4624,6 +5977,275 @@ class BranchDialog(QDialog):
|
|
|
4624
5977
|
|
|
4625
5978
|
|
|
4626
5979
|
|
|
5980
|
+
class IsolateDialog(QDialog):
|
|
5981
|
+
def __init__(self, parent=None):
|
|
5982
|
+
super().__init__(parent)
|
|
5983
|
+
self.setWindowTitle("Select Node types to isolate")
|
|
5984
|
+
self.setModal(True)
|
|
5985
|
+
layout = QFormLayout(self)
|
|
5986
|
+
|
|
5987
|
+
self.combo1 = QComboBox()
|
|
5988
|
+
self.combo1.addItems(list(set(my_network.node_identities.values())))
|
|
5989
|
+
self.combo1.setCurrentIndex(0)
|
|
5990
|
+
layout.addRow("ID 1:", self.combo1)
|
|
5991
|
+
|
|
5992
|
+
self.combo2 = QComboBox()
|
|
5993
|
+
self.combo2.addItems(list(set(my_network.node_identities.values())))
|
|
5994
|
+
self.combo2.setCurrentIndex(1)
|
|
5995
|
+
layout.addRow("ID 2:", self.combo2)
|
|
5996
|
+
|
|
5997
|
+
# Add submit button
|
|
5998
|
+
sub_button = QPushButton("Submit")
|
|
5999
|
+
sub_button.clicked.connect(self.submit_ids)
|
|
6000
|
+
layout.addRow(sub_button)
|
|
6001
|
+
|
|
6002
|
+
def submit_ids(self):
|
|
6003
|
+
try:
|
|
6004
|
+
id1 = self.combo1.currentText()
|
|
6005
|
+
id2 = self.combo2.currentText()
|
|
6006
|
+
if id1 == id2:
|
|
6007
|
+
print("Please select different identities")
|
|
6008
|
+
self.parent().show_isolate_dialog()
|
|
6009
|
+
return
|
|
6010
|
+
else:
|
|
6011
|
+
my_network.isolate_internode_connections(id1, id2)
|
|
6012
|
+
self.accept()
|
|
6013
|
+
except Exception as e:
|
|
6014
|
+
print(f"An error occurred: {e}")
|
|
6015
|
+
|
|
6016
|
+
class AlterDialog(QDialog):
|
|
6017
|
+
def __init__(self, parent=None):
|
|
6018
|
+
super().__init__(parent)
|
|
6019
|
+
self.setWindowTitle("Enter Node/Edge groups to add/remove")
|
|
6020
|
+
self.setModal(True)
|
|
6021
|
+
layout = QFormLayout(self)
|
|
6022
|
+
|
|
6023
|
+
# Node 1
|
|
6024
|
+
self.node1 = QLineEdit()
|
|
6025
|
+
self.node1.setPlaceholderText("Enter integer")
|
|
6026
|
+
layout.addRow("Node1:", self.node1)
|
|
6027
|
+
|
|
6028
|
+
# Node 2
|
|
6029
|
+
self.node2 = QLineEdit()
|
|
6030
|
+
self.node2.setPlaceholderText("Enter integer")
|
|
6031
|
+
layout.addRow("Node2:", self.node2)
|
|
6032
|
+
|
|
6033
|
+
# Edge
|
|
6034
|
+
self.edge = QLineEdit()
|
|
6035
|
+
self.edge.setPlaceholderText("Optional - Enter integer")
|
|
6036
|
+
layout.addRow("Edge:", self.edge)
|
|
6037
|
+
|
|
6038
|
+
# Add add button
|
|
6039
|
+
addbutton = QPushButton("Add pair")
|
|
6040
|
+
addbutton.clicked.connect(self.add)
|
|
6041
|
+
layout.addRow(addbutton)
|
|
6042
|
+
|
|
6043
|
+
# Add remove button
|
|
6044
|
+
removebutton = QPushButton("Remove pair")
|
|
6045
|
+
removebutton.clicked.connect(self.remove)
|
|
6046
|
+
layout.addRow(removebutton)
|
|
6047
|
+
|
|
6048
|
+
def add(self):
|
|
6049
|
+
try:
|
|
6050
|
+
node1 = int(self.node1.text()) if self.node1.text().strip() else None
|
|
6051
|
+
node2 = int(self.node2.text()) if self.node2.text().strip() else None
|
|
6052
|
+
edge = int(self.edge.text()) if self.edge.text().strip() else None
|
|
6053
|
+
|
|
6054
|
+
# Check if we have valid node pairs
|
|
6055
|
+
if node1 is not None and node2 is not None:
|
|
6056
|
+
# Add the node pair and its reverse
|
|
6057
|
+
my_network.network_lists[0].append(node1)
|
|
6058
|
+
my_network.network_lists[1].append(node2)
|
|
6059
|
+
# Add edge value (0 if none provided)
|
|
6060
|
+
my_network.network_lists[2].append(edge if edge is not None else 0)
|
|
6061
|
+
|
|
6062
|
+
# Add reverse pair with same edge value
|
|
6063
|
+
my_network.network_lists[0].append(node2)
|
|
6064
|
+
my_network.network_lists[1].append(node1)
|
|
6065
|
+
my_network.network_lists[2].append(edge if edge is not None else 0)
|
|
6066
|
+
try:
|
|
6067
|
+
if hasattr(my_network, 'network_lists'):
|
|
6068
|
+
model = PandasModel(my_network.network_lists)
|
|
6069
|
+
self.parent().network_table.setModel(model)
|
|
6070
|
+
# Adjust column widths to content
|
|
6071
|
+
for column in range(model.columnCount(None)):
|
|
6072
|
+
self.parent().network_table.resizeColumnToContents(column)
|
|
6073
|
+
except Exception as e:
|
|
6074
|
+
print(f"Error showing network table: {e}")
|
|
6075
|
+
except ValueError:
|
|
6076
|
+
import traceback
|
|
6077
|
+
print(traceback.format_exc())
|
|
6078
|
+
pass # Invalid input - do nothing
|
|
6079
|
+
|
|
6080
|
+
def remove(self):
|
|
6081
|
+
try:
|
|
6082
|
+
node1 = int(self.node1.text()) if self.node1.text().strip() else None
|
|
6083
|
+
node2 = int(self.node2.text()) if self.node2.text().strip() else None
|
|
6084
|
+
edge = int(self.edge.text()) if self.edge.text().strip() else None
|
|
6085
|
+
|
|
6086
|
+
# Check if we have valid node pairs
|
|
6087
|
+
if node1 is not None and node2 is not None:
|
|
6088
|
+
# Create lists for indices to remove
|
|
6089
|
+
indices_to_remove = []
|
|
6090
|
+
|
|
6091
|
+
# Loop through the lists to find matching pairs
|
|
6092
|
+
for i in range(len(my_network.network_lists[0])):
|
|
6093
|
+
forward_match = (my_network.network_lists[0][i] == node1 and
|
|
6094
|
+
my_network.network_lists[1][i] == node2)
|
|
6095
|
+
reverse_match = (my_network.network_lists[0][i] == node2 and
|
|
6096
|
+
my_network.network_lists[1][i] == node1)
|
|
6097
|
+
|
|
6098
|
+
if forward_match or reverse_match:
|
|
6099
|
+
# If edge value specified, only remove if edge matches
|
|
6100
|
+
if edge is not None:
|
|
6101
|
+
if my_network.network_lists[2][i] == edge:
|
|
6102
|
+
indices_to_remove.append(i)
|
|
6103
|
+
else:
|
|
6104
|
+
# If no edge specified, remove all matching pairs
|
|
6105
|
+
indices_to_remove.append(i)
|
|
6106
|
+
|
|
6107
|
+
# Remove elements in reverse order to maintain correct indices
|
|
6108
|
+
for i in sorted(indices_to_remove, reverse=True):
|
|
6109
|
+
my_network.network_lists[0].pop(i)
|
|
6110
|
+
my_network.network_lists[1].pop(i)
|
|
6111
|
+
my_network.network_lists[2].pop(i)
|
|
6112
|
+
|
|
6113
|
+
try:
|
|
6114
|
+
if hasattr(my_network, 'network_lists'):
|
|
6115
|
+
model = PandasModel(my_network.network_lists)
|
|
6116
|
+
self.parent().network_table.setModel(model)
|
|
6117
|
+
# Adjust column widths to content
|
|
6118
|
+
for column in range(model.columnCount(None)):
|
|
6119
|
+
self.parent().network_table.resizeColumnToContents(column)
|
|
6120
|
+
except Exception as e:
|
|
6121
|
+
print(f"Error showing network table: {e}")
|
|
6122
|
+
|
|
6123
|
+
except ValueError:
|
|
6124
|
+
import traceback
|
|
6125
|
+
print(traceback.format_exc())
|
|
6126
|
+
pass # Invalid input - do nothing
|
|
6127
|
+
|
|
6128
|
+
|
|
6129
|
+
class ModifyDialog(QDialog):
|
|
6130
|
+
def __init__(self, parent=None):
|
|
6131
|
+
super().__init__(parent)
|
|
6132
|
+
self.setWindowTitle("Create Nodes from Edge Vertices")
|
|
6133
|
+
self.setModal(True)
|
|
6134
|
+
layout = QFormLayout(self)
|
|
6135
|
+
|
|
6136
|
+
# trunk checkbox (default false)
|
|
6137
|
+
self.trunk = QPushButton("Remove Trunk")
|
|
6138
|
+
self.trunk.setCheckable(True)
|
|
6139
|
+
self.trunk.setChecked(False)
|
|
6140
|
+
layout.addRow("Remove Trunk? (Most connected edge - overrides below):", self.trunk)
|
|
6141
|
+
|
|
6142
|
+
# trunk checkbox (default false)
|
|
6143
|
+
self.trunknode = QPushButton("Trunk -> Node")
|
|
6144
|
+
self.trunknode.setCheckable(True)
|
|
6145
|
+
self.trunknode.setChecked(False)
|
|
6146
|
+
layout.addRow("Convert Trunk to Node? (Most connected edge):", self.trunknode)
|
|
6147
|
+
|
|
6148
|
+
# edgenode checkbox (default false)
|
|
6149
|
+
self.edgenode = QPushButton("Edges -> Nodes")
|
|
6150
|
+
self.edgenode.setCheckable(True)
|
|
6151
|
+
self.edgenode.setChecked(False)
|
|
6152
|
+
layout.addRow("Convert 'Edges (Labeled objects)' to node objects?:", self.edgenode)
|
|
6153
|
+
|
|
6154
|
+
# edgeweight checkbox (default false)
|
|
6155
|
+
self.edgeweight = QPushButton("Remove weights")
|
|
6156
|
+
self.edgeweight.setCheckable(True)
|
|
6157
|
+
self.edgeweight.setChecked(False)
|
|
6158
|
+
layout.addRow("Remove network weights?:", self.edgeweight)
|
|
6159
|
+
|
|
6160
|
+
# prune checkbox (default false)
|
|
6161
|
+
self.prune = QPushButton("Prune Same Type")
|
|
6162
|
+
self.prune.setCheckable(True)
|
|
6163
|
+
self.prune.setChecked(False)
|
|
6164
|
+
layout.addRow("Prune connections between nodes of the same type (if assigned)?:", self.prune)
|
|
6165
|
+
|
|
6166
|
+
# isolate checkbox (default false)
|
|
6167
|
+
self.isolate = QPushButton("Isolate Two Types")
|
|
6168
|
+
self.isolate.setCheckable(True)
|
|
6169
|
+
self.isolate.setChecked(False)
|
|
6170
|
+
layout.addRow("Isolate connections between two specific node types (if assigned)?:", self.isolate)
|
|
6171
|
+
|
|
6172
|
+
#change button
|
|
6173
|
+
change_button = QPushButton("Add/Remove Network Pairs")
|
|
6174
|
+
change_button.clicked.connect(self.show_alter_dialog)
|
|
6175
|
+
layout.addRow(change_button)
|
|
6176
|
+
|
|
6177
|
+
# Add Run button
|
|
6178
|
+
run_button = QPushButton("Make Changes")
|
|
6179
|
+
run_button.clicked.connect(self.run_changes)
|
|
6180
|
+
layout.addRow(run_button)
|
|
6181
|
+
|
|
6182
|
+
def show_isolate_dialog(self):
|
|
6183
|
+
|
|
6184
|
+
dialog = IsolateDialog(self)
|
|
6185
|
+
dialog.exec()
|
|
6186
|
+
|
|
6187
|
+
def show_alter_dialog(self):
|
|
6188
|
+
|
|
6189
|
+
dialog = AlterDialog(self.parent())
|
|
6190
|
+
dialog.exec()
|
|
6191
|
+
|
|
6192
|
+
def run_changes(self):
|
|
6193
|
+
|
|
6194
|
+
try:
|
|
6195
|
+
|
|
6196
|
+
trunk = self.trunk.isChecked()
|
|
6197
|
+
if not trunk:
|
|
6198
|
+
trunknode = self.trunknode.isChecked()
|
|
6199
|
+
else:
|
|
6200
|
+
trunknode = False
|
|
6201
|
+
edgenode = self.edgenode.isChecked()
|
|
6202
|
+
edgeweight = self.edgeweight.isChecked()
|
|
6203
|
+
prune = self.prune.isChecked()
|
|
6204
|
+
isolate = self.isolate.isChecked()
|
|
6205
|
+
|
|
6206
|
+
if isolate and my_network.node_identities is not None:
|
|
6207
|
+
self.show_isolate_dialog()
|
|
6208
|
+
|
|
6209
|
+
if edgeweight:
|
|
6210
|
+
my_network.remove_edge_weights()
|
|
6211
|
+
if prune and my_network.node_identities is not None:
|
|
6212
|
+
my_network.prune_samenode_connections()
|
|
6213
|
+
if trunk:
|
|
6214
|
+
my_network.remove_trunk_post()
|
|
6215
|
+
if trunknode:
|
|
6216
|
+
if my_network.node_centroids is None or my_network.edge_centroids is None:
|
|
6217
|
+
self.parent().show_centroid_dialog()
|
|
6218
|
+
my_network.trunk_to_node()
|
|
6219
|
+
self.parent().load_channel(0, my_network.nodes, True)
|
|
6220
|
+
if edgenode:
|
|
6221
|
+
if my_network.node_centroids is None or my_network.edge_centroids is None:
|
|
6222
|
+
self.parent().show_centroid_dialog()
|
|
6223
|
+
my_network.edge_to_node()
|
|
6224
|
+
self.parent().load_channel(0, my_network.nodes, True)
|
|
6225
|
+
self.parent().load_channel(1, my_network.edges, True)
|
|
6226
|
+
|
|
6227
|
+
try:
|
|
6228
|
+
if hasattr(my_network, 'network_lists'):
|
|
6229
|
+
model = PandasModel(my_network.network_lists)
|
|
6230
|
+
self.parent().network_table.setModel(model)
|
|
6231
|
+
# Adjust column widths to content
|
|
6232
|
+
for column in range(model.columnCount(None)):
|
|
6233
|
+
self.parent().network_table.resizeColumnToContents(column)
|
|
6234
|
+
except Exception as e:
|
|
6235
|
+
print(f"Error showing network table: {e}")
|
|
6236
|
+
|
|
6237
|
+
if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
|
|
6238
|
+
try:
|
|
6239
|
+
self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
|
|
6240
|
+
except Exception as e:
|
|
6241
|
+
print(f"Error loading node identity table: {e}")
|
|
6242
|
+
|
|
6243
|
+
self.parent().update_display()
|
|
6244
|
+
self.accept()
|
|
6245
|
+
|
|
6246
|
+
except Exception as e:
|
|
6247
|
+
print(f"An error occurred: {e}")
|
|
6248
|
+
|
|
4627
6249
|
|
|
4628
6250
|
|
|
4629
6251
|
|
|
@@ -5031,6 +6653,15 @@ class ProxDialog(QDialog):
|
|
|
5031
6653
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
5032
6654
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
5033
6655
|
|
|
6656
|
+
if my_network.node_identities is not None:
|
|
6657
|
+
self.id_selector = QComboBox()
|
|
6658
|
+
# Add all options from id dictionary
|
|
6659
|
+
self.id_selector.addItems(['None'] + list(set(my_network.node_identities.values())))
|
|
6660
|
+
self.id_selector.setCurrentIndex(0) # Default to Mode 1
|
|
6661
|
+
layout.addRow("Create Networks only from a specific node identity?:", self.id_selector)
|
|
6662
|
+
else:
|
|
6663
|
+
self.id_selector = None
|
|
6664
|
+
|
|
5034
6665
|
self.overlays = QPushButton("Overlays")
|
|
5035
6666
|
self.overlays.setCheckable(True)
|
|
5036
6667
|
self.overlays.setChecked(True)
|
|
@@ -5054,6 +6685,15 @@ class ProxDialog(QDialog):
|
|
|
5054
6685
|
|
|
5055
6686
|
mode = self.mode_selector.currentIndex()
|
|
5056
6687
|
|
|
6688
|
+
if self.id_selector is not None and self.id_selector.currentText() != 'None':
|
|
6689
|
+
target = self.id_selector.currentText()
|
|
6690
|
+
targets = []
|
|
6691
|
+
for node in my_network.node_identities:
|
|
6692
|
+
if target == my_network.node_identities[node]:
|
|
6693
|
+
targets.append(int(node))
|
|
6694
|
+
else:
|
|
6695
|
+
targets = None
|
|
6696
|
+
|
|
5057
6697
|
try:
|
|
5058
6698
|
directory = self.directory.text() if self.directory.text() else None
|
|
5059
6699
|
except:
|
|
@@ -5087,7 +6727,7 @@ class ProxDialog(QDialog):
|
|
|
5087
6727
|
my_network.nodes, _ = n3d.label_objects(my_network.nodes)
|
|
5088
6728
|
if my_network.node_centroids is None:
|
|
5089
6729
|
self.parent().show_centroid_dialog()
|
|
5090
|
-
my_network.morph_proximity(search = search)
|
|
6730
|
+
my_network.morph_proximity(search = search, targets = targets)
|
|
5091
6731
|
|
|
5092
6732
|
self.parent().load_channel(0, channel_data = my_network.nodes, data = True)
|
|
5093
6733
|
elif mode == 0:
|
|
@@ -5113,10 +6753,10 @@ class ProxDialog(QDialog):
|
|
|
5113
6753
|
return
|
|
5114
6754
|
|
|
5115
6755
|
if populate:
|
|
5116
|
-
my_network.nodes = my_network.kd_network(distance = search)
|
|
6756
|
+
my_network.nodes = my_network.kd_network(distance = search, targets = targets)
|
|
5117
6757
|
self.parent().load_channel(0, channel_data = my_network.nodes, data = True)
|
|
5118
6758
|
else:
|
|
5119
|
-
my_network.kd_network(distance = search)
|
|
6759
|
+
my_network.kd_network(distance = search, targets = targets)
|
|
5120
6760
|
|
|
5121
6761
|
|
|
5122
6762
|
my_network.dump(directory = directory)
|