nettracer3d 0.7.4__py3-none-any.whl → 0.7.5__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.
@@ -32,6 +32,7 @@ class InteractiveSegmenter:
32
32
  self.feature_cache = None
33
33
  self.lock = threading.Lock()
34
34
  self._currently_segmenting = None
35
+ self.use_gpu = True
35
36
 
36
37
  # Current position attributes
37
38
  self.current_z = None
@@ -62,67 +63,298 @@ class InteractiveSegmenter:
62
63
  self.dogs = [(1, 2), (2, 4), (4, 8)]
63
64
  self.master_chunk = 49
64
65
 
66
+ #Data when loading prev model:
67
+ self.previous_foreground = None
68
+ self.previous_background = None
69
+ self.previous_z_fore = None
70
+ self.previous_z_back = None
71
+
72
+ def segment_slice_chunked(self, slice_z, block_size=49):
73
+ """
74
+ A completely standalone method to segment a single z-slice in chunks
75
+ with improved safeguards.
76
+ """
77
+ # Check if we're already processing this slice
78
+ if self._currently_processing and self._currently_processing == slice_z:
79
+ return
80
+
81
+ # Set processing flag with the slice we're processing
82
+ self._currently_processing = slice_z
83
+
84
+ try:
85
+ # First attempt to get the feature map
86
+ feature_map = None
87
+
88
+ try:
89
+ if slice_z in self.feature_cache:
90
+ feature_map = self.feature_cache[slice_z]
91
+ elif hasattr(self, 'map_slice') and self.map_slice is not None and slice_z == self.current_z:
92
+ feature_map = self.map_slice
93
+ else:
94
+ # Generate new feature map
95
+ try:
96
+ feature_map = self.get_feature_map_slice(slice_z, self.current_speed, False)
97
+ self.map_slice = feature_map
98
+ except Exception as e:
99
+ print(f"Error generating feature map: {e}")
100
+ import traceback
101
+ traceback.print_exc()
102
+ return # Exit if we can't generate the feature map
103
+ except:
104
+ # Generate new feature map
105
+ try:
106
+ feature_map = self.get_feature_map_slice(slice_z, self.current_speed, False)
107
+ self.map_slice = feature_map
108
+ except Exception as e:
109
+ print(f"Error generating feature map: {e}")
110
+ import traceback
111
+ traceback.print_exc()
112
+ return # Exit if we can't generate the feature map
113
+
114
+ # Check that we have a valid feature map
115
+ if feature_map is None:
116
+ return
117
+
118
+ # Get dimensions of the slice
119
+ y_size, x_size = self.image_3d.shape[1], self.image_3d.shape[2]
120
+ chunk_count = 0
121
+
122
+ # Determine if feature_map is a CuPy array
123
+ is_cupy_array = hasattr(feature_map, 'get')
124
+
125
+ # Process in blocks for chunked feedback
126
+ for y_start in range(0, y_size, block_size):
127
+ if self._currently_processing != slice_z:
128
+ return
129
+
130
+ for x_start in range(0, x_size, block_size):
131
+ if self._currently_processing != slice_z:
132
+ return
133
+
134
+ y_end = min(y_start + block_size, y_size)
135
+ x_end = min(x_start + block_size, x_size)
136
+
137
+ # Create coordinates and features for this block
138
+ coords = []
139
+ features_list = []
140
+
141
+ for y in range(y_start, y_end):
142
+ for x in range(x_start, x_end):
143
+ coords.append((slice_z, y, x))
144
+ features_list.append(feature_map[y, x])
145
+
146
+ # Convert features to NumPy properly based on type
147
+ if is_cupy_array:
148
+ # If feature_map is a CuPy array, we need to extract a CuPy array
149
+ # from the list and then convert it to NumPy
150
+ try:
151
+ # Create a CuPy array from the list of feature vectors
152
+ features_array = cp.stack(features_list)
153
+ # Convert to NumPy explicitly using .get()
154
+ features = features_array.get()
155
+ except Exception as e:
156
+ print(f"Error converting features to NumPy: {e}")
157
+ # Fallback: convert each feature individually
158
+ features = []
159
+ for feat in features_list:
160
+ if hasattr(feat, 'get'):
161
+ features.append(feat.get())
162
+ else:
163
+ features.append(feat)
164
+ else:
165
+ # If it's already a NumPy array, we can use it directly
166
+ features = features_list
167
+
168
+ # Skip empty blocks
169
+ if not coords:
170
+ continue
171
+
172
+ # Predict
173
+ try:
174
+ try:
175
+ predictions = self.model.predict(features)
176
+ except ValueError:
177
+ self.feature_cache = None
178
+ self.map_slice = None
179
+ return None, None
180
+
181
+ # Split results
182
+ foreground = set()
183
+ background = set()
184
+
185
+ for coord, pred in zip(coords, predictions):
186
+ if pred:
187
+ foreground.add(coord)
188
+ else:
189
+ background.add(coord)
190
+
191
+ # Yield this chunk
192
+ chunk_count += 1
193
+ yield foreground, background
194
+
195
+ except Exception as e:
196
+ print(f"Error processing chunk: {e}")
197
+ import traceback
198
+ traceback.print_exc()
199
+
200
+ finally:
201
+ # Only clear if we're still processing the same slice
202
+ # (otherwise, another slice might have taken over)
203
+ if self._currently_processing == slice_z:
204
+ self._currently_processing = None
205
+
65
206
  def process_chunk(self, chunk_coords):
66
207
  """Process a chunk staying in CuPy as much as possible"""
67
208
 
68
209
  foreground_coords = [] # Keep as list of CuPy coordinates
69
210
  background_coords = []
211
+
212
+ if self.previewing or not self.use_two:
70
213
 
71
- if self.realtimechunks is None:
72
- z_min, z_max = chunk_coords[0], chunk_coords[1]
73
- y_min, y_max = chunk_coords[2], chunk_coords[3]
74
- x_min, x_max = chunk_coords[4], chunk_coords[5]
75
-
76
- # Create meshgrid using CuPy - already good
77
- z_range = cp.arange(z_min, z_max)
78
- y_range = cp.arange(y_min, y_max)
79
- x_range = cp.arange(x_min, x_max)
80
-
81
- # More efficient way to create coordinates
82
- chunk_coords_array = cp.stack(cp.meshgrid(
83
- z_range, y_range, x_range, indexing='ij'
84
- )).reshape(3, -1).T
214
+ if self.realtimechunks is None:
215
+ z_min, z_max = chunk_coords[0], chunk_coords[1]
216
+ y_min, y_max = chunk_coords[2], chunk_coords[3]
217
+ x_min, x_max = chunk_coords[4], chunk_coords[5]
218
+
219
+ # Create meshgrid using CuPy - already good
220
+ z_range = cp.arange(z_min, z_max)
221
+ y_range = cp.arange(y_min, y_max)
222
+ x_range = cp.arange(x_min, x_max)
223
+
224
+ # More efficient way to create coordinates
225
+ chunk_coords_array = cp.stack(cp.meshgrid(
226
+ z_range, y_range, x_range, indexing='ij'
227
+ )).reshape(3, -1).T
228
+
229
+ # Keep as CuPy array instead of converting to list
230
+ chunk_coords_gpu = chunk_coords_array
231
+ else:
232
+ # Convert list to CuPy array once
233
+ chunk_coords_gpu = cp.array(chunk_coords)
234
+ z_coords = chunk_coords_gpu[:, 0]
235
+ y_coords = chunk_coords_gpu[:, 1]
236
+ x_coords = chunk_coords_gpu[:, 2]
237
+
238
+ z_min, z_max = cp.min(z_coords).item(), cp.max(z_coords).item()
239
+ y_min, y_max = cp.min(y_coords).item(), cp.max(y_coords).item()
240
+ x_min, x_max = cp.min(x_coords).item(), cp.max(x_coords).item()
85
241
 
86
- # Keep as CuPy array instead of converting to list
87
- chunk_coords_gpu = chunk_coords_array
88
- else:
89
- # Convert list to CuPy array once
90
- chunk_coords_gpu = cp.array(chunk_coords)
91
- z_coords = chunk_coords_gpu[:, 0]
92
- y_coords = chunk_coords_gpu[:, 1]
93
- x_coords = chunk_coords_gpu[:, 2]
94
-
95
- z_min, z_max = cp.min(z_coords).item(), cp.max(z_coords).item()
96
- y_min, y_max = cp.min(y_coords).item(), cp.max(y_coords).item()
97
- x_min, x_max = cp.min(x_coords).item(), cp.max(x_coords).item()
98
-
99
- # Extract subarray - already good
100
- subarray = self.image_3d[z_min:z_max+1, y_min:y_max+1, x_min:x_max+1]
101
-
102
- # Compute features
103
- if self.speed:
104
- feature_map = self.compute_feature_maps_gpu(subarray)
242
+ # Extract subarray - already good
243
+ subarray = self.image_3d[z_min:z_max+1, y_min:y_max+1, x_min:x_max+1]
244
+
245
+ # Compute features
246
+ if self.speed:
247
+ feature_map = self.compute_feature_maps_gpu(subarray)
248
+ else:
249
+ feature_map = self.compute_deep_feature_maps_gpu(subarray)
250
+
251
+ # Extract features more efficiently
252
+ local_coords = chunk_coords_gpu.copy()
253
+ local_coords[:, 0] -= z_min
254
+ local_coords[:, 1] -= y_min
255
+ local_coords[:, 2] -= x_min
256
+
257
+ # Vectorized feature extraction
258
+ features_gpu = feature_map[local_coords[:, 0], local_coords[:, 1], local_coords[:, 2]]
259
+
260
+ features_cpu = cp.asnumpy(features_gpu)
261
+ predictions = self.model.predict(features_cpu)
262
+
263
+ # Keep coordinates as CuPy arrays
264
+ pred_mask = cp.array(predictions, dtype=bool)
265
+ foreground_coords = chunk_coords_gpu[pred_mask]
266
+ background_coords = chunk_coords_gpu[~pred_mask]
267
+
105
268
  else:
106
- feature_map = self.compute_deep_feature_maps_gpu(subarray)
269
+ # 2D implementation for GPU
270
+ foreground_coords = []
271
+ background_coords = []
272
+
273
+ # Check if chunk_coords is in the 2D format [z, y_start, y_end, x_start, x_end]
274
+ if len(chunk_coords) == 5:
275
+ z = chunk_coords[0]
276
+ y_start = chunk_coords[1]
277
+ y_end = chunk_coords[2]
278
+ x_start = chunk_coords[3]
279
+ x_end = chunk_coords[4]
280
+
281
+ # Generate coordinates for this slice or subchunk using the new function
282
+ coords_array = self.twodim_coords(z, y_start, y_end, x_start, x_end)
283
+
284
+ # Get the feature map for this z-slice
285
+ if self.feature_cache is None:
286
+ feature_map = self.get_feature_map_slice(z, self.speed, True) # Use GPU
287
+ elif z not in self.feature_cache and not self.previewing:
288
+ feature_map = self.get_feature_map_slice(z, self.speed, True) # Use GPU
289
+ elif (z not in self.feature_cache or self.feature_cache is None) and self.previewing:
290
+ feature_map = self.map_slice
291
+ if feature_map is None:
292
+ return [], []
293
+ else:
294
+ feature_map = self.feature_cache[z]
295
+
296
+ # Check if we have a valid feature map
297
+ if feature_map is None:
298
+ return [], []
299
+
300
+ # Extract y and x coordinates from the array
301
+ y_indices = coords_array[:, 1]
302
+ x_indices = coords_array[:, 2]
303
+
304
+ # Extract features using CuPy indexing
305
+ features_gpu = feature_map[y_indices, x_indices]
306
+
307
+ # Convert to NumPy for the model
308
+ features_cpu = features_gpu.get()
309
+
310
+ # Make predictions
311
+ predictions = self.model.predict(features_cpu)
312
+
313
+ # Create CuPy boolean mask from predictions
314
+ pred_mask = cp.array(predictions, dtype=bool)
315
+
316
+ # Split into foreground and background using the mask
317
+ fore_coords = coords_array[pred_mask]
318
+ back_coords = coords_array[~pred_mask]
319
+
320
+ return fore_coords, back_coords
321
+
322
+ return foreground_coords, background_coords
323
+
324
+ def twodim_coords(self, z, y_start, y_end, x_start, x_end):
325
+ """
326
+ Generate 2D coordinates for a z-slice using CuPy for GPU acceleration.
327
+
328
+ Args:
329
+ z (int): Z-slice index
330
+ y_start (int): Start index for y dimension
331
+ y_end (int): End index for y dimension
332
+ x_start (int): Start index for x dimension
333
+ x_end (int): End index for x dimension
334
+
335
+ Returns:
336
+ CuPy array of coordinates in format (z, y, x)
337
+ """
338
+ import cupy as cp
107
339
 
108
- # Extract features more efficiently
109
- local_coords = chunk_coords_gpu.copy()
110
- local_coords[:, 0] -= z_min
111
- local_coords[:, 1] -= y_min
112
- local_coords[:, 2] -= x_min
340
+ # Create ranges for y and x dimensions
341
+ y_range = cp.arange(y_start, y_end, dtype=int)
342
+ x_range = cp.arange(x_start, x_end, dtype=int)
113
343
 
114
- # Vectorized feature extraction
115
- features_gpu = feature_map[local_coords[:, 0], local_coords[:, 1], local_coords[:, 2]]
344
+ # Create meshgrid
345
+ y_coords, x_coords = cp.meshgrid(y_range, x_range, indexing='ij')
116
346
 
117
- features_cpu = cp.asnumpy(features_gpu)
118
- predictions = self.model.predict(features_cpu)
347
+ # Calculate total size
348
+ total_size = len(y_range) * len(x_range)
119
349
 
120
- # Keep coordinates as CuPy arrays
121
- pred_mask = cp.array(predictions, dtype=bool)
122
- foreground_coords = chunk_coords_gpu[pred_mask]
123
- background_coords = chunk_coords_gpu[~pred_mask]
350
+ # Stack coordinates with z values
351
+ slice_coords = cp.column_stack((
352
+ cp.full(total_size, z, dtype=int),
353
+ y_coords.ravel(),
354
+ x_coords.ravel()
355
+ ))
124
356
 
125
- return foreground_coords, background_coords
357
+ return slice_coords
126
358
 
127
359
  def compute_feature_maps_gpu(self, image_3d=None):
128
360
  """Compute feature maps using GPU with CuPy"""
@@ -233,6 +465,158 @@ class InteractiveSegmenter:
233
465
 
234
466
  return cp.stack(features, axis=-1)
235
467
 
468
+
469
+ def compute_feature_maps_gpu_2d(self, z=None):
470
+ """Compute feature maps for 2D images using GPU with CuPy"""
471
+ import cupy as cp
472
+ import cupyx.scipy.ndimage as cupy_ndimage
473
+
474
+ # Extract 2D slice if z is provided, otherwise use the image directly
475
+ if z is not None:
476
+ image_2d = cp.asarray(self.image_3d[z, :, :])
477
+ else:
478
+ # Assuming image_2d is already available or passed
479
+ image_2d = cp.asarray(self.image_2d)
480
+
481
+ original_shape = image_2d.shape
482
+ features = []
483
+
484
+ # Gaussian smoothing at different scales
485
+ for sigma in self.alphas:
486
+ smooth = cupy_ndimage.gaussian_filter(image_2d, sigma)
487
+ features.append(smooth)
488
+
489
+ # Difference of Gaussians
490
+ for (s1, s2) in self.dogs:
491
+ g1 = cupy_ndimage.gaussian_filter(image_2d, s1)
492
+ g2 = cupy_ndimage.gaussian_filter(image_2d, s2)
493
+ dog = g1 - g2
494
+ features.append(dog)
495
+
496
+ # Gradient computations for 2D
497
+ gx = cupy_ndimage.sobel(image_2d, axis=1, mode='reflect') # x direction
498
+ gy = cupy_ndimage.sobel(image_2d, axis=0, mode='reflect') # y direction
499
+
500
+ # Gradient magnitude (2D version - no z component)
501
+ gradient_magnitude = cp.sqrt(gx**2 + gy**2)
502
+ features.append(gradient_magnitude)
503
+
504
+ # Verify shapes
505
+ for i, feat in enumerate(features):
506
+ if feat.shape != original_shape:
507
+ # Check dimensionality and expand if needed
508
+ if len(feat.shape) < len(original_shape):
509
+ feat_adjusted = feat
510
+ missing_dims = len(original_shape) - len(feat.shape)
511
+ for _ in range(missing_dims):
512
+ feat_adjusted = cp.expand_dims(feat_adjusted, axis=0)
513
+
514
+ if feat_adjusted.shape != original_shape:
515
+ raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
516
+
517
+ features[i] = feat_adjusted
518
+
519
+ # Stack features along a new dimension
520
+ result = cp.stack(features, axis=-1)
521
+
522
+ # Optional: Return as numpy array if needed
523
+ # result = cp.asnumpy(result)
524
+
525
+ return result
526
+
527
+ def compute_deep_feature_maps_gpu_2d(self, z=None):
528
+ """Compute 2D feature maps using GPU with CuPy"""
529
+ import cupy as cp
530
+ import cupyx.scipy.ndimage as cupy_ndimage
531
+
532
+ # Extract 2D slice if z is provided, otherwise use the image directly
533
+ if z is not None:
534
+ image_2d = cp.asarray(self.image_3d[z, :, :])
535
+ else:
536
+ # Assuming image_2d is already available or passed
537
+ image_2d = cp.asarray(self.image_2d)
538
+
539
+ original_shape = image_2d.shape
540
+ features = []
541
+
542
+ # Stage 1: Compute all base features
543
+
544
+ # Gaussian smoothing
545
+ gaussian_results = {}
546
+ for sigma in self.alphas:
547
+ smooth = cupy_ndimage.gaussian_filter(image_2d, sigma)
548
+ gaussian_results[sigma] = smooth
549
+ features.append(smooth)
550
+
551
+ # Difference of Gaussians
552
+ for (s1, s2) in self.dogs:
553
+ g1 = cupy_ndimage.gaussian_filter(image_2d, s1)
554
+ g2 = cupy_ndimage.gaussian_filter(image_2d, s2)
555
+ dog = g1 - g2
556
+ features.append(dog)
557
+
558
+ # Local statistics using 2D kernel
559
+ window_size = self.windows
560
+ kernel = cp.ones((window_size, window_size)) / (window_size**2)
561
+
562
+ # Local mean
563
+ local_mean = cupy_ndimage.convolve(image_2d, kernel, mode='reflect')
564
+ features.append(local_mean)
565
+
566
+ # Local variance
567
+ mean = cp.mean(image_2d)
568
+ local_var = cupy_ndimage.convolve((image_2d - mean)**2, kernel, mode='reflect')
569
+ features.append(local_var)
570
+
571
+ # First-order gradients
572
+ gx = cupy_ndimage.sobel(image_2d, axis=1, mode='reflect') # x direction
573
+ gy = cupy_ndimage.sobel(image_2d, axis=0, mode='reflect') # y direction
574
+
575
+ # Gradient magnitude
576
+ gradient_magnitude = cp.sqrt(gx**2 + gy**2)
577
+ features.append(gradient_magnitude)
578
+
579
+ # Stage 2: Compute derived features
580
+
581
+ # Second-order gradients
582
+ gxx = cupy_ndimage.sobel(gx, axis=1, mode='reflect')
583
+ gyy = cupy_ndimage.sobel(gy, axis=0, mode='reflect')
584
+
585
+ # Cross derivatives for Hessian determinant
586
+ gxy = cupy_ndimage.sobel(gx, axis=0, mode='reflect')
587
+ gyx = cupy_ndimage.sobel(gy, axis=1, mode='reflect')
588
+
589
+ # Laplacian (sum of second derivatives)
590
+ laplacian = gxx + gyy
591
+ features.append(laplacian)
592
+
593
+ # Hessian determinant
594
+ hessian_det = gxx * gyy - gxy * gyx
595
+ features.append(hessian_det)
596
+
597
+ # Verify shapes
598
+ for i, feat in enumerate(features):
599
+ if feat.shape != original_shape:
600
+ # Check dimensionality and expand if needed
601
+ if len(feat.shape) < len(original_shape):
602
+ feat_adjusted = feat
603
+ missing_dims = len(original_shape) - len(feat.shape)
604
+ for _ in range(missing_dims):
605
+ feat_adjusted = cp.expand_dims(feat_adjusted, axis=0)
606
+
607
+ if feat_adjusted.shape != original_shape:
608
+ raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
609
+
610
+ features[i] = feat_adjusted
611
+
612
+ # Stack all features along a new dimension
613
+ result = cp.stack(features, axis=-1)
614
+
615
+ # Optional: Return as numpy array if needed
616
+ # result = cp.asnumpy(result)
617
+
618
+ return result
619
+
236
620
  def segment_volume(self, array, chunk_size=None, gpu=True):
237
621
  """Segment volume using parallel processing of chunks with vectorized chunk creation"""
238
622
 
@@ -242,53 +626,129 @@ class InteractiveSegmenter:
242
626
  self.map_slice = None
243
627
  chunk_size = self.master_chunk
244
628
 
245
- # Round to nearest multiple of 32 for better memory alignment
246
- chunk_size = ((chunk_size + 15) // 32) * 32
629
+ def create_2d_chunks():
630
+ """
631
+ Create chunks by z-slices for 2D processing.
632
+ Each chunk is a complete z-slice with all y,x coordinates,
633
+ unless the slice exceeds 262144 pixels, in which case it's divided into subchunks.
634
+
635
+ Returns:
636
+ List of chunks where each chunk contains the parameters needed for processing.
637
+ Format depends on subchunking:
638
+ - No subchunking: [y_dim, x_dim, z, total_pixels, None]
639
+ - Y subchunking: [y_dim, x_dim, z, None, ['y', start_y, end_y]]
640
+ - X subchunking: [y_dim, x_dim, z, None, ['x', start_x, end_x]]
641
+ """
642
+ MAX_CHUNK_SIZE = 262144
643
+ chunks = []
644
+
645
+ for z in range(self.image_3d.shape[0]):
646
+ # Get the dimensions of this z-slice
647
+ y_dim = self.image_3d.shape[1]
648
+ x_dim = self.image_3d.shape[2]
649
+ total_pixels = y_dim * x_dim
650
+
651
+ # If the slice is small enough, do not subchunk
652
+ if total_pixels <= MAX_CHUNK_SIZE:
653
+ chunks.append([z, 0, y_dim, 0, x_dim]) # [z_start, y_start, y_end, x_start, x_end]
654
+ else:
655
+ # Determine which dimension to divide (the largest one)
656
+ largest_dim = 'y' if y_dim >= x_dim else 'x'
657
+
658
+ # Calculate how many divisions we need
659
+ num_divisions = int(cp.ceil(total_pixels / MAX_CHUNK_SIZE))
660
+
661
+ # Calculate the approx size of each division along the largest dimension
662
+ if largest_dim == 'y':
663
+ div_size = int(cp.ceil(y_dim / num_divisions))
664
+ # Create subchunks by dividing the y-dimension
665
+ for i in range(0, y_dim, div_size):
666
+ end_i = min(i + div_size, y_dim)
667
+ chunks.append([z, i, end_i, 0, x_dim]) # [z, y_start, y_end, x_start, x_end]
668
+ else: # largest_dim == 'x'
669
+ div_size = int(cp.ceil(x_dim / num_divisions))
670
+ # Create subchunks by dividing the x-dimension
671
+ for i in range(0, x_dim, div_size):
672
+ end_i = min(i + div_size, x_dim)
673
+ chunks.append([z, 0, y_dim, i, end_i]) # [z, y_start, y_end, x_start, x_end]
674
+
675
+ return chunks
247
676
 
248
- # Calculate number of chunks in each dimension
249
- z_chunks = (self.image_3d.shape[0] + chunk_size - 1) // chunk_size
250
- y_chunks = (self.image_3d.shape[1] + chunk_size - 1) // chunk_size
251
- x_chunks = (self.image_3d.shape[2] + chunk_size - 1) // chunk_size
677
+ print("Chunking data...")
252
678
 
253
- # Create start indices for all chunks at once using CuPy
254
- chunk_starts = cp.array(cp.meshgrid(
255
- cp.arange(z_chunks) * chunk_size,
256
- cp.arange(y_chunks) * chunk_size,
257
- cp.arange(x_chunks) * chunk_size,
258
- indexing='ij'
259
- )).reshape(3, -1).T
679
+ if not self.use_two:
680
+ # 3D Processing - Create chunks for 3D volume
681
+ # Round to nearest multiple of 32 for better memory alignment
682
+ chunk_size = ((chunk_size + 15) // 32) * 32
683
+
684
+ # Calculate number of chunks in each dimension
685
+ z_chunks = (self.image_3d.shape[0] + chunk_size - 1) // chunk_size
686
+ y_chunks = (self.image_3d.shape[1] + chunk_size - 1) // chunk_size
687
+ x_chunks = (self.image_3d.shape[2] + chunk_size - 1) // chunk_size
688
+
689
+ # Create start indices for all chunks at once using CuPy
690
+ chunk_starts = cp.array(cp.meshgrid(
691
+ cp.arange(z_chunks) * chunk_size,
692
+ cp.arange(y_chunks) * chunk_size,
693
+ cp.arange(x_chunks) * chunk_size,
694
+ indexing='ij'
695
+ )).reshape(3, -1).T
696
+
697
+ chunks = []
698
+ for chunk_start_gpu in chunk_starts:
699
+ # Extract values from CuPy array
700
+ z_start = int(chunk_start_gpu[0]) # Convert to regular Python int
701
+ y_start = int(chunk_start_gpu[1])
702
+ x_start = int(chunk_start_gpu[2])
703
+
704
+ z_end = min(z_start + chunk_size, self.image_3d.shape[0])
705
+ y_end = min(y_start + chunk_size, self.image_3d.shape[1])
706
+ x_end = min(x_start + chunk_size, self.image_3d.shape[2])
707
+
708
+ coords = [z_start, z_end, y_start, y_end, x_start, x_end]
709
+ chunks.append(coords)
710
+ else:
711
+ # 2D Processing - Create chunks by z-slices
712
+ chunks = create_2d_chunks()
713
+ self.feature_cache = None # Reset feature cache for 2D processing
260
714
 
261
715
  # Process chunks
262
716
  print("Segmenting chunks...")
263
717
 
264
- for i, chunk_start_gpu in enumerate(chunk_starts):
265
- # Extract values from CuPy array
266
- z_start = int(chunk_start_gpu[0]) # Convert to regular Python int
267
- y_start = int(chunk_start_gpu[1])
268
- x_start = int(chunk_start_gpu[2])
269
-
270
- z_end = min(z_start + chunk_size, self.image_3d.shape[0])
271
- y_end = min(y_start + chunk_size, self.image_3d.shape[1])
272
- x_end = min(x_start + chunk_size, self.image_3d.shape[2])
273
-
274
- coords = [z_start, z_end, y_start, y_end, x_start, x_end]
718
+ for i, chunk in enumerate(chunks):
719
+ # Process chunk - returns CuPy arrays of coordinates
720
+ fore_coords, _ = self.process_chunk(chunk)
275
721
 
276
- # Process chunk - returns CuPy arrays
277
- fore_coords, _ = self.process_chunk(coords)
278
-
279
- if len(fore_coords) > 0:
722
+ if isinstance(fore_coords, list) and len(fore_coords) == 0:
723
+ # Skip empty results
724
+ pass
725
+ elif hasattr(fore_coords, 'shape') and len(fore_coords) > 0:
280
726
  # Direct indexing with CuPy arrays
281
- array[fore_coords[:, 0], fore_coords[:, 1], fore_coords[:, 2]] = 255
727
+ try:
728
+ array[fore_coords[:, 0], fore_coords[:, 1], fore_coords[:, 2]] = 255
729
+ except IndexError as e:
730
+ print(f"Index error when updating array: {e}")
731
+ # Fallback to a safer but slower approach
732
+ for coord in fore_coords:
733
+ try:
734
+ z, y, x = int(coord[0]), int(coord[1]), int(coord[2])
735
+ if 0 <= z < array.shape[0] and 0 <= y < array.shape[1] and 0 <= x < array.shape[2]:
736
+ array[z, y, x] = 255
737
+ except Exception as inner_e:
738
+ print(f"Error updating coordinate {coord}: {inner_e}")
739
+
740
+ # Memory management - release reference to chunk data
741
+ if i % 10 == 0: # Do periodic memory cleanup
742
+ cp.get_default_memory_pool().free_all_blocks()
282
743
 
283
- print(f"Processed {i}/{len(chunk_starts)} chunks")
744
+ print(f"Processed {i+1}/{len(chunks)} chunks")
284
745
 
285
746
  # Clean up GPU memory
286
747
  cp.get_default_memory_pool().free_all_blocks()
287
748
 
288
- # Only convert to NumPy at the very end for return
749
+ # Convert to NumPy at the very end for return
289
750
  return cp.asnumpy(array)
290
751
 
291
-
292
752
  def update_position(self, z=None, x=None, y=None):
293
753
  """Update current position for chunk prioritization with safeguards"""
294
754
 
@@ -379,13 +839,13 @@ class InteractiveSegmenter:
379
839
  """Segment volume in realtime using CuPy for GPU acceleration"""
380
840
  import cupy as cp
381
841
 
382
- try:
383
- from cuml.ensemble import RandomForestClassifier as cuRandomForestClassifier
384
- gpu_ml_available = True
385
- except:
386
- print("Cannot find cuML, using CPU to segment instead...")
387
- gpu_ml_available = False
388
- gpu = False
842
+ #try:
843
+ #from cuml.ensemble import RandomForestClassifier as cuRandomForestClassifier
844
+ #gpu_ml_available = True
845
+ #except:
846
+ #print("Cannot find cuML, using CPU to segment instead...")
847
+ #gpu_ml_available = False
848
+ #gpu = False
389
849
 
390
850
  if self.realtimechunks is None:
391
851
  self.get_realtime_chunks()
@@ -486,11 +946,13 @@ class InteractiveSegmenter:
486
946
  except Exception as e:
487
947
  print(f"Warning: Could not clean up GPU memory: {e}")
488
948
 
489
- def train_batch(self, foreground_array, speed=True, use_gpu=True, use_two=False, mem_lock=False):
949
+ def train_batch(self, foreground_array, speed=True, use_gpu=True, use_two=False, mem_lock=False, saving = False):
490
950
  """Train directly on foreground and background arrays using GPU acceleration"""
491
951
  import cupy as cp
492
-
493
- print("Training model...")
952
+
953
+ if not saving:
954
+ print("Training model...")
955
+
494
956
  self.speed = speed
495
957
  self.cur_gpu = use_gpu
496
958
  self.realtimechunks = None # dump ram
@@ -502,110 +964,323 @@ class InteractiveSegmenter:
502
964
  n_jobs=-1,
503
965
  max_depth=None
504
966
  )
967
+
968
+ if use_two:
969
+
970
+ #changed = [] #Track which slices need feature maps
971
+
972
+ if not self.use_two: #Clarifies if we need to redo feature cache for 2D
973
+ self.feature_cache = None
974
+ self.use_two = True
975
+
976
+ self.feature_cache = None #Decided this should reset, can remove this line to have it retain prev feature maps
977
+ self.two_slices = []
978
+
979
+ if self.feature_cache == None:
980
+ self.feature_cache = {}
981
+
982
+ foreground_array = cp.asarray(foreground_array)
983
+
984
+ # Get foreground coordinates and features
985
+ z_fore, y_fore, x_fore = cp.where(foreground_array == 1)
986
+
987
+ z_fore_cpu = cp.asnumpy(z_fore)
988
+ y_fore_cpu = cp.asnumpy(y_fore)
989
+ x_fore_cpu = cp.asnumpy(x_fore)
990
+
991
+ fore_coords = list(zip(z_fore_cpu, y_fore_cpu, x_fore_cpu))
992
+
993
+ # Get background coordinates and features
994
+ z_back, y_back, x_back = cp.where(foreground_array == 2)
995
+
996
+ z_back_cpu = cp.asnumpy(z_back)
997
+ y_back_cpu = cp.asnumpy(y_back)
998
+ x_back_cpu = cp.asnumpy(x_back)
999
+
1000
+ back_coords = list(zip(z_back_cpu, y_back_cpu, x_back_cpu))
1001
+
1002
+ foreground_features = []
1003
+ background_features = []
1004
+
1005
+ z_fores = self.organize_by_z(fore_coords)
1006
+ z_backs = self.organize_by_z(back_coords)
1007
+ slices = set(list(z_fores.keys()) + list(z_backs.keys()))
1008
+
1009
+ for z in slices:
1010
+
1011
+
1012
+ current_map = self.get_feature_map_slice(z, speed, use_gpu)
1013
+
1014
+ if z in z_fores:
1015
+
1016
+ for y, x in z_fores[z]:
1017
+ # Get the feature vector for this foreground point
1018
+ feature_vector = current_map[y, x]
1019
+
1020
+ # Add to our collection
1021
+ foreground_features.append(cp.asnumpy(feature_vector))
1022
+
1023
+ if z in z_backs:
1024
+
1025
+ for y, x in z_backs[z]:
1026
+ # Get the feature vector for this foreground point
1027
+ feature_vector = current_map[y, x]
1028
+
1029
+ # Add to our collection
1030
+ background_features.append(cp.asnumpy(feature_vector))
1031
+
1032
+ else:
505
1033
 
506
- box_size = self.master_chunk
507
-
508
- # Memory-efficient approach: compute features only for necessary subarrays
509
- foreground_features = []
510
- background_features = []
511
-
512
- # Convert foreground_array to CuPy array
513
- foreground_array_gpu = cp.asarray(foreground_array)
514
-
515
- # Find coordinates of foreground and background scribbles
516
- z_fore = cp.argwhere(foreground_array_gpu == 1)
517
- z_back = cp.argwhere(foreground_array_gpu == 2)
518
-
519
- # Convert back to NumPy for compatibility with the rest of the code
520
- z_fore_cpu = cp.asnumpy(z_fore)
521
- z_back_cpu = cp.asnumpy(z_back)
522
-
523
- # If no scribbles, return empty lists
524
- if len(z_fore_cpu) == 0 and len(z_back_cpu) == 0:
525
- return foreground_features, background_features
526
-
527
- # Get dimensions of the input array
528
- depth, height, width = foreground_array.shape
529
-
530
- # Determine the minimum number of boxes needed to cover all scribbles
531
- half_box = box_size // 2
532
-
533
- # Step 1: Find the minimum set of boxes that cover all scribbles
534
- # We'll divide the volume into a grid of boxes of size box_size
535
-
536
- # Calculate how many boxes are needed in each dimension
537
- z_grid_size = (depth + box_size - 1) // box_size
538
- y_grid_size = (height + box_size - 1) // box_size
539
- x_grid_size = (width + box_size - 1) // box_size
540
-
541
- # Track which grid cells contain scribbles
542
- grid_cells_with_scribbles = set()
543
-
544
- # Map original coordinates to grid cells
545
- for z, y, x in cp.vstack((z_fore_cpu, z_back_cpu)) if len(z_back_cpu) > 0 else z_fore_cpu:
546
- grid_z = int(z // box_size)
547
- grid_y = int(y // box_size)
548
- grid_x = int(x // box_size)
549
- grid_cells_with_scribbles.add((grid_z, grid_y, grid_x))
550
-
551
- # Step 2: Process each grid cell that contains scribbles
552
- for grid_z, grid_y, grid_x in grid_cells_with_scribbles:
553
- # Calculate the boundaries of this grid cell
554
- z_min = grid_z * box_size
555
- y_min = grid_y * box_size
556
- x_min = grid_x * box_size
1034
+ box_size = self.master_chunk
557
1035
 
558
- z_max = min(z_min + box_size, depth)
559
- y_max = min(y_min + box_size, height)
560
- x_max = min(x_min + box_size, width)
1036
+ # Memory-efficient approach: compute features only for necessary subarrays
1037
+ foreground_features = []
1038
+ background_features = []
561
1039
 
562
- # Extract the subarray (assuming image_3d is already a CuPy array)
563
- subarray = self.image_3d[z_min:z_max, y_min:y_max, x_min:x_max]
564
- subarray2 = foreground_array_gpu[z_min:z_max, y_min:y_max, x_min:x_max]
1040
+ # Convert foreground_array to CuPy array
1041
+ foreground_array_gpu = cp.asarray(foreground_array)
565
1042
 
566
- # Compute features for this subarray
567
- if self.speed:
568
- subarray_features = self.compute_feature_maps_gpu(subarray)
569
- else:
570
- subarray_features = self.compute_deep_feature_maps_gpu(subarray)
571
-
572
- # Extract foreground features using a direct mask comparison
573
- local_fore_coords = cp.argwhere(subarray2 == 1)
574
- for local_z, local_y, local_x in cp.asnumpy(local_fore_coords):
575
- feature = subarray_features[int(local_z), int(local_y), int(local_x)]
576
- foreground_features.append(cp.asnumpy(feature))
577
-
578
- # Extract background features using a direct mask comparison
579
- local_back_coords = cp.argwhere(subarray2 == 2)
580
- for local_z, local_y, local_x in cp.asnumpy(local_back_coords):
581
- feature = subarray_features[int(local_z), int(local_y), int(local_x)]
582
- background_features.append(cp.asnumpy(feature))
583
-
584
- # Combine features and labels - convert to NumPy for sklearn compatibility
585
- if foreground_features and background_features:
586
- X = np.vstack([np.array(foreground_features), np.array(background_features)])
587
- y = np.hstack([np.ones(len(z_fore_cpu)), np.zeros(len(z_back_cpu))])
588
- elif foreground_features:
589
- X = np.array(foreground_features)
590
- y = np.ones(len(z_fore_cpu))
591
- elif background_features:
592
- X = np.array(background_features)
593
- y = np.zeros(len(z_back_cpu))
594
- else:
595
- X = np.array([])
596
- y = np.array([])
1043
+ # Find coordinates of foreground and background scribbles
1044
+ z_fore = cp.argwhere(foreground_array_gpu == 1)
1045
+ z_back = cp.argwhere(foreground_array_gpu == 2)
1046
+
1047
+ # Convert back to NumPy for compatibility with the rest of the code
1048
+ z_fore_cpu = cp.asnumpy(z_fore)
1049
+ z_back_cpu = cp.asnumpy(z_back)
1050
+
1051
+ # If no scribbles, return empty lists
1052
+ if len(z_fore_cpu) == 0 and len(z_back_cpu) == 0:
1053
+ return foreground_features, background_features
1054
+
1055
+ # Get dimensions of the input array
1056
+ depth, height, width = foreground_array.shape
1057
+
1058
+ # Determine the minimum number of boxes needed to cover all scribbles
1059
+ half_box = box_size // 2
1060
+
1061
+ # Step 1: Find the minimum set of boxes that cover all scribbles
1062
+ # We'll divide the volume into a grid of boxes of size box_size
1063
+
1064
+ # Calculate how many boxes are needed in each dimension
1065
+ z_grid_size = (depth + box_size - 1) // box_size
1066
+ y_grid_size = (height + box_size - 1) // box_size
1067
+ x_grid_size = (width + box_size - 1) // box_size
1068
+
1069
+ # Track which grid cells contain scribbles
1070
+ grid_cells_with_scribbles = set()
1071
+
1072
+ # Map original coordinates to grid cells
1073
+ for z, y, x in cp.vstack((z_fore_cpu, z_back_cpu)) if len(z_back_cpu) > 0 else z_fore_cpu:
1074
+ grid_z = int(z // box_size)
1075
+ grid_y = int(y // box_size)
1076
+ grid_x = int(x // box_size)
1077
+ grid_cells_with_scribbles.add((grid_z, grid_y, grid_x))
1078
+
1079
+ # Step 2: Process each grid cell that contains scribbles
1080
+ for grid_z, grid_y, grid_x in grid_cells_with_scribbles:
1081
+ # Calculate the boundaries of this grid cell
1082
+ z_min = grid_z * box_size
1083
+ y_min = grid_y * box_size
1084
+ x_min = grid_x * box_size
1085
+
1086
+ z_max = min(z_min + box_size, depth)
1087
+ y_max = min(y_min + box_size, height)
1088
+ x_max = min(x_min + box_size, width)
1089
+
1090
+ # Extract the subarray (assuming image_3d is already a CuPy array)
1091
+ subarray = self.image_3d[z_min:z_max, y_min:y_max, x_min:x_max]
1092
+ subarray2 = foreground_array_gpu[z_min:z_max, y_min:y_max, x_min:x_max]
1093
+
1094
+ # Compute features for this subarray
1095
+ if self.speed:
1096
+ subarray_features = self.compute_feature_maps_gpu(subarray)
1097
+ else:
1098
+ subarray_features = self.compute_deep_feature_maps_gpu(subarray)
1099
+
1100
+ # Extract foreground features using a direct mask comparison
1101
+ local_fore_coords = cp.argwhere(subarray2 == 1)
1102
+ for local_z, local_y, local_x in cp.asnumpy(local_fore_coords):
1103
+ feature = subarray_features[int(local_z), int(local_y), int(local_x)]
1104
+ foreground_features.append(cp.asnumpy(feature))
1105
+
1106
+ # Extract background features using a direct mask comparison
1107
+ local_back_coords = cp.argwhere(subarray2 == 2)
1108
+ for local_z, local_y, local_x in cp.asnumpy(local_back_coords):
1109
+ feature = subarray_features[int(local_z), int(local_y), int(local_x)]
1110
+ background_features.append(cp.asnumpy(feature))
1111
+
1112
+ if self.previous_foreground is not None:
1113
+ failed = True
1114
+ try:
1115
+ # Make sure foreground_features is a NumPy array before vstack
1116
+ if isinstance(foreground_features, list):
1117
+ foreground_features = np.array(foreground_features)
1118
+
1119
+ # Convert CuPy arrays to NumPy if necessary
1120
+ if hasattr(foreground_features, 'get'):
1121
+ foreground_features = foreground_features.get()
1122
+
1123
+ foreground_features = np.vstack([self.previous_foreground, foreground_features])
1124
+ failed = False
1125
+ except Exception as e:
1126
+ pass
1127
+
1128
+ try:
1129
+ # Make sure background_features is a NumPy array before vstack
1130
+ if isinstance(background_features, list):
1131
+ background_features = np.array(background_features)
1132
+
1133
+ # Convert CuPy arrays to NumPy if necessary
1134
+ if hasattr(background_features, 'get'):
1135
+ background_features = background_features.get()
1136
+
1137
+ background_features = np.vstack([self.previous_background, background_features])
1138
+ failed = False
1139
+ except Exception as e:
1140
+ pass
1141
+ try:
1142
+ # Ensure coordinate arrays are NumPy arrays
1143
+ if hasattr(z_fore_cpu, 'get'):
1144
+ z_fore_cpu = z_fore_cpu.get()
1145
+ if hasattr(self.previous_z_fore, 'get'):
1146
+ self.previous_z_fore = self.previous_z_fore.get()
1147
+
1148
+ z_fore_cpu = np.concatenate([self.previous_z_fore, z_fore_cpu])
1149
+ except Exception as e:
1150
+ pass
1151
+ try:
1152
+ # Ensure coordinate arrays are NumPy arrays
1153
+ if hasattr(z_back_cpu, 'get'):
1154
+ z_back_cpu = z_back_cpu.get()
1155
+ if hasattr(self.previous_z_back, 'get'):
1156
+ self.previous_z_back = self.previous_z_back.get()
1157
+
1158
+ z_back_cpu = np.concatenate([self.previous_z_back, z_back_cpu])
1159
+ except Exception as e:
1160
+ pass
1161
+ if failed:
1162
+ 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...")
1163
+
1164
+ if saving:
1165
+ # Make sure to return NumPy arrays, not CuPy arrays
1166
+ if hasattr(foreground_features, 'get'):
1167
+ foreground_features = foreground_features.get()
1168
+ if hasattr(background_features, 'get'):
1169
+ background_features = background_features.get()
1170
+ if hasattr(z_fore_cpu, 'get'):
1171
+ z_fore_cpu = z_fore_cpu.get()
1172
+ if hasattr(z_back_cpu, 'get'):
1173
+ z_back_cpu = z_back_cpu.get()
1174
+
1175
+ return foreground_features, background_features, z_fore_cpu, z_back_cpu
1176
+
1177
+ # Make sure foreground_features and background_features are NumPy arrays
1178
+ if isinstance(foreground_features, list):
1179
+ foreground_features = np.array(foreground_features)
1180
+ elif hasattr(foreground_features, 'get'):
1181
+ foreground_features = foreground_features.get()
1182
+
1183
+ if isinstance(background_features, list):
1184
+ background_features = np.array(background_features)
1185
+ elif hasattr(background_features, 'get'):
1186
+ background_features = background_features.get()
1187
+
1188
+ # Combine features and labels for training
1189
+ X = np.vstack([foreground_features, background_features])
1190
+ y = np.hstack([np.ones(len(z_fore_cpu)), np.zeros(len(z_back_cpu))])
597
1191
 
598
1192
  # Train the model
599
1193
  try:
600
1194
  self.model.fit(X, y)
601
1195
  except Exception as e:
602
1196
  print(f"Error during model training: {e}")
603
- print(X)
604
- print(y)
1197
+ import traceback
605
1198
 
606
1199
  self.current_speed = speed
607
1200
 
608
1201
  # Clean up GPU memory
609
1202
  cp.get_default_memory_pool().free_all_blocks()
610
1203
 
611
- print("Done")
1204
+ print("Done")
1205
+
1206
+
1207
+ def save_model(self, file_name, foreground_array):
1208
+
1209
+ print("Saving model data")
1210
+
1211
+ 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)
1212
+
1213
+
1214
+ np.savez(file_name,
1215
+ foreground_features=foreground_features,
1216
+ background_features=background_features,
1217
+ z_fore=z_fore,
1218
+ z_back=z_back,
1219
+ speed=self.speed,
1220
+ use_gpu=self.use_gpu,
1221
+ use_two=self.use_two,
1222
+ mem_lock=self.mem_lock)
1223
+
1224
+ print(f"Model data saved to {file_name}")
1225
+
1226
+
1227
+ def load_model(self, file_name):
1228
+
1229
+ print("Loading model data")
1230
+
1231
+ data = np.load(file_name)
1232
+
1233
+ # Unpack the arrays
1234
+ self.previous_foreground = data['foreground_features']
1235
+ self.previous_background = data['background_features']
1236
+ self.previous_z_fore = data['z_fore']
1237
+ self.previous_z_back = data['z_back']
1238
+ self.speed = bool(data['speed'])
1239
+ self.use_gpu = bool(data['use_gpu'])
1240
+ self.use_two = bool(data['use_two'])
1241
+ self.mem_lock = bool(data['mem_lock'])
1242
+
1243
+ X = np.vstack([self.previous_foreground, self.previous_background])
1244
+ y = np.hstack([np.ones(len(self.previous_z_fore)), np.zeros(len(self.previous_z_back))])
1245
+
1246
+ try:
1247
+ self.model.fit(X, y)
1248
+ except:
1249
+ print(X)
1250
+ print(y)
1251
+
1252
+ print("Done")
1253
+
1254
+ def get_feature_map_slice(self, z, speed, use_gpu):
1255
+
1256
+ if self._currently_segmenting is not None:
1257
+ return
1258
+
1259
+ if speed:
1260
+ output = self.compute_feature_maps_gpu_2d(z = z)
1261
+
1262
+ elif not speed:
1263
+ output = self.compute_deep_feature_maps_gpu_2d(z = z)
1264
+
1265
+ return output
1266
+
1267
+
1268
+
1269
+ def organize_by_z(self, coordinates):
1270
+ """
1271
+ Organizes a list of [z, y, x] coordinates into a dictionary of [y, x] coordinates grouped by z-value.
1272
+
1273
+ Args:
1274
+ coordinates: List of [z, y, x] coordinate lists
1275
+
1276
+ Returns:
1277
+ Dictionary with z-values as keys and lists of corresponding [y, x] coordinates as values
1278
+ """
1279
+ z_dict = defaultdict(list)
1280
+
1281
+ for z, y, x in coordinates:
1282
+ z_dict[z].append((y, x))
1283
+
1284
+
1285
+ return dict(z_dict) # Convert back to regular dict
1286
+