nettracer3d 0.6.6__py3-none-any.whl → 0.6.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nettracer3d/community_extractor.py +0 -269
- nettracer3d/morphology.py +61 -36
- nettracer3d/nettracer.py +261 -238
- nettracer3d/nettracer_gui.py +523 -194
- nettracer3d/proximity.py +8 -7
- nettracer3d/segmenter.py +12 -18
- nettracer3d/smart_dilate.py +156 -82
- {nettracer3d-0.6.6.dist-info → nettracer3d-0.6.8.dist-info}/METADATA +4 -4
- nettracer3d-0.6.8.dist-info/RECORD +20 -0
- nettracer3d-0.6.6.dist-info/RECORD +0 -20
- {nettracer3d-0.6.6.dist-info → nettracer3d-0.6.8.dist-info}/WHEEL +0 -0
- {nettracer3d-0.6.6.dist-info → nettracer3d-0.6.8.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.6.6.dist-info → nettracer3d-0.6.8.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.6.6.dist-info → nettracer3d-0.6.8.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer.py
CHANGED
|
@@ -547,7 +547,7 @@ def remove_branches(skeleton, length):
|
|
|
547
547
|
return image_copy
|
|
548
548
|
|
|
549
549
|
|
|
550
|
-
def estimate_object_radii(labeled_array, gpu=False, n_jobs=None):
|
|
550
|
+
def estimate_object_radii(labeled_array, gpu=False, n_jobs=None, xy_scale = 1, z_scale = 1):
|
|
551
551
|
"""
|
|
552
552
|
Estimate the radii of labeled objects in a 3D numpy array.
|
|
553
553
|
Dispatches to appropriate implementation based on parameters.
|
|
@@ -579,9 +579,9 @@ def estimate_object_radii(labeled_array, gpu=False, n_jobs=None):
|
|
|
579
579
|
gpu = False
|
|
580
580
|
|
|
581
581
|
if gpu:
|
|
582
|
-
return morphology.estimate_object_radii_gpu(labeled_array)
|
|
582
|
+
return morphology.estimate_object_radii_gpu(labeled_array, xy_scale = xy_scale, z_scale = z_scale)
|
|
583
583
|
else:
|
|
584
|
-
return morphology.estimate_object_radii_cpu(labeled_array, n_jobs)
|
|
584
|
+
return morphology.estimate_object_radii_cpu(labeled_array, n_jobs, xy_scale = xy_scale, z_scale = z_scale)
|
|
585
585
|
|
|
586
586
|
|
|
587
587
|
def break_and_label_skeleton(skeleton, peaks = 1, branch_removal = 0, comp_dil = 0, max_vol = 0, directory = None, return_skele = False, nodes = None):
|
|
@@ -862,23 +862,48 @@ def _rescale(array, original_shape, xy_scale, z_scale):
|
|
|
862
862
|
return array
|
|
863
863
|
|
|
864
864
|
|
|
865
|
-
def remove_trunk(edges):
|
|
865
|
+
def remove_trunk(edges, num_iterations=1):
|
|
866
866
|
"""
|
|
867
|
-
|
|
868
|
-
|
|
867
|
+
Removes the largest connected objects from a 3D binary array in-place.
|
|
868
|
+
|
|
869
|
+
Parameters:
|
|
870
|
+
-----------
|
|
871
|
+
edges : ndarray
|
|
872
|
+
3D binary array containing objects to process.
|
|
873
|
+
Will be modified in-place.
|
|
874
|
+
num_iterations : int, optional
|
|
875
|
+
Number of largest objects to remove, default is 1.
|
|
876
|
+
|
|
877
|
+
Returns:
|
|
878
|
+
--------
|
|
879
|
+
ndarray
|
|
880
|
+
Reference to the modified input array.
|
|
869
881
|
"""
|
|
870
|
-
|
|
871
|
-
labeled_array = measure.label(edges)
|
|
872
|
-
|
|
873
|
-
#
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
#
|
|
880
|
-
|
|
881
|
-
|
|
882
|
+
# Label connected components
|
|
883
|
+
labeled_array, num_features = measure.label(edges, background=0, return_num=True)
|
|
884
|
+
|
|
885
|
+
# If there are fewer objects than requested iterations, adjust
|
|
886
|
+
iterations = min(num_iterations, num_features)
|
|
887
|
+
|
|
888
|
+
if iterations == 0 or num_features == 0:
|
|
889
|
+
return edges
|
|
890
|
+
|
|
891
|
+
# Count occurrences of each label
|
|
892
|
+
label_counts = np.bincount(labeled_array.ravel())
|
|
893
|
+
|
|
894
|
+
# Skip background (label 0)
|
|
895
|
+
label_counts = label_counts[1:]
|
|
896
|
+
|
|
897
|
+
# Find indices of largest objects (argsort returns ascending order, so we reverse it)
|
|
898
|
+
largest_indices = np.argsort(label_counts)[::-1][:iterations]
|
|
899
|
+
|
|
900
|
+
# Convert back to actual labels (add 1 because we skipped background)
|
|
901
|
+
largest_labels = largest_indices + 1
|
|
902
|
+
|
|
903
|
+
# Modify the input array in-place
|
|
904
|
+
for label in largest_labels:
|
|
905
|
+
edges[labeled_array == label] = 0
|
|
906
|
+
|
|
882
907
|
return edges
|
|
883
908
|
|
|
884
909
|
def hash_inners(search_region, inner_edges, GPU = True):
|
|
@@ -912,10 +937,155 @@ def dilate_2D(array, search, scaling = 1):
|
|
|
912
937
|
|
|
913
938
|
return inv
|
|
914
939
|
|
|
940
|
+
def erode_2D(array, search, scaling=1):
|
|
941
|
+
"""
|
|
942
|
+
Erode a 2D array using distance transform method.
|
|
943
|
+
|
|
944
|
+
Parameters:
|
|
945
|
+
array -- Input 2D binary array
|
|
946
|
+
search -- Distance within which to erode
|
|
947
|
+
scaling -- Scaling factor (default: 1)
|
|
948
|
+
|
|
949
|
+
Returns:
|
|
950
|
+
Eroded 2D array
|
|
951
|
+
"""
|
|
952
|
+
# For erosion, we work directly with the foreground
|
|
953
|
+
# No need to invert the array
|
|
954
|
+
|
|
955
|
+
# Compute distance transform on the foreground
|
|
956
|
+
dt = smart_dilate.compute_distance_transform_distance(array)
|
|
957
|
+
|
|
958
|
+
# Apply scaling
|
|
959
|
+
dt = dt * scaling
|
|
960
|
+
|
|
961
|
+
# Threshold to keep only points that are at least 'search' distance from the boundary
|
|
962
|
+
eroded = dt >= search
|
|
963
|
+
|
|
964
|
+
return eroded
|
|
965
|
+
|
|
966
|
+
def dilate_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0):
|
|
967
|
+
"""
|
|
968
|
+
Dilate a 3D array using distance transform method. Dt dilation produces perfect results but only works in euclidean geometry and lags in big arrays.
|
|
969
|
+
|
|
970
|
+
Parameters:
|
|
971
|
+
array -- Input 3D binary array
|
|
972
|
+
search_distance -- Distance within which to dilate
|
|
973
|
+
xy_scaling -- Scaling factor for x and y dimensions (default: 1.0)
|
|
974
|
+
z_scaling -- Scaling factor for z dimension (default: 1.0)
|
|
975
|
+
|
|
976
|
+
Returns:
|
|
977
|
+
Dilated 3D array
|
|
978
|
+
"""
|
|
979
|
+
|
|
980
|
+
if array.shape[0] == 1:
|
|
981
|
+
|
|
982
|
+
return dilate_2D(array, search_distance, scaling = xy_scaling) #Use the 2d method in psueod-3d cases
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
# Invert the array (find background)
|
|
986
|
+
inv = array < 1
|
|
987
|
+
|
|
988
|
+
del array
|
|
989
|
+
|
|
990
|
+
# Determine which dimension needs resampling
|
|
991
|
+
if (z_scaling > xy_scaling):
|
|
992
|
+
# Z dimension needs to be stretched
|
|
993
|
+
zoom_factor = [z_scaling/xy_scaling, 1, 1] # Scale factor for [z, y, x]
|
|
994
|
+
rev_factor = [xy_scaling/z_scaling, 1, 1]
|
|
995
|
+
cardinal = xy_scaling
|
|
996
|
+
elif (xy_scaling > z_scaling):
|
|
997
|
+
# XY dimensions need to be stretched
|
|
998
|
+
zoom_factor = [1, xy_scaling/z_scaling, xy_scaling/z_scaling] # Scale factor for [z, y, x]
|
|
999
|
+
rev_factor = [1, z_scaling/xy_scaling, z_scaling/xy_scaling] # Scale factor for [z, y, x]
|
|
1000
|
+
cardinal = z_scaling
|
|
1001
|
+
else:
|
|
1002
|
+
# Already uniform scaling, no need to resample
|
|
1003
|
+
zoom_factor = None
|
|
1004
|
+
rev_factor = None
|
|
1005
|
+
cardinal = xy_scaling
|
|
1006
|
+
|
|
1007
|
+
# Resample the mask if needed
|
|
1008
|
+
if zoom_factor:
|
|
1009
|
+
inv = ndimage.zoom(inv, zoom_factor, order=0) # Use order=0 for binary masks
|
|
1010
|
+
|
|
1011
|
+
# Compute distance transform (Euclidean)
|
|
1012
|
+
inv = smart_dilate.compute_distance_transform_distance(inv)
|
|
1013
|
+
|
|
1014
|
+
inv = inv * cardinal
|
|
1015
|
+
|
|
1016
|
+
# Threshold the distance transform to get dilated result
|
|
1017
|
+
inv = inv <= search_distance
|
|
1018
|
+
|
|
1019
|
+
if rev_factor:
|
|
1020
|
+
inv = ndimage.zoom(inv, rev_factor, order=0) # Use order=0 for binary masks
|
|
1021
|
+
|
|
1022
|
+
return inv.astype(np.uint8)
|
|
1023
|
+
|
|
1024
|
+
def erode_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0):
|
|
1025
|
+
"""
|
|
1026
|
+
Erode a 3D array using distance transform method. DT erosion produces perfect results
|
|
1027
|
+
with Euclidean geometry, but may be slower for large arrays.
|
|
1028
|
+
|
|
1029
|
+
Parameters:
|
|
1030
|
+
array -- Input 3D binary array
|
|
1031
|
+
search_distance -- Distance within which to erode
|
|
1032
|
+
xy_scaling -- Scaling factor for x and y dimensions (default: 1.0)
|
|
1033
|
+
z_scaling -- Scaling factor for z dimension (default: 1.0)
|
|
1034
|
+
GPU -- Whether to use GPU acceleration if available (default: False)
|
|
1035
|
+
|
|
1036
|
+
Returns:
|
|
1037
|
+
Eroded 3D array
|
|
1038
|
+
"""
|
|
1039
|
+
|
|
1040
|
+
if array.shape[0] == 1:
|
|
1041
|
+
# Handle 2D case
|
|
1042
|
+
return erode_2D(array, search_distance, scaling=xy_scaling)
|
|
1043
|
+
|
|
1044
|
+
# For erosion, we work directly with the foreground (no inversion needed)
|
|
1045
|
+
|
|
1046
|
+
# Determine which dimension needs resampling
|
|
1047
|
+
if (z_scaling > xy_scaling):
|
|
1048
|
+
# Z dimension needs to be stretched
|
|
1049
|
+
zoom_factor = [z_scaling/xy_scaling, 1, 1] # Scale factor for [z, y, x]
|
|
1050
|
+
rev_factor = [xy_scaling/z_scaling, 1, 1]
|
|
1051
|
+
cardinal = xy_scaling
|
|
1052
|
+
elif (xy_scaling > z_scaling):
|
|
1053
|
+
# XY dimensions need to be stretched
|
|
1054
|
+
zoom_factor = [1, xy_scaling/z_scaling, xy_scaling/z_scaling] # Scale factor for [z, y, x]
|
|
1055
|
+
rev_factor = [1, z_scaling/xy_scaling, z_scaling/xy_scaling] # Scale factor for [z, y, x]
|
|
1056
|
+
cardinal = z_scaling
|
|
1057
|
+
else:
|
|
1058
|
+
# Already uniform scaling, no need to resample
|
|
1059
|
+
zoom_factor = None
|
|
1060
|
+
rev_factor = None
|
|
1061
|
+
cardinal = xy_scaling
|
|
1062
|
+
|
|
1063
|
+
# Resample the mask if needed
|
|
1064
|
+
if zoom_factor:
|
|
1065
|
+
array = ndimage.zoom(array, zoom_factor, order=0) # Use order=0 for binary masks
|
|
1066
|
+
|
|
1067
|
+
print("Computing a distance transform for a perfect erosion...")
|
|
1068
|
+
|
|
1069
|
+
array = smart_dilate.compute_distance_transform_distance(array)
|
|
1070
|
+
|
|
1071
|
+
# Apply scaling factor
|
|
1072
|
+
array = array * cardinal
|
|
1073
|
+
|
|
1074
|
+
# Threshold the distance transform to get eroded result
|
|
1075
|
+
# For erosion, we keep only the points that are at least search_distance from the boundary
|
|
1076
|
+
array = array >= search_distance
|
|
1077
|
+
|
|
1078
|
+
# Resample back to original dimensions if needed
|
|
1079
|
+
if rev_factor:
|
|
1080
|
+
array = ndimage.zoom(array, rev_factor, order=0) # Use order=0 for binary masks
|
|
1081
|
+
|
|
1082
|
+
return array.astype(np.uint8)
|
|
1083
|
+
|
|
915
1084
|
|
|
916
1085
|
def dilate_3D(tiff_array, dilated_x, dilated_y, dilated_z):
|
|
917
1086
|
"""Internal method to dilate an array in 3D. Dilation this way is much faster than using a distance transform although the latter is theoretically more accurate.
|
|
918
|
-
Arguments are an array, and the desired pixel dilation amounts in X, Y, Z.
|
|
1087
|
+
Arguments are an array, and the desired pixel dilation amounts in X, Y, Z. Uses psuedo-3D kernels (imagine a 3D + sign rather than a cube) to approximate 3D neighborhoods but will miss diagonally located things with larger kernels, if those are needed use the distance transform version.
|
|
1088
|
+
"""
|
|
919
1089
|
|
|
920
1090
|
if tiff_array.shape[0] == 1:
|
|
921
1091
|
return dilate_2D(tiff_array, ((dilated_x - 1) / 2))
|
|
@@ -1041,118 +1211,41 @@ def dilate_3D(tiff_array, dilated_x, dilated_y, dilated_z):
|
|
|
1041
1211
|
|
|
1042
1212
|
return final_result
|
|
1043
1213
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
"""Recursive 3D dilation method that handles odd-numbered dilations properly.
|
|
1214
|
+
def dilate_3D_old(tiff_array, dilated_x=3, dilated_y=3, dilated_z=3):
|
|
1215
|
+
"""
|
|
1216
|
+
Dilate a 3D array using scipy.ndimage.binary_dilation with a 3x3x3 cubic kernel.
|
|
1048
1217
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1218
|
+
Arguments:
|
|
1219
|
+
tiff_array -- Input 3D binary array
|
|
1220
|
+
dilated_x -- Fixed at 3 for X dimension
|
|
1221
|
+
dilated_y -- Fixed at 3 for Y dimension
|
|
1222
|
+
dilated_z -- Fixed at 3 for Z dimension
|
|
1053
1223
|
|
|
1054
|
-
|
|
1224
|
+
Returns:
|
|
1225
|
+
Dilated 3D array
|
|
1055
1226
|
"""
|
|
1227
|
+
|
|
1228
|
+
# Handle special case for 2D arrays
|
|
1056
1229
|
if tiff_array.shape[0] == 1:
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
min_dim = min(tiff_array.shape)
|
|
1230
|
+
# Call 2D dilation function if needed
|
|
1231
|
+
return dilate_2D(tiff_array, 1) # For a 3x3 kernel, radius is 1
|
|
1060
1232
|
|
|
1061
|
-
#
|
|
1062
|
-
|
|
1063
|
-
if max_dilation < (0.2 * min_dim):
|
|
1064
|
-
return dilate_3D(tiff_array, dilated_x, dilated_y, dilated_z)
|
|
1065
|
-
elif dilated_x == 1 and dilated_y == 1 and dilated_z == 1: #Also if there is only a single dilation don't do it
|
|
1066
|
-
return dilate_3D(tiff_array, dilated_x, dilated_y, dilated_z)
|
|
1233
|
+
# Create a simple 3x3x3 cubic kernel (all ones)
|
|
1234
|
+
kernel = np.ones((3, 3, 3), dtype=bool)
|
|
1067
1235
|
|
|
1068
|
-
#
|
|
1069
|
-
|
|
1070
|
-
# Start with a reasonable step size based on the largest dilation
|
|
1071
|
-
step_size = min(5, max((max_dilation - 1) // 2 // 3, 1))
|
|
1236
|
+
# Perform binary dilation
|
|
1237
|
+
dilated_array = ndimage.binary_dilation(tiff_array.astype(bool), structure=kernel)
|
|
1072
1238
|
|
|
1073
|
-
|
|
1074
|
-
if step_size == 1 or (dilated_x <= 1 and dilated_y <= 1 and dilated_z <= 1):
|
|
1075
|
-
def create_circular_kernel(diameter):
|
|
1076
|
-
radius = diameter/2
|
|
1077
|
-
size = radius
|
|
1078
|
-
size = int(np.ceil(size))
|
|
1079
|
-
y, x = np.ogrid[-radius:radius+1, -radius:radius+1]
|
|
1080
|
-
distance = np.sqrt(x**2 + y**2)
|
|
1081
|
-
kernel = distance <= radius
|
|
1082
|
-
return kernel.astype(np.uint8)
|
|
1083
|
-
|
|
1084
|
-
def create_ellipsoidal_kernel(long_axis, short_axis):
|
|
1085
|
-
semi_major, semi_minor = long_axis / 2, short_axis / 2
|
|
1086
|
-
size_y = int(np.ceil(semi_minor))
|
|
1087
|
-
size_x = int(np.ceil(semi_major))
|
|
1088
|
-
y, x = np.ogrid[-semi_minor:semi_minor+1, -semi_major:semi_major+1]
|
|
1089
|
-
ellipse = (x**2 / semi_major**2) + (y**2 / semi_minor**2) <= 1
|
|
1090
|
-
return ellipse.astype(np.uint8)
|
|
1091
|
-
|
|
1092
|
-
def process_slice(z):
|
|
1093
|
-
tiff_slice = tiff_array[z].astype(np.uint8)
|
|
1094
|
-
dilated_slice = cv2.dilate(tiff_slice, kernel, iterations=1)
|
|
1095
|
-
return z, dilated_slice
|
|
1096
|
-
|
|
1097
|
-
def process_slice_other(y):
|
|
1098
|
-
tiff_slice = tiff_array[:, y, :].astype(np.uint8)
|
|
1099
|
-
dilated_slice = cv2.dilate(tiff_slice, kernel, iterations=1)
|
|
1100
|
-
return y, dilated_slice
|
|
1101
|
-
|
|
1102
|
-
# Create empty arrays for the dilated results
|
|
1103
|
-
dilated_xy = np.zeros_like(tiff_array, dtype=np.uint8)
|
|
1104
|
-
dilated_xz = np.zeros_like(tiff_array, dtype=np.uint8)
|
|
1105
|
-
|
|
1106
|
-
# Create kernels for final dilation
|
|
1107
|
-
kernel = create_circular_kernel(dilated_x)
|
|
1108
|
-
|
|
1109
|
-
# Process XY plane
|
|
1110
|
-
num_cores = mp.cpu_count()
|
|
1111
|
-
with ThreadPoolExecutor(max_workers=num_cores) as executor:
|
|
1112
|
-
futures = {executor.submit(process_slice, z): z for z in range(tiff_array.shape[0])}
|
|
1113
|
-
for future in as_completed(futures):
|
|
1114
|
-
z, dilated_slice = future.result()
|
|
1115
|
-
dilated_xy[z] = dilated_slice
|
|
1116
|
-
|
|
1117
|
-
# Process XZ plane
|
|
1118
|
-
kernel = create_ellipsoidal_kernel(dilated_x, dilated_z)
|
|
1119
|
-
with ThreadPoolExecutor(max_workers=num_cores) as executor:
|
|
1120
|
-
futures = {executor.submit(process_slice_other, y): y for y in range(tiff_array.shape[1])}
|
|
1121
|
-
for future in as_completed(futures):
|
|
1122
|
-
y, dilated_slice = future.result()
|
|
1123
|
-
dilated_xz[:, y, :] = dilated_slice
|
|
1239
|
+
return dilated_array.astype(np.uint8)
|
|
1124
1240
|
|
|
1125
|
-
return dilated_xy | dilated_xz
|
|
1126
|
-
|
|
1127
|
-
# Calculate current iteration's dilation sizes (must be odd numbers)
|
|
1128
|
-
current_x_steps = min((dilated_x - 1) // 2, step_size)
|
|
1129
|
-
current_y_steps = min((dilated_y - 1) // 2, step_size)
|
|
1130
|
-
current_z_steps = min((dilated_z - 1) // 2, step_size)
|
|
1131
|
-
|
|
1132
|
-
current_x_dilation = current_x_steps * 2 + 1
|
|
1133
|
-
current_y_dilation = current_y_steps * 2 + 1
|
|
1134
|
-
current_z_dilation = current_z_steps * 2 + 1
|
|
1135
|
-
|
|
1136
|
-
# Perform current iteration's dilation
|
|
1137
|
-
current_result = dilate_3D_recursive(tiff_array, current_x_dilation, current_y_dilation, current_z_dilation, step_size=1)
|
|
1138
|
-
|
|
1139
|
-
# Calculate remaining dilation needed
|
|
1140
|
-
# For X and Y, use the circle radius (current_x_steps)
|
|
1141
|
-
# For Z, use the ellipse short axis (current_z_steps)
|
|
1142
|
-
remaining_x = max(1, dilated_x - (current_x_steps * 2))
|
|
1143
|
-
remaining_y = max(1, dilated_y - (current_y_steps * 2))
|
|
1144
|
-
remaining_z = max(1, dilated_z - (current_z_steps * 2))
|
|
1145
|
-
|
|
1146
|
-
# If no more dilation needed, return current result
|
|
1147
|
-
if remaining_x == 1 and remaining_y == 1 and remaining_z == 1:
|
|
1148
|
-
return current_result
|
|
1149
|
-
|
|
1150
|
-
# Recursive call with remaining dilation and decreased step size
|
|
1151
|
-
return dilate_3D_recursive(current_result, remaining_x, remaining_y, remaining_z, step_size=max(1, step_size - 1))
|
|
1152
1241
|
|
|
1153
1242
|
def erode_3D(tiff_array, eroded_x, eroded_y, eroded_z):
|
|
1154
1243
|
"""Internal method to erode an array in 3D. Erosion this way is much faster than using a distance transform although the latter is theoretically more accurate.
|
|
1155
1244
|
Arguments are an array, and the desired pixel erosion amounts in X, Y, Z."""
|
|
1245
|
+
|
|
1246
|
+
if tiff_array.shape[0] == 1:
|
|
1247
|
+
return erode_2D(tiff_array, ((eroded_x - 1) / 2))
|
|
1248
|
+
|
|
1156
1249
|
def create_circular_kernel(diameter):
|
|
1157
1250
|
"""Create a 2D circular kernel with a given radius.
|
|
1158
1251
|
Parameters:
|
|
@@ -1247,43 +1340,6 @@ def erode_3D(tiff_array, eroded_x, eroded_y, eroded_z):
|
|
|
1247
1340
|
return final_result
|
|
1248
1341
|
|
|
1249
1342
|
|
|
1250
|
-
def dilate_3D_old(tiff_array, dilated_x, dilated_y, dilated_z):
|
|
1251
|
-
"""(For cubey dilation only). Internal method to dilate an array in 3D.
|
|
1252
|
-
Arguments are an array, and the desired pixel dilation amounts in X, Y, Z."""
|
|
1253
|
-
|
|
1254
|
-
# Create empty arrays to store the dilated results for the XY and XZ planes
|
|
1255
|
-
dilated_xy = np.zeros_like(tiff_array, dtype=np.uint8)
|
|
1256
|
-
dilated_xz = np.zeros_like(tiff_array, dtype=np.uint8)
|
|
1257
|
-
|
|
1258
|
-
# Perform 2D dilation in the XY plane
|
|
1259
|
-
for z in range(tiff_array.shape[0]):
|
|
1260
|
-
kernel_x = int(dilated_x)
|
|
1261
|
-
kernel_y = int(dilated_y)
|
|
1262
|
-
kernel = np.ones((kernel_y, kernel_x), dtype=np.uint8)
|
|
1263
|
-
|
|
1264
|
-
# Convert the slice to the appropriate data type
|
|
1265
|
-
tiff_slice = tiff_array[z].astype(np.uint8)
|
|
1266
|
-
|
|
1267
|
-
dilated_slice = cv2.dilate(tiff_slice, kernel, iterations=1)
|
|
1268
|
-
dilated_xy[z] = dilated_slice
|
|
1269
|
-
|
|
1270
|
-
# Perform 2D dilation in the XZ plane
|
|
1271
|
-
for y in range(tiff_array.shape[1]):
|
|
1272
|
-
kernel_x = int(dilated_x)
|
|
1273
|
-
kernel_z = int(dilated_z)
|
|
1274
|
-
kernel = np.ones((kernel_z, kernel_x), dtype=np.uint8)
|
|
1275
|
-
|
|
1276
|
-
# Convert the slice to the appropriate data type
|
|
1277
|
-
tiff_slice = tiff_array[:, y, :].astype(np.uint8)
|
|
1278
|
-
|
|
1279
|
-
dilated_slice = cv2.dilate(tiff_slice, kernel, iterations=1)
|
|
1280
|
-
dilated_xz[:, y, :] = dilated_slice
|
|
1281
|
-
|
|
1282
|
-
# Overlay the results
|
|
1283
|
-
final_result = dilated_xy | dilated_xz
|
|
1284
|
-
|
|
1285
|
-
return final_result
|
|
1286
|
-
|
|
1287
1343
|
def dilation_length_to_pixels(xy_scaling, z_scaling, micronx, micronz):
|
|
1288
1344
|
"""Internal method to find XY and Z dilation parameters based on voxel micron scaling"""
|
|
1289
1345
|
dilate_xy = 2 * int(round(micronx/xy_scaling))
|
|
@@ -1483,7 +1539,7 @@ def binarize(arrayimage, directory = None):
|
|
|
1483
1539
|
|
|
1484
1540
|
return arrayimage
|
|
1485
1541
|
|
|
1486
|
-
def dilate(arrayimage, amount, xy_scale = 1, z_scale = 1, directory = None, fast_dil = False, recursive = False):
|
|
1542
|
+
def dilate(arrayimage, amount, xy_scale = 1, z_scale = 1, directory = None, fast_dil = False, recursive = False, dilate_xy = None, dilate_z = None):
|
|
1487
1543
|
"""
|
|
1488
1544
|
Can be used to dilate a binary image in 3D. Dilated output will be saved to the active directory if none is specified. Note that dilation is done with single-instance kernels and not iterations, and therefore
|
|
1489
1545
|
objects will lose their shape somewhat and become cube-ish if the 'amount' param is ever significantly larger than the objects in quesiton.
|
|
@@ -1503,20 +1559,16 @@ def dilate(arrayimage, amount, xy_scale = 1, z_scale = 1, directory = None, fast
|
|
|
1503
1559
|
else:
|
|
1504
1560
|
image = None
|
|
1505
1561
|
|
|
1506
|
-
|
|
1562
|
+
if not dilate_xy:
|
|
1563
|
+
dilate_xy, dilate_z = dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
|
|
1507
1564
|
|
|
1508
1565
|
if len(np.unique(arrayimage)) > 2: #binarize
|
|
1509
1566
|
arrayimage = binarize(arrayimage)
|
|
1510
1567
|
|
|
1511
|
-
if
|
|
1512
|
-
arrayimage = (dilate_3D(arrayimage, dilate_xy, dilate_xy, dilate_z))
|
|
1513
|
-
if np.max(arrayimage) == 1:
|
|
1514
|
-
arrayimage = arrayimage * 255
|
|
1515
|
-
elif not recursive:
|
|
1516
|
-
arrayimage = (dilate_3D_old(arrayimage, dilate_xy, dilate_xy, dilate_z)) * 255
|
|
1568
|
+
if fast_dil:
|
|
1569
|
+
arrayimage = (dilate_3D(arrayimage, dilate_xy, dilate_xy, dilate_z))
|
|
1517
1570
|
else:
|
|
1518
|
-
arrayimage = (
|
|
1519
|
-
|
|
1571
|
+
arrayimage = (dilate_3D_dt(arrayimage, amount, xy_scale, z_scale))
|
|
1520
1572
|
|
|
1521
1573
|
|
|
1522
1574
|
if type(image) == str:
|
|
@@ -1530,15 +1582,16 @@ def dilate(arrayimage, amount, xy_scale = 1, z_scale = 1, directory = None, fast
|
|
|
1530
1582
|
|
|
1531
1583
|
return arrayimage
|
|
1532
1584
|
|
|
1533
|
-
def erode(arrayimage, amount, xy_scale = 1, z_scale = 1):
|
|
1585
|
+
def erode(arrayimage, amount, xy_scale = 1, z_scale = 1, mode = 0):
|
|
1534
1586
|
if len(np.unique(arrayimage)) > 2: #binarize
|
|
1535
1587
|
arrayimage = binarize(arrayimage)
|
|
1536
1588
|
erode_xy, erode_z = dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
|
|
1537
1589
|
|
|
1538
|
-
if
|
|
1539
|
-
arrayimage =
|
|
1590
|
+
if mode == 0:
|
|
1591
|
+
arrayimage = (erode_3D(arrayimage, erode_xy, erode_xy, erode_z)) * 255
|
|
1592
|
+
else:
|
|
1593
|
+
arrayimage = erode_3D_dt(arrayimage, amount, xy_scaling=xy_scale, z_scaling=z_scale)
|
|
1540
1594
|
|
|
1541
|
-
arrayimage = (erode_3D(arrayimage, erode_xy, erode_xy, erode_z)) * 255
|
|
1542
1595
|
if np.max(arrayimage) == 1:
|
|
1543
1596
|
arrayimage = arrayimage * 255
|
|
1544
1597
|
|
|
@@ -1693,7 +1746,7 @@ def fix_branches(array, G, communities, fix_val = None):
|
|
|
1693
1746
|
|
|
1694
1747
|
|
|
1695
1748
|
|
|
1696
|
-
def label_vertices(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol = 0, down_factor = 0, directory = None, return_skele = False, order = 0):
|
|
1749
|
+
def label_vertices(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol = 0, down_factor = 0, directory = None, return_skele = False, order = 0, fastdil = True):
|
|
1697
1750
|
"""
|
|
1698
1751
|
Can be used to label vertices (where multiple branches connect) 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.
|
|
1699
1752
|
Note that this can be used in tandem with an edge segmentation to create an image containing 'pseudo-nodes', meaning we can make a network out of just a single edge file.
|
|
@@ -1759,19 +1812,19 @@ def label_vertices(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
|
|
|
1759
1812
|
if peaks > 0:
|
|
1760
1813
|
image_copy = filter_size_by_peaks(image_copy, peaks)
|
|
1761
1814
|
if comp_dil > 0:
|
|
1762
|
-
image_copy = dilate(image_copy, comp_dil)
|
|
1815
|
+
image_copy = dilate(image_copy, comp_dil, fast_dil = fastdil)
|
|
1763
1816
|
|
|
1764
1817
|
labeled_image, num_labels = label_objects(image_copy)
|
|
1765
1818
|
elif max_vol > 0:
|
|
1766
1819
|
image_copy = filter_size_by_vol(image_copy, max_vol)
|
|
1767
1820
|
if comp_dil > 0:
|
|
1768
|
-
image_copy = dilate(image_copy, comp_dil)
|
|
1821
|
+
image_copy = dilate(image_copy, comp_dil, fast_dil = fastdil)
|
|
1769
1822
|
|
|
1770
1823
|
labeled_image, num_labels = label_objects(image_copy)
|
|
1771
1824
|
else:
|
|
1772
1825
|
|
|
1773
1826
|
if comp_dil > 0:
|
|
1774
|
-
image_copy = dilate(image_copy, comp_dil)
|
|
1827
|
+
image_copy = dilate(image_copy, comp_dil, fast_dil = fastdil)
|
|
1775
1828
|
labeled_image, num_labels = label_objects(image_copy)
|
|
1776
1829
|
|
|
1777
1830
|
#if down_factor > 0:
|
|
@@ -3010,7 +3063,7 @@ class Network_3D:
|
|
|
3010
3063
|
print(f"Assembling Network_3D object from files stored in directory: {directory}")
|
|
3011
3064
|
self.load_nodes(directory, node_path)
|
|
3012
3065
|
self.load_edges(directory, edge_path)
|
|
3013
|
-
self.load_search_region(directory, search_region_path)
|
|
3066
|
+
#self.load_search_region(directory, search_region_path)
|
|
3014
3067
|
self.load_network(directory, network_path)
|
|
3015
3068
|
self.load_node_centroids(directory, node_centroids_path)
|
|
3016
3069
|
self.load_node_identities(directory, node_identities_path)
|
|
@@ -3068,7 +3121,7 @@ class Network_3D:
|
|
|
3068
3121
|
|
|
3069
3122
|
self._edge_centroids = edge_centroids
|
|
3070
3123
|
|
|
3071
|
-
def calculate_search_region(self, search_region_size, GPU = True, fast_dil =
|
|
3124
|
+
def calculate_search_region(self, search_region_size, GPU = True, fast_dil = True, GPU_downsample = None):
|
|
3072
3125
|
|
|
3073
3126
|
"""
|
|
3074
3127
|
Method to obtain the search region that will be used to assign connectivity between nodes. May be skipped if nodes do not want to search and only want to look for their
|
|
@@ -3087,7 +3140,7 @@ class Network_3D:
|
|
|
3087
3140
|
|
|
3088
3141
|
if search_region_size != 0:
|
|
3089
3142
|
|
|
3090
|
-
self._search_region = smart_dilate.smart_dilate(self._nodes, dilate_xy, dilate_z, GPU = GPU, fast_dil = fast_dil, predownsample = GPU_downsample) #Call the smart dilate function which essentially is a fast way to enlarge nodes into a 'search region' while keeping their unique IDs.
|
|
3143
|
+
self._search_region = smart_dilate.smart_dilate(self._nodes, dilate_xy, dilate_z, GPU = GPU, fast_dil = fast_dil, predownsample = GPU_downsample, use_dt_dil_amount = search_region_size) #Call the smart dilate function which essentially is a fast way to enlarge nodes into a 'search region' while keeping their unique IDs.
|
|
3091
3144
|
|
|
3092
3145
|
else:
|
|
3093
3146
|
|
|
@@ -3121,14 +3174,15 @@ class Network_3D:
|
|
|
3121
3174
|
if skeletonized:
|
|
3122
3175
|
binary_edges = skeletonize(binary_edges)
|
|
3123
3176
|
|
|
3124
|
-
|
|
3177
|
+
|
|
3178
|
+
|
|
3179
|
+
if search is not None and hasattr(self, '_nodes') and self._nodes is not None and self._search_region is None:
|
|
3125
3180
|
search_region = binarize(self._nodes)
|
|
3126
3181
|
dilate_xy, dilate_z = dilation_length_to_pixels(self._xy_scale, self._z_scale, search, search)
|
|
3127
|
-
if
|
|
3182
|
+
if fast_dil:
|
|
3128
3183
|
search_region = dilate_3D(search_region, dilate_xy, dilate_xy, dilate_z)
|
|
3129
3184
|
else:
|
|
3130
|
-
search_region =
|
|
3131
|
-
|
|
3185
|
+
search_region = dilate_3D_dt(search_region, diledge, self._xy_scale, self._z_scale)
|
|
3132
3186
|
else:
|
|
3133
3187
|
search_region = binarize(self._search_region)
|
|
3134
3188
|
|
|
@@ -3138,19 +3192,20 @@ class Network_3D:
|
|
|
3138
3192
|
del binary_edges
|
|
3139
3193
|
|
|
3140
3194
|
if remove_edgetrunk > 0:
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
outer_edges = remove_trunk(outer_edges)
|
|
3195
|
+
print(f"Snipping trunks...")
|
|
3196
|
+
outer_edges = remove_trunk(outer_edges, remove_edgetrunk)
|
|
3144
3197
|
|
|
3145
3198
|
if diledge is not None:
|
|
3146
3199
|
dilate_xy, dilate_z = dilation_length_to_pixels(self._xy_scale, self._z_scale, diledge, diledge)
|
|
3147
|
-
|
|
3200
|
+
|
|
3201
|
+
if dilate_xy <= 3 and dilate_z <= 3:
|
|
3202
|
+
outer_edges = dilate_3D_old(outer_edges, dilate_xy, dilate_xy, dilate_z)
|
|
3203
|
+
elif fast_dil:
|
|
3148
3204
|
outer_edges = dilate_3D(outer_edges, dilate_xy, dilate_xy, dilate_z)
|
|
3149
3205
|
else:
|
|
3150
|
-
outer_edges =
|
|
3151
|
-
|
|
3206
|
+
outer_edges = dilate_3D_dt(outer_edges, diledge, self._xy_scale, self._z_scale)
|
|
3152
3207
|
else:
|
|
3153
|
-
outer_edges = dilate_3D_old(outer_edges
|
|
3208
|
+
outer_edges = dilate_3D_old(outer_edges)
|
|
3154
3209
|
|
|
3155
3210
|
labelled_edges, num_edge = label_objects(outer_edges)
|
|
3156
3211
|
|
|
@@ -3282,7 +3337,7 @@ class Network_3D:
|
|
|
3282
3337
|
self._network_lists = network_analysis.read_excel_to_lists(df)
|
|
3283
3338
|
self._network, net_weights = network_analysis.weighted_network(df)
|
|
3284
3339
|
|
|
3285
|
-
def calculate_all(self, nodes, edges, xy_scale = 1, z_scale = 1, down_factor = None, search = None, diledge = None, inners = True, hash_inners = True, remove_trunk = 0, ignore_search_region = False, other_nodes = None, label_nodes = True, directory = None, GPU = True, fast_dil =
|
|
3340
|
+
def calculate_all(self, nodes, edges, xy_scale = 1, z_scale = 1, down_factor = None, search = None, diledge = None, inners = True, hash_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):
|
|
3286
3341
|
"""
|
|
3287
3342
|
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.
|
|
3288
3343
|
:param nodes: (Mandatory; String or ndarray). Filepath to segmented nodes mask or a numpy array containing the same.
|
|
@@ -3304,7 +3359,7 @@ class Network_3D:
|
|
|
3304
3359
|
:param label_nodes: (Optional - Val = True; boolean). If True, all discrete objects in the node param (and all those contained in the optional other_nodes param) will be assigned a label. If files a prelabelled, set this to False to avoid labelling.
|
|
3305
3360
|
:param directory: (Optional - Val = None; string). Path to a directory to save to hard mem all Network_3D properties. If not set, these values will be saved to the active directory.
|
|
3306
3361
|
:param GPU: (Optional - Val = True; boolean). Will use GPU if avaialble for calculating the search_region step (including necessary downsampling for GPU RAM). Set to False to use CPU with no downsample. Note this only affects the search_region step.
|
|
3307
|
-
:param fast_dil: (Optional - Val = False, boolean) - A boolean that when True will utilize faster
|
|
3362
|
+
:param fast_dil: (Optional - Val = False, boolean) - A boolean that when True will utilize faster psuedo3d kernel dilation but when false will use slower dt-based dilation.
|
|
3308
3363
|
:param skeletonize: (Optional - Val = False, boolean) - A boolean of whether to skeletonize the edges when using them.
|
|
3309
3364
|
"""
|
|
3310
3365
|
|
|
@@ -4026,48 +4081,10 @@ class Network_3D:
|
|
|
4026
4081
|
return isolated nodes, isolated edges, and isolated network in that order). IF gen_images == False (Will return just the network).
|
|
4027
4082
|
"""
|
|
4028
4083
|
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
if not hasattr(self, '_edges') or self._edges is None:
|
|
4032
|
-
|
|
4033
|
-
connected_component, isonodes, _ = community_extractor.isolate_connected_component(self._nodes, self._network, key=key, directory = directory)
|
|
4034
|
-
|
|
4035
|
-
nodea = []
|
|
4036
|
-
nodeb = []
|
|
4037
|
-
edgec = []
|
|
4038
|
-
nodesa = self._network_lists[0]
|
|
4039
|
-
nodesb = self._network_lists[1]
|
|
4040
|
-
edgesc = self._network_lists[2]
|
|
4041
|
-
for i in range(len(nodesa)):
|
|
4042
|
-
if (nodesa[i], nodesb[i]) in connected_component:
|
|
4043
|
-
nodea.append(nodesa[i])
|
|
4044
|
-
nodeb.append(nodesb[i])
|
|
4045
|
-
edgec.append(edgesc[i])
|
|
4046
|
-
network_lists = [nodea, nodeb, edgec]
|
|
4047
|
-
network, weights = network_analysis.weighted_network(network_lists)
|
|
4084
|
+
#Removed depricated gen_images functions
|
|
4048
4085
|
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
else:
|
|
4052
|
-
if full_edges is not None:
|
|
4053
|
-
connected_component, isonodes, isoedges, searchers = community_extractor.isolate_connected_component(self._nodes, self._network, key=key, edge_file = self._edges, search_region = self.search_region, netlists = self._network_lists, directory = directory)
|
|
4054
|
-
|
|
4055
|
-
else:
|
|
4056
|
-
connected_component, isonodes, isoedges, searchers = community_extractor.isolate_connected_component(self._nodes, self._network, key=key, edge_file = self._edges, netlists = self._network_lists, directory = directory)
|
|
4057
|
-
|
|
4058
|
-
df = create_and_save_dataframe(connected_component)
|
|
4059
|
-
network_lists = network_analysis.read_excel_to_lists(df)
|
|
4060
|
-
network, net_weights = network_analysis.weighted_network(df)
|
|
4061
|
-
|
|
4062
|
-
if full_edges is not None:
|
|
4063
|
-
full_edges = tifffile.imread(full_edges)
|
|
4064
|
-
community_extractor.isolate_full_edges(searchers, full_edges, directory = directory)
|
|
4065
|
-
|
|
4066
|
-
return isonodes, isoedges, network
|
|
4067
|
-
|
|
4068
|
-
else:
|
|
4069
|
-
G = community_extractor._isolate_connected(self._network, key = key)
|
|
4070
|
-
return G
|
|
4086
|
+
G = community_extractor._isolate_connected(self._network, key = key)
|
|
4087
|
+
return G
|
|
4071
4088
|
|
|
4072
4089
|
|
|
4073
4090
|
def isolate_mothers(self, directory = None, down_factor = 1, louvain = True, ret_nodes = False, called = False):
|
|
@@ -4325,7 +4342,7 @@ class Network_3D:
|
|
|
4325
4342
|
return stats
|
|
4326
4343
|
|
|
4327
4344
|
|
|
4328
|
-
def neighborhood_identities(self, root, directory = None, mode = 0, search = 0):
|
|
4345
|
+
def neighborhood_identities(self, root, directory = None, mode = 0, search = 0, fastdil = False):
|
|
4329
4346
|
|
|
4330
4347
|
|
|
4331
4348
|
|
|
@@ -4363,7 +4380,7 @@ class Network_3D:
|
|
|
4363
4380
|
|
|
4364
4381
|
|
|
4365
4382
|
elif mode == 1: #Search neighborhoods morphologically, obtain densities
|
|
4366
|
-
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)
|
|
4383
|
+
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)
|
|
4367
4384
|
title1 = f'Volumetric Neighborhood Distribution of Nodes in image that are {search} from nodes: {root}'
|
|
4368
4385
|
title2 = f'Density Distribution of Nodes in image that are {search} from Nodes {root} as a proportion of total node volume of that ID'
|
|
4369
4386
|
|
|
@@ -4410,19 +4427,23 @@ class Network_3D:
|
|
|
4410
4427
|
|
|
4411
4428
|
|
|
4412
4429
|
|
|
4413
|
-
def interactions(self, search = 0, cores = 0, resize = None, save = False, skele = False):
|
|
4430
|
+
def interactions(self, search = 0, cores = 0, resize = None, save = False, skele = False, fastdil = False):
|
|
4431
|
+
|
|
4432
|
+
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)
|
|
4414
4433
|
|
|
4415
|
-
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)
|
|
4416
4434
|
|
|
4417
4435
|
|
|
4436
|
+
def morph_proximity(self, search = 0, targets = None, fastdil = False):
|
|
4418
4437
|
|
|
4419
|
-
|
|
4438
|
+
if type(search) == list:
|
|
4439
|
+
search_x, search_z = search #Suppose we just want to directly pass these params
|
|
4440
|
+
else:
|
|
4441
|
+
search_x, search_z = dilation_length_to_pixels(self._xy_scale, self._z_scale, search, search)
|
|
4420
4442
|
|
|
4421
|
-
search_x, search_z = dilation_length_to_pixels(self._xy_scale, self._z_scale, search, search)
|
|
4422
4443
|
|
|
4423
4444
|
num_nodes = np.max(self._nodes)
|
|
4424
4445
|
|
|
4425
|
-
my_dict = proximity.create_node_dictionary(self._nodes, num_nodes, search_x, search_z, targets = targets)
|
|
4446
|
+
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)
|
|
4426
4447
|
|
|
4427
4448
|
my_dict = proximity.find_shared_value_pairs(my_dict)
|
|
4428
4449
|
|
|
@@ -4495,6 +4516,8 @@ class Network_3D:
|
|
|
4495
4516
|
|
|
4496
4517
|
self._network_lists = network_analysis.read_excel_to_lists(network)
|
|
4497
4518
|
|
|
4519
|
+
#self._network is a networkx graph that stores the connections
|
|
4520
|
+
|
|
4498
4521
|
self.remove_edge_weights()
|
|
4499
4522
|
|
|
4500
4523
|
return array
|