Rhapso 0.1.92__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.
- Rhapso/__init__.py +1 -0
- Rhapso/data_prep/__init__.py +2 -0
- Rhapso/data_prep/n5_reader.py +188 -0
- Rhapso/data_prep/s3_big_stitcher_reader.py +55 -0
- Rhapso/data_prep/xml_to_dataframe.py +215 -0
- Rhapso/detection/__init__.py +5 -0
- Rhapso/detection/advanced_refinement.py +203 -0
- Rhapso/detection/difference_of_gaussian.py +324 -0
- Rhapso/detection/image_reader.py +117 -0
- Rhapso/detection/metadata_builder.py +130 -0
- Rhapso/detection/overlap_detection.py +327 -0
- Rhapso/detection/points_validation.py +49 -0
- Rhapso/detection/save_interest_points.py +265 -0
- Rhapso/detection/view_transform_models.py +67 -0
- Rhapso/fusion/__init__.py +0 -0
- Rhapso/fusion/affine_fusion/__init__.py +2 -0
- Rhapso/fusion/affine_fusion/blend.py +289 -0
- Rhapso/fusion/affine_fusion/fusion.py +601 -0
- Rhapso/fusion/affine_fusion/geometry.py +159 -0
- Rhapso/fusion/affine_fusion/io.py +546 -0
- Rhapso/fusion/affine_fusion/script_utils.py +111 -0
- Rhapso/fusion/affine_fusion/setup.py +4 -0
- Rhapso/fusion/affine_fusion_worker.py +234 -0
- Rhapso/fusion/multiscale/__init__.py +0 -0
- Rhapso/fusion/multiscale/aind_hcr_data_transformation/__init__.py +19 -0
- Rhapso/fusion/multiscale/aind_hcr_data_transformation/compress/__init__.py +3 -0
- Rhapso/fusion/multiscale/aind_hcr_data_transformation/compress/czi_to_zarr.py +698 -0
- Rhapso/fusion/multiscale/aind_hcr_data_transformation/compress/zarr_writer.py +265 -0
- Rhapso/fusion/multiscale/aind_hcr_data_transformation/models.py +81 -0
- Rhapso/fusion/multiscale/aind_hcr_data_transformation/utils/__init__.py +3 -0
- Rhapso/fusion/multiscale/aind_hcr_data_transformation/utils/utils.py +526 -0
- Rhapso/fusion/multiscale/aind_hcr_data_transformation/zeiss_job.py +249 -0
- Rhapso/fusion/multiscale/aind_z1_radial_correction/__init__.py +21 -0
- Rhapso/fusion/multiscale/aind_z1_radial_correction/array_to_zarr.py +257 -0
- Rhapso/fusion/multiscale/aind_z1_radial_correction/radial_correction.py +557 -0
- Rhapso/fusion/multiscale/aind_z1_radial_correction/run_capsule.py +98 -0
- Rhapso/fusion/multiscale/aind_z1_radial_correction/utils/__init__.py +3 -0
- Rhapso/fusion/multiscale/aind_z1_radial_correction/utils/utils.py +266 -0
- Rhapso/fusion/multiscale/aind_z1_radial_correction/worker.py +89 -0
- Rhapso/fusion/multiscale_worker.py +113 -0
- Rhapso/fusion/neuroglancer_link_gen/__init__.py +8 -0
- Rhapso/fusion/neuroglancer_link_gen/dispim_link.py +235 -0
- Rhapso/fusion/neuroglancer_link_gen/exaspim_link.py +127 -0
- Rhapso/fusion/neuroglancer_link_gen/hcr_link.py +368 -0
- Rhapso/fusion/neuroglancer_link_gen/iSPIM_top.py +47 -0
- Rhapso/fusion/neuroglancer_link_gen/link_utils.py +239 -0
- Rhapso/fusion/neuroglancer_link_gen/main.py +299 -0
- Rhapso/fusion/neuroglancer_link_gen/ng_layer.py +1434 -0
- Rhapso/fusion/neuroglancer_link_gen/ng_state.py +1123 -0
- Rhapso/fusion/neuroglancer_link_gen/parsers.py +336 -0
- Rhapso/fusion/neuroglancer_link_gen/raw_link.py +116 -0
- Rhapso/fusion/neuroglancer_link_gen/utils/__init__.py +4 -0
- Rhapso/fusion/neuroglancer_link_gen/utils/shader_utils.py +85 -0
- Rhapso/fusion/neuroglancer_link_gen/utils/transfer.py +43 -0
- Rhapso/fusion/neuroglancer_link_gen/utils/utils.py +303 -0
- Rhapso/fusion/neuroglancer_link_gen_worker.py +30 -0
- Rhapso/matching/__init__.py +0 -0
- Rhapso/matching/load_and_transform_points.py +458 -0
- Rhapso/matching/ransac_matching.py +544 -0
- Rhapso/matching/save_matches.py +120 -0
- Rhapso/matching/xml_parser.py +302 -0
- Rhapso/pipelines/__init__.py +0 -0
- Rhapso/pipelines/ray/__init__.py +0 -0
- Rhapso/pipelines/ray/aws/__init__.py +0 -0
- Rhapso/pipelines/ray/aws/alignment_pipeline.py +227 -0
- Rhapso/pipelines/ray/aws/config/__init__.py +0 -0
- Rhapso/pipelines/ray/evaluation.py +71 -0
- Rhapso/pipelines/ray/interest_point_detection.py +137 -0
- Rhapso/pipelines/ray/interest_point_matching.py +110 -0
- Rhapso/pipelines/ray/local/__init__.py +0 -0
- Rhapso/pipelines/ray/local/alignment_pipeline.py +167 -0
- Rhapso/pipelines/ray/matching_stats.py +104 -0
- Rhapso/pipelines/ray/param/__init__.py +0 -0
- Rhapso/pipelines/ray/solver.py +120 -0
- Rhapso/pipelines/ray/split_dataset.py +78 -0
- Rhapso/solver/__init__.py +0 -0
- Rhapso/solver/compute_tiles.py +562 -0
- Rhapso/solver/concatenate_models.py +116 -0
- Rhapso/solver/connected_graphs.py +111 -0
- Rhapso/solver/data_prep.py +181 -0
- Rhapso/solver/global_optimization.py +410 -0
- Rhapso/solver/model_and_tile_setup.py +109 -0
- Rhapso/solver/pre_align_tiles.py +323 -0
- Rhapso/solver/save_results.py +97 -0
- Rhapso/solver/view_transforms.py +75 -0
- Rhapso/solver/xml_to_dataframe_solver.py +213 -0
- Rhapso/split_dataset/__init__.py +0 -0
- Rhapso/split_dataset/compute_grid_rules.py +78 -0
- Rhapso/split_dataset/save_points.py +101 -0
- Rhapso/split_dataset/save_xml.py +377 -0
- Rhapso/split_dataset/split_images.py +537 -0
- Rhapso/split_dataset/xml_to_dataframe_split.py +219 -0
- rhapso-0.1.92.dist-info/METADATA +39 -0
- rhapso-0.1.92.dist-info/RECORD +101 -0
- rhapso-0.1.92.dist-info/WHEEL +5 -0
- rhapso-0.1.92.dist-info/licenses/LICENSE +21 -0
- rhapso-0.1.92.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_detection.py +17 -0
- tests/test_matching.py +21 -0
- tests/test_solving.py +21 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CZI to Zarr writer. It takes an input path
|
|
3
|
+
where 3D stacks are located, then these
|
|
4
|
+
stacks are loaded into memory and written
|
|
5
|
+
to zarr.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from typing import Dict, Hashable, List, Optional, Sequence, Tuple, Union, cast
|
|
11
|
+
|
|
12
|
+
import czifile
|
|
13
|
+
import dask
|
|
14
|
+
import dask.array as da
|
|
15
|
+
import numpy as np
|
|
16
|
+
import xarray_multiscale
|
|
17
|
+
import zarr
|
|
18
|
+
from numcodecs import blosc
|
|
19
|
+
from ome_zarr.format import CurrentFormat
|
|
20
|
+
from ome_zarr.io import parse_url
|
|
21
|
+
from ome_zarr.writer import write_multiscales_metadata
|
|
22
|
+
|
|
23
|
+
from .zarr_writer import (
|
|
24
|
+
BlockedArrayWriter,
|
|
25
|
+
)
|
|
26
|
+
from ..utils.utils import (
|
|
27
|
+
czi_block_generator,
|
|
28
|
+
pad_array_n_d,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _build_ome(
|
|
33
|
+
data_shape: Tuple[int, ...],
|
|
34
|
+
image_name: str,
|
|
35
|
+
channel_names: Optional[List[str]] = None,
|
|
36
|
+
channel_colors: Optional[List[int]] = None,
|
|
37
|
+
channel_minmax: Optional[List[Tuple[float, float]]] = None,
|
|
38
|
+
channel_startend: Optional[List[Tuple[float, float]]] = None,
|
|
39
|
+
) -> Dict:
|
|
40
|
+
"""
|
|
41
|
+
Create the necessary metadata for an OME tiff image
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
data_shape: A 5-d tuple, assumed to be TCZYX order
|
|
46
|
+
image_name: The name of the image
|
|
47
|
+
channel_names: The names for each channel
|
|
48
|
+
channel_colors: List of all channel colors
|
|
49
|
+
channel_minmax: List of all (min, max) pairs of channel pixel
|
|
50
|
+
ranges (min value of darkest pixel, max value of brightest)
|
|
51
|
+
channel_startend: List of all pairs for rendering where start is
|
|
52
|
+
a pixel value of darkness and end where a pixel value is
|
|
53
|
+
saturated
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
Dict: An "omero" metadata object suitable for writing to ome-zarr
|
|
58
|
+
"""
|
|
59
|
+
if channel_names is None:
|
|
60
|
+
channel_names = [
|
|
61
|
+
f"Channel:{image_name}:{i}" for i in range(data_shape[1])
|
|
62
|
+
]
|
|
63
|
+
if channel_colors is None:
|
|
64
|
+
channel_colors = [i for i in range(data_shape[1])]
|
|
65
|
+
if channel_minmax is None:
|
|
66
|
+
channel_minmax = [(0.0, 1.0) for _ in range(data_shape[1])]
|
|
67
|
+
if channel_startend is None:
|
|
68
|
+
channel_startend = channel_minmax
|
|
69
|
+
|
|
70
|
+
ch = []
|
|
71
|
+
for i in range(data_shape[1]):
|
|
72
|
+
ch.append(
|
|
73
|
+
{
|
|
74
|
+
"active": True,
|
|
75
|
+
"coefficient": 1,
|
|
76
|
+
"color": f"{channel_colors[i]:06x}",
|
|
77
|
+
"family": "linear",
|
|
78
|
+
"inverted": False,
|
|
79
|
+
"label": channel_names[i],
|
|
80
|
+
"window": {
|
|
81
|
+
"end": float(channel_startend[i][1]),
|
|
82
|
+
"max": float(channel_minmax[i][1]),
|
|
83
|
+
"min": float(channel_minmax[i][0]),
|
|
84
|
+
"start": float(channel_startend[i][0]),
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
omero = {
|
|
90
|
+
"id": 1, # ID in OMERO
|
|
91
|
+
"name": image_name, # Name as shown in the UI
|
|
92
|
+
"version": "0.4", # Current version
|
|
93
|
+
"channels": ch,
|
|
94
|
+
"rdefs": {
|
|
95
|
+
"defaultT": 0, # First timepoint to show the user
|
|
96
|
+
"defaultZ": data_shape[2] // 2, # First Z section to show the user
|
|
97
|
+
"model": "color", # "color" or "greyscale"
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
return omero
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _compute_scales(
|
|
104
|
+
scale_num_levels: int,
|
|
105
|
+
scale_factor: Tuple[float, float, float],
|
|
106
|
+
pixelsizes: Tuple[float, float, float],
|
|
107
|
+
chunks: Tuple[int, int, int, int, int],
|
|
108
|
+
data_shape: Tuple[int, int, int, int, int],
|
|
109
|
+
translations: Optional[List[float]] = None,
|
|
110
|
+
) -> Tuple[List, List]:
|
|
111
|
+
"""
|
|
112
|
+
Generate the list of coordinate transformations
|
|
113
|
+
and associated chunk options.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
scale_num_levels: the number of downsampling levels
|
|
118
|
+
scale_factor: a tuple of scale factors in each spatial dimension (Z, Y, X)
|
|
119
|
+
pixelsizes: a list of pixel sizes in each spatial dimension (Z, Y, X)
|
|
120
|
+
chunks: a 5D tuple of integers with size of each
|
|
121
|
+
chunk dimension (T, C, Z, Y, X)
|
|
122
|
+
data_shape: a 5D tuple of the full resolution image's shape
|
|
123
|
+
translation: a 5 element list specifying the offset
|
|
124
|
+
in physical units in each dimension
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
A tuple of the coordinate transforms and chunk options
|
|
129
|
+
"""
|
|
130
|
+
transforms = [
|
|
131
|
+
[
|
|
132
|
+
# the voxel size for the first scale level
|
|
133
|
+
{
|
|
134
|
+
"type": "scale",
|
|
135
|
+
"scale": [
|
|
136
|
+
1.0,
|
|
137
|
+
1.0,
|
|
138
|
+
pixelsizes[0],
|
|
139
|
+
pixelsizes[1],
|
|
140
|
+
pixelsizes[2],
|
|
141
|
+
],
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
]
|
|
145
|
+
if translations is not None:
|
|
146
|
+
transforms[0].append(
|
|
147
|
+
{"type": "translation", "translation": translations[0]}
|
|
148
|
+
)
|
|
149
|
+
chunk_sizes = []
|
|
150
|
+
lastz = data_shape[2]
|
|
151
|
+
lasty = data_shape[3]
|
|
152
|
+
lastx = data_shape[4]
|
|
153
|
+
opts = dict(
|
|
154
|
+
chunks=(
|
|
155
|
+
1,
|
|
156
|
+
1,
|
|
157
|
+
min(lastz, chunks[2]),
|
|
158
|
+
min(lasty, chunks[3]),
|
|
159
|
+
min(lastx, chunks[4]),
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
chunk_sizes.append(opts)
|
|
163
|
+
if scale_num_levels > 1:
|
|
164
|
+
for i in range(scale_num_levels - 1):
|
|
165
|
+
last_transform = transforms[-1][0]
|
|
166
|
+
last_scale = cast(List, last_transform["scale"])
|
|
167
|
+
transforms.append(
|
|
168
|
+
[
|
|
169
|
+
{
|
|
170
|
+
"type": "scale",
|
|
171
|
+
"scale": [
|
|
172
|
+
1.0,
|
|
173
|
+
1.0,
|
|
174
|
+
last_scale[2] * scale_factor[0],
|
|
175
|
+
last_scale[3] * scale_factor[1],
|
|
176
|
+
last_scale[4] * scale_factor[2],
|
|
177
|
+
],
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
)
|
|
181
|
+
if translations is not None:
|
|
182
|
+
transforms[-1].append(
|
|
183
|
+
{"type": "translation", "translation": translations[i + 1]}
|
|
184
|
+
)
|
|
185
|
+
lastz = int(np.ceil(lastz / scale_factor[0]))
|
|
186
|
+
lasty = int(np.ceil(lasty / scale_factor[1]))
|
|
187
|
+
lastx = int(np.ceil(lastx / scale_factor[2]))
|
|
188
|
+
opts = dict(
|
|
189
|
+
chunks=(
|
|
190
|
+
1,
|
|
191
|
+
1,
|
|
192
|
+
min(lastz, chunks[2]),
|
|
193
|
+
min(lasty, chunks[3]),
|
|
194
|
+
min(lastx, chunks[4]),
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
chunk_sizes.append(opts)
|
|
198
|
+
|
|
199
|
+
return transforms, chunk_sizes
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _get_axes_5d(
|
|
203
|
+
time_unit: str = "millisecond", space_unit: str = "micrometer"
|
|
204
|
+
) -> List[Dict]:
|
|
205
|
+
"""Generate the list of axes.
|
|
206
|
+
|
|
207
|
+
Parameters
|
|
208
|
+
----------
|
|
209
|
+
time_unit: the time unit string, e.g., "millisecond"
|
|
210
|
+
space_unit: the space unit string, e.g., "micrometer"
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
A list of dictionaries for each axis
|
|
215
|
+
"""
|
|
216
|
+
axes_5d = [
|
|
217
|
+
{"name": "t", "type": "time", "unit": f"{time_unit}"},
|
|
218
|
+
{"name": "c", "type": "channel"},
|
|
219
|
+
{"name": "z", "type": "space", "unit": f"{space_unit}"},
|
|
220
|
+
{"name": "y", "type": "space", "unit": f"{space_unit}"},
|
|
221
|
+
{"name": "x", "type": "space", "unit": f"{space_unit}"},
|
|
222
|
+
]
|
|
223
|
+
return axes_5d
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _downscale_origin(
|
|
227
|
+
array_shape: List[int],
|
|
228
|
+
origin: List[float],
|
|
229
|
+
voxel_size: List[float],
|
|
230
|
+
scale_factors: List[int],
|
|
231
|
+
n_levels: int,
|
|
232
|
+
):
|
|
233
|
+
"""
|
|
234
|
+
Calculate new origins for downscaled coordinate grids.
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
array_shape : List[int]
|
|
239
|
+
Shape of the array in [t, c, z, y, x] order.
|
|
240
|
+
origin : list or tuple of float
|
|
241
|
+
The initial origin coordinates (z, y, x) of the array.
|
|
242
|
+
voxel_size : list or tuple of float
|
|
243
|
+
The size of each voxel along the (z, y, x) dimensions.
|
|
244
|
+
scale_factors : list or tuple of int
|
|
245
|
+
The factors by which to downscale the coordinates along each axis
|
|
246
|
+
(z, y, x).
|
|
247
|
+
n_levels : int
|
|
248
|
+
The number of downscaling levels to calculate.
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
new_origins : list of list of float
|
|
253
|
+
A list of new origin coordinates for each downscaled level.
|
|
254
|
+
"""
|
|
255
|
+
current_shape = np.array(array_shape[-3:], dtype=np.int32)
|
|
256
|
+
current_origin = np.array(origin[-3:], dtype=np.float64)
|
|
257
|
+
current_voxel_size = np.array(voxel_size[-3:], dtype=np.float64)
|
|
258
|
+
scale_factors = np.array(scale_factors[-3:], dtype=np.int32)
|
|
259
|
+
|
|
260
|
+
new_origins = [current_origin.tolist()]
|
|
261
|
+
|
|
262
|
+
for _ in range(n_levels - 1):
|
|
263
|
+
|
|
264
|
+
# Calculate the center shift for the new origin
|
|
265
|
+
center_shift = (current_voxel_size * (scale_factors - 1)) / 2
|
|
266
|
+
current_origin += center_shift
|
|
267
|
+
|
|
268
|
+
current_shape = np.ceil(current_shape / scale_factors).astype(int)
|
|
269
|
+
next_voxel_size = current_voxel_size * scale_factors
|
|
270
|
+
current_voxel_size = next_voxel_size
|
|
271
|
+
|
|
272
|
+
# Append the new origin
|
|
273
|
+
new_origins.append([0.0, 0.0] + current_origin.tolist())
|
|
274
|
+
|
|
275
|
+
# Ensure the initial origin is 5D
|
|
276
|
+
if len(new_origins[0]) < 5:
|
|
277
|
+
new_origins[0] = [0.0, 0.0] + new_origins[0]
|
|
278
|
+
|
|
279
|
+
return new_origins
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def write_ome_ngff_metadata(
|
|
283
|
+
group: zarr.Group,
|
|
284
|
+
arr_shape: List[int],
|
|
285
|
+
final_chunksize: List[int],
|
|
286
|
+
image_name: str,
|
|
287
|
+
n_lvls: int,
|
|
288
|
+
scale_factors: tuple,
|
|
289
|
+
voxel_size: tuple,
|
|
290
|
+
channel_names: List[str] = None,
|
|
291
|
+
channel_colors: List[str] = None,
|
|
292
|
+
channel_minmax: List[float] = None,
|
|
293
|
+
channel_startend: List[float] = None,
|
|
294
|
+
origin: list = None,
|
|
295
|
+
metadata: dict = None,
|
|
296
|
+
):
|
|
297
|
+
"""
|
|
298
|
+
Write OME-NGFF metadata to a Zarr group.
|
|
299
|
+
|
|
300
|
+
Parameters
|
|
301
|
+
----------
|
|
302
|
+
group : zarr.Group
|
|
303
|
+
The output Zarr group.
|
|
304
|
+
arr_shape : List[int]
|
|
305
|
+
List of ints with the dataset shape.
|
|
306
|
+
image_name : str
|
|
307
|
+
The name of the image.
|
|
308
|
+
n_lvls : int
|
|
309
|
+
The number of pyramid levels.
|
|
310
|
+
scale_factors : tuple
|
|
311
|
+
The scale factors for downsampling along each dimension.
|
|
312
|
+
voxel_size : tuple
|
|
313
|
+
The voxel size along each dimension.
|
|
314
|
+
channel_names: List[str]
|
|
315
|
+
List of channel names to add to the OMENGFF metadata
|
|
316
|
+
channel_colors: List[str]
|
|
317
|
+
List of channel colors to visualize the data
|
|
318
|
+
chanel_minmax: List[float]
|
|
319
|
+
List of channel min and max values based on the
|
|
320
|
+
image dtype
|
|
321
|
+
channel_startend: List[float]
|
|
322
|
+
List of the channel start and end metadata. This is
|
|
323
|
+
used for visualization. The start and end range might be
|
|
324
|
+
different from the min max and it is usually inside the
|
|
325
|
+
range
|
|
326
|
+
metadata: dict
|
|
327
|
+
Extra metadata to write in the OME-NGFF metadata
|
|
328
|
+
"""
|
|
329
|
+
if metadata is None:
|
|
330
|
+
metadata = {}
|
|
331
|
+
fmt = CurrentFormat()
|
|
332
|
+
|
|
333
|
+
# Building the OMERO metadata
|
|
334
|
+
ome_json = _build_ome(
|
|
335
|
+
arr_shape,
|
|
336
|
+
image_name,
|
|
337
|
+
channel_names=channel_names,
|
|
338
|
+
channel_colors=channel_colors,
|
|
339
|
+
channel_minmax=channel_minmax,
|
|
340
|
+
channel_startend=channel_startend,
|
|
341
|
+
)
|
|
342
|
+
group.attrs["omero"] = ome_json
|
|
343
|
+
axes_5d = _get_axes_5d()
|
|
344
|
+
|
|
345
|
+
if origin is not None:
|
|
346
|
+
origin = _downscale_origin(
|
|
347
|
+
arr_shape, origin[-3:], voxel_size[-3:], scale_factors[-3:], n_lvls
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
coordinate_transformations, chunk_opts = _compute_scales(
|
|
351
|
+
n_lvls, scale_factors, voxel_size, final_chunksize, arr_shape, origin
|
|
352
|
+
)
|
|
353
|
+
fmt.validate_coordinate_transformations(
|
|
354
|
+
len(arr_shape), n_lvls, coordinate_transformations
|
|
355
|
+
)
|
|
356
|
+
# Setting coordinate transfomations
|
|
357
|
+
datasets = [{"path": str(i)} for i in range(n_lvls)]
|
|
358
|
+
if coordinate_transformations is not None:
|
|
359
|
+
for dataset, transform in zip(datasets, coordinate_transformations):
|
|
360
|
+
dataset["coordinateTransformations"] = transform
|
|
361
|
+
|
|
362
|
+
# Writing the multiscale metadata
|
|
363
|
+
write_multiscales_metadata(group, datasets, fmt, axes_5d, **metadata)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def create_czi_opts(codec: str, compression_level: int) -> dict:
|
|
367
|
+
"""
|
|
368
|
+
Creates CZI options for writing
|
|
369
|
+
the OMEZarr.
|
|
370
|
+
|
|
371
|
+
Parameters
|
|
372
|
+
----------
|
|
373
|
+
codec: str
|
|
374
|
+
Image codec used to write the image
|
|
375
|
+
|
|
376
|
+
compression_level: int
|
|
377
|
+
Compression level for the image
|
|
378
|
+
|
|
379
|
+
Returns
|
|
380
|
+
-------
|
|
381
|
+
dict
|
|
382
|
+
Dictionary with the blosc compression
|
|
383
|
+
to write the CZI image
|
|
384
|
+
"""
|
|
385
|
+
return {
|
|
386
|
+
"compressor": blosc.Blosc(
|
|
387
|
+
cname=codec, clevel=compression_level, shuffle=blosc.SHUFFLE
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _get_pyramid_metadata():
|
|
393
|
+
"""
|
|
394
|
+
Gets the image pyramid metadata
|
|
395
|
+
using xarray_multiscale package
|
|
396
|
+
"""
|
|
397
|
+
# Try to get version, fallback to 'unknown' if not available
|
|
398
|
+
try:
|
|
399
|
+
version = str(xarray_multiscale.__version__)
|
|
400
|
+
except AttributeError:
|
|
401
|
+
version = "unknown"
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
"metadata": {
|
|
405
|
+
"description": "Downscaling using the windowed mean",
|
|
406
|
+
"method": "xarray_multiscale.reducers.windowed_mean",
|
|
407
|
+
"version": version,
|
|
408
|
+
"args": "[false]",
|
|
409
|
+
# No extra parameters were used different
|
|
410
|
+
# from the orig. array and scales
|
|
411
|
+
"kwargs": {},
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def compute_pyramid(
|
|
417
|
+
data: dask.array.core.Array,
|
|
418
|
+
n_lvls: int,
|
|
419
|
+
scale_axis: Tuple[int],
|
|
420
|
+
chunks: Union[str, Sequence[int], Dict[Hashable, int]] = "auto",
|
|
421
|
+
) -> List[dask.array.core.Array]:
|
|
422
|
+
"""
|
|
423
|
+
Computes the pyramid levels given an input full resolution image data
|
|
424
|
+
|
|
425
|
+
Parameters
|
|
426
|
+
------------------------
|
|
427
|
+
|
|
428
|
+
data: dask.array.core.Array
|
|
429
|
+
Dask array of the image data
|
|
430
|
+
|
|
431
|
+
n_lvls: int
|
|
432
|
+
Number of downsampling levels
|
|
433
|
+
that will be applied to the original image
|
|
434
|
+
|
|
435
|
+
scale_axis: Tuple[int]
|
|
436
|
+
Scaling applied to each axis
|
|
437
|
+
|
|
438
|
+
chunks: Union[str, Sequence[int], Dict[Hashable, int]]
|
|
439
|
+
chunksize that will be applied to the multiscales
|
|
440
|
+
Default: "auto"
|
|
441
|
+
|
|
442
|
+
Returns
|
|
443
|
+
------------------------
|
|
444
|
+
|
|
445
|
+
Tuple[List[dask.array.core.Array], Dict]:
|
|
446
|
+
List with the downsampled image(s) and dictionary
|
|
447
|
+
with image metadata
|
|
448
|
+
"""
|
|
449
|
+
|
|
450
|
+
metadata = _get_pyramid_metadata()
|
|
451
|
+
|
|
452
|
+
pyramid = xarray_multiscale.multiscale(
|
|
453
|
+
array=data,
|
|
454
|
+
reduction=xarray_multiscale.reducers.windowed_mean, # func
|
|
455
|
+
scale_factors=scale_axis, # scale factors
|
|
456
|
+
preserve_dtype=True,
|
|
457
|
+
chunks=chunks,
|
|
458
|
+
)[:n_lvls]
|
|
459
|
+
|
|
460
|
+
return [pyramid_level.data for pyramid_level in pyramid], metadata
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def czi_stack_zarr_writer(
|
|
464
|
+
czi_path: str,
|
|
465
|
+
output_path: str,
|
|
466
|
+
voxel_size: List[float],
|
|
467
|
+
final_chunksize: List[int],
|
|
468
|
+
scale_factor: List[int],
|
|
469
|
+
n_lvls: int,
|
|
470
|
+
channel_name: str,
|
|
471
|
+
logger: logging.Logger,
|
|
472
|
+
stack_name: str,
|
|
473
|
+
writing_options,
|
|
474
|
+
target_size_mb: Optional[int] = 24000,
|
|
475
|
+
):
|
|
476
|
+
"""
|
|
477
|
+
Writes a fused Zeiss channel in OMEZarr
|
|
478
|
+
format. This channel was read as a lazy array.
|
|
479
|
+
|
|
480
|
+
Parameters
|
|
481
|
+
----------
|
|
482
|
+
czi_path: str
|
|
483
|
+
Path where the CZI file is stored.
|
|
484
|
+
|
|
485
|
+
output_path: PathLike
|
|
486
|
+
Path where we want to write the OMEZarr
|
|
487
|
+
channel
|
|
488
|
+
|
|
489
|
+
voxel_size: List[float]
|
|
490
|
+
Voxel size representing the dataset
|
|
491
|
+
|
|
492
|
+
final_chunksize: List[int]
|
|
493
|
+
Final chunksize we want to use to write
|
|
494
|
+
the final dataset
|
|
495
|
+
|
|
496
|
+
codec: str
|
|
497
|
+
Image codec for writing the Zarr
|
|
498
|
+
|
|
499
|
+
compression_level: int
|
|
500
|
+
Compression level
|
|
501
|
+
|
|
502
|
+
scale_factor: List[int]
|
|
503
|
+
Scale factor per axis. The dimensionality
|
|
504
|
+
is organized as ZYX.
|
|
505
|
+
|
|
506
|
+
n_lvls: int
|
|
507
|
+
Number of levels on the pyramid (multiresolution)
|
|
508
|
+
for better visualization
|
|
509
|
+
|
|
510
|
+
channel_name: str
|
|
511
|
+
Channel name we are currently writing
|
|
512
|
+
|
|
513
|
+
logger: logging.Logger
|
|
514
|
+
Logger object
|
|
515
|
+
|
|
516
|
+
target_size_mb: Optional[int]
|
|
517
|
+
Target size to pull from the CZI array.
|
|
518
|
+
|
|
519
|
+
"""
|
|
520
|
+
written_pyramid = []
|
|
521
|
+
start_time = time.time()
|
|
522
|
+
|
|
523
|
+
with czifile.CziFile(str(czi_path)) as czi:
|
|
524
|
+
dataset_shape = tuple(i for i in czi.shape if i != 1)
|
|
525
|
+
extra_axes = (1,) * (5 - len(dataset_shape))
|
|
526
|
+
dataset_shape = extra_axes + dataset_shape
|
|
527
|
+
|
|
528
|
+
final_chunksize = ([1] * (5 - len(final_chunksize))) + final_chunksize
|
|
529
|
+
# Getting channel color
|
|
530
|
+
channel_colors = None
|
|
531
|
+
|
|
532
|
+
print(f"Writing {dataset_shape} from {stack_name} to {output_path}")
|
|
533
|
+
|
|
534
|
+
# Creating Zarr dataset
|
|
535
|
+
store = parse_url(path=output_path, mode="w").store
|
|
536
|
+
root_group = zarr.group(store=store)
|
|
537
|
+
|
|
538
|
+
# Using 1 thread since is in single machine.
|
|
539
|
+
# Avoiding the use of multithreaded due to GIL
|
|
540
|
+
|
|
541
|
+
if np.issubdtype(czi.dtype, np.integer):
|
|
542
|
+
np_info_func = np.iinfo
|
|
543
|
+
|
|
544
|
+
else:
|
|
545
|
+
# Floating point
|
|
546
|
+
np_info_func = np.finfo
|
|
547
|
+
|
|
548
|
+
# Getting min max metadata for the dtype
|
|
549
|
+
channel_minmax = [
|
|
550
|
+
(
|
|
551
|
+
np_info_func(czi.dtype).min,
|
|
552
|
+
np_info_func(czi.dtype).max,
|
|
553
|
+
)
|
|
554
|
+
for _ in range(dataset_shape[1])
|
|
555
|
+
]
|
|
556
|
+
|
|
557
|
+
# Setting values for CZI
|
|
558
|
+
# Ideally we would use da.percentile(image_data, (0.1, 95))
|
|
559
|
+
# However, it would take so much time and resources and it is
|
|
560
|
+
# not used that much on neuroglancer
|
|
561
|
+
channel_startend = [(0.0, 550.0) for _ in range(dataset_shape[1])]
|
|
562
|
+
|
|
563
|
+
new_channel_group = root_group.create_group(
|
|
564
|
+
name=stack_name, overwrite=True
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
# Writing OME-NGFF metadata
|
|
568
|
+
write_ome_ngff_metadata(
|
|
569
|
+
group=new_channel_group,
|
|
570
|
+
arr_shape=dataset_shape,
|
|
571
|
+
image_name=stack_name,
|
|
572
|
+
n_lvls=n_lvls,
|
|
573
|
+
scale_factors=scale_factor,
|
|
574
|
+
voxel_size=voxel_size,
|
|
575
|
+
channel_names=[channel_name],
|
|
576
|
+
channel_colors=channel_colors,
|
|
577
|
+
channel_minmax=channel_minmax,
|
|
578
|
+
channel_startend=channel_startend,
|
|
579
|
+
metadata=_get_pyramid_metadata(),
|
|
580
|
+
final_chunksize=final_chunksize,
|
|
581
|
+
origin=[0, 0, 0],
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
# performance_report_path = f"{output_path}/report_{stack_name}.html"
|
|
585
|
+
|
|
586
|
+
# Writing zarr and performance report
|
|
587
|
+
# with performance_report(filename=performance_report_path):
|
|
588
|
+
logger.info(f"Writing channel {channel_name}/{stack_name}")
|
|
589
|
+
|
|
590
|
+
# Writing first multiscale by default
|
|
591
|
+
pyramid_group = new_channel_group.create_dataset(
|
|
592
|
+
name="0",
|
|
593
|
+
shape=dataset_shape,
|
|
594
|
+
chunks=final_chunksize,
|
|
595
|
+
dtype=czi.dtype,
|
|
596
|
+
compressor=writing_options,
|
|
597
|
+
dimension_separator="/",
|
|
598
|
+
overwrite=True,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
# final_chunksize must be TCZYX order
|
|
602
|
+
for block, axis_area in czi_block_generator(
|
|
603
|
+
czi,
|
|
604
|
+
axis_jumps=final_chunksize[-3],
|
|
605
|
+
slice_axis="z",
|
|
606
|
+
):
|
|
607
|
+
region = (
|
|
608
|
+
slice(None),
|
|
609
|
+
slice(None),
|
|
610
|
+
axis_area,
|
|
611
|
+
slice(0, dataset_shape[-2]),
|
|
612
|
+
slice(0, dataset_shape[-1]),
|
|
613
|
+
)
|
|
614
|
+
pyramid_group[region] = pad_array_n_d(block)
|
|
615
|
+
|
|
616
|
+
# Writing multiscales
|
|
617
|
+
previous_scale = da.from_zarr(pyramid_group, pyramid_group.chunks)
|
|
618
|
+
written_pyramid.append(previous_scale)
|
|
619
|
+
|
|
620
|
+
block_shape = list(
|
|
621
|
+
BlockedArrayWriter.get_block_shape(
|
|
622
|
+
arr=previous_scale,
|
|
623
|
+
target_size_mb=target_size_mb,
|
|
624
|
+
chunks=final_chunksize,
|
|
625
|
+
)
|
|
626
|
+
)
|
|
627
|
+
block_shape = extra_axes + tuple(block_shape)
|
|
628
|
+
|
|
629
|
+
for level in range(1, n_lvls):
|
|
630
|
+
previous_scale = da.from_zarr(pyramid_group, pyramid_group.chunks)
|
|
631
|
+
new_scale_factor = (
|
|
632
|
+
[1] * (len(previous_scale.shape) - len(scale_factor))
|
|
633
|
+
) + scale_factor
|
|
634
|
+
|
|
635
|
+
previous_scale_pyramid, _ = compute_pyramid(
|
|
636
|
+
data=previous_scale,
|
|
637
|
+
scale_axis=new_scale_factor,
|
|
638
|
+
chunks=final_chunksize,
|
|
639
|
+
n_lvls=2,
|
|
640
|
+
)
|
|
641
|
+
array_to_write = previous_scale_pyramid[-1]
|
|
642
|
+
|
|
643
|
+
logger.info(
|
|
644
|
+
f"[level {level}]: pyramid level: {array_to_write.shape}"
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
pyramid_group = new_channel_group.create_dataset(
|
|
648
|
+
name=str(level),
|
|
649
|
+
shape=array_to_write.shape,
|
|
650
|
+
chunks=final_chunksize,
|
|
651
|
+
dtype=array_to_write.dtype,
|
|
652
|
+
compressor=writing_options,
|
|
653
|
+
dimension_separator="/",
|
|
654
|
+
overwrite=True,
|
|
655
|
+
)
|
|
656
|
+
BlockedArrayWriter.store(
|
|
657
|
+
array_to_write, pyramid_group, block_shape
|
|
658
|
+
)
|
|
659
|
+
written_pyramid.append(array_to_write)
|
|
660
|
+
|
|
661
|
+
end_time = time.time()
|
|
662
|
+
logger.info(f"Time to write the dataset: {end_time - start_time}")
|
|
663
|
+
print(f"Time to write the dataset: {end_time - start_time}")
|
|
664
|
+
logger.info(f"Written pyramid: {written_pyramid}")
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def example():
|
|
668
|
+
"""
|
|
669
|
+
Conversion example
|
|
670
|
+
"""
|
|
671
|
+
from pathlib import Path
|
|
672
|
+
|
|
673
|
+
czi_test_stack = Path("path/to/data/tiles_test/SPIM/488_large.czi")
|
|
674
|
+
|
|
675
|
+
if czi_test_stack.exists():
|
|
676
|
+
writing_opts = create_czi_opts(codec="zstd", compression_level=3)
|
|
677
|
+
|
|
678
|
+
# for channel_name in
|
|
679
|
+
# for i, chn_name in enumerate(czi_file_reader.channel_names):
|
|
680
|
+
czi_stack_zarr_writer(
|
|
681
|
+
czi_path=str(czi_test_stack),
|
|
682
|
+
output_path=f"./{czi_test_stack.stem}",
|
|
683
|
+
voxel_size=[1.0, 1.0, 1.0],
|
|
684
|
+
final_chunksize=[128, 128, 128],
|
|
685
|
+
scale_factor=[2, 2, 2],
|
|
686
|
+
n_lvls=4,
|
|
687
|
+
channel_name=czi_test_stack.stem,
|
|
688
|
+
logger=logging.Logger(name="test"),
|
|
689
|
+
stack_name="test_conversion_czi_package.zarr",
|
|
690
|
+
writing_options=writing_opts["compressor"],
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
else:
|
|
694
|
+
print(f"File does not exist: {czi_test_stack}")
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
if __name__ == "__main__":
|
|
698
|
+
example()
|