Skip to content

Commit 81d612f

Browse files
committed
feat(ts): add downsampleItkWasm method
1 parent 4e91c0c commit 81d612f

File tree

6 files changed

+1015
-179
lines changed

6 files changed

+1015
-179
lines changed

ts/pixi.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[project]
1+
[workspace]
22
name = "ngff-zarr-ts"
33
version = "0.1.0"
44
description = "TypeScript implementation of ngff-zarr for Deno, Node, and the browser."

ts/src/io/to_multiscales.ts

Lines changed: 37 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,17 @@
1-
import * as zarr from "zarrita";
21
import { NgffImage } from "../types/ngff_image.ts";
32
import { Multiscales } from "../types/multiscales.ts";
43
import { Methods } from "../types/methods.ts";
5-
import type { MemoryStore } from "../io/from_ngff_zarr.ts";
64
import {
75
createAxis,
86
createDataset,
97
createMetadata,
108
createMultiscales,
119
} from "../utils/factory.ts";
1210
import { getMethodMetadata } from "../utils/method_metadata.ts";
11+
import { downsampleItkWasm } from "../methods/itkwasm.ts";
1312

14-
export interface ToNgffImageOptions {
15-
dims?: string[];
16-
scale?: Record<string, number>;
17-
translation?: Record<string, number>;
18-
name?: string;
19-
}
20-
21-
/**
22-
* Convert array data to NgffImage
23-
*
24-
* @param data - Input data as typed array or regular array
25-
* @param options - Configuration options for NgffImage creation
26-
* @returns NgffImage instance
27-
*/
28-
export async function toNgffImage(
29-
data: ArrayLike<number> | number[][] | number[][][],
30-
options: ToNgffImageOptions = {},
31-
): Promise<NgffImage> {
32-
const {
33-
dims = ["y", "x"],
34-
scale = {},
35-
translation = {},
36-
name = "image",
37-
} = options;
38-
39-
// Determine data shape and create typed array
40-
let typedData: Float32Array;
41-
let shape: number[];
42-
43-
if (Array.isArray(data)) {
44-
// Handle multi-dimensional arrays
45-
if (Array.isArray(data[0])) {
46-
if (Array.isArray((data[0] as unknown[])[0])) {
47-
// 3D array
48-
const d3 = data as number[][][];
49-
shape = [d3.length, d3[0].length, d3[0][0].length];
50-
typedData = new Float32Array(shape[0] * shape[1] * shape[2]);
51-
52-
let idx = 0;
53-
for (let i = 0; i < shape[0]; i++) {
54-
for (let j = 0; j < shape[1]; j++) {
55-
for (let k = 0; k < shape[2]; k++) {
56-
typedData[idx++] = d3[i][j][k];
57-
}
58-
}
59-
}
60-
} else {
61-
// 2D array
62-
const d2 = data as number[][];
63-
shape = [d2.length, d2[0].length];
64-
typedData = new Float32Array(shape[0] * shape[1]);
65-
66-
let idx = 0;
67-
for (let i = 0; i < shape[0]; i++) {
68-
for (let j = 0; j < shape[1]; j++) {
69-
typedData[idx++] = d2[i][j];
70-
}
71-
}
72-
}
73-
} else {
74-
// 1D array
75-
const d1 = data as unknown as number[];
76-
shape = [d1.length];
77-
typedData = new Float32Array(d1);
78-
}
79-
} else {
80-
// ArrayLike (already a typed array)
81-
typedData = new Float32Array(data as ArrayLike<number>);
82-
shape = [typedData.length];
83-
}
84-
85-
// Adjust shape to match dims length
86-
while (shape.length < dims.length) {
87-
shape.unshift(1);
88-
}
89-
90-
if (shape.length > dims.length) {
91-
throw new Error(
92-
`Data dimensionality (${shape.length}) exceeds dims length (${dims.length})`,
93-
);
94-
}
95-
96-
// Create in-memory zarr store and array
97-
const store: MemoryStore = new Map();
98-
const root = zarr.root(store);
99-
100-
// Calculate appropriate chunk size
101-
const chunkShape = shape.map((dim) => Math.min(dim, 256));
102-
103-
const zarrArray = await zarr.create(root.resolve("data"), {
104-
shape,
105-
chunk_shape: chunkShape,
106-
data_type: "float32",
107-
fill_value: 0,
108-
});
109-
110-
// Write data to zarr array
111-
await zarr.set(zarrArray, [], {
112-
data: typedData,
113-
shape,
114-
stride: calculateStride(shape),
115-
});
116-
117-
// Create scale and translation records with defaults
118-
const fullScale: Record<string, number> = {};
119-
const fullTranslation: Record<string, number> = {};
120-
121-
for (const dim of dims) {
122-
fullScale[dim] = scale[dim] ?? 1.0;
123-
fullTranslation[dim] = translation[dim] ?? 0.0;
124-
}
125-
126-
return new NgffImage({
127-
data: zarrArray,
128-
dims,
129-
scale: fullScale,
130-
translation: fullTranslation,
131-
name,
132-
axesUnits: undefined,
133-
computedCallbacks: undefined,
134-
});
135-
}
13+
// Re-export for convenience
14+
export { toNgffImage, type ToNgffImageOptions } from "./to_ngff_image.ts";
13615

13716
export interface ToMultiscalesOptions {
13817
scaleFactors?: (Record<string, number> | number)[];
@@ -147,19 +26,40 @@ export interface ToMultiscalesOptions {
14726
* @param options - Configuration options
14827
* @returns Multiscales object
14928
*/
150-
export function toMultiscales(
29+
export async function toMultiscales(
15130
image: NgffImage,
15231
options: ToMultiscalesOptions = {},
153-
): Multiscales {
32+
): Promise<Multiscales> {
15433
const {
15534
scaleFactors = [2, 4],
15635
method = Methods.ITKWASM_GAUSSIAN,
15736
chunks: _chunks,
15837
} = options;
15938

160-
// For now, create only the base image (no actual downsampling)
161-
// This is a simplified implementation for testing metadata functionality
162-
const images = [image];
39+
let images: NgffImage[];
40+
41+
// Check if we should perform actual downsampling
42+
if (
43+
method === Methods.ITKWASM_GAUSSIAN ||
44+
method === Methods.ITKWASM_BIN_SHRINK ||
45+
method === Methods.ITKWASM_LABEL_IMAGE
46+
) {
47+
// Perform actual downsampling using ITK-Wasm
48+
const smoothing = method === Methods.ITKWASM_GAUSSIAN
49+
? "gaussian"
50+
: method === Methods.ITKWASM_BIN_SHRINK
51+
? "bin_shrink"
52+
: "label_image";
53+
54+
images = await downsampleItkWasm(
55+
image,
56+
scaleFactors as (Record<string, number> | number)[],
57+
smoothing,
58+
);
59+
} else {
60+
// Fallback: create only the base image (no actual downsampling)
61+
images = [image];
62+
}
16363

16464
// Create axes from image dimensions
16565
const axes = image.dims.map((dim) => {
@@ -178,14 +78,14 @@ export function toMultiscales(
17878
}
17979
});
18080

181-
// Create datasets
182-
const datasets = [
183-
createDataset(
184-
"0",
185-
image.dims.map((dim) => image.scale[dim]),
186-
image.dims.map((dim) => image.translation[dim]),
187-
),
188-
];
81+
// Create datasets for all images
82+
const datasets = images.map((img, index) => {
83+
return createDataset(
84+
`${index}`,
85+
img.dims.map((dim) => img.scale[dim]),
86+
img.dims.map((dim) => img.translation[dim]),
87+
);
88+
});
18989

19090
// Create metadata with method information
19191
const methodMetadata = getMethodMetadata(method);
@@ -199,12 +99,3 @@ export function toMultiscales(
19999

200100
return createMultiscales(images, metadata, scaleFactors, method);
201101
}
202-
203-
function calculateStride(shape: number[]): number[] {
204-
const stride = new Array(shape.length);
205-
stride[shape.length - 1] = 1;
206-
for (let i = shape.length - 2; i >= 0; i--) {
207-
stride[i] = stride[i + 1] * shape[i + 1];
208-
}
209-
return stride;
210-
}

ts/src/io/to_ngff_image.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import * as zarr from "zarrita";
2+
import { NgffImage } from "../types/ngff_image.ts";
3+
import type { MemoryStore } from "../io/from_ngff_zarr.ts";
4+
5+
export interface ToNgffImageOptions {
6+
dims?: string[];
7+
scale?: Record<string, number>;
8+
translation?: Record<string, number>;
9+
name?: string;
10+
shape?: number[]; // Explicit shape for typed arrays
11+
}
12+
13+
/**
14+
* Convert array data to NgffImage
15+
*
16+
* @param data - Input data as typed array or regular array
17+
* @param options - Configuration options for NgffImage creation
18+
* @returns NgffImage instance
19+
*/
20+
export async function toNgffImage(
21+
data: ArrayLike<number> | number[][] | number[][][],
22+
options: ToNgffImageOptions = {},
23+
): Promise<NgffImage> {
24+
const {
25+
dims = ["y", "x"],
26+
scale = {},
27+
translation = {},
28+
name = "image",
29+
shape: explicitShape,
30+
} = options;
31+
32+
// Determine data shape and create typed array
33+
let typedData: Float32Array | Uint8Array | Uint16Array;
34+
let shape: number[];
35+
36+
if (Array.isArray(data)) {
37+
// Handle multi-dimensional arrays
38+
if (Array.isArray(data[0])) {
39+
if (Array.isArray((data[0] as unknown[])[0])) {
40+
// 3D array
41+
const d3 = data as number[][][];
42+
shape = [d3.length, d3[0].length, d3[0][0].length];
43+
typedData = new Float32Array(shape[0] * shape[1] * shape[2]);
44+
45+
let idx = 0;
46+
for (let i = 0; i < shape[0]; i++) {
47+
for (let j = 0; j < shape[1]; j++) {
48+
for (let k = 0; k < shape[2]; k++) {
49+
typedData[idx++] = d3[i][j][k];
50+
}
51+
}
52+
}
53+
} else {
54+
// 2D array
55+
const d2 = data as number[][];
56+
shape = [d2.length, d2[0].length];
57+
typedData = new Float32Array(shape[0] * shape[1]);
58+
59+
let idx = 0;
60+
for (let i = 0; i < shape[0]; i++) {
61+
for (let j = 0; j < shape[1]; j++) {
62+
typedData[idx++] = d2[i][j];
63+
}
64+
}
65+
}
66+
} else {
67+
// 1D array
68+
const d1 = data as unknown as number[];
69+
shape = [d1.length];
70+
typedData = new Float32Array(d1);
71+
}
72+
} else {
73+
// ArrayLike (already a typed array)
74+
// Use explicit shape if provided, otherwise infer from data length and dims
75+
if (explicitShape) {
76+
shape = [...explicitShape];
77+
} else {
78+
// Try to infer shape - this is a best guess
79+
shape = [data.length];
80+
}
81+
82+
// Preserve the original typed array type
83+
if (data instanceof Uint8Array) {
84+
typedData = data;
85+
} else if (data instanceof Uint16Array) {
86+
typedData = data;
87+
} else {
88+
typedData = new Float32Array(data as ArrayLike<number>);
89+
}
90+
}
91+
92+
// Adjust shape to match dims length if not explicitly provided
93+
if (!explicitShape) {
94+
while (shape.length < dims.length) {
95+
shape.unshift(1);
96+
}
97+
}
98+
99+
if (shape.length !== dims.length) {
100+
throw new Error(
101+
`Shape dimensionality (${shape.length}) must match dims length (${dims.length})`,
102+
);
103+
}
104+
105+
// Create in-memory zarr store and array
106+
const store: MemoryStore = new Map();
107+
const root = zarr.root(store);
108+
109+
// Calculate appropriate chunk size
110+
const chunkShape = shape.map((dim) => Math.min(dim, 256));
111+
112+
const zarrArray = await zarr.create(root.resolve("data"), {
113+
shape,
114+
chunk_shape: chunkShape,
115+
data_type: "float32",
116+
fill_value: 0,
117+
});
118+
119+
// Write data to zarr array
120+
await zarr.set(zarrArray, [], {
121+
data: typedData as Float32Array,
122+
shape,
123+
stride: calculateStride(shape),
124+
});
125+
126+
// Create scale and translation records with defaults
127+
const fullScale: Record<string, number> = {};
128+
const fullTranslation: Record<string, number> = {};
129+
130+
for (const dim of dims) {
131+
fullScale[dim] = scale[dim] ?? 1.0;
132+
fullTranslation[dim] = translation[dim] ?? 0.0;
133+
}
134+
135+
return new NgffImage({
136+
data: zarrArray,
137+
dims,
138+
scale: fullScale,
139+
translation: fullTranslation,
140+
name,
141+
axesUnits: undefined,
142+
computedCallbacks: undefined,
143+
});
144+
}
145+
146+
function calculateStride(shape: number[]): number[] {
147+
const stride = new Array(shape.length);
148+
stride[shape.length - 1] = 1;
149+
for (let i = shape.length - 2; i >= 0; i--) {
150+
stride[i] = stride[i + 1] * shape[i + 1];
151+
}
152+
return stride;
153+
}

0 commit comments

Comments
 (0)