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.
- nettracer3d/nettracer.py +24 -5
- nettracer3d/nettracer_gui.py +99 -67
- nettracer3d/segmenter - Copy.py +2097 -0
- nettracer3d/segmenter.py +75 -668
- nettracer3d/segmenter_GPU.py +611 -0
- {nettracer3d-0.7.3.dist-info → nettracer3d-0.7.4.dist-info}/METADATA +5 -6
- {nettracer3d-0.7.3.dist-info → nettracer3d-0.7.4.dist-info}/RECORD +11 -9
- {nettracer3d-0.7.3.dist-info → nettracer3d-0.7.4.dist-info}/WHEEL +1 -1
- {nettracer3d-0.7.3.dist-info → nettracer3d-0.7.4.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.7.3.dist-info → nettracer3d-0.7.4.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.7.3.dist-info → nettracer3d-0.7.4.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
|