Skip to content
Open
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
60 changes: 60 additions & 0 deletions python/tests/zarr_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,66 @@ def test_ome_zarr_0_6_affine(static_file_server, webdriver):
_check_sequence_result(model_space)


def test_ome_zarr_0_6_vs_0_5_affine(static_file_server, webdriver):
"""affine: Affine matrix (JSON form) applied to single scale. Same data as 0.6 example, and equivalent metadata for 0.5 as 0.6. But 0.6 should expose transform while 0.5 does not.
Example dataset: simple/affine_copy_0.5.zarr

Affine transform: Diagonal scale matrix with translation (equivalent to sequence of scale + translation).
Matrix:
[4, 0, 0, 32] - z axis: scale 4, translation 32
[0, 3, 0, 21] - y axis: scale 3, translation 21
[0, 0, 2, 10] - x axis: scale 2, translation 10
"""
test_dir_0_5 = OME_ZARR_0_6_ROOT / "simple" / "affine_copy_0.5.zarr"
server_url_0_5 = static_file_server(test_dir_0_5)
test_dir_0_6 = OME_ZARR_0_6_ROOT / "simple" / "affine.zarr"
server_url_0_6 = static_file_server(test_dir_0_6)
with webdriver.viewer.txn() as s:
s.layers.append(
name="affine_0_5",
layer=neuroglancer.ImageLayer(source=f"zarr3://{server_url_0_5}"),
)
s.layers.append(
name="affine_0_6",
layer=neuroglancer.ImageLayer(source=f"zarr3://{server_url_0_6}"),
)
webdriver.sync()

model_space_0_5 = _assert_renders(webdriver, "affine_0_5")
model_space_0_6 = _assert_renders(webdriver, "affine_0_6")

# Both scales should be the same
assert model_space_0_5["scales"] == model_space_0_6["scales"]

# Both of the volumes should be the same shape
assert model_space_0_5["volume"].shape == model_space_0_6["volume"].shape

# Both should render the same in the viewer with one visible at a time
with webdriver.viewer.txn() as s:
s.layers["affine_0_5"].visible = True
s.layers["affine_0_6"].visible = False
webdriver.sync()
screenshot_0_5 = webdriver.viewer.screenshot(size=[200, 200]).screenshot
with webdriver.viewer.txn() as s:
s.layers["affine_0_5"].visible = False
s.layers["affine_0_6"].visible = True
webdriver.sync()
screenshot_0_6 = webdriver.viewer.screenshot(size=[200, 200]).screenshot
np.testing.assert_array_equal(
screenshot_0_5.image_pixels, screenshot_0_6.image_pixels
)

# Both should obey the single voxel test
origin_0_5 = model_space_0_5["volume"].domain.origin
origin_0_6 = model_space_0_6["volume"].domain.origin
_verify_data_at_point(
model_space_0_5["volume"], TEST_VOXEL + np.array(origin_0_5), EXPECTED_VALUE
)
_verify_data_at_point(
model_space_0_6["volume"], TEST_VOXEL + np.array(origin_0_6), EXPECTED_VALUE
)


def test_ome_zarr_0_6_rotation(static_file_server, webdriver):
"""rotation: Rotation matrix (or axis permutation) example.
Example dataset: simple/rotation.zarr
Expand Down
96 changes: 58 additions & 38 deletions src/datasource/zarr/ome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ const SUPPORTED_OME_MULTISCALE_VERSIONS = new Set([
"0.6",
]);

// OME-Zarr versions (< 0.6) should not expose base transform
// to maintain backward compatibility with existing saved states
const OME_MULTISCALE_NO_EXPOSE_TRANSFORM_VERSIONS = new Set([
"0.4",
"0.5-dev",
"0.5",
]);

const OME_UNITS = new Map<string, { unit: string; scale: number }>([
["angstrom", { unit: "m", scale: 1e-10 }],
["foot", { unit: "m", scale: 0.3048 }],
Expand Down Expand Up @@ -560,6 +568,7 @@ function parseMultiscaleScale(
function parseOmeMultiscale(
url: string,
multiscale: unknown,
version: string,
): OmeMultiscaleMetadata {
verifyObject(multiscale);

Expand All @@ -578,7 +587,7 @@ function parseOmeMultiscale(
Array.isArray(coordinateSystemsRaw) &&
coordinateSystemsRaw.length > 0
) {
// OME-ZARR 0.6+: Use the last (intrinsic) coordinate system
// If coordinate systems specified, use the last (intrinsic) coordinate system
const coordinateSystems = parseArray(
coordinateSystemsRaw,
parseOmeCoordinateSystem,
Expand All @@ -595,7 +604,7 @@ function parseOmeMultiscale(
verifyString,
);
} else {
// OME-ZARR 0.4/0.5: Use axes directly
// Use axes directly if no coordinate systems (usually OME-zarr < 0.6)
coordinateSpace = verifyObjectProperty(multiscale, "axes", parseOmeAxes);
}

Expand Down Expand Up @@ -638,32 +647,48 @@ function parseOmeMultiscale(
coordinateSpace.scales[i] *= baseScales[i];
}

// The unscaled inverse of the base transform is used in the per-scale
// calculation of the affine transform to apply on top of the base transform.
const inverseBaseTransformUnscaled = new Float64Array(baseTransform.length);
matrix.inverse(
inverseBaseTransformUnscaled,
rank + 1,
baseTransform,
rank + 1,
rank + 1,
);
const shouldExposeBaseTransform =
!OME_MULTISCALE_NO_EXPOSE_TRANSFORM_VERSIONS.has(version);

// The base transform with scaling removed is used
// to provide a default transform in the layer source tab
// and for the bounding box transformation
const baseTransformScaled = new Float64Array(baseTransform.length);
matrix.copy(
baseTransformScaled,
rank + 1,
baseTransform,
rank + 1,
rank + 1,
rank + 1,
);
for (let i = 0; i < rank; ++i) {
for (let j = 0; j <= rank; ++j) {
baseTransformScaled[j * (rank + 1) + i] /= baseScales[i];
// Create the inverse base transform to make scale transforms relative.
// For OME-Zarr 0.6+: Use full inverse of base transform since transform is fully exposed
// For older versions: Use diagonal scale matrix inverse as only the scale is exposed
const inverseBaseTransform = new Float64Array(baseTransform.length);
if (shouldExposeBaseTransform) {
matrix.inverse(
inverseBaseTransform,
rank + 1,
baseTransform,
rank + 1,
rank + 1,
);
} else {
// Create inverse scale matrix using 1/baseScales
inverseBaseTransform.set(
matrix.createHomogeneousScaleMatrix(
Float64Array,
Float64Array.from(baseScales, (scale) => 1 / scale),
),
);
}

// For OME-Zarr 0.6+: Remove scaling from base transform since it will be
// exposed as the model transform and scales are in the coordinate space.
// For older versions: Set base transform to identity since it won't be exposed.
const baseTransformScaled = matrix.createIdentity(Float64Array, rank + 1);
if (shouldExposeBaseTransform) {
matrix.copy(
baseTransformScaled,
rank + 1,
baseTransform,
rank + 1,
rank + 1,
rank + 1,
);
for (let i = 0; i < rank; ++i) {
for (let j = 0; j <= rank; ++j) {
baseTransformScaled[j * (rank + 1) + i] /= baseScales[i];
}
}
}

Expand All @@ -679,24 +704,19 @@ function parseOmeMultiscale(
}
t[rank * (rank + 1) + i] -= offset;
}

// At each scale, we provide an affine transform matrix
// to get applied on top of the base transformation matrix
// This matrix should apply the per path scaling for moving between
// LODs as well as the per-lod offset in translations (for voxel center)
// In theory, if the transform at that path describes a different rotation
// shear etc, to the base transform that would be captured here as well
// though the common case is just scaling + translation differences
scale.transform = makeAffineRelativeToBaseTransform(
scale.transform,
inverseBaseTransformUnscaled,
inverseBaseTransform,
rank,
);
}
return {
coordinateSpace,
scales,
baseInfo: { baseScales, baseTransform: baseTransformScaled },
baseInfo: {
baseScales,
baseTransform: baseTransformScaled,
},
};
}

Expand Down Expand Up @@ -740,7 +760,7 @@ export function parseOmeMetadata(
);
continue;
}
const multiScaleInfo = parseOmeMultiscale(url, multiscale);
const multiScaleInfo = parseOmeMultiscale(url, multiscale, version);
const channelMetadata = omero ? parseOmeroMetadata(omero) : undefined;
return { multiscale: multiScaleInfo, channels: channelMetadata };
}
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"node_type": "array",
"chunk_key_encoding": {
"name": "default",
"configuration": {
"separator": "/"
}
},
"shape": [
27,
226,
186
],
"chunk_grid": {
"name": "regular",
"configuration": {
"chunk_shape": [
27,
226,
186
]
}
},
"codecs": [
{
"name": "bytes",
"configuration": {
"endian": "big"
}
},
{
"name": "zstd",
"configuration": {
"level": 3
}
}
],
"data_type": "uint8",
"fill_value": 0,
"zarr_format": 3
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"zarr_format": 3,
"node_type": "group",
"attributes": {
"ome": {
"version": "0.5",
"multiscales": [
{
"axes": [
{
"type": "space",
"name": "z",
"unit": "micrometer",
"discrete": false
},
{
"type": "space",
"name": "y",
"unit": "micrometer",
"discrete": false
},
{
"type": "space",
"name": "x",
"unit": "micrometer",
"discrete": false
}
],
"datasets": [
{
"path": "array",
"coordinateTransformations": [
{
"type": "affine",
"input": "array",
"output": "physical",
"name": "array-to-physical",
"affine": [
[
4,
0,
0,
32
],
[
0,
3,
0,
21
],
[
0,
0,
2,
10
]
]
}
]
}
]
}
]
}
}
}
Loading