Skip to content

Commit 87539de

Browse files
committed
test(ts): add test_multiscales_metadata_test.ts
1 parent 5311ce2 commit 87539de

File tree

7 files changed

+568
-0
lines changed

7 files changed

+568
-0
lines changed

ts/src/io/to_multiscales.ts

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import * as zarr from "zarrita";
2+
import { NgffImage } from "../types/ngff_image.ts";
3+
import { Multiscales } from "../types/multiscales.ts";
4+
import { Methods } from "../types/methods.ts";
5+
import type { MemoryStore } from "../io/from_ngff_zarr.ts";
6+
import {
7+
createAxis,
8+
createDataset,
9+
createMetadata,
10+
createMultiscales,
11+
} from "../utils/factory.ts";
12+
import { getMethodMetadata } from "../utils/method_metadata.ts";
13+
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+
}
136+
137+
export interface ToMultiscalesOptions {
138+
scaleFactors?: (Record<string, number> | number)[];
139+
method?: Methods;
140+
chunks?: number | number[] | Record<string, number>;
141+
}
142+
143+
/**
144+
* Generate multiple resolution scales for an NgffImage (simplified version for testing)
145+
*
146+
* @param image - Input NgffImage
147+
* @param options - Configuration options
148+
* @returns Multiscales object
149+
*/
150+
export function toMultiscales(
151+
image: NgffImage,
152+
options: ToMultiscalesOptions = {},
153+
): Multiscales {
154+
const {
155+
scaleFactors = [2, 4],
156+
method = Methods.ITKWASM_GAUSSIAN,
157+
chunks: _chunks,
158+
} = options;
159+
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];
163+
164+
// Create axes from image dimensions
165+
const axes = image.dims.map((dim) => {
166+
if (dim === "x" || dim === "y" || dim === "z") {
167+
return createAxis(
168+
dim as "x" | "y" | "z",
169+
"space",
170+
image.axesUnits?.[dim],
171+
);
172+
} else if (dim === "c") {
173+
return createAxis(dim as "c", "channel");
174+
} else if (dim === "t") {
175+
return createAxis(dim as "t", "time");
176+
} else {
177+
throw new Error(`Unsupported dimension: ${dim}`);
178+
}
179+
});
180+
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+
];
189+
190+
// Create metadata with method information
191+
const methodMetadata = getMethodMetadata(method);
192+
const metadata = createMetadata(axes, datasets, image.name);
193+
metadata.type = method;
194+
if (methodMetadata) {
195+
metadata.metadata = methodMetadata;
196+
}
197+
198+
return createMultiscales(images, metadata, scaleFactors, method);
199+
}
200+
201+
function calculateStride(shape: number[]): number[] {
202+
const stride = new Array(shape.length);
203+
stride[shape.length - 1] = 1;
204+
for (let i = shape.length - 2; i >= 0; i--) {
205+
stride[i] = stride[i + 1] * shape[i + 1];
206+
}
207+
return stride;
208+
}

ts/src/io/to_ngff_zarr.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ export async function toNgffZarr(
6868
coordinateTransformations:
6969
multiscales.metadata.coordinateTransformations,
7070
}),
71+
...(multiscales.metadata.type && {
72+
type: multiscales.metadata.type,
73+
}),
74+
...(multiscales.metadata.metadata && {
75+
metadata: multiscales.metadata.metadata,
76+
}),
7177
},
7278
],
7379
...(multiscales.metadata.omero && {

ts/src/mod.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ export {
2525
createMultiscales,
2626
createNgffImage,
2727
} from "./utils/factory.ts";
28+
export { getMethodMetadata } from "./utils/method_metadata.ts";
2829

2930
export * from "./io/from_ngff_zarr.ts";
3031
export * from "./io/to_ngff_zarr.ts";
32+
export * from "./io/to_multiscales.ts";
3133
export type { MemoryStore } from "./io/from_ngff_zarr.ts";
3234
export * from "./io/itk_image_to_ngff_image.ts";
3335
export * from "./io/ngff_image_to_itk_image.ts";

ts/src/schemas/zarr_metadata.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,18 +81,28 @@ export const OmeroSchema: z.ZodObject<{
8181
channels: z.array(OmeroChannelSchema),
8282
});
8383

84+
export const MethodMetadataSchema = z.object({
85+
description: z.string(),
86+
method: z.string(),
87+
version: z.string(),
88+
});
89+
8490
export const MetadataSchema: z.ZodObject<{
8591
axes: z.ZodArray<typeof AxisSchema>;
8692
datasets: z.ZodArray<typeof DatasetSchema>;
8793
coordinateTransformations: z.ZodOptional<z.ZodArray<typeof TransformSchema>>;
8894
omero: z.ZodOptional<typeof OmeroSchema>;
8995
name: z.ZodDefault<z.ZodString>;
9096
version: z.ZodDefault<z.ZodString>;
97+
type: z.ZodOptional<z.ZodString>;
98+
metadata: z.ZodOptional<typeof MethodMetadataSchema>;
9199
}> = z.object({
92100
axes: z.array(AxisSchema),
93101
datasets: z.array(DatasetSchema),
94102
coordinateTransformations: z.array(TransformSchema).optional(),
95103
omero: OmeroSchema.optional(),
96104
name: z.string().default("image"),
97105
version: z.string().default("0.4"),
106+
type: z.string().optional(),
107+
metadata: MethodMetadataSchema.optional(),
98108
});

ts/src/types/zarr_metadata.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,21 @@ export interface Omero {
4646
version?: string;
4747
}
4848

49+
export interface MethodMetadata {
50+
description: string;
51+
method: string;
52+
version: string;
53+
}
54+
4955
export interface Metadata {
5056
axes: Axis[];
5157
datasets: Dataset[];
5258
coordinateTransformations: Transform[] | undefined;
5359
omero: Omero | undefined;
5460
name: string;
5561
version: string;
62+
type?: string;
63+
metadata?: MethodMetadata;
5664
}
5765

5866
export function validateColor(color: string): void {

ts/src/utils/method_metadata.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { Methods } from "../types/methods.ts";
2+
import type { MethodMetadata } from "../types/zarr_metadata.ts";
3+
4+
interface MethodInfo {
5+
description: string;
6+
package: string;
7+
method: string;
8+
}
9+
10+
const METHOD_INFO: Record<string, MethodInfo> = {
11+
itkwasm_gaussian: {
12+
description:
13+
"Smoothed with a discrete gaussian filter to generate a scale space, ideal for intensity images. ITK-Wasm implementation is extremely portable and SIMD accelerated.",
14+
package: "itkwasm-downsample",
15+
method: "itkwasm_downsample.downsample",
16+
},
17+
itkwasm_bin_shrink: {
18+
description:
19+
"Uses the local mean for the output value. WebAssembly build. Fast but generates more artifacts than gaussian-based methods. Appropriate for intensity images.",
20+
package: "itkwasm-downsample",
21+
method: "itkwasm_downsample.downsample_bin_shrink",
22+
},
23+
itkwasm_label_image: {
24+
description:
25+
"A sample is the mode of the linearly weighted local labels in the image. Fast and minimal artifacts. For label images.",
26+
package: "itkwasm-downsample",
27+
method: "itkwasm_downsample.downsample_label_image",
28+
},
29+
};
30+
31+
/**
32+
* Get metadata information for a given downsampling method.
33+
*
34+
* @param method - The downsampling method enum
35+
* @returns MethodMetadata with description, method (package.function), and version
36+
*/
37+
export function getMethodMetadata(method: Methods): MethodMetadata | undefined {
38+
const methodInfo = METHOD_INFO[method];
39+
if (!methodInfo) {
40+
return undefined;
41+
}
42+
43+
// For TypeScript/browser environment, we can't easily get package versions
44+
// so we'll use a placeholder or try to infer from known versions
45+
const version = "unknown";
46+
47+
return {
48+
description: methodInfo.description,
49+
method: methodInfo.method,
50+
version,
51+
};
52+
}

0 commit comments

Comments
 (0)