nettracer3d 0.3.4__py3-none-any.whl → 0.3.6__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 +95 -178
- nettracer3d/nettracer_gui.py +236 -169
- {nettracer3d-0.3.4.dist-info → nettracer3d-0.3.6.dist-info}/METADATA +2 -2
- {nettracer3d-0.3.4.dist-info → nettracer3d-0.3.6.dist-info}/RECORD +7 -7
- {nettracer3d-0.3.4.dist-info → nettracer3d-0.3.6.dist-info}/LICENSE +0 -0
- {nettracer3d-0.3.4.dist-info → nettracer3d-0.3.6.dist-info}/WHEEL +0 -0
- {nettracer3d-0.3.4.dist-info → nettracer3d-0.3.6.dist-info}/top_level.txt +0 -0
nettracer3d/nettracer.py
CHANGED
|
@@ -10,7 +10,10 @@ import multiprocessing as mp
|
|
|
10
10
|
import os
|
|
11
11
|
import copy
|
|
12
12
|
import statistics as stats
|
|
13
|
-
|
|
13
|
+
try:
|
|
14
|
+
import napari
|
|
15
|
+
except:
|
|
16
|
+
pass
|
|
14
17
|
import networkx as nx
|
|
15
18
|
from scipy.signal import find_peaks
|
|
16
19
|
try:
|
|
@@ -396,7 +399,7 @@ def upsample_with_padding(data, factor=None, original_shape=None):
|
|
|
396
399
|
raise ValueError("original_shape must be provided")
|
|
397
400
|
|
|
398
401
|
# Handle 4D color arrays
|
|
399
|
-
is_color = len(data.shape) == 4 and data.shape[-1] == 3
|
|
402
|
+
is_color = len(data.shape) == 4 and (data.shape[-1] == 3 or data.shape[-1] == 4)
|
|
400
403
|
if is_color:
|
|
401
404
|
# Split into separate color channels
|
|
402
405
|
channels = [data[..., i] for i in range(3)]
|
|
@@ -623,6 +626,65 @@ def threshold(arr, proportion, custom_rad = None):
|
|
|
623
626
|
|
|
624
627
|
return arr
|
|
625
628
|
|
|
629
|
+
def show_3d(arrays_3d=None, arrays_4d=None, down_factor=None, order=0, xy_scale=1, z_scale=1, colors=['red', 'green', 'white', 'cyan', 'yellow']):
|
|
630
|
+
"""
|
|
631
|
+
Show 3d (or 2d) displays of array data using napari.
|
|
632
|
+
Params: arrays - A list of 3d or 2d numpy arrays to display
|
|
633
|
+
down_factor (int) - Optional downsampling factor to speed up display
|
|
634
|
+
"""
|
|
635
|
+
import os
|
|
636
|
+
# Force PyQt6 usage to avoid binding warning
|
|
637
|
+
os.environ['QT_API'] = 'pyqt6'
|
|
638
|
+
|
|
639
|
+
import napari
|
|
640
|
+
from qtpy.QtWidgets import QApplication
|
|
641
|
+
|
|
642
|
+
if down_factor is not None:
|
|
643
|
+
# Downsample arrays if specified
|
|
644
|
+
arrays_3d = [downsample(array, down_factor, order=order) for array in arrays_3d] if arrays_3d is not None else None
|
|
645
|
+
arrays_4d = [downsample(array, down_factor, order=order) for array in arrays_4d] if arrays_4d is not None else None
|
|
646
|
+
|
|
647
|
+
viewer = napari.Viewer(ndisplay=3)
|
|
648
|
+
scale = [z_scale, xy_scale, xy_scale] # [z, y, x] order for napari
|
|
649
|
+
|
|
650
|
+
# Add 3D arrays if provided
|
|
651
|
+
if arrays_3d is not None:
|
|
652
|
+
for arr, color in zip(arrays_3d, colors):
|
|
653
|
+
viewer.add_image(
|
|
654
|
+
arr,
|
|
655
|
+
scale=scale,
|
|
656
|
+
colormap=color,
|
|
657
|
+
rendering='mip',
|
|
658
|
+
blending='additive',
|
|
659
|
+
opacity=0.5,
|
|
660
|
+
name=f'Channel_{color}'
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
if arrays_4d is not None:
|
|
664
|
+
for i, arr in enumerate(arrays_4d):
|
|
665
|
+
# Check if the last dimension is 3 (RGB) or 4 (RGBA)
|
|
666
|
+
if arr.shape[-1] not in [3, 4]:
|
|
667
|
+
print(f"Warning: Array {i} doesn't appear to be RGB/RGBA. Skipping.")
|
|
668
|
+
continue
|
|
669
|
+
|
|
670
|
+
if arr.shape[3] == 4:
|
|
671
|
+
arr = arr[:, :, :, :3] # Remove alpha
|
|
672
|
+
|
|
673
|
+
# Add each color channel separately
|
|
674
|
+
colors = ['red', 'green', 'blue']
|
|
675
|
+
for c in range(3):
|
|
676
|
+
viewer.add_image(
|
|
677
|
+
arr[:,:,:,c], # Take just one color channel
|
|
678
|
+
scale=scale,
|
|
679
|
+
colormap=colors[c], # Use corresponding color
|
|
680
|
+
rendering='mip',
|
|
681
|
+
blending='additive',
|
|
682
|
+
opacity=0.5,
|
|
683
|
+
name=f'Channel_{colors[c]}_{i}'
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
napari.run()
|
|
687
|
+
|
|
626
688
|
def z_project(array3d, method='max'):
|
|
627
689
|
"""
|
|
628
690
|
Project a 3D numpy array along the Z axis to create a 2D array.
|
|
@@ -733,6 +795,18 @@ def fill_holes_3d(array):
|
|
|
733
795
|
def resize(array, factor, order = 0):
|
|
734
796
|
"""Simply resizes an array by a factor"""
|
|
735
797
|
|
|
798
|
+
if len(array.shape) == 4: # presumably this is a color image
|
|
799
|
+
processed_arrays = []
|
|
800
|
+
for i in range(array.shape[3]): # iterate through the color dimension
|
|
801
|
+
color_array = array[:, :, :, i] # get 3D array for each color channel
|
|
802
|
+
processed_color = zoom(color_array, (factor), order = order)
|
|
803
|
+
|
|
804
|
+
processed_arrays.append(processed_color)
|
|
805
|
+
|
|
806
|
+
# Stack them back together along the 4th dimension
|
|
807
|
+
result = np.stack(processed_arrays, axis=3)
|
|
808
|
+
return result
|
|
809
|
+
|
|
736
810
|
array = zoom(array, (factor), order = order)
|
|
737
811
|
|
|
738
812
|
return array
|
|
@@ -748,103 +822,6 @@ def _rescale(array, original_shape, xy_scale, z_scale):
|
|
|
748
822
|
array = zoom(array, (1, z_scale/xy_scale, z_scale/xy_scale))
|
|
749
823
|
return array
|
|
750
824
|
|
|
751
|
-
def visualize_3D(array, other_arrays=None, xy_scale = 1, z_scale = 1):
|
|
752
|
-
"""
|
|
753
|
-
Mostly internal method for 3D visualization, although can be run directly on tif files to view them. Uses plotly to visualize
|
|
754
|
-
a 3D, binarized isosurface of data. Note this method likely requires downsampling on objects before running.
|
|
755
|
-
:param array: (Mandatory; string or ndarray) - Either a path to a .tif file to visualize in 3D binary, or a ndarray of the same.
|
|
756
|
-
:param other_arrays: (Optional - Val = None; string, ndarray, or list) - Either a path to a an additional .tif file to visualize in 3D binary or an ndarray containing the same,
|
|
757
|
-
or otherwise a path to a directory containing ONLY other .tif files to visualize, or a list of ndarrays containing the same.
|
|
758
|
-
:param xy_scale: (Optional - Val = 1; float) - The xy pixel scaling of an image to visualize.
|
|
759
|
-
:param z_scale: (Optional - Val = 1; float) - The z voxel depth of an image to visualize.
|
|
760
|
-
"""
|
|
761
|
-
|
|
762
|
-
if isinstance(array, str):
|
|
763
|
-
array = tifffile.imread(array)
|
|
764
|
-
|
|
765
|
-
original_shape = array.shape[1]
|
|
766
|
-
|
|
767
|
-
array = _rescale(array, original_shape, xy_scale, z_scale)
|
|
768
|
-
array = binarize(array)
|
|
769
|
-
|
|
770
|
-
# Create a meshgrid for coordinates
|
|
771
|
-
x, y, z = np.indices(array.shape)
|
|
772
|
-
|
|
773
|
-
# Create a figure
|
|
774
|
-
fig = go.Figure()
|
|
775
|
-
|
|
776
|
-
# Plot the main array
|
|
777
|
-
_plot_3D(fig, x, y, z, array, 'red')
|
|
778
|
-
|
|
779
|
-
if other_arrays is not None and ((type(other_arrays) == str) or (type(other_arrays) == list)):
|
|
780
|
-
try: #Presume single tif
|
|
781
|
-
array = tifffile.imread(other_arrays)
|
|
782
|
-
if array.shape[1] != original_shape:
|
|
783
|
-
array = downsample(array, array.shape[1]/original_shape)
|
|
784
|
-
array = _rescale(array, original_shape, xy_scale, z_scale)
|
|
785
|
-
array = binarize(array)
|
|
786
|
-
_plot_3D(fig, x, y, z, array, 'green')
|
|
787
|
-
except: #presume directory or list
|
|
788
|
-
basic_colors = ['blue', 'yellow', 'cyan', 'magenta', 'black', 'white', 'gray', 'orange', 'brown', 'pink', 'purple', 'lime', 'teal', 'navy', 'maroon', 'olive', 'silver', 'red', 'green']
|
|
789
|
-
try: #presume directory
|
|
790
|
-
arrays = directory_info(other_arrays)
|
|
791
|
-
directory = other_arrays
|
|
792
|
-
except: #presume list
|
|
793
|
-
arrays = other_arrays
|
|
794
|
-
for i, array_path in enumerate(arrays):
|
|
795
|
-
try: #presume tif
|
|
796
|
-
array = tifffile.imread(f"{directory}/{array_path}")
|
|
797
|
-
if array.shape[1] != original_shape:
|
|
798
|
-
array = downsample(array, array.shape[1]/original_shape)
|
|
799
|
-
array = _rescale(array, original_shape, xy_scale, z_scale)
|
|
800
|
-
array = binarize(array)
|
|
801
|
-
except: #presume array
|
|
802
|
-
array = array_path
|
|
803
|
-
del array_path
|
|
804
|
-
if array is not None:
|
|
805
|
-
if array.shape[1] != original_shape:
|
|
806
|
-
array = downsample(array, array.shape[1]/original_shape)
|
|
807
|
-
array = _rescale(array, original_shape, xy_scale, z_scale)
|
|
808
|
-
array = binarize(array)
|
|
809
|
-
color = basic_colors[i % len(basic_colors)] # Ensure color index wraps around if more arrays than colors
|
|
810
|
-
if array is not None:
|
|
811
|
-
_plot_3D(fig, x, y, z, array, color)
|
|
812
|
-
else:
|
|
813
|
-
try:
|
|
814
|
-
other_arrays = _rescale(other_arrays, original_shape, xy_scale, z_scale)
|
|
815
|
-
other_arrays = binarize(other_arrays)
|
|
816
|
-
_plot_3D(fig, x, y, z, other_arrays, 'green')
|
|
817
|
-
except:
|
|
818
|
-
pass
|
|
819
|
-
|
|
820
|
-
# Set the layout for better visualization
|
|
821
|
-
fig.update_layout(scene=dict(
|
|
822
|
-
xaxis_title='Z Axis',
|
|
823
|
-
yaxis_title='Y Axis',
|
|
824
|
-
zaxis_title='X Axis'
|
|
825
|
-
))
|
|
826
|
-
|
|
827
|
-
fig.show()
|
|
828
|
-
|
|
829
|
-
def _plot_3D(fig, x, y, z, array, color):
|
|
830
|
-
"""Internal method used for 3D visualization"""
|
|
831
|
-
# Define the isosurface level
|
|
832
|
-
level = 0.5
|
|
833
|
-
|
|
834
|
-
# Add the isosurface to the figure
|
|
835
|
-
fig.add_trace(go.Isosurface(
|
|
836
|
-
x=x.flatten(),
|
|
837
|
-
y=y.flatten(),
|
|
838
|
-
z=z.flatten(),
|
|
839
|
-
value=array.flatten(),
|
|
840
|
-
isomin=level,
|
|
841
|
-
isomax=level,
|
|
842
|
-
opacity=0.6, # Adjust opacity
|
|
843
|
-
surface_count=1, # Show only the isosurface
|
|
844
|
-
colorscale=[[0, color], [1, color]], # Set uniform color
|
|
845
|
-
showscale=False # Hide color scale bar
|
|
846
|
-
))
|
|
847
|
-
|
|
848
825
|
|
|
849
826
|
def remove_trunk(edges):
|
|
850
827
|
"""
|
|
@@ -1380,6 +1357,17 @@ def downsample(data, factor, directory=None, order=0):
|
|
|
1380
1357
|
data = tifffile.imread(data)
|
|
1381
1358
|
else:
|
|
1382
1359
|
data2 = None
|
|
1360
|
+
|
|
1361
|
+
if len(data.shape) == 4: # presumably this is a color image
|
|
1362
|
+
processed_arrays = []
|
|
1363
|
+
for i in range(data.shape[3]): # iterate through the color dimension
|
|
1364
|
+
color_array = data[:, :, :, i] # get 3D array for each color channel
|
|
1365
|
+
processed_color = downsample(color_array, factor, directory = None, order = order) #right now this is only for internal use - color array downsampling that is
|
|
1366
|
+
processed_arrays.append(processed_color)
|
|
1367
|
+
|
|
1368
|
+
# Stack them back together along the 4th dimension
|
|
1369
|
+
result = np.stack(processed_arrays, axis=3)
|
|
1370
|
+
return result
|
|
1383
1371
|
|
|
1384
1372
|
# Check if Z dimension is too small relative to downsample factor
|
|
1385
1373
|
if data.ndim == 3 and data.shape[0] < factor * 4:
|
|
@@ -1389,6 +1377,7 @@ def downsample(data, factor, directory=None, order=0):
|
|
|
1389
1377
|
else:
|
|
1390
1378
|
zoom_factors = 1/factor
|
|
1391
1379
|
|
|
1380
|
+
|
|
1392
1381
|
# Apply downsampling
|
|
1393
1382
|
data = zoom(data, zoom_factors, order=order)
|
|
1394
1383
|
|
|
@@ -1524,7 +1513,7 @@ def skeletonize(arrayimage, directory = None):
|
|
|
1524
1513
|
|
|
1525
1514
|
return arrayimage
|
|
1526
1515
|
|
|
1527
|
-
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):
|
|
1516
|
+
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):
|
|
1528
1517
|
"""
|
|
1529
1518
|
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.
|
|
1530
1519
|
: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.
|
|
@@ -1546,7 +1535,7 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
|
|
|
1546
1535
|
array = downsample(array, down_factor)
|
|
1547
1536
|
arrayshape = array.shape
|
|
1548
1537
|
else:
|
|
1549
|
-
arrayshape =
|
|
1538
|
+
arrayshape = arrayshape
|
|
1550
1539
|
|
|
1551
1540
|
|
|
1552
1541
|
if nodes is None:
|
|
@@ -1564,6 +1553,7 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
|
|
|
1564
1553
|
array = upsample_with_padding(array, down_factor, arrayshape)
|
|
1565
1554
|
|
|
1566
1555
|
|
|
1556
|
+
|
|
1567
1557
|
if nodes is None:
|
|
1568
1558
|
|
|
1569
1559
|
array = smart_dilate.smart_label(array, other_array, GPU = GPU)
|
|
@@ -1594,7 +1584,7 @@ def label_branches(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
|
|
|
1594
1584
|
|
|
1595
1585
|
return array
|
|
1596
1586
|
|
|
1597
|
-
def label_vertices(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol = 0, down_factor = 0, directory = None, return_skele = False):
|
|
1587
|
+
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):
|
|
1598
1588
|
"""
|
|
1599
1589
|
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.
|
|
1600
1590
|
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.
|
|
@@ -1616,7 +1606,9 @@ def label_vertices(array, peaks = 0, branch_removal = 0, comp_dil = 0, max_vol =
|
|
|
1616
1606
|
|
|
1617
1607
|
if down_factor > 0:
|
|
1618
1608
|
array_shape = array.shape
|
|
1619
|
-
array = downsample(array, down_factor)
|
|
1609
|
+
array = downsample(array, down_factor, order)
|
|
1610
|
+
if order == 3:
|
|
1611
|
+
array = binarize(array)
|
|
1620
1612
|
|
|
1621
1613
|
array = array > 0
|
|
1622
1614
|
|
|
@@ -3738,81 +3730,6 @@ class Network_3D:
|
|
|
3738
3730
|
|
|
3739
3731
|
|
|
3740
3732
|
|
|
3741
|
-
#Methods relating to visualizing elements of the network in 3D
|
|
3742
|
-
|
|
3743
|
-
def show_3D(self, other_arrays = None, down_factor = 1):
|
|
3744
|
-
"""
|
|
3745
|
-
Allows the Network_3D object to be visualized in 3D using plotly. By default, this will show the nodes and edges properties. All arrays involved will be made binary.
|
|
3746
|
-
Note that nettracer_3D is not primarily a 3D visualization tool, so the funcionality of this method is limited, and additionally it should really only be run on downsampled data.
|
|
3747
|
-
:param other_arrays: (Optional - Val = None; string). A filepath to additional .tif files (or a directory containing only .tif files) to show alongside the Network_3D object, for example a node_indicies or network_lattice overlay.
|
|
3748
|
-
:param down_factor: (Optional - Val = 1; int). A downsampling factor to speed up showing the 3D display and improve processing. Note that ALL arrays being shown will be subject
|
|
3749
|
-
to this downsample factor. If you have files to be shown alongside the Network_3D object that were ALREADY downsampled, instead downsample the Network_3D object FIRST and pass nothing to this value.
|
|
3750
|
-
If arrays are sized to different shapes while show_3D() is being called, there may be unusual results.
|
|
3751
|
-
"""
|
|
3752
|
-
if down_factor > 1:
|
|
3753
|
-
xy_scale = down_factor * self._xy_scale
|
|
3754
|
-
z_scale = down_factor * self._z_scale
|
|
3755
|
-
try:
|
|
3756
|
-
nodes = downsample(self._nodes, down_factor, order = 3)
|
|
3757
|
-
nodes = binarize(nodes)
|
|
3758
|
-
except:
|
|
3759
|
-
pass
|
|
3760
|
-
try:
|
|
3761
|
-
edges = downsample(self._edges, down_factor, order = 3)
|
|
3762
|
-
edges = binarize(edges)
|
|
3763
|
-
except:
|
|
3764
|
-
edges = None
|
|
3765
|
-
try:
|
|
3766
|
-
if not isinstance(other_arrays, np.ndarray):
|
|
3767
|
-
other_arrays = tifffile.imread(other_arrays)
|
|
3768
|
-
if other_arrays.shape == self._nodes.shape:
|
|
3769
|
-
other_arrays = downsample(other_arrays, down_factor, order = 3)
|
|
3770
|
-
other_arrays = binarize(other_arrays)
|
|
3771
|
-
other_arrays = [edges, other_arrays]
|
|
3772
|
-
except:
|
|
3773
|
-
try:
|
|
3774
|
-
arrays = directory_info(other_arrays)
|
|
3775
|
-
directory = other_arrays
|
|
3776
|
-
other_arrays = []
|
|
3777
|
-
for array in arrays:
|
|
3778
|
-
array = tifffile.imread(f'{directory}/{array}')
|
|
3779
|
-
if array.shape == self._nodes.shape:
|
|
3780
|
-
array = downsample(array, down_factor, order = 3)
|
|
3781
|
-
array = binarize(array)
|
|
3782
|
-
other_arrays.append(array)
|
|
3783
|
-
other_arrays.insert(0, edges)
|
|
3784
|
-
except:
|
|
3785
|
-
other_arrays = edges
|
|
3786
|
-
visualize_3D(nodes, other_arrays, xy_scale = xy_scale, z_scale = z_scale)
|
|
3787
|
-
else:
|
|
3788
|
-
try:
|
|
3789
|
-
nodes = binarize(self._nodes)
|
|
3790
|
-
except:
|
|
3791
|
-
pass
|
|
3792
|
-
try:
|
|
3793
|
-
edges = binarize(self._edges)
|
|
3794
|
-
except:
|
|
3795
|
-
edges = None
|
|
3796
|
-
try:
|
|
3797
|
-
if not isinstance(other_arrays, np.ndarray):
|
|
3798
|
-
other_arrays = tifffile.imread(other_arrays)
|
|
3799
|
-
other_arrays = binarize(other_arrays)
|
|
3800
|
-
other_arrays = [edges, other_arrays]
|
|
3801
|
-
except:
|
|
3802
|
-
try:
|
|
3803
|
-
arrays = directory_info(other_arrays)
|
|
3804
|
-
directory = other_arrays
|
|
3805
|
-
other_arrays = []
|
|
3806
|
-
for array in arrays:
|
|
3807
|
-
array = tifffile.imread(f'{directory}/{array}')
|
|
3808
|
-
array = binarize(array)
|
|
3809
|
-
other_arrays.append(array)
|
|
3810
|
-
other_arrays.insert(0, self._edges)
|
|
3811
|
-
except:
|
|
3812
|
-
other_arrays = edges
|
|
3813
|
-
|
|
3814
|
-
visualize_3D(nodes, other_arrays, xy_scale = self._xy_scale, z_scale = self._z_scale)
|
|
3815
|
-
|
|
3816
3733
|
def get_degrees(self, down_factor = 1, directory = None, called = False, no_img = 0):
|
|
3817
3734
|
"""
|
|
3818
3735
|
Method to obtain information on the degrees of nodes in the network, also generating overlays that relate this information to the 3D structure.
|
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -1880,7 +1880,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1880
1880
|
searchoverlay_action.triggered.connect(self.show_search_dialog)
|
|
1881
1881
|
shuffle_action = overlay_menu.addAction("Shuffle")
|
|
1882
1882
|
shuffle_action.triggered.connect(self.show_shuffle_dialog)
|
|
1883
|
-
show3d_action = image_menu.addAction("Show 3D (
|
|
1883
|
+
show3d_action = image_menu.addAction("Show 3D (Napari)")
|
|
1884
1884
|
show3d_action.triggered.connect(self.show3d_dialog)
|
|
1885
1885
|
|
|
1886
1886
|
|
|
@@ -2339,7 +2339,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2339
2339
|
f"Select Directory for Network3D Object",
|
|
2340
2340
|
"",
|
|
2341
2341
|
QFileDialog.Option.ShowDirsOnly
|
|
2342
|
-
|
|
2342
|
+
)
|
|
2343
|
+
self.reset(nodes = True, network = True, xy_scale = 1, z_scale = 1, edges = True, search_region = True, network_overlay = True, id_overlay = True)
|
|
2344
|
+
|
|
2343
2345
|
|
|
2344
2346
|
my_network.assemble(directory)
|
|
2345
2347
|
|
|
@@ -2514,17 +2516,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2514
2516
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
2515
2517
|
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
2516
2518
|
|
|
2517
|
-
def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True
|
|
2519
|
+
def load_channel(self, channel_index, channel_data=None, data=False, assign_shape = True):
|
|
2518
2520
|
"""Load a channel and enable active channel selection if needed."""
|
|
2519
2521
|
|
|
2520
2522
|
try:
|
|
2521
|
-
# Store current zoom limits if they exist and weren't provided
|
|
2522
|
-
if preserve_zoom is None and hasattr(self, 'ax'):
|
|
2523
|
-
current_xlim = None
|
|
2524
|
-
current_ylim = None
|
|
2525
|
-
else:
|
|
2526
|
-
current_xlim, current_ylim = preserve_zoom if preserve_zoom else (None, None)
|
|
2527
|
-
|
|
2528
2523
|
if not data: # For solo loading
|
|
2529
2524
|
import tifffile
|
|
2530
2525
|
filename, _ = QFileDialog.getOpenFileName(
|
|
@@ -2604,14 +2599,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2604
2599
|
|
|
2605
2600
|
if assign_shape: #keep original shape tracked to undo resampling.
|
|
2606
2601
|
self.original_shape = self.channel_data[channel_index].shape
|
|
2602
|
+
if len(self.original_shape) == 4:
|
|
2603
|
+
self.original_shape = (self.original_shape[0], self.original_shape[1], self.original_shape[2])
|
|
2607
2604
|
|
|
2608
|
-
|
|
2609
|
-
if current_xlim is not None and current_ylim is not None:
|
|
2610
|
-
self.ax.set_xlim(current_xlim)
|
|
2611
|
-
self.ax.set_ylim(current_ylim)
|
|
2612
|
-
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
2613
|
-
else:
|
|
2614
|
-
self.update_display()
|
|
2605
|
+
self.update_display()
|
|
2615
2606
|
|
|
2616
2607
|
|
|
2617
2608
|
|
|
@@ -2835,137 +2826,160 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2835
2826
|
self.channel_brightness[channel_index]['max'] = max_val / 255
|
|
2836
2827
|
self.update_display(preserve_zoom = (current_xlim, current_ylim))
|
|
2837
2828
|
|
|
2838
|
-
def update_display(self, preserve_zoom=None):
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2829
|
+
def update_display(self, preserve_zoom=None, dims = None):
|
|
2830
|
+
"""Update the display with currently visible channels and highlight overlay."""
|
|
2831
|
+
|
|
2832
|
+
self.figure.clear()
|
|
2833
|
+
|
|
2834
|
+
# Get active channels and their dimensions
|
|
2835
|
+
active_channels = [i for i in range(4) if self.channel_data[i] is not None]
|
|
2836
|
+
if dims is None:
|
|
2837
|
+
if active_channels:
|
|
2838
|
+
dims = [(self.channel_data[i].shape[1:3] if len(self.channel_data[i].shape) >= 3 else
|
|
2839
|
+
self.channel_data[i].shape) for i in active_channels]
|
|
2840
|
+
min_height = min(d[0] for d in dims)
|
|
2841
|
+
min_width = min(d[1] for d in dims)
|
|
2850
2842
|
else:
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2843
|
+
min_height = 1
|
|
2844
|
+
min_width = 1
|
|
2845
|
+
else:
|
|
2846
|
+
min_height = dims[0]
|
|
2847
|
+
min_width = dims[1]
|
|
2848
|
+
|
|
2849
|
+
# Set axes limits before displaying any images
|
|
2850
|
+
self.ax.set_xlim(-0.5, min_width - 0.5)
|
|
2851
|
+
self.ax.set_ylim(min_height - 0.5, -0.5)
|
|
2852
|
+
|
|
2853
|
+
# Create subplot with tight layout and white figure background
|
|
2854
|
+
self.figure.patch.set_facecolor('white')
|
|
2855
|
+
self.ax = self.figure.add_subplot(111)
|
|
2856
|
+
|
|
2857
|
+
# Store current zoom limits if they exist and weren't provided
|
|
2858
|
+
if preserve_zoom is None and hasattr(self, 'ax'):
|
|
2859
|
+
current_xlim = self.ax.get_xlim() if self.ax.get_xlim() != (0, 1) else None
|
|
2860
|
+
current_ylim = self.ax.get_ylim() if self.ax.get_ylim() != (0, 1) else None
|
|
2861
|
+
else:
|
|
2862
|
+
current_xlim, current_ylim = preserve_zoom if preserve_zoom else (None, None)
|
|
2863
|
+
|
|
2864
|
+
# Define base colors for each channel with increased intensity
|
|
2865
|
+
base_colors = self.base_colors
|
|
2866
|
+
# Set only the axes (image area) background to black
|
|
2867
|
+
self.ax.set_facecolor('black')
|
|
2868
|
+
|
|
2869
|
+
# Display each visible channel
|
|
2870
|
+
for channel in range(4):
|
|
2871
|
+
if (self.channel_visible[channel] and
|
|
2872
|
+
self.channel_data[channel] is not None):
|
|
2873
|
+
|
|
2874
|
+
# Check if we're dealing with RGB data
|
|
2875
|
+
is_rgb = len(self.channel_data[channel].shape) == 4 and (self.channel_data[channel].shape[-1] == 3 or self.channel_data[channel].shape[-1] == 4)
|
|
2876
|
+
|
|
2877
|
+
if len(self.channel_data[channel].shape) == 3 and not is_rgb:
|
|
2878
|
+
current_image = self.channel_data[channel][self.current_slice, :, :]
|
|
2879
|
+
elif is_rgb:
|
|
2880
|
+
current_image = self.channel_data[channel][self.current_slice] # Already has RGB channels
|
|
2881
|
+
else:
|
|
2882
|
+
current_image = self.channel_data[channel]
|
|
2872
2883
|
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2884
|
+
if is_rgb and self.channel_data[channel].shape[-1] == 3:
|
|
2885
|
+
# For RGB images, just display directly without colormap
|
|
2886
|
+
self.ax.imshow(current_image,
|
|
2887
|
+
alpha=0.7)
|
|
2888
|
+
elif is_rgb and self.channel_data[channel].shape[-1] == 4:
|
|
2889
|
+
self.ax.imshow(current_image) #For images that already have an alpha value and RGB, don't update alpha
|
|
2879
2890
|
|
|
2891
|
+
else:
|
|
2892
|
+
# Regular channel processing with colormap
|
|
2893
|
+
# Calculate brightness/contrast limits from entire volume
|
|
2894
|
+
img_min = self.min_max[channel][0]
|
|
2895
|
+
img_max = self.min_max[channel][1]
|
|
2896
|
+
|
|
2897
|
+
# Calculate vmin and vmax, ensuring we don't get a zero range
|
|
2898
|
+
if img_min == img_max:
|
|
2899
|
+
vmin = img_min
|
|
2900
|
+
vmax = img_min + 1
|
|
2880
2901
|
else:
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
img_min = self.min_max[channel][0]
|
|
2884
|
-
img_max = self.min_max[channel][1]
|
|
2885
|
-
|
|
2886
|
-
# Calculate vmin and vmax, ensuring we don't get a zero range
|
|
2887
|
-
if img_min == img_max:
|
|
2888
|
-
vmin = img_min
|
|
2889
|
-
vmax = img_min + 1
|
|
2890
|
-
else:
|
|
2891
|
-
vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
|
|
2892
|
-
vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
|
|
2893
|
-
|
|
2894
|
-
# Normalize the image safely
|
|
2895
|
-
if vmin == vmax:
|
|
2896
|
-
normalized_image = np.zeros_like(current_image)
|
|
2897
|
-
else:
|
|
2898
|
-
normalized_image = np.clip((current_image - vmin) / (vmax - vmin), 0, 1)
|
|
2899
|
-
|
|
2900
|
-
# Create custom colormap with higher intensity
|
|
2901
|
-
color = base_colors[channel]
|
|
2902
|
-
custom_cmap = LinearSegmentedColormap.from_list(
|
|
2903
|
-
f'custom_{channel}',
|
|
2904
|
-
[(0,0,0,0), (*color,1)]
|
|
2905
|
-
)
|
|
2906
|
-
|
|
2907
|
-
# Display the image with slightly higher alpha
|
|
2908
|
-
self.ax.imshow(normalized_image,
|
|
2909
|
-
alpha=0.7,
|
|
2910
|
-
cmap=custom_cmap,
|
|
2911
|
-
vmin=0,
|
|
2912
|
-
vmax=1)
|
|
2913
|
-
|
|
2914
|
-
# Rest of the code remains the same...
|
|
2915
|
-
# Add highlight overlay if it exists
|
|
2916
|
-
if self.highlight_overlay is not None:
|
|
2917
|
-
highlight_slice = self.highlight_overlay[self.current_slice]
|
|
2918
|
-
highlight_cmap = LinearSegmentedColormap.from_list(
|
|
2919
|
-
'highlight',
|
|
2920
|
-
[(0, 0, 0, 0), (1, 1, 0, 1)] # yellow
|
|
2921
|
-
)
|
|
2922
|
-
self.ax.imshow(highlight_slice,
|
|
2923
|
-
cmap=highlight_cmap,
|
|
2924
|
-
alpha=0.5)
|
|
2925
|
-
|
|
2926
|
-
# Restore zoom limits if they existed
|
|
2927
|
-
if current_xlim is not None and current_ylim is not None:
|
|
2928
|
-
self.ax.set_xlim(current_xlim)
|
|
2929
|
-
self.ax.set_ylim(current_ylim)
|
|
2930
|
-
|
|
2931
|
-
# Style the axes
|
|
2932
|
-
self.ax.set_xlabel('X')
|
|
2933
|
-
self.ax.set_ylabel('Y')
|
|
2934
|
-
self.ax.set_title(f'Slice {self.current_slice}')
|
|
2935
|
-
|
|
2936
|
-
# Make axis labels and ticks black for visibility against white background
|
|
2937
|
-
self.ax.xaxis.label.set_color('black')
|
|
2938
|
-
self.ax.yaxis.label.set_color('black')
|
|
2939
|
-
self.ax.title.set_color('black')
|
|
2940
|
-
self.ax.tick_params(colors='black')
|
|
2941
|
-
for spine in self.ax.spines.values():
|
|
2942
|
-
spine.set_color('black')
|
|
2943
|
-
|
|
2944
|
-
# Adjust the layout to ensure the plot fits well in the figure
|
|
2945
|
-
self.figure.tight_layout()
|
|
2946
|
-
|
|
2947
|
-
# Redraw measurement points and their labels
|
|
2948
|
-
for point in self.measurement_points:
|
|
2949
|
-
x1, y1, z1 = point['point1']
|
|
2950
|
-
x2, y2, z2 = point['point2']
|
|
2951
|
-
pair_idx = point['pair_index']
|
|
2952
|
-
|
|
2953
|
-
# Draw points and labels if they're on current slice
|
|
2954
|
-
if z1 == self.current_slice:
|
|
2955
|
-
self.ax.plot(x1, y1, 'yo', markersize=8)
|
|
2956
|
-
self.ax.text(x1, y1+5, str(pair_idx),
|
|
2957
|
-
color='white', ha='center', va='bottom')
|
|
2958
|
-
if z2 == self.current_slice:
|
|
2959
|
-
self.ax.plot(x2, y2, 'yo', markersize=8)
|
|
2960
|
-
self.ax.text(x2, y2+5, str(pair_idx),
|
|
2961
|
-
color='white', ha='center', va='bottom')
|
|
2902
|
+
vmin = img_min + (img_max - img_min) * self.channel_brightness[channel]['min']
|
|
2903
|
+
vmax = img_min + (img_max - img_min) * self.channel_brightness[channel]['max']
|
|
2962
2904
|
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2905
|
+
# Normalize the image safely
|
|
2906
|
+
if vmin == vmax:
|
|
2907
|
+
normalized_image = np.zeros_like(current_image)
|
|
2908
|
+
else:
|
|
2909
|
+
normalized_image = np.clip((current_image - vmin) / (vmax - vmin), 0, 1)
|
|
2910
|
+
|
|
2911
|
+
# Create custom colormap with higher intensity
|
|
2912
|
+
color = base_colors[channel]
|
|
2913
|
+
custom_cmap = LinearSegmentedColormap.from_list(
|
|
2914
|
+
f'custom_{channel}',
|
|
2915
|
+
[(0,0,0,0), (*color,1)]
|
|
2916
|
+
)
|
|
2917
|
+
|
|
2918
|
+
# Display the image with slightly higher alpha
|
|
2919
|
+
self.ax.imshow(normalized_image,
|
|
2920
|
+
alpha=0.7,
|
|
2921
|
+
cmap=custom_cmap,
|
|
2922
|
+
vmin=0,
|
|
2923
|
+
vmax=1,
|
|
2924
|
+
extent=(-0.5, min_width-0.5, min_height-0.5, -0.5))
|
|
2925
|
+
|
|
2926
|
+
# Add highlight overlay if it exists
|
|
2927
|
+
if self.highlight_overlay is not None:
|
|
2928
|
+
highlight_slice = self.highlight_overlay[self.current_slice]
|
|
2929
|
+
highlight_cmap = LinearSegmentedColormap.from_list(
|
|
2930
|
+
'highlight',
|
|
2931
|
+
[(0, 0, 0, 0), (1, 1, 0, 1)] # yellow
|
|
2932
|
+
)
|
|
2933
|
+
self.ax.imshow(highlight_slice,
|
|
2934
|
+
cmap=highlight_cmap,
|
|
2935
|
+
alpha=0.5)
|
|
2936
|
+
|
|
2937
|
+
# Restore zoom limits if they existed
|
|
2938
|
+
if current_xlim is not None and current_ylim is not None:
|
|
2939
|
+
self.ax.set_xlim(current_xlim)
|
|
2940
|
+
self.ax.set_ylim(current_ylim)
|
|
2941
|
+
|
|
2942
|
+
# Style the axes
|
|
2943
|
+
self.ax.set_xlabel('X')
|
|
2944
|
+
self.ax.set_ylabel('Y')
|
|
2945
|
+
self.ax.set_title(f'Slice {self.current_slice}')
|
|
2946
|
+
|
|
2947
|
+
# Make axis labels and ticks black for visibility against white background
|
|
2948
|
+
self.ax.xaxis.label.set_color('black')
|
|
2949
|
+
self.ax.yaxis.label.set_color('black')
|
|
2950
|
+
self.ax.title.set_color('black')
|
|
2951
|
+
self.ax.tick_params(colors='black')
|
|
2952
|
+
for spine in self.ax.spines.values():
|
|
2953
|
+
spine.set_color('black')
|
|
2954
|
+
|
|
2955
|
+
# Adjust the layout to ensure the plot fits well in the figure
|
|
2956
|
+
self.figure.tight_layout()
|
|
2957
|
+
|
|
2958
|
+
# Redraw measurement points and their labels
|
|
2959
|
+
for point in self.measurement_points:
|
|
2960
|
+
x1, y1, z1 = point['point1']
|
|
2961
|
+
x2, y2, z2 = point['point2']
|
|
2962
|
+
pair_idx = point['pair_index']
|
|
2963
|
+
|
|
2964
|
+
# Draw points and labels if they're on current slice
|
|
2965
|
+
if z1 == self.current_slice:
|
|
2966
|
+
self.ax.plot(x1, y1, 'yo', markersize=8)
|
|
2967
|
+
self.ax.text(x1, y1+5, str(pair_idx),
|
|
2968
|
+
color='white', ha='center', va='bottom')
|
|
2969
|
+
if z2 == self.current_slice:
|
|
2970
|
+
self.ax.plot(x2, y2, 'yo', markersize=8)
|
|
2971
|
+
self.ax.text(x2, y2+5, str(pair_idx),
|
|
2972
|
+
color='white', ha='center', va='bottom')
|
|
2973
|
+
|
|
2974
|
+
# Draw line if both points are on current slice
|
|
2975
|
+
if z1 == z2 == self.current_slice:
|
|
2976
|
+
self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
|
|
2977
|
+
|
|
2978
|
+
if active_channels:
|
|
2979
|
+
self.ax.set_xlim(-0.5, min_width - 0.5)
|
|
2980
|
+
self.ax.set_ylim(min_height - 0.5, -0.5)
|
|
2966
2981
|
|
|
2967
|
-
|
|
2968
|
-
|
|
2982
|
+
self.canvas.draw()
|
|
2969
2983
|
def show_netshow_dialog(self):
|
|
2970
2984
|
dialog = NetShowDialog(self)
|
|
2971
2985
|
dialog.exec()
|
|
@@ -3913,19 +3927,19 @@ class ColorDialog(QDialog):
|
|
|
3913
3927
|
class Show3dDialog(QDialog):
|
|
3914
3928
|
def __init__(self, parent=None):
|
|
3915
3929
|
super().__init__(parent)
|
|
3916
|
-
self.setWindowTitle("Display Parameters")
|
|
3930
|
+
self.setWindowTitle("Display Parameters (Napari)")
|
|
3917
3931
|
self.setModal(True)
|
|
3918
3932
|
|
|
3919
3933
|
layout = QFormLayout(self)
|
|
3920
3934
|
|
|
3921
|
-
self.downsample = QLineEdit("
|
|
3922
|
-
layout.addRow("Downsample Factor (
|
|
3935
|
+
self.downsample = QLineEdit("")
|
|
3936
|
+
layout.addRow("Downsample Factor (Optional to speed up display):", self.downsample)
|
|
3923
3937
|
|
|
3924
3938
|
# Network Overlay checkbox (default True)
|
|
3925
|
-
self.
|
|
3926
|
-
self.
|
|
3927
|
-
self.
|
|
3928
|
-
layout.addRow("
|
|
3939
|
+
self.cubic = QPushButton("cubic")
|
|
3940
|
+
self.cubic.setCheckable(True)
|
|
3941
|
+
self.cubic.setChecked(False)
|
|
3942
|
+
layout.addRow("Use cubic downsample (Slower but preserves shape better potentially)?", self.cubic)
|
|
3929
3943
|
|
|
3930
3944
|
# Add Run button
|
|
3931
3945
|
run_button = QPushButton("Show 3D")
|
|
@@ -3939,22 +3953,49 @@ class Show3dDialog(QDialog):
|
|
|
3939
3953
|
|
|
3940
3954
|
# Get amount
|
|
3941
3955
|
try:
|
|
3942
|
-
downsample = float(self.downsample.text()) if self.downsample.text() else
|
|
3956
|
+
downsample = float(self.downsample.text()) if self.downsample.text() else None
|
|
3943
3957
|
except ValueError:
|
|
3944
|
-
downsample =
|
|
3958
|
+
downsample = None
|
|
3945
3959
|
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
my_network.show_3D(my_network.network_overlay, downsample)
|
|
3960
|
+
cubic = self.cubic.isChecked()
|
|
3961
|
+
|
|
3962
|
+
if cubic:
|
|
3963
|
+
order = 3
|
|
3951
3964
|
else:
|
|
3952
|
-
|
|
3965
|
+
order = 0
|
|
3966
|
+
|
|
3967
|
+
arrays_3d = []
|
|
3968
|
+
arrays_4d = []
|
|
3969
|
+
|
|
3970
|
+
color_template = ['red', 'green', 'white', 'cyan', 'yellow'] # color list
|
|
3971
|
+
colors = []
|
|
3972
|
+
|
|
3973
|
+
|
|
3974
|
+
for i, channel in enumerate(self.parent().channel_data):
|
|
3975
|
+
if channel is not None:
|
|
3976
|
+
|
|
3977
|
+
if len(channel.shape) == 3:
|
|
3978
|
+
visible = self.parent().channel_buttons[i].isChecked()
|
|
3979
|
+
if visible:
|
|
3980
|
+
arrays_3d.append(channel)
|
|
3981
|
+
colors.append(color_template[i])
|
|
3982
|
+
elif len(channel.shape) == 4:
|
|
3983
|
+
visible = self.parent().channel_buttons[i].isChecked()
|
|
3984
|
+
if visible:
|
|
3985
|
+
arrays_4d.append(channel)
|
|
3986
|
+
|
|
3987
|
+
if self.parent().highlight_overlay is not None:
|
|
3988
|
+
arrays_3d.append(self.parent().highlight_overlay)
|
|
3989
|
+
colors.append(color_template[4])
|
|
3990
|
+
|
|
3991
|
+
n3d.show_3d(arrays_3d, arrays_4d, down_factor = downsample, order = order, xy_scale = my_network.xy_scale, z_scale = my_network.z_scale, colors = colors)
|
|
3953
3992
|
|
|
3954
3993
|
self.accept()
|
|
3955
3994
|
|
|
3956
3995
|
except Exception as e:
|
|
3957
3996
|
print(f"Error: {e}")
|
|
3997
|
+
import traceback
|
|
3998
|
+
print(traceback.format_exc())
|
|
3958
3999
|
|
|
3959
4000
|
|
|
3960
4001
|
class NetOverlayDialog(QDialog):
|
|
@@ -4969,6 +5010,7 @@ class ResizeDialog(QDialog):
|
|
|
4969
5010
|
resized_data = n3d.resize(self.parent().channel_data[channel], resize, order)
|
|
4970
5011
|
self.parent().load_channel(channel, channel_data=resized_data, data=True, assign_shape = False)
|
|
4971
5012
|
|
|
5013
|
+
|
|
4972
5014
|
|
|
4973
5015
|
# Process highlight overlay if it exists
|
|
4974
5016
|
if self.parent().highlight_overlay is not None:
|
|
@@ -4990,8 +5032,8 @@ class ResizeDialog(QDialog):
|
|
|
4990
5032
|
# Process highlight overlay if it exists
|
|
4991
5033
|
if self.parent().highlight_overlay is not None:
|
|
4992
5034
|
self.parent().highlight_overlay = n3d.upsample_with_padding(self.parent().highlight_overlay, original_shape = self.parent().original_shape)
|
|
4993
|
-
|
|
4994
|
-
|
|
5035
|
+
if my_network.search_region is not None:
|
|
5036
|
+
my_network.search_region = n3d.upsample_with_padding(my_network.search_region, original_shape = self.parent().original_shape)
|
|
4995
5037
|
|
|
4996
5038
|
|
|
4997
5039
|
# Update slider range based on new z-dimension
|
|
@@ -5417,7 +5459,7 @@ class DilateDialog(QDialog):
|
|
|
5417
5459
|
)
|
|
5418
5460
|
|
|
5419
5461
|
# Update both the display data and the network object
|
|
5420
|
-
self.parent().load_channel(self.parent().active_channel, result, True
|
|
5462
|
+
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
5421
5463
|
|
|
5422
5464
|
self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
5423
5465
|
self.accept()
|
|
@@ -5503,7 +5545,7 @@ class ErodeDialog(QDialog):
|
|
|
5503
5545
|
)
|
|
5504
5546
|
|
|
5505
5547
|
|
|
5506
|
-
self.parent().load_channel(self.parent().active_channel, result, True
|
|
5548
|
+
self.parent().load_channel(self.parent().active_channel, result, True)
|
|
5507
5549
|
|
|
5508
5550
|
|
|
5509
5551
|
self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
@@ -5908,8 +5950,13 @@ class GenNodesDialog(QDialog):
|
|
|
5908
5950
|
if down_factor is None:
|
|
5909
5951
|
self.down_factor = QLineEdit("0")
|
|
5910
5952
|
layout.addRow("Downsample Factor (Speeds up calculation at the cost of fidelity):", self.down_factor)
|
|
5953
|
+
self.cubic = QPushButton("Cubic Downsample")
|
|
5954
|
+
self.cubic.setCheckable(True)
|
|
5955
|
+
self.cubic.setChecked(False)
|
|
5956
|
+
layout.addRow("(if downsampling): Use cubic dilation? (Slower but can preserve structure better)", self.cubic)
|
|
5911
5957
|
else:
|
|
5912
|
-
self.down_factor = down_factor
|
|
5958
|
+
self.down_factor = down_factor[0]
|
|
5959
|
+
self.cubic = down_factor[1]
|
|
5913
5960
|
|
|
5914
5961
|
self.directory = QLineEdit()
|
|
5915
5962
|
self.directory.setPlaceholderText("Leave empty to save in active dir")
|
|
@@ -5922,7 +5969,7 @@ class GenNodesDialog(QDialog):
|
|
|
5922
5969
|
self.retain.setChecked(True)
|
|
5923
5970
|
layout.addRow("Retain Original Edges? (Will be moved to overlay 2):", self.retain)
|
|
5924
5971
|
else:
|
|
5925
|
-
self.retain =
|
|
5972
|
+
self.retain = False
|
|
5926
5973
|
|
|
5927
5974
|
# Add Run button
|
|
5928
5975
|
run_button = QPushButton("Run Node Generation")
|
|
@@ -5956,17 +6003,24 @@ class GenNodesDialog(QDialog):
|
|
|
5956
6003
|
# Get down_factor
|
|
5957
6004
|
if type(self.down_factor) is int:
|
|
5958
6005
|
down_factor = self.down_factor
|
|
6006
|
+
cubic = self.cubic
|
|
5959
6007
|
else:
|
|
5960
6008
|
try:
|
|
5961
6009
|
down_factor = int(self.down_factor.text()) if self.down_factor.text() else 0
|
|
5962
6010
|
except ValueError:
|
|
5963
6011
|
down_factor = 0
|
|
6012
|
+
cubic = self.cubic.isChecked()
|
|
5964
6013
|
|
|
5965
6014
|
try:
|
|
5966
6015
|
retain = self.retain.isChecked()
|
|
5967
6016
|
except:
|
|
5968
6017
|
retain = True
|
|
5969
6018
|
|
|
6019
|
+
if cubic:
|
|
6020
|
+
order = 3
|
|
6021
|
+
else:
|
|
6022
|
+
order = 0
|
|
6023
|
+
|
|
5970
6024
|
|
|
5971
6025
|
result, skele = n3d.label_vertices(
|
|
5972
6026
|
my_network.edges,
|
|
@@ -5974,12 +6028,14 @@ class GenNodesDialog(QDialog):
|
|
|
5974
6028
|
branch_removal=branch_removal,
|
|
5975
6029
|
comp_dil=comp_dil,
|
|
5976
6030
|
down_factor=down_factor,
|
|
6031
|
+
order = order,
|
|
5977
6032
|
return_skele = True
|
|
5978
6033
|
|
|
5979
6034
|
)
|
|
5980
6035
|
|
|
5981
6036
|
if down_factor > 0 and not self.called:
|
|
5982
|
-
|
|
6037
|
+
|
|
6038
|
+
my_network.edges = n3d.downsample(my_network.edges, down_factor, order = order)
|
|
5983
6039
|
my_network.xy_scale = my_network.xy_scale * down_factor
|
|
5984
6040
|
my_network.z_scale = my_network.z_scale * down_factor
|
|
5985
6041
|
print("xy_scales and z_scales have been adjusted per downsample. Check image -> properties to manually reset them to 1 if desired.")
|
|
@@ -5995,13 +6051,14 @@ class GenNodesDialog(QDialog):
|
|
|
5995
6051
|
except:
|
|
5996
6052
|
pass
|
|
5997
6053
|
|
|
6054
|
+
self.parent().load_channel(1, channel_data = skele, data = True)
|
|
6055
|
+
|
|
6056
|
+
self.parent().load_channel(0, channel_data = result, data = True)
|
|
5998
6057
|
|
|
5999
6058
|
if retain:
|
|
6000
6059
|
self.parent().load_channel(3, channel_data = my_network.edges, data = True)
|
|
6001
6060
|
|
|
6002
|
-
self.parent().load_channel(1, channel_data = skele, data = True)
|
|
6003
6061
|
|
|
6004
|
-
self.parent().load_channel(0, channel_data = result, data = True)
|
|
6005
6062
|
|
|
6006
6063
|
self.parent().update_display()
|
|
6007
6064
|
self.accept()
|
|
@@ -6040,6 +6097,12 @@ class BranchDialog(QDialog):
|
|
|
6040
6097
|
self.down_factor = QLineEdit("0")
|
|
6041
6098
|
layout.addRow("Internal downsample (will have to recompute nodes)?:", self.down_factor)
|
|
6042
6099
|
|
|
6100
|
+
# cubic checkbox (default False)
|
|
6101
|
+
self.cubic = QPushButton("Cubic Downsample")
|
|
6102
|
+
self.cubic.setCheckable(True)
|
|
6103
|
+
self.cubic.setChecked(False)
|
|
6104
|
+
layout.addRow("(if downsampling): Use cubic dilation? (Slower but can preserve structure better)", self.cubic)
|
|
6105
|
+
|
|
6043
6106
|
# Add Run button
|
|
6044
6107
|
run_button = QPushButton("Run Branch Label")
|
|
6045
6108
|
run_button.clicked.connect(self.branch_label)
|
|
@@ -6056,25 +6119,29 @@ class BranchDialog(QDialog):
|
|
|
6056
6119
|
|
|
6057
6120
|
nodes = self.nodes.isChecked()
|
|
6058
6121
|
GPU = self.GPU.isChecked()
|
|
6122
|
+
cubic = self.cubic.isChecked()
|
|
6123
|
+
|
|
6059
6124
|
|
|
6060
6125
|
original_shape = my_network.edges.shape
|
|
6126
|
+
original_array = copy.deepcopy(my_network.edges)
|
|
6061
6127
|
|
|
6062
6128
|
if down_factor > 0:
|
|
6063
|
-
self.parent().show_gennodes_dialog(down_factor = down_factor, called = True)
|
|
6129
|
+
self.parent().show_gennodes_dialog(down_factor = [down_factor, cubic], called = True)
|
|
6064
6130
|
elif nodes:
|
|
6065
6131
|
self.parent().show_gennodes_dialog(called = True)
|
|
6066
6132
|
down_factor = None
|
|
6067
6133
|
|
|
6068
6134
|
if my_network.edges is not None and my_network.nodes is not None and my_network.id_overlay is not None:
|
|
6069
6135
|
|
|
6070
|
-
output = n3d.label_branches(my_network.edges, nodes = my_network.nodes, bonus_array =
|
|
6136
|
+
output = n3d.label_branches(my_network.edges, nodes = my_network.nodes, bonus_array = original_array, GPU = GPU, down_factor = down_factor, arrayshape = original_shape)
|
|
6071
6137
|
|
|
6072
6138
|
if down_factor is not None:
|
|
6073
6139
|
|
|
6074
|
-
self.parent().reset(nodes = True, id_overlay = True)
|
|
6140
|
+
self.parent().reset(nodes = True, id_overlay = True, edges = True)
|
|
6075
6141
|
|
|
6076
6142
|
else:
|
|
6077
6143
|
self.parent().reset(id_overlay = True)
|
|
6144
|
+
self.parent().update_display(dims = (output.shape[1], output.shape[2]))
|
|
6078
6145
|
|
|
6079
6146
|
self.parent().load_channel(1, channel_data = output, data = True)
|
|
6080
6147
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
|
|
5
5
|
Author-email: Liam McLaughlin <boom2449@gmail.com>
|
|
6
6
|
Project-URL: User_Manual, https://drive.google.com/drive/folders/1fTkz3n4LN9_VxKRKC8lVQSlrz_wq0bVn?usp=drive_link
|
|
@@ -20,7 +20,7 @@ Requires-Dist: networkx
|
|
|
20
20
|
Requires-Dist: opencv-python-headless
|
|
21
21
|
Requires-Dist: openpyxl
|
|
22
22
|
Requires-Dist: pandas
|
|
23
|
-
Requires-Dist:
|
|
23
|
+
Requires-Dist: napari
|
|
24
24
|
Requires-Dist: python-louvain
|
|
25
25
|
Requires-Dist: tifffile
|
|
26
26
|
Requires-Dist: PyQt6
|
|
@@ -3,16 +3,16 @@ nettracer3d/community_extractor.py,sha256=8bRDJOfZhOFLtpkJVaDQrQ4O8wUywyr-EfVvW5
|
|
|
3
3
|
nettracer3d/hub_getter.py,sha256=KiNtxdajLkwB1ftslvrh1FE1Ch9ZCFEmHSEEotwR-To,8298
|
|
4
4
|
nettracer3d/modularity.py,sha256=V1f3s_vGd8EuVz27mzq6ycIGr0BWIpH7c7NU4QjgAHU,30247
|
|
5
5
|
nettracer3d/morphology.py,sha256=wv7v06YUcn5lMyefcc_znQlXF5iDxvUdoc0fXOKlGTw,12982
|
|
6
|
-
nettracer3d/nettracer.py,sha256=
|
|
7
|
-
nettracer3d/nettracer_gui.py,sha256=
|
|
6
|
+
nettracer3d/nettracer.py,sha256=CfAdS3SnigL4TkfqwHRIg0WeBtwJ1nGysNq5pfSSwVU,201682
|
|
7
|
+
nettracer3d/nettracer_gui.py,sha256=e72Z9Yw0wxZ1ZIvrPv20oCtI49W3o0-YL7pNTLsM5Qc,289120
|
|
8
8
|
nettracer3d/network_analysis.py,sha256=MJBBjslA1k_R8ymid77U-qGSgzxFVfzGVQhE0IdhnbE,48046
|
|
9
9
|
nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
|
|
10
10
|
nettracer3d/node_draw.py,sha256=BMiD_FrlOHeGD4AQZ_Emd152PfxFuMgGf2x4S0TOTnw,9752
|
|
11
11
|
nettracer3d/proximity.py,sha256=KYs4QUbt1U79RLzTvt8BmrxeGVaeKOQ2brtzTjjA78c,11011
|
|
12
12
|
nettracer3d/simple_network.py,sha256=fP1gkDdtQcHruEZpUdasKdZeVacoLOxKhR3bY0L1CAQ,15426
|
|
13
13
|
nettracer3d/smart_dilate.py,sha256=howfO6Lw5PxNjkaOBSCjkmf7fyau_-_8iTct2mAuTAQ,22083
|
|
14
|
-
nettracer3d-0.3.
|
|
15
|
-
nettracer3d-0.3.
|
|
16
|
-
nettracer3d-0.3.
|
|
17
|
-
nettracer3d-0.3.
|
|
18
|
-
nettracer3d-0.3.
|
|
14
|
+
nettracer3d-0.3.6.dist-info/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
|
|
15
|
+
nettracer3d-0.3.6.dist-info/METADATA,sha256=8NWQk4tgtrZhISu9b210_FY4Q_GtcLN0Dhb1AdZukmg,2894
|
|
16
|
+
nettracer3d-0.3.6.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
17
|
+
nettracer3d-0.3.6.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
|
|
18
|
+
nettracer3d-0.3.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|