nettracer3d 1.0.0__py3-none-any.whl → 1.0.2__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/neighborhoods.py +193 -66
- nettracer3d/nettracer.py +71 -3
- nettracer3d/nettracer_gui.py +666 -135
- {nettracer3d-1.0.0.dist-info → nettracer3d-1.0.2.dist-info}/METADATA +4 -4
- {nettracer3d-1.0.0.dist-info → nettracer3d-1.0.2.dist-info}/RECORD +10 -10
- {nettracer3d-1.0.0.dist-info → nettracer3d-1.0.2.dist-info}/WHEEL +0 -0
- {nettracer3d-1.0.0.dist-info → nettracer3d-1.0.2.dist-info}/entry_points.txt +0 -0
- {nettracer3d-1.0.0.dist-info → nettracer3d-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-1.0.0.dist-info → nettracer3d-1.0.2.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -511,12 +511,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
511
511
|
data = df.iloc[:, 0].tolist() # First column as list
|
|
512
512
|
value = None
|
|
513
513
|
|
|
514
|
-
self.format_for_upperright_table(
|
|
515
|
-
|
|
516
|
-
metric=metric,
|
|
517
|
-
value=value,
|
|
518
|
-
title=title
|
|
519
|
-
)
|
|
514
|
+
df = self.format_for_upperright_table(data=data, metric=metric, value=value, title=title)
|
|
515
|
+
return df
|
|
520
516
|
else:
|
|
521
517
|
# Multiple columns: create dictionary as before
|
|
522
518
|
# First column header (for metric parameter)
|
|
@@ -542,12 +538,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
542
538
|
value = value[0]
|
|
543
539
|
|
|
544
540
|
# Call the parent method
|
|
545
|
-
self.format_for_upperright_table(
|
|
546
|
-
|
|
547
|
-
metric=metric,
|
|
548
|
-
value=value,
|
|
549
|
-
title=title
|
|
550
|
-
)
|
|
541
|
+
df = self.format_for_upperright_table(data=data_dict, metric=metric, value=value, title=title)
|
|
542
|
+
return df
|
|
551
543
|
|
|
552
544
|
QMessageBox.information(
|
|
553
545
|
self,
|
|
@@ -1320,32 +1312,40 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1320
1312
|
# Create measurement submenu
|
|
1321
1313
|
measure_menu = context_menu.addMenu("Measurements")
|
|
1322
1314
|
|
|
1323
|
-
# Distance measurement options
|
|
1324
1315
|
distance_menu = measure_menu.addMenu("Distance")
|
|
1325
1316
|
if self.current_point is None:
|
|
1326
1317
|
show_point_menu = distance_menu.addAction("Place First Point")
|
|
1327
1318
|
show_point_menu.triggered.connect(
|
|
1328
1319
|
lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
|
|
1329
|
-
|
|
1320
|
+
elif (self.current_point is not None and
|
|
1321
|
+
hasattr(self, 'measurement_mode') and
|
|
1322
|
+
self.measurement_mode == "distance"):
|
|
1330
1323
|
show_point_menu = distance_menu.addAction("Place Second Point")
|
|
1331
1324
|
show_point_menu.triggered.connect(
|
|
1332
1325
|
lambda: self.place_distance_point(x_idx, y_idx, self.current_slice))
|
|
1333
|
-
|
|
1326
|
+
|
|
1334
1327
|
# Angle measurement options
|
|
1335
1328
|
angle_menu = measure_menu.addMenu("Angle")
|
|
1336
1329
|
if self.current_point is None:
|
|
1337
1330
|
angle_first = angle_menu.addAction("Place First Point (A)")
|
|
1338
1331
|
angle_first.triggered.connect(
|
|
1339
1332
|
lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
|
|
1340
|
-
elif self.
|
|
1333
|
+
elif (self.current_point is not None and
|
|
1334
|
+
self.current_second_point is None and
|
|
1335
|
+
hasattr(self, 'measurement_mode') and
|
|
1336
|
+
self.measurement_mode == "angle"):
|
|
1341
1337
|
angle_second = angle_menu.addAction("Place Second Point (B - Vertex)")
|
|
1342
1338
|
angle_second.triggered.connect(
|
|
1343
1339
|
lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
|
|
1344
|
-
|
|
1340
|
+
elif (self.current_point is not None and
|
|
1341
|
+
self.current_second_point is not None and
|
|
1342
|
+
hasattr(self, 'measurement_mode') and
|
|
1343
|
+
self.measurement_mode == "angle"):
|
|
1345
1344
|
angle_third = angle_menu.addAction("Place Third Point (C)")
|
|
1346
1345
|
angle_third.triggered.connect(
|
|
1347
1346
|
lambda: self.place_angle_point(x_idx, y_idx, self.current_slice))
|
|
1348
1347
|
|
|
1348
|
+
|
|
1349
1349
|
show_remove_menu = measure_menu.addAction("Remove All Measurements")
|
|
1350
1350
|
show_remove_menu.triggered.connect(self.handle_remove_all_measurements)
|
|
1351
1351
|
|
|
@@ -1373,15 +1373,22 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1373
1373
|
except IndexError:
|
|
1374
1374
|
pass
|
|
1375
1375
|
|
|
1376
|
-
|
|
1377
1376
|
def place_distance_point(self, x, y, z):
|
|
1378
1377
|
"""Place a measurement point for distance measurement."""
|
|
1379
1378
|
if self.current_point is None:
|
|
1380
1379
|
# This is the first point
|
|
1381
1380
|
self.current_point = (x, y, z)
|
|
1382
|
-
|
|
1383
|
-
|
|
1381
|
+
|
|
1382
|
+
# Create and store the artists
|
|
1383
|
+
pt = self.ax.plot(x, y, 'yo', markersize=8)[0]
|
|
1384
|
+
txt = self.ax.text(x, y+5, f"D{self.current_pair_index}",
|
|
1384
1385
|
color='yellow', ha='center', va='bottom')
|
|
1386
|
+
|
|
1387
|
+
# Add to measurement_artists so they can be managed by update_display
|
|
1388
|
+
if not hasattr(self, 'measurement_artists'):
|
|
1389
|
+
self.measurement_artists = []
|
|
1390
|
+
self.measurement_artists.extend([pt, txt])
|
|
1391
|
+
|
|
1385
1392
|
self.canvas.draw()
|
|
1386
1393
|
self.measurement_mode = "distance"
|
|
1387
1394
|
else:
|
|
@@ -1395,21 +1402,28 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1395
1402
|
((z2-z1)*my_network.z_scale)**2)
|
|
1396
1403
|
distance2 = np.sqrt(((x2-x1))**2 + ((y2-y1))**2 + ((z2-z1))**2)
|
|
1397
1404
|
|
|
1398
|
-
# Store the point pair
|
|
1405
|
+
# Store the point pair with type indicator
|
|
1399
1406
|
self.measurement_points.append({
|
|
1400
1407
|
'pair_index': self.current_pair_index,
|
|
1401
1408
|
'point1': self.current_point,
|
|
1402
1409
|
'point2': (x2, y2, z2),
|
|
1403
1410
|
'distance': distance,
|
|
1404
|
-
'distance2': distance2
|
|
1411
|
+
'distance2': distance2,
|
|
1412
|
+
'type': 'distance' # Added type tracking
|
|
1405
1413
|
})
|
|
1406
1414
|
|
|
1407
|
-
# Draw second point and line
|
|
1408
|
-
self.ax.plot(x2, y2, 'yo', markersize=8)
|
|
1409
|
-
self.ax.text(x2, y2+5, f"D{self.current_pair_index}",
|
|
1415
|
+
# Draw second point and line, storing the artists
|
|
1416
|
+
pt2 = self.ax.plot(x2, y2, 'yo', markersize=8)[0]
|
|
1417
|
+
txt2 = self.ax.text(x2, y2+5, f"D{self.current_pair_index}",
|
|
1410
1418
|
color='yellow', ha='center', va='bottom')
|
|
1419
|
+
|
|
1420
|
+
# Add to measurement_artists
|
|
1421
|
+
self.measurement_artists.extend([pt2, txt2])
|
|
1422
|
+
|
|
1411
1423
|
if z1 == z2: # Only draw line if points are on same slice
|
|
1412
|
-
self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
|
|
1424
|
+
line = self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)[0]
|
|
1425
|
+
self.measurement_artists.append(line)
|
|
1426
|
+
|
|
1413
1427
|
self.canvas.draw()
|
|
1414
1428
|
|
|
1415
1429
|
# Update measurement display
|
|
@@ -1422,12 +1436,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1422
1436
|
|
|
1423
1437
|
def place_angle_point(self, x, y, z):
|
|
1424
1438
|
"""Place a measurement point for angle measurement."""
|
|
1439
|
+
if not hasattr(self, 'measurement_artists'):
|
|
1440
|
+
self.measurement_artists = []
|
|
1441
|
+
|
|
1425
1442
|
if self.current_point is None:
|
|
1426
1443
|
# First point (A)
|
|
1427
1444
|
self.current_point = (x, y, z)
|
|
1428
|
-
|
|
1429
|
-
|
|
1445
|
+
|
|
1446
|
+
# Create and store artists
|
|
1447
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
1448
|
+
txt = self.ax.text(x, y+5, f"A{self.current_trio_index}",
|
|
1430
1449
|
color='green', ha='center', va='bottom')
|
|
1450
|
+
self.measurement_artists.extend([pt, txt])
|
|
1451
|
+
|
|
1431
1452
|
self.canvas.draw()
|
|
1432
1453
|
self.measurement_mode = "angle"
|
|
1433
1454
|
|
|
@@ -1436,13 +1457,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1436
1457
|
self.current_second_point = (x, y, z)
|
|
1437
1458
|
x1, y1, z1 = self.current_point
|
|
1438
1459
|
|
|
1439
|
-
|
|
1440
|
-
self.ax.
|
|
1460
|
+
# Create and store artists
|
|
1461
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
1462
|
+
txt = self.ax.text(x, y+5, f"B{self.current_trio_index}",
|
|
1441
1463
|
color='green', ha='center', va='bottom')
|
|
1464
|
+
self.measurement_artists.extend([pt, txt])
|
|
1442
1465
|
|
|
1443
1466
|
# Draw line from A to B
|
|
1444
1467
|
if z1 == z:
|
|
1445
|
-
self.ax.plot([x1, x], [y1, y], 'g--', alpha=0.7)
|
|
1468
|
+
line = self.ax.plot([x1, x], [y1, y], 'g--', alpha=0.7)[0]
|
|
1469
|
+
self.measurement_artists.append(line)
|
|
1446
1470
|
self.canvas.draw()
|
|
1447
1471
|
|
|
1448
1472
|
else:
|
|
@@ -1465,7 +1489,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1465
1489
|
**angle_data
|
|
1466
1490
|
})
|
|
1467
1491
|
|
|
1468
|
-
# Also add the two distances as separate pairs
|
|
1492
|
+
# Also add the two distances as separate pairs with type indicator
|
|
1469
1493
|
dist_ab = np.sqrt(((x2-x1)*my_network.xy_scale)**2 +
|
|
1470
1494
|
((y2-y1)*my_network.xy_scale)**2 +
|
|
1471
1495
|
((z2-z1)*my_network.z_scale)**2)
|
|
@@ -1482,24 +1506,28 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1482
1506
|
'point1': (x1, y1, z1),
|
|
1483
1507
|
'point2': (x2, y2, z2),
|
|
1484
1508
|
'distance': dist_ab,
|
|
1485
|
-
'distance2': dist_ab_voxel
|
|
1509
|
+
'distance2': dist_ab_voxel,
|
|
1510
|
+
'type': 'angle' # Added type tracking
|
|
1486
1511
|
},
|
|
1487
1512
|
{
|
|
1488
1513
|
'pair_index': f"B{self.current_trio_index}-C{self.current_trio_index}",
|
|
1489
1514
|
'point1': (x2, y2, z2),
|
|
1490
1515
|
'point2': (x3, y3, z3),
|
|
1491
1516
|
'distance': dist_bc,
|
|
1492
|
-
'distance2': dist_bc_voxel
|
|
1517
|
+
'distance2': dist_bc_voxel,
|
|
1518
|
+
'type': 'angle' # Added type tracking
|
|
1493
1519
|
}
|
|
1494
1520
|
])
|
|
1495
1521
|
|
|
1496
|
-
# Draw third point and line
|
|
1497
|
-
self.ax.plot(x3, y3, 'go', markersize=8)
|
|
1498
|
-
self.ax.text(x3, y3+5, f"C{self.current_trio_index}",
|
|
1522
|
+
# Draw third point and line, storing artists
|
|
1523
|
+
pt3 = self.ax.plot(x3, y3, 'go', markersize=8)[0]
|
|
1524
|
+
txt3 = self.ax.text(x3, y3+5, f"C{self.current_trio_index}",
|
|
1499
1525
|
color='green', ha='center', va='bottom')
|
|
1526
|
+
self.measurement_artists.extend([pt3, txt3])
|
|
1500
1527
|
|
|
1501
1528
|
if z2 == z3: # Draw line from B to C if on same slice
|
|
1502
|
-
self.ax.plot([x2, x3], [y2, y3], 'g--', alpha=0.7)
|
|
1529
|
+
line = self.ax.plot([x2, x3], [y2, y3], 'g--', alpha=0.7)[0]
|
|
1530
|
+
self.measurement_artists.append(line)
|
|
1503
1531
|
self.canvas.draw()
|
|
1504
1532
|
|
|
1505
1533
|
# Update measurement display
|
|
@@ -1511,6 +1539,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1511
1539
|
self.current_trio_index += 1
|
|
1512
1540
|
self.measurement_mode = "angle"
|
|
1513
1541
|
|
|
1542
|
+
|
|
1514
1543
|
def calculate_3d_angle(self, point_a, point_b, point_c):
|
|
1515
1544
|
"""Calculate 3D angle at vertex B between points A-B-C."""
|
|
1516
1545
|
x1, y1, z1 = point_a
|
|
@@ -1825,23 +1854,27 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1825
1854
|
|
|
1826
1855
|
nodes = list(set(nodes))
|
|
1827
1856
|
|
|
1828
|
-
|
|
1829
|
-
original_df = self.network_table.model()._data
|
|
1857
|
+
try:
|
|
1830
1858
|
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1859
|
+
# Get the existing DataFrame from the model
|
|
1860
|
+
original_df = self.network_table.model()._data
|
|
1861
|
+
|
|
1862
|
+
# Create mask for rows for nodes in question
|
|
1863
|
+
mask = (
|
|
1864
|
+
(original_df.iloc[:, 0].isin(nodes) & original_df.iloc[:, 1].isin(nodes))
|
|
1865
|
+
)
|
|
1866
|
+
|
|
1867
|
+
# Filter the DataFrame to only include direct connections
|
|
1868
|
+
filtered_df = original_df[mask].copy()
|
|
1869
|
+
|
|
1870
|
+
# Create new model with filtered DataFrame and update selection table
|
|
1871
|
+
new_model = PandasModel(filtered_df)
|
|
1872
|
+
self.selection_table.setModel(new_model)
|
|
1873
|
+
|
|
1874
|
+
# Switch to selection table
|
|
1875
|
+
self.selection_button.click()
|
|
1876
|
+
except:
|
|
1877
|
+
pass
|
|
1845
1878
|
|
|
1846
1879
|
if edges:
|
|
1847
1880
|
edge_indices = filtered_df.iloc[:, 2].unique().tolist()
|
|
@@ -3778,6 +3811,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3778
3811
|
self.ax.clear()
|
|
3779
3812
|
self.ax.set_facecolor('black')
|
|
3780
3813
|
|
|
3814
|
+
# Reset measurement artists since we cleared the axes
|
|
3815
|
+
if not hasattr(self, 'measurement_artists'):
|
|
3816
|
+
self.measurement_artists = []
|
|
3817
|
+
else:
|
|
3818
|
+
self.measurement_artists = [] # Reset since ax.clear() removed all artists
|
|
3819
|
+
|
|
3781
3820
|
# Get original dimensions (before downsampling)
|
|
3782
3821
|
if hasattr(self, 'original_dims') and self.original_dims:
|
|
3783
3822
|
height, width = self.original_dims
|
|
@@ -3859,23 +3898,129 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3859
3898
|
for spine in self.ax.spines.values():
|
|
3860
3899
|
spine.set_color('black')
|
|
3861
3900
|
|
|
3862
|
-
# Add measurement points if they exist (
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3901
|
+
# Add measurement points if they exist (using the same logic as main update_display)
|
|
3902
|
+
if hasattr(self, 'measurement_points') and self.measurement_points:
|
|
3903
|
+
for point in self.measurement_points:
|
|
3904
|
+
x1, y1, z1 = point['point1']
|
|
3905
|
+
x2, y2, z2 = point['point2']
|
|
3906
|
+
pair_idx = point['pair_index']
|
|
3907
|
+
point_type = point.get('type', 'distance') # Default to distance for backward compatibility
|
|
3908
|
+
|
|
3909
|
+
# Determine colors based on type
|
|
3910
|
+
if point_type == 'angle':
|
|
3911
|
+
marker_color = 'go'
|
|
3912
|
+
text_color = 'green'
|
|
3913
|
+
line_color = 'g--'
|
|
3914
|
+
else: # distance
|
|
3915
|
+
marker_color = 'yo'
|
|
3916
|
+
text_color = 'yellow'
|
|
3917
|
+
line_color = 'r--'
|
|
3918
|
+
|
|
3919
|
+
# Check if points are in visible region and on current slice
|
|
3920
|
+
point1_visible = (z1 == self.current_slice and
|
|
3921
|
+
current_xlim[0] <= x1 <= current_xlim[1] and
|
|
3922
|
+
current_ylim[1] <= y1 <= current_ylim[0])
|
|
3923
|
+
point2_visible = (z2 == self.current_slice and
|
|
3924
|
+
current_xlim[0] <= x2 <= current_xlim[1] and
|
|
3925
|
+
current_ylim[1] <= y2 <= current_ylim[0])
|
|
3926
|
+
|
|
3927
|
+
# Draw individual points if they're on the current slice
|
|
3928
|
+
if point1_visible:
|
|
3929
|
+
pt1 = self.ax.plot(x1, y1, marker_color, markersize=8)[0]
|
|
3930
|
+
txt1 = self.ax.text(x1, y1+5, str(pair_idx), color=text_color, ha='center', va='bottom')
|
|
3931
|
+
self.measurement_artists.extend([pt1, txt1])
|
|
3932
|
+
|
|
3933
|
+
if point2_visible:
|
|
3934
|
+
pt2 = self.ax.plot(x2, y2, marker_color, markersize=8)[0]
|
|
3935
|
+
txt2 = self.ax.text(x2, y2+5, str(pair_idx), color=text_color, ha='center', va='bottom')
|
|
3936
|
+
self.measurement_artists.extend([pt2, txt2])
|
|
3937
|
+
|
|
3938
|
+
# Draw connecting line if both points are on the same slice
|
|
3939
|
+
if z1 == z2 == self.current_slice and (point1_visible or point2_visible):
|
|
3940
|
+
line = self.ax.plot([x1, x2], [y1, y2], line_color, alpha=0.5)[0]
|
|
3941
|
+
self.measurement_artists.append(line)
|
|
3942
|
+
|
|
3943
|
+
# Handle angle measurements if they exist
|
|
3944
|
+
if hasattr(self, 'angle_measurements') and self.angle_measurements:
|
|
3945
|
+
for angle in self.angle_measurements:
|
|
3946
|
+
xa, ya, za = angle['point_a']
|
|
3947
|
+
xb, yb, zb = angle['point_b'] # vertex
|
|
3948
|
+
xc, yc, zc = angle['point_c']
|
|
3949
|
+
trio_idx = angle['trio_index']
|
|
3950
|
+
|
|
3951
|
+
# Check if points are on current slice and visible
|
|
3952
|
+
point_a_visible = (za == self.current_slice and
|
|
3953
|
+
current_xlim[0] <= xa <= current_xlim[1] and
|
|
3954
|
+
current_ylim[1] <= ya <= current_ylim[0])
|
|
3955
|
+
point_b_visible = (zb == self.current_slice and
|
|
3956
|
+
current_xlim[0] <= xb <= current_xlim[1] and
|
|
3957
|
+
current_ylim[1] <= yb <= current_ylim[0])
|
|
3958
|
+
point_c_visible = (zc == self.current_slice and
|
|
3959
|
+
current_xlim[0] <= xc <= current_xlim[1] and
|
|
3960
|
+
current_ylim[1] <= yc <= current_ylim[0])
|
|
3961
|
+
|
|
3962
|
+
# Draw points
|
|
3963
|
+
if point_a_visible:
|
|
3964
|
+
pt_a = self.ax.plot(xa, ya, 'go', markersize=8)[0]
|
|
3965
|
+
txt_a = self.ax.text(xa, ya+5, f"A{trio_idx}", color='green', ha='center', va='bottom')
|
|
3966
|
+
self.measurement_artists.extend([pt_a, txt_a])
|
|
3967
|
+
|
|
3968
|
+
if point_b_visible:
|
|
3969
|
+
pt_b = self.ax.plot(xb, yb, 'go', markersize=8)[0]
|
|
3970
|
+
txt_b = self.ax.text(xb, yb+5, f"B{trio_idx}", color='green', ha='center', va='bottom')
|
|
3971
|
+
self.measurement_artists.extend([pt_b, txt_b])
|
|
3876
3972
|
|
|
3877
|
-
|
|
3878
|
-
|
|
3973
|
+
if point_c_visible:
|
|
3974
|
+
pt_c = self.ax.plot(xc, yc, 'go', markersize=8)[0]
|
|
3975
|
+
txt_c = self.ax.text(xc, yc+5, f"C{trio_idx}", color='green', ha='center', va='bottom')
|
|
3976
|
+
self.measurement_artists.extend([pt_c, txt_c])
|
|
3977
|
+
|
|
3978
|
+
# Draw lines only if points are on current slice
|
|
3979
|
+
if za == zb == self.current_slice and (point_a_visible or point_b_visible):
|
|
3980
|
+
line_ab = self.ax.plot([xa, xb], [ya, yb], 'g--', alpha=0.7)[0]
|
|
3981
|
+
self.measurement_artists.append(line_ab)
|
|
3982
|
+
|
|
3983
|
+
if zb == zc == self.current_slice and (point_b_visible or point_c_visible):
|
|
3984
|
+
line_bc = self.ax.plot([xb, xc], [yb, yc], 'g--', alpha=0.7)[0]
|
|
3985
|
+
self.measurement_artists.append(line_bc)
|
|
3986
|
+
|
|
3987
|
+
# Handle any partial measurements in progress (individual points without pairs yet)
|
|
3988
|
+
if hasattr(self, 'current_point') and self.current_point is not None:
|
|
3989
|
+
x, y, z = self.current_point
|
|
3990
|
+
if z == self.current_slice:
|
|
3991
|
+
if hasattr(self, 'measurement_mode') and self.measurement_mode == "angle":
|
|
3992
|
+
# Show green for angle mode
|
|
3993
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
3994
|
+
if hasattr(self, 'current_trio_index'):
|
|
3995
|
+
txt = self.ax.text(x, y+5, f"A{self.current_trio_index}", color='green', ha='center', va='bottom')
|
|
3996
|
+
else:
|
|
3997
|
+
txt = self.ax.text(x, y+5, "A", color='green', ha='center', va='bottom')
|
|
3998
|
+
else:
|
|
3999
|
+
# Show yellow for distance mode (default)
|
|
4000
|
+
pt = self.ax.plot(x, y, 'yo', markersize=8)[0]
|
|
4001
|
+
if hasattr(self, 'current_pair_index'):
|
|
4002
|
+
txt = self.ax.text(x, y+5, f"D{self.current_pair_index}", color='yellow', ha='center', va='bottom')
|
|
4003
|
+
else:
|
|
4004
|
+
txt = self.ax.text(x, y+5, "D", color='yellow', ha='center', va='bottom')
|
|
4005
|
+
self.measurement_artists.extend([pt, txt])
|
|
4006
|
+
|
|
4007
|
+
# Handle second point in angle measurements
|
|
4008
|
+
if hasattr(self, 'current_second_point') and self.current_second_point is not None:
|
|
4009
|
+
x, y, z = self.current_second_point
|
|
4010
|
+
if z == self.current_slice:
|
|
4011
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
4012
|
+
if hasattr(self, 'current_trio_index'):
|
|
4013
|
+
txt = self.ax.text(x, y+5, f"B{self.current_trio_index}", color='green', ha='center', va='bottom')
|
|
4014
|
+
else:
|
|
4015
|
+
txt = self.ax.text(x, y+5, "B", color='green', ha='center', va='bottom')
|
|
4016
|
+
self.measurement_artists.extend([pt, txt])
|
|
4017
|
+
|
|
4018
|
+
# Draw line from A to B if both are on current slice
|
|
4019
|
+
if (hasattr(self, 'current_point') and self.current_point is not None and
|
|
4020
|
+
self.current_point[2] == self.current_slice):
|
|
4021
|
+
x1, y1, z1 = self.current_point
|
|
4022
|
+
line = self.ax.plot([x1, x], [y1, y], 'g--', alpha=0.7)[0]
|
|
4023
|
+
self.measurement_artists.append(line)
|
|
3879
4024
|
|
|
3880
4025
|
#self.canvas.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
3881
4026
|
|
|
@@ -3992,30 +4137,33 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3992
4137
|
self.highlight_value_in_tables(self.clicked_values['edges'][-1])
|
|
3993
4138
|
self.handle_info('edge')
|
|
3994
4139
|
|
|
3995
|
-
|
|
4140
|
+
try:
|
|
4141
|
+
if len(self.clicked_values['nodes']) > 0 or len(self.clicked_values['edges']) > 0: # Check if we have any nodes selected
|
|
3996
4142
|
|
|
3997
|
-
|
|
4143
|
+
old_nodes = copy.deepcopy(self.clicked_values['nodes'])
|
|
3998
4144
|
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4145
|
+
# Get the existing DataFrame from the model
|
|
4146
|
+
original_df = self.network_table.model()._data
|
|
4147
|
+
|
|
4148
|
+
# Create mask for rows where one column is any original node AND the other column is any neighbor
|
|
4149
|
+
mask = (
|
|
4150
|
+
((original_df.iloc[:, 0].isin(self.clicked_values['nodes'])) &
|
|
4151
|
+
(original_df.iloc[:, 1].isin(self.clicked_values['nodes']))) |
|
|
4152
|
+
(original_df.iloc[:, 2].isin(self.clicked_values['edges']))
|
|
4153
|
+
)
|
|
4154
|
+
|
|
4155
|
+
# Filter the DataFrame to only include direct connections
|
|
4156
|
+
filtered_df = original_df[mask].copy()
|
|
4157
|
+
|
|
4158
|
+
# Create new model with filtered DataFrame and update selection table
|
|
4159
|
+
new_model = PandasModel(filtered_df)
|
|
4160
|
+
self.selection_table.setModel(new_model)
|
|
4161
|
+
|
|
4162
|
+
# Switch to selection table
|
|
4163
|
+
self.selection_button.click()
|
|
4164
|
+
except:
|
|
4165
|
+
pass
|
|
4166
|
+
|
|
4019
4167
|
elif not self.selecting and self.selection_start: # If we had a click but never started selection
|
|
4020
4168
|
# Handle as a normal click
|
|
4021
4169
|
self.on_mouse_click(event)
|
|
@@ -4436,6 +4584,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4436
4584
|
rad_action.triggered.connect(self.show_rad_dialog)
|
|
4437
4585
|
inter_action = stats_menu.addAction("Calculate Node < > Edge Interaction")
|
|
4438
4586
|
inter_action.triggered.connect(self.show_interaction_dialog)
|
|
4587
|
+
violin_action = stats_menu.addAction("Show Identity Violins/UMAP")
|
|
4588
|
+
violin_action.triggered.connect(self.show_violin_dialog)
|
|
4439
4589
|
overlay_menu = analysis_menu.addMenu("Data/Overlays")
|
|
4440
4590
|
degree_action = overlay_menu.addAction("Get Degree Information")
|
|
4441
4591
|
degree_action.triggered.connect(self.show_degree_dialog)
|
|
@@ -4891,6 +5041,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4891
5041
|
for column in range(table.model().columnCount(None)):
|
|
4892
5042
|
table.resizeColumnToContents(column)
|
|
4893
5043
|
|
|
5044
|
+
return df
|
|
5045
|
+
|
|
4894
5046
|
except:
|
|
4895
5047
|
pass
|
|
4896
5048
|
|
|
@@ -5912,7 +6064,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5912
6064
|
|
|
5913
6065
|
if self.shape == self.channel_data[channel_index].shape:
|
|
5914
6066
|
preserve_zoom = (self.ax.get_xlim(), self.ax.get_ylim())
|
|
5915
|
-
self.shape = self.channel_data[channel_index].shape
|
|
6067
|
+
self.shape = (self.channel_data[channel_index].shape[0], self.channel_data[channel_index].shape[1], self.channel_data[channel_index].shape[2])
|
|
5916
6068
|
if self.shape[1] * self.shape[2] > 3000 * 3000 * self.downsample_factor:
|
|
5917
6069
|
self.throttle = True
|
|
5918
6070
|
else:
|
|
@@ -6278,8 +6430,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6278
6430
|
for img in list(self.ax.get_images()):
|
|
6279
6431
|
img.remove()
|
|
6280
6432
|
# Clear measurement points
|
|
6281
|
-
self
|
|
6282
|
-
|
|
6433
|
+
if hasattr(self, 'measurement_artists'):
|
|
6434
|
+
for artist in self.measurement_artists:
|
|
6435
|
+
try:
|
|
6436
|
+
artist.remove()
|
|
6437
|
+
except:
|
|
6438
|
+
pass # Artist might already be removed
|
|
6439
|
+
self.measurement_artists = [] # Reset the list
|
|
6283
6440
|
# Determine the current view bounds (either from preserve_zoom or current state)
|
|
6284
6441
|
if preserve_zoom:
|
|
6285
6442
|
current_xlim, current_ylim = preserve_zoom
|
|
@@ -6346,7 +6503,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6346
6503
|
return cropped[::factor, ::factor, :]
|
|
6347
6504
|
else:
|
|
6348
6505
|
return cropped
|
|
6349
|
-
|
|
6350
6506
|
|
|
6351
6507
|
# Update channel images efficiently with cropping and downsampling
|
|
6352
6508
|
for channel in range(4):
|
|
@@ -6421,10 +6577,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6421
6577
|
|
|
6422
6578
|
im = self.ax.imshow(normalized_image, alpha=0.7, cmap=custom_cmap,
|
|
6423
6579
|
vmin=0, vmax=1, extent=crop_extent)
|
|
6424
|
-
|
|
6425
6580
|
# Handle preview, overlays, and measurements (apply cropping here too)
|
|
6426
|
-
#if self.preview and not called:
|
|
6427
|
-
# self.create_highlight_overlay_slice(self.targs, bounds=self.bounds)
|
|
6428
6581
|
|
|
6429
6582
|
# Overlay handling (optimized with cropping and downsampling)
|
|
6430
6583
|
if self.mini_overlay and self.highlight and self.machine_window is None:
|
|
@@ -6446,34 +6599,88 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6446
6599
|
[(0, 0, 0, 0), (1, 1, 0, 1), (0, 0.7, 1, 1)])
|
|
6447
6600
|
self.ax.imshow(display_highlight, cmap=highlight_cmap, vmin=0, vmax=2, alpha=0.3, extent=crop_extent)
|
|
6448
6601
|
|
|
6449
|
-
# Redraw measurement points efficiently
|
|
6602
|
+
# Redraw measurement points efficiently
|
|
6450
6603
|
# Only draw points that are within the visible region for additional performance
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
current_xlim[0] <= x1 <= current_xlim[1] and
|
|
6459
|
-
current_ylim[1] <= y1 <= current_ylim[0])
|
|
6460
|
-
point2_visible = (z2 == self.current_slice and
|
|
6461
|
-
current_xlim[0] <= x2 <= current_xlim[1] and
|
|
6462
|
-
current_ylim[1] <= y2 <= current_ylim[0])
|
|
6463
|
-
|
|
6464
|
-
if point1_visible:
|
|
6465
|
-
pt1 = self.ax.plot(x1, y1, 'yo', markersize=8)[0]
|
|
6466
|
-
txt1 = self.ax.text(x1, y1+5, str(pair_idx), color='white', ha='center', va='bottom')
|
|
6467
|
-
self.measurement_artists.extend([pt1, txt1])
|
|
6604
|
+
|
|
6605
|
+
if hasattr(self, 'measurement_points') and self.measurement_points:
|
|
6606
|
+
for point in self.measurement_points:
|
|
6607
|
+
x1, y1, z1 = point['point1']
|
|
6608
|
+
x2, y2, z2 = point['point2']
|
|
6609
|
+
pair_idx = point['pair_index']
|
|
6610
|
+
point_type = point.get('type', 'distance') # Default to distance for backward compatibility
|
|
6468
6611
|
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6612
|
+
# Determine colors based on type
|
|
6613
|
+
if point_type == 'angle':
|
|
6614
|
+
marker_color = 'go'
|
|
6615
|
+
text_color = 'green'
|
|
6616
|
+
line_color = 'g--'
|
|
6617
|
+
else: # distance
|
|
6618
|
+
marker_color = 'yo'
|
|
6619
|
+
text_color = 'yellow'
|
|
6620
|
+
line_color = 'r--'
|
|
6473
6621
|
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6622
|
+
# Check if points are in visible region and on current slice
|
|
6623
|
+
point1_visible = (z1 == self.current_slice and
|
|
6624
|
+
current_xlim[0] <= x1 <= current_xlim[1] and
|
|
6625
|
+
current_ylim[1] <= y1 <= current_ylim[0])
|
|
6626
|
+
point2_visible = (z2 == self.current_slice and
|
|
6627
|
+
current_xlim[0] <= x2 <= current_xlim[1] and
|
|
6628
|
+
current_ylim[1] <= y2 <= current_ylim[0])
|
|
6629
|
+
|
|
6630
|
+
# Always draw individual points if they're on the current slice (even without lines)
|
|
6631
|
+
if point1_visible:
|
|
6632
|
+
pt1 = self.ax.plot(x1, y1, marker_color, markersize=8)[0]
|
|
6633
|
+
txt1 = self.ax.text(x1, y1+5, str(pair_idx), color=text_color, ha='center', va='bottom')
|
|
6634
|
+
self.measurement_artists.extend([pt1, txt1])
|
|
6635
|
+
|
|
6636
|
+
if point2_visible:
|
|
6637
|
+
pt2 = self.ax.plot(x2, y2, marker_color, markersize=8)[0]
|
|
6638
|
+
txt2 = self.ax.text(x2, y2+5, str(pair_idx), color=text_color, ha='center', va='bottom')
|
|
6639
|
+
self.measurement_artists.extend([pt2, txt2])
|
|
6640
|
+
|
|
6641
|
+
# Only draw connecting line if both points are on the same slice AND visible
|
|
6642
|
+
if z1 == z2 == self.current_slice and (point1_visible or point2_visible):
|
|
6643
|
+
line = self.ax.plot([x1, x2], [y1, y2], line_color, alpha=0.5)[0]
|
|
6644
|
+
self.measurement_artists.append(line)
|
|
6645
|
+
|
|
6646
|
+
# Also handle any partial measurements in progress (individual points without pairs yet)
|
|
6647
|
+
# This shows individual points even when a measurement isn't complete
|
|
6648
|
+
if hasattr(self, 'current_point') and self.current_point is not None:
|
|
6649
|
+
x, y, z = self.current_point
|
|
6650
|
+
if z == self.current_slice:
|
|
6651
|
+
if hasattr(self, 'measurement_mode') and self.measurement_mode == "angle":
|
|
6652
|
+
# Show green for angle mode
|
|
6653
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
6654
|
+
if hasattr(self, 'current_trio_index'):
|
|
6655
|
+
txt = self.ax.text(x, y+5, f"A{self.current_trio_index}", color='green', ha='center', va='bottom')
|
|
6656
|
+
else:
|
|
6657
|
+
txt = self.ax.text(x, y+5, "A", color='green', ha='center', va='bottom')
|
|
6658
|
+
else:
|
|
6659
|
+
# Show yellow for distance mode (default)
|
|
6660
|
+
pt = self.ax.plot(x, y, 'yo', markersize=8)[0]
|
|
6661
|
+
if hasattr(self, 'current_pair_index'):
|
|
6662
|
+
txt = self.ax.text(x, y+5, f"D{self.current_pair_index}", color='yellow', ha='center', va='bottom')
|
|
6663
|
+
else:
|
|
6664
|
+
txt = self.ax.text(x, y+5, "D", color='yellow', ha='center', va='bottom')
|
|
6665
|
+
self.measurement_artists.extend([pt, txt])
|
|
6666
|
+
|
|
6667
|
+
# Handle second point in angle measurements
|
|
6668
|
+
if hasattr(self, 'current_second_point') and self.current_second_point is not None:
|
|
6669
|
+
x, y, z = self.current_second_point
|
|
6670
|
+
if z == self.current_slice:
|
|
6671
|
+
pt = self.ax.plot(x, y, 'go', markersize=8)[0]
|
|
6672
|
+
if hasattr(self, 'current_trio_index'):
|
|
6673
|
+
txt = self.ax.text(x, y+5, f"B{self.current_trio_index}", color='green', ha='center', va='bottom')
|
|
6674
|
+
else:
|
|
6675
|
+
txt = self.ax.text(x, y+5, "B", color='green', ha='center', va='bottom')
|
|
6676
|
+
self.measurement_artists.extend([pt, txt])
|
|
6677
|
+
|
|
6678
|
+
# Draw line from A to B if both are on current slice
|
|
6679
|
+
if (hasattr(self, 'current_point') and self.current_point is not None and
|
|
6680
|
+
self.current_point[2] == self.current_slice):
|
|
6681
|
+
x1, y1, z1 = self.current_point
|
|
6682
|
+
line = self.ax.plot([x1, x], [y1, y], 'g--', alpha=0.7)[0]
|
|
6683
|
+
self.measurement_artists.append(line)
|
|
6477
6684
|
|
|
6478
6685
|
# Store current view limits for next update
|
|
6479
6686
|
self.ax._current_xlim = current_xlim
|
|
@@ -6615,6 +6822,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6615
6822
|
dialog = InteractionDialog(self)
|
|
6616
6823
|
dialog.exec()
|
|
6617
6824
|
|
|
6825
|
+
def show_violin_dialog(self):
|
|
6826
|
+
dialog = ViolinDialog(self)
|
|
6827
|
+
dialog.show()
|
|
6828
|
+
|
|
6618
6829
|
def show_degree_dialog(self):
|
|
6619
6830
|
dialog = DegreeDialog(self)
|
|
6620
6831
|
dialog.exec()
|
|
@@ -8145,7 +8356,7 @@ class MergeNodeIdDialog(QDialog):
|
|
|
8145
8356
|
result = {key: np.array([d[key] for d in id_dicts]) for key in all_keys}
|
|
8146
8357
|
|
|
8147
8358
|
|
|
8148
|
-
self.parent().format_for_upperright_table(result, 'NodeID', good_list, 'Mean Intensity')
|
|
8359
|
+
self.parent().format_for_upperright_table(result, 'NodeID', good_list, 'Mean Intensity (Save this Table for "Analyze -> Stats -> Show Violins")')
|
|
8149
8360
|
if umap:
|
|
8150
8361
|
my_network.identity_umap(result)
|
|
8151
8362
|
|
|
@@ -8153,7 +8364,7 @@ class MergeNodeIdDialog(QDialog):
|
|
|
8153
8364
|
QMessageBox.information(
|
|
8154
8365
|
self,
|
|
8155
8366
|
"Success",
|
|
8156
|
-
"Node Identities Merged. New IDs represent presence of corresponding img foreground with +, absence with -.
|
|
8367
|
+
"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)"
|
|
8157
8368
|
)
|
|
8158
8369
|
|
|
8159
8370
|
self.accept()
|
|
@@ -8932,12 +9143,16 @@ class NearNeighDialog(QDialog):
|
|
|
8932
9143
|
if my_network.node_identities is not None:
|
|
8933
9144
|
|
|
8934
9145
|
self.root = QComboBox()
|
|
8935
|
-
|
|
9146
|
+
roots = list(set(my_network.node_identities.values()))
|
|
9147
|
+
roots.sort()
|
|
9148
|
+
roots.append("All (Excluding Targets)")
|
|
9149
|
+
self.root.addItems(roots)
|
|
8936
9150
|
self.root.setCurrentIndex(0)
|
|
8937
9151
|
identities_layout.addRow("Root Identity to Search for Neighbor's IDs?", self.root)
|
|
8938
9152
|
|
|
8939
9153
|
self.targ = QComboBox()
|
|
8940
9154
|
neighs = list(set(my_network.node_identities.values()))
|
|
9155
|
+
neighs.sort()
|
|
8941
9156
|
neighs.append("All Others (Excluding Self)")
|
|
8942
9157
|
self.targ.addItems(neighs)
|
|
8943
9158
|
self.targ.setCurrentIndex(0)
|
|
@@ -9062,6 +9277,10 @@ class NearNeighDialog(QDialog):
|
|
|
9062
9277
|
except:
|
|
9063
9278
|
targ = None
|
|
9064
9279
|
|
|
9280
|
+
if root == "All (Excluding Targets)" and targ == 'All Others (Excluding Self)':
|
|
9281
|
+
root = None
|
|
9282
|
+
targ = None
|
|
9283
|
+
|
|
9065
9284
|
heatmap = self.map.isChecked()
|
|
9066
9285
|
threed = self.threed.isChecked()
|
|
9067
9286
|
numpy = self.numpy.isChecked()
|
|
@@ -9681,6 +9900,266 @@ class InteractionDialog(QDialog):
|
|
|
9681
9900
|
print(f"Error finding interactions: {e}")
|
|
9682
9901
|
|
|
9683
9902
|
|
|
9903
|
+
class ViolinDialog(QDialog):
|
|
9904
|
+
|
|
9905
|
+
def __init__(self, parent=None):
|
|
9906
|
+
|
|
9907
|
+
super().__init__(parent)
|
|
9908
|
+
|
|
9909
|
+
QMessageBox.critical(
|
|
9910
|
+
self,
|
|
9911
|
+
"Notice",
|
|
9912
|
+
"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.)"
|
|
9913
|
+
)
|
|
9914
|
+
|
|
9915
|
+
try:
|
|
9916
|
+
try:
|
|
9917
|
+
self.df = self.parent().load_file()
|
|
9918
|
+
except:
|
|
9919
|
+
return
|
|
9920
|
+
|
|
9921
|
+
self.backup_df = copy.deepcopy(self.df)
|
|
9922
|
+
# Get all identity lists and normalize the dataframe
|
|
9923
|
+
identity_lists = self.get_all_identity_lists()
|
|
9924
|
+
self.df = self.normalize_df_with_identity_centerpoints(self.df, identity_lists)
|
|
9925
|
+
|
|
9926
|
+
self.setWindowTitle("Violin Parameters")
|
|
9927
|
+
self.setModal(False)
|
|
9928
|
+
|
|
9929
|
+
layout = QFormLayout(self)
|
|
9930
|
+
|
|
9931
|
+
if my_network.node_identities is not None:
|
|
9932
|
+
|
|
9933
|
+
self.idens = QComboBox()
|
|
9934
|
+
all_idens = list(set(my_network.node_identities.values()))
|
|
9935
|
+
idens = []
|
|
9936
|
+
for iden in all_idens:
|
|
9937
|
+
if '[' not in iden:
|
|
9938
|
+
idens.append(iden)
|
|
9939
|
+
idens.sort()
|
|
9940
|
+
idens.insert(0, "None")
|
|
9941
|
+
self.idens.addItems(idens)
|
|
9942
|
+
self.idens.setCurrentIndex(0)
|
|
9943
|
+
layout.addRow("Return Identity Violin Plots?", self.idens)
|
|
9944
|
+
|
|
9945
|
+
if my_network.communities is not None:
|
|
9946
|
+
self.coms = QComboBox()
|
|
9947
|
+
coms = list(set(my_network.communities.values()))
|
|
9948
|
+
coms.sort()
|
|
9949
|
+
coms.insert(0, "None")
|
|
9950
|
+
coms = [str(x) for x in coms]
|
|
9951
|
+
self.coms.addItems(coms)
|
|
9952
|
+
self.coms.setCurrentIndex(0)
|
|
9953
|
+
layout.addRow("Return Neighborhood/Community Violin Plots?", self.coms)
|
|
9954
|
+
|
|
9955
|
+
# Add Run button
|
|
9956
|
+
run_button = QPushButton("Show Z-score-like Violin")
|
|
9957
|
+
run_button.clicked.connect(self.run)
|
|
9958
|
+
layout.addWidget(run_button)
|
|
9959
|
+
|
|
9960
|
+
run_button2 = QPushButton("Show Z-score UMAP")
|
|
9961
|
+
run_button2.clicked.connect(self.run2)
|
|
9962
|
+
layout.addWidget(run_button2)
|
|
9963
|
+
except:
|
|
9964
|
+
QTimer.singleShot(0, self.close)
|
|
9965
|
+
|
|
9966
|
+
def get_all_identity_lists(self):
|
|
9967
|
+
"""
|
|
9968
|
+
Get all identity lists for normalization purposes.
|
|
9969
|
+
|
|
9970
|
+
Returns:
|
|
9971
|
+
dict: Dictionary where keys are identity names and values are lists of node IDs
|
|
9972
|
+
"""
|
|
9973
|
+
identity_lists = {}
|
|
9974
|
+
|
|
9975
|
+
# Get all unique identities
|
|
9976
|
+
all_identities = set()
|
|
9977
|
+
import ast
|
|
9978
|
+
for item in my_network.node_identities:
|
|
9979
|
+
try:
|
|
9980
|
+
parse = ast.literal_eval(my_network.node_identities[item])
|
|
9981
|
+
if isinstance(parse, (list, tuple, set)):
|
|
9982
|
+
all_identities.update(parse)
|
|
9983
|
+
else:
|
|
9984
|
+
all_identities.add(str(parse))
|
|
9985
|
+
except:
|
|
9986
|
+
all_identities.add(str(my_network.node_identities[item]))
|
|
9987
|
+
|
|
9988
|
+
# For each identity, get the list of nodes that have it
|
|
9989
|
+
for identity in all_identities:
|
|
9990
|
+
iden_list = []
|
|
9991
|
+
for item in my_network.node_identities:
|
|
9992
|
+
try:
|
|
9993
|
+
parse = ast.literal_eval(my_network.node_identities[item])
|
|
9994
|
+
if identity in parse:
|
|
9995
|
+
iden_list.append(item)
|
|
9996
|
+
except:
|
|
9997
|
+
if identity == str(my_network.node_identities[item]):
|
|
9998
|
+
iden_list.append(item)
|
|
9999
|
+
|
|
10000
|
+
if iden_list: # Only add if we found nodes
|
|
10001
|
+
identity_lists[identity] = iden_list
|
|
10002
|
+
|
|
10003
|
+
return identity_lists
|
|
10004
|
+
|
|
10005
|
+
def normalize_df_with_identity_centerpoints(self, df, identity_lists):
|
|
10006
|
+
"""
|
|
10007
|
+
Normalize the entire dataframe using identity-specific centerpoints.
|
|
10008
|
+
Uses Z-score-like normalization with identity centerpoint as the "mean".
|
|
10009
|
+
|
|
10010
|
+
Parameters:
|
|
10011
|
+
df (pd.DataFrame): Original dataframe
|
|
10012
|
+
identity_lists (dict): Dictionary where keys are identity names and values are lists of node IDs
|
|
10013
|
+
|
|
10014
|
+
Returns:
|
|
10015
|
+
pd.DataFrame: Normalized dataframe
|
|
10016
|
+
"""
|
|
10017
|
+
# Make a copy to avoid modifying the original dataframe
|
|
10018
|
+
df_copy = df.copy()
|
|
10019
|
+
|
|
10020
|
+
# Set the first column as the index (row headers)
|
|
10021
|
+
df_copy = df_copy.set_index(df_copy.columns[0])
|
|
10022
|
+
|
|
10023
|
+
# Convert all remaining columns to float type (batch conversion)
|
|
10024
|
+
df_copy = df_copy.astype(float)
|
|
10025
|
+
|
|
10026
|
+
# First, calculate the centerpoint for each column by finding the median across all identity groups
|
|
10027
|
+
column_centerpoints = {}
|
|
10028
|
+
|
|
10029
|
+
for column in df_copy.columns:
|
|
10030
|
+
centerpoint = None
|
|
10031
|
+
|
|
10032
|
+
for identity, node_list in identity_lists.items():
|
|
10033
|
+
# Get nodes that exist in both the identity list and the dataframe
|
|
10034
|
+
valid_nodes = [node for node in node_list if node in df_copy.index]
|
|
10035
|
+
if valid_nodes and ((str(identity) == str(column)) or str(identity) == f'{str(column)}+'):
|
|
10036
|
+
# Get the median value for this identity in this column
|
|
10037
|
+
identity_min = df_copy.loc[valid_nodes, column].median()
|
|
10038
|
+
centerpoint = identity_min
|
|
10039
|
+
break # Found the match, no need to continue
|
|
10040
|
+
|
|
10041
|
+
if centerpoint is not None:
|
|
10042
|
+
# Use the identity-specific centerpoint
|
|
10043
|
+
column_centerpoints[column] = centerpoint
|
|
10044
|
+
else:
|
|
10045
|
+
# Fallback: if no matching identity, use column median
|
|
10046
|
+
column_centerpoints[column] = df_copy[column].median()
|
|
10047
|
+
|
|
10048
|
+
# Now normalize each column using Z-score-like calculation with identity centerpoint
|
|
10049
|
+
df_normalized = df_copy.copy()
|
|
10050
|
+
for column in df_copy.columns:
|
|
10051
|
+
centerpoint = column_centerpoints[column]
|
|
10052
|
+
# Calculate standard deviation of the column
|
|
10053
|
+
std_dev = df_copy[column].std()
|
|
10054
|
+
|
|
10055
|
+
if std_dev > 0: # Avoid division by zero
|
|
10056
|
+
# Z-score-like: (value - centerpoint) / std_dev
|
|
10057
|
+
df_normalized[column] = (df_copy[column] - centerpoint) / std_dev
|
|
10058
|
+
else:
|
|
10059
|
+
# If std_dev is 0, just subtract centerpoint
|
|
10060
|
+
df_normalized[column] = df_copy[column] - centerpoint
|
|
10061
|
+
|
|
10062
|
+
# Convert back to original format with first column as regular column
|
|
10063
|
+
df_normalized = df_normalized.reset_index()
|
|
10064
|
+
|
|
10065
|
+
return df_normalized
|
|
10066
|
+
|
|
10067
|
+
def run(self):
|
|
10068
|
+
|
|
10069
|
+
def df_to_dict_by_rows(df, row_indices):
|
|
10070
|
+
"""
|
|
10071
|
+
Convert a pandas DataFrame to a dictionary by selecting specific rows.
|
|
10072
|
+
No normalization - dataframe is already normalized.
|
|
10073
|
+
|
|
10074
|
+
Parameters:
|
|
10075
|
+
df (pd.DataFrame): DataFrame with first column as row headers, remaining columns contain floats
|
|
10076
|
+
row_indices (list): List of values from the first column representing rows to include
|
|
10077
|
+
|
|
10078
|
+
Returns:
|
|
10079
|
+
dict: Dictionary where keys are column headers and values are lists of column values (as floats)
|
|
10080
|
+
for the specified rows
|
|
10081
|
+
"""
|
|
10082
|
+
# Make a copy to avoid modifying the original dataframe
|
|
10083
|
+
df_copy = df.copy()
|
|
10084
|
+
|
|
10085
|
+
# Set the first column as the index (row headers)
|
|
10086
|
+
df_copy = df_copy.set_index(df_copy.columns[0])
|
|
10087
|
+
|
|
10088
|
+
# Mask the dataframe to include only the specified rows
|
|
10089
|
+
masked_df = df_copy.loc[row_indices]
|
|
10090
|
+
|
|
10091
|
+
# Create empty dictionary
|
|
10092
|
+
result_dict = {}
|
|
10093
|
+
|
|
10094
|
+
# For each column, add the column header as key and column values as list
|
|
10095
|
+
for column in masked_df.columns:
|
|
10096
|
+
result_dict[column] = masked_df[column].tolist()
|
|
10097
|
+
|
|
10098
|
+
return result_dict
|
|
10099
|
+
|
|
10100
|
+
from . import neighborhoods
|
|
10101
|
+
|
|
10102
|
+
if self.idens.currentIndex() != 0:
|
|
10103
|
+
|
|
10104
|
+
iden = self.idens.currentText()
|
|
10105
|
+
iden_list = []
|
|
10106
|
+
import ast
|
|
10107
|
+
|
|
10108
|
+
for item in my_network.node_identities:
|
|
10109
|
+
|
|
10110
|
+
try:
|
|
10111
|
+
parse = ast.literal_eval(my_network.node_identities[item])
|
|
10112
|
+
if iden in parse:
|
|
10113
|
+
iden_list.append(item)
|
|
10114
|
+
except:
|
|
10115
|
+
if iden == item:
|
|
10116
|
+
iden_list.append(item)
|
|
10117
|
+
|
|
10118
|
+
violin_dict = df_to_dict_by_rows(self.df, iden_list)
|
|
10119
|
+
|
|
10120
|
+
neighborhoods.create_violin_plots(violin_dict, graph_title=f"Z-Score-like Channel Intensities of Identity {iden}, {len(iden_list)} Nodes")
|
|
10121
|
+
|
|
10122
|
+
|
|
10123
|
+
if self.coms.currentIndex() != 0:
|
|
10124
|
+
|
|
10125
|
+
com = self.coms.currentText()
|
|
10126
|
+
|
|
10127
|
+
com_dict = n3d.invert_dict(my_network.communities) # Fixed: should be communities
|
|
10128
|
+
|
|
10129
|
+
com_list = com_dict[int(com)]
|
|
10130
|
+
|
|
10131
|
+
violin_dict = df_to_dict_by_rows(self.df, com_list)
|
|
10132
|
+
|
|
10133
|
+
neighborhoods.create_violin_plots(violin_dict, graph_title=f"Z-Score-like Channel Intensities of Community/Neighborhood {com}, {len(com_list)} Nodes")
|
|
10134
|
+
|
|
10135
|
+
|
|
10136
|
+
def run2(self):
|
|
10137
|
+
def df_to_dict(df):
|
|
10138
|
+
# Make a copy to avoid modifying the original dataframe
|
|
10139
|
+
df_copy = df.copy()
|
|
10140
|
+
|
|
10141
|
+
# Set the first column as the index (row headers)
|
|
10142
|
+
df_copy = df_copy.set_index(df_copy.columns[0])
|
|
10143
|
+
|
|
10144
|
+
# Convert all remaining columns to float type (batch conversion)
|
|
10145
|
+
df_copy = df_copy.astype(float)
|
|
10146
|
+
|
|
10147
|
+
# Create the result dictionary
|
|
10148
|
+
result_dict = {}
|
|
10149
|
+
for row_idx in df_copy.index:
|
|
10150
|
+
result_dict[row_idx] = df_copy.loc[row_idx].tolist()
|
|
10151
|
+
|
|
10152
|
+
return result_dict
|
|
10153
|
+
|
|
10154
|
+
try:
|
|
10155
|
+
umap_dict = df_to_dict(self.backup_df)
|
|
10156
|
+
my_network.identity_umap(umap_dict)
|
|
10157
|
+
except:
|
|
10158
|
+
pass
|
|
10159
|
+
|
|
10160
|
+
|
|
10161
|
+
|
|
10162
|
+
|
|
9684
10163
|
class DegreeDialog(QDialog):
|
|
9685
10164
|
|
|
9686
10165
|
|
|
@@ -12254,13 +12733,23 @@ class HoleDialog(QDialog):
|
|
|
12254
12733
|
borders = self.borders.isChecked()
|
|
12255
12734
|
headon = self.headon.isChecked()
|
|
12256
12735
|
sep_holes = self.sep_holes.isChecked()
|
|
12736
|
+
|
|
12737
|
+
if borders:
|
|
12257
12738
|
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12739
|
+
# Call dilate method with parameters
|
|
12740
|
+
result = n3d.fill_holes_3d_old(
|
|
12741
|
+
active_data,
|
|
12742
|
+
head_on = headon,
|
|
12743
|
+
fill_borders = borders
|
|
12744
|
+
)
|
|
12745
|
+
|
|
12746
|
+
else:
|
|
12747
|
+
# Call dilate method with parameters
|
|
12748
|
+
result = n3d.fill_holes_3d(
|
|
12749
|
+
active_data,
|
|
12750
|
+
head_on = headon,
|
|
12751
|
+
fill_borders = borders
|
|
12752
|
+
)
|
|
12264
12753
|
|
|
12265
12754
|
if not sep_holes:
|
|
12266
12755
|
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
@@ -12710,16 +13199,58 @@ class GrayWaterDialog(QDialog):
|
|
|
12710
13199
|
run_button.clicked.connect(self.run_watershed)
|
|
12711
13200
|
layout.addRow(run_button)
|
|
12712
13201
|
|
|
13202
|
+
def wait_for_threshold_processing(self):
|
|
13203
|
+
"""
|
|
13204
|
+
Opens ThresholdWindow and waits for user to process the image.
|
|
13205
|
+
Returns True if completed, False if cancelled.
|
|
13206
|
+
The thresholded image will be available in the main window after completion.
|
|
13207
|
+
"""
|
|
13208
|
+
# Create event loop to wait for user
|
|
13209
|
+
loop = QEventLoop()
|
|
13210
|
+
result = {'completed': False}
|
|
13211
|
+
|
|
13212
|
+
# Create the threshold window
|
|
13213
|
+
thresh_window = ThresholdWindow(self.parent(), 0)
|
|
13214
|
+
|
|
13215
|
+
|
|
13216
|
+
# Connect signals
|
|
13217
|
+
def on_processing_complete():
|
|
13218
|
+
result['completed'] = True
|
|
13219
|
+
loop.quit()
|
|
13220
|
+
|
|
13221
|
+
def on_processing_cancelled():
|
|
13222
|
+
result['completed'] = False
|
|
13223
|
+
loop.quit()
|
|
13224
|
+
|
|
13225
|
+
thresh_window.processing_complete.connect(on_processing_complete)
|
|
13226
|
+
thresh_window.processing_cancelled.connect(on_processing_cancelled)
|
|
13227
|
+
|
|
13228
|
+
# Show window and wait
|
|
13229
|
+
thresh_window.show()
|
|
13230
|
+
thresh_window.raise_()
|
|
13231
|
+
thresh_window.activateWindow()
|
|
13232
|
+
|
|
13233
|
+
# Block until user clicks "Apply Threshold & Continue" or "Cancel"
|
|
13234
|
+
loop.exec()
|
|
13235
|
+
|
|
13236
|
+
# Clean up
|
|
13237
|
+
thresh_window.deleteLater()
|
|
13238
|
+
|
|
13239
|
+
return result['completed']
|
|
13240
|
+
|
|
12713
13241
|
def run_watershed(self):
|
|
12714
13242
|
|
|
12715
13243
|
try:
|
|
12716
13244
|
|
|
13245
|
+
self.accept()
|
|
13246
|
+
print("Please threshold foreground, or press cancel/skip if not desired:")
|
|
13247
|
+
self.wait_for_threshold_processing()
|
|
13248
|
+
data = self.parent().channel_data[self.parent().active_channel]
|
|
13249
|
+
|
|
12717
13250
|
min_intensity = float(self.min_intensity.text()) if self.min_intensity.text().strip() else None
|
|
12718
13251
|
|
|
12719
13252
|
min_peak_distance = int(self.min_peak_distance.text()) if self.min_peak_distance.text().strip() else 1
|
|
12720
13253
|
|
|
12721
|
-
data = self.parent().channel_data[self.parent().active_channel]
|
|
12722
|
-
|
|
12723
13254
|
data = n3d.gray_watershed(data, min_peak_distance, min_intensity)
|
|
12724
13255
|
|
|
12725
13256
|
self.parent().load_channel(self.parent().active_channel, data, data = True, preserve_zoom = (self.parent().ax.get_xlim(), self.parent().ax.get_ylim()))
|