Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 208 additions & 0 deletions ts/src/io/to_multiscales.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import * as zarr from "zarrita";
import { NgffImage } from "../types/ngff_image.ts";
import { Multiscales } from "../types/multiscales.ts";
import { Methods } from "../types/methods.ts";
import type { MemoryStore } from "../io/from_ngff_zarr.ts";
import {
createAxis,
createDataset,
createMetadata,
createMultiscales,
} from "../utils/factory.ts";
import { getMethodMetadata } from "../utils/method_metadata.ts";

export interface ToNgffImageOptions {
dims?: string[];
scale?: Record<string, number>;
translation?: Record<string, number>;
name?: string;
}

/**
* Convert array data to NgffImage
*
* @param data - Input data as typed array or regular array
* @param options - Configuration options for NgffImage creation
* @returns NgffImage instance
*/
export async function toNgffImage(
data: ArrayLike<number> | number[][] | number[][][],
options: ToNgffImageOptions = {},
): Promise<NgffImage> {
const {
dims = ["y", "x"],
scale = {},
translation = {},
name = "image",
} = options;

// Determine data shape and create typed array
let typedData: Float32Array;
let shape: number[];

if (Array.isArray(data)) {
// Handle multi-dimensional arrays
if (Array.isArray(data[0])) {
if (Array.isArray((data[0] as unknown[])[0])) {
// 3D array
const d3 = data as number[][][];
shape = [d3.length, d3[0].length, d3[0][0].length];
typedData = new Float32Array(shape[0] * shape[1] * shape[2]);

let idx = 0;
for (let i = 0; i < shape[0]; i++) {
for (let j = 0; j < shape[1]; j++) {
for (let k = 0; k < shape[2]; k++) {
typedData[idx++] = d3[i][j][k];
}
}
}
} else {
// 2D array
const d2 = data as number[][];
shape = [d2.length, d2[0].length];
typedData = new Float32Array(shape[0] * shape[1]);

let idx = 0;
for (let i = 0; i < shape[0]; i++) {
for (let j = 0; j < shape[1]; j++) {
typedData[idx++] = d2[i][j];
}
}
}
} else {
// 1D array
const d1 = data as unknown as number[];
shape = [d1.length];
typedData = new Float32Array(d1);
}
} else {
// ArrayLike (already a typed array)
typedData = new Float32Array(data as ArrayLike<number>);
shape = [typedData.length];
}

// Adjust shape to match dims length
while (shape.length < dims.length) {
shape.unshift(1);
}

if (shape.length > dims.length) {
throw new Error(
`Data dimensionality (${shape.length}) exceeds dims length (${dims.length})`,
);
}

// Create in-memory zarr store and array
const store: MemoryStore = new Map();
const root = zarr.root(store);

// Calculate appropriate chunk size
const chunkShape = shape.map((dim) => Math.min(dim, 256));

const zarrArray = await zarr.create(root.resolve("data"), {
shape,
chunk_shape: chunkShape,
data_type: "float32",
fill_value: 0,
});

// Write data to zarr array
await zarr.set(zarrArray, [], {
data: typedData,
shape,
stride: calculateStride(shape),
});

// Create scale and translation records with defaults
const fullScale: Record<string, number> = {};
const fullTranslation: Record<string, number> = {};

for (const dim of dims) {
fullScale[dim] = scale[dim] ?? 1.0;
fullTranslation[dim] = translation[dim] ?? 0.0;
}

return new NgffImage({
data: zarrArray,
dims,
scale: fullScale,
translation: fullTranslation,
name,
axesUnits: undefined,
computedCallbacks: undefined,
});
}

export interface ToMultiscalesOptions {
scaleFactors?: (Record<string, number> | number)[];
method?: Methods;
chunks?: number | number[] | Record<string, number>;
}

/**
* Generate multiple resolution scales for an NgffImage (simplified version for testing)
*
* @param image - Input NgffImage
* @param options - Configuration options
* @returns Multiscales object
*/
export function toMultiscales(
image: NgffImage,
options: ToMultiscalesOptions = {},
): Multiscales {
const {
scaleFactors = [2, 4],
method = Methods.ITKWASM_GAUSSIAN,
chunks: _chunks,
} = options;

// For now, create only the base image (no actual downsampling)
// This is a simplified implementation for testing metadata functionality
const images = [image];

// Create axes from image dimensions
const axes = image.dims.map((dim) => {
if (dim === "x" || dim === "y" || dim === "z") {
return createAxis(
dim as "x" | "y" | "z",
"space",
image.axesUnits?.[dim],
);
} else if (dim === "c") {
return createAxis(dim as "c", "channel");
} else if (dim === "t") {
return createAxis(dim as "t", "time");
} else {
throw new Error(`Unsupported dimension: ${dim}`);
}
});

// Create datasets
const datasets = [
createDataset(
"0",
image.dims.map((dim) => image.scale[dim]),
image.dims.map((dim) => image.translation[dim]),
),
];

// Create metadata with method information
const methodMetadata = getMethodMetadata(method);
const metadata = createMetadata(axes, datasets, image.name);
metadata.type = method;
if (methodMetadata) {
metadata.metadata = methodMetadata;
}

return createMultiscales(images, metadata, scaleFactors, method);
}

function calculateStride(shape: number[]): number[] {
const stride = new Array(shape.length);
stride[shape.length - 1] = 1;
for (let i = shape.length - 2; i >= 0; i--) {
stride[i] = stride[i + 1] * shape[i + 1];
}
return stride;
}
6 changes: 6 additions & 0 deletions ts/src/io/to_ngff_zarr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ export async function toNgffZarr(
coordinateTransformations:
multiscales.metadata.coordinateTransformations,
}),
...(multiscales.metadata.type && {
type: multiscales.metadata.type,
}),
...(multiscales.metadata.metadata && {
metadata: multiscales.metadata.metadata,
}),
},
],
...(multiscales.metadata.omero && {
Expand Down
2 changes: 2 additions & 0 deletions ts/src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ export {
createMultiscales,
createNgffImage,
} from "./utils/factory.ts";
export { getMethodMetadata } from "./utils/method_metadata.ts";

export * from "./io/from_ngff_zarr.ts";
export * from "./io/to_ngff_zarr.ts";
export * from "./io/to_multiscales.ts";
export type { MemoryStore } from "./io/from_ngff_zarr.ts";
export * from "./io/itk_image_to_ngff_image.ts";
export * from "./io/ngff_image_to_itk_image.ts";
Expand Down
10 changes: 10 additions & 0 deletions ts/src/schemas/zarr_metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,28 @@ export const OmeroSchema: z.ZodObject<{
channels: z.array(OmeroChannelSchema),
});

export const MethodMetadataSchema = z.object({
description: z.string(),
method: z.string(),
version: z.string(),
});

export const MetadataSchema: z.ZodObject<{
axes: z.ZodArray<typeof AxisSchema>;
datasets: z.ZodArray<typeof DatasetSchema>;
coordinateTransformations: z.ZodOptional<z.ZodArray<typeof TransformSchema>>;
omero: z.ZodOptional<typeof OmeroSchema>;
name: z.ZodDefault<z.ZodString>;
version: z.ZodDefault<z.ZodString>;
type: z.ZodOptional<z.ZodString>;
metadata: z.ZodOptional<typeof MethodMetadataSchema>;
}> = z.object({
axes: z.array(AxisSchema),
datasets: z.array(DatasetSchema),
coordinateTransformations: z.array(TransformSchema).optional(),
omero: OmeroSchema.optional(),
name: z.string().default("image"),
version: z.string().default("0.4"),
type: z.string().optional(),
metadata: MethodMetadataSchema.optional(),
});
8 changes: 8 additions & 0 deletions ts/src/types/zarr_metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,21 @@ export interface Omero {
version?: string;
}

export interface MethodMetadata {
description: string;
method: string;
version: string;
}

export interface Metadata {
axes: Axis[];
datasets: Dataset[];
coordinateTransformations: Transform[] | undefined;
omero: Omero | undefined;
name: string;
version: string;
type?: string;
metadata?: MethodMetadata;
}

export function validateColor(color: string): void {
Expand Down
52 changes: 52 additions & 0 deletions ts/src/utils/method_metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Methods } from "../types/methods.ts";
import type { MethodMetadata } from "../types/zarr_metadata.ts";

interface MethodInfo {
description: string;
package: string;
method: string;
}

const METHOD_INFO: Record<string, MethodInfo> = {
itkwasm_gaussian: {
description:
"Smoothed with a discrete gaussian filter to generate a scale space, ideal for intensity images. ITK-Wasm implementation is extremely portable and SIMD accelerated.",
package: "itkwasm-downsample",
method: "itkwasm_downsample.downsample",
},
itkwasm_bin_shrink: {
description:
"Uses the local mean for the output value. WebAssembly build. Fast but generates more artifacts than gaussian-based methods. Appropriate for intensity images.",
package: "itkwasm-downsample",
method: "itkwasm_downsample.downsample_bin_shrink",
},
itkwasm_label_image: {
description:
"A sample is the mode of the linearly weighted local labels in the image. Fast and minimal artifacts. For label images.",
package: "itkwasm-downsample",
method: "itkwasm_downsample.downsample_label_image",
},
};

/**
* Get metadata information for a given downsampling method.
*
* @param method - The downsampling method enum
* @returns MethodMetadata with description, method (package.function), and version
*/
export function getMethodMetadata(method: Methods): MethodMetadata | undefined {
const methodInfo = METHOD_INFO[method];
if (!methodInfo) {
return undefined;
}

// For TypeScript/browser environment, we can't easily get package versions
// so we'll use a placeholder or try to infer from known versions
const version = "unknown";

return {
description: methodInfo.description,
method: methodInfo.method,
version,
};
}
Loading
Loading