nettracer3d 0.9.9__py3-none-any.whl → 1.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nettracer3d/community_extractor.py +24 -8
- nettracer3d/morphology.py +113 -17
- nettracer3d/neighborhoods.py +201 -66
- nettracer3d/nettracer.py +516 -103
- nettracer3d/nettracer_gui.py +2072 -592
- nettracer3d/network_draw.py +9 -3
- nettracer3d/node_draw.py +41 -58
- nettracer3d/segmenter.py +67 -25
- nettracer3d/segmenter_GPU.py +67 -29
- nettracer3d/stats.py +861 -0
- {nettracer3d-0.9.9.dist-info → nettracer3d-1.1.5.dist-info}/METADATA +3 -4
- nettracer3d-1.1.5.dist-info/RECORD +26 -0
- nettracer3d-0.9.9.dist-info/RECORD +0 -25
- {nettracer3d-0.9.9.dist-info → nettracer3d-1.1.5.dist-info}/WHEEL +0 -0
- {nettracer3d-0.9.9.dist-info → nettracer3d-1.1.5.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.9.9.dist-info → nettracer3d-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.9.9.dist-info → nettracer3d-1.1.5.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer.py
CHANGED
|
@@ -35,6 +35,7 @@ from . import proximity
|
|
|
35
35
|
from skimage.segmentation import watershed as water
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
|
|
38
39
|
#These next several methods relate to searching with 3D objects by dilating each one in a subarray around their neighborhood although I don't explicitly use this anywhere... can call them deprecated although I may want to use them later again so I have them still written out here.
|
|
39
40
|
|
|
40
41
|
|
|
@@ -383,6 +384,13 @@ def invert_dict(d):
|
|
|
383
384
|
inverted.setdefault(value, []).append(key)
|
|
384
385
|
return inverted
|
|
385
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
|
+
|
|
386
394
|
def invert_dict_special(d):
|
|
387
395
|
|
|
388
396
|
d = invert_dict(d)
|
|
@@ -737,7 +745,7 @@ def estimate_object_radii(labeled_array, gpu=False, n_jobs=None, xy_scale = 1, z
|
|
|
737
745
|
return morphology.estimate_object_radii_cpu(labeled_array, n_jobs, xy_scale = xy_scale, z_scale = z_scale)
|
|
738
746
|
|
|
739
747
|
|
|
740
|
-
def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil = 0, max_vol = 0, directory = None, return_skele = False, nodes = None):
|
|
748
|
+
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, xy_scale = 1, z_scale = 1):
|
|
741
749
|
"""Internal method to break open a skeleton at its branchpoints and label the remaining components, for an 8bit binary array"""
|
|
742
750
|
|
|
743
751
|
if type(skeleton) == str:
|
|
@@ -746,18 +754,28 @@ def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil =
|
|
|
746
754
|
else:
|
|
747
755
|
broken_skele = None
|
|
748
756
|
|
|
749
|
-
#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
|
|
750
|
-
|
|
751
757
|
if nodes is None:
|
|
752
758
|
|
|
753
|
-
verts = label_vertices(skeleton, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, return_skele = return_skele)
|
|
759
|
+
verts = label_vertices(skeleton, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, return_skele = return_skele, compute = compute)
|
|
754
760
|
|
|
755
761
|
else:
|
|
756
762
|
verts = nodes
|
|
757
763
|
|
|
758
764
|
verts = invert_array(verts)
|
|
759
765
|
|
|
760
|
-
|
|
766
|
+
"""
|
|
767
|
+
if compute: # We are interested in the endpoints if we are doing the optional computation later
|
|
768
|
+
endpoints = []
|
|
769
|
+
image_copy = np.pad(skeleton, pad_width=1, mode='constant', constant_values=0)
|
|
770
|
+
nonzero_coords = np.transpose(np.nonzero(image_copy))
|
|
771
|
+
for x, y, z in nonzero_coords:
|
|
772
|
+
mini = image_copy[x-1:x+2, y-1:y+2, z-1:z+2]
|
|
773
|
+
nearby_sum = np.sum(mini)
|
|
774
|
+
threshold = 2 * image_copy[x, y, z]
|
|
775
|
+
|
|
776
|
+
if nearby_sum <= threshold:
|
|
777
|
+
endpoints.append((x, y, z))
|
|
778
|
+
"""
|
|
761
779
|
|
|
762
780
|
image_copy = skeleton * verts
|
|
763
781
|
|
|
@@ -775,9 +793,137 @@ def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil =
|
|
|
775
793
|
tifffile.imwrite(filename, labeled_image, photometric='minisblack')
|
|
776
794
|
print(f"Broken skeleton saved to {filename}")
|
|
777
795
|
|
|
778
|
-
|
|
796
|
+
if compute:
|
|
797
|
+
|
|
798
|
+
return labeled_image, None, skeleton, None
|
|
779
799
|
|
|
800
|
+
return labeled_image, None, None, None
|
|
780
801
|
|
|
802
|
+
def compute_optional_branchstats(verts, labeled_array, endpoints, xy_scale = 1, z_scale = 1):
|
|
803
|
+
|
|
804
|
+
#Lengths:
|
|
805
|
+
# Get all non-background coordinates and their labels in one pass
|
|
806
|
+
z, y, x = np.where(labeled_array != 0)
|
|
807
|
+
labels = labeled_array[z, y, x]
|
|
808
|
+
|
|
809
|
+
# Sort by label
|
|
810
|
+
sort_idx = np.argsort(labels)
|
|
811
|
+
labels_sorted = labels[sort_idx]
|
|
812
|
+
z_sorted = z[sort_idx]
|
|
813
|
+
y_sorted = y[sort_idx]
|
|
814
|
+
x_sorted = x[sort_idx]
|
|
815
|
+
|
|
816
|
+
# Find where each label starts
|
|
817
|
+
unique_labels, split_idx = np.unique(labels_sorted, return_index=True)
|
|
818
|
+
split_idx = split_idx[1:] # Remove first index for np.split
|
|
819
|
+
|
|
820
|
+
# Split into groups
|
|
821
|
+
z_split = np.split(z_sorted, split_idx)
|
|
822
|
+
y_split = np.split(y_sorted, split_idx)
|
|
823
|
+
x_split = np.split(x_sorted, split_idx)
|
|
824
|
+
|
|
825
|
+
# Build dict
|
|
826
|
+
coords_dict = {label: np.column_stack([z, y, x])
|
|
827
|
+
for label, z, y, x in zip(unique_labels, z_split, y_split, x_split)}
|
|
828
|
+
|
|
829
|
+
from sklearn.neighbors import NearestNeighbors
|
|
830
|
+
from scipy.spatial.distance import pdist, squareform
|
|
831
|
+
len_dict = {}
|
|
832
|
+
tortuosity_dict = {}
|
|
833
|
+
angle_dict = {}
|
|
834
|
+
for label, coords in coords_dict.items():
|
|
835
|
+
len_dict[label] = morphology.calculate_skeleton_lengths(labeled_array.shape, xy_scale=xy_scale, z_scale=z_scale, skeleton_coords=coords)
|
|
836
|
+
|
|
837
|
+
# Find neighbors for all points at once
|
|
838
|
+
nbrs = NearestNeighbors(radius=1.74, algorithm='kd_tree').fit(coords)
|
|
839
|
+
neighbor_counts = nbrs.radius_neighbors(coords, return_distance=False)
|
|
840
|
+
neighbor_counts = np.array([len(n) - 1 for n in neighbor_counts]) # -1 to exclude self
|
|
841
|
+
|
|
842
|
+
# Endpoints have exactly 1 neighbor
|
|
843
|
+
endpoints = coords[neighbor_counts == 1]
|
|
844
|
+
|
|
845
|
+
if len(endpoints) > 1:
|
|
846
|
+
# Scale endpoints
|
|
847
|
+
scaled_endpoints = endpoints.copy().astype(float)
|
|
848
|
+
scaled_endpoints[:, 0] *= z_scale # z dimension
|
|
849
|
+
scaled_endpoints[:, 1] *= xy_scale # y dimension
|
|
850
|
+
scaled_endpoints[:, 2] *= xy_scale # x dimension
|
|
851
|
+
|
|
852
|
+
# calculate distances on scaled coordinates
|
|
853
|
+
distances = pdist(scaled_endpoints, metric='euclidean')
|
|
854
|
+
max_distance = distances.max()
|
|
855
|
+
|
|
856
|
+
tortuosity_dict[label] = len_dict[label]/max_distance
|
|
857
|
+
|
|
858
|
+
"""
|
|
859
|
+
verts = invert_array(verts)
|
|
860
|
+
for x, y, z in endpoints:
|
|
861
|
+
try:
|
|
862
|
+
verts[z,y,x] = 1
|
|
863
|
+
except IndexError:
|
|
864
|
+
print(x, y, z)
|
|
865
|
+
|
|
866
|
+
temp_network = Network_3D(nodes = verts, edges = labeled_array, xy_scale = xy_scale, z_scale = z_scale)
|
|
867
|
+
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)
|
|
868
|
+
temp_network.calculate_node_centroids()
|
|
869
|
+
from itertools import combinations
|
|
870
|
+
for node in temp_network.network.nodes:
|
|
871
|
+
neighbors = list(temp_network.network.neighbors(node))
|
|
872
|
+
|
|
873
|
+
# Skip if fewer than 2 neighbors (endpoints or isolated nodes)
|
|
874
|
+
if len(neighbors) < 2:
|
|
875
|
+
continue
|
|
876
|
+
|
|
877
|
+
# Get all unique pairs of neighbors
|
|
878
|
+
neighbor_pairs = combinations(neighbors, 2)
|
|
879
|
+
|
|
880
|
+
angles = []
|
|
881
|
+
for neighbor1, neighbor2 in neighbor_pairs:
|
|
882
|
+
# Get coordinates from centroids
|
|
883
|
+
point_a = temp_network.node_centroids[neighbor1]
|
|
884
|
+
point_b = temp_network.node_centroids[node] # vertex
|
|
885
|
+
point_c = temp_network.node_centroids[neighbor2]
|
|
886
|
+
|
|
887
|
+
# Calculate angle
|
|
888
|
+
angle_result = calculate_3d_angle(point_a, point_b, point_c, xy_scale = xy_scale, z_scale = z_scale)
|
|
889
|
+
angles.append(angle_result)
|
|
890
|
+
|
|
891
|
+
angle_dict[node] = angles
|
|
892
|
+
"""
|
|
893
|
+
|
|
894
|
+
return len_dict, tortuosity_dict, angle_dict
|
|
895
|
+
|
|
896
|
+
def calculate_3d_angle(point_a, point_b, point_c, xy_scale = 1, z_scale = 1):
|
|
897
|
+
"""Calculate 3D angle at vertex B between points A-B-C."""
|
|
898
|
+
z1, y1, x1 = point_a
|
|
899
|
+
z2, y2, x2 = point_b # vertex
|
|
900
|
+
z3, y3, x3 = point_c
|
|
901
|
+
|
|
902
|
+
# Apply scaling
|
|
903
|
+
scaled_a = np.array([x1 * xy_scale, y1 * xy_scale, z1 * z_scale])
|
|
904
|
+
scaled_b = np.array([x2 * xy_scale, y2 * xy_scale, z2 * z_scale])
|
|
905
|
+
scaled_c = np.array([x3 * xy_scale, y3 * xy_scale, z3 * z_scale])
|
|
906
|
+
|
|
907
|
+
# Create vectors from vertex B
|
|
908
|
+
vec_ba = scaled_a - scaled_b
|
|
909
|
+
vec_bc = scaled_c - scaled_b
|
|
910
|
+
|
|
911
|
+
# Calculate angle using dot product
|
|
912
|
+
dot_product = np.dot(vec_ba, vec_bc)
|
|
913
|
+
magnitude_ba = np.linalg.norm(vec_ba)
|
|
914
|
+
magnitude_bc = np.linalg.norm(vec_bc)
|
|
915
|
+
|
|
916
|
+
# Avoid division by zero
|
|
917
|
+
if magnitude_ba == 0 or magnitude_bc == 0:
|
|
918
|
+
return {'angle_degrees': 0}
|
|
919
|
+
|
|
920
|
+
cos_angle = dot_product / (magnitude_ba * magnitude_bc)
|
|
921
|
+
cos_angle = np.clip(cos_angle, -1.0, 1.0) # Handle numerical errors
|
|
922
|
+
|
|
923
|
+
angle_radians = np.arccos(cos_angle)
|
|
924
|
+
angle_degrees = np.degrees(angle_radians)
|
|
925
|
+
|
|
926
|
+
return angle_degrees
|
|
781
927
|
|
|
782
928
|
def threshold(arr, proportion, custom_rad = None):
|
|
783
929
|
|
|
@@ -803,7 +949,7 @@ def threshold(arr, proportion, custom_rad = None):
|
|
|
803
949
|
|
|
804
950
|
threshold_index = int(len(sorted_values) * proportion)
|
|
805
951
|
threshold_value = sorted_values[threshold_index]
|
|
806
|
-
print(f"Thresholding as if smallest_radius
|
|
952
|
+
print(f"Thresholding as if smallest_radius was assigned {threshold_value}")
|
|
807
953
|
|
|
808
954
|
|
|
809
955
|
mask = arr > threshold_value
|
|
@@ -906,9 +1052,12 @@ def show_3d(arrays_3d=None, arrays_4d=None, down_factor=None, order=0, xy_scale=
|
|
|
906
1052
|
# Downsample arrays if specified
|
|
907
1053
|
arrays_3d = [downsample(array, down_factor, order=order) for array in arrays_3d] if arrays_3d is not None else None
|
|
908
1054
|
arrays_4d = [downsample(array, down_factor, order=order) for array in arrays_4d] if arrays_4d is not None else None
|
|
1055
|
+
scale = [z_scale * down_factor, xy_scale * down_factor, xy_scale * down_factor]
|
|
1056
|
+
else:
|
|
1057
|
+
scale = [z_scale, xy_scale, xy_scale]
|
|
1058
|
+
|
|
909
1059
|
|
|
910
1060
|
viewer = napari.Viewer(ndisplay=3)
|
|
911
|
-
scale = [z_scale, xy_scale, xy_scale] # [z, y, x] order for napari
|
|
912
1061
|
|
|
913
1062
|
# Add 3D arrays if provided
|
|
914
1063
|
if arrays_3d is not None:
|
|
@@ -992,6 +1141,61 @@ def z_project(array3d, method='max'):
|
|
|
992
1141
|
raise ValueError("Method must be one of: 'max', 'mean', 'min', 'sum', 'std'")
|
|
993
1142
|
|
|
994
1143
|
def fill_holes_3d(array, head_on = False, fill_borders = True):
|
|
1144
|
+
def process_slice(slice_2d, border_threshold=0.08, fill_borders = True):
|
|
1145
|
+
"""
|
|
1146
|
+
Process a 2D slice, considering components that touch less than border_threshold
|
|
1147
|
+
of any border length as potential holes.
|
|
1148
|
+
|
|
1149
|
+
Args:
|
|
1150
|
+
slice_2d: 2D binary array
|
|
1151
|
+
border_threshold: proportion of border that must be touched to be considered background
|
|
1152
|
+
"""
|
|
1153
|
+
from scipy.ndimage import binary_fill_holes
|
|
1154
|
+
|
|
1155
|
+
slice_2d = slice_2d.astype(np.uint8)
|
|
1156
|
+
|
|
1157
|
+
# Apply scipy's binary_fill_holes to the result
|
|
1158
|
+
slice_2d = binary_fill_holes(slice_2d)
|
|
1159
|
+
|
|
1160
|
+
return slice_2d
|
|
1161
|
+
|
|
1162
|
+
print("Filling Holes...")
|
|
1163
|
+
|
|
1164
|
+
array = binarize(array)
|
|
1165
|
+
#inv_array = invert_array(array)
|
|
1166
|
+
|
|
1167
|
+
# Create arrays for all three planes
|
|
1168
|
+
array_xy = np.zeros_like(array, dtype=np.uint8)
|
|
1169
|
+
array_xz = np.zeros_like(array, dtype=np.uint8)
|
|
1170
|
+
array_yz = np.zeros_like(array, dtype=np.uint8)
|
|
1171
|
+
|
|
1172
|
+
# Process XY plane
|
|
1173
|
+
for z in range(array.shape[0]):
|
|
1174
|
+
array_xy[z] = process_slice(array[z], fill_borders = fill_borders)
|
|
1175
|
+
|
|
1176
|
+
if (array.shape[0] > 3) and not head_on: #only use these dimensions for sufficiently large zstacks
|
|
1177
|
+
|
|
1178
|
+
# Process XZ plane
|
|
1179
|
+
for y in range(array.shape[1]):
|
|
1180
|
+
slice_xz = array[:, y, :]
|
|
1181
|
+
array_xz[:, y, :] = process_slice(slice_xz, fill_borders = fill_borders)
|
|
1182
|
+
|
|
1183
|
+
# Process YZ plane
|
|
1184
|
+
for x in range(array.shape[2]):
|
|
1185
|
+
slice_yz = array[:, :, x]
|
|
1186
|
+
array_yz[:, :, x] = process_slice(slice_yz, fill_borders = fill_borders)
|
|
1187
|
+
|
|
1188
|
+
# Combine results from all three planes
|
|
1189
|
+
filled = (array_xy | array_xz | array_yz) * 255
|
|
1190
|
+
return array + filled
|
|
1191
|
+
else:
|
|
1192
|
+
# Apply scipy's binary_fill_holes to each XY slice
|
|
1193
|
+
from scipy.ndimage import binary_fill_holes
|
|
1194
|
+
for z in range(array_xy.shape[0]):
|
|
1195
|
+
array_xy[z] = binary_fill_holes(array_xy[z])
|
|
1196
|
+
return array_xy * 255
|
|
1197
|
+
|
|
1198
|
+
def fill_holes_3d_old(array, head_on = False, fill_borders = True):
|
|
995
1199
|
|
|
996
1200
|
def process_slice(slice_2d, border_threshold=0.08, fill_borders = True):
|
|
997
1201
|
"""
|
|
@@ -1847,6 +2051,7 @@ def mirror_points_for_edge_correction(points_array, bounds, max_r, dim=3):
|
|
|
1847
2051
|
all_points = np.vstack([all_points, mirrored_points])
|
|
1848
2052
|
|
|
1849
2053
|
return all_points
|
|
2054
|
+
|
|
1850
2055
|
def get_max_r_from_proportion(bounds, proportion):
|
|
1851
2056
|
"""
|
|
1852
2057
|
Calculate max_r based on bounds and proportion, matching your generate_r_values logic.
|
|
@@ -2072,7 +2277,7 @@ def erode(arrayimage, amount, xy_scale = 1, z_scale = 1, mode = 0, preserve_labe
|
|
|
2072
2277
|
arrayimage = binarize(arrayimage)
|
|
2073
2278
|
erode_xy, erode_z = dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
|
|
2074
2279
|
|
|
2075
|
-
if mode ==
|
|
2280
|
+
if mode == 2:
|
|
2076
2281
|
arrayimage = (erode_3D(arrayimage, erode_xy, erode_xy, erode_z)) * 255
|
|
2077
2282
|
else:
|
|
2078
2283
|
arrayimage = erode_3D_dt(arrayimage, amount, xy_scaling=xy_scale, z_scaling=z_scale, preserve_labels = preserve_labels)
|
|
@@ -2127,7 +2332,7 @@ def skeletonize(arrayimage, directory = None):
|
|
|
2127
2332
|
|
|
2128
2333
|
return arrayimage
|
|
2129
2334
|
|
|
2130
|
-
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):
|
|
2335
|
+
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, xy_scale = 1, z_scale = 1):
|
|
2131
2336
|
"""
|
|
2132
2337
|
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.
|
|
2133
2338
|
: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.
|
|
@@ -2158,10 +2363,10 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
|
|
|
2158
2363
|
|
|
2159
2364
|
other_array = skeletonize(array)
|
|
2160
2365
|
|
|
2161
|
-
other_array = break_and_label_skeleton(other_array, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, nodes = nodes)
|
|
2366
|
+
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, xy_scale = xy_scale, z_scale = z_scale)
|
|
2162
2367
|
|
|
2163
2368
|
else:
|
|
2164
|
-
array = break_and_label_skeleton(array, peaks = peaks, branch_removal = branch_removal, comp_dil = comp_dil, max_vol = max_vol, nodes = nodes)
|
|
2369
|
+
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, xy_scale = xy_scale, z_scale = z_scale)
|
|
2165
2370
|
|
|
2166
2371
|
if nodes is not None and down_factor is not None:
|
|
2167
2372
|
array = upsample_with_padding(array, down_factor, arrayshape)
|
|
@@ -2200,7 +2405,7 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
|
|
|
2200
2405
|
print("Branches labelled")
|
|
2201
2406
|
|
|
2202
2407
|
|
|
2203
|
-
return array
|
|
2408
|
+
return array, verts, skele, endpoints
|
|
2204
2409
|
|
|
2205
2410
|
def fix_branches_network(array, G, communities, fix_val = None):
|
|
2206
2411
|
|
|
@@ -2870,7 +3075,7 @@ class Network_3D:
|
|
|
2870
3075
|
for _ in range(weight):
|
|
2871
3076
|
lista.append(u)
|
|
2872
3077
|
listb.append(v)
|
|
2873
|
-
listc.append(
|
|
3078
|
+
listc.append(0)
|
|
2874
3079
|
|
|
2875
3080
|
self._network_lists = [lista, listb, listc]
|
|
2876
3081
|
|
|
@@ -3086,6 +3291,15 @@ class Network_3D:
|
|
|
3086
3291
|
Can be called on a Network_3D object to save the nodes property to hard mem as a tif. It will save to the active directory if none is specified.
|
|
3087
3292
|
:param directory: (Optional - Val = None; String). The path to an indended directory to save the nodes to.
|
|
3088
3293
|
"""
|
|
3294
|
+
if self._nodes is not None:
|
|
3295
|
+
imagej_metadata = {
|
|
3296
|
+
'spacing': self.z_scale,
|
|
3297
|
+
'slices': self._nodes.shape[0],
|
|
3298
|
+
'channels': 1,
|
|
3299
|
+
'axes': 'ZYX'
|
|
3300
|
+
}
|
|
3301
|
+
resolution_value = 1.0 / self.xy_scale if self.xy_scale != 0 else 1
|
|
3302
|
+
|
|
3089
3303
|
if filename is None:
|
|
3090
3304
|
filename = "labelled_nodes.tif"
|
|
3091
3305
|
elif not filename.endswith(('.tif', '.tiff')):
|
|
@@ -3094,13 +3308,19 @@ class Network_3D:
|
|
|
3094
3308
|
if self._nodes is not None:
|
|
3095
3309
|
if directory is None:
|
|
3096
3310
|
try:
|
|
3097
|
-
|
|
3311
|
+
if len(self._nodes.shape) == 3:
|
|
3312
|
+
tifffile.imwrite(f"{filename}", self._nodes, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3313
|
+
else:
|
|
3314
|
+
tifffile.imwrite(f"{filename}", self._nodes)
|
|
3098
3315
|
print(f"Nodes saved to {filename}")
|
|
3099
3316
|
except Exception as e:
|
|
3100
3317
|
print("Could not save nodes")
|
|
3101
3318
|
if directory is not None:
|
|
3102
3319
|
try:
|
|
3103
|
-
|
|
3320
|
+
if len(self._nodes.shape) == 3:
|
|
3321
|
+
tifffile.imwrite(f"{directory}/{filename}", self._nodes, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3322
|
+
else:
|
|
3323
|
+
tifffile.imwrite(f"{directory}/{filename}")
|
|
3104
3324
|
print(f"Nodes saved to {directory}/{filename}")
|
|
3105
3325
|
except Exception as e:
|
|
3106
3326
|
print(f"Could not save nodes to {directory}")
|
|
@@ -3113,6 +3333,16 @@ class Network_3D:
|
|
|
3113
3333
|
:param directory: (Optional - Val = None; String). The path to an indended directory to save the edges to.
|
|
3114
3334
|
"""
|
|
3115
3335
|
|
|
3336
|
+
if self._edges is not None:
|
|
3337
|
+
imagej_metadata = {
|
|
3338
|
+
'spacing': self.z_scale,
|
|
3339
|
+
'slices': self._edges.shape[0],
|
|
3340
|
+
'channels': 1,
|
|
3341
|
+
'axes': 'ZYX'
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
resolution_value = 1.0 / self.xy_scale if self.xy_scale != 0 else 1
|
|
3345
|
+
|
|
3116
3346
|
if filename is None:
|
|
3117
3347
|
filename = "labelled_edges.tif"
|
|
3118
3348
|
elif not filename.endswith(('.tif', '.tiff')):
|
|
@@ -3120,11 +3350,11 @@ class Network_3D:
|
|
|
3120
3350
|
|
|
3121
3351
|
if self._edges is not None:
|
|
3122
3352
|
if directory is None:
|
|
3123
|
-
tifffile.imwrite(f"{filename}", self._edges)
|
|
3353
|
+
tifffile.imwrite(f"{filename}", self._edges, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3124
3354
|
print(f"Edges saved to {filename}")
|
|
3125
3355
|
|
|
3126
3356
|
if directory is not None:
|
|
3127
|
-
tifffile.imwrite(f"{directory}/{filename}", self._edges)
|
|
3357
|
+
tifffile.imwrite(f"{directory}/{filename}", self._edges, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3128
3358
|
print(f"Edges saved to {directory}/{filename}")
|
|
3129
3359
|
|
|
3130
3360
|
if self._edges is None:
|
|
@@ -3280,6 +3510,14 @@ class Network_3D:
|
|
|
3280
3510
|
|
|
3281
3511
|
def save_network_overlay(self, directory = None, filename = None):
|
|
3282
3512
|
|
|
3513
|
+
if self._network_overlay is not None:
|
|
3514
|
+
imagej_metadata = {
|
|
3515
|
+
'spacing': self.z_scale,
|
|
3516
|
+
'slices': self._network_overlay.shape[0],
|
|
3517
|
+
'channels': 1,
|
|
3518
|
+
'axes': 'ZYX'
|
|
3519
|
+
}
|
|
3520
|
+
resolution_value = 1.0 / self.xy_scale if self.xy_scale != 0 else 1
|
|
3283
3521
|
|
|
3284
3522
|
if filename is None:
|
|
3285
3523
|
filename = "overlay_1.tif"
|
|
@@ -3288,15 +3526,30 @@ class Network_3D:
|
|
|
3288
3526
|
|
|
3289
3527
|
if self._network_overlay is not None:
|
|
3290
3528
|
if directory is None:
|
|
3291
|
-
|
|
3529
|
+
if len(self._network_overlay.shape) == 3:
|
|
3530
|
+
tifffile.imwrite(f"{filename}", self._network_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3531
|
+
else:
|
|
3532
|
+
tifffile.imwrite(f"{filename}", self._network_overlay)
|
|
3292
3533
|
print(f"Network overlay saved to {filename}")
|
|
3293
3534
|
|
|
3294
3535
|
if directory is not None:
|
|
3295
|
-
|
|
3536
|
+
if len(self._network_overlay.shape) == 3:
|
|
3537
|
+
tifffile.imwrite(f"{directory}/{filename}", self._network_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3538
|
+
else:
|
|
3539
|
+
tifffile.imwrite(f"{directory}/{filename}", self._network_overlay)
|
|
3296
3540
|
print(f"Network overlay saved to {directory}/{filename}")
|
|
3297
3541
|
|
|
3298
3542
|
def save_id_overlay(self, directory = None, filename = None):
|
|
3299
3543
|
|
|
3544
|
+
if self._id_overlay is not None:
|
|
3545
|
+
imagej_metadata = {
|
|
3546
|
+
'spacing': self.z_scale,
|
|
3547
|
+
'slices': self._id_overlay.shape[0],
|
|
3548
|
+
'channels': 1,
|
|
3549
|
+
'axes': 'ZYX'
|
|
3550
|
+
}
|
|
3551
|
+
resolution_value = 1.0 / self.xy_scale if self.xy_scale != 0 else 1
|
|
3552
|
+
|
|
3300
3553
|
if filename is None:
|
|
3301
3554
|
filename = "overlay_2.tif"
|
|
3302
3555
|
if not filename.endswith(('.tif', '.tiff')):
|
|
@@ -3304,11 +3557,17 @@ class Network_3D:
|
|
|
3304
3557
|
|
|
3305
3558
|
if self._id_overlay is not None:
|
|
3306
3559
|
if directory is None:
|
|
3307
|
-
|
|
3560
|
+
if len(self._id_overlay.shape) == 3:
|
|
3561
|
+
tifffile.imwrite(f"{filename}", self._id_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3562
|
+
else:
|
|
3563
|
+
tifffile.imwrite(f"{filename}", self._id_overlay, imagej=True)
|
|
3308
3564
|
print(f"Network overlay saved to {filename}")
|
|
3309
3565
|
|
|
3310
3566
|
if directory is not None:
|
|
3311
|
-
|
|
3567
|
+
if len(self._id_overlay.shape) == 3:
|
|
3568
|
+
tifffile.imwrite(f"{directory}/{filename}", self._id_overlay, imagej=True, metadata=imagej_metadata, resolution=(resolution_value, resolution_value))
|
|
3569
|
+
else:
|
|
3570
|
+
tifffile.imwrite(f"{directory}/{filename}", self._id_overlay)
|
|
3312
3571
|
print(f"ID overlay saved to {directory}/{filename}")
|
|
3313
3572
|
|
|
3314
3573
|
|
|
@@ -3431,7 +3690,7 @@ class Network_3D:
|
|
|
3431
3690
|
|
|
3432
3691
|
if file_path is not None:
|
|
3433
3692
|
self._xy_scale, self_z_scale = read_scalings(file_path)
|
|
3434
|
-
print("Succesfully loaded voxel_scalings")
|
|
3693
|
+
print(f"Succesfully loaded voxel_scalings; values overriden to xy_scale: {self.xy_scale}, z_scale: {self.z_scale}")
|
|
3435
3694
|
return
|
|
3436
3695
|
|
|
3437
3696
|
items = directory_info(directory)
|
|
@@ -3440,11 +3699,11 @@ class Network_3D:
|
|
|
3440
3699
|
if item == 'voxel_scalings.txt':
|
|
3441
3700
|
if directory is not None:
|
|
3442
3701
|
self._xy_scale, self._z_scale = read_scalings(f"{directory}/{item}")
|
|
3443
|
-
print("Succesfully loaded voxel_scalings")
|
|
3702
|
+
print(f"Succesfully loaded voxel_scalings; values overriden to xy_scale: {self.xy_scale}, z_scale: {self.z_scale}")
|
|
3444
3703
|
return
|
|
3445
3704
|
else:
|
|
3446
3705
|
self._xy_scale, self._z_scale = read_scalings(item)
|
|
3447
|
-
print("Succesfully loaded
|
|
3706
|
+
print(f"Succesfully loaded voxel_scaling; values overriden to xy_scale: {self.xy_scale}, z_scale: {self.z_scale}s")
|
|
3448
3707
|
return
|
|
3449
3708
|
|
|
3450
3709
|
print("Could not find voxel scalings. They must be in the specified directory and named 'voxel_scalings.txt'")
|
|
@@ -3877,7 +4136,7 @@ class Network_3D:
|
|
|
3877
4136
|
"""
|
|
3878
4137
|
self._nodes, num_nodes = label_objects(nodes, structure_3d)
|
|
3879
4138
|
|
|
3880
|
-
def combine_nodes(self, root_nodes, other_nodes, other_ID, identity_dict, root_ID = None, centroids = False):
|
|
4139
|
+
def combine_nodes(self, root_nodes, other_nodes, other_ID, identity_dict, root_ID = None, centroids = False, down_factor = None):
|
|
3881
4140
|
|
|
3882
4141
|
"""Internal method to merge two labelled node arrays into one"""
|
|
3883
4142
|
|
|
@@ -3888,7 +4147,10 @@ class Network_3D:
|
|
|
3888
4147
|
max_val = np.max(root_nodes)
|
|
3889
4148
|
other_nodes[:] = np.where(mask, other_nodes + max_val, 0)
|
|
3890
4149
|
if centroids:
|
|
3891
|
-
new_dict = network_analysis._find_centroids(other_nodes)
|
|
4150
|
+
new_dict = network_analysis._find_centroids(other_nodes, down_factor = down_factor)
|
|
4151
|
+
if down_factor is not None:
|
|
4152
|
+
for item in new_dict:
|
|
4153
|
+
new_dict[item] = down_factor * new_dict[item]
|
|
3892
4154
|
self.node_centroids.update(new_dict)
|
|
3893
4155
|
|
|
3894
4156
|
if root_ID is not None:
|
|
@@ -3928,7 +4190,7 @@ class Network_3D:
|
|
|
3928
4190
|
|
|
3929
4191
|
return nodes, identity_dict
|
|
3930
4192
|
|
|
3931
|
-
def merge_nodes(self, addn_nodes_name, label_nodes = True, root_id = "Root_Nodes", centroids = False):
|
|
4193
|
+
def merge_nodes(self, addn_nodes_name, label_nodes = True, root_id = "Root_Nodes", centroids = False, down_factor = None):
|
|
3932
4194
|
"""
|
|
3933
4195
|
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,
|
|
3934
4196
|
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
|
|
@@ -3949,19 +4211,21 @@ class Network_3D:
|
|
|
3949
4211
|
identity_dict = {} #A dictionary to deliniate the node identities
|
|
3950
4212
|
|
|
3951
4213
|
if centroids:
|
|
3952
|
-
self.node_centroids = network_analysis._find_centroids(self._nodes)
|
|
3953
|
-
|
|
4214
|
+
self.node_centroids = network_analysis._find_centroids(self._nodes, down_factor = down_factor)
|
|
4215
|
+
if down_factor is not None:
|
|
4216
|
+
for item in self.node_centroids:
|
|
4217
|
+
self.node_centroids[item] = down_factor * self.node_centroids[item]
|
|
3954
4218
|
|
|
3955
4219
|
try: #Try presumes the input is a tif
|
|
3956
4220
|
addn_nodes = tifffile.imread(addn_nodes_name) #If not this will fail and activate the except block
|
|
3957
4221
|
|
|
3958
4222
|
if label_nodes is True:
|
|
3959
4223
|
addn_nodes, num_nodes2 = label_objects(addn_nodes) # Label the node objects. Note this presumes no overlap between node masks.
|
|
3960
|
-
node_labels, identity_dict = self.combine_nodes(self._nodes, addn_nodes, addn_nodes_name, identity_dict, nodes_name, centroids = centroids) #This method stacks labelled arrays
|
|
4224
|
+
node_labels, identity_dict = self.combine_nodes(self._nodes, addn_nodes, addn_nodes_name, identity_dict, nodes_name, centroids = centroids, down_factor = down_factor) #This method stacks labelled arrays
|
|
3961
4225
|
num_nodes = np.max(node_labels)
|
|
3962
4226
|
|
|
3963
4227
|
else: #If nodes already labelled
|
|
3964
|
-
node_labels, identity_dict = self.combine_nodes(self._nodes, addn_nodes, addn_nodes_name, identity_dict, nodes_name, centroids = centroids)
|
|
4228
|
+
node_labels, identity_dict = self.combine_nodes(self._nodes, addn_nodes, addn_nodes_name, identity_dict, nodes_name, centroids = centroids, down_factor = down_factor)
|
|
3965
4229
|
num_nodes = int(np.max(node_labels))
|
|
3966
4230
|
|
|
3967
4231
|
except: #Exception presumes the input is a directory containing multiple tifs, to allow multi-node stackage.
|
|
@@ -3979,15 +4243,15 @@ class Network_3D:
|
|
|
3979
4243
|
if label_nodes is True:
|
|
3980
4244
|
addn_nodes, num_nodes2 = label_objects(addn_nodes) # Label the node objects. Note this presumes no overlap between node masks.
|
|
3981
4245
|
if i == 0:
|
|
3982
|
-
node_labels, identity_dict = self.combine_nodes(self._nodes, addn_nodes, addn_nodes_ID, identity_dict, nodes_name, centroids = centroids)
|
|
4246
|
+
node_labels, identity_dict = self.combine_nodes(self._nodes, addn_nodes, addn_nodes_ID, identity_dict, nodes_name, centroids = centroids, down_factor = down_factor)
|
|
3983
4247
|
else:
|
|
3984
|
-
node_labels, identity_dict = self.combine_nodes(node_labels, addn_nodes, addn_nodes_ID, identity_dict, centroids = centroids)
|
|
4248
|
+
node_labels, identity_dict = self.combine_nodes(node_labels, addn_nodes, addn_nodes_ID, identity_dict, centroids = centroids, down_factor = down_factor)
|
|
3985
4249
|
|
|
3986
4250
|
else:
|
|
3987
4251
|
if i == 0:
|
|
3988
|
-
node_labels, identity_dict = self.combine_nodes(self._nodes, addn_nodes, addn_nodes_ID, identity_dict, nodes_name, centroids = centroids)
|
|
4252
|
+
node_labels, identity_dict = self.combine_nodes(self._nodes, addn_nodes, addn_nodes_ID, identity_dict, nodes_name, centroids = centroids, down_factor = down_factor)
|
|
3989
4253
|
else:
|
|
3990
|
-
node_labels, identity_dict = self.combine_nodes(node_labels, addn_nodes, addn_nodes_ID, identity_dict, centroids = centroids)
|
|
4254
|
+
node_labels, identity_dict = self.combine_nodes(node_labels, addn_nodes, addn_nodes_ID, identity_dict, centroids = centroids, down_factor = down_factor)
|
|
3991
4255
|
except Exception as e:
|
|
3992
4256
|
print("Could not open additional nodes, verify they are being inputted correctly...")
|
|
3993
4257
|
|
|
@@ -4037,6 +4301,88 @@ class Network_3D:
|
|
|
4037
4301
|
self._network_lists = network_analysis.read_excel_to_lists(df)
|
|
4038
4302
|
self._network, net_weights = network_analysis.weighted_network(df)
|
|
4039
4303
|
|
|
4304
|
+
def create_id_network(self, n=5):
|
|
4305
|
+
import ast
|
|
4306
|
+
import random
|
|
4307
|
+
|
|
4308
|
+
if self.node_identities is None:
|
|
4309
|
+
return
|
|
4310
|
+
|
|
4311
|
+
def invert_dict(d):
|
|
4312
|
+
inverted = {}
|
|
4313
|
+
for key, value in d.items():
|
|
4314
|
+
inverted.setdefault(value, []).append(key)
|
|
4315
|
+
return inverted
|
|
4316
|
+
|
|
4317
|
+
# Invert to get identity -> list of nodes
|
|
4318
|
+
identity_to_nodes = invert_dict(self.node_identities)
|
|
4319
|
+
|
|
4320
|
+
G = nx.Graph()
|
|
4321
|
+
edge_set = set()
|
|
4322
|
+
|
|
4323
|
+
# Step 1: Connect nodes within same exact identity
|
|
4324
|
+
for identity, nodes in identity_to_nodes.items():
|
|
4325
|
+
if len(nodes) <= 1:
|
|
4326
|
+
continue
|
|
4327
|
+
|
|
4328
|
+
# Each node chooses n random neighbors from its identity group
|
|
4329
|
+
for node in nodes:
|
|
4330
|
+
available = [other for other in nodes if other != node]
|
|
4331
|
+
num_to_choose = min(n, len(available))
|
|
4332
|
+
neighbors = random.sample(available, num_to_choose)
|
|
4333
|
+
|
|
4334
|
+
for neighbor in neighbors:
|
|
4335
|
+
edge = tuple(sorted([node, neighbor]))
|
|
4336
|
+
edge_set.add(edge)
|
|
4337
|
+
|
|
4338
|
+
# Step 2: For list-like identities, connect across groups with shared sub-identities
|
|
4339
|
+
for identity, nodes in identity_to_nodes.items():
|
|
4340
|
+
if identity.startswith('['):
|
|
4341
|
+
try:
|
|
4342
|
+
sub_identities = ast.literal_eval(identity)
|
|
4343
|
+
|
|
4344
|
+
# For each sub-identity in this list-like identity
|
|
4345
|
+
for sub_id in sub_identities:
|
|
4346
|
+
# Find all OTHER identity groups that contain this sub-identity
|
|
4347
|
+
for other_identity, other_nodes in identity_to_nodes.items():
|
|
4348
|
+
if other_identity == identity:
|
|
4349
|
+
continue # Skip connecting to same exact identity (already done in Step 1)
|
|
4350
|
+
|
|
4351
|
+
# Check if other_identity contains sub_id
|
|
4352
|
+
contains_sub_id = False
|
|
4353
|
+
|
|
4354
|
+
if other_identity.startswith('['):
|
|
4355
|
+
try:
|
|
4356
|
+
other_sub_ids = ast.literal_eval(other_identity)
|
|
4357
|
+
if sub_id in other_sub_ids:
|
|
4358
|
+
contains_sub_id = True
|
|
4359
|
+
except (ValueError, SyntaxError):
|
|
4360
|
+
pass
|
|
4361
|
+
elif other_identity == sub_id:
|
|
4362
|
+
# Single identity that matches our sub-identity
|
|
4363
|
+
contains_sub_id = True
|
|
4364
|
+
|
|
4365
|
+
if contains_sub_id:
|
|
4366
|
+
# Each node from current identity connects to n nodes from other_identity
|
|
4367
|
+
for node in nodes:
|
|
4368
|
+
num_to_choose = min(n, len(other_nodes))
|
|
4369
|
+
if num_to_choose > 0:
|
|
4370
|
+
neighbors = random.sample(other_nodes, num_to_choose)
|
|
4371
|
+
|
|
4372
|
+
for neighbor in neighbors:
|
|
4373
|
+
edge = tuple(sorted([node, neighbor]))
|
|
4374
|
+
edge_set.add(edge)
|
|
4375
|
+
|
|
4376
|
+
except (ValueError, SyntaxError):
|
|
4377
|
+
pass # Not a valid list, treat as already handled in Step 1
|
|
4378
|
+
|
|
4379
|
+
G.add_edges_from(edge_set)
|
|
4380
|
+
self.network = G
|
|
4381
|
+
|
|
4382
|
+
|
|
4383
|
+
|
|
4384
|
+
|
|
4385
|
+
|
|
4040
4386
|
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):
|
|
4041
4387
|
"""
|
|
4042
4388
|
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.
|
|
@@ -5232,32 +5578,35 @@ class Network_3D:
|
|
|
5232
5578
|
new_list.append(centroid)
|
|
5233
5579
|
|
|
5234
5580
|
else:
|
|
5235
|
-
|
|
5236
5581
|
if mode == 1:
|
|
5237
|
-
|
|
5238
5582
|
legal = self.edges != 0
|
|
5239
|
-
|
|
5240
5583
|
elif mode == 2:
|
|
5241
|
-
|
|
5242
5584
|
legal = self.network_overlay != 0
|
|
5243
|
-
|
|
5244
5585
|
elif mode == 3:
|
|
5245
|
-
|
|
5246
5586
|
legal = self.id_overlay != 0
|
|
5247
|
-
|
|
5248
5587
|
if self.nodes is None:
|
|
5249
|
-
|
|
5250
5588
|
temp_array = proximity.populate_array(self.node_centroids, shape = legal.shape)
|
|
5251
5589
|
else:
|
|
5252
5590
|
temp_array = self.nodes
|
|
5253
|
-
|
|
5254
5591
|
if dim == 2:
|
|
5255
5592
|
volume = np.count_nonzero(legal) * self.xy_scale**2
|
|
5593
|
+
# Pad in x and y dimensions (assuming shape is [y, x])
|
|
5594
|
+
legal = np.pad(legal, pad_width=1, mode='constant', constant_values=0)
|
|
5256
5595
|
else:
|
|
5257
5596
|
volume = np.count_nonzero(legal) * self.z_scale * self.xy_scale**2
|
|
5597
|
+
# Pad in x, y, and z dimensions (assuming shape is [z, y, x])
|
|
5598
|
+
legal = np.pad(legal, pad_width=1, mode='constant', constant_values=0)
|
|
5599
|
+
|
|
5258
5600
|
print(f"Using {volume} for the volume measurement (Volume of provided mask as scaled by xy and z scaling)")
|
|
5259
|
-
|
|
5260
|
-
|
|
5601
|
+
|
|
5602
|
+
# Compute distance transform on padded array
|
|
5603
|
+
legal = smart_dilate.compute_distance_transform_distance(legal, sampling = [self.z_scale, self.xy_scale, self.xy_scale])
|
|
5604
|
+
|
|
5605
|
+
# Remove padding after distance transform
|
|
5606
|
+
if dim == 2:
|
|
5607
|
+
legal = legal[1:-1, 1:-1] # Remove padding from x and y dimensions
|
|
5608
|
+
else:
|
|
5609
|
+
legal = legal[1:-1, 1:-1, 1:-1] # Remove padding from x, y, and z dimensions
|
|
5261
5610
|
|
|
5262
5611
|
max_avail = np.max(legal) # Most internal point
|
|
5263
5612
|
min_legal = factor * max_avail # Values of stuff 25% within the tissue
|
|
@@ -5338,9 +5687,9 @@ class Network_3D:
|
|
|
5338
5687
|
|
|
5339
5688
|
|
|
5340
5689
|
|
|
5341
|
-
def interactions(self, search = 0, cores = 0, resize = None, save = False, skele = False, fastdil = False):
|
|
5690
|
+
def interactions(self, search = 0, cores = 0, resize = None, save = False, skele = False, length = False, auto = True, fastdil = False):
|
|
5342
5691
|
|
|
5343
|
-
return morphology.quantify_edge_node(self._nodes, self._edges, search = search, xy_scale = self._xy_scale, z_scale = self._z_scale, cores = cores, resize = resize, save = save, skele = skele, fastdil = fastdil)
|
|
5692
|
+
return morphology.quantify_edge_node(self._nodes, self._edges, search = search, xy_scale = self._xy_scale, z_scale = self._z_scale, cores = cores, resize = resize, save = save, skele = skele, length = length, auto = auto, fastdil = fastdil)
|
|
5344
5693
|
|
|
5345
5694
|
|
|
5346
5695
|
|
|
@@ -5511,7 +5860,7 @@ class Network_3D:
|
|
|
5511
5860
|
neighborhoods.visualize_cluster_composition_umap(self.node_centroids, None, id_dictionary = self.node_identities, graph_label = "Node ID", title = 'UMAP Visualization of Node Centroids')
|
|
5512
5861
|
|
|
5513
5862
|
|
|
5514
|
-
def identity_umap(self, data):
|
|
5863
|
+
def identity_umap(self, data, mode = 0):
|
|
5515
5864
|
|
|
5516
5865
|
try:
|
|
5517
5866
|
|
|
@@ -5531,16 +5880,18 @@ class Network_3D:
|
|
|
5531
5880
|
else:
|
|
5532
5881
|
del umap_dict[item]
|
|
5533
5882
|
|
|
5534
|
-
from scipy.stats import zscore
|
|
5883
|
+
#from scipy.stats import zscore
|
|
5535
5884
|
|
|
5536
5885
|
# Z-score normalize each marker (column)
|
|
5537
|
-
for key in umap_dict:
|
|
5538
|
-
umap_dict[key] = zscore(umap_dict[key])
|
|
5539
|
-
|
|
5886
|
+
#for key in umap_dict:
|
|
5887
|
+
#umap_dict[key] = zscore(umap_dict[key])
|
|
5540
5888
|
|
|
5541
5889
|
from . import neighborhoods
|
|
5542
5890
|
|
|
5543
|
-
|
|
5891
|
+
if mode == 0:
|
|
5892
|
+
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')
|
|
5893
|
+
else:
|
|
5894
|
+
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)
|
|
5544
5895
|
|
|
5545
5896
|
except Exception as e:
|
|
5546
5897
|
import traceback
|
|
@@ -5659,7 +6010,7 @@ class Network_3D:
|
|
|
5659
6010
|
neighbor_group[com] = neighbors[node]
|
|
5660
6011
|
except:
|
|
5661
6012
|
neighbor_group[com] = 0
|
|
5662
|
-
neighborhoods.visualize_cluster_composition_umap(umap_dict, id_set, neighborhoods = neighbor_group)
|
|
6013
|
+
neighborhoods.visualize_cluster_composition_umap(umap_dict, id_set, neighborhoods = neighbor_group, original_communities = neighbors)
|
|
5663
6014
|
elif label == 1:
|
|
5664
6015
|
neighborhoods.visualize_cluster_composition_umap(umap_dict, id_set, label = True)
|
|
5665
6016
|
else:
|
|
@@ -5671,6 +6022,19 @@ class Network_3D:
|
|
|
5671
6022
|
return output, id_set
|
|
5672
6023
|
|
|
5673
6024
|
|
|
6025
|
+
def group_nodes_by_intensity(self, data, count = None):
|
|
6026
|
+
|
|
6027
|
+
from . import neighborhoods
|
|
6028
|
+
|
|
6029
|
+
clusters = neighborhoods.cluster_arrays(data, count, seed = 42)
|
|
6030
|
+
|
|
6031
|
+
coms = {}
|
|
6032
|
+
|
|
6033
|
+
for i, cluster in enumerate(clusters):
|
|
6034
|
+
coms[i + 1] = cluster
|
|
6035
|
+
|
|
6036
|
+
self.communities = revert_dict(coms)
|
|
6037
|
+
|
|
5674
6038
|
def assign_neighborhoods(self, seed, count, limit = None, prev_coms = None, proportional = False, mode = 0):
|
|
5675
6039
|
|
|
5676
6040
|
from . import neighborhoods
|
|
@@ -5821,13 +6185,6 @@ class Network_3D:
|
|
|
5821
6185
|
|
|
5822
6186
|
def community_cells(self, size = 32, xy_scale = 1, z_scale = 1):
|
|
5823
6187
|
|
|
5824
|
-
def revert_dict(d):
|
|
5825
|
-
inverted = {}
|
|
5826
|
-
for key, value_list in d.items():
|
|
5827
|
-
for value in value_list:
|
|
5828
|
-
inverted[value] = key
|
|
5829
|
-
return inverted
|
|
5830
|
-
|
|
5831
6188
|
size_x = int(size * xy_scale)
|
|
5832
6189
|
size_z = int(size * z_scale)
|
|
5833
6190
|
|
|
@@ -5992,57 +6349,94 @@ class Network_3D:
|
|
|
5992
6349
|
self.node_identities = modify_dict
|
|
5993
6350
|
|
|
5994
6351
|
|
|
5995
|
-
def nearest_neighbors_avg(self, root, targ, xy_scale = 1, z_scale = 1, num = 1, heatmap = False, threed = True, numpy = False, quant = False, centroids = True):
|
|
5996
|
-
|
|
5997
|
-
def distribute_points_uniformly(n, shape, z_scale, xy_scale, num, is_2d=False):
|
|
6352
|
+
def nearest_neighbors_avg(self, root, targ, xy_scale = 1, z_scale = 1, num = 1, heatmap = False, threed = True, numpy = False, quant = False, centroids = True, mask = None):
|
|
5998
6353
|
|
|
6354
|
+
def distribute_points_uniformly(n, shape, z_scale, xy_scale, num, is_2d=False, mask=None):
|
|
5999
6355
|
from scipy.spatial import KDTree
|
|
6000
|
-
|
|
6001
6356
|
if n <= 1:
|
|
6002
6357
|
return 0
|
|
6003
|
-
|
|
6004
|
-
# Calculate total positions and sampling step
|
|
6005
|
-
total_positions = np.prod(shape)
|
|
6006
|
-
if n >= total_positions:
|
|
6007
|
-
# If we want more points than positions, just return scaled unit distance
|
|
6008
|
-
return xy_scale if is_2d else min(z_scale, xy_scale)
|
|
6009
|
-
|
|
6010
|
-
# Create uniformly spaced indices
|
|
6011
|
-
indices = np.linspace(0, total_positions - 1, n, dtype=int)
|
|
6012
6358
|
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6359
|
+
if mask is not None:
|
|
6360
|
+
# Handle mask-based distribution
|
|
6361
|
+
# Find all valid positions where mask is True
|
|
6362
|
+
valid_positions = np.where(mask)
|
|
6363
|
+
total_valid_positions = len(valid_positions[0])
|
|
6364
|
+
|
|
6365
|
+
if total_valid_positions == 0:
|
|
6366
|
+
raise ValueError("No valid positions found in mask")
|
|
6367
|
+
|
|
6368
|
+
if n >= total_valid_positions:
|
|
6369
|
+
# If we want more points than valid positions, return scaled unit distance
|
|
6370
|
+
return xy_scale if is_2d else min(z_scale, xy_scale)
|
|
6371
|
+
|
|
6372
|
+
# Create uniformly spaced indices within valid positions
|
|
6373
|
+
valid_indices = np.linspace(0, total_valid_positions - 1, n, dtype=int)
|
|
6374
|
+
|
|
6375
|
+
# Convert to coordinates and apply scaling
|
|
6376
|
+
coords = []
|
|
6377
|
+
for idx in valid_indices:
|
|
6378
|
+
if len(shape) == 3:
|
|
6379
|
+
coord = (valid_positions[0][idx], valid_positions[1][idx], valid_positions[2][idx])
|
|
6380
|
+
scaled_coord = [coord[0] * z_scale, coord[1] * xy_scale, coord[2] * xy_scale]
|
|
6381
|
+
elif len(shape) == 2:
|
|
6382
|
+
coord = (valid_positions[0][idx], valid_positions[1][idx])
|
|
6383
|
+
scaled_coord = [coord[0] * xy_scale, coord[1] * xy_scale]
|
|
6384
|
+
coords.append(scaled_coord)
|
|
6385
|
+
|
|
6386
|
+
coords = np.array(coords)
|
|
6387
|
+
|
|
6388
|
+
# Find a good query point (closest to center of valid region)
|
|
6017
6389
|
if len(shape) == 3:
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6390
|
+
center_pos = [np.mean(valid_positions[0]) * z_scale,
|
|
6391
|
+
np.mean(valid_positions[1]) * xy_scale,
|
|
6392
|
+
np.mean(valid_positions[2]) * xy_scale]
|
|
6393
|
+
else:
|
|
6394
|
+
center_pos = [np.mean(valid_positions[0]) * xy_scale,
|
|
6395
|
+
np.mean(valid_positions[1]) * xy_scale]
|
|
6396
|
+
|
|
6397
|
+
# Find point closest to center of valid region
|
|
6398
|
+
center_distances = np.sum((coords - center_pos)**2, axis=1)
|
|
6399
|
+
middle_idx = np.argmin(center_distances)
|
|
6400
|
+
query_point = coords[middle_idx]
|
|
6401
|
+
|
|
6402
|
+
else:
|
|
6403
|
+
# Original behavior when no mask is provided
|
|
6404
|
+
total_positions = np.prod(shape)
|
|
6405
|
+
if n >= total_positions:
|
|
6406
|
+
return xy_scale if is_2d else min(z_scale, xy_scale)
|
|
6407
|
+
|
|
6408
|
+
# Create uniformly spaced indices
|
|
6409
|
+
indices = np.linspace(0, total_positions - 1, n, dtype=int)
|
|
6410
|
+
|
|
6411
|
+
# Convert flat indices to coordinates
|
|
6412
|
+
coords = []
|
|
6413
|
+
for idx in indices:
|
|
6414
|
+
coord = np.unravel_index(idx, shape)
|
|
6415
|
+
if len(shape) == 3:
|
|
6416
|
+
scaled_coord = [coord[0] * z_scale, coord[1] * xy_scale, coord[2] * xy_scale]
|
|
6417
|
+
elif len(shape) == 2:
|
|
6418
|
+
scaled_coord = [coord[0] * xy_scale, coord[1] * xy_scale]
|
|
6419
|
+
coords.append(scaled_coord)
|
|
6420
|
+
|
|
6421
|
+
coords = np.array(coords)
|
|
6422
|
+
|
|
6423
|
+
# Pick a point near the middle of the array
|
|
6424
|
+
middle_idx = len(coords) // 2
|
|
6425
|
+
query_point = coords[middle_idx]
|
|
6026
6426
|
|
|
6027
6427
|
# Build KDTree
|
|
6028
6428
|
tree = KDTree(coords)
|
|
6029
6429
|
|
|
6030
|
-
# Pick a point near the middle of the array
|
|
6031
|
-
middle_idx = len(coords) // 2
|
|
6032
|
-
query_point = coords[middle_idx]
|
|
6033
|
-
|
|
6034
6430
|
# Find the num+1 nearest neighbors (including the point itself)
|
|
6035
6431
|
distances, indices = tree.query(query_point, k=num+1)
|
|
6036
6432
|
|
|
6037
6433
|
# Exclude the point itself (distance 0) and get the actual neighbors
|
|
6038
6434
|
neighbor_distances = distances[1:num+1]
|
|
6039
|
-
|
|
6040
6435
|
if num == n:
|
|
6041
6436
|
neighbor_distances[-1] = neighbor_distances[-2]
|
|
6042
6437
|
|
|
6043
6438
|
avg_distance = np.mean(neighbor_distances)
|
|
6044
6439
|
|
|
6045
|
-
|
|
6046
6440
|
return avg_distance
|
|
6047
6441
|
|
|
6048
6442
|
do_borders = not centroids
|
|
@@ -6064,14 +6458,25 @@ class Network_3D:
|
|
|
6064
6458
|
|
|
6065
6459
|
for node, iden in self.node_identities.items():
|
|
6066
6460
|
|
|
6067
|
-
if iden == root:
|
|
6461
|
+
if iden == root: # Standard behavior
|
|
6068
6462
|
|
|
6069
6463
|
root_set.append(node)
|
|
6070
6464
|
|
|
6071
|
-
elif
|
|
6465
|
+
elif '[' in iden and root != "All (Excluding Targets)": # For multiple nodes
|
|
6466
|
+
if root in iden:
|
|
6467
|
+
root_set.append(node)
|
|
6468
|
+
|
|
6469
|
+
elif (iden == targ) or (targ == 'All Others (Excluding Self)'): # The other group
|
|
6072
6470
|
|
|
6073
6471
|
compare_set.append(node)
|
|
6074
6472
|
|
|
6473
|
+
elif '[' in iden: # The other group, for multiple nodes
|
|
6474
|
+
if targ in iden:
|
|
6475
|
+
compare_set.append(node)
|
|
6476
|
+
|
|
6477
|
+
elif root == "All (Excluding Targets)": # If not assigned to the other group but the comprehensive root option is used
|
|
6478
|
+
root_set.append(node)
|
|
6479
|
+
|
|
6075
6480
|
if root == targ:
|
|
6076
6481
|
|
|
6077
6482
|
compare_set = root_set
|
|
@@ -6094,11 +6499,11 @@ class Network_3D:
|
|
|
6094
6499
|
if heatmap:
|
|
6095
6500
|
root_set = []
|
|
6096
6501
|
compare_set = []
|
|
6097
|
-
if root is None:
|
|
6098
|
-
|
|
6099
|
-
root_set = list(self.node_centroids.keys())
|
|
6502
|
+
if root is None and not do_borders:
|
|
6100
6503
|
compare_set = root_set
|
|
6101
|
-
|
|
6504
|
+
if not do_borders:
|
|
6505
|
+
root_set = list(self.node_centroids.keys())
|
|
6506
|
+
elif self.node_identities is not None:
|
|
6102
6507
|
for node, iden in self.node_identities.items():
|
|
6103
6508
|
|
|
6104
6509
|
if iden == root:
|
|
@@ -6126,7 +6531,8 @@ class Network_3D:
|
|
|
6126
6531
|
targ = [targ]
|
|
6127
6532
|
|
|
6128
6533
|
compare_set_neigh = approx_boundaries(self.nodes, targ, self.node_identities, keep_labels = False)
|
|
6129
|
-
|
|
6534
|
+
|
|
6535
|
+
avg, output = proximity.average_nearest_neighbor_distances(self.node_centroids, root_set_neigh, compare_set_neigh, xy_scale=self.xy_scale, z_scale=self.z_scale, num = num, do_borders = do_borders)
|
|
6130
6536
|
|
|
6131
6537
|
if quant:
|
|
6132
6538
|
try:
|
|
@@ -6168,7 +6574,15 @@ class Network_3D:
|
|
|
6168
6574
|
else:
|
|
6169
6575
|
is_2d = False
|
|
6170
6576
|
|
|
6171
|
-
|
|
6577
|
+
if root_set == []:
|
|
6578
|
+
avail_nodes = np.unique(self.nodes)
|
|
6579
|
+
compare_set = list(avail_nodes)
|
|
6580
|
+
if 0 in compare_set:
|
|
6581
|
+
del compare_set[0]
|
|
6582
|
+
root_set = compare_set
|
|
6583
|
+
elif compare_set == []:
|
|
6584
|
+
compare_set = root_set
|
|
6585
|
+
pred = distribute_points_uniformly(len(compare_set), bounds, self.z_scale, self.xy_scale, num = num, is_2d = is_2d, mask = mask)
|
|
6172
6586
|
|
|
6173
6587
|
node_intensity = {}
|
|
6174
6588
|
import math
|
|
@@ -6176,7 +6590,6 @@ class Network_3D:
|
|
|
6176
6590
|
|
|
6177
6591
|
for node in root_set:
|
|
6178
6592
|
node_intensity[node] = math.log(pred/output[node])
|
|
6179
|
-
#print(output[node])
|
|
6180
6593
|
node_centroids[node] = self.node_centroids[node]
|
|
6181
6594
|
|
|
6182
6595
|
if numpy:
|