nettracer3d 0.6.9__py3-none-any.whl → 0.7.1__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/morphology.py CHANGED
@@ -518,6 +518,7 @@ def compute_distance_transform_distance_GPU(nodes, sampling = [1,1,1]):
518
518
  is_pseudo_3d = nodes.shape[0] == 1
519
519
  if is_pseudo_3d:
520
520
  nodes = cp.squeeze(nodes) # Convert to 2D for processing
521
+ del sampling[0]
521
522
 
522
523
  # Compute the distance transform on the GPU
523
524
  distance = cpx.distance_transform_edt(nodes, sampling = sampling)
@@ -533,6 +534,7 @@ def compute_distance_transform_distance(nodes, sampling = [1,1,1]):
533
534
  is_pseudo_3d = nodes.shape[0] == 1
534
535
  if is_pseudo_3d:
535
536
  nodes = np.squeeze(nodes) # Convert to 2D for processing
537
+ del sampling[0]
536
538
 
537
539
  # Fallback to CPU if there's an issue with GPU computation
538
540
  distance = ndimage.distance_transform_edt(nodes, sampling = sampling)
nettracer3d/nettracer.py CHANGED
@@ -4431,6 +4431,101 @@ class Network_3D:
4431
4431
  return neighborhood_dict, proportion_dict, title1, title2, densities
4432
4432
 
4433
4433
 
4434
+ def get_ripley(self, root = None, targ = None, distance = 1, edgecorrect = True, bounds = None, ignore_dims = False, proportion = 0.5):
4435
+
4436
+
4437
+ if root is None or targ is None: #Self clustering in this case
4438
+ roots = self._node_centroids.values()
4439
+ targs = self._node_centroids.values()
4440
+ else:
4441
+ roots = []
4442
+ targs = []
4443
+
4444
+ for node, nodeid in self.node_identities.items(): #Otherwise we need to pull out this info
4445
+ if nodeid == root:
4446
+ roots.append(self._node_centroids[node])
4447
+ elif nodeid == targ:
4448
+ targs.append(self._node_centroids[node])
4449
+
4450
+ rooties = proximity.convert_centroids_to_array(roots, xy_scale = self.xy_scale, z_scale = self.z_scale)
4451
+ targs = proximity.convert_centroids_to_array(roots, xy_scale = self.xy_scale, z_scale = self.z_scale)
4452
+ points_array = np.vstack((rooties, targs))
4453
+ del rooties
4454
+
4455
+ try:
4456
+ if self.nodes.shape[0] == 1:
4457
+ dim = 2
4458
+ else:
4459
+ dim = 3
4460
+ except:
4461
+ dim = 2
4462
+ for centroid in self.node_centroids.values():
4463
+ if centroid[0] != 0:
4464
+ dim = 3
4465
+ break
4466
+
4467
+
4468
+ if ignore_dims:
4469
+
4470
+ factor = 0.25
4471
+
4472
+
4473
+ if bounds is None:
4474
+ if dim == 2:
4475
+ min_coords = np.array([0,0])
4476
+ else:
4477
+ min_coords = np.array([0,0,0])
4478
+ max_coords = np.max(points_array, axis=0)
4479
+ max_coords = np.flip(max_coords)
4480
+ bounds = (min_coords, max_coords)
4481
+ else:
4482
+ min_coords, max_coords = bounds
4483
+
4484
+ dim_list = max_coords - min_coords
4485
+
4486
+ new_list = []
4487
+
4488
+
4489
+ if dim == 3:
4490
+ for centroid in roots:
4491
+
4492
+ if ((centroid[2] - min_coords[0]) > dim_list[0] * factor) and ((max_coords[0] - centroid[2]) > dim_list[0] * factor) and ((centroid[1] - min_coords[1]) > dim_list[1] * factor) and ((max_coords[1] - centroid[1]) > dim_list[1] * factor) and ((centroid[0] - min_coords[2]) > dim_list[2] * factor) and ((max_coords[2] - centroid[0]) > dim_list[2] * factor):
4493
+ new_list.append(centroid)
4494
+ #print(f"dim_list: {dim_list}, centroid: {centroid}, min_coords: {min_coords}, max_coords: {max_coords}")
4495
+ else:
4496
+ for centroid in roots:
4497
+
4498
+ if ((centroid[2] - min_coords[0]) > dim_list[0] * factor) and ((max_coords[0] - centroid[2]) > dim_list[0] * factor) and ((centroid[1] - min_coords[1]) > dim_list[1] * factor) and ((max_coords[1] - centroid[1]) > dim_list[1] * factor):
4499
+ new_list.append(centroid)
4500
+
4501
+ roots = new_list
4502
+ print(f"Utilizing {len(roots)} root points. Note that low n values are unstable.")
4503
+ is_subset = True
4504
+ else:
4505
+ is_subset = False
4506
+
4507
+
4508
+
4509
+
4510
+ roots = proximity.convert_centroids_to_array(roots, xy_scale = self.xy_scale, z_scale = self.z_scale)
4511
+
4512
+
4513
+ if dim == 2:
4514
+ roots = proximity.convert_augmented_array_to_points(roots)
4515
+ targs = proximity.convert_augmented_array_to_points(targs)
4516
+
4517
+ r_vals = proximity.generate_r_values(points_array, distance, bounds = bounds, dim = dim, max_proportion=proportion)
4518
+
4519
+ k_vals = proximity.optimized_ripleys_k(roots, targs, r_vals, bounds=bounds, edge_correction=edgecorrect, dim = dim, is_subset = is_subset)
4520
+
4521
+ h_vals = proximity.compute_ripleys_h(k_vals, r_vals, dim)
4522
+
4523
+ proximity.plot_ripley_functions(r_vals, k_vals, h_vals, dim)
4524
+
4525
+ return r_vals, k_vals, h_vals
4526
+
4527
+
4528
+
4434
4529
 
4435
4530
  #Morphological stats or network linking:
4436
4531
 
@@ -4490,6 +4585,87 @@ class Network_3D:
4490
4585
 
4491
4586
  return array
4492
4587
 
4588
+
4589
+
4590
+ def random_nodes(self, bounds = None, mask = None):
4591
+
4592
+ if self.nodes is not None:
4593
+ try:
4594
+ self.nodes = np.zeros_like(self.nodes)
4595
+ except:
4596
+ pass
4597
+
4598
+
4599
+ if mask is not None:
4600
+ coords = np.argwhere(mask != 0)
4601
+ else:
4602
+ if bounds is not None:
4603
+ (z1, y1, x1), (z2, y2, x2) = bounds
4604
+ z1, y1, x1 = int(z1), int(y1), int(x1)
4605
+ z2, y2, x2 = int(z2), int(y2), int(x2)
4606
+ z_range = np.arange(z1, z2 + 1)
4607
+ y_range = np.arange(y1, y2 + 1)
4608
+ x_range = np.arange(x1, x2 + 1)
4609
+ z_grid, y_grid, x_grid = np.meshgrid(z_range, y_range, x_range, indexing='ij')
4610
+ del z_range
4611
+ del y_range
4612
+ del x_range
4613
+ coords = np.stack([z_grid.flatten(), y_grid.flatten(), x_grid.flatten()], axis=1)
4614
+ del z_grid
4615
+ del y_grid
4616
+ del x_grid
4617
+ else:
4618
+ shape = ()
4619
+ try:
4620
+ shape = self.nodes.shape
4621
+ except:
4622
+ try:
4623
+ shape = self.edges.shape
4624
+ except:
4625
+ try:
4626
+ shape = self._network_overlay.shape
4627
+ except:
4628
+ try:
4629
+ shape = self._id_overlay.shape
4630
+ except:
4631
+ pass
4632
+
4633
+ ranges = [np.arange(s) for s in shape]
4634
+
4635
+ # Create meshgrid
4636
+ mesh = np.meshgrid(*ranges, indexing='ij')
4637
+ del ranges
4638
+
4639
+ # Stack and reshape
4640
+ coords = np.stack(mesh, axis=-1).reshape(-1, len(shape))
4641
+ del mesh
4642
+
4643
+ if len(coords) < len(self.node_centroids):
4644
+ print(f"Warning: Only {len(coords)} positions available for {len(self.node_centroids)} labels")
4645
+
4646
+ new_centroids = {}
4647
+
4648
+ # Generate random indices without replacement
4649
+ available_count = min(len(coords), len(self.node_centroids))
4650
+ rand_indices = np.random.choice(len(coords), available_count, replace=False)
4651
+
4652
+ # Assign random positions to labels
4653
+ for i, label in enumerate(self.node_centroids.keys()):
4654
+ if i < len(rand_indices):
4655
+ centroid = coords[rand_indices[i]]
4656
+ new_centroids[label] = centroid
4657
+ z, y, x = centroid
4658
+ try:
4659
+ self.nodes[z, y, x] = label
4660
+ except:
4661
+ pass
4662
+
4663
+ # Update the centroids dictionary
4664
+ self.node_centroids = new_centroids
4665
+
4666
+ return self.node_centroids, self._nodes
4667
+
4668
+
4493
4669
  def community_id_info(self):
4494
4670
  def invert_dict(d):
4495
4671
  inverted = {}
@@ -1246,7 +1246,7 @@ class ImageViewerWindow(QMainWindow):
1246
1246
  except:
1247
1247
  pass
1248
1248
 
1249
- print(f"Found {len(filtered_df)} direct connections between nodes of ID {sort} and their neighbors (of any ID)")
1249
+ #print(f"Found {len(filtered_df)} direct connections between nodes of ID {sort} and their neighbors (of any ID)")
1250
1250
 
1251
1251
  if self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2] > self.mini_thresh:
1252
1252
  self.mini_overlay = True
@@ -2687,8 +2687,8 @@ class ImageViewerWindow(QMainWindow):
2687
2687
  degree_dist_action.triggered.connect(self.show_degree_dist_dialog)
2688
2688
  neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
2689
2689
  neighbor_id_action.triggered.connect(self.show_neighbor_id_dialog)
2690
- random_action = stats_menu.addAction("Generate Equivalent Random Network")
2691
- random_action.triggered.connect(self.show_random_dialog)
2690
+ ripley_action = stats_menu.addAction("Clustering Analysis")
2691
+ ripley_action.triggered.connect(self.show_ripley_dialog)
2692
2692
  vol_action = stats_menu.addAction("Calculate Volumes")
2693
2693
  vol_action.triggered.connect(self.volumes)
2694
2694
  rad_action = stats_menu.addAction("Calculate Radii")
@@ -2707,6 +2707,13 @@ class ImageViewerWindow(QMainWindow):
2707
2707
  id_code_action = overlay_menu.addAction("Code Identities")
2708
2708
  id_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Identity'))
2709
2709
 
2710
+ rand_menu = analysis_menu.addMenu("Randomize")
2711
+ random_action = rand_menu.addAction("Generate Equivalent Random Network")
2712
+ random_action.triggered.connect(self.show_random_dialog)
2713
+ random_nodes = rand_menu.addAction("Scramble Nodes (Centroids)")
2714
+ random_nodes.triggered.connect(self.show_randnode_dialog)
2715
+
2716
+
2710
2717
 
2711
2718
  # Process menu
2712
2719
  process_menu = menubar.addMenu("Process")
@@ -3501,8 +3508,8 @@ class ImageViewerWindow(QMainWindow):
3501
3508
  nii_img = nib.load(filename)
3502
3509
  # Get data and transpose to match TIFF orientation
3503
3510
  # If X needs to become Z, we move axis 2 (X) to position 0 (Z)
3504
- data = nii_img.get_fdata()
3505
- self.channel_data[channel_index] = np.transpose(data, (2, 1, 0))
3511
+ arraydata = nii_img.get_fdata()
3512
+ self.channel_data[channel_index] = np.transpose(arraydata, (2, 1, 0))
3506
3513
 
3507
3514
  elif file_extension in ['jpg', 'jpeg', 'png']:
3508
3515
  from PIL import Image
@@ -4157,10 +4164,18 @@ class ImageViewerWindow(QMainWindow):
4157
4164
  dialog = NeighborIdentityDialog(self)
4158
4165
  dialog.exec()
4159
4166
 
4167
+ def show_ripley_dialog(self):
4168
+ dialog = RipleyDialog(self)
4169
+ dialog.exec()
4170
+
4160
4171
  def show_random_dialog(self):
4161
4172
  dialog = RandomDialog(self)
4162
4173
  dialog.exec()
4163
4174
 
4175
+ def show_randnode_dialog(self):
4176
+ dialog = RandNodeDialog(self)
4177
+ dialog.exec()
4178
+
4164
4179
  def show_rad_dialog(self):
4165
4180
  dialog = RadDialog(self)
4166
4181
  dialog.exec()
@@ -5977,9 +5992,121 @@ class NeighborIdentityDialog(QDialog):
5977
5992
 
5978
5993
 
5979
5994
 
5995
+ class RipleyDialog(QDialog):
5996
+
5997
+ def __init__(self, parent=None):
5998
+
5999
+ super().__init__(parent)
6000
+ self.setWindowTitle(f"Find Ripley's H Function From Centroids")
6001
+ self.setModal(True)
6002
+
6003
+ layout = QFormLayout(self)
6004
+
6005
+ if my_network.node_identities is not None:
6006
+ self.root = QComboBox()
6007
+ self.root.addItems(list(set(my_network.node_identities.values())))
6008
+ self.root.setCurrentIndex(0)
6009
+ layout.addRow("Root Identity to Search for Neighbors", self.root)
6010
+ else:
6011
+ self.root = None
6012
+
6013
+ if my_network.node_identities is not None:
6014
+ self.targ = QComboBox()
6015
+ self.targ.addItems(list(set(my_network.node_identities.values())))
6016
+ self.targ.setCurrentIndex(0)
6017
+ layout.addRow("Targ Identity to be Searched For", self.targ)
6018
+ else:
6019
+ self.targ = None
6020
+
6021
+ self.distance = QLineEdit("5")
6022
+ layout.addRow("Bucket Distance for Searching For Clusters (automatically scaled by xy and z scales):", self.distance)
6023
+
6024
+
6025
+ self.proportion = QLineEdit("0.5")
6026
+ layout.addRow("Proportion of image to search? (0-1, high vals increase border artifacts): ", self.proportion)
6027
+
6028
+ self.edgecorrect = QPushButton("Border Correction")
6029
+ self.edgecorrect.setCheckable(True)
6030
+ self.edgecorrect.setChecked(False)
6031
+ layout.addRow("Use Border Correction (Extrapolate for points beyond the border):", self.edgecorrect)
6032
+
6033
+ self.ignore = QPushButton("Ignore Border Roots")
6034
+ self.ignore.setCheckable(True)
6035
+ self.ignore.setChecked(False)
6036
+ layout.addRow("Exclude Root Nodes Near Borders?:", self.ignore)
6037
+
6038
+ # Add Run button
6039
+ run_button = QPushButton("Get Ripley's H")
6040
+ run_button.clicked.connect(self.ripley)
6041
+ layout.addWidget(run_button)
6042
+
6043
+ def ripley(self):
6044
+
6045
+ try:
6046
+
6047
+ if my_network.node_centroids is None:
6048
+ self.parent().show_centroid_dialog()
6049
+
6050
+ try:
6051
+ root = self.root.currentText()
6052
+ except:
6053
+ root = None
6054
+
6055
+ try:
6056
+ targ = self.targ.currentText()
6057
+ except:
6058
+ targ = None
6059
+
6060
+ try:
6061
+ distance = float(self.distance.text())
6062
+ except:
6063
+ return
6064
+
6065
+
6066
+ try:
6067
+ proportion = abs(float(self.proportion.text()))
6068
+ except:
6069
+ proportion = 0.5
6070
+
6071
+ if proportion > 1 or proportion <= 0:
6072
+ print("Utilizing proportion = 0.5")
6073
+ proportion = 0.5
6074
+
6075
+
6076
+ edgecorrect = self.edgecorrect.isChecked()
6077
+
6078
+ ignore = self.ignore.isChecked()
6079
+
6080
+ if my_network.nodes is not None:
6081
+
6082
+ if my_network.nodes.shape[0] == 1:
6083
+ bounds = (np.array([0, 0]), np.array([my_network.nodes.shape[2], my_network.nodes.shape[1]]))
6084
+ else:
6085
+ bounds = (np.array([0, 0, 0]), np.array([my_network.nodes.shape[2], my_network.nodes.shape[1], my_network.nodes.shape[0]]))
6086
+ else:
6087
+ bounds = None
6088
+
6089
+ r_vals, k_vals, h_vals = my_network.get_ripley(root, targ, distance, edgecorrect, bounds, ignore, proportion)
6090
+
6091
+ k_dict = dict(zip(r_vals, k_vals))
6092
+ h_dict = dict(zip(r_vals, h_vals))
6093
+
6094
+
6095
+ self.parent().format_for_upperright_table(k_dict, metric='Radius (scaled)', value='L Value', title="Ripley's K")
6096
+ self.parent().format_for_upperright_table(h_dict, metric='Radius (scaled)', value='L Normed', title="Ripley's H")
5980
6097
 
5981
6098
 
6099
+ self.accept()
5982
6100
 
6101
+ except Exception as e:
6102
+ QMessageBox.critical(
6103
+ self,
6104
+ "Error:",
6105
+ f"Failed to preform cluster analysis: {str(e)}"
6106
+ )
6107
+ import traceback
6108
+ print(traceback.format_exc())
6109
+ print(f"Error: {e}")
5983
6110
 
5984
6111
  class RandomDialog(QDialog):
5985
6112
 
@@ -6019,6 +6146,79 @@ class RandomDialog(QDialog):
6019
6146
 
6020
6147
  self.accept()
6021
6148
 
6149
+ class RandNodeDialog(QDialog):
6150
+
6151
+ def __init__(self, parent=None):
6152
+
6153
+ super().__init__(parent)
6154
+ self.setWindowTitle("Random Node Parameters")
6155
+ self.setModal(True)
6156
+ layout = QFormLayout(self)
6157
+
6158
+
6159
+ self.mode = QComboBox()
6160
+ self.mode.addItems(["Anywhere", "Within Dimensional Bounds of Nodes", "Within Masked Bounds of Edges", "Within Masked Bounds of Overlay1", "Within Masked Bounds of Overlay2"])
6161
+ self.mode.setCurrentIndex(0)
6162
+ layout.addRow("Mode", self.mode)
6163
+
6164
+ # Add Run button
6165
+ run_button = QPushButton("Get Random Nodes (Will go in Nodes)")
6166
+ run_button.clicked.connect(self.random)
6167
+ layout.addWidget(run_button)
6168
+
6169
+ def random(self):
6170
+
6171
+ try:
6172
+
6173
+ if my_network.node_centroids is None:
6174
+ self.parent().show_centroid_dialog()
6175
+
6176
+ bounds = None
6177
+ mask = None
6178
+
6179
+ mode = self.mode.currentIndex()
6180
+
6181
+ if mode == 0 and not (my_network.nodes is None and my_network.edges is None and my_network.network_overlay is None and my_network.id_overlay is None):
6182
+ pass
6183
+ elif mode == 1 or (my_network.nodes is None and my_network.edges is None and my_network.network_overlay is None and my_network.id_overlay is None):
6184
+ print("HELLO")
6185
+ # Convert string labels to integers if necessary
6186
+ if any(isinstance(k, str) for k in my_network.node_centroids.keys()):
6187
+ label_map = {label: idx for idx, label in enumerate(my_network.node_centroids.keys())}
6188
+ my_network.node_centroids = {label_map[k]: v for k, v in my_network.node_centroids.items()}
6189
+
6190
+ # Convert centroids to array and keep track of labels
6191
+ labels = np.array(list(my_network.node_centroids.keys()), dtype=np.uint32)
6192
+ centroid_points = np.array([my_network.node_centroids[label] for label in labels])
6193
+
6194
+ # Calculate shape if not provided
6195
+ max_coords = centroid_points.max(axis=0)
6196
+ max_shape = tuple(max_coord + 1 for max_coord in max_coords)
6197
+ min_coords = centroid_points.min(axis=0)
6198
+ min_shape = tuple(min_coord + 1 for min_coord in min_coords)
6199
+ bounds = (min_shape, max_shape)
6200
+ else:
6201
+ mask = n3d.binarize(self.parent().channel_data[mode - 1])
6202
+
6203
+ centroids, array = my_network.random_nodes(bounds = bounds, mask = mask)
6204
+
6205
+ if my_network.nodes is not None:
6206
+ try:
6207
+ self.parent().load_channel(0, array, data = True)
6208
+ except:
6209
+ pass
6210
+
6211
+ self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
6212
+
6213
+ except Exception as e:
6214
+ QMessageBox.critical(
6215
+ self,
6216
+ "Error:",
6217
+ f"Failed to randomize: {str(e)}"
6218
+ )
6219
+ print(f"Error: {e}")
6220
+
6221
+
6022
6222
  class RadDialog(QDialog):
6023
6223
 
6024
6224
  def __init__(self, parent=None):
@@ -7208,8 +7408,14 @@ class MachineWindow(QMainWindow):
7208
7408
  train_quick.clicked.connect(lambda: self.train_model(speed=True))
7209
7409
  train_detailed = QPushButton("Train More Detailed Model")
7210
7410
  train_detailed.clicked.connect(lambda: self.train_model(speed=False))
7411
+ save = QPushButton("Save Model")
7412
+ save.clicked.connect(self.save_model)
7413
+ load = QPushButton("Load Model")
7414
+ load.clicked.connect(self.load_model)
7211
7415
  training_layout.addWidget(train_quick)
7212
7416
  training_layout.addWidget(train_detailed)
7417
+ training_layout.addWidget(save)
7418
+ training_layout.addWidget(load)
7213
7419
  training_group.setLayout(training_layout)
7214
7420
 
7215
7421
  # Group 4: Segmentation Options
@@ -7280,6 +7486,33 @@ class MachineWindow(QMainWindow):
7280
7486
  except:
7281
7487
  pass
7282
7488
 
7489
+ def save_model(self):
7490
+
7491
+ filename, _ = QFileDialog.getSaveFileName(
7492
+ self,
7493
+ f"Save Model As",
7494
+ "", # Default directory
7495
+ "numpy data (*.npz);;All Files (*)" # File type filter
7496
+ )
7497
+
7498
+ if filename: # Only proceed if user didn't cancel
7499
+ # If user didn't type an extension, add .tif
7500
+ if not filename.endswith(('.npz')):
7501
+ filename += '.npz'
7502
+
7503
+ self.segmenter.save_model(filename, self.parent().channel_data[2])
7504
+
7505
+ def load_model(self):
7506
+
7507
+ filename, _ = QFileDialog.getOpenFileName(
7508
+ self,
7509
+ f"Load Model",
7510
+ "",
7511
+ "numpy data (*.npz)"
7512
+ )
7513
+
7514
+ self.segmenter.load_model(filename)
7515
+ self.trained = True
7283
7516
 
7284
7517
  def toggle_two(self):
7285
7518
  if self.two.isChecked():
nettracer3d/proximity.py CHANGED
@@ -7,6 +7,7 @@ from scipy import ndimage
7
7
  import concurrent.futures
8
8
  import multiprocessing as mp
9
9
  import pandas as pd
10
+ import matplotlib.pyplot as plt
10
11
  from typing import Dict, Union, Tuple, List, Optional
11
12
 
12
13
 
@@ -288,7 +289,6 @@ def create_voronoi_3d_kdtree(centroids: Dict[Union[int, str], Union[Tuple[int, i
288
289
  Returns:
289
290
  3D numpy array where each cell contains the label of the closest centroid as uint32
290
291
  """
291
- from scipy.spatial import cKDTree
292
292
 
293
293
  # Convert string labels to integers if necessary
294
294
  if any(isinstance(k, str) for k in centroids.keys()):
@@ -305,7 +305,7 @@ def create_voronoi_3d_kdtree(centroids: Dict[Union[int, str], Union[Tuple[int, i
305
305
  shape = tuple(max_coord + 1 for max_coord in max_coords)
306
306
 
307
307
  # Create KD-tree
308
- tree = cKDTree(centroid_points)
308
+ tree = KDTree(centroid_points)
309
309
 
310
310
  # Create coordinate arrays
311
311
  coords = np.array(np.meshgrid(
@@ -322,4 +322,328 @@ def create_voronoi_3d_kdtree(centroids: Dict[Union[int, str], Union[Tuple[int, i
322
322
  label_array = labels[indices].astype(np.uint32)
323
323
 
324
324
  # Reshape to final shape
325
- return label_array.reshape(shape)
325
+ return label_array.reshape(shape)
326
+
327
+
328
+
329
+ #Ripley cluster analysis:
330
+
331
+ def convert_centroids_to_array(centroids_list, xy_scale = 1, z_scale = 1):
332
+ """
333
+ Convert a dictionary of centroids to a numpy array suitable for Ripley's K calculation.
334
+
335
+ Parameters:
336
+ centroids_list: List of centroid coordinate arrays
337
+
338
+ Returns:
339
+ numpy array of shape (n, d) where n is number of points and d is dimensionality
340
+ """
341
+ # Determine how many centroids we have
342
+ n_points = len(centroids_list)
343
+
344
+ # Get dimensionality from the first centroid
345
+ dim = len(list(centroids_list)[0])
346
+
347
+ # Create empty array
348
+ points_array = np.zeros((n_points, dim))
349
+
350
+ # Fill array with coordinates
351
+ for i, coords in enumerate(centroids_list):
352
+ points_array[i] = coords
353
+
354
+ points_array[:, 1:] = points_array[:, 1:] * xy_scale #account for scaling
355
+
356
+ points_array[:, 0] = points_array[:, 0] * z_scale #account for scaling
357
+
358
+ return points_array
359
+
360
+ def generate_r_values(points_array, step_size, bounds = None, dim = 2, max_proportion=0.5):
361
+ """
362
+ Generate an array of r values based on point distribution and step size.
363
+
364
+ Parameters:
365
+ points_array: numpy array of shape (n, d) with point coordinates
366
+ step_size: user-defined step size for r values
367
+ max_proportion: maximum proportion of the study area extent to use (default 0.5)
368
+ This prevents analyzing at distances where edge effects dominate
369
+
370
+ Returns:
371
+ numpy array of r values
372
+ """
373
+
374
+ if bounds is None:
375
+ if dim == 2:
376
+ min_coords = np.array([0,0])
377
+ else:
378
+ min_coords = np.array([0,0,0])
379
+ max_coords = np.max(points_array, axis=0)
380
+ max_coords = np.flip(max_coords)
381
+ else:
382
+ min_coords, max_coords = bounds
383
+
384
+
385
+ # Calculate the longest dimension
386
+ dimensions = max_coords - min_coords
387
+ max_dimension = np.max(dimensions)
388
+
389
+ # Calculate maximum r value (typically half the shortest side for 2D,
390
+ # or scaled by max_proportion for general use)
391
+ max_r = max_dimension * max_proportion
392
+
393
+ # Generate r values from 0 to max_r with step_size increments
394
+ num_steps = int(max_r / step_size)
395
+ r_values = np.linspace(step_size, max_r, num_steps)
396
+
397
+ if r_values[0] == 0:
398
+ np.delete(r_values, 0)
399
+
400
+ return r_values
401
+
402
+ def convert_augmented_array_to_points(augmented_array):
403
+ """
404
+ Convert an array where first column is 1 and remaining columns are coordinates.
405
+
406
+ Parameters:
407
+ augmented_array: 2D array where first column is 1 and rest are coordinates
408
+
409
+ Returns:
410
+ numpy array with just the coordinate columns
411
+ """
412
+ # Extract just the coordinate columns (all except first column)
413
+ return augmented_array[:, 1:]
414
+
415
+ def optimized_ripleys_k(reference_points, subset_points, r_values, bounds=None, edge_correction=True, dim = 2, is_subset = False):
416
+ """
417
+ Optimized computation of Ripley's K function using KD-Tree with simplified but effective edge correction.
418
+
419
+ Parameters:
420
+ reference_points: numpy array of shape (n, d) containing coordinates (d=2 or d=3)
421
+ subset_points: numpy array of shape (m, d) containing coordinates
422
+ r_values: numpy array of distances at which to compute K
423
+ bounds: tuple of (min_coords, max_coords) defining the study area boundaries
424
+ edge_correction: Boolean indicating whether to apply edge correction
425
+
426
+ Returns:
427
+ K_values: numpy array of K values corresponding to r_values
428
+ """
429
+ n_ref = len(reference_points)
430
+ n_subset = len(subset_points)
431
+
432
+ # Determine bounds if not provided
433
+ if bounds is None:
434
+ min_coords = np.min(reference_points, axis=0)
435
+ max_coords = np.max(reference_points, axis=0)
436
+ bounds = (min_coords, max_coords)
437
+
438
+ # Calculate volume of study area
439
+ min_bounds, max_bounds = bounds
440
+ sides = max_bounds - min_bounds
441
+ volume = np.prod(sides)
442
+
443
+ # Point intensity (points per unit volume)
444
+ intensity = n_ref / volume
445
+
446
+ # Build KD-Tree for efficient nearest neighbor search
447
+ tree = KDTree(reference_points)
448
+
449
+ # Initialize K values
450
+ K_values = np.zeros(len(r_values))
451
+
452
+ # For each r value, compute cumulative counts
453
+ for i, r in enumerate(r_values):
454
+ total_count = 0
455
+
456
+ # Query the tree for all points within radius r of each subset point
457
+ for j, point in enumerate(subset_points):
458
+ # Find all reference points within radius r
459
+ indices = tree.query_ball_point(point, r)
460
+ count = len(indices)
461
+
462
+ # Apply edge correction if needed
463
+ if edge_correction:
464
+ # Calculate edge correction weight
465
+ weight = 1.0
466
+
467
+ if dim == 2:
468
+ # For 2D - check all four boundaries
469
+ x, y = point
470
+
471
+ # Distances to all boundaries
472
+ x_min_dist = x - min_bounds[0]
473
+ x_max_dist = max_bounds[0] - x
474
+ y_min_dist = y - min_bounds[1]
475
+ y_max_dist = max_bounds[1] - y
476
+
477
+ proportion_in = 1.0
478
+ # Apply correction for each boundary if needed
479
+ if x_min_dist < r:
480
+ proportion_in -= 0.5 * (1 - x_min_dist/r)
481
+ if x_max_dist < r:
482
+ proportion_in -= 0.5 * (1 - x_max_dist/r)
483
+ if y_min_dist < r:
484
+ proportion_in -= 0.5 * (1 - y_min_dist/r)
485
+ if y_max_dist < r:
486
+ proportion_in -= 0.5 * (1 - y_max_dist/r)
487
+
488
+ # Corner correction
489
+ if ((x_min_dist < r and y_min_dist < r) or
490
+ (x_min_dist < r and y_max_dist < r) or
491
+ (x_max_dist < r and y_min_dist < r) or
492
+ (x_max_dist < r and y_max_dist < r)):
493
+ proportion_in += 0.1 # Add a small boost for corners
494
+
495
+ elif dim == 3:
496
+ # For 3D - check all six boundaries
497
+ x, y, z = point
498
+
499
+ # Distances to all boundaries
500
+ x_min_dist = x - min_bounds[0]
501
+ x_max_dist = max_bounds[0] - x
502
+ y_min_dist = y - min_bounds[1]
503
+ y_max_dist = max_bounds[1] - y
504
+ z_min_dist = z - min_bounds[2]
505
+ z_max_dist = max_bounds[2] - z
506
+
507
+ proportion_in = 1.0
508
+ # Apply correction for each boundary if needed
509
+ if x_min_dist < r:
510
+ proportion_in -= 0.25 * (1 - x_min_dist/r)
511
+ if x_max_dist < r:
512
+ proportion_in -= 0.25 * (1 - x_max_dist/r)
513
+ if y_min_dist < r:
514
+ proportion_in -= 0.25 * (1 - y_min_dist/r)
515
+ if y_max_dist < r:
516
+ proportion_in -= 0.25 * (1 - y_max_dist/r)
517
+ if z_min_dist < r:
518
+ proportion_in -= 0.25 * (1 - z_min_dist/r)
519
+ if z_max_dist < r:
520
+ proportion_in -= 0.25 * (1 - z_max_dist/r)
521
+
522
+ # Corner correction for 3D (if point is near a corner)
523
+ num_close_edges = (
524
+ (x_min_dist < r) + (x_max_dist < r) +
525
+ (y_min_dist < r) + (y_max_dist < r) +
526
+ (z_min_dist < r) + (z_max_dist < r)
527
+ )
528
+ if num_close_edges >= 2:
529
+ proportion_in += 0.05 * num_close_edges # Stronger boost for more edges
530
+
531
+ # Ensure proportion_in stays within reasonable bounds
532
+ proportion_in = max(0.1, min(1.0, proportion_in))
533
+ weight = 1.0 / proportion_in
534
+
535
+ count *= weight
536
+
537
+ total_count += count
538
+
539
+ # Subtract self-counts if points appear in both sets
540
+ if is_subset or np.array_equal(reference_points, subset_points):
541
+ total_count -= n_ref # Subtract all self-counts
542
+
543
+ # Normalize
544
+ K_values[i] = total_count / (n_subset * intensity)
545
+
546
+ return K_values
547
+
548
+ def ripleys_h_function_3d(k_values, r_values):
549
+ """
550
+ Convert K values to H values for 3D point patterns with edge correction.
551
+
552
+ Parameters:
553
+ k_values: numpy array of K function values
554
+ r_values: numpy array of distances at which K was computed
555
+ edge_weights: optional array of edge correction weights
556
+
557
+ Returns:
558
+ h_values: numpy array of H function values
559
+ """
560
+ h_values = np.cbrt(k_values / (4/3 * np.pi)) - r_values
561
+
562
+ return h_values
563
+
564
+ def ripleys_h_function_2d(k_values, r_values):
565
+ """
566
+ Convert K values to H values for 2D point patterns with edge correction.
567
+
568
+ Parameters:
569
+ k_values: numpy array of K function values
570
+ r_values: numpy array of distances at which K was computed
571
+ edge_weights: optional array of edge correction weights
572
+
573
+ Returns:
574
+ h_values: numpy array of H function values
575
+ """
576
+ h_values = np.sqrt(k_values / np.pi) - r_values
577
+
578
+ return h_values
579
+
580
+ def compute_ripleys_h(k_values, r_values, dimension=2):
581
+ """
582
+ Compute Ripley's H function (normalized K) with edge correction.
583
+
584
+ Parameters:
585
+ k_values: numpy array of K function values
586
+ r_values: numpy array of distances at which K was computed
587
+ edge_weights: optional array of edge correction weights
588
+ dimension: dimensionality of the point pattern (2 for 2D, 3 for 3D)
589
+
590
+ Returns:
591
+ h_values: numpy array of H function values
592
+ """
593
+ if dimension == 2:
594
+ return ripleys_h_function_2d(k_values, r_values)
595
+ elif dimension == 3:
596
+ return ripleys_h_function_3d(k_values, r_values)
597
+ else:
598
+ raise ValueError("Dimension must be 2 or 3")
599
+
600
+ def plot_ripley_functions(r_values, k_values, h_values, dimension=2, figsize=(12, 5)):
601
+ """
602
+ Plot Ripley's K and H functions with theoretical Poisson distribution references
603
+ adjusted for edge effects.
604
+
605
+ Parameters:
606
+ r_values: numpy array of distances at which K and H were computed
607
+ k_values: numpy array of K function values
608
+ h_values: numpy array of H function values (normalized K)
609
+ edge_weights: optional array of edge correction weights
610
+ dimension: dimensionality of the point pattern (2 for 2D, 3 for 3D)
611
+ figsize: tuple specifying figure size (width, height)
612
+ """
613
+
614
+ #plt.figure()
615
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
616
+
617
+ # Theoretical values for complete spatial randomness (CSR)
618
+ if dimension == 2:
619
+ theo_k = np.pi * r_values**2 # πr² for 2D
620
+ elif dimension == 3:
621
+ theo_k = (4/3) * np.pi * r_values**3 # (4/3)πr³ for 3D
622
+ else:
623
+ raise ValueError("Dimension must be 2 or 3")
624
+
625
+ # Theoretical H values are always 0 for CSR
626
+ theo_h = np.zeros_like(r_values)
627
+
628
+ # Plot K function
629
+ ax1.plot(r_values, k_values, 'b-', label='Observed K(r)')
630
+ ax1.plot(r_values, theo_k, 'r--', label='Theoretical K(r) for CSR')
631
+ ax1.set_xlabel('Distance (r)')
632
+ ax1.set_ylabel('L(r)')
633
+ ax1.set_title("Ripley's K Function")
634
+ ax1.legend()
635
+ ax1.grid(True, alpha=0.3)
636
+
637
+ # Plot H function
638
+ ax2.plot(r_values, h_values, 'b-', label='Observed H(r)')
639
+ ax2.plot(r_values, theo_h, 'r--', label='Theoretical H(r) for CSR')
640
+ ax2.set_xlabel('Distance (r)')
641
+ ax2.set_ylabel('L(r) Normalized')
642
+ ax2.set_title("Ripley's H Function")
643
+ ax2.axhline(y=0, color='k', linestyle='-', alpha=0.3)
644
+ ax2.legend()
645
+ ax2.grid(True, alpha=0.3)
646
+
647
+ plt.tight_layout()
648
+ plt.show()
649
+ #plt.clf()
nettracer3d/segmenter.py CHANGED
@@ -75,6 +75,12 @@ class InteractiveSegmenter:
75
75
  self.dogs = [(1, 2), (2, 4), (4, 8)]
76
76
  self.master_chunk = 49
77
77
 
78
+ #Data when loading prev model:
79
+ self.previous_foreground = None
80
+ self.previous_background = None
81
+ self.previous_z_fore = None
82
+ self.previous_z_back = None
83
+
78
84
  def segment_slice_chunked(self, slice_z, block_size = 49):
79
85
  """
80
86
  A completely standalone method to segment a single z-slice in chunks
@@ -1757,10 +1763,11 @@ class InteractiveSegmenter:
1757
1763
  except:
1758
1764
  pass
1759
1765
 
1760
- def train_batch(self, foreground_array, speed = True, use_gpu = False, use_two = False, mem_lock = False):
1766
+ def train_batch(self, foreground_array, speed = True, use_gpu = False, use_two = False, mem_lock = False, saving = False):
1761
1767
  """Train directly on foreground and background arrays"""
1762
1768
 
1763
- print("Training model...")
1769
+ if not saving:
1770
+ print("Training model...")
1764
1771
  self.speed = speed
1765
1772
  self.cur_gpu = use_gpu
1766
1773
  if mem_lock != self.mem_lock:
@@ -1969,12 +1976,40 @@ class InteractiveSegmenter:
1969
1976
  z_back, y_back, x_back = np.where(foreground_array == 2)
1970
1977
  background_features = self.feature_cache[z_back, y_back, x_back]
1971
1978
  except:
1972
- print("Features maps computed, but no segmentation examples were provided so the model was not trained")
1979
+ pass
1980
+
1981
+
1982
+ if self.previous_foreground is not None:
1983
+ failed = True
1984
+ try:
1985
+ foreground_features = np.vstack([self.previous_foreground, foreground_features])
1986
+ failed = False
1987
+ except:
1988
+ pass
1989
+ try:
1990
+ background_features = np.vstack([self.previous_background, background_features])
1991
+ failed = False
1992
+ except:
1993
+ pass
1994
+ try:
1995
+ z_fore = np.concatenate([self.previous_z_fore, z_fore])
1996
+ except:
1997
+ pass
1998
+ try:
1999
+ z_back = np.concatenate([self.previous_z_back, z_back])
2000
+ except:
2001
+ pass
2002
+ if failed:
2003
+ print("Could not combine new model with old loaded model. Perhaps you are trying to combine a quick model with a deep model? I cannot combine these...")
2004
+
2005
+ if saving:
1973
2006
 
2007
+ return foreground_features, background_features, z_fore, z_back
1974
2008
 
1975
2009
  # Combine features and labels
1976
2010
  X = np.vstack([foreground_features, background_features])
1977
2011
  y = np.hstack([np.ones(len(z_fore)), np.zeros(len(z_back))])
2012
+
1978
2013
 
1979
2014
  # Train the model
1980
2015
  try:
@@ -1988,6 +2023,54 @@ class InteractiveSegmenter:
1988
2023
 
1989
2024
 
1990
2025
 
2026
+ print("Done")
2027
+
2028
+
2029
+ def save_model(self, file_name, foreground_array):
2030
+
2031
+ print("Saving model data")
2032
+
2033
+ foreground_features, background_features, z_fore, z_back = self.train_batch(foreground_array, speed = self.speed, use_gpu = self.use_gpu, use_two = self.use_two, mem_lock = self.mem_lock, saving = True)
2034
+
2035
+
2036
+ np.savez(file_name,
2037
+ foreground_features=foreground_features,
2038
+ background_features=background_features,
2039
+ z_fore=z_fore,
2040
+ z_back=z_back,
2041
+ speed=self.speed,
2042
+ use_gpu=self.use_gpu,
2043
+ use_two=self.use_two,
2044
+ mem_lock=self.mem_lock)
2045
+
2046
+ print(f"Model data saved to {file_name}")
2047
+
2048
+
2049
+ def load_model(self, file_name):
2050
+
2051
+ print("Loading model data")
2052
+
2053
+ data = np.load(file_name)
2054
+
2055
+ # Unpack the arrays
2056
+ self.previous_foreground = data['foreground_features']
2057
+ self.previous_background = data['background_features']
2058
+ self.previous_z_fore = data['z_fore']
2059
+ self.previous_z_back = data['z_back']
2060
+ self.speed = bool(data['speed'])
2061
+ self.use_gpu = bool(data['use_gpu'])
2062
+ self.use_two = bool(data['use_two'])
2063
+ self.mem_lock = bool(data['mem_lock'])
2064
+
2065
+ X = np.vstack([self.previous_foreground, self.previous_background])
2066
+ y = np.hstack([np.ones(len(self.previous_z_fore)), np.zeros(len(self.previous_z_back))])
2067
+
2068
+ try:
2069
+ self.model.fit(X, y)
2070
+ except:
2071
+ print(X)
2072
+ print(y)
2073
+
1991
2074
  print("Done")
1992
2075
 
1993
2076
  def get_feature_map_slice(self, z, speed, use_gpu):
@@ -188,9 +188,6 @@ def dilate_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0, GPU = Fa
188
188
  Returns:
189
189
  Dilated 3D array
190
190
  """
191
- if array.shape[0] == 1:
192
-
193
- return nettracer.dilate_2D(array, search_distance, scaling = xy_scaling)
194
191
 
195
192
  # Determine which dimension needs resampling. the moral of the story is read documentation before you do something unecessary.
196
193
  """
@@ -235,7 +232,6 @@ def dilate_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0, GPU = Fa
235
232
  # Threshold the distance transform to get dilated result
236
233
  inv = inv <= search_distance
237
234
 
238
-
239
235
  return inv.astype(np.uint8), indices, array
240
236
 
241
237
 
@@ -475,6 +471,7 @@ def compute_distance_transform_GPU(nodes, return_dists = False, sampling = [1, 1
475
471
  is_pseudo_3d = nodes.shape[0] == 1
476
472
  if is_pseudo_3d:
477
473
  nodes = np.squeeze(nodes) # Convert to 2D for processing
474
+ del sampling[0]
478
475
 
479
476
  # Convert numpy array to CuPy array
480
477
  nodes_cp = cp.asarray(nodes)
@@ -507,6 +504,7 @@ def compute_distance_transform(nodes, return_dists = False, sampling = [1, 1, 1]
507
504
  is_pseudo_3d = nodes.shape[0] == 1
508
505
  if is_pseudo_3d:
509
506
  nodes = np.squeeze(nodes) # Convert to 2D for processing
507
+ del sampling[0]
510
508
 
511
509
  dists, nearest_label_indices = distance_transform_edt(nodes, return_indices=True, sampling = sampling)
512
510
 
@@ -533,6 +531,7 @@ def compute_distance_transform_distance_GPU(nodes, sampling = [1, 1, 1]):
533
531
  is_pseudo_3d = nodes.shape[0] == 1
534
532
  if is_pseudo_3d:
535
533
  nodes = np.squeeze(nodes) # Convert to 2D for processing
534
+ del sampling[0]
536
535
 
537
536
  # Convert numpy array to CuPy array
538
537
  nodes_cp = cp.asarray(nodes)
@@ -554,6 +553,7 @@ def compute_distance_transform_distance(nodes, sampling = [1, 1, 1]):
554
553
  is_pseudo_3d = nodes.shape[0] == 1
555
554
  if is_pseudo_3d:
556
555
  nodes = np.squeeze(nodes) # Convert to 2D for processing
556
+ del sampling[0]
557
557
 
558
558
  # Fallback to CPU if there's an issue with GPU computation
559
559
  distance = distance_transform_edt(nodes, sampling = sampling)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nettracer3d
3
- Version: 0.6.9
3
+ Version: 0.7.1
4
4
  Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
5
5
  Author-email: Liam McLaughlin <liamm@wustl.edu>
6
6
  Project-URL: Documentation, https://nettracer3d.readthedocs.io/en/latest/
@@ -9,26 +9,26 @@ Project-URL: Reference_Citation_For_Use, https://doi.org/10.1101/2024.07.29.6056
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: License :: Other/Proprietary License
11
11
  Classifier: Operating System :: OS Independent
12
- Requires-Python: ==3.11
12
+ Requires-Python: >=3.7
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
- Requires-Dist: numpy==1.26.4
16
- Requires-Dist: scipy==1.14.1
17
- Requires-Dist: scikit-image==0.25.0
18
- Requires-Dist: Pillow==11.1.0
19
- Requires-Dist: matplotlib==3.9.2
20
- Requires-Dist: networkx==3.2.1
21
- Requires-Dist: opencv-python-headless==4.10.0.84
22
- Requires-Dist: openpyxl==3.1.2
23
- Requires-Dist: pandas==2.2.0
24
- Requires-Dist: napari==0.5.5
25
- Requires-Dist: python-louvain==0.16
26
- Requires-Dist: tifffile==2023.7.18
27
- Requires-Dist: qtrangeslider==0.1.5
28
- Requires-Dist: PyQt6==6.8.0
29
- Requires-Dist: scikit-learn==1.6.1
30
- Requires-Dist: nibabel==5.2.0
31
- Requires-Dist: setuptools>=65.0.0
15
+ Requires-Dist: numpy
16
+ Requires-Dist: scipy
17
+ Requires-Dist: scikit-image
18
+ Requires-Dist: Pillow
19
+ Requires-Dist: matplotlib
20
+ Requires-Dist: networkx
21
+ Requires-Dist: opencv-python-headless
22
+ Requires-Dist: openpyxl
23
+ Requires-Dist: pandas
24
+ Requires-Dist: napari
25
+ Requires-Dist: python-louvain
26
+ Requires-Dist: tifffile
27
+ Requires-Dist: qtrangeslider
28
+ Requires-Dist: PyQt6
29
+ Requires-Dist: scikit-learn
30
+ Requires-Dist: nibabel
31
+ Requires-Dist: setuptools
32
32
  Provides-Extra: cuda11
33
33
  Requires-Dist: cupy-cuda11x; extra == "cuda11"
34
34
  Provides-Extra: cuda12
@@ -73,12 +73,14 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
73
73
 
74
74
  NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
75
75
 
76
- -- Version 0.6.9 updates --
76
+ -- Version 0.7.0 Updates --
77
77
 
78
- 1. Adjusted all distance transform-based dilation/radius calculating methods to simply use the already supported scipy.ndimage.distance_transform_edt() sampling parameter to account for differentially scaled axis (previously the image was being resampled but now it no longer will need to do that).
78
+ 1. Added new function in 'Analyze -> Stats -> Cluster Analysis'
79
+ * This function allows the user to create a ripley's K or H function to compare the relative clustering of two types of nodes, or of one type of node vs itself.
79
80
 
80
- 2. Added new right click option to extract highlighted regions and implant their data onto a separate image or into a new empty image.
81
+ 2. Added new function in 'Analyze -> Randomize -> Scramble Nodes'
82
+ * This function randomly rearranges the node (centroids) for comparison with other centroid-using methods, as a possible way to demonstrate non-random behavior.
83
+ * The randomize menu is likewise new and the 'Generate Equivalent Random Network' method was moved there.
81
84
 
82
- 3. General bug fixes and improvements.
83
-
84
- 4. Now specifies python 3.11.
85
+ 3. Bug fixes.
86
+ * Importantly fixed a bug with dt-based dilation not working in 2D, which I had accidentally introduced recently.
@@ -0,0 +1,20 @@
1
+ nettracer3d/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ nettracer3d/community_extractor.py,sha256=5v9SCCLX3P1RX0fjPVKH5NHMFkMolZ5BTe0bR_a67xg,24479
3
+ nettracer3d/modularity.py,sha256=FH3GpTHorRNkdQULe-2DWgFE3i0_u__hrao7Nx_6Ge4,30249
4
+ nettracer3d/morphology.py,sha256=kEzuzBxHHpWt3o_LHMXYJGJT8-Z_dEP99QpZjOkavzE,19742
5
+ nettracer3d/nettracer.py,sha256=dnnWEr9xpzoxV-NqLJm4DAFmZJ25CSjhA85AR_LT8Qc,218452
6
+ nettracer3d/nettracer_gui.py,sha256=jeqsjaLGdaXdPcuy3Z3iEVnaOvyGDTEjD8uVkoNPFtk,426501
7
+ nettracer3d/network_analysis.py,sha256=q1q7lxtA3lebxitfC_jfiT9cnpYXJw4q0Oy2_-Aj8qE,48068
8
+ nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
9
+ nettracer3d/node_draw.py,sha256=k3sCTfUCJs3aH1C1q1gTNxDz9EAQbBd1hsUIJajxRx8,9823
10
+ nettracer3d/proximity.py,sha256=2Fj1UTKDEFBf7r1SgWXhiSINOTMjP4G5_Egu58l2zTk,24756
11
+ nettracer3d/run.py,sha256=xYeaAc8FCx8MuzTGyL3NR3mK7WZzffAYAH23bNRZYO4,127
12
+ nettracer3d/segmenter.py,sha256=gJS2AXqHhnw29cbzIxAah2LsrE7_7XnzG7mYSAovZ4I,87847
13
+ nettracer3d/simple_network.py,sha256=fP1gkDdtQcHruEZpUdasKdZeVacoLOxKhR3bY0L1CAQ,15426
14
+ nettracer3d/smart_dilate.py,sha256=MNFz-7P56OFwkNx2N24SH4gV0kL3KwzmZvAxg-T7f3U,25781
15
+ nettracer3d-0.7.1.dist-info/licenses/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
16
+ nettracer3d-0.7.1.dist-info/METADATA,sha256=-RaYqwgIm9jxvt5ihvTLlhR-TMlS2YH38SAUHvQWxUA,4823
17
+ nettracer3d-0.7.1.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
18
+ nettracer3d-0.7.1.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
19
+ nettracer3d-0.7.1.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
20
+ nettracer3d-0.7.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (79.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,20 +0,0 @@
1
- nettracer3d/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- nettracer3d/community_extractor.py,sha256=5v9SCCLX3P1RX0fjPVKH5NHMFkMolZ5BTe0bR_a67xg,24479
3
- nettracer3d/modularity.py,sha256=FH3GpTHorRNkdQULe-2DWgFE3i0_u__hrao7Nx_6Ge4,30249
4
- nettracer3d/morphology.py,sha256=y6OXLvyRmIBFpF3snC7ZirOW3Hg9jsHE11wbe0HrzQM,19692
5
- nettracer3d/nettracer.py,sha256=muv5Z33KOLKzzzbnNLDad_z1LYCKbuQUuKQLFQ0HAPY,211734
6
- nettracer3d/nettracer_gui.py,sha256=UCls6Z8O3SaXJUyxzdJlPDxIug7w290VbrojSWQDKs8,417722
7
- nettracer3d/network_analysis.py,sha256=q1q7lxtA3lebxitfC_jfiT9cnpYXJw4q0Oy2_-Aj8qE,48068
8
- nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
9
- nettracer3d/node_draw.py,sha256=k3sCTfUCJs3aH1C1q1gTNxDz9EAQbBd1hsUIJajxRx8,9823
10
- nettracer3d/proximity.py,sha256=-HoJseoD2O2Q1eLDBMMmM_UYPqM1vZg7bpG4_9Ime-o,12516
11
- nettracer3d/run.py,sha256=xYeaAc8FCx8MuzTGyL3NR3mK7WZzffAYAH23bNRZYO4,127
12
- nettracer3d/segmenter.py,sha256=GnQTVisQUKhackgjFxoldMlbArHEwHGiZPwv-sgPTj0,85072
13
- nettracer3d/simple_network.py,sha256=fP1gkDdtQcHruEZpUdasKdZeVacoLOxKhR3bY0L1CAQ,15426
14
- nettracer3d/smart_dilate.py,sha256=SKm5OSu7jcnA7eUKz58_LscadV-lCvY2ZzdVzCvTmkY,25796
15
- nettracer3d-0.6.9.dist-info/licenses/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
16
- nettracer3d-0.6.9.dist-info/METADATA,sha256=5JFlDK6cwwRKV25onj8KGX6ylD4vFXMaoZM1lzZCGaw,4755
17
- nettracer3d-0.6.9.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
18
- nettracer3d-0.6.9.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
19
- nettracer3d-0.6.9.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
20
- nettracer3d-0.6.9.dist-info/RECORD,,