nettracer3d 0.7.5__py3-none-any.whl → 0.7.7__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
@@ -348,6 +348,7 @@ def create_and_save_dataframe(pairwise_connections, excel_filename = None):
348
348
 
349
349
  #General supporting methods below:
350
350
 
351
+
351
352
  def invert_array(array):
352
353
  """Internal method used to flip node array indices. 0 becomes 255 and vice versa."""
353
354
  inverted_array = np.where(array == 0, 255, 0).astype(np.uint8)
@@ -658,7 +659,84 @@ def threshold(arr, proportion, custom_rad = None):
658
659
 
659
660
  return arr
660
661
 
661
- def show_3d(arrays_3d=None, arrays_4d=None, down_factor=None, order=0, xy_scale=1, z_scale=1, colors=['red', 'green', 'white', 'cyan', 'yellow']):
662
+ def generate_3d_bounding_box(shape, foreground_value=1, background_value=0):
663
+ """
664
+ Generate a 3D bounding box array with edges connecting the corners.
665
+
666
+ Parameters:
667
+ -----------
668
+ shape : tuple
669
+ Shape of the array in format (Z, Y, X)
670
+ foreground_value : int or float, default=1
671
+ Value to use for the bounding box edges and corners
672
+ background_value : int or float, default=0
673
+ Value to use for the background
674
+
675
+ Returns:
676
+ --------
677
+ numpy.ndarray
678
+ 3D array with bounding box edges
679
+ """
680
+ if len(shape) > 3:
681
+ shape = (shape[0], shape[1], shape[2])
682
+
683
+ z_size, y_size, x_size = shape
684
+
685
+ # Create empty array filled with background value
686
+ box_array = np.full(shape, background_value, dtype=np.float64)
687
+
688
+ # Define the 8 corners of the 3D box
689
+ corners = [
690
+ (0, 0, 0), # corner 0
691
+ (0, 0, x_size-1), # corner 1
692
+ (0, y_size-1, 0), # corner 2
693
+ (0, y_size-1, x_size-1), # corner 3
694
+ (z_size-1, 0, 0), # corner 4
695
+ (z_size-1, 0, x_size-1), # corner 5
696
+ (z_size-1, y_size-1, 0), # corner 6
697
+ (z_size-1, y_size-1, x_size-1) # corner 7
698
+ ]
699
+
700
+ # Set corner values
701
+ for corner in corners:
702
+ box_array[corner] = foreground_value
703
+
704
+ # Define edges connecting adjacent corners
705
+ # Each edge connects two corners that differ by only one coordinate
706
+ edges = [
707
+ # Bottom face edges (z=0)
708
+ (0, 1), (1, 3), (3, 2), (2, 0),
709
+ # Top face edges (z=max)
710
+ (4, 5), (5, 7), (7, 6), (6, 4),
711
+ # Vertical edges connecting bottom to top
712
+ (0, 4), (1, 5), (2, 6), (3, 7)
713
+ ]
714
+
715
+ # Draw edges using linspace
716
+ for start_idx, end_idx in edges:
717
+ start_corner = corners[start_idx]
718
+ end_corner = corners[end_idx]
719
+
720
+ # Calculate the maximum distance along any axis to determine number of points
721
+ max_distance = max(
722
+ abs(end_corner[0] - start_corner[0]),
723
+ abs(end_corner[1] - start_corner[1]),
724
+ abs(end_corner[2] - start_corner[2])
725
+ )
726
+ num_points = max_distance + 1
727
+
728
+ # Generate points along the edge using linspace
729
+ z_points = np.linspace(start_corner[0], end_corner[0], num_points, dtype=int)
730
+ y_points = np.linspace(start_corner[1], end_corner[1], num_points, dtype=int)
731
+ x_points = np.linspace(start_corner[2], end_corner[2], num_points, dtype=int)
732
+
733
+ # Set foreground values along the edge
734
+ for z, y, x in zip(z_points, y_points, x_points):
735
+ box_array[int(z), int(y), int(x)] = foreground_value
736
+
737
+ return box_array
738
+
739
+ def show_3d(arrays_3d=None, arrays_4d=None, down_factor=None, order=0, xy_scale=1, z_scale=1, colors=['red', 'green', 'white', 'cyan', 'yellow'], box = False):
662
740
  """
663
741
  Show 3d (or 2d) displays of array data using napari.
664
742
  Params: arrays - A list of 3d or 2d numpy arrays to display
@@ -682,6 +760,7 @@ def show_3d(arrays_3d=None, arrays_4d=None, down_factor=None, order=0, xy_scale=
682
760
  # Add 3D arrays if provided
683
761
  if arrays_3d is not None:
684
762
  for arr, color in zip(arrays_3d, colors):
763
+ shape = arr.shape
685
764
  viewer.add_image(
686
765
  arr,
687
766
  scale=scale,
@@ -702,6 +781,8 @@ def show_3d(arrays_3d=None, arrays_4d=None, down_factor=None, order=0, xy_scale=
702
781
  if arr.shape[3] == 4:
703
782
  arr = arr[:, :, :, :3] # Remove alpha
704
783
 
784
+ shape = arr.shape
785
+
705
786
  # Add each color channel separately
706
787
  colors = ['red', 'green', 'blue']
707
788
  for c in range(3):
@@ -715,6 +796,19 @@ def show_3d(arrays_3d=None, arrays_4d=None, down_factor=None, order=0, xy_scale=
715
796
  name=f'Channel_{colors[c]}_{i}'
716
797
  )
717
798
 
799
+ if box:
800
+ viewer.add_image(
801
+ generate_3d_bounding_box(shape),
802
+ scale=scale,
803
+ colormap='white',
804
+ rendering='mip',
805
+ blending='additive',
806
+ opacity=0.5,
807
+ name=f'Bounding Box'
808
+ )
809
+
810
+
811
+
718
812
  napari.run()
719
813
 
720
814
  def z_project(array3d, method='max'):
@@ -1673,20 +1767,16 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
1673
1767
  if nodes is not None and down_factor is not None:
1674
1768
  array = upsample_with_padding(array, down_factor, arrayshape)
1675
1769
 
1676
-
1677
-
1678
1770
  if nodes is None:
1679
1771
 
1680
- array = smart_dilate.smart_label(array, other_array, GPU = GPU)
1772
+ array = smart_dilate.smart_label(array, other_array, GPU = GPU, remove_template = True)
1681
1773
 
1682
1774
  else:
1683
1775
  if down_factor is not None:
1684
- array = smart_dilate.smart_label(bonus_array, array, GPU = GPU, predownsample = down_factor)
1776
+ array = smart_dilate.smart_label(bonus_array, array, GPU = GPU, predownsample = down_factor, remove_template = True)
1685
1777
  else:
1686
1778
 
1687
- array = smart_dilate.smart_label(bonus_array, array, GPU = GPU)
1688
-
1689
-
1779
+ array = smart_dilate.smart_label(bonus_array, array, GPU = GPU, remove_template = True)
1690
1780
 
1691
1781
  if down_factor is not None and nodes is None:
1692
1782
  array = upsample_with_padding(array, down_factor, arrayshape)
@@ -1705,7 +1795,7 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
1705
1795
 
1706
1796
  return array
1707
1797
 
1708
- def fix_branches(array, G, communities, fix_val = None):
1798
+ def fix_branches_network(array, G, communities, fix_val = None):
1709
1799
 
1710
1800
  def invert_dict(d):
1711
1801
  inverted = {}
@@ -1748,6 +1838,65 @@ def fix_branches(array, G, communities, fix_val = None):
1748
1838
 
1749
1839
  return targs
1750
1840
 
1841
+ def fix_branches(array, G, max_val):
1842
+ """
1843
+ Parameters:
1844
+ array: numpy array containing the labeled regions
1845
+ G: Graph representing connectivity relationships
1846
+ max_val: The target value to find neighbors for
1847
+
1848
+ Returns:
1849
+ Modified array with fused regions
1850
+ """
1851
+ # Get all nodes
1852
+ all_nodes = set(G.nodes())
1853
+
1854
+ # Initially safe nodes are direct neighbors of max_val
1855
+ safe_initial = set(G.neighbors(max_val))
1856
+
1857
+ # Not-safe nodes are all other nodes except max_val
1858
+ not_safe_initial = all_nodes - safe_initial - {max_val}
1859
+
1860
+ # Get adjacency view (much faster for repeated neighbor lookups)
1861
+ adj = G.adj
1862
+
1863
+ # Find all neighbors of not_safe nodes in one pass
1864
+ neighbors_of_not_safe = set()
1865
+ for node in not_safe_initial:
1866
+ neighbors_of_not_safe.update(adj[node])
1867
+
1868
+ # Remove max_val if present
1869
+ neighbors_of_not_safe.discard(max_val)
1870
+
1871
+ # Find safe nodes that should be moved
1872
+ nodes_to_move = safe_initial & neighbors_of_not_safe
1873
+
1874
+ # Update sets
1875
+ not_safe = not_safe_initial | nodes_to_move
1876
+
1877
+ # The rest of the function - FIX STARTS HERE
1878
+ targs = np.array(list(not_safe))
1879
+
1880
+ if len(targs) == 0:
1881
+ return array
1882
+
1883
+ mask = np.isin(array, targs)
1884
+
1885
+ labeled, num_components = label_objects(mask)
1886
+
1887
+ # Get the current maximum label in the array to avoid collisions
1888
+ current_max = np.max(array)
1889
+
1890
+ # Assign new unique labels to each connected component
1891
+ for component_id in range(1, num_components + 1):
1892
+ component_mask = labeled == component_id
1893
+ array[component_mask] = current_max + component_id
1894
+
1895
+ return array
1896
+
1897
+
1898
+
1899
+
1751
1900
 
1752
1901
 
1753
1902
  def label_vertices(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol = 0, down_factor = 0, directory = None, return_skele = False, order = 0, fastdil = True):
@@ -3564,7 +3713,7 @@ class Network_3D:
3564
3713
 
3565
3714
  #Some methods that may be useful:
3566
3715
 
3567
- def community_partition(self, weighted = False, style = 0, dostats = True, seed = None):
3716
+ def community_partition(self, weighted = False, style = 0, dostats = True, seed = 42):
3568
3717
  """
3569
3718
  Sets the communities attribute by splitting the network into communities
3570
3719
  """
@@ -3979,6 +4128,45 @@ class Network_3D:
3979
4128
  self.node_centroids = new_centroids
3980
4129
 
3981
4130
 
4131
+ def purge_properties(self):
4132
+
4133
+ """Eliminate nodes from properties that are no longer present in the nodes channel"""
4134
+
4135
+ print("Trimming properties. Note this does not update the network...")
4136
+
4137
+ def filter_dict_by_list(input_dict, filter_list):
4138
+ """
4139
+ Remove dictionary entries where the key is not in the filter list.
4140
+
4141
+ Args:
4142
+ input_dict (dict): Dictionary with integer values
4143
+ filter_list (list): List of integers to keep
4144
+
4145
+ Returns:
4146
+ dict: New dictionary with only keys that exist in filter_list
4147
+ """
4148
+ return {key: value for key, value in input_dict.items() if key in filter_list}
4149
+
4150
+ nodes = np.unique(self.nodes)
4151
+
4152
+ if 0 in nodes:
4153
+ np.delete(nodes, 0)
4154
+
4155
+ try:
4156
+ self.node_centroids = filter_dict_by_list(self.node_centroids, nodes)
4157
+ print("Updated centroids")
4158
+ except:
4159
+ pass
4160
+ try:
4161
+ self.communities = filter_dict_by_list(self.communities, nodes)
4162
+ print("Updated communities")
4163
+ except:
4164
+ pass
4165
+ try:
4166
+ self.node_identities = filter_dict_by_list(self.node_identities, nodes)
4167
+ print("Updated identities")
4168
+ except:
4169
+ pass
3982
4170
 
3983
4171
  def remove_trunk_post(self):
3984
4172
  """
@@ -4523,12 +4711,19 @@ class Network_3D:
4523
4711
 
4524
4712
  self.remove_edge_weights()
4525
4713
 
4526
- def centroid_array(self):
4714
+ def centroid_array(self, clip = False):
4527
4715
  """Use the centroids to populate a node array"""
4528
4716
 
4529
- array = proximity.populate_array(self.node_centroids)
4717
+ if clip:
4530
4718
 
4531
- return array
4719
+ array, centroids = proximity.populate_array(self.node_centroids, clip = True)
4720
+ return array, centroids
4721
+
4722
+ else:
4723
+
4724
+ array = proximity.populate_array(self.node_centroids)
4725
+
4726
+ return array
4532
4727
 
4533
4728
 
4534
4729
 
@@ -4612,6 +4807,7 @@ class Network_3D:
4612
4807
 
4613
4808
 
4614
4809
  def community_id_info(self):
4810
+
4615
4811
  def invert_dict(d):
4616
4812
  inverted = {}
4617
4813
  for key, value in d.items():
@@ -4654,7 +4850,99 @@ class Network_3D:
4654
4850
 
4655
4851
  return output
4656
4852
 
4853
+ def community_id_info_per_com(self, umap = False, label = False):
4854
+
4855
+ def invert_dict(d):
4856
+ inverted = {}
4857
+ for key, value in d.items():
4858
+ inverted.setdefault(value, []).append(key)
4859
+ return inverted
4657
4860
 
4861
+ community_dict = invert_dict(self.communities)
4862
+ summation = 0
4863
+ id_set = set(self.node_identities.values())
4864
+ id_dict = {}
4865
+ for i, iden in enumerate(id_set):
4866
+ id_dict[iden] = i
4867
+
4868
+ output = {}
4869
+
4870
+ for community in community_dict:
4871
+
4872
+ counter = np.zeros(len(id_set))
4873
+
4874
+ nodes = community_dict[community]
4875
+ size = len(nodes)
4876
+
4877
+ # Count identities in this community
4878
+ for node in nodes:
4879
+ counter[id_dict[self.node_identities[node]]] += 1 # Keep them as arrays
4880
+
4881
+ for i in range(len(counter)): # Translate them into proportions out of 1
4882
+
4883
+ counter[i] = counter[i]/size
4884
+
4885
+ output[community] = counter #Assign the finding here
4886
+
4887
+ if umap:
4888
+ from . import neighborhoods
4889
+ neighborhoods.visualize_cluster_composition_umap(output, id_set, label = label)
4890
+
4891
+ return output, id_set
4892
+
4893
+
4894
+ def assign_neighborhoods(self, seed, count, limit = None, prev_coms = None):
4895
+
4896
+ from . import neighborhoods
4897
+
4898
+ def invert_dict(d):
4899
+ inverted = {}
4900
+ for key, value in d.items():
4901
+ inverted.setdefault(value, []).append(key)
4902
+ return inverted
4903
+
4904
+ if prev_coms is not None:
4905
+ self.communities = copy.deepcopy(prev_coms)
4906
+
4907
+ identities, _ = self.community_id_info_per_com()
4908
+
4909
+ if limit is not None:
4910
+
4911
+ coms = invert_dict(self.communities)
4912
+
4913
+ zero_group = {}
4914
+
4915
+ for com, nodes in coms.items():
4916
+
4917
+ if len(nodes) < limit:
4918
+
4919
+ zero_group[com] = 0
4920
+
4921
+ del identities[com]
4922
+
4923
+
4924
+ clusters = neighborhoods.cluster_arrays(identities, count, seed = seed) # dict: {cluster_id: {'keys': [keys], 'arrays': [arrays]}}
4925
+
4926
+ coms = {}
4927
+
4928
+ neighbors = {}
4929
+
4930
+ for i, cluster in enumerate(clusters):
4931
+
4932
+ for com in cluster: # For community ID per list
4933
+
4934
+ coms[com] = i + 1
4935
+
4936
+ if limit is not None:
4937
+ coms.update(zero_group)
4938
+
4939
+ for node, com in self.communities.items():
4940
+
4941
+ self.communities[node] = coms[com]
4942
+
4943
+ identities, id_set = self.community_id_info_per_com()
4944
+
4945
+ neighborhoods.plot_dict_heatmap(identities, id_set)
4658
4946
 
4659
4947
 
4660
4948
  def kd_network(self, distance = 100, targets = None, make_array = False):
@@ -4694,6 +4982,19 @@ class Network_3D:
4694
4982
 
4695
4983
  return array
4696
4984
 
4985
+ def community_cells(self, size = 32):
4986
+
4987
+ def invert_dict(d):
4988
+ inverted = {}
4989
+ for key, value_list in d.items():
4990
+ for value in value_list:
4991
+ inverted[value] = key
4992
+ return inverted
4993
+
4994
+ com_dict = proximity.partition_objects_into_cells(self.node_centroids, size)
4995
+
4996
+ self.communities = invert_dict(com_dict)
4997
+
4697
4998
 
4698
4999
 
4699
5000