nettracer3d 0.7.9__py3-none-any.whl → 0.8.0__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.
@@ -492,48 +492,39 @@ def generate_distinct_colors(n_colors: int) -> List[Tuple[int, int, int]]:
492
492
  return colors
493
493
 
494
494
  def assign_node_colors(node_list: List[int], labeled_array: np.ndarray) -> Tuple[np.ndarray, Dict[int, str]]:
495
- """
496
- Assign distinct colors to nodes and create an RGBA image.
497
-
498
- Args:
499
- node_list: List of node IDs
500
- labeled_array: 3D numpy array with labels corresponding to node IDs
495
+ """fast version using lookup table approach."""
501
496
 
502
- Returns:
503
- Tuple of (RGBA-coded numpy array (H, W, D, 4), dictionary mapping nodes to color names)
504
- """
505
-
506
- # Sort communities by size (descending)
507
- sorted_nodes= sorted(node_list, reverse=True)
497
+ # Sort nodes by size (descending)
498
+ sorted_nodes = sorted(node_list, reverse=True)
508
499
 
509
500
  # Generate distinct colors
510
501
  colors = generate_distinct_colors(len(node_list))
511
- random.shuffle(colors) #Randomly sorted to make adjacent structures likely stand out
502
+ random.shuffle(colors) # Randomly sorted to make adjacent structures likely stand out
512
503
 
513
504
  # Convert RGB colors to RGBA by adding alpha channel
514
- colors_rgba = [(r, g, b, 255) for r, g, b in colors] # Full opacity for colored regions
505
+ colors_rgba = np.array([(r, g, b, 255) for r, g, b in colors], dtype=np.uint8)
515
506
 
516
- # Create mapping from community to color
507
+ # Create mapping from node to color
517
508
  node_to_color = {node: colors_rgba[i] for i, node in enumerate(sorted_nodes)}
518
509
 
519
- # Create RGBA array (initialize with transparent background)
520
- rgba_array = np.zeros((*labeled_array.shape, 4), dtype=np.uint8)
510
+ # Create lookup table
511
+ max_label = max(max(labeled_array.flat), max(node_list) if node_list else 0)
512
+ color_lut = np.zeros((max_label + 1, 4), dtype=np.uint8) # Transparent by default
521
513
 
522
- # Assign colors to each voxel based on its label
523
- for label in np.unique(labeled_array):
524
- if label in node_to_color: # Skip background (usually label 0)
525
- mask = labeled_array == label
526
- for i in range(4): # RGBA channels
527
- rgba_array[mask, i] = node_to_color[label][i]
528
-
529
- # Convert the RGB portion of community_to_color back to RGB for color naming
514
+ for node_id, color in node_to_color.items():
515
+ color_lut[node_id] = color
516
+
517
+ # Single vectorized operation - eliminates all loops!
518
+ rgba_array = color_lut[labeled_array]
519
+
520
+ # Convert colors for naming
530
521
  node_to_color_rgb = {k: tuple(v[:3]) for k, v in node_to_color.items()}
531
522
  node_to_color_names = convert_node_colors_to_names(node_to_color_rgb)
532
523
 
533
524
  return rgba_array, node_to_color_names
534
525
 
535
526
  def assign_community_colors(community_dict: Dict[int, int], labeled_array: np.ndarray) -> Tuple[np.ndarray, Dict[int, str]]:
536
- """Ultra-fast version using lookup table approach."""
527
+ """fast version using lookup table approach."""
537
528
 
538
529
  # Same setup as before
539
530
  communities = set(community_dict.values())
nettracer3d/nettracer.py CHANGED
@@ -548,6 +548,7 @@ def remove_branches(skeleton, length):
548
548
  return image_copy
549
549
 
550
550
 
551
+
551
552
  def estimate_object_radii(labeled_array, gpu=False, n_jobs=None, xy_scale = 1, z_scale = 1):
552
553
  """
553
554
  Estimate the radii of labeled objects in a 3D numpy array.
@@ -1485,21 +1486,21 @@ def remove_zeros(input_list):
1485
1486
 
1486
1487
 
1487
1488
  def combine_edges(edge_labels_1, edge_labels_2):
1488
- """Internal method to combine the edges and 'inner edges' into a single array while preserving their IDs. Prioritizes 'edge_labels_1' when overlapped"""
1489
-
1490
- edge_labels_1 = edge_labels_1.astype(np.uint32)
1491
- edge_labels_2 = edge_labels_2.astype(np.uint32)
1492
-
1493
- max_val = np.max(edge_labels_1)
1494
- edge_bools_1 = edge_labels_1 == 0 #Get boolean mask where edges do not exist.
1495
- edge_bools_2 = edge_labels_2 > 0 #Get boolean mask where inner edges exist.
1496
- edge_labels_2 = edge_labels_2 + max_val #Add the maximum edge ID to all inner edges so the two can be merged without overriding eachother
1497
- edge_labels_2 = edge_labels_2 * edge_bools_2 #Eliminate any indices that should be 0 from inner edges.
1498
- edge_labels_2 = edge_labels_2 * edge_bools_1 #Eliminate any indices where outer edges overlap inner edges (Outer edges are giving overlap priority)
1499
- edge_labels = edge_labels_1 + edge_labels_2 #Combine the outer edges with the inner edges modified via the above steps
1500
-
1501
- return edge_labels
1502
-
1489
+ """
1490
+ let NumPy handle promotion automatically
1491
+ """
1492
+ # Early exit if no combination needed
1493
+ mask = (edge_labels_1 == 0) & (edge_labels_2 > 0)
1494
+ if not np.any(mask):
1495
+ return edge_labels_1.copy()
1496
+
1497
+ max_val = np.max(edge_labels_1)
1498
+
1499
+ # Let NumPy handle dtype promotion automatically
1500
+ # This will promote to the smallest type that can handle the operation
1501
+ offset_labels = edge_labels_2 + max_val
1502
+
1503
+ return np.where(mask, offset_labels, edge_labels_1)
1503
1504
 
1504
1505
  def combine_nodes(root_nodes, other_nodes, other_ID, identity_dict, root_ID = None):
1505
1506
 
@@ -1507,15 +1508,10 @@ def combine_nodes(root_nodes, other_nodes, other_ID, identity_dict, root_ID = No
1507
1508
 
1508
1509
  print("Combining node arrays")
1509
1510
 
1510
- root_nodes = root_nodes.astype(np.uint32)
1511
- other_nodes = other_nodes.astype(np.uint32)
1512
-
1513
- max_val = np.max(root_nodes)
1514
- root_bools = root_nodes == 0 #Get boolean mask where root nodes do not exist.
1515
- other_bools = other_nodes > 0 #Get boolean mask where other nodes exist.
1516
- other_nodes = other_nodes + max_val #Add the maximum root node labels to other nodes so the two can be merged without overriding eachother
1517
- other_nodes = other_nodes * other_bools #Eliminate any indices that should be 0 from other_nodes.
1518
- other_nodes = other_nodes * root_bools #Eliminate any indices where other nodes overlap root nodes (root node are giving overlap priority)
1511
+ mask = (root_nodes == 0) & (other_nodes > 0)
1512
+ if np.any(mask):
1513
+ max_val = np.max(root_nodes)
1514
+ other_nodes[:] = np.where(mask, other_nodes + max_val, 0)
1519
1515
 
1520
1516
  if root_ID is not None:
1521
1517
  rootIDs = list(np.unique(root_nodes)) #Sets up adding these vals to the identitiy dictionary. Gets skipped if this has already been done.
@@ -1530,6 +1526,11 @@ def combine_nodes(root_nodes, other_nodes, other_ID, identity_dict, root_ID = No
1530
1526
 
1531
1527
  if root_ID is not None: #Adds the root vals to the dictionary if it hasn't already
1532
1528
 
1529
+ if other_ID.endswith('.tiff'):
1530
+ other_ID = other_ID[:-5]
1531
+ elif other_ID.endswith('.tif'):
1532
+ other_ID = other_ID[:-4]
1533
+
1533
1534
  for item in rootIDs:
1534
1535
  identity_dict[item] = root_ID
1535
1536
 
@@ -1538,6 +1539,11 @@ def combine_nodes(root_nodes, other_nodes, other_ID, identity_dict, root_ID = No
1538
1539
  other_ID = os.path.basename(other_ID)
1539
1540
  except:
1540
1541
  pass
1542
+ if other_ID.endswith('.tiff'):
1543
+ other_ID = other_ID[:-5]
1544
+ elif other_ID.endswith('.tif'):
1545
+ other_ID = other_ID[:-4]
1546
+
1541
1547
  identity_dict[item] = other_ID
1542
1548
 
1543
1549
  nodes = root_nodes + other_nodes #Combine the outer edges with the inner edges modified via the above steps
@@ -1609,6 +1615,17 @@ def downsample(data, factor, directory=None, order=0):
1609
1615
 
1610
1616
  return data
1611
1617
 
1618
+
1619
+ def otsu_binarize(image_array):
1620
+
1621
+ """Automated binarize method for seperating the foreground"""
1622
+
1623
+ from skimage.filters import threshold_otsu
1624
+
1625
+ threshold = threshold_otsu(image_array)
1626
+ binary_mask = image_array > threshold
1627
+ return binary_mask
1628
+
1612
1629
  def binarize(arrayimage, directory = None):
1613
1630
  """
1614
1631
  Can be used to binarize an image. Binary output will be saved to the active directory if none is specified.
@@ -3428,7 +3445,7 @@ class Network_3D:
3428
3445
  """
3429
3446
  self._nodes, num_nodes = label_objects(nodes, structure_3d)
3430
3447
 
3431
- def merge_nodes(self, addn_nodes_name, label_nodes = True):
3448
+ def merge_nodes(self, addn_nodes_name, label_nodes = True, root_id = "Root_Nodes"):
3432
3449
  """
3433
3450
  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,
3434
3451
  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
@@ -3439,8 +3456,13 @@ class Network_3D:
3439
3456
  :param label_nodes: (Optional - Val = True; Boolean). Will label all discrete objects in each node file being merged if True. If False, will not label.
3440
3457
  """
3441
3458
 
3442
- nodes_name = 'Root_Nodes'
3459
+ nodes_name = root_id
3443
3460
 
3461
+ try:
3462
+ nodes_name = os.path.splitext(os.path.basename(nodes_name))[0]
3463
+ except:
3464
+ pass
3465
+
3444
3466
  identity_dict = {} #A dictionary to deliniate the node identities
3445
3467
 
3446
3468
  try: #Try presumes the input is a tif
@@ -3462,7 +3484,10 @@ class Network_3D:
3462
3484
  for i, addn_nodes in enumerate(addn_nodes_list):
3463
3485
  try:
3464
3486
  addn_nodes_ID = addn_nodes
3465
- addn_nodes = tifffile.imread(f'{addn_nodes_name}/{addn_nodes}')
3487
+ try:
3488
+ addn_nodes = tifffile.imread(f'{addn_nodes_name}/{addn_nodes}')
3489
+ except:
3490
+ continue
3466
3491
 
3467
3492
  if label_nodes is True:
3468
3493
  addn_nodes, num_nodes2 = label_objects(addn_nodes) # Label the node objects. Note this presumes no overlap between node masks.
@@ -4649,11 +4674,15 @@ class Network_3D:
4649
4674
  else:
4650
4675
  min_coords, max_coords = bounds
4651
4676
 
4652
- dim_list = max_coords - min_coords
4677
+ try:
4678
+ dim_list = max_coords - min_coords
4679
+ except:
4680
+ min_coords = np.array([0,0,0])
4681
+ bounds = (min_coords, max_coords)
4682
+ dim_list = max_coords - min_coords
4653
4683
 
4654
4684
  new_list = []
4655
4685
 
4656
-
4657
4686
  if dim == 3:
4658
4687
  for centroid in roots:
4659
4688
 
@@ -4688,7 +4717,7 @@ class Network_3D:
4688
4717
 
4689
4718
  h_vals = proximity.compute_ripleys_h(k_vals, r_vals, dim)
4690
4719
 
4691
- proximity.plot_ripley_functions(r_vals, k_vals, h_vals, dim)
4720
+ proximity.plot_ripley_functions(r_vals, k_vals, h_vals, dim, root, targ)
4692
4721
 
4693
4722
  return r_vals, k_vals, h_vals
4694
4723
 
@@ -5082,6 +5111,58 @@ class Network_3D:
5082
5111
 
5083
5112
  return heat_dict
5084
5113
 
5114
+ def merge_node_ids(self, path, data):
5115
+
5116
+ if self.node_identities is None: # Prepare modular dict
5117
+
5118
+ self.node_identities = {}
5119
+
5120
+ nodes = list(np.unique(data))
5121
+ if 0 in nodes:
5122
+ del nodes[0]
5123
+
5124
+ for node in nodes:
5125
+ self.node_identities[node] = ''
5126
+
5127
+ img_list = directory_info(path)
5128
+
5129
+ for i, img in enumerate(img_list):
5130
+ mask = tifffile.imread(f'{path}/{img}')
5131
+
5132
+ if len(np.unique(mask)) != 2:
5133
+
5134
+ mask = otsu_binarize(mask)
5135
+
5136
+ nodes = data * mask
5137
+ nodes = list(np.unique(nodes))
5138
+ if 0 in nodes:
5139
+ del nodes[0]
5140
+
5141
+ if img.endswith('.tiff'):
5142
+ base_name = img[:-5]
5143
+ elif img.endswith('.tif'):
5144
+ base_name = img[:-4]
5145
+ else:
5146
+ base_name = img
5147
+
5148
+ for node in self.node_identities.keys():
5149
+
5150
+ try:
5151
+
5152
+ if node in nodes:
5153
+
5154
+ self.node_identities[node] += f" {base_name}+"
5155
+
5156
+ else:
5157
+
5158
+ self.node_identities[node] += f" {base_name}-"
5159
+
5160
+ except:
5161
+ pass
5162
+
5163
+
5164
+
5165
+
5085
5166
 
5086
5167
 
5087
5168
  if __name__ == "__main__":