Skip to content
Draft
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
4 changes: 4 additions & 0 deletions fixtures/13457537.zarr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
source: https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.4/idr0101A/13457537.zarr
name: 13457537.zarr
features:
multiscale: true
4 changes: 4 additions & 0 deletions fixtures/13457539.zarr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
source: https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.4/idr0101A/13457539.zarr
name: 13457539.zarr
features:
multiscale: true
2 changes: 2 additions & 0 deletions fixtures/resolution_transformationsv0.5.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
source: https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.4/idr0101A/13457537.zarr
name: 13457537.zarr
2 changes: 2 additions & 0 deletions fixtures/transformationsv0.5.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
source: https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.4/idr0101A/13457539.zarr
name: 13457539.zarr
110 changes: 110 additions & 0 deletions viewer/src/coordinate-transformations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Matrix4 } from "math.gl";
import { getNgffAxes, isMultiscales, assert } from "./utils";
import * as zarr from "zarrita";
import type * as viv from "@vivjs/types";


/**
* Convert an array of coordinateTransformations objects to a 16-element
* plain JS array using Matrix4 linear algebra transformation functions.
*
* Adapted from Vitessce: https://github.com/vitessce/vitessce/blob/c267ebecab1824dae68d6f2640a6c5ce7250efbb/packages/utils/spatial-utils/src/spatial.js#L403-L524
*
* @param coordinateTransformations List of objects matching the OME-NGFF v0.4 coordinateTransformations spec.
* @param axes - Axes in OME-NGFF v0.4 format
*
* @returns Array of 16 numbers representing the Matrix4.
*/
export function coordinateTransformationsToMatrix(coordinateTransformations: Ome.CoordinateTransformation[], axes: Ome.Axis[]) {
let mat = new Matrix4().identity();


// Apply each transformation sequentially and in order according to the OME-NGFF v0.4 spec.
// Reference: https://ngff.openmicroscopy.org/0.4/#trafo-md
for (const transform of coordinateTransformations ?? []) {
if (transform.type === "translation") {
const { translation: axisOrderedTranslation } = transform;
if (axisOrderedTranslation.length !== axes.length) {
throw new Error("Length of translation array was expected to match length of axes.");
}
const cartesianTranslation = getCartesianTransformation(axes, axisOrderedTranslation, 0)
mat = coordinateTransformationToMatrix('translation', cartesianTranslation, mat)

}
if (transform.type === "scale") {
const { scale: axisOrderedScale } = transform;
// Add in z dimension needed for Matrix4 scale API.
if (axisOrderedScale.length !== axes.length) {
throw new Error("Length of scale array was expected to match length of axes.");
}
const cartesianTranslation = getCartesianTransformation(axes, axisOrderedScale, 1)
mat = coordinateTransformationToMatrix('scale', cartesianTranslation, mat)
}
}

return mat;
}

function getCartesianTransformation(axes: Ome.Axis[], transformation: Array<number>, defaultValue: number): Array<number> {
const xyzIndices = ["x", "y", "z"].map((name) =>
axes.findIndex((axisObj) => axisObj.type === "space" && axisObj.name === name),
);

// Get the translation values for [x, y, z].
return xyzIndices.map((axisIndex) =>
axisIndex >= 0 ? transformation[axisIndex] : defaultValue,
);

}

function coordinateTransformationToMatrix(type: string, transformation: Array<number>, modelMatrix: Matrix4 = new Matrix4().identity()): Matrix4 {
switch (type) {
case 'translation':
return applyCoordinateTranslationToMatrix(modelMatrix, transformation)
case 'scale':
return applyCoordinateScalingToMatrix(modelMatrix, transformation)
default:
assert(type === 'translation' || type === 'scale')
}


}

function applyCoordinateScalingToMatrix(matrix: Matrix4, scale: Array<number>): Matrix4 {
// Get the scale values for [x, y, z].
const nextMat = new Matrix4().scale(scale);
return matrix.multiplyLeft(nextMat);
}

function applyCoordinateTranslationToMatrix(matrix: Matrix4, translation: Array<number>): Matrix4 {

const defaultValue = 0;
// Get the translation values for [x, y, z].
const nextMat = new Matrix4().translate(translation);
return matrix.multiplyLeft(nextMat);
}


/**
*Get physical size for specific resolution in multiscale image
*/
export function getPhysicalSizes(attrs: zarr.Attributes) {
if (isMultiscales(attrs)) {
const axes = getNgffAxes(attrs.multiscales);
const ct = coordinateTransformationsToMatrix(attrs.multiscales);
const matrixIndices = {
x: 0,
y: 5,
z: 10,
};
const physicalSizes = axes
.filter((a) => a.type === "space")
.reduce((acc: { [key: string]: viv.PhysicalSize }, { name, unit }: Ome.Axis) => {
acc[name] = { size: ct[matrixIndices[name as keyof typeof matrixIndices]], unit: unit ?? "" };
return acc;
}, {});
// @TODO: get t size from multiscales.coordinateTransformations if axis is present
return physicalSizes;
}
}

16 changes: 13 additions & 3 deletions viewer/src/ome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { ImageLabels, ImageLayerConfig, OnClickData, SourceData } from "./s
import { ZarrPixelSource } from "./ZarrPixelSource";
import * as utils from "./utils";

import { getPhysicalSizes, coordinateTransformationsToMatrix } from "./coordinate-transformations";

export async function loadWell(
config: ImageLayerConfig,
grp: zarr.Group<zarr.Readable>,
Expand Down Expand Up @@ -256,6 +258,14 @@ function isDownsampledZ(
return !data.every((element) => element.shape[zIndex] === originalSizeZ);
}

function getOrderedTransformations(metadata: Ome.Multiscale[]): Ome.CoordinateTransformation[] {
const resolutionTransformations = metadata[0].datasets[0]?.coordinateTransformations ? metadata[0].datasets[0]?.coordinateTransformations : []
const imageTransformations = metadata[0].coordinateTransformations ? metadata[0].coordinateTransformations : []


return [...resolutionTransformations, ...imageTransformations]
}

/**
* Load a multiscale OME-NGFF image
*/
Expand All @@ -280,7 +290,7 @@ export async function loadOmeMultiscales(
}
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 physicalSizes = getPhysicalSizes(utils.resolveAttrs(attrs));
const loader = data.map(
(arr, i) =>
new ZarrPixelSource(arr, {
Expand All @@ -296,7 +306,7 @@ export async function loadOmeMultiscales(
axis_labels,
model_matrix: config.model_matrix
? utils.parseMatrix(config.model_matrix)
: utils.coordinateTransformationsToMatrix(attrs.multiscales),
: coordinateTransformationsToMatrix(getOrderedTransformations(attrs.multiscales), utils.getNgffAxes(attrs.multiscales)),
defaults: {
selection: meta.defaultSelection,
colormap,
Expand All @@ -321,7 +331,7 @@ async function loadOmeImageLabel(root: zarr.Location<zarr.Readable>, name: strin
const colors = (attrs["image-label"].colors ?? []).map((d) => ({ labelValue: d["label-value"], rgba: d.rgba }));
return {
name,
modelMatrix: utils.coordinateTransformationsToMatrix(attrs.multiscales),
modelMatrix: coordinateTransformationsToMatrix(attrs.multiscales),
loader: data.map((arr) => new ZarrPixelSource(arr, { labels, tileSize })),
colors: colors.length > 0 ? colors : undefined,
};
Expand Down
13 changes: 7 additions & 6 deletions viewer/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ declare namespace Ome {

type CoordinateTransformation =
| {
type: "scale";
scale: Array<number>;
}
type: "scale";
scale: Array<number>;
}
| {
type: "translation";
translation: Array<number>;
};
type: "translation";
translation: Array<number>;
};

interface Dataset {
path: string;
Expand All @@ -53,6 +53,7 @@ declare namespace Ome {
datasets: Array<Dataset>;
version?: string;
axes?: string[] | Axis[];
coordinateTransformations?: CoordinateTransformation[]
}

interface Bioformats2rawlayout {
Expand Down
56 changes: 4 additions & 52 deletions viewer/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Matrix4 } from "math.gl";

import * as zarr from "zarrita";

import type * as viv from "@vivjs/types";
Expand Down Expand Up @@ -552,57 +552,6 @@ export function resolveLoaderFromLayerProps(
return isGridLayerProps(layerProps) ? layerProps.loaders[0].loader : layerProps.loader;
}

/**
* Convert an array of coordinateTransformations objects to a 16-element
* plain JS array using Matrix4 linear algebra transformation functions.
*
* Adapted from Vitessce: https://github.com/vitessce/vitessce/blob/c267ebecab1824dae68d6f2640a6c5ce7250efbb/packages/utils/spatial-utils/src/spatial.js#L403-L524
*
* @param coordinateTransformations List of objects matching the OME-NGFF v0.4 coordinateTransformations spec.
* @param axes - Axes in OME-NGFF v0.4 format
*
* @returns Array of 16 numbers representing the Matrix4.
*/
export function coordinateTransformationsToMatrix(multiscales: Array<Ome.Multiscale>) {
let mat = new Matrix4().identity();
const axes = getNgffAxes(multiscales);
const coordinateTransformations = multiscales[0].datasets[0]?.coordinateTransformations;
const xyzIndices = ["x", "y", "z"].map((name) =>
axes.findIndex((axisObj) => axisObj.type === "space" && axisObj.name === name),
);

// Apply each transformation sequentially and in order according to the OME-NGFF v0.4 spec.
// Reference: https://ngff.openmicroscopy.org/0.4/#trafo-md
for (const transform of coordinateTransformations ?? []) {
if (transform.type === "translation") {
const { translation: axisOrderedTranslation } = transform;
if (axisOrderedTranslation.length !== axes.length) {
throw new Error("Length of translation array was expected to match length of axes.");
}
const defaultValue = 0;
// Get the translation values for [x, y, z].
const xyzTranslation = xyzIndices.map((axisIndex) =>
axisIndex >= 0 ? axisOrderedTranslation[axisIndex] : defaultValue,
);
const nextMat = new Matrix4().translate(xyzTranslation);
mat = mat.multiplyLeft(nextMat);
}
if (transform.type === "scale") {
const { scale: axisOrderedScale } = transform;
// Add in z dimension needed for Matrix4 scale API.
if (axisOrderedScale.length !== axes.length) {
throw new Error("Length of scale array was expected to match length of axes.");
}
const defaultValue = 1;
// Get the scale values for [x, y, z].
const xyzScale = xyzIndices.map((axisIndex) => (axisIndex >= 0 ? axisOrderedScale[axisIndex] : defaultValue));
const nextMat = new Matrix4().scale(xyzScale);
mat = mat.multiplyLeft(nextMat);
}
}

return mat;
}

/**
* Builds N-tuples of elements from the given N arrays with matching indices,
Expand Down Expand Up @@ -633,6 +582,9 @@ export function getLayerSize({ props }: VizarrLayer) {
return { height, width, maxZoom };
}

/**
*Get physical size for specific resolution in multiscale image
*/
export function getPhysicalSizes(attrs: zarr.Attributes) {
if (isMultiscales(attrs)) {
const axes = getNgffAxes(attrs.multiscales);
Expand Down
20 changes: 20 additions & 0 deletions viewer/tests/transformations.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { test } from "vitest";
import yaml from 'yaml';
import fs from 'node:fs'
import path from 'node:path'
import { createSourceData } from '../src/io'

test('Resolution-level transformations should be applied', async () => {
const sourcePath = path.resolve(path.join(__dirname, "..", "..", "fixtures", "resolution_transformationsv0.5.yaml"))
const source = yaml.parse(fs.readFileSync(sourcePath, 'utf-8'))
const sourceData = await createSourceData(source)
console.log(sourceData.model_matrix)
})

test('Image-level transformations should be applied', async () => {
const sourcePath = path.resolve(path.join(__dirname, "..", "..", "fixtures", "transformationsv0.5.yaml"))
const source = yaml.parse(fs.readFileSync(sourcePath, 'utf-8'))
const sourceData = await createSourceData(source)
console.log(sourceData.model_matrix)
console.log(sourceData)
})
12 changes: 12 additions & 0 deletions viewer/tests/transformations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { test } from "vitest";
import yaml from 'yaml';
import fs from 'node:fs'
import path from 'node:path'
import { createSourceData } from '../src/io'

test('Resolution-level transformations should be applied', async () => {
const sourcePath = path.resolve(path.join(__dirname, "..", "..", "fixtures", "resolution_transformationsv0.5.yaml"))
const source = yaml.parse(fs.readFileSync(sourcePath, 'utf-8'))
const sourceData = await createSourceData(source)
console.log(sourceData.model_matrix)
})
Loading