nettracer3d 1.1.1__py3-none-any.whl → 1.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nettracer3d might be problematic. Click here for more details.
- nettracer3d/branch_stitcher.py +420 -0
- nettracer3d/filaments.py +1068 -0
- nettracer3d/morphology.py +9 -4
- nettracer3d/neighborhoods.py +99 -67
- nettracer3d/nettracer.py +398 -52
- nettracer3d/nettracer_gui.py +1746 -483
- nettracer3d/network_draw.py +9 -3
- nettracer3d/node_draw.py +41 -58
- nettracer3d/proximity.py +123 -2
- nettracer3d/smart_dilate.py +36 -0
- nettracer3d/tutorial.py +2874 -0
- {nettracer3d-1.1.1.dist-info → nettracer3d-1.2.4.dist-info}/METADATA +6 -6
- nettracer3d-1.2.4.dist-info/RECORD +29 -0
- nettracer3d-1.1.1.dist-info/RECORD +0 -26
- {nettracer3d-1.1.1.dist-info → nettracer3d-1.2.4.dist-info}/WHEEL +0 -0
- {nettracer3d-1.1.1.dist-info → nettracer3d-1.2.4.dist-info}/entry_points.txt +0 -0
- {nettracer3d-1.1.1.dist-info → nettracer3d-1.2.4.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-1.1.1.dist-info → nettracer3d-1.2.4.dist-info}/top_level.txt +0 -0
nettracer3d/network_draw.py
CHANGED
|
@@ -77,7 +77,12 @@ def draw_line_inplace(start, end, array):
|
|
|
77
77
|
"""
|
|
78
78
|
|
|
79
79
|
# Calculate the distances between start and end coordinates
|
|
80
|
-
|
|
80
|
+
try:
|
|
81
|
+
distances = end - start
|
|
82
|
+
except:
|
|
83
|
+
end = np.array(end)
|
|
84
|
+
start = np.array(start)
|
|
85
|
+
distances = end - start
|
|
81
86
|
|
|
82
87
|
# Determine the number of steps along the line
|
|
83
88
|
num_steps = int(max(np.abs(distances)) + 1)
|
|
@@ -242,7 +247,8 @@ def draw_network_from_centroids(nodes, network, centroids, twod_bool, directory
|
|
|
242
247
|
centroid_dic[item] = centroid
|
|
243
248
|
|
|
244
249
|
except KeyError:
|
|
245
|
-
|
|
250
|
+
pass
|
|
251
|
+
#print(f"Centroid {item} missing")
|
|
246
252
|
output_stack = np.zeros(np.shape(nodes), dtype=np.uint8)
|
|
247
253
|
|
|
248
254
|
for i, pair1_val in enumerate(pair1):
|
|
@@ -253,7 +259,7 @@ def draw_network_from_centroids(nodes, network, centroids, twod_bool, directory
|
|
|
253
259
|
pair2_centroid = centroid_dic[pair2_val]
|
|
254
260
|
draw_line_inplace(pair1_centroid, pair2_centroid, output_stack)
|
|
255
261
|
except KeyError:
|
|
256
|
-
print(f"Missing centroid {i}")
|
|
262
|
+
#print(f"Missing centroid {i}")
|
|
257
263
|
pass
|
|
258
264
|
|
|
259
265
|
if twod_bool:
|
nettracer3d/node_draw.py
CHANGED
|
@@ -3,7 +3,7 @@ import tifffile
|
|
|
3
3
|
from scipy import ndimage
|
|
4
4
|
from PIL import Image, ImageDraw, ImageFont
|
|
5
5
|
from scipy.ndimage import zoom
|
|
6
|
-
|
|
6
|
+
import cv2
|
|
7
7
|
|
|
8
8
|
def downsample(data, factor, directory=None, order=0):
|
|
9
9
|
"""
|
|
@@ -121,83 +121,66 @@ def draw_nodes(nodes, num_nodes):
|
|
|
121
121
|
# Save the draw_array as a 3D TIFF file
|
|
122
122
|
tifffile.imwrite("labelled_nodes.tif", draw_array)
|
|
123
123
|
|
|
124
|
-
def draw_from_centroids(nodes, num_nodes, centroids, twod_bool, directory
|
|
125
|
-
"""
|
|
126
|
-
print("Drawing node IDs
|
|
127
|
-
# Create a new 3D array to draw on with the same dimensions as the original array
|
|
124
|
+
def draw_from_centroids(nodes, num_nodes, centroids, twod_bool, directory=None):
|
|
125
|
+
"""Optimized version using OpenCV"""
|
|
126
|
+
print("Drawing node IDs...")
|
|
128
127
|
draw_array = np.zeros_like(nodes, dtype=np.uint8)
|
|
129
|
-
|
|
130
|
-
#
|
|
131
|
-
|
|
132
|
-
# Iterate through each centroid
|
|
128
|
+
|
|
129
|
+
# Draw text using OpenCV (no PIL conversions needed)
|
|
133
130
|
for idx in centroids.keys():
|
|
134
131
|
centroid = centroids[idx]
|
|
135
|
-
z, y, x = centroid.astype(int)
|
|
136
|
-
|
|
137
|
-
try:
|
|
138
|
-
draw_array = _draw_at_plane(z, y, x, draw_array, idx)
|
|
139
|
-
except IndexError:
|
|
140
|
-
pass
|
|
141
132
|
|
|
142
133
|
try:
|
|
143
|
-
|
|
144
|
-
except
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
134
|
+
z, y, x = centroid.astype(int)
|
|
135
|
+
except:
|
|
136
|
+
z, y, x = centroid
|
|
137
|
+
|
|
138
|
+
for z_offset in [0, 1, -1]:
|
|
139
|
+
z_target = z + z_offset
|
|
140
|
+
if 0 <= z_target < draw_array.shape[0]:
|
|
141
|
+
cv2.putText(draw_array[z_target], str(idx), (x, y),
|
|
142
|
+
cv2.FONT_HERSHEY_SIMPLEX, 0.4, 255, 1, cv2.LINE_AA)
|
|
143
|
+
|
|
152
144
|
if twod_bool:
|
|
153
145
|
draw_array = draw_array[0,:,:] | draw_array[1,:,:]
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if directory is None:
|
|
157
|
-
filename = 'labelled_node_indices.tif'
|
|
158
|
-
else:
|
|
159
|
-
filename = f'{directory}/labelled_node_indices.tif'
|
|
160
|
-
|
|
146
|
+
|
|
147
|
+
filename = f'{directory}/labelled_node_indices.tif' if directory else 'labelled_node_indices.tif'
|
|
161
148
|
try:
|
|
162
|
-
|
|
163
|
-
# Save the draw_array as a 3D TIFF file
|
|
164
149
|
tifffile.imwrite(filename, draw_array)
|
|
165
|
-
|
|
166
150
|
except Exception as e:
|
|
167
151
|
print(f"Could not save node indices to {filename}")
|
|
168
|
-
|
|
152
|
+
|
|
169
153
|
return draw_array
|
|
170
154
|
|
|
171
155
|
def degree_draw(degree_dict, centroid_dict, nodes):
|
|
156
|
+
"""Draw node degrees at centroid locations using OpenCV"""
|
|
172
157
|
# Create a new 3D array to draw on with the same dimensions as the original array
|
|
173
158
|
draw_array = np.zeros_like(nodes, dtype=np.uint8)
|
|
174
|
-
|
|
175
|
-
|
|
159
|
+
|
|
176
160
|
for node in centroid_dict:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
degree = degree_dict[node]
|
|
180
|
-
except:
|
|
161
|
+
# Skip if node not in degree_dict
|
|
162
|
+
if node not in degree_dict:
|
|
181
163
|
continue
|
|
182
164
|
|
|
165
|
+
degree = degree_dict[node]
|
|
183
166
|
z, y, x = centroid_dict[node].astype(int)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
167
|
+
|
|
168
|
+
# Draw on current z-plane and adjacent planes
|
|
169
|
+
for z_offset in [0, 1, -1]:
|
|
170
|
+
z_target = z + z_offset
|
|
171
|
+
# Check bounds
|
|
172
|
+
if 0 <= z_target < draw_array.shape[0]:
|
|
173
|
+
cv2.putText(
|
|
174
|
+
draw_array[z_target], # Image to draw on
|
|
175
|
+
str(degree), # Text to draw
|
|
176
|
+
(x, y), # Position (x, y)
|
|
177
|
+
cv2.FONT_HERSHEY_SIMPLEX, # Font
|
|
178
|
+
0.4, # Font scale
|
|
179
|
+
255, # Color (white)
|
|
180
|
+
1, # Thickness
|
|
181
|
+
cv2.LINE_AA # Anti-aliasing
|
|
182
|
+
)
|
|
183
|
+
|
|
201
184
|
return draw_array
|
|
202
185
|
|
|
203
186
|
def degree_infect(degree_dict, nodes, make_floats = False):
|
nettracer3d/proximity.py
CHANGED
|
@@ -12,6 +12,7 @@ from typing import Dict, Union, Tuple, List, Optional
|
|
|
12
12
|
from collections import defaultdict
|
|
13
13
|
from multiprocessing import Pool, cpu_count
|
|
14
14
|
import functools
|
|
15
|
+
from . import smart_dilate as sdl
|
|
15
16
|
|
|
16
17
|
# Related to morphological border searching:
|
|
17
18
|
|
|
@@ -104,7 +105,6 @@ def create_node_dictionary(nodes, num_nodes, dilate_xy, dilate_z, targets=None,
|
|
|
104
105
|
"""pre-compute all bounding boxes using find_objects"""
|
|
105
106
|
node_dict = {}
|
|
106
107
|
array_shape = nodes.shape
|
|
107
|
-
|
|
108
108
|
|
|
109
109
|
# Get all bounding boxes at once
|
|
110
110
|
bounding_boxes = ndimage.find_objects(nodes)
|
|
@@ -1056,4 +1056,125 @@ def create_dict_entry_id(node_dict, label, sub_nodes, sub_edges):
|
|
|
1056
1056
|
if label is None:
|
|
1057
1057
|
pass
|
|
1058
1058
|
else:
|
|
1059
|
-
node_dict[label] = _get_node_edge_dict_id(sub_nodes, sub_edges, label)
|
|
1059
|
+
node_dict[label] = _get_node_edge_dict_id(sub_nodes, sub_edges, label)
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
# For the continuous structure labeler:
|
|
1063
|
+
|
|
1064
|
+
def get_reslice_space(slice_obj, array_shape):
|
|
1065
|
+
z_slice, y_slice, x_slice = slice_obj
|
|
1066
|
+
|
|
1067
|
+
# Extract min/max from slices
|
|
1068
|
+
z_min, z_max = z_slice.start, z_slice.stop - 1
|
|
1069
|
+
y_min, y_max = y_slice.start, y_slice.stop - 1
|
|
1070
|
+
x_min, x_max = x_slice.start, x_slice.stop - 1
|
|
1071
|
+
# Add padding
|
|
1072
|
+
y_max = y_max + 1
|
|
1073
|
+
y_min = y_min - 1
|
|
1074
|
+
x_max = x_max + 1
|
|
1075
|
+
x_min = x_min - 1
|
|
1076
|
+
z_max = z_max + 1
|
|
1077
|
+
z_min = z_min - 1
|
|
1078
|
+
# Boundary checks
|
|
1079
|
+
y_max = min(y_max, array_shape[1] - 1)
|
|
1080
|
+
x_max = min(x_max, array_shape[2] - 1)
|
|
1081
|
+
z_max = min(z_max, array_shape[0] - 1)
|
|
1082
|
+
y_min = max(y_min, 0)
|
|
1083
|
+
x_min = max(x_min, 0)
|
|
1084
|
+
z_min = max(z_min, 0)
|
|
1085
|
+
return [z_min, z_max], [y_min, y_max], [x_min, x_max]
|
|
1086
|
+
|
|
1087
|
+
def reslice_array(args):
|
|
1088
|
+
"""Internal method used for the secondary algorithm to reslice subarrays."""
|
|
1089
|
+
input_array, z_range, y_range, x_range = args
|
|
1090
|
+
z_start, z_end = z_range
|
|
1091
|
+
z_start, z_end = int(z_start), int(z_end)
|
|
1092
|
+
y_start, y_end = y_range
|
|
1093
|
+
y_start, y_end = int(y_start), int(y_end)
|
|
1094
|
+
x_start, x_end = x_range
|
|
1095
|
+
x_start, x_end = int(x_start), int(x_end)
|
|
1096
|
+
|
|
1097
|
+
# Reslice the array
|
|
1098
|
+
resliced_array = input_array[z_start:z_end + 1, y_start:y_end + 1, x_start:x_end + 1]
|
|
1099
|
+
|
|
1100
|
+
return resliced_array
|
|
1101
|
+
|
|
1102
|
+
def _reassign_label_by_continuous_proximity(sub_to_assign, sub_labels, label):
|
|
1103
|
+
"""Internal method used for the secondary algorithm to find pixel involvement of component to be labeled based on nearby labels."""
|
|
1104
|
+
|
|
1105
|
+
# Create a boolean mask where elements with the specified label are True
|
|
1106
|
+
sub_to_assign = sub_to_assign == label
|
|
1107
|
+
sub_to_assign = nettracer.dilate_3D_old(sub_to_assign, 3, 3, 3) #Dilate the label by 1 to see where the dilated label overlaps
|
|
1108
|
+
sub_to_assign = sub_to_assign != 0
|
|
1109
|
+
sub_labels = sub_labels * sub_to_assign # Isolate only adjacent label
|
|
1110
|
+
sub_to_assign = sdl.smart_label_single(sub_to_assign, sub_labels) # Assign labeling schema from 'sub_to_assign' to 'sub_labels'
|
|
1111
|
+
return sub_to_assign
|
|
1112
|
+
|
|
1113
|
+
def process_and_write_voxels(args):
|
|
1114
|
+
"""Optimized version using vectorized operations"""
|
|
1115
|
+
to_assign, labels, label, array_shape, bounding_boxes, result_array = args
|
|
1116
|
+
print(f"Processing node {label}")
|
|
1117
|
+
|
|
1118
|
+
# Get the pre-computed bounding box for this label
|
|
1119
|
+
slice_obj = bounding_boxes[label-1] # -1 because label numbers start at 1
|
|
1120
|
+
|
|
1121
|
+
z_vals, y_vals, x_vals = get_reslice_space(slice_obj, array_shape)
|
|
1122
|
+
z_start, z_end = z_vals
|
|
1123
|
+
y_start, y_end = y_vals
|
|
1124
|
+
x_start, x_end = x_vals
|
|
1125
|
+
|
|
1126
|
+
# Extract subarrays
|
|
1127
|
+
sub_to_assign = reslice_array((to_assign, z_vals, y_vals, x_vals))
|
|
1128
|
+
sub_labels = reslice_array((labels, z_vals, y_vals, x_vals))
|
|
1129
|
+
|
|
1130
|
+
# Create mask for this label BEFORE relabeling (critical!)
|
|
1131
|
+
label_mask = (sub_to_assign == label)
|
|
1132
|
+
|
|
1133
|
+
# Get local coordinates of voxels belonging to this label
|
|
1134
|
+
local_coords = np.where(label_mask)
|
|
1135
|
+
|
|
1136
|
+
# Do the relabeling on the subarray
|
|
1137
|
+
# Note: relabeled may contain multiple different label values now
|
|
1138
|
+
relabeled = _reassign_label_by_continuous_proximity(sub_to_assign, sub_labels, label)
|
|
1139
|
+
|
|
1140
|
+
# Apply offsets (vectorized)
|
|
1141
|
+
global_coords = (
|
|
1142
|
+
local_coords[0] + z_start,
|
|
1143
|
+
local_coords[1] + y_start,
|
|
1144
|
+
local_coords[2] + x_start
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
# Single vectorized write operation
|
|
1148
|
+
# This copies all new label values (potentially multiple different labels)
|
|
1149
|
+
# for voxels that originally belonged to 'label'
|
|
1150
|
+
result_array[global_coords] = relabeled[local_coords]
|
|
1151
|
+
|
|
1152
|
+
def create_label_map(to_assign, labels, num_labels, array_shape):
|
|
1153
|
+
"""Modified to pre-compute all bounding boxes and write voxels in parallel"""
|
|
1154
|
+
|
|
1155
|
+
# Get all bounding boxes at once
|
|
1156
|
+
bounding_boxes = ndimage.find_objects(to_assign)
|
|
1157
|
+
|
|
1158
|
+
# Clone to_assign for modifications (original used for reading subarrays)
|
|
1159
|
+
result_array = to_assign.copy()
|
|
1160
|
+
|
|
1161
|
+
# Use ThreadPoolExecutor for parallel execution
|
|
1162
|
+
with ThreadPoolExecutor(max_workers=mp.cpu_count()) as executor:
|
|
1163
|
+
# Create args list with bounding_boxes and result_array included
|
|
1164
|
+
args_list = [(to_assign, labels, i, array_shape, bounding_boxes, result_array)
|
|
1165
|
+
for i in range(1, num_labels + 1)]
|
|
1166
|
+
|
|
1167
|
+
# Execute parallel tasks - each writes directly to result_array
|
|
1168
|
+
futures = [executor.submit(process_and_write_voxels, args) for args in args_list]
|
|
1169
|
+
|
|
1170
|
+
# Wait for all to complete
|
|
1171
|
+
for future in futures:
|
|
1172
|
+
future.result()
|
|
1173
|
+
|
|
1174
|
+
return result_array
|
|
1175
|
+
|
|
1176
|
+
def label_continuous(to_assign, labels):
|
|
1177
|
+
array_shape = to_assign.shape
|
|
1178
|
+
num_labels = np.max(to_assign)
|
|
1179
|
+
result = create_label_map(to_assign, labels, num_labels, array_shape)
|
|
1180
|
+
return result
|
nettracer3d/smart_dilate.py
CHANGED
|
@@ -495,6 +495,42 @@ def smart_label(binary_array, label_array, directory = None, GPU = True, predown
|
|
|
495
495
|
|
|
496
496
|
return dilated_nodes_with_labels
|
|
497
497
|
|
|
498
|
+
def smart_label_single(binary_array, label_array):
|
|
499
|
+
|
|
500
|
+
# Step 1: Binarize the labeled array
|
|
501
|
+
binary_core = binarize(label_array)
|
|
502
|
+
binary_array = binarize(binary_array)
|
|
503
|
+
|
|
504
|
+
# Step 3: Isolate the ring (binary dilated mask minus original binary mask)
|
|
505
|
+
ring_mask = binary_array & invert_array(binary_core)
|
|
506
|
+
|
|
507
|
+
nearest_label_indices = compute_distance_transform(invert_array(label_array))
|
|
508
|
+
|
|
509
|
+
# Step 5: Process the entire array without parallelization
|
|
510
|
+
dilated_nodes_with_labels = process_chunk(0, label_array.shape[1], label_array, ring_mask, nearest_label_indices)
|
|
511
|
+
|
|
512
|
+
dilated_nodes_with_labels = dilated_nodes_with_labels * binary_array
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
return dilated_nodes_with_labels
|
|
516
|
+
|
|
517
|
+
def neighbor_label(binary_array, label_array):
|
|
518
|
+
|
|
519
|
+
binary_array = binary_array != 0
|
|
520
|
+
|
|
521
|
+
targets = binary_array - binarize(label_array)
|
|
522
|
+
|
|
523
|
+
label_array = smart_dilate(label_array, 3, 3, GPU = False, fast_dil = False, xy_scale = 1, z_scale = 1)
|
|
524
|
+
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
#TBD: This requires the serial dilation strategy methinks, unfortunately. Get labeled components of all outer regions of the binary array to be labeled. For each, cut out a sub array from the old label array and pad by one in all dimensions. Apply boolean threshold to get just the label we want. Apply binary dilation on the binary subarray of just one and multiply that against the labeled sub array - we have isolated the available labelers. Finally do smart dilate on this sub array region.
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
|
|
498
534
|
def compute_distance_transform_GPU(nodes, return_dists = False, sampling = [1, 1, 1]):
|
|
499
535
|
is_pseudo_3d = nodes.shape[0] == 1
|
|
500
536
|
if is_pseudo_3d:
|