nettracer3d 0.6.9__tar.gz → 0.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nettracer3d might be problematic. Click here for more details.

Files changed (27) hide show
  1. {nettracer3d-0.6.9/src/nettracer3d.egg-info → nettracer3d-0.7.1}/PKG-INFO +27 -25
  2. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/README.md +8 -6
  3. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/pyproject.toml +19 -19
  4. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d/morphology.py +2 -0
  5. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d/nettracer.py +176 -0
  6. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d/nettracer_gui.py +238 -5
  7. nettracer3d-0.7.1/src/nettracer3d/proximity.py +649 -0
  8. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d/segmenter.py +86 -3
  9. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d/smart_dilate.py +4 -4
  10. {nettracer3d-0.6.9 → nettracer3d-0.7.1/src/nettracer3d.egg-info}/PKG-INFO +27 -25
  11. nettracer3d-0.7.1/src/nettracer3d.egg-info/requires.txt +26 -0
  12. nettracer3d-0.6.9/src/nettracer3d/proximity.py +0 -325
  13. nettracer3d-0.6.9/src/nettracer3d.egg-info/requires.txt +0 -26
  14. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/LICENSE +0 -0
  15. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/setup.cfg +0 -0
  16. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d/__init__.py +0 -0
  17. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d/community_extractor.py +0 -0
  18. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d/modularity.py +0 -0
  19. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d/network_analysis.py +0 -0
  20. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d/network_draw.py +0 -0
  21. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d/node_draw.py +0 -0
  22. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d/run.py +0 -0
  23. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d/simple_network.py +0 -0
  24. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
  25. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  26. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d.egg-info/entry_points.txt +0 -0
  27. {nettracer3d-0.6.9 → nettracer3d-0.7.1}/src/nettracer3d.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.6.9
3
+ Version: 0.7.1
4
4
  Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
5
5
  Author-email: Liam McLaughlin <liamm@wustl.edu>
6
6
  Project-URL: Documentation, https://nettracer3d.readthedocs.io/en/latest/
@@ -9,26 +9,26 @@ Project-URL: Reference_Citation_For_Use, https://doi.org/10.1101/2024.07.29.6056
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: License :: Other/Proprietary License
11
11
  Classifier: Operating System :: OS Independent
12
- Requires-Python: ==3.11
12
+ Requires-Python: >=3.7
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
- Requires-Dist: numpy==1.26.4
16
- Requires-Dist: scipy==1.14.1
17
- Requires-Dist: scikit-image==0.25.0
18
- Requires-Dist: Pillow==11.1.0
19
- Requires-Dist: matplotlib==3.9.2
20
- Requires-Dist: networkx==3.2.1
21
- Requires-Dist: opencv-python-headless==4.10.0.84
22
- Requires-Dist: openpyxl==3.1.2
23
- Requires-Dist: pandas==2.2.0
24
- Requires-Dist: napari==0.5.5
25
- Requires-Dist: python-louvain==0.16
26
- Requires-Dist: tifffile==2023.7.18
27
- Requires-Dist: qtrangeslider==0.1.5
28
- Requires-Dist: PyQt6==6.8.0
29
- Requires-Dist: scikit-learn==1.6.1
30
- Requires-Dist: nibabel==5.2.0
31
- Requires-Dist: setuptools>=65.0.0
15
+ Requires-Dist: numpy
16
+ Requires-Dist: scipy
17
+ Requires-Dist: scikit-image
18
+ Requires-Dist: Pillow
19
+ Requires-Dist: matplotlib
20
+ Requires-Dist: networkx
21
+ Requires-Dist: opencv-python-headless
22
+ Requires-Dist: openpyxl
23
+ Requires-Dist: pandas
24
+ Requires-Dist: napari
25
+ Requires-Dist: python-louvain
26
+ Requires-Dist: tifffile
27
+ Requires-Dist: qtrangeslider
28
+ Requires-Dist: PyQt6
29
+ Requires-Dist: scikit-learn
30
+ Requires-Dist: nibabel
31
+ Requires-Dist: setuptools
32
32
  Provides-Extra: cuda11
33
33
  Requires-Dist: cupy-cuda11x; extra == "cuda11"
34
34
  Provides-Extra: cuda12
@@ -73,12 +73,14 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
73
73
 
74
74
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
75
75
 
76
- -- Version 0.6.9 updates --
76
+ -- Version 0.7.0 Updates --
77
77
 
78
- 1. Adjusted all distance transform-based dilation/radius calculating methods to simply use the already supported scipy.ndimage.distance_transform_edt() sampling parameter to account for differentially scaled axis (previously the image was being resampled but now it no longer will need to do that).
78
+ 1. Added new function in 'Analyze -> Stats -> Cluster Analysis'
79
+ * This function allows the user to create a ripley's K or H function to compare the relative clustering of two types of nodes, or of one type of node vs itself.
79
80
 
80
- 2. Added new right click option to extract highlighted regions and implant their data onto a separate image or into a new empty image.
81
+ 2. Added new function in 'Analyze -> Randomize -> Scramble Nodes'
82
+ * This function randomly rearranges the node (centroids) for comparison with other centroid-using methods, as a possible way to demonstrate non-random behavior.
83
+ * The randomize menu is likewise new and the 'Generate Equivalent Random Network' method was moved there.
81
84
 
82
- 3. General bug fixes and improvements.
83
-
84
- 4. Now specifies python 3.11.
85
+ 3. Bug fixes.
86
+ * Importantly fixed a bug with dt-based dilation not working in 2D, which I had accidentally introduced recently.
@@ -34,12 +34,14 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
34
34
 
35
35
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
36
36
 
37
- -- Version 0.6.9 updates --
37
+ -- Version 0.7.0 Updates --
38
38
 
39
- 1. Adjusted all distance transform-based dilation/radius calculating methods to simply use the already supported scipy.ndimage.distance_transform_edt() sampling parameter to account for differentially scaled axis (previously the image was being resampled but now it no longer will need to do that).
39
+ 1. Added new function in 'Analyze -> Stats -> Cluster Analysis'
40
+ * This function allows the user to create a ripley's K or H function to compare the relative clustering of two types of nodes, or of one type of node vs itself.
40
41
 
41
- 2. Added new right click option to extract highlighted regions and implant their data onto a separate image or into a new empty image.
42
+ 2. Added new function in 'Analyze -> Randomize -> Scramble Nodes'
43
+ * This function randomly rearranges the node (centroids) for comparison with other centroid-using methods, as a possible way to demonstrate non-random behavior.
44
+ * The randomize menu is likewise new and the 'Generate Equivalent Random Network' method was moved there.
42
45
 
43
- 3. General bug fixes and improvements.
44
-
45
- 4. Now specifies python 3.11.
46
+ 3. Bug fixes.
47
+ * Importantly fixed a bug with dt-based dilation not working in 2D, which I had accidentally introduced recently.
@@ -1,33 +1,33 @@
1
1
  [project]
2
2
  name = "nettracer3d"
3
- version = "0.6.9"
3
+ version = "0.7.1"
4
4
  authors = [
5
5
  { name="Liam McLaughlin", email="liamm@wustl.edu" },
6
6
  ]
7
7
  description = "Scripts for intializing and analyzing networks from segmentations of three dimensional images."
8
8
 
9
9
  dependencies = [
10
- "numpy == 1.26.4",
11
- "scipy == 1.14.1",
12
- "scikit-image == 0.25.0",
13
- "Pillow == 11.1.0",
14
- "matplotlib == 3.9.2",
15
- "networkx == 3.2.1",
16
- "opencv-python-headless == 4.10.0.84",
17
- "openpyxl == 3.1.2",
18
- "pandas == 2.2.0",
19
- "napari == 0.5.5",
20
- "python-louvain == 0.16",
21
- "tifffile == 2023.7.18",
22
- "qtrangeslider == 0.1.5",
23
- "PyQt6 == 6.8.0",
24
- "scikit-learn == 1.6.1",
25
- "nibabel == 5.2.0",
26
- "setuptools >= 65.0.0"
10
+ "numpy",
11
+ "scipy",
12
+ "scikit-image",
13
+ "Pillow",
14
+ "matplotlib",
15
+ "networkx",
16
+ "opencv-python-headless",
17
+ "openpyxl",
18
+ "pandas",
19
+ "napari",
20
+ "python-louvain",
21
+ "tifffile",
22
+ "qtrangeslider",
23
+ "PyQt6",
24
+ "scikit-learn",
25
+ "nibabel",
26
+ "setuptools"
27
27
  ]
28
28
 
29
29
  readme = "README.md"
30
- requires-python = "==3.11"
30
+ requires-python = ">=3.7"
31
31
  classifiers = [
32
32
  "Programming Language :: Python :: 3",
33
33
  "License :: Other/Proprietary License",
@@ -518,6 +518,7 @@ def compute_distance_transform_distance_GPU(nodes, sampling = [1,1,1]):
518
518
  is_pseudo_3d = nodes.shape[0] == 1
519
519
  if is_pseudo_3d:
520
520
  nodes = cp.squeeze(nodes) # Convert to 2D for processing
521
+ del sampling[0]
521
522
 
522
523
  # Compute the distance transform on the GPU
523
524
  distance = cpx.distance_transform_edt(nodes, sampling = sampling)
@@ -533,6 +534,7 @@ def compute_distance_transform_distance(nodes, sampling = [1,1,1]):
533
534
  is_pseudo_3d = nodes.shape[0] == 1
534
535
  if is_pseudo_3d:
535
536
  nodes = np.squeeze(nodes) # Convert to 2D for processing
537
+ del sampling[0]
536
538
 
537
539
  # Fallback to CPU if there's an issue with GPU computation
538
540
  distance = ndimage.distance_transform_edt(nodes, sampling = sampling)
@@ -4431,6 +4431,101 @@ class Network_3D:
4431
4431
  return neighborhood_dict, proportion_dict, title1, title2, densities
4432
4432
 
4433
4433
 
4434
+ def get_ripley(self, root = None, targ = None, distance = 1, edgecorrect = True, bounds = None, ignore_dims = False, proportion = 0.5):
4435
+
4436
+
4437
+ if root is None or targ is None: #Self clustering in this case
4438
+ roots = self._node_centroids.values()
4439
+ targs = self._node_centroids.values()
4440
+ else:
4441
+ roots = []
4442
+ targs = []
4443
+
4444
+ for node, nodeid in self.node_identities.items(): #Otherwise we need to pull out this info
4445
+ if nodeid == root:
4446
+ roots.append(self._node_centroids[node])
4447
+ elif nodeid == targ:
4448
+ targs.append(self._node_centroids[node])
4449
+
4450
+ rooties = proximity.convert_centroids_to_array(roots, xy_scale = self.xy_scale, z_scale = self.z_scale)
4451
+ targs = proximity.convert_centroids_to_array(roots, xy_scale = self.xy_scale, z_scale = self.z_scale)
4452
+ points_array = np.vstack((rooties, targs))
4453
+ del rooties
4454
+
4455
+ try:
4456
+ if self.nodes.shape[0] == 1:
4457
+ dim = 2
4458
+ else:
4459
+ dim = 3
4460
+ except:
4461
+ dim = 2
4462
+ for centroid in self.node_centroids.values():
4463
+ if centroid[0] != 0:
4464
+ dim = 3
4465
+ break
4466
+
4467
+
4468
+ if ignore_dims:
4469
+
4470
+ factor = 0.25
4471
+
4472
+
4473
+ if bounds is None:
4474
+ if dim == 2:
4475
+ min_coords = np.array([0,0])
4476
+ else:
4477
+ min_coords = np.array([0,0,0])
4478
+ max_coords = np.max(points_array, axis=0)
4479
+ max_coords = np.flip(max_coords)
4480
+ bounds = (min_coords, max_coords)
4481
+ else:
4482
+ min_coords, max_coords = bounds
4483
+
4484
+ dim_list = max_coords - min_coords
4485
+
4486
+ new_list = []
4487
+
4488
+
4489
+ if dim == 3:
4490
+ for centroid in roots:
4491
+
4492
+ if ((centroid[2] - min_coords[0]) > dim_list[0] * factor) and ((max_coords[0] - centroid[2]) > dim_list[0] * factor) and ((centroid[1] - min_coords[1]) > dim_list[1] * factor) and ((max_coords[1] - centroid[1]) > dim_list[1] * factor) and ((centroid[0] - min_coords[2]) > dim_list[2] * factor) and ((max_coords[2] - centroid[0]) > dim_list[2] * factor):
4493
+ new_list.append(centroid)
4494
+ #print(f"dim_list: {dim_list}, centroid: {centroid}, min_coords: {min_coords}, max_coords: {max_coords}")
4495
+ else:
4496
+ for centroid in roots:
4497
+
4498
+ if ((centroid[2] - min_coords[0]) > dim_list[0] * factor) and ((max_coords[0] - centroid[2]) > dim_list[0] * factor) and ((centroid[1] - min_coords[1]) > dim_list[1] * factor) and ((max_coords[1] - centroid[1]) > dim_list[1] * factor):
4499
+ new_list.append(centroid)
4500
+
4501
+ roots = new_list
4502
+ print(f"Utilizing {len(roots)} root points. Note that low n values are unstable.")
4503
+ is_subset = True
4504
+ else:
4505
+ is_subset = False
4506
+
4507
+
4508
+
4509
+
4510
+ roots = proximity.convert_centroids_to_array(roots, xy_scale = self.xy_scale, z_scale = self.z_scale)
4511
+
4512
+
4513
+ if dim == 2:
4514
+ roots = proximity.convert_augmented_array_to_points(roots)
4515
+ targs = proximity.convert_augmented_array_to_points(targs)
4516
+
4517
+ r_vals = proximity.generate_r_values(points_array, distance, bounds = bounds, dim = dim, max_proportion=proportion)
4518
+
4519
+ k_vals = proximity.optimized_ripleys_k(roots, targs, r_vals, bounds=bounds, edge_correction=edgecorrect, dim = dim, is_subset = is_subset)
4520
+
4521
+ h_vals = proximity.compute_ripleys_h(k_vals, r_vals, dim)
4522
+
4523
+ proximity.plot_ripley_functions(r_vals, k_vals, h_vals, dim)
4524
+
4525
+ return r_vals, k_vals, h_vals
4526
+
4527
+
4528
+
4434
4529
 
4435
4530
  #Morphological stats or network linking:
4436
4531
 
@@ -4490,6 +4585,87 @@ class Network_3D:
4490
4585
 
4491
4586
  return array
4492
4587
 
4588
+
4589
+
4590
+ def random_nodes(self, bounds = None, mask = None):
4591
+
4592
+ if self.nodes is not None:
4593
+ try:
4594
+ self.nodes = np.zeros_like(self.nodes)
4595
+ except:
4596
+ pass
4597
+
4598
+
4599
+ if mask is not None:
4600
+ coords = np.argwhere(mask != 0)
4601
+ else:
4602
+ if bounds is not None:
4603
+ (z1, y1, x1), (z2, y2, x2) = bounds
4604
+ z1, y1, x1 = int(z1), int(y1), int(x1)
4605
+ z2, y2, x2 = int(z2), int(y2), int(x2)
4606
+ z_range = np.arange(z1, z2 + 1)
4607
+ y_range = np.arange(y1, y2 + 1)
4608
+ x_range = np.arange(x1, x2 + 1)
4609
+ z_grid, y_grid, x_grid = np.meshgrid(z_range, y_range, x_range, indexing='ij')
4610
+ del z_range
4611
+ del y_range
4612
+ del x_range
4613
+ coords = np.stack([z_grid.flatten(), y_grid.flatten(), x_grid.flatten()], axis=1)
4614
+ del z_grid
4615
+ del y_grid
4616
+ del x_grid
4617
+ else:
4618
+ shape = ()
4619
+ try:
4620
+ shape = self.nodes.shape
4621
+ except:
4622
+ try:
4623
+ shape = self.edges.shape
4624
+ except:
4625
+ try:
4626
+ shape = self._network_overlay.shape
4627
+ except:
4628
+ try:
4629
+ shape = self._id_overlay.shape
4630
+ except:
4631
+ pass
4632
+
4633
+ ranges = [np.arange(s) for s in shape]
4634
+
4635
+ # Create meshgrid
4636
+ mesh = np.meshgrid(*ranges, indexing='ij')
4637
+ del ranges
4638
+
4639
+ # Stack and reshape
4640
+ coords = np.stack(mesh, axis=-1).reshape(-1, len(shape))
4641
+ del mesh
4642
+
4643
+ if len(coords) < len(self.node_centroids):
4644
+ print(f"Warning: Only {len(coords)} positions available for {len(self.node_centroids)} labels")
4645
+
4646
+ new_centroids = {}
4647
+
4648
+ # Generate random indices without replacement
4649
+ available_count = min(len(coords), len(self.node_centroids))
4650
+ rand_indices = np.random.choice(len(coords), available_count, replace=False)
4651
+
4652
+ # Assign random positions to labels
4653
+ for i, label in enumerate(self.node_centroids.keys()):
4654
+ if i < len(rand_indices):
4655
+ centroid = coords[rand_indices[i]]
4656
+ new_centroids[label] = centroid
4657
+ z, y, x = centroid
4658
+ try:
4659
+ self.nodes[z, y, x] = label
4660
+ except:
4661
+ pass
4662
+
4663
+ # Update the centroids dictionary
4664
+ self.node_centroids = new_centroids
4665
+
4666
+ return self.node_centroids, self._nodes
4667
+
4668
+
4493
4669
  def community_id_info(self):
4494
4670
  def invert_dict(d):
4495
4671
  inverted = {}
@@ -1246,7 +1246,7 @@ class ImageViewerWindow(QMainWindow):
1246
1246
  except:
1247
1247
  pass
1248
1248
 
1249
- print(f"Found {len(filtered_df)} direct connections between nodes of ID {sort} and their neighbors (of any ID)")
1249
+ #print(f"Found {len(filtered_df)} direct connections between nodes of ID {sort} and their neighbors (of any ID)")
1250
1250
 
1251
1251
  if self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2] > self.mini_thresh:
1252
1252
  self.mini_overlay = True
@@ -2687,8 +2687,8 @@ class ImageViewerWindow(QMainWindow):
2687
2687
  degree_dist_action.triggered.connect(self.show_degree_dist_dialog)
2688
2688
  neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
2689
2689
  neighbor_id_action.triggered.connect(self.show_neighbor_id_dialog)
2690
- random_action = stats_menu.addAction("Generate Equivalent Random Network")
2691
- random_action.triggered.connect(self.show_random_dialog)
2690
+ ripley_action = stats_menu.addAction("Clustering Analysis")
2691
+ ripley_action.triggered.connect(self.show_ripley_dialog)
2692
2692
  vol_action = stats_menu.addAction("Calculate Volumes")
2693
2693
  vol_action.triggered.connect(self.volumes)
2694
2694
  rad_action = stats_menu.addAction("Calculate Radii")
@@ -2707,6 +2707,13 @@ class ImageViewerWindow(QMainWindow):
2707
2707
  id_code_action = overlay_menu.addAction("Code Identities")
2708
2708
  id_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Identity'))
2709
2709
 
2710
+ rand_menu = analysis_menu.addMenu("Randomize")
2711
+ random_action = rand_menu.addAction("Generate Equivalent Random Network")
2712
+ random_action.triggered.connect(self.show_random_dialog)
2713
+ random_nodes = rand_menu.addAction("Scramble Nodes (Centroids)")
2714
+ random_nodes.triggered.connect(self.show_randnode_dialog)
2715
+
2716
+
2710
2717
 
2711
2718
  # Process menu
2712
2719
  process_menu = menubar.addMenu("Process")
@@ -3501,8 +3508,8 @@ class ImageViewerWindow(QMainWindow):
3501
3508
  nii_img = nib.load(filename)
3502
3509
  # Get data and transpose to match TIFF orientation
3503
3510
  # If X needs to become Z, we move axis 2 (X) to position 0 (Z)
3504
- data = nii_img.get_fdata()
3505
- self.channel_data[channel_index] = np.transpose(data, (2, 1, 0))
3511
+ arraydata = nii_img.get_fdata()
3512
+ self.channel_data[channel_index] = np.transpose(arraydata, (2, 1, 0))
3506
3513
 
3507
3514
  elif file_extension in ['jpg', 'jpeg', 'png']:
3508
3515
  from PIL import Image
@@ -4157,10 +4164,18 @@ class ImageViewerWindow(QMainWindow):
4157
4164
  dialog = NeighborIdentityDialog(self)
4158
4165
  dialog.exec()
4159
4166
 
4167
+ def show_ripley_dialog(self):
4168
+ dialog = RipleyDialog(self)
4169
+ dialog.exec()
4170
+
4160
4171
  def show_random_dialog(self):
4161
4172
  dialog = RandomDialog(self)
4162
4173
  dialog.exec()
4163
4174
 
4175
+ def show_randnode_dialog(self):
4176
+ dialog = RandNodeDialog(self)
4177
+ dialog.exec()
4178
+
4164
4179
  def show_rad_dialog(self):
4165
4180
  dialog = RadDialog(self)
4166
4181
  dialog.exec()
@@ -5977,9 +5992,121 @@ class NeighborIdentityDialog(QDialog):
5977
5992
 
5978
5993
 
5979
5994
 
5995
+ class RipleyDialog(QDialog):
5996
+
5997
+ def __init__(self, parent=None):
5998
+
5999
+ super().__init__(parent)
6000
+ self.setWindowTitle(f"Find Ripley's H Function From Centroids")
6001
+ self.setModal(True)
6002
+
6003
+ layout = QFormLayout(self)
6004
+
6005
+ if my_network.node_identities is not None:
6006
+ self.root = QComboBox()
6007
+ self.root.addItems(list(set(my_network.node_identities.values())))
6008
+ self.root.setCurrentIndex(0)
6009
+ layout.addRow("Root Identity to Search for Neighbors", self.root)
6010
+ else:
6011
+ self.root = None
6012
+
6013
+ if my_network.node_identities is not None:
6014
+ self.targ = QComboBox()
6015
+ self.targ.addItems(list(set(my_network.node_identities.values())))
6016
+ self.targ.setCurrentIndex(0)
6017
+ layout.addRow("Targ Identity to be Searched For", self.targ)
6018
+ else:
6019
+ self.targ = None
6020
+
6021
+ self.distance = QLineEdit("5")
6022
+ layout.addRow("Bucket Distance for Searching For Clusters (automatically scaled by xy and z scales):", self.distance)
6023
+
6024
+
6025
+ self.proportion = QLineEdit("0.5")
6026
+ layout.addRow("Proportion of image to search? (0-1, high vals increase border artifacts): ", self.proportion)
6027
+
6028
+ self.edgecorrect = QPushButton("Border Correction")
6029
+ self.edgecorrect.setCheckable(True)
6030
+ self.edgecorrect.setChecked(False)
6031
+ layout.addRow("Use Border Correction (Extrapolate for points beyond the border):", self.edgecorrect)
6032
+
6033
+ self.ignore = QPushButton("Ignore Border Roots")
6034
+ self.ignore.setCheckable(True)
6035
+ self.ignore.setChecked(False)
6036
+ layout.addRow("Exclude Root Nodes Near Borders?:", self.ignore)
6037
+
6038
+ # Add Run button
6039
+ run_button = QPushButton("Get Ripley's H")
6040
+ run_button.clicked.connect(self.ripley)
6041
+ layout.addWidget(run_button)
6042
+
6043
+ def ripley(self):
6044
+
6045
+ try:
6046
+
6047
+ if my_network.node_centroids is None:
6048
+ self.parent().show_centroid_dialog()
6049
+
6050
+ try:
6051
+ root = self.root.currentText()
6052
+ except:
6053
+ root = None
6054
+
6055
+ try:
6056
+ targ = self.targ.currentText()
6057
+ except:
6058
+ targ = None
6059
+
6060
+ try:
6061
+ distance = float(self.distance.text())
6062
+ except:
6063
+ return
6064
+
6065
+
6066
+ try:
6067
+ proportion = abs(float(self.proportion.text()))
6068
+ except:
6069
+ proportion = 0.5
6070
+
6071
+ if proportion > 1 or proportion <= 0:
6072
+ print("Utilizing proportion = 0.5")
6073
+ proportion = 0.5
6074
+
6075
+
6076
+ edgecorrect = self.edgecorrect.isChecked()
6077
+
6078
+ ignore = self.ignore.isChecked()
6079
+
6080
+ if my_network.nodes is not None:
6081
+
6082
+ if my_network.nodes.shape[0] == 1:
6083
+ bounds = (np.array([0, 0]), np.array([my_network.nodes.shape[2], my_network.nodes.shape[1]]))
6084
+ else:
6085
+ bounds = (np.array([0, 0, 0]), np.array([my_network.nodes.shape[2], my_network.nodes.shape[1], my_network.nodes.shape[0]]))
6086
+ else:
6087
+ bounds = None
6088
+
6089
+ r_vals, k_vals, h_vals = my_network.get_ripley(root, targ, distance, edgecorrect, bounds, ignore, proportion)
6090
+
6091
+ k_dict = dict(zip(r_vals, k_vals))
6092
+ h_dict = dict(zip(r_vals, h_vals))
6093
+
6094
+
6095
+ self.parent().format_for_upperright_table(k_dict, metric='Radius (scaled)', value='L Value', title="Ripley's K")
6096
+ self.parent().format_for_upperright_table(h_dict, metric='Radius (scaled)', value='L Normed', title="Ripley's H")
5980
6097
 
5981
6098
 
6099
+ self.accept()
5982
6100
 
6101
+ except Exception as e:
6102
+ QMessageBox.critical(
6103
+ self,
6104
+ "Error:",
6105
+ f"Failed to preform cluster analysis: {str(e)}"
6106
+ )
6107
+ import traceback
6108
+ print(traceback.format_exc())
6109
+ print(f"Error: {e}")
5983
6110
 
5984
6111
  class RandomDialog(QDialog):
5985
6112
 
@@ -6019,6 +6146,79 @@ class RandomDialog(QDialog):
6019
6146
 
6020
6147
  self.accept()
6021
6148
 
6149
+ class RandNodeDialog(QDialog):
6150
+
6151
+ def __init__(self, parent=None):
6152
+
6153
+ super().__init__(parent)
6154
+ self.setWindowTitle("Random Node Parameters")
6155
+ self.setModal(True)
6156
+ layout = QFormLayout(self)
6157
+
6158
+
6159
+ self.mode = QComboBox()
6160
+ self.mode.addItems(["Anywhere", "Within Dimensional Bounds of Nodes", "Within Masked Bounds of Edges", "Within Masked Bounds of Overlay1", "Within Masked Bounds of Overlay2"])
6161
+ self.mode.setCurrentIndex(0)
6162
+ layout.addRow("Mode", self.mode)
6163
+
6164
+ # Add Run button
6165
+ run_button = QPushButton("Get Random Nodes (Will go in Nodes)")
6166
+ run_button.clicked.connect(self.random)
6167
+ layout.addWidget(run_button)
6168
+
6169
+ def random(self):
6170
+
6171
+ try:
6172
+
6173
+ if my_network.node_centroids is None:
6174
+ self.parent().show_centroid_dialog()
6175
+
6176
+ bounds = None
6177
+ mask = None
6178
+
6179
+ mode = self.mode.currentIndex()
6180
+
6181
+ if mode == 0 and not (my_network.nodes is None and my_network.edges is None and my_network.network_overlay is None and my_network.id_overlay is None):
6182
+ pass
6183
+ elif mode == 1 or (my_network.nodes is None and my_network.edges is None and my_network.network_overlay is None and my_network.id_overlay is None):
6184
+ print("HELLO")
6185
+ # Convert string labels to integers if necessary
6186
+ if any(isinstance(k, str) for k in my_network.node_centroids.keys()):
6187
+ label_map = {label: idx for idx, label in enumerate(my_network.node_centroids.keys())}
6188
+ my_network.node_centroids = {label_map[k]: v for k, v in my_network.node_centroids.items()}
6189
+
6190
+ # Convert centroids to array and keep track of labels
6191
+ labels = np.array(list(my_network.node_centroids.keys()), dtype=np.uint32)
6192
+ centroid_points = np.array([my_network.node_centroids[label] for label in labels])
6193
+
6194
+ # Calculate shape if not provided
6195
+ max_coords = centroid_points.max(axis=0)
6196
+ max_shape = tuple(max_coord + 1 for max_coord in max_coords)
6197
+ min_coords = centroid_points.min(axis=0)
6198
+ min_shape = tuple(min_coord + 1 for min_coord in min_coords)
6199
+ bounds = (min_shape, max_shape)
6200
+ else:
6201
+ mask = n3d.binarize(self.parent().channel_data[mode - 1])
6202
+
6203
+ centroids, array = my_network.random_nodes(bounds = bounds, mask = mask)
6204
+
6205
+ if my_network.nodes is not None:
6206
+ try:
6207
+ self.parent().load_channel(0, array, data = True)
6208
+ except:
6209
+ pass
6210
+
6211
+ self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
6212
+
6213
+ except Exception as e:
6214
+ QMessageBox.critical(
6215
+ self,
6216
+ "Error:",
6217
+ f"Failed to randomize: {str(e)}"
6218
+ )
6219
+ print(f"Error: {e}")
6220
+
6221
+
6022
6222
  class RadDialog(QDialog):
6023
6223
 
6024
6224
  def __init__(self, parent=None):
@@ -7208,8 +7408,14 @@ class MachineWindow(QMainWindow):
7208
7408
  train_quick.clicked.connect(lambda: self.train_model(speed=True))
7209
7409
  train_detailed = QPushButton("Train More Detailed Model")
7210
7410
  train_detailed.clicked.connect(lambda: self.train_model(speed=False))
7411
+ save = QPushButton("Save Model")
7412
+ save.clicked.connect(self.save_model)
7413
+ load = QPushButton("Load Model")
7414
+ load.clicked.connect(self.load_model)
7211
7415
  training_layout.addWidget(train_quick)
7212
7416
  training_layout.addWidget(train_detailed)
7417
+ training_layout.addWidget(save)
7418
+ training_layout.addWidget(load)
7213
7419
  training_group.setLayout(training_layout)
7214
7420
 
7215
7421
  # Group 4: Segmentation Options
@@ -7280,6 +7486,33 @@ class MachineWindow(QMainWindow):
7280
7486
  except:
7281
7487
  pass
7282
7488
 
7489
+ def save_model(self):
7490
+
7491
+ filename, _ = QFileDialog.getSaveFileName(
7492
+ self,
7493
+ f"Save Model As",
7494
+ "", # Default directory
7495
+ "numpy data (*.npz);;All Files (*)" # File type filter
7496
+ )
7497
+
7498
+ if filename: # Only proceed if user didn't cancel
7499
+ # If user didn't type an extension, add .tif
7500
+ if not filename.endswith(('.npz')):
7501
+ filename += '.npz'
7502
+
7503
+ self.segmenter.save_model(filename, self.parent().channel_data[2])
7504
+
7505
+ def load_model(self):
7506
+
7507
+ filename, _ = QFileDialog.getOpenFileName(
7508
+ self,
7509
+ f"Load Model",
7510
+ "",
7511
+ "numpy data (*.npz)"
7512
+ )
7513
+
7514
+ self.segmenter.load_model(filename)
7515
+ self.trained = True
7283
7516
 
7284
7517
  def toggle_two(self):
7285
7518
  if self.two.isChecked():