@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.
Files changed (174) hide show
  1. package/README.md +9 -2
  2. package/esm/io/from_ngff_zarr.d.ts +4 -1
  3. package/esm/io/from_ngff_zarr.d.ts.map +1 -1
  4. package/esm/io/from_ngff_zarr.js +94 -27
  5. package/esm/io/hcs.d.ts +18 -0
  6. package/esm/io/hcs.d.ts.map +1 -0
  7. package/esm/io/hcs.js +51 -0
  8. package/esm/io/itk_image_to_ngff_image.d.ts +25 -0
  9. package/esm/io/itk_image_to_ngff_image.d.ts.map +1 -0
  10. package/esm/io/itk_image_to_ngff_image.js +127 -0
  11. package/esm/io/ngff_image_to_itk_image.d.ts +30 -0
  12. package/esm/io/ngff_image_to_itk_image.d.ts.map +1 -0
  13. package/esm/io/ngff_image_to_itk_image.js +216 -0
  14. package/esm/io/to_multiscales.d.ts +18 -0
  15. package/esm/io/to_multiscales.d.ts.map +1 -0
  16. package/esm/io/to_multiscales.js +62 -0
  17. package/esm/io/to_ngff_image.d.ts +17 -0
  18. package/esm/io/to_ngff_image.d.ts.map +1 -0
  19. package/esm/io/to_ngff_image.js +136 -0
  20. package/esm/io/to_ngff_zarr.d.ts +3 -2
  21. package/esm/io/to_ngff_zarr.d.ts.map +1 -1
  22. package/esm/io/to_ngff_zarr.js +273 -26
  23. package/esm/methods/itkwasm.d.ts +6 -0
  24. package/esm/methods/itkwasm.d.ts.map +1 -0
  25. package/esm/methods/itkwasm.js +816 -0
  26. package/esm/mod.d.ts +9 -2
  27. package/esm/mod.d.ts.map +1 -1
  28. package/esm/mod.js +10 -2
  29. package/esm/schemas/coordinate_systems.d.ts +644 -0
  30. package/esm/schemas/coordinate_systems.d.ts.map +1 -0
  31. package/esm/schemas/coordinate_systems.js +140 -0
  32. package/esm/schemas/index.d.ts +9 -0
  33. package/esm/schemas/index.d.ts.map +1 -0
  34. package/esm/schemas/index.js +38 -0
  35. package/esm/schemas/methods.d.ts.map +1 -1
  36. package/esm/schemas/methods.js +2 -0
  37. package/esm/schemas/multiscales.d.ts.map +1 -1
  38. package/esm/schemas/multiscales.js +2 -0
  39. package/esm/schemas/ngff_image.d.ts +9 -2
  40. package/esm/schemas/ngff_image.d.ts.map +1 -1
  41. package/esm/schemas/ngff_image.js +11 -2
  42. package/esm/schemas/ome_zarr.d.ts +581 -0
  43. package/esm/schemas/ome_zarr.d.ts.map +1 -0
  44. package/esm/schemas/ome_zarr.js +208 -0
  45. package/esm/schemas/rfc4.d.ts +439 -0
  46. package/esm/schemas/rfc4.d.ts.map +1 -0
  47. package/esm/schemas/rfc4.js +129 -0
  48. package/esm/schemas/units.d.ts.map +1 -1
  49. package/esm/schemas/units.js +5 -0
  50. package/esm/schemas/zarr_metadata.d.ts +302 -9
  51. package/esm/schemas/zarr_metadata.d.ts.map +1 -1
  52. package/esm/schemas/zarr_metadata.js +22 -1
  53. package/esm/types/array_interface.d.ts +7 -0
  54. package/esm/types/array_interface.d.ts.map +1 -0
  55. package/esm/types/array_interface.js +1 -0
  56. package/esm/types/hcs.d.ts +70 -0
  57. package/esm/types/hcs.d.ts.map +1 -0
  58. package/esm/types/hcs.js +204 -0
  59. package/esm/types/methods.d.ts.map +1 -1
  60. package/esm/types/methods.js +2 -0
  61. package/esm/types/multiscales.d.ts.map +1 -1
  62. package/esm/types/ngff_image.d.ts +6 -3
  63. package/esm/types/ngff_image.d.ts.map +1 -1
  64. package/esm/types/ngff_image.js +13 -1
  65. package/esm/types/rfc4.d.ts +94 -0
  66. package/esm/types/rfc4.d.ts.map +1 -0
  67. package/esm/types/rfc4.js +135 -0
  68. package/esm/types/units.d.ts +1 -1
  69. package/esm/types/units.d.ts.map +1 -1
  70. package/esm/types/zarr_metadata.d.ts +14 -5
  71. package/esm/types/zarr_metadata.d.ts.map +1 -1
  72. package/esm/utils/create_queue.d.ts +6 -0
  73. package/esm/utils/create_queue.d.ts.map +1 -0
  74. package/esm/utils/create_queue.js +11 -0
  75. package/esm/utils/factory.d.ts +1 -1
  76. package/esm/utils/factory.d.ts.map +1 -1
  77. package/esm/utils/factory.js +16 -7
  78. package/esm/utils/method_metadata.d.ts +10 -0
  79. package/esm/utils/method_metadata.d.ts.map +1 -0
  80. package/esm/utils/method_metadata.js +37 -0
  81. package/esm/utils/validation.d.ts.map +1 -1
  82. package/package.json +7 -1
  83. package/script/io/from_ngff_zarr.d.ts +4 -1
  84. package/script/io/from_ngff_zarr.d.ts.map +1 -1
  85. package/script/io/from_ngff_zarr.js +94 -27
  86. package/script/io/hcs.d.ts +18 -0
  87. package/script/io/hcs.d.ts.map +1 -0
  88. package/script/io/hcs.js +55 -0
  89. package/script/io/itk_image_to_ngff_image.d.ts +25 -0
  90. package/script/io/itk_image_to_ngff_image.d.ts.map +1 -0
  91. package/script/io/itk_image_to_ngff_image.js +153 -0
  92. package/script/io/ngff_image_to_itk_image.d.ts +30 -0
  93. package/script/io/ngff_image_to_itk_image.d.ts.map +1 -0
  94. package/script/io/ngff_image_to_itk_image.js +242 -0
  95. package/script/io/to_multiscales.d.ts +18 -0
  96. package/script/io/to_multiscales.d.ts.map +1 -0
  97. package/script/io/to_multiscales.js +67 -0
  98. package/script/io/to_ngff_image.d.ts +17 -0
  99. package/script/io/to_ngff_image.d.ts.map +1 -0
  100. package/script/io/to_ngff_image.js +162 -0
  101. package/script/io/to_ngff_zarr.d.ts +3 -2
  102. package/script/io/to_ngff_zarr.d.ts.map +1 -1
  103. package/script/io/to_ngff_zarr.js +273 -26
  104. package/script/methods/itkwasm.d.ts +6 -0
  105. package/script/methods/itkwasm.d.ts.map +1 -0
  106. package/script/methods/itkwasm.js +842 -0
  107. package/script/mod.d.ts +9 -2
  108. package/script/mod.d.ts.map +1 -1
  109. package/script/mod.js +12 -3
  110. package/script/schemas/coordinate_systems.d.ts +644 -0
  111. package/script/schemas/coordinate_systems.d.ts.map +1 -0
  112. package/script/schemas/coordinate_systems.js +143 -0
  113. package/script/schemas/index.d.ts +9 -0
  114. package/script/schemas/index.d.ts.map +1 -0
  115. package/script/schemas/index.js +101 -0
  116. package/script/schemas/methods.d.ts.map +1 -1
  117. package/script/schemas/methods.js +2 -0
  118. package/script/schemas/multiscales.d.ts.map +1 -1
  119. package/script/schemas/multiscales.js +2 -0
  120. package/script/schemas/ngff_image.d.ts +9 -2
  121. package/script/schemas/ngff_image.d.ts.map +1 -1
  122. package/script/schemas/ngff_image.js +11 -2
  123. package/script/schemas/ome_zarr.d.ts +581 -0
  124. package/script/schemas/ome_zarr.d.ts.map +1 -0
  125. package/script/schemas/ome_zarr.js +211 -0
  126. package/script/schemas/rfc4.d.ts +439 -0
  127. package/script/schemas/rfc4.d.ts.map +1 -0
  128. package/script/schemas/rfc4.js +132 -0
  129. package/script/schemas/units.d.ts.map +1 -1
  130. package/script/schemas/units.js +5 -0
  131. package/script/schemas/zarr_metadata.d.ts +302 -9
  132. package/script/schemas/zarr_metadata.d.ts.map +1 -1
  133. package/script/schemas/zarr_metadata.js +23 -2
  134. package/script/types/array_interface.d.ts +7 -0
  135. package/script/types/array_interface.d.ts.map +1 -0
  136. package/script/types/array_interface.js +2 -0
  137. package/script/types/hcs.d.ts +70 -0
  138. package/script/types/hcs.d.ts.map +1 -0
  139. package/script/types/hcs.js +233 -0
  140. package/script/types/methods.d.ts.map +1 -1
  141. package/script/types/methods.js +2 -0
  142. package/script/types/multiscales.d.ts.map +1 -1
  143. package/script/types/ngff_image.d.ts +6 -3
  144. package/script/types/ngff_image.d.ts.map +1 -1
  145. package/script/types/ngff_image.js +13 -1
  146. package/script/types/rfc4.d.ts +94 -0
  147. package/script/types/rfc4.d.ts.map +1 -0
  148. package/script/types/rfc4.js +143 -0
  149. package/script/types/units.d.ts +1 -1
  150. package/script/types/units.d.ts.map +1 -1
  151. package/script/types/zarr_metadata.d.ts +14 -5
  152. package/script/types/zarr_metadata.d.ts.map +1 -1
  153. package/script/utils/create_queue.d.ts +6 -0
  154. package/script/utils/create_queue.d.ts.map +1 -0
  155. package/script/utils/create_queue.js +17 -0
  156. package/script/utils/factory.d.ts +1 -1
  157. package/script/utils/factory.d.ts.map +1 -1
  158. package/script/utils/factory.js +39 -7
  159. package/script/utils/method_metadata.d.ts +10 -0
  160. package/script/utils/method_metadata.d.ts.map +1 -0
  161. package/script/utils/method_metadata.js +40 -0
  162. package/script/utils/validation.d.ts.map +1 -1
  163. package/esm/schemas/lazy_array.d.ts +0 -8
  164. package/esm/schemas/lazy_array.d.ts.map +0 -1
  165. package/esm/schemas/lazy_array.js +0 -7
  166. package/esm/types/lazy_array.d.ts +0 -18
  167. package/esm/types/lazy_array.d.ts.map +0 -1
  168. package/esm/types/lazy_array.js +0 -27
  169. package/script/schemas/lazy_array.d.ts +0 -8
  170. package/script/schemas/lazy_array.d.ts.map +0 -1
  171. package/script/schemas/lazy_array.js +0 -10
  172. package/script/types/lazy_array.d.ts +0 -18
  173. package/script/types/lazy_array.d.ts.map +0 -1
  174. 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
+ }