nettracer3d 0.6.2__tar.gz → 0.6.3__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.3}/PKG-INFO +8 -5
  2. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/README.md +5 -3
  3. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/pyproject.toml +1 -1
  4. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d/nettracer_gui.py +190 -8
  5. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d/segmenter.py +5 -1
  6. {nettracer3d-0.6.2 → nettracer3d-0.6.3/src/nettracer3d.egg-info}/PKG-INFO +8 -5
  7. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/LICENSE +0 -0
  8. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/setup.cfg +0 -0
  9. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d/__init__.py +0 -0
  10. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d/community_extractor.py +0 -0
  11. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d/modularity.py +0 -0
  12. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d/morphology.py +0 -0
  13. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d/nettracer.py +0 -0
  14. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d/network_analysis.py +0 -0
  15. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d/network_draw.py +0 -0
  16. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d/node_draw.py +0 -0
  17. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d/proximity.py +0 -0
  18. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d/run.py +0 -0
  19. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d/simple_network.py +0 -0
  20. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d/smart_dilate.py +0 -0
  21. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
  22. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
  23. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d.egg-info/entry_points.txt +0 -0
  24. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/src/nettracer3d.egg-info/requires.txt +0 -0
  25. {nettracer3d-0.6.2 → nettracer3d-0.6.3}/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.3
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,10 @@ 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.3 updates --
48
49
 
49
- 1. Fixed bug with performing 2D distance transforms on CPU
50
+ 1. Fixed bug with the active channel indicator on the GUI not always updating when the active channel was changed by internal loading.
50
51
 
51
- 2. Updated ram_lock mode in the segmenter to use smaller chunks and garbage collect better.
52
+ 2. Updated ram_lock mode in the segmenter to garbage collect better... again.
53
+
54
+ 3. Added new function (Label neighborhoods). Find it in process -> image -> label neighborhoods. Allows a 3d labeled array to act as a kernel and extend its labels along a secondary binary array. This can be useful in evaluating nearest neighbors en-masse. The method behind this (smart label) was previously used internally for some methods (ie watershedding) but I thought I'd include it as an actual function since it has some more general uses.
@@ -8,8 +8,10 @@ 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.3 updates --
12
12
 
13
- 1. Fixed bug with performing 2D distance transforms on CPU
13
+ 1. Fixed bug with the active channel indicator on the GUI not always updating when the active channel was changed by internal loading.
14
14
 
15
- 2. Updated ram_lock mode in the segmenter to use smaller chunks and garbage collect better.
15
+ 2. Updated ram_lock mode in the segmenter to garbage collect better... again.
16
+
17
+ 3. Added new function (Label neighborhoods). Find it in process -> image -> label neighborhoods. Allows a 3d labeled array to act as a kernel and extend its labels along a secondary binary array. This can be useful in evaluating nearest neighbors en-masse. The method behind this (smart label) was previously used internally for some methods (ie watershedding) but I thought I'd include it as an actual function since it has some more general uses.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nettracer3d"
3
- version = "0.6.2"
3
+ version = "0.6.3"
4
4
  authors = [
5
5
  { name="Liam McLaughlin", email="mclaughlinliam99@gmail.com" },
6
6
  ]
@@ -2429,10 +2429,14 @@ class ImageViewerWindow(QMainWindow):
2429
2429
  binarize_action.triggered.connect(self.show_binarize_dialog)
2430
2430
  label_action = image_menu.addAction("Label Objects")
2431
2431
  label_action.triggered.connect(self.show_label_dialog)
2432
+ slabel_action = image_menu.addAction("Neighborhood Labels")
2433
+ slabel_action.triggered.connect(self.show_slabel_dialog)
2432
2434
  thresh_action = image_menu.addAction("Threshold/Segment")
2433
2435
  thresh_action.triggered.connect(self.show_thresh_dialog)
2434
2436
  mask_action = image_menu.addAction("Mask Channel")
2435
2437
  mask_action.triggered.connect(self.show_mask_dialog)
2438
+ type_action = image_menu.addAction("Channel dtype")
2439
+ type_action.triggered.connect(self.show_type_dialog)
2436
2440
  skeletonize_action = image_menu.addAction("Skeletonize")
2437
2441
  skeletonize_action.triggered.connect(self.show_skeletonize_dialog)
2438
2442
  watershed_action = image_menu.addAction("Watershed")
@@ -2657,6 +2661,11 @@ class ImageViewerWindow(QMainWindow):
2657
2661
  dialog = LabelDialog(self)
2658
2662
  dialog.exec()
2659
2663
 
2664
+ def show_slabel_dialog(self):
2665
+ """Show the slabel dialog"""
2666
+ dialog = SLabelDialog(self)
2667
+ dialog.exec()
2668
+
2660
2669
  def show_thresh_dialog(self):
2661
2670
  """Show threshold dialog"""
2662
2671
  if self.machine_window is not None:
@@ -2671,6 +2680,11 @@ class ImageViewerWindow(QMainWindow):
2671
2680
  dialog = MaskDialog(self)
2672
2681
  dialog.exec()
2673
2682
 
2683
+ def show_type_dialog(self):
2684
+ """Show the type dialog"""
2685
+ dialog = TypeDialog(self)
2686
+ dialog.exec()
2687
+
2674
2688
  def show_skeletonize_dialog(self):
2675
2689
  """show the skeletonize dialog"""
2676
2690
  dialog = SkeletonizeDialog(self)
@@ -3066,6 +3080,7 @@ class ImageViewerWindow(QMainWindow):
3066
3080
  def set_active_channel(self, index):
3067
3081
  """Set the active channel and update UI accordingly."""
3068
3082
  self.active_channel = index
3083
+ self.active_channel_combo.setCurrentIndex(index)
3069
3084
  # Update button appearances to show active channel
3070
3085
  for i, btn in enumerate(self.channel_buttons):
3071
3086
  if i == index and btn.isEnabled():
@@ -6261,6 +6276,91 @@ class LabelDialog(QDialog):
6261
6276
  f"Error running label: {str(e)}"
6262
6277
  )
6263
6278
 
6279
+
6280
+ class SLabelDialog(QDialog):
6281
+ def __init__(self, parent=None):
6282
+ super().__init__(parent)
6283
+ self.setWindowTitle("Smart Label (Use label array to assign label neighborhoods to binary array)?")
6284
+ self.setModal(True)
6285
+
6286
+ layout = QFormLayout(self)
6287
+
6288
+
6289
+ # Add mode selection dropdown
6290
+ self.mode_selector = QComboBox()
6291
+ self.mode_selector.addItems(["Nodes", "Edges", "Overlay 1", "Overlay 2"])
6292
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
6293
+ layout.addRow("Prelabeled Array:", self.mode_selector)
6294
+
6295
+ layout.addRow(QLabel("Will Label Neighborhoods in: "))
6296
+
6297
+ # Add mode selection dropdown
6298
+ self.target_selector = QComboBox()
6299
+ self.target_selector.addItems(["Nodes", "Edges", "Overlay 1", "Overlay 2"])
6300
+ self.target_selector.setCurrentIndex(1) # Default to Mode 1
6301
+ layout.addRow("Binary Array:", self.target_selector)
6302
+
6303
+ # GPU checkbox (default True)
6304
+ self.GPU = QPushButton("GPU")
6305
+ self.GPU.setCheckable(True)
6306
+ self.GPU.setChecked(True)
6307
+ layout.addRow("Use GPU:", self.GPU)
6308
+
6309
+ self.down_factor = QLineEdit("")
6310
+ layout.addRow("Internal Downsample for GPU (if needed):", self.down_factor)
6311
+
6312
+ # Add Run button
6313
+ run_button = QPushButton("Run Smart Label")
6314
+ run_button.clicked.connect(self.run_slabel)
6315
+ layout.addRow(run_button)
6316
+
6317
+ def run_slabel(self):
6318
+
6319
+ try:
6320
+
6321
+ accepted_source = self.mode_selector.currentIndex()
6322
+ accepted_target = self.target_selector.currentIndex()
6323
+ GPU = self.GPU.isChecked()
6324
+
6325
+
6326
+ if accepted_source == accepted_target:
6327
+ return
6328
+
6329
+ binary_array = self.parent().channel_data[accepted_target]
6330
+
6331
+ label_array = self.parent().channel_data[accepted_source]
6332
+
6333
+ down_factor = float(self.down_factor.text()) if self.down_factor.text().strip() else None
6334
+
6335
+
6336
+ try:
6337
+
6338
+ # Update both the display data and the network object
6339
+ binary_array = sdl.smart_label(binary_array, label_array, directory = None, GPU = GPU, predownsample = down_factor)
6340
+
6341
+ label_array = sdl.invert_array(label_array)
6342
+
6343
+ binary_array = binary_array * label_array
6344
+
6345
+ self.parent().load_channel(accepted_target, binary_array, True)
6346
+
6347
+ self.accept()
6348
+
6349
+ except Exception as e:
6350
+ QMessageBox.critical(
6351
+ self,
6352
+ "Error",
6353
+ f"Error running smart label: {str(e)}"
6354
+ )
6355
+
6356
+ except Exception as e:
6357
+ QMessageBox.critical(
6358
+ self,
6359
+ "Error",
6360
+ f"Error running smart label: {str(e)}"
6361
+ )
6362
+
6363
+
6264
6364
  class ThresholdDialog(QDialog):
6265
6365
  def __init__(self, parent=None):
6266
6366
  super().__init__(parent)
@@ -6380,9 +6480,8 @@ class MachineWindow(QMainWindow):
6380
6480
  act_channel = 1
6381
6481
 
6382
6482
 
6383
- array1 = np.zeros_like(active_data)
6384
- array2 = np.zeros_like(active_data)
6385
- array3 = np.zeros_like(active_data)
6483
+ array1 = np.zeros_like(active_data).astype(np.uint8)
6484
+ array3 = np.zeros_like(active_data).astype(np.uint8)
6386
6485
  self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
6387
6486
 
6388
6487
  self.parent().load_channel(2, array1, True)
@@ -6460,7 +6559,7 @@ class MachineWindow(QMainWindow):
6460
6559
 
6461
6560
  # Group 3: Training Options
6462
6561
  training_group = QGroupBox("Training")
6463
- training_layout = QVBoxLayout()
6562
+ training_layout = QHBoxLayout()
6464
6563
  train_quick = QPushButton("Train Quick Model")
6465
6564
  train_quick.clicked.connect(lambda: self.train_model(speed=True))
6466
6565
  train_detailed = QPushButton("Train More Detailed Model")
@@ -6471,11 +6570,13 @@ class MachineWindow(QMainWindow):
6471
6570
 
6472
6571
  # Group 4: Segmentation Options
6473
6572
  segmentation_group = QGroupBox("Segmentation")
6474
- segmentation_layout = QVBoxLayout()
6573
+ segmentation_layout = QHBoxLayout()
6475
6574
  seg_button = QPushButton("Preview Segment")
6476
6575
  self.seg_button = seg_button
6477
6576
  seg_button.clicked.connect(self.start_segmentation)
6478
- self.lock_button = QPushButton("🔒 Memory lock - (Prioritize RAM. Recommended unless you have a lot)")
6577
+ self.pause_button = QPushButton("▶/⏸️")
6578
+ self.pause_button.clicked.connect(self.pause)
6579
+ self.lock_button = QPushButton("🔒 Memory lock - (Prioritize RAM)")
6479
6580
  self.lock_button.setCheckable(True)
6480
6581
  self.lock_button.setChecked(True)
6481
6582
  self.lock_button.clicked.connect(self.toggle_lock)
@@ -6483,6 +6584,7 @@ class MachineWindow(QMainWindow):
6483
6584
  full_button = QPushButton("Segment All")
6484
6585
  full_button.clicked.connect(self.segment)
6485
6586
  segmentation_layout.addWidget(seg_button)
6587
+ #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
6588
  segmentation_layout.addWidget(self.lock_button)
6487
6589
  segmentation_layout.addWidget(full_button)
6488
6590
  segmentation_group.setLayout(segmentation_layout)
@@ -6507,6 +6609,27 @@ class MachineWindow(QMainWindow):
6507
6609
 
6508
6610
  self.mem_lock = self.lock_button.isChecked()
6509
6611
 
6612
+ def pause(self):
6613
+
6614
+ if self.segmentation_worker is not None:
6615
+ try:
6616
+ print("Pausing segmenter")
6617
+ self.previewing = False
6618
+ self.segmentation_finished
6619
+ del self.segmentation_worker
6620
+ self.segmentation_worker = None
6621
+ except:
6622
+ pass
6623
+
6624
+ else:
6625
+ try:
6626
+ print("Restarting segmenter")
6627
+ self.previewing = True
6628
+ self.start_segmentation
6629
+ except:
6630
+ pass
6631
+
6632
+
6510
6633
 
6511
6634
  def toggle_GPU(self):
6512
6635
 
@@ -6615,7 +6738,7 @@ class MachineWindow(QMainWindow):
6615
6738
  else:
6616
6739
  active_data = self.parent().channel_data[1]
6617
6740
 
6618
- array3 = np.zeros_like(active_data)
6741
+ array3 = np.zeros_like(active_data).astype(np.uint8)
6619
6742
  self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
6620
6743
 
6621
6744
  if not self.trained:
@@ -6801,7 +6924,7 @@ class MachineWindow(QMainWindow):
6801
6924
  else:
6802
6925
  active_data = self.parent().channel_data[1]
6803
6926
 
6804
- array3 = np.zeros_like(active_data)
6927
+ array3 = np.zeros_like(active_data).astype(np.uint8)
6805
6928
  self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
6806
6929
 
6807
6930
  print("Segmenting entire volume with model...")
@@ -7589,6 +7712,65 @@ class MaskDialog(QDialog):
7589
7712
  print(f"Error masking: {e}")
7590
7713
 
7591
7714
 
7715
+ class TypeDialog(QDialog):
7716
+
7717
+ def __init__(self, parent=None):
7718
+
7719
+ super().__init__(parent)
7720
+ self.setWindowTitle("Active Channel dtype")
7721
+ self.setModal(True)
7722
+
7723
+ layout = QFormLayout(self)
7724
+
7725
+ self.active_chan = self.parent().active_channel
7726
+
7727
+ active_data = self.parent().channel_data[self.active_chan]
7728
+
7729
+ layout.addRow("Info:", QLabel(f"Active dtype (Channel {self.active_chan}): {active_data.dtype}"))
7730
+
7731
+ # Add mode selection dropdown
7732
+ self.mode_selector = QComboBox()
7733
+ self.mode_selector.addItems(["8bit int", "16bit int", "32bit int", "32bit float", "64bit float"])
7734
+ self.mode_selector.setCurrentIndex(0) # Default to Mode 1
7735
+ layout.addRow("Change to?:", self.mode_selector)
7736
+
7737
+ # Add Run button
7738
+ run_button = QPushButton("Run")
7739
+ run_button.clicked.connect(lambda: self.run_type(active_data))
7740
+ layout.addRow(run_button)
7741
+
7742
+ def run_type(self, active_data):
7743
+
7744
+ mode = self.mode_selector.currentIndex()
7745
+
7746
+ if mode == 0:
7747
+
7748
+ active_data = active_data.astype(np.uint8)
7749
+
7750
+ elif mode == 1:
7751
+
7752
+ active_data = active_data.astype(np.uint16)
7753
+
7754
+ elif mode == 2:
7755
+
7756
+ active_data = active_data.astype(np.uint32)
7757
+
7758
+ elif mode == 3:
7759
+
7760
+ active_data = active_data.astype(np.float32)
7761
+
7762
+ elif mode == 4:
7763
+
7764
+ active_data = active_data.astype(np.float64)
7765
+
7766
+ self.parent().load_channel(self.active_chan, active_data, True)
7767
+
7768
+
7769
+ print(f"Channel {self.active_chan}) dtype now: {self.parent().channel_data[self.active_chan].dtype}")
7770
+ self.accept()
7771
+
7772
+
7773
+
7592
7774
 
7593
7775
  class SkeletonizeDialog(QDialog):
7594
7776
  def __init__(self, parent=None):
@@ -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.3
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,10 @@ 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.3 updates --
48
49
 
49
- 1. Fixed bug with performing 2D distance transforms on CPU
50
+ 1. Fixed bug with the active channel indicator on the GUI not always updating when the active channel was changed by internal loading.
50
51
 
51
- 2. Updated ram_lock mode in the segmenter to use smaller chunks and garbage collect better.
52
+ 2. Updated ram_lock mode in the segmenter to garbage collect better... again.
53
+
54
+ 3. Added new function (Label neighborhoods). Find it in process -> image -> label neighborhoods. Allows a 3d labeled array to act as a kernel and extend its labels along a secondary binary array. This can be useful in evaluating nearest neighbors en-masse. The method behind this (smart label) was previously used internally for some methods (ie watershedding) but I thought I'd include it as an actual function since it has some more general uses.
File without changes
File without changes