From 272ae8b26c64f562d2e2056e33ffa7ca9573a023 Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 19 Nov 2025 14:02:03 +0000 Subject: [PATCH 01/10] ZarrPixelSource adjusts z-index off smaller arrays with respect to originalSizeZ --- src/ZarrPixelSource.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/ZarrPixelSource.ts b/src/ZarrPixelSource.ts index 115a4b01..66d8f2eb 100644 --- a/src/ZarrPixelSource.ts +++ b/src/ZarrPixelSource.ts @@ -19,6 +19,7 @@ type VivPixelData = { export class ZarrPixelSource implements viv.PixelSource> { readonly labels: viv.Labels>; readonly tileSize: number; + originalSizeZ: number; readonly dtype: viv.SupportedDtype; readonly #arr: zarr.Array; readonly #transform: ( @@ -43,6 +44,7 @@ export class ZarrPixelSource implements viv.PixelSource> { this.#arr = arr; this.labels = options.labels; this.tileSize = options.tileSize; + this.originalSizeZ = this.shape[this.labels.indexOf('z')]; /** * Some `zarrita` data types are not supported by Viv and require casting. * @@ -64,6 +66,10 @@ export class ZarrPixelSource implements viv.PixelSource> { } } + setOriginalSizeZ(sizeZ: number) { + this.originalSizeZ = sizeZ; + } + get #width() { const lastIndex = this.shape.length - 1; return this.shape[this.labels.indexOf("c") === lastIndex ? lastIndex - 1 : lastIndex]; @@ -103,14 +109,21 @@ export class ZarrPixelSource implements viv.PixelSource> { signal?: AbortSignal; }): Promise { const { x, y, selection, signal } = options; - return this.#fetchData({ - selection: buildZarrSelection(selection, { + let zarrSelection = buildZarrSelection(selection, { labels: this.labels, slices: { x: zarr.slice(x * this.tileSize, Math.min((x + 1) * this.tileSize, this.#width)), y: zarr.slice(y * this.tileSize, Math.min((y + 1) * this.tileSize, this.#height)), }, - }), + }); + // If we know the original sizeZ, adjust the z index of this array to account for downsampling + let zIndex = this.labels.indexOf('z'); + if (this.originalSizeZ > 0 && zarrSelection[zIndex] instanceof Number) { + let z = zarrSelection[zIndex] as number; + zarrSelection[zIndex] = Math.floor(z * this.shape[zIndex] / this.originalSizeZ); + } + return this.#fetchData({ + selection: zarrSelection, signal, }); } From f725eeaac60abdf858fdb1c134d920aaf9786dbd Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 19 Nov 2025 14:04:13 +0000 Subject: [PATCH 02/10] loadOmeMultiscales() sets originalSizeZ for all ZarrPixelSources in pyramid --- src/ome.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ome.ts b/src/ome.ts index 6fcf6bfb..66b1607f 100644 --- a/src/ome.ts +++ b/src/ome.ts @@ -267,6 +267,9 @@ export async function loadOmeMultiscales( meta = await defaultMeta(lowresSource, axis_labels); } const loader = data.map((arr) => new ZarrPixelSource(arr, { labels: axis_labels, tileSize })); + // Set originalSizeZ for all ZarrPixelSource arrays so they can adjust for Z downsampling + let originalSizeZ = loader[0].shape[axis_labels.indexOf("z")]; + loader.forEach((l) => l.setOriginalSizeZ(originalSizeZ)); const labels = await resolveOmeLabelsFromMultiscales(grp); return { loader: loader, From 19d87d25837b100da17cf8c7596c4340a9c9308e Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 19 Nov 2025 14:22:05 +0000 Subject: [PATCH 03/10] linting fixes --- src/ZarrPixelSource.ts | 18 +++++++++--------- src/ome.ts | 4 +++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/ZarrPixelSource.ts b/src/ZarrPixelSource.ts index 66d8f2eb..5e2add62 100644 --- a/src/ZarrPixelSource.ts +++ b/src/ZarrPixelSource.ts @@ -44,7 +44,7 @@ export class ZarrPixelSource implements viv.PixelSource> { this.#arr = arr; this.labels = options.labels; this.tileSize = options.tileSize; - this.originalSizeZ = this.shape[this.labels.indexOf('z')]; + this.originalSizeZ = this.shape[this.labels.indexOf("z")]; /** * Some `zarrita` data types are not supported by Viv and require casting. * @@ -110,17 +110,17 @@ export class ZarrPixelSource implements viv.PixelSource> { }): Promise { const { x, y, selection, signal } = options; let zarrSelection = buildZarrSelection(selection, { - labels: this.labels, - slices: { - x: zarr.slice(x * this.tileSize, Math.min((x + 1) * this.tileSize, this.#width)), - y: zarr.slice(y * this.tileSize, Math.min((y + 1) * this.tileSize, this.#height)), - }, - }); + labels: this.labels, + slices: { + x: zarr.slice(x * this.tileSize, Math.min((x + 1) * this.tileSize, this.#width)), + y: zarr.slice(y * this.tileSize, Math.min((y + 1) * this.tileSize, this.#height)), + }, + }); // If we know the original sizeZ, adjust the z index of this array to account for downsampling - let zIndex = this.labels.indexOf('z'); + let zIndex = this.labels.indexOf("z"); if (this.originalSizeZ > 0 && zarrSelection[zIndex] instanceof Number) { let z = zarrSelection[zIndex] as number; - zarrSelection[zIndex] = Math.floor(z * this.shape[zIndex] / this.originalSizeZ); + zarrSelection[zIndex] = Math.floor((z * this.shape[zIndex]) / this.originalSizeZ); } return this.#fetchData({ selection: zarrSelection, diff --git a/src/ome.ts b/src/ome.ts index 66b1607f..ce6d1dea 100644 --- a/src/ome.ts +++ b/src/ome.ts @@ -269,7 +269,9 @@ export async function loadOmeMultiscales( const loader = data.map((arr) => new ZarrPixelSource(arr, { labels: axis_labels, tileSize })); // Set originalSizeZ for all ZarrPixelSource arrays so they can adjust for Z downsampling let originalSizeZ = loader[0].shape[axis_labels.indexOf("z")]; - loader.forEach((l) => l.setOriginalSizeZ(originalSizeZ)); + for (const l of loader) { + l.setOriginalSizeZ(originalSizeZ); + } const labels = await resolveOmeLabelsFromMultiscales(grp); return { loader: loader, From 3bc25f2596c6f261cd2c1945e31c419bd482e7d1 Mon Sep 17 00:00:00 2001 From: Alex Surtees Date: Tue, 9 Dec 2025 09:52:40 +0000 Subject: [PATCH 04/10] Document and minor refactor --- src/ZarrPixelSource.ts | 49 ++++++++++++++++++++++++++++++++++++++---- src/ome.ts | 3 +++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/ZarrPixelSource.ts b/src/ZarrPixelSource.ts index 5ecc74a7..5efe7910 100644 --- a/src/ZarrPixelSource.ts +++ b/src/ZarrPixelSource.ts @@ -8,6 +8,7 @@ type Slice = ReturnType; const X_AXIS_NAME = "x"; const Y_AXIS_NAME = "y"; +const Z_AXIS_NAME = 'z'; const RGBA_CHANNEL_AXIS_NAME = "_c"; type VivPixelData = { @@ -16,7 +17,29 @@ type VivPixelData = { height: number; }; +/** + * Configuration options for ZarrPixelSource + */ +interface ZarrPixelSourceOptions { + /** + * Array dimension labels e.g. [x, y, z, c] + */ + labels: viv.Labels>; + /** + * The size of each tile, in pixels + */ + tileSize: number; + /** + * Additional meta options + */ + meta?: viv.PixelSourceMeta +} + +/** + * Class for loading pixel data from a .zarr source + */ export class ZarrPixelSource implements viv.PixelSource> { + readonly labels: viv.Labels>; readonly tileSize: number; originalSizeZ: number; @@ -37,9 +60,14 @@ export class ZarrPixelSource implements viv.PixelSource> { }; }> = []; + /** + * Create a ZarrPixelSource + * @param {zarr.Array} arr - A Zarr array object + * @param {ZarrPixelSourceOptions} options - An object defining options + */ constructor( arr: zarr.Array, - options: { labels: viv.Labels>; tileSize: number; meta?: viv.PixelSourceMeta }, + options: ZarrPixelSourceOptions, ) { assert(arr.is("number") || arr.is("bigint"), `Unsupported viv dtype: ${arr.dtype}`); this.#arr = arr; @@ -86,6 +114,7 @@ export class ZarrPixelSource implements viv.PixelSource> { return this.#arr.shape; } + async getRaster(options: { selection: viv.PixelSourceSelection> | Array; signal?: AbortSignal; @@ -104,6 +133,14 @@ export class ZarrPixelSource implements viv.PixelSource> { // no-op } + hasZIndex(): boolean { + return this.labels.includes('z') + } + + recalculateZSelection(z: number, zIndex: number): number { + return Math.floor((z * this.shape[zIndex]) / this.originalSizeZ); + } + async getTile(options: { x: number; y: number; @@ -119,10 +156,11 @@ export class ZarrPixelSource implements viv.PixelSource> { }, }); // If we know the original sizeZ, adjust the z index of this array to account for downsampling + let zIndex = this.labels.indexOf("z"); - if (this.originalSizeZ > 0 && zarrSelection[zIndex] instanceof Number) { - let z = zarrSelection[zIndex] as number; - zarrSelection[zIndex] = Math.floor((z * this.shape[zIndex]) / this.originalSizeZ); + if (this.hasZIndex()) { + let currentZSelection = zarrSelection[zIndex] as number; + zarrSelection[zIndex] = this.recalculateZSelection(currentZSelection, zIndex) } return this.#fetchData({ selection: zarrSelection, @@ -161,6 +199,7 @@ export class ZarrPixelSource implements viv.PixelSource> { } } + function buildZarrSelection( baseSelection: Record | Array, options: { @@ -193,3 +232,5 @@ function capitalize(s: T): Capitalize { // @ts-expect-error - TypeScript can't verify that the return type is correct return s[0].toUpperCase() + s.slice(1); } + + diff --git a/src/ome.ts b/src/ome.ts index 6c626d24..76c4d09d 100644 --- a/src/ome.ts +++ b/src/ome.ts @@ -248,6 +248,9 @@ export async function loadPlate( return sourceData; } +/** + * Load a multiscale OME-NGFF image + */ export async function loadOmeMultiscales( config: ImageLayerConfig, grp: zarr.Group, From e5ac75b6b4d790ae7d6aaeec0f0eb7ac0e67b991 Mon Sep 17 00:00:00 2001 From: Alex Surtees Date: Wed, 10 Dec 2025 12:17:08 +0000 Subject: [PATCH 05/10] Remove unused variable --- src/ZarrPixelSource.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ZarrPixelSource.ts b/src/ZarrPixelSource.ts index 5efe7910..f616b0d1 100644 --- a/src/ZarrPixelSource.ts +++ b/src/ZarrPixelSource.ts @@ -8,7 +8,6 @@ type Slice = ReturnType; const X_AXIS_NAME = "x"; const Y_AXIS_NAME = "y"; -const Z_AXIS_NAME = 'z'; const RGBA_CHANNEL_AXIS_NAME = "_c"; type VivPixelData = { From 05a4e4649e38a3ef77a2fd913629c7fede2b3deb Mon Sep 17 00:00:00 2001 From: Alex Surtees Date: Wed, 10 Dec 2025 12:22:00 +0000 Subject: [PATCH 06/10] Clean up documentation --- src/ZarrPixelSource.ts | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/ZarrPixelSource.ts b/src/ZarrPixelSource.ts index f616b0d1..73ba9e29 100644 --- a/src/ZarrPixelSource.ts +++ b/src/ZarrPixelSource.ts @@ -16,27 +16,17 @@ type VivPixelData = { height: number; }; -/** - * Configuration options for ZarrPixelSource - */ +/** Configuration options for ZarrPixelSource */ interface ZarrPixelSourceOptions { - /** - * Array dimension labels e.g. [x, y, z, c] - */ + /** Array dimension labels e.g. [x, y, z, c] */ labels: viv.Labels>; - /** - * The size of each tile, in pixels - */ + /** The size of each tile, in pixels */ tileSize: number; - /** - * Additional meta options - */ + /**Additional meta options */ meta?: viv.PixelSourceMeta } -/** - * Class for loading pixel data from a .zarr source - */ +/** Class for loading pixel data from a .zarr source */ export class ZarrPixelSource implements viv.PixelSource> { readonly labels: viv.Labels>; From b995fbc7f413be7ed0e0100fb4f53e5ecf52474c Mon Sep 17 00:00:00 2001 From: Alex Surtees Date: Wed, 10 Dec 2025 12:36:48 +0000 Subject: [PATCH 07/10] Linting --- src/ZarrPixelSource.ts | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/ZarrPixelSource.ts b/src/ZarrPixelSource.ts index 73ba9e29..8e2d57c2 100644 --- a/src/ZarrPixelSource.ts +++ b/src/ZarrPixelSource.ts @@ -23,12 +23,11 @@ interface ZarrPixelSourceOptions { /** The size of each tile, in pixels */ tileSize: number; /**Additional meta options */ - meta?: viv.PixelSourceMeta + meta?: viv.PixelSourceMeta; } /** Class for loading pixel data from a .zarr source */ export class ZarrPixelSource implements viv.PixelSource> { - readonly labels: viv.Labels>; readonly tileSize: number; originalSizeZ: number; @@ -51,13 +50,10 @@ export class ZarrPixelSource implements viv.PixelSource> { /** * Create a ZarrPixelSource - * @param {zarr.Array} arr - A Zarr array object - * @param {ZarrPixelSourceOptions} options - An object defining options + * @param {zarr.Array} arr - A Zarr array object + * @param {ZarrPixelSourceOptions} options - An object defining options */ - constructor( - arr: zarr.Array, - options: ZarrPixelSourceOptions, - ) { + constructor(arr: zarr.Array, options: ZarrPixelSourceOptions) { assert(arr.is("number") || arr.is("bigint"), `Unsupported viv dtype: ${arr.dtype}`); this.#arr = arr; this.labels = options.labels; @@ -103,7 +99,6 @@ export class ZarrPixelSource implements viv.PixelSource> { return this.#arr.shape; } - async getRaster(options: { selection: viv.PixelSourceSelection> | Array; signal?: AbortSignal; @@ -123,7 +118,7 @@ export class ZarrPixelSource implements viv.PixelSource> { } hasZIndex(): boolean { - return this.labels.includes('z') + return this.labels.includes("z"); } recalculateZSelection(z: number, zIndex: number): number { @@ -149,7 +144,7 @@ export class ZarrPixelSource implements viv.PixelSource> { let zIndex = this.labels.indexOf("z"); if (this.hasZIndex()) { let currentZSelection = zarrSelection[zIndex] as number; - zarrSelection[zIndex] = this.recalculateZSelection(currentZSelection, zIndex) + zarrSelection[zIndex] = this.recalculateZSelection(currentZSelection, zIndex); } return this.#fetchData({ selection: zarrSelection, @@ -188,7 +183,6 @@ export class ZarrPixelSource implements viv.PixelSource> { } } - function buildZarrSelection( baseSelection: Record | Array, options: { @@ -221,5 +215,3 @@ function capitalize(s: T): Capitalize { // @ts-expect-error - TypeScript can't verify that the return type is correct return s[0].toUpperCase() + s.slice(1); } - - From aaea6c32700d4e6f5690d47133db04d8a4bc1197 Mon Sep 17 00:00:00 2001 From: Alex Surtees Date: Thu, 11 Dec 2025 14:56:16 +0000 Subject: [PATCH 08/10] Tidy up and document --- src/ZarrPixelSource.ts | 19 +++++++++++++++---- src/ome.ts | 18 +++++++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/ZarrPixelSource.ts b/src/ZarrPixelSource.ts index 8e2d57c2..f2306676 100644 --- a/src/ZarrPixelSource.ts +++ b/src/ZarrPixelSource.ts @@ -24,13 +24,15 @@ interface ZarrPixelSourceOptions { tileSize: number; /**Additional meta options */ meta?: viv.PixelSourceMeta; + /**The original (e.g. maximum resolution) number of z-slices for images which have been downsampled in the z-index*/ + originalSizeZ?: number; } /** Class for loading pixel data from a .zarr source */ export class ZarrPixelSource implements viv.PixelSource> { readonly labels: viv.Labels>; readonly tileSize: number; - originalSizeZ: number; + readonly originalSizeZ?: number; readonly dtype: viv.SupportedDtype; readonly meta?: viv.PixelSourceMeta; readonly #arr: zarr.Array; @@ -58,7 +60,7 @@ export class ZarrPixelSource implements viv.PixelSource> { this.#arr = arr; this.labels = options.labels; this.tileSize = options.tileSize; - this.originalSizeZ = this.shape[this.labels.indexOf("z")]; + this.originalSizeZ = options.originalSizeZ; this.meta = options.meta; /** * Some `zarrita` data types are not supported by Viv and require casting. @@ -121,8 +123,17 @@ export class ZarrPixelSource implements viv.PixelSource> { return this.labels.includes("z"); } - recalculateZSelection(z: number, zIndex: number): number { - return Math.floor((z * this.shape[zIndex]) / this.originalSizeZ); + /** Recalculate the Z selection for images that are downsampled in the Z axis when the resolution is not the original resolution + * @param{number} currentZSelection - The current z selection (i.e. at the original resolution) + * @param{number} zIndex - The index corresponding to the z-axis in the shape array + * @returns{number} The new zIndex, adjusting for any changes from the original resolution + * */ + recalculateZSelection(currentZSelection: number, zIndex: number): number { + if (this.originalSizeZ && this.originalSizeZ !== this.shape[zIndex]) { + console.log("Recalculating z-index"); + return Math.floor((currentZSelection * this.shape[zIndex]) / this.originalSizeZ); + } + return currentZSelection; } async getTile(options: { diff --git a/src/ome.ts b/src/ome.ts index 76c4d09d..9bbab76d 100644 --- a/src/ome.ts +++ b/src/ome.ts @@ -248,6 +248,14 @@ export async function loadPlate( return sourceData; } +function isDownsampledZ( + data: Array>, + zIndex: number, + originalSizeZ: number, +): boolean { + return !data.every((element) => element.shape[zIndex] === originalSizeZ); +} + /** * Load a multiscale OME-NGFF image */ @@ -270,6 +278,9 @@ export async function loadOmeMultiscales( const lowresSource = new ZarrPixelSource(lowresArray, { labels: axis_labels, tileSize }); meta = await defaultMeta(lowresSource, axis_labels); } + + const originalSizeZ = data[0].shape[axis_labels.indexOf("z")]; + const zDownsampled = isDownsampledZ(data, axis_labels.indexOf("z"), originalSizeZ); const physicalSizes = utils.getPhysicalSizes(utils.resolveAttrs(attrs)); const loader = data.map( (arr, i) => @@ -277,13 +288,10 @@ export async function loadOmeMultiscales( labels: axis_labels, tileSize, ...(i === 0 ? { meta: { physicalSizes } } : {}), + originalSizeZ: zDownsampled ? originalSizeZ : undefined, }), ); - // Set originalSizeZ for all ZarrPixelSource arrays so they can adjust for Z downsampling - let originalSizeZ = loader[0].shape[axis_labels.indexOf("z")]; - for (const l of loader) { - l.setOriginalSizeZ(originalSizeZ); - } + const labels = await resolveOmeLabelsFromMultiscales(grp); return { loader: loader, From 550f3695fc805b395bb1f43e19e1b1996d38aed3 Mon Sep 17 00:00:00 2001 From: Alex Surtees Date: Thu, 11 Dec 2025 14:59:13 +0000 Subject: [PATCH 09/10] Remove console log --- src/ZarrPixelSource.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ZarrPixelSource.ts b/src/ZarrPixelSource.ts index f2306676..5f148626 100644 --- a/src/ZarrPixelSource.ts +++ b/src/ZarrPixelSource.ts @@ -123,14 +123,13 @@ export class ZarrPixelSource implements viv.PixelSource> { return this.labels.includes("z"); } - /** Recalculate the Z selection for images that are downsampled in the Z axis when the resolution is not the original resolution + /** Recalculate the Z selection for images that are downsampled in the Z axis when the selected resolution is not the original one (i.e. highest resolution) * @param{number} currentZSelection - The current z selection (i.e. at the original resolution) * @param{number} zIndex - The index corresponding to the z-axis in the shape array * @returns{number} The new zIndex, adjusting for any changes from the original resolution * */ recalculateZSelection(currentZSelection: number, zIndex: number): number { if (this.originalSizeZ && this.originalSizeZ !== this.shape[zIndex]) { - console.log("Recalculating z-index"); return Math.floor((currentZSelection * this.shape[zIndex]) / this.originalSizeZ); } return currentZSelection; From f2ad48f248b1cb5de9372f32856c83a8bc3c6dc5 Mon Sep 17 00:00:00 2001 From: Alex Surtees Date: Thu, 11 Dec 2025 15:01:24 +0000 Subject: [PATCH 10/10] Remove deprecated function --- src/ZarrPixelSource.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/ZarrPixelSource.ts b/src/ZarrPixelSource.ts index 5f148626..3dc8fed9 100644 --- a/src/ZarrPixelSource.ts +++ b/src/ZarrPixelSource.ts @@ -83,10 +83,6 @@ export class ZarrPixelSource implements viv.PixelSource> { } } - setOriginalSizeZ(sizeZ: number) { - this.originalSizeZ = sizeZ; - } - get #width() { const lastIndex = this.shape.length - 1; return this.shape[this.labels.indexOf("c") === lastIndex ? lastIndex - 1 : lastIndex];