nettracer3d 0.9.8__py3-none-any.whl → 1.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nettracer3d might be problematic. Click here for more details.
- nettracer3d/community_extractor.py +24 -8
- nettracer3d/morphology.py +113 -17
- nettracer3d/neighborhoods.py +201 -66
- nettracer3d/nettracer.py +516 -103
- nettracer3d/nettracer_gui.py +2164 -546
- nettracer3d/network_draw.py +9 -3
- nettracer3d/node_draw.py +41 -58
- nettracer3d/segmenter.py +67 -25
- nettracer3d/segmenter_GPU.py +67 -29
- nettracer3d/stats.py +861 -0
- {nettracer3d-0.9.8.dist-info → nettracer3d-1.1.5.dist-info}/METADATA +3 -3
- nettracer3d-1.1.5.dist-info/RECORD +26 -0
- nettracer3d-0.9.8.dist-info/RECORD +0 -25
- {nettracer3d-0.9.8.dist-info → nettracer3d-1.1.5.dist-info}/WHEEL +0 -0
- {nettracer3d-0.9.8.dist-info → nettracer3d-1.1.5.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.9.8.dist-info → nettracer3d-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.9.8.dist-info → nettracer3d-1.1.5.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -4,7 +4,7 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QG
|
|
|
4
4
|
QHBoxLayout, QSlider, QMenuBar, QMenu, QDialog,
|
|
5
5
|
QFormLayout, QLineEdit, QPushButton, QFileDialog,
|
|
6
6
|
QLabel, QComboBox, QMessageBox, QTableView, QInputDialog,
|
|
7
|
-
QMenu, QTabWidget, QGroupBox)
|
|
7
|
+
QMenu, QTabWidget, QGroupBox, QCheckBox)
|
|
8
8
|
from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal, QObject, QCoreApplication, QEvent, QEventLoop)
|
|
9
9
|
import numpy as np
|
|
10
10
|
import time
|
|
@@ -36,6 +36,7 @@ from threading import Lock
|
|
|
36
36
|
from scipy import ndimage
|
|
37
37
|
import os
|
|
38
38
|
from . import painting
|
|
39
|
+
from . import stats as net_stats
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
|
|
@@ -183,6 +184,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
183
184
|
3: None
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
self.branch_dict = {
|
|
188
|
+
0: None,
|
|
189
|
+
1: None
|
|
190
|
+
|
|
191
|
+
}
|
|
192
|
+
|
|
186
193
|
self.original_shape = None #For undoing resamples
|
|
187
194
|
|
|
188
195
|
# Create control panel
|
|
@@ -425,8 +432,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
425
432
|
self.canvas.mpl_connect('button_release_event', self.on_mouse_release)
|
|
426
433
|
self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
|
|
427
434
|
|
|
428
|
-
#self.canvas.mpl_connect('button_press_event', self.on_mouse_click)
|
|
429
|
-
|
|
430
435
|
# Initialize measurement tracking
|
|
431
436
|
self.measurement_points = [] # List to store point pairs
|
|
432
437
|
self.angle_measurements = [] # NEW: List to store angle trios
|
|
@@ -462,10 +467,97 @@ class ImageViewerWindow(QMainWindow):
|
|
|
462
467
|
self.last_paint_pos = None
|
|
463
468
|
|
|
464
469
|
self.resume = False
|
|
465
|
-
|
|
466
|
-
self.hold_update = False
|
|
467
470
|
self._first_pan_done = False
|
|
471
|
+
self.thresh_window_ref = None
|
|
468
472
|
|
|
473
|
+
|
|
474
|
+
def load_file(self):
|
|
475
|
+
"""Load CSV or Excel file and convert to dictionary format."""
|
|
476
|
+
try:
|
|
477
|
+
# Open file dialog
|
|
478
|
+
file_filter = "Spreadsheet Files (*.csv *.xlsx);;CSV Files (*.csv);;Excel Files (*.xlsx)"
|
|
479
|
+
filename, _ = QFileDialog.getOpenFileName(
|
|
480
|
+
self,
|
|
481
|
+
"Load File",
|
|
482
|
+
"",
|
|
483
|
+
file_filter
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
if not filename:
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
# Read the file
|
|
490
|
+
if filename.endswith('.csv'):
|
|
491
|
+
df = pd.read_csv(filename)
|
|
492
|
+
elif filename.endswith('.xlsx'):
|
|
493
|
+
df = pd.read_excel(filename)
|
|
494
|
+
else:
|
|
495
|
+
QMessageBox.warning(self, "Error", "Please select a CSV or Excel file.")
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
if df.empty:
|
|
499
|
+
QMessageBox.warning(self, "Error", "The file appears to be empty.")
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
# Extract headers
|
|
503
|
+
headers = df.columns.tolist()
|
|
504
|
+
if len(headers) < 1:
|
|
505
|
+
QMessageBox.warning(self, "Error", "File must have at least 1 column.")
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
# Extract filename without extension for title
|
|
509
|
+
import os
|
|
510
|
+
title = os.path.splitext(os.path.basename(filename))[0]
|
|
511
|
+
|
|
512
|
+
if len(headers) == 1:
|
|
513
|
+
# Single column: pass header to metric, column data as list to data, nothing to value
|
|
514
|
+
metric = headers[0]
|
|
515
|
+
data = df.iloc[:, 0].tolist() # First column as list
|
|
516
|
+
value = None
|
|
517
|
+
|
|
518
|
+
df = self.format_for_upperright_table(data=data, metric=metric, value=value, title=title)
|
|
519
|
+
return df
|
|
520
|
+
else:
|
|
521
|
+
# Multiple columns: create dictionary as before
|
|
522
|
+
# First column header (for metric parameter)
|
|
523
|
+
metric = headers[0]
|
|
524
|
+
|
|
525
|
+
# Remaining headers (for value parameter)
|
|
526
|
+
value = headers[1:]
|
|
527
|
+
|
|
528
|
+
# Create dictionary
|
|
529
|
+
data_dict = {}
|
|
530
|
+
|
|
531
|
+
for index, row in df.iterrows():
|
|
532
|
+
key = row.iloc[0] # First column value as key
|
|
533
|
+
|
|
534
|
+
if len(headers) == 2:
|
|
535
|
+
# If only 2 columns, store single value
|
|
536
|
+
data_dict[key] = row.iloc[1]
|
|
537
|
+
else:
|
|
538
|
+
# If more than 2 columns, store as list
|
|
539
|
+
data_dict[key] = row.iloc[1:].tolist()
|
|
540
|
+
|
|
541
|
+
if len(value) == 1:
|
|
542
|
+
value = value[0]
|
|
543
|
+
|
|
544
|
+
# Call the parent method
|
|
545
|
+
df = self.format_for_upperright_table(data=data_dict, metric=metric, value=value, title=title)
|
|
546
|
+
return df
|
|
547
|
+
|
|
548
|
+
QMessageBox.information(
|
|
549
|
+
self,
|
|
550
|
+
"Success",
|
|
551
|
+
f"File '{title}' loaded successfully with {len(df)} entries."
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
except Exception as e:
|
|
555
|
+
QMessageBox.critical(
|
|
556
|
+
self,
|
|
557
|
+
"Error",
|
|
558
|
+
f"Failed to load file: {str(e)}"
|
|
559
|
+
)
|
|
560
|
+
|
|
469
561
|
def popup_canvas(self):
|
|
470
562
|
"""Pop the canvas out into its own window"""
|
|
471
563
|
if hasattr(self, 'popup_window') and self.popup_window.isVisible():
|
|
@@ -791,6 +883,21 @@ class ImageViewerWindow(QMainWindow):
|
|
|
791
883
|
elif self.scroll_direction > 0 and new_value <= self.slice_slider.maximum():
|
|
792
884
|
self.slice_slider.setValue(new_value)
|
|
793
885
|
|
|
886
|
+
|
|
887
|
+
def confirm_mini_thresh(self):
|
|
888
|
+
|
|
889
|
+
if self.shape[0] * self.shape[1] * self.shape[2] > self.mini_thresh:
|
|
890
|
+
self.mini_overlay = True
|
|
891
|
+
return True
|
|
892
|
+
else:
|
|
893
|
+
return False
|
|
894
|
+
|
|
895
|
+
def evaluate_mini(self, mode = 'nodes'):
|
|
896
|
+
if self.confirm_mini_thresh():
|
|
897
|
+
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
898
|
+
else:
|
|
899
|
+
self.create_highlight_overlay(node_indices=self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
900
|
+
|
|
794
901
|
def create_highlight_overlay(self, node_indices=None, edge_indices=None, overlay1_indices = None, overlay2_indices = None, bounds = False):
|
|
795
902
|
"""
|
|
796
903
|
Create a binary overlay highlighting specific nodes and/or edges using parallel processing.
|
|
@@ -916,13 +1023,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
916
1023
|
|
|
917
1024
|
# Combine results
|
|
918
1025
|
if node_overlay is not None:
|
|
919
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, node_overlay)
|
|
1026
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, node_overlay).astype(np.uint8)
|
|
920
1027
|
if edge_overlay is not None:
|
|
921
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, edge_overlay)
|
|
1028
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, edge_overlay).astype(np.uint8)
|
|
922
1029
|
if overlay1_overlay is not None:
|
|
923
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay1_overlay)
|
|
1030
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay1_overlay).astype(np.uint8)
|
|
924
1031
|
if overlay2_overlay is not None:
|
|
925
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay2_overlay)
|
|
1032
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay2_overlay).astype(np.uint8)
|
|
926
1033
|
|
|
927
1034
|
# Update display
|
|
928
1035
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
@@ -931,6 +1038,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
931
1038
|
|
|
932
1039
|
"""Highlight overlay generation method specific for the segmenter interactive mode"""
|
|
933
1040
|
|
|
1041
|
+
self.mini_overlay_data = None
|
|
1042
|
+
self.highlight_overlay = None
|
|
934
1043
|
|
|
935
1044
|
def process_chunk_bounds(chunk_data, indices_to_check):
|
|
936
1045
|
"""Process a single chunk of the array to create highlight mask"""
|
|
@@ -1010,6 +1119,30 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1010
1119
|
current_ylim = self.ax.get_ylim()
|
|
1011
1120
|
self.update_display_pan_mode(current_xlim, current_ylim)
|
|
1012
1121
|
|
|
1122
|
+
if my_network.network is not None:
|
|
1123
|
+
try:
|
|
1124
|
+
if self.active_channel == 0:
|
|
1125
|
+
|
|
1126
|
+
# Get the existing DataFrame from the model
|
|
1127
|
+
original_df = self.network_table.model()._data
|
|
1128
|
+
|
|
1129
|
+
# Create mask for rows where one column is any original node AND the other column is any neighbor
|
|
1130
|
+
mask = (
|
|
1131
|
+
(original_df.iloc[:, 0].isin(indices)) &
|
|
1132
|
+
(original_df.iloc[:, 1].isin(indices)))
|
|
1133
|
+
|
|
1134
|
+
# Filter the DataFrame to only include direct connections
|
|
1135
|
+
filtered_df = original_df[mask].copy()
|
|
1136
|
+
|
|
1137
|
+
# Create new model with filtered DataFrame and update selection table
|
|
1138
|
+
new_model = PandasModel(filtered_df)
|
|
1139
|
+
self.selection_table.setModel(new_model)
|
|
1140
|
+
|
|
1141
|
+
# Switch to selection table
|
|
1142
|
+
self.selection_button.click()
|
|
1143
|
+
except:
|
|
1144
|
+
pass
|
|
1145
|
+
|
|
1013
1146
|
|
|
1014
1147
|
|
|
1015
1148
|
def create_mini_overlay(self, node_indices = None, edge_indices = None):
|
|
@@ -1169,7 +1302,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1169
1302
|
|
|
1170
1303
|
if my_network.node_identities is not None:
|
|
1171
1304
|
identity_menu = QMenu("Show Identity", self)
|
|
1172
|
-
|
|
1305
|
+
idens = list(set(my_network.node_identities.values()))
|
|
1306
|
+
idens.sort()
|
|
1307
|
+
for item in idens:
|
|
1173
1308
|
show_identity = identity_menu.addAction(f"ID: {item}")
|
|
1174
1309
|
show_identity.triggered.connect(lambda checked, item=item: self.handle_show_identities(sort=item))
|
|
1175
1310
|
context_menu.addMenu(identity_menu)
|
|
@@ -1178,6 +1313,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1178
1313
|
select_nodes = select_all_menu.addAction("Nodes")
|
|
1179
1314
|
select_both = select_all_menu.addAction("Nodes + Edges")
|
|
1180
1315
|
select_edges = select_all_menu.addAction("Edges")
|
|
1316
|
+
select_net_nodes = select_all_menu.addAction("Nodes in Network")
|
|
1317
|
+
select_net_both = select_all_menu.addAction("Nodes + Edges in Network")
|
|
1318
|
+
select_net_edges = select_all_menu.addAction("Edges in Network")
|
|
1181
1319
|
context_menu.addMenu(select_all_menu)
|
|
1182
1320
|
|
|
1183
1321
|
if len(self.clicked_values['nodes']) > 0 or len(self.clicked_values['edges']) > 0:
|
|
@@ -1201,32 +1339,40 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1201
1339
|
# Create measurement submenu
|
|
1202
1340
|
measure_menu = context_menu.addMenu("Measurements")
|
|
1203
1341
|
|
|
1204
|
-
# Distance measurement options
|
|
1205
1342
|
distance_menu = measure_menu.addMenu("Distance")
|
|
1206
1343
|
if self.current_point is None:
|
|
1207
1344
|
show_point_menu = distance_menu.addAction("Place First Point")
|
|
1208
1345
|
show_point_menu.triggered.connect(
|
|
1209
1346
|
lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
|
|
1210
|
-
|
|
1347
|
+
elif (self.current_point is not None and
|
|
1348
|
+
hasattr(self, 'measurement_mode') and
|
|
1349
|
+
self.measurement_mode == "distance"):
|
|
1211
1350
|
show_point_menu = distance_menu.addAction("Place Second Point")
|
|
1212
1351
|
show_point_menu.triggered.connect(
|
|
1213
1352
|
lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
|
|
1214
|
-
|
|
1353
|
+
|
|
1215
1354
|
# Angle measurement options
|
|
1216
1355
|
angle_menu = measure_menu.addMenu("Angle")
|
|
1217
1356
|
if self.current_point is None:
|
|
1218
1357
|
angle_first = angle_menu.addAction("Place First Point (A)")
|
|
1219
1358
|
angle_first.triggered.connect(
|
|
1220
1359
|
lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
|
|
1221
|
-
elif self.
|
|
1360
|
+
elif (self.current_point is not None and
|
|
1361
|
+
self.current_second_point is None and
|
|
1362
|
+
hasattr(self, 'measurement_mode') and
|
|
1363
|
+
self.measurement_mode == "angle"):
|
|
1222
1364
|
angle_second = angle_menu.addAction("Place Second Point (B - Vertex)")
|
|
1223
1365
|
angle_second.triggered.connect(
|
|
1224
1366
|
lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
|
|
1225
|
-
|
|
1367
|
+
elif (self.current_point is not None and
|
|
1368
|
+
self.current_second_point is not None and
|
|
1369
|
+
hasattr(self, 'measurement_mode') and
|
|
1370
|
+
self.measurement_mode == "angle"):
|
|
1226
1371
|
angle_third = angle_menu.addAction("Place Third Point (C)")
|
|
1227
1372
|
angle_third.triggered.connect(
|
|
1228
1373
|
lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
|
|
1229
1374
|
|
|
1375
|
+
|
|
1230
1376
|
show_remove_menu = measure_menu.addAction("Remove All Measurements")
|
|
1231
1377
|
show_remove_menu.triggered.connect(self.handle_remove_all_measurements)
|
|
1232
1378
|
|
|
@@ -1244,6 +1390,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1244
1390
|
select_nodes.triggered.connect(lambda: self.handle_select_all(edges = False, nodes = True))
|
|
1245
1391
|
select_both.triggered.connect(lambda: self.handle_select_all(edges = True))
|
|
1246
1392
|
select_edges.triggered.connect(lambda: self.handle_select_all(edges = True, nodes = False))
|
|
1393
|
+
select_net_nodes.triggered.connect(lambda: self.handle_select_all(edges = False, nodes = True, network = True))
|
|
1394
|
+
select_net_both.triggered.connect(lambda: self.handle_select_all(edges = True, network = True))
|
|
1395
|
+
select_net_edges.triggered.connect(lambda: self.handle_select_all(edges = True, nodes = False, network = True))
|
|
1247
1396
|
if self.highlight_overlay is not None or self.mini_overlay_data is not None:
|
|
1248
1397
|
highlight_select = context_menu.addAction("Add highlight in network selection")
|
|
1249
1398
|
highlight_select.triggered.connect(self.handle_highlight_select)
|
|
@@ -1254,15 +1403,22 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1254
1403
|
except IndexError:
|
|
1255
1404
|
pass
|
|
1256
1405
|
|
|
1257
|
-
|
|
1258
1406
|
def place_distance_point(self, x, y, z):
|
|
1259
1407
|
"""Place a measurement point for distance measurement."""
|
|
1260
1408
|
if self.current_point is None:
|
|
1261
1409
|
# This is the first point
|
|
1262
1410
|
self.current_point = (x, y, z)
|
|
1263
|
-
|
|
1264
|
-
|
|
1411
|
+
|
|
1412
|
+
# Create and store the artists
|
|
1413
|
+
pt = self.ax.plot(x, y, 'yo', markersize=8)[0]
|
|
1414
|
+
txt = self.ax.text(x, y+5, f"D{self.current_pair_index}",
|
|
1265
1415
|
color='yellow', ha='center', va='bottom')
|
|
1416
|
+
|
|
1417
|
+
# Add to measurement_artists so they can be managed by update_display
|
|
1418
|
+
if not hasattr(self, 'measurement_artists'):
|
|
1419
|
+
self.measurement_artists = []
|
|
1420
|
+
self.measurement_artists.extend([pt, txt])
|
|
1421
|
+
|
|
1266
1422
|
self.canvas.draw()
|
|
1267
1423
|
self.measurement_mode = "distance"
|
|
1268
1424
|
else:
|
|
@@ -1276,21 +1432,28 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1276
1432
|
((z2-z1)*my_network.z_scale)**2)
|
|
1277
1433
|
distance2 = np.sqrt(((x2-x1))**2 + ((y2-y1))**2 + ((z2-z1))**2)
|
|
1278
1434
|
|
|
1279
|
-
# Store the point pair
|
|
1435
|
+
# Store the point pair with type indicator
|
|
1280
1436
|
self.measurement_points.append({
|
|
1281
1437
|
'pair_index': self.current_pair_index,
|
|
1282
1438
|
'point1': self.current_point,
|
|
1283
1439
|
'point2': (x2, y2, z2),
|
|
1284
1440
|
'distance': distance,
|
|
1285
|
-
'distance2': distance2
|
|
1441
|
+
'distance2': distance2,
|
|
1442
|
+
'type': 'distance' # Added type tracking
|
|
1286
1443
|
})
|
|
1287
1444
|
|
|
1288
|
-
# Draw second point and line
|
|
1289
|
-
self.ax.plot(x2, y2, 'yo', markersize=8)
|
|
1290
|
-
self.ax.text(x2, y2+5, f"D{self.current_pair_index}",
|
|
1445
|
+
# Draw second point and line, storing the artists
|
|
1446
|
+
pt2 = self.ax.plot(x2, y2, 'yo', markersize=8)[0]
|
|
1447
|
+
txt2 = self.ax.text(x2, y2+5, f"D{self.current_pair_index}",
|
|
1291
1448
|
color='yellow', ha='center', va='bottom')
|
|
1449
|
+
|
|
1450
|
+
# Add to measurement_artists
|
|
1451
|
+
self.measurement_artists.extend([pt2, txt2])
|
|
1452
|
+
|
|
1292
1453
|
if z1 == z2: # Only draw line if points are on same slice
|
|
1293
|
-
self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
|
|
1454
|
+
line = self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)[0]
|
|
1455
|
+
self.measurement_artists.append(line)
|
|
1456
|
+
|
|
1294
1457
|
self.canvas.draw()
|
|
1295
1458
|
|
|
1296
1459
|
# Update measurement display
|
|
@@ -1303,12 +1466,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1303
1466
|
|
|
1304
1467
|
def place_angle_point(self, x, y, z):
|
|
1305
1468
|
"""Place a measurement point for angle measurement."""
|
|
1469
|
+
if not hasattr(self, 'measurement_artists'):
|
|
1470
|
+
self.measurement_artists = []
|
|
1471
|
+
|
|
1306
1472
|
if self.current_point is None:
|
|
1307
1473
|
# First point (A)
|
|
1308
1474
|
self.current_point = (x, y, z)
|
|
1309
|
-
|
|
1310
|
-
|
|
1475
|
+
|
|
1476
|
+
# Create and store artists
|
|
1477
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
1478
|
+
txt = self.ax.text(x, y+5, f"A{self.current_trio_index}",
|
|
1311
1479
|
color='green', ha='center', va='bottom')
|
|
1480
|
+
self.measurement_artists.extend([pt, txt])
|
|
1481
|
+
|
|
1312
1482
|
self.canvas.draw()
|
|
1313
1483
|
self.measurement_mode = "angle"
|
|
1314
1484
|
|
|
@@ -1317,13 +1487,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1317
1487
|
self.current_second_point = (x, y, z)
|
|
1318
1488
|
x1, y1, z1 = self.current_point
|
|
1319
1489
|
|
|
1320
|
-
|
|
1321
|
-
self.ax.
|
|
1490
|
+
# Create and store artists
|
|
1491
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
1492
|
+
txt = self.ax.text(x, y+5, f"B{self.current_trio_index}",
|
|
1322
1493
|
color='green', ha='center', va='bottom')
|
|
1494
|
+
self.measurement_artists.extend([pt, txt])
|
|
1323
1495
|
|
|
1324
1496
|
# Draw line from A to B
|
|
1325
1497
|
if z1 == z:
|
|
1326
|
-
self.ax.plot([x1, x], [y1, y], 'g--', alpha=0.7)
|
|
1498
|
+
line = self.ax.plot([x1, x], [y1, y], 'g--', alpha=0.7)[0]
|
|
1499
|
+
self.measurement_artists.append(line)
|
|
1327
1500
|
self.canvas.draw()
|
|
1328
1501
|
|
|
1329
1502
|
else:
|
|
@@ -1346,7 +1519,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1346
1519
|
**angle_data
|
|
1347
1520
|
})
|
|
1348
1521
|
|
|
1349
|
-
# Also add the two distances as separate pairs
|
|
1522
|
+
# Also add the two distances as separate pairs with type indicator
|
|
1350
1523
|
dist_ab = np.sqrt(((x2-x1)*my_network.xy_scale)**2 +
|
|
1351
1524
|
((y2-y1)*my_network.xy_scale)**2 +
|
|
1352
1525
|
((z2-z1)*my_network.z_scale)**2)
|
|
@@ -1363,24 +1536,28 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1363
1536
|
'point1': (x1, y1, z1),
|
|
1364
1537
|
'point2': (x2, y2, z2),
|
|
1365
1538
|
'distance': dist_ab,
|
|
1366
|
-
'distance2': dist_ab_voxel
|
|
1539
|
+
'distance2': dist_ab_voxel,
|
|
1540
|
+
'type': 'angle' # Added type tracking
|
|
1367
1541
|
},
|
|
1368
1542
|
{
|
|
1369
1543
|
'pair_index': f"B{self.current_trio_index}-C{self.current_trio_index}",
|
|
1370
1544
|
'point1': (x2, y2, z2),
|
|
1371
1545
|
'point2': (x3, y3, z3),
|
|
1372
1546
|
'distance': dist_bc,
|
|
1373
|
-
'distance2': dist_bc_voxel
|
|
1547
|
+
'distance2': dist_bc_voxel,
|
|
1548
|
+
'type': 'angle' # Added type tracking
|
|
1374
1549
|
}
|
|
1375
1550
|
])
|
|
1376
1551
|
|
|
1377
|
-
# Draw third point and line
|
|
1378
|
-
self.ax.plot(x3, y3, 'go', markersize=8)
|
|
1379
|
-
self.ax.text(x3, y3+5, f"C{self.current_trio_index}",
|
|
1552
|
+
# Draw third point and line, storing artists
|
|
1553
|
+
pt3 = self.ax.plot(x3, y3, 'go', markersize=8)[0]
|
|
1554
|
+
txt3 = self.ax.text(x3, y3+5, f"C{self.current_trio_index}",
|
|
1380
1555
|
color='green', ha='center', va='bottom')
|
|
1556
|
+
self.measurement_artists.extend([pt3, txt3])
|
|
1381
1557
|
|
|
1382
1558
|
if z2 == z3: # Draw line from B to C if on same slice
|
|
1383
|
-
self.ax.plot([x2, x3], [y2, y3], 'g--', alpha=0.7)
|
|
1559
|
+
line = self.ax.plot([x2, x3], [y2, y3], 'g--', alpha=0.7)[0]
|
|
1560
|
+
self.measurement_artists.append(line)
|
|
1384
1561
|
self.canvas.draw()
|
|
1385
1562
|
|
|
1386
1563
|
# Update measurement display
|
|
@@ -1392,6 +1569,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1392
1569
|
self.current_trio_index += 1
|
|
1393
1570
|
self.measurement_mode = "angle"
|
|
1394
1571
|
|
|
1572
|
+
|
|
1395
1573
|
def calculate_3d_angle(self, point_a, point_b, point_c):
|
|
1396
1574
|
"""Calculate 3D angle at vertex B between points A-B-C."""
|
|
1397
1575
|
x1, y1, z1 = point_a
|
|
@@ -1555,28 +1733,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1555
1733
|
if edges:
|
|
1556
1734
|
edge_indices = filtered_df.iloc[:, 2].unique().tolist()
|
|
1557
1735
|
self.clicked_values['edges'] = edge_indices
|
|
1558
|
-
|
|
1559
|
-
if self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2] > self.mini_thresh:
|
|
1560
|
-
self.mini_overlay = True
|
|
1561
|
-
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1562
|
-
else:
|
|
1563
|
-
self.create_highlight_overlay(
|
|
1564
|
-
node_indices=self.clicked_values['nodes'],
|
|
1565
|
-
edge_indices=self.clicked_values['edges']
|
|
1566
|
-
)
|
|
1736
|
+
self.evaluate_mini(mode = 'edges')
|
|
1567
1737
|
else:
|
|
1568
|
-
|
|
1569
|
-
self.mini_overlay = True
|
|
1570
|
-
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1571
|
-
else:
|
|
1572
|
-
self.create_highlight_overlay(
|
|
1573
|
-
node_indices=self.clicked_values['nodes'],
|
|
1574
|
-
edge_indices = self.clicked_values['edges']
|
|
1575
|
-
)
|
|
1576
|
-
|
|
1738
|
+
self.evaluate_mini()
|
|
1577
1739
|
|
|
1578
1740
|
except Exception as e:
|
|
1579
|
-
print(f"Error
|
|
1741
|
+
print(f"Error showing neighbors: {e}")
|
|
1580
1742
|
|
|
1581
1743
|
|
|
1582
1744
|
def handle_show_component(self, edges = False, nodes = True):
|
|
@@ -1647,23 +1809,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1647
1809
|
if edges:
|
|
1648
1810
|
edge_indices = filtered_df.iloc[:, 2].unique().tolist()
|
|
1649
1811
|
self.clicked_values['edges'] = edge_indices
|
|
1650
|
-
|
|
1651
|
-
self.mini_overlay = True
|
|
1652
|
-
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1653
|
-
else:
|
|
1654
|
-
self.create_highlight_overlay(
|
|
1655
|
-
node_indices=self.clicked_values['nodes'],
|
|
1656
|
-
edge_indices=edge_indices
|
|
1657
|
-
)
|
|
1812
|
+
self.evaluate_mini(mode = 'edges')
|
|
1658
1813
|
else:
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1662
|
-
else:
|
|
1663
|
-
self.create_highlight_overlay(
|
|
1664
|
-
node_indices = self.clicked_values['nodes'],
|
|
1665
|
-
edge_indices = self.clicked_values['edges']
|
|
1666
|
-
)
|
|
1814
|
+
self.evaluate_mini()
|
|
1815
|
+
|
|
1667
1816
|
|
|
1668
1817
|
except Exception as e:
|
|
1669
1818
|
|
|
@@ -1706,23 +1855,27 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1706
1855
|
|
|
1707
1856
|
nodes = list(set(nodes))
|
|
1708
1857
|
|
|
1709
|
-
|
|
1710
|
-
original_df = self.network_table.model()._data
|
|
1858
|
+
try:
|
|
1711
1859
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1860
|
+
# Get the existing DataFrame from the model
|
|
1861
|
+
original_df = self.network_table.model()._data
|
|
1862
|
+
|
|
1863
|
+
# Create mask for rows for nodes in question
|
|
1864
|
+
mask = (
|
|
1865
|
+
(original_df.iloc[:, 0].isin(nodes) & original_df.iloc[:, 1].isin(nodes))
|
|
1866
|
+
)
|
|
1867
|
+
|
|
1868
|
+
# Filter the DataFrame to only include direct connections
|
|
1869
|
+
filtered_df = original_df[mask].copy()
|
|
1870
|
+
|
|
1871
|
+
# Create new model with filtered DataFrame and update selection table
|
|
1872
|
+
new_model = PandasModel(filtered_df)
|
|
1873
|
+
self.selection_table.setModel(new_model)
|
|
1874
|
+
|
|
1875
|
+
# Switch to selection table
|
|
1876
|
+
self.selection_button.click()
|
|
1877
|
+
except:
|
|
1878
|
+
pass
|
|
1726
1879
|
|
|
1727
1880
|
if edges:
|
|
1728
1881
|
edge_indices = filtered_df.iloc[:, 2].unique().tolist()
|
|
@@ -1855,7 +2008,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1855
2008
|
self.parent().toggle_channel(1)
|
|
1856
2009
|
# Navigate to the Z-slice
|
|
1857
2010
|
self.parent().slice_slider.setValue(int(centroid[0]))
|
|
1858
|
-
print(f"Found edge {value} at Z
|
|
2011
|
+
print(f"Found edge {value} at [Z,Y,X] -> {centroid}")
|
|
1859
2012
|
|
|
1860
2013
|
else:
|
|
1861
2014
|
print(f"Edge {value} not found in centroids dictionary")
|
|
@@ -1891,9 +2044,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1891
2044
|
# Navigate to the Z-slice
|
|
1892
2045
|
self.parent().slice_slider.setValue(int(centroid[0]))
|
|
1893
2046
|
if mode == 0:
|
|
1894
|
-
print(f"Found node {value} at Z
|
|
2047
|
+
print(f"Found node {value} at [Z,Y,X] -> {centroid}")
|
|
1895
2048
|
elif mode == 2:
|
|
1896
|
-
print(f"Found node {value} from community {com} at Z
|
|
2049
|
+
print(f"Found node {value} from community {com} at [Z,Y,X] -> {centroid}")
|
|
1897
2050
|
|
|
1898
2051
|
|
|
1899
2052
|
else:
|
|
@@ -1924,12 +2077,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1924
2077
|
|
|
1925
2078
|
|
|
1926
2079
|
|
|
1927
|
-
def handle_select_all(self, nodes = True, edges = False):
|
|
2080
|
+
def handle_select_all(self, nodes = True, edges = False, network = False):
|
|
1928
2081
|
|
|
1929
2082
|
try:
|
|
1930
2083
|
|
|
1931
2084
|
if nodes:
|
|
1932
|
-
|
|
2085
|
+
if not network:
|
|
2086
|
+
nodes = list(np.unique(my_network.nodes))
|
|
2087
|
+
else:
|
|
2088
|
+
nodes = list(set(my_network.network_lists[0] + my_network.network_lists[1]))
|
|
1933
2089
|
if nodes[0] == 0:
|
|
1934
2090
|
del nodes[0]
|
|
1935
2091
|
num = (self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2])
|
|
@@ -1937,7 +2093,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1937
2093
|
else:
|
|
1938
2094
|
nodes = []
|
|
1939
2095
|
if edges:
|
|
1940
|
-
|
|
2096
|
+
if not network:
|
|
2097
|
+
edges = list(np.unique(my_network.edges))
|
|
2098
|
+
else:
|
|
2099
|
+
edges = my_network.network_lists[2]
|
|
1941
2100
|
num = (self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2])
|
|
1942
2101
|
if edges[0] == 0:
|
|
1943
2102
|
del edges[0]
|
|
@@ -2015,6 +2174,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2015
2174
|
except:
|
|
2016
2175
|
pass
|
|
2017
2176
|
|
|
2177
|
+
if self.branch_dict[0] is not None:
|
|
2178
|
+
try:
|
|
2179
|
+
info_dict['Branch Length'] = self.branch_dict[0][0][label]
|
|
2180
|
+
except:
|
|
2181
|
+
pass
|
|
2182
|
+
try:
|
|
2183
|
+
info_dict['Branch Tortuosity'] = self.branch_dict[0][1][label]
|
|
2184
|
+
except:
|
|
2185
|
+
pass
|
|
2186
|
+
|
|
2018
2187
|
|
|
2019
2188
|
elif sort == 'edge':
|
|
2020
2189
|
|
|
@@ -2060,7 +2229,17 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2060
2229
|
except:
|
|
2061
2230
|
pass
|
|
2062
2231
|
|
|
2063
|
-
|
|
2232
|
+
if self.branch_dict[1] is not None:
|
|
2233
|
+
try:
|
|
2234
|
+
info_dict['Branch Length'] = self.branch_dict[1][0][label]
|
|
2235
|
+
except:
|
|
2236
|
+
pass
|
|
2237
|
+
try:
|
|
2238
|
+
info_dict['Branch Tortuosity'] = self.branch_dict[1][1][label]
|
|
2239
|
+
except:
|
|
2240
|
+
pass
|
|
2241
|
+
|
|
2242
|
+
self.format_for_upperright_table(info_dict, title = f'Info on Object', sort = False)
|
|
2064
2243
|
|
|
2065
2244
|
except:
|
|
2066
2245
|
pass
|
|
@@ -2178,7 +2357,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2178
2357
|
unique_labels = np.unique(input_array[binary_mask])
|
|
2179
2358
|
print(f"Processing {len(unique_labels)} unique labels")
|
|
2180
2359
|
|
|
2181
|
-
# Get all bounding boxes at once
|
|
2360
|
+
# Get all bounding boxes at once
|
|
2182
2361
|
bounding_boxes = ndimage.find_objects(input_array)
|
|
2183
2362
|
|
|
2184
2363
|
# Prepare work items - just check if bounding box exists for each label
|
|
@@ -2194,7 +2373,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2194
2373
|
bbox = bounding_boxes[bbox_index]
|
|
2195
2374
|
work_items.append((orig_label, bbox))
|
|
2196
2375
|
|
|
2197
|
-
print(f"Created {len(work_items)} work items")
|
|
2376
|
+
#print(f"Created {len(work_items)} work items")
|
|
2198
2377
|
|
|
2199
2378
|
# If we have work items, process them
|
|
2200
2379
|
if len(work_items) == 0:
|
|
@@ -2214,7 +2393,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2214
2393
|
return orig_label, bbox, labeled_sub, num_cc
|
|
2215
2394
|
|
|
2216
2395
|
except Exception as e:
|
|
2217
|
-
print(f"Error processing label {orig_label}: {e}")
|
|
2396
|
+
#print(f"Error processing label {orig_label}: {e}")
|
|
2218
2397
|
return orig_label, bbox, None, 0
|
|
2219
2398
|
|
|
2220
2399
|
# Execute in parallel
|
|
@@ -2230,7 +2409,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2230
2409
|
|
|
2231
2410
|
for orig_label, bbox, labeled_sub, num_cc in results:
|
|
2232
2411
|
if num_cc > 0 and labeled_sub is not None:
|
|
2233
|
-
print(f"Label {orig_label}: {num_cc} components")
|
|
2412
|
+
#print(f"Label {orig_label}: {num_cc} components")
|
|
2234
2413
|
# Remap labels and place in output
|
|
2235
2414
|
for cc_id in range(1, num_cc + 1):
|
|
2236
2415
|
mask = labeled_sub == cc_id
|
|
@@ -2243,7 +2422,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2243
2422
|
|
|
2244
2423
|
def handle_seperate(self):
|
|
2245
2424
|
"""
|
|
2246
|
-
|
|
2425
|
+
Seperate objects in an array that share a label but do not touch
|
|
2247
2426
|
"""
|
|
2248
2427
|
try:
|
|
2249
2428
|
# Handle nodes
|
|
@@ -2252,7 +2431,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2252
2431
|
# Create highlight overlay (this should preserve original label values)
|
|
2253
2432
|
self.create_highlight_overlay(node_indices=self.clicked_values['nodes'])
|
|
2254
2433
|
|
|
2255
|
-
# DON'T convert to boolean yet - we need the original labels!
|
|
2256
2434
|
# Create a boolean mask for where we have highlighted values
|
|
2257
2435
|
highlight_mask = self.highlight_overlay != 0
|
|
2258
2436
|
|
|
@@ -2262,7 +2440,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2262
2440
|
# Get non-highlighted part of the array
|
|
2263
2441
|
non_highlighted = np.where(highlight_mask, 0, my_network.nodes)
|
|
2264
2442
|
|
|
2265
|
-
# Calculate max_val
|
|
2443
|
+
# Calculate max_val
|
|
2266
2444
|
max_val = np.max(non_highlighted) if np.any(non_highlighted) else 0
|
|
2267
2445
|
|
|
2268
2446
|
# Process highlighted part
|
|
@@ -2487,9 +2665,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2487
2665
|
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
2488
2666
|
self.needs_mini = False
|
|
2489
2667
|
else:
|
|
2490
|
-
self.
|
|
2668
|
+
self.evaluate_mini()
|
|
2491
2669
|
else:
|
|
2492
|
-
self.
|
|
2670
|
+
self.evaluate_mini()
|
|
2493
2671
|
|
|
2494
2672
|
|
|
2495
2673
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
@@ -3554,17 +3732,23 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3554
3732
|
|
|
3555
3733
|
# Add highlight overlays if they exist (with downsampling)
|
|
3556
3734
|
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3735
|
+
try:
|
|
3736
|
+
display_overlay = crop_and_downsample_image(self.mini_overlay_data, y_min_padded, y_max_padded, x_min_padded, x_max_padded, downsample_factor)
|
|
3737
|
+
highlight_rgba = self.create_highlight_rgba(display_overlay, yellow=True)
|
|
3738
|
+
composite = self.blend_layers(composite, highlight_rgba)
|
|
3739
|
+
except:
|
|
3740
|
+
pass
|
|
3560
3741
|
elif self.highlight_overlay is not None and self.highlight:
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3742
|
+
try:
|
|
3743
|
+
highlight_slice = self.highlight_overlay[self.current_slice]
|
|
3744
|
+
display_highlight = crop_and_downsample_image(highlight_slice, y_min_padded, y_max_padded, x_min_padded, x_max_padded, downsample_factor)
|
|
3745
|
+
if self.machine_window is None:
|
|
3746
|
+
highlight_rgba = self.create_highlight_rgba(display_highlight, yellow=True)
|
|
3747
|
+
else:
|
|
3748
|
+
highlight_rgba = self.create_highlight_rgba(display_highlight, yellow=False)
|
|
3749
|
+
composite = self.blend_layers(composite, highlight_rgba)
|
|
3750
|
+
except:
|
|
3751
|
+
pass
|
|
3568
3752
|
|
|
3569
3753
|
# Convert to 0-255 range for display
|
|
3570
3754
|
return (composite * 255).astype(np.uint8)
|
|
@@ -3659,6 +3843,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3659
3843
|
self.ax.clear()
|
|
3660
3844
|
self.ax.set_facecolor('black')
|
|
3661
3845
|
|
|
3846
|
+
# Reset measurement artists since we cleared the axes
|
|
3847
|
+
if not hasattr(self, 'measurement_artists'):
|
|
3848
|
+
self.measurement_artists = []
|
|
3849
|
+
else:
|
|
3850
|
+
self.measurement_artists = [] # Reset since ax.clear() removed all artists
|
|
3851
|
+
|
|
3662
3852
|
# Get original dimensions (before downsampling)
|
|
3663
3853
|
if hasattr(self, 'original_dims') and self.original_dims:
|
|
3664
3854
|
height, width = self.original_dims
|
|
@@ -3740,23 +3930,129 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3740
3930
|
for spine in self.ax.spines.values():
|
|
3741
3931
|
spine.set_color('black')
|
|
3742
3932
|
|
|
3743
|
-
# Add measurement points if they exist (
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3933
|
+
# Add measurement points if they exist (using the same logic as main update_display)
|
|
3934
|
+
if hasattr(self, 'measurement_points') and self.measurement_points:
|
|
3935
|
+
for point in self.measurement_points:
|
|
3936
|
+
x1, y1, z1 = point['point1']
|
|
3937
|
+
x2, y2, z2 = point['point2']
|
|
3938
|
+
pair_idx = point['pair_index']
|
|
3939
|
+
point_type = point.get('type', 'distance') # Default to distance for backward compatibility
|
|
3940
|
+
|
|
3941
|
+
# Determine colors based on type
|
|
3942
|
+
if point_type == 'angle':
|
|
3943
|
+
marker_color = 'go'
|
|
3944
|
+
text_color = 'green'
|
|
3945
|
+
line_color = 'g--'
|
|
3946
|
+
else: # distance
|
|
3947
|
+
marker_color = 'yo'
|
|
3948
|
+
text_color = 'yellow'
|
|
3949
|
+
line_color = 'r--'
|
|
3950
|
+
|
|
3951
|
+
# Check if points are in visible region and on current slice
|
|
3952
|
+
point1_visible = (z1 == self.current_slice and
|
|
3953
|
+
current_xlim[0] <= x1 <= current_xlim[1] and
|
|
3954
|
+
current_ylim[1] <= y1 <= current_ylim[0])
|
|
3955
|
+
point2_visible = (z2 == self.current_slice and
|
|
3956
|
+
current_xlim[0] <= x2 <= current_xlim[1] and
|
|
3957
|
+
current_ylim[1] <= y2 <= current_ylim[0])
|
|
3958
|
+
|
|
3959
|
+
# Draw individual points if they're on the current slice
|
|
3960
|
+
if point1_visible:
|
|
3961
|
+
pt1 = self.ax.plot(x1, y1, marker_color, markersize=8)[0]
|
|
3962
|
+
txt1 = self.ax.text(x1, y1+5, str(pair_idx), color=text_color, ha='center', va='bottom')
|
|
3963
|
+
self.measurement_artists.extend([pt1, txt1])
|
|
3964
|
+
|
|
3965
|
+
if point2_visible:
|
|
3966
|
+
pt2 = self.ax.plot(x2, y2, marker_color, markersize=8)[0]
|
|
3967
|
+
txt2 = self.ax.text(x2, y2+5, str(pair_idx), color=text_color, ha='center', va='bottom')
|
|
3968
|
+
self.measurement_artists.extend([pt2, txt2])
|
|
3969
|
+
|
|
3970
|
+
# Draw connecting line if both points are on the same slice
|
|
3971
|
+
if z1 == z2 == self.current_slice and (point1_visible or point2_visible):
|
|
3972
|
+
line = self.ax.plot([x1, x2], [y1, y2], line_color, alpha=0.5)[0]
|
|
3973
|
+
self.measurement_artists.append(line)
|
|
3974
|
+
|
|
3975
|
+
# Handle angle measurements if they exist
|
|
3976
|
+
if hasattr(self, 'angle_measurements') and self.angle_measurements:
|
|
3977
|
+
for angle in self.angle_measurements:
|
|
3978
|
+
xa, ya, za = angle['point_a']
|
|
3979
|
+
xb, yb, zb = angle['point_b'] # vertex
|
|
3980
|
+
xc, yc, zc = angle['point_c']
|
|
3981
|
+
trio_idx = angle['trio_index']
|
|
3982
|
+
|
|
3983
|
+
# Check if points are on current slice and visible
|
|
3984
|
+
point_a_visible = (za == self.current_slice and
|
|
3985
|
+
current_xlim[0] <= xa <= current_xlim[1] and
|
|
3986
|
+
current_ylim[1] <= ya <= current_ylim[0])
|
|
3987
|
+
point_b_visible = (zb == self.current_slice and
|
|
3988
|
+
current_xlim[0] <= xb <= current_xlim[1] and
|
|
3989
|
+
current_ylim[1] <= yb <= current_ylim[0])
|
|
3990
|
+
point_c_visible = (zc == self.current_slice and
|
|
3991
|
+
current_xlim[0] <= xc <= current_xlim[1] and
|
|
3992
|
+
current_ylim[1] <= yc <= current_ylim[0])
|
|
3993
|
+
|
|
3994
|
+
# Draw points
|
|
3995
|
+
if point_a_visible:
|
|
3996
|
+
pt_a = self.ax.plot(xa, ya, 'go', markersize=8)[0]
|
|
3997
|
+
txt_a = self.ax.text(xa, ya+5, f"A{trio_idx}", color='green', ha='center', va='bottom')
|
|
3998
|
+
self.measurement_artists.extend([pt_a, txt_a])
|
|
3999
|
+
|
|
4000
|
+
if point_b_visible:
|
|
4001
|
+
pt_b = self.ax.plot(xb, yb, 'go', markersize=8)[0]
|
|
4002
|
+
txt_b = self.ax.text(xb, yb+5, f"B{trio_idx}", color='green', ha='center', va='bottom')
|
|
4003
|
+
self.measurement_artists.extend([pt_b, txt_b])
|
|
4004
|
+
|
|
4005
|
+
if point_c_visible:
|
|
4006
|
+
pt_c = self.ax.plot(xc, yc, 'go', markersize=8)[0]
|
|
4007
|
+
txt_c = self.ax.text(xc, yc+5, f"C{trio_idx}", color='green', ha='center', va='bottom')
|
|
4008
|
+
self.measurement_artists.extend([pt_c, txt_c])
|
|
4009
|
+
|
|
4010
|
+
# Draw lines only if points are on current slice
|
|
4011
|
+
if za == zb == self.current_slice and (point_a_visible or point_b_visible):
|
|
4012
|
+
line_ab = self.ax.plot([xa, xb], [ya, yb], 'g--', alpha=0.7)[0]
|
|
4013
|
+
self.measurement_artists.append(line_ab)
|
|
3757
4014
|
|
|
3758
|
-
|
|
3759
|
-
|
|
4015
|
+
if zb == zc == self.current_slice and (point_b_visible or point_c_visible):
|
|
4016
|
+
line_bc = self.ax.plot([xb, xc], [yb, yc], 'g--', alpha=0.7)[0]
|
|
4017
|
+
self.measurement_artists.append(line_bc)
|
|
4018
|
+
|
|
4019
|
+
# Handle any partial measurements in progress (individual points without pairs yet)
|
|
4020
|
+
if hasattr(self, 'current_point') and self.current_point is not None:
|
|
4021
|
+
x, y, z = self.current_point
|
|
4022
|
+
if z == self.current_slice:
|
|
4023
|
+
if hasattr(self, 'measurement_mode') and self.measurement_mode == "angle":
|
|
4024
|
+
# Show green for angle mode
|
|
4025
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
4026
|
+
if hasattr(self, 'current_trio_index'):
|
|
4027
|
+
txt = self.ax.text(x, y+5, f"A{self.current_trio_index}", color='green', ha='center', va='bottom')
|
|
4028
|
+
else:
|
|
4029
|
+
txt = self.ax.text(x, y+5, "A", color='green', ha='center', va='bottom')
|
|
4030
|
+
else:
|
|
4031
|
+
# Show yellow for distance mode (default)
|
|
4032
|
+
pt = self.ax.plot(x, y, 'yo', markersize=8)[0]
|
|
4033
|
+
if hasattr(self, 'current_pair_index'):
|
|
4034
|
+
txt = self.ax.text(x, y+5, f"D{self.current_pair_index}", color='yellow', ha='center', va='bottom')
|
|
4035
|
+
else:
|
|
4036
|
+
txt = self.ax.text(x, y+5, "D", color='yellow', ha='center', va='bottom')
|
|
4037
|
+
self.measurement_artists.extend([pt, txt])
|
|
4038
|
+
|
|
4039
|
+
# Handle second point in angle measurements
|
|
4040
|
+
if hasattr(self, 'current_second_point') and self.current_second_point is not None:
|
|
4041
|
+
x, y, z = self.current_second_point
|
|
4042
|
+
if z == self.current_slice:
|
|
4043
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
4044
|
+
if hasattr(self, 'current_trio_index'):
|
|
4045
|
+
txt = self.ax.text(x, y+5, f"B{self.current_trio_index}", color='green', ha='center', va='bottom')
|
|
4046
|
+
else:
|
|
4047
|
+
txt = self.ax.text(x, y+5, "B", color='green', ha='center', va='bottom')
|
|
4048
|
+
self.measurement_artists.extend([pt, txt])
|
|
4049
|
+
|
|
4050
|
+
# Draw line from A to B if both are on current slice
|
|
4051
|
+
if (hasattr(self, 'current_point') and self.current_point is not None and
|
|
4052
|
+
self.current_point[2] == self.current_slice):
|
|
4053
|
+
x1, y1, z1 = self.current_point
|
|
4054
|
+
line = self.ax.plot([x1, x], [y1, y], 'g--', alpha=0.7)[0]
|
|
4055
|
+
self.measurement_artists.append(line)
|
|
3760
4056
|
|
|
3761
4057
|
#self.canvas.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
3762
4058
|
|
|
@@ -3872,7 +4168,34 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3872
4168
|
if len(self.clicked_values['edges']):
|
|
3873
4169
|
self.highlight_value_in_tables(self.clicked_values['edges'][-1])
|
|
3874
4170
|
self.handle_info('edge')
|
|
3875
|
-
|
|
4171
|
+
|
|
4172
|
+
try:
|
|
4173
|
+
if len(self.clicked_values['nodes']) > 0 or len(self.clicked_values['edges']) > 0: # Check if we have any nodes selected
|
|
4174
|
+
|
|
4175
|
+
old_nodes = copy.deepcopy(self.clicked_values['nodes'])
|
|
4176
|
+
|
|
4177
|
+
# Get the existing DataFrame from the model
|
|
4178
|
+
original_df = self.network_table.model()._data
|
|
4179
|
+
|
|
4180
|
+
# Create mask for rows where one column is any original node AND the other column is any neighbor
|
|
4181
|
+
mask = (
|
|
4182
|
+
((original_df.iloc[:, 0].isin(self.clicked_values['nodes'])) &
|
|
4183
|
+
(original_df.iloc[:, 1].isin(self.clicked_values['nodes']))) |
|
|
4184
|
+
(original_df.iloc[:, 2].isin(self.clicked_values['edges']))
|
|
4185
|
+
)
|
|
4186
|
+
|
|
4187
|
+
# Filter the DataFrame to only include direct connections
|
|
4188
|
+
filtered_df = original_df[mask].copy()
|
|
4189
|
+
|
|
4190
|
+
# Create new model with filtered DataFrame and update selection table
|
|
4191
|
+
new_model = PandasModel(filtered_df)
|
|
4192
|
+
self.selection_table.setModel(new_model)
|
|
4193
|
+
|
|
4194
|
+
# Switch to selection table
|
|
4195
|
+
self.selection_button.click()
|
|
4196
|
+
except:
|
|
4197
|
+
pass
|
|
4198
|
+
|
|
3876
4199
|
elif not self.selecting and self.selection_start: # If we had a click but never started selection
|
|
3877
4200
|
# Handle as a normal click
|
|
3878
4201
|
self.on_mouse_click(event)
|
|
@@ -3895,10 +4218,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3895
4218
|
elif self.zoom_mode:
|
|
3896
4219
|
# Handle zoom mode press
|
|
3897
4220
|
if self.original_xlim is None:
|
|
3898
|
-
self.original_xlim = self.
|
|
3899
|
-
|
|
3900
|
-
self.original_ylim = self.ax.get_ylim()
|
|
3901
|
-
#print(self.original_ylim)
|
|
4221
|
+
self.original_xlim = (-0.5, self.shape[2] - 0.5)
|
|
4222
|
+
self.original_ylim = (self.shape[1] + 0.5, -0.5)
|
|
3902
4223
|
|
|
3903
4224
|
current_xlim = self.ax.get_xlim()
|
|
3904
4225
|
current_ylim = self.ax.get_ylim()
|
|
@@ -4060,8 +4381,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4060
4381
|
if self.zoom_mode:
|
|
4061
4382
|
# Existing zoom functionality
|
|
4062
4383
|
if self.original_xlim is None:
|
|
4063
|
-
self.original_xlim = self.
|
|
4064
|
-
self.original_ylim = self.
|
|
4384
|
+
self.original_xlim = (-0.5, self.shape[2] - 0.5)
|
|
4385
|
+
self.original_ylim = (self.shape[1] + 0.5, -0.5)
|
|
4065
4386
|
|
|
4066
4387
|
current_xlim = self.ax.get_xlim()
|
|
4067
4388
|
current_ylim = self.ax.get_ylim()
|
|
@@ -4237,6 +4558,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4237
4558
|
for i in range(4):
|
|
4238
4559
|
load_action = load_menu.addAction(f"Load {self.channel_names[i]}")
|
|
4239
4560
|
load_action.triggered.connect(lambda checked, ch=i: self.load_channel(ch))
|
|
4561
|
+
load_action = load_menu.addAction("Load Full-Sized Highlight Overlay")
|
|
4562
|
+
load_action.triggered.connect(lambda: self.load_channel(channel_index = 4, load_highlight = True))
|
|
4240
4563
|
load_action = load_menu.addAction("Load Network")
|
|
4241
4564
|
load_action.triggered.connect(self.load_network)
|
|
4242
4565
|
load_action = load_menu.addAction("Load From Excel Helper")
|
|
@@ -4277,6 +4600,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4277
4600
|
allstats_action.triggered.connect(self.stats)
|
|
4278
4601
|
histos_action = stats_menu.addAction("Network Statistic Histograms")
|
|
4279
4602
|
histos_action.triggered.connect(self.histos)
|
|
4603
|
+
sig_action = stats_menu.addAction("Significance Testing")
|
|
4604
|
+
sig_action.triggered.connect(self.sig_test)
|
|
4280
4605
|
radial_action = stats_menu.addAction("Radial Distribution Analysis")
|
|
4281
4606
|
radial_action.triggered.connect(self.show_radial_dialog)
|
|
4282
4607
|
neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
|
|
@@ -4291,8 +4616,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4291
4616
|
vol_action.triggered.connect(self.volumes)
|
|
4292
4617
|
rad_action = stats_menu.addAction("Calculate Radii")
|
|
4293
4618
|
rad_action.triggered.connect(self.show_rad_dialog)
|
|
4619
|
+
branch_stats = stats_menu.addAction("Calculate Branch Stats (Lengths, Tortuosities)")
|
|
4620
|
+
branch_stats.triggered.connect(self.show_branchstat_dialog)
|
|
4294
4621
|
inter_action = stats_menu.addAction("Calculate Node < > Edge Interaction")
|
|
4295
4622
|
inter_action.triggered.connect(self.show_interaction_dialog)
|
|
4623
|
+
violin_action = stats_menu.addAction("Show Identity Violins/UMAP/Assign Intensity Neighborhoods")
|
|
4624
|
+
violin_action.triggered.connect(self.show_violin_dialog)
|
|
4296
4625
|
overlay_menu = analysis_menu.addMenu("Data/Overlays")
|
|
4297
4626
|
degree_action = overlay_menu.addAction("Get Degree Information")
|
|
4298
4627
|
degree_action.triggered.connect(self.show_degree_dialog)
|
|
@@ -4326,12 +4655,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4326
4655
|
calc_branch_action.triggered.connect(self.handle_calc_branch)
|
|
4327
4656
|
calc_branchprox_action = calculate_menu.addAction("Calculate Branch Adjacency Network (Of Edges)")
|
|
4328
4657
|
calc_branchprox_action.triggered.connect(self.handle_branchprox_calc)
|
|
4658
|
+
#calc_id_net_action = calculate_menu.addAction("Calculate Identity Network (beta)")
|
|
4659
|
+
#calc_id_net_action.triggered.connect(self.handle_identity_net_calc)
|
|
4329
4660
|
centroid_action = calculate_menu.addAction("Calculate Centroids (Active Image)")
|
|
4330
4661
|
centroid_action.triggered.connect(self.show_centroid_dialog)
|
|
4331
4662
|
|
|
4332
4663
|
image_menu = process_menu.addMenu("Image")
|
|
4333
4664
|
resize_action = image_menu.addAction("Resize (Up/Downsample)")
|
|
4334
4665
|
resize_action.triggered.connect(self.show_resize_dialog)
|
|
4666
|
+
clean_action = image_menu.addAction("Clean Segmentation")
|
|
4667
|
+
clean_action.triggered.connect(self.show_clean_dialog)
|
|
4335
4668
|
dilate_action = image_menu.addAction("Dilate")
|
|
4336
4669
|
dilate_action.triggered.connect(self.show_dilate_dialog)
|
|
4337
4670
|
erode_action = image_menu.addAction("Erode")
|
|
@@ -4342,7 +4675,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4342
4675
|
binarize_action.triggered.connect(self.show_binarize_dialog)
|
|
4343
4676
|
label_action = image_menu.addAction("Label Objects")
|
|
4344
4677
|
label_action.triggered.connect(self.show_label_dialog)
|
|
4345
|
-
slabel_action = image_menu.addAction("
|
|
4678
|
+
slabel_action = image_menu.addAction("Neighbor Labels")
|
|
4346
4679
|
slabel_action.triggered.connect(self.show_slabel_dialog)
|
|
4347
4680
|
thresh_action = image_menu.addAction("Threshold/Segment")
|
|
4348
4681
|
thresh_action.triggered.connect(self.show_thresh_dialog)
|
|
@@ -4418,7 +4751,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4418
4751
|
|
|
4419
4752
|
|
|
4420
4753
|
# Add after your other buttons
|
|
4421
|
-
self.popup_button = QPushButton("⤴")
|
|
4754
|
+
self.popup_button = QPushButton("⤴")
|
|
4422
4755
|
self.popup_button.setFixedSize(40, 40)
|
|
4423
4756
|
self.popup_button.setToolTip("Pop out canvas")
|
|
4424
4757
|
self.popup_button.clicked.connect(self.popup_canvas)
|
|
@@ -4433,6 +4766,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4433
4766
|
cam_button.setStyleSheet("font-size: 24px;")
|
|
4434
4767
|
cam_button.clicked.connect(self.snap)
|
|
4435
4768
|
corner_layout.addWidget(cam_button)
|
|
4769
|
+
|
|
4770
|
+
load_button = QPushButton("📁")
|
|
4771
|
+
load_button.setFixedSize(40, 40)
|
|
4772
|
+
load_button.setStyleSheet("font-size: 24px;")
|
|
4773
|
+
load_button.clicked.connect(self.load_file)
|
|
4774
|
+
corner_layout.addWidget(load_button)
|
|
4436
4775
|
|
|
4437
4776
|
# Set as corner widget
|
|
4438
4777
|
menubar.setCornerWidget(corner_widget, Qt.Corner.TopRightCorner)
|
|
@@ -4480,7 +4819,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4480
4819
|
# Invalid input - reset to default
|
|
4481
4820
|
self.downsample_factor = 1
|
|
4482
4821
|
|
|
4483
|
-
|
|
4822
|
+
try:
|
|
4823
|
+
self.throttle = self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor
|
|
4824
|
+
except:
|
|
4825
|
+
self.throttle = False
|
|
4484
4826
|
|
|
4485
4827
|
# Optional: Trigger display update if you want immediate effect
|
|
4486
4828
|
if update:
|
|
@@ -4565,8 +4907,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4565
4907
|
self.cellpose_launcher.launch_cellpose_gui(use_3d = use_3d)
|
|
4566
4908
|
|
|
4567
4909
|
except:
|
|
4568
|
-
|
|
4569
|
-
|
|
4910
|
+
QMessageBox.critical(
|
|
4911
|
+
self,
|
|
4912
|
+
"Error",
|
|
4913
|
+
f"Error starting cellpose: {str(e)}\nNote: You may need to install cellpose with corresponding torch first - in your environment, please call 'pip install cellpose'. Please see: 'https://pytorch.org/get-started/locally/' to see what torch install command corresponds to your NVIDIA GPU"
|
|
4914
|
+
)
|
|
4570
4915
|
pass
|
|
4571
4916
|
|
|
4572
4917
|
|
|
@@ -4613,6 +4958,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4613
4958
|
except Exception as e:
|
|
4614
4959
|
print(f"Error creating histogram selector: {e}")
|
|
4615
4960
|
|
|
4961
|
+
def sig_test(self):
|
|
4962
|
+
# Get the existing QApplication instance
|
|
4963
|
+
app = QApplication.instance()
|
|
4964
|
+
|
|
4965
|
+
# Create the statistical GUI window without starting a new event loop
|
|
4966
|
+
stats_window = net_stats.main(app)
|
|
4967
|
+
|
|
4968
|
+
# Keep a reference so it doesn't get garbage collected
|
|
4969
|
+
self.stats_window = stats_window
|
|
4970
|
+
|
|
4616
4971
|
def volumes(self):
|
|
4617
4972
|
|
|
4618
4973
|
|
|
@@ -4638,7 +4993,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4638
4993
|
|
|
4639
4994
|
|
|
4640
4995
|
|
|
4641
|
-
def format_for_upperright_table(self, data, metric='Metric', value='Value', title=None):
|
|
4996
|
+
def format_for_upperright_table(self, data, metric='Metric', value='Value', title=None, sort = True):
|
|
4642
4997
|
"""
|
|
4643
4998
|
Format dictionary or list data for display in upper right table.
|
|
4644
4999
|
|
|
@@ -4724,11 +5079,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4724
5079
|
table = CustomTableView(self)
|
|
4725
5080
|
table.setModel(PandasModel(df))
|
|
4726
5081
|
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
5082
|
+
if sort:
|
|
5083
|
+
try:
|
|
5084
|
+
first_column_name = table.model()._data.columns[0]
|
|
5085
|
+
table.sort_table(first_column_name, ascending=True)
|
|
5086
|
+
except:
|
|
5087
|
+
pass
|
|
4732
5088
|
|
|
4733
5089
|
# Add to tabbed widget
|
|
4734
5090
|
if title is None:
|
|
@@ -4742,6 +5098,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4742
5098
|
for column in range(table.model().columnCount(None)):
|
|
4743
5099
|
table.resizeColumnToContents(column)
|
|
4744
5100
|
|
|
5101
|
+
return df
|
|
5102
|
+
|
|
4745
5103
|
except:
|
|
4746
5104
|
pass
|
|
4747
5105
|
|
|
@@ -4758,6 +5116,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4758
5116
|
dialog = MergeNodeIdDialog(self)
|
|
4759
5117
|
dialog.exec()
|
|
4760
5118
|
|
|
5119
|
+
def show_multichan_dialog(self, data):
|
|
5120
|
+
dialog = MultiChanDialog(self, data)
|
|
5121
|
+
dialog.show()
|
|
5122
|
+
|
|
4761
5123
|
def show_gray_water_dialog(self):
|
|
4762
5124
|
"""Show the gray watershed parameter dialog."""
|
|
4763
5125
|
dialog = GrayWaterDialog(self)
|
|
@@ -4860,7 +5222,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4860
5222
|
|
|
4861
5223
|
my_network.edges = (my_network.nodes == 0) * my_network.edges
|
|
4862
5224
|
|
|
4863
|
-
my_network.calculate_all(my_network.nodes, my_network.edges, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale, search = None, diledge = None, inners = False,
|
|
5225
|
+
my_network.calculate_all(my_network.nodes, my_network.edges, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale, search = None, diledge = None, inners = False, remove_trunk = 0, ignore_search_region = True, other_nodes = None, label_nodes = True, directory = None, GPU = False, fast_dil = False, skeletonize = False, GPU_downsample = None)
|
|
4864
5226
|
|
|
4865
5227
|
self.load_channel(1, my_network.edges, data = True)
|
|
4866
5228
|
self.load_channel(0, my_network.nodes, data = True)
|
|
@@ -4894,6 +5256,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4894
5256
|
|
|
4895
5257
|
self.load_channel(0, my_network.edges, data = True)
|
|
4896
5258
|
|
|
5259
|
+
try:
|
|
5260
|
+
self.branch_dict[0] = self.branch_dict[1]
|
|
5261
|
+
self.branch_dict[1] = None
|
|
5262
|
+
except:
|
|
5263
|
+
pass
|
|
5264
|
+
|
|
4897
5265
|
self.delete_channel(1, False)
|
|
4898
5266
|
|
|
4899
5267
|
my_network.morph_proximity(search = [3,3], fastdil = True)
|
|
@@ -4910,14 +5278,51 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4910
5278
|
dialog = CentroidDialog(self)
|
|
4911
5279
|
dialog.exec()
|
|
4912
5280
|
|
|
4913
|
-
def
|
|
5281
|
+
def handle_identity_net_calc(self):
|
|
5282
|
+
|
|
5283
|
+
try:
|
|
5284
|
+
|
|
5285
|
+
def confirm_dialog():
|
|
5286
|
+
"""Shows a dialog asking user to confirm and input connection limit"""
|
|
5287
|
+
from PyQt6.QtWidgets import QInputDialog
|
|
5288
|
+
|
|
5289
|
+
value, ok = QInputDialog.getInt(
|
|
5290
|
+
None, # parent widget
|
|
5291
|
+
"Confirm", # window title
|
|
5292
|
+
"Calculate Identity Network\n\n"
|
|
5293
|
+
"Connect nodes that share an identity - useful for nodes that\n"
|
|
5294
|
+
"overlap in identity to some degree.\n\n"
|
|
5295
|
+
"Enter maximum connections per node within same identity:",
|
|
5296
|
+
5, # default value
|
|
5297
|
+
1, # minimum value
|
|
5298
|
+
1000, # maximum value
|
|
5299
|
+
1 # step
|
|
5300
|
+
)
|
|
5301
|
+
|
|
5302
|
+
if ok:
|
|
5303
|
+
return True, value
|
|
5304
|
+
else:
|
|
5305
|
+
return False, None
|
|
5306
|
+
|
|
5307
|
+
confirm, val = confirm_dialog()
|
|
5308
|
+
|
|
5309
|
+
if confirm:
|
|
5310
|
+
my_network.create_id_network(val)
|
|
5311
|
+
self.table_load_attrs()
|
|
5312
|
+
else:
|
|
5313
|
+
return
|
|
5314
|
+
|
|
5315
|
+
except:
|
|
5316
|
+
pass
|
|
5317
|
+
|
|
5318
|
+
def show_dilate_dialog(self, args = None):
|
|
4914
5319
|
"""show the dilate dialog"""
|
|
4915
|
-
dialog = DilateDialog(self)
|
|
5320
|
+
dialog = DilateDialog(self, args)
|
|
4916
5321
|
dialog.exec()
|
|
4917
5322
|
|
|
4918
|
-
def show_erode_dialog(self):
|
|
5323
|
+
def show_erode_dialog(self, args = None):
|
|
4919
5324
|
"""show the erode dialog"""
|
|
4920
|
-
dialog = ErodeDialog(self)
|
|
5325
|
+
dialog = ErodeDialog(self, args)
|
|
4921
5326
|
dialog.exec()
|
|
4922
5327
|
|
|
4923
5328
|
def show_hole_dialog(self):
|
|
@@ -5016,6 +5421,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5016
5421
|
dialog = ResizeDialog(self)
|
|
5017
5422
|
dialog.exec()
|
|
5018
5423
|
|
|
5424
|
+
def show_clean_dialog(self):
|
|
5425
|
+
dialog = CleanDialog(self)
|
|
5426
|
+
dialog.show()
|
|
5019
5427
|
|
|
5020
5428
|
def show_properties_dialog(self):
|
|
5021
5429
|
"""Show the properties dialog"""
|
|
@@ -5170,7 +5578,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5170
5578
|
|
|
5171
5579
|
elif sort == 'Merge Nodes':
|
|
5172
5580
|
try:
|
|
5173
|
-
|
|
5174
5581
|
if my_network.nodes is None:
|
|
5175
5582
|
QMessageBox.critical(
|
|
5176
5583
|
self,
|
|
@@ -5178,72 +5585,118 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5178
5585
|
"Please load your first set of nodes into the 'Nodes' channel first"
|
|
5179
5586
|
)
|
|
5180
5587
|
return
|
|
5181
|
-
|
|
5182
5588
|
if len(np.unique(my_network.nodes)) < 3:
|
|
5183
5589
|
self.show_label_dialog()
|
|
5184
|
-
|
|
5185
|
-
#
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5590
|
+
|
|
5591
|
+
# Create custom dialog
|
|
5592
|
+
dialog = QDialog(self)
|
|
5593
|
+
dialog.setWindowTitle("Merge Nodes Configuration")
|
|
5594
|
+
dialog.setModal(True)
|
|
5595
|
+
dialog.resize(400, 200)
|
|
5596
|
+
|
|
5597
|
+
layout = QVBoxLayout(dialog)
|
|
5598
|
+
|
|
5599
|
+
# Selection type
|
|
5600
|
+
type_layout = QHBoxLayout()
|
|
5601
|
+
type_label = QLabel("Selection Type:")
|
|
5602
|
+
type_combo = QComboBox()
|
|
5603
|
+
type_combo.addItems(["TIFF File", "Directory"])
|
|
5604
|
+
type_layout.addWidget(type_label)
|
|
5605
|
+
type_layout.addWidget(type_combo)
|
|
5606
|
+
layout.addLayout(type_layout)
|
|
5607
|
+
|
|
5608
|
+
# Centroids checkbox
|
|
5609
|
+
centroids_layout = QHBoxLayout()
|
|
5610
|
+
centroids_check = QCheckBox("Compute node centroids for each image prior to merging")
|
|
5611
|
+
centroids_layout.addWidget(centroids_check)
|
|
5612
|
+
layout.addLayout(centroids_layout)
|
|
5613
|
+
|
|
5614
|
+
# Down factor for centroid calculation
|
|
5615
|
+
down_factor_layout = QHBoxLayout()
|
|
5616
|
+
down_factor_label = QLabel("Down Factor (for centroid calculation downsampling):")
|
|
5617
|
+
down_factor_edit = QLineEdit()
|
|
5618
|
+
down_factor_edit.setText("1") # Default value
|
|
5619
|
+
down_factor_edit.setPlaceholderText("Enter down factor (e.g., 1, 2, 4)")
|
|
5620
|
+
down_factor_layout.addWidget(down_factor_label)
|
|
5621
|
+
down_factor_layout.addWidget(down_factor_edit)
|
|
5622
|
+
layout.addLayout(down_factor_layout)
|
|
5623
|
+
|
|
5624
|
+
# Buttons
|
|
5625
|
+
button_layout = QHBoxLayout()
|
|
5626
|
+
accept_button = QPushButton("Accept")
|
|
5627
|
+
cancel_button = QPushButton("Cancel")
|
|
5628
|
+
button_layout.addWidget(accept_button)
|
|
5629
|
+
button_layout.addWidget(cancel_button)
|
|
5630
|
+
layout.addLayout(button_layout)
|
|
5631
|
+
|
|
5632
|
+
# Connect buttons
|
|
5633
|
+
accept_button.clicked.connect(dialog.accept)
|
|
5634
|
+
cancel_button.clicked.connect(dialog.reject)
|
|
5635
|
+
|
|
5636
|
+
# Execute dialog
|
|
5637
|
+
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
5638
|
+
# Get values from dialog
|
|
5639
|
+
selection_type = type_combo.currentText()
|
|
5640
|
+
centroids = centroids_check.isChecked()
|
|
5641
|
+
|
|
5642
|
+
# Validate and get down_factor
|
|
5237
5643
|
try:
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5644
|
+
down_factor = int(down_factor_edit.text())
|
|
5645
|
+
if down_factor <= 0:
|
|
5646
|
+
raise ValueError("Down factor must be positive")
|
|
5647
|
+
except ValueError as e:
|
|
5648
|
+
QMessageBox.critical(
|
|
5649
|
+
self,
|
|
5650
|
+
"Invalid Input",
|
|
5651
|
+
f"Invalid down factor: {str(e)}"
|
|
5652
|
+
)
|
|
5653
|
+
return
|
|
5654
|
+
|
|
5655
|
+
# Handle file/directory selection based on combo box choice
|
|
5656
|
+
if selection_type == "TIFF File":
|
|
5657
|
+
filename, _ = QFileDialog.getOpenFileName(
|
|
5658
|
+
self,
|
|
5659
|
+
"Select TIFF file",
|
|
5660
|
+
"",
|
|
5661
|
+
"TIFF files (*.tiff *.tif)"
|
|
5662
|
+
)
|
|
5663
|
+
if filename:
|
|
5664
|
+
selected_path = filename
|
|
5665
|
+
else:
|
|
5666
|
+
return # User cancelled file selection
|
|
5667
|
+
else: # Directory
|
|
5668
|
+
file_dialog = QFileDialog(self)
|
|
5669
|
+
file_dialog.setOption(QFileDialog.Option.DontUseNativeDialog)
|
|
5670
|
+
file_dialog.setOption(QFileDialog.Option.ReadOnly)
|
|
5671
|
+
file_dialog.setFileMode(QFileDialog.FileMode.Directory)
|
|
5672
|
+
file_dialog.setViewMode(QFileDialog.ViewMode.Detail)
|
|
5673
|
+
if file_dialog.exec() == QFileDialog.DialogCode.Accepted:
|
|
5674
|
+
selected_path = file_dialog.directory().absolutePath()
|
|
5675
|
+
else:
|
|
5676
|
+
return # User cancelled directory selection
|
|
5677
|
+
|
|
5678
|
+
if down_factor == 1:
|
|
5679
|
+
down_factor = None
|
|
5680
|
+
# Call merge_nodes with all parameters
|
|
5681
|
+
my_network.merge_nodes(
|
|
5682
|
+
selected_path,
|
|
5683
|
+
root_id=self.node_name,
|
|
5684
|
+
centroids=centroids,
|
|
5685
|
+
down_factor=down_factor
|
|
5686
|
+
)
|
|
5687
|
+
|
|
5688
|
+
self.load_channel(0, my_network.nodes, True)
|
|
5689
|
+
|
|
5690
|
+
if hasattr(my_network, 'node_identities') and my_network.node_identities is not None:
|
|
5691
|
+
try:
|
|
5692
|
+
self.format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity', 'Node Identities')
|
|
5693
|
+
except Exception as e:
|
|
5694
|
+
print(f"Error loading node identity table: {e}")
|
|
5695
|
+
|
|
5696
|
+
if centroids:
|
|
5697
|
+
self.format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
|
|
5698
|
+
|
|
5245
5699
|
except Exception as e:
|
|
5246
|
-
|
|
5247
5700
|
QMessageBox.critical(
|
|
5248
5701
|
self,
|
|
5249
5702
|
"Error Merging",
|
|
@@ -5266,8 +5719,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5266
5719
|
)
|
|
5267
5720
|
|
|
5268
5721
|
self.last_load = directory
|
|
5269
|
-
|
|
5722
|
+
self.last_saved = os.path.dirname(directory)
|
|
5723
|
+
self.last_save_name = directory
|
|
5270
5724
|
|
|
5725
|
+
self.channel_data = [None] * 5
|
|
5271
5726
|
if directory != "":
|
|
5272
5727
|
|
|
5273
5728
|
self.reset(network = True, xy_scale = 1, z_scale = 1, nodes = True, edges = True, network_overlay = True, id_overlay = True, update = False)
|
|
@@ -5570,6 +6025,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5570
6025
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
5571
6026
|
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
5572
6027
|
|
|
6028
|
+
def confirm_multichan_dialog(self):
|
|
6029
|
+
"""Shows a dialog asking user to confirm if image is multichan"""
|
|
6030
|
+
msg = QMessageBox()
|
|
6031
|
+
msg.setIcon(QMessageBox.Icon.Question)
|
|
6032
|
+
msg.setText("Image Format Alert")
|
|
6033
|
+
msg.setInformativeText("Is this a Multi-Channel (4D) image?")
|
|
6034
|
+
msg.setWindowTitle("Confirm Image Format")
|
|
6035
|
+
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
6036
|
+
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
6037
|
+
|
|
5573
6038
|
def confirm_resize_dialog(self):
|
|
5574
6039
|
"""Shows a dialog asking user to resize image"""
|
|
5575
6040
|
msg = QMessageBox()
|
|
@@ -5580,12 +6045,41 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5580
6045
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
5581
6046
|
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
5582
6047
|
|
|
5583
|
-
def
|
|
6048
|
+
def get_scaling_metadata_only(self, filename):
|
|
6049
|
+
# This only reads headers/metadata, not image data
|
|
6050
|
+
with tifffile.TiffFile(filename) as tif:
|
|
6051
|
+
x_scale = y_scale = z_scale = unit = None
|
|
6052
|
+
|
|
6053
|
+
# ImageJ metadata (very lightweight)
|
|
6054
|
+
if hasattr(tif, 'imagej_metadata') and tif.imagej_metadata:
|
|
6055
|
+
metadata = tif.imagej_metadata
|
|
6056
|
+
z_scale = metadata.get('spacing')
|
|
6057
|
+
unit = metadata.get('unit')
|
|
6058
|
+
|
|
6059
|
+
# TIFF tags (also lightweight - just header info)
|
|
6060
|
+
page = tif.pages[0] # This doesn't load image data
|
|
6061
|
+
tags = page.tags
|
|
6062
|
+
|
|
6063
|
+
if 'XResolution' in tags:
|
|
6064
|
+
x_res = tags['XResolution'].value
|
|
6065
|
+
x_scale = x_res[1] / x_res[0] if isinstance(x_res, tuple) else 1.0 / x_res
|
|
6066
|
+
|
|
6067
|
+
if 'YResolution' in tags:
|
|
6068
|
+
y_res = tags['YResolution'].value
|
|
6069
|
+
y_scale = y_res[1] / y_res[0] if isinstance(y_res, tuple) else 1.0 / y_res
|
|
6070
|
+
|
|
6071
|
+
if x_scale is None:
|
|
6072
|
+
x_scale = 1
|
|
6073
|
+
if z_scale is None:
|
|
6074
|
+
z_scale = 1
|
|
6075
|
+
|
|
6076
|
+
return x_scale, z_scale
|
|
6077
|
+
|
|
6078
|
+
def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True, preserve_zoom = None, end_paint = False, begin_paint = False, color = False, load_highlight = False):
|
|
5584
6079
|
"""Load a channel and enable active channel selection if needed."""
|
|
5585
6080
|
|
|
5586
6081
|
try:
|
|
5587
6082
|
|
|
5588
|
-
self.hold_update = True
|
|
5589
6083
|
if not data: # For solo loading
|
|
5590
6084
|
filename, _ = QFileDialog.getOpenFileName(
|
|
5591
6085
|
self,
|
|
@@ -5605,7 +6099,18 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5605
6099
|
try:
|
|
5606
6100
|
if file_extension in ['tif', 'tiff']:
|
|
5607
6101
|
import tifffile
|
|
5608
|
-
self.channel_data[channel_index] =
|
|
6102
|
+
self.channel_data[channel_index] = None
|
|
6103
|
+
if (self.channel_data[0] is None and self.channel_data[1] is None) and (channel_index == 0 or channel_index == 1):
|
|
6104
|
+
try:
|
|
6105
|
+
my_network.xy_scale, my_network.z_scale = self.get_scaling_metadata_only(filename)
|
|
6106
|
+
print(f"xy_scale property set to {my_network.xy_scale}; z_scale property set to {my_network.z_scale}")
|
|
6107
|
+
except:
|
|
6108
|
+
pass
|
|
6109
|
+
test_channel_data = tifffile.imread(filename)
|
|
6110
|
+
if len(test_channel_data.shape) not in (2, 3, 4):
|
|
6111
|
+
print("Invalid Shape")
|
|
6112
|
+
return
|
|
6113
|
+
self.channel_data[channel_index] = test_channel_data
|
|
5609
6114
|
|
|
5610
6115
|
elif file_extension == 'nii':
|
|
5611
6116
|
import nibabel as nib
|
|
@@ -5653,7 +6158,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5653
6158
|
try:
|
|
5654
6159
|
if len(self.channel_data[channel_index].shape) == 3: # potentially 2D RGB
|
|
5655
6160
|
if self.channel_data[channel_index].shape[-1] in (3, 4): # last dim is 3 or 4
|
|
5656
|
-
if not data
|
|
6161
|
+
if not data:
|
|
5657
6162
|
if self.confirm_rgb_dialog():
|
|
5658
6163
|
# User confirmed it's 2D RGB, expand to 4D
|
|
5659
6164
|
self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
|
|
@@ -5663,12 +6168,18 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5663
6168
|
except:
|
|
5664
6169
|
pass
|
|
5665
6170
|
|
|
5666
|
-
if
|
|
5667
|
-
|
|
5668
|
-
if
|
|
6171
|
+
if len(self.channel_data[channel_index].shape) == 4:
|
|
6172
|
+
if not self.channel_data[channel_index].shape[-1] in (3, 4):
|
|
6173
|
+
if self.confirm_multichan_dialog(): # User is trying to load 4D channel stack:
|
|
6174
|
+
my_data = copy.deepcopy(self.channel_data[channel_index])
|
|
6175
|
+
self.channel_data[channel_index] = None
|
|
6176
|
+
self.show_multichan_dialog(data = my_data)
|
|
6177
|
+
return
|
|
6178
|
+
elif not color and (channel_index == 0 or channel_index == 1):
|
|
6179
|
+
try:
|
|
5669
6180
|
self.channel_data[channel_index] = self.reduce_rgb_dimension(self.channel_data[channel_index], 'weight')
|
|
5670
|
-
|
|
5671
|
-
|
|
6181
|
+
except:
|
|
6182
|
+
pass
|
|
5672
6183
|
|
|
5673
6184
|
reset_resize = False
|
|
5674
6185
|
|
|
@@ -5677,7 +6188,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5677
6188
|
if self.highlight_overlay is not None: #Make sure highlight overlay is always the same shape as new images
|
|
5678
6189
|
try:
|
|
5679
6190
|
if self.channel_data[i].shape[:3] != self.highlight_overlay.shape:
|
|
5680
|
-
self.resizing = True
|
|
5681
6191
|
reset_resize = True
|
|
5682
6192
|
self.highlight_overlay = None
|
|
5683
6193
|
except:
|
|
@@ -5704,51 +6214,52 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5704
6214
|
my_network.id_overlay = self.channel_data[channel_index]
|
|
5705
6215
|
|
|
5706
6216
|
# Enable the channel button
|
|
5707
|
-
|
|
5708
|
-
|
|
6217
|
+
if channel_index != 4:
|
|
6218
|
+
self.channel_buttons[channel_index].setEnabled(True)
|
|
6219
|
+
self.delete_buttons[channel_index].setEnabled(True)
|
|
5709
6220
|
|
|
5710
6221
|
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
6222
|
+
# Enable active channel selector if this is the first channel loaded
|
|
6223
|
+
if not self.active_channel_combo.isEnabled():
|
|
6224
|
+
self.active_channel_combo.setEnabled(True)
|
|
5714
6225
|
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
6226
|
+
# Update slider range if this is the first channel loaded
|
|
6227
|
+
try:
|
|
6228
|
+
if len(self.channel_data[channel_index].shape) == 3 or len(self.channel_data[channel_index].shape) == 4:
|
|
6229
|
+
if not self.slice_slider.isEnabled():
|
|
6230
|
+
self.slice_slider.setEnabled(True)
|
|
6231
|
+
self.slice_slider.setMinimum(0)
|
|
6232
|
+
self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
|
|
6233
|
+
if self.slice_slider.value() < self.channel_data[channel_index].shape[0] - 1:
|
|
6234
|
+
self.current_slice = self.slice_slider.value()
|
|
6235
|
+
else:
|
|
6236
|
+
self.slice_slider.setValue(0)
|
|
6237
|
+
self.current_slice = 0
|
|
5724
6238
|
else:
|
|
5725
|
-
self.slice_slider.
|
|
5726
|
-
self.
|
|
6239
|
+
self.slice_slider.setEnabled(True)
|
|
6240
|
+
self.slice_slider.setMinimum(0)
|
|
6241
|
+
self.slice_slider.setMaximum(self.channel_data[channel_index].shape[0] - 1)
|
|
6242
|
+
if self.slice_slider.value() < self.channel_data[channel_index].shape[0] - 1:
|
|
6243
|
+
self.current_slice = self.slice_slider.value()
|
|
6244
|
+
else:
|
|
6245
|
+
self.current_slice = 0
|
|
6246
|
+
self.slice_slider.setValue(0)
|
|
5727
6247
|
else:
|
|
5728
|
-
self.slice_slider.setEnabled(
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
if self.slice_slider.value() < self.channel_data[channel_index].shape[0] - 1:
|
|
5732
|
-
self.current_slice = self.slice_slider.value()
|
|
5733
|
-
else:
|
|
5734
|
-
self.current_slice = 0
|
|
5735
|
-
self.slice_slider.setValue(0)
|
|
5736
|
-
else:
|
|
5737
|
-
self.slice_slider.setEnabled(False)
|
|
5738
|
-
except:
|
|
5739
|
-
pass
|
|
6248
|
+
self.slice_slider.setEnabled(False)
|
|
6249
|
+
except:
|
|
6250
|
+
pass
|
|
5740
6251
|
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
6252
|
+
|
|
6253
|
+
# If this is the first channel loaded, make it active
|
|
6254
|
+
if all(not btn.isEnabled() for btn in self.channel_buttons[:channel_index]):
|
|
6255
|
+
self.set_active_channel(channel_index)
|
|
5745
6256
|
|
|
5746
|
-
|
|
5747
|
-
|
|
6257
|
+
if not self.channel_buttons[channel_index].isChecked():
|
|
6258
|
+
self.channel_buttons[channel_index].click()
|
|
5748
6259
|
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
6260
|
+
self.min_max[channel_index][0] = np.min(self.channel_data[channel_index])
|
|
6261
|
+
self.min_max[channel_index][1] = np.max(self.channel_data[channel_index])
|
|
6262
|
+
self.volume_dict[channel_index] = None #reset volumes
|
|
5752
6263
|
|
|
5753
6264
|
try:
|
|
5754
6265
|
if assign_shape: #keep original shape tracked to undo resampling.
|
|
@@ -5763,7 +6274,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5763
6274
|
|
|
5764
6275
|
if self.shape == self.channel_data[channel_index].shape:
|
|
5765
6276
|
preserve_zoom = (self.ax.get_xlim(), self.ax.get_ylim())
|
|
5766
|
-
|
|
6277
|
+
self.shape = (self.channel_data[channel_index].shape[0], self.channel_data[channel_index].shape[1], self.channel_data[channel_index].shape[2])
|
|
6278
|
+
else:
|
|
6279
|
+
if self.shape is not None:
|
|
6280
|
+
self.resizing = True
|
|
6281
|
+
self.shape = (self.channel_data[channel_index].shape[0], self.channel_data[channel_index].shape[1], self.channel_data[channel_index].shape[2])
|
|
6282
|
+
ylim, xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
|
|
6283
|
+
preserve_zoom = (xlim, ylim)
|
|
5767
6284
|
if self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor:
|
|
5768
6285
|
self.throttle = True
|
|
5769
6286
|
else:
|
|
@@ -5771,8 +6288,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5771
6288
|
|
|
5772
6289
|
|
|
5773
6290
|
self.img_height, self.img_width = self.shape[1], self.shape[2]
|
|
5774
|
-
self.original_ylim, self.original_xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
|
|
5775
|
-
#print(self.original_xlim)
|
|
5776
6291
|
|
|
5777
6292
|
self.completed_paint_strokes = [] #Reset pending paint operations
|
|
5778
6293
|
self.current_stroke_points = []
|
|
@@ -5782,6 +6297,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5782
6297
|
self.current_operation = []
|
|
5783
6298
|
self.current_operation_type = None
|
|
5784
6299
|
|
|
6300
|
+
if load_highlight:
|
|
6301
|
+
self.highlight_overlay = n3d.binarize(self.channel_data[4].astype(np.uint8))
|
|
6302
|
+
self.mini_overlay_data = None
|
|
6303
|
+
self.mini_overlay = False
|
|
6304
|
+
self.channel_data[4] = None
|
|
6305
|
+
|
|
5785
6306
|
if self.pan_mode:
|
|
5786
6307
|
self.pan_button.click()
|
|
5787
6308
|
if self.show_channels:
|
|
@@ -5790,7 +6311,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5790
6311
|
elif not end_paint:
|
|
5791
6312
|
|
|
5792
6313
|
self.update_display(reset_resize = reset_resize, preserve_zoom = preserve_zoom)
|
|
5793
|
-
|
|
5794
6314
|
|
|
5795
6315
|
except Exception as e:
|
|
5796
6316
|
|
|
@@ -5799,7 +6319,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5799
6319
|
QMessageBox.critical(
|
|
5800
6320
|
self,
|
|
5801
6321
|
"Error Loading File",
|
|
5802
|
-
f"Failed to load
|
|
6322
|
+
f"Failed to load file: {str(e)}"
|
|
5803
6323
|
)
|
|
5804
6324
|
|
|
5805
6325
|
def delete_channel(self, channel_index, called = True, update = True):
|
|
@@ -6001,12 +6521,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6001
6521
|
def update_slice(self):
|
|
6002
6522
|
"""Queue a slice update when slider moves."""
|
|
6003
6523
|
# Store current view settings
|
|
6004
|
-
if
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
else:
|
|
6008
|
-
current_xlim = None
|
|
6009
|
-
current_ylim = None
|
|
6524
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
6525
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
6526
|
+
|
|
6010
6527
|
|
|
6011
6528
|
# Store the pending slice and view settings
|
|
6012
6529
|
self.pending_slice = (self.slice_slider.value(), (current_xlim, current_ylim))
|
|
@@ -6028,15 +6545,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6028
6545
|
self.pm.convert_virtual_strokes_to_data()
|
|
6029
6546
|
self.current_slice = slice_value
|
|
6030
6547
|
if self.preview:
|
|
6548
|
+
self.highlight_overlay = None
|
|
6549
|
+
self.mini_overlay_data = None
|
|
6550
|
+
self.mini_overlay = False
|
|
6031
6551
|
self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
|
|
6032
6552
|
elif self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
|
|
6033
6553
|
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
self.
|
|
6038
|
-
|
|
6039
|
-
|
|
6554
|
+
|
|
6555
|
+
if self.resizing:
|
|
6556
|
+
print('hello')
|
|
6557
|
+
self.highlight_overlay = None
|
|
6558
|
+
view_settings = ((-0.5, self.shape[2] - 0.5), (self.shape[1] - 0.5, -0.5))
|
|
6559
|
+
self.resizing = False
|
|
6560
|
+
self.update_display(preserve_zoom=view_settings)
|
|
6040
6561
|
if self.pan_mode:
|
|
6041
6562
|
self.pan_button.click()
|
|
6042
6563
|
self.pending_slice = None
|
|
@@ -6053,7 +6574,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6053
6574
|
self.channel_brightness[channel_index]['max'] = max_val / 65535
|
|
6054
6575
|
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
6055
6576
|
|
|
6056
|
-
|
|
6057
6577
|
def update_display(self, preserve_zoom=None, dims=None, called=False, reset_resize=False, skip=False):
|
|
6058
6578
|
"""Optimized display update with view-based cropping for performance."""
|
|
6059
6579
|
try:
|
|
@@ -6073,8 +6593,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6073
6593
|
if self.resume:
|
|
6074
6594
|
self.machine_window.segmentation_worker.resume()
|
|
6075
6595
|
self.resume = False
|
|
6076
|
-
if self.prev_down != self.downsample_factor:
|
|
6077
|
-
self.validate_downsample_input(text = self.prev_down)
|
|
6078
6596
|
|
|
6079
6597
|
if self.static_background is not None:
|
|
6080
6598
|
# Your existing virtual strokes conversion logic
|
|
@@ -6131,10 +6649,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6131
6649
|
for img in list(self.ax.get_images()):
|
|
6132
6650
|
img.remove()
|
|
6133
6651
|
# Clear measurement points
|
|
6134
|
-
|
|
6135
|
-
artist.
|
|
6136
|
-
|
|
6137
|
-
|
|
6652
|
+
if hasattr(self, 'measurement_artists'):
|
|
6653
|
+
for artist in self.measurement_artists:
|
|
6654
|
+
try:
|
|
6655
|
+
artist.remove()
|
|
6656
|
+
except:
|
|
6657
|
+
pass # Artist might already be removed
|
|
6658
|
+
self.measurement_artists = [] # Reset the list
|
|
6138
6659
|
# Determine the current view bounds (either from preserve_zoom or current state)
|
|
6139
6660
|
if preserve_zoom:
|
|
6140
6661
|
current_xlim, current_ylim = preserve_zoom
|
|
@@ -6201,7 +6722,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6201
6722
|
return cropped[::factor, ::factor, :]
|
|
6202
6723
|
else:
|
|
6203
6724
|
return cropped
|
|
6204
|
-
|
|
6205
6725
|
|
|
6206
6726
|
# Update channel images efficiently with cropping and downsampling
|
|
6207
6727
|
for channel in range(4):
|
|
@@ -6276,13 +6796,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6276
6796
|
|
|
6277
6797
|
im = self.ax.imshow(normalized_image, alpha=0.7, cmap=custom_cmap,
|
|
6278
6798
|
vmin=0, vmax=1, extent=crop_extent)
|
|
6279
|
-
|
|
6280
6799
|
# Handle preview, overlays, and measurements (apply cropping here too)
|
|
6281
|
-
#if self.preview and not called:
|
|
6282
|
-
# self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
|
|
6283
6800
|
|
|
6284
6801
|
# Overlay handling (optimized with cropping and downsampling)
|
|
6285
|
-
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
6802
|
+
if self.mini_overlay and self.highlight and self.machine_window is None and not self.preview:
|
|
6286
6803
|
highlight_cmap = LinearSegmentedColormap.from_list('highlight', [(0, 0, 0, 0), (1, 1, 0, 1)])
|
|
6287
6804
|
display_overlay = crop_and_downsample_image(
|
|
6288
6805
|
self.mini_overlay_data, y_min_padded, y_max_padded,
|
|
@@ -6301,34 +6818,88 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6301
6818
|
[(0, 0, 0, 0), (1, 1, 0, 1), (0, 0.7, 1, 1)])
|
|
6302
6819
|
self.ax.imshow(display_highlight, cmap=highlight_cmap, vmin=0, vmax=2, alpha=0.3, extent=crop_extent)
|
|
6303
6820
|
|
|
6304
|
-
# Redraw measurement points efficiently
|
|
6821
|
+
# Redraw measurement points efficiently
|
|
6305
6822
|
# Only draw points that are within the visible region for additional performance
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6823
|
+
|
|
6824
|
+
if hasattr(self, 'measurement_points') and self.measurement_points:
|
|
6825
|
+
for point in self.measurement_points:
|
|
6826
|
+
x1, y1, z1 = point['point1']
|
|
6827
|
+
x2, y2, z2 = point['point2']
|
|
6828
|
+
pair_idx = point['pair_index']
|
|
6829
|
+
point_type = point.get('type', 'distance') # Default to distance for backward compatibility
|
|
6830
|
+
|
|
6831
|
+
# Determine colors based on type
|
|
6832
|
+
if point_type == 'angle':
|
|
6833
|
+
marker_color = 'go'
|
|
6834
|
+
text_color = 'green'
|
|
6835
|
+
line_color = 'g--'
|
|
6836
|
+
else: # distance
|
|
6837
|
+
marker_color = 'yo'
|
|
6838
|
+
text_color = 'yellow'
|
|
6839
|
+
line_color = 'r--'
|
|
6323
6840
|
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6841
|
+
# Check if points are in visible region and on current slice
|
|
6842
|
+
point1_visible = (z1 == self.current_slice and
|
|
6843
|
+
current_xlim[0] <= x1 <= current_xlim[1] and
|
|
6844
|
+
current_ylim[1] <= y1 <= current_ylim[0])
|
|
6845
|
+
point2_visible = (z2 == self.current_slice and
|
|
6846
|
+
current_xlim[0] <= x2 <= current_xlim[1] and
|
|
6847
|
+
current_ylim[1] <= y2 <= current_ylim[0])
|
|
6848
|
+
|
|
6849
|
+
# Always draw individual points if they're on the current slice (even without lines)
|
|
6850
|
+
if point1_visible:
|
|
6851
|
+
pt1 = self.ax.plot(x1, y1, marker_color, markersize=8)[0]
|
|
6852
|
+
txt1 = self.ax.text(x1, y1+5, str(pair_idx), color=text_color, ha='center', va='bottom')
|
|
6853
|
+
self.measurement_artists.extend([pt1, txt1])
|
|
6854
|
+
|
|
6855
|
+
if point2_visible:
|
|
6856
|
+
pt2 = self.ax.plot(x2, y2, marker_color, markersize=8)[0]
|
|
6857
|
+
txt2 = self.ax.text(x2, y2+5, str(pair_idx), color=text_color, ha='center', va='bottom')
|
|
6858
|
+
self.measurement_artists.extend([pt2, txt2])
|
|
6859
|
+
|
|
6860
|
+
# Only draw connecting line if both points are on the same slice AND visible
|
|
6861
|
+
if z1 == z2 == self.current_slice and (point1_visible or point2_visible):
|
|
6862
|
+
line = self.ax.plot([x1, x2], [y1, y2], line_color, alpha=0.5)[0]
|
|
6863
|
+
self.measurement_artists.append(line)
|
|
6864
|
+
|
|
6865
|
+
# Also handle any partial measurements in progress (individual points without pairs yet)
|
|
6866
|
+
# This shows individual points even when a measurement isn't complete
|
|
6867
|
+
if hasattr(self, 'current_point') and self.current_point is not None:
|
|
6868
|
+
x, y, z = self.current_point
|
|
6869
|
+
if z == self.current_slice:
|
|
6870
|
+
if hasattr(self, 'measurement_mode') and self.measurement_mode == "angle":
|
|
6871
|
+
# Show green for angle mode
|
|
6872
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
6873
|
+
if hasattr(self, 'current_trio_index'):
|
|
6874
|
+
txt = self.ax.text(x, y+5, f"A{self.current_trio_index}", color='green', ha='center', va='bottom')
|
|
6875
|
+
else:
|
|
6876
|
+
txt = self.ax.text(x, y+5, "A", color='green', ha='center', va='bottom')
|
|
6877
|
+
else:
|
|
6878
|
+
# Show yellow for distance mode (default)
|
|
6879
|
+
pt = self.ax.plot(x, y, 'yo', markersize=8)[0]
|
|
6880
|
+
if hasattr(self, 'current_pair_index'):
|
|
6881
|
+
txt = self.ax.text(x, y+5, f"D{self.current_pair_index}", color='yellow', ha='center', va='bottom')
|
|
6882
|
+
else:
|
|
6883
|
+
txt = self.ax.text(x, y+5, "D", color='yellow', ha='center', va='bottom')
|
|
6884
|
+
self.measurement_artists.extend([pt, txt])
|
|
6885
|
+
|
|
6886
|
+
# Handle second point in angle measurements
|
|
6887
|
+
if hasattr(self, 'current_second_point') and self.current_second_point is not None:
|
|
6888
|
+
x, y, z = self.current_second_point
|
|
6889
|
+
if z == self.current_slice:
|
|
6890
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
6891
|
+
if hasattr(self, 'current_trio_index'):
|
|
6892
|
+
txt = self.ax.text(x, y+5, f"B{self.current_trio_index}", color='green', ha='center', va='bottom')
|
|
6893
|
+
else:
|
|
6894
|
+
txt = self.ax.text(x, y+5, "B", color='green', ha='center', va='bottom')
|
|
6895
|
+
self.measurement_artists.extend([pt, txt])
|
|
6328
6896
|
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6897
|
+
# Draw line from A to B if both are on current slice
|
|
6898
|
+
if (hasattr(self, 'current_point') and self.current_point is not None and
|
|
6899
|
+
self.current_point[2] == self.current_slice):
|
|
6900
|
+
x1, y1, z1 = self.current_point
|
|
6901
|
+
line = self.ax.plot([x1, x], [y1, y], 'g--', alpha=0.7)[0]
|
|
6902
|
+
self.measurement_artists.append(line)
|
|
6332
6903
|
|
|
6333
6904
|
# Store current view limits for next update
|
|
6334
6905
|
self.ax._current_xlim = current_xlim
|
|
@@ -6347,15 +6918,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6347
6918
|
if reset_resize:
|
|
6348
6919
|
self.resizing = False
|
|
6349
6920
|
|
|
6350
|
-
#
|
|
6921
|
+
# draw_idle
|
|
6351
6922
|
self.canvas.draw_idle()
|
|
6352
6923
|
|
|
6924
|
+
|
|
6353
6925
|
except Exception as e:
|
|
6354
6926
|
pass
|
|
6355
|
-
#import traceback
|
|
6356
|
-
#print(traceback.format_exc())
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
6927
|
|
|
6360
6928
|
|
|
6361
6929
|
def get_channel_image(self, channel):
|
|
@@ -6465,10 +7033,18 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6465
7033
|
dialog = RadDialog(self)
|
|
6466
7034
|
dialog.exec()
|
|
6467
7035
|
|
|
7036
|
+
def show_branchstat_dialog(self):
|
|
7037
|
+
dialog = BranchStatDialog(self)
|
|
7038
|
+
dialog.exec()
|
|
7039
|
+
|
|
6468
7040
|
def show_interaction_dialog(self):
|
|
6469
7041
|
dialog = InteractionDialog(self)
|
|
6470
7042
|
dialog.exec()
|
|
6471
7043
|
|
|
7044
|
+
def show_violin_dialog(self):
|
|
7045
|
+
dialog = ViolinDialog(self)
|
|
7046
|
+
dialog.show()
|
|
7047
|
+
|
|
6472
7048
|
def show_degree_dialog(self):
|
|
6473
7049
|
dialog = DegreeDialog(self)
|
|
6474
7050
|
dialog.exec()
|
|
@@ -6655,15 +7231,19 @@ class CustomTableView(QTableView):
|
|
|
6655
7231
|
desc_action.triggered.connect(lambda checked, c=col: self.sort_table(c, ascending=False))
|
|
6656
7232
|
|
|
6657
7233
|
# Different menus for top and bottom tables
|
|
6658
|
-
if self
|
|
7234
|
+
if self.is_top_table: # Use the flag instead of checking membership
|
|
6659
7235
|
save_menu = context_menu.addMenu("Save As")
|
|
6660
7236
|
save_csv = save_menu.addAction("CSV")
|
|
6661
7237
|
save_excel = save_menu.addAction("Excel")
|
|
7238
|
+
|
|
7239
|
+
if self.model() and len(self.model()._data.columns) == 2:
|
|
7240
|
+
thresh_action = context_menu.addAction("Use to Threshold Nodes")
|
|
7241
|
+
thresh_action.triggered.connect(lambda: self.thresh(self.create_threshold_dict()))
|
|
7242
|
+
|
|
6662
7243
|
close_action = context_menu.addAction("Close All")
|
|
6663
|
-
|
|
6664
7244
|
close_action.triggered.connect(self.close_all)
|
|
6665
7245
|
|
|
6666
|
-
# Connect the actions
|
|
7246
|
+
# Connect the save actions
|
|
6667
7247
|
save_csv.triggered.connect(lambda: self.save_table_as('csv'))
|
|
6668
7248
|
save_excel.triggered.connect(lambda: self.save_table_as('xlsx'))
|
|
6669
7249
|
else: # Bottom tables
|
|
@@ -6707,6 +7287,38 @@ class CustomTableView(QTableView):
|
|
|
6707
7287
|
cursor_pos = QCursor.pos()
|
|
6708
7288
|
context_menu.exec(cursor_pos)
|
|
6709
7289
|
|
|
7290
|
+
|
|
7291
|
+
|
|
7292
|
+
def thresh(self, special_dict):
|
|
7293
|
+
try:
|
|
7294
|
+
self.parent.special_dict = special_dict
|
|
7295
|
+
thresh_window = ThresholdWindow(self.parent, 4)
|
|
7296
|
+
thresh_window.show()
|
|
7297
|
+
except:
|
|
7298
|
+
pass
|
|
7299
|
+
|
|
7300
|
+
def create_threshold_dict(self):
|
|
7301
|
+
try:
|
|
7302
|
+
"""Create a dictionary from the 2-column table data."""
|
|
7303
|
+
if not self.model() or not hasattr(self.model(), '_data'):
|
|
7304
|
+
return {}
|
|
7305
|
+
|
|
7306
|
+
df = self.model()._data
|
|
7307
|
+
if len(df.columns) != 2:
|
|
7308
|
+
return {}
|
|
7309
|
+
|
|
7310
|
+
# Create dictionary: {column_0_value: column_1_value}
|
|
7311
|
+
threshold_dict = {}
|
|
7312
|
+
for index, row in df.iterrows():
|
|
7313
|
+
key = row.iloc[0] # Column 0 value
|
|
7314
|
+
value = row.iloc[1] # Column 1 value
|
|
7315
|
+
threshold_dict[int(key)] = float(value)
|
|
7316
|
+
|
|
7317
|
+
return threshold_dict
|
|
7318
|
+
except:
|
|
7319
|
+
pass
|
|
7320
|
+
|
|
7321
|
+
|
|
6710
7322
|
def sort_table(self, column, ascending=True):
|
|
6711
7323
|
"""Sort the table by the specified column."""
|
|
6712
7324
|
try:
|
|
@@ -7766,11 +8378,6 @@ class MergeNodeIdDialog(QDialog):
|
|
|
7766
8378
|
self.mode_selector.addItems(["Auto-Binarize(Otsu)/Presegmented", "Manual (Interactive Thresholder)"])
|
|
7767
8379
|
self.mode_selector.setCurrentIndex(1) # Default to Mode 1
|
|
7768
8380
|
layout.addRow("Binarization Strategy:", self.mode_selector)
|
|
7769
|
-
|
|
7770
|
-
self.umap = QPushButton("If using manual threshold: Also generate identity UMAP?")
|
|
7771
|
-
self.umap.setCheckable(True)
|
|
7772
|
-
self.umap.setChecked(True)
|
|
7773
|
-
layout.addWidget(self.umap)
|
|
7774
8381
|
|
|
7775
8382
|
self.include = QPushButton("Include When a Node is Negative for an ID?")
|
|
7776
8383
|
self.include.setCheckable(True)
|
|
@@ -7827,7 +8434,7 @@ class MergeNodeIdDialog(QDialog):
|
|
|
7827
8434
|
z_scale = float(self.z_scale.text()) if self.z_scale.text().strip() else 1
|
|
7828
8435
|
data = self.parent().channel_data[0]
|
|
7829
8436
|
include = self.include.isChecked()
|
|
7830
|
-
umap =
|
|
8437
|
+
umap = True
|
|
7831
8438
|
|
|
7832
8439
|
if data is None:
|
|
7833
8440
|
return
|
|
@@ -7963,15 +8570,13 @@ class MergeNodeIdDialog(QDialog):
|
|
|
7963
8570
|
result = {key: np.array([d[key] for d in id_dicts]) for key in all_keys}
|
|
7964
8571
|
|
|
7965
8572
|
|
|
7966
|
-
self.parent().format_for_upperright_table(result, 'NodeID', good_list, 'Mean Intensity')
|
|
7967
|
-
if umap:
|
|
7968
|
-
my_network.identity_umap(result)
|
|
8573
|
+
self.parent().format_for_upperright_table(result, 'NodeID', good_list, 'Mean Intensity (Save this Table for "Analyze -> Stats -> Show Violins")')
|
|
7969
8574
|
|
|
7970
8575
|
|
|
7971
8576
|
QMessageBox.information(
|
|
7972
8577
|
self,
|
|
7973
8578
|
"Success",
|
|
7974
|
-
"Node Identities Merged. New IDs represent presence of corresponding img foreground with +, absence with -.
|
|
8579
|
+
"Node Identities Merged. New IDs represent presence of corresponding img foreground with +, absence with -. If desired, please save your new identities as csv, then use File -> Load -> Load From Excel Helper to bulk search and rename desired combinations. If desired, please save the outputted mean intensity table to use with 'Analyze -> Stats -> Show Violins'. (Press Help [above] for more info)"
|
|
7975
8580
|
)
|
|
7976
8581
|
|
|
7977
8582
|
self.accept()
|
|
@@ -7993,6 +8598,89 @@ class MergeNodeIdDialog(QDialog):
|
|
|
7993
8598
|
print(traceback.format_exc())
|
|
7994
8599
|
#print(f"Error: {e}")
|
|
7995
8600
|
|
|
8601
|
+
class MultiChanDialog(QDialog):
|
|
8602
|
+
|
|
8603
|
+
def __init__(self, parent=None, data = None):
|
|
8604
|
+
|
|
8605
|
+
super().__init__(parent)
|
|
8606
|
+
self.setWindowTitle("Channel Loading")
|
|
8607
|
+
self.setModal(False)
|
|
8608
|
+
|
|
8609
|
+
layout = QFormLayout(self)
|
|
8610
|
+
|
|
8611
|
+
self.data = data
|
|
8612
|
+
|
|
8613
|
+
self.nodes = QComboBox()
|
|
8614
|
+
self.edges = QComboBox()
|
|
8615
|
+
self.overlay1 = QComboBox()
|
|
8616
|
+
self.overlay2 = QComboBox()
|
|
8617
|
+
options = ["None"]
|
|
8618
|
+
for i in range(self.data.shape[0]):
|
|
8619
|
+
options.append(str(i))
|
|
8620
|
+
self.nodes.addItems(options)
|
|
8621
|
+
self.edges.addItems(options)
|
|
8622
|
+
self.overlay1.addItems(options)
|
|
8623
|
+
self.overlay2.addItems(options)
|
|
8624
|
+
self.nodes.setCurrentIndex(0)
|
|
8625
|
+
self.edges.setCurrentIndex(0)
|
|
8626
|
+
self.overlay1.setCurrentIndex(0)
|
|
8627
|
+
self.overlay2.setCurrentIndex(0)
|
|
8628
|
+
layout.addRow("Load this channel into nodes?", self.nodes)
|
|
8629
|
+
layout.addRow("Load this channel into edges?", self.edges)
|
|
8630
|
+
layout.addRow("Load this channel into overlay1?", self.overlay1)
|
|
8631
|
+
layout.addRow("Load this channel into overlay2?", self.overlay2)
|
|
8632
|
+
|
|
8633
|
+
run_button = QPushButton("Load Channels")
|
|
8634
|
+
run_button.clicked.connect(self.run)
|
|
8635
|
+
layout.addWidget(run_button)
|
|
8636
|
+
|
|
8637
|
+
run_button2 = QPushButton("Save Channels to Directory")
|
|
8638
|
+
run_button2.clicked.connect(self.run2)
|
|
8639
|
+
layout.addWidget(run_button2)
|
|
8640
|
+
|
|
8641
|
+
|
|
8642
|
+
def run(self):
|
|
8643
|
+
|
|
8644
|
+
try:
|
|
8645
|
+
node_chan = int(self.nodes.currentText())
|
|
8646
|
+
self.parent().load_channel(0, self.data[node_chan, :, :, :], data = True)
|
|
8647
|
+
except:
|
|
8648
|
+
pass
|
|
8649
|
+
try:
|
|
8650
|
+
edge_chan = int(self.edges.currentText())
|
|
8651
|
+
self.parent().load_channel(1, self.data[edge_chan, :, :, :], data = True)
|
|
8652
|
+
except:
|
|
8653
|
+
pass
|
|
8654
|
+
try:
|
|
8655
|
+
overlay1_chan = int(self.overlay1.currentText())
|
|
8656
|
+
self.parent().load_channel(2, self.data[overlay1_chan, :, :, :], data = True)
|
|
8657
|
+
except:
|
|
8658
|
+
pass
|
|
8659
|
+
try:
|
|
8660
|
+
overlay2_chan = int(self.overlay2.currentText())
|
|
8661
|
+
self.parent().load_channel(3, self.data[overlay2_chan, :, :, :], data = True)
|
|
8662
|
+
except:
|
|
8663
|
+
pass
|
|
8664
|
+
|
|
8665
|
+
def run2(self):
|
|
8666
|
+
|
|
8667
|
+
try:
|
|
8668
|
+
# First let user select parent directory
|
|
8669
|
+
parent_dir = QFileDialog.getExistingDirectory(
|
|
8670
|
+
self,
|
|
8671
|
+
"Select Location to Save Channels",
|
|
8672
|
+
"",
|
|
8673
|
+
QFileDialog.Option.ShowDirsOnly
|
|
8674
|
+
)
|
|
8675
|
+
|
|
8676
|
+
for i in range(self.data.shape[0]):
|
|
8677
|
+
try:
|
|
8678
|
+
tifffile.imwrite(f'{parent_dir}/C{i}.tif', self.data[i, :, :, :])
|
|
8679
|
+
except:
|
|
8680
|
+
continue
|
|
8681
|
+
except:
|
|
8682
|
+
pass
|
|
8683
|
+
|
|
7996
8684
|
|
|
7997
8685
|
class Show3dDialog(QDialog):
|
|
7998
8686
|
def __init__(self, parent=None):
|
|
@@ -8060,6 +8748,9 @@ class Show3dDialog(QDialog):
|
|
|
8060
8748
|
if visible:
|
|
8061
8749
|
arrays_4d.append(channel)
|
|
8062
8750
|
|
|
8751
|
+
if self.parent().thresh_window_ref is not None:
|
|
8752
|
+
self.parent().thresh_window_ref.make_full_highlight()
|
|
8753
|
+
|
|
8063
8754
|
if self.parent().highlight_overlay is not None or self.parent().mini_overlay_data is not None:
|
|
8064
8755
|
if self.parent().mini_overlay == True:
|
|
8065
8756
|
self.parent().create_highlight_overlay(node_indices = self.parent().clicked_values['nodes'], edge_indices = self.parent().clicked_values['edges'])
|
|
@@ -8071,6 +8762,11 @@ class Show3dDialog(QDialog):
|
|
|
8071
8762
|
self.accept()
|
|
8072
8763
|
|
|
8073
8764
|
except Exception as e:
|
|
8765
|
+
QMessageBox.critical(
|
|
8766
|
+
self,
|
|
8767
|
+
"Error",
|
|
8768
|
+
f"Error showing 3D: {str(e)}\nNote: You may need to install napari first - in your environment, please call 'pip install napari'"
|
|
8769
|
+
)
|
|
8074
8770
|
print(f"Error: {e}")
|
|
8075
8771
|
import traceback
|
|
8076
8772
|
print(traceback.format_exc())
|
|
@@ -8086,6 +8782,9 @@ class NetOverlayDialog(QDialog):
|
|
|
8086
8782
|
|
|
8087
8783
|
layout = QFormLayout(self)
|
|
8088
8784
|
|
|
8785
|
+
self.downsample = QLineEdit("")
|
|
8786
|
+
layout.addRow("Downsample Factor While Drawing? (Int - Makes the outputted lines larger):", self.downsample)
|
|
8787
|
+
|
|
8089
8788
|
# Add Run button
|
|
8090
8789
|
run_button = QPushButton("Generate (Will go to Overlay 1)")
|
|
8091
8790
|
run_button.clicked.connect(self.netoverlay)
|
|
@@ -8102,7 +8801,16 @@ class NetOverlayDialog(QDialog):
|
|
|
8102
8801
|
if my_network.node_centroids is None:
|
|
8103
8802
|
return
|
|
8104
8803
|
|
|
8105
|
-
|
|
8804
|
+
try:
|
|
8805
|
+
downsample = float(self.downsample.text()) if self.downsample.text() else None
|
|
8806
|
+
except ValueError:
|
|
8807
|
+
downsample = None
|
|
8808
|
+
|
|
8809
|
+
my_network.network_overlay = my_network.draw_network(down_factor = downsample)
|
|
8810
|
+
|
|
8811
|
+
if downsample is not None:
|
|
8812
|
+
my_network.network_overlay = n3d.upsample_with_padding(my_network.network_overlay, original_shape = self.parent().shape)
|
|
8813
|
+
|
|
8106
8814
|
|
|
8107
8815
|
self.parent().load_channel(2, channel_data = my_network.network_overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
8108
8816
|
|
|
@@ -8129,6 +8837,9 @@ class IdOverlayDialog(QDialog):
|
|
|
8129
8837
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
8130
8838
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
8131
8839
|
|
|
8840
|
+
self.downsample = QLineEdit("")
|
|
8841
|
+
layout.addRow("Downsample Factor While Drawing? (Int - Makes the outputted numbers larger):", self.downsample)
|
|
8842
|
+
|
|
8132
8843
|
# Add Run button
|
|
8133
8844
|
run_button = QPushButton("Generate (Will go to Overlay 2)")
|
|
8134
8845
|
run_button.clicked.connect(self.idoverlay)
|
|
@@ -8136,38 +8847,51 @@ class IdOverlayDialog(QDialog):
|
|
|
8136
8847
|
|
|
8137
8848
|
def idoverlay(self):
|
|
8138
8849
|
|
|
8139
|
-
|
|
8850
|
+
try:
|
|
8140
8851
|
|
|
8141
|
-
|
|
8852
|
+
accepted_mode = self.mode_selector.currentIndex()
|
|
8142
8853
|
|
|
8143
|
-
|
|
8854
|
+
try:
|
|
8855
|
+
downsample = float(self.downsample.text()) if self.downsample.text() else None
|
|
8856
|
+
except ValueError:
|
|
8857
|
+
downsample = None
|
|
8144
8858
|
|
|
8145
|
-
|
|
8859
|
+
if accepted_mode == 0:
|
|
8146
8860
|
|
|
8147
|
-
|
|
8148
|
-
return
|
|
8861
|
+
if my_network.node_centroids is None:
|
|
8149
8862
|
|
|
8150
|
-
|
|
8863
|
+
self.parent().show_centroid_dialog()
|
|
8151
8864
|
|
|
8152
|
-
|
|
8865
|
+
if my_network.node_centroids is None:
|
|
8866
|
+
return
|
|
8153
8867
|
|
|
8154
|
-
|
|
8868
|
+
elif accepted_mode == 1:
|
|
8155
8869
|
|
|
8156
|
-
|
|
8157
|
-
|
|
8870
|
+
if my_network.edge_centroids is None:
|
|
8871
|
+
|
|
8872
|
+
self.parent().show_centroid_dialog()
|
|
8158
8873
|
|
|
8159
|
-
|
|
8874
|
+
if my_network.edge_centroids is None:
|
|
8875
|
+
return
|
|
8160
8876
|
|
|
8161
|
-
|
|
8877
|
+
if accepted_mode == 0:
|
|
8162
8878
|
|
|
8163
|
-
|
|
8879
|
+
my_network.id_overlay = my_network.draw_node_indices(down_factor = downsample)
|
|
8164
8880
|
|
|
8165
|
-
|
|
8881
|
+
elif accepted_mode == 1:
|
|
8166
8882
|
|
|
8883
|
+
my_network.id_overlay = my_network.draw_edge_indices(down_factor = downsample)
|
|
8167
8884
|
|
|
8168
|
-
|
|
8885
|
+
if downsample is not None:
|
|
8886
|
+
my_network.id_overlay = n3d.upsample_with_padding(my_network.id_overlay, original_shape = self.parent().shape)
|
|
8887
|
+
|
|
8888
|
+
self.parent().load_channel(3, channel_data = my_network.id_overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
8889
|
+
|
|
8890
|
+
self.accept()
|
|
8891
|
+
|
|
8892
|
+
except:
|
|
8893
|
+
print(f"Error with Overlay Generation: {e}")
|
|
8169
8894
|
|
|
8170
|
-
self.accept()
|
|
8171
8895
|
|
|
8172
8896
|
class ColorOverlayDialog(QDialog):
|
|
8173
8897
|
|
|
@@ -8189,7 +8913,7 @@ class ColorOverlayDialog(QDialog):
|
|
|
8189
8913
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
8190
8914
|
|
|
8191
8915
|
self.down_factor = QLineEdit("")
|
|
8192
|
-
layout.addRow("down_factor (for speeding up overlay generation - optional):", self.down_factor)
|
|
8916
|
+
layout.addRow("down_factor (int - for speeding up overlay generation - optional):", self.down_factor)
|
|
8193
8917
|
|
|
8194
8918
|
# Add Run button
|
|
8195
8919
|
run_button = QPushButton("Generate (Will go to Overlay 2)")
|
|
@@ -8343,11 +9067,6 @@ class NetShowDialog(QDialog):
|
|
|
8343
9067
|
self.weighted.setCheckable(True)
|
|
8344
9068
|
self.weighted.setChecked(True)
|
|
8345
9069
|
layout.addRow("Use Weighted Network (Only for community graphs):", self.weighted)
|
|
8346
|
-
|
|
8347
|
-
# Optional saving:
|
|
8348
|
-
self.directory = QLineEdit()
|
|
8349
|
-
self.directory.setPlaceholderText("Does not save when empty")
|
|
8350
|
-
layout.addRow("Output Directory:", self.directory)
|
|
8351
9070
|
|
|
8352
9071
|
# Add Run button
|
|
8353
9072
|
run_button = QPushButton("Show Network")
|
|
@@ -8363,7 +9082,7 @@ class NetShowDialog(QDialog):
|
|
|
8363
9082
|
self.parent().show_centroid_dialog()
|
|
8364
9083
|
accepted_mode = self.mode_selector.currentIndex() # Convert to 1-based index
|
|
8365
9084
|
# Get directory (None if empty)
|
|
8366
|
-
directory =
|
|
9085
|
+
directory = None
|
|
8367
9086
|
|
|
8368
9087
|
weighted = self.weighted.isChecked()
|
|
8369
9088
|
|
|
@@ -8406,7 +9125,7 @@ class PartitionDialog(QDialog):
|
|
|
8406
9125
|
|
|
8407
9126
|
# Add mode selection dropdown
|
|
8408
9127
|
self.mode_selector = QComboBox()
|
|
8409
|
-
self.mode_selector.addItems(["
|
|
9128
|
+
self.mode_selector.addItems(["Louvain", "Label Propogation"])
|
|
8410
9129
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
8411
9130
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
8412
9131
|
|
|
@@ -8429,6 +9148,10 @@ class PartitionDialog(QDialog):
|
|
|
8429
9148
|
self.parent().prev_coms = None
|
|
8430
9149
|
|
|
8431
9150
|
accepted_mode = self.mode_selector.currentIndex()
|
|
9151
|
+
if accepted_mode == 0: #I switched where these are in the selection box
|
|
9152
|
+
accepted_mode = 1
|
|
9153
|
+
elif accepted_mode == 1:
|
|
9154
|
+
accepted_mode = 0
|
|
8432
9155
|
weighted = self.weighted.isChecked()
|
|
8433
9156
|
dostats = self.stats.isChecked()
|
|
8434
9157
|
|
|
@@ -8600,7 +9323,7 @@ class ComNeighborDialog(QDialog):
|
|
|
8600
9323
|
|
|
8601
9324
|
mode = self.mode.currentIndex()
|
|
8602
9325
|
|
|
8603
|
-
seed =
|
|
9326
|
+
seed = int(self.seed.text()) if self.seed.text().strip() else 42
|
|
8604
9327
|
|
|
8605
9328
|
limit = int(self.limit.text()) if self.limit.text().strip() else None
|
|
8606
9329
|
|
|
@@ -8705,9 +9428,6 @@ class RadialDialog(QDialog):
|
|
|
8705
9428
|
self.distance = QLineEdit("50")
|
|
8706
9429
|
layout.addRow("Bucket Distance for Searching For Node Neighbors (automatically scaled by xy and z scales):", self.distance)
|
|
8707
9430
|
|
|
8708
|
-
self.directory = QLineEdit("")
|
|
8709
|
-
layout.addRow("Output Directory:", self.directory)
|
|
8710
|
-
|
|
8711
9431
|
# Add Run button
|
|
8712
9432
|
run_button = QPushButton("Get Radial Distribution")
|
|
8713
9433
|
run_button.clicked.connect(self.radial)
|
|
@@ -8719,7 +9439,7 @@ class RadialDialog(QDialog):
|
|
|
8719
9439
|
|
|
8720
9440
|
distance = float(self.distance.text()) if self.distance.text().strip() else 50
|
|
8721
9441
|
|
|
8722
|
-
directory =
|
|
9442
|
+
directory = None
|
|
8723
9443
|
|
|
8724
9444
|
if my_network.node_centroids is None:
|
|
8725
9445
|
self.parent().show_centroid_dialog()
|
|
@@ -8750,12 +9470,16 @@ class NearNeighDialog(QDialog):
|
|
|
8750
9470
|
if my_network.node_identities is not None:
|
|
8751
9471
|
|
|
8752
9472
|
self.root = QComboBox()
|
|
8753
|
-
|
|
9473
|
+
roots = list(set(my_network.node_identities.values()))
|
|
9474
|
+
roots.sort()
|
|
9475
|
+
roots.append("All (Excluding Targets)")
|
|
9476
|
+
self.root.addItems(roots)
|
|
8754
9477
|
self.root.setCurrentIndex(0)
|
|
8755
9478
|
identities_layout.addRow("Root Identity to Search for Neighbor's IDs?", self.root)
|
|
8756
9479
|
|
|
8757
9480
|
self.targ = QComboBox()
|
|
8758
9481
|
neighs = list(set(my_network.node_identities.values()))
|
|
9482
|
+
neighs.sort()
|
|
8759
9483
|
neighs.append("All Others (Excluding Self)")
|
|
8760
9484
|
self.targ.addItems(neighs)
|
|
8761
9485
|
self.targ.setCurrentIndex(0)
|
|
@@ -8793,6 +9517,11 @@ class NearNeighDialog(QDialog):
|
|
|
8793
9517
|
self.numpy.setChecked(False)
|
|
8794
9518
|
self.numpy.clicked.connect(self.toggle_map)
|
|
8795
9519
|
heatmap_layout.addRow("Overlay:", self.numpy)
|
|
9520
|
+
|
|
9521
|
+
self.mode = QComboBox()
|
|
9522
|
+
self.mode.addItems(["Anywhere", "Within Masked Bounds of Edges", "Within Masked Bounds of Overlay1", "Within Masked Bounds of Overlay2"])
|
|
9523
|
+
self.mode.setCurrentIndex(0)
|
|
9524
|
+
heatmap_layout.addRow("For heatmap, measure theoretical point distribution how?", self.mode)
|
|
8796
9525
|
|
|
8797
9526
|
main_layout.addWidget(heatmap_group)
|
|
8798
9527
|
|
|
@@ -8880,35 +9609,52 @@ class NearNeighDialog(QDialog):
|
|
|
8880
9609
|
except:
|
|
8881
9610
|
targ = None
|
|
8882
9611
|
|
|
9612
|
+
if root == "All (Excluding Targets)" and targ == 'All Others (Excluding Self)':
|
|
9613
|
+
root = None
|
|
9614
|
+
targ = None
|
|
9615
|
+
|
|
9616
|
+
mode = self.mode.currentIndex()
|
|
9617
|
+
|
|
9618
|
+
if mode == 0:
|
|
9619
|
+
mask = None
|
|
9620
|
+
else:
|
|
9621
|
+
try:
|
|
9622
|
+
mask = self.parent().channel_data[mode] != 0
|
|
9623
|
+
except:
|
|
9624
|
+
print("Could not binarize mask")
|
|
9625
|
+
mask = None
|
|
9626
|
+
|
|
8883
9627
|
heatmap = self.map.isChecked()
|
|
8884
9628
|
threed = self.threed.isChecked()
|
|
8885
9629
|
numpy = self.numpy.isChecked()
|
|
8886
9630
|
num = int(self.num.text()) if self.num.text().strip() else 1
|
|
8887
9631
|
quant = self.quant.isChecked()
|
|
8888
9632
|
centroids = self.centroids.isChecked()
|
|
9633
|
+
|
|
8889
9634
|
if not centroids:
|
|
9635
|
+
print("Using 1 nearest neighbor due to not using centroids")
|
|
8890
9636
|
num = 1
|
|
8891
9637
|
|
|
8892
9638
|
if root is not None and targ is not None:
|
|
8893
9639
|
title = f"Nearest {num} Neighbor(s) Distance of {targ} from {root}"
|
|
8894
|
-
header = f"Shortest Distance to Closest {num} {targ}(s)"
|
|
9640
|
+
header = f"Avg Shortest Distance to Closest {num} {targ}(s)"
|
|
8895
9641
|
header2 = f"{root} Node ID"
|
|
8896
9642
|
header3 = f'Theoretical Uniform Distance to Closest {num} {targ}(s)'
|
|
8897
9643
|
else:
|
|
8898
9644
|
title = f"Nearest {num} Neighbor(s) Distance Between Nodes"
|
|
8899
|
-
header = f"Shortest Distance to Closest {num} Nodes"
|
|
9645
|
+
header = f"Avg Shortest Distance to Closest {num} Nodes"
|
|
8900
9646
|
header2 = "Root Node ID"
|
|
8901
9647
|
header3 = f'Simulated Theoretical Uniform Distance to Closest {num} Nodes'
|
|
8902
9648
|
|
|
8903
|
-
if
|
|
9649
|
+
if my_network.node_centroids is None:
|
|
8904
9650
|
self.parent().show_centroid_dialog()
|
|
8905
9651
|
if my_network.node_centroids is None:
|
|
8906
9652
|
return
|
|
8907
9653
|
|
|
8908
9654
|
if not numpy:
|
|
8909
|
-
avg, output, quant_overlay, pred = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, quant = quant, centroids = centroids)
|
|
9655
|
+
avg, output, quant_overlay, pred = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, quant = quant, centroids = centroids, mask = mask)
|
|
8910
9656
|
else:
|
|
8911
|
-
avg, output, overlay, quant_overlay, pred = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, numpy = True, quant = quant, centroids = centroids)
|
|
9657
|
+
avg, output, overlay, quant_overlay, pred = my_network.nearest_neighbors_avg(root, targ, my_network.xy_scale, my_network.z_scale, num = num, heatmap = heatmap, threed = threed, numpy = True, quant = quant, centroids = centroids, mask = mask)
|
|
8912
9658
|
self.parent().load_channel(3, overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
8913
9659
|
|
|
8914
9660
|
if quant_overlay is not None:
|
|
@@ -8977,9 +9723,6 @@ class NeighborIdentityDialog(QDialog):
|
|
|
8977
9723
|
else:
|
|
8978
9724
|
self.root = None
|
|
8979
9725
|
|
|
8980
|
-
self.directory = QLineEdit("")
|
|
8981
|
-
layout.addRow("Output Directory:", self.directory)
|
|
8982
|
-
|
|
8983
9726
|
self.mode = QComboBox()
|
|
8984
9727
|
self.mode.addItems(["From Network - Based on Absolute Connectivity", "Use Labeled Nodes - Based on Morphological Neighborhood Densities"])
|
|
8985
9728
|
self.mode.setCurrentIndex(0)
|
|
@@ -9007,7 +9750,7 @@ class NeighborIdentityDialog(QDialog):
|
|
|
9007
9750
|
except:
|
|
9008
9751
|
pass
|
|
9009
9752
|
|
|
9010
|
-
directory =
|
|
9753
|
+
directory = None
|
|
9011
9754
|
|
|
9012
9755
|
mode = self.mode.currentIndex()
|
|
9013
9756
|
|
|
@@ -9461,6 +10204,22 @@ class InteractionDialog(QDialog):
|
|
|
9461
10204
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
9462
10205
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
9463
10206
|
|
|
10207
|
+
self.length = QPushButton("Return Lengths")
|
|
10208
|
+
self.length.setCheckable(True)
|
|
10209
|
+
self.length.setChecked(False)
|
|
10210
|
+
layout.addRow("(Will Skeletonize the Edge Mirror and use that to calculate adjacent length of edges, as opposed to default volumes):", self.length)
|
|
10211
|
+
|
|
10212
|
+
self.auto = QPushButton("Auto")
|
|
10213
|
+
self.auto.setCheckable(True)
|
|
10214
|
+
try:
|
|
10215
|
+
if self.parent().shape[0] == 1:
|
|
10216
|
+
self.auto.setChecked(False)
|
|
10217
|
+
else:
|
|
10218
|
+
self.auto.setChecked(True)
|
|
10219
|
+
except:
|
|
10220
|
+
self.auto.setChecked(False)
|
|
10221
|
+
layout.addRow("(If Above): Attempt to Auto Correct Skeleton Looping:", self.auto)
|
|
10222
|
+
|
|
9464
10223
|
self.fastdil = QPushButton("Fast Dilate")
|
|
9465
10224
|
self.fastdil.setCheckable(True)
|
|
9466
10225
|
self.fastdil.setChecked(False)
|
|
@@ -9484,10 +10243,16 @@ class InteractionDialog(QDialog):
|
|
|
9484
10243
|
|
|
9485
10244
|
|
|
9486
10245
|
fastdil = self.fastdil.isChecked()
|
|
10246
|
+
length = self.length.isChecked()
|
|
10247
|
+
auto = self.auto.isChecked()
|
|
10248
|
+
|
|
10249
|
+
result = my_network.interactions(search = node_search, cores = accepted_mode, skele = length, length = length, auto = auto, fastdil = fastdil)
|
|
9487
10250
|
|
|
9488
|
-
|
|
10251
|
+
if not length:
|
|
10252
|
+
self.parent().format_for_upperright_table(result, 'Node ID', ['Volume of Nearby Edge (Scaled)', 'Volume of Search Region (Scaled)'], title = 'Node/Edge Interactions')
|
|
10253
|
+
else:
|
|
10254
|
+
self.parent().format_for_upperright_table(result, 'Node ID', ['~Length of Nearby Edge (Scaled)', 'Volume of Search Region (Scaled)'], title = 'Node/Edge Interactions')
|
|
9489
10255
|
|
|
9490
|
-
self.parent().format_for_upperright_table(result, 'Node ID', ['Volume of Nearby Edge (Scaled)', 'Volume of Search Region'], title = 'Node/Edge Interactions')
|
|
9491
10256
|
|
|
9492
10257
|
self.accept()
|
|
9493
10258
|
|
|
@@ -9499,21 +10264,422 @@ class InteractionDialog(QDialog):
|
|
|
9499
10264
|
print(f"Error finding interactions: {e}")
|
|
9500
10265
|
|
|
9501
10266
|
|
|
9502
|
-
class
|
|
9503
|
-
|
|
10267
|
+
class ViolinDialog(QDialog):
|
|
9504
10268
|
|
|
9505
10269
|
def __init__(self, parent=None):
|
|
9506
10270
|
|
|
9507
10271
|
super().__init__(parent)
|
|
9508
|
-
self.setWindowTitle("Degree Parameters")
|
|
9509
|
-
self.setModal(True)
|
|
9510
|
-
|
|
9511
|
-
layout = QFormLayout(self)
|
|
9512
10272
|
|
|
9513
|
-
|
|
10273
|
+
QMessageBox.critical(
|
|
10274
|
+
self,
|
|
10275
|
+
"Notice",
|
|
10276
|
+
"Please select spreadsheet (Should be table output of 'File -> Images -> Node Identities -> Assign Node Identities from Overlap with Other Images'. Make sure to save that table as .csv/.xlsx and then load it here to use this.)"
|
|
10277
|
+
)
|
|
9514
10278
|
|
|
9515
|
-
|
|
9516
|
-
|
|
10279
|
+
try:
|
|
10280
|
+
try:
|
|
10281
|
+
self.df = self.parent().load_file()
|
|
10282
|
+
except:
|
|
10283
|
+
return
|
|
10284
|
+
|
|
10285
|
+
self.backup_df = copy.deepcopy(self.df)
|
|
10286
|
+
try:
|
|
10287
|
+
# Get all identity lists and normalize the dataframe
|
|
10288
|
+
identity_lists = self.get_all_identity_lists()
|
|
10289
|
+
self.df = self.normalize_df_with_identity_centerpoints(self.df, identity_lists)
|
|
10290
|
+
except:
|
|
10291
|
+
pass
|
|
10292
|
+
|
|
10293
|
+
self.setWindowTitle("Violin Parameters")
|
|
10294
|
+
self.setModal(False)
|
|
10295
|
+
|
|
10296
|
+
layout = QFormLayout(self)
|
|
10297
|
+
|
|
10298
|
+
if my_network.node_identities is not None:
|
|
10299
|
+
|
|
10300
|
+
self.idens = QComboBox()
|
|
10301
|
+
all_idens = list(set(my_network.node_identities.values()))
|
|
10302
|
+
idens = []
|
|
10303
|
+
for iden in all_idens:
|
|
10304
|
+
if '[' not in iden:
|
|
10305
|
+
idens.append(iden)
|
|
10306
|
+
idens.sort()
|
|
10307
|
+
idens.insert(0, "None")
|
|
10308
|
+
self.idens.addItems(idens)
|
|
10309
|
+
self.idens.setCurrentIndex(0)
|
|
10310
|
+
layout.addRow("Return Identity Violin Plots?", self.idens)
|
|
10311
|
+
|
|
10312
|
+
if my_network.communities is not None:
|
|
10313
|
+
self.coms = QComboBox()
|
|
10314
|
+
coms = list(set(my_network.communities.values()))
|
|
10315
|
+
coms.sort()
|
|
10316
|
+
coms.insert(0, "None")
|
|
10317
|
+
coms = [str(x) for x in coms]
|
|
10318
|
+
self.coms.addItems(coms)
|
|
10319
|
+
self.coms.setCurrentIndex(0)
|
|
10320
|
+
layout.addRow("Return Neighborhood/Community Violin Plots?", self.coms)
|
|
10321
|
+
|
|
10322
|
+
# Add Run button
|
|
10323
|
+
run_button = QPushButton("Show Z-score-like Violin")
|
|
10324
|
+
run_button.clicked.connect(self.run)
|
|
10325
|
+
layout.addWidget(run_button)
|
|
10326
|
+
|
|
10327
|
+
run_button2 = QPushButton("Show Z-score UMAP")
|
|
10328
|
+
run_button2.clicked.connect(self.run2)
|
|
10329
|
+
|
|
10330
|
+
self.mode_selector = QComboBox()
|
|
10331
|
+
self.mode_selector.addItems(["Label UMAP By Identity", "Label UMAP By Neighborhood/Community"])
|
|
10332
|
+
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
10333
|
+
layout.addRow("Execution Mode:", self.mode_selector)
|
|
10334
|
+
|
|
10335
|
+
layout.addRow(self.mode_selector, run_button2)
|
|
10336
|
+
|
|
10337
|
+
# Button in left column, input in right column
|
|
10338
|
+
run_button3 = QPushButton("Assign Neighborhoods via KMeans Clustering")
|
|
10339
|
+
run_button3.clicked.connect(self.run3)
|
|
10340
|
+
|
|
10341
|
+
self.kmeans_num_input = QLineEdit()
|
|
10342
|
+
self.kmeans_num_input.setPlaceholderText("Auto (num neighborhoods)")
|
|
10343
|
+
self.kmeans_num_input.setMaximumWidth(150)
|
|
10344
|
+
from PyQt6.QtGui import QIntValidator
|
|
10345
|
+
self.kmeans_num_input.setValidator(QIntValidator(1, 1000))
|
|
10346
|
+
|
|
10347
|
+
# QFormLayout's addRow takes (label/widget, field/widget)
|
|
10348
|
+
layout.addRow(run_button3, self.kmeans_num_input)
|
|
10349
|
+
|
|
10350
|
+
except:
|
|
10351
|
+
import traceback
|
|
10352
|
+
print(traceback.format_exc())
|
|
10353
|
+
QTimer.singleShot(0, self.close)
|
|
10354
|
+
|
|
10355
|
+
def get_all_identity_lists(self):
|
|
10356
|
+
"""
|
|
10357
|
+
Get all identity lists for normalization purposes.
|
|
10358
|
+
|
|
10359
|
+
Returns:
|
|
10360
|
+
dict: Dictionary where keys are identity names and values are lists of node IDs
|
|
10361
|
+
"""
|
|
10362
|
+
identity_lists = {}
|
|
10363
|
+
|
|
10364
|
+
# Get all unique identities
|
|
10365
|
+
all_identities = set()
|
|
10366
|
+
import ast
|
|
10367
|
+
for item in my_network.node_identities:
|
|
10368
|
+
try:
|
|
10369
|
+
parse = ast.literal_eval(my_network.node_identities[item])
|
|
10370
|
+
if isinstance(parse, (list, tuple, set)):
|
|
10371
|
+
all_identities.update(parse)
|
|
10372
|
+
else:
|
|
10373
|
+
all_identities.add(str(parse))
|
|
10374
|
+
except:
|
|
10375
|
+
all_identities.add(str(my_network.node_identities[item]))
|
|
10376
|
+
|
|
10377
|
+
# For each identity, get the list of nodes that have it
|
|
10378
|
+
for identity in all_identities:
|
|
10379
|
+
iden_list = []
|
|
10380
|
+
for item in my_network.node_identities:
|
|
10381
|
+
try:
|
|
10382
|
+
parse = ast.literal_eval(my_network.node_identities[item])
|
|
10383
|
+
if identity in parse:
|
|
10384
|
+
iden_list.append(item)
|
|
10385
|
+
except:
|
|
10386
|
+
if identity == str(my_network.node_identities[item]):
|
|
10387
|
+
iden_list.append(item)
|
|
10388
|
+
|
|
10389
|
+
if iden_list: # Only add if we found nodes
|
|
10390
|
+
identity_lists[identity] = iden_list
|
|
10391
|
+
|
|
10392
|
+
return identity_lists
|
|
10393
|
+
|
|
10394
|
+
def prepare_data_for_umap(self, df, node_identities=None):
|
|
10395
|
+
"""
|
|
10396
|
+
Prepare data for UMAP visualization by z-score normalizing columns.
|
|
10397
|
+
|
|
10398
|
+
Args:
|
|
10399
|
+
df: DataFrame with first column as NodeID, rest as marker intensities
|
|
10400
|
+
node_identities: Optional dict mapping node_id (int) -> identity (string).
|
|
10401
|
+
If provided, only nodes present as keys will be kept.
|
|
10402
|
+
|
|
10403
|
+
Returns:
|
|
10404
|
+
dict: {node_id: [normalized_marker_values]}
|
|
10405
|
+
"""
|
|
10406
|
+
from sklearn.preprocessing import StandardScaler
|
|
10407
|
+
import numpy as np
|
|
10408
|
+
|
|
10409
|
+
# Filter dataframe if node_identities is provided
|
|
10410
|
+
if my_network.node_identities is not None:
|
|
10411
|
+
# Get the valid node IDs from node_identities keys
|
|
10412
|
+
valid_node_ids = set(my_network.node_identities.keys())
|
|
10413
|
+
|
|
10414
|
+
# Filter df to only keep rows where first column value is in valid_node_ids
|
|
10415
|
+
mask = df.iloc[:, 0].isin(valid_node_ids)
|
|
10416
|
+
df = df[mask].copy()
|
|
10417
|
+
|
|
10418
|
+
# Optional: Check if any rows remain after filtering
|
|
10419
|
+
if len(df) == 0:
|
|
10420
|
+
raise ValueError("No matching nodes found between df and node_identities")
|
|
10421
|
+
|
|
10422
|
+
# Extract node IDs from first column
|
|
10423
|
+
node_ids = df.iloc[:, 0].values
|
|
10424
|
+
|
|
10425
|
+
# Extract marker data (all columns except first)
|
|
10426
|
+
X = df.iloc[:, 1:].values
|
|
10427
|
+
|
|
10428
|
+
# Z-score normalization (column-wise)
|
|
10429
|
+
scaler = StandardScaler()
|
|
10430
|
+
X_normalized = scaler.fit_transform(X)
|
|
10431
|
+
|
|
10432
|
+
# Create dictionary mapping node_id -> normalized row
|
|
10433
|
+
result_dict = {
|
|
10434
|
+
int(node_ids[i]): X_normalized[i].tolist()
|
|
10435
|
+
for i in range(len(node_ids))
|
|
10436
|
+
}
|
|
10437
|
+
|
|
10438
|
+
return result_dict
|
|
10439
|
+
|
|
10440
|
+
|
|
10441
|
+
def normalize_df_with_identity_centerpoints(self, df, identity_lists):
|
|
10442
|
+
"""
|
|
10443
|
+
Normalize the entire dataframe using identity-specific centerpoints.
|
|
10444
|
+
Uses Z-score-like normalization with identity centerpoint as the "mean".
|
|
10445
|
+
|
|
10446
|
+
Parameters:
|
|
10447
|
+
df (pd.DataFrame): Original dataframe
|
|
10448
|
+
identity_lists (dict): Dictionary where keys are identity names and values are lists of node IDs
|
|
10449
|
+
|
|
10450
|
+
Returns:
|
|
10451
|
+
pd.DataFrame: Normalized dataframe
|
|
10452
|
+
"""
|
|
10453
|
+
# Make a copy to avoid modifying the original dataframe
|
|
10454
|
+
df_copy = df.copy()
|
|
10455
|
+
|
|
10456
|
+
# Set the first column as the index (row headers)
|
|
10457
|
+
df_copy = df_copy.set_index(df_copy.columns[0])
|
|
10458
|
+
|
|
10459
|
+
# Convert all remaining columns to float type (batch conversion)
|
|
10460
|
+
df_copy = df_copy.astype(float)
|
|
10461
|
+
|
|
10462
|
+
# First, calculate the centerpoint for each column by finding the min across all identity groups
|
|
10463
|
+
column_centerpoints = {}
|
|
10464
|
+
|
|
10465
|
+
for column in df_copy.columns:
|
|
10466
|
+
centerpoint = None
|
|
10467
|
+
|
|
10468
|
+
for identity, node_list in identity_lists.items():
|
|
10469
|
+
# Get nodes that exist in both the identity list and the dataframe
|
|
10470
|
+
valid_nodes = [node for node in node_list if node in df_copy.index]
|
|
10471
|
+
if valid_nodes and ((str(identity) == str(column)) or str(identity) == f'{str(column)}+'):
|
|
10472
|
+
# Get the min value for this identity in this column
|
|
10473
|
+
identity_min = df_copy.loc[valid_nodes, column].min()
|
|
10474
|
+
centerpoint = identity_min
|
|
10475
|
+
break # Found the match, no need to continue
|
|
10476
|
+
|
|
10477
|
+
if centerpoint is not None:
|
|
10478
|
+
# Use the identity-specific centerpoint
|
|
10479
|
+
column_centerpoints[column] = centerpoint
|
|
10480
|
+
else:
|
|
10481
|
+
# Fallback: if no matching identity, use column median
|
|
10482
|
+
print(f"Could not find {str(column)} in node identities. As a fallback, using the median of all values in this channel rather than the minimum of user-designated valid values.")
|
|
10483
|
+
column_centerpoints[column] = df_copy[column].median()
|
|
10484
|
+
|
|
10485
|
+
# Now normalize each column using Z-score-like calculation with identity centerpoint
|
|
10486
|
+
df_normalized = df_copy.copy()
|
|
10487
|
+
for column in df_copy.columns:
|
|
10488
|
+
centerpoint = column_centerpoints[column]
|
|
10489
|
+
# Calculate standard deviation of the column
|
|
10490
|
+
std_dev = df_copy[column].std()
|
|
10491
|
+
|
|
10492
|
+
if std_dev > 0: # Avoid division by zero
|
|
10493
|
+
# Z-score-like: (value - centerpoint) / std_dev
|
|
10494
|
+
df_normalized[column] = (df_copy[column] - centerpoint) / std_dev
|
|
10495
|
+
else:
|
|
10496
|
+
# If std_dev is 0, just subtract centerpoint
|
|
10497
|
+
df_normalized[column] = df_copy[column] - centerpoint
|
|
10498
|
+
|
|
10499
|
+
# Convert back to original format with first column as regular column
|
|
10500
|
+
df_normalized = df_normalized.reset_index()
|
|
10501
|
+
|
|
10502
|
+
return df_normalized
|
|
10503
|
+
|
|
10504
|
+
def show_in_table(self, df, metric, title):
|
|
10505
|
+
|
|
10506
|
+
# Create new table
|
|
10507
|
+
table = CustomTableView(self.parent())
|
|
10508
|
+
table.setModel(PandasModel(df))
|
|
10509
|
+
|
|
10510
|
+
try:
|
|
10511
|
+
first_column_name = table.model()._data.columns[0]
|
|
10512
|
+
table.sort_table(first_column_name, ascending=True)
|
|
10513
|
+
except:
|
|
10514
|
+
pass
|
|
10515
|
+
|
|
10516
|
+
# Add to tabbed widget
|
|
10517
|
+
if title is None:
|
|
10518
|
+
self.parent().tabbed_data.add_table(f"{metric} Analysis", table)
|
|
10519
|
+
else:
|
|
10520
|
+
self.parent().tabbed_data.add_table(f"{title}", table)
|
|
10521
|
+
|
|
10522
|
+
|
|
10523
|
+
|
|
10524
|
+
# Adjust column widths to content
|
|
10525
|
+
for column in range(table.model().columnCount(None)):
|
|
10526
|
+
table.resizeColumnToContents(column)
|
|
10527
|
+
|
|
10528
|
+
def run(self):
|
|
10529
|
+
|
|
10530
|
+
def df_to_dict_by_rows(df, row_indices, title):
|
|
10531
|
+
"""
|
|
10532
|
+
Convert a pandas DataFrame to a dictionary by selecting specific rows.
|
|
10533
|
+
No normalization - dataframe is already normalized.
|
|
10534
|
+
|
|
10535
|
+
Parameters:
|
|
10536
|
+
df (pd.DataFrame): DataFrame with first column as row headers, remaining columns contain floats
|
|
10537
|
+
row_indices (list): List of values from the first column representing rows to include
|
|
10538
|
+
|
|
10539
|
+
Returns:
|
|
10540
|
+
dict: Dictionary where keys are column headers and values are lists of column values (as floats)
|
|
10541
|
+
for the specified rows
|
|
10542
|
+
"""
|
|
10543
|
+
# Make a copy to avoid modifying the original dataframe
|
|
10544
|
+
df_copy = df.copy()
|
|
10545
|
+
|
|
10546
|
+
# Set the first column as the index (row headers)
|
|
10547
|
+
df_copy = df_copy.set_index(df_copy.columns[0])
|
|
10548
|
+
|
|
10549
|
+
# Mask the dataframe to include only the specified rows
|
|
10550
|
+
masked_df = df_copy.loc[row_indices]
|
|
10551
|
+
|
|
10552
|
+
# Create empty dictionary
|
|
10553
|
+
result_dict = {}
|
|
10554
|
+
|
|
10555
|
+
# For each column, add the column header as key and column values as list
|
|
10556
|
+
for column in masked_df.columns:
|
|
10557
|
+
result_dict[column] = masked_df[column].tolist()
|
|
10558
|
+
|
|
10559
|
+
masked_df.insert(0, "NodeIDs", row_indices)
|
|
10560
|
+
self.show_in_table(masked_df, metric = "NodeID", title = title)
|
|
10561
|
+
|
|
10562
|
+
|
|
10563
|
+
return result_dict
|
|
10564
|
+
|
|
10565
|
+
from . import neighborhoods
|
|
10566
|
+
|
|
10567
|
+
try:
|
|
10568
|
+
|
|
10569
|
+
if self.idens.currentIndex() != 0:
|
|
10570
|
+
|
|
10571
|
+
iden = self.idens.currentText()
|
|
10572
|
+
iden_list = []
|
|
10573
|
+
import ast
|
|
10574
|
+
|
|
10575
|
+
for item in my_network.node_identities:
|
|
10576
|
+
|
|
10577
|
+
try:
|
|
10578
|
+
parse = ast.literal_eval(my_network.node_identities[item])
|
|
10579
|
+
if iden in parse:
|
|
10580
|
+
iden_list.append(item)
|
|
10581
|
+
except:
|
|
10582
|
+
if (iden == my_network.node_identities[item]):
|
|
10583
|
+
iden_list.append(item)
|
|
10584
|
+
|
|
10585
|
+
violin_dict = df_to_dict_by_rows(self.df, iden_list, f"Z-Score-like Channel Intensities of Identity {iden}, {len(iden_list)} Nodes")
|
|
10586
|
+
|
|
10587
|
+
neighborhoods.create_violin_plots(violin_dict, graph_title=f"Z-Score-like Channel Intensities of Identity {iden}, {len(iden_list)} Nodes")
|
|
10588
|
+
except:
|
|
10589
|
+
pass
|
|
10590
|
+
|
|
10591
|
+
try:
|
|
10592
|
+
if self.coms.currentIndex() != 0:
|
|
10593
|
+
|
|
10594
|
+
com = self.coms.currentText()
|
|
10595
|
+
|
|
10596
|
+
com_dict = n3d.invert_dict(my_network.communities)
|
|
10597
|
+
|
|
10598
|
+
com_list = com_dict[int(com)]
|
|
10599
|
+
|
|
10600
|
+
violin_dict = df_to_dict_by_rows(self.df, com_list, f"Z-Score-like Channel Intensities of Community/Neighborhood {com}, {len(com_list)} Nodes")
|
|
10601
|
+
|
|
10602
|
+
neighborhoods.create_violin_plots(violin_dict, graph_title=f"Z-Score-like Channel Intensities of Community/Neighborhood {com}, {len(com_list)} Nodes")
|
|
10603
|
+
except:
|
|
10604
|
+
pass
|
|
10605
|
+
|
|
10606
|
+
"""
|
|
10607
|
+
def run2(self):
|
|
10608
|
+
def df_to_dict(df):
|
|
10609
|
+
# Make a copy to avoid modifying the original dataframe
|
|
10610
|
+
df_copy = df.copy()
|
|
10611
|
+
|
|
10612
|
+
# Set the first column as the index (row headers)
|
|
10613
|
+
df_copy = df_copy.set_index(df_copy.columns[0])
|
|
10614
|
+
|
|
10615
|
+
# Convert all remaining columns to float type (batch conversion)
|
|
10616
|
+
df_copy = df_copy.astype(float)
|
|
10617
|
+
|
|
10618
|
+
# Create the result dictionary
|
|
10619
|
+
result_dict = {}
|
|
10620
|
+
for row_idx in df_copy.index:
|
|
10621
|
+
result_dict[row_idx] = df_copy.loc[row_idx].tolist()
|
|
10622
|
+
|
|
10623
|
+
return result_dict
|
|
10624
|
+
|
|
10625
|
+
try:
|
|
10626
|
+
umap_dict = df_to_dict(self.backup_df)
|
|
10627
|
+
my_network.identity_umap(umap_dict)
|
|
10628
|
+
except:
|
|
10629
|
+
pass
|
|
10630
|
+
"""
|
|
10631
|
+
|
|
10632
|
+
def run2(self):
|
|
10633
|
+
|
|
10634
|
+
try:
|
|
10635
|
+
umap_dict = self.prepare_data_for_umap(self.backup_df)
|
|
10636
|
+
mode = self.mode_selector.currentIndex()
|
|
10637
|
+
my_network.identity_umap(umap_dict, mode)
|
|
10638
|
+
except:
|
|
10639
|
+
import traceback
|
|
10640
|
+
print(traceback.format_exc())
|
|
10641
|
+
pass
|
|
10642
|
+
|
|
10643
|
+
def run3(self):
|
|
10644
|
+
|
|
10645
|
+
num_clusters_text = self.kmeans_num_input.text()
|
|
10646
|
+
|
|
10647
|
+
if num_clusters_text:
|
|
10648
|
+
num_clusters = int(num_clusters_text)
|
|
10649
|
+
# Use specified number of clusters
|
|
10650
|
+
print(f"Using {num_clusters} clusters")
|
|
10651
|
+
else:
|
|
10652
|
+
num_clusters = None # Auto-determine
|
|
10653
|
+
print("Auto-determining number of clusters")
|
|
10654
|
+
|
|
10655
|
+
try:
|
|
10656
|
+
cluster_dict = self.prepare_data_for_umap(self.backup_df)
|
|
10657
|
+
my_network.group_nodes_by_intensity(cluster_dict, count = num_clusters)
|
|
10658
|
+
self.parent().format_for_upperright_table(my_network.communities, 'NodeID', 'Community')
|
|
10659
|
+
self.accept()
|
|
10660
|
+
except:
|
|
10661
|
+
import traceback
|
|
10662
|
+
print(traceback.format_exc())
|
|
10663
|
+
pass
|
|
10664
|
+
|
|
10665
|
+
|
|
10666
|
+
|
|
10667
|
+
|
|
10668
|
+
class DegreeDialog(QDialog):
|
|
10669
|
+
|
|
10670
|
+
|
|
10671
|
+
def __init__(self, parent=None):
|
|
10672
|
+
|
|
10673
|
+
super().__init__(parent)
|
|
10674
|
+
self.setWindowTitle("Degree Parameters")
|
|
10675
|
+
self.setModal(True)
|
|
10676
|
+
|
|
10677
|
+
layout = QFormLayout(self)
|
|
10678
|
+
|
|
10679
|
+
layout.addRow("Note:", QLabel(f"This operation will be executed on the image in 'Active Image', unless it is set to edges in which case it will use the nodes. \n (This is because you may want to run it on isolated nodes that have been placed in the Overlay channels)\nWe can draw optional overlays to Overlay 2 as described below:"))
|
|
10680
|
+
|
|
10681
|
+
# Add mode selection dropdown
|
|
10682
|
+
self.mode_selector = QComboBox()
|
|
9517
10683
|
self.mode_selector.addItems(["Just make table", "Draw degree of node as overlay (literally draws 1, 2, 3, etc... faster)", "Label nodes by degree (nodes will take on the value 1, 2, 3, etc, based on their degree, to export for array based analysis)", "Create Heatmap of Degrees"])
|
|
9518
10684
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
9519
10685
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
@@ -9754,6 +10920,9 @@ class MotherDialog(QDialog):
|
|
|
9754
10920
|
|
|
9755
10921
|
except Exception as e:
|
|
9756
10922
|
|
|
10923
|
+
import traceback
|
|
10924
|
+
print(traceback.format_exc())
|
|
10925
|
+
|
|
9757
10926
|
print(f"Error finding mothers: {e}")
|
|
9758
10927
|
|
|
9759
10928
|
|
|
@@ -9872,7 +11041,7 @@ class ResizeDialog(QDialog):
|
|
|
9872
11041
|
|
|
9873
11042
|
def run_resize(self, undo = False, upsize = True, special = False):
|
|
9874
11043
|
try:
|
|
9875
|
-
self.parent().resizing =
|
|
11044
|
+
self.parent().resizing = True
|
|
9876
11045
|
# Get parameters
|
|
9877
11046
|
try:
|
|
9878
11047
|
resize = float(self.resize.text()) if self.resize.text() else None
|
|
@@ -9987,6 +11156,7 @@ class ResizeDialog(QDialog):
|
|
|
9987
11156
|
if channel is not None:
|
|
9988
11157
|
self.parent().slice_slider.setMinimum(0)
|
|
9989
11158
|
self.parent().slice_slider.setMaximum(channel.shape[0] - 1)
|
|
11159
|
+
self.parent().shape = channel.shape
|
|
9990
11160
|
break
|
|
9991
11161
|
|
|
9992
11162
|
if not special:
|
|
@@ -10056,10 +11226,7 @@ class ResizeDialog(QDialog):
|
|
|
10056
11226
|
except Exception as e:
|
|
10057
11227
|
print(f"Error loading edge centroid table: {e}")
|
|
10058
11228
|
|
|
10059
|
-
|
|
10060
11229
|
self.parent().update_display()
|
|
10061
|
-
self.reset_fields()
|
|
10062
|
-
self.parent().resizing = False
|
|
10063
11230
|
self.accept()
|
|
10064
11231
|
|
|
10065
11232
|
except Exception as e:
|
|
@@ -10068,6 +11235,79 @@ class ResizeDialog(QDialog):
|
|
|
10068
11235
|
print(traceback.format_exc())
|
|
10069
11236
|
QMessageBox.critical(self, "Error", f"Failed to resize: {str(e)}")
|
|
10070
11237
|
|
|
11238
|
+
class CleanDialog(QDialog):
|
|
11239
|
+
def __init__(self, parent=None):
|
|
11240
|
+
super().__init__(parent)
|
|
11241
|
+
self.setWindowTitle("Some options for cleaning segmentation")
|
|
11242
|
+
self.setModal(False)
|
|
11243
|
+
|
|
11244
|
+
layout = QFormLayout(self)
|
|
11245
|
+
|
|
11246
|
+
# Add Run button
|
|
11247
|
+
run_button = QPushButton("Close")
|
|
11248
|
+
run_button.clicked.connect(self.close)
|
|
11249
|
+
layout.addRow("Close (Fill Small Gaps - Dilate then Erode by same amount):", run_button)
|
|
11250
|
+
|
|
11251
|
+
# Add Run button
|
|
11252
|
+
run_button = QPushButton("Open")
|
|
11253
|
+
run_button.clicked.connect(self.open)
|
|
11254
|
+
layout.addRow("Open (Eliminate Noise, Jagged Borders, and Small Connections Between Objects - Erode then Dilate by same amount):", run_button)
|
|
11255
|
+
|
|
11256
|
+
# Add Run button
|
|
11257
|
+
run_button = QPushButton("Fill Holes")
|
|
11258
|
+
run_button.clicked.connect(self.holes)
|
|
11259
|
+
layout.addRow("Call the fill holes function:", run_button)
|
|
11260
|
+
|
|
11261
|
+
# Add Run button
|
|
11262
|
+
run_button = QPushButton("Threshold Noise")
|
|
11263
|
+
run_button.clicked.connect(self.thresh)
|
|
11264
|
+
layout.addRow("Threshold Noise By Volume:", run_button)
|
|
11265
|
+
|
|
11266
|
+
def close(self):
|
|
11267
|
+
|
|
11268
|
+
try:
|
|
11269
|
+
self.parent().show_dilate_dialog(args = [1])
|
|
11270
|
+
self.parent().show_erode_dialog(args = [self.parent().last_dil])
|
|
11271
|
+
except:
|
|
11272
|
+
pass
|
|
11273
|
+
|
|
11274
|
+
def open(self):
|
|
11275
|
+
|
|
11276
|
+
try:
|
|
11277
|
+
self.parent().show_erode_dialog(args = [1])
|
|
11278
|
+
self.parent().show_dilate_dialog(args = [self.parent().last_ero])
|
|
11279
|
+
except:
|
|
11280
|
+
pass
|
|
11281
|
+
|
|
11282
|
+
def holes(self):
|
|
11283
|
+
|
|
11284
|
+
try:
|
|
11285
|
+
self.parent().show_hole_dialog()
|
|
11286
|
+
except:
|
|
11287
|
+
pass
|
|
11288
|
+
|
|
11289
|
+
def thresh(self):
|
|
11290
|
+
try:
|
|
11291
|
+
if len(np.unique(self.parent().channel_data[self.parent().active_channel])) < 3:
|
|
11292
|
+
self.parent().show_label_dialog()
|
|
11293
|
+
|
|
11294
|
+
if self.parent().volume_dict[self.parent().active_channel] is None:
|
|
11295
|
+
self.parent().volumes()
|
|
11296
|
+
|
|
11297
|
+
thresh_window = ThresholdWindow(self.parent(), 1)
|
|
11298
|
+
thresh_window.show() # Non-modal window
|
|
11299
|
+
self.parent().highlight_overlay = None
|
|
11300
|
+
#self.mini_overlay = False
|
|
11301
|
+
self.parent().mini_overlay_data = None
|
|
11302
|
+
except:
|
|
11303
|
+
import traceback
|
|
11304
|
+
print(traceback.format_exc())
|
|
11305
|
+
pass
|
|
11306
|
+
|
|
11307
|
+
|
|
11308
|
+
|
|
11309
|
+
|
|
11310
|
+
|
|
10071
11311
|
|
|
10072
11312
|
class OverrideDialog(QDialog):
|
|
10073
11313
|
def __init__(self, parent=None):
|
|
@@ -10301,7 +11541,7 @@ class LabelDialog(QDialog):
|
|
|
10301
11541
|
class SLabelDialog(QDialog):
|
|
10302
11542
|
def __init__(self, parent=None):
|
|
10303
11543
|
super().__init__(parent)
|
|
10304
|
-
self.setWindowTitle("
|
|
11544
|
+
self.setWindowTitle("Label a binary image based on it's voxels proximity to labeled components of a second image?")
|
|
10305
11545
|
self.setModal(True)
|
|
10306
11546
|
|
|
10307
11547
|
layout = QFormLayout(self)
|
|
@@ -10313,7 +11553,7 @@ class SLabelDialog(QDialog):
|
|
|
10313
11553
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
10314
11554
|
layout.addRow("Prelabeled Array:", self.mode_selector)
|
|
10315
11555
|
|
|
10316
|
-
layout.addRow(QLabel("Will Label
|
|
11556
|
+
layout.addRow(QLabel("Will Label Binary Foreground Voxels in: "))
|
|
10317
11557
|
|
|
10318
11558
|
# Add mode selection dropdown
|
|
10319
11559
|
self.target_selector = QComboBox()
|
|
@@ -10359,10 +11599,6 @@ class SLabelDialog(QDialog):
|
|
|
10359
11599
|
# Update both the display data and the network object
|
|
10360
11600
|
binary_array = sdl.smart_label(binary_array, label_array, directory = None, GPU = GPU, predownsample = down_factor, remove_template = True)
|
|
10361
11601
|
|
|
10362
|
-
label_array = sdl.invert_array(label_array)
|
|
10363
|
-
|
|
10364
|
-
binary_array = binary_array * label_array
|
|
10365
|
-
|
|
10366
11602
|
self.parent().load_channel(accepted_target, binary_array, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
10367
11603
|
|
|
10368
11604
|
self.accept()
|
|
@@ -10441,12 +11677,11 @@ class ThresholdDialog(QDialog):
|
|
|
10441
11677
|
print("Error - please calculate network first")
|
|
10442
11678
|
return
|
|
10443
11679
|
|
|
10444
|
-
if self.parent().mini_overlay_data is not None:
|
|
10445
|
-
self.parent().mini_overlay_data = None
|
|
10446
|
-
|
|
10447
11680
|
thresh_window = ThresholdWindow(self.parent(), accepted_mode)
|
|
10448
11681
|
thresh_window.show() # Non-modal window
|
|
10449
11682
|
self.highlight_overlay = None
|
|
11683
|
+
#self.mini_overlay = False
|
|
11684
|
+
self.mini_overlay_data = None
|
|
10450
11685
|
self.accept()
|
|
10451
11686
|
except:
|
|
10452
11687
|
import traceback
|
|
@@ -11379,6 +12614,7 @@ class ThresholdWindow(QMainWindow):
|
|
|
11379
12614
|
|
|
11380
12615
|
def __init__(self, parent=None, accepted_mode=0):
|
|
11381
12616
|
super().__init__(parent)
|
|
12617
|
+
self.parent().thresh_window_ref = self
|
|
11382
12618
|
self.setWindowTitle("Threshold")
|
|
11383
12619
|
|
|
11384
12620
|
self.accepted_mode = accepted_mode
|
|
@@ -11420,16 +12656,23 @@ class ThresholdWindow(QMainWindow):
|
|
|
11420
12656
|
self.parent().bounds = False
|
|
11421
12657
|
|
|
11422
12658
|
elif accepted_mode == 0:
|
|
11423
|
-
|
|
11424
|
-
|
|
11425
|
-
|
|
11426
|
-
|
|
11427
|
-
|
|
11428
|
-
|
|
11429
|
-
|
|
11430
|
-
self.
|
|
11431
|
-
|
|
11432
|
-
self.histo_list = self.
|
|
12659
|
+
data = self.parent().channel_data[self.parent().active_channel]
|
|
12660
|
+
nonzero_data = data[data != 0]
|
|
12661
|
+
|
|
12662
|
+
if nonzero_data.size > 578009537:
|
|
12663
|
+
# For large arrays, use numpy histogram directly
|
|
12664
|
+
counts, bin_edges = np.histogram(nonzero_data, bins= min(int(np.sqrt(nonzero_data.size)), 500), density=False)
|
|
12665
|
+
# Store min/max separately if needed elsewhere
|
|
12666
|
+
self.data_min = np.min(nonzero_data)
|
|
12667
|
+
self.data_max = np.max(nonzero_data)
|
|
12668
|
+
self.histo_list = [self.data_min, self.data_max]
|
|
12669
|
+
else:
|
|
12670
|
+
# For smaller arrays, can still use histogram method for consistency
|
|
12671
|
+
counts, bin_edges = np.histogram(nonzero_data, bins='auto', density=False)
|
|
12672
|
+
self.data_min = np.min(nonzero_data)
|
|
12673
|
+
self.data_max = np.max(nonzero_data)
|
|
12674
|
+
self.histo_list = [self.data_min, self.data_max]
|
|
12675
|
+
|
|
11433
12676
|
self.bounds = True
|
|
11434
12677
|
self.parent().bounds = True
|
|
11435
12678
|
|
|
@@ -11442,16 +12685,26 @@ class ThresholdWindow(QMainWindow):
|
|
|
11442
12685
|
layout.addWidget(self.canvas)
|
|
11443
12686
|
|
|
11444
12687
|
# Pre-compute histogram with numpy
|
|
11445
|
-
|
|
11446
|
-
|
|
12688
|
+
if accepted_mode != 0:
|
|
12689
|
+
counts, bin_edges = np.histogram(self.histo_list, bins=50)
|
|
12690
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
12691
|
+
# Store histogram bounds
|
|
12692
|
+
if self.bounds:
|
|
12693
|
+
self.data_min = 0
|
|
12694
|
+
else:
|
|
12695
|
+
self.data_min = min(self.histo_list)
|
|
12696
|
+
self.data_max = max(self.histo_list)
|
|
12697
|
+
else:
|
|
12698
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
12699
|
+
bin_width = bin_edges[1] - bin_edges[0]
|
|
11447
12700
|
|
|
11448
12701
|
# Plot pre-computed histogram
|
|
11449
12702
|
self.ax = fig.add_subplot(111)
|
|
11450
12703
|
self.ax.bar(bin_centers, counts, width=bin_edges[1] - bin_edges[0], alpha=0.5)
|
|
11451
12704
|
|
|
11452
12705
|
# Add vertical lines for thresholds
|
|
11453
|
-
self.min_line = self.ax.axvline(
|
|
11454
|
-
self.max_line = self.ax.axvline(
|
|
12706
|
+
self.min_line = self.ax.axvline(self.data_min, color='r')
|
|
12707
|
+
self.max_line = self.ax.axvline(self.data_max, color='b')
|
|
11455
12708
|
|
|
11456
12709
|
# Connect events for dragging
|
|
11457
12710
|
self.canvas.mpl_connect('button_press_event', self.on_press)
|
|
@@ -11459,13 +12712,6 @@ class ThresholdWindow(QMainWindow):
|
|
|
11459
12712
|
self.canvas.mpl_connect('button_release_event', self.on_release)
|
|
11460
12713
|
|
|
11461
12714
|
self.dragging = None
|
|
11462
|
-
|
|
11463
|
-
# Store histogram bounds
|
|
11464
|
-
if self.bounds:
|
|
11465
|
-
self.data_min = 0
|
|
11466
|
-
else:
|
|
11467
|
-
self.data_min = min(self.histo_list)
|
|
11468
|
-
self.data_max = max(self.histo_list)
|
|
11469
12715
|
|
|
11470
12716
|
# Create form layout for inputs
|
|
11471
12717
|
form_layout = QFormLayout()
|
|
@@ -11498,7 +12744,7 @@ class ThresholdWindow(QMainWindow):
|
|
|
11498
12744
|
button_layout.addWidget(run_button)
|
|
11499
12745
|
|
|
11500
12746
|
# Add Cancel button for external dialog use
|
|
11501
|
-
cancel_button = QPushButton("Cancel/Skip")
|
|
12747
|
+
cancel_button = QPushButton("Cancel/Skip (Retains Selection)")
|
|
11502
12748
|
cancel_button.clicked.connect(self.cancel_processing)
|
|
11503
12749
|
button_layout.addWidget(cancel_button)
|
|
11504
12750
|
|
|
@@ -11522,10 +12768,8 @@ class ThresholdWindow(QMainWindow):
|
|
|
11522
12768
|
self.processing_cancelled.emit()
|
|
11523
12769
|
self.close()
|
|
11524
12770
|
|
|
11525
|
-
def
|
|
11526
|
-
|
|
11527
|
-
self.parent().targs = None
|
|
11528
|
-
self.parent().bounds = False
|
|
12771
|
+
def make_full_highlight(self):
|
|
12772
|
+
|
|
11529
12773
|
try: # could probably be refactored but this just handles keeping the highlight elements if the user presses X
|
|
11530
12774
|
if self.chan == 0:
|
|
11531
12775
|
if not self.bounds:
|
|
@@ -11559,6 +12803,14 @@ class ThresholdWindow(QMainWindow):
|
|
|
11559
12803
|
pass
|
|
11560
12804
|
|
|
11561
12805
|
|
|
12806
|
+
def closeEvent(self, event):
|
|
12807
|
+
self.parent().preview = False
|
|
12808
|
+
self.parent().targs = None
|
|
12809
|
+
self.parent().bounds = False
|
|
12810
|
+
self.parent().thresh_window_ref = None
|
|
12811
|
+
self.make_full_highlight()
|
|
12812
|
+
|
|
12813
|
+
|
|
11562
12814
|
def get_values_in_range_all_vols(self, chan, min_val, max_val):
|
|
11563
12815
|
output = []
|
|
11564
12816
|
if self.accepted_mode == 1:
|
|
@@ -11825,36 +13077,33 @@ class SmartDilateDialog(QDialog):
|
|
|
11825
13077
|
|
|
11826
13078
|
|
|
11827
13079
|
class DilateDialog(QDialog):
|
|
11828
|
-
def __init__(self, parent=None):
|
|
13080
|
+
def __init__(self, parent=None, args = None):
|
|
11829
13081
|
super().__init__(parent)
|
|
11830
13082
|
self.setWindowTitle("Dilate Parameters")
|
|
11831
13083
|
self.setModal(True)
|
|
11832
13084
|
|
|
11833
13085
|
layout = QFormLayout(self)
|
|
11834
13086
|
|
|
11835
|
-
|
|
11836
|
-
|
|
11837
|
-
|
|
11838
|
-
if my_network.xy_scale is not None:
|
|
11839
|
-
xy_scale = f"{my_network.xy_scale}"
|
|
13087
|
+
if args:
|
|
13088
|
+
self.parent().last_dil = args[0]
|
|
13089
|
+
self.index = 1
|
|
11840
13090
|
else:
|
|
11841
|
-
|
|
13091
|
+
self.parent().last_dil = 1
|
|
13092
|
+
self.index = 0
|
|
11842
13093
|
|
|
11843
|
-
self.
|
|
11844
|
-
layout.addRow("
|
|
13094
|
+
self.amount = QLineEdit(f"{self.parent().last_dil}")
|
|
13095
|
+
layout.addRow("Dilation Radius:", self.amount)
|
|
11845
13096
|
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
else:
|
|
11849
|
-
z_scale = "1"
|
|
13097
|
+
self.xy_scale = QLineEdit("1")
|
|
13098
|
+
layout.addRow("xy_scale:", self.xy_scale)
|
|
11850
13099
|
|
|
11851
|
-
self.z_scale = QLineEdit(
|
|
13100
|
+
self.z_scale = QLineEdit("1")
|
|
11852
13101
|
layout.addRow("z_scale:", self.z_scale)
|
|
11853
13102
|
|
|
11854
13103
|
# Add mode selection dropdown
|
|
11855
13104
|
self.mode_selector = QComboBox()
|
|
11856
|
-
self.mode_selector.addItems(["
|
|
11857
|
-
self.mode_selector.setCurrentIndex(
|
|
13105
|
+
self.mode_selector.addItems(["Distance Transform-Based (Slower but more accurate at larger dilations)", "Preserve Labels (slower)", "Pseudo3D Binary Kernels (For Fast, small dilations)"])
|
|
13106
|
+
self.mode_selector.setCurrentIndex(self.index) # Default to Mode 1
|
|
11858
13107
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
11859
13108
|
|
|
11860
13109
|
# Add Run button
|
|
@@ -11896,13 +13145,15 @@ class DilateDialog(QDialog):
|
|
|
11896
13145
|
if active_data is None:
|
|
11897
13146
|
raise ValueError("No active image selected")
|
|
11898
13147
|
|
|
13148
|
+
self.parent().last_dil = amount
|
|
13149
|
+
|
|
11899
13150
|
if accepted_mode == 1:
|
|
11900
13151
|
dialog = SmartDilateDialog(self.parent(), [active_data, amount, xy_scale, z_scale])
|
|
11901
13152
|
dialog.exec()
|
|
11902
13153
|
self.accept()
|
|
11903
13154
|
return
|
|
11904
13155
|
|
|
11905
|
-
if accepted_mode ==
|
|
13156
|
+
if accepted_mode == 0:
|
|
11906
13157
|
result = n3d.dilate_3D_dt(active_data, amount, xy_scaling = xy_scale, z_scaling = z_scale)
|
|
11907
13158
|
else:
|
|
11908
13159
|
|
|
@@ -11932,36 +13183,33 @@ class DilateDialog(QDialog):
|
|
|
11932
13183
|
)
|
|
11933
13184
|
|
|
11934
13185
|
class ErodeDialog(QDialog):
|
|
11935
|
-
def __init__(self, parent=None):
|
|
13186
|
+
def __init__(self, parent=None, args = None):
|
|
11936
13187
|
super().__init__(parent)
|
|
11937
13188
|
self.setWindowTitle("Erosion Parameters")
|
|
11938
13189
|
self.setModal(True)
|
|
11939
13190
|
|
|
11940
13191
|
layout = QFormLayout(self)
|
|
11941
13192
|
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
if my_network.xy_scale is not None:
|
|
11946
|
-
xy_scale = f"{my_network.xy_scale}"
|
|
13193
|
+
if args:
|
|
13194
|
+
self.parent().last_ero = args[0]
|
|
13195
|
+
self.index = 1
|
|
11947
13196
|
else:
|
|
11948
|
-
|
|
13197
|
+
self.parent().last_ero = 1
|
|
13198
|
+
self.index = 0
|
|
11949
13199
|
|
|
11950
|
-
self.
|
|
11951
|
-
layout.addRow("
|
|
13200
|
+
self.amount = QLineEdit(f"{self.parent().last_ero}")
|
|
13201
|
+
layout.addRow("Erosion Radius:", self.amount)
|
|
11952
13202
|
|
|
11953
|
-
|
|
11954
|
-
|
|
11955
|
-
else:
|
|
11956
|
-
z_scale = "1"
|
|
13203
|
+
self.xy_scale = QLineEdit("1")
|
|
13204
|
+
layout.addRow("xy_scale:", self.xy_scale)
|
|
11957
13205
|
|
|
11958
|
-
self.z_scale = QLineEdit(
|
|
13206
|
+
self.z_scale = QLineEdit("1")
|
|
11959
13207
|
layout.addRow("z_scale:", self.z_scale)
|
|
11960
13208
|
|
|
11961
13209
|
# Add mode selection dropdown
|
|
11962
13210
|
self.mode_selector = QComboBox()
|
|
11963
|
-
self.mode_selector.addItems(["
|
|
11964
|
-
self.mode_selector.setCurrentIndex(
|
|
13211
|
+
self.mode_selector.addItems(["Distance Transform-Based (Slower but more accurate at larger erosions)", "Preserve Labels (Slower)", "Pseudo3D Binary Kernels (For Fast, small erosions)"])
|
|
13212
|
+
self.mode_selector.setCurrentIndex(self.index) # Default to Mode 1
|
|
11965
13213
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
11966
13214
|
|
|
11967
13215
|
# Add Run button
|
|
@@ -11997,8 +13245,7 @@ class ErodeDialog(QDialog):
|
|
|
11997
13245
|
|
|
11998
13246
|
mode = self.mode_selector.currentIndex()
|
|
11999
13247
|
|
|
12000
|
-
if mode ==
|
|
12001
|
-
mode = 1
|
|
13248
|
+
if mode == 1:
|
|
12002
13249
|
preserve_labels = True
|
|
12003
13250
|
else:
|
|
12004
13251
|
preserve_labels = False
|
|
@@ -12020,7 +13267,7 @@ class ErodeDialog(QDialog):
|
|
|
12020
13267
|
|
|
12021
13268
|
|
|
12022
13269
|
self.parent().load_channel(self.parent().active_channel, result, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
12023
|
-
|
|
13270
|
+
self.parent().last_ero = amount
|
|
12024
13271
|
self.accept()
|
|
12025
13272
|
|
|
12026
13273
|
except Exception as e:
|
|
@@ -12050,6 +13297,11 @@ class HoleDialog(QDialog):
|
|
|
12050
13297
|
self.borders.setChecked(False)
|
|
12051
13298
|
layout.addRow("Fill Small Holes Along Borders:", self.borders)
|
|
12052
13299
|
|
|
13300
|
+
self.preserve_labels = QPushButton("Preserve Labels")
|
|
13301
|
+
self.preserve_labels.setCheckable(True)
|
|
13302
|
+
self.preserve_labels.setChecked(False)
|
|
13303
|
+
layout.addRow("Preserve Labels (Slower):", self.preserve_labels)
|
|
13304
|
+
|
|
12053
13305
|
self.sep_holes = QPushButton("Seperate Hole Mask")
|
|
12054
13306
|
self.sep_holes.setCheckable(True)
|
|
12055
13307
|
self.sep_holes.setChecked(False)
|
|
@@ -12072,15 +13324,32 @@ class HoleDialog(QDialog):
|
|
|
12072
13324
|
borders = self.borders.isChecked()
|
|
12073
13325
|
headon = self.headon.isChecked()
|
|
12074
13326
|
sep_holes = self.sep_holes.isChecked()
|
|
13327
|
+
preserve_labels = self.preserve_labels.isChecked()
|
|
13328
|
+
if preserve_labels:
|
|
13329
|
+
label_copy = np.copy(active_data)
|
|
13330
|
+
|
|
13331
|
+
if borders:
|
|
12075
13332
|
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
|
|
12079
|
-
|
|
12080
|
-
|
|
12081
|
-
|
|
13333
|
+
# Call dilate method with parameters
|
|
13334
|
+
result = n3d.fill_holes_3d_old(
|
|
13335
|
+
active_data,
|
|
13336
|
+
head_on = headon,
|
|
13337
|
+
fill_borders = borders
|
|
13338
|
+
)
|
|
13339
|
+
|
|
13340
|
+
else:
|
|
13341
|
+
# Call dilate method with parameters
|
|
13342
|
+
result = n3d.fill_holes_3d(
|
|
13343
|
+
active_data,
|
|
13344
|
+
head_on = headon,
|
|
13345
|
+
fill_borders = borders
|
|
13346
|
+
)
|
|
13347
|
+
|
|
12082
13348
|
|
|
12083
13349
|
if not sep_holes:
|
|
13350
|
+
if preserve_labels:
|
|
13351
|
+
result = sdl.smart_label(result, label_copy, directory = None, GPU = False, remove_template = True)
|
|
13352
|
+
|
|
12084
13353
|
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
12085
13354
|
else:
|
|
12086
13355
|
self.parent().load_channel(3, active_data - result, True)
|
|
@@ -12425,7 +13694,13 @@ class SkeletonizeDialog(QDialog):
|
|
|
12425
13694
|
# auto checkbox (default True)
|
|
12426
13695
|
self.auto = QPushButton("Auto")
|
|
12427
13696
|
self.auto.setCheckable(True)
|
|
12428
|
-
|
|
13697
|
+
try:
|
|
13698
|
+
if self.shape[0] == 1:
|
|
13699
|
+
self.auto.setChecked(False)
|
|
13700
|
+
else:
|
|
13701
|
+
self.auto.setChecked(True)
|
|
13702
|
+
except:
|
|
13703
|
+
self.auto.setChecked(True)
|
|
12429
13704
|
layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
|
|
12430
13705
|
|
|
12431
13706
|
# Add Run button
|
|
@@ -12481,6 +13756,86 @@ class SkeletonizeDialog(QDialog):
|
|
|
12481
13756
|
f"Error running skeletonize: {str(e)}"
|
|
12482
13757
|
)
|
|
12483
13758
|
|
|
13759
|
+
|
|
13760
|
+
class BranchStatDialog(QDialog):
|
|
13761
|
+
|
|
13762
|
+
def __init__(self, parent=None):
|
|
13763
|
+
super().__init__(parent)
|
|
13764
|
+
self.setWindowTitle("Make sure branches are labeled first (Image -> Generate -> Label Branches)")
|
|
13765
|
+
self.setModal(True)
|
|
13766
|
+
|
|
13767
|
+
layout = QFormLayout(self)
|
|
13768
|
+
|
|
13769
|
+
info_label = QLabel("Skeletonization Params for Getting Branch Stats, Make sure xy and z scale are set correctly in properties")
|
|
13770
|
+
layout.addRow(info_label)
|
|
13771
|
+
|
|
13772
|
+
self.remove = QLineEdit("0")
|
|
13773
|
+
layout.addRow("Remove Branches Pixel Length (int):", self.remove)
|
|
13774
|
+
|
|
13775
|
+
# auto checkbox (default True)
|
|
13776
|
+
self.auto = QPushButton("Auto")
|
|
13777
|
+
self.auto.setCheckable(True)
|
|
13778
|
+
try:
|
|
13779
|
+
if self.shape[0] == 1:
|
|
13780
|
+
self.auto.setChecked(False)
|
|
13781
|
+
else:
|
|
13782
|
+
self.auto.setChecked(True)
|
|
13783
|
+
except:
|
|
13784
|
+
self.auto.setChecked(True)
|
|
13785
|
+
layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
|
|
13786
|
+
|
|
13787
|
+
# Add Run button
|
|
13788
|
+
run_button = QPushButton("Get Branchstats (For Active Image)")
|
|
13789
|
+
run_button.clicked.connect(self.run)
|
|
13790
|
+
layout.addRow(run_button)
|
|
13791
|
+
|
|
13792
|
+
def run(self):
|
|
13793
|
+
|
|
13794
|
+
try:
|
|
13795
|
+
|
|
13796
|
+
# Get branch removal
|
|
13797
|
+
try:
|
|
13798
|
+
remove = int(self.remove.text()) if self.remove.text() else 0
|
|
13799
|
+
except ValueError:
|
|
13800
|
+
remove = 0
|
|
13801
|
+
|
|
13802
|
+
auto = self.auto.isChecked()
|
|
13803
|
+
|
|
13804
|
+
# Get the active channel data from parent
|
|
13805
|
+
active_data = np.copy(self.parent().channel_data[self.parent().active_channel])
|
|
13806
|
+
if active_data is None:
|
|
13807
|
+
raise ValueError("No active image selected")
|
|
13808
|
+
|
|
13809
|
+
if auto:
|
|
13810
|
+
active_data = n3d.skeletonize(active_data)
|
|
13811
|
+
active_data = n3d.fill_holes_3d(active_data)
|
|
13812
|
+
|
|
13813
|
+
active_data = n3d.skeletonize(
|
|
13814
|
+
active_data
|
|
13815
|
+
)
|
|
13816
|
+
|
|
13817
|
+
if remove > 0:
|
|
13818
|
+
active_data = n3d.remove_branches_new(active_data, remove)
|
|
13819
|
+
active_data = n3d.dilate_3D(active_data, 3, 3, 3)
|
|
13820
|
+
active_data = n3d.skeletonize(active_data)
|
|
13821
|
+
|
|
13822
|
+
active_data = active_data * self.parent().channel_data[self.parent().active_channel]
|
|
13823
|
+
len_dict, tortuosity_dict, angle_dict = n3d.compute_optional_branchstats(None, active_data, None, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
|
|
13824
|
+
|
|
13825
|
+
if self.parent().active_channel == 0:
|
|
13826
|
+
self.parent().branch_dict[0] = [len_dict, tortuosity_dict]
|
|
13827
|
+
elif self.parent().active_channel == 1:
|
|
13828
|
+
self.parent().branch_dict[1] = [len_dict, tortuosity_dict]
|
|
13829
|
+
|
|
13830
|
+
self.parent().format_for_upperright_table(len_dict, 'BranchID', 'Length (Scaled)', 'Branch Lengths')
|
|
13831
|
+
self.parent().format_for_upperright_table(tortuosity_dict, 'BranchID', 'Tortuosity', 'Branch Tortuosities')
|
|
13832
|
+
|
|
13833
|
+
|
|
13834
|
+
self.accept()
|
|
13835
|
+
|
|
13836
|
+
except Exception as e:
|
|
13837
|
+
print(f"Error: {e}")
|
|
13838
|
+
|
|
12484
13839
|
class DistanceDialog(QDialog):
|
|
12485
13840
|
def __init__(self, parent=None):
|
|
12486
13841
|
super().__init__(parent)
|
|
@@ -12528,16 +13883,58 @@ class GrayWaterDialog(QDialog):
|
|
|
12528
13883
|
run_button.clicked.connect(self.run_watershed)
|
|
12529
13884
|
layout.addRow(run_button)
|
|
12530
13885
|
|
|
13886
|
+
def wait_for_threshold_processing(self):
|
|
13887
|
+
"""
|
|
13888
|
+
Opens ThresholdWindow and waits for user to process the image.
|
|
13889
|
+
Returns True if completed, False if cancelled.
|
|
13890
|
+
The thresholded image will be available in the main window after completion.
|
|
13891
|
+
"""
|
|
13892
|
+
# Create event loop to wait for user
|
|
13893
|
+
loop = QEventLoop()
|
|
13894
|
+
result = {'completed': False}
|
|
13895
|
+
|
|
13896
|
+
# Create the threshold window
|
|
13897
|
+
thresh_window = ThresholdWindow(self.parent(), 0)
|
|
13898
|
+
|
|
13899
|
+
|
|
13900
|
+
# Connect signals
|
|
13901
|
+
def on_processing_complete():
|
|
13902
|
+
result['completed'] = True
|
|
13903
|
+
loop.quit()
|
|
13904
|
+
|
|
13905
|
+
def on_processing_cancelled():
|
|
13906
|
+
result['completed'] = False
|
|
13907
|
+
loop.quit()
|
|
13908
|
+
|
|
13909
|
+
thresh_window.processing_complete.connect(on_processing_complete)
|
|
13910
|
+
thresh_window.processing_cancelled.connect(on_processing_cancelled)
|
|
13911
|
+
|
|
13912
|
+
# Show window and wait
|
|
13913
|
+
thresh_window.show()
|
|
13914
|
+
thresh_window.raise_()
|
|
13915
|
+
thresh_window.activateWindow()
|
|
13916
|
+
|
|
13917
|
+
# Block until user clicks "Apply Threshold & Continue" or "Cancel"
|
|
13918
|
+
loop.exec()
|
|
13919
|
+
|
|
13920
|
+
# Clean up
|
|
13921
|
+
thresh_window.deleteLater()
|
|
13922
|
+
|
|
13923
|
+
return result['completed']
|
|
13924
|
+
|
|
12531
13925
|
def run_watershed(self):
|
|
12532
13926
|
|
|
12533
13927
|
try:
|
|
12534
13928
|
|
|
13929
|
+
self.accept()
|
|
13930
|
+
print("Please threshold foreground, or press cancel/skip if not desired:")
|
|
13931
|
+
self.wait_for_threshold_processing()
|
|
13932
|
+
data = self.parent().channel_data[self.parent().active_channel]
|
|
13933
|
+
|
|
12535
13934
|
min_intensity = float(self.min_intensity.text()) if self.min_intensity.text().strip() else None
|
|
12536
13935
|
|
|
12537
13936
|
min_peak_distance = int(self.min_peak_distance.text()) if self.min_peak_distance.text().strip() else 1
|
|
12538
13937
|
|
|
12539
|
-
data = self.parent().channel_data[self.parent().active_channel]
|
|
12540
|
-
|
|
12541
13938
|
data = n3d.gray_watershed(data, min_peak_distance, min_intensity)
|
|
12542
13939
|
|
|
12543
13940
|
self.parent().load_channel(self.parent().active_channel, data, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
@@ -12557,11 +13954,6 @@ class WatershedDialog(QDialog):
|
|
|
12557
13954
|
self.setModal(True)
|
|
12558
13955
|
|
|
12559
13956
|
layout = QFormLayout(self)
|
|
12560
|
-
|
|
12561
|
-
# Directory (empty by default)
|
|
12562
|
-
self.directory = QLineEdit()
|
|
12563
|
-
self.directory.setPlaceholderText("Leave empty for None")
|
|
12564
|
-
layout.addRow("Output Directory:", self.directory)
|
|
12565
13957
|
|
|
12566
13958
|
try:
|
|
12567
13959
|
|
|
@@ -12612,7 +14004,7 @@ class WatershedDialog(QDialog):
|
|
|
12612
14004
|
def run_watershed(self):
|
|
12613
14005
|
try:
|
|
12614
14006
|
# Get directory (None if empty)
|
|
12615
|
-
directory =
|
|
14007
|
+
directory = None
|
|
12616
14008
|
|
|
12617
14009
|
# Get proportion (0.1 if empty or invalid)
|
|
12618
14010
|
try:
|
|
@@ -12970,7 +14362,7 @@ class GenNodesDialog(QDialog):
|
|
|
12970
14362
|
|
|
12971
14363
|
if my_network.edges is None and my_network.nodes is not None:
|
|
12972
14364
|
self.parent().load_channel(1, my_network.nodes, data = True)
|
|
12973
|
-
self.parent().delete_channel(0,
|
|
14365
|
+
self.parent().delete_channel(0, False)
|
|
12974
14366
|
# Get directory (None if empty)
|
|
12975
14367
|
#directory = self.directory.text() if self.directory.text() else None
|
|
12976
14368
|
|
|
@@ -13032,7 +14424,6 @@ class GenNodesDialog(QDialog):
|
|
|
13032
14424
|
order = order,
|
|
13033
14425
|
return_skele = True,
|
|
13034
14426
|
fastdil = fastdil
|
|
13035
|
-
|
|
13036
14427
|
)
|
|
13037
14428
|
|
|
13038
14429
|
if down_factor > 0 and not self.called:
|
|
@@ -13124,7 +14515,7 @@ class BranchDialog(QDialog):
|
|
|
13124
14515
|
self.fix3.setChecked(True)
|
|
13125
14516
|
else:
|
|
13126
14517
|
self.fix3.setChecked(False)
|
|
13127
|
-
correction_layout.addWidget(QLabel("Split Nontouching Branches
|
|
14518
|
+
correction_layout.addWidget(QLabel("Split Nontouching Branches?: "), 4, 0)
|
|
13128
14519
|
correction_layout.addWidget(self.fix3, 4, 1)
|
|
13129
14520
|
|
|
13130
14521
|
correction_group.setLayout(correction_layout)
|
|
@@ -13152,20 +14543,27 @@ class BranchDialog(QDialog):
|
|
|
13152
14543
|
# --- Misc Options Group ---
|
|
13153
14544
|
misc_group = QGroupBox("Misc Options")
|
|
13154
14545
|
misc_layout = QGridLayout()
|
|
14546
|
+
|
|
14547
|
+
# optional computation checkbox
|
|
14548
|
+
self.compute = QPushButton("Branch Stats")
|
|
14549
|
+
self.compute.setCheckable(True)
|
|
14550
|
+
self.compute.setChecked(True)
|
|
14551
|
+
misc_layout.addWidget(QLabel("Compute Branch Stats (Branch Lengths, Tortuosity. Set xy_scale and z_scale in properties first if real distances are desired.):"), 0, 0)
|
|
14552
|
+
misc_layout.addWidget(self.compute, 0, 1)
|
|
13155
14553
|
|
|
13156
14554
|
# Nodes checkbox
|
|
13157
14555
|
self.nodes = QPushButton("Generate Nodes")
|
|
13158
14556
|
self.nodes.setCheckable(True)
|
|
13159
14557
|
self.nodes.setChecked(True)
|
|
13160
|
-
misc_layout.addWidget(QLabel("Generate nodes from edges? (Skip if already completed):"),
|
|
13161
|
-
misc_layout.addWidget(self.nodes,
|
|
14558
|
+
misc_layout.addWidget(QLabel("Generate nodes from edges? (Skip if already completed):"), 1, 0)
|
|
14559
|
+
misc_layout.addWidget(self.nodes, 1, 1)
|
|
13162
14560
|
|
|
13163
14561
|
# GPU checkbox
|
|
13164
14562
|
self.GPU = QPushButton("GPU")
|
|
13165
14563
|
self.GPU.setCheckable(True)
|
|
13166
14564
|
self.GPU.setChecked(False)
|
|
13167
|
-
misc_layout.addWidget(QLabel("Use GPU (May downsample large images):"),
|
|
13168
|
-
misc_layout.addWidget(self.GPU,
|
|
14565
|
+
misc_layout.addWidget(QLabel("Use GPU (May downsample large images):"), 2, 0)
|
|
14566
|
+
misc_layout.addWidget(self.GPU, 2, 1)
|
|
13169
14567
|
|
|
13170
14568
|
misc_group.setLayout(misc_layout)
|
|
13171
14569
|
main_layout.addWidget(misc_group)
|
|
@@ -13199,10 +14597,11 @@ class BranchDialog(QDialog):
|
|
|
13199
14597
|
fix3 = self.fix3.isChecked()
|
|
13200
14598
|
fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
|
|
13201
14599
|
seed = int(self.seed.text()) if self.seed.text() else None
|
|
14600
|
+
compute = self.compute.isChecked()
|
|
13202
14601
|
|
|
13203
14602
|
if my_network.edges is None and my_network.nodes is not None:
|
|
13204
14603
|
self.parent().load_channel(1, my_network.nodes, data = True)
|
|
13205
|
-
self.parent().delete_channel(0,
|
|
14604
|
+
self.parent().delete_channel(0, False)
|
|
13206
14605
|
|
|
13207
14606
|
original_shape = my_network.edges.shape
|
|
13208
14607
|
original_array = copy.deepcopy(my_network.edges)
|
|
@@ -13215,7 +14614,7 @@ class BranchDialog(QDialog):
|
|
|
13215
14614
|
|
|
13216
14615
|
if my_network.edges is not None and my_network.nodes is not None and my_network.id_overlay is not None:
|
|
13217
14616
|
|
|
13218
|
-
output = n3d.label_branches(my_network.edges, nodes = my_network.nodes, bonus_array = original_array, GPU = GPU, down_factor = down_factor, arrayshape = original_shape)
|
|
14617
|
+
output, verts, skeleton, endpoints = n3d.label_branches(my_network.edges, nodes = my_network.nodes, bonus_array = original_array, GPU = GPU, down_factor = down_factor, arrayshape = original_shape, compute = compute, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
|
|
13219
14618
|
|
|
13220
14619
|
if fix2:
|
|
13221
14620
|
|
|
@@ -13254,6 +14653,19 @@ class BranchDialog(QDialog):
|
|
|
13254
14653
|
|
|
13255
14654
|
output = self.parent().separate_nontouching_objects(output, max_val=np.max(output))
|
|
13256
14655
|
|
|
14656
|
+
if compute:
|
|
14657
|
+
labeled_image = (skeleton != 0) * output
|
|
14658
|
+
len_dict, tortuosity_dict, angle_dict = n3d.compute_optional_branchstats(verts, labeled_image, endpoints, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale)
|
|
14659
|
+
self.parent().branch_dict[1] = [len_dict, tortuosity_dict]
|
|
14660
|
+
#max_length = max(len(v) for v in angle_dict.values())
|
|
14661
|
+
#title = [str(i+1) if i < 2 else i+1 for i in range(max_length)]
|
|
14662
|
+
|
|
14663
|
+
#del labeled_image
|
|
14664
|
+
|
|
14665
|
+
self.parent().format_for_upperright_table(len_dict, 'BranchID', 'Length (Scaled)', 'Branch Lengths')
|
|
14666
|
+
self.parent().format_for_upperright_table(tortuosity_dict, 'BranchID', 'Tortuosity', 'Branch Tortuosities')
|
|
14667
|
+
#self.parent().format_for_upperright_table(angle_dict, 'Vertex ID', title, 'Branch Angles')
|
|
14668
|
+
|
|
13257
14669
|
|
|
13258
14670
|
if down_factor is not None:
|
|
13259
14671
|
|
|
@@ -13653,10 +15065,6 @@ class CentroidDialog(QDialog):
|
|
|
13653
15065
|
|
|
13654
15066
|
layout = QFormLayout(self)
|
|
13655
15067
|
|
|
13656
|
-
self.directory = QLineEdit()
|
|
13657
|
-
self.directory.setPlaceholderText("Leave empty for active directory")
|
|
13658
|
-
layout.addRow("Output Directory:", self.directory)
|
|
13659
|
-
|
|
13660
15068
|
self.downsample = QLineEdit("1")
|
|
13661
15069
|
layout.addRow("Downsample Factor:", self.downsample)
|
|
13662
15070
|
|
|
@@ -13686,7 +15094,7 @@ class CentroidDialog(QDialog):
|
|
|
13686
15094
|
ignore_empty = self.ignore_empty.isChecked()
|
|
13687
15095
|
|
|
13688
15096
|
# Get directory (None if empty)
|
|
13689
|
-
directory =
|
|
15097
|
+
directory = None
|
|
13690
15098
|
|
|
13691
15099
|
# Get downsample
|
|
13692
15100
|
try:
|
|
@@ -13767,7 +15175,6 @@ class CentroidDialog(QDialog):
|
|
|
13767
15175
|
|
|
13768
15176
|
class CalcAllDialog(QDialog):
|
|
13769
15177
|
# Class variables to store previous settings
|
|
13770
|
-
prev_directory = ""
|
|
13771
15178
|
prev_search = ""
|
|
13772
15179
|
prev_diledge = ""
|
|
13773
15180
|
prev_down_factor = ""
|
|
@@ -13839,7 +15246,7 @@ class CalcAllDialog(QDialog):
|
|
|
13839
15246
|
|
|
13840
15247
|
self.down_factor = QLineEdit(self.prev_down_factor)
|
|
13841
15248
|
self.down_factor.setPlaceholderText("Leave empty for None")
|
|
13842
|
-
speedup_layout.addRow("Downsample for Centroids (int):", self.down_factor)
|
|
15249
|
+
speedup_layout.addRow("Downsample for Centroids/Overlays (int):", self.down_factor)
|
|
13843
15250
|
|
|
13844
15251
|
self.GPU_downsample = QLineEdit(self.prev_GPU_downsample)
|
|
13845
15252
|
self.GPU_downsample.setPlaceholderText("Leave empty for None")
|
|
@@ -13861,10 +15268,6 @@ class CalcAllDialog(QDialog):
|
|
|
13861
15268
|
output_group = QGroupBox("Output Options")
|
|
13862
15269
|
output_layout = QFormLayout(output_group)
|
|
13863
15270
|
|
|
13864
|
-
self.directory = QLineEdit(self.prev_directory)
|
|
13865
|
-
self.directory.setPlaceholderText("Will Have to Save Manually If Empty")
|
|
13866
|
-
output_layout.addRow("Output Directory:", self.directory)
|
|
13867
|
-
|
|
13868
15271
|
self.overlays = QPushButton("Overlays")
|
|
13869
15272
|
self.overlays.setCheckable(True)
|
|
13870
15273
|
self.overlays.setChecked(self.prev_overlays)
|
|
@@ -13886,7 +15289,7 @@ class CalcAllDialog(QDialog):
|
|
|
13886
15289
|
|
|
13887
15290
|
try:
|
|
13888
15291
|
# Get directory (None if empty)
|
|
13889
|
-
directory =
|
|
15292
|
+
directory = None
|
|
13890
15293
|
|
|
13891
15294
|
# Get xy_scale and z_scale (1 if empty or invalid)
|
|
13892
15295
|
try:
|
|
@@ -13963,7 +15366,6 @@ class CalcAllDialog(QDialog):
|
|
|
13963
15366
|
)
|
|
13964
15367
|
|
|
13965
15368
|
# Store current values as previous values
|
|
13966
|
-
CalcAllDialog.prev_directory = self.directory.text()
|
|
13967
15369
|
CalcAllDialog.prev_search = self.search.text()
|
|
13968
15370
|
CalcAllDialog.prev_diledge = self.diledge.text()
|
|
13969
15371
|
CalcAllDialog.prev_down_factor = self.down_factor.text()
|
|
@@ -13997,8 +15399,12 @@ class CalcAllDialog(QDialog):
|
|
|
13997
15399
|
directory = 'my_network'
|
|
13998
15400
|
|
|
13999
15401
|
# Generate and update overlays
|
|
14000
|
-
my_network.network_overlay = my_network.draw_network(directory=directory)
|
|
14001
|
-
my_network.id_overlay = my_network.draw_node_indices(directory=directory)
|
|
15402
|
+
my_network.network_overlay = my_network.draw_network(directory=directory, down_factor = down_factor)
|
|
15403
|
+
my_network.id_overlay = my_network.draw_node_indices(directory=directory, down_factor = down_factor)
|
|
15404
|
+
|
|
15405
|
+
if down_factor is not None:
|
|
15406
|
+
my_network.id_overlay = n3d.upsample_with_padding(my_network.id_overlay, original_shape = self.parent().shape)
|
|
15407
|
+
my_network.network_overlay = n3d.upsample_with_padding(my_network.network_overlay, original_shape = self.parent().shape)
|
|
14002
15408
|
|
|
14003
15409
|
# Update channel data
|
|
14004
15410
|
self.parent().load_channel(2, my_network.network_overlay, True)
|
|
@@ -14111,14 +15517,13 @@ class ProxDialog(QDialog):
|
|
|
14111
15517
|
output_group = QGroupBox("Output Options")
|
|
14112
15518
|
output_layout = QFormLayout(output_group)
|
|
14113
15519
|
|
|
14114
|
-
self.directory = QLineEdit('')
|
|
14115
|
-
self.directory.setPlaceholderText("Leave empty for 'my_network'")
|
|
14116
|
-
output_layout.addRow("Output Directory:", self.directory)
|
|
14117
|
-
|
|
14118
15520
|
self.overlays = QPushButton("Overlays")
|
|
14119
15521
|
self.overlays.setCheckable(True)
|
|
14120
15522
|
self.overlays.setChecked(True)
|
|
14121
15523
|
output_layout.addRow("Generate Overlays:", self.overlays)
|
|
15524
|
+
|
|
15525
|
+
self.downsample = QLineEdit()
|
|
15526
|
+
output_layout.addRow("(If above): Downsample factor for drawing overlays (Int - Makes Overlay Elements Larger):", self.downsample)
|
|
14122
15527
|
|
|
14123
15528
|
self.populate = QPushButton("Populate Nodes from Centroids?")
|
|
14124
15529
|
self.populate.setCheckable(True)
|
|
@@ -14163,10 +15568,8 @@ class ProxDialog(QDialog):
|
|
|
14163
15568
|
else:
|
|
14164
15569
|
targets = None
|
|
14165
15570
|
|
|
14166
|
-
|
|
14167
|
-
|
|
14168
|
-
except:
|
|
14169
|
-
directory = None
|
|
15571
|
+
directory = None
|
|
15572
|
+
|
|
14170
15573
|
|
|
14171
15574
|
# Get xy_scale and z_scale (1 if empty or invalid)
|
|
14172
15575
|
try:
|
|
@@ -14190,6 +15593,12 @@ class ProxDialog(QDialog):
|
|
|
14190
15593
|
except:
|
|
14191
15594
|
max_neighbors = None
|
|
14192
15595
|
|
|
15596
|
+
|
|
15597
|
+
try:
|
|
15598
|
+
downsample = int(self.downsample.text()) if self.downsample.text() else None
|
|
15599
|
+
except:
|
|
15600
|
+
downsample = None
|
|
15601
|
+
|
|
14193
15602
|
overlays = self.overlays.isChecked()
|
|
14194
15603
|
fastdil = self.fastdil.isChecked()
|
|
14195
15604
|
|
|
@@ -14245,8 +15654,12 @@ class ProxDialog(QDialog):
|
|
|
14245
15654
|
directory = 'my_network'
|
|
14246
15655
|
|
|
14247
15656
|
# Generate and update overlays
|
|
14248
|
-
my_network.network_overlay = my_network.draw_network(directory=directory)
|
|
14249
|
-
my_network.id_overlay = my_network.draw_node_indices(directory=directory)
|
|
15657
|
+
my_network.network_overlay = my_network.draw_network(directory=directory, down_factor = downsample)
|
|
15658
|
+
my_network.id_overlay = my_network.draw_node_indices(directory=directory, down_factor = downsample)
|
|
15659
|
+
|
|
15660
|
+
if downsample is not None:
|
|
15661
|
+
my_network.id_overlay = n3d.upsample_with_padding(my_network.id_overlay, original_shape = self.parent().shape)
|
|
15662
|
+
my_network.network_overlay = n3d.upsample_with_padding(my_network.network_overlay, original_shape = self.parent().shape)
|
|
14250
15663
|
|
|
14251
15664
|
# Update channel data
|
|
14252
15665
|
self.parent().load_channel(2, channel_data = my_network.network_overlay, data = True)
|
|
@@ -14368,31 +15781,81 @@ class HistogramSelector(QWidget):
|
|
|
14368
15781
|
""")
|
|
14369
15782
|
layout.addWidget(button)
|
|
14370
15783
|
|
|
15784
|
+
|
|
14371
15785
|
def shortest_path_histogram(self):
|
|
14372
15786
|
try:
|
|
14373
|
-
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
15787
|
+
# Check if graph has multiple disconnected components
|
|
15788
|
+
components = list(nx.connected_components(self.G))
|
|
15789
|
+
|
|
15790
|
+
if len(components) > 1:
|
|
15791
|
+
print(f"Warning: Graph has {len(components)} disconnected components. Computing shortest paths within each component separately.")
|
|
15792
|
+
|
|
15793
|
+
# Initialize variables to collect data from all components
|
|
15794
|
+
all_path_lengths = []
|
|
15795
|
+
max_diameter = 0
|
|
15796
|
+
|
|
15797
|
+
# Process each component separately
|
|
15798
|
+
for i, component in enumerate(components):
|
|
15799
|
+
subgraph = self.G.subgraph(component)
|
|
15800
|
+
|
|
15801
|
+
if len(component) < 2:
|
|
15802
|
+
# Skip single-node components (no paths to compute)
|
|
15803
|
+
continue
|
|
15804
|
+
|
|
15805
|
+
# Compute shortest paths for this component
|
|
15806
|
+
shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(subgraph))
|
|
15807
|
+
component_diameter = max(nx.eccentricity(subgraph, sp=shortest_path_lengths).values())
|
|
15808
|
+
max_diameter = max(max_diameter, component_diameter)
|
|
15809
|
+
|
|
15810
|
+
# Collect path lengths from this component
|
|
15811
|
+
for pls in shortest_path_lengths.values():
|
|
15812
|
+
all_path_lengths.extend(list(pls.values()))
|
|
15813
|
+
|
|
15814
|
+
# Remove self-paths (length 0) and create histogram
|
|
15815
|
+
all_path_lengths = [pl for pl in all_path_lengths if pl > 0]
|
|
15816
|
+
|
|
15817
|
+
if not all_path_lengths:
|
|
15818
|
+
print("No paths found across components (only single-node components)")
|
|
15819
|
+
return
|
|
15820
|
+
|
|
15821
|
+
# Create combined histogram
|
|
15822
|
+
path_lengths = np.zeros(max_diameter + 1, dtype=int)
|
|
15823
|
+
pl, cnts = np.unique(all_path_lengths, return_counts=True)
|
|
14379
15824
|
path_lengths[pl] += cnts
|
|
14380
|
-
|
|
15825
|
+
|
|
15826
|
+
title_suffix = f" (across {len(components)} components)"
|
|
15827
|
+
|
|
15828
|
+
else:
|
|
15829
|
+
# Single component
|
|
15830
|
+
shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(self.G))
|
|
15831
|
+
diameter = max(nx.eccentricity(self.G, sp=shortest_path_lengths).values())
|
|
15832
|
+
path_lengths = np.zeros(diameter + 1, dtype=int)
|
|
15833
|
+
for pls in shortest_path_lengths.values():
|
|
15834
|
+
pl, cnts = np.unique(list(pls.values()), return_counts=True)
|
|
15835
|
+
path_lengths[pl] += cnts
|
|
15836
|
+
max_diameter = diameter
|
|
15837
|
+
title_suffix = ""
|
|
15838
|
+
|
|
15839
|
+
# Generate visualization and results (same for both cases)
|
|
14381
15840
|
freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
|
|
14382
|
-
|
|
14383
15841
|
fig, ax = plt.subplots(figsize=(15, 8))
|
|
14384
|
-
ax.bar(np.arange(1,
|
|
15842
|
+
ax.bar(np.arange(1, max_diameter + 1), height=freq_percent)
|
|
14385
15843
|
ax.set_title(
|
|
14386
|
-
"Distribution of shortest path length in G
|
|
15844
|
+
f"Distribution of shortest path length in G{title_suffix}",
|
|
15845
|
+
fontdict={"size": 35}, loc="center"
|
|
14387
15846
|
)
|
|
14388
15847
|
ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
|
|
14389
15848
|
ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
|
|
14390
15849
|
plt.show()
|
|
14391
15850
|
|
|
14392
15851
|
freq_dict = {freq: length for length, freq in enumerate(freq_percent, start=1)}
|
|
14393
|
-
self.network_analysis.format_for_upperright_table(
|
|
14394
|
-
|
|
14395
|
-
|
|
15852
|
+
self.network_analysis.format_for_upperright_table(
|
|
15853
|
+
freq_dict,
|
|
15854
|
+
metric='Frequency (%)',
|
|
15855
|
+
value='Shortest Path Length',
|
|
15856
|
+
title=f"Distribution of shortest path length in G{title_suffix}"
|
|
15857
|
+
)
|
|
15858
|
+
|
|
14396
15859
|
except Exception as e:
|
|
14397
15860
|
print(f"Error generating shortest path histogram: {e}")
|
|
14398
15861
|
|
|
@@ -14411,20 +15874,62 @@ class HistogramSelector(QWidget):
|
|
|
14411
15874
|
title="Degree Centrality Table")
|
|
14412
15875
|
except Exception as e:
|
|
14413
15876
|
print(f"Error generating degree centrality histogram: {e}")
|
|
14414
|
-
|
|
15877
|
+
|
|
14415
15878
|
def betweenness_centrality_histogram(self):
|
|
14416
15879
|
try:
|
|
14417
|
-
|
|
15880
|
+
# Check if graph has multiple disconnected components
|
|
15881
|
+
components = list(nx.connected_components(self.G))
|
|
15882
|
+
|
|
15883
|
+
if len(components) > 1:
|
|
15884
|
+
print(f"Warning: Graph has {len(components)} disconnected components. Computing betweenness centrality within each component separately.")
|
|
15885
|
+
|
|
15886
|
+
# Initialize dictionary to collect betweenness centrality from all components
|
|
15887
|
+
combined_betweenness_centrality = {}
|
|
15888
|
+
|
|
15889
|
+
# Process each component separately
|
|
15890
|
+
for i, component in enumerate(components):
|
|
15891
|
+
if len(component) < 2:
|
|
15892
|
+
# For single-node components, betweenness centrality is 0
|
|
15893
|
+
for node in component:
|
|
15894
|
+
combined_betweenness_centrality[node] = 0.0
|
|
15895
|
+
continue
|
|
15896
|
+
|
|
15897
|
+
# Create subgraph for this component
|
|
15898
|
+
subgraph = self.G.subgraph(component)
|
|
15899
|
+
|
|
15900
|
+
# Compute betweenness centrality for this component
|
|
15901
|
+
component_betweenness = nx.centrality.betweenness_centrality(subgraph)
|
|
15902
|
+
|
|
15903
|
+
# Add to combined results
|
|
15904
|
+
combined_betweenness_centrality.update(component_betweenness)
|
|
15905
|
+
|
|
15906
|
+
betweenness_centrality = combined_betweenness_centrality
|
|
15907
|
+
title_suffix = f" (across {len(components)} components)"
|
|
15908
|
+
|
|
15909
|
+
else:
|
|
15910
|
+
# Single component
|
|
15911
|
+
betweenness_centrality = nx.centrality.betweenness_centrality(self.G)
|
|
15912
|
+
title_suffix = ""
|
|
15913
|
+
|
|
15914
|
+
# Generate visualization and results (same for both cases)
|
|
14418
15915
|
plt.figure(figsize=(15, 8))
|
|
14419
15916
|
plt.hist(betweenness_centrality.values(), bins=100)
|
|
14420
15917
|
plt.xticks(ticks=[0, 0.02, 0.1, 0.2, 0.3, 0.4, 0.5])
|
|
14421
|
-
plt.title(
|
|
15918
|
+
plt.title(
|
|
15919
|
+
f"Betweenness Centrality Histogram{title_suffix}",
|
|
15920
|
+
fontdict={"size": 35}, loc="center"
|
|
15921
|
+
)
|
|
14422
15922
|
plt.xlabel("Betweenness Centrality", fontdict={"size": 20})
|
|
14423
15923
|
plt.ylabel("Counts", fontdict={"size": 20})
|
|
14424
15924
|
plt.show()
|
|
14425
|
-
|
|
14426
|
-
|
|
14427
|
-
|
|
15925
|
+
|
|
15926
|
+
self.network_analysis.format_for_upperright_table(
|
|
15927
|
+
betweenness_centrality,
|
|
15928
|
+
metric='Node',
|
|
15929
|
+
value='Betweenness Centrality',
|
|
15930
|
+
title=f"Betweenness Centrality Table{title_suffix}"
|
|
15931
|
+
)
|
|
15932
|
+
|
|
14428
15933
|
except Exception as e:
|
|
14429
15934
|
print(f"Error generating betweenness centrality histogram: {e}")
|
|
14430
15935
|
|
|
@@ -14477,7 +15982,27 @@ class HistogramSelector(QWidget):
|
|
|
14477
15982
|
def bridges_analysis(self):
|
|
14478
15983
|
try:
|
|
14479
15984
|
bridges = list(nx.bridges(self.G))
|
|
14480
|
-
|
|
15985
|
+
try:
|
|
15986
|
+
# Get the existing DataFrame from the model
|
|
15987
|
+
original_df = self.network_analysis.network_table.model()._data
|
|
15988
|
+
|
|
15989
|
+
# Create boolean mask
|
|
15990
|
+
mask = pd.Series([False] * len(original_df))
|
|
15991
|
+
|
|
15992
|
+
for u, v in bridges:
|
|
15993
|
+
# Check for both (u,v) and (v,u) orientations
|
|
15994
|
+
bridge_mask = (
|
|
15995
|
+
((original_df.iloc[:, 0] == u) & (original_df.iloc[:, 1] == v)) |
|
|
15996
|
+
((original_df.iloc[:, 0] == v) & (original_df.iloc[:, 1] == u))
|
|
15997
|
+
)
|
|
15998
|
+
mask |= bridge_mask
|
|
15999
|
+
# Filter the DataFrame to only include bridge connections
|
|
16000
|
+
filtered_df = original_df[mask].copy()
|
|
16001
|
+
df_dict = {i: row.tolist() for i, row in enumerate(filtered_df.values)}
|
|
16002
|
+
self.network_analysis.format_for_upperright_table(df_dict, metric='Bridge ID', value = ['NodeA', 'NodeB', 'EdgeC'],
|
|
16003
|
+
title="Bridges")
|
|
16004
|
+
except:
|
|
16005
|
+
self.network_analysis.format_for_upperright_table(bridges, metric='Node Pair',
|
|
14481
16006
|
title="Bridges")
|
|
14482
16007
|
except Exception as e:
|
|
14483
16008
|
print(f"Error generating bridges analysis: {e}")
|
|
@@ -14504,7 +16029,7 @@ class HistogramSelector(QWidget):
|
|
|
14504
16029
|
def node_connectivity_histogram(self):
|
|
14505
16030
|
"""Local node connectivity - minimum number of nodes that must be removed to disconnect neighbors"""
|
|
14506
16031
|
try:
|
|
14507
|
-
if self.G.number_of_nodes() > 500:
|
|
16032
|
+
if self.G.number_of_nodes() > 500:
|
|
14508
16033
|
print("Note this analysis may be slow for large network (>500 nodes)")
|
|
14509
16034
|
#return
|
|
14510
16035
|
|
|
@@ -14582,7 +16107,7 @@ class HistogramSelector(QWidget):
|
|
|
14582
16107
|
def load_centrality_histogram(self):
|
|
14583
16108
|
"""Load centrality - fraction of shortest paths passing through each node"""
|
|
14584
16109
|
try:
|
|
14585
|
-
if self.G.number_of_nodes() > 1000:
|
|
16110
|
+
if self.G.number_of_nodes() > 1000:
|
|
14586
16111
|
print("Note this analysis may be slow for large network (>1000 nodes)")
|
|
14587
16112
|
#return
|
|
14588
16113
|
|
|
@@ -14601,21 +16126,67 @@ class HistogramSelector(QWidget):
|
|
|
14601
16126
|
def communicability_centrality_histogram(self):
|
|
14602
16127
|
"""Communicability centrality - based on communicability between nodes"""
|
|
14603
16128
|
try:
|
|
14604
|
-
if self.G.number_of_nodes() > 500:
|
|
16129
|
+
if self.G.number_of_nodes() > 500:
|
|
14605
16130
|
print("Note this analysis may be slow for large network (>500 nodes)")
|
|
14606
16131
|
#return
|
|
16132
|
+
|
|
16133
|
+
# Check if graph has multiple disconnected components
|
|
16134
|
+
components = list(nx.connected_components(self.G))
|
|
16135
|
+
|
|
16136
|
+
if len(components) > 1:
|
|
16137
|
+
print(f"Warning: Graph has {len(components)} disconnected components. Computing communicability centrality within each component separately.")
|
|
14607
16138
|
|
|
14608
|
-
|
|
14609
|
-
|
|
16139
|
+
# Initialize dictionary to collect communicability centrality from all components
|
|
16140
|
+
combined_comm_centrality = {}
|
|
16141
|
+
|
|
16142
|
+
# Process each component separately
|
|
16143
|
+
for i, component in enumerate(components):
|
|
16144
|
+
if len(component) < 2:
|
|
16145
|
+
# For single-node components, communicability betweenness centrality is 0
|
|
16146
|
+
for node in component:
|
|
16147
|
+
combined_comm_centrality[node] = 0.0
|
|
16148
|
+
continue
|
|
16149
|
+
|
|
16150
|
+
# Create subgraph for this component
|
|
16151
|
+
subgraph = self.G.subgraph(component)
|
|
16152
|
+
|
|
16153
|
+
# Compute communicability betweenness centrality for this component
|
|
16154
|
+
try:
|
|
16155
|
+
component_comm_centrality = nx.communicability_betweenness_centrality(subgraph)
|
|
16156
|
+
# Add to combined results
|
|
16157
|
+
combined_comm_centrality.update(component_comm_centrality)
|
|
16158
|
+
except Exception as comp_e:
|
|
16159
|
+
print(f"Error computing communicability centrality for component {i+1}: {comp_e}")
|
|
16160
|
+
# Set centrality to 0 for nodes in this component if computation fails
|
|
16161
|
+
for node in component:
|
|
16162
|
+
combined_comm_centrality[node] = 0.0
|
|
16163
|
+
|
|
16164
|
+
comm_centrality = combined_comm_centrality
|
|
16165
|
+
title_suffix = f" (across {len(components)} components)"
|
|
16166
|
+
|
|
16167
|
+
else:
|
|
16168
|
+
# Single component
|
|
16169
|
+
comm_centrality = nx.communicability_betweenness_centrality(self.G)
|
|
16170
|
+
title_suffix = ""
|
|
16171
|
+
|
|
16172
|
+
# Generate visualization and results (same for both cases)
|
|
14610
16173
|
plt.figure(figsize=(15, 8))
|
|
14611
16174
|
plt.hist(comm_centrality.values(), bins=50, alpha=0.7)
|
|
14612
|
-
plt.title(
|
|
16175
|
+
plt.title(
|
|
16176
|
+
f"Communicability Betweenness Centrality Distribution{title_suffix}",
|
|
16177
|
+
fontdict={"size": 35}, loc="center"
|
|
16178
|
+
)
|
|
14613
16179
|
plt.xlabel("Communicability Betweenness Centrality", fontdict={"size": 20})
|
|
14614
16180
|
plt.ylabel("Frequency", fontdict={"size": 20})
|
|
14615
16181
|
plt.show()
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
16182
|
+
|
|
16183
|
+
self.network_analysis.format_for_upperright_table(
|
|
16184
|
+
comm_centrality,
|
|
16185
|
+
metric='Node',
|
|
16186
|
+
value='Communicability Betweenness Centrality',
|
|
16187
|
+
title=f"Communicability Betweenness Centrality Table{title_suffix}"
|
|
16188
|
+
)
|
|
16189
|
+
|
|
14619
16190
|
except Exception as e:
|
|
14620
16191
|
print(f"Error generating communicability betweenness centrality histogram: {e}")
|
|
14621
16192
|
|
|
@@ -14638,20 +16209,67 @@ class HistogramSelector(QWidget):
|
|
|
14638
16209
|
def current_flow_betweenness_histogram(self):
|
|
14639
16210
|
"""Current flow betweenness - models network as electrical circuit"""
|
|
14640
16211
|
try:
|
|
14641
|
-
if self.G.number_of_nodes() > 500:
|
|
16212
|
+
if self.G.number_of_nodes() > 500:
|
|
14642
16213
|
print("Note this analysis may be slow for large network (>500 nodes)")
|
|
14643
16214
|
#return
|
|
16215
|
+
|
|
16216
|
+
# Check if graph has multiple disconnected components
|
|
16217
|
+
components = list(nx.connected_components(self.G))
|
|
16218
|
+
|
|
16219
|
+
if len(components) > 1:
|
|
16220
|
+
print(f"Warning: Graph has {len(components)} disconnected components. Computing current flow betweenness centrality within each component separately.")
|
|
16221
|
+
|
|
16222
|
+
# Initialize dictionary to collect current flow betweenness from all components
|
|
16223
|
+
combined_current_flow = {}
|
|
14644
16224
|
|
|
14645
|
-
|
|
16225
|
+
# Process each component separately
|
|
16226
|
+
for i, component in enumerate(components):
|
|
16227
|
+
if len(component) < 2:
|
|
16228
|
+
# For single-node components, current flow betweenness centrality is 0
|
|
16229
|
+
for node in component:
|
|
16230
|
+
combined_current_flow[node] = 0.0
|
|
16231
|
+
continue
|
|
16232
|
+
|
|
16233
|
+
# Create subgraph for this component
|
|
16234
|
+
subgraph = self.G.subgraph(component)
|
|
16235
|
+
|
|
16236
|
+
# Compute current flow betweenness centrality for this component
|
|
16237
|
+
try:
|
|
16238
|
+
component_current_flow = nx.current_flow_betweenness_centrality(subgraph)
|
|
16239
|
+
# Add to combined results
|
|
16240
|
+
combined_current_flow.update(component_current_flow)
|
|
16241
|
+
except Exception as comp_e:
|
|
16242
|
+
print(f"Error computing current flow betweenness for component {i+1}: {comp_e}")
|
|
16243
|
+
# Set centrality to 0 for nodes in this component if computation fails
|
|
16244
|
+
for node in component:
|
|
16245
|
+
combined_current_flow[node] = 0.0
|
|
16246
|
+
|
|
16247
|
+
current_flow = combined_current_flow
|
|
16248
|
+
title_suffix = f" (across {len(components)} components)"
|
|
16249
|
+
|
|
16250
|
+
else:
|
|
16251
|
+
# Single component
|
|
16252
|
+
current_flow = nx.current_flow_betweenness_centrality(self.G)
|
|
16253
|
+
title_suffix = ""
|
|
16254
|
+
|
|
16255
|
+
# Generate visualization and results (same for both cases)
|
|
14646
16256
|
plt.figure(figsize=(15, 8))
|
|
14647
16257
|
plt.hist(current_flow.values(), bins=50, alpha=0.7)
|
|
14648
|
-
plt.title(
|
|
16258
|
+
plt.title(
|
|
16259
|
+
f"Current Flow Betweenness Centrality Distribution{title_suffix}",
|
|
16260
|
+
fontdict={"size": 35}, loc="center"
|
|
16261
|
+
)
|
|
14649
16262
|
plt.xlabel("Current Flow Betweenness Centrality", fontdict={"size": 20})
|
|
14650
16263
|
plt.ylabel("Frequency", fontdict={"size": 20})
|
|
14651
16264
|
plt.show()
|
|
14652
|
-
|
|
14653
|
-
|
|
14654
|
-
|
|
16265
|
+
|
|
16266
|
+
self.network_analysis.format_for_upperright_table(
|
|
16267
|
+
current_flow,
|
|
16268
|
+
metric='Node',
|
|
16269
|
+
value='Current Flow Betweenness',
|
|
16270
|
+
title=f"Current Flow Betweenness Table{title_suffix}"
|
|
16271
|
+
)
|
|
16272
|
+
|
|
14655
16273
|
except Exception as e:
|
|
14656
16274
|
print(f"Error generating current flow betweenness histogram: {e}")
|
|
14657
16275
|
|