nettracer3d 0.7.6__py3-none-any.whl → 0.7.8__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.

Potentially problematic release.


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

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
  """
@@ -3682,6 +3831,41 @@ class Network_3D:
3682
3831
  self._nodes = self._nodes.astype(np.uint16)
3683
3832
 
3684
3833
 
3834
+ def com_by_size(self):
3835
+ """Reassign communities based on size, starting with 1 for largest."""
3836
+
3837
+ from collections import Counter
3838
+
3839
+ # Convert all community values to regular ints (handles numpy scalars)
3840
+ clean_communities = {
3841
+ node: comm.item() if hasattr(comm, 'item') else comm
3842
+ for node, comm in self.communities.items()
3843
+ }
3844
+
3845
+ # Count community sizes and create mapping in one go
3846
+ community_sizes = Counter(clean_communities.values())
3847
+
3848
+ # Create old->new mapping: sort by size (desc), then by community ID for ties
3849
+ old_to_new = {
3850
+ old_comm: new_comm
3851
+ for new_comm, (old_comm, _) in enumerate(
3852
+ sorted(community_sizes.items(), key=lambda x: (-x[1], x[0])),
3853
+ start=1
3854
+ )
3855
+ }
3856
+
3857
+ # Apply mapping
3858
+ self.communities = {
3859
+ node: old_to_new[comm]
3860
+ for node, comm in clean_communities.items()
3861
+ }
3862
+
3863
+
3864
+
3865
+
3866
+
3867
+
3868
+
3685
3869
  def com_to_node(self, targets = None):
3686
3870
 
3687
3871
  def invert_dict(d):
@@ -3979,6 +4163,45 @@ class Network_3D:
3979
4163
  self.node_centroids = new_centroids
3980
4164
 
3981
4165
 
4166
+ def purge_properties(self):
4167
+
4168
+ """Eliminate nodes from properties that are no longer present in the nodes channel"""
4169
+
4170
+ print("Trimming properties. Note this does not update the network...")
4171
+
4172
+ def filter_dict_by_list(input_dict, filter_list):
4173
+ """
4174
+ Remove dictionary entries where the key is not in the filter list.
4175
+
4176
+ Args:
4177
+ input_dict (dict): Dictionary with integer values
4178
+ filter_list (list): List of integers to keep
4179
+
4180
+ Returns:
4181
+ dict: New dictionary with only keys that exist in filter_list
4182
+ """
4183
+ return {key: value for key, value in input_dict.items() if key in filter_list}
4184
+
4185
+ nodes = np.unique(self.nodes)
4186
+
4187
+ if 0 in nodes:
4188
+ np.delete(nodes, 0)
4189
+
4190
+ try:
4191
+ self.node_centroids = filter_dict_by_list(self.node_centroids, nodes)
4192
+ print("Updated centroids")
4193
+ except:
4194
+ pass
4195
+ try:
4196
+ self.communities = filter_dict_by_list(self.communities, nodes)
4197
+ print("Updated communities")
4198
+ except:
4199
+ pass
4200
+ try:
4201
+ self.node_identities = filter_dict_by_list(self.node_identities, nodes)
4202
+ print("Updated identities")
4203
+ except:
4204
+ pass
3982
4205
 
3983
4206
  def remove_trunk_post(self):
3984
4207
  """
@@ -4523,12 +4746,19 @@ class Network_3D:
4523
4746
 
4524
4747
  self.remove_edge_weights()
4525
4748
 
4526
- def centroid_array(self):
4749
+ def centroid_array(self, clip = False):
4527
4750
  """Use the centroids to populate a node array"""
4528
4751
 
4529
- array = proximity.populate_array(self.node_centroids)
4752
+ if clip:
4530
4753
 
4531
- return array
4754
+ array, centroids = proximity.populate_array(self.node_centroids, clip = True)
4755
+ return array, centroids
4756
+
4757
+ else:
4758
+
4759
+ array = proximity.populate_array(self.node_centroids)
4760
+
4761
+ return array
4532
4762
 
4533
4763
 
4534
4764
 
@@ -4612,6 +4842,7 @@ class Network_3D:
4612
4842
 
4613
4843
 
4614
4844
  def community_id_info(self):
4845
+
4615
4846
  def invert_dict(d):
4616
4847
  inverted = {}
4617
4848
  for key, value in d.items():
@@ -4654,7 +4885,99 @@ class Network_3D:
4654
4885
 
4655
4886
  return output
4656
4887
 
4888
+ def community_id_info_per_com(self, umap = False, label = False):
4657
4889
 
4890
+ def invert_dict(d):
4891
+ inverted = {}
4892
+ for key, value in d.items():
4893
+ inverted.setdefault(value, []).append(key)
4894
+ return inverted
4895
+
4896
+ community_dict = invert_dict(self.communities)
4897
+ summation = 0
4898
+ id_set = set(self.node_identities.values())
4899
+ id_dict = {}
4900
+ for i, iden in enumerate(id_set):
4901
+ id_dict[iden] = i
4902
+
4903
+ output = {}
4904
+
4905
+ for community in community_dict:
4906
+
4907
+ counter = np.zeros(len(id_set))
4908
+
4909
+ nodes = community_dict[community]
4910
+ size = len(nodes)
4911
+
4912
+ # Count identities in this community
4913
+ for node in nodes:
4914
+ counter[id_dict[self.node_identities[node]]] += 1 # Keep them as arrays
4915
+
4916
+ for i in range(len(counter)): # Translate them into proportions out of 1
4917
+
4918
+ counter[i] = counter[i]/size
4919
+
4920
+ output[community] = counter #Assign the finding here
4921
+
4922
+ if umap:
4923
+ from . import neighborhoods
4924
+ neighborhoods.visualize_cluster_composition_umap(output, id_set, label = label)
4925
+
4926
+ return output, id_set
4927
+
4928
+
4929
+ def assign_neighborhoods(self, seed, count, limit = None, prev_coms = None):
4930
+
4931
+ from . import neighborhoods
4932
+
4933
+ def invert_dict(d):
4934
+ inverted = {}
4935
+ for key, value in d.items():
4936
+ inverted.setdefault(value, []).append(key)
4937
+ return inverted
4938
+
4939
+ if prev_coms is not None:
4940
+ self.communities = copy.deepcopy(prev_coms)
4941
+
4942
+ identities, _ = self.community_id_info_per_com()
4943
+
4944
+ if limit is not None:
4945
+
4946
+ coms = invert_dict(self.communities)
4947
+
4948
+ zero_group = {}
4949
+
4950
+ for com, nodes in coms.items():
4951
+
4952
+ if len(nodes) < limit:
4953
+
4954
+ zero_group[com] = 0
4955
+
4956
+ del identities[com]
4957
+
4958
+
4959
+ clusters = neighborhoods.cluster_arrays(identities, count, seed = seed) # dict: {cluster_id: {'keys': [keys], 'arrays': [arrays]}}
4960
+
4961
+ coms = {}
4962
+
4963
+ neighbors = {}
4964
+
4965
+ for i, cluster in enumerate(clusters):
4966
+
4967
+ for com in cluster: # For community ID per list
4968
+
4969
+ coms[com] = i + 1
4970
+
4971
+ if limit is not None:
4972
+ coms.update(zero_group)
4973
+
4974
+ for node, com in self.communities.items():
4975
+
4976
+ self.communities[node] = coms[com]
4977
+
4978
+ identities, id_set = self.community_id_info_per_com()
4979
+
4980
+ neighborhoods.plot_dict_heatmap(identities, id_set)
4658
4981
 
4659
4982
 
4660
4983
  def kd_network(self, distance = 100, targets = None, make_array = False):
@@ -4694,6 +5017,70 @@ class Network_3D:
4694
5017
 
4695
5018
  return array
4696
5019
 
5020
+ def community_cells(self, size = 32, xy_scale = 1, z_scale = 1):
5021
+
5022
+ def invert_dict(d):
5023
+ inverted = {}
5024
+ for key, value_list in d.items():
5025
+ for value in value_list:
5026
+ inverted[value] = key
5027
+ return inverted
5028
+
5029
+ size_x = int(size * xy_scale)
5030
+ size_z = int(size * z_scale)
5031
+
5032
+ if size_x == size_z:
5033
+
5034
+ com_dict = proximity.partition_objects_into_cells(self.node_centroids, size_x)
5035
+
5036
+ else:
5037
+
5038
+ com_dict = proximity.partition_objects_into_cells(self.node_centroids, (size_z, size_x, size_x))
5039
+
5040
+ self.communities = invert_dict(com_dict)
5041
+
5042
+ def community_heatmap(self, num_nodes = None, is3d = True):
5043
+
5044
+ import math
5045
+
5046
+ def invert_dict(d):
5047
+ inverted = {}
5048
+ for key, value in d.items():
5049
+ inverted.setdefault(value, []).append(key)
5050
+ return inverted
5051
+
5052
+ if num_nodes == None:
5053
+
5054
+ try:
5055
+ num_nodes = len(self.network.nodes())
5056
+ except:
5057
+ try:
5058
+ num_nodes = len(self.node_centroids.keys())
5059
+ except:
5060
+ try:
5061
+ num_nodes = len(self.node_identities.keys())
5062
+ except:
5063
+ try:
5064
+ unique = np.unique(self.nodes)
5065
+ num_nodes = len(unique)
5066
+ if unique[0] == 0:
5067
+ num_nodes -= 1
5068
+ except:
5069
+ return
5070
+
5071
+ coms = invert_dict(self.communities)
5072
+
5073
+ rand_dens = num_nodes / len(coms.keys())
5074
+
5075
+ heat_dict = {}
5076
+
5077
+ for com, nodes in coms.items():
5078
+ heat_dict[com] = math.log(len(nodes)/rand_dens)
5079
+
5080
+ from . import neighborhoods
5081
+ neighborhoods.create_community_heatmap(heat_dict, self.communities, self.node_centroids, is_3d=is3d)
5082
+
5083
+ return heat_dict
4697
5084
 
4698
5085
 
4699
5086