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.
@@ -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
- data=data,
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
- data=data_dict,
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
- else:
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.current_second_point is None:
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
- else:
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
- self.ax.plot(x, y, 'yo', markersize=8)
1383
- self.ax.text(x, y+5, f"D{self.current_pair_index}",
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
- self.ax.plot(x, y, 'go', markersize=8)
1429
- self.ax.text(x, y+5, f"A{self.current_trio_index}",
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
- self.ax.plot(x, y, 'go', markersize=8)
1440
- self.ax.text(x, y+5, f"B{self.current_trio_index}",
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
- # Get the existing DataFrame from the model
1829
- original_df = self.network_table.model()._data
1857
+ try:
1830
1858
 
1831
- # Create mask for rows for nodes in question
1832
- mask = (
1833
- (original_df.iloc[:, 0].isin(nodes) & original_df.iloc[:, 1].isin(nodes))
1834
- )
1835
-
1836
- # Filter the DataFrame to only include direct connections
1837
- filtered_df = original_df[mask].copy()
1838
-
1839
- # Create new model with filtered DataFrame and update selection table
1840
- new_model = PandasModel(filtered_df)
1841
- self.selection_table.setModel(new_model)
1842
-
1843
- # Switch to selection table
1844
- self.selection_button.click()
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 (coordinates remain in original space)
3863
- for point in self.measurement_points:
3864
- x1, y1, z1 = point['point1']
3865
- x2, y2, z2 = point['point2']
3866
- pair_idx = point['pair_index']
3867
-
3868
- if z1 == self.current_slice:
3869
- self.ax.plot(x1, y1, 'yo', markersize=8)
3870
- self.ax.text(x1, y1+5, str(pair_idx),
3871
- color='white', ha='center', va='bottom')
3872
- if z2 == self.current_slice:
3873
- self.ax.plot(x2, y2, 'yo', markersize=8)
3874
- self.ax.text(x2, y2+5, str(pair_idx),
3875
- color='white', ha='center', va='bottom')
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
- if z1 == z2 == self.current_slice:
3878
- self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
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
- if len(self.clicked_values['nodes']) > 0 or len(self.clicked_values['edges']) > 0: # Check if we have any nodes selected
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
- old_nodes = copy.deepcopy(self.clicked_values['nodes'])
4143
+ old_nodes = copy.deepcopy(self.clicked_values['nodes'])
3998
4144
 
3999
- # Get the existing DataFrame from the model
4000
- original_df = self.network_table.model()._data
4001
-
4002
- # Create mask for rows where one column is any original node AND the other column is any neighbor
4003
- mask = (
4004
- ((original_df.iloc[:, 0].isin(self.clicked_values['nodes'])) &
4005
- (original_df.iloc[:, 1].isin(self.clicked_values['nodes']))) |
4006
- (original_df.iloc[:, 2].isin(self.clicked_values['edges']))
4007
- )
4008
-
4009
- # Filter the DataFrame to only include direct connections
4010
- filtered_df = original_df[mask].copy()
4011
-
4012
- # Create new model with filtered DataFrame and update selection table
4013
- new_model = PandasModel(filtered_df)
4014
- self.selection_table.setModel(new_model)
4015
-
4016
- # Switch to selection table
4017
- self.selection_button.click()
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.measurement_artists.clear()
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 (no cropping needed - these are vector graphics)
6602
+ # Redraw measurement points efficiently
6450
6603
  # Only draw points that are within the visible region for additional performance
6451
- for point in self.measurement_points:
6452
- x1, y1, z1 = point['point1']
6453
- x2, y2, z2 = point['point2']
6454
- pair_idx = point['pair_index']
6455
-
6456
- # Check if points are in visible region
6457
- point1_visible = (z1 == self.current_slice and
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
- if point2_visible:
6470
- pt2 = self.ax.plot(x2, y2, 'yo', markersize=8)[0]
6471
- txt2 = self.ax.text(x2, y2+5, str(pair_idx), color='white', ha='center', va='bottom')
6472
- self.measurement_artists.extend([pt2, txt2])
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
- if z1 == z2 == self.current_slice and (point1_visible or point2_visible):
6475
- line = self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)[0]
6476
- self.measurement_artists.append(line)
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 -. Please save your new identities as csv, then use File -> Load -> Load From Excel Helper to bulk search and rename desired combinations. (Press Help [above] for more info)"
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
- self.root.addItems(list(set(my_network.node_identities.values())))
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
- # Call dilate method with parameters
12259
- result = n3d.fill_holes_3d(
12260
- active_data,
12261
- head_on = headon,
12262
- fill_borders = borders
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()))