nettracer3d 1.2.5__py3-none-any.whl → 1.3.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.

Potentially problematic release.


This version of nettracer3d might be problematic. Click here for more details.

@@ -3,12 +3,19 @@ import numpy as np
3
3
  from scipy.ndimage import binary_dilation, distance_transform_edt
4
4
  from scipy.ndimage import gaussian_filter
5
5
  from scipy import ndimage
6
- from concurrent.futures import ThreadPoolExecutor, as_completed
6
+ from concurrent.futures import ThreadPoolExecutor, as_completed, ProcessPoolExecutor
7
+ from skimage.segmentation import watershed
7
8
  import cv2
8
9
  import os
10
+ try:
11
+ import edt
12
+ print("Parallel search functions enabled")
13
+ except:
14
+ print("Some parallel search functions disabled (requires edt package), will fall back to single-threaded")
9
15
  import math
10
16
  import re
11
17
  from . import nettracer
18
+ from multiprocessing import shared_memory
12
19
  import multiprocessing as mp
13
20
  try:
14
21
  import cupy as cp
@@ -177,6 +184,7 @@ def dilate_3D_old(tiff_array, dilated_x=3, dilated_y=3, dilated_z=3):
177
184
 
178
185
  return dilated_array.astype(np.uint8)
179
186
 
187
+
180
188
  def dilate_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0, GPU = False):
181
189
  """
182
190
  Dilate a 3D array using distance transform method. Dt dilation produces perfect results but only works in euclidean geometry and lags in big arrays.
@@ -286,74 +294,34 @@ def process_chunk(start_idx, end_idx, nodes, ring_mask, nearest_label_indices):
286
294
 
287
295
  return dilated_nodes_with_labels_chunk
288
296
 
289
- def smart_dilate(nodes, dilate_xy, dilate_z, directory = None, GPU = True, fast_dil = True, predownsample = None, use_dt_dil_amount = None, xy_scale = 1, z_scale = 1):
297
+ def smart_dilate(nodes, dilate_xy = 0, dilate_z = 0, directory = None, GPU = True, fast_dil = True, predownsample = None, use_dt_dil_amount = None, xy_scale = 1, z_scale = 1):
290
298
 
291
- original_shape = nodes.shape
292
-
293
-
294
- #Dilate the binarized array
295
299
  if fast_dil:
296
- # Step : Binarize the labeled array
297
- binary_nodes = binarize(nodes)
298
- dilated_binary_nodes = dilate_3D(binary_nodes, dilate_xy, dilate_xy, dilate_z)
300
+ try:
301
+ import edt
302
+ dilated = nettracer.dilate_3D_dt(nodes, use_dt_dil_amount, xy_scale, z_scale, fast_dil = True)
303
+ return smart_label_watershed(dilated, nodes, directory = None, remove_template = False)
304
+ except:
305
+ print("edt package not found. Please use 'pip install edt' if you would like to enable parallel searching.")
306
+ return smart_dilate_short(nodes, use_dt_dil_amount, directory, xy_scale, z_scale)
299
307
  else:
300
- dilated_binary_nodes, nearest_label_indices, nodes = dilate_3D_dt(nodes, use_dt_dil_amount, GPU = GPU, xy_scaling = xy_scale, z_scaling = z_scale)
301
- binary_nodes = binarize(nodes)
308
+ return smart_dilate_short(nodes, use_dt_dil_amount, directory, xy_scale, z_scale)
302
309
 
303
- # Step 3: Isolate the ring (binary dilated mask minus original binary mask)
304
- ring_mask = dilated_binary_nodes & invert_array(binary_nodes)
305
310
 
306
- del binary_nodes
307
311
 
308
- print("Preforming distance transform for smart search... this step may take some time if computed on CPU...")
309
312
 
310
- if fast_dil:
311
313
 
312
- try:
314
+ def smart_dilate_short(nodes, amount = None, directory = None, xy_scale = 1, z_scale = 1):
313
315
 
314
- if GPU == True and cp.cuda.runtime.getDeviceCount() > 0:
315
- print("GPU detected. Using CuPy for distance transform.")
316
-
317
- try:
318
-
319
- if predownsample is None:
320
-
321
- # Step 4: Find the nearest label for each voxel in the ring
322
- nearest_label_indices = compute_distance_transform_GPU(invert_array(nodes))
323
-
324
- else:
325
- gotoexcept = 1/0
326
-
327
- except (cp.cuda.memory.OutOfMemoryError, ZeroDivisionError) as e:
328
- if predownsample is None:
329
- down_factor = catch_memory(e) #Obtain downsample amount based on memory missing
330
- else:
331
- down_factor = (predownsample)**3
332
-
333
- while True:
334
- downsample_needed = down_factor**(1./3.)
335
- small_nodes = nettracer.downsample(nodes, downsample_needed) #Apply downsample
336
- try:
337
- nearest_label_indices = compute_distance_transform_GPU(invert_array(small_nodes)) #Retry dt on downsample
338
- print(f"Using {down_factor} downsample ({downsample_needed} in each dim - Largest possible with this GPU unless user specified downsample)")
339
- break
340
- except cp.cuda.memory.OutOfMemoryError:
341
- down_factor += 1
342
- binary_nodes = binarize(small_nodes) #Recompute variables for downsample
343
- dilated_mask = dilated_binary_nodes #Need this for later to stamp out the correct output
344
- dilated_binary_nodes = dilate_3D(binary_nodes, 2 + round_to_odd(dilate_xy/downsample_needed), 2 + round_to_odd(dilate_xy/downsample_needed), 2 + round_to_odd(dilate_z/downsample_needed)) #Mod dilation to recompute variables for downsample while also over dilatiing
345
-
346
- ring_mask = dilated_binary_nodes & invert_array(binary_nodes)
347
- nodes = small_nodes
348
- del small_nodes
349
- else:
350
- goto_except = 1/0
351
- except Exception as e:
352
- print("GPU dt failed or did not detect GPU (cupy must be installed with a CUDA toolkit setup...). Computing CPU distance transform instead.")
353
- if GPU:
354
- print(f"Error message: {str(e)}")
355
- nearest_label_indices = compute_distance_transform(invert_array(nodes))
316
+ original_shape = nodes.shape
317
+
318
+ print("Performing distance transform for smart search...")
356
319
 
320
+ dilated_binary_nodes, nearest_label_indices, nodes = dilate_3D_dt(nodes, amount, xy_scaling = xy_scale, z_scaling = z_scale)
321
+ binary_nodes = binarize(nodes)
322
+ ring_mask = dilated_binary_nodes & (~binary_nodes)
323
+ del dilated_binary_nodes
324
+ del binary_nodes
357
325
 
358
326
  # Step 5: Process in parallel chunks using ThreadPoolExecutor
359
327
  num_cores = mp.cpu_count() # Use all available CPU cores
@@ -370,11 +338,6 @@ def smart_dilate(nodes, dilate_xy, dilate_z, directory = None, GPU = True, fast_
370
338
  # Combine results from chunks
371
339
  dilated_nodes_with_labels = np.concatenate(results, axis=1)
372
340
 
373
-
374
- if (dilated_nodes_with_labels.shape[1] < original_shape[1]) and fast_dil: #If downsample was used, upsample output
375
- dilated_nodes_with_labels = nettracer.upsample_with_padding(dilated_nodes_with_labels, downsample_needed, original_shape)
376
- dilated_nodes_with_labels = dilated_nodes_with_labels * dilated_mask
377
-
378
341
  if directory is not None:
379
342
  try:
380
343
  tifffile.imwrite(f"{directory}/search_region.tif", dilated_nodes_with_labels)
@@ -393,7 +356,63 @@ def round_to_odd(number):
393
356
  rounded -= 1
394
357
  return rounded
395
358
 
396
- def smart_label(binary_array, label_array, directory = None, GPU = True, predownsample = None, remove_template = False):
359
+ def smart_label_watershed(binary_array, label_array, directory = None, remove_template = False):
360
+ """
361
+ Watershed-based version - much lower memory footprint
362
+ """
363
+ original_shape = binary_array.shape
364
+
365
+ if type(binary_array) == str or type(label_array) == str:
366
+ string_bool = True
367
+ else:
368
+ string_bool = None
369
+ if type(binary_array) == str:
370
+ binary_array = tifffile.imread(binary_array)
371
+ if type(label_array) == str:
372
+ label_array = tifffile.imread(label_array)
373
+
374
+ # Binarize
375
+ binary_array = binarize(binary_array)
376
+
377
+ print("Performing watershed label propagation...")
378
+
379
+ # Watershed approach: propagate existing labels into the dilated region
380
+ # The labels themselves are the "markers" (seeds)
381
+ # We use the binary mask to define where labels can spread
382
+
383
+ # Simple elevation map: distance from edges (lower = closer to labeled regions)
384
+ # This makes watershed flow from labeled regions outward
385
+ elevation = binary_array.astype(np.float32)
386
+
387
+ # Apply watershed - labels propagate into binary_array region
388
+ dilated_nodes_with_labels = watershed(
389
+ elevation, # Elevation map (flat works fine)
390
+ markers=label_array, # Seed labels
391
+ mask=binary_array, # Where to propagate
392
+ compactness=0 # Pure distance-based (not shape-based)
393
+ )
394
+
395
+ if remove_template:
396
+ dilated_nodes_with_labels *= binary_array
397
+
398
+ if string_bool:
399
+ if directory is not None:
400
+ try:
401
+ tifffile.imwrite(f"{directory}/smart_labelled_array.tif", dilated_nodes_with_labels)
402
+ except Exception as e:
403
+ print(f"Could not save search region file to {directory}")
404
+ else:
405
+ try:
406
+ tifffile.imwrite("smart_labelled_array.tif", dilated_nodes_with_labels)
407
+ except Exception as e:
408
+ print(f"Could not save search region file to active directory")
409
+
410
+ return dilated_nodes_with_labels
411
+
412
+ def smart_label(binary_array, label_array, directory = None, GPU = True, predownsample = None, remove_template = False, mode = 0):
413
+
414
+ if mode == 1:
415
+ return smart_label_watershed(binary_array, label_array, directory, remove_template)
397
416
 
398
417
  original_shape = binary_array.shape
399
418
 
@@ -406,79 +425,93 @@ def smart_label(binary_array, label_array, directory = None, GPU = True, predown
406
425
  if type(label_array) == str:
407
426
  label_array = tifffile.imread(label_array)
408
427
 
409
- # Step 1: Binarize the labeled array
410
- binary_core = binarize(label_array)
428
+ # Binarize inputs
411
429
  binary_array = binarize(binary_array)
430
+
431
+ print("Performing distance transform for smart label...")
412
432
 
413
- # Step 3: Isolate the ring (binary dilated mask minus original binary mask)
414
- ring_mask = binary_array & invert_array(binary_core)
415
-
416
-
433
+ downsample_needed = None # Track if we downsampled
434
+
417
435
  try:
418
-
419
436
  if GPU == True and cp.cuda.runtime.getDeviceCount() > 0:
420
437
  print("GPU detected. Using CuPy for distance transform.")
421
438
 
422
439
  try:
423
-
424
440
  if predownsample is None:
425
-
426
- # Step 4: Find the nearest label for each voxel in the ring
441
+ # Compute binary_core only when needed
442
+ binary_core = binarize(label_array)
427
443
  nearest_label_indices = compute_distance_transform_GPU(invert_array(binary_core))
428
-
444
+ del binary_core # Free immediately after use
429
445
  else:
430
- gotoexcept = 1/0
446
+ raise ZeroDivisionError # Force downsample path
431
447
 
432
448
  except (cp.cuda.memory.OutOfMemoryError, ZeroDivisionError) as e:
433
449
  if predownsample is None:
434
- down_factor = catch_memory(e) #Obtain downsample amount based on memory missing
450
+ down_factor = catch_memory(e)
435
451
  else:
436
452
  down_factor = (predownsample)**3
437
453
 
438
454
  while True:
439
455
  downsample_needed = down_factor**(1./3.)
440
- small_array = nettracer.downsample(label_array, downsample_needed) #Apply downsample
456
+ small_array = nettracer.downsample(label_array, downsample_needed)
441
457
  try:
442
- nearest_label_indices = compute_distance_transform_GPU(invert_array(small_array)) #Retry dt on downsample
443
- print(f"Using {down_factor} downsample ({downsample_needed} in each dim - Largest possible with this GPU unless user specified downsample)")
458
+ binary_core = binarize(small_array)
459
+ nearest_label_indices = compute_distance_transform_GPU(invert_array(binary_core))
460
+ print(f"Using {down_factor} downsample ({downsample_needed} in each dim)")
461
+ del small_array # Don't need small_array anymore
444
462
  break
445
463
  except cp.cuda.memory.OutOfMemoryError:
464
+ del small_array, binary_core # Clean up before retry
446
465
  down_factor += 1
447
- binary_core = binarize(small_array)
448
- label_array = small_array
466
+
467
+ # Update label_array for later use
468
+ label_array = nettracer.downsample(label_array, downsample_needed)
449
469
  binary_small = nettracer.downsample(binary_array, downsample_needed)
450
470
  binary_small = nettracer.dilate_3D_old(binary_small)
451
471
  ring_mask = binary_small & invert_array(binary_core)
452
-
472
+ del binary_small, binary_core # Free after creating ring_mask
453
473
  else:
454
- goto_except = 1/0
474
+ raise Exception("GPU not available")
475
+
455
476
  except Exception as e:
456
477
  if GPU:
457
- print("GPU dt failed or did not detect GPU (cupy must be installed with a CUDA toolkit setup...). Computing CPU distance transform instead.")
478
+ print("GPU dt failed or did not detect GPU. Computing CPU distance transform instead.")
458
479
  print(f"Error message: {str(e)}")
459
480
  import traceback
460
481
  print(traceback.format_exc())
461
- nearest_label_indices = compute_distance_transform(invert_array(label_array))
482
+ binary_core = binarize(label_array)
483
+ nearest_label_indices = compute_distance_transform(invert_array(binary_core))
484
+ del binary_core
462
485
 
463
- print("Preforming distance transform for smart label...")
464
-
465
- # Step 5: Process in parallel chunks using ThreadPoolExecutor
466
- num_cores = mp.cpu_count() # Use all available CPU cores
467
- chunk_size = label_array.shape[1] // num_cores # Divide the array into chunks along the z-axis
486
+ # Compute ring_mask only if not already computed in downsample path
487
+ if 'ring_mask' not in locals():
488
+ binary_core = binarize(label_array)
489
+ ring_mask = binary_array & invert_array(binary_core)
490
+ del binary_core
468
491
 
492
+ # Step 5: Process in parallel chunks
493
+ num_cores = mp.cpu_count()
494
+ chunk_size = label_array.shape[1] // num_cores
469
495
 
470
496
  with ThreadPoolExecutor(max_workers=num_cores) as executor:
471
- args_list = [(i * chunk_size, (i + 1) * chunk_size if i != num_cores - 1 else label_array.shape[1], label_array, ring_mask, nearest_label_indices) for i in range(num_cores)]
497
+ args_list = [(i * chunk_size, (i + 1) * chunk_size if i != num_cores - 1 else label_array.shape[1],
498
+ label_array, ring_mask, nearest_label_indices) for i in range(num_cores)]
472
499
  results = list(executor.map(lambda args: process_chunk(*args), args_list))
473
500
 
474
- # Combine results from chunks
501
+ # Free large arrays no longer needed
502
+ del label_array, ring_mask, nearest_label_indices
503
+
504
+ # Combine results
475
505
  dilated_nodes_with_labels = np.concatenate(results, axis=1)
506
+ del results # Free the list of chunks
476
507
 
477
- if label_array.shape[1] < original_shape[1]: #If downsample was used, upsample output
508
+ if downsample_needed is not None: # If downsample was used
478
509
  dilated_nodes_with_labels = nettracer.upsample_with_padding(dilated_nodes_with_labels, downsample_needed, original_shape)
479
- dilated_nodes_with_labels = dilated_nodes_with_labels * binary_array
510
+ dilated_nodes_with_labels *= binary_array # In-place multiply if possible
480
511
  elif remove_template:
481
- dilated_nodes_with_labels = dilated_nodes_with_labels * binary_array
512
+ dilated_nodes_with_labels *= binary_array # In-place multiply if possible
513
+
514
+ del binary_array # Done with this
482
515
 
483
516
  if string_bool:
484
517
  if directory is not None:
@@ -492,7 +525,6 @@ def smart_label(binary_array, label_array, directory = None, GPU = True, predown
492
525
  except Exception as e:
493
526
  print(f"Could not save search region file to active directory")
494
527
 
495
-
496
528
  return dilated_nodes_with_labels
497
529
 
498
530
  def smart_label_single(binary_array, label_array):
@@ -613,24 +645,97 @@ def compute_distance_transform_distance_GPU(nodes, sampling = [1, 1, 1]):
613
645
  return distance
614
646
 
615
647
 
616
- def compute_distance_transform_distance(nodes, sampling = [1, 1, 1]):
617
-
618
- #print("(Now doing distance transform...)")
648
+ def _run_edt_in_process_shm(input_shm_name, output_shm_name, shape, dtype_str, sampling_tuple):
649
+ """Helper function to run edt in a separate process using shared memory."""
650
+ import edt # Import here to ensure it's available in child process
651
+
652
+ input_shm = shared_memory.SharedMemory(name=input_shm_name)
653
+ output_shm = shared_memory.SharedMemory(name=output_shm_name)
654
+
655
+ try:
656
+ nodes_arr = np.ndarray(shape, dtype=dtype_str, buffer=input_shm.buf)
657
+
658
+ n_cores = mp.cpu_count()
659
+ result = edt.edt(
660
+ nodes_arr.astype(bool),
661
+ anisotropy=sampling_tuple,
662
+ parallel=n_cores
663
+ )
664
+
665
+ result_array = np.ndarray(result.shape, dtype=result.dtype, buffer=output_shm.buf)
666
+ np.copyto(result_array, result)
667
+
668
+ return result.shape, str(result.dtype)
669
+ finally:
670
+ input_shm.close()
671
+ output_shm.close()
619
672
 
673
+ def compute_distance_transform_distance(nodes, sampling=[1, 1, 1], fast_dil=False):
674
+ """
675
+ Compute distance transform with automatic parallelization when available.
676
+
677
+ Args:
678
+ nodes: Binary array (True/1 for objects)
679
+ sampling: Voxel spacing [z, y, x] for anisotropic data
680
+
681
+ Returns:
682
+ Distance transform array
683
+ """
620
684
  is_pseudo_3d = nodes.shape[0] == 1
685
+
621
686
  if is_pseudo_3d:
622
- nodes = np.squeeze(nodes) # Convert to 2D for processing
687
+ nodes = np.squeeze(nodes)
623
688
  sampling = [sampling[1], sampling[2]]
624
-
625
- # Fallback to CPU if there's an issue with GPU computation
626
- distance = distance_transform_edt(nodes, sampling = sampling)
689
+
690
+ if fast_dil:
691
+ try:
692
+ # Use shared memory for all array sizes
693
+ input_shm = shared_memory.SharedMemory(create=True, size=nodes.nbytes)
694
+ output_size = nodes.size * np.dtype(np.float64).itemsize
695
+ output_shm = shared_memory.SharedMemory(create=True, size=output_size)
696
+
697
+ try:
698
+ shm_array = np.ndarray(nodes.shape, dtype=nodes.dtype, buffer=input_shm.buf)
699
+ np.copyto(shm_array, nodes)
700
+
701
+ with ProcessPoolExecutor(max_workers=1) as executor:
702
+ future = executor.submit(
703
+ _run_edt_in_process_shm,
704
+ input_shm.name,
705
+ output_shm.name,
706
+ nodes.shape,
707
+ str(nodes.dtype),
708
+ tuple(sampling)
709
+ )
710
+ result_shape, result_dtype = future.result()
711
+
712
+ distance = np.ndarray(result_shape, dtype=result_dtype, buffer=output_shm.buf).copy()
713
+
714
+ finally:
715
+ input_shm.close()
716
+ input_shm.unlink()
717
+ output_shm.close()
718
+ output_shm.unlink()
719
+
720
+ except Exception as e:
721
+ print(f"Parallel distance transform failed ({e}), falling back to scipy")
722
+ try:
723
+ import edt
724
+ import traceback
725
+ traceback.print_exc() # See the full error
726
+ except:
727
+ print("edt package not found. Please use 'pip install edt' if you would like to enable parallel searching.")
728
+ distance = distance_transform_edt(nodes, sampling=sampling)
729
+ else:
730
+ distance = distance_transform_edt(nodes, sampling=sampling)
731
+
627
732
  if is_pseudo_3d:
628
- distance = np.expand_dims(distance, axis = 0)
733
+ distance = np.expand_dims(distance, axis=0)
734
+
629
735
  return distance
630
736
 
631
737
 
632
738
 
633
-
634
739
  def gaussian(search_region, GPU = True):
635
740
  try:
636
741
  if GPU == True and cp.cuda.runtime.getDeviceCount() > 0: