nettracer3d 0.6.8__py3-none-any.whl → 0.7.0__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/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
 
@@ -178,9 +179,31 @@ def populate_array(centroids):
178
179
 
179
180
  return array
180
181
 
181
- def find_neighbors_kdtree(array, radius, targets=None):
182
+ def find_neighbors_kdtree(radius, centroids=None, array=None, targets=None):
182
183
  # Get coordinates of nonzero points
183
- points = np.transpose(np.nonzero(array))
184
+ if centroids:
185
+ # If centroids is a dictionary mapping node IDs to coordinates
186
+ if isinstance(centroids, dict):
187
+ # Extract the node IDs and points
188
+ node_ids = list(centroids.keys())
189
+ points_list = list(centroids.values())
190
+ points = np.array(points_list, dtype=np.int32)
191
+ else:
192
+ # If centroids is just a list of points
193
+ points = np.array(centroids, dtype=np.int32)
194
+ node_ids = list(range(1, len(points) + 1)) # Default sequential IDs
195
+
196
+ # Create a temporary array only if we need it for lookups
197
+ if array is None:
198
+ max_coords = np.max(points, axis=0) + 1
199
+ array = np.zeros(tuple(max_coords), dtype=np.int32)
200
+ for i, point in enumerate(points):
201
+ array[tuple(point)] = node_ids[i] # Use the actual node ID
202
+ elif array is not None:
203
+ points = np.transpose(np.nonzero(array))
204
+ node_ids = None # Not used in array-based mode
205
+ else:
206
+ return []
184
207
 
185
208
  # Create KD-tree from all nonzero points
186
209
  tree = KDTree(points)
@@ -188,19 +211,29 @@ def find_neighbors_kdtree(array, radius, targets=None):
188
211
  if targets is None:
189
212
  # Original behavior: find neighbors for all points
190
213
  query_points = points
191
- query_indices = range(len(points)) # Add this line
214
+ query_indices = range(len(points))
192
215
  else:
193
216
  # Find coordinates of target values
194
217
  target_points = []
195
- target_indices = [] # Add this line
196
- for idx, point in enumerate(points):
197
- if array[tuple(point)] in targets:
198
- target_points.append(point)
199
- target_indices.append(idx) # Add this line
218
+ target_indices = []
219
+
220
+ if array is not None:
221
+ # Standard array-based filtering
222
+ for idx, point in enumerate(points):
223
+ point_tuple = tuple(point)
224
+ if array[point_tuple] in targets:
225
+ target_points.append(point)
226
+ target_indices.append(idx)
227
+ else:
228
+ # Filter based on node IDs directly
229
+ for idx, node_id in enumerate(node_ids):
230
+ if node_id in targets:
231
+ target_points.append(points[idx])
232
+ target_indices.append(idx)
200
233
 
201
234
  # Convert to numpy array for querying
202
235
  query_points = np.array(target_points)
203
- query_indices = target_indices # Add this line
236
+ query_indices = target_indices
204
237
 
205
238
  # Handle case where no target values were found
206
239
  if len(query_points) == 0:
@@ -214,17 +247,21 @@ def find_neighbors_kdtree(array, radius, targets=None):
214
247
 
215
248
  # Generate pairs
216
249
  for i, neighbors in enumerate(neighbor_indices):
217
- query_idx = query_indices[i] # Modified this line
250
+ query_idx = query_indices[i]
218
251
  for neighbor_idx in neighbors:
219
252
  # Skip self-pairing
220
253
  if neighbor_idx != query_idx:
221
- query_value = array[tuple(points[query_idx])]
222
- neighbor_value = array[tuple(points[neighbor_idx])]
254
+ # Use node IDs from the dictionary if available
255
+ if array is not None:
256
+ query_value = array[tuple(points[query_idx])]
257
+ neighbor_value = array[tuple(points[neighbor_idx])]
258
+ else:
259
+ query_value = node_ids[query_idx]
260
+ neighbor_value = node_ids[neighbor_idx]
223
261
  output.append([query_value, neighbor_value, 0])
224
262
 
225
263
  return output
226
264
 
227
-
228
265
  def extract_pairwise_connections(connections):
229
266
  output = []
230
267
 
@@ -252,7 +289,6 @@ def create_voronoi_3d_kdtree(centroids: Dict[Union[int, str], Union[Tuple[int, i
252
289
  Returns:
253
290
  3D numpy array where each cell contains the label of the closest centroid as uint32
254
291
  """
255
- from scipy.spatial import cKDTree
256
292
 
257
293
  # Convert string labels to integers if necessary
258
294
  if any(isinstance(k, str) for k in centroids.keys()):
@@ -269,7 +305,7 @@ def create_voronoi_3d_kdtree(centroids: Dict[Union[int, str], Union[Tuple[int, i
269
305
  shape = tuple(max_coord + 1 for max_coord in max_coords)
270
306
 
271
307
  # Create KD-tree
272
- tree = cKDTree(centroid_points)
308
+ tree = KDTree(centroid_points)
273
309
 
274
310
  # Create coordinate arrays
275
311
  coords = np.array(np.meshgrid(
@@ -286,4 +322,328 @@ def create_voronoi_3d_kdtree(centroids: Dict[Union[int, str], Union[Tuple[int, i
286
322
  label_array = labels[indices].astype(np.uint32)
287
323
 
288
324
  # Reshape to final shape
289
- 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
@@ -1011,7 +1017,7 @@ class InteractiveSegmenter:
1011
1017
  futures.append(future)
1012
1018
 
1013
1019
  def compute_dog_local(img, s1, s2):
1014
- g1 = ndimage.gaussian_filter(img, s1)
1020
+ g1 = ndimage.gaussian_filter(img, s1) # Consider just having this return the gaussians to
1015
1021
  g2 = ndimage.gaussian_filter(img, s2)
1016
1022
  return g1 - g2
1017
1023
 
@@ -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):