nettracer3d 0.6.5__py3-none-any.whl → 0.6.7__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/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):
@@ -744,9 +744,9 @@ def z_project(array3d, method='max'):
744
744
  else:
745
745
  raise ValueError("Method must be one of: 'max', 'mean', 'min', 'sum', 'std'")
746
746
 
747
- def fill_holes_3d(array):
747
+ def fill_holes_3d(array, head_on = False, fill_borders = True):
748
748
 
749
- def process_slice(slice_2d, border_threshold=0.08):
749
+ def process_slice(slice_2d, border_threshold=0.08, fill_borders = True):
750
750
  """
751
751
  Process a 2D slice, considering components that touch less than border_threshold
752
752
  of any border length as potential holes.
@@ -757,6 +757,9 @@ def fill_holes_3d(array):
757
757
  """
758
758
  slice_2d = slice_2d.astype(np.uint8)
759
759
  labels, num_features = ndimage.label(slice_2d)
760
+
761
+ if not fill_borders:
762
+ border_threshold = 0 #Testing
760
763
 
761
764
  if num_features == 0:
762
765
  return np.zeros_like(slice_2d)
@@ -781,8 +784,10 @@ def fill_holes_3d(array):
781
784
 
782
785
  # Create mask of components that either don't touch borders
783
786
  # or touch less than the threshold proportion
787
+
784
788
  background_labels = {label for label, prop in border_proportions.items()
785
789
  if prop > border_threshold}
790
+
786
791
 
787
792
  holes_mask = ~np.isin(labels, list(background_labels))
788
793
 
@@ -802,19 +807,19 @@ def fill_holes_3d(array):
802
807
 
803
808
  # Process XY plane
804
809
  for z in range(inv_array.shape[0]):
805
- array_xy[z] = process_slice(inv_array[z])
810
+ array_xy[z] = process_slice(inv_array[z], fill_borders = fill_borders)
806
811
 
807
- if array.shape[0] > 3: #only use these dimensions for sufficiently large zstacks
812
+ if (array.shape[0] > 3) and not head_on: #only use these dimensions for sufficiently large zstacks
808
813
 
809
814
  # Process XZ plane
810
815
  for y in range(inv_array.shape[1]):
811
816
  slice_xz = inv_array[:, y, :]
812
- array_xz[:, y, :] = process_slice(slice_xz)
817
+ array_xz[:, y, :] = process_slice(slice_xz, fill_borders = fill_borders)
813
818
 
814
819
  # Process YZ plane
815
820
  for x in range(inv_array.shape[2]):
816
821
  slice_yz = inv_array[:, :, x]
817
- array_yz[:, :, x] = process_slice(slice_yz)
822
+ array_yz[:, :, x] = process_slice(slice_yz, fill_borders = fill_borders)
818
823
 
819
824
  # Combine results from all three planes
820
825
  filled = (array_xy | array_xz | array_yz) * 255
@@ -857,23 +862,48 @@ def _rescale(array, original_shape, xy_scale, z_scale):
857
862
  return array
858
863
 
859
864
 
860
- def remove_trunk(edges):
865
+ def remove_trunk(edges, num_iterations=1):
861
866
  """
862
- Internal method used to remove the edge trunk. Essentially removes the largest object from
863
- a binary array.
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.
864
881
  """
865
- # Label connected components in the binary array
866
- labeled_array = measure.label(edges)
867
-
868
- # Get unique labels and their counts
869
- unique_labels, label_counts = np.unique(labeled_array, return_counts=True)
870
-
871
- # Find the label corresponding to the largest object
872
- largest_label = unique_labels[np.argmax(label_counts[1:]) + 1]
873
-
874
- # Set indices of the largest object to 0
875
- edges[labeled_array == largest_label] = 0
876
-
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
+
877
907
  return edges
878
908
 
879
909
  def hash_inners(search_region, inner_edges, GPU = True):
@@ -907,10 +937,155 @@ def dilate_2D(array, search, scaling = 1):
907
937
 
908
938
  return inv
909
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
+
910
1084
 
911
1085
  def dilate_3D(tiff_array, dilated_x, dilated_y, dilated_z):
912
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.
913
- 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
+ """
914
1089
 
915
1090
  if tiff_array.shape[0] == 1:
916
1091
  return dilate_2D(tiff_array, ((dilated_x - 1) / 2))
@@ -1036,118 +1211,41 @@ def dilate_3D(tiff_array, dilated_x, dilated_y, dilated_z):
1036
1211
 
1037
1212
  return final_result
1038
1213
 
1039
-
1040
-
1041
- def dilate_3D_recursive(tiff_array, dilated_x, dilated_y, dilated_z, step_size=None):
1042
- """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.
1043
1217
 
1044
- Args:
1045
- tiff_array: Input 3D array
1046
- dilated_x, dilated_y, dilated_z: Odd numbers representing total dilation size
1047
- step_size: Size of dilation step for this iteration
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
1048
1223
 
1049
- Each dilation parameter represents (n-1)/2 steps outward from the object.
1224
+ Returns:
1225
+ Dilated 3D array
1050
1226
  """
1227
+
1228
+ # Handle special case for 2D arrays
1051
1229
  if tiff_array.shape[0] == 1:
1052
- return dilate_2D(tiff_array, ((dilated_x - 1) / 2))
1053
- # Calculate the smallest dimension of the array
1054
- 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
1055
1232
 
1056
- # For small dilations relative to array size, don't use recursion
1057
- max_dilation = max(dilated_x, dilated_y, dilated_z)
1058
- if max_dilation < (0.2 * min_dim):
1059
- return dilate_3D(tiff_array, dilated_x, dilated_y, dilated_z)
1060
- elif dilated_x == 1 and dilated_y == 1 and dilated_z == 1: #Also if there is only a single dilation don't do it
1061
- 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)
1062
1235
 
1063
- # Initialize step_size for first call
1064
- if step_size is None:
1065
- # Start with a reasonable step size based on the largest dilation
1066
- 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)
1067
1238
 
1068
- # Base case: if step_size is 1 or we've achieved full dilation
1069
- if step_size == 1 or (dilated_x <= 1 and dilated_y <= 1 and dilated_z <= 1):
1070
- def create_circular_kernel(diameter):
1071
- radius = diameter/2
1072
- size = radius
1073
- size = int(np.ceil(size))
1074
- y, x = np.ogrid[-radius:radius+1, -radius:radius+1]
1075
- distance = np.sqrt(x**2 + y**2)
1076
- kernel = distance <= radius
1077
- return kernel.astype(np.uint8)
1078
-
1079
- def create_ellipsoidal_kernel(long_axis, short_axis):
1080
- semi_major, semi_minor = long_axis / 2, short_axis / 2
1081
- size_y = int(np.ceil(semi_minor))
1082
- size_x = int(np.ceil(semi_major))
1083
- y, x = np.ogrid[-semi_minor:semi_minor+1, -semi_major:semi_major+1]
1084
- ellipse = (x**2 / semi_major**2) + (y**2 / semi_minor**2) <= 1
1085
- return ellipse.astype(np.uint8)
1086
-
1087
- def process_slice(z):
1088
- tiff_slice = tiff_array[z].astype(np.uint8)
1089
- dilated_slice = cv2.dilate(tiff_slice, kernel, iterations=1)
1090
- return z, dilated_slice
1091
-
1092
- def process_slice_other(y):
1093
- tiff_slice = tiff_array[:, y, :].astype(np.uint8)
1094
- dilated_slice = cv2.dilate(tiff_slice, kernel, iterations=1)
1095
- return y, dilated_slice
1096
-
1097
- # Create empty arrays for the dilated results
1098
- dilated_xy = np.zeros_like(tiff_array, dtype=np.uint8)
1099
- dilated_xz = np.zeros_like(tiff_array, dtype=np.uint8)
1100
-
1101
- # Create kernels for final dilation
1102
- kernel = create_circular_kernel(dilated_x)
1103
-
1104
- # Process XY plane
1105
- num_cores = mp.cpu_count()
1106
- with ThreadPoolExecutor(max_workers=num_cores) as executor:
1107
- futures = {executor.submit(process_slice, z): z for z in range(tiff_array.shape[0])}
1108
- for future in as_completed(futures):
1109
- z, dilated_slice = future.result()
1110
- dilated_xy[z] = dilated_slice
1239
+ return dilated_array.astype(np.uint8)
1111
1240
 
1112
- # Process XZ plane
1113
- kernel = create_ellipsoidal_kernel(dilated_x, dilated_z)
1114
- with ThreadPoolExecutor(max_workers=num_cores) as executor:
1115
- futures = {executor.submit(process_slice_other, y): y for y in range(tiff_array.shape[1])}
1116
- for future in as_completed(futures):
1117
- y, dilated_slice = future.result()
1118
- dilated_xz[:, y, :] = dilated_slice
1119
-
1120
- return dilated_xy | dilated_xz
1121
-
1122
- # Calculate current iteration's dilation sizes (must be odd numbers)
1123
- current_x_steps = min((dilated_x - 1) // 2, step_size)
1124
- current_y_steps = min((dilated_y - 1) // 2, step_size)
1125
- current_z_steps = min((dilated_z - 1) // 2, step_size)
1126
-
1127
- current_x_dilation = current_x_steps * 2 + 1
1128
- current_y_dilation = current_y_steps * 2 + 1
1129
- current_z_dilation = current_z_steps * 2 + 1
1130
-
1131
- # Perform current iteration's dilation
1132
- current_result = dilate_3D_recursive(tiff_array, current_x_dilation, current_y_dilation, current_z_dilation, step_size=1)
1133
-
1134
- # Calculate remaining dilation needed
1135
- # For X and Y, use the circle radius (current_x_steps)
1136
- # For Z, use the ellipse short axis (current_z_steps)
1137
- remaining_x = max(1, dilated_x - (current_x_steps * 2))
1138
- remaining_y = max(1, dilated_y - (current_y_steps * 2))
1139
- remaining_z = max(1, dilated_z - (current_z_steps * 2))
1140
-
1141
- # If no more dilation needed, return current result
1142
- if remaining_x == 1 and remaining_y == 1 and remaining_z == 1:
1143
- return current_result
1144
-
1145
- # Recursive call with remaining dilation and decreased step size
1146
- return dilate_3D_recursive(current_result, remaining_x, remaining_y, remaining_z, step_size=max(1, step_size - 1))
1147
1241
 
1148
1242
  def erode_3D(tiff_array, eroded_x, eroded_y, eroded_z):
1149
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.
1150
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
+
1151
1249
  def create_circular_kernel(diameter):
1152
1250
  """Create a 2D circular kernel with a given radius.
1153
1251
  Parameters:
@@ -1242,43 +1340,6 @@ def erode_3D(tiff_array, eroded_x, eroded_y, eroded_z):
1242
1340
  return final_result
1243
1341
 
1244
1342
 
1245
- def dilate_3D_old(tiff_array, dilated_x, dilated_y, dilated_z):
1246
- """(For cubey dilation only). Internal method to dilate an array in 3D.
1247
- Arguments are an array, and the desired pixel dilation amounts in X, Y, Z."""
1248
-
1249
- # Create empty arrays to store the dilated results for the XY and XZ planes
1250
- dilated_xy = np.zeros_like(tiff_array, dtype=np.uint8)
1251
- dilated_xz = np.zeros_like(tiff_array, dtype=np.uint8)
1252
-
1253
- # Perform 2D dilation in the XY plane
1254
- for z in range(tiff_array.shape[0]):
1255
- kernel_x = int(dilated_x)
1256
- kernel_y = int(dilated_y)
1257
- kernel = np.ones((kernel_y, kernel_x), dtype=np.uint8)
1258
-
1259
- # Convert the slice to the appropriate data type
1260
- tiff_slice = tiff_array[z].astype(np.uint8)
1261
-
1262
- dilated_slice = cv2.dilate(tiff_slice, kernel, iterations=1)
1263
- dilated_xy[z] = dilated_slice
1264
-
1265
- # Perform 2D dilation in the XZ plane
1266
- for y in range(tiff_array.shape[1]):
1267
- kernel_x = int(dilated_x)
1268
- kernel_z = int(dilated_z)
1269
- kernel = np.ones((kernel_z, kernel_x), dtype=np.uint8)
1270
-
1271
- # Convert the slice to the appropriate data type
1272
- tiff_slice = tiff_array[:, y, :].astype(np.uint8)
1273
-
1274
- dilated_slice = cv2.dilate(tiff_slice, kernel, iterations=1)
1275
- dilated_xz[:, y, :] = dilated_slice
1276
-
1277
- # Overlay the results
1278
- final_result = dilated_xy | dilated_xz
1279
-
1280
- return final_result
1281
-
1282
1343
  def dilation_length_to_pixels(xy_scaling, z_scaling, micronx, micronz):
1283
1344
  """Internal method to find XY and Z dilation parameters based on voxel micron scaling"""
1284
1345
  dilate_xy = 2 * int(round(micronx/xy_scaling))
@@ -1478,7 +1539,7 @@ def binarize(arrayimage, directory = None):
1478
1539
 
1479
1540
  return arrayimage
1480
1541
 
1481
- 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):
1482
1543
  """
1483
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
1484
1545
  objects will lose their shape somewhat and become cube-ish if the 'amount' param is ever significantly larger than the objects in quesiton.
@@ -1498,20 +1559,16 @@ def dilate(arrayimage, amount, xy_scale = 1, z_scale = 1, directory = None, fast
1498
1559
  else:
1499
1560
  image = None
1500
1561
 
1501
- dilate_xy, dilate_z = dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
1562
+ if not dilate_xy:
1563
+ dilate_xy, dilate_z = dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
1502
1564
 
1503
1565
  if len(np.unique(arrayimage)) > 2: #binarize
1504
1566
  arrayimage = binarize(arrayimage)
1505
1567
 
1506
- if not fast_dil and not recursive:
1507
- arrayimage = (dilate_3D(arrayimage, dilate_xy, dilate_xy, dilate_z)) * 255
1508
- if np.max(arrayimage) == 1:
1509
- arrayimage = arrayimage * 255
1510
- elif not recursive:
1511
- 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))
1512
1570
  else:
1513
- arrayimage = (dilate_3D_recursive(arrayimage, dilate_xy, dilate_xy, dilate_z)) * 255
1514
-
1571
+ arrayimage = (dilate_3D_dt(arrayimage, amount, xy_scale, z_scale))
1515
1572
 
1516
1573
 
1517
1574
  if type(image) == str:
@@ -1525,15 +1582,16 @@ def dilate(arrayimage, amount, xy_scale = 1, z_scale = 1, directory = None, fast
1525
1582
 
1526
1583
  return arrayimage
1527
1584
 
1528
- def erode(arrayimage, amount, xy_scale = 1, z_scale = 1):
1585
+ def erode(arrayimage, amount, xy_scale = 1, z_scale = 1, mode = 0):
1529
1586
  if len(np.unique(arrayimage)) > 2: #binarize
1530
1587
  arrayimage = binarize(arrayimage)
1531
1588
  erode_xy, erode_z = dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
1532
1589
 
1533
- if len(np.unique(arrayimage)) > 2: #binarize
1534
- arrayimage = binarize(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)
1535
1594
 
1536
- arrayimage = (erode_3D(arrayimage, erode_xy, erode_xy, erode_z)) * 255
1537
1595
  if np.max(arrayimage) == 1:
1538
1596
  arrayimage = arrayimage * 255
1539
1597
 
@@ -1688,7 +1746,7 @@ def fix_branches(array, G, communities, fix_val = None):
1688
1746
 
1689
1747
 
1690
1748
 
1691
- 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):
1692
1750
  """
1693
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.
1694
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.
@@ -1754,19 +1812,19 @@ def label_vertices(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
1754
1812
  if peaks > 0:
1755
1813
  image_copy = filter_size_by_peaks(image_copy, peaks)
1756
1814
  if comp_dil > 0:
1757
- image_copy = dilate(image_copy, comp_dil)
1815
+ image_copy = dilate(image_copy, comp_dil, fast_dil = fastdil)
1758
1816
 
1759
1817
  labeled_image, num_labels = label_objects(image_copy)
1760
1818
  elif max_vol > 0:
1761
1819
  image_copy = filter_size_by_vol(image_copy, max_vol)
1762
1820
  if comp_dil > 0:
1763
- image_copy = dilate(image_copy, comp_dil)
1821
+ image_copy = dilate(image_copy, comp_dil, fast_dil = fastdil)
1764
1822
 
1765
1823
  labeled_image, num_labels = label_objects(image_copy)
1766
1824
  else:
1767
1825
 
1768
1826
  if comp_dil > 0:
1769
- image_copy = dilate(image_copy, comp_dil)
1827
+ image_copy = dilate(image_copy, comp_dil, fast_dil = fastdil)
1770
1828
  labeled_image, num_labels = label_objects(image_copy)
1771
1829
 
1772
1830
  #if down_factor > 0:
@@ -3005,7 +3063,7 @@ class Network_3D:
3005
3063
  print(f"Assembling Network_3D object from files stored in directory: {directory}")
3006
3064
  self.load_nodes(directory, node_path)
3007
3065
  self.load_edges(directory, edge_path)
3008
- self.load_search_region(directory, search_region_path)
3066
+ #self.load_search_region(directory, search_region_path)
3009
3067
  self.load_network(directory, network_path)
3010
3068
  self.load_node_centroids(directory, node_centroids_path)
3011
3069
  self.load_node_identities(directory, node_identities_path)
@@ -3063,7 +3121,7 @@ class Network_3D:
3063
3121
 
3064
3122
  self._edge_centroids = edge_centroids
3065
3123
 
3066
- def calculate_search_region(self, search_region_size, GPU = True, fast_dil = False, GPU_downsample = None):
3124
+ def calculate_search_region(self, search_region_size, GPU = True, fast_dil = True, GPU_downsample = None):
3067
3125
 
3068
3126
  """
3069
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
@@ -3082,7 +3140,7 @@ class Network_3D:
3082
3140
 
3083
3141
  if search_region_size != 0:
3084
3142
 
3085
- 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.
3086
3144
 
3087
3145
  else:
3088
3146
 
@@ -3116,14 +3174,15 @@ class Network_3D:
3116
3174
  if skeletonized:
3117
3175
  binary_edges = skeletonize(binary_edges)
3118
3176
 
3119
- if search is not None and hasattr(self, '_nodes') and self._nodes is not None:
3177
+
3178
+
3179
+ if search is not None and hasattr(self, '_nodes') and self._nodes is not None and self._search_region is None:
3120
3180
  search_region = binarize(self._nodes)
3121
3181
  dilate_xy, dilate_z = dilation_length_to_pixels(self._xy_scale, self._z_scale, search, search)
3122
- if not fast_dil:
3182
+ if fast_dil:
3123
3183
  search_region = dilate_3D(search_region, dilate_xy, dilate_xy, dilate_z)
3124
3184
  else:
3125
- search_region = dilate_3D_old(search_region, dilate_xy, dilate_xy, dilate_z)
3126
-
3185
+ search_region = dilate_3D_dt(search_region, diledge, self._xy_scale, self._z_scale)
3127
3186
  else:
3128
3187
  search_region = binarize(self._search_region)
3129
3188
 
@@ -3133,19 +3192,20 @@ class Network_3D:
3133
3192
  del binary_edges
3134
3193
 
3135
3194
  if remove_edgetrunk > 0:
3136
- for i in range(remove_edgetrunk):
3137
- print(f"Snipping trunk {i + 1}...")
3138
- outer_edges = remove_trunk(outer_edges)
3195
+ print(f"Snipping trunks...")
3196
+ outer_edges = remove_trunk(outer_edges, remove_edgetrunk)
3139
3197
 
3140
3198
  if diledge is not None:
3141
3199
  dilate_xy, dilate_z = dilation_length_to_pixels(self._xy_scale, self._z_scale, diledge, diledge)
3142
- if not fast_dil and dilate_xy > 3 and dilate_z > 3:
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:
3143
3204
  outer_edges = dilate_3D(outer_edges, dilate_xy, dilate_xy, dilate_z)
3144
3205
  else:
3145
- outer_edges = dilate_3D_old(outer_edges, dilate_xy, dilate_xy, dilate_z)
3146
-
3206
+ outer_edges = dilate_3D_dt(outer_edges, diledge, self._xy_scale, self._z_scale)
3147
3207
  else:
3148
- outer_edges = dilate_3D_old(outer_edges, 3, 3, 3)
3208
+ outer_edges = dilate_3D_old(outer_edges)
3149
3209
 
3150
3210
  labelled_edges, num_edge = label_objects(outer_edges)
3151
3211
 
@@ -3277,7 +3337,7 @@ class Network_3D:
3277
3337
  self._network_lists = network_analysis.read_excel_to_lists(df)
3278
3338
  self._network, net_weights = network_analysis.weighted_network(df)
3279
3339
 
3280
- 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 = False, skeletonize = False, GPU_downsample = None):
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):
3281
3341
  """
3282
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.
3283
3343
  :param nodes: (Mandatory; String or ndarray). Filepath to segmented nodes mask or a numpy array containing the same.
@@ -3299,7 +3359,7 @@ class Network_3D:
3299
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.
3300
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.
3301
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.
3302
- :param fast_dil: (Optional - Val = False, boolean) - A boolean that when True will utilize faster cube dilation but when false will use slower spheroid dilation.
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.
3303
3363
  :param skeletonize: (Optional - Val = False, boolean) - A boolean of whether to skeletonize the edges when using them.
3304
3364
  """
3305
3365
 
@@ -4021,48 +4081,10 @@ class Network_3D:
4021
4081
  return isolated nodes, isolated edges, and isolated network in that order). IF gen_images == False (Will return just the network).
4022
4082
  """
4023
4083
 
4024
- if gen_images:
4025
-
4026
- if not hasattr(self, '_edges') or self._edges is None:
4027
-
4028
- connected_component, isonodes, _ = community_extractor.isolate_connected_component(self._nodes, self._network, key=key, directory = directory)
4029
-
4030
- nodea = []
4031
- nodeb = []
4032
- edgec = []
4033
- nodesa = self._network_lists[0]
4034
- nodesb = self._network_lists[1]
4035
- edgesc = self._network_lists[2]
4036
- for i in range(len(nodesa)):
4037
- if (nodesa[i], nodesb[i]) in connected_component:
4038
- nodea.append(nodesa[i])
4039
- nodeb.append(nodesb[i])
4040
- edgec.append(edgesc[i])
4041
- network_lists = [nodea, nodeb, edgec]
4042
- network, weights = network_analysis.weighted_network(network_lists)
4084
+ #Removed depricated gen_images functions
4043
4085
 
4044
- return isonodes, network
4045
-
4046
- else:
4047
- if full_edges is not None:
4048
- 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)
4049
-
4050
- else:
4051
- 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)
4052
-
4053
- df = create_and_save_dataframe(connected_component)
4054
- network_lists = network_analysis.read_excel_to_lists(df)
4055
- network, net_weights = network_analysis.weighted_network(df)
4056
-
4057
- if full_edges is not None:
4058
- full_edges = tifffile.imread(full_edges)
4059
- community_extractor.isolate_full_edges(searchers, full_edges, directory = directory)
4060
-
4061
- return isonodes, isoedges, network
4062
-
4063
- else:
4064
- G = community_extractor._isolate_connected(self._network, key = key)
4065
- return G
4086
+ G = community_extractor._isolate_connected(self._network, key = key)
4087
+ return G
4066
4088
 
4067
4089
 
4068
4090
  def isolate_mothers(self, directory = None, down_factor = 1, louvain = True, ret_nodes = False, called = False):
@@ -4320,7 +4342,7 @@ class Network_3D:
4320
4342
  return stats
4321
4343
 
4322
4344
 
4323
- 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):
4324
4346
 
4325
4347
 
4326
4348
 
@@ -4358,7 +4380,7 @@ class Network_3D:
4358
4380
 
4359
4381
 
4360
4382
  elif mode == 1: #Search neighborhoods morphologically, obtain densities
4361
- 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)
4362
4384
  title1 = f'Volumetric Neighborhood Distribution of Nodes in image that are {search} from nodes: {root}'
4363
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'
4364
4386
 
@@ -4405,19 +4427,23 @@ class Network_3D:
4405
4427
 
4406
4428
 
4407
4429
 
4408
- 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)
4409
4433
 
4410
- 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)
4411
4434
 
4412
4435
 
4436
+ def morph_proximity(self, search = 0, targets = None, fastdil = False):
4413
4437
 
4414
- def morph_proximity(self, search = 0, targets = None):
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)
4415
4442
 
4416
- search_x, search_z = dilation_length_to_pixels(self._xy_scale, self._z_scale, search, search)
4417
4443
 
4418
4444
  num_nodes = np.max(self._nodes)
4419
4445
 
4420
- 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)
4421
4447
 
4422
4448
  my_dict = proximity.find_shared_value_pairs(my_dict)
4423
4449
 
@@ -4490,6 +4516,8 @@ class Network_3D:
4490
4516
 
4491
4517
  self._network_lists = network_analysis.read_excel_to_lists(network)
4492
4518
 
4519
+ #self._network is a networkx graph that stores the connections
4520
+
4493
4521
  self.remove_edge_weights()
4494
4522
 
4495
4523
  return array