nettracer3d 0.7.3__py3-none-any.whl → 0.7.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nettracer3d/community_extractor.py +17 -39
- nettracer3d/modularity.py +41 -264
- nettracer3d/nettracer.py +31 -99
- nettracer3d/nettracer_gui.py +506 -271
- nettracer3d/network_analysis.py +0 -178
- nettracer3d/segmenter.py +84 -724
- nettracer3d/segmenter_GPU.py +1286 -0
- nettracer3d/simple_network.py +2 -150
- {nettracer3d-0.7.3.dist-info → nettracer3d-0.7.5.dist-info}/METADATA +8 -7
- nettracer3d-0.7.5.dist-info/RECORD +21 -0
- {nettracer3d-0.7.3.dist-info → nettracer3d-0.7.5.dist-info}/WHEEL +1 -1
- nettracer3d-0.7.3.dist-info/RECORD +0 -20
- {nettracer3d-0.7.3.dist-info → nettracer3d-0.7.5.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.7.3.dist-info → nettracer3d-0.7.5.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.7.3.dist-info → nettracer3d-0.7.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1286 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
#try:
|
|
3
|
+
#import torch
|
|
4
|
+
#except:
|
|
5
|
+
#pass
|
|
6
|
+
import cupy as cp
|
|
7
|
+
import cupyx.scipy.ndimage as cpx
|
|
8
|
+
#try:
|
|
9
|
+
#from cuml.ensemble import RandomForestClassifier as cuRandomForestClassifier
|
|
10
|
+
#except:
|
|
11
|
+
#pass
|
|
12
|
+
import concurrent.futures
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
14
|
+
import threading
|
|
15
|
+
from scipy import ndimage
|
|
16
|
+
import multiprocessing
|
|
17
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
|
|
20
|
+
class InteractiveSegmenter:
|
|
21
|
+
def __init__(self, image_3d):
|
|
22
|
+
image_3d = cp.asarray(image_3d)
|
|
23
|
+
self.image_3d = image_3d
|
|
24
|
+
self.patterns = []
|
|
25
|
+
|
|
26
|
+
self.model = RandomForestClassifier(
|
|
27
|
+
n_estimators=100,
|
|
28
|
+
n_jobs=-1,
|
|
29
|
+
max_depth=None
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
self.feature_cache = None
|
|
33
|
+
self.lock = threading.Lock()
|
|
34
|
+
self._currently_segmenting = None
|
|
35
|
+
self.use_gpu = True
|
|
36
|
+
|
|
37
|
+
# Current position attributes
|
|
38
|
+
self.current_z = None
|
|
39
|
+
self.current_x = None
|
|
40
|
+
self.current_y = None
|
|
41
|
+
|
|
42
|
+
self.realtimechunks = None
|
|
43
|
+
self.current_speed = False
|
|
44
|
+
|
|
45
|
+
# Tracking if we're using 2d or 3d segs
|
|
46
|
+
self.use_two = False
|
|
47
|
+
self.two_slices = []
|
|
48
|
+
self.speed = True
|
|
49
|
+
self.cur_gpu = False
|
|
50
|
+
self.map_slice = None
|
|
51
|
+
self.prev_z = None
|
|
52
|
+
self.previewing = False
|
|
53
|
+
|
|
54
|
+
# flags to track state
|
|
55
|
+
self._currently_processing = False
|
|
56
|
+
self._skip_next_update = False
|
|
57
|
+
self._last_processed_slice = None
|
|
58
|
+
self.mem_lock = False
|
|
59
|
+
|
|
60
|
+
#Adjustable feature map params:
|
|
61
|
+
self.alphas = [1,2,4,8]
|
|
62
|
+
self.windows = 10
|
|
63
|
+
self.dogs = [(1, 2), (2, 4), (4, 8)]
|
|
64
|
+
self.master_chunk = 49
|
|
65
|
+
|
|
66
|
+
#Data when loading prev model:
|
|
67
|
+
self.previous_foreground = None
|
|
68
|
+
self.previous_background = None
|
|
69
|
+
self.previous_z_fore = None
|
|
70
|
+
self.previous_z_back = None
|
|
71
|
+
|
|
72
|
+
def segment_slice_chunked(self, slice_z, block_size=49):
|
|
73
|
+
"""
|
|
74
|
+
A completely standalone method to segment a single z-slice in chunks
|
|
75
|
+
with improved safeguards.
|
|
76
|
+
"""
|
|
77
|
+
# Check if we're already processing this slice
|
|
78
|
+
if self._currently_processing and self._currently_processing == slice_z:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# Set processing flag with the slice we're processing
|
|
82
|
+
self._currently_processing = slice_z
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# First attempt to get the feature map
|
|
86
|
+
feature_map = None
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
if slice_z in self.feature_cache:
|
|
90
|
+
feature_map = self.feature_cache[slice_z]
|
|
91
|
+
elif hasattr(self, 'map_slice') and self.map_slice is not None and slice_z == self.current_z:
|
|
92
|
+
feature_map = self.map_slice
|
|
93
|
+
else:
|
|
94
|
+
# Generate new feature map
|
|
95
|
+
try:
|
|
96
|
+
feature_map = self.get_feature_map_slice(slice_z, self.current_speed, False)
|
|
97
|
+
self.map_slice = feature_map
|
|
98
|
+
except Exception as e:
|
|
99
|
+
print(f"Error generating feature map: {e}")
|
|
100
|
+
import traceback
|
|
101
|
+
traceback.print_exc()
|
|
102
|
+
return # Exit if we can't generate the feature map
|
|
103
|
+
except:
|
|
104
|
+
# Generate new feature map
|
|
105
|
+
try:
|
|
106
|
+
feature_map = self.get_feature_map_slice(slice_z, self.current_speed, False)
|
|
107
|
+
self.map_slice = feature_map
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(f"Error generating feature map: {e}")
|
|
110
|
+
import traceback
|
|
111
|
+
traceback.print_exc()
|
|
112
|
+
return # Exit if we can't generate the feature map
|
|
113
|
+
|
|
114
|
+
# Check that we have a valid feature map
|
|
115
|
+
if feature_map is None:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# Get dimensions of the slice
|
|
119
|
+
y_size, x_size = self.image_3d.shape[1], self.image_3d.shape[2]
|
|
120
|
+
chunk_count = 0
|
|
121
|
+
|
|
122
|
+
# Determine if feature_map is a CuPy array
|
|
123
|
+
is_cupy_array = hasattr(feature_map, 'get')
|
|
124
|
+
|
|
125
|
+
# Process in blocks for chunked feedback
|
|
126
|
+
for y_start in range(0, y_size, block_size):
|
|
127
|
+
if self._currently_processing != slice_z:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
for x_start in range(0, x_size, block_size):
|
|
131
|
+
if self._currently_processing != slice_z:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
y_end = min(y_start + block_size, y_size)
|
|
135
|
+
x_end = min(x_start + block_size, x_size)
|
|
136
|
+
|
|
137
|
+
# Create coordinates and features for this block
|
|
138
|
+
coords = []
|
|
139
|
+
features_list = []
|
|
140
|
+
|
|
141
|
+
for y in range(y_start, y_end):
|
|
142
|
+
for x in range(x_start, x_end):
|
|
143
|
+
coords.append((slice_z, y, x))
|
|
144
|
+
features_list.append(feature_map[y, x])
|
|
145
|
+
|
|
146
|
+
# Convert features to NumPy properly based on type
|
|
147
|
+
if is_cupy_array:
|
|
148
|
+
# If feature_map is a CuPy array, we need to extract a CuPy array
|
|
149
|
+
# from the list and then convert it to NumPy
|
|
150
|
+
try:
|
|
151
|
+
# Create a CuPy array from the list of feature vectors
|
|
152
|
+
features_array = cp.stack(features_list)
|
|
153
|
+
# Convert to NumPy explicitly using .get()
|
|
154
|
+
features = features_array.get()
|
|
155
|
+
except Exception as e:
|
|
156
|
+
print(f"Error converting features to NumPy: {e}")
|
|
157
|
+
# Fallback: convert each feature individually
|
|
158
|
+
features = []
|
|
159
|
+
for feat in features_list:
|
|
160
|
+
if hasattr(feat, 'get'):
|
|
161
|
+
features.append(feat.get())
|
|
162
|
+
else:
|
|
163
|
+
features.append(feat)
|
|
164
|
+
else:
|
|
165
|
+
# If it's already a NumPy array, we can use it directly
|
|
166
|
+
features = features_list
|
|
167
|
+
|
|
168
|
+
# Skip empty blocks
|
|
169
|
+
if not coords:
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
# Predict
|
|
173
|
+
try:
|
|
174
|
+
try:
|
|
175
|
+
predictions = self.model.predict(features)
|
|
176
|
+
except ValueError:
|
|
177
|
+
self.feature_cache = None
|
|
178
|
+
self.map_slice = None
|
|
179
|
+
return None, None
|
|
180
|
+
|
|
181
|
+
# Split results
|
|
182
|
+
foreground = set()
|
|
183
|
+
background = set()
|
|
184
|
+
|
|
185
|
+
for coord, pred in zip(coords, predictions):
|
|
186
|
+
if pred:
|
|
187
|
+
foreground.add(coord)
|
|
188
|
+
else:
|
|
189
|
+
background.add(coord)
|
|
190
|
+
|
|
191
|
+
# Yield this chunk
|
|
192
|
+
chunk_count += 1
|
|
193
|
+
yield foreground, background
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
print(f"Error processing chunk: {e}")
|
|
197
|
+
import traceback
|
|
198
|
+
traceback.print_exc()
|
|
199
|
+
|
|
200
|
+
finally:
|
|
201
|
+
# Only clear if we're still processing the same slice
|
|
202
|
+
# (otherwise, another slice might have taken over)
|
|
203
|
+
if self._currently_processing == slice_z:
|
|
204
|
+
self._currently_processing = None
|
|
205
|
+
|
|
206
|
+
def process_chunk(self, chunk_coords):
|
|
207
|
+
"""Process a chunk staying in CuPy as much as possible"""
|
|
208
|
+
|
|
209
|
+
foreground_coords = [] # Keep as list of CuPy coordinates
|
|
210
|
+
background_coords = []
|
|
211
|
+
|
|
212
|
+
if self.previewing or not self.use_two:
|
|
213
|
+
|
|
214
|
+
if self.realtimechunks is None:
|
|
215
|
+
z_min, z_max = chunk_coords[0], chunk_coords[1]
|
|
216
|
+
y_min, y_max = chunk_coords[2], chunk_coords[3]
|
|
217
|
+
x_min, x_max = chunk_coords[4], chunk_coords[5]
|
|
218
|
+
|
|
219
|
+
# Create meshgrid using CuPy - already good
|
|
220
|
+
z_range = cp.arange(z_min, z_max)
|
|
221
|
+
y_range = cp.arange(y_min, y_max)
|
|
222
|
+
x_range = cp.arange(x_min, x_max)
|
|
223
|
+
|
|
224
|
+
# More efficient way to create coordinates
|
|
225
|
+
chunk_coords_array = cp.stack(cp.meshgrid(
|
|
226
|
+
z_range, y_range, x_range, indexing='ij'
|
|
227
|
+
)).reshape(3, -1).T
|
|
228
|
+
|
|
229
|
+
# Keep as CuPy array instead of converting to list
|
|
230
|
+
chunk_coords_gpu = chunk_coords_array
|
|
231
|
+
else:
|
|
232
|
+
# Convert list to CuPy array once
|
|
233
|
+
chunk_coords_gpu = cp.array(chunk_coords)
|
|
234
|
+
z_coords = chunk_coords_gpu[:, 0]
|
|
235
|
+
y_coords = chunk_coords_gpu[:, 1]
|
|
236
|
+
x_coords = chunk_coords_gpu[:, 2]
|
|
237
|
+
|
|
238
|
+
z_min, z_max = cp.min(z_coords).item(), cp.max(z_coords).item()
|
|
239
|
+
y_min, y_max = cp.min(y_coords).item(), cp.max(y_coords).item()
|
|
240
|
+
x_min, x_max = cp.min(x_coords).item(), cp.max(x_coords).item()
|
|
241
|
+
|
|
242
|
+
# Extract subarray - already good
|
|
243
|
+
subarray = self.image_3d[z_min:z_max+1, y_min:y_max+1, x_min:x_max+1]
|
|
244
|
+
|
|
245
|
+
# Compute features
|
|
246
|
+
if self.speed:
|
|
247
|
+
feature_map = self.compute_feature_maps_gpu(subarray)
|
|
248
|
+
else:
|
|
249
|
+
feature_map = self.compute_deep_feature_maps_gpu(subarray)
|
|
250
|
+
|
|
251
|
+
# Extract features more efficiently
|
|
252
|
+
local_coords = chunk_coords_gpu.copy()
|
|
253
|
+
local_coords[:, 0] -= z_min
|
|
254
|
+
local_coords[:, 1] -= y_min
|
|
255
|
+
local_coords[:, 2] -= x_min
|
|
256
|
+
|
|
257
|
+
# Vectorized feature extraction
|
|
258
|
+
features_gpu = feature_map[local_coords[:, 0], local_coords[:, 1], local_coords[:, 2]]
|
|
259
|
+
|
|
260
|
+
features_cpu = cp.asnumpy(features_gpu)
|
|
261
|
+
predictions = self.model.predict(features_cpu)
|
|
262
|
+
|
|
263
|
+
# Keep coordinates as CuPy arrays
|
|
264
|
+
pred_mask = cp.array(predictions, dtype=bool)
|
|
265
|
+
foreground_coords = chunk_coords_gpu[pred_mask]
|
|
266
|
+
background_coords = chunk_coords_gpu[~pred_mask]
|
|
267
|
+
|
|
268
|
+
else:
|
|
269
|
+
# 2D implementation for GPU
|
|
270
|
+
foreground_coords = []
|
|
271
|
+
background_coords = []
|
|
272
|
+
|
|
273
|
+
# Check if chunk_coords is in the 2D format [z, y_start, y_end, x_start, x_end]
|
|
274
|
+
if len(chunk_coords) == 5:
|
|
275
|
+
z = chunk_coords[0]
|
|
276
|
+
y_start = chunk_coords[1]
|
|
277
|
+
y_end = chunk_coords[2]
|
|
278
|
+
x_start = chunk_coords[3]
|
|
279
|
+
x_end = chunk_coords[4]
|
|
280
|
+
|
|
281
|
+
# Generate coordinates for this slice or subchunk using the new function
|
|
282
|
+
coords_array = self.twodim_coords(z, y_start, y_end, x_start, x_end)
|
|
283
|
+
|
|
284
|
+
# Get the feature map for this z-slice
|
|
285
|
+
if self.feature_cache is None:
|
|
286
|
+
feature_map = self.get_feature_map_slice(z, self.speed, True) # Use GPU
|
|
287
|
+
elif z not in self.feature_cache and not self.previewing:
|
|
288
|
+
feature_map = self.get_feature_map_slice(z, self.speed, True) # Use GPU
|
|
289
|
+
elif (z not in self.feature_cache or self.feature_cache is None) and self.previewing:
|
|
290
|
+
feature_map = self.map_slice
|
|
291
|
+
if feature_map is None:
|
|
292
|
+
return [], []
|
|
293
|
+
else:
|
|
294
|
+
feature_map = self.feature_cache[z]
|
|
295
|
+
|
|
296
|
+
# Check if we have a valid feature map
|
|
297
|
+
if feature_map is None:
|
|
298
|
+
return [], []
|
|
299
|
+
|
|
300
|
+
# Extract y and x coordinates from the array
|
|
301
|
+
y_indices = coords_array[:, 1]
|
|
302
|
+
x_indices = coords_array[:, 2]
|
|
303
|
+
|
|
304
|
+
# Extract features using CuPy indexing
|
|
305
|
+
features_gpu = feature_map[y_indices, x_indices]
|
|
306
|
+
|
|
307
|
+
# Convert to NumPy for the model
|
|
308
|
+
features_cpu = features_gpu.get()
|
|
309
|
+
|
|
310
|
+
# Make predictions
|
|
311
|
+
predictions = self.model.predict(features_cpu)
|
|
312
|
+
|
|
313
|
+
# Create CuPy boolean mask from predictions
|
|
314
|
+
pred_mask = cp.array(predictions, dtype=bool)
|
|
315
|
+
|
|
316
|
+
# Split into foreground and background using the mask
|
|
317
|
+
fore_coords = coords_array[pred_mask]
|
|
318
|
+
back_coords = coords_array[~pred_mask]
|
|
319
|
+
|
|
320
|
+
return fore_coords, back_coords
|
|
321
|
+
|
|
322
|
+
return foreground_coords, background_coords
|
|
323
|
+
|
|
324
|
+
def twodim_coords(self, z, y_start, y_end, x_start, x_end):
|
|
325
|
+
"""
|
|
326
|
+
Generate 2D coordinates for a z-slice using CuPy for GPU acceleration.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
z (int): Z-slice index
|
|
330
|
+
y_start (int): Start index for y dimension
|
|
331
|
+
y_end (int): End index for y dimension
|
|
332
|
+
x_start (int): Start index for x dimension
|
|
333
|
+
x_end (int): End index for x dimension
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
CuPy array of coordinates in format (z, y, x)
|
|
337
|
+
"""
|
|
338
|
+
import cupy as cp
|
|
339
|
+
|
|
340
|
+
# Create ranges for y and x dimensions
|
|
341
|
+
y_range = cp.arange(y_start, y_end, dtype=int)
|
|
342
|
+
x_range = cp.arange(x_start, x_end, dtype=int)
|
|
343
|
+
|
|
344
|
+
# Create meshgrid
|
|
345
|
+
y_coords, x_coords = cp.meshgrid(y_range, x_range, indexing='ij')
|
|
346
|
+
|
|
347
|
+
# Calculate total size
|
|
348
|
+
total_size = len(y_range) * len(x_range)
|
|
349
|
+
|
|
350
|
+
# Stack coordinates with z values
|
|
351
|
+
slice_coords = cp.column_stack((
|
|
352
|
+
cp.full(total_size, z, dtype=int),
|
|
353
|
+
y_coords.ravel(),
|
|
354
|
+
x_coords.ravel()
|
|
355
|
+
))
|
|
356
|
+
|
|
357
|
+
return slice_coords
|
|
358
|
+
|
|
359
|
+
def compute_feature_maps_gpu(self, image_3d=None):
|
|
360
|
+
"""Compute feature maps using GPU with CuPy"""
|
|
361
|
+
import cupy as cp
|
|
362
|
+
import cupyx.scipy.ndimage as cupy_ndimage
|
|
363
|
+
|
|
364
|
+
features = []
|
|
365
|
+
if image_3d is None:
|
|
366
|
+
image_3d = self.image_3d # Assuming this is already a cupy array
|
|
367
|
+
|
|
368
|
+
original_shape = image_3d.shape
|
|
369
|
+
|
|
370
|
+
# Gaussian smoothing at different scales
|
|
371
|
+
for sigma in self.alphas:
|
|
372
|
+
smooth = cupy_ndimage.gaussian_filter(image_3d, sigma)
|
|
373
|
+
features.append(smooth)
|
|
374
|
+
|
|
375
|
+
# Difference of Gaussians
|
|
376
|
+
for (s1, s2) in self.dogs:
|
|
377
|
+
g1 = cupy_ndimage.gaussian_filter(image_3d, s1)
|
|
378
|
+
g2 = cupy_ndimage.gaussian_filter(image_3d, s2)
|
|
379
|
+
dog = g1 - g2
|
|
380
|
+
features.append(dog)
|
|
381
|
+
|
|
382
|
+
# Gradient computations using cupyx
|
|
383
|
+
gx = cupy_ndimage.sobel(image_3d, axis=2, mode='reflect') # x direction
|
|
384
|
+
gy = cupy_ndimage.sobel(image_3d, axis=1, mode='reflect') # y direction
|
|
385
|
+
gz = cupy_ndimage.sobel(image_3d, axis=0, mode='reflect') # z direction
|
|
386
|
+
|
|
387
|
+
# Gradient magnitude
|
|
388
|
+
gradient_magnitude = cp.sqrt(gx**2 + gy**2 + gz**2)
|
|
389
|
+
features.append(gradient_magnitude)
|
|
390
|
+
|
|
391
|
+
# Verify shapes
|
|
392
|
+
for i, feat in enumerate(features):
|
|
393
|
+
if feat.shape != original_shape:
|
|
394
|
+
feat_adjusted = cp.expand_dims(feat, axis=0)
|
|
395
|
+
if feat_adjusted.shape != original_shape:
|
|
396
|
+
raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
|
|
397
|
+
features[i] = feat_adjusted
|
|
398
|
+
|
|
399
|
+
return cp.stack(features, axis=-1)
|
|
400
|
+
|
|
401
|
+
def compute_deep_feature_maps_gpu(self, image_3d=None):
|
|
402
|
+
"""Compute feature maps using GPU"""
|
|
403
|
+
import cupy as cp
|
|
404
|
+
import cupyx.scipy.ndimage as cupy_ndimage
|
|
405
|
+
|
|
406
|
+
features = []
|
|
407
|
+
if image_3d is None:
|
|
408
|
+
image_3d = self.image_3d # Assuming this is already a cupy array
|
|
409
|
+
original_shape = image_3d.shape
|
|
410
|
+
|
|
411
|
+
# Gaussian and DoG using cupyx
|
|
412
|
+
for sigma in self.alphas:
|
|
413
|
+
smooth = cupy_ndimage.gaussian_filter(image_3d, sigma)
|
|
414
|
+
features.append(smooth)
|
|
415
|
+
|
|
416
|
+
# Difference of Gaussians
|
|
417
|
+
for (s1, s2) in self.dogs:
|
|
418
|
+
g1 = cupy_ndimage.gaussian_filter(image_3d, s1)
|
|
419
|
+
g2 = cupy_ndimage.gaussian_filter(image_3d, s2)
|
|
420
|
+
dog = g1 - g2
|
|
421
|
+
features.append(dog)
|
|
422
|
+
|
|
423
|
+
# Local statistics using cupyx's convolve
|
|
424
|
+
window_size = self.windows
|
|
425
|
+
kernel = cp.ones((window_size, window_size, window_size)) / (window_size**3)
|
|
426
|
+
|
|
427
|
+
# Local mean
|
|
428
|
+
local_mean = cupy_ndimage.convolve(image_3d, kernel, mode='reflect')
|
|
429
|
+
features.append(local_mean)
|
|
430
|
+
|
|
431
|
+
# Local variance
|
|
432
|
+
mean = cp.mean(image_3d)
|
|
433
|
+
local_var = cupy_ndimage.convolve((image_3d - mean)**2, kernel, mode='reflect')
|
|
434
|
+
features.append(local_var)
|
|
435
|
+
|
|
436
|
+
# Gradient computations using cupyx
|
|
437
|
+
gx = cupy_ndimage.sobel(image_3d, axis=2, mode='reflect')
|
|
438
|
+
gy = cupy_ndimage.sobel(image_3d, axis=1, mode='reflect')
|
|
439
|
+
gz = cupy_ndimage.sobel(image_3d, axis=0, mode='reflect')
|
|
440
|
+
|
|
441
|
+
# Gradient magnitude
|
|
442
|
+
gradient_magnitude = cp.sqrt(gx**2 + gy**2 + gz**2)
|
|
443
|
+
features.append(gradient_magnitude)
|
|
444
|
+
|
|
445
|
+
# Second-order gradients
|
|
446
|
+
gxx = cupy_ndimage.sobel(gx, axis=2, mode='reflect')
|
|
447
|
+
gyy = cupy_ndimage.sobel(gy, axis=1, mode='reflect')
|
|
448
|
+
gzz = cupy_ndimage.sobel(gz, axis=0, mode='reflect')
|
|
449
|
+
|
|
450
|
+
# Laplacian (sum of second derivatives)
|
|
451
|
+
laplacian = gxx + gyy + gzz
|
|
452
|
+
features.append(laplacian)
|
|
453
|
+
|
|
454
|
+
# Hessian determinant
|
|
455
|
+
hessian_det = gxx * gyy * gzz
|
|
456
|
+
features.append(hessian_det)
|
|
457
|
+
|
|
458
|
+
# Verify shapes
|
|
459
|
+
for i, feat in enumerate(features):
|
|
460
|
+
if feat.shape != original_shape:
|
|
461
|
+
feat_adjusted = cp.expand_dims(feat, axis=0)
|
|
462
|
+
if feat_adjusted.shape != original_shape:
|
|
463
|
+
raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
|
|
464
|
+
features[i] = feat_adjusted
|
|
465
|
+
|
|
466
|
+
return cp.stack(features, axis=-1)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def compute_feature_maps_gpu_2d(self, z=None):
|
|
470
|
+
"""Compute feature maps for 2D images using GPU with CuPy"""
|
|
471
|
+
import cupy as cp
|
|
472
|
+
import cupyx.scipy.ndimage as cupy_ndimage
|
|
473
|
+
|
|
474
|
+
# Extract 2D slice if z is provided, otherwise use the image directly
|
|
475
|
+
if z is not None:
|
|
476
|
+
image_2d = cp.asarray(self.image_3d[z, :, :])
|
|
477
|
+
else:
|
|
478
|
+
# Assuming image_2d is already available or passed
|
|
479
|
+
image_2d = cp.asarray(self.image_2d)
|
|
480
|
+
|
|
481
|
+
original_shape = image_2d.shape
|
|
482
|
+
features = []
|
|
483
|
+
|
|
484
|
+
# Gaussian smoothing at different scales
|
|
485
|
+
for sigma in self.alphas:
|
|
486
|
+
smooth = cupy_ndimage.gaussian_filter(image_2d, sigma)
|
|
487
|
+
features.append(smooth)
|
|
488
|
+
|
|
489
|
+
# Difference of Gaussians
|
|
490
|
+
for (s1, s2) in self.dogs:
|
|
491
|
+
g1 = cupy_ndimage.gaussian_filter(image_2d, s1)
|
|
492
|
+
g2 = cupy_ndimage.gaussian_filter(image_2d, s2)
|
|
493
|
+
dog = g1 - g2
|
|
494
|
+
features.append(dog)
|
|
495
|
+
|
|
496
|
+
# Gradient computations for 2D
|
|
497
|
+
gx = cupy_ndimage.sobel(image_2d, axis=1, mode='reflect') # x direction
|
|
498
|
+
gy = cupy_ndimage.sobel(image_2d, axis=0, mode='reflect') # y direction
|
|
499
|
+
|
|
500
|
+
# Gradient magnitude (2D version - no z component)
|
|
501
|
+
gradient_magnitude = cp.sqrt(gx**2 + gy**2)
|
|
502
|
+
features.append(gradient_magnitude)
|
|
503
|
+
|
|
504
|
+
# Verify shapes
|
|
505
|
+
for i, feat in enumerate(features):
|
|
506
|
+
if feat.shape != original_shape:
|
|
507
|
+
# Check dimensionality and expand if needed
|
|
508
|
+
if len(feat.shape) < len(original_shape):
|
|
509
|
+
feat_adjusted = feat
|
|
510
|
+
missing_dims = len(original_shape) - len(feat.shape)
|
|
511
|
+
for _ in range(missing_dims):
|
|
512
|
+
feat_adjusted = cp.expand_dims(feat_adjusted, axis=0)
|
|
513
|
+
|
|
514
|
+
if feat_adjusted.shape != original_shape:
|
|
515
|
+
raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
|
|
516
|
+
|
|
517
|
+
features[i] = feat_adjusted
|
|
518
|
+
|
|
519
|
+
# Stack features along a new dimension
|
|
520
|
+
result = cp.stack(features, axis=-1)
|
|
521
|
+
|
|
522
|
+
# Optional: Return as numpy array if needed
|
|
523
|
+
# result = cp.asnumpy(result)
|
|
524
|
+
|
|
525
|
+
return result
|
|
526
|
+
|
|
527
|
+
def compute_deep_feature_maps_gpu_2d(self, z=None):
|
|
528
|
+
"""Compute 2D feature maps using GPU with CuPy"""
|
|
529
|
+
import cupy as cp
|
|
530
|
+
import cupyx.scipy.ndimage as cupy_ndimage
|
|
531
|
+
|
|
532
|
+
# Extract 2D slice if z is provided, otherwise use the image directly
|
|
533
|
+
if z is not None:
|
|
534
|
+
image_2d = cp.asarray(self.image_3d[z, :, :])
|
|
535
|
+
else:
|
|
536
|
+
# Assuming image_2d is already available or passed
|
|
537
|
+
image_2d = cp.asarray(self.image_2d)
|
|
538
|
+
|
|
539
|
+
original_shape = image_2d.shape
|
|
540
|
+
features = []
|
|
541
|
+
|
|
542
|
+
# Stage 1: Compute all base features
|
|
543
|
+
|
|
544
|
+
# Gaussian smoothing
|
|
545
|
+
gaussian_results = {}
|
|
546
|
+
for sigma in self.alphas:
|
|
547
|
+
smooth = cupy_ndimage.gaussian_filter(image_2d, sigma)
|
|
548
|
+
gaussian_results[sigma] = smooth
|
|
549
|
+
features.append(smooth)
|
|
550
|
+
|
|
551
|
+
# Difference of Gaussians
|
|
552
|
+
for (s1, s2) in self.dogs:
|
|
553
|
+
g1 = cupy_ndimage.gaussian_filter(image_2d, s1)
|
|
554
|
+
g2 = cupy_ndimage.gaussian_filter(image_2d, s2)
|
|
555
|
+
dog = g1 - g2
|
|
556
|
+
features.append(dog)
|
|
557
|
+
|
|
558
|
+
# Local statistics using 2D kernel
|
|
559
|
+
window_size = self.windows
|
|
560
|
+
kernel = cp.ones((window_size, window_size)) / (window_size**2)
|
|
561
|
+
|
|
562
|
+
# Local mean
|
|
563
|
+
local_mean = cupy_ndimage.convolve(image_2d, kernel, mode='reflect')
|
|
564
|
+
features.append(local_mean)
|
|
565
|
+
|
|
566
|
+
# Local variance
|
|
567
|
+
mean = cp.mean(image_2d)
|
|
568
|
+
local_var = cupy_ndimage.convolve((image_2d - mean)**2, kernel, mode='reflect')
|
|
569
|
+
features.append(local_var)
|
|
570
|
+
|
|
571
|
+
# First-order gradients
|
|
572
|
+
gx = cupy_ndimage.sobel(image_2d, axis=1, mode='reflect') # x direction
|
|
573
|
+
gy = cupy_ndimage.sobel(image_2d, axis=0, mode='reflect') # y direction
|
|
574
|
+
|
|
575
|
+
# Gradient magnitude
|
|
576
|
+
gradient_magnitude = cp.sqrt(gx**2 + gy**2)
|
|
577
|
+
features.append(gradient_magnitude)
|
|
578
|
+
|
|
579
|
+
# Stage 2: Compute derived features
|
|
580
|
+
|
|
581
|
+
# Second-order gradients
|
|
582
|
+
gxx = cupy_ndimage.sobel(gx, axis=1, mode='reflect')
|
|
583
|
+
gyy = cupy_ndimage.sobel(gy, axis=0, mode='reflect')
|
|
584
|
+
|
|
585
|
+
# Cross derivatives for Hessian determinant
|
|
586
|
+
gxy = cupy_ndimage.sobel(gx, axis=0, mode='reflect')
|
|
587
|
+
gyx = cupy_ndimage.sobel(gy, axis=1, mode='reflect')
|
|
588
|
+
|
|
589
|
+
# Laplacian (sum of second derivatives)
|
|
590
|
+
laplacian = gxx + gyy
|
|
591
|
+
features.append(laplacian)
|
|
592
|
+
|
|
593
|
+
# Hessian determinant
|
|
594
|
+
hessian_det = gxx * gyy - gxy * gyx
|
|
595
|
+
features.append(hessian_det)
|
|
596
|
+
|
|
597
|
+
# Verify shapes
|
|
598
|
+
for i, feat in enumerate(features):
|
|
599
|
+
if feat.shape != original_shape:
|
|
600
|
+
# Check dimensionality and expand if needed
|
|
601
|
+
if len(feat.shape) < len(original_shape):
|
|
602
|
+
feat_adjusted = feat
|
|
603
|
+
missing_dims = len(original_shape) - len(feat.shape)
|
|
604
|
+
for _ in range(missing_dims):
|
|
605
|
+
feat_adjusted = cp.expand_dims(feat_adjusted, axis=0)
|
|
606
|
+
|
|
607
|
+
if feat_adjusted.shape != original_shape:
|
|
608
|
+
raise ValueError(f"Feature {i} has shape {feat.shape}, expected {original_shape}")
|
|
609
|
+
|
|
610
|
+
features[i] = feat_adjusted
|
|
611
|
+
|
|
612
|
+
# Stack all features along a new dimension
|
|
613
|
+
result = cp.stack(features, axis=-1)
|
|
614
|
+
|
|
615
|
+
# Optional: Return as numpy array if needed
|
|
616
|
+
# result = cp.asnumpy(result)
|
|
617
|
+
|
|
618
|
+
return result
|
|
619
|
+
|
|
620
|
+
def segment_volume(self, array, chunk_size=None, gpu=True):
|
|
621
|
+
"""Segment volume using parallel processing of chunks with vectorized chunk creation"""
|
|
622
|
+
|
|
623
|
+
array = cp.asarray(array) # Ensure CuPy array
|
|
624
|
+
|
|
625
|
+
self.realtimechunks = None
|
|
626
|
+
self.map_slice = None
|
|
627
|
+
chunk_size = self.master_chunk
|
|
628
|
+
|
|
629
|
+
def create_2d_chunks():
|
|
630
|
+
"""
|
|
631
|
+
Create chunks by z-slices for 2D processing.
|
|
632
|
+
Each chunk is a complete z-slice with all y,x coordinates,
|
|
633
|
+
unless the slice exceeds 262144 pixels, in which case it's divided into subchunks.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
List of chunks where each chunk contains the parameters needed for processing.
|
|
637
|
+
Format depends on subchunking:
|
|
638
|
+
- No subchunking: [y_dim, x_dim, z, total_pixels, None]
|
|
639
|
+
- Y subchunking: [y_dim, x_dim, z, None, ['y', start_y, end_y]]
|
|
640
|
+
- X subchunking: [y_dim, x_dim, z, None, ['x', start_x, end_x]]
|
|
641
|
+
"""
|
|
642
|
+
MAX_CHUNK_SIZE = 262144
|
|
643
|
+
chunks = []
|
|
644
|
+
|
|
645
|
+
for z in range(self.image_3d.shape[0]):
|
|
646
|
+
# Get the dimensions of this z-slice
|
|
647
|
+
y_dim = self.image_3d.shape[1]
|
|
648
|
+
x_dim = self.image_3d.shape[2]
|
|
649
|
+
total_pixels = y_dim * x_dim
|
|
650
|
+
|
|
651
|
+
# If the slice is small enough, do not subchunk
|
|
652
|
+
if total_pixels <= MAX_CHUNK_SIZE:
|
|
653
|
+
chunks.append([z, 0, y_dim, 0, x_dim]) # [z_start, y_start, y_end, x_start, x_end]
|
|
654
|
+
else:
|
|
655
|
+
# Determine which dimension to divide (the largest one)
|
|
656
|
+
largest_dim = 'y' if y_dim >= x_dim else 'x'
|
|
657
|
+
|
|
658
|
+
# Calculate how many divisions we need
|
|
659
|
+
num_divisions = int(cp.ceil(total_pixels / MAX_CHUNK_SIZE))
|
|
660
|
+
|
|
661
|
+
# Calculate the approx size of each division along the largest dimension
|
|
662
|
+
if largest_dim == 'y':
|
|
663
|
+
div_size = int(cp.ceil(y_dim / num_divisions))
|
|
664
|
+
# Create subchunks by dividing the y-dimension
|
|
665
|
+
for i in range(0, y_dim, div_size):
|
|
666
|
+
end_i = min(i + div_size, y_dim)
|
|
667
|
+
chunks.append([z, i, end_i, 0, x_dim]) # [z, y_start, y_end, x_start, x_end]
|
|
668
|
+
else: # largest_dim == 'x'
|
|
669
|
+
div_size = int(cp.ceil(x_dim / num_divisions))
|
|
670
|
+
# Create subchunks by dividing the x-dimension
|
|
671
|
+
for i in range(0, x_dim, div_size):
|
|
672
|
+
end_i = min(i + div_size, x_dim)
|
|
673
|
+
chunks.append([z, 0, y_dim, i, end_i]) # [z, y_start, y_end, x_start, x_end]
|
|
674
|
+
|
|
675
|
+
return chunks
|
|
676
|
+
|
|
677
|
+
print("Chunking data...")
|
|
678
|
+
|
|
679
|
+
if not self.use_two:
|
|
680
|
+
# 3D Processing - Create chunks for 3D volume
|
|
681
|
+
# Round to nearest multiple of 32 for better memory alignment
|
|
682
|
+
chunk_size = ((chunk_size + 15) // 32) * 32
|
|
683
|
+
|
|
684
|
+
# Calculate number of chunks in each dimension
|
|
685
|
+
z_chunks = (self.image_3d.shape[0] + chunk_size - 1) // chunk_size
|
|
686
|
+
y_chunks = (self.image_3d.shape[1] + chunk_size - 1) // chunk_size
|
|
687
|
+
x_chunks = (self.image_3d.shape[2] + chunk_size - 1) // chunk_size
|
|
688
|
+
|
|
689
|
+
# Create start indices for all chunks at once using CuPy
|
|
690
|
+
chunk_starts = cp.array(cp.meshgrid(
|
|
691
|
+
cp.arange(z_chunks) * chunk_size,
|
|
692
|
+
cp.arange(y_chunks) * chunk_size,
|
|
693
|
+
cp.arange(x_chunks) * chunk_size,
|
|
694
|
+
indexing='ij'
|
|
695
|
+
)).reshape(3, -1).T
|
|
696
|
+
|
|
697
|
+
chunks = []
|
|
698
|
+
for chunk_start_gpu in chunk_starts:
|
|
699
|
+
# Extract values from CuPy array
|
|
700
|
+
z_start = int(chunk_start_gpu[0]) # Convert to regular Python int
|
|
701
|
+
y_start = int(chunk_start_gpu[1])
|
|
702
|
+
x_start = int(chunk_start_gpu[2])
|
|
703
|
+
|
|
704
|
+
z_end = min(z_start + chunk_size, self.image_3d.shape[0])
|
|
705
|
+
y_end = min(y_start + chunk_size, self.image_3d.shape[1])
|
|
706
|
+
x_end = min(x_start + chunk_size, self.image_3d.shape[2])
|
|
707
|
+
|
|
708
|
+
coords = [z_start, z_end, y_start, y_end, x_start, x_end]
|
|
709
|
+
chunks.append(coords)
|
|
710
|
+
else:
|
|
711
|
+
# 2D Processing - Create chunks by z-slices
|
|
712
|
+
chunks = create_2d_chunks()
|
|
713
|
+
self.feature_cache = None # Reset feature cache for 2D processing
|
|
714
|
+
|
|
715
|
+
# Process chunks
|
|
716
|
+
print("Segmenting chunks...")
|
|
717
|
+
|
|
718
|
+
for i, chunk in enumerate(chunks):
|
|
719
|
+
# Process chunk - returns CuPy arrays of coordinates
|
|
720
|
+
fore_coords, _ = self.process_chunk(chunk)
|
|
721
|
+
|
|
722
|
+
if isinstance(fore_coords, list) and len(fore_coords) == 0:
|
|
723
|
+
# Skip empty results
|
|
724
|
+
pass
|
|
725
|
+
elif hasattr(fore_coords, 'shape') and len(fore_coords) > 0:
|
|
726
|
+
# Direct indexing with CuPy arrays
|
|
727
|
+
try:
|
|
728
|
+
array[fore_coords[:, 0], fore_coords[:, 1], fore_coords[:, 2]] = 255
|
|
729
|
+
except IndexError as e:
|
|
730
|
+
print(f"Index error when updating array: {e}")
|
|
731
|
+
# Fallback to a safer but slower approach
|
|
732
|
+
for coord in fore_coords:
|
|
733
|
+
try:
|
|
734
|
+
z, y, x = int(coord[0]), int(coord[1]), int(coord[2])
|
|
735
|
+
if 0 <= z < array.shape[0] and 0 <= y < array.shape[1] and 0 <= x < array.shape[2]:
|
|
736
|
+
array[z, y, x] = 255
|
|
737
|
+
except Exception as inner_e:
|
|
738
|
+
print(f"Error updating coordinate {coord}: {inner_e}")
|
|
739
|
+
|
|
740
|
+
# Memory management - release reference to chunk data
|
|
741
|
+
if i % 10 == 0: # Do periodic memory cleanup
|
|
742
|
+
cp.get_default_memory_pool().free_all_blocks()
|
|
743
|
+
|
|
744
|
+
print(f"Processed {i+1}/{len(chunks)} chunks")
|
|
745
|
+
|
|
746
|
+
# Clean up GPU memory
|
|
747
|
+
cp.get_default_memory_pool().free_all_blocks()
|
|
748
|
+
|
|
749
|
+
# Convert to NumPy at the very end for return
|
|
750
|
+
return cp.asnumpy(array)
|
|
751
|
+
|
|
752
|
+
def update_position(self, z=None, x=None, y=None):
|
|
753
|
+
"""Update current position for chunk prioritization with safeguards"""
|
|
754
|
+
|
|
755
|
+
# Check if we should skip this update
|
|
756
|
+
if hasattr(self, '_skip_next_update') and self._skip_next_update:
|
|
757
|
+
self._skip_next_update = False
|
|
758
|
+
return
|
|
759
|
+
|
|
760
|
+
# Store the previous z-position if not set
|
|
761
|
+
if not hasattr(self, 'prev_z') or self.prev_z is None:
|
|
762
|
+
self.prev_z = z
|
|
763
|
+
|
|
764
|
+
# Check if currently processing - if so, only update position but don't trigger map_slice changes
|
|
765
|
+
if hasattr(self, '_currently_processing') and self._currently_processing:
|
|
766
|
+
self.current_z = z
|
|
767
|
+
self.current_x = x
|
|
768
|
+
self.current_y = y
|
|
769
|
+
self.prev_z = z
|
|
770
|
+
return
|
|
771
|
+
|
|
772
|
+
# Update current positions
|
|
773
|
+
self.current_z = z
|
|
774
|
+
self.current_x = x
|
|
775
|
+
self.current_y = y
|
|
776
|
+
|
|
777
|
+
# Only clear map_slice if z changes and we're not already generating a new one
|
|
778
|
+
if self.current_z != self.prev_z:
|
|
779
|
+
# Instead of setting to None, check if we already have it in the cache
|
|
780
|
+
if hasattr(self, 'feature_cache') and self.feature_cache is not None:
|
|
781
|
+
if self.current_z not in self.feature_cache:
|
|
782
|
+
self.map_slice = None
|
|
783
|
+
self._currently_segmenting = None
|
|
784
|
+
|
|
785
|
+
# Update previous z
|
|
786
|
+
self.prev_z = z
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def get_realtime_chunks(self, chunk_size=49):
|
|
790
|
+
|
|
791
|
+
# Determine if we need to chunk XY planes
|
|
792
|
+
small_dims = (self.image_3d.shape[1] <= chunk_size and
|
|
793
|
+
self.image_3d.shape[2] <= chunk_size)
|
|
794
|
+
few_z = self.image_3d.shape[0] <= 100 # arbitrary threshold
|
|
795
|
+
|
|
796
|
+
# If small enough, each Z is one chunk
|
|
797
|
+
if small_dims and few_z:
|
|
798
|
+
chunk_size_xy = max(self.image_3d.shape[1], self.image_3d.shape[2])
|
|
799
|
+
else:
|
|
800
|
+
chunk_size_xy = chunk_size
|
|
801
|
+
|
|
802
|
+
# Calculate chunks for XY plane
|
|
803
|
+
y_chunks = (self.image_3d.shape[1] + chunk_size_xy - 1) // chunk_size_xy
|
|
804
|
+
x_chunks = (self.image_3d.shape[2] + chunk_size_xy - 1) // chunk_size_xy
|
|
805
|
+
|
|
806
|
+
# Populate chunk dictionary
|
|
807
|
+
chunk_dict = {}
|
|
808
|
+
|
|
809
|
+
# Create chunks for each Z plane
|
|
810
|
+
for z in range(self.image_3d.shape[0]):
|
|
811
|
+
if small_dims:
|
|
812
|
+
|
|
813
|
+
chunk_dict[(z, 0, 0)] = {
|
|
814
|
+
'coords': [0, self.image_3d.shape[1], 0, self.image_3d.shape[2]],
|
|
815
|
+
'processed': False,
|
|
816
|
+
'z': z
|
|
817
|
+
}
|
|
818
|
+
else:
|
|
819
|
+
# Multiple chunks per Z
|
|
820
|
+
for y_chunk in range(y_chunks):
|
|
821
|
+
for x_chunk in range(x_chunks):
|
|
822
|
+
y_start = y_chunk * chunk_size_xy
|
|
823
|
+
x_start = x_chunk * chunk_size_xy
|
|
824
|
+
y_end = min(y_start + chunk_size_xy, self.image_3d.shape[1])
|
|
825
|
+
x_end = min(x_start + chunk_size_xy, self.image_3d.shape[2])
|
|
826
|
+
|
|
827
|
+
chunk_dict[(z, y_start, x_start)] = {
|
|
828
|
+
'coords': [y_start, y_end, x_start, x_end],
|
|
829
|
+
'processed': False,
|
|
830
|
+
'z': z
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
self.realtimechunks = chunk_dict
|
|
834
|
+
|
|
835
|
+
print("Ready!")
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def segment_volume_realtime(self, gpu=True):
|
|
839
|
+
"""Segment volume in realtime using CuPy for GPU acceleration"""
|
|
840
|
+
import cupy as cp
|
|
841
|
+
|
|
842
|
+
#try:
|
|
843
|
+
#from cuml.ensemble import RandomForestClassifier as cuRandomForestClassifier
|
|
844
|
+
#gpu_ml_available = True
|
|
845
|
+
#except:
|
|
846
|
+
#print("Cannot find cuML, using CPU to segment instead...")
|
|
847
|
+
#gpu_ml_available = False
|
|
848
|
+
#gpu = False
|
|
849
|
+
|
|
850
|
+
if self.realtimechunks is None:
|
|
851
|
+
self.get_realtime_chunks()
|
|
852
|
+
else:
|
|
853
|
+
for chunk_pos in self.realtimechunks: # chunk_pos is the (z, y_start, x_start) tuple
|
|
854
|
+
self.realtimechunks[chunk_pos]['processed'] = False
|
|
855
|
+
|
|
856
|
+
chunk_dict = self.realtimechunks
|
|
857
|
+
|
|
858
|
+
def get_nearest_unprocessed_chunk(self):
|
|
859
|
+
"""Get nearest unprocessed chunk prioritizing current Z"""
|
|
860
|
+
curr_z = self.current_z if self.current_z is not None else self.image_3d.shape[0] // 2
|
|
861
|
+
curr_y = self.current_y if self.current_y is not None else self.image_3d.shape[1] // 2
|
|
862
|
+
curr_x = self.current_x if self.current_x is not None else self.image_3d.shape[2] // 2
|
|
863
|
+
|
|
864
|
+
# First try to find chunks at current Z
|
|
865
|
+
current_z_chunks = [(pos, info) for pos, info in chunk_dict.items()
|
|
866
|
+
if pos[0] == curr_z and not info['processed']]
|
|
867
|
+
|
|
868
|
+
if current_z_chunks:
|
|
869
|
+
# Find nearest chunk in current Z plane using the chunk positions from the key
|
|
870
|
+
nearest = min(current_z_chunks,
|
|
871
|
+
key=lambda x: ((x[0][1] - curr_y) ** 2 +
|
|
872
|
+
(x[0][2] - curr_x) ** 2))
|
|
873
|
+
return nearest[0]
|
|
874
|
+
|
|
875
|
+
# If no chunks at current Z, find nearest Z with available chunks
|
|
876
|
+
available_z = sorted(
|
|
877
|
+
[(pos[0], pos) for pos, info in chunk_dict.items()
|
|
878
|
+
if not info['processed']],
|
|
879
|
+
key=lambda x: abs(x[0] - curr_z)
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
if available_z:
|
|
883
|
+
target_z = available_z[0][0]
|
|
884
|
+
# Find nearest chunk in target Z plane
|
|
885
|
+
z_chunks = [(pos, info) for pos, info in chunk_dict.items()
|
|
886
|
+
if pos[0] == target_z and not info['processed']]
|
|
887
|
+
nearest = min(z_chunks,
|
|
888
|
+
key=lambda x: ((x[0][1] - curr_y) ** 2 +
|
|
889
|
+
(x[0][2] - curr_x) ** 2))
|
|
890
|
+
return nearest[0]
|
|
891
|
+
|
|
892
|
+
return None
|
|
893
|
+
|
|
894
|
+
while True:
|
|
895
|
+
# Find nearest unprocessed chunk using class attributes
|
|
896
|
+
chunk_idx = get_nearest_unprocessed_chunk(self)
|
|
897
|
+
if chunk_idx is None:
|
|
898
|
+
break
|
|
899
|
+
|
|
900
|
+
# Process the chunk directly
|
|
901
|
+
chunk = chunk_dict[chunk_idx]
|
|
902
|
+
chunk['processed'] = True
|
|
903
|
+
coords = chunk['coords']
|
|
904
|
+
|
|
905
|
+
# Use CuPy for meshgrid
|
|
906
|
+
coords_array = cp.stack(cp.meshgrid(
|
|
907
|
+
cp.array([chunk['z']]),
|
|
908
|
+
cp.arange(coords[0], coords[1]),
|
|
909
|
+
cp.arange(coords[2], coords[3]),
|
|
910
|
+
indexing='ij'
|
|
911
|
+
)).reshape(3, -1).T
|
|
912
|
+
|
|
913
|
+
# Convert to CPU for further processing - add cp.asnumpy() here
|
|
914
|
+
coords = list(map(tuple, cp.asnumpy(coords_array)))
|
|
915
|
+
|
|
916
|
+
# Process the chunk directly based on whether GPU is available
|
|
917
|
+
fore, back = self.process_chunk(coords)
|
|
918
|
+
|
|
919
|
+
# Yield the results
|
|
920
|
+
yield cp.asnumpy(fore), cp.asnumpy(back)
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def cleanup(self):
|
|
924
|
+
"""Clean up GPU memory"""
|
|
925
|
+
import cupy as cp
|
|
926
|
+
|
|
927
|
+
try:
|
|
928
|
+
# Force garbage collection first
|
|
929
|
+
import gc
|
|
930
|
+
gc.collect()
|
|
931
|
+
|
|
932
|
+
# Clean up CuPy memory pools
|
|
933
|
+
mempool = cp.get_default_memory_pool()
|
|
934
|
+
pinned_mempool = cp.get_default_pinned_memory_pool()
|
|
935
|
+
|
|
936
|
+
# Print memory usage before cleanup (optional)
|
|
937
|
+
# print(f"Used GPU memory: {mempool.used_bytes() / 1024**2:.2f} MB")
|
|
938
|
+
|
|
939
|
+
# Free all blocks
|
|
940
|
+
mempool.free_all_blocks()
|
|
941
|
+
pinned_mempool.free_all_blocks()
|
|
942
|
+
|
|
943
|
+
# Print memory usage after cleanup (optional)
|
|
944
|
+
# print(f"Used GPU memory after cleanup: {mempool.used_bytes() / 1024**2:.2f} MB")
|
|
945
|
+
|
|
946
|
+
except Exception as e:
|
|
947
|
+
print(f"Warning: Could not clean up GPU memory: {e}")
|
|
948
|
+
|
|
949
|
+
def train_batch(self, foreground_array, speed=True, use_gpu=True, use_two=False, mem_lock=False, saving = False):
|
|
950
|
+
"""Train directly on foreground and background arrays using GPU acceleration"""
|
|
951
|
+
import cupy as cp
|
|
952
|
+
|
|
953
|
+
if not saving:
|
|
954
|
+
print("Training model...")
|
|
955
|
+
|
|
956
|
+
self.speed = speed
|
|
957
|
+
self.cur_gpu = use_gpu
|
|
958
|
+
self.realtimechunks = None # dump ram
|
|
959
|
+
|
|
960
|
+
self.mem_lock = mem_lock
|
|
961
|
+
|
|
962
|
+
self.model = RandomForestClassifier(
|
|
963
|
+
n_estimators=100,
|
|
964
|
+
n_jobs=-1,
|
|
965
|
+
max_depth=None
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
if use_two:
|
|
969
|
+
|
|
970
|
+
#changed = [] #Track which slices need feature maps
|
|
971
|
+
|
|
972
|
+
if not self.use_two: #Clarifies if we need to redo feature cache for 2D
|
|
973
|
+
self.feature_cache = None
|
|
974
|
+
self.use_two = True
|
|
975
|
+
|
|
976
|
+
self.feature_cache = None #Decided this should reset, can remove this line to have it retain prev feature maps
|
|
977
|
+
self.two_slices = []
|
|
978
|
+
|
|
979
|
+
if self.feature_cache == None:
|
|
980
|
+
self.feature_cache = {}
|
|
981
|
+
|
|
982
|
+
foreground_array = cp.asarray(foreground_array)
|
|
983
|
+
|
|
984
|
+
# Get foreground coordinates and features
|
|
985
|
+
z_fore, y_fore, x_fore = cp.where(foreground_array == 1)
|
|
986
|
+
|
|
987
|
+
z_fore_cpu = cp.asnumpy(z_fore)
|
|
988
|
+
y_fore_cpu = cp.asnumpy(y_fore)
|
|
989
|
+
x_fore_cpu = cp.asnumpy(x_fore)
|
|
990
|
+
|
|
991
|
+
fore_coords = list(zip(z_fore_cpu, y_fore_cpu, x_fore_cpu))
|
|
992
|
+
|
|
993
|
+
# Get background coordinates and features
|
|
994
|
+
z_back, y_back, x_back = cp.where(foreground_array == 2)
|
|
995
|
+
|
|
996
|
+
z_back_cpu = cp.asnumpy(z_back)
|
|
997
|
+
y_back_cpu = cp.asnumpy(y_back)
|
|
998
|
+
x_back_cpu = cp.asnumpy(x_back)
|
|
999
|
+
|
|
1000
|
+
back_coords = list(zip(z_back_cpu, y_back_cpu, x_back_cpu))
|
|
1001
|
+
|
|
1002
|
+
foreground_features = []
|
|
1003
|
+
background_features = []
|
|
1004
|
+
|
|
1005
|
+
z_fores = self.organize_by_z(fore_coords)
|
|
1006
|
+
z_backs = self.organize_by_z(back_coords)
|
|
1007
|
+
slices = set(list(z_fores.keys()) + list(z_backs.keys()))
|
|
1008
|
+
|
|
1009
|
+
for z in slices:
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
current_map = self.get_feature_map_slice(z, speed, use_gpu)
|
|
1013
|
+
|
|
1014
|
+
if z in z_fores:
|
|
1015
|
+
|
|
1016
|
+
for y, x in z_fores[z]:
|
|
1017
|
+
# Get the feature vector for this foreground point
|
|
1018
|
+
feature_vector = current_map[y, x]
|
|
1019
|
+
|
|
1020
|
+
# Add to our collection
|
|
1021
|
+
foreground_features.append(cp.asnumpy(feature_vector))
|
|
1022
|
+
|
|
1023
|
+
if z in z_backs:
|
|
1024
|
+
|
|
1025
|
+
for y, x in z_backs[z]:
|
|
1026
|
+
# Get the feature vector for this foreground point
|
|
1027
|
+
feature_vector = current_map[y, x]
|
|
1028
|
+
|
|
1029
|
+
# Add to our collection
|
|
1030
|
+
background_features.append(cp.asnumpy(feature_vector))
|
|
1031
|
+
|
|
1032
|
+
else:
|
|
1033
|
+
|
|
1034
|
+
box_size = self.master_chunk
|
|
1035
|
+
|
|
1036
|
+
# Memory-efficient approach: compute features only for necessary subarrays
|
|
1037
|
+
foreground_features = []
|
|
1038
|
+
background_features = []
|
|
1039
|
+
|
|
1040
|
+
# Convert foreground_array to CuPy array
|
|
1041
|
+
foreground_array_gpu = cp.asarray(foreground_array)
|
|
1042
|
+
|
|
1043
|
+
# Find coordinates of foreground and background scribbles
|
|
1044
|
+
z_fore = cp.argwhere(foreground_array_gpu == 1)
|
|
1045
|
+
z_back = cp.argwhere(foreground_array_gpu == 2)
|
|
1046
|
+
|
|
1047
|
+
# Convert back to NumPy for compatibility with the rest of the code
|
|
1048
|
+
z_fore_cpu = cp.asnumpy(z_fore)
|
|
1049
|
+
z_back_cpu = cp.asnumpy(z_back)
|
|
1050
|
+
|
|
1051
|
+
# If no scribbles, return empty lists
|
|
1052
|
+
if len(z_fore_cpu) == 0 and len(z_back_cpu) == 0:
|
|
1053
|
+
return foreground_features, background_features
|
|
1054
|
+
|
|
1055
|
+
# Get dimensions of the input array
|
|
1056
|
+
depth, height, width = foreground_array.shape
|
|
1057
|
+
|
|
1058
|
+
# Determine the minimum number of boxes needed to cover all scribbles
|
|
1059
|
+
half_box = box_size // 2
|
|
1060
|
+
|
|
1061
|
+
# Step 1: Find the minimum set of boxes that cover all scribbles
|
|
1062
|
+
# We'll divide the volume into a grid of boxes of size box_size
|
|
1063
|
+
|
|
1064
|
+
# Calculate how many boxes are needed in each dimension
|
|
1065
|
+
z_grid_size = (depth + box_size - 1) // box_size
|
|
1066
|
+
y_grid_size = (height + box_size - 1) // box_size
|
|
1067
|
+
x_grid_size = (width + box_size - 1) // box_size
|
|
1068
|
+
|
|
1069
|
+
# Track which grid cells contain scribbles
|
|
1070
|
+
grid_cells_with_scribbles = set()
|
|
1071
|
+
|
|
1072
|
+
# Map original coordinates to grid cells
|
|
1073
|
+
for z, y, x in cp.vstack((z_fore_cpu, z_back_cpu)) if len(z_back_cpu) > 0 else z_fore_cpu:
|
|
1074
|
+
grid_z = int(z // box_size)
|
|
1075
|
+
grid_y = int(y // box_size)
|
|
1076
|
+
grid_x = int(x // box_size)
|
|
1077
|
+
grid_cells_with_scribbles.add((grid_z, grid_y, grid_x))
|
|
1078
|
+
|
|
1079
|
+
# Step 2: Process each grid cell that contains scribbles
|
|
1080
|
+
for grid_z, grid_y, grid_x in grid_cells_with_scribbles:
|
|
1081
|
+
# Calculate the boundaries of this grid cell
|
|
1082
|
+
z_min = grid_z * box_size
|
|
1083
|
+
y_min = grid_y * box_size
|
|
1084
|
+
x_min = grid_x * box_size
|
|
1085
|
+
|
|
1086
|
+
z_max = min(z_min + box_size, depth)
|
|
1087
|
+
y_max = min(y_min + box_size, height)
|
|
1088
|
+
x_max = min(x_min + box_size, width)
|
|
1089
|
+
|
|
1090
|
+
# Extract the subarray (assuming image_3d is already a CuPy array)
|
|
1091
|
+
subarray = self.image_3d[z_min:z_max, y_min:y_max, x_min:x_max]
|
|
1092
|
+
subarray2 = foreground_array_gpu[z_min:z_max, y_min:y_max, x_min:x_max]
|
|
1093
|
+
|
|
1094
|
+
# Compute features for this subarray
|
|
1095
|
+
if self.speed:
|
|
1096
|
+
subarray_features = self.compute_feature_maps_gpu(subarray)
|
|
1097
|
+
else:
|
|
1098
|
+
subarray_features = self.compute_deep_feature_maps_gpu(subarray)
|
|
1099
|
+
|
|
1100
|
+
# Extract foreground features using a direct mask comparison
|
|
1101
|
+
local_fore_coords = cp.argwhere(subarray2 == 1)
|
|
1102
|
+
for local_z, local_y, local_x in cp.asnumpy(local_fore_coords):
|
|
1103
|
+
feature = subarray_features[int(local_z), int(local_y), int(local_x)]
|
|
1104
|
+
foreground_features.append(cp.asnumpy(feature))
|
|
1105
|
+
|
|
1106
|
+
# Extract background features using a direct mask comparison
|
|
1107
|
+
local_back_coords = cp.argwhere(subarray2 == 2)
|
|
1108
|
+
for local_z, local_y, local_x in cp.asnumpy(local_back_coords):
|
|
1109
|
+
feature = subarray_features[int(local_z), int(local_y), int(local_x)]
|
|
1110
|
+
background_features.append(cp.asnumpy(feature))
|
|
1111
|
+
|
|
1112
|
+
if self.previous_foreground is not None:
|
|
1113
|
+
failed = True
|
|
1114
|
+
try:
|
|
1115
|
+
# Make sure foreground_features is a NumPy array before vstack
|
|
1116
|
+
if isinstance(foreground_features, list):
|
|
1117
|
+
foreground_features = np.array(foreground_features)
|
|
1118
|
+
|
|
1119
|
+
# Convert CuPy arrays to NumPy if necessary
|
|
1120
|
+
if hasattr(foreground_features, 'get'):
|
|
1121
|
+
foreground_features = foreground_features.get()
|
|
1122
|
+
|
|
1123
|
+
foreground_features = np.vstack([self.previous_foreground, foreground_features])
|
|
1124
|
+
failed = False
|
|
1125
|
+
except Exception as e:
|
|
1126
|
+
pass
|
|
1127
|
+
|
|
1128
|
+
try:
|
|
1129
|
+
# Make sure background_features is a NumPy array before vstack
|
|
1130
|
+
if isinstance(background_features, list):
|
|
1131
|
+
background_features = np.array(background_features)
|
|
1132
|
+
|
|
1133
|
+
# Convert CuPy arrays to NumPy if necessary
|
|
1134
|
+
if hasattr(background_features, 'get'):
|
|
1135
|
+
background_features = background_features.get()
|
|
1136
|
+
|
|
1137
|
+
background_features = np.vstack([self.previous_background, background_features])
|
|
1138
|
+
failed = False
|
|
1139
|
+
except Exception as e:
|
|
1140
|
+
pass
|
|
1141
|
+
try:
|
|
1142
|
+
# Ensure coordinate arrays are NumPy arrays
|
|
1143
|
+
if hasattr(z_fore_cpu, 'get'):
|
|
1144
|
+
z_fore_cpu = z_fore_cpu.get()
|
|
1145
|
+
if hasattr(self.previous_z_fore, 'get'):
|
|
1146
|
+
self.previous_z_fore = self.previous_z_fore.get()
|
|
1147
|
+
|
|
1148
|
+
z_fore_cpu = np.concatenate([self.previous_z_fore, z_fore_cpu])
|
|
1149
|
+
except Exception as e:
|
|
1150
|
+
pass
|
|
1151
|
+
try:
|
|
1152
|
+
# Ensure coordinate arrays are NumPy arrays
|
|
1153
|
+
if hasattr(z_back_cpu, 'get'):
|
|
1154
|
+
z_back_cpu = z_back_cpu.get()
|
|
1155
|
+
if hasattr(self.previous_z_back, 'get'):
|
|
1156
|
+
self.previous_z_back = self.previous_z_back.get()
|
|
1157
|
+
|
|
1158
|
+
z_back_cpu = np.concatenate([self.previous_z_back, z_back_cpu])
|
|
1159
|
+
except Exception as e:
|
|
1160
|
+
pass
|
|
1161
|
+
if failed:
|
|
1162
|
+
print("Could not combine new model with old loaded model. Perhaps you are trying to combine a quick model with a deep model? I cannot combine these...")
|
|
1163
|
+
|
|
1164
|
+
if saving:
|
|
1165
|
+
# Make sure to return NumPy arrays, not CuPy arrays
|
|
1166
|
+
if hasattr(foreground_features, 'get'):
|
|
1167
|
+
foreground_features = foreground_features.get()
|
|
1168
|
+
if hasattr(background_features, 'get'):
|
|
1169
|
+
background_features = background_features.get()
|
|
1170
|
+
if hasattr(z_fore_cpu, 'get'):
|
|
1171
|
+
z_fore_cpu = z_fore_cpu.get()
|
|
1172
|
+
if hasattr(z_back_cpu, 'get'):
|
|
1173
|
+
z_back_cpu = z_back_cpu.get()
|
|
1174
|
+
|
|
1175
|
+
return foreground_features, background_features, z_fore_cpu, z_back_cpu
|
|
1176
|
+
|
|
1177
|
+
# Make sure foreground_features and background_features are NumPy arrays
|
|
1178
|
+
if isinstance(foreground_features, list):
|
|
1179
|
+
foreground_features = np.array(foreground_features)
|
|
1180
|
+
elif hasattr(foreground_features, 'get'):
|
|
1181
|
+
foreground_features = foreground_features.get()
|
|
1182
|
+
|
|
1183
|
+
if isinstance(background_features, list):
|
|
1184
|
+
background_features = np.array(background_features)
|
|
1185
|
+
elif hasattr(background_features, 'get'):
|
|
1186
|
+
background_features = background_features.get()
|
|
1187
|
+
|
|
1188
|
+
# Combine features and labels for training
|
|
1189
|
+
X = np.vstack([foreground_features, background_features])
|
|
1190
|
+
y = np.hstack([np.ones(len(z_fore_cpu)), np.zeros(len(z_back_cpu))])
|
|
1191
|
+
|
|
1192
|
+
# Train the model
|
|
1193
|
+
try:
|
|
1194
|
+
self.model.fit(X, y)
|
|
1195
|
+
except Exception as e:
|
|
1196
|
+
print(f"Error during model training: {e}")
|
|
1197
|
+
import traceback
|
|
1198
|
+
|
|
1199
|
+
self.current_speed = speed
|
|
1200
|
+
|
|
1201
|
+
# Clean up GPU memory
|
|
1202
|
+
cp.get_default_memory_pool().free_all_blocks()
|
|
1203
|
+
|
|
1204
|
+
print("Done")
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def save_model(self, file_name, foreground_array):
|
|
1208
|
+
|
|
1209
|
+
print("Saving model data")
|
|
1210
|
+
|
|
1211
|
+
foreground_features, background_features, z_fore, z_back = self.train_batch(foreground_array, speed = self.speed, use_gpu = self.use_gpu, use_two = self.use_two, mem_lock = self.mem_lock, saving = True)
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
np.savez(file_name,
|
|
1215
|
+
foreground_features=foreground_features,
|
|
1216
|
+
background_features=background_features,
|
|
1217
|
+
z_fore=z_fore,
|
|
1218
|
+
z_back=z_back,
|
|
1219
|
+
speed=self.speed,
|
|
1220
|
+
use_gpu=self.use_gpu,
|
|
1221
|
+
use_two=self.use_two,
|
|
1222
|
+
mem_lock=self.mem_lock)
|
|
1223
|
+
|
|
1224
|
+
print(f"Model data saved to {file_name}")
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
def load_model(self, file_name):
|
|
1228
|
+
|
|
1229
|
+
print("Loading model data")
|
|
1230
|
+
|
|
1231
|
+
data = np.load(file_name)
|
|
1232
|
+
|
|
1233
|
+
# Unpack the arrays
|
|
1234
|
+
self.previous_foreground = data['foreground_features']
|
|
1235
|
+
self.previous_background = data['background_features']
|
|
1236
|
+
self.previous_z_fore = data['z_fore']
|
|
1237
|
+
self.previous_z_back = data['z_back']
|
|
1238
|
+
self.speed = bool(data['speed'])
|
|
1239
|
+
self.use_gpu = bool(data['use_gpu'])
|
|
1240
|
+
self.use_two = bool(data['use_two'])
|
|
1241
|
+
self.mem_lock = bool(data['mem_lock'])
|
|
1242
|
+
|
|
1243
|
+
X = np.vstack([self.previous_foreground, self.previous_background])
|
|
1244
|
+
y = np.hstack([np.ones(len(self.previous_z_fore)), np.zeros(len(self.previous_z_back))])
|
|
1245
|
+
|
|
1246
|
+
try:
|
|
1247
|
+
self.model.fit(X, y)
|
|
1248
|
+
except:
|
|
1249
|
+
print(X)
|
|
1250
|
+
print(y)
|
|
1251
|
+
|
|
1252
|
+
print("Done")
|
|
1253
|
+
|
|
1254
|
+
def get_feature_map_slice(self, z, speed, use_gpu):
|
|
1255
|
+
|
|
1256
|
+
if self._currently_segmenting is not None:
|
|
1257
|
+
return
|
|
1258
|
+
|
|
1259
|
+
if speed:
|
|
1260
|
+
output = self.compute_feature_maps_gpu_2d(z = z)
|
|
1261
|
+
|
|
1262
|
+
elif not speed:
|
|
1263
|
+
output = self.compute_deep_feature_maps_gpu_2d(z = z)
|
|
1264
|
+
|
|
1265
|
+
return output
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
def organize_by_z(self, coordinates):
|
|
1270
|
+
"""
|
|
1271
|
+
Organizes a list of [z, y, x] coordinates into a dictionary of [y, x] coordinates grouped by z-value.
|
|
1272
|
+
|
|
1273
|
+
Args:
|
|
1274
|
+
coordinates: List of [z, y, x] coordinate lists
|
|
1275
|
+
|
|
1276
|
+
Returns:
|
|
1277
|
+
Dictionary with z-values as keys and lists of corresponding [y, x] coordinates as values
|
|
1278
|
+
"""
|
|
1279
|
+
z_dict = defaultdict(list)
|
|
1280
|
+
|
|
1281
|
+
for z, y, x in coordinates:
|
|
1282
|
+
z_dict[z].append((y, x))
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
return dict(z_dict) # Convert back to regular dict
|
|
1286
|
+
|