nettracer3d 1.2.4__py3-none-any.whl → 1.2.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/branch_stitcher.py +9 -4
- nettracer3d/filaments.py +11 -4
- nettracer3d/modularity.py +15 -6
- nettracer3d/morphology.py +1 -1
- nettracer3d/nettracer.py +53 -155
- nettracer3d/nettracer_gui.py +218 -127
- nettracer3d/network_analysis.py +36 -48
- nettracer3d/network_draw.py +16 -15
- nettracer3d/node_draw.py +4 -4
- nettracer3d/proximity.py +36 -150
- nettracer3d/simple_network.py +28 -9
- nettracer3d/smart_dilate.py +200 -107
- nettracer3d/tutorial.py +32 -65
- {nettracer3d-1.2.4.dist-info → nettracer3d-1.2.7.dist-info}/METADATA +24 -10
- nettracer3d-1.2.7.dist-info/RECORD +29 -0
- {nettracer3d-1.2.4.dist-info → nettracer3d-1.2.7.dist-info}/licenses/LICENSE +2 -2
- nettracer3d-1.2.4.dist-info/RECORD +0 -29
- {nettracer3d-1.2.4.dist-info → nettracer3d-1.2.7.dist-info}/WHEEL +0 -0
- {nettracer3d-1.2.4.dist-info → nettracer3d-1.2.7.dist-info}/entry_points.txt +0 -0
- {nettracer3d-1.2.4.dist-info → nettracer3d-1.2.7.dist-info}/top_level.txt +0 -0
nettracer3d/branch_stitcher.py
CHANGED
|
@@ -3,6 +3,7 @@ import networkx as nx
|
|
|
3
3
|
from . import nettracer as n3d
|
|
4
4
|
from scipy.ndimage import distance_transform_edt, gaussian_filter, binary_fill_holes
|
|
5
5
|
from scipy.spatial import cKDTree
|
|
6
|
+
from . import smart_dilate as sdl
|
|
6
7
|
from skimage.morphology import remove_small_objects, skeletonize
|
|
7
8
|
import warnings
|
|
8
9
|
warnings.filterwarnings('ignore')
|
|
@@ -14,9 +15,13 @@ class VesselDenoiser:
|
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
17
|
def __init__(self,
|
|
17
|
-
score_thresh = 2
|
|
18
|
+
score_thresh = 2,
|
|
19
|
+
xy_scale = 1,
|
|
20
|
+
z_scale = 1):
|
|
18
21
|
|
|
19
22
|
self.score_thresh = score_thresh
|
|
23
|
+
self.xy_scale = xy_scale
|
|
24
|
+
self.z_scale = z_scale
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
def select_kernel_points_topology(self, data, skeleton):
|
|
@@ -336,7 +341,7 @@ class VesselDenoiser:
|
|
|
336
341
|
# Compute distance transform
|
|
337
342
|
if verbose:
|
|
338
343
|
print("Computing distance transform...")
|
|
339
|
-
distance_map =
|
|
344
|
+
distance_map = sdl.compute_distance_transform_distance(data, fast_dil = True)
|
|
340
345
|
|
|
341
346
|
# Extract endpoints
|
|
342
347
|
if verbose:
|
|
@@ -390,14 +395,14 @@ class VesselDenoiser:
|
|
|
390
395
|
return label_dict
|
|
391
396
|
|
|
392
397
|
|
|
393
|
-
def trace(data, labeled_skeleton, verts, score_thresh=10, verbose=False):
|
|
398
|
+
def trace(data, labeled_skeleton, verts, score_thresh=10, xy_scale = 1, z_scale = 1, verbose=False):
|
|
394
399
|
"""
|
|
395
400
|
Trace and unify skeleton labels using vertex-based endpoint grouping
|
|
396
401
|
"""
|
|
397
402
|
skeleton = n3d.binarize(labeled_skeleton)
|
|
398
403
|
|
|
399
404
|
# Create denoiser
|
|
400
|
-
denoiser = VesselDenoiser(score_thresh=score_thresh)
|
|
405
|
+
denoiser = VesselDenoiser(score_thresh=score_thresh, xy_scale = xy_scale, z_scale = z_scale)
|
|
401
406
|
|
|
402
407
|
# Run label unification
|
|
403
408
|
label_dict = denoiser.denoise(data, skeleton, labeled_skeleton, verts, verbose=verbose)
|
nettracer3d/filaments.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import networkx as nx
|
|
3
3
|
from . import nettracer as n3d
|
|
4
|
-
from scipy.ndimage import
|
|
4
|
+
from scipy.ndimage import gaussian_filter, binary_fill_holes
|
|
5
5
|
from scipy.spatial import cKDTree
|
|
6
6
|
from skimage.morphology import remove_small_objects, skeletonize
|
|
7
7
|
import warnings
|
|
8
|
+
from . import smart_dilate as sdl
|
|
8
9
|
warnings.filterwarnings('ignore')
|
|
9
10
|
|
|
10
11
|
|
|
@@ -22,6 +23,8 @@ class VesselDenoiser:
|
|
|
22
23
|
blob_volume = 200,
|
|
23
24
|
spine_removal=0,
|
|
24
25
|
score_thresh = 2,
|
|
26
|
+
xy_scale = 1,
|
|
27
|
+
z_scale = 1,
|
|
25
28
|
radius_aware_distance=True):
|
|
26
29
|
"""
|
|
27
30
|
Parameters:
|
|
@@ -46,6 +49,8 @@ class VesselDenoiser:
|
|
|
46
49
|
self.spine_removal = spine_removal
|
|
47
50
|
self.radius_aware_distance = radius_aware_distance
|
|
48
51
|
self.score_thresh = score_thresh
|
|
52
|
+
self.xy_scale = xy_scale
|
|
53
|
+
self.z_scale = z_scale
|
|
49
54
|
|
|
50
55
|
self._sphere_cache = {} # Cache sphere masks for different radii
|
|
51
56
|
|
|
@@ -939,7 +944,7 @@ class VesselDenoiser:
|
|
|
939
944
|
|
|
940
945
|
if verbose:
|
|
941
946
|
print("Step 3: Computing distance transform...")
|
|
942
|
-
distance_map =
|
|
947
|
+
distance_map = sdl.compute_distance_transform_distance(cleaned, fast_dil = True)
|
|
943
948
|
|
|
944
949
|
# Step 3: Sample kernels along skeleton
|
|
945
950
|
if verbose:
|
|
@@ -1036,7 +1041,7 @@ class VesselDenoiser:
|
|
|
1036
1041
|
return result
|
|
1037
1042
|
|
|
1038
1043
|
|
|
1039
|
-
def trace(data, kernel_spacing = 1, max_distance = 20, min_component = 20, gap_tolerance = 5, blob_sphericity = 1.0, blob_volume = 200, spine_removal = 0, score_thresh = 2):
|
|
1044
|
+
def trace(data, kernel_spacing = 1, max_distance = 20, min_component = 20, gap_tolerance = 5, blob_sphericity = 1.0, blob_volume = 200, spine_removal = 0, score_thresh = 2, xy_scale = 1, z_scale = 1):
|
|
1040
1045
|
|
|
1041
1046
|
"""Main function with user prompts"""
|
|
1042
1047
|
|
|
@@ -1054,7 +1059,9 @@ def trace(data, kernel_spacing = 1, max_distance = 20, min_component = 20, gap_t
|
|
|
1054
1059
|
blob_sphericity = blob_sphericity,
|
|
1055
1060
|
blob_volume = blob_volume,
|
|
1056
1061
|
spine_removal = spine_removal,
|
|
1057
|
-
score_thresh = score_thresh
|
|
1062
|
+
score_thresh = score_thresh,
|
|
1063
|
+
xy_scale = xy_scale,
|
|
1064
|
+
z_scale = z_scale
|
|
1058
1065
|
)
|
|
1059
1066
|
|
|
1060
1067
|
# Run denoising
|
nettracer3d/modularity.py
CHANGED
|
@@ -103,7 +103,7 @@ def read_excel_to_lists(file_path, sheet_name=0):
|
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
|
|
106
|
-
def show_communities_flex(G, master_list, normalized_weights, geo_info=None, geometric=False, directory=None, weighted=True, partition=None, style=0):
|
|
106
|
+
def show_communities_flex(G, master_list, normalized_weights, geo_info=None, geometric=False, directory=None, weighted=True, partition=None, style=0, show_labels = True):
|
|
107
107
|
|
|
108
108
|
if normalized_weights is None:
|
|
109
109
|
G, edge_weights = network_analysis.weighted_network(master_list)
|
|
@@ -179,7 +179,8 @@ def show_communities_flex(G, master_list, normalized_weights, geo_info=None, geo
|
|
|
179
179
|
normalized_weight = G[edge[0]][edge[1]]['weight']
|
|
180
180
|
nx.draw_networkx_edges(G, pos, edgelist=[edge], width=5 * normalized_weight, edge_color='black')
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
if show_labels:
|
|
183
|
+
nx.draw_networkx_labels(G, pos)
|
|
183
184
|
|
|
184
185
|
else:
|
|
185
186
|
pos = nx.spring_layout(G)
|
|
@@ -195,20 +196,28 @@ def show_communities_flex(G, master_list, normalized_weights, geo_info=None, geo
|
|
|
195
196
|
normalized_weight = G[edge[0]][edge[1]]['weight']
|
|
196
197
|
nx.draw_networkx_edges(G, pos, edgelist=[edge], width=5 * normalized_weight, edge_color='black')
|
|
197
198
|
|
|
198
|
-
|
|
199
|
+
if show_labels:
|
|
200
|
+
nx.draw_networkx_labels(G, pos)
|
|
199
201
|
|
|
200
202
|
else:
|
|
201
203
|
# Create node color list based on partition and the same color mapping
|
|
202
|
-
node_colors = [
|
|
204
|
+
node_colors = []
|
|
205
|
+
for node in G.nodes():
|
|
206
|
+
try:
|
|
207
|
+
node_colors.append(colors_matplotlib[partition[node]])
|
|
208
|
+
except:
|
|
209
|
+
node_colors.append((1, 1, 1))
|
|
210
|
+
|
|
211
|
+
#node_colors = [colors_matplotlib[partition[node]] for node in G.nodes()]
|
|
203
212
|
|
|
204
213
|
if geometric:
|
|
205
214
|
pos, z_pos = simple_network.geometric_positions(geo_info[0], geo_info[1])
|
|
206
215
|
node_sizes_list = [z_pos[node] for node in G.nodes()]
|
|
207
|
-
nx.draw(G, pos, with_labels=
|
|
216
|
+
nx.draw(G, pos, with_labels=show_labels, font_color='black', font_weight='bold',
|
|
208
217
|
node_size=node_sizes_list, node_color=node_colors, alpha=0.8, font_size=12)
|
|
209
218
|
else:
|
|
210
219
|
pos = nx.spring_layout(G)
|
|
211
|
-
nx.draw(G, pos, with_labels=
|
|
220
|
+
nx.draw(G, pos, with_labels=show_labels, font_color='black', font_weight='bold',
|
|
212
221
|
node_size=100, node_color=node_colors, alpha=0.8)
|
|
213
222
|
|
|
214
223
|
plt.axis('off')
|
nettracer3d/morphology.py
CHANGED
|
@@ -337,7 +337,7 @@ def search_neighbor_ids(nodes, targets, id_dict, neighborhood_dict, totals, sear
|
|
|
337
337
|
targets = np.isin(nodes, targets)
|
|
338
338
|
targets = nettracer.binarize(targets)
|
|
339
339
|
|
|
340
|
-
dilated = nettracer.
|
|
340
|
+
dilated = nettracer.dilate_3D_dt(targets, search, xy_scaling = xy_scale, z_scaling = z_scale, fast_dil = fastdil)
|
|
341
341
|
dilated = dilated - targets #technically we dont need the cores
|
|
342
342
|
search_vol = np.count_nonzero(dilated) * xy_scale * xy_scale * z_scale #need this for density
|
|
343
343
|
targets = dilated != 0
|
nettracer3d/nettracer.py
CHANGED
|
@@ -90,6 +90,7 @@ def reslice_3d_array(args):
|
|
|
90
90
|
def _get_node_edge_dict(label_array, edge_array, label, dilate_xy, dilate_z):
|
|
91
91
|
"""Internal method used for the secondary algorithm to find which nodes interact with which edges."""
|
|
92
92
|
|
|
93
|
+
import tifffile
|
|
93
94
|
# Create a boolean mask where elements with the specified label are True
|
|
94
95
|
label_array = label_array == label
|
|
95
96
|
label_array = dilate_3D(label_array, dilate_xy, dilate_xy, dilate_z) #Dilate the label to see where the dilated label overlaps
|
|
@@ -104,7 +105,7 @@ def _get_node_edge_dict(label_array, edge_array, label, dilate_xy, dilate_z):
|
|
|
104
105
|
def process_label(args):
|
|
105
106
|
"""Modified to use pre-computed bounding boxes instead of argwhere"""
|
|
106
107
|
nodes, edges, label, dilate_xy, dilate_z, array_shape, bounding_boxes = args
|
|
107
|
-
print(f"Processing node {label}")
|
|
108
|
+
#print(f"Processing node {label}")
|
|
108
109
|
|
|
109
110
|
# Get the pre-computed bounding box for this label
|
|
110
111
|
slice_obj = bounding_boxes[int(label)-1] # -1 because label numbers start at 1
|
|
@@ -122,6 +123,7 @@ def process_label(args):
|
|
|
122
123
|
|
|
123
124
|
def create_node_dictionary(nodes, edges, num_nodes, dilate_xy, dilate_z):
|
|
124
125
|
"""Modified to pre-compute all bounding boxes using find_objects"""
|
|
126
|
+
print("Calculating network...")
|
|
125
127
|
node_dict = {}
|
|
126
128
|
array_shape = nodes.shape
|
|
127
129
|
|
|
@@ -1520,7 +1522,7 @@ def dilate_2D(array, search, scaling = 1):
|
|
|
1520
1522
|
return inv
|
|
1521
1523
|
|
|
1522
1524
|
|
|
1523
|
-
def dilate_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0):
|
|
1525
|
+
def dilate_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0, fast_dil = False):
|
|
1524
1526
|
"""
|
|
1525
1527
|
Dilate a 3D array using distance transform method. Dt dilation produces perfect results but only works in euclidean geometry and lags in big arrays.
|
|
1526
1528
|
|
|
@@ -1568,7 +1570,7 @@ def dilate_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0):
|
|
|
1568
1570
|
"""
|
|
1569
1571
|
|
|
1570
1572
|
# Compute distance transform (Euclidean)
|
|
1571
|
-
inv = smart_dilate.compute_distance_transform_distance(inv, sampling = [z_scaling, xy_scaling, xy_scaling])
|
|
1573
|
+
inv = smart_dilate.compute_distance_transform_distance(inv, sampling = [z_scaling, xy_scaling, xy_scaling], fast_dil = fast_dil)
|
|
1572
1574
|
|
|
1573
1575
|
#inv = inv * cardinal
|
|
1574
1576
|
|
|
@@ -1615,7 +1617,7 @@ def erode_2D(array, search, scaling=1, preserve_labels = False):
|
|
|
1615
1617
|
|
|
1616
1618
|
return array
|
|
1617
1619
|
|
|
1618
|
-
def erode_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0, preserve_labels = False):
|
|
1620
|
+
def erode_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0, fast_dil = False, preserve_labels = False):
|
|
1619
1621
|
"""
|
|
1620
1622
|
Erode a 3D array using distance transform method. DT erosion produces perfect results
|
|
1621
1623
|
with Euclidean geometry, but may be slower for large arrays.
|
|
@@ -1643,34 +1645,30 @@ def erode_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0, preserve_
|
|
|
1643
1645
|
|
|
1644
1646
|
borders = find_boundaries(array, mode='thick')
|
|
1645
1647
|
mask = array * invert_array(borders)
|
|
1646
|
-
mask = smart_dilate.compute_distance_transform_distance(mask, sampling = [z_scaling, xy_scaling, xy_scaling])
|
|
1648
|
+
mask = smart_dilate.compute_distance_transform_distance(mask, sampling = [z_scaling, xy_scaling, xy_scaling], fast_dil = fast_dil)
|
|
1647
1649
|
mask = mask >= search_distance
|
|
1648
1650
|
array = mask * array
|
|
1649
1651
|
else:
|
|
1650
|
-
array = smart_dilate.compute_distance_transform_distance(array, sampling = [z_scaling, xy_scaling, xy_scaling])
|
|
1652
|
+
array = smart_dilate.compute_distance_transform_distance(array, sampling = [z_scaling, xy_scaling, xy_scaling], fast_dil = fast_dil)
|
|
1651
1653
|
# Threshold the distance transform to get eroded result
|
|
1652
1654
|
# For erosion, we keep only the points that are at least search_distance from the boundary
|
|
1653
1655
|
array = array > search_distance
|
|
1654
1656
|
|
|
1655
|
-
# Resample back to original dimensions if needed
|
|
1656
|
-
#if rev_factor:
|
|
1657
|
-
#array = ndimage.zoom(array, rev_factor, order=0) # Use order=0 for binary masks
|
|
1658
|
-
|
|
1659
1657
|
return array.astype(np.uint8)
|
|
1660
1658
|
|
|
1661
1659
|
|
|
1662
1660
|
def dilate_3D(tiff_array, dilated_x, dilated_y, dilated_z):
|
|
1663
|
-
"""Internal method to dilate an array in 3D. Dilation this way is much faster than using a distance transform although the latter is
|
|
1661
|
+
"""Internal method to dilate an array in 3D. Dilation this way is much faster than using a distance transform although the latter is more accurate.
|
|
1664
1662
|
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.
|
|
1665
1663
|
"""
|
|
1666
1664
|
|
|
1667
|
-
if tiff_array.shape[0] == 1:
|
|
1668
|
-
return dilate_2D(tiff_array, ((dilated_x - 1) / 2))
|
|
1669
|
-
|
|
1670
1665
|
if dilated_x == 3 and dilated_y == 3 and dilated_z == 3:
|
|
1671
1666
|
|
|
1672
1667
|
return dilate_3D_old(tiff_array, dilated_x, dilated_y, dilated_z)
|
|
1673
1668
|
|
|
1669
|
+
if tiff_array.shape[0] == 1:
|
|
1670
|
+
return dilate_2D(tiff_array, ((dilated_x - 1) / 2))
|
|
1671
|
+
|
|
1674
1672
|
def create_circular_kernel(diameter):
|
|
1675
1673
|
"""Create a 2D circular kernel with a given radius.
|
|
1676
1674
|
|
|
@@ -1802,11 +1800,6 @@ def dilate_3D_old(tiff_array, dilated_x=3, dilated_y=3, dilated_z=3):
|
|
|
1802
1800
|
Dilated 3D array
|
|
1803
1801
|
"""
|
|
1804
1802
|
|
|
1805
|
-
# Handle special case for 2D arrays
|
|
1806
|
-
if tiff_array.shape[0] == 1:
|
|
1807
|
-
# Call 2D dilation function if needed
|
|
1808
|
-
return dilate_2D(tiff_array, 1) # For a 3x3 kernel, radius is 1
|
|
1809
|
-
|
|
1810
1803
|
# Create a simple 3x3x3 cubic kernel (all ones)
|
|
1811
1804
|
kernel = np.ones((3, 3, 3), dtype=bool)
|
|
1812
1805
|
|
|
@@ -1816,107 +1809,6 @@ def dilate_3D_old(tiff_array, dilated_x=3, dilated_y=3, dilated_z=3):
|
|
|
1816
1809
|
return dilated_array.astype(np.uint8)
|
|
1817
1810
|
|
|
1818
1811
|
|
|
1819
|
-
def erode_3D(tiff_array, eroded_x, eroded_y, eroded_z):
|
|
1820
|
-
"""Internal method to erode an array in 3D. Erosion this way is faster than using a distance transform although the latter is theoretically more accurate.
|
|
1821
|
-
Arguments are an array, and the desired pixel erosion amounts in X, Y, Z."""
|
|
1822
|
-
|
|
1823
|
-
if tiff_array.shape[0] == 1:
|
|
1824
|
-
return erode_2D(tiff_array, ((eroded_x - 1) / 2))
|
|
1825
|
-
|
|
1826
|
-
def create_circular_kernel(diameter):
|
|
1827
|
-
"""Create a 2D circular kernel with a given radius.
|
|
1828
|
-
Parameters:
|
|
1829
|
-
radius (int or float): The radius of the circle.
|
|
1830
|
-
Returns:
|
|
1831
|
-
numpy.ndarray: A 2D numpy array representing the circular kernel.
|
|
1832
|
-
"""
|
|
1833
|
-
# Determine the size of the kernel
|
|
1834
|
-
radius = diameter/2
|
|
1835
|
-
size = radius # Diameter of the circle
|
|
1836
|
-
size = int(np.ceil(size)) # Ensure size is an integer
|
|
1837
|
-
|
|
1838
|
-
# Create a grid of (x, y) coordinates
|
|
1839
|
-
y, x = np.ogrid[-radius:radius+1, -radius:radius+1]
|
|
1840
|
-
|
|
1841
|
-
# Calculate the distance from the center (0,0)
|
|
1842
|
-
distance = np.sqrt(x**2 + y**2)
|
|
1843
|
-
|
|
1844
|
-
# Create the circular kernel: points within the radius are 1, others are 0
|
|
1845
|
-
kernel = distance <= radius
|
|
1846
|
-
|
|
1847
|
-
# Convert the boolean array to integer (0 and 1)
|
|
1848
|
-
return kernel.astype(np.uint8)
|
|
1849
|
-
|
|
1850
|
-
def create_ellipsoidal_kernel(long_axis, short_axis):
|
|
1851
|
-
"""Create a 2D ellipsoidal kernel with specified axis lengths and orientation.
|
|
1852
|
-
Parameters:
|
|
1853
|
-
long_axis (int or float): The length of the long axis.
|
|
1854
|
-
short_axis (int or float): The length of the short axis.
|
|
1855
|
-
Returns:
|
|
1856
|
-
numpy.ndarray: A 2D numpy array representing the ellipsoidal kernel.
|
|
1857
|
-
"""
|
|
1858
|
-
semi_major, semi_minor = long_axis / 2, short_axis / 2
|
|
1859
|
-
# Determine the size of the kernel
|
|
1860
|
-
size_y = int(np.ceil(semi_minor))
|
|
1861
|
-
size_x = int(np.ceil(semi_major))
|
|
1862
|
-
|
|
1863
|
-
# Create a grid of (x, y) coordinates centered at (0,0)
|
|
1864
|
-
y, x = np.ogrid[-semi_minor:semi_minor+1, -semi_major:semi_major+1]
|
|
1865
|
-
|
|
1866
|
-
# Ellipsoid equation: (x/a)^2 + (y/b)^2 <= 1
|
|
1867
|
-
ellipse = (x**2 / semi_major**2) + (y**2 / semi_minor**2) <= 1
|
|
1868
|
-
|
|
1869
|
-
return ellipse.astype(np.uint8)
|
|
1870
|
-
|
|
1871
|
-
z_depth = tiff_array.shape[0]
|
|
1872
|
-
|
|
1873
|
-
# Function to process each slice
|
|
1874
|
-
def process_slice(z):
|
|
1875
|
-
tiff_slice = tiff_array[z].astype(np.uint8)
|
|
1876
|
-
eroded_slice = cv2.erode(tiff_slice, kernel, iterations=1)
|
|
1877
|
-
return z, eroded_slice
|
|
1878
|
-
|
|
1879
|
-
def process_slice_other(y):
|
|
1880
|
-
tiff_slice = tiff_array[:, y, :].astype(np.uint8)
|
|
1881
|
-
eroded_slice = cv2.erode(tiff_slice, kernel, iterations=1)
|
|
1882
|
-
return y, eroded_slice
|
|
1883
|
-
|
|
1884
|
-
# Create empty arrays to store the eroded results for the XY and XZ planes
|
|
1885
|
-
eroded_xy = np.zeros_like(tiff_array, dtype=np.uint8)
|
|
1886
|
-
eroded_xz = np.zeros_like(tiff_array, dtype=np.uint8)
|
|
1887
|
-
|
|
1888
|
-
kernel_x = int(eroded_x)
|
|
1889
|
-
kernel = create_circular_kernel(kernel_x)
|
|
1890
|
-
|
|
1891
|
-
num_cores = mp.cpu_count()
|
|
1892
|
-
with ThreadPoolExecutor(max_workers=num_cores) as executor:
|
|
1893
|
-
futures = {executor.submit(process_slice, z): z for z in range(tiff_array.shape[0])}
|
|
1894
|
-
for future in as_completed(futures):
|
|
1895
|
-
z, eroded_slice = future.result()
|
|
1896
|
-
eroded_xy[z] = eroded_slice
|
|
1897
|
-
|
|
1898
|
-
kernel_x = int(eroded_x)
|
|
1899
|
-
kernel_z = int(eroded_z)
|
|
1900
|
-
kernel = create_ellipsoidal_kernel(kernel_x, kernel_z)
|
|
1901
|
-
|
|
1902
|
-
if z_depth != 2:
|
|
1903
|
-
|
|
1904
|
-
with ThreadPoolExecutor(max_workers=num_cores) as executor:
|
|
1905
|
-
futures = {executor.submit(process_slice_other, y): y for y in range(tiff_array.shape[1])}
|
|
1906
|
-
|
|
1907
|
-
for future in as_completed(futures):
|
|
1908
|
-
y, eroded_slice = future.result()
|
|
1909
|
-
eroded_xz[:, y, :] = eroded_slice
|
|
1910
|
-
|
|
1911
|
-
# Overlay the results using AND operation instead of OR for erosion
|
|
1912
|
-
if z_depth != 2:
|
|
1913
|
-
final_result = eroded_xy & eroded_xz
|
|
1914
|
-
else:
|
|
1915
|
-
return eroded_xy
|
|
1916
|
-
|
|
1917
|
-
return final_result
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
1812
|
def dilation_length_to_pixels(xy_scaling, z_scaling, micronx, micronz):
|
|
1921
1813
|
"""Internal method to find XY and Z dilation parameters based on voxel micron scaling"""
|
|
1922
1814
|
dilate_xy = 2 * int(round(micronx/xy_scaling))
|
|
@@ -2321,12 +2213,13 @@ def dilate(arrayimage, amount, xy_scale = 1, z_scale = 1, directory = None, fast
|
|
|
2321
2213
|
def erode(arrayimage, amount, xy_scale = 1, z_scale = 1, mode = 0, preserve_labels = False):
|
|
2322
2214
|
if not preserve_labels and len(np.unique(arrayimage)) > 2: #binarize
|
|
2323
2215
|
arrayimage = binarize(arrayimage)
|
|
2324
|
-
erode_xy, erode_z = dilation_length_to_pixels(xy_scale, z_scale, amount, amount)
|
|
2325
2216
|
|
|
2326
|
-
if mode == 2:
|
|
2327
|
-
|
|
2217
|
+
if mode == 0 or mode == 2:
|
|
2218
|
+
fast_dil = True
|
|
2328
2219
|
else:
|
|
2329
|
-
|
|
2220
|
+
fast_dil = False
|
|
2221
|
+
|
|
2222
|
+
arrayimage = erode_3D_dt(arrayimage, amount, xy_scaling=xy_scale, z_scaling=z_scale, fast_dil = fast_dil, preserve_labels = preserve_labels)
|
|
2330
2223
|
|
|
2331
2224
|
if np.max(arrayimage) == 1:
|
|
2332
2225
|
arrayimage = arrayimage * 255
|
|
@@ -2378,7 +2271,7 @@ def skeletonize(arrayimage, directory = None):
|
|
|
2378
2271
|
|
|
2379
2272
|
return arrayimage
|
|
2380
2273
|
|
|
2381
|
-
def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol = 0, down_factor = None, directory = None, nodes = None, bonus_array = None, GPU = True, arrayshape = None, compute = False, unify = False, union_val = 10, xy_scale = 1, z_scale = 1):
|
|
2274
|
+
def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol = 0, down_factor = None, directory = None, nodes = None, bonus_array = None, GPU = True, arrayshape = None, compute = False, unify = False, union_val = 10, mode = 0, xy_scale = 1, z_scale = 1):
|
|
2382
2275
|
"""
|
|
2383
2276
|
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.
|
|
2384
2277
|
: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.
|
|
@@ -2419,24 +2312,25 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
|
|
|
2419
2312
|
from . import branch_stitcher
|
|
2420
2313
|
verts = dilate_3D_old(verts, 3, 3, 3,)
|
|
2421
2314
|
verts, _ = label_objects(verts)
|
|
2422
|
-
|
|
2315
|
+
print("Merging branches...")
|
|
2316
|
+
array = branch_stitcher.trace(bonus_array, array, verts, score_thresh = union_val, xy_scale = xy_scale, z_scale = z_scale)
|
|
2423
2317
|
verts = None
|
|
2424
2318
|
|
|
2425
2319
|
|
|
2426
2320
|
if nodes is None:
|
|
2427
2321
|
|
|
2428
|
-
array = smart_dilate.smart_label(array, other_array, GPU = GPU, remove_template = True)
|
|
2322
|
+
array = smart_dilate.smart_label(array, other_array, GPU = GPU, remove_template = True, mode = mode)
|
|
2429
2323
|
#distance = smart_dilate.compute_distance_transform_distance(array)
|
|
2430
2324
|
#array = water(-distance, other_array, mask=array) #Tried out skimage watershed as shown and found it did not label branches as well as smart_label (esp combined combined with post-processing label splitting if needed)
|
|
2431
2325
|
|
|
2432
2326
|
else:
|
|
2433
2327
|
if down_factor is not None:
|
|
2434
|
-
array = smart_dilate.smart_label(bonus_array, array, GPU = GPU, predownsample = down_factor, remove_template = True)
|
|
2328
|
+
array = smart_dilate.smart_label(bonus_array, array, GPU = GPU, predownsample = down_factor, remove_template = True, mode = mode)
|
|
2435
2329
|
#distance = smart_dilate.compute_distance_transform_distance(bonus_array)
|
|
2436
2330
|
#array = water(-distance, array, mask=bonus_array)
|
|
2437
2331
|
else:
|
|
2438
2332
|
|
|
2439
|
-
array = smart_dilate.smart_label(bonus_array, array, GPU = GPU, remove_template = True)
|
|
2333
|
+
array = smart_dilate.smart_label(bonus_array, array, GPU = GPU, remove_template = True, mode = mode)
|
|
2440
2334
|
#distance = smart_dilate.compute_distance_transform_distance(bonus_array)
|
|
2441
2335
|
#array = water(-distance, array, mask=bonus_array)
|
|
2442
2336
|
|
|
@@ -2628,19 +2522,18 @@ def label_vertices(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
|
|
|
2628
2522
|
if peaks > 0:
|
|
2629
2523
|
image_copy = filter_size_by_peaks(image_copy, peaks)
|
|
2630
2524
|
if comp_dil > 0:
|
|
2631
|
-
image_copy =
|
|
2525
|
+
image_copy = dilate_3D_dt(image_copy, comp_dil, fast_dil = fastdil)
|
|
2632
2526
|
|
|
2633
2527
|
labeled_image, num_labels = label_objects(image_copy)
|
|
2634
2528
|
elif max_vol > 0:
|
|
2635
2529
|
image_copy = filter_size_by_vol(image_copy, max_vol)
|
|
2636
2530
|
if comp_dil > 0:
|
|
2637
|
-
image_copy =
|
|
2531
|
+
image_copy = dilate_3D_dt(image_copy, comp_dil, fast_dil = fastdil)
|
|
2638
2532
|
|
|
2639
2533
|
labeled_image, num_labels = label_objects(image_copy)
|
|
2640
2534
|
else:
|
|
2641
|
-
|
|
2642
2535
|
if comp_dil > 0:
|
|
2643
|
-
image_copy =
|
|
2536
|
+
image_copy = dilate_3D_dt(image_copy, comp_dil, fast_dil = fastdil)
|
|
2644
2537
|
labeled_image, num_labels = label_objects(image_copy)
|
|
2645
2538
|
|
|
2646
2539
|
#if down_factor > 0:
|
|
@@ -2775,7 +2668,7 @@ def gray_watershed(image, min_distance = 1, threshold_abs = None):
|
|
|
2775
2668
|
return image
|
|
2776
2669
|
|
|
2777
2670
|
|
|
2778
|
-
def watershed(image, directory = None, proportion = 0.1, GPU = True, smallest_rad = None, predownsample = None, predownsample2 = None):
|
|
2671
|
+
def watershed(image, directory = None, proportion = 0.1, GPU = True, smallest_rad = None, fast_dil = False, predownsample = None, predownsample2 = None):
|
|
2779
2672
|
"""
|
|
2780
2673
|
Can be used to 3D watershed a binary image. Watershedding attempts to use an algorithm to split touching objects into seperate labelled components. Labelled output will be saved to the active directory if none is specified.
|
|
2781
2674
|
This watershed algo essentially uses the distance transform to decide where peaks are and then after thresholding out the non-peaks, uses the peaks as labelling kernels for a smart label. It runs semi slow without GPU accel since it requires two dts to be computed.
|
|
@@ -2838,7 +2731,7 @@ def watershed(image, directory = None, proportion = 0.1, GPU = True, smallest_ra
|
|
|
2838
2731
|
if GPU:
|
|
2839
2732
|
print("GPU dt failed or did not detect GPU (cupy must be installed with a CUDA toolkit setup...). Computing CPU distance transform instead.")
|
|
2840
2733
|
print(f"Error message: {str(e)}")
|
|
2841
|
-
distance = smart_dilate.compute_distance_transform_distance(image)
|
|
2734
|
+
distance = smart_dilate.compute_distance_transform_distance(image, fast_dil = fast_dil)
|
|
2842
2735
|
|
|
2843
2736
|
|
|
2844
2737
|
distance = threshold(distance, proportion, custom_rad = smallest_rad)
|
|
@@ -4189,10 +4082,7 @@ class Network_3D:
|
|
|
4189
4082
|
if search is not None and hasattr(self, '_nodes') and self._nodes is not None and self._search_region is None:
|
|
4190
4083
|
search_region = binarize(self._nodes)
|
|
4191
4084
|
dilate_xy, dilate_z = dilation_length_to_pixels(self._xy_scale, self._z_scale, search, search)
|
|
4192
|
-
|
|
4193
|
-
search_region = dilate_3D(search_region, dilate_xy, dilate_xy, dilate_z)
|
|
4194
|
-
else:
|
|
4195
|
-
search_region = dilate_3D_dt(search_region, diledge, self._xy_scale, self._z_scale)
|
|
4085
|
+
search_region = dilate_3D_dt(search_region, diledge, self._xy_scale, self._z_scale, fast_dil = fast_dil)
|
|
4196
4086
|
else:
|
|
4197
4087
|
search_region = binarize(self._search_region)
|
|
4198
4088
|
|
|
@@ -4210,10 +4100,8 @@ class Network_3D:
|
|
|
4210
4100
|
|
|
4211
4101
|
if dilate_xy <= 3 and dilate_z <= 3:
|
|
4212
4102
|
outer_edges = dilate_3D_old(outer_edges, dilate_xy, dilate_xy, dilate_z)
|
|
4213
|
-
elif fast_dil:
|
|
4214
|
-
outer_edges = dilate_3D(outer_edges, dilate_xy, dilate_xy, dilate_z)
|
|
4215
4103
|
else:
|
|
4216
|
-
outer_edges = dilate_3D_dt(outer_edges, diledge, self._xy_scale, self._z_scale)
|
|
4104
|
+
outer_edges = dilate_3D_dt(outer_edges, diledge, self._xy_scale, self._z_scale, fast_dil = fast_dil)
|
|
4217
4105
|
else:
|
|
4218
4106
|
outer_edges = dilate_3D_old(outer_edges)
|
|
4219
4107
|
|
|
@@ -4539,9 +4427,6 @@ class Network_3D:
|
|
|
4539
4427
|
self._nodes = nodes
|
|
4540
4428
|
del nodes
|
|
4541
4429
|
|
|
4542
|
-
if self._nodes.shape[0] == 1:
|
|
4543
|
-
fast_dil = True #Set this to true because the 2D algo always uses the distance transform and doesnt need this special ver
|
|
4544
|
-
|
|
4545
4430
|
if label_nodes:
|
|
4546
4431
|
self._nodes, num_nodes = label_objects(self._nodes)
|
|
4547
4432
|
if other_nodes is not None:
|
|
@@ -4937,7 +4822,17 @@ class Network_3D:
|
|
|
4937
4822
|
nodeb = []
|
|
4938
4823
|
edgec = []
|
|
4939
4824
|
|
|
4940
|
-
|
|
4825
|
+
from collections import Counter
|
|
4826
|
+
counts = Counter(edgesc)
|
|
4827
|
+
if 0 not in edgesc:
|
|
4828
|
+
trunk = stats.mode(edgesc)
|
|
4829
|
+
else:
|
|
4830
|
+
try:
|
|
4831
|
+
sorted_edges = counts.most_common()
|
|
4832
|
+
trunk = sorted_edges[1][0]
|
|
4833
|
+
except:
|
|
4834
|
+
return
|
|
4835
|
+
|
|
4941
4836
|
addtrunk = max(set(nodesa + nodesb)) + 1
|
|
4942
4837
|
|
|
4943
4838
|
for i in range(len(nodesa)):
|
|
@@ -4955,7 +4850,10 @@ class Network_3D:
|
|
|
4955
4850
|
|
|
4956
4851
|
self.network_lists = [nodea, nodeb, edgec]
|
|
4957
4852
|
|
|
4958
|
-
|
|
4853
|
+
try:
|
|
4854
|
+
self._node_centroids[addtrunk] = self._edge_centroids[trunk]
|
|
4855
|
+
except:
|
|
4856
|
+
pass
|
|
4959
4857
|
|
|
4960
4858
|
if self._node_identities is None:
|
|
4961
4859
|
self._node_identities = {}
|
|
@@ -5193,7 +5091,7 @@ class Network_3D:
|
|
|
5193
5091
|
|
|
5194
5092
|
#Methods related to visualizing the network using networkX and matplotlib
|
|
5195
5093
|
|
|
5196
|
-
def show_network(self, geometric = False, directory = None):
|
|
5094
|
+
def show_network(self, geometric = False, directory = None, show_labels = True):
|
|
5197
5095
|
"""
|
|
5198
5096
|
Shows the network property as a simplistic graph, and some basic stats. Does not support viewing edge weights.
|
|
5199
5097
|
:param geoemtric: (Optional - Val = False; boolean). If False, node placement in the graph will be random. If True, nodes
|
|
@@ -5204,19 +5102,19 @@ class Network_3D:
|
|
|
5204
5102
|
|
|
5205
5103
|
if not geometric:
|
|
5206
5104
|
|
|
5207
|
-
simple_network.show_simple_network(self._network_lists, directory = directory)
|
|
5105
|
+
simple_network.show_simple_network(self._network_lists, directory = directory, show_labels = show_labels)
|
|
5208
5106
|
|
|
5209
5107
|
else:
|
|
5210
|
-
simple_network.show_simple_network(self._network_lists, geometric = True, geo_info = [self._node_centroids, self._nodes.shape], directory = directory)
|
|
5108
|
+
simple_network.show_simple_network(self._network_lists, geometric = True, geo_info = [self._node_centroids, self._nodes.shape], directory = directory, show_labels = show_labels)
|
|
5211
5109
|
|
|
5212
|
-
def show_communities_flex(self, geometric = False, directory = None, weighted = True, partition = False, style = 0):
|
|
5110
|
+
def show_communities_flex(self, geometric = False, directory = None, weighted = True, partition = False, style = 0, show_labels = True):
|
|
5213
5111
|
|
|
5214
5112
|
|
|
5215
|
-
self._communities, self.normalized_weights = modularity.show_communities_flex(self._network, self._network_lists, self.normalized_weights, geo_info = [self._node_centroids, self._nodes.shape], geometric = geometric, directory = directory, weighted = weighted, partition = partition, style = style)
|
|
5113
|
+
self._communities, self.normalized_weights = modularity.show_communities_flex(self._network, self._network_lists, self.normalized_weights, geo_info = [self._node_centroids, self._nodes.shape], geometric = geometric, directory = directory, weighted = weighted, partition = partition, style = style, show_labels = show_labels)
|
|
5216
5114
|
|
|
5217
5115
|
|
|
5218
5116
|
|
|
5219
|
-
def show_identity_network(self, geometric = False, directory = None):
|
|
5117
|
+
def show_identity_network(self, geometric = False, directory = None, show_labels = True):
|
|
5220
5118
|
"""
|
|
5221
5119
|
Shows the network property, and some basic stats, as a graph where nodes are labelled by colors representing the identity of the node as detailed in the node_identities property. Does not support viewing edge weights.
|
|
5222
5120
|
:param geoemtric: (Optional - Val = False; boolean). If False, node placement in the graph will be random. If True, nodes
|
|
@@ -5225,9 +5123,9 @@ class Network_3D:
|
|
|
5225
5123
|
:param directory: (Optional – Val = None; string). An optional string path to a directory to save the network plot image to. If not set, nothing will be saved.
|
|
5226
5124
|
"""
|
|
5227
5125
|
if not geometric:
|
|
5228
|
-
simple_network.show_identity_network(self._network_lists, self._node_identities, geometric = False, directory = directory)
|
|
5126
|
+
simple_network.show_identity_network(self._network_lists, self._node_identities, geometric = False, directory = directory, show_labels = show_labels)
|
|
5229
5127
|
else:
|
|
5230
|
-
simple_network.show_identity_network(self._network_lists, self._node_identities, geometric = True, geo_info = [self._node_centroids, self._nodes.shape], directory = directory)
|
|
5128
|
+
simple_network.show_identity_network(self._network_lists, self._node_identities, geometric = True, geo_info = [self._node_centroids, self._nodes.shape], directory = directory, show_labels = show_labels)
|
|
5231
5129
|
|
|
5232
5130
|
|
|
5233
5131
|
|