nettracer3d 0.9.9__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.
- 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 +2072 -592
- 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.9.dist-info → nettracer3d-1.1.5.dist-info}/METADATA +3 -4
- nettracer3d-1.1.5.dist-info/RECORD +26 -0
- nettracer3d-0.9.9.dist-info/RECORD +0 -25
- {nettracer3d-0.9.9.dist-info → nettracer3d-1.1.5.dist-info}/WHEEL +0 -0
- {nettracer3d-0.9.9.dist-info → nettracer3d-1.1.5.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.9.9.dist-info → nettracer3d-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.9.9.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,9 +467,8 @@ 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
|
|
|
469
473
|
|
|
470
474
|
def load_file(self):
|
|
@@ -511,12 +515,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
511
515
|
data = df.iloc[:, 0].tolist() # First column as list
|
|
512
516
|
value = None
|
|
513
517
|
|
|
514
|
-
self.format_for_upperright_table(
|
|
515
|
-
|
|
516
|
-
metric=metric,
|
|
517
|
-
value=value,
|
|
518
|
-
title=title
|
|
519
|
-
)
|
|
518
|
+
df = self.format_for_upperright_table(data=data, metric=metric, value=value, title=title)
|
|
519
|
+
return df
|
|
520
520
|
else:
|
|
521
521
|
# Multiple columns: create dictionary as before
|
|
522
522
|
# First column header (for metric parameter)
|
|
@@ -542,12 +542,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
542
542
|
value = value[0]
|
|
543
543
|
|
|
544
544
|
# Call the parent method
|
|
545
|
-
self.format_for_upperright_table(
|
|
546
|
-
|
|
547
|
-
metric=metric,
|
|
548
|
-
value=value,
|
|
549
|
-
title=title
|
|
550
|
-
)
|
|
545
|
+
df = self.format_for_upperright_table(data=data_dict, metric=metric, value=value, title=title)
|
|
546
|
+
return df
|
|
551
547
|
|
|
552
548
|
QMessageBox.information(
|
|
553
549
|
self,
|
|
@@ -887,6 +883,21 @@ class ImageViewerWindow(QMainWindow):
|
|
|
887
883
|
elif self.scroll_direction > 0 and new_value <= self.slice_slider.maximum():
|
|
888
884
|
self.slice_slider.setValue(new_value)
|
|
889
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
|
+
|
|
890
901
|
def create_highlight_overlay(self, node_indices=None, edge_indices=None, overlay1_indices = None, overlay2_indices = None, bounds = False):
|
|
891
902
|
"""
|
|
892
903
|
Create a binary overlay highlighting specific nodes and/or edges using parallel processing.
|
|
@@ -1012,13 +1023,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1012
1023
|
|
|
1013
1024
|
# Combine results
|
|
1014
1025
|
if node_overlay is not None:
|
|
1015
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, node_overlay)
|
|
1026
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, node_overlay).astype(np.uint8)
|
|
1016
1027
|
if edge_overlay is not None:
|
|
1017
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, edge_overlay)
|
|
1028
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, edge_overlay).astype(np.uint8)
|
|
1018
1029
|
if overlay1_overlay is not None:
|
|
1019
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay1_overlay)
|
|
1030
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay1_overlay).astype(np.uint8)
|
|
1020
1031
|
if overlay2_overlay is not None:
|
|
1021
|
-
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay2_overlay)
|
|
1032
|
+
self.highlight_overlay = np.maximum(self.highlight_overlay, overlay2_overlay).astype(np.uint8)
|
|
1022
1033
|
|
|
1023
1034
|
# Update display
|
|
1024
1035
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
@@ -1027,6 +1038,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1027
1038
|
|
|
1028
1039
|
"""Highlight overlay generation method specific for the segmenter interactive mode"""
|
|
1029
1040
|
|
|
1041
|
+
self.mini_overlay_data = None
|
|
1042
|
+
self.highlight_overlay = None
|
|
1030
1043
|
|
|
1031
1044
|
def process_chunk_bounds(chunk_data, indices_to_check):
|
|
1032
1045
|
"""Process a single chunk of the array to create highlight mask"""
|
|
@@ -1106,6 +1119,30 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1106
1119
|
current_ylim = self.ax.get_ylim()
|
|
1107
1120
|
self.update_display_pan_mode(current_xlim, current_ylim)
|
|
1108
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
|
+
|
|
1109
1146
|
|
|
1110
1147
|
|
|
1111
1148
|
def create_mini_overlay(self, node_indices = None, edge_indices = None):
|
|
@@ -1265,7 +1302,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1265
1302
|
|
|
1266
1303
|
if my_network.node_identities is not None:
|
|
1267
1304
|
identity_menu = QMenu("Show Identity", self)
|
|
1268
|
-
|
|
1305
|
+
idens = list(set(my_network.node_identities.values()))
|
|
1306
|
+
idens.sort()
|
|
1307
|
+
for item in idens:
|
|
1269
1308
|
show_identity = identity_menu.addAction(f"ID: {item}")
|
|
1270
1309
|
show_identity.triggered.connect(lambda checked, item=item: self.handle_show_identities(sort=item))
|
|
1271
1310
|
context_menu.addMenu(identity_menu)
|
|
@@ -1274,6 +1313,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1274
1313
|
select_nodes = select_all_menu.addAction("Nodes")
|
|
1275
1314
|
select_both = select_all_menu.addAction("Nodes + Edges")
|
|
1276
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")
|
|
1277
1319
|
context_menu.addMenu(select_all_menu)
|
|
1278
1320
|
|
|
1279
1321
|
if len(self.clicked_values['nodes']) > 0 or len(self.clicked_values['edges']) > 0:
|
|
@@ -1297,32 +1339,40 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1297
1339
|
# Create measurement submenu
|
|
1298
1340
|
measure_menu = context_menu.addMenu("Measurements")
|
|
1299
1341
|
|
|
1300
|
-
# Distance measurement options
|
|
1301
1342
|
distance_menu = measure_menu.addMenu("Distance")
|
|
1302
1343
|
if self.current_point is None:
|
|
1303
1344
|
show_point_menu = distance_menu.addAction("Place First Point")
|
|
1304
1345
|
show_point_menu.triggered.connect(
|
|
1305
1346
|
lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
|
|
1306
|
-
|
|
1347
|
+
elif (self.current_point is not None and
|
|
1348
|
+
hasattr(self, 'measurement_mode') and
|
|
1349
|
+
self.measurement_mode == "distance"):
|
|
1307
1350
|
show_point_menu = distance_menu.addAction("Place Second Point")
|
|
1308
1351
|
show_point_menu.triggered.connect(
|
|
1309
1352
|
lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
|
|
1310
|
-
|
|
1353
|
+
|
|
1311
1354
|
# Angle measurement options
|
|
1312
1355
|
angle_menu = measure_menu.addMenu("Angle")
|
|
1313
1356
|
if self.current_point is None:
|
|
1314
1357
|
angle_first = angle_menu.addAction("Place First Point (A)")
|
|
1315
1358
|
angle_first.triggered.connect(
|
|
1316
1359
|
lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
|
|
1317
|
-
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"):
|
|
1318
1364
|
angle_second = angle_menu.addAction("Place Second Point (B - Vertex)")
|
|
1319
1365
|
angle_second.triggered.connect(
|
|
1320
1366
|
lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
|
|
1321
|
-
|
|
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"):
|
|
1322
1371
|
angle_third = angle_menu.addAction("Place Third Point (C)")
|
|
1323
1372
|
angle_third.triggered.connect(
|
|
1324
1373
|
lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
|
|
1325
1374
|
|
|
1375
|
+
|
|
1326
1376
|
show_remove_menu = measure_menu.addAction("Remove All Measurements")
|
|
1327
1377
|
show_remove_menu.triggered.connect(self.handle_remove_all_measurements)
|
|
1328
1378
|
|
|
@@ -1340,6 +1390,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1340
1390
|
select_nodes.triggered.connect(lambda: self.handle_select_all(edges = False, nodes = True))
|
|
1341
1391
|
select_both.triggered.connect(lambda: self.handle_select_all(edges = True))
|
|
1342
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))
|
|
1343
1396
|
if self.highlight_overlay is not None or self.mini_overlay_data is not None:
|
|
1344
1397
|
highlight_select = context_menu.addAction("Add highlight in network selection")
|
|
1345
1398
|
highlight_select.triggered.connect(self.handle_highlight_select)
|
|
@@ -1350,15 +1403,22 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1350
1403
|
except IndexError:
|
|
1351
1404
|
pass
|
|
1352
1405
|
|
|
1353
|
-
|
|
1354
1406
|
def place_distance_point(self, x, y, z):
|
|
1355
1407
|
"""Place a measurement point for distance measurement."""
|
|
1356
1408
|
if self.current_point is None:
|
|
1357
1409
|
# This is the first point
|
|
1358
1410
|
self.current_point = (x, y, z)
|
|
1359
|
-
|
|
1360
|
-
|
|
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}",
|
|
1361
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
|
+
|
|
1362
1422
|
self.canvas.draw()
|
|
1363
1423
|
self.measurement_mode = "distance"
|
|
1364
1424
|
else:
|
|
@@ -1372,21 +1432,28 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1372
1432
|
((z2-z1)*my_network.z_scale)**2)
|
|
1373
1433
|
distance2 = np.sqrt(((x2-x1))**2 + ((y2-y1))**2 + ((z2-z1))**2)
|
|
1374
1434
|
|
|
1375
|
-
# Store the point pair
|
|
1435
|
+
# Store the point pair with type indicator
|
|
1376
1436
|
self.measurement_points.append({
|
|
1377
1437
|
'pair_index': self.current_pair_index,
|
|
1378
1438
|
'point1': self.current_point,
|
|
1379
1439
|
'point2': (x2, y2, z2),
|
|
1380
1440
|
'distance': distance,
|
|
1381
|
-
'distance2': distance2
|
|
1441
|
+
'distance2': distance2,
|
|
1442
|
+
'type': 'distance' # Added type tracking
|
|
1382
1443
|
})
|
|
1383
1444
|
|
|
1384
|
-
# Draw second point and line
|
|
1385
|
-
self.ax.plot(x2, y2, 'yo', markersize=8)
|
|
1386
|
-
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}",
|
|
1387
1448
|
color='yellow', ha='center', va='bottom')
|
|
1449
|
+
|
|
1450
|
+
# Add to measurement_artists
|
|
1451
|
+
self.measurement_artists.extend([pt2, txt2])
|
|
1452
|
+
|
|
1388
1453
|
if z1 == z2: # Only draw line if points are on same slice
|
|
1389
|
-
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
|
+
|
|
1390
1457
|
self.canvas.draw()
|
|
1391
1458
|
|
|
1392
1459
|
# Update measurement display
|
|
@@ -1399,12 +1466,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1399
1466
|
|
|
1400
1467
|
def place_angle_point(self, x, y, z):
|
|
1401
1468
|
"""Place a measurement point for angle measurement."""
|
|
1469
|
+
if not hasattr(self, 'measurement_artists'):
|
|
1470
|
+
self.measurement_artists = []
|
|
1471
|
+
|
|
1402
1472
|
if self.current_point is None:
|
|
1403
1473
|
# First point (A)
|
|
1404
1474
|
self.current_point = (x, y, z)
|
|
1405
|
-
|
|
1406
|
-
|
|
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}",
|
|
1407
1479
|
color='green', ha='center', va='bottom')
|
|
1480
|
+
self.measurement_artists.extend([pt, txt])
|
|
1481
|
+
|
|
1408
1482
|
self.canvas.draw()
|
|
1409
1483
|
self.measurement_mode = "angle"
|
|
1410
1484
|
|
|
@@ -1413,13 +1487,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1413
1487
|
self.current_second_point = (x, y, z)
|
|
1414
1488
|
x1, y1, z1 = self.current_point
|
|
1415
1489
|
|
|
1416
|
-
|
|
1417
|
-
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}",
|
|
1418
1493
|
color='green', ha='center', va='bottom')
|
|
1494
|
+
self.measurement_artists.extend([pt, txt])
|
|
1419
1495
|
|
|
1420
1496
|
# Draw line from A to B
|
|
1421
1497
|
if z1 == z:
|
|
1422
|
-
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)
|
|
1423
1500
|
self.canvas.draw()
|
|
1424
1501
|
|
|
1425
1502
|
else:
|
|
@@ -1442,7 +1519,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1442
1519
|
**angle_data
|
|
1443
1520
|
})
|
|
1444
1521
|
|
|
1445
|
-
# Also add the two distances as separate pairs
|
|
1522
|
+
# Also add the two distances as separate pairs with type indicator
|
|
1446
1523
|
dist_ab = np.sqrt(((x2-x1)*my_network.xy_scale)**2 +
|
|
1447
1524
|
((y2-y1)*my_network.xy_scale)**2 +
|
|
1448
1525
|
((z2-z1)*my_network.z_scale)**2)
|
|
@@ -1459,24 +1536,28 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1459
1536
|
'point1': (x1, y1, z1),
|
|
1460
1537
|
'point2': (x2, y2, z2),
|
|
1461
1538
|
'distance': dist_ab,
|
|
1462
|
-
'distance2': dist_ab_voxel
|
|
1539
|
+
'distance2': dist_ab_voxel,
|
|
1540
|
+
'type': 'angle' # Added type tracking
|
|
1463
1541
|
},
|
|
1464
1542
|
{
|
|
1465
1543
|
'pair_index': f"B{self.current_trio_index}-C{self.current_trio_index}",
|
|
1466
1544
|
'point1': (x2, y2, z2),
|
|
1467
1545
|
'point2': (x3, y3, z3),
|
|
1468
1546
|
'distance': dist_bc,
|
|
1469
|
-
'distance2': dist_bc_voxel
|
|
1547
|
+
'distance2': dist_bc_voxel,
|
|
1548
|
+
'type': 'angle' # Added type tracking
|
|
1470
1549
|
}
|
|
1471
1550
|
])
|
|
1472
1551
|
|
|
1473
|
-
# Draw third point and line
|
|
1474
|
-
self.ax.plot(x3, y3, 'go', markersize=8)
|
|
1475
|
-
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}",
|
|
1476
1555
|
color='green', ha='center', va='bottom')
|
|
1556
|
+
self.measurement_artists.extend([pt3, txt3])
|
|
1477
1557
|
|
|
1478
1558
|
if z2 == z3: # Draw line from B to C if on same slice
|
|
1479
|
-
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)
|
|
1480
1561
|
self.canvas.draw()
|
|
1481
1562
|
|
|
1482
1563
|
# Update measurement display
|
|
@@ -1488,6 +1569,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1488
1569
|
self.current_trio_index += 1
|
|
1489
1570
|
self.measurement_mode = "angle"
|
|
1490
1571
|
|
|
1572
|
+
|
|
1491
1573
|
def calculate_3d_angle(self, point_a, point_b, point_c):
|
|
1492
1574
|
"""Calculate 3D angle at vertex B between points A-B-C."""
|
|
1493
1575
|
x1, y1, z1 = point_a
|
|
@@ -1651,28 +1733,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1651
1733
|
if edges:
|
|
1652
1734
|
edge_indices = filtered_df.iloc[:, 2].unique().tolist()
|
|
1653
1735
|
self.clicked_values['edges'] = edge_indices
|
|
1654
|
-
|
|
1655
|
-
if self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2] > self.mini_thresh:
|
|
1656
|
-
self.mini_overlay = True
|
|
1657
|
-
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1658
|
-
else:
|
|
1659
|
-
self.create_highlight_overlay(
|
|
1660
|
-
node_indices=self.clicked_values['nodes'],
|
|
1661
|
-
edge_indices=self.clicked_values['edges']
|
|
1662
|
-
)
|
|
1736
|
+
self.evaluate_mini(mode = 'edges')
|
|
1663
1737
|
else:
|
|
1664
|
-
|
|
1665
|
-
self.mini_overlay = True
|
|
1666
|
-
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1667
|
-
else:
|
|
1668
|
-
self.create_highlight_overlay(
|
|
1669
|
-
node_indices=self.clicked_values['nodes'],
|
|
1670
|
-
edge_indices = self.clicked_values['edges']
|
|
1671
|
-
)
|
|
1672
|
-
|
|
1738
|
+
self.evaluate_mini()
|
|
1673
1739
|
|
|
1674
1740
|
except Exception as e:
|
|
1675
|
-
print(f"Error
|
|
1741
|
+
print(f"Error showing neighbors: {e}")
|
|
1676
1742
|
|
|
1677
1743
|
|
|
1678
1744
|
def handle_show_component(self, edges = False, nodes = True):
|
|
@@ -1743,23 +1809,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1743
1809
|
if edges:
|
|
1744
1810
|
edge_indices = filtered_df.iloc[:, 2].unique().tolist()
|
|
1745
1811
|
self.clicked_values['edges'] = edge_indices
|
|
1746
|
-
|
|
1747
|
-
self.mini_overlay = True
|
|
1748
|
-
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1749
|
-
else:
|
|
1750
|
-
self.create_highlight_overlay(
|
|
1751
|
-
node_indices=self.clicked_values['nodes'],
|
|
1752
|
-
edge_indices=edge_indices
|
|
1753
|
-
)
|
|
1812
|
+
self.evaluate_mini(mode = 'edges')
|
|
1754
1813
|
else:
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
1758
|
-
else:
|
|
1759
|
-
self.create_highlight_overlay(
|
|
1760
|
-
node_indices = self.clicked_values['nodes'],
|
|
1761
|
-
edge_indices = self.clicked_values['edges']
|
|
1762
|
-
)
|
|
1814
|
+
self.evaluate_mini()
|
|
1815
|
+
|
|
1763
1816
|
|
|
1764
1817
|
except Exception as e:
|
|
1765
1818
|
|
|
@@ -1802,23 +1855,27 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1802
1855
|
|
|
1803
1856
|
nodes = list(set(nodes))
|
|
1804
1857
|
|
|
1805
|
-
|
|
1806
|
-
original_df = self.network_table.model()._data
|
|
1858
|
+
try:
|
|
1807
1859
|
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
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
|
|
1822
1879
|
|
|
1823
1880
|
if edges:
|
|
1824
1881
|
edge_indices = filtered_df.iloc[:, 2].unique().tolist()
|
|
@@ -1951,7 +2008,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1951
2008
|
self.parent().toggle_channel(1)
|
|
1952
2009
|
# Navigate to the Z-slice
|
|
1953
2010
|
self.parent().slice_slider.setValue(int(centroid[0]))
|
|
1954
|
-
print(f"Found edge {value} at Z
|
|
2011
|
+
print(f"Found edge {value} at [Z,Y,X] -> {centroid}")
|
|
1955
2012
|
|
|
1956
2013
|
else:
|
|
1957
2014
|
print(f"Edge {value} not found in centroids dictionary")
|
|
@@ -1987,9 +2044,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1987
2044
|
# Navigate to the Z-slice
|
|
1988
2045
|
self.parent().slice_slider.setValue(int(centroid[0]))
|
|
1989
2046
|
if mode == 0:
|
|
1990
|
-
print(f"Found node {value} at Z
|
|
2047
|
+
print(f"Found node {value} at [Z,Y,X] -> {centroid}")
|
|
1991
2048
|
elif mode == 2:
|
|
1992
|
-
print(f"Found node {value} from community {com} at Z
|
|
2049
|
+
print(f"Found node {value} from community {com} at [Z,Y,X] -> {centroid}")
|
|
1993
2050
|
|
|
1994
2051
|
|
|
1995
2052
|
else:
|
|
@@ -2020,12 +2077,15 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2020
2077
|
|
|
2021
2078
|
|
|
2022
2079
|
|
|
2023
|
-
def handle_select_all(self, nodes = True, edges = False):
|
|
2080
|
+
def handle_select_all(self, nodes = True, edges = False, network = False):
|
|
2024
2081
|
|
|
2025
2082
|
try:
|
|
2026
2083
|
|
|
2027
2084
|
if nodes:
|
|
2028
|
-
|
|
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]))
|
|
2029
2089
|
if nodes[0] == 0:
|
|
2030
2090
|
del nodes[0]
|
|
2031
2091
|
num = (self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2])
|
|
@@ -2033,7 +2093,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2033
2093
|
else:
|
|
2034
2094
|
nodes = []
|
|
2035
2095
|
if edges:
|
|
2036
|
-
|
|
2096
|
+
if not network:
|
|
2097
|
+
edges = list(np.unique(my_network.edges))
|
|
2098
|
+
else:
|
|
2099
|
+
edges = my_network.network_lists[2]
|
|
2037
2100
|
num = (self.channel_data[1].shape[0] * self.channel_data[1].shape[1] * self.channel_data[1].shape[2])
|
|
2038
2101
|
if edges[0] == 0:
|
|
2039
2102
|
del edges[0]
|
|
@@ -2111,6 +2174,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2111
2174
|
except:
|
|
2112
2175
|
pass
|
|
2113
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
|
+
|
|
2114
2187
|
|
|
2115
2188
|
elif sort == 'edge':
|
|
2116
2189
|
|
|
@@ -2156,7 +2229,17 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2156
2229
|
except:
|
|
2157
2230
|
pass
|
|
2158
2231
|
|
|
2159
|
-
|
|
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)
|
|
2160
2243
|
|
|
2161
2244
|
except:
|
|
2162
2245
|
pass
|
|
@@ -2274,7 +2357,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2274
2357
|
unique_labels = np.unique(input_array[binary_mask])
|
|
2275
2358
|
print(f"Processing {len(unique_labels)} unique labels")
|
|
2276
2359
|
|
|
2277
|
-
# Get all bounding boxes at once
|
|
2360
|
+
# Get all bounding boxes at once
|
|
2278
2361
|
bounding_boxes = ndimage.find_objects(input_array)
|
|
2279
2362
|
|
|
2280
2363
|
# Prepare work items - just check if bounding box exists for each label
|
|
@@ -2290,7 +2373,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2290
2373
|
bbox = bounding_boxes[bbox_index]
|
|
2291
2374
|
work_items.append((orig_label, bbox))
|
|
2292
2375
|
|
|
2293
|
-
print(f"Created {len(work_items)} work items")
|
|
2376
|
+
#print(f"Created {len(work_items)} work items")
|
|
2294
2377
|
|
|
2295
2378
|
# If we have work items, process them
|
|
2296
2379
|
if len(work_items) == 0:
|
|
@@ -2310,7 +2393,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2310
2393
|
return orig_label, bbox, labeled_sub, num_cc
|
|
2311
2394
|
|
|
2312
2395
|
except Exception as e:
|
|
2313
|
-
print(f"Error processing label {orig_label}: {e}")
|
|
2396
|
+
#print(f"Error processing label {orig_label}: {e}")
|
|
2314
2397
|
return orig_label, bbox, None, 0
|
|
2315
2398
|
|
|
2316
2399
|
# Execute in parallel
|
|
@@ -2326,7 +2409,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2326
2409
|
|
|
2327
2410
|
for orig_label, bbox, labeled_sub, num_cc in results:
|
|
2328
2411
|
if num_cc > 0 and labeled_sub is not None:
|
|
2329
|
-
print(f"Label {orig_label}: {num_cc} components")
|
|
2412
|
+
#print(f"Label {orig_label}: {num_cc} components")
|
|
2330
2413
|
# Remap labels and place in output
|
|
2331
2414
|
for cc_id in range(1, num_cc + 1):
|
|
2332
2415
|
mask = labeled_sub == cc_id
|
|
@@ -2339,7 +2422,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2339
2422
|
|
|
2340
2423
|
def handle_seperate(self):
|
|
2341
2424
|
"""
|
|
2342
|
-
|
|
2425
|
+
Seperate objects in an array that share a label but do not touch
|
|
2343
2426
|
"""
|
|
2344
2427
|
try:
|
|
2345
2428
|
# Handle nodes
|
|
@@ -2348,7 +2431,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2348
2431
|
# Create highlight overlay (this should preserve original label values)
|
|
2349
2432
|
self.create_highlight_overlay(node_indices=self.clicked_values['nodes'])
|
|
2350
2433
|
|
|
2351
|
-
# DON'T convert to boolean yet - we need the original labels!
|
|
2352
2434
|
# Create a boolean mask for where we have highlighted values
|
|
2353
2435
|
highlight_mask = self.highlight_overlay != 0
|
|
2354
2436
|
|
|
@@ -2358,7 +2440,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2358
2440
|
# Get non-highlighted part of the array
|
|
2359
2441
|
non_highlighted = np.where(highlight_mask, 0, my_network.nodes)
|
|
2360
2442
|
|
|
2361
|
-
# Calculate max_val
|
|
2443
|
+
# Calculate max_val
|
|
2362
2444
|
max_val = np.max(non_highlighted) if np.any(non_highlighted) else 0
|
|
2363
2445
|
|
|
2364
2446
|
# Process highlighted part
|
|
@@ -2583,9 +2665,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2583
2665
|
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
2584
2666
|
self.needs_mini = False
|
|
2585
2667
|
else:
|
|
2586
|
-
self.
|
|
2668
|
+
self.evaluate_mini()
|
|
2587
2669
|
else:
|
|
2588
|
-
self.
|
|
2670
|
+
self.evaluate_mini()
|
|
2589
2671
|
|
|
2590
2672
|
|
|
2591
2673
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
@@ -3650,17 +3732,23 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3650
3732
|
|
|
3651
3733
|
# Add highlight overlays if they exist (with downsampling)
|
|
3652
3734
|
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
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
|
|
3656
3741
|
elif self.highlight_overlay is not None and self.highlight:
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
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
|
|
3664
3752
|
|
|
3665
3753
|
# Convert to 0-255 range for display
|
|
3666
3754
|
return (composite * 255).astype(np.uint8)
|
|
@@ -3755,6 +3843,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3755
3843
|
self.ax.clear()
|
|
3756
3844
|
self.ax.set_facecolor('black')
|
|
3757
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
|
+
|
|
3758
3852
|
# Get original dimensions (before downsampling)
|
|
3759
3853
|
if hasattr(self, 'original_dims') and self.original_dims:
|
|
3760
3854
|
height, width = self.original_dims
|
|
@@ -3836,23 +3930,129 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3836
3930
|
for spine in self.ax.spines.values():
|
|
3837
3931
|
spine.set_color('black')
|
|
3838
3932
|
|
|
3839
|
-
# Add measurement points if they exist (
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
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])
|
|
3853
3999
|
|
|
3854
|
-
|
|
3855
|
-
|
|
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)
|
|
4014
|
+
|
|
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)
|
|
3856
4056
|
|
|
3857
4057
|
#self.canvas.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
3858
4058
|
|
|
@@ -3968,7 +4168,34 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3968
4168
|
if len(self.clicked_values['edges']):
|
|
3969
4169
|
self.highlight_value_in_tables(self.clicked_values['edges'][-1])
|
|
3970
4170
|
self.handle_info('edge')
|
|
3971
|
-
|
|
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
|
+
|
|
3972
4199
|
elif not self.selecting and self.selection_start: # If we had a click but never started selection
|
|
3973
4200
|
# Handle as a normal click
|
|
3974
4201
|
self.on_mouse_click(event)
|
|
@@ -3991,10 +4218,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3991
4218
|
elif self.zoom_mode:
|
|
3992
4219
|
# Handle zoom mode press
|
|
3993
4220
|
if self.original_xlim is None:
|
|
3994
|
-
self.original_xlim = self.
|
|
3995
|
-
|
|
3996
|
-
self.original_ylim = self.ax.get_ylim()
|
|
3997
|
-
#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)
|
|
3998
4223
|
|
|
3999
4224
|
current_xlim = self.ax.get_xlim()
|
|
4000
4225
|
current_ylim = self.ax.get_ylim()
|
|
@@ -4156,8 +4381,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4156
4381
|
if self.zoom_mode:
|
|
4157
4382
|
# Existing zoom functionality
|
|
4158
4383
|
if self.original_xlim is None:
|
|
4159
|
-
self.original_xlim = self.
|
|
4160
|
-
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)
|
|
4161
4386
|
|
|
4162
4387
|
current_xlim = self.ax.get_xlim()
|
|
4163
4388
|
current_ylim = self.ax.get_ylim()
|
|
@@ -4333,6 +4558,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4333
4558
|
for i in range(4):
|
|
4334
4559
|
load_action = load_menu.addAction(f"Load {self.channel_names[i]}")
|
|
4335
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))
|
|
4336
4563
|
load_action = load_menu.addAction("Load Network")
|
|
4337
4564
|
load_action.triggered.connect(self.load_network)
|
|
4338
4565
|
load_action = load_menu.addAction("Load From Excel Helper")
|
|
@@ -4373,6 +4600,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4373
4600
|
allstats_action.triggered.connect(self.stats)
|
|
4374
4601
|
histos_action = stats_menu.addAction("Network Statistic Histograms")
|
|
4375
4602
|
histos_action.triggered.connect(self.histos)
|
|
4603
|
+
sig_action = stats_menu.addAction("Significance Testing")
|
|
4604
|
+
sig_action.triggered.connect(self.sig_test)
|
|
4376
4605
|
radial_action = stats_menu.addAction("Radial Distribution Analysis")
|
|
4377
4606
|
radial_action.triggered.connect(self.show_radial_dialog)
|
|
4378
4607
|
neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
|
|
@@ -4387,8 +4616,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4387
4616
|
vol_action.triggered.connect(self.volumes)
|
|
4388
4617
|
rad_action = stats_menu.addAction("Calculate Radii")
|
|
4389
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)
|
|
4390
4621
|
inter_action = stats_menu.addAction("Calculate Node < > Edge Interaction")
|
|
4391
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)
|
|
4392
4625
|
overlay_menu = analysis_menu.addMenu("Data/Overlays")
|
|
4393
4626
|
degree_action = overlay_menu.addAction("Get Degree Information")
|
|
4394
4627
|
degree_action.triggered.connect(self.show_degree_dialog)
|
|
@@ -4422,12 +4655,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4422
4655
|
calc_branch_action.triggered.connect(self.handle_calc_branch)
|
|
4423
4656
|
calc_branchprox_action = calculate_menu.addAction("Calculate Branch Adjacency Network (Of Edges)")
|
|
4424
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)
|
|
4425
4660
|
centroid_action = calculate_menu.addAction("Calculate Centroids (Active Image)")
|
|
4426
4661
|
centroid_action.triggered.connect(self.show_centroid_dialog)
|
|
4427
4662
|
|
|
4428
4663
|
image_menu = process_menu.addMenu("Image")
|
|
4429
4664
|
resize_action = image_menu.addAction("Resize (Up/Downsample)")
|
|
4430
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)
|
|
4431
4668
|
dilate_action = image_menu.addAction("Dilate")
|
|
4432
4669
|
dilate_action.triggered.connect(self.show_dilate_dialog)
|
|
4433
4670
|
erode_action = image_menu.addAction("Erode")
|
|
@@ -4438,7 +4675,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4438
4675
|
binarize_action.triggered.connect(self.show_binarize_dialog)
|
|
4439
4676
|
label_action = image_menu.addAction("Label Objects")
|
|
4440
4677
|
label_action.triggered.connect(self.show_label_dialog)
|
|
4441
|
-
slabel_action = image_menu.addAction("
|
|
4678
|
+
slabel_action = image_menu.addAction("Neighbor Labels")
|
|
4442
4679
|
slabel_action.triggered.connect(self.show_slabel_dialog)
|
|
4443
4680
|
thresh_action = image_menu.addAction("Threshold/Segment")
|
|
4444
4681
|
thresh_action.triggered.connect(self.show_thresh_dialog)
|
|
@@ -4514,7 +4751,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4514
4751
|
|
|
4515
4752
|
|
|
4516
4753
|
# Add after your other buttons
|
|
4517
|
-
self.popup_button = QPushButton("⤴")
|
|
4754
|
+
self.popup_button = QPushButton("⤴")
|
|
4518
4755
|
self.popup_button.setFixedSize(40, 40)
|
|
4519
4756
|
self.popup_button.setToolTip("Pop out canvas")
|
|
4520
4757
|
self.popup_button.clicked.connect(self.popup_canvas)
|
|
@@ -4582,7 +4819,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4582
4819
|
# Invalid input - reset to default
|
|
4583
4820
|
self.downsample_factor = 1
|
|
4584
4821
|
|
|
4585
|
-
|
|
4822
|
+
try:
|
|
4823
|
+
self.throttle = self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor
|
|
4824
|
+
except:
|
|
4825
|
+
self.throttle = False
|
|
4586
4826
|
|
|
4587
4827
|
# Optional: Trigger display update if you want immediate effect
|
|
4588
4828
|
if update:
|
|
@@ -4667,8 +4907,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4667
4907
|
self.cellpose_launcher.launch_cellpose_gui(use_3d = use_3d)
|
|
4668
4908
|
|
|
4669
4909
|
except:
|
|
4670
|
-
|
|
4671
|
-
|
|
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
|
+
)
|
|
4672
4915
|
pass
|
|
4673
4916
|
|
|
4674
4917
|
|
|
@@ -4715,6 +4958,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4715
4958
|
except Exception as e:
|
|
4716
4959
|
print(f"Error creating histogram selector: {e}")
|
|
4717
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
|
+
|
|
4718
4971
|
def volumes(self):
|
|
4719
4972
|
|
|
4720
4973
|
|
|
@@ -4740,7 +4993,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4740
4993
|
|
|
4741
4994
|
|
|
4742
4995
|
|
|
4743
|
-
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):
|
|
4744
4997
|
"""
|
|
4745
4998
|
Format dictionary or list data for display in upper right table.
|
|
4746
4999
|
|
|
@@ -4826,11 +5079,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4826
5079
|
table = CustomTableView(self)
|
|
4827
5080
|
table.setModel(PandasModel(df))
|
|
4828
5081
|
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
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
|
|
4834
5088
|
|
|
4835
5089
|
# Add to tabbed widget
|
|
4836
5090
|
if title is None:
|
|
@@ -4844,6 +5098,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4844
5098
|
for column in range(table.model().columnCount(None)):
|
|
4845
5099
|
table.resizeColumnToContents(column)
|
|
4846
5100
|
|
|
5101
|
+
return df
|
|
5102
|
+
|
|
4847
5103
|
except:
|
|
4848
5104
|
pass
|
|
4849
5105
|
|
|
@@ -4860,6 +5116,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4860
5116
|
dialog = MergeNodeIdDialog(self)
|
|
4861
5117
|
dialog.exec()
|
|
4862
5118
|
|
|
5119
|
+
def show_multichan_dialog(self, data):
|
|
5120
|
+
dialog = MultiChanDialog(self, data)
|
|
5121
|
+
dialog.show()
|
|
5122
|
+
|
|
4863
5123
|
def show_gray_water_dialog(self):
|
|
4864
5124
|
"""Show the gray watershed parameter dialog."""
|
|
4865
5125
|
dialog = GrayWaterDialog(self)
|
|
@@ -4962,7 +5222,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4962
5222
|
|
|
4963
5223
|
my_network.edges = (my_network.nodes == 0) * my_network.edges
|
|
4964
5224
|
|
|
4965
|
-
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)
|
|
4966
5226
|
|
|
4967
5227
|
self.load_channel(1, my_network.edges, data = True)
|
|
4968
5228
|
self.load_channel(0, my_network.nodes, data = True)
|
|
@@ -4996,6 +5256,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4996
5256
|
|
|
4997
5257
|
self.load_channel(0, my_network.edges, data = True)
|
|
4998
5258
|
|
|
5259
|
+
try:
|
|
5260
|
+
self.branch_dict[0] = self.branch_dict[1]
|
|
5261
|
+
self.branch_dict[1] = None
|
|
5262
|
+
except:
|
|
5263
|
+
pass
|
|
5264
|
+
|
|
4999
5265
|
self.delete_channel(1, False)
|
|
5000
5266
|
|
|
5001
5267
|
my_network.morph_proximity(search = [3,3], fastdil = True)
|
|
@@ -5012,14 +5278,51 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5012
5278
|
dialog = CentroidDialog(self)
|
|
5013
5279
|
dialog.exec()
|
|
5014
5280
|
|
|
5015
|
-
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):
|
|
5016
5319
|
"""show the dilate dialog"""
|
|
5017
|
-
dialog = DilateDialog(self)
|
|
5320
|
+
dialog = DilateDialog(self, args)
|
|
5018
5321
|
dialog.exec()
|
|
5019
5322
|
|
|
5020
|
-
def show_erode_dialog(self):
|
|
5323
|
+
def show_erode_dialog(self, args = None):
|
|
5021
5324
|
"""show the erode dialog"""
|
|
5022
|
-
dialog = ErodeDialog(self)
|
|
5325
|
+
dialog = ErodeDialog(self, args)
|
|
5023
5326
|
dialog.exec()
|
|
5024
5327
|
|
|
5025
5328
|
def show_hole_dialog(self):
|
|
@@ -5118,6 +5421,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5118
5421
|
dialog = ResizeDialog(self)
|
|
5119
5422
|
dialog.exec()
|
|
5120
5423
|
|
|
5424
|
+
def show_clean_dialog(self):
|
|
5425
|
+
dialog = CleanDialog(self)
|
|
5426
|
+
dialog.show()
|
|
5121
5427
|
|
|
5122
5428
|
def show_properties_dialog(self):
|
|
5123
5429
|
"""Show the properties dialog"""
|
|
@@ -5272,7 +5578,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5272
5578
|
|
|
5273
5579
|
elif sort == 'Merge Nodes':
|
|
5274
5580
|
try:
|
|
5275
|
-
|
|
5276
5581
|
if my_network.nodes is None:
|
|
5277
5582
|
QMessageBox.critical(
|
|
5278
5583
|
self,
|
|
@@ -5280,72 +5585,118 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5280
5585
|
"Please load your first set of nodes into the 'Nodes' channel first"
|
|
5281
5586
|
)
|
|
5282
5587
|
return
|
|
5283
|
-
|
|
5284
5588
|
if len(np.unique(my_network.nodes)) < 3:
|
|
5285
5589
|
self.show_label_dialog()
|
|
5286
|
-
|
|
5287
|
-
#
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
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
|
|
5339
5643
|
try:
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
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
|
+
|
|
5347
5699
|
except Exception as e:
|
|
5348
|
-
|
|
5349
5700
|
QMessageBox.critical(
|
|
5350
5701
|
self,
|
|
5351
5702
|
"Error Merging",
|
|
@@ -5368,8 +5719,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5368
5719
|
)
|
|
5369
5720
|
|
|
5370
5721
|
self.last_load = directory
|
|
5371
|
-
|
|
5722
|
+
self.last_saved = os.path.dirname(directory)
|
|
5723
|
+
self.last_save_name = directory
|
|
5372
5724
|
|
|
5725
|
+
self.channel_data = [None] * 5
|
|
5373
5726
|
if directory != "":
|
|
5374
5727
|
|
|
5375
5728
|
self.reset(network = True, xy_scale = 1, z_scale = 1, nodes = True, edges = True, network_overlay = True, id_overlay = True, update = False)
|
|
@@ -5672,6 +6025,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5672
6025
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
5673
6026
|
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
5674
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
|
+
|
|
5675
6038
|
def confirm_resize_dialog(self):
|
|
5676
6039
|
"""Shows a dialog asking user to resize image"""
|
|
5677
6040
|
msg = QMessageBox()
|
|
@@ -5682,12 +6045,41 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5682
6045
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
5683
6046
|
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
5684
6047
|
|
|
5685
|
-
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):
|
|
5686
6079
|
"""Load a channel and enable active channel selection if needed."""
|
|
5687
6080
|
|
|
5688
6081
|
try:
|
|
5689
6082
|
|
|
5690
|
-
self.hold_update = True
|
|
5691
6083
|
if not data: # For solo loading
|
|
5692
6084
|
filename, _ = QFileDialog.getOpenFileName(
|
|
5693
6085
|
self,
|
|
@@ -5707,8 +6099,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5707
6099
|
try:
|
|
5708
6100
|
if file_extension in ['tif', 'tiff']:
|
|
5709
6101
|
import tifffile
|
|
5710
|
-
self.channel_data[channel_index] =
|
|
5711
|
-
|
|
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
|
|
6114
|
+
|
|
5712
6115
|
elif file_extension == 'nii':
|
|
5713
6116
|
import nibabel as nib
|
|
5714
6117
|
nii_img = nib.load(filename)
|
|
@@ -5755,7 +6158,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5755
6158
|
try:
|
|
5756
6159
|
if len(self.channel_data[channel_index].shape) == 3: # potentially 2D RGB
|
|
5757
6160
|
if self.channel_data[channel_index].shape[-1] in (3, 4): # last dim is 3 or 4
|
|
5758
|
-
if not data
|
|
6161
|
+
if not data:
|
|
5759
6162
|
if self.confirm_rgb_dialog():
|
|
5760
6163
|
# User confirmed it's 2D RGB, expand to 4D
|
|
5761
6164
|
self.channel_data[channel_index] = np.expand_dims(self.channel_data[channel_index], axis=0)
|
|
@@ -5765,12 +6168,18 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5765
6168
|
except:
|
|
5766
6169
|
pass
|
|
5767
6170
|
|
|
5768
|
-
if
|
|
5769
|
-
|
|
5770
|
-
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:
|
|
5771
6180
|
self.channel_data[channel_index] = self.reduce_rgb_dimension(self.channel_data[channel_index], 'weight')
|
|
5772
|
-
|
|
5773
|
-
|
|
6181
|
+
except:
|
|
6182
|
+
pass
|
|
5774
6183
|
|
|
5775
6184
|
reset_resize = False
|
|
5776
6185
|
|
|
@@ -5779,7 +6188,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5779
6188
|
if self.highlight_overlay is not None: #Make sure highlight overlay is always the same shape as new images
|
|
5780
6189
|
try:
|
|
5781
6190
|
if self.channel_data[i].shape[:3] != self.highlight_overlay.shape:
|
|
5782
|
-
self.resizing = True
|
|
5783
6191
|
reset_resize = True
|
|
5784
6192
|
self.highlight_overlay = None
|
|
5785
6193
|
except:
|
|
@@ -5806,51 +6214,52 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5806
6214
|
my_network.id_overlay = self.channel_data[channel_index]
|
|
5807
6215
|
|
|
5808
6216
|
# Enable the channel button
|
|
5809
|
-
|
|
5810
|
-
|
|
6217
|
+
if channel_index != 4:
|
|
6218
|
+
self.channel_buttons[channel_index].setEnabled(True)
|
|
6219
|
+
self.delete_buttons[channel_index].setEnabled(True)
|
|
5811
6220
|
|
|
5812
6221
|
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
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)
|
|
5816
6225
|
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
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
|
|
5826
6238
|
else:
|
|
5827
|
-
self.slice_slider.
|
|
5828
|
-
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)
|
|
5829
6247
|
else:
|
|
5830
|
-
self.slice_slider.setEnabled(
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
if self.slice_slider.value() < self.channel_data[channel_index].shape[0] - 1:
|
|
5834
|
-
self.current_slice = self.slice_slider.value()
|
|
5835
|
-
else:
|
|
5836
|
-
self.current_slice = 0
|
|
5837
|
-
self.slice_slider.setValue(0)
|
|
5838
|
-
else:
|
|
5839
|
-
self.slice_slider.setEnabled(False)
|
|
5840
|
-
except:
|
|
5841
|
-
pass
|
|
6248
|
+
self.slice_slider.setEnabled(False)
|
|
6249
|
+
except:
|
|
6250
|
+
pass
|
|
5842
6251
|
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
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)
|
|
5847
6256
|
|
|
5848
|
-
|
|
5849
|
-
|
|
6257
|
+
if not self.channel_buttons[channel_index].isChecked():
|
|
6258
|
+
self.channel_buttons[channel_index].click()
|
|
5850
6259
|
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
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
|
|
5854
6263
|
|
|
5855
6264
|
try:
|
|
5856
6265
|
if assign_shape: #keep original shape tracked to undo resampling.
|
|
@@ -5865,7 +6274,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5865
6274
|
|
|
5866
6275
|
if self.shape == self.channel_data[channel_index].shape:
|
|
5867
6276
|
preserve_zoom = (self.ax.get_xlim(), self.ax.get_ylim())
|
|
5868
|
-
|
|
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)
|
|
5869
6284
|
if self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor:
|
|
5870
6285
|
self.throttle = True
|
|
5871
6286
|
else:
|
|
@@ -5873,8 +6288,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5873
6288
|
|
|
5874
6289
|
|
|
5875
6290
|
self.img_height, self.img_width = self.shape[1], self.shape[2]
|
|
5876
|
-
self.original_ylim, self.original_xlim = (self.shape[1] + 0.5, -0.5), (-0.5, self.shape[2] - 0.5)
|
|
5877
|
-
#print(self.original_xlim)
|
|
5878
6291
|
|
|
5879
6292
|
self.completed_paint_strokes = [] #Reset pending paint operations
|
|
5880
6293
|
self.current_stroke_points = []
|
|
@@ -5884,6 +6297,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5884
6297
|
self.current_operation = []
|
|
5885
6298
|
self.current_operation_type = None
|
|
5886
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
|
+
|
|
5887
6306
|
if self.pan_mode:
|
|
5888
6307
|
self.pan_button.click()
|
|
5889
6308
|
if self.show_channels:
|
|
@@ -5892,7 +6311,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5892
6311
|
elif not end_paint:
|
|
5893
6312
|
|
|
5894
6313
|
self.update_display(reset_resize = reset_resize, preserve_zoom = preserve_zoom)
|
|
5895
|
-
|
|
5896
6314
|
|
|
5897
6315
|
except Exception as e:
|
|
5898
6316
|
|
|
@@ -5901,7 +6319,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5901
6319
|
QMessageBox.critical(
|
|
5902
6320
|
self,
|
|
5903
6321
|
"Error Loading File",
|
|
5904
|
-
f"Failed to load
|
|
6322
|
+
f"Failed to load file: {str(e)}"
|
|
5905
6323
|
)
|
|
5906
6324
|
|
|
5907
6325
|
def delete_channel(self, channel_index, called = True, update = True):
|
|
@@ -6103,12 +6521,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6103
6521
|
def update_slice(self):
|
|
6104
6522
|
"""Queue a slice update when slider moves."""
|
|
6105
6523
|
# Store current view settings
|
|
6106
|
-
if
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
else:
|
|
6110
|
-
current_xlim = None
|
|
6111
|
-
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
|
+
|
|
6112
6527
|
|
|
6113
6528
|
# Store the pending slice and view settings
|
|
6114
6529
|
self.pending_slice = (self.slice_slider.value(), (current_xlim, current_ylim))
|
|
@@ -6130,15 +6545,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6130
6545
|
self.pm.convert_virtual_strokes_to_data()
|
|
6131
6546
|
self.current_slice = slice_value
|
|
6132
6547
|
if self.preview:
|
|
6548
|
+
self.highlight_overlay = None
|
|
6549
|
+
self.mini_overlay_data = None
|
|
6550
|
+
self.mini_overlay = False
|
|
6133
6551
|
self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
|
|
6134
6552
|
elif self.mini_overlay == True: #If we are rendering the highlight overlay for selected values one at a time.
|
|
6135
6553
|
self.create_mini_overlay(node_indices = self.clicked_values['nodes'], edge_indices = self.clicked_values['edges'])
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
self.
|
|
6140
|
-
|
|
6141
|
-
|
|
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)
|
|
6142
6561
|
if self.pan_mode:
|
|
6143
6562
|
self.pan_button.click()
|
|
6144
6563
|
self.pending_slice = None
|
|
@@ -6155,7 +6574,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6155
6574
|
self.channel_brightness[channel_index]['max'] = max_val / 65535
|
|
6156
6575
|
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
6157
6576
|
|
|
6158
|
-
|
|
6159
6577
|
def update_display(self, preserve_zoom=None, dims=None, called=False, reset_resize=False, skip=False):
|
|
6160
6578
|
"""Optimized display update with view-based cropping for performance."""
|
|
6161
6579
|
try:
|
|
@@ -6175,8 +6593,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6175
6593
|
if self.resume:
|
|
6176
6594
|
self.machine_window.segmentation_worker.resume()
|
|
6177
6595
|
self.resume = False
|
|
6178
|
-
if self.prev_down != self.downsample_factor:
|
|
6179
|
-
self.validate_downsample_input(text = self.prev_down)
|
|
6180
6596
|
|
|
6181
6597
|
if self.static_background is not None:
|
|
6182
6598
|
# Your existing virtual strokes conversion logic
|
|
@@ -6233,10 +6649,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6233
6649
|
for img in list(self.ax.get_images()):
|
|
6234
6650
|
img.remove()
|
|
6235
6651
|
# Clear measurement points
|
|
6236
|
-
|
|
6237
|
-
artist.
|
|
6238
|
-
|
|
6239
|
-
|
|
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
|
|
6240
6659
|
# Determine the current view bounds (either from preserve_zoom or current state)
|
|
6241
6660
|
if preserve_zoom:
|
|
6242
6661
|
current_xlim, current_ylim = preserve_zoom
|
|
@@ -6303,7 +6722,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6303
6722
|
return cropped[::factor, ::factor, :]
|
|
6304
6723
|
else:
|
|
6305
6724
|
return cropped
|
|
6306
|
-
|
|
6307
6725
|
|
|
6308
6726
|
# Update channel images efficiently with cropping and downsampling
|
|
6309
6727
|
for channel in range(4):
|
|
@@ -6378,13 +6796,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6378
6796
|
|
|
6379
6797
|
im = self.ax.imshow(normalized_image, alpha=0.7, cmap=custom_cmap,
|
|
6380
6798
|
vmin=0, vmax=1, extent=crop_extent)
|
|
6381
|
-
|
|
6382
6799
|
# Handle preview, overlays, and measurements (apply cropping here too)
|
|
6383
|
-
#if self.preview and not called:
|
|
6384
|
-
# self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
|
|
6385
6800
|
|
|
6386
6801
|
# Overlay handling (optimized with cropping and downsampling)
|
|
6387
|
-
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:
|
|
6388
6803
|
highlight_cmap = LinearSegmentedColormap.from_list('highlight', [(0, 0, 0, 0), (1, 1, 0, 1)])
|
|
6389
6804
|
display_overlay = crop_and_downsample_image(
|
|
6390
6805
|
self.mini_overlay_data, y_min_padded, y_max_padded,
|
|
@@ -6403,34 +6818,88 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6403
6818
|
[(0, 0, 0, 0), (1, 1, 0, 1), (0, 0.7, 1, 1)])
|
|
6404
6819
|
self.ax.imshow(display_highlight, cmap=highlight_cmap, vmin=0, vmax=2, alpha=0.3, extent=crop_extent)
|
|
6405
6820
|
|
|
6406
|
-
# Redraw measurement points efficiently
|
|
6821
|
+
# Redraw measurement points efficiently
|
|
6407
6822
|
# Only draw points that are within the visible region for additional performance
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
current_xlim[0] <= x1 <= current_xlim[1] and
|
|
6416
|
-
current_ylim[1] <= y1 <= current_ylim[0])
|
|
6417
|
-
point2_visible = (z2 == self.current_slice and
|
|
6418
|
-
current_xlim[0] <= x2 <= current_xlim[1] and
|
|
6419
|
-
current_ylim[1] <= y2 <= current_ylim[0])
|
|
6420
|
-
|
|
6421
|
-
if point1_visible:
|
|
6422
|
-
pt1 = self.ax.plot(x1, y1, 'yo', markersize=8)[0]
|
|
6423
|
-
txt1 = self.ax.text(x1, y1+5, str(pair_idx), color='white', ha='center', va='bottom')
|
|
6424
|
-
self.measurement_artists.extend([pt1, txt1])
|
|
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
|
|
6425
6830
|
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
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--'
|
|
6840
|
+
|
|
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])
|
|
6430
6896
|
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
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)
|
|
6434
6903
|
|
|
6435
6904
|
# Store current view limits for next update
|
|
6436
6905
|
self.ax._current_xlim = current_xlim
|
|
@@ -6449,15 +6918,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6449
6918
|
if reset_resize:
|
|
6450
6919
|
self.resizing = False
|
|
6451
6920
|
|
|
6452
|
-
#
|
|
6921
|
+
# draw_idle
|
|
6453
6922
|
self.canvas.draw_idle()
|
|
6454
6923
|
|
|
6924
|
+
|
|
6455
6925
|
except Exception as e:
|
|
6456
6926
|
pass
|
|
6457
|
-
#import traceback
|
|
6458
|
-
#print(traceback.format_exc())
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
6927
|
|
|
6462
6928
|
|
|
6463
6929
|
def get_channel_image(self, channel):
|
|
@@ -6567,10 +7033,18 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6567
7033
|
dialog = RadDialog(self)
|
|
6568
7034
|
dialog.exec()
|
|
6569
7035
|
|
|
7036
|
+
def show_branchstat_dialog(self):
|
|
7037
|
+
dialog = BranchStatDialog(self)
|
|
7038
|
+
dialog.exec()
|
|
7039
|
+
|
|
6570
7040
|
def show_interaction_dialog(self):
|
|
6571
7041
|
dialog = InteractionDialog(self)
|
|
6572
7042
|
dialog.exec()
|
|
6573
7043
|
|
|
7044
|
+
def show_violin_dialog(self):
|
|
7045
|
+
dialog = ViolinDialog(self)
|
|
7046
|
+
dialog.show()
|
|
7047
|
+
|
|
6574
7048
|
def show_degree_dialog(self):
|
|
6575
7049
|
dialog = DegreeDialog(self)
|
|
6576
7050
|
dialog.exec()
|
|
@@ -7904,11 +8378,6 @@ class MergeNodeIdDialog(QDialog):
|
|
|
7904
8378
|
self.mode_selector.addItems(["Auto-Binarize(Otsu)/Presegmented", "Manual (Interactive Thresholder)"])
|
|
7905
8379
|
self.mode_selector.setCurrentIndex(1) # Default to Mode 1
|
|
7906
8380
|
layout.addRow("Binarization Strategy:", self.mode_selector)
|
|
7907
|
-
|
|
7908
|
-
self.umap = QPushButton("If using manual threshold: Also generate identity UMAP?")
|
|
7909
|
-
self.umap.setCheckable(True)
|
|
7910
|
-
self.umap.setChecked(True)
|
|
7911
|
-
layout.addWidget(self.umap)
|
|
7912
8381
|
|
|
7913
8382
|
self.include = QPushButton("Include When a Node is Negative for an ID?")
|
|
7914
8383
|
self.include.setCheckable(True)
|
|
@@ -7965,7 +8434,7 @@ class MergeNodeIdDialog(QDialog):
|
|
|
7965
8434
|
z_scale = float(self.z_scale.text()) if self.z_scale.text().strip() else 1
|
|
7966
8435
|
data = self.parent().channel_data[0]
|
|
7967
8436
|
include = self.include.isChecked()
|
|
7968
|
-
umap =
|
|
8437
|
+
umap = True
|
|
7969
8438
|
|
|
7970
8439
|
if data is None:
|
|
7971
8440
|
return
|
|
@@ -8101,15 +8570,13 @@ class MergeNodeIdDialog(QDialog):
|
|
|
8101
8570
|
result = {key: np.array([d[key] for d in id_dicts]) for key in all_keys}
|
|
8102
8571
|
|
|
8103
8572
|
|
|
8104
|
-
self.parent().format_for_upperright_table(result, 'NodeID', good_list, 'Mean Intensity')
|
|
8105
|
-
if umap:
|
|
8106
|
-
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")')
|
|
8107
8574
|
|
|
8108
8575
|
|
|
8109
8576
|
QMessageBox.information(
|
|
8110
8577
|
self,
|
|
8111
8578
|
"Success",
|
|
8112
|
-
"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)"
|
|
8113
8580
|
)
|
|
8114
8581
|
|
|
8115
8582
|
self.accept()
|
|
@@ -8131,6 +8598,89 @@ class MergeNodeIdDialog(QDialog):
|
|
|
8131
8598
|
print(traceback.format_exc())
|
|
8132
8599
|
#print(f"Error: {e}")
|
|
8133
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
|
+
|
|
8134
8684
|
|
|
8135
8685
|
class Show3dDialog(QDialog):
|
|
8136
8686
|
def __init__(self, parent=None):
|
|
@@ -8198,6 +8748,9 @@ class Show3dDialog(QDialog):
|
|
|
8198
8748
|
if visible:
|
|
8199
8749
|
arrays_4d.append(channel)
|
|
8200
8750
|
|
|
8751
|
+
if self.parent().thresh_window_ref is not None:
|
|
8752
|
+
self.parent().thresh_window_ref.make_full_highlight()
|
|
8753
|
+
|
|
8201
8754
|
if self.parent().highlight_overlay is not None or self.parent().mini_overlay_data is not None:
|
|
8202
8755
|
if self.parent().mini_overlay == True:
|
|
8203
8756
|
self.parent().create_highlight_overlay(node_indices = self.parent().clicked_values['nodes'], edge_indices = self.parent().clicked_values['edges'])
|
|
@@ -8209,6 +8762,11 @@ class Show3dDialog(QDialog):
|
|
|
8209
8762
|
self.accept()
|
|
8210
8763
|
|
|
8211
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
|
+
)
|
|
8212
8770
|
print(f"Error: {e}")
|
|
8213
8771
|
import traceback
|
|
8214
8772
|
print(traceback.format_exc())
|
|
@@ -8224,6 +8782,9 @@ class NetOverlayDialog(QDialog):
|
|
|
8224
8782
|
|
|
8225
8783
|
layout = QFormLayout(self)
|
|
8226
8784
|
|
|
8785
|
+
self.downsample = QLineEdit("")
|
|
8786
|
+
layout.addRow("Downsample Factor While Drawing? (Int - Makes the outputted lines larger):", self.downsample)
|
|
8787
|
+
|
|
8227
8788
|
# Add Run button
|
|
8228
8789
|
run_button = QPushButton("Generate (Will go to Overlay 1)")
|
|
8229
8790
|
run_button.clicked.connect(self.netoverlay)
|
|
@@ -8240,7 +8801,16 @@ class NetOverlayDialog(QDialog):
|
|
|
8240
8801
|
if my_network.node_centroids is None:
|
|
8241
8802
|
return
|
|
8242
8803
|
|
|
8243
|
-
|
|
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
|
+
|
|
8244
8814
|
|
|
8245
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()))
|
|
8246
8816
|
|
|
@@ -8267,6 +8837,9 @@ class IdOverlayDialog(QDialog):
|
|
|
8267
8837
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
8268
8838
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
8269
8839
|
|
|
8840
|
+
self.downsample = QLineEdit("")
|
|
8841
|
+
layout.addRow("Downsample Factor While Drawing? (Int - Makes the outputted numbers larger):", self.downsample)
|
|
8842
|
+
|
|
8270
8843
|
# Add Run button
|
|
8271
8844
|
run_button = QPushButton("Generate (Will go to Overlay 2)")
|
|
8272
8845
|
run_button.clicked.connect(self.idoverlay)
|
|
@@ -8274,38 +8847,51 @@ class IdOverlayDialog(QDialog):
|
|
|
8274
8847
|
|
|
8275
8848
|
def idoverlay(self):
|
|
8276
8849
|
|
|
8277
|
-
|
|
8850
|
+
try:
|
|
8278
8851
|
|
|
8279
|
-
|
|
8852
|
+
accepted_mode = self.mode_selector.currentIndex()
|
|
8280
8853
|
|
|
8281
|
-
|
|
8854
|
+
try:
|
|
8855
|
+
downsample = float(self.downsample.text()) if self.downsample.text() else None
|
|
8856
|
+
except ValueError:
|
|
8857
|
+
downsample = None
|
|
8282
8858
|
|
|
8283
|
-
|
|
8859
|
+
if accepted_mode == 0:
|
|
8284
8860
|
|
|
8285
|
-
|
|
8286
|
-
return
|
|
8861
|
+
if my_network.node_centroids is None:
|
|
8287
8862
|
|
|
8288
|
-
|
|
8863
|
+
self.parent().show_centroid_dialog()
|
|
8289
8864
|
|
|
8290
|
-
|
|
8865
|
+
if my_network.node_centroids is None:
|
|
8866
|
+
return
|
|
8291
8867
|
|
|
8292
|
-
|
|
8868
|
+
elif accepted_mode == 1:
|
|
8293
8869
|
|
|
8294
|
-
|
|
8295
|
-
return
|
|
8870
|
+
if my_network.edge_centroids is None:
|
|
8296
8871
|
|
|
8297
|
-
|
|
8872
|
+
self.parent().show_centroid_dialog()
|
|
8298
8873
|
|
|
8299
|
-
|
|
8874
|
+
if my_network.edge_centroids is None:
|
|
8875
|
+
return
|
|
8300
8876
|
|
|
8301
|
-
|
|
8877
|
+
if accepted_mode == 0:
|
|
8302
8878
|
|
|
8303
|
-
|
|
8879
|
+
my_network.id_overlay = my_network.draw_node_indices(down_factor = downsample)
|
|
8304
8880
|
|
|
8881
|
+
elif accepted_mode == 1:
|
|
8305
8882
|
|
|
8306
|
-
|
|
8883
|
+
my_network.id_overlay = my_network.draw_edge_indices(down_factor = downsample)
|
|
8884
|
+
|
|
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}")
|
|
8307
8894
|
|
|
8308
|
-
self.accept()
|
|
8309
8895
|
|
|
8310
8896
|
class ColorOverlayDialog(QDialog):
|
|
8311
8897
|
|
|
@@ -8327,7 +8913,7 @@ class ColorOverlayDialog(QDialog):
|
|
|
8327
8913
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
8328
8914
|
|
|
8329
8915
|
self.down_factor = QLineEdit("")
|
|
8330
|
-
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)
|
|
8331
8917
|
|
|
8332
8918
|
# Add Run button
|
|
8333
8919
|
run_button = QPushButton("Generate (Will go to Overlay 2)")
|
|
@@ -8481,11 +9067,6 @@ class NetShowDialog(QDialog):
|
|
|
8481
9067
|
self.weighted.setCheckable(True)
|
|
8482
9068
|
self.weighted.setChecked(True)
|
|
8483
9069
|
layout.addRow("Use Weighted Network (Only for community graphs):", self.weighted)
|
|
8484
|
-
|
|
8485
|
-
# Optional saving:
|
|
8486
|
-
self.directory = QLineEdit()
|
|
8487
|
-
self.directory.setPlaceholderText("Does not save when empty")
|
|
8488
|
-
layout.addRow("Output Directory:", self.directory)
|
|
8489
9070
|
|
|
8490
9071
|
# Add Run button
|
|
8491
9072
|
run_button = QPushButton("Show Network")
|
|
@@ -8501,7 +9082,7 @@ class NetShowDialog(QDialog):
|
|
|
8501
9082
|
self.parent().show_centroid_dialog()
|
|
8502
9083
|
accepted_mode = self.mode_selector.currentIndex() # Convert to 1-based index
|
|
8503
9084
|
# Get directory (None if empty)
|
|
8504
|
-
directory =
|
|
9085
|
+
directory = None
|
|
8505
9086
|
|
|
8506
9087
|
weighted = self.weighted.isChecked()
|
|
8507
9088
|
|
|
@@ -8544,7 +9125,7 @@ class PartitionDialog(QDialog):
|
|
|
8544
9125
|
|
|
8545
9126
|
# Add mode selection dropdown
|
|
8546
9127
|
self.mode_selector = QComboBox()
|
|
8547
|
-
self.mode_selector.addItems(["
|
|
9128
|
+
self.mode_selector.addItems(["Louvain", "Label Propogation"])
|
|
8548
9129
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
8549
9130
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
8550
9131
|
|
|
@@ -8567,6 +9148,10 @@ class PartitionDialog(QDialog):
|
|
|
8567
9148
|
self.parent().prev_coms = None
|
|
8568
9149
|
|
|
8569
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
|
|
8570
9155
|
weighted = self.weighted.isChecked()
|
|
8571
9156
|
dostats = self.stats.isChecked()
|
|
8572
9157
|
|
|
@@ -8738,7 +9323,7 @@ class ComNeighborDialog(QDialog):
|
|
|
8738
9323
|
|
|
8739
9324
|
mode = self.mode.currentIndex()
|
|
8740
9325
|
|
|
8741
|
-
seed =
|
|
9326
|
+
seed = int(self.seed.text()) if self.seed.text().strip() else 42
|
|
8742
9327
|
|
|
8743
9328
|
limit = int(self.limit.text()) if self.limit.text().strip() else None
|
|
8744
9329
|
|
|
@@ -8843,9 +9428,6 @@ class RadialDialog(QDialog):
|
|
|
8843
9428
|
self.distance = QLineEdit("50")
|
|
8844
9429
|
layout.addRow("Bucket Distance for Searching For Node Neighbors (automatically scaled by xy and z scales):", self.distance)
|
|
8845
9430
|
|
|
8846
|
-
self.directory = QLineEdit("")
|
|
8847
|
-
layout.addRow("Output Directory:", self.directory)
|
|
8848
|
-
|
|
8849
9431
|
# Add Run button
|
|
8850
9432
|
run_button = QPushButton("Get Radial Distribution")
|
|
8851
9433
|
run_button.clicked.connect(self.radial)
|
|
@@ -8857,7 +9439,7 @@ class RadialDialog(QDialog):
|
|
|
8857
9439
|
|
|
8858
9440
|
distance = float(self.distance.text()) if self.distance.text().strip() else 50
|
|
8859
9441
|
|
|
8860
|
-
directory =
|
|
9442
|
+
directory = None
|
|
8861
9443
|
|
|
8862
9444
|
if my_network.node_centroids is None:
|
|
8863
9445
|
self.parent().show_centroid_dialog()
|
|
@@ -8888,12 +9470,16 @@ class NearNeighDialog(QDialog):
|
|
|
8888
9470
|
if my_network.node_identities is not None:
|
|
8889
9471
|
|
|
8890
9472
|
self.root = QComboBox()
|
|
8891
|
-
|
|
9473
|
+
roots = list(set(my_network.node_identities.values()))
|
|
9474
|
+
roots.sort()
|
|
9475
|
+
roots.append("All (Excluding Targets)")
|
|
9476
|
+
self.root.addItems(roots)
|
|
8892
9477
|
self.root.setCurrentIndex(0)
|
|
8893
9478
|
identities_layout.addRow("Root Identity to Search for Neighbor's IDs?", self.root)
|
|
8894
9479
|
|
|
8895
9480
|
self.targ = QComboBox()
|
|
8896
9481
|
neighs = list(set(my_network.node_identities.values()))
|
|
9482
|
+
neighs.sort()
|
|
8897
9483
|
neighs.append("All Others (Excluding Self)")
|
|
8898
9484
|
self.targ.addItems(neighs)
|
|
8899
9485
|
self.targ.setCurrentIndex(0)
|
|
@@ -8931,6 +9517,11 @@ class NearNeighDialog(QDialog):
|
|
|
8931
9517
|
self.numpy.setChecked(False)
|
|
8932
9518
|
self.numpy.clicked.connect(self.toggle_map)
|
|
8933
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)
|
|
8934
9525
|
|
|
8935
9526
|
main_layout.addWidget(heatmap_group)
|
|
8936
9527
|
|
|
@@ -9018,35 +9609,52 @@ class NearNeighDialog(QDialog):
|
|
|
9018
9609
|
except:
|
|
9019
9610
|
targ = None
|
|
9020
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
|
+
|
|
9021
9627
|
heatmap = self.map.isChecked()
|
|
9022
9628
|
threed = self.threed.isChecked()
|
|
9023
9629
|
numpy = self.numpy.isChecked()
|
|
9024
9630
|
num = int(self.num.text()) if self.num.text().strip() else 1
|
|
9025
9631
|
quant = self.quant.isChecked()
|
|
9026
9632
|
centroids = self.centroids.isChecked()
|
|
9633
|
+
|
|
9027
9634
|
if not centroids:
|
|
9635
|
+
print("Using 1 nearest neighbor due to not using centroids")
|
|
9028
9636
|
num = 1
|
|
9029
9637
|
|
|
9030
9638
|
if root is not None and targ is not None:
|
|
9031
9639
|
title = f"Nearest {num} Neighbor(s) Distance of {targ} from {root}"
|
|
9032
|
-
header = f"Shortest Distance to Closest {num} {targ}(s)"
|
|
9640
|
+
header = f"Avg Shortest Distance to Closest {num} {targ}(s)"
|
|
9033
9641
|
header2 = f"{root} Node ID"
|
|
9034
9642
|
header3 = f'Theoretical Uniform Distance to Closest {num} {targ}(s)'
|
|
9035
9643
|
else:
|
|
9036
9644
|
title = f"Nearest {num} Neighbor(s) Distance Between Nodes"
|
|
9037
|
-
header = f"Shortest Distance to Closest {num} Nodes"
|
|
9645
|
+
header = f"Avg Shortest Distance to Closest {num} Nodes"
|
|
9038
9646
|
header2 = "Root Node ID"
|
|
9039
9647
|
header3 = f'Simulated Theoretical Uniform Distance to Closest {num} Nodes'
|
|
9040
9648
|
|
|
9041
|
-
if
|
|
9649
|
+
if my_network.node_centroids is None:
|
|
9042
9650
|
self.parent().show_centroid_dialog()
|
|
9043
9651
|
if my_network.node_centroids is None:
|
|
9044
9652
|
return
|
|
9045
9653
|
|
|
9046
9654
|
if not numpy:
|
|
9047
|
-
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)
|
|
9048
9656
|
else:
|
|
9049
|
-
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)
|
|
9050
9658
|
self.parent().load_channel(3, overlay, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
9051
9659
|
|
|
9052
9660
|
if quant_overlay is not None:
|
|
@@ -9115,9 +9723,6 @@ class NeighborIdentityDialog(QDialog):
|
|
|
9115
9723
|
else:
|
|
9116
9724
|
self.root = None
|
|
9117
9725
|
|
|
9118
|
-
self.directory = QLineEdit("")
|
|
9119
|
-
layout.addRow("Output Directory:", self.directory)
|
|
9120
|
-
|
|
9121
9726
|
self.mode = QComboBox()
|
|
9122
9727
|
self.mode.addItems(["From Network - Based on Absolute Connectivity", "Use Labeled Nodes - Based on Morphological Neighborhood Densities"])
|
|
9123
9728
|
self.mode.setCurrentIndex(0)
|
|
@@ -9145,7 +9750,7 @@ class NeighborIdentityDialog(QDialog):
|
|
|
9145
9750
|
except:
|
|
9146
9751
|
pass
|
|
9147
9752
|
|
|
9148
|
-
directory =
|
|
9753
|
+
directory = None
|
|
9149
9754
|
|
|
9150
9755
|
mode = self.mode.currentIndex()
|
|
9151
9756
|
|
|
@@ -9599,6 +10204,22 @@ class InteractionDialog(QDialog):
|
|
|
9599
10204
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
9600
10205
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
9601
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
|
+
|
|
9602
10223
|
self.fastdil = QPushButton("Fast Dilate")
|
|
9603
10224
|
self.fastdil.setCheckable(True)
|
|
9604
10225
|
self.fastdil.setChecked(False)
|
|
@@ -9622,10 +10243,16 @@ class InteractionDialog(QDialog):
|
|
|
9622
10243
|
|
|
9623
10244
|
|
|
9624
10245
|
fastdil = self.fastdil.isChecked()
|
|
10246
|
+
length = self.length.isChecked()
|
|
10247
|
+
auto = self.auto.isChecked()
|
|
9625
10248
|
|
|
9626
|
-
result = my_network.interactions(search = node_search, cores = accepted_mode, fastdil = fastdil)
|
|
10249
|
+
result = my_network.interactions(search = node_search, cores = accepted_mode, skele = length, length = length, auto = auto, fastdil = fastdil)
|
|
10250
|
+
|
|
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')
|
|
9627
10255
|
|
|
9628
|
-
self.parent().format_for_upperright_table(result, 'Node ID', ['Volume of Nearby Edge (Scaled)', 'Volume of Search Region'], title = 'Node/Edge Interactions')
|
|
9629
10256
|
|
|
9630
10257
|
self.accept()
|
|
9631
10258
|
|
|
@@ -9637,70 +10264,471 @@ class InteractionDialog(QDialog):
|
|
|
9637
10264
|
print(f"Error finding interactions: {e}")
|
|
9638
10265
|
|
|
9639
10266
|
|
|
9640
|
-
class
|
|
9641
|
-
|
|
10267
|
+
class ViolinDialog(QDialog):
|
|
9642
10268
|
|
|
9643
10269
|
def __init__(self, parent=None):
|
|
9644
10270
|
|
|
9645
10271
|
super().__init__(parent)
|
|
9646
|
-
self.setWindowTitle("Degree Parameters")
|
|
9647
|
-
self.setModal(True)
|
|
9648
10272
|
|
|
9649
|
-
|
|
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
|
+
)
|
|
9650
10278
|
|
|
9651
|
-
|
|
10279
|
+
try:
|
|
10280
|
+
try:
|
|
10281
|
+
self.df = self.parent().load_file()
|
|
10282
|
+
except:
|
|
10283
|
+
return
|
|
9652
10284
|
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
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
|
|
9658
10292
|
|
|
9659
|
-
|
|
9660
|
-
|
|
10293
|
+
self.setWindowTitle("Violin Parameters")
|
|
10294
|
+
self.setModal(False)
|
|
9661
10295
|
|
|
9662
|
-
|
|
9663
|
-
layout.addRow("down_factor (for speeding up overlay generation - ignore if only returning degrees:", self.down_factor)
|
|
10296
|
+
layout = QFormLayout(self)
|
|
9664
10297
|
|
|
9665
|
-
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
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)
|
|
9669
10321
|
|
|
9670
|
-
|
|
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)
|
|
9671
10326
|
|
|
9672
|
-
|
|
10327
|
+
run_button2 = QPushButton("Show Z-score UMAP")
|
|
10328
|
+
run_button2.clicked.connect(self.run2)
|
|
9673
10329
|
|
|
9674
|
-
|
|
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)
|
|
9675
10334
|
|
|
9676
|
-
|
|
9677
|
-
degree_dict, overlay = my_network.get_degrees(heatmap = True)
|
|
9678
|
-
self.parent().format_for_upperright_table(degree_dict, 'Node ID', 'Degree', title = 'Degrees of nodes')
|
|
9679
|
-
self.parent().load_channel(3, channel_data = overlay, data = True)
|
|
9680
|
-
self.accept()
|
|
9681
|
-
return
|
|
10335
|
+
layout.addRow(self.mode_selector, run_button2)
|
|
9682
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)
|
|
9683
10340
|
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
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))
|
|
9688
10346
|
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
except ValueError:
|
|
9692
|
-
mask_limiter = 1
|
|
10347
|
+
# QFormLayout's addRow takes (label/widget, field/widget)
|
|
10348
|
+
layout.addRow(run_button3, self.kmeans_num_input)
|
|
9693
10349
|
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
active_data = self.parent().channel_data[self.parent().active_channel]
|
|
9699
|
-
if active_data is None:
|
|
9700
|
-
raise ValueError("No active image selected")
|
|
10350
|
+
except:
|
|
10351
|
+
import traceback
|
|
10352
|
+
print(traceback.format_exc())
|
|
10353
|
+
QTimer.singleShot(0, self.close)
|
|
9701
10354
|
|
|
9702
|
-
|
|
9703
|
-
|
|
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()
|
|
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"])
|
|
10684
|
+
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
10685
|
+
layout.addRow("Execution Mode:", self.mode_selector)
|
|
10686
|
+
|
|
10687
|
+
self.mask_limiter = QLineEdit("1")
|
|
10688
|
+
layout.addRow("Proportion of high degree nodes to keep (ignore if only returning degrees)", self.mask_limiter)
|
|
10689
|
+
|
|
10690
|
+
self.down_factor = QLineEdit("1")
|
|
10691
|
+
layout.addRow("down_factor (for speeding up overlay generation - ignore if only returning degrees:", self.down_factor)
|
|
10692
|
+
|
|
10693
|
+
# Add Run button
|
|
10694
|
+
run_button = QPushButton("Get Degrees")
|
|
10695
|
+
run_button.clicked.connect(self.degs)
|
|
10696
|
+
layout.addWidget(run_button)
|
|
10697
|
+
|
|
10698
|
+
def degs(self):
|
|
10699
|
+
|
|
10700
|
+
try:
|
|
10701
|
+
|
|
10702
|
+
accepted_mode = self.mode_selector.currentIndex()
|
|
10703
|
+
|
|
10704
|
+
if accepted_mode == 3:
|
|
10705
|
+
degree_dict, overlay = my_network.get_degrees(heatmap = True)
|
|
10706
|
+
self.parent().format_for_upperright_table(degree_dict, 'Node ID', 'Degree', title = 'Degrees of nodes')
|
|
10707
|
+
self.parent().load_channel(3, channel_data = overlay, data = True)
|
|
10708
|
+
self.accept()
|
|
10709
|
+
return
|
|
10710
|
+
|
|
10711
|
+
|
|
10712
|
+
try:
|
|
10713
|
+
down_factor = float(self.down_factor.text()) if self.down_factor.text() else 1
|
|
10714
|
+
except ValueError:
|
|
10715
|
+
down_factor = 1
|
|
10716
|
+
|
|
10717
|
+
try:
|
|
10718
|
+
mask_limiter = float(self.mask_limiter.text()) if self.mask_limiter.text() else 1
|
|
10719
|
+
except ValueError:
|
|
10720
|
+
mask_limiter = 1
|
|
10721
|
+
|
|
10722
|
+
if self.parent().active_channel == 1:
|
|
10723
|
+
active_data = self.parent().channel_data[0]
|
|
10724
|
+
else:
|
|
10725
|
+
# Get the active channel data from parent
|
|
10726
|
+
active_data = self.parent().channel_data[self.parent().active_channel]
|
|
10727
|
+
if active_data is None:
|
|
10728
|
+
raise ValueError("No active image selected")
|
|
10729
|
+
|
|
10730
|
+
if my_network.node_centroids is None and accepted_mode > 0:
|
|
10731
|
+
self.parent().show_centroid_dialog()
|
|
9704
10732
|
if my_network.node_centroids is None:
|
|
9705
10733
|
accepted_mode == 0
|
|
9706
10734
|
print("Error retrieving centroids")
|
|
@@ -9892,6 +10920,9 @@ class MotherDialog(QDialog):
|
|
|
9892
10920
|
|
|
9893
10921
|
except Exception as e:
|
|
9894
10922
|
|
|
10923
|
+
import traceback
|
|
10924
|
+
print(traceback.format_exc())
|
|
10925
|
+
|
|
9895
10926
|
print(f"Error finding mothers: {e}")
|
|
9896
10927
|
|
|
9897
10928
|
|
|
@@ -10010,7 +11041,7 @@ class ResizeDialog(QDialog):
|
|
|
10010
11041
|
|
|
10011
11042
|
def run_resize(self, undo = False, upsize = True, special = False):
|
|
10012
11043
|
try:
|
|
10013
|
-
self.parent().resizing =
|
|
11044
|
+
self.parent().resizing = True
|
|
10014
11045
|
# Get parameters
|
|
10015
11046
|
try:
|
|
10016
11047
|
resize = float(self.resize.text()) if self.resize.text() else None
|
|
@@ -10125,6 +11156,7 @@ class ResizeDialog(QDialog):
|
|
|
10125
11156
|
if channel is not None:
|
|
10126
11157
|
self.parent().slice_slider.setMinimum(0)
|
|
10127
11158
|
self.parent().slice_slider.setMaximum(channel.shape[0] - 1)
|
|
11159
|
+
self.parent().shape = channel.shape
|
|
10128
11160
|
break
|
|
10129
11161
|
|
|
10130
11162
|
if not special:
|
|
@@ -10194,10 +11226,7 @@ class ResizeDialog(QDialog):
|
|
|
10194
11226
|
except Exception as e:
|
|
10195
11227
|
print(f"Error loading edge centroid table: {e}")
|
|
10196
11228
|
|
|
10197
|
-
|
|
10198
11229
|
self.parent().update_display()
|
|
10199
|
-
self.reset_fields()
|
|
10200
|
-
self.parent().resizing = False
|
|
10201
11230
|
self.accept()
|
|
10202
11231
|
|
|
10203
11232
|
except Exception as e:
|
|
@@ -10206,6 +11235,79 @@ class ResizeDialog(QDialog):
|
|
|
10206
11235
|
print(traceback.format_exc())
|
|
10207
11236
|
QMessageBox.critical(self, "Error", f"Failed to resize: {str(e)}")
|
|
10208
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
|
+
|
|
10209
11311
|
|
|
10210
11312
|
class OverrideDialog(QDialog):
|
|
10211
11313
|
def __init__(self, parent=None):
|
|
@@ -10439,7 +11541,7 @@ class LabelDialog(QDialog):
|
|
|
10439
11541
|
class SLabelDialog(QDialog):
|
|
10440
11542
|
def __init__(self, parent=None):
|
|
10441
11543
|
super().__init__(parent)
|
|
10442
|
-
self.setWindowTitle("
|
|
11544
|
+
self.setWindowTitle("Label a binary image based on it's voxels proximity to labeled components of a second image?")
|
|
10443
11545
|
self.setModal(True)
|
|
10444
11546
|
|
|
10445
11547
|
layout = QFormLayout(self)
|
|
@@ -10451,7 +11553,7 @@ class SLabelDialog(QDialog):
|
|
|
10451
11553
|
self.mode_selector.setCurrentIndex(0) # Default to Mode 1
|
|
10452
11554
|
layout.addRow("Prelabeled Array:", self.mode_selector)
|
|
10453
11555
|
|
|
10454
|
-
layout.addRow(QLabel("Will Label
|
|
11556
|
+
layout.addRow(QLabel("Will Label Binary Foreground Voxels in: "))
|
|
10455
11557
|
|
|
10456
11558
|
# Add mode selection dropdown
|
|
10457
11559
|
self.target_selector = QComboBox()
|
|
@@ -10497,10 +11599,6 @@ class SLabelDialog(QDialog):
|
|
|
10497
11599
|
# Update both the display data and the network object
|
|
10498
11600
|
binary_array = sdl.smart_label(binary_array, label_array, directory = None, GPU = GPU, predownsample = down_factor, remove_template = True)
|
|
10499
11601
|
|
|
10500
|
-
label_array = sdl.invert_array(label_array)
|
|
10501
|
-
|
|
10502
|
-
binary_array = binary_array * label_array
|
|
10503
|
-
|
|
10504
11602
|
self.parent().load_channel(accepted_target, binary_array, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
10505
11603
|
|
|
10506
11604
|
self.accept()
|
|
@@ -10579,12 +11677,11 @@ class ThresholdDialog(QDialog):
|
|
|
10579
11677
|
print("Error - please calculate network first")
|
|
10580
11678
|
return
|
|
10581
11679
|
|
|
10582
|
-
if self.parent().mini_overlay_data is not None:
|
|
10583
|
-
self.parent().mini_overlay_data = None
|
|
10584
|
-
|
|
10585
11680
|
thresh_window = ThresholdWindow(self.parent(), accepted_mode)
|
|
10586
11681
|
thresh_window.show() # Non-modal window
|
|
10587
11682
|
self.highlight_overlay = None
|
|
11683
|
+
#self.mini_overlay = False
|
|
11684
|
+
self.mini_overlay_data = None
|
|
10588
11685
|
self.accept()
|
|
10589
11686
|
except:
|
|
10590
11687
|
import traceback
|
|
@@ -11517,6 +12614,7 @@ class ThresholdWindow(QMainWindow):
|
|
|
11517
12614
|
|
|
11518
12615
|
def __init__(self, parent=None, accepted_mode=0):
|
|
11519
12616
|
super().__init__(parent)
|
|
12617
|
+
self.parent().thresh_window_ref = self
|
|
11520
12618
|
self.setWindowTitle("Threshold")
|
|
11521
12619
|
|
|
11522
12620
|
self.accepted_mode = accepted_mode
|
|
@@ -11558,16 +12656,23 @@ class ThresholdWindow(QMainWindow):
|
|
|
11558
12656
|
self.parent().bounds = False
|
|
11559
12657
|
|
|
11560
12658
|
elif accepted_mode == 0:
|
|
11561
|
-
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
|
|
11565
|
-
|
|
11566
|
-
|
|
11567
|
-
|
|
11568
|
-
self.
|
|
11569
|
-
|
|
11570
|
-
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
|
+
|
|
11571
12676
|
self.bounds = True
|
|
11572
12677
|
self.parent().bounds = True
|
|
11573
12678
|
|
|
@@ -11580,16 +12685,26 @@ class ThresholdWindow(QMainWindow):
|
|
|
11580
12685
|
layout.addWidget(self.canvas)
|
|
11581
12686
|
|
|
11582
12687
|
# Pre-compute histogram with numpy
|
|
11583
|
-
|
|
11584
|
-
|
|
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]
|
|
11585
12700
|
|
|
11586
12701
|
# Plot pre-computed histogram
|
|
11587
12702
|
self.ax = fig.add_subplot(111)
|
|
11588
12703
|
self.ax.bar(bin_centers, counts, width=bin_edges[1] - bin_edges[0], alpha=0.5)
|
|
11589
12704
|
|
|
11590
12705
|
# Add vertical lines for thresholds
|
|
11591
|
-
self.min_line = self.ax.axvline(
|
|
11592
|
-
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')
|
|
11593
12708
|
|
|
11594
12709
|
# Connect events for dragging
|
|
11595
12710
|
self.canvas.mpl_connect('button_press_event', self.on_press)
|
|
@@ -11597,13 +12712,6 @@ class ThresholdWindow(QMainWindow):
|
|
|
11597
12712
|
self.canvas.mpl_connect('button_release_event', self.on_release)
|
|
11598
12713
|
|
|
11599
12714
|
self.dragging = None
|
|
11600
|
-
|
|
11601
|
-
# Store histogram bounds
|
|
11602
|
-
if self.bounds:
|
|
11603
|
-
self.data_min = 0
|
|
11604
|
-
else:
|
|
11605
|
-
self.data_min = min(self.histo_list)
|
|
11606
|
-
self.data_max = max(self.histo_list)
|
|
11607
12715
|
|
|
11608
12716
|
# Create form layout for inputs
|
|
11609
12717
|
form_layout = QFormLayout()
|
|
@@ -11636,7 +12744,7 @@ class ThresholdWindow(QMainWindow):
|
|
|
11636
12744
|
button_layout.addWidget(run_button)
|
|
11637
12745
|
|
|
11638
12746
|
# Add Cancel button for external dialog use
|
|
11639
|
-
cancel_button = QPushButton("Cancel/Skip")
|
|
12747
|
+
cancel_button = QPushButton("Cancel/Skip (Retains Selection)")
|
|
11640
12748
|
cancel_button.clicked.connect(self.cancel_processing)
|
|
11641
12749
|
button_layout.addWidget(cancel_button)
|
|
11642
12750
|
|
|
@@ -11660,10 +12768,8 @@ class ThresholdWindow(QMainWindow):
|
|
|
11660
12768
|
self.processing_cancelled.emit()
|
|
11661
12769
|
self.close()
|
|
11662
12770
|
|
|
11663
|
-
def
|
|
11664
|
-
|
|
11665
|
-
self.parent().targs = None
|
|
11666
|
-
self.parent().bounds = False
|
|
12771
|
+
def make_full_highlight(self):
|
|
12772
|
+
|
|
11667
12773
|
try: # could probably be refactored but this just handles keeping the highlight elements if the user presses X
|
|
11668
12774
|
if self.chan == 0:
|
|
11669
12775
|
if not self.bounds:
|
|
@@ -11697,6 +12803,14 @@ class ThresholdWindow(QMainWindow):
|
|
|
11697
12803
|
pass
|
|
11698
12804
|
|
|
11699
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
|
+
|
|
11700
12814
|
def get_values_in_range_all_vols(self, chan, min_val, max_val):
|
|
11701
12815
|
output = []
|
|
11702
12816
|
if self.accepted_mode == 1:
|
|
@@ -11963,36 +13077,33 @@ class SmartDilateDialog(QDialog):
|
|
|
11963
13077
|
|
|
11964
13078
|
|
|
11965
13079
|
class DilateDialog(QDialog):
|
|
11966
|
-
def __init__(self, parent=None):
|
|
13080
|
+
def __init__(self, parent=None, args = None):
|
|
11967
13081
|
super().__init__(parent)
|
|
11968
13082
|
self.setWindowTitle("Dilate Parameters")
|
|
11969
13083
|
self.setModal(True)
|
|
11970
13084
|
|
|
11971
13085
|
layout = QFormLayout(self)
|
|
11972
13086
|
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
if my_network.xy_scale is not None:
|
|
11977
|
-
xy_scale = f"{my_network.xy_scale}"
|
|
13087
|
+
if args:
|
|
13088
|
+
self.parent().last_dil = args[0]
|
|
13089
|
+
self.index = 1
|
|
11978
13090
|
else:
|
|
11979
|
-
|
|
13091
|
+
self.parent().last_dil = 1
|
|
13092
|
+
self.index = 0
|
|
11980
13093
|
|
|
11981
|
-
self.
|
|
11982
|
-
layout.addRow("
|
|
13094
|
+
self.amount = QLineEdit(f"{self.parent().last_dil}")
|
|
13095
|
+
layout.addRow("Dilation Radius:", self.amount)
|
|
11983
13096
|
|
|
11984
|
-
|
|
11985
|
-
|
|
11986
|
-
else:
|
|
11987
|
-
z_scale = "1"
|
|
13097
|
+
self.xy_scale = QLineEdit("1")
|
|
13098
|
+
layout.addRow("xy_scale:", self.xy_scale)
|
|
11988
13099
|
|
|
11989
|
-
self.z_scale = QLineEdit(
|
|
13100
|
+
self.z_scale = QLineEdit("1")
|
|
11990
13101
|
layout.addRow("z_scale:", self.z_scale)
|
|
11991
13102
|
|
|
11992
13103
|
# Add mode selection dropdown
|
|
11993
13104
|
self.mode_selector = QComboBox()
|
|
11994
|
-
self.mode_selector.addItems(["
|
|
11995
|
-
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
|
|
11996
13107
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
11997
13108
|
|
|
11998
13109
|
# Add Run button
|
|
@@ -12034,13 +13145,15 @@ class DilateDialog(QDialog):
|
|
|
12034
13145
|
if active_data is None:
|
|
12035
13146
|
raise ValueError("No active image selected")
|
|
12036
13147
|
|
|
13148
|
+
self.parent().last_dil = amount
|
|
13149
|
+
|
|
12037
13150
|
if accepted_mode == 1:
|
|
12038
13151
|
dialog = SmartDilateDialog(self.parent(), [active_data, amount, xy_scale, z_scale])
|
|
12039
13152
|
dialog.exec()
|
|
12040
13153
|
self.accept()
|
|
12041
13154
|
return
|
|
12042
13155
|
|
|
12043
|
-
if accepted_mode ==
|
|
13156
|
+
if accepted_mode == 0:
|
|
12044
13157
|
result = n3d.dilate_3D_dt(active_data, amount, xy_scaling = xy_scale, z_scaling = z_scale)
|
|
12045
13158
|
else:
|
|
12046
13159
|
|
|
@@ -12070,36 +13183,33 @@ class DilateDialog(QDialog):
|
|
|
12070
13183
|
)
|
|
12071
13184
|
|
|
12072
13185
|
class ErodeDialog(QDialog):
|
|
12073
|
-
def __init__(self, parent=None):
|
|
13186
|
+
def __init__(self, parent=None, args = None):
|
|
12074
13187
|
super().__init__(parent)
|
|
12075
13188
|
self.setWindowTitle("Erosion Parameters")
|
|
12076
13189
|
self.setModal(True)
|
|
12077
13190
|
|
|
12078
13191
|
layout = QFormLayout(self)
|
|
12079
13192
|
|
|
12080
|
-
|
|
12081
|
-
|
|
12082
|
-
|
|
12083
|
-
if my_network.xy_scale is not None:
|
|
12084
|
-
xy_scale = f"{my_network.xy_scale}"
|
|
13193
|
+
if args:
|
|
13194
|
+
self.parent().last_ero = args[0]
|
|
13195
|
+
self.index = 1
|
|
12085
13196
|
else:
|
|
12086
|
-
|
|
13197
|
+
self.parent().last_ero = 1
|
|
13198
|
+
self.index = 0
|
|
12087
13199
|
|
|
12088
|
-
self.
|
|
12089
|
-
layout.addRow("
|
|
13200
|
+
self.amount = QLineEdit(f"{self.parent().last_ero}")
|
|
13201
|
+
layout.addRow("Erosion Radius:", self.amount)
|
|
12090
13202
|
|
|
12091
|
-
|
|
12092
|
-
|
|
12093
|
-
else:
|
|
12094
|
-
z_scale = "1"
|
|
13203
|
+
self.xy_scale = QLineEdit("1")
|
|
13204
|
+
layout.addRow("xy_scale:", self.xy_scale)
|
|
12095
13205
|
|
|
12096
|
-
self.z_scale = QLineEdit(
|
|
13206
|
+
self.z_scale = QLineEdit("1")
|
|
12097
13207
|
layout.addRow("z_scale:", self.z_scale)
|
|
12098
13208
|
|
|
12099
13209
|
# Add mode selection dropdown
|
|
12100
13210
|
self.mode_selector = QComboBox()
|
|
12101
|
-
self.mode_selector.addItems(["
|
|
12102
|
-
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
|
|
12103
13213
|
layout.addRow("Execution Mode:", self.mode_selector)
|
|
12104
13214
|
|
|
12105
13215
|
# Add Run button
|
|
@@ -12135,8 +13245,7 @@ class ErodeDialog(QDialog):
|
|
|
12135
13245
|
|
|
12136
13246
|
mode = self.mode_selector.currentIndex()
|
|
12137
13247
|
|
|
12138
|
-
if mode ==
|
|
12139
|
-
mode = 1
|
|
13248
|
+
if mode == 1:
|
|
12140
13249
|
preserve_labels = True
|
|
12141
13250
|
else:
|
|
12142
13251
|
preserve_labels = False
|
|
@@ -12158,7 +13267,7 @@ class ErodeDialog(QDialog):
|
|
|
12158
13267
|
|
|
12159
13268
|
|
|
12160
13269
|
self.parent().load_channel(self.parent().active_channel, result, True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
12161
|
-
|
|
13270
|
+
self.parent().last_ero = amount
|
|
12162
13271
|
self.accept()
|
|
12163
13272
|
|
|
12164
13273
|
except Exception as e:
|
|
@@ -12188,6 +13297,11 @@ class HoleDialog(QDialog):
|
|
|
12188
13297
|
self.borders.setChecked(False)
|
|
12189
13298
|
layout.addRow("Fill Small Holes Along Borders:", self.borders)
|
|
12190
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
|
+
|
|
12191
13305
|
self.sep_holes = QPushButton("Seperate Hole Mask")
|
|
12192
13306
|
self.sep_holes.setCheckable(True)
|
|
12193
13307
|
self.sep_holes.setChecked(False)
|
|
@@ -12210,15 +13324,32 @@ class HoleDialog(QDialog):
|
|
|
12210
13324
|
borders = self.borders.isChecked()
|
|
12211
13325
|
headon = self.headon.isChecked()
|
|
12212
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:
|
|
12213
13332
|
|
|
12214
|
-
|
|
12215
|
-
|
|
12216
|
-
|
|
12217
|
-
|
|
12218
|
-
|
|
12219
|
-
|
|
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
|
+
|
|
12220
13348
|
|
|
12221
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
|
+
|
|
12222
13353
|
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
12223
13354
|
else:
|
|
12224
13355
|
self.parent().load_channel(3, active_data - result, True)
|
|
@@ -12563,7 +13694,13 @@ class SkeletonizeDialog(QDialog):
|
|
|
12563
13694
|
# auto checkbox (default True)
|
|
12564
13695
|
self.auto = QPushButton("Auto")
|
|
12565
13696
|
self.auto.setCheckable(True)
|
|
12566
|
-
|
|
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)
|
|
12567
13704
|
layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
|
|
12568
13705
|
|
|
12569
13706
|
# Add Run button
|
|
@@ -12619,6 +13756,86 @@ class SkeletonizeDialog(QDialog):
|
|
|
12619
13756
|
f"Error running skeletonize: {str(e)}"
|
|
12620
13757
|
)
|
|
12621
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
|
+
|
|
12622
13839
|
class DistanceDialog(QDialog):
|
|
12623
13840
|
def __init__(self, parent=None):
|
|
12624
13841
|
super().__init__(parent)
|
|
@@ -12666,16 +13883,58 @@ class GrayWaterDialog(QDialog):
|
|
|
12666
13883
|
run_button.clicked.connect(self.run_watershed)
|
|
12667
13884
|
layout.addRow(run_button)
|
|
12668
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
|
+
|
|
12669
13925
|
def run_watershed(self):
|
|
12670
13926
|
|
|
12671
13927
|
try:
|
|
12672
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
|
+
|
|
12673
13934
|
min_intensity = float(self.min_intensity.text()) if self.min_intensity.text().strip() else None
|
|
12674
13935
|
|
|
12675
13936
|
min_peak_distance = int(self.min_peak_distance.text()) if self.min_peak_distance.text().strip() else 1
|
|
12676
13937
|
|
|
12677
|
-
data = self.parent().channel_data[self.parent().active_channel]
|
|
12678
|
-
|
|
12679
13938
|
data = n3d.gray_watershed(data, min_peak_distance, min_intensity)
|
|
12680
13939
|
|
|
12681
13940
|
self.parent().load_channel(self.parent().active_channel, data, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|
|
@@ -12695,11 +13954,6 @@ class WatershedDialog(QDialog):
|
|
|
12695
13954
|
self.setModal(True)
|
|
12696
13955
|
|
|
12697
13956
|
layout = QFormLayout(self)
|
|
12698
|
-
|
|
12699
|
-
# Directory (empty by default)
|
|
12700
|
-
self.directory = QLineEdit()
|
|
12701
|
-
self.directory.setPlaceholderText("Leave empty for None")
|
|
12702
|
-
layout.addRow("Output Directory:", self.directory)
|
|
12703
13957
|
|
|
12704
13958
|
try:
|
|
12705
13959
|
|
|
@@ -12750,7 +14004,7 @@ class WatershedDialog(QDialog):
|
|
|
12750
14004
|
def run_watershed(self):
|
|
12751
14005
|
try:
|
|
12752
14006
|
# Get directory (None if empty)
|
|
12753
|
-
directory =
|
|
14007
|
+
directory = None
|
|
12754
14008
|
|
|
12755
14009
|
# Get proportion (0.1 if empty or invalid)
|
|
12756
14010
|
try:
|
|
@@ -13108,7 +14362,7 @@ class GenNodesDialog(QDialog):
|
|
|
13108
14362
|
|
|
13109
14363
|
if my_network.edges is None and my_network.nodes is not None:
|
|
13110
14364
|
self.parent().load_channel(1, my_network.nodes, data = True)
|
|
13111
|
-
self.parent().delete_channel(0,
|
|
14365
|
+
self.parent().delete_channel(0, False)
|
|
13112
14366
|
# Get directory (None if empty)
|
|
13113
14367
|
#directory = self.directory.text() if self.directory.text() else None
|
|
13114
14368
|
|
|
@@ -13170,7 +14424,6 @@ class GenNodesDialog(QDialog):
|
|
|
13170
14424
|
order = order,
|
|
13171
14425
|
return_skele = True,
|
|
13172
14426
|
fastdil = fastdil
|
|
13173
|
-
|
|
13174
14427
|
)
|
|
13175
14428
|
|
|
13176
14429
|
if down_factor > 0 and not self.called:
|
|
@@ -13262,7 +14515,7 @@ class BranchDialog(QDialog):
|
|
|
13262
14515
|
self.fix3.setChecked(True)
|
|
13263
14516
|
else:
|
|
13264
14517
|
self.fix3.setChecked(False)
|
|
13265
|
-
correction_layout.addWidget(QLabel("Split Nontouching Branches
|
|
14518
|
+
correction_layout.addWidget(QLabel("Split Nontouching Branches?: "), 4, 0)
|
|
13266
14519
|
correction_layout.addWidget(self.fix3, 4, 1)
|
|
13267
14520
|
|
|
13268
14521
|
correction_group.setLayout(correction_layout)
|
|
@@ -13290,20 +14543,27 @@ class BranchDialog(QDialog):
|
|
|
13290
14543
|
# --- Misc Options Group ---
|
|
13291
14544
|
misc_group = QGroupBox("Misc Options")
|
|
13292
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)
|
|
13293
14553
|
|
|
13294
14554
|
# Nodes checkbox
|
|
13295
14555
|
self.nodes = QPushButton("Generate Nodes")
|
|
13296
14556
|
self.nodes.setCheckable(True)
|
|
13297
14557
|
self.nodes.setChecked(True)
|
|
13298
|
-
misc_layout.addWidget(QLabel("Generate nodes from edges? (Skip if already completed):"),
|
|
13299
|
-
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)
|
|
13300
14560
|
|
|
13301
14561
|
# GPU checkbox
|
|
13302
14562
|
self.GPU = QPushButton("GPU")
|
|
13303
14563
|
self.GPU.setCheckable(True)
|
|
13304
14564
|
self.GPU.setChecked(False)
|
|
13305
|
-
misc_layout.addWidget(QLabel("Use GPU (May downsample large images):"),
|
|
13306
|
-
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)
|
|
13307
14567
|
|
|
13308
14568
|
misc_group.setLayout(misc_layout)
|
|
13309
14569
|
main_layout.addWidget(misc_group)
|
|
@@ -13337,10 +14597,11 @@ class BranchDialog(QDialog):
|
|
|
13337
14597
|
fix3 = self.fix3.isChecked()
|
|
13338
14598
|
fix_val = float(self.fix_val.text()) if self.fix_val.text() else None
|
|
13339
14599
|
seed = int(self.seed.text()) if self.seed.text() else None
|
|
14600
|
+
compute = self.compute.isChecked()
|
|
13340
14601
|
|
|
13341
14602
|
if my_network.edges is None and my_network.nodes is not None:
|
|
13342
14603
|
self.parent().load_channel(1, my_network.nodes, data = True)
|
|
13343
|
-
self.parent().delete_channel(0,
|
|
14604
|
+
self.parent().delete_channel(0, False)
|
|
13344
14605
|
|
|
13345
14606
|
original_shape = my_network.edges.shape
|
|
13346
14607
|
original_array = copy.deepcopy(my_network.edges)
|
|
@@ -13353,7 +14614,7 @@ class BranchDialog(QDialog):
|
|
|
13353
14614
|
|
|
13354
14615
|
if my_network.edges is not None and my_network.nodes is not None and my_network.id_overlay is not None:
|
|
13355
14616
|
|
|
13356
|
-
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)
|
|
13357
14618
|
|
|
13358
14619
|
if fix2:
|
|
13359
14620
|
|
|
@@ -13392,6 +14653,19 @@ class BranchDialog(QDialog):
|
|
|
13392
14653
|
|
|
13393
14654
|
output = self.parent().separate_nontouching_objects(output, max_val=np.max(output))
|
|
13394
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
|
+
|
|
13395
14669
|
|
|
13396
14670
|
if down_factor is not None:
|
|
13397
14671
|
|
|
@@ -13791,10 +15065,6 @@ class CentroidDialog(QDialog):
|
|
|
13791
15065
|
|
|
13792
15066
|
layout = QFormLayout(self)
|
|
13793
15067
|
|
|
13794
|
-
self.directory = QLineEdit()
|
|
13795
|
-
self.directory.setPlaceholderText("Leave empty for active directory")
|
|
13796
|
-
layout.addRow("Output Directory:", self.directory)
|
|
13797
|
-
|
|
13798
15068
|
self.downsample = QLineEdit("1")
|
|
13799
15069
|
layout.addRow("Downsample Factor:", self.downsample)
|
|
13800
15070
|
|
|
@@ -13824,7 +15094,7 @@ class CentroidDialog(QDialog):
|
|
|
13824
15094
|
ignore_empty = self.ignore_empty.isChecked()
|
|
13825
15095
|
|
|
13826
15096
|
# Get directory (None if empty)
|
|
13827
|
-
directory =
|
|
15097
|
+
directory = None
|
|
13828
15098
|
|
|
13829
15099
|
# Get downsample
|
|
13830
15100
|
try:
|
|
@@ -13905,7 +15175,6 @@ class CentroidDialog(QDialog):
|
|
|
13905
15175
|
|
|
13906
15176
|
class CalcAllDialog(QDialog):
|
|
13907
15177
|
# Class variables to store previous settings
|
|
13908
|
-
prev_directory = ""
|
|
13909
15178
|
prev_search = ""
|
|
13910
15179
|
prev_diledge = ""
|
|
13911
15180
|
prev_down_factor = ""
|
|
@@ -13977,7 +15246,7 @@ class CalcAllDialog(QDialog):
|
|
|
13977
15246
|
|
|
13978
15247
|
self.down_factor = QLineEdit(self.prev_down_factor)
|
|
13979
15248
|
self.down_factor.setPlaceholderText("Leave empty for None")
|
|
13980
|
-
speedup_layout.addRow("Downsample for Centroids (int):", self.down_factor)
|
|
15249
|
+
speedup_layout.addRow("Downsample for Centroids/Overlays (int):", self.down_factor)
|
|
13981
15250
|
|
|
13982
15251
|
self.GPU_downsample = QLineEdit(self.prev_GPU_downsample)
|
|
13983
15252
|
self.GPU_downsample.setPlaceholderText("Leave empty for None")
|
|
@@ -13999,10 +15268,6 @@ class CalcAllDialog(QDialog):
|
|
|
13999
15268
|
output_group = QGroupBox("Output Options")
|
|
14000
15269
|
output_layout = QFormLayout(output_group)
|
|
14001
15270
|
|
|
14002
|
-
self.directory = QLineEdit(self.prev_directory)
|
|
14003
|
-
self.directory.setPlaceholderText("Will Have to Save Manually If Empty")
|
|
14004
|
-
output_layout.addRow("Output Directory:", self.directory)
|
|
14005
|
-
|
|
14006
15271
|
self.overlays = QPushButton("Overlays")
|
|
14007
15272
|
self.overlays.setCheckable(True)
|
|
14008
15273
|
self.overlays.setChecked(self.prev_overlays)
|
|
@@ -14024,7 +15289,7 @@ class CalcAllDialog(QDialog):
|
|
|
14024
15289
|
|
|
14025
15290
|
try:
|
|
14026
15291
|
# Get directory (None if empty)
|
|
14027
|
-
directory =
|
|
15292
|
+
directory = None
|
|
14028
15293
|
|
|
14029
15294
|
# Get xy_scale and z_scale (1 if empty or invalid)
|
|
14030
15295
|
try:
|
|
@@ -14101,7 +15366,6 @@ class CalcAllDialog(QDialog):
|
|
|
14101
15366
|
)
|
|
14102
15367
|
|
|
14103
15368
|
# Store current values as previous values
|
|
14104
|
-
CalcAllDialog.prev_directory = self.directory.text()
|
|
14105
15369
|
CalcAllDialog.prev_search = self.search.text()
|
|
14106
15370
|
CalcAllDialog.prev_diledge = self.diledge.text()
|
|
14107
15371
|
CalcAllDialog.prev_down_factor = self.down_factor.text()
|
|
@@ -14135,8 +15399,12 @@ class CalcAllDialog(QDialog):
|
|
|
14135
15399
|
directory = 'my_network'
|
|
14136
15400
|
|
|
14137
15401
|
# Generate and update overlays
|
|
14138
|
-
my_network.network_overlay = my_network.draw_network(directory=directory)
|
|
14139
|
-
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)
|
|
14140
15408
|
|
|
14141
15409
|
# Update channel data
|
|
14142
15410
|
self.parent().load_channel(2, my_network.network_overlay, True)
|
|
@@ -14249,14 +15517,13 @@ class ProxDialog(QDialog):
|
|
|
14249
15517
|
output_group = QGroupBox("Output Options")
|
|
14250
15518
|
output_layout = QFormLayout(output_group)
|
|
14251
15519
|
|
|
14252
|
-
self.directory = QLineEdit('')
|
|
14253
|
-
self.directory.setPlaceholderText("Leave empty for 'my_network'")
|
|
14254
|
-
output_layout.addRow("Output Directory:", self.directory)
|
|
14255
|
-
|
|
14256
15520
|
self.overlays = QPushButton("Overlays")
|
|
14257
15521
|
self.overlays.setCheckable(True)
|
|
14258
15522
|
self.overlays.setChecked(True)
|
|
14259
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)
|
|
14260
15527
|
|
|
14261
15528
|
self.populate = QPushButton("Populate Nodes from Centroids?")
|
|
14262
15529
|
self.populate.setCheckable(True)
|
|
@@ -14301,10 +15568,8 @@ class ProxDialog(QDialog):
|
|
|
14301
15568
|
else:
|
|
14302
15569
|
targets = None
|
|
14303
15570
|
|
|
14304
|
-
|
|
14305
|
-
|
|
14306
|
-
except:
|
|
14307
|
-
directory = None
|
|
15571
|
+
directory = None
|
|
15572
|
+
|
|
14308
15573
|
|
|
14309
15574
|
# Get xy_scale and z_scale (1 if empty or invalid)
|
|
14310
15575
|
try:
|
|
@@ -14328,6 +15593,12 @@ class ProxDialog(QDialog):
|
|
|
14328
15593
|
except:
|
|
14329
15594
|
max_neighbors = None
|
|
14330
15595
|
|
|
15596
|
+
|
|
15597
|
+
try:
|
|
15598
|
+
downsample = int(self.downsample.text()) if self.downsample.text() else None
|
|
15599
|
+
except:
|
|
15600
|
+
downsample = None
|
|
15601
|
+
|
|
14331
15602
|
overlays = self.overlays.isChecked()
|
|
14332
15603
|
fastdil = self.fastdil.isChecked()
|
|
14333
15604
|
|
|
@@ -14383,8 +15654,12 @@ class ProxDialog(QDialog):
|
|
|
14383
15654
|
directory = 'my_network'
|
|
14384
15655
|
|
|
14385
15656
|
# Generate and update overlays
|
|
14386
|
-
my_network.network_overlay = my_network.draw_network(directory=directory)
|
|
14387
|
-
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)
|
|
14388
15663
|
|
|
14389
15664
|
# Update channel data
|
|
14390
15665
|
self.parent().load_channel(2, channel_data = my_network.network_overlay, data = True)
|
|
@@ -14506,31 +15781,81 @@ class HistogramSelector(QWidget):
|
|
|
14506
15781
|
""")
|
|
14507
15782
|
layout.addWidget(button)
|
|
14508
15783
|
|
|
15784
|
+
|
|
14509
15785
|
def shortest_path_histogram(self):
|
|
14510
15786
|
try:
|
|
14511
|
-
|
|
14512
|
-
|
|
14513
|
-
|
|
14514
|
-
|
|
14515
|
-
|
|
14516
|
-
|
|
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)
|
|
14517
15824
|
path_lengths[pl] += cnts
|
|
14518
|
-
|
|
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)
|
|
14519
15840
|
freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
|
|
14520
|
-
|
|
14521
15841
|
fig, ax = plt.subplots(figsize=(15, 8))
|
|
14522
|
-
ax.bar(np.arange(1,
|
|
15842
|
+
ax.bar(np.arange(1, max_diameter + 1), height=freq_percent)
|
|
14523
15843
|
ax.set_title(
|
|
14524
|
-
"Distribution of shortest path length in G
|
|
15844
|
+
f"Distribution of shortest path length in G{title_suffix}",
|
|
15845
|
+
fontdict={"size": 35}, loc="center"
|
|
14525
15846
|
)
|
|
14526
15847
|
ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
|
|
14527
15848
|
ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
|
|
14528
15849
|
plt.show()
|
|
14529
15850
|
|
|
14530
15851
|
freq_dict = {freq: length for length, freq in enumerate(freq_percent, start=1)}
|
|
14531
|
-
self.network_analysis.format_for_upperright_table(
|
|
14532
|
-
|
|
14533
|
-
|
|
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
|
+
|
|
14534
15859
|
except Exception as e:
|
|
14535
15860
|
print(f"Error generating shortest path histogram: {e}")
|
|
14536
15861
|
|
|
@@ -14549,20 +15874,62 @@ class HistogramSelector(QWidget):
|
|
|
14549
15874
|
title="Degree Centrality Table")
|
|
14550
15875
|
except Exception as e:
|
|
14551
15876
|
print(f"Error generating degree centrality histogram: {e}")
|
|
14552
|
-
|
|
15877
|
+
|
|
14553
15878
|
def betweenness_centrality_histogram(self):
|
|
14554
15879
|
try:
|
|
14555
|
-
|
|
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)
|
|
14556
15915
|
plt.figure(figsize=(15, 8))
|
|
14557
15916
|
plt.hist(betweenness_centrality.values(), bins=100)
|
|
14558
15917
|
plt.xticks(ticks=[0, 0.02, 0.1, 0.2, 0.3, 0.4, 0.5])
|
|
14559
|
-
plt.title(
|
|
15918
|
+
plt.title(
|
|
15919
|
+
f"Betweenness Centrality Histogram{title_suffix}",
|
|
15920
|
+
fontdict={"size": 35}, loc="center"
|
|
15921
|
+
)
|
|
14560
15922
|
plt.xlabel("Betweenness Centrality", fontdict={"size": 20})
|
|
14561
15923
|
plt.ylabel("Counts", fontdict={"size": 20})
|
|
14562
15924
|
plt.show()
|
|
14563
|
-
|
|
14564
|
-
|
|
14565
|
-
|
|
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
|
+
|
|
14566
15933
|
except Exception as e:
|
|
14567
15934
|
print(f"Error generating betweenness centrality histogram: {e}")
|
|
14568
15935
|
|
|
@@ -14615,7 +15982,27 @@ class HistogramSelector(QWidget):
|
|
|
14615
15982
|
def bridges_analysis(self):
|
|
14616
15983
|
try:
|
|
14617
15984
|
bridges = list(nx.bridges(self.G))
|
|
14618
|
-
|
|
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',
|
|
14619
16006
|
title="Bridges")
|
|
14620
16007
|
except Exception as e:
|
|
14621
16008
|
print(f"Error generating bridges analysis: {e}")
|
|
@@ -14642,7 +16029,7 @@ class HistogramSelector(QWidget):
|
|
|
14642
16029
|
def node_connectivity_histogram(self):
|
|
14643
16030
|
"""Local node connectivity - minimum number of nodes that must be removed to disconnect neighbors"""
|
|
14644
16031
|
try:
|
|
14645
|
-
if self.G.number_of_nodes() > 500:
|
|
16032
|
+
if self.G.number_of_nodes() > 500:
|
|
14646
16033
|
print("Note this analysis may be slow for large network (>500 nodes)")
|
|
14647
16034
|
#return
|
|
14648
16035
|
|
|
@@ -14720,7 +16107,7 @@ class HistogramSelector(QWidget):
|
|
|
14720
16107
|
def load_centrality_histogram(self):
|
|
14721
16108
|
"""Load centrality - fraction of shortest paths passing through each node"""
|
|
14722
16109
|
try:
|
|
14723
|
-
if self.G.number_of_nodes() > 1000:
|
|
16110
|
+
if self.G.number_of_nodes() > 1000:
|
|
14724
16111
|
print("Note this analysis may be slow for large network (>1000 nodes)")
|
|
14725
16112
|
#return
|
|
14726
16113
|
|
|
@@ -14739,21 +16126,67 @@ class HistogramSelector(QWidget):
|
|
|
14739
16126
|
def communicability_centrality_histogram(self):
|
|
14740
16127
|
"""Communicability centrality - based on communicability between nodes"""
|
|
14741
16128
|
try:
|
|
14742
|
-
if self.G.number_of_nodes() > 500:
|
|
16129
|
+
if self.G.number_of_nodes() > 500:
|
|
14743
16130
|
print("Note this analysis may be slow for large network (>500 nodes)")
|
|
14744
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.")
|
|
16138
|
+
|
|
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)"
|
|
14745
16166
|
|
|
14746
|
-
|
|
14747
|
-
|
|
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)
|
|
14748
16173
|
plt.figure(figsize=(15, 8))
|
|
14749
16174
|
plt.hist(comm_centrality.values(), bins=50, alpha=0.7)
|
|
14750
|
-
plt.title(
|
|
16175
|
+
plt.title(
|
|
16176
|
+
f"Communicability Betweenness Centrality Distribution{title_suffix}",
|
|
16177
|
+
fontdict={"size": 35}, loc="center"
|
|
16178
|
+
)
|
|
14751
16179
|
plt.xlabel("Communicability Betweenness Centrality", fontdict={"size": 20})
|
|
14752
16180
|
plt.ylabel("Frequency", fontdict={"size": 20})
|
|
14753
16181
|
plt.show()
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
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
|
+
|
|
14757
16190
|
except Exception as e:
|
|
14758
16191
|
print(f"Error generating communicability betweenness centrality histogram: {e}")
|
|
14759
16192
|
|
|
@@ -14776,20 +16209,67 @@ class HistogramSelector(QWidget):
|
|
|
14776
16209
|
def current_flow_betweenness_histogram(self):
|
|
14777
16210
|
"""Current flow betweenness - models network as electrical circuit"""
|
|
14778
16211
|
try:
|
|
14779
|
-
if self.G.number_of_nodes() > 500:
|
|
16212
|
+
if self.G.number_of_nodes() > 500:
|
|
14780
16213
|
print("Note this analysis may be slow for large network (>500 nodes)")
|
|
14781
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 = {}
|
|
14782
16224
|
|
|
14783
|
-
|
|
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)
|
|
14784
16256
|
plt.figure(figsize=(15, 8))
|
|
14785
16257
|
plt.hist(current_flow.values(), bins=50, alpha=0.7)
|
|
14786
|
-
plt.title(
|
|
16258
|
+
plt.title(
|
|
16259
|
+
f"Current Flow Betweenness Centrality Distribution{title_suffix}",
|
|
16260
|
+
fontdict={"size": 35}, loc="center"
|
|
16261
|
+
)
|
|
14787
16262
|
plt.xlabel("Current Flow Betweenness Centrality", fontdict={"size": 20})
|
|
14788
16263
|
plt.ylabel("Frequency", fontdict={"size": 20})
|
|
14789
16264
|
plt.show()
|
|
14790
|
-
|
|
14791
|
-
|
|
14792
|
-
|
|
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
|
+
|
|
14793
16273
|
except Exception as e:
|
|
14794
16274
|
print(f"Error generating current flow betweenness histogram: {e}")
|
|
14795
16275
|
|