nettracer3d 1.1.1__py3-none-any.whl → 1.2.4__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
@@ -384,6 +384,13 @@ def invert_dict(d):
384
384
  inverted.setdefault(value, []).append(key)
385
385
  return inverted
386
386
 
387
+ def revert_dict(d):
388
+ inverted = {}
389
+ for key, value_list in d.items():
390
+ for value in value_list:
391
+ inverted[value] = key
392
+ return inverted
393
+
387
394
  def invert_dict_special(d):
388
395
 
389
396
  d = invert_dict(d)
@@ -626,6 +633,10 @@ def remove_branches_new(skeleton, length):
626
633
  image_copy = (image_copy[1:-1, 1:-1, 1:-1]).astype(np.uint8)
627
634
  return image_copy
628
635
 
636
+ import numpy as np
637
+ from collections import deque, defaultdict
638
+
639
+
629
640
  def remove_branches(skeleton, length):
630
641
  """Used to compensate for overly-branched skeletons resulting from the scipy 3d skeletonization algorithm"""
631
642
 
@@ -737,8 +748,42 @@ def estimate_object_radii(labeled_array, gpu=False, n_jobs=None, xy_scale = 1, z
737
748
  else:
738
749
  return morphology.estimate_object_radii_cpu(labeled_array, n_jobs, xy_scale = xy_scale, z_scale = z_scale)
739
750
 
751
+ def get_surface_areas(labeled, xy_scale=1, z_scale=1):
752
+ labels = np.unique(labeled)
753
+ labels = labels[labels > 0]
754
+ max_label = int(np.max(labeled))
755
+
756
+ surface_areas = np.zeros(max_label + 1, dtype=np.float64)
757
+
758
+ for axis in range(3):
759
+ if axis == 2:
760
+ face_area = xy_scale * xy_scale
761
+ else:
762
+ face_area = xy_scale * z_scale
763
+
764
+ for direction in [-1, 1]:
765
+ # Pad with zeros only on the axis we're checking
766
+ pad_width = [(1, 1) if i == axis else (0, 0) for i in range(3)]
767
+ padded = np.pad(labeled, pad_width, mode='constant', constant_values=0)
768
+
769
+ # Roll the padded array
770
+ shifted = np.roll(padded, direction, axis=axis)
771
+
772
+ # Extract the center region (original size) from shifted
773
+ slices = [slice(1, -1) if i == axis else slice(None) for i in range(3)]
774
+ shifted_cropped = shifted[tuple(slices)]
775
+
776
+ # Find exposed faces
777
+ exposed_faces = (labeled != shifted_cropped) & (labeled > 0)
778
+
779
+ face_counts = np.bincount(labeled[exposed_faces],
780
+ minlength=max_label + 1)
781
+ surface_areas += face_counts * face_area
782
+
783
+ result = {int(label): float(surface_areas[label]) for label in labels}
784
+ return result
740
785
 
741
- def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil = 0, max_vol = 0, directory = None, return_skele = False, nodes = None):
786
+ def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil = 0, max_vol = 0, directory = None, return_skele = False, nodes = None, compute = True, unify = False, xy_scale = 1, z_scale = 1):
742
787
  """Internal method to break open a skeleton at its branchpoints and label the remaining components, for an 8bit binary array"""
743
788
 
744
789
  if type(skeleton) == str:
@@ -747,18 +792,28 @@ def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil =
747
792
  else:
748
793
  broken_skele = None
749
794
 
750
- #old_skeleton = copy.deepcopy(skeleton) # The skeleton might get modified in label_vertices so we can make a preserved copy of it to use later
751
-
752
795
  if nodes is None:
753
796
 
754
- verts = label_vertices(skeleton, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, return_skele = return_skele)
797
+ verts = label_vertices(skeleton, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, return_skele = return_skele, compute = compute)
755
798
 
756
799
  else:
757
800
  verts = nodes
758
801
 
759
802
  verts = invert_array(verts)
760
803
 
761
- #skeleton = old_skeleton
804
+ """
805
+ if compute: # We are interested in the endpoints if we are doing the optional computation later
806
+ endpoints = []
807
+ image_copy = np.pad(skeleton, pad_width=1, mode='constant', constant_values=0)
808
+ nonzero_coords = np.transpose(np.nonzero(image_copy))
809
+ for x, y, z in nonzero_coords:
810
+ mini = image_copy[x-1:x+2, y-1:y+2, z-1:z+2]
811
+ nearby_sum = np.sum(mini)
812
+ threshold = 2 * image_copy[x, y, z]
813
+
814
+ if nearby_sum <= threshold:
815
+ endpoints.append((x, y, z))
816
+ """
762
817
 
763
818
  image_copy = skeleton * verts
764
819
 
@@ -776,9 +831,147 @@ def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil =
776
831
  tifffile.imwrite(filename, labeled_image, photometric='minisblack')
777
832
  print(f"Broken skeleton saved to {filename}")
778
833
 
779
- return labeled_image
834
+ if not unify:
835
+ verts = None
836
+ else:
837
+ verts = invert_array(verts)
838
+
839
+ if compute:
840
+
841
+ return labeled_image, verts, skeleton, None
842
+
843
+ return labeled_image, verts, None, None
844
+
845
+ def compute_optional_branchstats(verts, labeled_array, endpoints, xy_scale = 1, z_scale = 1):
846
+
847
+ #Lengths:
848
+ # Get all non-background coordinates and their labels in one pass
849
+ z, y, x = np.where(labeled_array != 0)
850
+ labels = labeled_array[z, y, x]
851
+
852
+ # Sort by label
853
+ sort_idx = np.argsort(labels)
854
+ labels_sorted = labels[sort_idx]
855
+ z_sorted = z[sort_idx]
856
+ y_sorted = y[sort_idx]
857
+ x_sorted = x[sort_idx]
858
+
859
+ # Find where each label starts
860
+ unique_labels, split_idx = np.unique(labels_sorted, return_index=True)
861
+ split_idx = split_idx[1:] # Remove first index for np.split
862
+
863
+ # Split into groups
864
+ z_split = np.split(z_sorted, split_idx)
865
+ y_split = np.split(y_sorted, split_idx)
866
+ x_split = np.split(x_sorted, split_idx)
867
+
868
+ # Build dict
869
+ coords_dict = {label: np.column_stack([z, y, x])
870
+ for label, z, y, x in zip(unique_labels, z_split, y_split, x_split)}
871
+
872
+ from sklearn.neighbors import NearestNeighbors
873
+ from scipy.spatial.distance import pdist, squareform
874
+ len_dict = {}
875
+ tortuosity_dict = {}
876
+ angle_dict = {}
877
+ for label, coords in coords_dict.items():
878
+ len_dict[label] = morphology.calculate_skeleton_lengths(labeled_array.shape, xy_scale=xy_scale, z_scale=z_scale, skeleton_coords=coords)
879
+
880
+ # Find neighbors for all points at once
881
+ nbrs = NearestNeighbors(radius=1.74, algorithm='kd_tree').fit(coords)
882
+ neighbor_counts = nbrs.radius_neighbors(coords, return_distance=False)
883
+ neighbor_counts = np.array([len(n) - 1 for n in neighbor_counts]) # -1 to exclude self
884
+
885
+ # Endpoints have exactly 1 neighbor
886
+ endpoints = coords[neighbor_counts == 1]
887
+
888
+ if len(endpoints) > 1:
889
+ # Scale endpoints
890
+ scaled_endpoints = endpoints.copy().astype(float)
891
+ scaled_endpoints[:, 0] *= z_scale # z dimension
892
+ scaled_endpoints[:, 1] *= xy_scale # y dimension
893
+ scaled_endpoints[:, 2] *= xy_scale # x dimension
894
+
895
+ # calculate distances on scaled coordinates
896
+ distances = pdist(scaled_endpoints, metric='euclidean')
897
+ max_distance = distances.max()
898
+
899
+ tortuosity_dict[label] = len_dict[label]/max_distance
900
+
901
+ for branch, length in len_dict.items():
902
+ if length == 0: # This can happen for branches that are 1 pixel which shouldn't have '0' length technically, so we just set them to the length of a pixel
903
+ len_dict[branch] = xy_scale
904
+ tortuosity_dict[branch] = 1
780
905
 
906
+ """
907
+ verts = invert_array(verts)
908
+ for x, y, z in endpoints:
909
+ try:
910
+ verts[z,y,x] = 1
911
+ except IndexError:
912
+ print(x, y, z)
913
+
914
+ temp_network = Network_3D(nodes = verts, edges = labeled_array, xy_scale = xy_scale, z_scale = z_scale)
915
+ temp_network.calculate_all(temp_network.nodes, temp_network.edges, xy_scale = temp_network.xy_scale, z_scale = temp_network.z_scale, search = None, diledge = None, inners = False, remove_trunk = 0, ignore_search_region = True, other_nodes = None, label_nodes = True, directory = None, GPU = False, fast_dil = False, skeletonize = False, GPU_downsample = None)
916
+ temp_network.calculate_node_centroids()
917
+ from itertools import combinations
918
+ for node in temp_network.network.nodes:
919
+ neighbors = list(temp_network.network.neighbors(node))
920
+
921
+ # Skip if fewer than 2 neighbors (endpoints or isolated nodes)
922
+ if len(neighbors) < 2:
923
+ continue
924
+
925
+ # Get all unique pairs of neighbors
926
+ neighbor_pairs = combinations(neighbors, 2)
927
+
928
+ angles = []
929
+ for neighbor1, neighbor2 in neighbor_pairs:
930
+ # Get coordinates from centroids
931
+ point_a = temp_network.node_centroids[neighbor1]
932
+ point_b = temp_network.node_centroids[node] # vertex
933
+ point_c = temp_network.node_centroids[neighbor2]
934
+
935
+ # Calculate angle
936
+ angle_result = calculate_3d_angle(point_a, point_b, point_c, xy_scale = xy_scale, z_scale = z_scale)
937
+ angles.append(angle_result)
938
+
939
+ angle_dict[node] = angles
940
+ """
781
941
 
942
+ return len_dict, tortuosity_dict, angle_dict
943
+
944
+ def calculate_3d_angle(point_a, point_b, point_c, xy_scale = 1, z_scale = 1):
945
+ """Calculate 3D angle at vertex B between points A-B-C."""
946
+ z1, y1, x1 = point_a
947
+ z2, y2, x2 = point_b # vertex
948
+ z3, y3, x3 = point_c
949
+
950
+ # Apply scaling
951
+ scaled_a = np.array([x1 * xy_scale, y1 * xy_scale, z1 * z_scale])
952
+ scaled_b = np.array([x2 * xy_scale, y2 * xy_scale, z2 * z_scale])
953
+ scaled_c = np.array([x3 * xy_scale, y3 * xy_scale, z3 * z_scale])
954
+
955
+ # Create vectors from vertex B
956
+ vec_ba = scaled_a - scaled_b
957
+ vec_bc = scaled_c - scaled_b
958
+
959
+ # Calculate angle using dot product
960
+ dot_product = np.dot(vec_ba, vec_bc)
961
+ magnitude_ba = np.linalg.norm(vec_ba)
962
+ magnitude_bc = np.linalg.norm(vec_bc)
963
+
964
+ # Avoid division by zero
965
+ if magnitude_ba == 0 or magnitude_bc == 0:
966
+ return {'angle_degrees': 0}
967
+
968
+ cos_angle = dot_product / (magnitude_ba * magnitude_bc)
969
+ cos_angle = np.clip(cos_angle, -1.0, 1.0) # Handle numerical errors
970
+
971
+ angle_radians = np.arccos(cos_angle)
972
+ angle_degrees = np.degrees(angle_radians)
973
+
974
+ return angle_degrees
782
975
 
783
976
  def threshold(arr, proportion, custom_rad = None):
784
977
 
@@ -907,9 +1100,12 @@ def show_3d(arrays_3d=None, arrays_4d=None, down_factor=None, order=0, xy_scale=
907
1100
  # Downsample arrays if specified
908
1101
  arrays_3d = [downsample(array, down_factor, order=order) for array in arrays_3d] if arrays_3d is not None else None
909
1102
  arrays_4d = [downsample(array, down_factor, order=order) for array in arrays_4d] if arrays_4d is not None else None
1103
+ scale = [z_scale * down_factor, xy_scale * down_factor, xy_scale * down_factor]
1104
+ else:
1105
+ scale = [z_scale, xy_scale, xy_scale]
1106
+
910
1107
 
911
1108
  viewer = napari.Viewer(ndisplay=3)
912
- scale = [z_scale, xy_scale, xy_scale] # [z, y, x] order for napari
913
1109
 
914
1110
  # Add 3D arrays if provided
915
1111
  if arrays_3d is not None:
@@ -2067,8 +2263,6 @@ def binarize(arrayimage, directory = None):
2067
2263
 
2068
2264
  arrayimage = arrayimage != 0
2069
2265
 
2070
- arrayimage = arrayimage.astype(np.uint8)
2071
-
2072
2266
  arrayimage = arrayimage * 255
2073
2267
 
2074
2268
  if type(arrayimage) == str:
@@ -2079,7 +2273,7 @@ def binarize(arrayimage, directory = None):
2079
2273
  tifffile.imwrite(f"{directory}/binary.tif", arrayimage)
2080
2274
 
2081
2275
 
2082
- return arrayimage
2276
+ return arrayimage.astype(np.uint8)
2083
2277
 
2084
2278
  def dilate(arrayimage, amount, xy_scale = 1, z_scale = 1, directory = None, fast_dil = False, recursive = False, dilate_xy = None, dilate_z = None):
2085
2279
  """
@@ -2129,7 +2323,7 @@ def erode(arrayimage, amount, xy_scale = 1, z_scale = 1, mode = 0, preserve_labe
2129
2323
  arrayimage = binarize(arrayimage)
2130
2324
  erode_xy, erode_z = dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
2131
2325
 
2132
- if mode == 0:
2326
+ if mode == 2:
2133
2327
  arrayimage = (erode_3D(arrayimage, erode_xy, erode_xy, erode_z)) * 255
2134
2328
  else:
2135
2329
  arrayimage = erode_3D_dt(arrayimage, amount, xy_scaling=xy_scale, z_scaling=z_scale, preserve_labels = preserve_labels)
@@ -2184,7 +2378,7 @@ def skeletonize(arrayimage, directory = None):
2184
2378
 
2185
2379
  return arrayimage
2186
2380
 
2187
- def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol = 0, down_factor = None, directory = None, nodes = None, bonus_array = None, GPU = True, arrayshape = None):
2381
+ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol = 0, down_factor = None, directory = None, nodes = None, bonus_array = None, GPU = True, arrayshape = None, compute = False, unify = False, union_val = 10, xy_scale = 1, z_scale = 1):
2188
2382
  """
2189
2383
  Can be used to label branches a binary image. Labelled output will be saved to the active directory if none is specified. Note this works better on already thin filaments and may over-divide larger trunkish objects.
2190
2384
  :param array: (Mandatory, string or ndarray) - If string, a path to a tif file to label. Note that the ndarray alternative is for internal use mainly and will not save its output.
@@ -2208,26 +2402,31 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
2208
2402
  else:
2209
2403
  arrayshape = arrayshape
2210
2404
 
2211
-
2212
2405
  if nodes is None:
2213
2406
 
2214
2407
  array = array > 0
2215
2408
 
2216
2409
  other_array = skeletonize(array)
2217
2410
 
2218
- other_array = break_and_label_skeleton(other_array, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, nodes = nodes)
2411
+ other_array, verts, skele, endpoints = break_and_label_skeleton(other_array, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, nodes = nodes, compute = compute, unify = unify, xy_scale = xy_scale, z_scale = z_scale)
2219
2412
 
2220
2413
  else:
2221
- array = break_and_label_skeleton(array, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, nodes = nodes)
2414
+ if down_factor is not None:
2415
+ bonus_array = downsample(bonus_array, down_factor)
2416
+ array, verts, skele, endpoints = break_and_label_skeleton(array, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, nodes = nodes, compute = compute, unify = unify, xy_scale = xy_scale, z_scale = z_scale)
2417
+
2418
+ if unify is True and nodes is not None:
2419
+ from . import branch_stitcher
2420
+ verts = dilate_3D_old(verts, 3, 3, 3,)
2421
+ verts, _ = label_objects(verts)
2422
+ array = branch_stitcher.trace(bonus_array, array, verts, score_thresh = union_val)
2423
+ verts = None
2222
2424
 
2223
- if nodes is not None and down_factor is not None:
2224
- array = upsample_with_padding(array, down_factor, arrayshape)
2225
2425
 
2226
2426
  if nodes is None:
2227
2427
 
2228
2428
  array = smart_dilate.smart_label(array, other_array, GPU = GPU, remove_template = True)
2229
2429
  #distance = smart_dilate.compute_distance_transform_distance(array)
2230
- print("Watershedding result...")
2231
2430
  #array = water(-distance, other_array, mask=array) #Tried out skimage watershed as shown and found it did not label branches as well as smart_label (esp combined combined with post-processing label splitting if needed)
2232
2431
 
2233
2432
  else:
@@ -2256,8 +2455,11 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
2256
2455
  else:
2257
2456
  print("Branches labelled")
2258
2457
 
2458
+ if nodes is not None and down_factor is not None:
2459
+ array = upsample_with_padding(array, down_factor, arrayshape)
2259
2460
 
2260
- return array
2461
+
2462
+ return array, verts, skele, endpoints
2261
2463
 
2262
2464
  def fix_branches_network(array, G, communities, fix_val = None):
2263
2465
 
@@ -2377,7 +2579,7 @@ def label_vertices(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
2377
2579
  else:
2378
2580
  broken_skele = None
2379
2581
 
2380
- if down_factor > 0:
2582
+ if down_factor > 1:
2381
2583
  array_shape = array.shape
2382
2584
  array = downsample(array, down_factor, order)
2383
2585
  if order == 3:
@@ -2927,7 +3129,7 @@ class Network_3D:
2927
3129
  for _ in range(weight):
2928
3130
  lista.append(u)
2929
3131
  listb.append(v)
2930
- listc.append(weight)
3132
+ listc.append(0)
2931
3133
 
2932
3134
  self._network_lists = [lista, listb, listc]
2933
3135
 
@@ -3161,7 +3363,14 @@ class Network_3D:
3161
3363
  if directory is None:
3162
3364
  try:
3163
3365
  if len(self._nodes.shape) == 3:
3164
- tifffile.imwrite(f"{filename}", self._nodes, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3366
+ try:
3367
+ tifffile.imwrite(f"{filename}", self._nodes, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3368
+ except:
3369
+ try:
3370
+ tifffile.imwrite(f"{filename}", self._nodes)
3371
+ except:
3372
+ self._nodes = binarize(self._nodes)
3373
+ tifffile.imwrite(f"{filename}", self._nodes, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3165
3374
  else:
3166
3375
  tifffile.imwrite(f"{filename}", self._nodes)
3167
3376
  print(f"Nodes saved to {filename}")
@@ -3170,9 +3379,16 @@ class Network_3D:
3170
3379
  if directory is not None:
3171
3380
  try:
3172
3381
  if len(self._nodes.shape) == 3:
3173
- tifffile.imwrite(f"{directory}/{filename}", self._nodes, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3382
+ try:
3383
+ tifffile.imwrite(f"{directory}/{filename}", self._nodes, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3384
+ except:
3385
+ try:
3386
+ tifffile.imwrite(f"{directory}/{filename}", self._nodes)
3387
+ except:
3388
+ self._nodes = binarize(self._nodes)
3389
+ tifffile.imwrite(f"{directory}/{filename}", self._nodes, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3174
3390
  else:
3175
- tifffile.imwrite(f"{directory}/{filename}")
3391
+ tifffile.imwrite(f"{directory}/{filename}", self._nodes)
3176
3392
  print(f"Nodes saved to {directory}/{filename}")
3177
3393
  except Exception as e:
3178
3394
  print(f"Could not save nodes to {directory}")
@@ -3202,11 +3418,25 @@ class Network_3D:
3202
3418
 
3203
3419
  if self._edges is not None:
3204
3420
  if directory is None:
3205
- tifffile.imwrite(f"{filename}", self._edges, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3421
+ try:
3422
+ tifffile.imwrite(f"{filename}", self._edges, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3423
+ except:
3424
+ try:
3425
+ tifffile.imwrite(f"{filename}", self._edges)
3426
+ except:
3427
+ self._edges = binarize(self._edges)
3428
+ tifffile.imwrite(f"{filename}", self._edges, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3206
3429
  print(f"Edges saved to {filename}")
3207
3430
 
3208
3431
  if directory is not None:
3209
- tifffile.imwrite(f"{directory}/{filename}", self._edges, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3432
+ try:
3433
+ tifffile.imwrite(f"{directory}/{filename}", self._edges, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3434
+ except:
3435
+ try:
3436
+ tifffile.imwrite(f"{directory}/{filename}", self._edges)
3437
+ except:
3438
+ self._edges = binarize(self._edges)
3439
+ tifffile.imwrite(f"{directory}/{filename}", self._edges, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3210
3440
  print(f"Edges saved to {directory}/{filename}")
3211
3441
 
3212
3442
  if self._edges is None:
@@ -3379,14 +3609,28 @@ class Network_3D:
3379
3609
  if self._network_overlay is not None:
3380
3610
  if directory is None:
3381
3611
  if len(self._network_overlay.shape) == 3:
3382
- tifffile.imwrite(f"{filename}", self._network_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3612
+ try:
3613
+ tifffile.imwrite(f"{filename}", self._network_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3614
+ except:
3615
+ try:
3616
+ tifffile.imwrite(f"{filename}", self._network_overlay)
3617
+ except:
3618
+ self._network_overlay = binarize(self._network_overlay)
3619
+ tifffile.imwrite(f"{filename}", self._network_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3383
3620
  else:
3384
3621
  tifffile.imwrite(f"{filename}", self._network_overlay)
3385
3622
  print(f"Network overlay saved to {filename}")
3386
3623
 
3387
3624
  if directory is not None:
3388
3625
  if len(self._network_overlay.shape) == 3:
3389
- tifffile.imwrite(f"{directory}/{filename}", self._network_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3626
+ try:
3627
+ tifffile.imwrite(f"{directory}/{filename}", self._network_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3628
+ except:
3629
+ try:
3630
+ tifffile.imwrite(f"{directory}/{filename}", self._network_overlay)
3631
+ except:
3632
+ self._network_overlay = binarize(self._network_overlay)
3633
+ tifffile.imwrite(f"{directory}/{filename}", self._network_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3390
3634
  else:
3391
3635
  tifffile.imwrite(f"{directory}/{filename}", self._network_overlay)
3392
3636
  print(f"Network overlay saved to {directory}/{filename}")
@@ -3410,14 +3654,28 @@ class Network_3D:
3410
3654
  if self._id_overlay is not None:
3411
3655
  if directory is None:
3412
3656
  if len(self._id_overlay.shape) == 3:
3413
- tifffile.imwrite(f"{filename}", self._id_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3657
+ try:
3658
+ tifffile.imwrite(f"{filename}", self._id_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3659
+ except:
3660
+ try:
3661
+ tifffile.imwrite(f"{filename}", self._id_overlay)
3662
+ except:
3663
+ self._id_overlay = binarize(self._id_overlay)
3664
+ tifffile.imwrite(f"{filename}", self._id_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3414
3665
  else:
3415
3666
  tifffile.imwrite(f"{filename}", self._id_overlay, imagej=True)
3416
3667
  print(f"Network overlay saved to {filename}")
3417
3668
 
3418
3669
  if directory is not None:
3419
3670
  if len(self._id_overlay.shape) == 3:
3420
- tifffile.imwrite(f"{directory}/{filename}", self._id_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3671
+ try:
3672
+ tifffile.imwrite(f"{directory}/{filename}", self._id_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3673
+ except:
3674
+ try:
3675
+ tifffile.imwrite(f"{directory}/{filename}", self._id_overlay)
3676
+ except:
3677
+ self._id_overlay = binarize(self._id_overlay)
3678
+ tifffile.imwrite(f"{directory}/{filename}", self._id_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
3421
3679
  else:
3422
3680
  tifffile.imwrite(f"{directory}/{filename}", self._id_overlay)
3423
3681
  print(f"ID overlay saved to {directory}/{filename}")
@@ -4153,6 +4411,88 @@ class Network_3D:
4153
4411
  self._network_lists = network_analysis.read_excel_to_lists(df)
4154
4412
  self._network, net_weights = network_analysis.weighted_network(df)
4155
4413
 
4414
+ def create_id_network(self, n=5):
4415
+ import ast
4416
+ import random
4417
+
4418
+ if self.node_identities is None:
4419
+ return
4420
+
4421
+ def invert_dict(d):
4422
+ inverted = {}
4423
+ for key, value in d.items():
4424
+ inverted.setdefault(value, []).append(key)
4425
+ return inverted
4426
+
4427
+ # Invert to get identity -> list of nodes
4428
+ identity_to_nodes = invert_dict(self.node_identities)
4429
+
4430
+ G = nx.Graph()
4431
+ edge_set = set()
4432
+
4433
+ # Step 1: Connect nodes within same exact identity
4434
+ for identity, nodes in identity_to_nodes.items():
4435
+ if len(nodes) <= 1:
4436
+ continue
4437
+
4438
+ # Each node chooses n random neighbors from its identity group
4439
+ for node in nodes:
4440
+ available = [other for other in nodes if other != node]
4441
+ num_to_choose = min(n, len(available))
4442
+ neighbors = random.sample(available, num_to_choose)
4443
+
4444
+ for neighbor in neighbors:
4445
+ edge = tuple(sorted([node, neighbor]))
4446
+ edge_set.add(edge)
4447
+
4448
+ # Step 2: For list-like identities, connect across groups with shared sub-identities
4449
+ for identity, nodes in identity_to_nodes.items():
4450
+ if identity.startswith('['):
4451
+ try:
4452
+ sub_identities = ast.literal_eval(identity)
4453
+
4454
+ # For each sub-identity in this list-like identity
4455
+ for sub_id in sub_identities:
4456
+ # Find all OTHER identity groups that contain this sub-identity
4457
+ for other_identity, other_nodes in identity_to_nodes.items():
4458
+ if other_identity == identity:
4459
+ continue # Skip connecting to same exact identity (already done in Step 1)
4460
+
4461
+ # Check if other_identity contains sub_id
4462
+ contains_sub_id = False
4463
+
4464
+ if other_identity.startswith('['):
4465
+ try:
4466
+ other_sub_ids = ast.literal_eval(other_identity)
4467
+ if sub_id in other_sub_ids:
4468
+ contains_sub_id = True
4469
+ except (ValueError, SyntaxError):
4470
+ pass
4471
+ elif other_identity == sub_id:
4472
+ # Single identity that matches our sub-identity
4473
+ contains_sub_id = True
4474
+
4475
+ if contains_sub_id:
4476
+ # Each node from current identity connects to n nodes from other_identity
4477
+ for node in nodes:
4478
+ num_to_choose = min(n, len(other_nodes))
4479
+ if num_to_choose > 0:
4480
+ neighbors = random.sample(other_nodes, num_to_choose)
4481
+
4482
+ for neighbor in neighbors:
4483
+ edge = tuple(sorted([node, neighbor]))
4484
+ edge_set.add(edge)
4485
+
4486
+ except (ValueError, SyntaxError):
4487
+ pass # Not a valid list, treat as already handled in Step 1
4488
+
4489
+ G.add_edges_from(edge_set)
4490
+ self.network = G
4491
+
4492
+
4493
+
4494
+
4495
+
4156
4496
  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):
4157
4497
  """
4158
4498
  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.
@@ -5222,14 +5562,14 @@ class Network_3D:
5222
5562
  except:
5223
5563
  pass
5224
5564
 
5225
- title1 = f'Neighborhood Distribution of Nodes in Network from Nodes: {root}'
5226
- title2 = f'Neighborhood Distribution of Nodes in Network from Nodes {root} as a proportion of total nodes of that ID'
5565
+ title1 = f'Neighborhood Distribution of Nodes in Network from Node Type: {root}'
5566
+ title2 = f'Neighborhood Distribution of Nodes in Network from Node Type {root} as a Proportion (# neighbors with ID x / Total # ID x)'
5227
5567
 
5228
5568
 
5229
5569
  elif mode == 1: #Search neighborhoods morphologically, obtain densities
5230
5570
  neighborhood_dict, total_dict, densities = morphology.search_neighbor_ids(self._nodes, targets, node_identities, neighborhood_dict, total_dict, search, self._xy_scale, self._z_scale, root, fastdil = fastdil)
5231
- title1 = f'Volumetric Neighborhood Distribution of Nodes in image that are {search} from nodes: {root}'
5232
- title2 = f'Density Distribution of Nodes in image that are {search} from Nodes {root} as a proportion of total node volume of that ID'
5571
+ title1 = f'Volumetric Neighborhood Distribution of Nodes in image that are {search} from Node Type: {root}'
5572
+ title2 = f'Density Distribution of Nodes in image that are {search} from Node Type {root} as a proportion (Vol neighors with ID x / Total vol ID x)'
5233
5573
 
5234
5574
 
5235
5575
  for identity in neighborhood_dict:
@@ -5240,7 +5580,7 @@ class Network_3D:
5240
5580
  network_analysis.create_bar_graph(proportion_dict, title2, "Node Identity", "Proportion", directory=directory)
5241
5581
 
5242
5582
  try:
5243
- network_analysis.create_bar_graph(densities, f'Clustering Factor of Node Identities with {search} from nodes {root}', "Node Identity", "Density Search/Density Total", directory=directory)
5583
+ network_analysis.create_bar_graph(densities, f'Relative Density of Node Identities with {search} from Node Type {root}', "Node Identity", "Density within search region/Density within entire image", directory=directory)
5244
5584
  except:
5245
5585
  densities = None
5246
5586
 
@@ -5470,7 +5810,6 @@ class Network_3D:
5470
5810
  else:
5471
5811
  search_x, search_z = dilation_length_to_pixels(self._xy_scale, self._z_scale, search, search)
5472
5812
 
5473
-
5474
5813
  num_nodes = int(np.max(self._nodes))
5475
5814
 
5476
5815
  my_dict = proximity.create_node_dictionary(self._nodes, num_nodes, search_x, search_z, targets = targets, fastdil = fastdil, xy_scale = self._xy_scale, z_scale = self._z_scale, search = search)
@@ -5630,7 +5969,7 @@ class Network_3D:
5630
5969
  neighborhoods.visualize_cluster_composition_umap(self.node_centroids, None, id_dictionary = self.node_identities, graph_label = "Node ID", title = 'UMAP Visualization of Node Centroids')
5631
5970
 
5632
5971
 
5633
- def identity_umap(self, data):
5972
+ def identity_umap(self, data, mode = 0):
5634
5973
 
5635
5974
  try:
5636
5975
 
@@ -5650,16 +5989,18 @@ class Network_3D:
5650
5989
  else:
5651
5990
  del umap_dict[item]
5652
5991
 
5653
- from scipy.stats import zscore
5992
+ #from scipy.stats import zscore
5654
5993
 
5655
5994
  # Z-score normalize each marker (column)
5656
- for key in umap_dict:
5657
- umap_dict[key] = zscore(umap_dict[key])
5658
-
5995
+ #for key in umap_dict:
5996
+ #umap_dict[key] = zscore(umap_dict[key])
5659
5997
 
5660
5998
  from . import neighborhoods
5661
5999
 
5662
- 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')
6000
+ if mode == 0:
6001
+ 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')
6002
+ else:
6003
+ 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', neighborhoods = self.communities, original_communities = self.communities)
5663
6004
 
5664
6005
  except Exception as e:
5665
6006
  import traceback
@@ -5778,7 +6119,6 @@ class Network_3D:
5778
6119
  neighbor_group[com] = neighbors[node]
5779
6120
  except:
5780
6121
  neighbor_group[com] = 0
5781
- print(neighbors)
5782
6122
  neighborhoods.visualize_cluster_composition_umap(umap_dict, id_set, neighborhoods = neighbor_group, original_communities = neighbors)
5783
6123
  elif label == 1:
5784
6124
  neighborhoods.visualize_cluster_composition_umap(umap_dict, id_set, label = True)
@@ -5791,6 +6131,19 @@ class Network_3D:
5791
6131
  return output, id_set
5792
6132
 
5793
6133
 
6134
+ def group_nodes_by_intensity(self, data, count = None):
6135
+
6136
+ from . import neighborhoods
6137
+
6138
+ clusters = neighborhoods.cluster_arrays(data, count, seed = 42)
6139
+
6140
+ coms = {}
6141
+
6142
+ for i, cluster in enumerate(clusters):
6143
+ coms[i + 1] = cluster
6144
+
6145
+ self.communities = revert_dict(coms)
6146
+
5794
6147
  def assign_neighborhoods(self, seed, count, limit = None, prev_coms = None, proportional = False, mode = 0):
5795
6148
 
5796
6149
  from . import neighborhoods
@@ -5882,14 +6235,14 @@ class Network_3D:
5882
6235
  if proportional:
5883
6236
 
5884
6237
  identities2, id_set2 = self.community_id_info_per_com(proportional = True)
5885
- output = neighborhoods.plot_dict_heatmap(identities2, id_set2, title = "Neighborhood Heatmap by Proportional Composition of Nodes in Neighborhood vs All Nodes")
6238
+ output = neighborhoods.plot_dict_heatmap(identities2, id_set2, title = "Neighborhood Heatmap by Proportional Composition of Nodes in Neighborhood vs All Nodes in Image")
5886
6239
  matrixes.append(output)
5887
6240
 
5888
6241
  identities3 = {}
5889
6242
  for iden in identities2:
5890
6243
  identities3[iden] = identities2[iden]/len_dict[iden][1]
5891
6244
 
5892
- output = neighborhoods.plot_dict_heatmap(identities3, id_set2, title = "Neighborhood Heatmap by Proportional Composition of Nodes in Neighborhood vs All Nodes Divided by Neighborhood Total Proportion of All Nodes (val < 1 = underrepresented, val > 1 = overrepresented)", center_at_one = True)
6245
+ output = neighborhoods.plot_dict_heatmap(identities3, id_set2, title = "Over/Underrepresentation of Node Identities per Neighborhood (val < 1 = underrepresented, val > 1 = overrepresented)", center_at_one = True)
5893
6246
  matrixes.append(output)
5894
6247
 
5895
6248
  return len_dict, matrixes, id_set
@@ -5941,13 +6294,6 @@ class Network_3D:
5941
6294
 
5942
6295
  def community_cells(self, size = 32, xy_scale = 1, z_scale = 1):
5943
6296
 
5944
- def revert_dict(d):
5945
- inverted = {}
5946
- for key, value_list in d.items():
5947
- for value in value_list:
5948
- inverted[value] = key
5949
- return inverted
5950
-
5951
6297
  size_x = int(size * xy_scale)
5952
6298
  size_z = int(size * z_scale)
5953
6299