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/branch_stitcher.py +420 -0
- nettracer3d/filaments.py +1068 -0
- nettracer3d/morphology.py +9 -4
- nettracer3d/neighborhoods.py +99 -67
- nettracer3d/nettracer.py +398 -52
- nettracer3d/nettracer_gui.py +1746 -483
- nettracer3d/network_draw.py +9 -3
- nettracer3d/node_draw.py +41 -58
- nettracer3d/proximity.py +123 -2
- nettracer3d/smart_dilate.py +36 -0
- nettracer3d/tutorial.py +2874 -0
- {nettracer3d-1.1.1.dist-info → nettracer3d-1.2.4.dist-info}/METADATA +6 -6
- nettracer3d-1.2.4.dist-info/RECORD +29 -0
- nettracer3d-1.1.1.dist-info/RECORD +0 -26
- {nettracer3d-1.1.1.dist-info → nettracer3d-1.2.4.dist-info}/WHEEL +0 -0
- {nettracer3d-1.1.1.dist-info → nettracer3d-1.2.4.dist-info}/entry_points.txt +0 -0
- {nettracer3d-1.1.1.dist-info → nettracer3d-1.2.4.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-1.1.1.dist-info → nettracer3d-1.2.4.dist-info}/top_level.txt +0 -0
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
|
-
|
|
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
|
-
|
|
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 ==
|
|
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
|
-
|
|
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
|
-
|
|
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 >
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5226
|
-
title2 = f'Neighborhood Distribution of Nodes in Network from
|
|
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
|
|
5232
|
-
title2 = f'Density Distribution of Nodes in image that are {search} from
|
|
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'
|
|
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
|
-
|
|
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 = "
|
|
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
|
|