isoview 0.1.0__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.
- isoview/__init__.py +36 -0
- isoview/array.py +11 -0
- isoview/config.py +213 -0
- isoview/corrections.py +135 -0
- isoview/fusion.py +979 -0
- isoview/intensity.py +427 -0
- isoview/io.py +942 -0
- isoview/masks.py +421 -0
- isoview/pipeline.py +913 -0
- isoview/segmentation.py +173 -0
- isoview/temporal.py +373 -0
- isoview/transforms.py +1115 -0
- isoview/viz.py +723 -0
- isoview-0.1.0.dist-info/METADATA +370 -0
- isoview-0.1.0.dist-info/RECORD +17 -0
- isoview-0.1.0.dist-info/WHEEL +4 -0
- isoview-0.1.0.dist-info/entry_points.txt +2 -0
isoview/pipeline.py
ADDED
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
"""Main processing pipeline for isoview microscopy data."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import shutil
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from .config import ProcessingConfig
|
|
11
|
+
from .corrections import correct_dead_pixels, estimate_background, percentile_interp
|
|
12
|
+
from .io import (
|
|
13
|
+
find_stack_file,
|
|
14
|
+
infer_file_structure,
|
|
15
|
+
read_volume,
|
|
16
|
+
read_all_xml_metadata,
|
|
17
|
+
read_background_values,
|
|
18
|
+
write_volume,
|
|
19
|
+
write_to_consolidated_zarr,
|
|
20
|
+
setup_consolidated_zarr,
|
|
21
|
+
add_label_to_registry,
|
|
22
|
+
)
|
|
23
|
+
from .segmentation import create_coordinate_masks, fuse_masks, segment_foreground
|
|
24
|
+
from .transforms import apply_mask, crop_volume, flip_volume, rotate_volume
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class IsoviewProcessor:
|
|
28
|
+
"""Process multi-view light sheet microscopy data."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, config: ProcessingConfig):
|
|
31
|
+
"""Initialize processor with configuration."""
|
|
32
|
+
self.config = config
|
|
33
|
+
self.structure = infer_file_structure(
|
|
34
|
+
config.input_dir, config.specimen, config.channels
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Load metadata from all XML files
|
|
38
|
+
self.metadata, self.per_camera_metadata = read_all_xml_metadata(config.input_dir, config.specimen)
|
|
39
|
+
|
|
40
|
+
self.background_values = read_background_values(config.input_dir, percentile=3.0)
|
|
41
|
+
|
|
42
|
+
# create output directories
|
|
43
|
+
self.config.output_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
self.config.projection_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
def process_timepoint(self, timepoint: int) -> None:
|
|
47
|
+
"""Process single timepoint across all cameras and channels.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
timepoint: timepoint index to process
|
|
51
|
+
"""
|
|
52
|
+
# create output subdirectory
|
|
53
|
+
output_subdir = self.config.output_dir / f"TM{timepoint:06d}"
|
|
54
|
+
output_subdir.mkdir(exist_ok=True)
|
|
55
|
+
|
|
56
|
+
# determine channel processing groups
|
|
57
|
+
if self.config.reference_channels is not None:
|
|
58
|
+
ref_groups = self.config.reference_channels
|
|
59
|
+
dep_groups = self.config.dependent_channels or [[] for _ in ref_groups]
|
|
60
|
+
else:
|
|
61
|
+
# each channel is independent
|
|
62
|
+
ref_groups = [[ch] for ch in self.config.channels]
|
|
63
|
+
dep_groups = [[] for _ in self.config.channels]
|
|
64
|
+
|
|
65
|
+
# process each camera
|
|
66
|
+
for camera in self.config.cameras:
|
|
67
|
+
self._process_camera(timepoint, camera, output_subdir, ref_groups, dep_groups)
|
|
68
|
+
|
|
69
|
+
def _process_camera(
|
|
70
|
+
self,
|
|
71
|
+
timepoint: int,
|
|
72
|
+
camera: int,
|
|
73
|
+
output_dir: Path,
|
|
74
|
+
ref_groups: list[list[int]],
|
|
75
|
+
dep_groups: list[list[int]],
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Process single camera for given timepoint."""
|
|
78
|
+
|
|
79
|
+
# process each reference group
|
|
80
|
+
for refs, deps in zip(ref_groups, dep_groups):
|
|
81
|
+
self._process_channel_group(
|
|
82
|
+
timepoint, camera, output_dir, refs, deps
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def _process_channel_group(
|
|
86
|
+
self,
|
|
87
|
+
timepoint: int,
|
|
88
|
+
camera: int,
|
|
89
|
+
output_dir: Path,
|
|
90
|
+
ref_channels: list[int],
|
|
91
|
+
dep_channels: list[int],
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Process group of reference and dependent channels."""
|
|
94
|
+
# load and process reference channels
|
|
95
|
+
ref_volumes = []
|
|
96
|
+
ref_masks = []
|
|
97
|
+
|
|
98
|
+
for channel in ref_channels:
|
|
99
|
+
volume = self._load_and_correct(timepoint, camera, channel)
|
|
100
|
+
|
|
101
|
+
if self.config.segment:
|
|
102
|
+
# generate mask
|
|
103
|
+
mask = segment_foreground(
|
|
104
|
+
volume,
|
|
105
|
+
self.config.gauss_sigma,
|
|
106
|
+
self.config.gauss_kernel,
|
|
107
|
+
self.config.segment_threshold,
|
|
108
|
+
self.config.mask_percentile,
|
|
109
|
+
self.config.scaling,
|
|
110
|
+
self.config.splitting,
|
|
111
|
+
self.config.subsample_factor,
|
|
112
|
+
verbose=True,
|
|
113
|
+
)
|
|
114
|
+
ref_masks.append(mask)
|
|
115
|
+
|
|
116
|
+
# apply mask
|
|
117
|
+
volume = apply_mask(volume, mask)
|
|
118
|
+
|
|
119
|
+
ref_volumes.append(volume)
|
|
120
|
+
|
|
121
|
+
# save reference volume and get path for consolidated mode
|
|
122
|
+
base_zarr_path = self._save_volume(timepoint, camera, channel, volume, output_dir)
|
|
123
|
+
|
|
124
|
+
# fuse masks if multiple references
|
|
125
|
+
if len(ref_masks) > 1:
|
|
126
|
+
fused_mask = fuse_masks(ref_masks)
|
|
127
|
+
elif ref_masks:
|
|
128
|
+
fused_mask = ref_masks[0]
|
|
129
|
+
else:
|
|
130
|
+
fused_mask = None
|
|
131
|
+
|
|
132
|
+
# save mask and coordinate masks
|
|
133
|
+
if fused_mask is not None and self.config.segment:
|
|
134
|
+
# Determine camera_subpath for consolidated mode
|
|
135
|
+
camera_subpath = None
|
|
136
|
+
if self.config.zarr_consolidate and self.config.output_format == "zarr":
|
|
137
|
+
camera_subpath = f"camera_{camera}"
|
|
138
|
+
self._save_mask(timepoint, camera, ref_channels[0], fused_mask, output_dir, base_zarr_path, camera_subpath)
|
|
139
|
+
|
|
140
|
+
# process dependent channels
|
|
141
|
+
for channel in dep_channels:
|
|
142
|
+
volume = self._load_and_correct(timepoint, camera, channel)
|
|
143
|
+
|
|
144
|
+
if fused_mask is not None:
|
|
145
|
+
volume = apply_mask(volume, fused_mask)
|
|
146
|
+
|
|
147
|
+
self._save_volume(timepoint, camera, channel, volume, output_dir)
|
|
148
|
+
|
|
149
|
+
def _load_and_correct(
|
|
150
|
+
self,
|
|
151
|
+
timepoint: int,
|
|
152
|
+
camera: int,
|
|
153
|
+
channel: int,
|
|
154
|
+
) -> np.ndarray:
|
|
155
|
+
"""Load volume and apply corrections."""
|
|
156
|
+
# find stack file
|
|
157
|
+
stack_path = find_stack_file(
|
|
158
|
+
self.config.input_dir,
|
|
159
|
+
self.config.specimen,
|
|
160
|
+
timepoint,
|
|
161
|
+
camera,
|
|
162
|
+
channel,
|
|
163
|
+
self.structure,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
volume = read_volume(stack_path)
|
|
167
|
+
|
|
168
|
+
crop_top = self.config.crop_top[camera] if self.config.crop_top else 0
|
|
169
|
+
crop_left = self.config.crop_left[camera] if self.config.crop_left else 0
|
|
170
|
+
crop_height = self.config.crop_height[camera] if self.config.crop_height else None
|
|
171
|
+
crop_width = self.config.crop_width[camera] if self.config.crop_width else None
|
|
172
|
+
crop_front = self.config.crop_front[camera] if self.config.crop_front else 0
|
|
173
|
+
crop_depth = self.config.crop_depth[camera] if self.config.crop_depth else None
|
|
174
|
+
|
|
175
|
+
if crop_top or crop_left or crop_front or crop_height or crop_width or crop_depth:
|
|
176
|
+
volume = crop_volume(
|
|
177
|
+
volume,
|
|
178
|
+
top=crop_top,
|
|
179
|
+
left=crop_left,
|
|
180
|
+
height=crop_height,
|
|
181
|
+
width=crop_width,
|
|
182
|
+
front=crop_front,
|
|
183
|
+
depth=crop_depth,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# apply rotation
|
|
187
|
+
if self.config.rotation != 0:
|
|
188
|
+
volume = rotate_volume(volume, self.config.rotation)
|
|
189
|
+
|
|
190
|
+
# apply flips to second camera in each pair
|
|
191
|
+
if self.config.is_second_in_pair(camera):
|
|
192
|
+
if self.config.flip_horizontal or self.config.flip_vertical:
|
|
193
|
+
volume = flip_volume(
|
|
194
|
+
volume,
|
|
195
|
+
self.config.flip_horizontal,
|
|
196
|
+
self.config.flip_vertical,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if self.config.median_kernel is not None:
|
|
200
|
+
cam_idx = self.config.cameras.index(camera) if camera in self.config.cameras else 0
|
|
201
|
+
if cam_idx < len(self.background_values):
|
|
202
|
+
bg = self.background_values[cam_idx]
|
|
203
|
+
else:
|
|
204
|
+
bg = estimate_background(volume, percentile=3.0, subsample=self.config.subsample_factor)
|
|
205
|
+
|
|
206
|
+
volume = correct_dead_pixels(
|
|
207
|
+
volume,
|
|
208
|
+
bg,
|
|
209
|
+
self.config.median_kernel,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return volume
|
|
213
|
+
|
|
214
|
+
def _save_volume(
|
|
215
|
+
self,
|
|
216
|
+
timepoint: int,
|
|
217
|
+
camera: int,
|
|
218
|
+
channel: int,
|
|
219
|
+
volume: np.ndarray,
|
|
220
|
+
output_dir: Path,
|
|
221
|
+
) -> Path:
|
|
222
|
+
"""Save processed volume and projection.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Path to the main output file (for consolidated mode)
|
|
226
|
+
"""
|
|
227
|
+
# Determine output path based on consolidate mode
|
|
228
|
+
if self.config.zarr_consolidate and self.config.output_format == "zarr":
|
|
229
|
+
# Multi-camera consolidated structure: header.zarr/camera_N/
|
|
230
|
+
data_header = self.metadata.get("data_header", "data")
|
|
231
|
+
base_filename = f"{data_header}_TM{timepoint:06d}_SPM{self.config.specimen:02d}.zarr"
|
|
232
|
+
main_path = output_dir / base_filename
|
|
233
|
+
camera_subpath = f"camera_{camera}"
|
|
234
|
+
else:
|
|
235
|
+
# Traditional single-file structure
|
|
236
|
+
filename = (
|
|
237
|
+
f"SPM{self.config.specimen:02d}_TM{timepoint:06d}_"
|
|
238
|
+
f"CM{camera:02d}_CHN{channel:02d}{self.config.extension}"
|
|
239
|
+
)
|
|
240
|
+
main_path = output_dir / filename
|
|
241
|
+
camera_subpath = None
|
|
242
|
+
|
|
243
|
+
# Get camera-specific metadata if available
|
|
244
|
+
camera_metadata = self.per_camera_metadata.get(camera, {}).copy()
|
|
245
|
+
|
|
246
|
+
# Add min intensity to camera metadata
|
|
247
|
+
camera_metadata["min_intensity"] = float(np.min(volume))
|
|
248
|
+
|
|
249
|
+
write_volume(
|
|
250
|
+
volume,
|
|
251
|
+
main_path,
|
|
252
|
+
self.config.pixel_spacing,
|
|
253
|
+
compression=self.config.zarr_compression,
|
|
254
|
+
compression_level=self.config.zarr_compression_level,
|
|
255
|
+
chunks=self.config.zarr_chunks,
|
|
256
|
+
shards=self.config.zarr_shards,
|
|
257
|
+
metadata=self.metadata,
|
|
258
|
+
camera_subpath=camera_subpath,
|
|
259
|
+
camera_metadata=camera_metadata,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Setup consolidated structure if needed
|
|
263
|
+
if self.config.zarr_consolidate and self.config.output_format == "zarr":
|
|
264
|
+
setup_consolidated_zarr(main_path, self.metadata, camera_subpath)
|
|
265
|
+
|
|
266
|
+
# save xy projection
|
|
267
|
+
proj = np.max(volume, axis=0)
|
|
268
|
+
|
|
269
|
+
if self.config.zarr_consolidate and self.config.output_format == "zarr":
|
|
270
|
+
# Write projection into the consolidated file
|
|
271
|
+
write_to_consolidated_zarr(
|
|
272
|
+
main_path,
|
|
273
|
+
"projections/xy",
|
|
274
|
+
proj[np.newaxis, :, :],
|
|
275
|
+
pixel_spacing=self.config.pixel_spacing,
|
|
276
|
+
compression=self.config.zarr_compression,
|
|
277
|
+
compression_level=self.config.zarr_compression_level,
|
|
278
|
+
chunks=self.config.zarr_chunks,
|
|
279
|
+
shards=self.config.zarr_shards,
|
|
280
|
+
camera_subpath=camera_subpath,
|
|
281
|
+
)
|
|
282
|
+
else:
|
|
283
|
+
# Write as separate file
|
|
284
|
+
proj_filename = (
|
|
285
|
+
f"SPM{self.config.specimen:02d}_TM{timepoint:06d}_"
|
|
286
|
+
f"CM{camera:02d}_CHN{channel:02d}.xyProjection{self.config.extension}"
|
|
287
|
+
)
|
|
288
|
+
write_volume(
|
|
289
|
+
proj[np.newaxis, :, :],
|
|
290
|
+
self.config.projection_dir / proj_filename,
|
|
291
|
+
self.config.pixel_spacing,
|
|
292
|
+
compression=self.config.zarr_compression,
|
|
293
|
+
compression_level=self.config.zarr_compression_level,
|
|
294
|
+
chunks=self.config.zarr_chunks,
|
|
295
|
+
shards=self.config.zarr_shards,
|
|
296
|
+
metadata=self.metadata,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return main_path
|
|
300
|
+
|
|
301
|
+
def _save_mask(
|
|
302
|
+
self,
|
|
303
|
+
timepoint: int,
|
|
304
|
+
camera: int,
|
|
305
|
+
channel: int,
|
|
306
|
+
mask: np.ndarray,
|
|
307
|
+
output_dir: Path,
|
|
308
|
+
base_zarr_path: Optional[Path] = None,
|
|
309
|
+
camera_subpath: Optional[str] = None,
|
|
310
|
+
) -> None:
|
|
311
|
+
"""Save segmentation mask and coordinate masks.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
timepoint: Timepoint index
|
|
315
|
+
camera: Camera index
|
|
316
|
+
channel: Channel index
|
|
317
|
+
mask: Segmentation mask array
|
|
318
|
+
output_dir: Output directory
|
|
319
|
+
base_zarr_path: Path to consolidated Zarr file (if consolidate mode)
|
|
320
|
+
camera_subpath: Optional camera subgroup path for multi-camera structure
|
|
321
|
+
"""
|
|
322
|
+
prefix = f"SPM{self.config.specimen:02d}_TM{timepoint:06d}_CM{camera:02d}_CHN{channel:02d}"
|
|
323
|
+
|
|
324
|
+
# Generate coordinate masks
|
|
325
|
+
xz_mask, xy_mask = create_coordinate_masks(mask, self.config.splitting)
|
|
326
|
+
|
|
327
|
+
if self.config.zarr_consolidate and self.config.output_format == "zarr" and base_zarr_path:
|
|
328
|
+
# Write masks into consolidated file
|
|
329
|
+
# Binary segmentation mask
|
|
330
|
+
write_to_consolidated_zarr(
|
|
331
|
+
base_zarr_path,
|
|
332
|
+
"labels/segmentation",
|
|
333
|
+
mask,
|
|
334
|
+
compression=self.config.zarr_compression,
|
|
335
|
+
compression_level=self.config.zarr_compression_level,
|
|
336
|
+
chunks=self.config.zarr_chunks,
|
|
337
|
+
shards=self.config.zarr_shards,
|
|
338
|
+
label_metadata={
|
|
339
|
+
"version": "0.5",
|
|
340
|
+
"colors": [
|
|
341
|
+
{"label-value": 0, "rgba": [0, 0, 0, 0]}, # background: transparent
|
|
342
|
+
{"label-value": 1, "rgba": [0, 255, 0, 128]}, # foreground: semi-transparent green
|
|
343
|
+
],
|
|
344
|
+
"source": {"image": "../../0"},
|
|
345
|
+
},
|
|
346
|
+
camera_subpath=camera_subpath,
|
|
347
|
+
)
|
|
348
|
+
add_label_to_registry(base_zarr_path, "segmentation", camera_subpath)
|
|
349
|
+
|
|
350
|
+
# XZ coordinate mask
|
|
351
|
+
write_to_consolidated_zarr(
|
|
352
|
+
base_zarr_path,
|
|
353
|
+
"labels/xz_coords",
|
|
354
|
+
xz_mask[np.newaxis, :, :],
|
|
355
|
+
compression=self.config.zarr_compression,
|
|
356
|
+
compression_level=self.config.zarr_compression_level,
|
|
357
|
+
chunks=self.config.zarr_chunks,
|
|
358
|
+
shards=self.config.zarr_shards,
|
|
359
|
+
label_metadata={
|
|
360
|
+
"version": "0.5",
|
|
361
|
+
"source": {"image": "../../0"},
|
|
362
|
+
},
|
|
363
|
+
camera_subpath=camera_subpath,
|
|
364
|
+
)
|
|
365
|
+
add_label_to_registry(base_zarr_path, "xz_coords", camera_subpath)
|
|
366
|
+
|
|
367
|
+
# XY coordinate mask
|
|
368
|
+
write_to_consolidated_zarr(
|
|
369
|
+
base_zarr_path,
|
|
370
|
+
"labels/xy_coords",
|
|
371
|
+
xy_mask[np.newaxis, :, :],
|
|
372
|
+
compression=self.config.zarr_compression,
|
|
373
|
+
compression_level=self.config.zarr_compression_level,
|
|
374
|
+
chunks=self.config.zarr_chunks,
|
|
375
|
+
shards=self.config.zarr_shards,
|
|
376
|
+
label_metadata={
|
|
377
|
+
"version": "0.5",
|
|
378
|
+
"source": {"image": "../../0"},
|
|
379
|
+
},
|
|
380
|
+
camera_subpath=camera_subpath,
|
|
381
|
+
)
|
|
382
|
+
add_label_to_registry(base_zarr_path, "xy_coords", camera_subpath)
|
|
383
|
+
|
|
384
|
+
else:
|
|
385
|
+
# Write as separate files
|
|
386
|
+
mask_file = output_dir / f"{prefix}.segmentationMask{self.config.extension}"
|
|
387
|
+
write_volume(
|
|
388
|
+
mask,
|
|
389
|
+
mask_file,
|
|
390
|
+
compression=self.config.zarr_compression,
|
|
391
|
+
compression_level=self.config.zarr_compression_level,
|
|
392
|
+
chunks=self.config.zarr_chunks,
|
|
393
|
+
shards=self.config.zarr_shards,
|
|
394
|
+
metadata=self.metadata,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
xz_file = output_dir / f"{prefix}.xzMask{self.config.extension}"
|
|
398
|
+
write_volume(
|
|
399
|
+
xz_mask[np.newaxis, :, :],
|
|
400
|
+
xz_file,
|
|
401
|
+
compression=self.config.zarr_compression,
|
|
402
|
+
compression_level=self.config.zarr_compression_level,
|
|
403
|
+
chunks=self.config.zarr_chunks,
|
|
404
|
+
shards=self.config.zarr_shards,
|
|
405
|
+
metadata=self.metadata,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
xy_file = output_dir / f"{prefix}.xyMask{self.config.extension}"
|
|
409
|
+
write_volume(
|
|
410
|
+
xy_mask[np.newaxis, :, :],
|
|
411
|
+
xy_file,
|
|
412
|
+
compression=self.config.zarr_compression,
|
|
413
|
+
compression_level=self.config.zarr_compression_level,
|
|
414
|
+
chunks=self.config.zarr_chunks,
|
|
415
|
+
shards=self.config.zarr_shards,
|
|
416
|
+
metadata=self.metadata,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def process_all(self) -> None:
|
|
420
|
+
"""Process all timepoints."""
|
|
421
|
+
for timepoint in self.config.timepoints:
|
|
422
|
+
self.process_timepoint(timepoint)
|
|
423
|
+
|
|
424
|
+
def process_single_timepoint(self, timepoint: int) -> None:
|
|
425
|
+
"""Process single timepoint.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
timepoint: timepoint index to process
|
|
429
|
+
"""
|
|
430
|
+
# create output subdirectory
|
|
431
|
+
output_subdir = self.config.output_dir / f"TM{timepoint:06d}"
|
|
432
|
+
output_subdir.mkdir(exist_ok=True, parents=True)
|
|
433
|
+
|
|
434
|
+
# load global mask if segment_mode == 3
|
|
435
|
+
global_mask = None
|
|
436
|
+
if self.config.segment_mode == 3:
|
|
437
|
+
# search for global mask in any supported format
|
|
438
|
+
global_mask_path = None
|
|
439
|
+
for ext in [".klb", ".zarr", ".tif", ".tiff"]:
|
|
440
|
+
candidate = self.config.global_mask_dir / f"Global.segmentationMask{ext}"
|
|
441
|
+
if candidate.exists():
|
|
442
|
+
global_mask_path = candidate
|
|
443
|
+
break
|
|
444
|
+
if global_mask_path is not None:
|
|
445
|
+
global_mask = read_volume(global_mask_path)
|
|
446
|
+
else:
|
|
447
|
+
raise FileNotFoundError(
|
|
448
|
+
f"global mask not found in {self.config.global_mask_dir} "
|
|
449
|
+
"(tried .klb, .zarr, .tif, .tiff)"
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
if self.config.reference_channels is not None:
|
|
453
|
+
ref_groups = self.config.reference_channels
|
|
454
|
+
dep_groups = self.config.dependent_channels or [[] for _ in ref_groups]
|
|
455
|
+
else:
|
|
456
|
+
# each channel is independent reference
|
|
457
|
+
ref_groups = [[ch] for ch in self.config.channels]
|
|
458
|
+
dep_groups = [[] for _ in self.config.channels]
|
|
459
|
+
|
|
460
|
+
# process each camera
|
|
461
|
+
for cam_idx, camera in enumerate(self.config.cameras):
|
|
462
|
+
self._process_single_camera(
|
|
463
|
+
timepoint, camera, cam_idx, output_subdir,
|
|
464
|
+
ref_groups, dep_groups, global_mask
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def _process_single_camera(
|
|
468
|
+
self,
|
|
469
|
+
timepoint: int,
|
|
470
|
+
camera: int,
|
|
471
|
+
cam_idx: int,
|
|
472
|
+
output_dir: Path,
|
|
473
|
+
ref_groups: list[list[int]],
|
|
474
|
+
dep_groups: list[list[int]],
|
|
475
|
+
global_mask: Optional[np.ndarray],
|
|
476
|
+
) -> None:
|
|
477
|
+
"""Process single camera."""
|
|
478
|
+
# get the valid channel for this camera from the mapping
|
|
479
|
+
valid_channel = self.config.camera_channel_map.get(camera)
|
|
480
|
+
|
|
481
|
+
# process each reference group, but only if it matches the camera's channel
|
|
482
|
+
for r_idx, (ref_channels, dep_channels) in enumerate(zip(ref_groups, dep_groups)):
|
|
483
|
+
# filter ref_channels to only include the valid channel for this camera
|
|
484
|
+
filtered_refs = [ch for ch in ref_channels if ch == valid_channel]
|
|
485
|
+
filtered_deps = [ch for ch in dep_channels if ch == valid_channel]
|
|
486
|
+
|
|
487
|
+
if not filtered_refs:
|
|
488
|
+
# no valid channels for this camera in this group
|
|
489
|
+
continue
|
|
490
|
+
|
|
491
|
+
self._process_single_channel_group(
|
|
492
|
+
timepoint, camera, cam_idx, output_dir,
|
|
493
|
+
filtered_refs, filtered_deps, global_mask
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
def _process_single_channel_group(
|
|
497
|
+
self,
|
|
498
|
+
timepoint: int,
|
|
499
|
+
camera: int,
|
|
500
|
+
cam_idx: int,
|
|
501
|
+
output_dir: Path,
|
|
502
|
+
ref_channels: list[int],
|
|
503
|
+
dep_channels: list[int],
|
|
504
|
+
global_mask: Optional[np.ndarray],
|
|
505
|
+
) -> None:
|
|
506
|
+
"""Process channel group."""
|
|
507
|
+
ref_stacks = []
|
|
508
|
+
ref_masks = []
|
|
509
|
+
|
|
510
|
+
for channel in ref_channels:
|
|
511
|
+
stack = self._load_and_preprocess(timepoint, camera, cam_idx, channel)
|
|
512
|
+
|
|
513
|
+
subsampled = stack.ravel()[::self.config.subsample_factor]
|
|
514
|
+
subsampled = subsampled[subsampled > 0]
|
|
515
|
+
min_intensity_stack = percentile_interp(subsampled, self.config.background_percentile)
|
|
516
|
+
|
|
517
|
+
# generate segmentation mask if needed
|
|
518
|
+
if self.config.segment_mode in (1, 2):
|
|
519
|
+
mask = segment_foreground(
|
|
520
|
+
stack,
|
|
521
|
+
kernel_sigma=self.config.gauss_sigma,
|
|
522
|
+
kernel_size=self.config.gauss_kernel,
|
|
523
|
+
threshold=self.config.segment_threshold,
|
|
524
|
+
mask_percentile=self.config.mask_percentile,
|
|
525
|
+
scaling=self.config.scaling,
|
|
526
|
+
splitting=self.config.splitting,
|
|
527
|
+
subsample_factor=self.config.subsample_factor,
|
|
528
|
+
verbose=True,
|
|
529
|
+
)
|
|
530
|
+
ref_masks.append(mask)
|
|
531
|
+
|
|
532
|
+
# save min intensity for both mask and stack
|
|
533
|
+
min_intensity_mask = percentile_interp(
|
|
534
|
+
mask.ravel()[::self.config.subsample_factor],
|
|
535
|
+
self.config.mask_percentile
|
|
536
|
+
)
|
|
537
|
+
min_intensity = np.array([min_intensity_mask, min_intensity_stack])
|
|
538
|
+
else:
|
|
539
|
+
min_intensity = min_intensity_stack
|
|
540
|
+
|
|
541
|
+
# save min intensity
|
|
542
|
+
self._save_min_intensity(output_dir, timepoint, camera, channel, min_intensity)
|
|
543
|
+
|
|
544
|
+
ref_stacks.append(stack)
|
|
545
|
+
|
|
546
|
+
# fuse masks if multiple references
|
|
547
|
+
if len(ref_masks) > 1 and self.config.segment_mode in (1, 2):
|
|
548
|
+
fused_mask = fuse_masks(ref_masks)
|
|
549
|
+
elif ref_masks:
|
|
550
|
+
fused_mask = ref_masks[0]
|
|
551
|
+
else:
|
|
552
|
+
fused_mask = None
|
|
553
|
+
|
|
554
|
+
# build master channel list (refs + deps)
|
|
555
|
+
master_list = list(ref_channels) + list(dep_channels)
|
|
556
|
+
|
|
557
|
+
# save segmentation mask
|
|
558
|
+
if self.config.segment_mode in (1, 2) and fused_mask is not None:
|
|
559
|
+
self._save_segmentation_mask_file(
|
|
560
|
+
output_dir, timepoint, camera, master_list[0], fused_mask
|
|
561
|
+
)
|
|
562
|
+
# copy to other channels
|
|
563
|
+
if len(master_list) > 1 and self.config.segment_mode == 1:
|
|
564
|
+
for other_ch in master_list[1:]:
|
|
565
|
+
self._copy_mask_file(output_dir, timepoint, camera, master_list[0], other_ch)
|
|
566
|
+
|
|
567
|
+
# skip volume saving if segment_mode == 2 (masks only)
|
|
568
|
+
if self.config.segment_mode == 2:
|
|
569
|
+
return
|
|
570
|
+
|
|
571
|
+
# apply mask and save reference stacks
|
|
572
|
+
for stack, channel in zip(ref_stacks, ref_channels):
|
|
573
|
+
# apply mask
|
|
574
|
+
if self.config.segment_mode == 1 and fused_mask is not None:
|
|
575
|
+
stack = apply_mask(stack, fused_mask)
|
|
576
|
+
elif self.config.segment_mode == 3 and global_mask is not None:
|
|
577
|
+
stack = apply_mask(stack, global_mask)
|
|
578
|
+
|
|
579
|
+
# save volume
|
|
580
|
+
self._save_volume_file(output_dir, timepoint, camera, channel, stack)
|
|
581
|
+
|
|
582
|
+
# save xy projection
|
|
583
|
+
projection = np.max(stack, axis=0)
|
|
584
|
+
self._save_projection_file(timepoint, camera, channel, projection)
|
|
585
|
+
|
|
586
|
+
# generate and save coordinate masks
|
|
587
|
+
if self.config.segment_mode == 1 and fused_mask is not None:
|
|
588
|
+
xz_mask, xy_mask = create_coordinate_masks(fused_mask, self.config.splitting)
|
|
589
|
+
self._save_coordinate_masks(output_dir, timepoint, camera, master_list[0], xz_mask, xy_mask)
|
|
590
|
+
# copy to other channels
|
|
591
|
+
if len(master_list) > 1:
|
|
592
|
+
for other_ch in master_list[1:]:
|
|
593
|
+
self._copy_coord_masks(output_dir, timepoint, camera, master_list[0], other_ch)
|
|
594
|
+
|
|
595
|
+
for channel in dep_channels:
|
|
596
|
+
stack = self._load_and_preprocess(timepoint, camera, cam_idx, channel)
|
|
597
|
+
|
|
598
|
+
subsampled = stack.ravel()[::self.config.subsample_factor]
|
|
599
|
+
subsampled = subsampled[subsampled > 0]
|
|
600
|
+
min_intensity = percentile_interp(subsampled, self.config.background_percentile)
|
|
601
|
+
self._save_min_intensity(output_dir, timepoint, camera, channel, min_intensity)
|
|
602
|
+
|
|
603
|
+
# apply mask
|
|
604
|
+
if self.config.segment_mode == 1 and fused_mask is not None:
|
|
605
|
+
stack = apply_mask(stack, fused_mask)
|
|
606
|
+
elif self.config.segment_mode == 3 and global_mask is not None:
|
|
607
|
+
stack = apply_mask(stack, global_mask)
|
|
608
|
+
|
|
609
|
+
# save volume
|
|
610
|
+
self._save_volume_file(output_dir, timepoint, camera, channel, stack)
|
|
611
|
+
|
|
612
|
+
# save xy projection
|
|
613
|
+
projection = np.max(stack, axis=0)
|
|
614
|
+
self._save_projection_file(timepoint, camera, channel, projection)
|
|
615
|
+
|
|
616
|
+
def _load_and_preprocess(
|
|
617
|
+
self,
|
|
618
|
+
timepoint: int,
|
|
619
|
+
camera: int,
|
|
620
|
+
cam_idx: int,
|
|
621
|
+
channel: int,
|
|
622
|
+
) -> np.ndarray:
|
|
623
|
+
"""Load stack and apply preprocessing (crop, rotate, flip, dead pixel correction)."""
|
|
624
|
+
# find and read stack
|
|
625
|
+
stack_path = find_stack_file(
|
|
626
|
+
self.config.input_dir,
|
|
627
|
+
self.config.specimen,
|
|
628
|
+
timepoint,
|
|
629
|
+
camera,
|
|
630
|
+
channel,
|
|
631
|
+
self.structure,
|
|
632
|
+
)
|
|
633
|
+
stack = read_volume(stack_path)
|
|
634
|
+
|
|
635
|
+
# apply cropping (uses cam_idx to index into per-camera arrays, supports partial params)
|
|
636
|
+
crop_top = self.config.crop_top[cam_idx] if self.config.crop_top else 0
|
|
637
|
+
crop_left = self.config.crop_left[cam_idx] if self.config.crop_left else 0
|
|
638
|
+
crop_height = self.config.crop_height[cam_idx] if self.config.crop_height else None
|
|
639
|
+
crop_width = self.config.crop_width[cam_idx] if self.config.crop_width else None
|
|
640
|
+
crop_front = self.config.crop_front[cam_idx] if self.config.crop_front else 0
|
|
641
|
+
crop_depth = self.config.crop_depth[cam_idx] if self.config.crop_depth else None
|
|
642
|
+
|
|
643
|
+
if crop_top or crop_left or crop_front or crop_height or crop_width or crop_depth:
|
|
644
|
+
stack = crop_volume(
|
|
645
|
+
stack,
|
|
646
|
+
top=crop_top,
|
|
647
|
+
left=crop_left,
|
|
648
|
+
height=crop_height,
|
|
649
|
+
width=crop_width,
|
|
650
|
+
front=crop_front,
|
|
651
|
+
depth=crop_depth,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
# apply rotation
|
|
655
|
+
if self.config.rotation != 0:
|
|
656
|
+
stack = rotate_volume(stack, self.config.rotation)
|
|
657
|
+
|
|
658
|
+
# apply flips to second camera in each pair
|
|
659
|
+
if self.config.is_second_in_pair(camera):
|
|
660
|
+
if self.config.flip_horizontal or self.config.flip_vertical:
|
|
661
|
+
stack = flip_volume(stack, self.config.flip_horizontal, self.config.flip_vertical)
|
|
662
|
+
|
|
663
|
+
if self.config.median_kernel is not None:
|
|
664
|
+
if cam_idx < len(self.background_values):
|
|
665
|
+
bg = self.background_values[cam_idx]
|
|
666
|
+
else:
|
|
667
|
+
bg = estimate_background(stack, percentile=3.0, subsample=self.config.subsample_factor)
|
|
668
|
+
stack = correct_dead_pixels(stack, bg, self.config.median_kernel)
|
|
669
|
+
|
|
670
|
+
return stack
|
|
671
|
+
|
|
672
|
+
def _save_min_intensity(
|
|
673
|
+
self,
|
|
674
|
+
output_dir: Path,
|
|
675
|
+
timepoint: int,
|
|
676
|
+
camera: int,
|
|
677
|
+
channel: int,
|
|
678
|
+
min_intensity,
|
|
679
|
+
) -> None:
|
|
680
|
+
"""Save min intensity to npz file."""
|
|
681
|
+
filename = f"SPM{self.config.specimen:02d}_TM{timepoint:06d}_CM{camera:02d}_CHN{channel:02d}.minIntensity.npz"
|
|
682
|
+
np.savez(output_dir / filename, min_intensity=min_intensity)
|
|
683
|
+
|
|
684
|
+
def _save_segmentation_mask_file(
|
|
685
|
+
self,
|
|
686
|
+
output_dir: Path,
|
|
687
|
+
timepoint: int,
|
|
688
|
+
camera: int,
|
|
689
|
+
channel: int,
|
|
690
|
+
mask: np.ndarray,
|
|
691
|
+
) -> None:
|
|
692
|
+
"""Save segmentation mask."""
|
|
693
|
+
prefix = f"SPM{self.config.specimen:02d}_TM{timepoint:06d}_CM{camera:02d}_CHN{channel:02d}"
|
|
694
|
+
write_volume(
|
|
695
|
+
mask,
|
|
696
|
+
output_dir / f"{prefix}.segmentationMask{self.config.extension}",
|
|
697
|
+
compression=self.config.zarr_compression,
|
|
698
|
+
compression_level=self.config.zarr_compression_level,
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
def _copy_mask_file(
|
|
702
|
+
self,
|
|
703
|
+
output_dir: Path,
|
|
704
|
+
timepoint: int,
|
|
705
|
+
camera: int,
|
|
706
|
+
src_channel: int,
|
|
707
|
+
dst_channel: int,
|
|
708
|
+
) -> None:
|
|
709
|
+
"""Copy mask file to another channel."""
|
|
710
|
+
prefix_src = f"SPM{self.config.specimen:02d}_TM{timepoint:06d}_CM{camera:02d}_CHN{src_channel:02d}"
|
|
711
|
+
prefix_dst = f"SPM{self.config.specimen:02d}_TM{timepoint:06d}_CM{camera:02d}_CHN{dst_channel:02d}"
|
|
712
|
+
src = output_dir / f"{prefix_src}.segmentationMask{self.config.extension}"
|
|
713
|
+
dst = output_dir / f"{prefix_dst}.segmentationMask{self.config.extension}"
|
|
714
|
+
if src.exists():
|
|
715
|
+
if src.is_dir():
|
|
716
|
+
shutil.copytree(src, dst, dirs_exist_ok=True)
|
|
717
|
+
else:
|
|
718
|
+
shutil.copy2(src, dst)
|
|
719
|
+
|
|
720
|
+
def _save_coordinate_masks(
|
|
721
|
+
self,
|
|
722
|
+
output_dir: Path,
|
|
723
|
+
timepoint: int,
|
|
724
|
+
camera: int,
|
|
725
|
+
channel: int,
|
|
726
|
+
xz_mask: np.ndarray,
|
|
727
|
+
xy_mask: np.ndarray,
|
|
728
|
+
) -> None:
|
|
729
|
+
"""Save xz and xy coordinate masks."""
|
|
730
|
+
prefix = f"SPM{self.config.specimen:02d}_TM{timepoint:06d}_CM{camera:02d}_CHN{channel:02d}"
|
|
731
|
+
|
|
732
|
+
write_volume(
|
|
733
|
+
xz_mask[np.newaxis, :, :],
|
|
734
|
+
output_dir / f"{prefix}.xzMask{self.config.extension}",
|
|
735
|
+
compression=self.config.zarr_compression,
|
|
736
|
+
compression_level=self.config.zarr_compression_level,
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
write_volume(
|
|
740
|
+
xy_mask[np.newaxis, :, :],
|
|
741
|
+
output_dir / f"{prefix}.xyMask{self.config.extension}",
|
|
742
|
+
compression=self.config.zarr_compression,
|
|
743
|
+
compression_level=self.config.zarr_compression_level,
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
def _copy_coord_masks(
|
|
747
|
+
self,
|
|
748
|
+
output_dir: Path,
|
|
749
|
+
timepoint: int,
|
|
750
|
+
camera: int,
|
|
751
|
+
src_channel: int,
|
|
752
|
+
dst_channel: int,
|
|
753
|
+
) -> None:
|
|
754
|
+
"""Copy coordinate mask files to another channel."""
|
|
755
|
+
prefix_src = f"SPM{self.config.specimen:02d}_TM{timepoint:06d}_CM{camera:02d}_CHN{src_channel:02d}"
|
|
756
|
+
prefix_dst = f"SPM{self.config.specimen:02d}_TM{timepoint:06d}_CM{camera:02d}_CHN{dst_channel:02d}"
|
|
757
|
+
|
|
758
|
+
for mask_type in ["xzMask", "xyMask"]:
|
|
759
|
+
src = output_dir / f"{prefix_src}.{mask_type}{self.config.extension}"
|
|
760
|
+
dst = output_dir / f"{prefix_dst}.{mask_type}{self.config.extension}"
|
|
761
|
+
if src.exists():
|
|
762
|
+
if src.is_dir():
|
|
763
|
+
shutil.copytree(src, dst, dirs_exist_ok=True)
|
|
764
|
+
else:
|
|
765
|
+
shutil.copy2(src, dst)
|
|
766
|
+
|
|
767
|
+
def _save_volume_file(
|
|
768
|
+
self,
|
|
769
|
+
output_dir: Path,
|
|
770
|
+
timepoint: int,
|
|
771
|
+
camera: int,
|
|
772
|
+
channel: int,
|
|
773
|
+
volume: np.ndarray,
|
|
774
|
+
) -> None:
|
|
775
|
+
"""Save processed volume."""
|
|
776
|
+
prefix = f"SPM{self.config.specimen:02d}_TM{timepoint:06d}_CM{camera:02d}_CHN{channel:02d}"
|
|
777
|
+
write_volume(
|
|
778
|
+
volume,
|
|
779
|
+
output_dir / f"{prefix}{self.config.extension}",
|
|
780
|
+
pixel_spacing=self.config.pixel_spacing,
|
|
781
|
+
compression=self.config.zarr_compression,
|
|
782
|
+
compression_level=self.config.zarr_compression_level,
|
|
783
|
+
chunks=self.config.zarr_chunks,
|
|
784
|
+
shards=self.config.zarr_shards,
|
|
785
|
+
metadata=self.metadata,
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
def _save_projection_file(
|
|
789
|
+
self,
|
|
790
|
+
timepoint: int,
|
|
791
|
+
camera: int,
|
|
792
|
+
channel: int,
|
|
793
|
+
projection: np.ndarray,
|
|
794
|
+
) -> None:
|
|
795
|
+
"""Save xy projection."""
|
|
796
|
+
self.config.projection_dir.mkdir(parents=True, exist_ok=True)
|
|
797
|
+
prefix = f"SPM{self.config.specimen:02d}_TM{timepoint:06d}_CM{camera:02d}_CHN{channel:02d}"
|
|
798
|
+
write_volume(
|
|
799
|
+
projection[np.newaxis, :, :],
|
|
800
|
+
self.config.projection_dir / f"{prefix}.xyProjection{self.config.extension}",
|
|
801
|
+
pixel_spacing=self.config.pixel_spacing,
|
|
802
|
+
compression=self.config.zarr_compression,
|
|
803
|
+
compression_level=self.config.zarr_compression_level,
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
def run(self) -> None:
|
|
807
|
+
"""Process all timepoints."""
|
|
808
|
+
n_timepoints = len(self.config.timepoints)
|
|
809
|
+
print(f"processing {n_timepoints} timepoints: {self.config.timepoints[0]} to {self.config.timepoints[-1]}")
|
|
810
|
+
print(f"cameras: {self.config.cameras}, channels: {self.config.channels}")
|
|
811
|
+
print(f"segment_mode: {self.config.segment_mode}, output: {self.config.output_format}")
|
|
812
|
+
print("-" * 60)
|
|
813
|
+
|
|
814
|
+
total_start = time.time()
|
|
815
|
+
timepoint_times = []
|
|
816
|
+
|
|
817
|
+
for i, timepoint in enumerate(self.config.timepoints):
|
|
818
|
+
tp_start = time.time()
|
|
819
|
+
print(f"[{i+1}/{n_timepoints}] timepoint {timepoint:04d}\n", end="", flush=True)
|
|
820
|
+
|
|
821
|
+
self.process_single_timepoint(timepoint)
|
|
822
|
+
|
|
823
|
+
tp_elapsed = time.time() - tp_start
|
|
824
|
+
timepoint_times.append(tp_elapsed)
|
|
825
|
+
print(f" - {tp_elapsed:.1f}s")
|
|
826
|
+
|
|
827
|
+
total_elapsed = time.time() - total_start
|
|
828
|
+
avg_time = total_elapsed / n_timepoints if n_timepoints > 0 else 0
|
|
829
|
+
|
|
830
|
+
print("-" * 60)
|
|
831
|
+
print(f"completed {n_timepoints} timepoints in {total_elapsed:.1f}s ({avg_time:.2f}s/timepoint)")
|
|
832
|
+
|
|
833
|
+
# generate global mask if segment_mode == 2
|
|
834
|
+
if self.config.segment_mode == 2:
|
|
835
|
+
self.generate_global_mask()
|
|
836
|
+
|
|
837
|
+
def generate_global_mask(self) -> None:
|
|
838
|
+
"""Generate global segmentation mask from individual timepoint masks."""
|
|
839
|
+
print("generating global segmentation mask")
|
|
840
|
+
self.config.global_mask_dir.mkdir(parents=True, exist_ok=True)
|
|
841
|
+
|
|
842
|
+
global_mask = None
|
|
843
|
+
mask_count = 0
|
|
844
|
+
|
|
845
|
+
for timepoint in self.config.timepoints:
|
|
846
|
+
mask_dir = self.config.output_dir / f"TM{timepoint:06d}"
|
|
847
|
+
|
|
848
|
+
for camera in self.config.cameras:
|
|
849
|
+
# determine which channels have masks
|
|
850
|
+
if self.config.reference_channels is not None:
|
|
851
|
+
channels_to_check = [ref[0] for ref in self.config.reference_channels]
|
|
852
|
+
else:
|
|
853
|
+
channels_to_check = self.config.channels
|
|
854
|
+
|
|
855
|
+
for channel in channels_to_check:
|
|
856
|
+
prefix = f"SPM{self.config.specimen:02d}_TM{timepoint:06d}_CM{camera:02d}_CHN{channel:02d}"
|
|
857
|
+
mask_path = mask_dir / f"{prefix}.segmentationMask{self.config.extension}"
|
|
858
|
+
|
|
859
|
+
if mask_path.exists():
|
|
860
|
+
print(f"reading mask: {mask_path.name}")
|
|
861
|
+
mask = read_volume(mask_path)
|
|
862
|
+
|
|
863
|
+
if global_mask is None:
|
|
864
|
+
global_mask = mask == 1
|
|
865
|
+
else:
|
|
866
|
+
global_mask = global_mask | (mask == 1)
|
|
867
|
+
|
|
868
|
+
mask_count += 1
|
|
869
|
+
|
|
870
|
+
if global_mask is None:
|
|
871
|
+
print("no masks found")
|
|
872
|
+
return
|
|
873
|
+
|
|
874
|
+
print(f"total masks read: {mask_count}")
|
|
875
|
+
|
|
876
|
+
# save global mask
|
|
877
|
+
global_mask_uint16 = global_mask.astype(np.uint16)
|
|
878
|
+
write_volume(
|
|
879
|
+
global_mask_uint16,
|
|
880
|
+
self.config.global_mask_dir / f"Global.segmentationMask{self.config.extension}",
|
|
881
|
+
compression=self.config.zarr_compression,
|
|
882
|
+
compression_level=self.config.zarr_compression_level,
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
# generate and save coordinate masks
|
|
886
|
+
xz_mask, xy_mask = create_coordinate_masks(global_mask_uint16, self.config.splitting)
|
|
887
|
+
|
|
888
|
+
write_volume(
|
|
889
|
+
xz_mask[np.newaxis, :, :],
|
|
890
|
+
self.config.global_mask_dir / f"Global.xzMask{self.config.extension}",
|
|
891
|
+
compression=self.config.zarr_compression,
|
|
892
|
+
compression_level=self.config.zarr_compression_level,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
write_volume(
|
|
896
|
+
xy_mask[np.newaxis, :, :],
|
|
897
|
+
self.config.global_mask_dir / f"Global.xyMask{self.config.extension}",
|
|
898
|
+
compression=self.config.zarr_compression,
|
|
899
|
+
compression_level=self.config.zarr_compression_level,
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
print("global mask saved")
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def process_timepoint(config: ProcessingConfig) -> None:
|
|
906
|
+
"""Process all timepoints in config (matches MATLAB processTimepoint)."""
|
|
907
|
+
processor = IsoviewProcessor(config)
|
|
908
|
+
processor.run()
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def process_dataset(config: ProcessingConfig) -> None:
|
|
912
|
+
processor = IsoviewProcessor(config)
|
|
913
|
+
processor.process_all()
|