nettracer3d 0.6.2__tar.gz → 0.6.4__tar.gz

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.
Files changed (25) hide show
  1. {nettracer3d-0.6.2/src/nettracer3d.egg-info → nettracer3d-0.6.4}/PKG-INFO +13 -5
  2. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/README.md +10 -3
  3. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/pyproject.toml +1 -1
  4. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d/nettracer.py +45 -12
  5. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d/nettracer_gui.py +253 -31
  6. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d/network_analysis.py +6 -6
  7. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d/segmenter.py +5 -1
  8. {nettracer3d-0.6.2 → nettracer3d-0.6.4/src/nettracer3d.egg-info}/PKG-INFO +13 -5
  9. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/LICENSE +0 -0
  10. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/setup.cfg +0 -0
  11. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d/__init__.py +0 -0
  12. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d/community_extractor.py +0 -0
  13. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d/modularity.py +0 -0
  14. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d/morphology.py +0 -0
  15. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d/network_draw.py +0 -0
  16. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d/node_draw.py +0 -0
  17. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d/proximity.py +0 -0
  18. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d/run.py +0 -0
  19. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d/simple_network.py +0 -0
  20. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d/smart_dilate.py +0 -0
  21. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
  22. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  23. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d.egg-info/entry_points.txt +0 -0
  24. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d.egg-info/requires.txt +0 -0
  25. {nettracer3d-0.6.2 → nettracer3d-0.6.4}/src/nettracer3d.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
5
5
  Author-email: Liam McLaughlin <mclaughlinliam99@gmail.com>
6
6
  Project-URL: User_Tutorial, https://www.youtube.com/watch?v=cRatn5VTWDY
@@ -33,6 +33,7 @@ Provides-Extra: cuda12
33
33
  Requires-Dist: cupy-cuda12x; extra == "cuda12"
34
34
  Provides-Extra: cupy
35
35
  Requires-Dist: cupy; extra == "cupy"
36
+ Dynamic: license-file
36
37
 
37
38
  NetTracer3D is a python package developed for both 2D and 3D analysis of microscopic images in the .tif file format. It supports generation of 3D networks showing the relationships between objects (or nodes) in three dimensional space, either based on their own proximity or connectivity via connecting objects such as nerves or blood vessels. In addition to these functionalities are several advanced 3D data processing algorithms, such as labeling of branched structures or abstraction of branched structures into networks. Note that nettracer3d uses segmented data, which can be segmented from other softwares such as ImageJ and imported into NetTracer3D, although it does offer its own segmentation via intensity and volumetric thresholding, or random forest machine learning segmentation. NetTracer3D currently has a fully functional GUI. To use the GUI, after installing the nettracer3d package via pip, enter the command 'nettracer3d' in your command prompt:
38
39
 
@@ -44,8 +45,15 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
44
45
 
45
46
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
46
47
 
47
- -- Version 0.6.2 updates --
48
+ -- Version 0.6.4 updates --
48
49
 
49
- 1. Fixed bug with performing 2D distance transforms on CPU
50
+ 1. Fixed bug with tabulated data in top right having a right click window corresponding to the bottom left.
50
51
 
51
- 2. Updated ram_lock mode in the segmenter to use smaller chunks and garbage collect better.
52
+ 2. Fixed bug when converting communities to nodes that let the same community connect to itself in the new network.
53
+
54
+ 3. Removed attempted trendline fitting from degree distribution
55
+
56
+ 4. Added new feature to skeletonization (and corresponding branch labeler/gennodes)
57
+ Now you can have the program attempt to auto-correct 3D skeletonization loop artifacts through a method that just runs the 3d fill holes algo and then attempts to reskeletonize the output. This worked well in my own testing.
58
+
59
+ 5. Other minor fixes/improvements
@@ -8,8 +8,15 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
8
8
 
9
9
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
10
10
 
11
- -- Version 0.6.2 updates --
11
+ -- Version 0.6.4 updates --
12
12
 
13
- 1. Fixed bug with performing 2D distance transforms on CPU
13
+ 1. Fixed bug with tabulated data in top right having a right click window corresponding to the bottom left.
14
14
 
15
- 2. Updated ram_lock mode in the segmenter to use smaller chunks and garbage collect better.
15
+ 2. Fixed bug when converting communities to nodes that let the same community connect to itself in the new network.
16
+
17
+ 3. Removed attempted trendline fitting from degree distribution
18
+
19
+ 4. Added new feature to skeletonization (and corresponding branch labeler/gennodes)
20
+ Now you can have the program attempt to auto-correct 3D skeletonization loop artifacts through a method that just runs the 3d fill holes algo and then attempts to reskeletonize the output. This worked well in my own testing.
21
+
22
+ 5. Other minor fixes/improvements
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nettracer3d"
3
- version = "0.6.2"
3
+ version = "0.6.4"
4
4
  authors = [
5
5
  { name="Liam McLaughlin", email="mclaughlinliam99@gmail.com" },
6
6
  ]
@@ -751,6 +751,8 @@ def fill_holes_3d(array):
751
751
 
752
752
  return holes_mask
753
753
 
754
+ print("Filling Holes...")
755
+
754
756
  array = binarize(array)
755
757
  inv_array = invert_array(array)
756
758
 
@@ -3270,7 +3272,10 @@ class Network_3D:
3270
3272
  self._xy_scale = xy_scale
3271
3273
  self._z_scale = z_scale
3272
3274
 
3273
- self.save_scaling(directory)
3275
+ try:
3276
+ self.save_scaling(directory)
3277
+ except:
3278
+ pass
3274
3279
 
3275
3280
  if search is None and ignore_search_region == False:
3276
3281
  search = 0
@@ -3286,29 +3291,51 @@ class Network_3D:
3286
3291
  if other_nodes is not None:
3287
3292
  self.merge_nodes(other_nodes, label_nodes)
3288
3293
 
3289
- self.save_nodes(directory)
3290
- self.save_node_identities(directory)
3294
+ try:
3295
+ self.save_nodes(directory)
3296
+ except:
3297
+ pass
3298
+ try:
3299
+ self.save_node_identities(directory)
3300
+ except:
3301
+ pass
3291
3302
 
3292
3303
  if not ignore_search_region:
3293
3304
  self.calculate_search_region(search, GPU = GPU, fast_dil = fast_dil, GPU_downsample = GPU_downsample)
3294
- self._nodes = None
3305
+ #self._nodes = None # I originally put this here to micromanage RAM a little bit (it writes it to disk so I wanted to purge it from mem briefly but now idt thats necessary and I'd rather give it flexibility when lacking write permissions)
3295
3306
  search = None
3296
- self.save_search_region(directory)
3307
+ try:
3308
+ self.save_search_region(directory)
3309
+ except:
3310
+ pass
3297
3311
 
3298
3312
  self.calculate_edges(edges, diledge = diledge, inners = inners, hash_inner_edges = hash_inners, search = search, remove_edgetrunk = remove_trunk, GPU = GPU, fast_dil = fast_dil, skeletonized = skeletonize)
3299
3313
  del edges
3300
- self.save_edges(directory)
3314
+ try:
3315
+ self.save_edges(directory)
3316
+ except:
3317
+ pass
3301
3318
 
3302
3319
  self.calculate_network(search = search, ignore_search_region = ignore_search_region)
3303
- self.save_network(directory)
3320
+
3321
+ try:
3322
+ self.save_network(directory)
3323
+ except:
3324
+ pass
3304
3325
 
3305
3326
  if self._nodes is None:
3306
3327
  self.load_nodes(directory)
3307
3328
 
3308
3329
  self.calculate_node_centroids(down_factor)
3309
- self.save_node_centroids(directory)
3330
+ try:
3331
+ self.save_node_centroids(directory)
3332
+ except:
3333
+ pass
3310
3334
  self.calculate_edge_centroids(down_factor)
3311
- self.save_edge_centroids(directory)
3335
+ try:
3336
+ self.save_edge_centroids(directory)
3337
+ except:
3338
+ pass
3312
3339
 
3313
3340
 
3314
3341
  def draw_network(self, directory = None, down_factor = None, GPU = False):
@@ -3557,15 +3584,21 @@ class Network_3D:
3557
3584
  list1 = self._network_lists[0] #Get network lists to change
3558
3585
  list2 = self._network_lists[1]
3559
3586
  list3 = self._network_lists[2]
3587
+ return1 = []
3588
+ return2 = []
3589
+ return3 = []
3560
3590
 
3561
3591
  for i in range(len(list1)):
3562
3592
  list1[i] = self.communities[list1[i]] #Set node at network list spot to its community instead
3563
3593
  list2[i] = self.communities[list2[i]]
3564
- if list1[i] == list2[i]: #If the edge corresponding there joins different communities, it will not be set to 0
3565
- list3[i] = 0
3594
+ if list1[i] != list2[i]: #Avoid self - self connections
3595
+ return1.append(list1[i])
3596
+ return2.append(list2[i])
3597
+ return3.append(list3[i])
3598
+
3566
3599
 
3567
3600
 
3568
- self.network_lists = [list1, list2, list3]
3601
+ self.network_lists = [return1, return2, return3]
3569
3602
 
3570
3603
  if self._nodes is not None:
3571
3604
  self._nodes = update_array(self._nodes, inverted, targets = targets) #Set the array to match the new network
@@ -325,7 +325,7 @@ class ImageViewerWindow(QMainWindow):
325
325
  self.tabbed_data = TabbedDataWidget(self)
326
326
  right_layout.addWidget(self.tabbed_data)
327
327
  # Initialize data_table property to None - it will be set when tabs are added
328
- self.data_table = None
328
+ self.data_table = []
329
329
 
330
330
  # Create table control panel
331
331
  table_control = QWidget()
@@ -1814,6 +1814,7 @@ class ImageViewerWindow(QMainWindow):
1814
1814
  new_xlim = [xdata - x_range, xdata + x_range]
1815
1815
  new_ylim = [ydata - y_range, ydata + y_range]
1816
1816
 
1817
+
1817
1818
  if (new_xlim[0] <= self.original_xlim[0] or
1818
1819
  new_xlim[1] >= self.original_xlim[1] or
1819
1820
  new_ylim[0] <= self.original_ylim[0] or
@@ -2429,10 +2430,14 @@ class ImageViewerWindow(QMainWindow):
2429
2430
  binarize_action.triggered.connect(self.show_binarize_dialog)
2430
2431
  label_action = image_menu.addAction("Label Objects")
2431
2432
  label_action.triggered.connect(self.show_label_dialog)
2433
+ slabel_action = image_menu.addAction("Neighborhood Labels")
2434
+ slabel_action.triggered.connect(self.show_slabel_dialog)
2432
2435
  thresh_action = image_menu.addAction("Threshold/Segment")
2433
2436
  thresh_action.triggered.connect(self.show_thresh_dialog)
2434
2437
  mask_action = image_menu.addAction("Mask Channel")
2435
2438
  mask_action.triggered.connect(self.show_mask_dialog)
2439
+ type_action = image_menu.addAction("Channel dtype")
2440
+ type_action.triggered.connect(self.show_type_dialog)
2436
2441
  skeletonize_action = image_menu.addAction("Skeletonize")
2437
2442
  skeletonize_action.triggered.connect(self.show_skeletonize_dialog)
2438
2443
  watershed_action = image_menu.addAction("Watershed")
@@ -2657,6 +2662,11 @@ class ImageViewerWindow(QMainWindow):
2657
2662
  dialog = LabelDialog(self)
2658
2663
  dialog.exec()
2659
2664
 
2665
+ def show_slabel_dialog(self):
2666
+ """Show the slabel dialog"""
2667
+ dialog = SLabelDialog(self)
2668
+ dialog.exec()
2669
+
2660
2670
  def show_thresh_dialog(self):
2661
2671
  """Show threshold dialog"""
2662
2672
  if self.machine_window is not None:
@@ -2671,6 +2681,11 @@ class ImageViewerWindow(QMainWindow):
2671
2681
  dialog = MaskDialog(self)
2672
2682
  dialog.exec()
2673
2683
 
2684
+ def show_type_dialog(self):
2685
+ """Show the type dialog"""
2686
+ dialog = TypeDialog(self)
2687
+ dialog.exec()
2688
+
2674
2689
  def show_skeletonize_dialog(self):
2675
2690
  """show the skeletonize dialog"""
2676
2691
  dialog = SkeletonizeDialog(self)
@@ -3066,6 +3081,7 @@ class ImageViewerWindow(QMainWindow):
3066
3081
  def set_active_channel(self, index):
3067
3082
  """Set the active channel and update UI accordingly."""
3068
3083
  self.active_channel = index
3084
+ self.active_channel_combo.setCurrentIndex(index)
3069
3085
  # Update button appearances to show active channel
3070
3086
  for i, btn in enumerate(self.channel_buttons):
3071
3087
  if i == index and btn.isEnabled():
@@ -3213,6 +3229,9 @@ class ImageViewerWindow(QMainWindow):
3213
3229
  self.highlight_overlay = None
3214
3230
  except:
3215
3231
  pass
3232
+ if not data:
3233
+ self.original_xlim = None
3234
+ self.original_ylim = None
3216
3235
  continue
3217
3236
  else:
3218
3237
  old_shape = self.channel_data[i].shape[:3] #Ask user to resize images that are shaped differently
@@ -3972,7 +3991,7 @@ class CustomTableView(QTableView):
3972
3991
  desc_action.triggered.connect(lambda checked, c=col: self.sort_table(c, ascending=False))
3973
3992
 
3974
3993
  # Different menus for top and bottom tables
3975
- if self == self.parent.data_table: # Top table
3994
+ if self in self.parent.data_table: # Top table
3976
3995
  save_menu = context_menu.addMenu("Save As")
3977
3996
  save_csv = save_menu.addAction("CSV")
3978
3997
  save_excel = save_menu.addAction("Excel")
@@ -4081,7 +4100,7 @@ class CustomTableView(QTableView):
4081
4100
  df = self.model()._data
4082
4101
 
4083
4102
  # Get table name for the file dialog title
4084
- if self == self.parent.data_table:
4103
+ if self in self.parent.data_table:
4085
4104
  table_name = "Statistics"
4086
4105
  elif self == self.parent.network_table:
4087
4106
  table_name = "Network"
@@ -4090,7 +4109,7 @@ class CustomTableView(QTableView):
4090
4109
 
4091
4110
  # Get save file name
4092
4111
  file_filter = ("CSV Files (*.csv)" if file_type == 'csv' else
4093
- "Excel Files (*.xlsx)" if file_type == 'excel' else
4112
+ "Excel Files (*.xlsx)" if file_type == 'xlsx' else
4094
4113
  "Gephi Graph (*.gexf)" if file_type == 'gexf' else
4095
4114
  "GraphML (*.graphml)" if file_type == 'graphml' else
4096
4115
  "Pajek Network (*.net)")
@@ -4475,7 +4494,13 @@ class TabbedDataWidget(QTabWidget):
4475
4494
  """Add a new table with the given name"""
4476
4495
  if name in self.tables:
4477
4496
  # If tab already exists, update its content
4478
- idx = self.indexOf(self.tables[name])
4497
+ old_table = self.tables[name]
4498
+ idx = self.indexOf(old_table)
4499
+
4500
+ # Remove the old table reference from parent's data_table
4501
+ if self.parent_window and old_table in self.parent_window.data_table:
4502
+ self.parent_window.data_table.remove(old_table)
4503
+
4479
4504
  self.removeTab(idx)
4480
4505
 
4481
4506
  # Create a new CustomTableView with is_top_table=True
@@ -4495,7 +4520,7 @@ class TabbedDataWidget(QTabWidget):
4495
4520
 
4496
4521
  # Update parent's data_table reference
4497
4522
  if self.parent_window:
4498
- self.parent_window.data_table = new_table
4523
+ self.parent_window.data_table.append(new_table)
4499
4524
 
4500
4525
  def close_tab(self, index):
4501
4526
  """Close the tab at the given index"""
@@ -4510,12 +4535,12 @@ class TabbedDataWidget(QTabWidget):
4510
4535
  if name_to_remove:
4511
4536
  del self.tables[name_to_remove]
4512
4537
 
4513
- self.removeTab(index)
4514
-
4515
- # Update parent's data_table reference to current table
4516
- if self.parent_window and self.count() > 0:
4517
- self.parent_window.data_table = self.currentWidget()
4538
+ # Update parent's data_table reference by removing the widget
4539
+ if self.parent_window and widget in self.parent_window.data_table:
4540
+ self.parent_window.data_table.remove(widget)
4518
4541
 
4542
+ self.removeTab(index)
4543
+
4519
4544
  def clear_all_tabs(self):
4520
4545
  """Remove all tabs"""
4521
4546
  while self.count() > 0:
@@ -5418,18 +5443,23 @@ class RadialDialog(QDialog):
5418
5443
 
5419
5444
  def radial(self):
5420
5445
 
5421
- distance = float(self.distance.text()) if self.distance.text().strip() else 50
5446
+ try:
5422
5447
 
5423
- directory = str(self.distance.text()) if self.directory.text().strip() else None
5448
+ distance = float(self.distance.text()) if self.distance.text().strip() else 50
5424
5449
 
5425
- if my_network.node_centroids is None:
5426
- self.parent().show_centroid_dialog()
5450
+ directory = str(self.distance.text()) if self.directory.text().strip() else None
5427
5451
 
5428
- radial = my_network.radial_distribution(distance, directory = directory)
5452
+ if my_network.node_centroids is None:
5453
+ self.parent().show_centroid_dialog()
5429
5454
 
5430
- self.parent().format_for_upperright_table(radial, 'Radial Distance From Any Node', 'Average Number of Neighboring Nodes', title = 'Radial Distribution Analysis')
5455
+ radial = my_network.radial_distribution(distance, directory = directory)
5431
5456
 
5432
- self.accept()
5457
+ self.parent().format_for_upperright_table(radial, 'Radial Distance From Any Node', 'Average Number of Neighboring Nodes', title = 'Radial Distribution Analysis')
5458
+
5459
+ self.accept()
5460
+
5461
+ except Exception as e:
5462
+ print(f"An error occurred: {e}")
5433
5463
 
5434
5464
  class DegreeDistDialog(QDialog):
5435
5465
 
@@ -5462,7 +5492,7 @@ class DegreeDistDialog(QDialog):
5462
5492
 
5463
5493
  self.accept()
5464
5494
 
5465
- except Excpetion as e:
5495
+ except Exception as e:
5466
5496
  print(f"An error occurred: {e}")
5467
5497
 
5468
5498
  class NeighborIdentityDialog(QDialog):
@@ -6261,6 +6291,91 @@ class LabelDialog(QDialog):
6261
6291
  f"Error running label: {str(e)}"
6262
6292
  )
6263
6293
 
6294
+
6295
+ class SLabelDialog(QDialog):
6296
+ def __init__(self, parent=None):
6297
+ super().__init__(parent)
6298
+ self.setWindowTitle("Smart Label (Use label array to assign label neighborhoods to binary array)?")
6299
+ self.setModal(True)
6300
+
6301
+ layout = QFormLayout(self)
6302
+
6303
+
6304
+ # Add mode selection dropdown
6305
+ self.mode_selector = QComboBox()
6306
+ self.mode_selector.addItems(["Nodes", "Edges", "Overlay 1", "Overlay 2"])
6307
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
6308
+ layout.addRow("Prelabeled Array:", self.mode_selector)
6309
+
6310
+ layout.addRow(QLabel("Will Label Neighborhoods in: "))
6311
+
6312
+ # Add mode selection dropdown
6313
+ self.target_selector = QComboBox()
6314
+ self.target_selector.addItems(["Nodes", "Edges", "Overlay 1", "Overlay 2"])
6315
+ self.target_selector.setCurrentIndex(1) # Default to Mode 1
6316
+ layout.addRow("Binary Array:", self.target_selector)
6317
+
6318
+ # GPU checkbox (default True)
6319
+ self.GPU = QPushButton("GPU")
6320
+ self.GPU.setCheckable(True)
6321
+ self.GPU.setChecked(True)
6322
+ layout.addRow("Use GPU:", self.GPU)
6323
+
6324
+ self.down_factor = QLineEdit("")
6325
+ layout.addRow("Internal Downsample for GPU (if needed):", self.down_factor)
6326
+
6327
+ # Add Run button
6328
+ run_button = QPushButton("Run Smart Label")
6329
+ run_button.clicked.connect(self.run_slabel)
6330
+ layout.addRow(run_button)
6331
+
6332
+ def run_slabel(self):
6333
+
6334
+ try:
6335
+
6336
+ accepted_source = self.mode_selector.currentIndex()
6337
+ accepted_target = self.target_selector.currentIndex()
6338
+ GPU = self.GPU.isChecked()
6339
+
6340
+
6341
+ if accepted_source == accepted_target:
6342
+ return
6343
+
6344
+ binary_array = self.parent().channel_data[accepted_target]
6345
+
6346
+ label_array = self.parent().channel_data[accepted_source]
6347
+
6348
+ down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
6349
+
6350
+
6351
+ try:
6352
+
6353
+ # Update both the display data and the network object
6354
+ binary_array = sdl.smart_label(binary_array, label_array, directory = None, GPU = GPU, predownsample = down_factor)
6355
+
6356
+ label_array = sdl.invert_array(label_array)
6357
+
6358
+ binary_array = binary_array * label_array
6359
+
6360
+ self.parent().load_channel(accepted_target, binary_array, True)
6361
+
6362
+ self.accept()
6363
+
6364
+ except Exception as e:
6365
+ QMessageBox.critical(
6366
+ self,
6367
+ "Error",
6368
+ f"Error running smart label: {str(e)}"
6369
+ )
6370
+
6371
+ except Exception as e:
6372
+ QMessageBox.critical(
6373
+ self,
6374
+ "Error",
6375
+ f"Error running smart label: {str(e)}"
6376
+ )
6377
+
6378
+
6264
6379
  class ThresholdDialog(QDialog):
6265
6380
  def __init__(self, parent=None):
6266
6381
  super().__init__(parent)
@@ -6380,9 +6495,8 @@ class MachineWindow(QMainWindow):
6380
6495
  act_channel = 1
6381
6496
 
6382
6497
 
6383
- array1 = np.zeros_like(active_data)
6384
- array2 = np.zeros_like(active_data)
6385
- array3 = np.zeros_like(active_data)
6498
+ array1 = np.zeros_like(active_data).astype(np.uint8)
6499
+ array3 = np.zeros_like(active_data).astype(np.uint8)
6386
6500
  self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
6387
6501
 
6388
6502
  self.parent().load_channel(2, array1, True)
@@ -6460,7 +6574,7 @@ class MachineWindow(QMainWindow):
6460
6574
 
6461
6575
  # Group 3: Training Options
6462
6576
  training_group = QGroupBox("Training")
6463
- training_layout = QVBoxLayout()
6577
+ training_layout = QHBoxLayout()
6464
6578
  train_quick = QPushButton("Train Quick Model")
6465
6579
  train_quick.clicked.connect(lambda: self.train_model(speed=True))
6466
6580
  train_detailed = QPushButton("Train More Detailed Model")
@@ -6471,11 +6585,13 @@ class MachineWindow(QMainWindow):
6471
6585
 
6472
6586
  # Group 4: Segmentation Options
6473
6587
  segmentation_group = QGroupBox("Segmentation")
6474
- segmentation_layout = QVBoxLayout()
6588
+ segmentation_layout = QHBoxLayout()
6475
6589
  seg_button = QPushButton("Preview Segment")
6476
6590
  self.seg_button = seg_button
6477
6591
  seg_button.clicked.connect(self.start_segmentation)
6478
- self.lock_button = QPushButton("🔒 Memory lock - (Prioritize RAM. Recommended unless you have a lot)")
6592
+ self.pause_button = QPushButton("▶/⏸️")
6593
+ self.pause_button.clicked.connect(self.pause)
6594
+ self.lock_button = QPushButton("🔒 Memory lock - (Prioritize RAM)")
6479
6595
  self.lock_button.setCheckable(True)
6480
6596
  self.lock_button.setChecked(True)
6481
6597
  self.lock_button.clicked.connect(self.toggle_lock)
@@ -6483,6 +6599,7 @@ class MachineWindow(QMainWindow):
6483
6599
  full_button = QPushButton("Segment All")
6484
6600
  full_button.clicked.connect(self.segment)
6485
6601
  segmentation_layout.addWidget(seg_button)
6602
+ #segmentation_layout.addWidget(self.pause_button) # <--- for some reason the segmenter preview is still running even when killed, may be regenerating itself somewhere. May or may not actually try to resolve this because this feature isnt that necessary.
6486
6603
  segmentation_layout.addWidget(self.lock_button)
6487
6604
  segmentation_layout.addWidget(full_button)
6488
6605
  segmentation_group.setLayout(segmentation_layout)
@@ -6507,6 +6624,27 @@ class MachineWindow(QMainWindow):
6507
6624
 
6508
6625
  self.mem_lock = self.lock_button.isChecked()
6509
6626
 
6627
+ def pause(self):
6628
+
6629
+ if self.segmentation_worker is not None:
6630
+ try:
6631
+ print("Pausing segmenter")
6632
+ self.previewing = False
6633
+ self.segmentation_finished
6634
+ del self.segmentation_worker
6635
+ self.segmentation_worker = None
6636
+ except:
6637
+ pass
6638
+
6639
+ else:
6640
+ try:
6641
+ print("Restarting segmenter")
6642
+ self.previewing = True
6643
+ self.start_segmentation
6644
+ except:
6645
+ pass
6646
+
6647
+
6510
6648
 
6511
6649
  def toggle_GPU(self):
6512
6650
 
@@ -6615,7 +6753,7 @@ class MachineWindow(QMainWindow):
6615
6753
  else:
6616
6754
  active_data = self.parent().channel_data[1]
6617
6755
 
6618
- array3 = np.zeros_like(active_data)
6756
+ array3 = np.zeros_like(active_data).astype(np.uint8)
6619
6757
  self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
6620
6758
 
6621
6759
  if not self.trained:
@@ -6801,7 +6939,7 @@ class MachineWindow(QMainWindow):
6801
6939
  else:
6802
6940
  active_data = self.parent().channel_data[1]
6803
6941
 
6804
- array3 = np.zeros_like(active_data)
6942
+ array3 = np.zeros_like(active_data).astype(np.uint8)
6805
6943
  self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
6806
6944
 
6807
6945
  print("Segmenting entire volume with model...")
@@ -7589,6 +7727,65 @@ class MaskDialog(QDialog):
7589
7727
  print(f"Error masking: {e}")
7590
7728
 
7591
7729
 
7730
+ class TypeDialog(QDialog):
7731
+
7732
+ def __init__(self, parent=None):
7733
+
7734
+ super().__init__(parent)
7735
+ self.setWindowTitle("Active Channel dtype")
7736
+ self.setModal(True)
7737
+
7738
+ layout = QFormLayout(self)
7739
+
7740
+ self.active_chan = self.parent().active_channel
7741
+
7742
+ active_data = self.parent().channel_data[self.active_chan]
7743
+
7744
+ layout.addRow("Info:", QLabel(f"Active dtype (Channel {self.active_chan}): {active_data.dtype}"))
7745
+
7746
+ # Add mode selection dropdown
7747
+ self.mode_selector = QComboBox()
7748
+ self.mode_selector.addItems(["8bit int", "16bit int", "32bit int", "32bit float", "64bit float"])
7749
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
7750
+ layout.addRow("Change to?:", self.mode_selector)
7751
+
7752
+ # Add Run button
7753
+ run_button = QPushButton("Run")
7754
+ run_button.clicked.connect(lambda: self.run_type(active_data))
7755
+ layout.addRow(run_button)
7756
+
7757
+ def run_type(self, active_data):
7758
+
7759
+ mode = self.mode_selector.currentIndex()
7760
+
7761
+ if mode == 0:
7762
+
7763
+ active_data = active_data.astype(np.uint8)
7764
+
7765
+ elif mode == 1:
7766
+
7767
+ active_data = active_data.astype(np.uint16)
7768
+
7769
+ elif mode == 2:
7770
+
7771
+ active_data = active_data.astype(np.uint32)
7772
+
7773
+ elif mode == 3:
7774
+
7775
+ active_data = active_data.astype(np.float32)
7776
+
7777
+ elif mode == 4:
7778
+
7779
+ active_data = active_data.astype(np.float64)
7780
+
7781
+ self.parent().load_channel(self.active_chan, active_data, True)
7782
+
7783
+
7784
+ print(f"Channel {self.active_chan}) dtype now: {self.parent().channel_data[self.active_chan].dtype}")
7785
+ self.accept()
7786
+
7787
+
7788
+
7592
7789
 
7593
7790
  class SkeletonizeDialog(QDialog):
7594
7791
  def __init__(self, parent=None):
@@ -7601,6 +7798,12 @@ class SkeletonizeDialog(QDialog):
7601
7798
  self.remove = QLineEdit("0")
7602
7799
  layout.addRow("Remove Branches Pixel Length (int):", self.remove)
7603
7800
 
7801
+ # auto checkbox (default True)
7802
+ self.auto = QPushButton("Auto")
7803
+ self.auto.setCheckable(True)
7804
+ self.auto.setChecked(False)
7805
+ layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
7806
+
7604
7807
  # Add Run button
7605
7808
  run_button = QPushButton("Run Skeletonize")
7606
7809
  run_button.clicked.connect(self.run_skeletonize)
@@ -7614,11 +7817,17 @@ class SkeletonizeDialog(QDialog):
7614
7817
  remove = int(self.remove.text()) if self.remove.text() else 0
7615
7818
  except ValueError:
7616
7819
  remove = 0
7820
+
7821
+ auto = self.auto.isChecked()
7617
7822
 
7618
7823
  # Get the active channel data from parent
7619
7824
  active_data = self.parent().channel_data[self.parent().active_channel]
7620
7825
  if active_data is None:
7621
7826
  raise ValueError("No active image selected")
7827
+
7828
+ if auto:
7829
+ active_data = n3d.skeletonize(active_data)
7830
+ active_data = n3d.fill_holes_3d(active_data)
7622
7831
 
7623
7832
  # Call dilate method with parameters
7624
7833
  result = n3d.skeletonize(
@@ -7930,11 +8139,17 @@ class GenNodesDialog(QDialog):
7930
8139
  self.branch_removal = QLineEdit("0")
7931
8140
  layout.addRow("Skeleton Voxel Branch Length to Remove (int) (Compensates for spines off medial axis):", self.branch_removal)
7932
8141
 
8142
+ self.comp_dil = QLineEdit("0")
8143
+ layout.addRow("Voxel distance to merge nearby nodes (Int - compensates for multi-branch identification along thick branch regions):", self.comp_dil)
8144
+
7933
8145
  self.max_vol = QLineEdit("0")
7934
8146
  layout.addRow("Maximum Voxel Volume of Vertices to Retain (int - Compensates for skeleton looping - occurs before any node merging - the smallest objects are always 27 voxels):", self.max_vol)
7935
8147
 
7936
- self.comp_dil = QLineEdit("0")
7937
- layout.addRow("Voxel distance to merge nearby nodes (Int - compensates for multi-branch identification along thick branch regions):", self.comp_dil)
8148
+ # auto checkbox (default True)
8149
+ self.auto = QPushButton("Auto")
8150
+ self.auto.setCheckable(True)
8151
+ self.auto.setChecked(False)
8152
+ layout.addRow("Attempt to Auto Correct Skeleton Looping:", self.auto)
7938
8153
 
7939
8154
  if not down_factor:
7940
8155
  down_factor = None
@@ -8012,6 +8227,13 @@ class GenNodesDialog(QDialog):
8012
8227
  else:
8013
8228
  order = 0
8014
8229
 
8230
+ auto = self.auto.isChecked()
8231
+
8232
+
8233
+ if auto:
8234
+ my_network.edges = n3d.skeletonize(my_network.edges)
8235
+ my_network.edges = n3d.fill_holes_3d(my_network.edges)
8236
+
8015
8237
 
8016
8238
  result, skele = n3d.label_vertices(
8017
8239
  my_network.edges,
@@ -8066,6 +8288,7 @@ class GenNodesDialog(QDialog):
8066
8288
  )
8067
8289
 
8068
8290
 
8291
+
8069
8292
  class BranchDialog(QDialog):
8070
8293
 
8071
8294
  def __init__(self, parent=None):
@@ -8093,8 +8316,7 @@ class BranchDialog(QDialog):
8093
8316
  self.fix.setChecked(False)
8094
8317
  layout.addRow("Attempt to auto-correct branch labels:", self.fix)
8095
8318
 
8096
- self.fix_val = QLineEdit()
8097
- self.fix_val.setPlaceholderText("Empty = default value...")
8319
+ self.fix_val = QLineEdit('4')
8098
8320
  layout.addRow("If checked above - Avg Degree of Nearby Branch Communities to Merge (Attempt to fix branch labeling - try 4 to 6 to start or leave empty):", self.fix_val)
8099
8321
 
8100
8322
  self.down_factor = QLineEdit("0")
@@ -1024,11 +1024,8 @@ def histogram(counts, y_vals, directory = None):
1024
1024
  plt.ylabel('Avg Number of Neigbhoring Vertices')
1025
1025
 
1026
1026
  try:
1027
-
1028
1027
  if directory is not None:
1029
1028
  plt.savefig(f'{directory}/radial_plot.png')
1030
- else:
1031
- plt.savefig('radial_plot.png')
1032
1029
  except:
1033
1030
  pass
1034
1031
 
@@ -1439,6 +1436,7 @@ def degree_distribution(G, directory = None):
1439
1436
 
1440
1437
  def power_trendline(x, y, directory = None):
1441
1438
  # Handle zeros in y for logarithmic transformations
1439
+ """
1442
1440
  y = np.array(y)
1443
1441
  x = np.array(x)
1444
1442
  y[y == 0] += 0.001
@@ -1460,6 +1458,8 @@ def power_trendline(x, y, directory = None):
1460
1458
  ss_res = np.sum((y - y_pred) ** 2)
1461
1459
  ss_tot = np.sum((y - np.mean(y)) ** 2)
1462
1460
  r2 = 1 - (ss_res / ss_tot)
1461
+ """
1462
+ # ^ I commented out this power trendline stuff because I decided I no longer want it to do that so.
1463
1463
 
1464
1464
  # Create a scatterplot
1465
1465
  plt.scatter(x, y, label='Data')
@@ -1468,9 +1468,10 @@ def power_trendline(x, y, directory = None):
1468
1468
  plt.title('Degree Distribution of Network')
1469
1469
 
1470
1470
  # Plot the power trendline
1471
- plt.plot(x_fit, y_fit, color='red', label=f'Power Trendline: $y = {a:.2f}x^{{{b:.2f}}}$')
1471
+ #plt.plot(x_fit, y_fit, color='red', label=f'Power Trendline: $y = {a:.2f}x^{{{b:.2f}}}$')
1472
1472
 
1473
1473
  # Annotate the plot with the trendline equation and R-squared value
1474
+ """
1474
1475
  plt.text(
1475
1476
  0.05, 0.95,
1476
1477
  f'$y = {a:.2f}x^{{{b:.2f}}}$\n$R^2 = {r2:.2f}$',
@@ -1478,13 +1479,12 @@ def power_trendline(x, y, directory = None):
1478
1479
  fontsize=12,
1479
1480
  verticalalignment='top'
1480
1481
  )
1482
+ """
1481
1483
 
1482
1484
  try:
1483
1485
 
1484
1486
  if directory is not None:
1485
1487
  plt.savefig(f'{directory}/degree_plot.png')
1486
- else:
1487
- plt.savefig('degree_plot.png')
1488
1488
  except:
1489
1489
  pass
1490
1490
 
@@ -1289,6 +1289,9 @@ class InteractiveSegmenter:
1289
1289
  """Segment volume using parallel processing of chunks with vectorized chunk creation"""
1290
1290
  #Change the above chunk size to None to have it auto-compute largest chunks (not sure which is faster, 64 seems reasonable in test cases)
1291
1291
 
1292
+ self.realtimechunks = None # Presumably no longer need this.
1293
+ self.map_slice = None
1294
+
1292
1295
  if self.mem_lock:
1293
1296
  chunk_size = 32 #memory efficient chunk
1294
1297
 
@@ -1302,7 +1305,7 @@ class InteractiveSegmenter:
1302
1305
  Returns:
1303
1306
  List of chunks, where each chunk contains the coordinates for one z-slice or subchunk
1304
1307
  """
1305
- MAX_CHUNK_SIZE = 32768000
1308
+ MAX_CHUNK_SIZE = 32768
1306
1309
  chunks = []
1307
1310
 
1308
1311
  for z in range(self.image_3d.shape[0]):
@@ -1690,6 +1693,7 @@ class InteractiveSegmenter:
1690
1693
  def train_batch(self, foreground_array, speed = True, use_gpu = False, use_two = False, mem_lock = False):
1691
1694
  """Train directly on foreground and background arrays"""
1692
1695
 
1696
+ print("Training model...")
1693
1697
  self.speed = speed
1694
1698
  self.cur_gpu = use_gpu
1695
1699
  if mem_lock != self.mem_lock:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
5
5
  Author-email: Liam McLaughlin <mclaughlinliam99@gmail.com>
6
6
  Project-URL: User_Tutorial, https://www.youtube.com/watch?v=cRatn5VTWDY
@@ -33,6 +33,7 @@ Provides-Extra: cuda12
33
33
  Requires-Dist: cupy-cuda12x; extra == "cuda12"
34
34
  Provides-Extra: cupy
35
35
  Requires-Dist: cupy; extra == "cupy"
36
+ Dynamic: license-file
36
37
 
37
38
  NetTracer3D is a python package developed for both 2D and 3D analysis of microscopic images in the .tif file format. It supports generation of 3D networks showing the relationships between objects (or nodes) in three dimensional space, either based on their own proximity or connectivity via connecting objects such as nerves or blood vessels. In addition to these functionalities are several advanced 3D data processing algorithms, such as labeling of branched structures or abstraction of branched structures into networks. Note that nettracer3d uses segmented data, which can be segmented from other softwares such as ImageJ and imported into NetTracer3D, although it does offer its own segmentation via intensity and volumetric thresholding, or random forest machine learning segmentation. NetTracer3D currently has a fully functional GUI. To use the GUI, after installing the nettracer3d package via pip, enter the command 'nettracer3d' in your command prompt:
38
39
 
@@ -44,8 +45,15 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
44
45
 
45
46
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
46
47
 
47
- -- Version 0.6.2 updates --
48
+ -- Version 0.6.4 updates --
48
49
 
49
- 1. Fixed bug with performing 2D distance transforms on CPU
50
+ 1. Fixed bug with tabulated data in top right having a right click window corresponding to the bottom left.
50
51
 
51
- 2. Updated ram_lock mode in the segmenter to use smaller chunks and garbage collect better.
52
+ 2. Fixed bug when converting communities to nodes that let the same community connect to itself in the new network.
53
+
54
+ 3. Removed attempted trendline fitting from degree distribution
55
+
56
+ 4. Added new feature to skeletonization (and corresponding branch labeler/gennodes)
57
+ Now you can have the program attempt to auto-correct 3D skeletonization loop artifacts through a method that just runs the 3d fill holes algo and then attempts to reskeletonize the output. This worked well in my own testing.
58
+
59
+ 5. Other minor fixes/improvements
File without changes
File without changes