nettracer3d 0.7.3__py3-none-any.whl → 0.7.4__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.
@@ -0,0 +1,2097 @@
1
+ from sklearn.ensemble import RandomForestClassifier
2
+ import numpy as np
3
+ import concurrent.futures
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ import threading
6
+ from scipy import ndimage
7
+ import multiprocessing
8
+ from collections import defaultdict
9
+
10
+ class InteractiveSegmenter:
11
+ def __init__(self, image_3d, use_gpu=False):
12
+ self.image_3d = image_3d
13
+ self.patterns = []
14
+
15
+ try:
16
+ self.use_gpu = use_gpu and cp.cuda.is_available()
17
+ except:
18
+ self.use_gpu = False
19
+ if self.use_gpu:
20
+ try:
21
+ print(f"Using GPU: {torch.cuda.get_device_name()}")
22
+ except:
23
+ pass
24
+ self.image_gpu = cp.asarray(image_3d)
25
+ try:
26
+ self.model = cuRandomForestClassifier(
27
+ n_estimators=100,
28
+ max_depth=None
29
+ )
30
+ except:
31
+ self.model = RandomForestClassifier(
32
+ n_estimators=100,
33
+ n_jobs=-1,
34
+ max_depth=None
35
+ )
36
+
37
+ else:
38
+
39
+ self.model = RandomForestClassifier(
40
+ n_estimators=100,
41
+ n_jobs=-1,
42
+ max_depth=None
43
+ )
44
+
45
+ self.feature_cache = None
46
+ self.lock = threading.Lock()
47
+ self._currently_segmenting = None
48
+
49
+ # Current position attributes
50
+ self.current_z = None
51
+ self.current_x = None
52
+ self.current_y = None
53
+
54
+ self.realtimechunks = None
55
+ self.current_speed = False
56
+
57
+ # Tracking if we're using 2d or 3d segs
58
+ self.use_two = False
59
+ self.two_slices = []
60
+ self.speed = True
61
+ self.cur_gpu = False
62
+ self.map_slice = None
63
+ self.prev_z = None
64
+ self.previewing = False
65
+
66
+ # flags to track state
67
+ self._currently_processing = False
68
+ self._skip_next_update = False
69
+ self._last_processed_slice = None
70
+ self.mem_lock = False
71
+
72
+ #Adjustable feature map params:
73
+ self.alphas = [1,2,4,8]
74
+ self.windows = 10
75
+ self.dogs = [(1, 2), (2, 4), (4, 8)]
76
+ self.master_chunk = 49
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
+
84
+ def segment_slice_chunked(self, slice_z, block_size = 49):
85
+ """
86
+ A completely standalone method to segment a single z-slice in chunks
87
+ with improved safeguards.
88
+ """
89
+ # Check if we're already processing this slice
90
+ if self._currently_processing and self._currently_processing == slice_z:
91
+ return
92
+
93
+ # Set processing flag with the slice we're processing
94
+ self._currently_processing = slice_z
95
+
96
+ try:
97
+
98
+ # First attempt to get the feature map
99
+ feature_map = None
100
+
101
+ try:
102
+ if slice_z in self.feature_cache:
103
+ feature_map = self.feature_cache[slice_z]
104
+ elif hasattr(self, 'map_slice') and self.map_slice is not None and slice_z == self.current_z:
105
+ feature_map = self.map_slice
106
+ else:
107
+ # Generate new feature map
108
+ try:
109
+ feature_map = self.get_feature_map_slice(slice_z, self.current_speed, False)
110
+ self.map_slice = feature_map
111
+ # Cache the feature map for future use
112
+ #if not hasattr(self, 'feature_cache'):
113
+ #self.feature_cache = {}
114
+ #self.feature_cache[slice_z] = feature_map
115
+ except Exception as e:
116
+ print(f"Error generating feature map: {e}")
117
+ import traceback
118
+ traceback.print_exc()
119
+ return # Exit if we can't generate the feature map
120
+ except:
121
+ # Generate new feature map
122
+ #self.feature_cache = {}
123
+ try:
124
+ feature_map = self.get_feature_map_slice(slice_z, self.current_speed, False)
125
+ self.map_slice = feature_map
126
+ # Cache the feature map for future use
127
+ #if not hasattr(self, 'feature_cache'):
128
+ #self.feature_cache = {}
129
+ #self.feature_cache[slice_z] = feature_map
130
+ except Exception as e:
131
+ print(f"Error generating feature map: {e}")
132
+ import traceback
133
+ traceback.print_exc()
134
+ return # Exit if we can't generate the feature map
135
+
136
+
137
+ # Check that we have a valid feature map
138
+ if feature_map is None:
139
+ return
140
+
141
+ # Get dimensions of the slice
142
+ y_size, x_size = self.image_3d.shape[1], self.image_3d.shape[2]
143
+ chunk_count = 0
144
+
145
+ # Process in blocks for chunked feedback
146
+ for y_start in range(0, y_size, block_size):
147
+ if self._currently_processing != slice_z:
148
+ return
149
+
150
+ for x_start in range(0, x_size, block_size):
151
+ if self._currently_processing != slice_z:
152
+ return
153
+
154
+ y_end = min(y_start + block_size, y_size)
155
+ x_end = min(x_start + block_size, x_size)
156
+
157
+ # Create coordinates and features for this block
158
+ coords = []
159
+ features = []
160
+
161
+ for y in range(y_start, y_end):
162
+ for x in range(x_start, x_end):
163
+ coords.append((slice_z, y, x))
164
+ features.append(feature_map[y, x])
165
+
166
+ # Skip empty blocks
167
+ if not coords:
168
+ continue
169
+
170
+ # Predict
171
+ try:
172
+ try:
173
+ predictions = self.model.predict(features)
174
+ except ValueError:
175
+ self.feature_cache = None
176
+ self.map_slice = None
177
+ return None, None
178
+
179
+ # Split results
180
+ foreground = set()
181
+ background = set()
182
+
183
+ for coord, pred in zip(coords, predictions):
184
+ if pred:
185
+ foreground.add(coord)
186
+ else:
187
+ background.add(coord)
188
+
189
+ # Yield this chunk
190
+ chunk_count += 1
191
+ yield foreground, background
192
+
193
+ except Exception as e:
194
+ print(f"Error processing chunk: {e}")
195
+ import traceback
196
+ traceback.print_exc()
197
+
198
+
199
+ finally:
200
+ # Only clear if we're still processing the same slice
201
+ # (otherwise, another slice might have taken over)
202
+ if self._currently_processing == slice_z:
203
+ self._currently_processing = None
204
+
205
+ def compute_deep_feature_maps_cpu(self, image_3d = None):
206
+ """Compute feature maps using CPU"""
207
+ features = []
208
+ if image_3d is None:
209
+ image_3d = self.image_3d
210
+ original_shape = image_3d.shape
211
+
212
+ # Gaussian and DoG using scipy
213
+ #print("Obtaining gaussians")
214
+ for sigma in self.alphas:
215
+ smooth = ndimage.gaussian_filter(image_3d, sigma)
216
+ features.append(smooth)
217
+
218
+ # Difference of Gaussians
219
+ for (s1, s2) in self.dogs:
220
+ g1 = ndimage.gaussian_filter(image_3d, s1)
221
+ g2 = ndimage.gaussian_filter(image_3d, s2)
222
+ dog = g1 - g2
223
+ features.append(dog)
224
+
225
+ #print("Computing local statistics")
226
+ # Local statistics using scipy's convolve
227
+ window_size = self.windows
228
+ kernel = np.ones((window_size, window_size, window_size)) / (window_size**3)
229
+
230
+ # Local mean
231
+ local_mean = ndimage.convolve(image_3d, kernel, mode='reflect')
232
+ features.append(local_mean)
233
+
234
+ # Local variance
235
+ mean = np.mean(image_3d)
236
+ local_var = ndimage.convolve((image_3d - mean)**2, kernel, mode='reflect')
237
+ features.append(local_var)
238
+
239
+ #print("Computing sobel and gradients")
240
+ # Gradient computations using scipy
241
+ gx = ndimage.sobel(image_3d, axis=2, mode='reflect')
242
+ gy = ndimage.sobel(image_3d, axis=1, mode='reflect')
243
+ gz = ndimage.sobel(image_3d, axis=0, mode='reflect')
244
+
245
+ # Gradient magnitude
246
+ gradient_magnitude = np.sqrt(gx**2 + gy**2 + gz**2)
247
+ features.append(gradient_magnitude)
248
+
249
+ #print("Computing second-order features")
250
+ # Second-order gradients
251
+ gxx = ndimage.sobel(gx, axis=2, mode='reflect')
252
+ gyy = ndimage.sobel(gy, axis=1, mode='reflect')
253
+ gzz = ndimage.sobel(gz, axis=0, mode='reflect')
254
+
255
+ # Laplacian (sum of second derivatives)
256
+ laplacian = gxx + gyy + gzz
257
+ features.append(laplacian)
258
+
259
+ # Hessian determinant
260
+ hessian_det = gxx * gyy * gzz
261
+ features.append(hessian_det)
262
+
263
+ #print("Verifying shapes")
264
+ for i, feat in enumerate(features):
265
+ if feat.shape != original_shape:
266
+ feat_adjusted = np.expand_dims(feat, axis=0)
267
+ if feat_adjusted.shape != original_shape:
268
+ raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
269
+ features[i] = feat_adjusted
270
+
271
+ return np.stack(features, axis=-1)
272
+
273
+ def compute_deep_feature_maps_cpu_parallel(self, image_3d=None):
274
+ """Compute deep feature maps using CPU with thread-based parallelism"""
275
+ if image_3d is None:
276
+ image_3d = self.image_3d
277
+
278
+ original_shape = image_3d.shape
279
+
280
+ # Use ThreadPoolExecutor for parallelization
281
+ with ThreadPoolExecutor(max_workers=min(7, multiprocessing.cpu_count())) as executor:
282
+ # Stage 1: Independent computations that can be parallelized
283
+ futures = []
284
+
285
+ # Gaussian smoothing
286
+ def compute_gaussian(sigma):
287
+ return ndimage.gaussian_filter(image_3d, sigma)
288
+
289
+ for sigma in self.alphas:
290
+ future = executor.submit(compute_gaussian, sigma)
291
+ futures.append(('gaussian', sigma, future))
292
+
293
+ def compute_dog_local(img, s1, s2):
294
+ g1 = ndimage.gaussian_filter(img, s1)
295
+ g2 = ndimage.gaussian_filter(img, s2)
296
+ return g1 - g2
297
+
298
+ # Difference of Gaussians
299
+ for (s1, s2) in self.dogs:
300
+
301
+ future = executor.submit(compute_dog_local, image_3d, s1, s2)
302
+ futures.append(('dog', s1, future))
303
+
304
+ # Local statistics computation
305
+ def compute_local_mean():
306
+ window_size = self.windows
307
+ kernel = np.ones((window_size, window_size, window_size)) / (window_size**3)
308
+ return ndimage.convolve(image_3d, kernel, mode='reflect')
309
+
310
+ future = executor.submit(compute_local_mean)
311
+ futures.append(('local_mean', None, future))
312
+
313
+ def compute_local_variance():
314
+ window_size = self.windows
315
+ kernel = np.ones((window_size, window_size, window_size)) / (window_size**3)
316
+ mean = np.mean(image_3d)
317
+ return ndimage.convolve((image_3d - mean)**2, kernel, mode='reflect')
318
+
319
+ future = executor.submit(compute_local_variance)
320
+ futures.append(('local_var', None, future))
321
+
322
+ # Gradient computation
323
+ def compute_gradients():
324
+ gx = ndimage.sobel(image_3d, axis=2, mode='reflect')
325
+ gy = ndimage.sobel(image_3d, axis=1, mode='reflect')
326
+ gz = ndimage.sobel(image_3d, axis=0, mode='reflect')
327
+ return gx, gy, gz
328
+
329
+ future = executor.submit(compute_gradients)
330
+ futures.append(('gradients', None, future))
331
+
332
+ # Collect results for the independent computations
333
+ results = {}
334
+ for task_type, params, future in futures:
335
+ try:
336
+ result = future.result()
337
+ if task_type == 'gradients':
338
+ # Store the gradient components separately
339
+ gx, gy, gz = result
340
+ results['gx'] = gx
341
+ results['gy'] = gy
342
+ results['gz'] = gz
343
+ else:
344
+ results[f"{task_type}_{params}" if params is not None else task_type] = result
345
+ except Exception as e:
346
+ raise RuntimeError(f"Error in task {task_type}: {str(e)}")
347
+
348
+ # Stage 2: Dependent computations that need results from Stage 1
349
+ futures = []
350
+
351
+ # Gradient magnitude (depends on gradients)
352
+ def compute_gradient_magnitude(gx, gy, gz):
353
+ return np.sqrt(gx**2 + gy**2 + gz**2)
354
+
355
+ future = executor.submit(compute_gradient_magnitude,
356
+ results['gx'], results['gy'], results['gz'])
357
+ futures.append(('gradient_magnitude', None, future))
358
+
359
+ # Second-order gradients (depend on first gradients)
360
+ def compute_second_derivatives(gx, gy, gz):
361
+ gxx = ndimage.sobel(gx, axis=2, mode='reflect')
362
+ gyy = ndimage.sobel(gy, axis=1, mode='reflect')
363
+ gzz = ndimage.sobel(gz, axis=0, mode='reflect')
364
+ return gxx, gyy, gzz
365
+
366
+ future = executor.submit(compute_second_derivatives,
367
+ results['gx'], results['gy'], results['gz'])
368
+ futures.append(('second_derivatives', None, future))
369
+
370
+ # Collect results for the dependent computations
371
+ for task_type, params, future in futures:
372
+ try:
373
+ result = future.result()
374
+ if task_type == 'second_derivatives':
375
+ # Store the second derivative components separately
376
+ gxx, gyy, gzz = result
377
+ results['gxx'] = gxx
378
+ results['gyy'] = gyy
379
+ results['gzz'] = gzz
380
+ else:
381
+ results[task_type] = result
382
+ except Exception as e:
383
+ raise RuntimeError(f"Error in task {task_type}: {str(e)}")
384
+
385
+ # Stage 3: Final computations that depend on Stage 2 results
386
+ futures = []
387
+
388
+ # Laplacian and Hessian determinant (depend on second derivatives)
389
+ def compute_laplacian(gxx, gyy, gzz):
390
+ return gxx + gyy + gzz
391
+
392
+ future = executor.submit(compute_laplacian,
393
+ results['gxx'], results['gyy'], results['gzz'])
394
+ futures.append(('laplacian', None, future))
395
+
396
+ def compute_hessian_det(gxx, gyy, gzz):
397
+ return gxx * gyy * gzz
398
+
399
+ future = executor.submit(compute_hessian_det,
400
+ results['gxx'], results['gyy'], results['gzz'])
401
+ futures.append(('hessian_det', None, future))
402
+
403
+ # Collect final results
404
+ for task_type, params, future in futures:
405
+ try:
406
+ result = future.result()
407
+ results[task_type] = result
408
+ except Exception as e:
409
+ raise RuntimeError(f"Error in task {task_type}: {str(e)}")
410
+
411
+ # Organize results in the expected order
412
+ features = []
413
+
414
+ # Add Gaussian features
415
+ for sigma in self.alphas:
416
+ features.append(results[f'gaussian_{sigma}'])
417
+
418
+ for sigma in self.dogs:
419
+ features.append(results[f'dog_{sigma[0]}'])
420
+
421
+ # Add local statistics
422
+ features.append(results['local_mean'])
423
+ features.append(results['local_var'])
424
+
425
+ # Add gradient magnitude
426
+ features.append(results['gradient_magnitude'])
427
+
428
+ # Add Laplacian and Hessian determinant
429
+ features.append(results['laplacian'])
430
+ features.append(results['hessian_det'])
431
+
432
+ # Verify shapes
433
+ for i, feat in enumerate(features):
434
+ if feat.shape != original_shape:
435
+ feat_adjusted = np.expand_dims(feat, axis=0)
436
+ if feat_adjusted.shape != original_shape:
437
+ raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
438
+ features[i] = feat_adjusted
439
+
440
+ return np.stack(features, axis=-1)
441
+
442
+ def compute_deep_feature_maps_cpu_2d(self, z = None):
443
+ """Compute 2D feature maps using CPU"""
444
+ features = []
445
+
446
+ image_2d = self.image_3d[z, :, :]
447
+ original_shape = image_2d.shape
448
+
449
+ # Gaussian using scipy
450
+ for sigma in [0.5, 1.0, 2.0, 4.0]:
451
+ smooth = ndimage.gaussian_filter(image_2d, sigma)
452
+ features.append(smooth)
453
+
454
+ # Local statistics using scipy's convolve - adjusted for 2D
455
+ window_size = 5
456
+ kernel = np.ones((window_size, window_size)) / (window_size**2)
457
+
458
+ # Local mean
459
+ local_mean = ndimage.convolve(image_2d, kernel, mode='reflect')
460
+ features.append(local_mean)
461
+
462
+ # Local variance
463
+ mean = np.mean(image_2d)
464
+ local_var = ndimage.convolve((image_2d - mean)**2, kernel, mode='reflect')
465
+ features.append(local_var)
466
+
467
+ # Gradient computations using scipy - adjusted axes for 2D
468
+ gx = ndimage.sobel(image_2d, axis=1, mode='reflect') # x direction
469
+ gy = ndimage.sobel(image_2d, axis=0, mode='reflect') # y direction
470
+
471
+ # Gradient magnitude (2D version)
472
+ gradient_magnitude = np.sqrt(gx**2 + gy**2)
473
+ features.append(gradient_magnitude)
474
+
475
+ # Second-order gradients
476
+ gxx = ndimage.sobel(gx, axis=1, mode='reflect')
477
+ gyy = ndimage.sobel(gy, axis=0, mode='reflect')
478
+
479
+ # Laplacian (sum of second derivatives) - 2D version
480
+ laplacian = gxx + gyy
481
+ features.append(laplacian)
482
+
483
+ # Hessian determinant - 2D version
484
+ hessian_det = gxx * gyy - ndimage.sobel(gx, axis=0, mode='reflect') * ndimage.sobel(gy, axis=1, mode='reflect')
485
+ features.append(hessian_det)
486
+
487
+ for i, feat in enumerate(features):
488
+ if feat.shape != original_shape:
489
+ # Check dimensionality and expand if needed
490
+ if len(feat.shape) < len(original_shape):
491
+ feat_adjusted = feat
492
+ missing_dims = len(original_shape) - len(feat.shape)
493
+ for _ in range(missing_dims):
494
+ feat_adjusted = np.expand_dims(feat_adjusted, axis=0)
495
+
496
+ if feat_adjusted.shape != original_shape:
497
+ raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
498
+
499
+ features[i] = feat_adjusted
500
+
501
+ return np.stack(features, axis=-1)
502
+
503
+ def compute_deep_feature_maps_cpu_2d_parallel(self, z=None):
504
+ """Compute 2D feature maps using CPU with thread-based parallelism"""
505
+ image_2d = self.image_3d[z, :, :]
506
+ original_shape = image_2d.shape
507
+
508
+ # Use ThreadPoolExecutor for parallelization
509
+ with ThreadPoolExecutor(max_workers=min(7, multiprocessing.cpu_count())) as executor:
510
+ # Stage 1: Independent computations that can be parallelized
511
+ futures = []
512
+
513
+ # Gaussian smoothing
514
+ def compute_gaussian(sigma):
515
+ return ndimage.gaussian_filter(image_2d, sigma)
516
+
517
+ for sigma in self.alphas:
518
+ future = executor.submit(compute_gaussian, sigma)
519
+ futures.append(('gaussian', sigma, future))
520
+
521
+ # Difference of Gaussians
522
+ def compute_dog(s1, s2):
523
+ g1 = ndimage.gaussian_filter(image_2d, s1)
524
+ g2 = ndimage.gaussian_filter(image_2d, s2)
525
+ return g1 - g2
526
+
527
+ dog_pairs = self.dogs
528
+ for (s1, s2) in dog_pairs:
529
+ future = executor.submit(compute_dog, s1, s2)
530
+ futures.append(('dog', s1, future))
531
+
532
+ # Local statistics computation
533
+ def compute_local_mean():
534
+ window_size = self.windows
535
+ kernel = np.ones((window_size, window_size)) / (window_size**2)
536
+ return ndimage.convolve(image_2d, kernel, mode='reflect')
537
+
538
+ future = executor.submit(compute_local_mean)
539
+ futures.append(('local_mean', None, future))
540
+
541
+ def compute_local_variance():
542
+ window_size = self.windows
543
+ kernel = np.ones((window_size, window_size)) / (window_size**2)
544
+ mean = np.mean(image_2d)
545
+ return ndimage.convolve((image_2d - mean)**2, kernel, mode='reflect')
546
+
547
+ future = executor.submit(compute_local_variance)
548
+ futures.append(('local_var', None, future))
549
+
550
+ # Gradient computation
551
+ def compute_gradients():
552
+ gx = ndimage.sobel(image_2d, axis=1, mode='reflect') # x direction
553
+ gy = ndimage.sobel(image_2d, axis=0, mode='reflect') # y direction
554
+ return gx, gy
555
+
556
+ future = executor.submit(compute_gradients)
557
+ futures.append(('gradients', None, future))
558
+
559
+ # Collect results for the independent computations
560
+ results = {}
561
+ for task_type, params, future in futures:
562
+ try:
563
+ result = future.result()
564
+ if task_type == 'gradients':
565
+ # Store the gradient components separately
566
+ gx, gy = result
567
+ results['gx'] = gx
568
+ results['gy'] = gy
569
+ else:
570
+ results[f"{task_type}_{params}" if params is not None else task_type] = result
571
+ except Exception as e:
572
+ raise RuntimeError(f"Error in task {task_type}: {str(e)}")
573
+
574
+ # Stage 2: Dependent computations that need results from Stage 1
575
+ futures = []
576
+
577
+ # Gradient magnitude (depends on gradients)
578
+ def compute_gradient_magnitude(gx, gy):
579
+ return np.sqrt(gx**2 + gy**2)
580
+
581
+ future = executor.submit(compute_gradient_magnitude, results['gx'], results['gy'])
582
+ futures.append(('gradient_magnitude', None, future))
583
+
584
+ # Second-order gradients (depend on first gradients)
585
+ def compute_second_derivatives(gx, gy):
586
+ gxx = ndimage.sobel(gx, axis=1, mode='reflect')
587
+ gyy = ndimage.sobel(gy, axis=0, mode='reflect')
588
+ # Cross derivatives for Hessian determinant
589
+ gxy = ndimage.sobel(gx, axis=0, mode='reflect')
590
+ gyx = ndimage.sobel(gy, axis=1, mode='reflect')
591
+ return gxx, gyy, gxy, gyx
592
+
593
+ future = executor.submit(compute_second_derivatives, results['gx'], results['gy'])
594
+ futures.append(('second_derivatives', None, future))
595
+
596
+ # Collect results for the dependent computations
597
+ for task_type, params, future in futures:
598
+ try:
599
+ result = future.result()
600
+ if task_type == 'second_derivatives':
601
+ # Store the second derivative components separately
602
+ gxx, gyy, gxy, gyx = result
603
+ results['gxx'] = gxx
604
+ results['gyy'] = gyy
605
+ results['gxy'] = gxy
606
+ results['gyx'] = gyx
607
+ else:
608
+ results[task_type] = result
609
+ except Exception as e:
610
+ raise RuntimeError(f"Error in task {task_type}: {str(e)}")
611
+
612
+ # Stage 3: Final computations that depend on Stage 2 results
613
+ futures = []
614
+
615
+ # Laplacian and Hessian determinant (depend on second derivatives)
616
+ def compute_laplacian(gxx, gyy):
617
+ return gxx + gyy
618
+
619
+ future = executor.submit(compute_laplacian, results['gxx'], results['gyy'])
620
+ futures.append(('laplacian', None, future))
621
+
622
+ def compute_hessian_det(gxx, gyy, gxy, gyx):
623
+ return gxx * gyy - gxy * gyx
624
+
625
+ future = executor.submit(compute_hessian_det,
626
+ results['gxx'], results['gyy'],
627
+ results['gxy'], results['gyx'])
628
+ futures.append(('hessian_det', None, future))
629
+
630
+ # Collect final results
631
+ for task_type, params, future in futures:
632
+ try:
633
+ result = future.result()
634
+ results[task_type] = result
635
+ except Exception as e:
636
+ raise RuntimeError(f"Error in task {task_type}: {str(e)}")
637
+
638
+ # Organize results in the expected order
639
+ features = []
640
+
641
+ # Add Gaussian features
642
+ for sigma in self.alphas:
643
+ features.append(results[f'gaussian_{sigma}'])
644
+
645
+ for sigma in self.dogs:
646
+ features.append(results[f'dog_{sigma[0]}'])
647
+
648
+ # Add local statistics
649
+ features.append(results['local_mean'])
650
+ features.append(results['local_var'])
651
+
652
+ # Add gradient magnitude
653
+ features.append(results['gradient_magnitude'])
654
+
655
+ # Add Laplacian and Hessian determinant
656
+ features.append(results['laplacian'])
657
+ features.append(results['hessian_det'])
658
+
659
+ # Verify shapes
660
+ for i, feat in enumerate(features):
661
+ if feat.shape != original_shape:
662
+ # Check dimensionality and expand if needed
663
+ if len(feat.shape) < len(original_shape):
664
+ feat_adjusted = feat
665
+ missing_dims = len(original_shape) - len(feat.shape)
666
+ for _ in range(missing_dims):
667
+ feat_adjusted = np.expand_dims(feat_adjusted, axis=0)
668
+
669
+ if feat_adjusted.shape != original_shape:
670
+ raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
671
+
672
+ features[i] = feat_adjusted
673
+
674
+ return np.stack(features, axis=-1)
675
+
676
+ def compute_feature_maps(self):
677
+ """Compute all feature maps using GPU acceleration"""
678
+ #if not self.use_gpu:
679
+ #return super().compute_feature_maps()
680
+
681
+ features = []
682
+ image = self.image_gpu
683
+ image_3d = self.image_3d
684
+ original_shape = self.image_3d.shape
685
+
686
+
687
+
688
+ # Gaussian smoothing at different scales
689
+ print("Obtaining gaussians")
690
+ for sigma in [0.5, 1.0, 2.0, 4.0]:
691
+ smooth = cp.asnumpy(self.gaussian_filter_gpu(image, sigma))
692
+ features.append(smooth)
693
+
694
+ print("Obtaining dif of gaussians")
695
+
696
+ # Difference of Gaussians
697
+ for (s1, s2) in [(1, 2), (2, 4)]:
698
+ g1 = self.gaussian_filter_gpu(image, s1)
699
+ g2 = self.gaussian_filter_gpu(image, s2)
700
+ dog = cp.asnumpy(g1 - g2)
701
+ features.append(dog)
702
+
703
+ # Convert image to PyTorch tensor for gradient operations
704
+ image_torch = torch.from_numpy(image_3d).cuda()
705
+ image_torch = image_torch.float().unsqueeze(0).unsqueeze(0)
706
+
707
+ # Calculate required padding
708
+ kernel_size = 3
709
+ padding = kernel_size // 2
710
+
711
+ # Create a single padded version with same padding
712
+ pad = torch.nn.functional.pad(image_torch, (padding, padding, padding, padding, padding, padding), mode='replicate')
713
+
714
+ print("Computing sobel kernels")
715
+
716
+ # Create sobel kernels
717
+ sobel_x = torch.tensor([-1, 0, 1], device='cuda').float().view(1,1,1,1,3)
718
+ sobel_y = torch.tensor([-1, 0, 1], device='cuda').float().view(1,1,1,3,1)
719
+ sobel_z = torch.tensor([-1, 0, 1], device='cuda').float().view(1,1,3,1,1)
720
+
721
+ # Compute gradients
722
+ print("Computing gradiants")
723
+
724
+ gx = torch.nn.functional.conv3d(pad, sobel_x, padding=0)[:,:,:original_shape[0],:original_shape[1],:original_shape[2]]
725
+ gy = torch.nn.functional.conv3d(pad, sobel_y, padding=0)[:,:,:original_shape[0],:original_shape[1],:original_shape[2]]
726
+ gz = torch.nn.functional.conv3d(pad, sobel_z, padding=0)[:,:,:original_shape[0],:original_shape[1],:original_shape[2]]
727
+
728
+ # Compute gradient magnitude
729
+ print("Computing gradiant mags")
730
+
731
+ gradient_magnitude = torch.sqrt(gx**2 + gy**2 + gz**2)
732
+ gradient_feature = gradient_magnitude.cpu().numpy().squeeze()
733
+
734
+ features.append(gradient_feature)
735
+
736
+ print(features.shape)
737
+
738
+ # Verify shapes
739
+ for i, feat in enumerate(features):
740
+ if feat.shape != original_shape:
741
+ # Create a copy of the feature to modify
742
+ feat_adjusted = np.expand_dims(feat, axis=0)
743
+ if feat_adjusted.shape != original_shape:
744
+ raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
745
+ # Important: Update the original features list with the expanded version
746
+ features[i] = feat_adjusted
747
+
748
+ return np.stack(features, axis=-1)
749
+
750
+ def compute_feature_maps_2d(self, z=None):
751
+ """Compute all feature maps for 2D images using GPU acceleration"""
752
+
753
+ features = []
754
+
755
+ image = self.image_gpu[z, :, :]
756
+ image_2d = self.image_3d[z, :, :]
757
+ original_shape = image_2d.shape
758
+
759
+ # Gaussian smoothing at different scales
760
+ print("Obtaining gaussians")
761
+ for sigma in [0.5, 1.0, 2.0, 4.0]:
762
+ smooth = cp.asnumpy(self.gaussian_filter_gpu(image, sigma))
763
+ features.append(smooth)
764
+
765
+ print("Obtaining diff of gaussians")
766
+ # Difference of Gaussians
767
+ for (s1, s2) in [(1, 2), (2, 4)]:
768
+ g1 = self.gaussian_filter_gpu(image, s1)
769
+ g2 = self.gaussian_filter_gpu(image, s2)
770
+ dog = cp.asnumpy(g1 - g2)
771
+ features.append(dog)
772
+
773
+ # Convert image to PyTorch tensor for gradient operations
774
+ image_torch = torch.from_numpy(image_2d).cuda()
775
+ image_torch = image_torch.float().unsqueeze(0).unsqueeze(0)
776
+
777
+ # Calculate required padding
778
+ kernel_size = 3
779
+ padding = kernel_size // 2
780
+
781
+ # Create a single padded version with same padding
782
+ pad = torch.nn.functional.pad(image_torch, (padding, padding, padding, padding), mode='replicate')
783
+
784
+ print("Computing sobel kernels")
785
+ # Create 2D sobel kernels
786
+ sobel_x = torch.tensor([-1, 0, 1], device='cuda').float().view(1, 1, 1, 3)
787
+ sobel_y = torch.tensor([-1, 0, 1], device='cuda').float().view(1, 1, 3, 1)
788
+
789
+ # Compute gradients
790
+ print("Computing gradients")
791
+ gx = torch.nn.functional.conv2d(pad, sobel_x, padding=0)[:, :, :original_shape[0], :original_shape[1]]
792
+ gy = torch.nn.functional.conv2d(pad, sobel_y, padding=0)[:, :, :original_shape[0], :original_shape[1]]
793
+
794
+ # Compute gradient magnitude (no z component in 2D)
795
+ print("Computing gradient mags")
796
+ gradient_magnitude = torch.sqrt(gx**2 + gy**2)
797
+ gradient_feature = gradient_magnitude.cpu().numpy().squeeze()
798
+
799
+ features.append(gradient_feature)
800
+
801
+ # Verify shapes
802
+ for i, feat in enumerate(features):
803
+ if feat.shape != original_shape:
804
+ # Create a copy of the feature to modify
805
+ feat_adjusted = feat
806
+ # Check dimensionality and expand if needed
807
+ if len(feat.shape) < len(original_shape):
808
+ missing_dims = len(original_shape) - len(feat.shape)
809
+ for _ in range(missing_dims):
810
+ feat_adjusted = np.expand_dims(feat_adjusted, axis=0)
811
+
812
+ if feat_adjusted.shape != original_shape:
813
+ raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
814
+
815
+ # Update the original features list with the adjusted version
816
+ features[i] = feat_adjusted
817
+
818
+ return np.stack(features, axis=-1)
819
+
820
+ def compute_feature_maps_cpu_2d(self, z = None):
821
+ """Compute feature maps for 2D images using CPU"""
822
+
823
+
824
+ features = []
825
+
826
+ image_2d = self.image_3d[z, :, :]
827
+ original_shape = image_2d.shape
828
+
829
+ # Gaussian smoothing at different scales
830
+ for sigma in [0.5, 1.0, 2.0, 4.0]:
831
+ smooth = ndimage.gaussian_filter(image_2d, sigma)
832
+ features.append(smooth)
833
+
834
+ # Difference of Gaussians
835
+ for (s1, s2) in [(1, 2), (2, 4)]:
836
+ g1 = ndimage.gaussian_filter(image_2d, s1)
837
+ g2 = ndimage.gaussian_filter(image_2d, s2)
838
+ dog = g1 - g2
839
+ features.append(dog)
840
+
841
+ # Gradient computations using scipy - note axis changes for 2D
842
+ gx = ndimage.sobel(image_2d, axis=1, mode='reflect') # x direction
843
+ gy = ndimage.sobel(image_2d, axis=0, mode='reflect') # y direction
844
+
845
+ # Gradient magnitude (no z component in 2D)
846
+ gradient_magnitude = np.sqrt(gx**2 + gy**2)
847
+ features.append(gradient_magnitude)
848
+
849
+ # Verify shapes
850
+ for i, feat in enumerate(features):
851
+ if feat.shape != original_shape:
852
+ # Check dimensionality and expand if needed
853
+ if len(feat.shape) < len(original_shape):
854
+ feat_adjusted = feat
855
+ missing_dims = len(original_shape) - len(feat.shape)
856
+ for _ in range(missing_dims):
857
+ feat_adjusted = np.expand_dims(feat_adjusted, axis=0)
858
+
859
+ if feat_adjusted.shape != original_shape:
860
+ raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
861
+
862
+ features[i] = feat_adjusted
863
+
864
+ return np.stack(features, axis=-1)
865
+
866
+ def compute_feature_maps_cpu_2d_parallel(self, z=None):
867
+ """Compute feature maps for 2D images using CPU with thread-based parallelism"""
868
+ image_2d = self.image_3d[z, :, :]
869
+ original_shape = image_2d.shape
870
+
871
+ # Use ThreadPoolExecutor for parallelization
872
+ with ThreadPoolExecutor(max_workers=min(7, multiprocessing.cpu_count())) as executor:
873
+ # Submit tasks for independent computations
874
+ futures = []
875
+
876
+ # Gaussian smoothing at different scales
877
+ def compute_gaussian(sigma):
878
+ return ndimage.gaussian_filter(image_2d, sigma)
879
+
880
+ gaussian_sigmas = self.alphas
881
+ for sigma in gaussian_sigmas:
882
+ future = executor.submit(compute_gaussian, sigma)
883
+ futures.append(('gaussian', sigma, future))
884
+
885
+ # Difference of Gaussians
886
+ def compute_dog(s1, s2):
887
+ g1 = ndimage.gaussian_filter(image_2d, s1)
888
+ g2 = ndimage.gaussian_filter(image_2d, s2)
889
+ return g1 - g2
890
+
891
+ dog_pairs = self.dogs
892
+ for (s1, s2) in dog_pairs:
893
+ future = executor.submit(compute_dog, s1, s2)
894
+ futures.append(('dog', (s1, s2), future))
895
+
896
+ # Gradient computation
897
+ def compute_gradient_magnitude():
898
+ gx = ndimage.sobel(image_2d, axis=1, mode='reflect') # x direction
899
+ gy = ndimage.sobel(image_2d, axis=0, mode='reflect') # y direction
900
+ return np.sqrt(gx**2 + gy**2)
901
+
902
+ future = executor.submit(compute_gradient_magnitude)
903
+ futures.append(('gradient_magnitude', None, future))
904
+
905
+ # Collect results
906
+ results = {}
907
+ for task_type, params, future in futures:
908
+ try:
909
+ result = future.result()
910
+ if params is not None:
911
+ if task_type == 'dog':
912
+ s1, s2 = params
913
+ results[f"{task_type}_{s1}_{s2}"] = result
914
+ else:
915
+ results[f"{task_type}_{params}"] = result
916
+ else:
917
+ results[task_type] = result
918
+ except Exception as e:
919
+ raise RuntimeError(f"Error in task {task_type} with params {params}: {str(e)}")
920
+
921
+ # Organize results in the expected order
922
+ features = []
923
+
924
+ # Add Gaussian features
925
+ for sigma in gaussian_sigmas:
926
+ features.append(results[f'gaussian_{sigma}'])
927
+
928
+ # Add Difference of Gaussians features
929
+ for (s1, s2) in dog_pairs:
930
+ features.append(results[f'dog_{s1}_{s2}'])
931
+
932
+ # Add gradient magnitude
933
+ features.append(results['gradient_magnitude'])
934
+
935
+ # Verify shapes
936
+ for i, feat in enumerate(features):
937
+ if feat.shape != original_shape:
938
+ # Check dimensionality and expand if needed
939
+ if len(feat.shape) < len(original_shape):
940
+ feat_adjusted = feat
941
+ missing_dims = len(original_shape) - len(feat.shape)
942
+ for _ in range(missing_dims):
943
+ feat_adjusted = np.expand_dims(feat_adjusted, axis=0)
944
+
945
+ if feat_adjusted.shape != original_shape:
946
+ raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
947
+
948
+ features[i] = feat_adjusted
949
+
950
+ return np.stack(features, axis=-1)
951
+
952
+ def compute_feature_maps_cpu(self, image_3d = None):
953
+ """Compute feature maps using CPU"""
954
+ features = []
955
+ if image_3d is None:
956
+ image_3d = self.image_3d
957
+
958
+ original_shape = image_3d.shape
959
+
960
+
961
+ # Gaussian smoothing at different scales
962
+ #print("Obtaining gaussians")
963
+ for sigma in self.alphas:
964
+ smooth = ndimage.gaussian_filter(image_3d, sigma)
965
+ features.append(smooth)
966
+
967
+ #print("Obtaining dif of gaussians")
968
+ # Difference of Gaussians
969
+ for (s1, s2) in self.dogs:
970
+ g1 = ndimage.gaussian_filter(image_3d, s1)
971
+ g2 = ndimage.gaussian_filter(image_3d, s2)
972
+ dog = g1 - g2
973
+ features.append(dog)
974
+
975
+ #print("Computing sobel and gradients")
976
+ # Gradient computations using scipy
977
+ gx = ndimage.sobel(image_3d, axis=2, mode='reflect') # x direction
978
+ gy = ndimage.sobel(image_3d, axis=1, mode='reflect') # y direction
979
+ gz = ndimage.sobel(image_3d, axis=0, mode='reflect') # z direction
980
+
981
+ # Gradient magnitude
982
+ #print("Computing gradient magnitude")
983
+ gradient_magnitude = np.sqrt(gx**2 + gy**2 + gz**2)
984
+ features.append(gradient_magnitude)
985
+
986
+ # Verify shapes
987
+ #print("Verifying shapes")
988
+ for i, feat in enumerate(features):
989
+ if feat.shape != original_shape:
990
+ feat_adjusted = np.expand_dims(feat, axis=0)
991
+ if feat_adjusted.shape != original_shape:
992
+ raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
993
+ features[i] = feat_adjusted
994
+
995
+ return np.stack(features, axis=-1)
996
+
997
+ def compute_feature_maps_cpu_parallel(self, image_3d=None):
998
+ """Use ThreadPoolExecutor
999
+
1000
+ While threads don't give true parallelism for CPU-bound tasks due to the GIL,
1001
+ numpy/scipy release the GIL during computation, so this can still be effective.
1002
+ """
1003
+ if image_3d is None:
1004
+ image_3d = self.image_3d
1005
+ original_shape = image_3d.shape
1006
+
1007
+ features = []
1008
+
1009
+ # Using ThreadPoolExecutor which is more compatible with GUI applications
1010
+ with ThreadPoolExecutor(max_workers=min(7, multiprocessing.cpu_count())) as executor:
1011
+ # Submit all tasks to the executor
1012
+ futures = []
1013
+
1014
+ # Gaussian smoothing at different scales
1015
+ for sigma in self.alphas:
1016
+ future = executor.submit(ndimage.gaussian_filter, image_3d, sigma)
1017
+ futures.append(future)
1018
+
1019
+ def compute_dog_local(img, s1, s2):
1020
+ g1 = ndimage.gaussian_filter(img, s1) # Consider just having this return the gaussians to
1021
+ g2 = ndimage.gaussian_filter(img, s2)
1022
+ return g1 - g2
1023
+
1024
+ # Difference of Gaussians
1025
+ for (s1, s2) in self.dogs:
1026
+
1027
+ future = executor.submit(compute_dog_local, image_3d, s1, s2)
1028
+ futures.append(future)
1029
+
1030
+ # Gradient magnitude
1031
+ def compute_gradient_local(img):
1032
+ gx = ndimage.sobel(img, axis=2, mode='reflect')
1033
+ gy = ndimage.sobel(img, axis=1, mode='reflect')
1034
+ gz = ndimage.sobel(img, axis=0, mode='reflect')
1035
+ return np.sqrt(gx**2 + gy**2 + gz**2)
1036
+
1037
+ future = executor.submit(compute_gradient_local, image_3d)
1038
+ futures.append(future)
1039
+
1040
+ # Collect results
1041
+ for future in futures:
1042
+ result = future.result()
1043
+ features.append(result)
1044
+
1045
+ # Verify shapes
1046
+ for i, feat in enumerate(features):
1047
+ if feat.shape != original_shape:
1048
+ feat_adjusted = np.expand_dims(feat, axis=0)
1049
+ if feat_adjusted.shape != original_shape:
1050
+ raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
1051
+ features[i] = feat_adjusted
1052
+
1053
+ return np.stack(features, axis=-1)
1054
+
1055
+ def compute_deep_feature_maps(self):
1056
+ """Compute all feature maps using GPU acceleration"""
1057
+ #if not self.use_gpu:
1058
+ #return super().compute_feature_maps()
1059
+
1060
+ features = []
1061
+ image = self.image_gpu
1062
+ original_shape = self.image_3d.shape
1063
+
1064
+ # Original features (Gaussians and DoG)
1065
+ print("Obtaining gaussians")
1066
+ for sigma in [0.5, 1.0, 2.0, 4.0]:
1067
+ smooth = cp.asnumpy(self.gaussian_filter_gpu(image, sigma))
1068
+ features.append(smooth)
1069
+
1070
+ print("Computing local statistics")
1071
+ image_torch = torch.from_numpy(self.image_3d).cuda()
1072
+ image_torch = image_torch.float().unsqueeze(0).unsqueeze(1) # [1, 1, 1, 512, 384]
1073
+
1074
+ # Create kernel
1075
+ window_size = 5
1076
+ pad = window_size // 2
1077
+
1078
+ if image_torch.shape[2] == 1: # Single slice case
1079
+ # Squeeze out the z dimension for 2D operations
1080
+ image_2d = image_torch.squeeze(2) # Now [1, 1, 512, 384]
1081
+ kernel_2d = torch.ones((1, 1, window_size, window_size), device='cuda')
1082
+ kernel_2d = kernel_2d / (window_size**2)
1083
+
1084
+ # 2D padding and convolution
1085
+ padded = torch.nn.functional.pad(image_2d,
1086
+ (pad, pad, # x dimension
1087
+ pad, pad), # y dimension
1088
+ mode='reflect')
1089
+
1090
+ local_mean = torch.nn.functional.conv2d(padded, kernel_2d)
1091
+ local_mean = local_mean.unsqueeze(2) # Add z dimension back
1092
+ features.append(local_mean.cpu().numpy().squeeze())
1093
+
1094
+ # Local variance
1095
+ mean = torch.mean(image_2d)
1096
+ padded_sq = torch.nn.functional.pad((image_2d - mean)**2,
1097
+ (pad, pad, pad, pad),
1098
+ mode='reflect')
1099
+ local_var = torch.nn.functional.conv2d(padded_sq, kernel_2d)
1100
+ local_var = local_var.unsqueeze(2) # Add z dimension back
1101
+ features.append(local_var.cpu().numpy().squeeze())
1102
+ else:
1103
+ # Original 3D operations for multi-slice case
1104
+ kernel = torch.ones((1, 1, window_size, window_size, window_size), device='cuda')
1105
+ kernel = kernel / (window_size**3)
1106
+
1107
+ padded = torch.nn.functional.pad(image_torch,
1108
+ (pad, pad, # x dimension
1109
+ pad, pad, # y dimension
1110
+ pad, pad), # z dimension
1111
+ mode='reflect')
1112
+ local_mean = torch.nn.functional.conv3d(padded, kernel)
1113
+ features.append(local_mean.cpu().numpy().squeeze())
1114
+
1115
+ mean = torch.mean(image_torch)
1116
+ padded_sq = torch.nn.functional.pad((image_torch - mean)**2,
1117
+ (pad, pad, pad, pad, pad, pad),
1118
+ mode='reflect')
1119
+ local_var = torch.nn.functional.conv3d(padded_sq, kernel)
1120
+ features.append(local_var.cpu().numpy().squeeze())
1121
+
1122
+ # Original gradient computations
1123
+ print("Computing sobel and gradients")
1124
+ kernel_size = 3
1125
+ padding = kernel_size // 2
1126
+ pad = torch.nn.functional.pad(image_torch, (padding,)*6, mode='replicate')
1127
+
1128
+ sobel_x = torch.tensor([-1, 0, 1], device='cuda').float().view(1,1,1,1,3)
1129
+ sobel_y = torch.tensor([-1, 0, 1], device='cuda').float().view(1,1,1,3,1)
1130
+ sobel_z = torch.tensor([-1, 0, 1], device='cuda').float().view(1,1,3,1,1)
1131
+
1132
+ gx = torch.nn.functional.conv3d(pad, sobel_x, padding=0)[:,:,:original_shape[0],:original_shape[1],:original_shape[2]]
1133
+ gy = torch.nn.functional.conv3d(pad, sobel_y, padding=0)[:,:,:original_shape[0],:original_shape[1],:original_shape[2]]
1134
+ gz = torch.nn.functional.conv3d(pad, sobel_z, padding=0)[:,:,:original_shape[0],:original_shape[1],:original_shape[2]]
1135
+
1136
+ gradient_magnitude = torch.sqrt(gx**2 + gy**2 + gz**2)
1137
+ features.append(gradient_magnitude.cpu().numpy().squeeze())
1138
+
1139
+ # Second-order gradients
1140
+ print("Computing second-order features")
1141
+ gxx = torch.nn.functional.conv3d(gx, sobel_x, padding=padding)
1142
+ gyy = torch.nn.functional.conv3d(gy, sobel_y, padding=padding)
1143
+ gzz = torch.nn.functional.conv3d(gz, sobel_z, padding=padding)
1144
+
1145
+ # Get minimum size in each dimension
1146
+ min_size_0 = min(gxx.size(2), gyy.size(2), gzz.size(2))
1147
+ min_size_1 = min(gxx.size(3), gyy.size(3), gzz.size(3))
1148
+ min_size_2 = min(gxx.size(4), gyy.size(4), gzz.size(4))
1149
+
1150
+ # Crop to smallest common size
1151
+ gxx = gxx[:, :, :min_size_0, :min_size_1, :min_size_2]
1152
+ gyy = gyy[:, :, :min_size_0, :min_size_1, :min_size_2]
1153
+ gzz = gzz[:, :, :min_size_0, :min_size_1, :min_size_2]
1154
+
1155
+ laplacian = gxx + gyy + gzz # Second derivatives in each direction
1156
+ features.append(laplacian.cpu().numpy().squeeze())
1157
+
1158
+ # Now they should have matching dimensions for multiplication
1159
+ hessian_det = gxx * gyy * gzz
1160
+ features.append(hessian_det.cpu().numpy().squeeze())
1161
+
1162
+ print("Verifying shapes")
1163
+ for i, feat in enumerate(features):
1164
+ if feat.shape != original_shape:
1165
+ feat_adjusted = np.expand_dims(feat, axis=0)
1166
+ if feat_adjusted.shape != original_shape:
1167
+ raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
1168
+ features[i] = feat_adjusted
1169
+
1170
+ return np.stack(features, axis=-1)
1171
+
1172
+ def gaussian_filter_gpu(self, image, sigma):
1173
+ """GPU-accelerated Gaussian filter"""
1174
+ # Create Gaussian kernel
1175
+ result = cpx.gaussian_filter(image, sigma=sigma)
1176
+
1177
+ return result
1178
+
1179
+ def process_chunk_GPU(self, chunk_coords):
1180
+ """Process a chunk of coordinates using GPU acceleration"""
1181
+ coords = np.array(chunk_coords)
1182
+ z, y, x = coords.T
1183
+
1184
+ # Extract features
1185
+ features = self.feature_cache[z, y, x]
1186
+
1187
+ if self.use_gpu:
1188
+ # Move to GPU
1189
+ features_gpu = cp.array(features)
1190
+
1191
+ # Predict on GPU
1192
+ predictions = self.model.predict(features_gpu)
1193
+ predictions = cp.asnumpy(predictions)
1194
+ else:
1195
+ predictions = self.model.predict(features)
1196
+
1197
+ # Split results
1198
+ foreground_mask = predictions == 1
1199
+ background_mask = ~foreground_mask
1200
+
1201
+ foreground = set(map(tuple, coords[foreground_mask]))
1202
+ background = set(map(tuple, coords[background_mask]))
1203
+
1204
+ return foreground, background
1205
+
1206
+ def organize_by_z(self, coordinates):
1207
+ """
1208
+ Organizes a list of [z, y, x] coordinates into a dictionary of [y, x] coordinates grouped by z-value.
1209
+
1210
+ Args:
1211
+ coordinates: List of [z, y, x] coordinate lists
1212
+
1213
+ Returns:
1214
+ Dictionary with z-values as keys and lists of corresponding [y, x] coordinates as values
1215
+ """
1216
+ z_dict = defaultdict(list)
1217
+
1218
+ for z, y, x in coordinates:
1219
+ z_dict[z].append((y, x))
1220
+
1221
+
1222
+ return dict(z_dict) # Convert back to regular dict
1223
+
1224
+ def process_chunk(self, chunk_coords):
1225
+ """
1226
+ Process a chunk of coordinates, handling both mem_lock and non-mem_lock cases.
1227
+ Uses a consistent approach based on coordinates.
1228
+
1229
+ Parameters:
1230
+ -----------
1231
+ chunk_coords : list of tuples
1232
+ List of (z,y,x) coordinate tuples to process
1233
+
1234
+ Returns:
1235
+ --------
1236
+ tuple : (foreground, background)
1237
+ Sets of coordinates classified as foreground or background
1238
+ """
1239
+ foreground = set()
1240
+ background = set()
1241
+
1242
+ if self.previewing or not self.use_two:
1243
+ if self.mem_lock:
1244
+ # For mem_lock, we need to extract a subarray and compute features
1245
+
1246
+ if self.realtimechunks is None: #Presuming we're segmenting all
1247
+ z_min, z_max = chunk_coords[0], chunk_coords[1]
1248
+ y_min, y_max = chunk_coords[2], chunk_coords[3]
1249
+ x_min, x_max = chunk_coords[4], chunk_coords[5]
1250
+
1251
+ # Consider moving this to process chunk ??
1252
+ chunk_coords = np.stack(np.meshgrid(
1253
+ np.arange(z_min, z_max),
1254
+ np.arange(y_min, y_max),
1255
+ np.arange(x_min, x_max),
1256
+ indexing='ij'
1257
+ )).reshape(3, -1).T
1258
+
1259
+ chunk_coords = (list(map(tuple, chunk_coords)))
1260
+ else: #Presumes we're not segmenting all
1261
+ # Find min/max bounds of the coordinates to get the smallest containing subarray
1262
+ z_coords = [z for z, y, x in chunk_coords]
1263
+ y_coords = [y for z, y, x in chunk_coords]
1264
+ x_coords = [x for z, y, x in chunk_coords]
1265
+
1266
+ z_min, z_max = min(z_coords), max(z_coords)
1267
+ y_min, y_max = min(y_coords), max(y_coords)
1268
+ x_min, x_max = min(x_coords), max(x_coords)
1269
+
1270
+
1271
+ # Extract the subarray
1272
+ subarray = self.image_3d[z_min:z_max+1, y_min:y_max+1, x_min:x_max+1]
1273
+
1274
+ # Compute features for this subarray
1275
+ if self.speed:
1276
+ feature_map = self.compute_feature_maps_cpu_parallel(subarray) #If the interactive segmenter is slow
1277
+ else: #Due to the parallel, consider singleton implementation for it specifically
1278
+ feature_map = self.compute_deep_feature_maps_cpu_parallel(subarray)
1279
+
1280
+ # Extract features for each coordinate, adjusting for subarray offset
1281
+ features = []
1282
+ for z, y, x in chunk_coords:
1283
+ # Transform global coordinates to local subarray coordinates
1284
+ local_z = z - z_min
1285
+ local_y = y - y_min
1286
+ local_x = x - x_min
1287
+
1288
+ # Get feature at this position
1289
+ feature = feature_map[local_z, local_y, local_x]
1290
+ features.append(feature)
1291
+
1292
+ else:
1293
+ # For non-mem_lock, simply use the feature cache
1294
+ features = [self.feature_cache[z, y, x] for z, y, x in chunk_coords]
1295
+
1296
+ # Make predictions
1297
+ predictions = self.model.predict(features)
1298
+
1299
+ # Assign coordinates based on predictions
1300
+ for coord, pred in zip(chunk_coords, predictions):
1301
+ if pred:
1302
+ foreground.add(coord)
1303
+ else:
1304
+ background.add(coord)
1305
+
1306
+ else:
1307
+
1308
+ if self.mem_lock:
1309
+ chunk_coords = self.twodim_coords(chunk_coords[0], chunk_coords[1], chunk_coords[2], chunk_coords[3], chunk_coords[4])
1310
+
1311
+ chunk_coords = self.organize_by_z(chunk_coords)
1312
+
1313
+ for z, coords in chunk_coords.items():
1314
+
1315
+ if self.feature_cache is None:
1316
+ features = self.get_feature_map_slice(z, self.speed, self.cur_gpu)
1317
+ features = [features[y, x] for y, x in coords]
1318
+ elif z not in self.feature_cache and not self.previewing:
1319
+ features = self.get_feature_map_slice(z, self.speed, self.cur_gpu)
1320
+ features = [features[y, x] for y, x in coords]
1321
+ elif z not in self.feature_cache or self.feature_cache is None and self.previewing:
1322
+ features = self.map_slice
1323
+ try:
1324
+ features = [features[y, x] for y, x in coords]
1325
+ except:
1326
+ return [], []
1327
+ else:
1328
+ features = [self.feature_cache[z][y, x] for y, x in coords]
1329
+
1330
+ predictions = self.model.predict(features)
1331
+
1332
+ for (y, x), pred in zip(coords, predictions):
1333
+ coord = (z, y, x) # Reconstruct the 3D coordinate as a tuple
1334
+ if pred:
1335
+ foreground.add(coord)
1336
+ else:
1337
+ background.add(coord)
1338
+
1339
+ return foreground, background
1340
+
1341
+ def twodim_coords(self, y_dim, x_dim, z, chunk_size = None, subrange = None):
1342
+
1343
+ if subrange is None:
1344
+ y_coords, x_coords = np.meshgrid(
1345
+ np.arange(y_dim),
1346
+ np.arange(x_dim),
1347
+ indexing='ij'
1348
+ )
1349
+
1350
+ slice_coords = np.column_stack((
1351
+ np.full(chunk_size, z),
1352
+ y_coords.ravel(),
1353
+ x_coords.ravel()
1354
+ ))
1355
+
1356
+ elif subrange[0] == 'y':
1357
+
1358
+ y_subrange = np.arange(subrange[1], subrange[2])
1359
+
1360
+ # Create meshgrid for this subchunk
1361
+ y_sub, x_sub = np.meshgrid(
1362
+ y_subrange,
1363
+ np.arange(x_dim),
1364
+ indexing='ij'
1365
+ )
1366
+
1367
+ # Create coordinates for this subchunk
1368
+ subchunk_size = len(y_subrange) * x_dim
1369
+ slice_coords = np.column_stack((
1370
+ np.full(subchunk_size, z),
1371
+ y_sub.ravel(),
1372
+ x_sub.ravel()
1373
+ ))
1374
+
1375
+ elif subrange[0] == 'x':
1376
+
1377
+ x_subrange = np.arange(subrange[1], subrange[2])
1378
+
1379
+ # Create meshgrid for this subchunk
1380
+ y_sub, x_sub = np.meshgrid(
1381
+ np.arange(y_dim),
1382
+ x_subrange,
1383
+ indexing='ij'
1384
+ )
1385
+
1386
+ # Create coordinates for this subchunk
1387
+ subchunk_size = y_dim * len(x_subrange)
1388
+ slice_coords = np.column_stack((
1389
+ np.full(subchunk_size, z),
1390
+ y_sub.ravel(),
1391
+ x_sub.ravel()
1392
+ ))
1393
+
1394
+
1395
+
1396
+ return list(map(tuple, slice_coords))
1397
+
1398
+
1399
+
1400
+ def segment_volume(self, chunk_size=None, gpu=False):
1401
+ """Segment volume using parallel processing of chunks with vectorized chunk creation"""
1402
+ #Change the above chunk size to None to have it auto-compute largest chunks (not sure which is faster, 64 seems reasonable in test cases)
1403
+
1404
+ self.realtimechunks = None # Presumably no longer need this.
1405
+ self.map_slice = None
1406
+
1407
+ if self.mem_lock:
1408
+ chunk_size = self.master_chunk #memory efficient chunk
1409
+
1410
+
1411
+ def create_2d_chunks():
1412
+ """
1413
+ Create chunks by z-slices for 2D processing.
1414
+ Each chunk is a complete z-slice with all y,x coordinates,
1415
+ unless the slice exceeds 262144 pixels, in which case it's divided into subchunks.
1416
+
1417
+ Returns:
1418
+ List of chunks, where each chunk contains the coordinates for one z-slice or subchunk
1419
+ """
1420
+ MAX_CHUNK_SIZE = 262144
1421
+ if not self.mem_lock:
1422
+ MAX_CHUNK_SIZE = 10000000000000000000000000 #unlimited i guess
1423
+ chunks = []
1424
+
1425
+ for z in range(self.image_3d.shape[0]):
1426
+ # Get the dimensions of this z-slice
1427
+ y_dim = self.image_3d.shape[1]
1428
+ x_dim = self.image_3d.shape[2]
1429
+ total_pixels = y_dim * x_dim
1430
+
1431
+ # If the slice is small enough, do not subchunk
1432
+ if total_pixels <= MAX_CHUNK_SIZE:
1433
+
1434
+
1435
+ if not self.mem_lock:
1436
+ chunks.append(self.twodim_coords(y_dim, x_dim, z, total_pixels))
1437
+ else:
1438
+ chunks.append([y_dim, x_dim, z, total_pixels, None])
1439
+
1440
+
1441
+
1442
+ else:
1443
+ # Determine which dimension to divide (the largest one)
1444
+ largest_dim = 'y' if y_dim >= x_dim else 'x'
1445
+
1446
+ # Calculate how many divisions we need
1447
+ num_divisions = int(np.ceil(total_pixels / MAX_CHUNK_SIZE))
1448
+
1449
+ # Calculate the approx size of each division along the largest dimension
1450
+ if largest_dim == 'y':
1451
+ div_size = int(np.ceil(y_dim / num_divisions))
1452
+ # Create subchunks by dividing the y-dimension
1453
+ for i in range(0, y_dim, div_size):
1454
+ end_i = min(i + div_size, y_dim)
1455
+
1456
+ if not self.mem_lock:
1457
+ chunks.append(self.twodim_coords(y_dim, x_dim, z, None, ['y', i, end_i]))
1458
+ else:
1459
+ chunks.append([y_dim, x_dim, z, None, ['y', i, end_i]])
1460
+
1461
+ else: # largest_dim == 'x'
1462
+ div_size = int(np.ceil(x_dim / num_divisions))
1463
+ # Create subchunks by dividing the x-dimension
1464
+ for i in range(0, x_dim, div_size):
1465
+ end_i = min(i + div_size, x_dim)
1466
+
1467
+ if not self.mem_lock:
1468
+ chunks.append(self.twodim_coords(y_dim, x_dim, z, None, ['x', i, end_i]))
1469
+ else:
1470
+ chunks.append([y_dim, x_dim, z, None, ['x', i, end_i]])
1471
+
1472
+ return chunks
1473
+
1474
+ #try:
1475
+ #from cuml.ensemble import RandomForestClassifier as cuRandomForestClassifier
1476
+ #except:
1477
+ #print("Cannot find cuML, using CPU to segment instead...")
1478
+ #gpu = False
1479
+
1480
+ if self.feature_cache is None and not self.mem_lock and not self.use_two:
1481
+ with self.lock:
1482
+ if self.feature_cache is None:
1483
+ self.feature_cache = self.compute_feature_maps()
1484
+
1485
+ print("Chunking data...")
1486
+
1487
+ if not self.use_two:
1488
+ # Determine optimal chunk size based on number of cores if not specified
1489
+ if chunk_size is None:
1490
+ total_cores = multiprocessing.cpu_count()
1491
+
1492
+ # Calculate total volume and target volume per core
1493
+ total_volume = np.prod(self.image_3d.shape)
1494
+ target_volume_per_chunk = total_volume / total_cores
1495
+
1496
+ # Calculate chunk size that would give us roughly one chunk per core
1497
+ # Using cube root since we want roughly equal sizes in all dimensions
1498
+ chunk_size = int(np.cbrt(target_volume_per_chunk))
1499
+
1500
+ # Ensure chunk size is at least 32 (minimum reasonable size) and not larger than smallest dimension
1501
+ chunk_size = max(32, min(chunk_size, min(self.image_3d.shape)))
1502
+
1503
+ # Round to nearest multiple of 32 for better memory alignment
1504
+ chunk_size = ((chunk_size + 15) // 32) * 32
1505
+
1506
+ # Calculate number of chunks in each dimension
1507
+ z_chunks = (self.image_3d.shape[0] + chunk_size - 1) // chunk_size
1508
+ y_chunks = (self.image_3d.shape[1] + chunk_size - 1) // chunk_size
1509
+ x_chunks = (self.image_3d.shape[2] + chunk_size - 1) // chunk_size
1510
+
1511
+ # Create start indices for all chunks at once
1512
+ chunk_starts = np.array(np.meshgrid(
1513
+ np.arange(z_chunks) * chunk_size,
1514
+ np.arange(y_chunks) * chunk_size,
1515
+ np.arange(x_chunks) * chunk_size,
1516
+ indexing='ij'
1517
+ )).reshape(3, -1).T
1518
+
1519
+ chunks = []
1520
+ for z_start, y_start, x_start in chunk_starts:
1521
+ z_end = min(z_start + chunk_size, self.image_3d.shape[0])
1522
+ y_end = min(y_start + chunk_size, self.image_3d.shape[1])
1523
+ x_end = min(x_start + chunk_size, self.image_3d.shape[2])
1524
+
1525
+ if self.mem_lock:
1526
+ # Create coordinates for this chunk efficiently
1527
+ coords = [z_start, z_end, y_start, y_end, x_start, x_end]
1528
+ chunks.append(coords)
1529
+
1530
+ else:
1531
+ # Consider moving this to process chunk ??
1532
+ coords = np.stack(np.meshgrid(
1533
+ np.arange(z_start, z_end),
1534
+ np.arange(y_start, y_end),
1535
+ np.arange(x_start, x_end),
1536
+ indexing='ij'
1537
+ )).reshape(3, -1).T
1538
+
1539
+ chunks.append(list(map(tuple, coords)))
1540
+
1541
+
1542
+
1543
+ else:
1544
+ chunks = create_2d_chunks()
1545
+ self.feature_cache = None #Decided this should not maintain training data for segmenting 2D
1546
+
1547
+ foreground_coords = set()
1548
+ background_coords = set()
1549
+
1550
+ print("Segmenting chunks...")
1551
+
1552
+ if not self.mem_lock:
1553
+ with ThreadPoolExecutor(max_workers=multiprocessing.cpu_count()) as executor:
1554
+ if gpu:
1555
+ try:
1556
+ futures = [executor.submit(self.process_chunk_GPU, chunk) for chunk in chunks]
1557
+ except:
1558
+ futures = [executor.submit(self.process_chunk, chunk) for chunk in chunks]
1559
+
1560
+ else:
1561
+ futures = [executor.submit(self.process_chunk, chunk) for chunk in chunks]
1562
+
1563
+ for i, future in enumerate(futures):
1564
+ fore, back = future.result()
1565
+ foreground_coords.update(fore)
1566
+ background_coords.update(back)
1567
+ print(f"Processed {i}/{len(chunks)} chunks")
1568
+ else: #Prioritize RAM
1569
+ for i, chunk in enumerate(chunks):
1570
+ fore, back = self.process_chunk(chunk)
1571
+ foreground_coords.update(fore)
1572
+ background_coords.update(back)
1573
+ try:
1574
+ chunk[i] = None #Help garbage collection
1575
+ except:
1576
+ pass
1577
+ print(f"Processed {i}/{len(chunks)} chunks")
1578
+
1579
+ return foreground_coords, background_coords
1580
+
1581
+ def update_position(self, z=None, x=None, y=None):
1582
+ """Update current position for chunk prioritization with safeguards"""
1583
+
1584
+ # Check if we should skip this update
1585
+ if hasattr(self, '_skip_next_update') and self._skip_next_update:
1586
+ self._skip_next_update = False
1587
+ return
1588
+
1589
+ # Store the previous z-position if not set
1590
+ if not hasattr(self, 'prev_z') or self.prev_z is None:
1591
+ self.prev_z = z
1592
+
1593
+ # Check if currently processing - if so, only update position but don't trigger map_slice changes
1594
+ if hasattr(self, '_currently_processing') and self._currently_processing:
1595
+ self.current_z = z
1596
+ self.current_x = x
1597
+ self.current_y = y
1598
+ self.prev_z = z
1599
+ return
1600
+
1601
+ # Update current positions
1602
+ self.current_z = z
1603
+ self.current_x = x
1604
+ self.current_y = y
1605
+
1606
+ # Only clear map_slice if z changes and we're not already generating a new one
1607
+ if self.current_z != self.prev_z:
1608
+ # Instead of setting to None, check if we already have it in the cache
1609
+ if hasattr(self, 'feature_cache') and self.feature_cache is not None:
1610
+ if self.current_z not in self.feature_cache:
1611
+ self.map_slice = None
1612
+ self._currently_segmenting = None
1613
+
1614
+ # Update previous z
1615
+ self.prev_z = z
1616
+
1617
+
1618
+ def get_realtime_chunks(self, chunk_size = 49):
1619
+
1620
+ # Determine if we need to chunk XY planes
1621
+ small_dims = (self.image_3d.shape[1] <= chunk_size and
1622
+ self.image_3d.shape[2] <= chunk_size)
1623
+ few_z = self.image_3d.shape[0] <= 100 # arbitrary threshold
1624
+
1625
+ # If small enough, each Z is one chunk
1626
+ if small_dims and few_z:
1627
+ chunk_size_xy = max(self.image_3d.shape[1], self.image_3d.shape[2])
1628
+ else:
1629
+ chunk_size_xy = chunk_size
1630
+
1631
+ # Calculate chunks for XY plane
1632
+ y_chunks = (self.image_3d.shape[1] + chunk_size_xy - 1) // chunk_size_xy
1633
+ x_chunks = (self.image_3d.shape[2] + chunk_size_xy - 1) // chunk_size_xy
1634
+
1635
+ # Populate chunk dictionary
1636
+ chunk_dict = {}
1637
+
1638
+ # Create chunks for each Z plane
1639
+ for z in range(self.image_3d.shape[0]):
1640
+ if small_dims:
1641
+
1642
+ chunk_dict[(z, 0, 0)] = {
1643
+ 'coords': [0, self.image_3d.shape[1], 0, self.image_3d.shape[2]],
1644
+ 'processed': False,
1645
+ 'z': z
1646
+ }
1647
+ else:
1648
+ # Multiple chunks per Z
1649
+ for y_chunk in range(y_chunks):
1650
+ for x_chunk in range(x_chunks):
1651
+ y_start = y_chunk * chunk_size_xy
1652
+ x_start = x_chunk * chunk_size_xy
1653
+ y_end = min(y_start + chunk_size_xy, self.image_3d.shape[1])
1654
+ x_end = min(x_start + chunk_size_xy, self.image_3d.shape[2])
1655
+
1656
+ chunk_dict[(z, y_start, x_start)] = {
1657
+ 'coords': [y_start, y_end, x_start, x_end],
1658
+ 'processed': False,
1659
+ 'z': z
1660
+ }
1661
+
1662
+ self.realtimechunks = chunk_dict
1663
+
1664
+ print("Ready!")
1665
+
1666
+
1667
+ def segment_volume_realtime(self, gpu = False):
1668
+
1669
+ try:
1670
+ from cuml.ensemble import RandomForestClassifier as cuRandomForestClassifier
1671
+ except:
1672
+ print("Cannot find cuML, using CPU to segment instead...")
1673
+ gpu = False
1674
+
1675
+
1676
+
1677
+ if self.realtimechunks is None:
1678
+ self.get_realtime_chunks()
1679
+ else:
1680
+ for chunk_pos in self.realtimechunks: # chunk_pos is the (z, y_start, x_start) tuple
1681
+ self.realtimechunks[chunk_pos]['processed'] = False
1682
+
1683
+ chunk_dict = self.realtimechunks
1684
+
1685
+
1686
+ def get_nearest_unprocessed_chunk(self):
1687
+ """Get nearest unprocessed chunk prioritizing current Z"""
1688
+ curr_z = self.current_z if self.current_z is not None else self.image_3d.shape[0] // 2
1689
+ curr_y = self.current_y if self.current_y is not None else self.image_3d.shape[1] // 2
1690
+ curr_x = self.current_x if self.current_x is not None else self.image_3d.shape[2] // 2
1691
+
1692
+ # First try to find chunks at current Z
1693
+ current_z_chunks = [(pos, info) for pos, info in chunk_dict.items()
1694
+ if pos[0] == curr_z and not info['processed']]
1695
+
1696
+ if current_z_chunks:
1697
+ # Find nearest chunk in current Z plane using the chunk positions from the key
1698
+ nearest = min(current_z_chunks,
1699
+ key=lambda x: ((x[0][1] - curr_y) ** 2 +
1700
+ (x[0][2] - curr_x) ** 2))
1701
+ return nearest[0]
1702
+
1703
+ # If no chunks at current Z, find nearest Z with available chunks
1704
+ available_z = sorted(
1705
+ [(pos[0], pos) for pos, info in chunk_dict.items()
1706
+ if not info['processed']],
1707
+ key=lambda x: abs(x[0] - curr_z)
1708
+ )
1709
+
1710
+ if available_z:
1711
+ target_z = available_z[0][0]
1712
+ # Find nearest chunk in target Z plane
1713
+ z_chunks = [(pos, info) for pos, info in chunk_dict.items()
1714
+ if pos[0] == target_z and not info['processed']]
1715
+ nearest = min(z_chunks,
1716
+ key=lambda x: ((x[0][1] - curr_y) ** 2 +
1717
+ (x[0][2] - curr_x) ** 2))
1718
+ return nearest[0]
1719
+
1720
+ return None
1721
+
1722
+
1723
+ while True:
1724
+ # Find nearest unprocessed chunk using class attributes
1725
+ chunk_idx = get_nearest_unprocessed_chunk(self)
1726
+ if chunk_idx is None:
1727
+ break
1728
+
1729
+ # Process the chunk directly
1730
+ chunk = chunk_dict[chunk_idx]
1731
+ chunk['processed'] = True
1732
+ coords = chunk['coords']
1733
+
1734
+ coords = np.stack(np.meshgrid(
1735
+ [chunk['z']],
1736
+ np.arange(coords[0], coords[1]),
1737
+ np.arange(coords[2], coords[3]),
1738
+ indexing='ij'
1739
+ )).reshape(3, -1).T
1740
+
1741
+ coords = list(map(tuple, coords))
1742
+
1743
+
1744
+ # Process the chunk directly based on whether GPU is available
1745
+ if gpu:
1746
+ try:
1747
+ fore, back = self.process_chunk_GPU(coords)
1748
+ except:
1749
+ fore, back = self.process_chunk(coords)
1750
+ else:
1751
+ fore, back = self.process_chunk(coords)
1752
+
1753
+ # Yield the results
1754
+ yield fore, back
1755
+
1756
+
1757
+ def cleanup(self):
1758
+ """Clean up GPU memory"""
1759
+ if self.use_gpu:
1760
+ try:
1761
+ cp.get_default_memory_pool().free_all_blocks()
1762
+ torch.cuda.empty_cache()
1763
+ except:
1764
+ pass
1765
+
1766
+ def train_batch(self, foreground_array, speed = True, use_gpu = False, use_two = False, mem_lock = False, saving = False):
1767
+ """Train directly on foreground and background arrays"""
1768
+
1769
+ if not saving:
1770
+ print("Training model...")
1771
+ self.speed = speed
1772
+ self.cur_gpu = use_gpu
1773
+ if mem_lock != self.mem_lock:
1774
+ self.realtimechunks = None #dump ram
1775
+ self.feature_cache = None
1776
+
1777
+ if not use_two:
1778
+ self.use_two = False
1779
+
1780
+ self.mem_lock = mem_lock
1781
+
1782
+ if self.current_speed != speed:
1783
+ self.feature_cache = None
1784
+ if use_gpu:
1785
+ try:
1786
+ self.model = cuRandomForestClassifier(
1787
+ n_estimators=100,
1788
+ max_depth=None
1789
+ )
1790
+ except:
1791
+ self.model = RandomForestClassifier(
1792
+ n_estimators=100,
1793
+ n_jobs=-1,
1794
+ max_depth=None
1795
+ )
1796
+ else:
1797
+ self.model = RandomForestClassifier(
1798
+ n_estimators=100,
1799
+ n_jobs=-1,
1800
+ max_depth=None
1801
+ )
1802
+
1803
+
1804
+ if use_two:
1805
+
1806
+ #changed = [] #Track which slices need feature maps
1807
+
1808
+ if not self.use_two: #Clarifies if we need to redo feature cache for 2D
1809
+ self.feature_cache = None
1810
+ self.use_two = True
1811
+
1812
+ self.feature_cache = None #Decided this should reset, can remove this line to have it retain prev feature maps
1813
+ self.two_slices = []
1814
+
1815
+ if self.feature_cache == None:
1816
+ self.feature_cache = {}
1817
+
1818
+ # Get foreground coordinates and features
1819
+ z_fore, y_fore, x_fore = np.where(foreground_array == 1)
1820
+
1821
+
1822
+ fore_coords = list(zip(z_fore, y_fore, x_fore))
1823
+
1824
+ # Get background coordinates and features
1825
+ z_back, y_back, x_back = np.where(foreground_array == 2)
1826
+
1827
+ back_coords = list(zip(z_back, y_back, x_back))
1828
+
1829
+
1830
+ #slices = set(list(z_back) + list(z_fore))
1831
+
1832
+ #for z in slices:
1833
+ #if z not in self.two_slices:
1834
+ #changed.append(z)
1835
+ #self.two_slices.append(z) #Tracks assigning coords to feature map slices
1836
+
1837
+ foreground_features = []
1838
+ background_features = []
1839
+
1840
+ z_fores = self.organize_by_z(fore_coords)
1841
+ z_backs = self.organize_by_z(back_coords)
1842
+ slices = set(list(z_fores.keys()) + list(z_backs.keys()))
1843
+
1844
+ for z in slices:
1845
+
1846
+
1847
+ current_map = self.get_feature_map_slice(z, speed, use_gpu)
1848
+
1849
+ if z in z_fores:
1850
+
1851
+ for y, x in z_fores[z]:
1852
+ # Get the feature vector for this foreground point
1853
+ feature_vector = current_map[y, x]
1854
+
1855
+ # Add to our collection
1856
+ foreground_features.append(feature_vector)
1857
+
1858
+ if z in z_backs:
1859
+
1860
+ for y, x in z_backs[z]:
1861
+ # Get the feature vector for this foreground point
1862
+ feature_vector = current_map[y, x]
1863
+
1864
+ # Add to our collection
1865
+ background_features.append(feature_vector)
1866
+
1867
+
1868
+ elif mem_lock: #Forces ram efficiency
1869
+
1870
+ box_size = self.master_chunk
1871
+
1872
+ # Memory-efficient approach: compute features only for necessary subarrays
1873
+ foreground_features = []
1874
+ background_features = []
1875
+
1876
+ # Find coordinates of foreground and background scribbles
1877
+ z_fore = np.argwhere(foreground_array == 1)
1878
+ z_back = np.argwhere(foreground_array == 2)
1879
+
1880
+ # If no scribbles, return empty lists
1881
+ if len(z_fore) == 0 and len(z_back) == 0:
1882
+ return foreground_features, background_features
1883
+
1884
+ # Get dimensions of the input array
1885
+ depth, height, width = foreground_array.shape
1886
+
1887
+ # Determine the minimum number of boxes needed to cover all scribbles
1888
+ half_box = box_size // 2
1889
+
1890
+ # Step 1: Find the minimum set of boxes that cover all scribbles
1891
+ # We'll divide the volume into a grid of boxes of size box_size
1892
+
1893
+ # Calculate how many boxes are needed in each dimension
1894
+ z_grid_size = (depth + box_size - 1) // box_size
1895
+ y_grid_size = (height + box_size - 1) // box_size
1896
+ x_grid_size = (width + box_size - 1) // box_size
1897
+
1898
+ # Track which grid cells contain scribbles
1899
+ grid_cells_with_scribbles = set()
1900
+
1901
+ # Map original coordinates to grid cells
1902
+ for z, y, x in np.vstack((z_fore, z_back)) if len(z_back) > 0 else z_fore:
1903
+ grid_z = z // box_size
1904
+ grid_y = y // box_size
1905
+ grid_x = x // box_size
1906
+ grid_cells_with_scribbles.add((grid_z, grid_y, grid_x))
1907
+
1908
+ # Create a mapping from original coordinates to their corresponding subarray and local coordinates
1909
+ coord_mapping = {}
1910
+
1911
+ # Step 2: Process each grid cell that contains scribbles
1912
+ for grid_z, grid_y, grid_x in grid_cells_with_scribbles:
1913
+ # Calculate the boundaries of this grid cell
1914
+ z_min = grid_z * box_size
1915
+ y_min = grid_y * box_size
1916
+ x_min = grid_x * box_size
1917
+
1918
+ z_max = min(z_min + box_size, depth)
1919
+ y_max = min(y_min + box_size, height)
1920
+ x_max = min(x_min + box_size, width)
1921
+
1922
+ # Extract the subarray
1923
+ subarray = self.image_3d[z_min:z_max, y_min:y_max, x_min:x_max]
1924
+ subarray2 = foreground_array[z_min:z_max, y_min:y_max, x_min:x_max]
1925
+
1926
+ # Compute features for this subarray
1927
+ if self.speed:
1928
+ subarray_features = self.compute_feature_maps_cpu_parallel(subarray)
1929
+ else:
1930
+ subarray_features = self.compute_deep_feature_maps_cpu_parallel(subarray)
1931
+
1932
+ # For each foreground point in this grid cell, extract its feature
1933
+ # Extract foreground features using a direct mask comparison
1934
+ local_fore_coords = np.argwhere(subarray2 == 1)
1935
+ for local_z, local_y, local_x in local_fore_coords:
1936
+ feature = subarray_features[local_z, local_y, local_x]
1937
+ foreground_features.append(feature)
1938
+
1939
+ # Extract background features using a direct mask comparison
1940
+ local_back_coords = np.argwhere(subarray2 == 2)
1941
+ for local_z, local_y, local_x in local_back_coords:
1942
+ feature = subarray_features[local_z, local_y, local_x]
1943
+ background_features.append(feature)
1944
+
1945
+ else:
1946
+
1947
+ self.two_slices = []
1948
+
1949
+ if self.use_two: #Clarifies if we need to redo feature cache for 3D
1950
+
1951
+ self.feature_cache = None
1952
+ self.use_two = False
1953
+
1954
+ if self.feature_cache is None:
1955
+ with self.lock:
1956
+ if self.feature_cache is None and speed:
1957
+ if use_gpu:
1958
+ self.feature_cache = self.compute_feature_maps()
1959
+ else:
1960
+ self.feature_cache = self.compute_feature_maps_cpu()
1961
+
1962
+ elif self.feature_cache is None and not speed:
1963
+ if use_gpu:
1964
+
1965
+ self.feature_cache = self.compute_deep_feature_maps()
1966
+ else:
1967
+ self.feature_cache = self.compute_deep_feature_maps_cpu()
1968
+
1969
+
1970
+ try:
1971
+ # Get foreground coordinates and features
1972
+ z_fore, y_fore, x_fore = np.where(foreground_array == 1)
1973
+ foreground_features = self.feature_cache[z_fore, y_fore, x_fore]
1974
+
1975
+ # Get background coordinates and features
1976
+ z_back, y_back, x_back = np.where(foreground_array == 2)
1977
+ background_features = self.feature_cache[z_back, y_back, x_back]
1978
+ except:
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:
2006
+
2007
+ return foreground_features, background_features, z_fore, z_back
2008
+
2009
+ # Combine features and labels
2010
+ X = np.vstack([foreground_features, background_features])
2011
+ y = np.hstack([np.ones(len(z_fore)), np.zeros(len(z_back))])
2012
+
2013
+
2014
+ # Train the model
2015
+ try:
2016
+ self.model.fit(X, y)
2017
+ except:
2018
+ print(X)
2019
+ print(y)
2020
+
2021
+ self.current_speed = speed
2022
+
2023
+
2024
+
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
+
2074
+ print("Done")
2075
+
2076
+ def get_feature_map_slice(self, z, speed, use_gpu):
2077
+
2078
+ if self._currently_segmenting is not None:
2079
+ return
2080
+
2081
+ #with self.lock <- cant remember why this was here
2082
+ if speed:
2083
+
2084
+ if self.mem_lock:
2085
+ output = self.compute_feature_maps_cpu_2d_parallel(z = z)
2086
+ else:
2087
+ output = self.compute_feature_maps_cpu_2d(z = z)
2088
+
2089
+ elif not speed:
2090
+
2091
+ if self.mem_lock:
2092
+ output = self.compute_deep_feature_maps_cpu_2d_parallel(z = z)
2093
+ else:
2094
+ output = self.compute_deep_feature_maps_cpu_2d(z = z)
2095
+
2096
+ return output
2097
+