Skip to main content

Coordinate Transformations

Coordinate transformations are fundamental to SpatialData, allowing different elements to be placed in shared coordinate systems. The @spatialdata/core package aims to provide a comprehensive implementation of transformation classes that can be represented in the standard.

Supported transformation types

RFC-5 typeSupport
identity
scale
translation
affine✅ (inline parameters only, not "by path")
rotation✅ (inline parameters only, not "by path")
mapAxis
sequence
byDimension❌ Not implemented
bijection❌ Not implemented
displacements❌ Not implemented
affine/rotation by path (params in an external zarr array)❌ Not implemented
non-spatial axes (e.g. c, t) alongside spatial ones⚠️ Spatial axes only; the matrix path is XYZ-only

We validate this against the upstream ome-zarr-transformations-conformance package (pinned to commit 6f93379), run via dev_scripts/conformance-dingus/ — see that directory's README for how to run it and the current pass/error/fail baseline. That conformance run is the source of truth for the table above.

:::info Read-Only API

The current version of @spatialdata/core is focused on reading SpatialData stores. Stores are treated as immutable—you load elements and access their transformations, but you don't construct or modify them.

Direct construction of transformation instances (e.g., new Scale([2, 2])) is primarily for internal use. Application code should retrieve transformations from elements via methods like element.getTransformation().

Future revisions may introduce APIs for creating and modifying SpatialData stores - but this implies issues around (meta)data integrity etc.

:::

Accessing Transformations

The primary way to work with transformations is through element methods:

const sdata = await readZarr('https://example.com/data.zarr');
const image = sdata.images?.['my_image'];

// Get available coordinate systems
console.log(image.coordinateSystems); // ['global', 'anatomical', ...]

// Get transformation to a specific coordinate system
const result = image.getTransformation('global');
if (result.ok) {
const matrix = result.value.toMatrix(); // Matrix4 for rendering
}

// Get all transformations
const allTransforms = image.getAllTransformations();

For rendering, you'll typically want the Matrix4 form:

import { getTransformMatrix } from '@spatialdata/core';

// Convenience function that unwraps and returns Matrix4 directly
const matrix = getTransformMatrix(image, 'global');

Transformation Classes Reference

Transformations parsed from store metadata are represented by classes extending BaseTransformation:

abstract class BaseTransformation {
readonly input?: CoordinateSystemRef; // Source coordinate system
readonly output?: CoordinateSystemRef; // Target coordinate system

abstract toArray(): number[]; // 16-element column-major array
toMatrix(): Matrix4; // Matrix4 from @math.gl/core
inverse(): Matrix4; // Inverted Matrix4; throws if singular
abstract get type(): string; // 'identity' | 'scale' | 'translation' | etc.
}

The transformation classes are:

TypeDescription
IdentityNo transformation (identity matrix)
ScaleUniform or non-uniform scaling
TranslationCoordinate offset
AffineFull affine matrix (2×3, 3×3, or 4×4 formats)
RotationRotation matrix (square, no translation)
MapAxisAxis remapping/permutation
SequenceComposition of multiple transformations

See Supported transformation types above for what's not yet implemented.

Multiscale Image Transforms

For multiscale images, transformations exist at both the element level and per-dataset (resolution) level. Use the convenience method on ImageElement / LabelsElement:

const image = sdata.images?.['my_image'];

const { element, dataset } = image.getTransformationForLevel(0, 'global');
// element: Result<BaseTransformation> - element-level transform
// dataset: BaseTransformation - per-resolution-level transform

For advanced use cases, you can compose transforms manually:

import { composeTransforms } from '@spatialdata/core';

// Element-level transforms (element space → coordinate system)
const elementTransforms = image.attrs.multiscales[0].coordinateTransformations;

// Dataset-level transforms (pixel space → element space)
const datasetTransforms = image.attrs.multiscales[0].datasets[0].coordinateTransformations;

// Compose into a single matrix (dataset first, then element)
const fullMatrix = composeTransforms(elementTransforms, datasetTransforms);

Coordinate System References

Transformations in NGFF 0.5+ can specify input and output coordinate systems:

// Example transformation with coordinate system references
{
type: 'affine',
affine: [[2, 0, 0], [0, 2, 0]],
input: {
name: 'pixel_space',
axes: [{ name: 'x', type: 'space' }, { name: 'y', type: 'space' }]
},
output: {
name: 'global',
axes: [{ name: 'x', type: 'space', unit: 'micrometer' }, { name: 'y', type: 'space', unit: 'micrometer' }]
}
}

Access these on parsed transformations:

const transform = element.getTransformation('global');
if (transform.ok) {
console.log(transform.value.input?.name); // Source coordinate system
console.log(transform.value.output?.name); // Target coordinate system
}

import { Matrix4 } from '@math.gl/core';

const transformResult = element.getTransformation('global');
if (!transformResult.ok) {
console.error('Could not get transformation');
return;
}
const matrix = transformResult.value.toMatrix();

// Transform a point
const point = [100, 200, 0, 1]; // Homogeneous coordinates
const transformed = matrix.transform(point);
// (nb doing this to large numbers of points is sub-optimal)

// Compose with another matrix
const combined = matrix.clone().multiplyRight(otherMatrix);

// Get the inverse (throws if the transformation is singular/non-invertible)
const inverse = transformResult.value.inverse();

See Also

  • Error Handling - Working with Result types from getTransformation()
  • Internals - Transformation parsing and internal APIs