From 78283c865061b68c47379a5ffeff043e1e5e1a71 Mon Sep 17 00:00:00 2001 From: Xiao Gui Date: Wed, 6 Nov 2024 20:17:53 +0100 Subject: [PATCH] feat: fully support ome zarr --- frontend/src/const.ts | 48 ++++++++++++ frontend/src/state/inputs/selectors.ts | 76 ++++++++++++++++++- .../input-volumes.component.html | 4 +- 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/frontend/src/const.ts b/frontend/src/const.ts index 0ed556a3..8cfaefb3 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -78,6 +78,7 @@ export const XFORM_FILE_TYPE = "https://voluba.apps.hbp.eu/@types/transform" type CoordSpace = Record<'x'|'y'|'z', export_nehuba.Dimension> export const cvtToNm = { + micrometer: (v: number) => v * 1e3, pm: (v: number) => v * 1e-3, nm: (v: number) => v, μm: (v: number) => v * 1e3, @@ -91,6 +92,7 @@ export const cvtToNm = { export type VoxelUnit = keyof typeof cvtToNm export const cvtNmTo: Record number> = { + micrometer: (v: number) => v * 1e-3, pm: v => v * 1e3, nm: v => v, μm: v => v * 1e-3, @@ -231,6 +233,7 @@ type Volume = { 'neuroglancer/precomputed'?: string 'neuroglancer/precomputed/surface'?: string 'neuroglancer/n5'?: string + 'neuroglancer/zarr'?: string } } @@ -261,6 +264,7 @@ export function parseGSUrl(url: string): string { const protocol: Record = { "precomputed://": "neuroglancer/precomputed", "n5://": "neuroglancer/n5", + "zarr://": "neuroglancer/zarr", } export function extractProtocolUrl(ngUrl: string): Volume { @@ -417,3 +421,47 @@ export type EbrainsWorkflowPollResponse = { progresses: EbrainsPublishResult[] user: User } + +export type ZarrAttrs = { + multiscales: { + name: string + version: string + axes: { + name: "x" | "y" | "z" | string + type: "space" | string + units: "micrometer" | string + }[] + datasets: { + coordinateTransformations: { + scale: number[] + type: "scale" | string + }[] + path: string + }[] + }[] +} + +export type BloscCompressor = { + blocksize: 0 | number + clevel: number + cname: "zstd" | string + id: "blosc" + shuffle: 1 | number +} + +export type CompressorType = BloscCompressor + +export type ZarrArray = { + chunks: number[] + dimension_separator: string + + dtype: " state[nameSpace] as LocalState; @@ -55,6 +55,7 @@ const incInfoPipe = pipe( const volume = incoming.volumes[0] const precomputedUrl = volume.providers["neuroglancer/precomputed"] const n5Url = volume.providers["neuroglancer/n5"] + const zarrUrl = volume.providers["neuroglancer/zarr"] if (!!precomputedUrl) { return from( fetch(`${precomputedUrl}/info`).then(res => res.json() as Promise<{ scales: { size: number[], resolution: number[] }[] }>) @@ -82,6 +83,58 @@ const incInfoPipe = pipe( }) ) } + if (!!zarrUrl) { + + return from( + fetch(parseGSUrl(`${zarrUrl}/.zattrs`)).then(res => res.json() as Promise) + ).pipe( + switchMap(zarrattr => { + const { multiscales } = zarrattr + if (multiscales.length !== 1) { + return throwError(() => new Error(`Can only deal with one dataset, but got ${multiscales.length}`)) + } + const scale = multiscales[0] + const { axes, datasets } = scale + if (axes.length !== 3) { + return throwError(() => new Error(`Can only deal with axes with length 3`)) + } + for (const { type, units } of axes){ + if (type !== "space") { + return throwError(() => new Error(`Can only deal with space axes`)) + } + } + if (datasets.length === 0) { + return throwError(() => new Error(`must have at least one dataset`)) + } + const dataset = datasets[0] + const { coordinateTransformations, path } = dataset + if (coordinateTransformations.length !== 1) { + return throwError(() => new Error(`Can only deal with one coordate transforms, but got ${coordinateTransformations.length}`)) + } + const coordinateTransformation = coordinateTransformations[0] + const { scale: _scale } = coordinateTransformation + + return from( + fetch(parseGSUrl(`${zarrUrl}/${path}/.zarray`)).then(res => res.json() as Promise) + ).pipe( + map(({ shape }) => { + return { + "neuroglancer/zarr": { + axes, + datasets: [ + { + scale: _scale, + shape + } + ] + } + } as IncInfo + }) + ) + + }) + ) + } return throwError(() => new Error(`volume is neither precomputed nor n5`)) }) ) @@ -117,6 +170,20 @@ export const incVoxelSize = pipe( return cvtToNm[unit](resolution[idx]) }) } + if (!!v["neuroglancer/zarr"]) { + const { datasets, axes } = v["neuroglancer/zarr"] + const dataset = datasets[0] + const { scale, shape } = dataset + return (scale as number[]).map((v, idx: number) => { + const unit = axes[idx].units + if (!(unit in cvtToNm)) { + console.warn(`${unit} cannot be converted. Using 1 as default`) + return 1 + } + return cvtToNm[unit as keyof typeof cvtToNm](v) + }) + + } console.warn(`v voxel size cannot be found: ${v}`) return null }) @@ -139,7 +206,12 @@ export const incVoxelExtents = pipe( const { dimensions } = s0 as { blockSize: number[], dimensions: number[] } return dimensions } - console.warn(`v voxel size cannot be found: ${v}`) + if (!!v["neuroglancer/zarr"]) { + const { datasets } = v["neuroglancer/zarr"] + const dataset = datasets[0] + return dataset.shape as number[] + } + console.warn(`voxel extent cannot be found: ${v}`) return null }) ) diff --git a/frontend/src/views/input-volumes/input-volumes.component.html b/frontend/src/views/input-volumes/input-volumes.component.html index 6921420c..c8feb2b8 100644 --- a/frontend/src/views/input-volumes/input-volumes.component.html +++ b/frontend/src/views/input-volumes/input-volumes.component.html @@ -171,10 +171,10 @@ - Neuroglancer URL, must start with precomputed:// or n5:// + Neuroglancer URL, must start with precomputed://, zarr:// or n5:// - Malformed URL. Must start with precomputed:// or n5:// + Malformed URL. Must start with precomputed://, zarr:// or n5://