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 CHANGED
@@ -10,7 +10,10 @@ import multiprocessing as mp
10
10
  import os
11
11
  import copy
12
12
  import statistics as stats
13
- import plotly.graph_objects as go
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 = bonus_array.shape
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.
@@ -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 (beta)")
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, preserve_zoom = None):
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
- # Restore zoom limits if they existed
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
- """Update the display with currently visible channels and highlight overlay."""
2840
- self.figure.clear()
2841
-
2842
- # Create subplot with tight layout and white figure background
2843
- self.figure.patch.set_facecolor('white')
2844
- self.ax = self.figure.add_subplot(111)
2845
-
2846
- # Store current zoom limits if they exist and weren't provided
2847
- if preserve_zoom is None and hasattr(self, 'ax'):
2848
- current_xlim = self.ax.get_xlim() if self.ax.get_xlim() != (0, 1) else None
2849
- current_ylim = self.ax.get_ylim() if self.ax.get_ylim() != (0, 1) else None
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
- current_xlim, current_ylim = preserve_zoom if preserve_zoom else (None, None)
2852
-
2853
- # Define base colors for each channel with increased intensity
2854
- base_colors = self.base_colors
2855
- # Set only the axes (image area) background to black
2856
- self.ax.set_facecolor('black')
2857
-
2858
- # Display each visible channel
2859
- for channel in range(4):
2860
- if (self.channel_visible[channel] and
2861
- self.channel_data[channel] is not None):
2862
-
2863
- # Check if we're dealing with RGB data
2864
- 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)
2865
-
2866
- if len(self.channel_data[channel].shape) == 3 and not is_rgb:
2867
- current_image = self.channel_data[channel][self.current_slice, :, :]
2868
- elif is_rgb:
2869
- current_image = self.channel_data[channel][self.current_slice] # Already has RGB channels
2870
- else:
2871
- current_image = self.channel_data[channel]
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
- if is_rgb and self.channel_data[channel].shape[-1] == 3:
2874
- # For RGB images, just display directly without colormap
2875
- self.ax.imshow(current_image,
2876
- alpha=0.7)
2877
- elif is_rgb and self.channel_data[channel].shape[-1] == 4:
2878
- self.ax.imshow(current_image) #For images that already have an alpha value and RGB, don't update alpha
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
- # Regular channel processing with colormap
2882
- # Calculate brightness/contrast limits from entire volume
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
- # Draw line if both points are on current slice
2964
- if z1 == z2 == self.current_slice:
2965
- self.ax.plot([x1, x2], [y1, y2], 'r--', alpha=0.5)
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
- self.canvas.draw()
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("1")
3922
- layout.addRow("Downsample Factor (Expect Slowness on Large Images):", self.downsample)
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.overlay = QPushButton("Overlay 1")
3926
- self.overlay.setCheckable(True)
3927
- self.overlay.setChecked(True)
3928
- layout.addRow("Include Overlay 1?", self.overlay)
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 1
3956
+ downsample = float(self.downsample.text()) if self.downsample.text() else None
3943
3957
  except ValueError:
3944
- downsample = 1
3958
+ downsample = None
3945
3959
 
3946
- overlay = self.overlay.isChecked()
3947
- if overlay:
3948
-
3949
- # Example analysis plot
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
- my_network.show_3D(down_factor = downsample)
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
- my_network.search_region = n3d.upsample_with_padding(my_network.search_region, original_shape = self.parent().original_shape)
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, preserve_zoom=(current_xlim, current_ylim))
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, preserve_zoom=(current_xlim, current_ylim))
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 = True
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
- my_network.edges = n3d.downsample(my_network.edges, down_factor)
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 = my_network.id_overlay, GPU = GPU, down_factor = down_factor)
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.4
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: plotly
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=ViIDJz8k-wcxVgcknH_YpG86QyfH8F86T1wKb5d4Cns,206526
7
- nettracer3d/nettracer_gui.py,sha256=3T5jLilk0DCXx1qMFW_s0Yq_o0hU_9WH0LkTnl6K-sg,286748
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.4.dist-info/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
15
- nettracer3d-0.3.4.dist-info/METADATA,sha256=_WReeJl26IM6fTvZNZgtiFsFH59Fo1YA3DV_msrBMrc,2894
16
- nettracer3d-0.3.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
17
- nettracer3d-0.3.4.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
18
- nettracer3d-0.3.4.dist-info/RECORD,,
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,,