nettracer3d 0.9.4__py3-none-any.whl → 0.9.6__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/nettracer.py CHANGED
@@ -1267,31 +1267,6 @@ def dilate_2D(array, search, scaling = 1):
1267
1267
 
1268
1268
  return inv
1269
1269
 
1270
- def erode_2D(array, search, scaling=1):
1271
- """
1272
- Erode a 2D array using distance transform method.
1273
-
1274
- Parameters:
1275
- array -- Input 2D binary array
1276
- search -- Distance within which to erode
1277
- scaling -- Scaling factor (default: 1)
1278
-
1279
- Returns:
1280
- Eroded 2D array
1281
- """
1282
- # For erosion, we work directly with the foreground
1283
- # No need to invert the array
1284
-
1285
- # Compute distance transform on the foreground
1286
- dt = smart_dilate.compute_distance_transform_distance(array)
1287
-
1288
- # Apply scaling
1289
- dt = dt * scaling
1290
-
1291
- # Threshold to keep only points that are at least 'search' distance from the boundary
1292
- eroded = dt >= search
1293
-
1294
- return eroded
1295
1270
 
1296
1271
  def dilate_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0):
1297
1272
  """
@@ -1353,7 +1328,42 @@ def dilate_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0):
1353
1328
 
1354
1329
  return inv.astype(np.uint8)
1355
1330
 
1356
- def erode_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0):
1331
+ def erode_2D(array, search, scaling=1, preserve_labels = False):
1332
+ """
1333
+ Erode a 2D array using distance transform method.
1334
+
1335
+ Parameters:
1336
+ array -- Input 2D binary array
1337
+ search -- Distance within which to erode
1338
+ scaling -- Scaling factor (default: 1)
1339
+
1340
+ Returns:
1341
+ Eroded 2D array
1342
+ """
1343
+ # For erosion, we work directly with the foreground
1344
+ # No need to invert the array
1345
+
1346
+ if preserve_labels:
1347
+ from skimage.segmentation import find_boundaries
1348
+ borders = find_boundaries(array, mode='thick')
1349
+ mask = array * invert_array(borders)
1350
+ mask = smart_dilate.compute_distance_transform_distance(mask)
1351
+ mask = mask * scaling
1352
+ mask = mask >= search
1353
+ array = mask * array
1354
+ else:
1355
+ # Compute distance transform on the foreground
1356
+ dt = smart_dilate.compute_distance_transform_distance(array)
1357
+
1358
+ # Apply scaling
1359
+ dt = dt * scaling
1360
+
1361
+ # Threshold to keep only points that are at least 'search' distance from the boundary
1362
+ array = dt > search
1363
+
1364
+ return array
1365
+
1366
+ def erode_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0, preserve_labels = False):
1357
1367
  """
1358
1368
  Erode a 3D array using distance transform method. DT erosion produces perfect results
1359
1369
  with Euclidean geometry, but may be slower for large arrays.
@@ -1371,43 +1381,24 @@ def erode_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0):
1371
1381
 
1372
1382
  if array.shape[0] == 1:
1373
1383
  # Handle 2D case
1374
- return erode_2D(array, search_distance, scaling=xy_scaling)
1384
+ return erode_2D(array, search_distance, scaling=xy_scaling, preserve_labels = True)
1375
1385
 
1376
- # For erosion, we work directly with the foreground (no inversion needed)
1377
-
1378
- """
1379
- # Determine which dimension needs resampling
1380
- if (z_scaling > xy_scaling):
1381
- # Z dimension needs to be stretched
1382
- zoom_factor = [z_scaling/xy_scaling, 1, 1] # Scale factor for [z, y, x]
1383
- rev_factor = [xy_scaling/z_scaling, 1, 1]
1384
- cardinal = xy_scaling
1385
- elif (xy_scaling > z_scaling):
1386
- # XY dimensions need to be stretched
1387
- zoom_factor = [1, xy_scaling/z_scaling, xy_scaling/z_scaling] # Scale factor for [z, y, x]
1388
- rev_factor = [1, z_scaling/xy_scaling, z_scaling/xy_scaling] # Scale factor for [z, y, x]
1389
- cardinal = z_scaling
1390
- else:
1391
- # Already uniform scaling, no need to resample
1392
- zoom_factor = None
1393
- rev_factor = None
1394
- cardinal = xy_scaling
1395
-
1396
- # Resample the mask if needed
1397
- if zoom_factor:
1398
- array = ndimage.zoom(array, zoom_factor, order=0) # Use order=0 for binary masks
1399
- """
1400
-
1401
- print("Computing a distance transform for a perfect erosion...")
1402
1386
 
1403
- array = smart_dilate.compute_distance_transform_distance(array, sampling = [z_scaling, xy_scaling, xy_scaling])
1404
-
1405
- # Apply scaling factor
1406
- #array = array * cardinal
1407
-
1408
- # Threshold the distance transform to get eroded result
1409
- # For erosion, we keep only the points that are at least search_distance from the boundary
1410
- array = array >= search_distance
1387
+ if preserve_labels:
1388
+
1389
+
1390
+ from skimage.segmentation import find_boundaries
1391
+
1392
+ borders = find_boundaries(array, mode='thick')
1393
+ mask = array * invert_array(borders)
1394
+ mask = smart_dilate.compute_distance_transform_distance(mask, sampling = [z_scaling, xy_scaling, xy_scaling])
1395
+ mask = mask >= search_distance
1396
+ array = mask * array
1397
+ else:
1398
+ array = smart_dilate.compute_distance_transform_distance(array, sampling = [z_scaling, xy_scaling, xy_scaling])
1399
+ # Threshold the distance transform to get eroded result
1400
+ # For erosion, we keep only the points that are at least search_distance from the boundary
1401
+ array = array > search_distance
1411
1402
 
1412
1403
  # Resample back to original dimensions if needed
1413
1404
  #if rev_factor:
@@ -1574,7 +1565,7 @@ def dilate_3D_old(tiff_array, dilated_x=3, dilated_y=3, dilated_z=3):
1574
1565
 
1575
1566
 
1576
1567
  def erode_3D(tiff_array, eroded_x, eroded_y, eroded_z):
1577
- """Internal method to erode an array in 3D. Erosion this way is much faster than using a distance transform although the latter is theoretically more accurate.
1568
+ """Internal method to erode an array in 3D. Erosion this way is faster than using a distance transform although the latter is theoretically more accurate.
1578
1569
  Arguments are an array, and the desired pixel erosion amounts in X, Y, Z."""
1579
1570
 
1580
1571
  if tiff_array.shape[0] == 1:
@@ -1744,54 +1735,6 @@ def combine_edges(edge_labels_1, edge_labels_2):
1744
1735
 
1745
1736
  return np.where(mask, offset_labels, edge_labels_1)
1746
1737
 
1747
- def combine_nodes(root_nodes, other_nodes, other_ID, identity_dict, root_ID = None):
1748
-
1749
- """Internal method to merge two labelled node arrays into one"""
1750
-
1751
- print("Combining node arrays")
1752
-
1753
- mask = (root_nodes == 0) & (other_nodes > 0)
1754
- if np.any(mask):
1755
- max_val = np.max(root_nodes)
1756
- other_nodes[:] = np.where(mask, other_nodes + max_val, 0)
1757
-
1758
- if root_ID is not None:
1759
- rootIDs = list(np.unique(root_nodes)) #Sets up adding these vals to the identitiy dictionary. Gets skipped if this has already been done.
1760
-
1761
- if rootIDs[0] == 0: #np unique can include 0 which we don't want.
1762
- del rootIDs[0]
1763
-
1764
- otherIDs = list(np.unique(other_nodes)) #Sets up adding other vals to the identity dictionary.
1765
-
1766
- if otherIDs[0] == 0:
1767
- del otherIDs[0]
1768
-
1769
- if root_ID is not None: #Adds the root vals to the dictionary if it hasn't already
1770
-
1771
- if other_ID.endswith('.tiff'):
1772
- other_ID = other_ID[:-5]
1773
- elif other_ID.endswith('.tif'):
1774
- other_ID = other_ID[:-4]
1775
-
1776
- for item in rootIDs:
1777
- identity_dict[item] = root_ID
1778
-
1779
- for item in otherIDs: #Always adds the other vals to the dictionary
1780
- try:
1781
- other_ID = os.path.basename(other_ID)
1782
- except:
1783
- pass
1784
- if other_ID.endswith('.tiff'):
1785
- other_ID = other_ID[:-5]
1786
- elif other_ID.endswith('.tif'):
1787
- other_ID = other_ID[:-4]
1788
-
1789
- identity_dict[item] = other_ID
1790
-
1791
- nodes = root_nodes + other_nodes #Combine the outer edges with the inner edges modified via the above steps
1792
-
1793
- return nodes, identity_dict
1794
-
1795
1738
  def directory_info(directory = None):
1796
1739
  """Internal method to get the files in a directory, optionally the current directory if nothing passed"""
1797
1740
 
@@ -2124,15 +2067,15 @@ def dilate(arrayimage, amount, xy_scale = 1, z_scale = 1, directory = None, fast
2124
2067
 
2125
2068
  return arrayimage
2126
2069
 
2127
- def erode(arrayimage, amount, xy_scale = 1, z_scale = 1, mode = 0):
2128
- if len(np.unique(arrayimage)) > 2: #binarize
2070
+ def erode(arrayimage, amount, xy_scale = 1, z_scale = 1, mode = 0, preserve_labels = False):
2071
+ if not preserve_labels and len(np.unique(arrayimage)) > 2: #binarize
2129
2072
  arrayimage = binarize(arrayimage)
2130
2073
  erode_xy, erode_z = dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
2131
2074
 
2132
2075
  if mode == 0:
2133
2076
  arrayimage = (erode_3D(arrayimage, erode_xy, erode_xy, erode_z)) * 255
2134
2077
  else:
2135
- arrayimage = erode_3D_dt(arrayimage, amount, xy_scaling=xy_scale, z_scaling=z_scale)
2078
+ arrayimage = erode_3D_dt(arrayimage, amount, xy_scaling=xy_scale, z_scaling=z_scale, preserve_labels = preserve_labels)
2136
2079
 
2137
2080
  if np.max(arrayimage) == 1:
2138
2081
  arrayimage = arrayimage * 255
@@ -3847,7 +3790,7 @@ class Network_3D:
3847
3790
 
3848
3791
  self._search_region = self._nodes
3849
3792
 
3850
- def calculate_edges(self, binary_edges, diledge = None, inners = True, hash_inner_edges = True, search = None, remove_edgetrunk = 0, GPU = True, fast_dil = False, skeletonized = False):
3793
+ def calculate_edges(self, binary_edges, diledge = None, inners = True, search = None, remove_edgetrunk = 0, GPU = True, fast_dil = False, skeletonized = False):
3851
3794
  """
3852
3795
  Method to calculate the edges that are used to directly connect nodes. May be done with or without the search region, however using search_region is recommended.
3853
3796
  The search_region property must be set to use the search region, otherwise the nodes property must be set. Sets the edges property
@@ -3856,7 +3799,6 @@ class Network_3D:
3856
3799
  so some amount of dilation is recommended if there are any, but not so much to create overconnectivity. This is a value that needs to be tuned by the user.
3857
3800
  :param inners: (Optional - Val = True; boolean). Will use inner edges if True, will not if False. Inner edges are parts of the edge mask that exist within search regions. If search regions overlap,
3858
3801
  any edges that exist within the overlap will only assert connectivity if 'inners' is True.
3859
- :param hash_inner_edges: (Optional - Val = True; boolean). If False, all search regions that contain an edge object connecting multiple nodes will be assigned as connected.
3860
3802
  If True, an extra processing step is used to sort the correct connectivity amongst these search_regions. Can only be computed when search_regions property is set.
3861
3803
  :param search: (Optional - Val = None; int). Amount for nodes to search for connections, assuming the search_regions are not being used. Assigning a value to this param will utilize the secondary algorithm and not the search_regions.
3862
3804
  :param remove_edgetrunk: (Optional - Val = 0; int). Amount of times to remove the 'Trunk' from the edges. A trunk in this case is the largest (by vol) edge object remaining after nodes have broken up the edges.
@@ -3909,11 +3851,7 @@ class Network_3D:
3909
3851
  labelled_edges, num_edge = label_objects(outer_edges)
3910
3852
 
3911
3853
  if inners:
3912
-
3913
- if search is None and hash_inner_edges is True:
3914
- inner_edges = hash_inners(self._search_region, binary_edges, GPU = GPU)
3915
- else:
3916
- inner_edges = establish_inner_edges(search_region, binary_edges)
3854
+ inner_edges = hash_inners(self._search_region, binary_edges, GPU = GPU)
3917
3855
 
3918
3856
  del binary_edges
3919
3857
 
@@ -3939,7 +3877,58 @@ class Network_3D:
3939
3877
  """
3940
3878
  self._nodes, num_nodes = label_objects(nodes, structure_3d)
3941
3879
 
3942
- def merge_nodes(self, addn_nodes_name, label_nodes = True, root_id = "Root_Nodes"):
3880
+ def combine_nodes(self, root_nodes, other_nodes, other_ID, identity_dict, root_ID = None, centroids = False):
3881
+
3882
+ """Internal method to merge two labelled node arrays into one"""
3883
+
3884
+ print("Combining node arrays")
3885
+
3886
+ mask = (root_nodes == 0) & (other_nodes > 0)
3887
+ if np.any(mask):
3888
+ max_val = np.max(root_nodes)
3889
+ other_nodes[:] = np.where(mask, other_nodes + max_val, 0)
3890
+ if centroids:
3891
+ new_dict = network_analysis._find_centroids(other_nodes)
3892
+ self.node_centroids.update(new_dict)
3893
+
3894
+ if root_ID is not None:
3895
+ rootIDs = list(np.unique(root_nodes)) #Sets up adding these vals to the identitiy dictionary. Gets skipped if this has already been done.
3896
+
3897
+ if rootIDs[0] == 0: #np unique can include 0 which we don't want.
3898
+ del rootIDs[0]
3899
+
3900
+ otherIDs = list(np.unique(other_nodes)) #Sets up adding other vals to the identity dictionary.
3901
+
3902
+ if otherIDs[0] == 0:
3903
+ del otherIDs[0]
3904
+
3905
+ if root_ID is not None: #Adds the root vals to the dictionary if it hasn't already
3906
+
3907
+ if other_ID.endswith('.tiff'):
3908
+ other_ID = other_ID[:-5]
3909
+ elif other_ID.endswith('.tif'):
3910
+ other_ID = other_ID[:-4]
3911
+
3912
+ for item in rootIDs:
3913
+ identity_dict[item] = root_ID
3914
+
3915
+ for item in otherIDs: #Always adds the other vals to the dictionary
3916
+ try:
3917
+ other_ID = os.path.basename(other_ID)
3918
+ except:
3919
+ pass
3920
+ if other_ID.endswith('.tiff'):
3921
+ other_ID = other_ID[:-5]
3922
+ elif other_ID.endswith('.tif'):
3923
+ other_ID = other_ID[:-4]
3924
+
3925
+ identity_dict[item] = other_ID
3926
+
3927
+ nodes = root_nodes + other_nodes #Combine the outer edges with the inner edges modified via the above steps
3928
+
3929
+ return nodes, identity_dict
3930
+
3931
+ def merge_nodes(self, addn_nodes_name, label_nodes = True, root_id = "Root_Nodes", centroids = False):
3943
3932
  """
3944
3933
  Merges the self._nodes attribute with alternate labelled node images. The alternate nodes can be inputted as a string for a filepath to a tif,
3945
3934
  or as a directory address containing only tif images, which will merge the _nodes attribute with all tifs in the folder. The _node_identities attribute
@@ -3959,16 +3948,20 @@ class Network_3D:
3959
3948
 
3960
3949
  identity_dict = {} #A dictionary to deliniate the node identities
3961
3950
 
3951
+ if centroids:
3952
+ self.node_centroids = network_analysis._find_centroids(self._nodes)
3953
+
3954
+
3962
3955
  try: #Try presumes the input is a tif
3963
3956
  addn_nodes = tifffile.imread(addn_nodes_name) #If not this will fail and activate the except block
3964
3957
 
3965
3958
  if label_nodes is True:
3966
3959
  addn_nodes, num_nodes2 = label_objects(addn_nodes) # Label the node objects. Note this presumes no overlap between node masks.
3967
- node_labels, identity_dict = combine_nodes(self._nodes, addn_nodes, addn_nodes_name, identity_dict, nodes_name) #This method stacks labelled arrays
3960
+ node_labels, identity_dict = self.combine_nodes(self._nodes, addn_nodes, addn_nodes_name, identity_dict, nodes_name, centroids = centroids) #This method stacks labelled arrays
3968
3961
  num_nodes = np.max(node_labels)
3969
3962
 
3970
3963
  else: #If nodes already labelled
3971
- node_labels, identity_dict = combine_nodes(self._nodes, addn_nodes, addn_nodes_name, identity_dict, nodes_name)
3964
+ node_labels, identity_dict = self.combine_nodes(self._nodes, addn_nodes, addn_nodes_name, identity_dict, nodes_name, centroids = centroids)
3972
3965
  num_nodes = int(np.max(node_labels))
3973
3966
 
3974
3967
  except: #Exception presumes the input is a directory containing multiple tifs, to allow multi-node stackage.
@@ -3986,16 +3979,15 @@ class Network_3D:
3986
3979
  if label_nodes is True:
3987
3980
  addn_nodes, num_nodes2 = label_objects(addn_nodes) # Label the node objects. Note this presumes no overlap between node masks.
3988
3981
  if i == 0:
3989
- node_labels, identity_dict = combine_nodes(self._nodes, addn_nodes, addn_nodes_ID, identity_dict, nodes_name)
3990
-
3982
+ node_labels, identity_dict = self.combine_nodes(self._nodes, addn_nodes, addn_nodes_ID, identity_dict, nodes_name, centroids = centroids)
3991
3983
  else:
3992
- node_labels, identity_dict = combine_nodes(node_labels, addn_nodes, addn_nodes_ID, identity_dict)
3984
+ node_labels, identity_dict = self.combine_nodes(node_labels, addn_nodes, addn_nodes_ID, identity_dict, centroids = centroids)
3993
3985
 
3994
3986
  else:
3995
3987
  if i == 0:
3996
- node_labels, identity_dict = combine_nodes(self._nodes, addn_nodes, addn_nodes_ID, identity_dict, nodes_name)
3988
+ node_labels, identity_dict = self.combine_nodes(self._nodes, addn_nodes, addn_nodes_ID, identity_dict, nodes_name, centroids = centroids)
3997
3989
  else:
3998
- node_labels, identity_dict = combine_nodes(node_labels, addn_nodes, addn_nodes_ID, identity_dict)
3990
+ node_labels, identity_dict = self.combine_nodes(node_labels, addn_nodes, addn_nodes_ID, identity_dict, centroids = centroids)
3999
3991
  except Exception as e:
4000
3992
  print("Could not open additional nodes, verify they are being inputted correctly...")
4001
3993
 
@@ -4045,7 +4037,7 @@ class Network_3D:
4045
4037
  self._network_lists = network_analysis.read_excel_to_lists(df)
4046
4038
  self._network, net_weights = network_analysis.weighted_network(df)
4047
4039
 
4048
- def calculate_all(self, nodes, edges, xy_scale = 1, z_scale = 1, down_factor = None, search = None, diledge = None, inners = True, hash_inners = True, remove_trunk = 0, ignore_search_region = False, other_nodes = None, label_nodes = True, directory = None, GPU = True, fast_dil = True, skeletonize = False, GPU_downsample = None):
4040
+ def calculate_all(self, nodes, edges, xy_scale = 1, z_scale = 1, down_factor = None, search = None, diledge = None, inners = True, remove_trunk = 0, ignore_search_region = False, other_nodes = None, label_nodes = True, directory = None, GPU = True, fast_dil = True, skeletonize = False, GPU_downsample = None):
4049
4041
  """
4050
4042
  Method to calculate and save to mem all properties of a Network_3D object. In general, after initializing a Network_3D object, this method should be called on the node and edge masks that will be used to calculate the network.
4051
4043
  :param nodes: (Mandatory; String or ndarray). Filepath to segmented nodes mask or a numpy array containing the same.
@@ -4058,7 +4050,6 @@ class Network_3D:
4058
4050
  so some amount of dilation is recommended if there are any, but not so much to create overconnectivity. This is a value that needs to be tuned by the user.
4059
4051
  :param inners: (Optional - Val = True; boolean). Will use inner edges if True, will not if False. Inner edges are parts of the edge mask that exist within search regions. If search regions overlap,
4060
4052
  any edges that exist within the overlap will only assert connectivity if 'inners' is True.
4061
- :param hash_inners: (Optional - Val = True; boolean). If False, all search regions that contain an edge object connecting multiple nodes will be assigned as connected.
4062
4053
  If True, an extra processing step is used to sort the correct connectivity amongst these search_regions. Can only be computed when search_regions property is set.
4063
4054
  :param remove_trunk: (Optional - Val = 0; int). Amount of times to remove the 'Trunk' from the edges. A trunk in this case is the largest (by vol) edge object remaining after nodes have broken up the edges.
4064
4055
  Any 'Trunks' removed will be absent for connection calculations.
@@ -4120,7 +4111,7 @@ class Network_3D:
4120
4111
  except:
4121
4112
  pass
4122
4113
 
4123
- self.calculate_edges(edges, diledge = diledge, inners = inners, hash_inner_edges = hash_inners, search = search, remove_edgetrunk = remove_trunk, GPU = GPU, fast_dil = fast_dil, skeletonized = skeletonize) #Will have to be moved out if the second method becomes more directly implemented
4114
+ self.calculate_edges(edges, diledge = diledge, inners = inners, search = search, remove_edgetrunk = remove_trunk, GPU = GPU, fast_dil = fast_dil, skeletonized = skeletonize) #Will have to be moved out if the second method becomes more directly implemented
4124
4115
  else:
4125
4116
  self._edges, _ = label_objects(edges)
4126
4117
 
@@ -5104,13 +5095,16 @@ class Network_3D:
5104
5095
 
5105
5096
 
5106
5097
  for node in G.nodes():
5107
- nodeid = node_identities[node]
5108
- neighbors = list(G.neighbors(node))
5109
- for subnode in neighbors:
5110
- subnodeid = node_identities[subnode]
5111
- if subnodeid == root:
5112
- neighborhood_dict[nodeid] += 1
5113
- break
5098
+ try:
5099
+ nodeid = node_identities[node]
5100
+ neighbors = list(G.neighbors(node))
5101
+ for subnode in neighbors:
5102
+ subnodeid = node_identities[subnode]
5103
+ if subnodeid == root:
5104
+ neighborhood_dict[nodeid] += 1
5105
+ break
5106
+ except:
5107
+ pass
5114
5108
 
5115
5109
  title1 = f'Neighborhood Distribution of Nodes in Network from Nodes: {root}'
5116
5110
  title2 = f'Neighborhood Distribution of Nodes in Network from Nodes {root} as a proportion of total nodes of that ID'
@@ -5219,34 +5213,22 @@ class Network_3D:
5219
5213
  bounds = (min_coords, max_coords)
5220
5214
  dim_list = max_coords - min_coords
5221
5215
 
5222
-
5223
- if dim == 3:
5224
-
5225
- """
5226
- if self.xy_scale > self.z_scale: # xy will be 'expanded' more so its components will be arbitrarily further from the border than z ones
5227
- factor_xy = (self.z_scale/self.xy_scale) * factor # So 'factor' in the xy dim has to get smaller
5228
- factor_z = factor
5229
- elif self.z_scale > self.xy_scale: # Same idea
5230
- factor_z = (self.xy_scale/self.z_scale) * factor
5231
- factor_xy = factor
5232
- else:
5233
- factor_z = factor
5234
- factor_xy = factor
5235
- """
5236
-
5237
- for centroid in roots:
5238
-
5239
- 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):
5216
+ for centroid in roots:
5217
+ # Assuming centroid is [z, y, x] based on your indexing
5218
+ z, y, x = centroid[0], centroid[1], centroid[2]
5219
+
5220
+ # Check x-dimension
5221
+ x_ok = (x - min_coords[0]) > dim_list[0] * factor and (max_coords[0] - x) > dim_list[0] * factor
5222
+ # Check y-dimension
5223
+ y_ok = (y - min_coords[1]) > dim_list[1] * factor and (max_coords[1] - y) > dim_list[1] * factor
5224
+
5225
+ if dim == 3: # 3D case
5226
+ # Check z-dimension
5227
+ z_ok = (z - min_coords[2]) > dim_list[2] * factor and (max_coords[2] - z) > dim_list[2] * factor
5228
+ if x_ok and y_ok and z_ok:
5240
5229
  new_list.append(centroid)
5241
-
5242
-
5243
- #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):
5244
- #new_list.append(centroid)
5245
- #print(f"dim_list: {dim_list}, centroid: {centroid}, min_coords: {min_coords}, max_coords: {max_coords}")
5246
- else:
5247
- for centroid in roots:
5248
-
5249
- 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):
5230
+ else: # 2D case
5231
+ if x_ok and y_ok:
5250
5232
  new_list.append(centroid)
5251
5233
 
5252
5234
  else:
@@ -5299,8 +5281,6 @@ class Network_3D:
5299
5281
  print(f"Utilizing {len(roots)} root points. Note that low n values are unstable.")
5300
5282
  is_subset = True
5301
5283
 
5302
-
5303
-
5304
5284
  roots = proximity.convert_centroids_to_array(roots, xy_scale = self.xy_scale, z_scale = self.z_scale)
5305
5285
 
5306
5286
  n_subset = len(targs)
@@ -5531,55 +5511,42 @@ class Network_3D:
5531
5511
  neighborhoods.visualize_cluster_composition_umap(self.node_centroids, None, id_dictionary = self.node_identities, graph_label = "Node ID", title = 'UMAP Visualization of Node Centroids')
5532
5512
 
5533
5513
 
5534
-
5535
- def identity_umap(self):
5514
+ def identity_umap(self, data):
5536
5515
 
5537
5516
  try:
5538
5517
 
5539
- id_set = iden_set(self.node_identities.values())
5540
-
5541
- template = np.zeros(len(id_set))
5518
+ neighbor_classes = {}
5519
+ import random
5542
5520
 
5543
- id_dict = {}
5544
- for i, iden in enumerate(id_set):
5545
- id_dict[iden] = i
5521
+ umap_dict = copy.deepcopy(data)
5546
5522
 
5547
- umap_dict = {}
5523
+ for item in data.keys():
5524
+ if item in self.node_identities:
5525
+ try:
5526
+ parse = ast.literal_eval(self.node_identities[item])
5527
+ neighbor_classes[item] = random.choice(parse)
5528
+ except:
5529
+ neighbor_classes[item] = self.node_identities[item]
5548
5530
 
5549
- for node in self.node_identities.keys():
5550
- umap_dict[node] = copy.deepcopy(template)
5551
- try:
5552
- idens = ast.literal_eval(self.node_identities[node])
5553
- for iden in idens:
5554
- index = id_dict[iden]
5555
- ref = umap_dict[node]
5556
- ref[index] = 1
5557
- umap_dict[node] = ref
5558
- except:
5559
- index = id_dict[self.node_identities[node]]
5560
- ref = umap_dict[node]
5561
- ref[index] = 1
5562
- umap_dict[node] = ref
5531
+ else:
5532
+ del umap_dict[item]
5563
5533
 
5564
- neighbor_classes = {}
5565
- import random
5534
+ from scipy.stats import zscore
5566
5535
 
5567
- for node, iden in self.node_identities.items():
5568
- try:
5569
- idens = ast.literal_eval(iden)
5570
- neighbor_classes[node] = random.choice(idens)
5571
- except:
5572
- neighbor_classes[node] = iden
5536
+ # Z-score normalize each marker (column)
5537
+ for key in umap_dict:
5538
+ umap_dict[key] = zscore(umap_dict[key])
5573
5539
 
5574
5540
 
5575
5541
  from . import neighborhoods
5576
5542
 
5577
- neighborhoods.visualize_cluster_composition_umap(umap_dict, None, id_dictionary = neighbor_classes, graph_label = "Node ID", title = 'UMAP Visualization of Node Identities', draw_lines = True)
5543
+ neighborhoods.visualize_cluster_composition_umap(umap_dict, None, id_dictionary = neighbor_classes, graph_label = "Node ID", title = 'UMAP Visualization of Node Identities by Z-Score')
5578
5544
 
5579
5545
  except Exception as e:
5546
+ import traceback
5547
+ print(traceback.format_exc())
5580
5548
  print(f"Error: {e}")
5581
5549
 
5582
-
5583
5550
  def community_id_info_per_com(self, umap = False, label = 0, limit = 0, proportional = False, neighbors = None):
5584
5551
 
5585
5552
  community_dict = invert_dict(self.communities)
@@ -5608,7 +5575,10 @@ class Network_3D:
5608
5575
  for iden in idens:
5609
5576
  counter[id_dict[iden]] += 1
5610
5577
  except:
5611
- counter[id_dict[self.node_identities[node]]] += 1 # Keep them as arrays
5578
+ try:
5579
+ counter[id_dict[self.node_identities[node]]] += 1 # Keep them as arrays
5580
+ except:
5581
+ pass
5612
5582
 
5613
5583
  for i in range(len(counter)): # Translate them into proportions out of 1
5614
5584
 
@@ -5645,7 +5615,10 @@ class Network_3D:
5645
5615
  for iden in idents:
5646
5616
  iden_tracker[iden] += 1
5647
5617
  except:
5648
- iden_tracker[self.node_identities[node]] += 1
5618
+ try:
5619
+ iden_tracker[self.node_identities[node]] += 1
5620
+ except:
5621
+ pass
5649
5622
 
5650
5623
  i = 0
5651
5624
 
@@ -5915,6 +5888,23 @@ class Network_3D:
5915
5888
  overlay = neighborhoods.create_community_heatmap(heat_dict, self.communities, self.node_centroids, shape = shape, is_3d=is3d, labeled_array = self.nodes)
5916
5889
  return heat_dict, overlay
5917
5890
 
5891
+ def get_merge_node_dictionaries(self, path, data):
5892
+
5893
+ img_list = directory_info(path)
5894
+ id_dicts = []
5895
+ num_nodes = np.max(data)
5896
+
5897
+ for i, img in enumerate(img_list):
5898
+ if img.endswith('.tiff') or img.endswith('.tif'):
5899
+ print(f"Processing image {img}")
5900
+ mask = tifffile.imread(f'{path}/{img}')
5901
+ if len(mask.shape) == 2:
5902
+ mask = np.expand_dims(mask, axis = 0)
5903
+
5904
+ id_dict = proximity.create_node_dictionary_id(data, mask, num_nodes)
5905
+ id_dicts.append(id_dict)
5906
+
5907
+ return id_dicts
5918
5908
 
5919
5909
  def merge_node_ids(self, path, data, include = True):
5920
5910
 
@@ -5940,46 +5930,49 @@ class Network_3D:
5940
5930
  img_list = directory_info(path)
5941
5931
 
5942
5932
  for i, img in enumerate(img_list):
5943
- mask = tifffile.imread(f'{path}/{img}')
5944
5933
 
5945
- if len(np.unique(mask)) != 2:
5934
+ if img.endswith('.tiff') or img.endswith('.tif'):
5946
5935
 
5947
- mask = otsu_binarize(mask)
5948
- else:
5949
- mask = mask != 0
5936
+ mask = tifffile.imread(f'{path}/{img}')
5950
5937
 
5951
- nodes = data * mask
5952
- nodes = np.unique(nodes)
5953
- nodes = nodes.tolist()
5954
- if 0 in nodes:
5955
- del nodes[0]
5938
+ if len(np.unique(mask)) != 2:
5956
5939
 
5957
- if img.endswith('.tiff'):
5958
- base_name = img[:-5]
5959
- elif img.endswith('.tif'):
5960
- base_name = img[:-4]
5961
- else:
5962
- base_name = img
5940
+ mask = otsu_binarize(mask)
5941
+ else:
5942
+ mask = mask != 0
5943
+
5944
+ nodes = data * mask
5945
+ nodes = np.unique(nodes)
5946
+ nodes = nodes.tolist()
5947
+ if 0 in nodes:
5948
+ del nodes[0]
5949
+
5950
+ if img.endswith('.tiff'):
5951
+ base_name = img[:-5]
5952
+ elif img.endswith('.tif'):
5953
+ base_name = img[:-4]
5954
+ else:
5955
+ base_name = img
5963
5956
 
5964
- assigned = {}
5957
+ assigned = {}
5965
5958
 
5966
5959
 
5967
- for node in self.node_identities.keys():
5960
+ for node in self.node_identities.keys():
5968
5961
 
5969
- try:
5962
+ try:
5970
5963
 
5971
- if int(node) in nodes:
5964
+ if int(node) in nodes:
5972
5965
 
5973
- self.node_identities[node].append(f'{base_name}+')
5966
+ self.node_identities[node].append(f'{base_name}+')
5974
5967
 
5975
- elif include:
5968
+ elif include:
5976
5969
 
5977
- self.node_identities[node].append(f'{base_name}-')
5970
+ self.node_identities[node].append(f'{base_name}-')
5978
5971
 
5979
- except:
5980
- pass
5972
+ except:
5973
+ pass
5981
5974
 
5982
- modify_dict = copy.deepcopy(self.node_identities)
5975
+ modify_dict = copy.deepcopy(self.node_identities)
5983
5976
 
5984
5977
  for node, iden in self.node_identities.items():
5985
5978