@fideus-labs/ngff-zarr 0.0.1 → 0.1.0
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.
- package/README.md +9 -2
- package/esm/io/from_ngff_zarr.d.ts +4 -1
- package/esm/io/from_ngff_zarr.d.ts.map +1 -1
- package/esm/io/from_ngff_zarr.js +94 -27
- package/esm/io/hcs.d.ts +18 -0
- package/esm/io/hcs.d.ts.map +1 -0
- package/esm/io/hcs.js +51 -0
- package/esm/io/itk_image_to_ngff_image.d.ts +25 -0
- package/esm/io/itk_image_to_ngff_image.d.ts.map +1 -0
- package/esm/io/itk_image_to_ngff_image.js +127 -0
- package/esm/io/ngff_image_to_itk_image.d.ts +30 -0
- package/esm/io/ngff_image_to_itk_image.d.ts.map +1 -0
- package/esm/io/ngff_image_to_itk_image.js +216 -0
- package/esm/io/to_multiscales.d.ts +18 -0
- package/esm/io/to_multiscales.d.ts.map +1 -0
- package/esm/io/to_multiscales.js +62 -0
- package/esm/io/to_ngff_image.d.ts +17 -0
- package/esm/io/to_ngff_image.d.ts.map +1 -0
- package/esm/io/to_ngff_image.js +136 -0
- package/esm/io/to_ngff_zarr.d.ts +3 -2
- package/esm/io/to_ngff_zarr.d.ts.map +1 -1
- package/esm/io/to_ngff_zarr.js +273 -26
- package/esm/methods/itkwasm.d.ts +6 -0
- package/esm/methods/itkwasm.d.ts.map +1 -0
- package/esm/methods/itkwasm.js +816 -0
- package/esm/mod.d.ts +9 -2
- package/esm/mod.d.ts.map +1 -1
- package/esm/mod.js +10 -2
- package/esm/schemas/coordinate_systems.d.ts +644 -0
- package/esm/schemas/coordinate_systems.d.ts.map +1 -0
- package/esm/schemas/coordinate_systems.js +140 -0
- package/esm/schemas/index.d.ts +9 -0
- package/esm/schemas/index.d.ts.map +1 -0
- package/esm/schemas/index.js +38 -0
- package/esm/schemas/methods.d.ts.map +1 -1
- package/esm/schemas/methods.js +2 -0
- package/esm/schemas/multiscales.d.ts.map +1 -1
- package/esm/schemas/multiscales.js +2 -0
- package/esm/schemas/ngff_image.d.ts +9 -2
- package/esm/schemas/ngff_image.d.ts.map +1 -1
- package/esm/schemas/ngff_image.js +11 -2
- package/esm/schemas/ome_zarr.d.ts +581 -0
- package/esm/schemas/ome_zarr.d.ts.map +1 -0
- package/esm/schemas/ome_zarr.js +208 -0
- package/esm/schemas/rfc4.d.ts +439 -0
- package/esm/schemas/rfc4.d.ts.map +1 -0
- package/esm/schemas/rfc4.js +129 -0
- package/esm/schemas/units.d.ts.map +1 -1
- package/esm/schemas/units.js +5 -0
- package/esm/schemas/zarr_metadata.d.ts +302 -9
- package/esm/schemas/zarr_metadata.d.ts.map +1 -1
- package/esm/schemas/zarr_metadata.js +22 -1
- package/esm/types/array_interface.d.ts +7 -0
- package/esm/types/array_interface.d.ts.map +1 -0
- package/esm/types/array_interface.js +1 -0
- package/esm/types/hcs.d.ts +70 -0
- package/esm/types/hcs.d.ts.map +1 -0
- package/esm/types/hcs.js +204 -0
- package/esm/types/methods.d.ts.map +1 -1
- package/esm/types/methods.js +2 -0
- package/esm/types/multiscales.d.ts.map +1 -1
- package/esm/types/ngff_image.d.ts +6 -3
- package/esm/types/ngff_image.d.ts.map +1 -1
- package/esm/types/ngff_image.js +13 -1
- package/esm/types/rfc4.d.ts +94 -0
- package/esm/types/rfc4.d.ts.map +1 -0
- package/esm/types/rfc4.js +135 -0
- package/esm/types/units.d.ts +1 -1
- package/esm/types/units.d.ts.map +1 -1
- package/esm/types/zarr_metadata.d.ts +14 -5
- package/esm/types/zarr_metadata.d.ts.map +1 -1
- package/esm/utils/create_queue.d.ts +6 -0
- package/esm/utils/create_queue.d.ts.map +1 -0
- package/esm/utils/create_queue.js +11 -0
- package/esm/utils/factory.d.ts +1 -1
- package/esm/utils/factory.d.ts.map +1 -1
- package/esm/utils/factory.js +16 -7
- package/esm/utils/method_metadata.d.ts +10 -0
- package/esm/utils/method_metadata.d.ts.map +1 -0
- package/esm/utils/method_metadata.js +37 -0
- package/esm/utils/validation.d.ts.map +1 -1
- package/package.json +7 -1
- package/script/io/from_ngff_zarr.d.ts +4 -1
- package/script/io/from_ngff_zarr.d.ts.map +1 -1
- package/script/io/from_ngff_zarr.js +94 -27
- package/script/io/hcs.d.ts +18 -0
- package/script/io/hcs.d.ts.map +1 -0
- package/script/io/hcs.js +55 -0
- package/script/io/itk_image_to_ngff_image.d.ts +25 -0
- package/script/io/itk_image_to_ngff_image.d.ts.map +1 -0
- package/script/io/itk_image_to_ngff_image.js +153 -0
- package/script/io/ngff_image_to_itk_image.d.ts +30 -0
- package/script/io/ngff_image_to_itk_image.d.ts.map +1 -0
- package/script/io/ngff_image_to_itk_image.js +242 -0
- package/script/io/to_multiscales.d.ts +18 -0
- package/script/io/to_multiscales.d.ts.map +1 -0
- package/script/io/to_multiscales.js +67 -0
- package/script/io/to_ngff_image.d.ts +17 -0
- package/script/io/to_ngff_image.d.ts.map +1 -0
- package/script/io/to_ngff_image.js +162 -0
- package/script/io/to_ngff_zarr.d.ts +3 -2
- package/script/io/to_ngff_zarr.d.ts.map +1 -1
- package/script/io/to_ngff_zarr.js +273 -26
- package/script/methods/itkwasm.d.ts +6 -0
- package/script/methods/itkwasm.d.ts.map +1 -0
- package/script/methods/itkwasm.js +842 -0
- package/script/mod.d.ts +9 -2
- package/script/mod.d.ts.map +1 -1
- package/script/mod.js +12 -3
- package/script/schemas/coordinate_systems.d.ts +644 -0
- package/script/schemas/coordinate_systems.d.ts.map +1 -0
- package/script/schemas/coordinate_systems.js +143 -0
- package/script/schemas/index.d.ts +9 -0
- package/script/schemas/index.d.ts.map +1 -0
- package/script/schemas/index.js +101 -0
- package/script/schemas/methods.d.ts.map +1 -1
- package/script/schemas/methods.js +2 -0
- package/script/schemas/multiscales.d.ts.map +1 -1
- package/script/schemas/multiscales.js +2 -0
- package/script/schemas/ngff_image.d.ts +9 -2
- package/script/schemas/ngff_image.d.ts.map +1 -1
- package/script/schemas/ngff_image.js +11 -2
- package/script/schemas/ome_zarr.d.ts +581 -0
- package/script/schemas/ome_zarr.d.ts.map +1 -0
- package/script/schemas/ome_zarr.js +211 -0
- package/script/schemas/rfc4.d.ts +439 -0
- package/script/schemas/rfc4.d.ts.map +1 -0
- package/script/schemas/rfc4.js +132 -0
- package/script/schemas/units.d.ts.map +1 -1
- package/script/schemas/units.js +5 -0
- package/script/schemas/zarr_metadata.d.ts +302 -9
- package/script/schemas/zarr_metadata.d.ts.map +1 -1
- package/script/schemas/zarr_metadata.js +23 -2
- package/script/types/array_interface.d.ts +7 -0
- package/script/types/array_interface.d.ts.map +1 -0
- package/script/types/array_interface.js +2 -0
- package/script/types/hcs.d.ts +70 -0
- package/script/types/hcs.d.ts.map +1 -0
- package/script/types/hcs.js +233 -0
- package/script/types/methods.d.ts.map +1 -1
- package/script/types/methods.js +2 -0
- package/script/types/multiscales.d.ts.map +1 -1
- package/script/types/ngff_image.d.ts +6 -3
- package/script/types/ngff_image.d.ts.map +1 -1
- package/script/types/ngff_image.js +13 -1
- package/script/types/rfc4.d.ts +94 -0
- package/script/types/rfc4.d.ts.map +1 -0
- package/script/types/rfc4.js +143 -0
- package/script/types/units.d.ts +1 -1
- package/script/types/units.d.ts.map +1 -1
- package/script/types/zarr_metadata.d.ts +14 -5
- package/script/types/zarr_metadata.d.ts.map +1 -1
- package/script/utils/create_queue.d.ts +6 -0
- package/script/utils/create_queue.d.ts.map +1 -0
- package/script/utils/create_queue.js +17 -0
- package/script/utils/factory.d.ts +1 -1
- package/script/utils/factory.d.ts.map +1 -1
- package/script/utils/factory.js +39 -7
- package/script/utils/method_metadata.d.ts +10 -0
- package/script/utils/method_metadata.d.ts.map +1 -0
- package/script/utils/method_metadata.js +40 -0
- package/script/utils/validation.d.ts.map +1 -1
- package/esm/schemas/lazy_array.d.ts +0 -8
- package/esm/schemas/lazy_array.d.ts.map +0 -1
- package/esm/schemas/lazy_array.js +0 -7
- package/esm/types/lazy_array.d.ts +0 -18
- package/esm/types/lazy_array.d.ts.map +0 -1
- package/esm/types/lazy_array.js +0 -27
- package/script/schemas/lazy_array.d.ts +0 -8
- package/script/schemas/lazy_array.d.ts.map +0 -1
- package/script/schemas/lazy_array.js +0 -10
- package/script/types/lazy_array.d.ts +0 -18
- package/script/types/lazy_array.d.ts.map +0 -1
- package/script/types/lazy_array.js +0 -31
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { downsampleBinShrinkNode as downsampleBinShrink, downsampleLabelImageNode as downsampleLabelImage, downsampleNode as downsample, gaussianKernelRadiusNode as gaussianKernelRadius, } from "@itk-wasm/downsample";
|
|
4
|
+
import * as zarr from "zarrita";
|
|
5
|
+
import { NgffImage } from "../types/ngff_image.js";
|
|
6
|
+
const SPATIAL_DIMS = ["x", "y", "z"];
|
|
7
|
+
/**
|
|
8
|
+
* Convert dimension scale factors to ITK-Wasm format
|
|
9
|
+
*/
|
|
10
|
+
function dimScaleFactors(dims, scaleFactor, previousDimFactors) {
|
|
11
|
+
const dimFactors = {};
|
|
12
|
+
if (typeof scaleFactor === "number") {
|
|
13
|
+
for (const dim of dims) {
|
|
14
|
+
if (SPATIAL_DIMS.includes(dim)) {
|
|
15
|
+
dimFactors[dim] = scaleFactor;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
dimFactors[dim] = previousDimFactors[dim] || 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
for (const dim of dims) {
|
|
24
|
+
if (dim in scaleFactor) {
|
|
25
|
+
dimFactors[dim] = scaleFactor[dim];
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
dimFactors[dim] = previousDimFactors[dim] || 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return dimFactors;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Update previous dimension factors
|
|
36
|
+
*/
|
|
37
|
+
function updatePreviousDimFactors(scaleFactor, spatialDims, previousDimFactors) {
|
|
38
|
+
const updated = { ...previousDimFactors };
|
|
39
|
+
if (typeof scaleFactor === "number") {
|
|
40
|
+
for (const dim of spatialDims) {
|
|
41
|
+
updated[dim] = scaleFactor;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
for (const dim of spatialDims) {
|
|
46
|
+
if (dim in scaleFactor) {
|
|
47
|
+
updated[dim] = scaleFactor[dim];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return updated;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Compute next scale metadata
|
|
55
|
+
*/
|
|
56
|
+
function nextScaleMetadata(image, dimFactors, spatialDims) {
|
|
57
|
+
const translation = {};
|
|
58
|
+
const scale = {};
|
|
59
|
+
for (const dim of image.dims) {
|
|
60
|
+
if (spatialDims.includes(dim)) {
|
|
61
|
+
const factor = dimFactors[dim];
|
|
62
|
+
scale[dim] = image.scale[dim] * factor;
|
|
63
|
+
translation[dim] = image.translation[dim] +
|
|
64
|
+
0.5 * (factor - 1) * image.scale[dim];
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Only copy non-spatial dimensions if they exist in the source
|
|
68
|
+
if (dim in image.scale) {
|
|
69
|
+
scale[dim] = image.scale[dim];
|
|
70
|
+
}
|
|
71
|
+
if (dim in image.translation) {
|
|
72
|
+
translation[dim] = image.translation[dim];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [translation, scale];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Compute Gaussian kernel sigma values in pixel units for downsampling.
|
|
80
|
+
*
|
|
81
|
+
* Formula: sigma = sqrt((k^2 - 1^2)/(2*sqrt(2*ln(2)))^2)
|
|
82
|
+
*
|
|
83
|
+
* Reference:
|
|
84
|
+
* - https://discourse.itk.org/t/resampling-to-isotropic-signal-processing-theory/1403/16
|
|
85
|
+
* - https://doi.org/10.1007/978-3-319-24571-3_81
|
|
86
|
+
* - http://discovery.ucl.ac.uk/1469251/1/scale-factor-point-5.pdf
|
|
87
|
+
*
|
|
88
|
+
* @param shrinkFactors - Shrink ratio along each axis
|
|
89
|
+
* @returns Standard deviation of Gaussian kernel along each axis
|
|
90
|
+
*/
|
|
91
|
+
function computeSigma(shrinkFactors) {
|
|
92
|
+
const denominator = Math.pow(2 * Math.sqrt(2 * Math.log(2)), 2);
|
|
93
|
+
return shrinkFactors.map((factor) => Math.sqrt((factor * factor - 1) / denominator));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Convert zarr array to ITK-Wasm Image format
|
|
97
|
+
* If isVector is true, ensures "c" dimension is last by transposing if needed
|
|
98
|
+
*/
|
|
99
|
+
async function zarrToItkImage(array, dims, isVector = false) {
|
|
100
|
+
// Read the full array data
|
|
101
|
+
const result = await zarr.get(array);
|
|
102
|
+
// Ensure we have the data
|
|
103
|
+
if (!result.data || result.data.length === 0) {
|
|
104
|
+
throw new Error("Zarr array data is empty");
|
|
105
|
+
}
|
|
106
|
+
let data;
|
|
107
|
+
let shape = result.shape;
|
|
108
|
+
let _finalDims = dims;
|
|
109
|
+
// If vector image, ensure "c" is last dimension
|
|
110
|
+
if (isVector) {
|
|
111
|
+
const cIndex = dims.indexOf("c");
|
|
112
|
+
if (cIndex !== -1 && cIndex !== dims.length - 1) {
|
|
113
|
+
// Need to transpose to move "c" to the end
|
|
114
|
+
const permutation = dims.map((_, i) => i).filter((i) => i !== cIndex);
|
|
115
|
+
permutation.push(cIndex);
|
|
116
|
+
// Reorder dims
|
|
117
|
+
_finalDims = permutation.map((i) => dims[i]);
|
|
118
|
+
// Reorder shape
|
|
119
|
+
shape = permutation.map((i) => result.shape[i]);
|
|
120
|
+
// Transpose the data
|
|
121
|
+
data = transposeArray(result.data, result.shape, permutation, getItkComponentType(result.data));
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// "c" already at end or not present, just copy data
|
|
125
|
+
data = copyTypedArray(result.data);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Not a vector image, just copy data
|
|
130
|
+
data = copyTypedArray(result.data);
|
|
131
|
+
}
|
|
132
|
+
// For vector images, the last dimension is the component count, not a spatial dimension
|
|
133
|
+
const spatialShape = isVector ? shape.slice(0, -1) : shape;
|
|
134
|
+
const components = isVector ? shape[shape.length - 1] : 1;
|
|
135
|
+
// Create ITK-Wasm image
|
|
136
|
+
const itkImage = {
|
|
137
|
+
imageType: {
|
|
138
|
+
dimension: spatialShape.length,
|
|
139
|
+
componentType: getItkComponentType(data),
|
|
140
|
+
pixelType: isVector ? "VariableLengthVector" : "Scalar",
|
|
141
|
+
components,
|
|
142
|
+
},
|
|
143
|
+
name: "image",
|
|
144
|
+
origin: spatialShape.map(() => 0),
|
|
145
|
+
spacing: spatialShape.map(() => 1),
|
|
146
|
+
direction: createIdentityMatrix(spatialShape.length),
|
|
147
|
+
size: spatialShape,
|
|
148
|
+
data,
|
|
149
|
+
metadata: new Map(),
|
|
150
|
+
};
|
|
151
|
+
return itkImage;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Copy typed array to appropriate type
|
|
155
|
+
*/
|
|
156
|
+
function copyTypedArray(data) {
|
|
157
|
+
if (data instanceof Float32Array) {
|
|
158
|
+
return new Float32Array(data);
|
|
159
|
+
}
|
|
160
|
+
else if (data instanceof Float64Array) {
|
|
161
|
+
return new Float64Array(data);
|
|
162
|
+
}
|
|
163
|
+
else if (data instanceof Uint8Array) {
|
|
164
|
+
return new Uint8Array(data);
|
|
165
|
+
}
|
|
166
|
+
else if (data instanceof Int8Array) {
|
|
167
|
+
return new Int8Array(data);
|
|
168
|
+
}
|
|
169
|
+
else if (data instanceof Uint16Array) {
|
|
170
|
+
return new Uint16Array(data);
|
|
171
|
+
}
|
|
172
|
+
else if (data instanceof Int16Array) {
|
|
173
|
+
return new Int16Array(data);
|
|
174
|
+
}
|
|
175
|
+
else if (data instanceof Uint32Array) {
|
|
176
|
+
return new Uint32Array(data);
|
|
177
|
+
}
|
|
178
|
+
else if (data instanceof Int32Array) {
|
|
179
|
+
return new Int32Array(data);
|
|
180
|
+
}
|
|
181
|
+
else if (data instanceof BigInt64Array) {
|
|
182
|
+
return new BigInt64Array(data);
|
|
183
|
+
}
|
|
184
|
+
else if (data instanceof BigUint64Array) {
|
|
185
|
+
return new BigUint64Array(data);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// Convert to Float32Array as fallback
|
|
189
|
+
return new Float32Array(data);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Transpose array data according to permutation
|
|
194
|
+
*/
|
|
195
|
+
function transposeArray(data, shape, permutation, componentType) {
|
|
196
|
+
const typedData = data;
|
|
197
|
+
// Create output array of same type
|
|
198
|
+
let output;
|
|
199
|
+
const totalSize = typedData.length;
|
|
200
|
+
switch (componentType) {
|
|
201
|
+
case "uint8":
|
|
202
|
+
output = new Uint8Array(totalSize);
|
|
203
|
+
break;
|
|
204
|
+
case "int8":
|
|
205
|
+
output = new Int8Array(totalSize);
|
|
206
|
+
break;
|
|
207
|
+
case "int16":
|
|
208
|
+
output = new Int16Array(totalSize);
|
|
209
|
+
break;
|
|
210
|
+
case "uint16":
|
|
211
|
+
output = new Uint16Array(totalSize);
|
|
212
|
+
break;
|
|
213
|
+
case "int32":
|
|
214
|
+
output = new Int32Array(totalSize);
|
|
215
|
+
break;
|
|
216
|
+
case "uint32":
|
|
217
|
+
output = new Uint32Array(totalSize);
|
|
218
|
+
break;
|
|
219
|
+
case "int64":
|
|
220
|
+
output = new BigInt64Array(totalSize);
|
|
221
|
+
break;
|
|
222
|
+
case "uint64":
|
|
223
|
+
output = new BigUint64Array(totalSize);
|
|
224
|
+
break;
|
|
225
|
+
case "float64":
|
|
226
|
+
output = new Float64Array(totalSize);
|
|
227
|
+
break;
|
|
228
|
+
case "float32":
|
|
229
|
+
default:
|
|
230
|
+
output = new Float32Array(totalSize);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
// Calculate strides for source
|
|
234
|
+
const sourceStride = calculateStride(shape);
|
|
235
|
+
// Calculate new shape after permutation
|
|
236
|
+
const newShape = permutation.map((i) => shape[i]);
|
|
237
|
+
const targetStride = calculateStride(newShape);
|
|
238
|
+
// Perform transpose
|
|
239
|
+
const indices = new Array(shape.length).fill(0);
|
|
240
|
+
for (let i = 0; i < totalSize; i++) {
|
|
241
|
+
// Calculate source index from multi-dimensional indices
|
|
242
|
+
let sourceIdx = 0;
|
|
243
|
+
for (let j = 0; j < shape.length; j++) {
|
|
244
|
+
sourceIdx += indices[j] * sourceStride[j];
|
|
245
|
+
}
|
|
246
|
+
// Calculate target index with permuted dimensions
|
|
247
|
+
let targetIdx = 0;
|
|
248
|
+
for (let j = 0; j < permutation.length; j++) {
|
|
249
|
+
targetIdx += indices[permutation[j]] * targetStride[j];
|
|
250
|
+
}
|
|
251
|
+
output[targetIdx] = typedData[sourceIdx];
|
|
252
|
+
// Increment indices
|
|
253
|
+
for (let j = shape.length - 1; j >= 0; j--) {
|
|
254
|
+
indices[j]++;
|
|
255
|
+
if (indices[j] < shape[j])
|
|
256
|
+
break;
|
|
257
|
+
indices[j] = 0;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return output;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Get ITK component type from typed array
|
|
264
|
+
*/
|
|
265
|
+
function getItkComponentType(data) {
|
|
266
|
+
if (data instanceof Uint8Array)
|
|
267
|
+
return "uint8";
|
|
268
|
+
if (data instanceof Int8Array)
|
|
269
|
+
return "int8";
|
|
270
|
+
if (data instanceof Uint16Array)
|
|
271
|
+
return "uint16";
|
|
272
|
+
if (data instanceof Int16Array)
|
|
273
|
+
return "int16";
|
|
274
|
+
if (data instanceof Uint32Array)
|
|
275
|
+
return "uint32";
|
|
276
|
+
if (data instanceof Int32Array)
|
|
277
|
+
return "int32";
|
|
278
|
+
if (data instanceof BigUint64Array)
|
|
279
|
+
return "uint64";
|
|
280
|
+
if (data instanceof BigInt64Array)
|
|
281
|
+
return "int64";
|
|
282
|
+
if (data instanceof Float64Array)
|
|
283
|
+
return "float64";
|
|
284
|
+
return "float32";
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Create identity matrix for ITK direction
|
|
288
|
+
*/
|
|
289
|
+
function createIdentityMatrix(dimension) {
|
|
290
|
+
const matrix = new Float64Array(dimension * dimension);
|
|
291
|
+
for (let i = 0; i < dimension * dimension; i++) {
|
|
292
|
+
matrix[i] = i % (dimension + 1) === 0 ? 1 : 0;
|
|
293
|
+
}
|
|
294
|
+
return matrix;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Convert ITK-Wasm Image back to zarr array
|
|
298
|
+
*/
|
|
299
|
+
async function itkImageToZarr(itkImage, path, chunkShape) {
|
|
300
|
+
// Use in-memory store
|
|
301
|
+
const store = new Map();
|
|
302
|
+
const root = zarr.root(store);
|
|
303
|
+
// Determine data type
|
|
304
|
+
let dataType;
|
|
305
|
+
if (itkImage.data instanceof Uint8Array) {
|
|
306
|
+
dataType = "uint8";
|
|
307
|
+
}
|
|
308
|
+
else if (itkImage.data instanceof Int8Array) {
|
|
309
|
+
dataType = "int8";
|
|
310
|
+
}
|
|
311
|
+
else if (itkImage.data instanceof Int16Array) {
|
|
312
|
+
dataType = "int16";
|
|
313
|
+
}
|
|
314
|
+
else if (itkImage.data instanceof Uint16Array) {
|
|
315
|
+
dataType = "uint16";
|
|
316
|
+
}
|
|
317
|
+
else if (itkImage.data instanceof Int32Array) {
|
|
318
|
+
dataType = "int32";
|
|
319
|
+
}
|
|
320
|
+
else if (itkImage.data instanceof Uint32Array) {
|
|
321
|
+
dataType = "uint32";
|
|
322
|
+
}
|
|
323
|
+
else if (itkImage.data instanceof BigInt64Array) {
|
|
324
|
+
dataType = "int64";
|
|
325
|
+
}
|
|
326
|
+
else if (itkImage.data instanceof BigUint64Array) {
|
|
327
|
+
dataType = "uint64";
|
|
328
|
+
}
|
|
329
|
+
else if (itkImage.data instanceof Float64Array) {
|
|
330
|
+
dataType = "float64";
|
|
331
|
+
}
|
|
332
|
+
else if (itkImage.data instanceof Float32Array) {
|
|
333
|
+
dataType = "float32";
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
dataType = "float32";
|
|
337
|
+
}
|
|
338
|
+
const array = await zarr.create(root.resolve(path), {
|
|
339
|
+
shape: itkImage.size,
|
|
340
|
+
chunk_shape: chunkShape,
|
|
341
|
+
data_type: dataType,
|
|
342
|
+
fill_value: 0,
|
|
343
|
+
});
|
|
344
|
+
// Write data
|
|
345
|
+
await zarr.set(array, [], {
|
|
346
|
+
data: itkImage.data,
|
|
347
|
+
shape: itkImage.size,
|
|
348
|
+
stride: calculateStride(itkImage.size),
|
|
349
|
+
});
|
|
350
|
+
return array;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Calculate stride for array
|
|
354
|
+
*/
|
|
355
|
+
function calculateStride(shape) {
|
|
356
|
+
const stride = new Array(shape.length);
|
|
357
|
+
stride[shape.length - 1] = 1;
|
|
358
|
+
for (let i = shape.length - 2; i >= 0; i--) {
|
|
359
|
+
stride[i] = stride[i + 1] * shape[i + 1];
|
|
360
|
+
}
|
|
361
|
+
return stride;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Process channel-first data by downsampling each channel separately
|
|
365
|
+
*/
|
|
366
|
+
async function downsampleChannelFirst(image, dimFactors, spatialDims, smoothing) {
|
|
367
|
+
// Get the channel index and count
|
|
368
|
+
const cIndex = image.dims.indexOf("c");
|
|
369
|
+
const result = await zarr.get(image.data);
|
|
370
|
+
const channelCount = result.shape[cIndex];
|
|
371
|
+
// Process each channel separately
|
|
372
|
+
const downsampledChannels = [];
|
|
373
|
+
for (let channelIdx = 0; channelIdx < channelCount; channelIdx++) {
|
|
374
|
+
// Extract single channel data
|
|
375
|
+
const channelSlice = extractChannel(result, cIndex, channelIdx);
|
|
376
|
+
// Create temporary zarr array for this channel
|
|
377
|
+
const store = new Map();
|
|
378
|
+
const root = zarr.root(store);
|
|
379
|
+
const channelDims = image.dims.filter((d) => d !== "c");
|
|
380
|
+
const channelShape = result.shape.filter((_, i) => i !== cIndex);
|
|
381
|
+
const chunkShape = channelShape.map((s) => Math.min(s, 256));
|
|
382
|
+
const channelArray = await zarr.create(root.resolve("channel"), {
|
|
383
|
+
shape: channelShape,
|
|
384
|
+
chunk_shape: chunkShape,
|
|
385
|
+
data_type: getItkComponentType(result.data),
|
|
386
|
+
fill_value: 0,
|
|
387
|
+
});
|
|
388
|
+
await zarr.set(channelArray, [], {
|
|
389
|
+
data: channelSlice,
|
|
390
|
+
shape: channelShape,
|
|
391
|
+
stride: calculateStride(channelShape),
|
|
392
|
+
});
|
|
393
|
+
// Create NgffImage for this channel (unused but kept for potential future use)
|
|
394
|
+
// const _channelImage = new NgffImage({
|
|
395
|
+
// data: channelArray,
|
|
396
|
+
// dims: channelDims,
|
|
397
|
+
// scale: Object.fromEntries(
|
|
398
|
+
// Object.entries(image.scale).filter(([k]) => k !== "c")
|
|
399
|
+
// ),
|
|
400
|
+
// translation: Object.fromEntries(
|
|
401
|
+
// Object.entries(image.translation).filter(([k]) => k !== "c")
|
|
402
|
+
// ),
|
|
403
|
+
// name: image.name,
|
|
404
|
+
// axesUnits: image.axesUnits,
|
|
405
|
+
// computedCallbacks: image.computedCallbacks,
|
|
406
|
+
// });
|
|
407
|
+
// Downsample this channel
|
|
408
|
+
const itkImage = await zarrToItkImage(channelArray, channelDims, false);
|
|
409
|
+
const shrinkFactors = [];
|
|
410
|
+
for (let i = 0; i < channelDims.length; i++) {
|
|
411
|
+
const dim = channelDims[i];
|
|
412
|
+
if (SPATIAL_DIMS.includes(dim)) {
|
|
413
|
+
shrinkFactors.push(dimFactors[dim] || 1);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
shrinkFactors.push(1); // Non-spatial dimensions don't shrink
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
let downsampled;
|
|
420
|
+
if (smoothing === "gaussian") {
|
|
421
|
+
const blockSize = itkImage.size.slice().reverse();
|
|
422
|
+
const sigma = computeSigma(shrinkFactors);
|
|
423
|
+
const { radius: _radius } = await gaussianKernelRadius({
|
|
424
|
+
size: blockSize,
|
|
425
|
+
sigma,
|
|
426
|
+
});
|
|
427
|
+
const result = await downsample(itkImage, {
|
|
428
|
+
shrinkFactors,
|
|
429
|
+
cropRadius: shrinkFactors.map(() => 0),
|
|
430
|
+
});
|
|
431
|
+
downsampled = result.downsampled;
|
|
432
|
+
}
|
|
433
|
+
else if (smoothing === "bin_shrink") {
|
|
434
|
+
const result = await downsampleBinShrink(itkImage, {
|
|
435
|
+
shrinkFactors,
|
|
436
|
+
});
|
|
437
|
+
downsampled = result.downsampled;
|
|
438
|
+
}
|
|
439
|
+
else if (smoothing === "label_image") {
|
|
440
|
+
const blockSize = itkImage.size.slice().reverse();
|
|
441
|
+
const sigma = computeSigma(shrinkFactors);
|
|
442
|
+
const { radius: _radius } = await gaussianKernelRadius({
|
|
443
|
+
size: blockSize,
|
|
444
|
+
sigma,
|
|
445
|
+
});
|
|
446
|
+
const result = await downsampleLabelImage(itkImage, {
|
|
447
|
+
shrinkFactors,
|
|
448
|
+
cropRadius: shrinkFactors.map(() => 0),
|
|
449
|
+
});
|
|
450
|
+
downsampled = result.downsampled;
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
throw new Error(`Unknown smoothing method: ${smoothing}`);
|
|
454
|
+
}
|
|
455
|
+
// Convert back to zarr array
|
|
456
|
+
const downsampledChunkShape = downsampled.size.map((s) => Math.min(s, 256));
|
|
457
|
+
const downsampledArray = await itkImageToZarr(downsampled, "downsampled_channel", downsampledChunkShape);
|
|
458
|
+
downsampledChannels.push(downsampledArray);
|
|
459
|
+
}
|
|
460
|
+
// Combine all channels back together
|
|
461
|
+
const combinedArray = await combineChannels(downsampledChannels, cIndex, image.dims);
|
|
462
|
+
// Compute new metadata
|
|
463
|
+
const [translation, scale] = nextScaleMetadata(image, dimFactors, spatialDims);
|
|
464
|
+
return new NgffImage({
|
|
465
|
+
data: combinedArray,
|
|
466
|
+
dims: image.dims,
|
|
467
|
+
scale,
|
|
468
|
+
translation,
|
|
469
|
+
name: image.name,
|
|
470
|
+
axesUnits: image.axesUnits,
|
|
471
|
+
computedCallbacks: image.computedCallbacks,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Extract a single channel from the data
|
|
476
|
+
*/
|
|
477
|
+
function extractChannel(result, cIndex, channelIdx) {
|
|
478
|
+
const typedData = result.data;
|
|
479
|
+
const shape = result.shape;
|
|
480
|
+
// Calculate output size (all dims except channel)
|
|
481
|
+
const outputSize = shape.reduce((acc, s, i) => (i === cIndex ? acc : acc * s), 1);
|
|
482
|
+
let output;
|
|
483
|
+
if (typedData instanceof Uint8Array) {
|
|
484
|
+
output = new Uint8Array(outputSize);
|
|
485
|
+
}
|
|
486
|
+
else if (typedData instanceof Int8Array) {
|
|
487
|
+
output = new Int8Array(outputSize);
|
|
488
|
+
}
|
|
489
|
+
else if (typedData instanceof Int16Array) {
|
|
490
|
+
output = new Int16Array(outputSize);
|
|
491
|
+
}
|
|
492
|
+
else if (typedData instanceof Uint16Array) {
|
|
493
|
+
output = new Uint16Array(outputSize);
|
|
494
|
+
}
|
|
495
|
+
else if (typedData instanceof Int32Array) {
|
|
496
|
+
output = new Int32Array(outputSize);
|
|
497
|
+
}
|
|
498
|
+
else if (typedData instanceof Uint32Array) {
|
|
499
|
+
output = new Uint32Array(outputSize);
|
|
500
|
+
}
|
|
501
|
+
else if (typedData instanceof BigInt64Array) {
|
|
502
|
+
output = new BigInt64Array(outputSize);
|
|
503
|
+
}
|
|
504
|
+
else if (typedData instanceof BigUint64Array) {
|
|
505
|
+
output = new BigUint64Array(outputSize);
|
|
506
|
+
}
|
|
507
|
+
else if (typedData instanceof Float64Array) {
|
|
508
|
+
output = new Float64Array(outputSize);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
output = new Float32Array(outputSize);
|
|
512
|
+
}
|
|
513
|
+
// Calculate strides
|
|
514
|
+
const stride = calculateStride(shape);
|
|
515
|
+
const outputShape = shape.filter((_, i) => i !== cIndex);
|
|
516
|
+
const _outputStride = calculateStride(outputShape);
|
|
517
|
+
// Extract channel
|
|
518
|
+
const indices = new Array(shape.length).fill(0);
|
|
519
|
+
let outputIdx = 0;
|
|
520
|
+
for (let i = 0; i < outputSize; i++) {
|
|
521
|
+
// Set channel index
|
|
522
|
+
indices[cIndex] = channelIdx;
|
|
523
|
+
// Calculate source index
|
|
524
|
+
let sourceIdx = 0;
|
|
525
|
+
for (let j = 0; j < shape.length; j++) {
|
|
526
|
+
sourceIdx += indices[j] * stride[j];
|
|
527
|
+
}
|
|
528
|
+
output[outputIdx++] = typedData[sourceIdx];
|
|
529
|
+
// Increment indices (skip channel dimension)
|
|
530
|
+
for (let j = shape.length - 1; j >= 0; j--) {
|
|
531
|
+
if (j === cIndex)
|
|
532
|
+
continue;
|
|
533
|
+
indices[j]++;
|
|
534
|
+
if (indices[j] < shape[j])
|
|
535
|
+
break;
|
|
536
|
+
indices[j] = 0;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return output;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Combine multiple channel arrays back into a single multi-channel array
|
|
543
|
+
*/
|
|
544
|
+
async function combineChannels(channels, cIndex, _originalDims) {
|
|
545
|
+
// Read all channel data
|
|
546
|
+
const channelData = await Promise.all(channels.map((c) => zarr.get(c)));
|
|
547
|
+
// Determine combined shape
|
|
548
|
+
const firstChannel = channelData[0];
|
|
549
|
+
const channelShape = firstChannel.shape;
|
|
550
|
+
const combinedShape = [...channelShape];
|
|
551
|
+
combinedShape.splice(cIndex, 0, channels.length);
|
|
552
|
+
// Create combined array
|
|
553
|
+
const store = new Map();
|
|
554
|
+
const root = zarr.root(store);
|
|
555
|
+
const chunkShape = combinedShape.map((s) => Math.min(s, 256));
|
|
556
|
+
const dataType = getItkComponentType(firstChannel.data);
|
|
557
|
+
const combinedArray = await zarr.create(root.resolve("combined"), {
|
|
558
|
+
shape: combinedShape,
|
|
559
|
+
chunk_shape: chunkShape,
|
|
560
|
+
data_type: dataType,
|
|
561
|
+
fill_value: 0,
|
|
562
|
+
});
|
|
563
|
+
// Combine all channels
|
|
564
|
+
const totalSize = combinedShape.reduce((acc, s) => acc * s, 1);
|
|
565
|
+
let combined;
|
|
566
|
+
if (dataType === "uint8") {
|
|
567
|
+
combined = new Uint8Array(totalSize);
|
|
568
|
+
}
|
|
569
|
+
else if (dataType === "int8") {
|
|
570
|
+
combined = new Int8Array(totalSize);
|
|
571
|
+
}
|
|
572
|
+
else if (dataType === "int16") {
|
|
573
|
+
combined = new Int16Array(totalSize);
|
|
574
|
+
}
|
|
575
|
+
else if (dataType === "uint16") {
|
|
576
|
+
combined = new Uint16Array(totalSize);
|
|
577
|
+
}
|
|
578
|
+
else if (dataType === "int32") {
|
|
579
|
+
combined = new Int32Array(totalSize);
|
|
580
|
+
}
|
|
581
|
+
else if (dataType === "uint32") {
|
|
582
|
+
combined = new Uint32Array(totalSize);
|
|
583
|
+
}
|
|
584
|
+
else if (dataType === "int64") {
|
|
585
|
+
combined = new BigInt64Array(totalSize);
|
|
586
|
+
}
|
|
587
|
+
else if (dataType === "uint64") {
|
|
588
|
+
combined = new BigUint64Array(totalSize);
|
|
589
|
+
}
|
|
590
|
+
else if (dataType === "float64") {
|
|
591
|
+
combined = new Float64Array(totalSize);
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
combined = new Float32Array(totalSize);
|
|
595
|
+
}
|
|
596
|
+
const stride = calculateStride(combinedShape);
|
|
597
|
+
const _channelStride = calculateStride(channelShape);
|
|
598
|
+
// Copy each channel's data
|
|
599
|
+
for (let c = 0; c < channels.length; c++) {
|
|
600
|
+
const channelTypedData = channelData[c].data;
|
|
601
|
+
const indices = new Array(combinedShape.length).fill(0);
|
|
602
|
+
for (let i = 0; i < channelTypedData.length; i++) {
|
|
603
|
+
// Set channel index
|
|
604
|
+
indices[cIndex] = c;
|
|
605
|
+
// Calculate target index in combined array
|
|
606
|
+
let targetIdx = 0;
|
|
607
|
+
for (let j = 0; j < combinedShape.length; j++) {
|
|
608
|
+
targetIdx += indices[j] * stride[j];
|
|
609
|
+
}
|
|
610
|
+
combined[targetIdx] = channelTypedData[i];
|
|
611
|
+
// Increment indices (skip channel dimension)
|
|
612
|
+
for (let j = combinedShape.length - 1; j >= 0; j--) {
|
|
613
|
+
if (j === cIndex)
|
|
614
|
+
continue;
|
|
615
|
+
indices[j]++;
|
|
616
|
+
if (indices[j] < combinedShape[j])
|
|
617
|
+
break;
|
|
618
|
+
indices[j] = 0;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// Write combined data
|
|
623
|
+
await zarr.set(combinedArray, [], {
|
|
624
|
+
data: combined,
|
|
625
|
+
shape: combinedShape,
|
|
626
|
+
stride,
|
|
627
|
+
});
|
|
628
|
+
return combinedArray;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Perform Gaussian downsampling using ITK-Wasm
|
|
632
|
+
*/
|
|
633
|
+
async function downsampleGaussian(image, dimFactors, spatialDims) {
|
|
634
|
+
const cIndex = image.dims.indexOf("c");
|
|
635
|
+
const isVector = cIndex === image.dims.length - 1;
|
|
636
|
+
const isChannelFirst = cIndex !== -1 && cIndex < image.dims.length - 1 &&
|
|
637
|
+
!isVector;
|
|
638
|
+
// If channel is first (before spatial dims), process each channel separately
|
|
639
|
+
if (isChannelFirst) {
|
|
640
|
+
return await downsampleChannelFirst(image, dimFactors, spatialDims, "gaussian");
|
|
641
|
+
}
|
|
642
|
+
// Convert to ITK-Wasm format
|
|
643
|
+
const itkImage = await zarrToItkImage(image.data, image.dims, isVector);
|
|
644
|
+
// Prepare shrink factors - need to be for spatial dimensions only
|
|
645
|
+
// For vector images, the last dimension (c) is NOT a spatial dimension in the ITK image
|
|
646
|
+
const shrinkFactors = [];
|
|
647
|
+
const effectiveDims = isVector ? image.dims.slice(0, -1) : image.dims;
|
|
648
|
+
for (let i = 0; i < effectiveDims.length; i++) {
|
|
649
|
+
const dim = effectiveDims[i];
|
|
650
|
+
if (SPATIAL_DIMS.includes(dim)) {
|
|
651
|
+
shrinkFactors.push(dimFactors[dim] || 1);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
shrinkFactors.push(1); // Non-spatial dimensions don't shrink
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// Compute kernel radius - sigma should also be for ALL dimensions
|
|
658
|
+
const blockSize = itkImage.size.slice().reverse();
|
|
659
|
+
const sigma = computeSigma(shrinkFactors);
|
|
660
|
+
const { radius: _radius } = await gaussianKernelRadius({
|
|
661
|
+
size: blockSize,
|
|
662
|
+
sigma,
|
|
663
|
+
});
|
|
664
|
+
// Perform downsampling
|
|
665
|
+
const { downsampled } = await downsample(itkImage, {
|
|
666
|
+
shrinkFactors,
|
|
667
|
+
cropRadius: shrinkFactors.map(() => 0),
|
|
668
|
+
});
|
|
669
|
+
// Compute new metadata
|
|
670
|
+
const [translation, scale] = nextScaleMetadata(image, dimFactors, spatialDims);
|
|
671
|
+
// Convert back to zarr array
|
|
672
|
+
const chunkShape = downsampled.size.map((s) => Math.min(s, 256));
|
|
673
|
+
const array = await itkImageToZarr(downsampled, "downsampled", chunkShape);
|
|
674
|
+
return new NgffImage({
|
|
675
|
+
data: array,
|
|
676
|
+
dims: image.dims,
|
|
677
|
+
scale,
|
|
678
|
+
translation,
|
|
679
|
+
name: image.name,
|
|
680
|
+
axesUnits: image.axesUnits,
|
|
681
|
+
computedCallbacks: image.computedCallbacks,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Perform bin shrink downsampling using ITK-Wasm
|
|
686
|
+
*/
|
|
687
|
+
async function downsampleBinShrinkImpl(image, dimFactors, spatialDims) {
|
|
688
|
+
const cIndex = image.dims.indexOf("c");
|
|
689
|
+
const isVector = cIndex === image.dims.length - 1;
|
|
690
|
+
const isChannelFirst = cIndex !== -1 && cIndex < image.dims.length - 1 &&
|
|
691
|
+
!isVector;
|
|
692
|
+
// If channel is first (before spatial dims), process each channel separately
|
|
693
|
+
if (isChannelFirst) {
|
|
694
|
+
return await downsampleChannelFirst(image, dimFactors, spatialDims, "bin_shrink");
|
|
695
|
+
}
|
|
696
|
+
// Convert to ITK-Wasm format
|
|
697
|
+
const itkImage = await zarrToItkImage(image.data, image.dims, isVector);
|
|
698
|
+
// Prepare shrink factors - need to be for spatial dimensions only
|
|
699
|
+
// For vector images, the last dimension (c) is NOT a spatial dimension in the ITK image
|
|
700
|
+
const shrinkFactors = [];
|
|
701
|
+
const effectiveDims = isVector ? image.dims.slice(0, -1) : image.dims;
|
|
702
|
+
for (let i = 0; i < effectiveDims.length; i++) {
|
|
703
|
+
const dim = effectiveDims[i];
|
|
704
|
+
if (SPATIAL_DIMS.includes(dim)) {
|
|
705
|
+
shrinkFactors.push(dimFactors[dim] || 1);
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
shrinkFactors.push(1); // Non-spatial dimensions don't shrink
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// Perform downsampling
|
|
712
|
+
const { downsampled } = await downsampleBinShrink(itkImage, {
|
|
713
|
+
shrinkFactors,
|
|
714
|
+
});
|
|
715
|
+
// Compute new metadata
|
|
716
|
+
const [translation, scale] = nextScaleMetadata(image, dimFactors, spatialDims);
|
|
717
|
+
// Convert back to zarr array
|
|
718
|
+
const chunkShape = downsampled.size.map((s) => Math.min(s, 256));
|
|
719
|
+
const array = await itkImageToZarr(downsampled, "downsampled", chunkShape);
|
|
720
|
+
return new NgffImage({
|
|
721
|
+
data: array,
|
|
722
|
+
dims: image.dims,
|
|
723
|
+
scale,
|
|
724
|
+
translation,
|
|
725
|
+
name: image.name,
|
|
726
|
+
axesUnits: image.axesUnits,
|
|
727
|
+
computedCallbacks: image.computedCallbacks,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Perform label image downsampling using ITK-Wasm
|
|
732
|
+
*/
|
|
733
|
+
async function downsampleLabelImageImpl(image, dimFactors, spatialDims) {
|
|
734
|
+
const cIndex = image.dims.indexOf("c");
|
|
735
|
+
const isVector = cIndex === image.dims.length - 1;
|
|
736
|
+
const isChannelFirst = cIndex !== -1 && cIndex < image.dims.length - 1 &&
|
|
737
|
+
!isVector;
|
|
738
|
+
// If channel is first (before spatial dims), process each channel separately
|
|
739
|
+
if (isChannelFirst) {
|
|
740
|
+
return await downsampleChannelFirst(image, dimFactors, spatialDims, "label_image");
|
|
741
|
+
}
|
|
742
|
+
// Convert to ITK-Wasm format
|
|
743
|
+
const itkImage = await zarrToItkImage(image.data, image.dims, isVector);
|
|
744
|
+
// Prepare shrink factors - need to be for spatial dimensions only
|
|
745
|
+
// For vector images, the last dimension (c) is NOT a spatial dimension in the ITK image
|
|
746
|
+
const shrinkFactors = [];
|
|
747
|
+
const effectiveDims = isVector ? image.dims.slice(0, -1) : image.dims;
|
|
748
|
+
for (let i = 0; i < effectiveDims.length; i++) {
|
|
749
|
+
const dim = effectiveDims[i];
|
|
750
|
+
if (SPATIAL_DIMS.includes(dim)) {
|
|
751
|
+
shrinkFactors.push(dimFactors[dim] || 1);
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
shrinkFactors.push(1); // Non-spatial dimensions don't shrink
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
// Compute kernel radius
|
|
758
|
+
const blockSize = itkImage.size.slice().reverse();
|
|
759
|
+
const sigma = computeSigma(shrinkFactors);
|
|
760
|
+
const { radius: _radius } = await gaussianKernelRadius({
|
|
761
|
+
size: blockSize,
|
|
762
|
+
sigma,
|
|
763
|
+
});
|
|
764
|
+
// Perform downsampling
|
|
765
|
+
const { downsampled } = await downsampleLabelImage(itkImage, {
|
|
766
|
+
shrinkFactors,
|
|
767
|
+
cropRadius: shrinkFactors.map(() => 0),
|
|
768
|
+
});
|
|
769
|
+
// Compute new metadata
|
|
770
|
+
const [translation, scale] = nextScaleMetadata(image, dimFactors, spatialDims);
|
|
771
|
+
// Convert back to zarr array
|
|
772
|
+
const chunkShape = downsampled.size.map((s) => Math.min(s, 256));
|
|
773
|
+
const array = await itkImageToZarr(downsampled, "downsampled", chunkShape);
|
|
774
|
+
return new NgffImage({
|
|
775
|
+
data: array,
|
|
776
|
+
dims: image.dims,
|
|
777
|
+
scale,
|
|
778
|
+
translation,
|
|
779
|
+
name: image.name,
|
|
780
|
+
axesUnits: image.axesUnits,
|
|
781
|
+
computedCallbacks: image.computedCallbacks,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Main downsampling function for ITK-Wasm
|
|
786
|
+
*/
|
|
787
|
+
export async function downsampleItkWasm(ngffImage, scaleFactors, smoothing) {
|
|
788
|
+
const multiscales = [ngffImage];
|
|
789
|
+
let previousImage = ngffImage;
|
|
790
|
+
const dims = ngffImage.dims;
|
|
791
|
+
let previousDimFactors = {};
|
|
792
|
+
for (const dim of dims) {
|
|
793
|
+
previousDimFactors[dim] = 1;
|
|
794
|
+
}
|
|
795
|
+
const spatialDims = dims.filter((dim) => SPATIAL_DIMS.includes(dim));
|
|
796
|
+
for (const scaleFactor of scaleFactors) {
|
|
797
|
+
const dimFactors = dimScaleFactors(dims, scaleFactor, previousDimFactors);
|
|
798
|
+
previousDimFactors = updatePreviousDimFactors(scaleFactor, spatialDims, previousDimFactors);
|
|
799
|
+
let downsampled;
|
|
800
|
+
if (smoothing === "gaussian") {
|
|
801
|
+
downsampled = await downsampleGaussian(previousImage, dimFactors, spatialDims);
|
|
802
|
+
}
|
|
803
|
+
else if (smoothing === "bin_shrink") {
|
|
804
|
+
downsampled = await downsampleBinShrinkImpl(previousImage, dimFactors, spatialDims);
|
|
805
|
+
}
|
|
806
|
+
else if (smoothing === "label_image") {
|
|
807
|
+
downsampled = await downsampleLabelImageImpl(previousImage, dimFactors, spatialDims);
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
throw new Error(`Unknown smoothing method: ${smoothing}`);
|
|
811
|
+
}
|
|
812
|
+
multiscales.push(downsampled);
|
|
813
|
+
previousImage = downsampled;
|
|
814
|
+
}
|
|
815
|
+
return multiscales;
|
|
816
|
+
}
|