diff --git a/.gitmodules b/.gitmodules index 37c054ac..a40eb0e8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "testdata/ome/v0.6/examples"] path = testdata/ome/v0.6/examples url = https://github.com/jo-mueller/ngff-rfc5-coordinate-transformation-examples +[submodule "testdata/ome/v0.6/ngff-spec"] + path = testdata/ome/v0.6/ngff-spec + url = https://github.com/ome/ngff-spec diff --git a/USERGUIDE-OME-ZARR.md b/USERGUIDE-OME-ZARR.md index 31e0df2d..488b9e5e 100644 --- a/USERGUIDE-OME-ZARR.md +++ b/USERGUIDE-OME-ZARR.md @@ -74,6 +74,25 @@ Scene roots (groups with `ome.scene`) are supported via `dev.zarr.conventions.om Notes: - Parsing is permissive and explicit (no strict full-spec validation). - Scene-level references (`input`/`output`) are resolved against scene-root coordinate systems and child image coordinate systems for graph inspection. + +### v0.6 coordinate system references + +In v0.6, the `input`/`output` of a coordinate transformation is a `CoordinateSystemRef` (`{"name": ..., "path": ...}`, both optional): + +```java +import dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef; +import dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation; + +// multiscales > datasets: input references the dataset path, output the intrinsic coordinate system +CoordinateTransformation.scale(Arrays.asList(0.5, 0.5), + CoordinateSystemRef.ofPath("s0"), CoordinateSystemRef.ofName("physical")); +// scene > coordinateTransformations: name + path of the image group +CoordinateTransformation.identity( + CoordinateSystemRef.of("physical", "imageA"), CoordinateSystemRef.of("physical", "imageB")); +``` + +`createScaleLevel(...)` on a v0.6 image fills these in automatically (input `{"path": }`, output `{"name": }`, where the intrinsic name is taken from existing datasets or the first entry of `coordinateSystems`); passing several transformations wraps them in a `sequence`. +Pre-release string references (e.g. `"input": "s0"`, `"output": "physical"`) are still read: a bare string is a coordinate system name, except the input of a dataset transformation, which is the dataset path. They are always written back in object form. - Path-based transform assets can be normalized with `Scene.normalizeCoordinateTransformPath(...)` and grouped under `coordinateTransformations/` via `createCoordinateTransformationsGroup()`. ## Read example diff --git a/src/main/java/dev/zarr/conventions/ome/MultiscaleImage.java b/src/main/java/dev/zarr/conventions/ome/MultiscaleImage.java index 7af5b641..b589afaa 100644 --- a/src/main/java/dev/zarr/conventions/ome/MultiscaleImage.java +++ b/src/main/java/dev/zarr/conventions/ome/MultiscaleImage.java @@ -69,15 +69,24 @@ default List getAxisNames() throws ZarrException { default List getLabels() throws IOException, ZarrException { StoreHandle labelsHandle = getStoreHandle().resolve("labels"); - // Try v0.5: labels/zarr.json with {"attributes": {"labels": [...]}} + // Try v0.5+: labels/zarr.json with {"attributes": {"ome": {"labels": [...]}}}. + // Falls back to the non-conformant legacy layout {"attributes": {"labels": [...]}}. StoreHandle zarrJson = labelsHandle.resolve(Node.ZARR_JSON); if (zarrJson.exists()) { com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v3.Node.makeObjectMapper(); byte[] bytes = Utils.toArray(zarrJson.readNonNull()); com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes); com.fasterxml.jackson.databind.JsonNode attrs = root.get("attributes"); - if (attrs != null && attrs.has("labels")) { - com.fasterxml.jackson.databind.JsonNode labelsNode = attrs.get("labels"); + com.fasterxml.jackson.databind.JsonNode labelsNode = null; + if (attrs != null) { + com.fasterxml.jackson.databind.JsonNode ome = attrs.get("ome"); + if (ome != null && ome.has("labels")) { + labelsNode = ome.get("labels"); + } else if (attrs.has("labels")) { + labelsNode = attrs.get("labels"); + } + } + if (labelsNode != null) { List result = new ArrayList<>(); for (com.fasterxml.jackson.databind.JsonNode item : labelsNode) { result.add(item.asText()); diff --git a/src/main/java/dev/zarr/conventions/ome/metadata/Axis.java b/src/main/java/dev/zarr/conventions/ome/metadata/Axis.java index 1a9d9ea5..450f75c1 100644 --- a/src/main/java/dev/zarr/conventions/ome/metadata/Axis.java +++ b/src/main/java/dev/zarr/conventions/ome/metadata/Axis.java @@ -1,5 +1,6 @@ package dev.zarr.conventions.ome.metadata; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -16,8 +17,9 @@ public final class Axis { public final String unit; @Nullable public final Boolean discrete; + /** Human-readable axis name, serialized as {@code longName} ({@code long_name} is accepted on read). */ @Nullable - @JsonProperty("long_name") + @JsonProperty("longName") public final String longName; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) @@ -26,7 +28,7 @@ public Axis( @Nullable @JsonProperty("type") String type, @Nullable @JsonProperty("unit") String unit, @Nullable @JsonProperty("discrete") Boolean discrete, - @Nullable @JsonProperty("long_name") String longName + @Nullable @JsonProperty("longName") @JsonAlias("long_name") String longName ) { this.name = name; this.type = type; diff --git a/src/main/java/dev/zarr/conventions/ome/metadata/ImageLabel.java b/src/main/java/dev/zarr/conventions/ome/metadata/ImageLabel.java new file mode 100644 index 00000000..a26cb88a --- /dev/null +++ b/src/main/java/dev/zarr/conventions/ome/metadata/ImageLabel.java @@ -0,0 +1,125 @@ +package dev.zarr.conventions.ome.metadata; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * OME-Zarr {@code image-label} metadata of a label image (display colors, per-label properties + * and the source image). + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class ImageLabel { + + @Nullable + public final List colors; + @Nullable + public final List properties; + @Nullable + public final Source source; + /** Older OME-Zarr versions carried a {@code version} inside {@code image-label}; kept for round-tripping. */ + @Nullable + public final String version; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public ImageLabel( + @Nullable @JsonProperty("colors") List colors, + @Nullable @JsonProperty("properties") List properties, + @Nullable @JsonProperty("source") Source source, + @Nullable @JsonProperty("version") String version + ) { + this.colors = colors; + this.properties = properties; + this.source = source; + this.version = version; + } + + public ImageLabel( + @Nullable List colors, + @Nullable List properties, + @Nullable Source source + ) { + this(colors, properties, source, null); + } + + /** Display color of a single label value. Additional keys are preserved. */ + @JsonInclude(JsonInclude.Include.NON_NULL) + public static final class Color { + @JsonProperty("label-value") + public final int labelValue; + @Nullable + public final List rgba; + private final Map additionalProperties = new LinkedHashMap<>(); + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Color( + @JsonProperty(value = "label-value", required = true) int labelValue, + @Nullable @JsonProperty("rgba") List rgba + ) { + this.labelValue = labelValue; + this.rgba = rgba; + } + + /** Keys other than {@code label-value} and {@code rgba}. */ + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String key, Object value) { + additionalProperties.put(key, value); + } + } + + /** Arbitrary properties associated with a single label value. */ + public static final class Property { + @JsonProperty("label-value") + public final int labelValue; + private final Map additionalProperties = new LinkedHashMap<>(); + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Property(@JsonProperty(value = "label-value", required = true) int labelValue) { + this.labelValue = labelValue; + } + + /** Keys other than {@code label-value}. */ + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String key, Object value) { + additionalProperties.put(key, value); + } + } + + /** Reference to the image the label image was derived from. */ + @JsonInclude(JsonInclude.Include.NON_NULL) + public static final class Source { + /** Spec default for {@link #image} when absent. */ + public static final String DEFAULT_IMAGE = "../../"; + + /** Relative path to the source image group, or null if absent (spec default {@code ../../}). */ + @Nullable + public final String image; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Source(@Nullable @JsonProperty("image") String image) { + this.image = image; + } + + /** Returns {@link #image}, falling back to the spec default {@code ../../}. */ + public String resolveImage() { + return image != null ? image : DEFAULT_IMAGE; + } + } +} diff --git a/src/main/java/dev/zarr/conventions/ome/metadata/OmeMetadata.java b/src/main/java/dev/zarr/conventions/ome/metadata/OmeMetadata.java index b4dbd877..9ab5363f 100644 --- a/src/main/java/dev/zarr/conventions/ome/metadata/OmeMetadata.java +++ b/src/main/java/dev/zarr/conventions/ome/metadata/OmeMetadata.java @@ -23,6 +23,16 @@ public final class OmeMetadata { public final PlateMetadata plate; @Nullable public final WellMetadata well; + /** Paths of the label images, present in the metadata of a {@code labels} group. */ + @Nullable + public final List labels; + /** Display/source information of a label image. */ + @Nullable + @JsonProperty("image-label") + public final ImageLabel imageLabel; + /** Image paths of a bioformats2raw collection, present in the metadata of the {@code OME} group. */ + @Nullable + public final List series; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public OmeMetadata( @@ -31,7 +41,10 @@ public OmeMetadata( @Nullable @JsonProperty("omero") OmeroMetadata omero, @Nullable @JsonProperty("bioformats2raw.layout") Integer bioformats2rawLayout, @Nullable @JsonProperty("plate") PlateMetadata plate, - @Nullable @JsonProperty("well") WellMetadata well + @Nullable @JsonProperty("well") WellMetadata well, + @Nullable @JsonProperty("labels") List labels, + @Nullable @JsonProperty("image-label") ImageLabel imageLabel, + @Nullable @JsonProperty("series") List series ) { this.version = version; this.multiscales = multiscales; @@ -39,10 +52,30 @@ public OmeMetadata( this.bioformats2rawLayout = bioformats2rawLayout; this.plate = plate; this.well = well; + this.labels = labels; + this.imageLabel = imageLabel; + this.series = series; + } + + public OmeMetadata( + String version, + @Nullable List multiscales, + @Nullable OmeroMetadata omero, + @Nullable Integer bioformats2rawLayout, + @Nullable PlateMetadata plate, + @Nullable WellMetadata well + ) { + this(version, multiscales, omero, bioformats2rawLayout, plate, well, null, null, null); } /** Convenience constructor for multiscale images (omero/layout/plate/well all null). */ public OmeMetadata(String version, List multiscales) { this(version, multiscales, null, null, null, null); } + + /** Returns a copy with {@code multiscales} replaced and all other fields preserved. */ + public OmeMetadata withMultiscales(@Nullable List multiscales) { + return new OmeMetadata(version, multiscales, omero, bioformats2rawLayout, plate, well, + labels, imageLabel, series); + } } diff --git a/src/main/java/dev/zarr/conventions/ome/metadata/OmeroChannel.java b/src/main/java/dev/zarr/conventions/ome/metadata/OmeroChannel.java index 7ea263ef..4cbfa716 100644 --- a/src/main/java/dev/zarr/conventions/ome/metadata/OmeroChannel.java +++ b/src/main/java/dev/zarr/conventions/ome/metadata/OmeroChannel.java @@ -1,10 +1,15 @@ package dev.zarr.conventions.ome.metadata; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import javax.annotation.Nullable; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; /** OMERO channel rendering metadata. */ @JsonInclude(JsonInclude.Include.NON_NULL) @@ -16,6 +21,7 @@ public final class OmeroChannel { @Nullable public final Boolean inverted; @Nullable public final String label; @Nullable public final OmeroWindow window; + private final Map extraFields; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public OmeroChannel( @@ -25,7 +31,8 @@ public OmeroChannel( @Nullable @JsonProperty("family") String family, @Nullable @JsonProperty("inverted") Boolean inverted, @Nullable @JsonProperty("label") String label, - @Nullable @JsonProperty("window") OmeroWindow window + @Nullable @JsonProperty("window") OmeroWindow window, + @Nullable @JsonAnySetter Map extraFields ) { this.active = active; this.coefficient = coefficient; @@ -34,5 +41,31 @@ public OmeroChannel( this.inverted = inverted; this.label = label; this.window = window; + this.extraFields = extraFields == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new LinkedHashMap<>(extraFields)); + } + + public OmeroChannel( + @Nullable Boolean active, + @Nullable Double coefficient, + @Nullable String color, + @Nullable String family, + @Nullable Boolean inverted, + @Nullable String label, + @Nullable OmeroWindow window + ) { + this(active, coefficient, color, family, inverted, label, window, null); + } + + /** + * Members zarr-java does not model, such as {@code wavelength_id}. The omero block is + * transitional and open-ended, so they are written back unchanged. + * + * @return the extra fields, never {@code null} + */ + @JsonAnyGetter + public Map extraFields() { + return extraFields; } } diff --git a/src/main/java/dev/zarr/conventions/ome/v0_5/MultiscaleImage.java b/src/main/java/dev/zarr/conventions/ome/v0_5/MultiscaleImage.java index a162e4f0..8dfee612 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_5/MultiscaleImage.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_5/MultiscaleImage.java @@ -81,6 +81,14 @@ public Integer getBioformats2rawLayout() { return omeMetadata.bioformats2rawLayout; } + /** + * Returns the {@code image-label} metadata if this multiscale image is a label image, or null if not. + */ + @Nullable + public dev.zarr.conventions.ome.metadata.ImageLabel getImageLabel() { + return omeMetadata.imageLabel; + } + @Override public dev.zarr.zarrjava.core.Array openScaleLevel(int i) throws IOException, ZarrException { String path = getMultiscalesEntry(0).datasets.get(i).path; @@ -107,7 +115,7 @@ public void createScaleLevel( MultiscalesEntry updated = current.withDataset(new Dataset(path, coordinateTransformations)); List updatedList = new java.util.ArrayList<>(omeMetadata.multiscales); updatedList.set(0, updated); - omeMetadata = new OmeMetadata(omeMetadata.version, updatedList); + omeMetadata = omeMetadata.withMultiscales(updatedList); setAttributes(omeAttributes(omeMetadata)); } } diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/MultiscaleImage.java b/src/main/java/dev/zarr/conventions/ome/v0_6/MultiscaleImage.java index 65c78fa1..06194e8e 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/MultiscaleImage.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/MultiscaleImage.java @@ -98,6 +98,14 @@ public Integer getBioformats2rawLayout() { return omeMetadata.bioformats2rawLayout; } + /** + * Returns the {@code image-label} metadata if this multiscale image is a label image, or null if not. + */ + @Nullable + public dev.zarr.conventions.ome.metadata.ImageLabel getImageLabel() { + return omeMetadata.imageLabel; + } + OmeMetadata getRawOmeMetadata() { return omeMetadata; } @@ -122,9 +130,21 @@ public void createScaleLevel( if (!(arrayMetadata instanceof dev.zarr.zarrjava.v3.ArrayMetadata)) { throw new ZarrException("Expected v3.ArrayMetadata for OME-Zarr v0.6, got " + arrayMetadata.getClass()); } + String intrinsicName = intrinsicCoordinateSystemName(omeMetadata.multiscales.get(0)); Array.create(storeHandle.resolve(path), (dev.zarr.zarrjava.v3.ArrayMetadata) arrayMetadata); // Convert ome.metadata.CoordinateTransformation to v0.6 CoordinateTransformation + // Per the 0.6 spec, a dataset carries a single transformation (scale, identity, or a sequence of + // scale + translation) whose input is {"path": } and whose output is + // {"name": }. Multiple transformations are wrapped in a sequence; + // nested transformations carry no input/output. + boolean wrapInSequence = coordinateTransformations.size() > 1; + dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef datasetInput = + dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef.ofPath(path); + dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef datasetOutput = + dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef.ofName(intrinsicName); + dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef input = wrapInSequence ? null : datasetInput; + dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef output = wrapInSequence ? null : datasetOutput; List v06Transforms = new ArrayList<>(); for (CoordinateTransformation ct : coordinateTransformations) { String type = ct.type; @@ -156,79 +176,108 @@ public void createScaleLevel( if ("sequence".equals(type)) { v06Transforms.add(new dev.zarr.conventions.ome.v0_6.metadata.transform.SequenceCoordinateTransformation( - null, null, null, castV06TransformList(raw.get("transformations")))); + input, output, null, castV06TransformList(raw.get("transformations")))); continue; } if ("mapAxis".equals(type)) { v06Transforms.add(new dev.zarr.conventions.ome.v0_6.metadata.transform.MapAxisCoordinateTransformation( - null, null, null, castIntList(raw.get("mapAxis")), castV06Transform(raw.get("transformation")))); + input, output, null, castIntList(raw.get("mapAxis")), castV06Transform(raw.get("transformation")))); + continue; + } + if ("projectAxis".equals(type)) { + v06Transforms.add(new dev.zarr.conventions.ome.v0_6.metadata.transform.ProjectAxisCoordinateTransformation( + null, null, null, castIntList(raw.get("droppedInputs")), castIntList(raw.get("createdOutputs")))); continue; } if ("affine".equals(type)) { v06Transforms.add(new dev.zarr.conventions.ome.v0_6.metadata.transform.AffineCoordinateTransformation( - null, null, null, castMatrix(raw.get("affine")), rawPath)); + input, output, null, castMatrix(raw.get("affine")), rawPath)); continue; } if ("rotation".equals(type)) { v06Transforms.add(new dev.zarr.conventions.ome.v0_6.metadata.transform.RotationCoordinateTransformation( - null, null, null, castMatrix(raw.get("rotation")), rawPath)); + input, output, null, castMatrix(raw.get("rotation")), rawPath)); continue; } if ("displacements".equals(type)) { v06Transforms.add(new dev.zarr.conventions.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation( - null, null, null, rawPath)); + input, output, null, rawPath, castString(raw.get("interpolation")))); continue; } if ("coordinates".equals(type)) { v06Transforms.add(new dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinatesCoordinateTransformation( - null, null, null, rawPath)); + input, output, null, rawPath, castString(raw.get("interpolation")))); continue; } if ("bijection".equals(type)) { v06Transforms.add(new dev.zarr.conventions.ome.v0_6.metadata.transform.BijectionCoordinateTransformation( - null, null, null, castV06Transform(raw.get("forward")), castV06Transform(raw.get("inverse")))); + input, output, null, castV06Transform(raw.get("forward")), castV06Transform(raw.get("inverse")))); continue; } if ("byDimension".equals(type)) { v06Transforms.add(new dev.zarr.conventions.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation( - null, null, null, castByDimensionTransformList(raw.get("transformations")))); + input, output, null, castByDimensionTransformList(raw.get("transformations")))); continue; } } if ("scale".equals(type)) { v06Transforms.add(new dev.zarr.conventions.ome.v0_6.metadata.transform.ScaleCoordinateTransformation( - null, null, null, scale, rawPath)); + input, output, null, scale, rawPath)); } else if ("translation".equals(type)) { v06Transforms.add(new dev.zarr.conventions.ome.v0_6.metadata.transform.TranslationCoordinateTransformation( - null, null, null, translation, rawPath)); + input, output, null, translation, rawPath)); } else if ("identity".equals(type)) { v06Transforms.add(new dev.zarr.conventions.ome.v0_6.metadata.transform.IdentityCoordinateTransformation( - null, null, null, rawPath)); + input, output, null, rawPath)); } else { dev.zarr.conventions.ome.v0_6.metadata.transform.GenericCoordinateTransformation generic = - new dev.zarr.conventions.ome.v0_6.metadata.transform.GenericCoordinateTransformation(type, null, null, null); + new dev.zarr.conventions.ome.v0_6.metadata.transform.GenericCoordinateTransformation(type, input, output, null); if (scale != null) generic.raw.put("scale", scale); if (translation != null) generic.raw.put("translation", translation); if (rawPath != null) generic.raw.put("path", rawPath); v06Transforms.add(generic); } } + if (wrapInSequence) { + v06Transforms = Collections.singletonList( + new dev.zarr.conventions.ome.v0_6.metadata.transform.SequenceCoordinateTransformation( + datasetInput, datasetOutput, null, v06Transforms)); + } MultiscalesEntry current = omeMetadata.multiscales.get(0); MultiscalesEntry updated = current.withDataset(new dev.zarr.conventions.ome.v0_6.metadata.Dataset(path, v06Transforms)); List updatedList = new ArrayList<>(omeMetadata.multiscales); updatedList.set(0, updated); - omeMetadata = new OmeMetadata( - omeMetadata.version, - updatedList, - omeMetadata.omero, - omeMetadata.bioformats2rawLayout, - omeMetadata.scene, - omeMetadata.plate, - omeMetadata.well); + omeMetadata = omeMetadata.withMultiscales(updatedList); setAttributes(omeAttributes(omeMetadata)); } + /** + * Name of the "intrinsic" coordinate system of a multiscale: the output of the existing dataset + * transformations if any, otherwise the first entry of {@code coordinateSystems}. + */ + private static String intrinsicCoordinateSystemName(MultiscalesEntry entry) throws ZarrException { + if (entry.datasets != null) { + for (dev.zarr.conventions.ome.v0_6.metadata.Dataset ds : entry.datasets) { + if (ds.coordinateTransformations == null) { + continue; + } + for (dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation ct : ds.coordinateTransformations) { + if (ct != null && ct.getOutput() != null && ct.getOutput().name != null) { + return ct.getOutput().name; + } + } + } + } + if (entry.coordinateSystems != null && !entry.coordinateSystems.isEmpty() + && entry.coordinateSystems.get(0).name != null) { + return entry.coordinateSystems.get(0).name; + } + throw new ZarrException( + "Cannot determine the intrinsic coordinate system for a new OME-Zarr v0.6 scale level: " + + "the multiscales entry has neither dataset transformations with an output nor coordinateSystems"); + } + @Override public dev.zarr.conventions.ome.metadata.MultiscalesEntry getMultiscaleNode(int i) throws ZarrException { MultiscalesEntry entry = getMultiscalesEntry(i); @@ -236,13 +285,21 @@ public dev.zarr.conventions.ome.metadata.MultiscalesEntry getMultiscaleNode(int for (dev.zarr.conventions.ome.v0_6.metadata.Dataset ds : entry.datasets) { List mapped = new ArrayList<>(); for (dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation ct : ds.coordinateTransformations) { + if (ct instanceof dev.zarr.conventions.ome.v0_6.metadata.transform.SequenceCoordinateTransformation + && ((dev.zarr.conventions.ome.v0_6.metadata.transform.SequenceCoordinateTransformation) ct).transformations != null) { + // 0.6 datasets wrap scale + translation in a sequence; the version-independent view + // uses the flat [scale, translation] list of earlier versions. + mapped.addAll(mapTransformList( + ((dev.zarr.conventions.ome.v0_6.metadata.transform.SequenceCoordinateTransformation) ct).transformations)); + continue; + } mapped.add(mapTransform(ct)); } mappedDatasets.add(new dev.zarr.conventions.ome.metadata.Dataset(ds.path, mapped)); } List axes = entry.axes; - if ((axes == null || axes.isEmpty()) && entry.coordinateSystems != null && !entry.coordinateSystems.isEmpty()) { - axes = entry.coordinateSystems.get(0).axes; + if ((axes == null || axes.isEmpty()) && entry.intrinsicCoordinateSystem() != null) { + axes = entry.intrinsicCoordinateSystem().axes; } return new dev.zarr.conventions.ome.metadata.MultiscalesEntry( axes != null ? axes : Collections.emptyList(), @@ -294,6 +351,19 @@ private static CoordinateTransformation mapTransform( } return generic; } + if (ct instanceof dev.zarr.conventions.ome.v0_6.metadata.transform.ProjectAxisCoordinateTransformation) { + dev.zarr.conventions.ome.v0_6.metadata.transform.ProjectAxisCoordinateTransformation t = + (dev.zarr.conventions.ome.v0_6.metadata.transform.ProjectAxisCoordinateTransformation) ct; + dev.zarr.conventions.ome.metadata.transform.GenericCoordinateTransformation generic = + new dev.zarr.conventions.ome.metadata.transform.GenericCoordinateTransformation("projectAxis"); + if (t.droppedInputs != null) { + generic.raw.put("droppedInputs", t.droppedInputs); + } + if (t.createdOutputs != null) { + generic.raw.put("createdOutputs", t.createdOutputs); + } + return generic; + } if (ct instanceof dev.zarr.conventions.ome.v0_6.metadata.transform.AffineCoordinateTransformation) { dev.zarr.conventions.ome.v0_6.metadata.transform.AffineCoordinateTransformation t = (dev.zarr.conventions.ome.v0_6.metadata.transform.AffineCoordinateTransformation) ct; @@ -322,6 +392,9 @@ private static CoordinateTransformation mapTransform( dev.zarr.conventions.ome.metadata.transform.GenericCoordinateTransformation generic = new dev.zarr.conventions.ome.metadata.transform.GenericCoordinateTransformation("displacements"); generic.raw.put("path", t.path); + if (t.interpolation != null) { + generic.raw.put("interpolation", t.interpolation); + } return generic; } if (ct instanceof dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinatesCoordinateTransformation) { @@ -330,6 +403,9 @@ private static CoordinateTransformation mapTransform( dev.zarr.conventions.ome.metadata.transform.GenericCoordinateTransformation generic = new dev.zarr.conventions.ome.metadata.transform.GenericCoordinateTransformation("coordinates"); generic.raw.put("path", t.path); + if (t.interpolation != null) { + generic.raw.put("interpolation", t.interpolation); + } return generic; } if (ct instanceof dev.zarr.conventions.ome.v0_6.metadata.transform.BijectionCoordinateTransformation) { @@ -354,8 +430,8 @@ private static CoordinateTransformation mapTransform( List> transformed = new ArrayList<>(); for (dev.zarr.conventions.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation.ByDimensionTransformation item : t.transformations) { Map map = new LinkedHashMap<>(); - map.put("input_axes", item.inputAxes); - map.put("output_axes", item.outputAxes); + map.put("inputAxes", item.inputAxes); + map.put("outputAxes", item.outputAxes); map.put("transformation", item.transformation != null ? mapTransform(item.transformation) : null); transformed.add(map); } @@ -411,6 +487,10 @@ private static List castDoubleList(List values) { return out; } + private static String castString(Object raw) { + return raw instanceof String ? (String) raw : null; + } + private static List castIntList(Object raw) { if (!(raw instanceof List)) { return null; @@ -489,8 +569,8 @@ private static List map = (Map) item; out.add(new dev.zarr.conventions.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation.ByDimensionTransformation( - castIntList(map.get("input_axes")), - castIntList(map.get("output_axes")), + castIntList(map.containsKey("inputAxes") ? map.get("inputAxes") : map.get("input_axes")), + castIntList(map.containsKey("outputAxes") ? map.get("outputAxes") : map.get("output_axes")), castV06Transform(map.get("transformation")))); } return out; diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/SceneReferenceResolver.java b/src/main/java/dev/zarr/conventions/ome/v0_6/SceneReferenceResolver.java index c63df2e2..f3ee24d6 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/SceneReferenceResolver.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/SceneReferenceResolver.java @@ -3,6 +3,7 @@ import dev.zarr.conventions.ome.v0_6.metadata.CoordinateSystem; import dev.zarr.conventions.ome.v0_6.metadata.MultiscalesEntry; import dev.zarr.conventions.ome.v0_6.metadata.SceneMetadata; +import dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef; import javax.annotation.Nullable; import java.util.ArrayList; @@ -40,15 +41,16 @@ final class SceneReferenceResolver { } } + /** + * Resolves a scene-level reference. A {@code null}/empty path refers to the scene's own + * {@code coordinateSystems}; otherwise the path names an image subgroup of the scene. + */ @Nullable - ResolvedCoordinateSystem resolve(@Nullable String reference) { - if (reference == null || reference.isEmpty()) { + ResolvedCoordinateSystem resolve(@Nullable CoordinateSystemRef reference) { + if (reference == null || reference.name == null) { return null; } - if (!reference.contains("#")) { - return null; - } - return index.get(reference); + return index.get(nodeId(reference.path, reference.name)); } List list() { @@ -59,11 +61,24 @@ private void addCoordinateSystem(String groupPath, CoordinateSystem coordinateSy if (coordinateSystem == null || coordinateSystem.name == null) { return; } - String canonicalPath = groupPath == null || groupPath.isEmpty() ? "." : groupPath; - String id = canonicalPath + "#" + coordinateSystem.name; + String canonicalPath = canonicalGroupPath(groupPath); + String id = nodeId(canonicalPath, coordinateSystem.name); index.put(id, new ResolvedCoordinateSystem(id, canonicalPath, coordinateSystem)); } + private static String canonicalGroupPath(@Nullable String groupPath) { + String normalized = Scene.normalizeCoordinateTransformPath(groupPath); + while (normalized != null && normalized.endsWith("/")) { + normalized = normalized.substring(0, normalized.length() - 1); + } + return normalized == null || normalized.isEmpty() || ".".equals(normalized) ? "." : normalized; + } + + /** Graph node id: {@code "#"}, with {@code "."} as the scene root's group path. */ + private static String nodeId(@Nullable String groupPath, String name) { + return canonicalGroupPath(groupPath) + "#" + name; + } + static final class ResolvedCoordinateSystem { final String id; final String groupPath; diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/Dataset.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/Dataset.java index 86d1a822..f933a597 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/Dataset.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/Dataset.java @@ -3,8 +3,16 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.node.ObjectNode; import dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation; +import java.io.IOException; import java.util.List; @JsonInclude(JsonInclude.Include.NON_NULL) @@ -18,9 +26,38 @@ public final class Dataset { public Dataset( @JsonProperty(value = "path", required = true) String path, @JsonProperty(value = "coordinateTransformations", required = true) + @JsonDeserialize(using = TransformationsDeserializer.class) List coordinateTransformations ) { this.path = path; this.coordinateTransformations = coordinateTransformations; } + + /** + * Reads dataset-level transformations, mapping the legacy (pre-release 0.6) bare-string + * {@code "input": ""} to {@code {"path": ""}}. Everywhere else a bare + * string reference is read as a coordinate system name (see + * {@link dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef}). + */ + static final class TransformationsDeserializer extends JsonDeserializer> { + @Override + public List deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode tree = p.readValueAsTree(); + if (tree != null && tree.isArray()) { + for (JsonNode item : tree) { + if (item instanceof ObjectNode) { + ObjectNode obj = (ObjectNode) item; + JsonNode input = obj.get("input"); + if (input != null && input.isTextual()) { + ObjectNode ref = obj.objectNode(); + ref.put("path", input.asText()); + obj.set("input", ref); + } + } + } + } + JavaType type = ctxt.getTypeFactory().constructCollectionType(List.class, CoordinateTransformation.class); + return ctxt.readTreeAsValue(tree, type); + } + } } diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/MultiscalesEntry.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/MultiscalesEntry.java index 1648e6d6..9461bd1d 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/MultiscalesEntry.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/MultiscalesEntry.java @@ -1,13 +1,18 @@ package dev.zarr.conventions.ome.v0_6.metadata; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import dev.zarr.conventions.ome.metadata.Axis; +import dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef; import dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation; import javax.annotation.Nullable; import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -21,6 +26,7 @@ public final class MultiscalesEntry { @Nullable public final String name; @Nullable public final String type; @Nullable public final Map metadata; + private final Map extraFields; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public MultiscalesEntry( @@ -30,7 +36,8 @@ public MultiscalesEntry( @Nullable @JsonProperty("coordinateSystems") List coordinateSystems, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("type") String type, - @Nullable @JsonProperty("metadata") Map metadata + @Nullable @JsonProperty("metadata") Map metadata, + @Nullable @JsonAnySetter Map extraFields ) { this.axes = axes; this.datasets = datasets; @@ -39,16 +46,69 @@ public MultiscalesEntry( this.name = name; this.type = type; this.metadata = metadata; + this.extraFields = extraFields == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new LinkedHashMap<>(extraFields)); + } + + public MultiscalesEntry( + @Nullable List axes, + List datasets, + @Nullable List coordinateTransformations, + @Nullable List coordinateSystems, + @Nullable String name, + @Nullable String type, + @Nullable Map metadata + ) { + this(axes, datasets, coordinateTransformations, coordinateSystems, name, type, metadata, null); } public MultiscalesEntry(List datasets, List coordinateSystems, String name) { this(null, datasets, null, coordinateSystems, name, null, null); } + /** + * The coordinate system the datasets map into (named by the {@code output} of their + * transformations), or the first listed coordinate system if no dataset names one. + */ + @Nullable + public CoordinateSystem intrinsicCoordinateSystem() { + if (coordinateSystems == null || coordinateSystems.isEmpty()) { + return null; + } + for (Dataset dataset : datasets) { + if (dataset.coordinateTransformations == null || dataset.coordinateTransformations.isEmpty()) { + continue; + } + CoordinateSystemRef output = dataset.coordinateTransformations.get(0).getOutput(); + if (output == null || output.name == null) { + continue; + } + for (CoordinateSystem coordinateSystem : coordinateSystems) { + if (output.name.equals(coordinateSystem.name)) { + return coordinateSystem; + } + } + break; + } + return coordinateSystems.get(0); + } + /** Returns a new MultiscalesEntry with the given dataset appended. */ public MultiscalesEntry withDataset(Dataset dataset) { List updated = new ArrayList<>(this.datasets); updated.add(dataset); - return new MultiscalesEntry(axes, updated, coordinateTransformations, coordinateSystems, name, type, metadata); + return new MultiscalesEntry(axes, updated, coordinateTransformations, coordinateSystems, name, type, metadata, extraFields); + } + + /** + * Members zarr-java does not model, such as the JSON-LD key {@code @id}. + * They are written back unchanged, so that a metadata rewrite does not drop them. + * + * @return the extra fields, never {@code null} + */ + @JsonAnyGetter + public Map extraFields() { + return extraFields; } } diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/OmeMetadata.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/OmeMetadata.java index 00f5d419..66856d17 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/OmeMetadata.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/OmeMetadata.java @@ -1,14 +1,20 @@ package dev.zarr.conventions.ome.v0_6.metadata; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import dev.zarr.conventions.ome.metadata.ImageLabel; import dev.zarr.conventions.ome.metadata.OmeroMetadata; import dev.zarr.conventions.ome.metadata.PlateMetadata; import dev.zarr.conventions.ome.metadata.WellMetadata; import javax.annotation.Nullable; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** OME-Zarr v0.6 metadata stored under {@code attributes["ome"]}. */ @JsonInclude(JsonInclude.Include.NON_NULL) @@ -23,6 +29,15 @@ public final class OmeMetadata { @Nullable public final SceneMetadata scene; @Nullable public final PlateMetadata plate; @Nullable public final WellMetadata well; + /** Paths of the label images, present in the metadata of a {@code labels} group. */ + @Nullable public final List labels; + /** Display/source information of a label image. */ + @Nullable + @JsonProperty("image-label") + public final ImageLabel imageLabel; + /** Image paths of a bioformats2raw collection, present in the metadata of the {@code OME} group. */ + @Nullable public final List series; + private final Map extraFields; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public OmeMetadata( @@ -32,7 +47,11 @@ public OmeMetadata( @Nullable @JsonProperty("bioformats2raw.layout") Integer bioformats2rawLayout, @Nullable @JsonProperty("scene") SceneMetadata scene, @Nullable @JsonProperty("plate") PlateMetadata plate, - @Nullable @JsonProperty("well") WellMetadata well + @Nullable @JsonProperty("well") WellMetadata well, + @Nullable @JsonProperty("labels") List labels, + @Nullable @JsonProperty("image-label") ImageLabel imageLabel, + @Nullable @JsonProperty("series") List series, + @Nullable @JsonAnySetter Map extraFields ) { this.version = version; this.multiscales = multiscales; @@ -41,6 +60,39 @@ public OmeMetadata( this.scene = scene; this.plate = plate; this.well = well; + this.labels = labels; + this.imageLabel = imageLabel; + this.series = series; + this.extraFields = extraFields == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new LinkedHashMap<>(extraFields)); + } + + public OmeMetadata( + String version, + @Nullable List multiscales, + @Nullable OmeroMetadata omero, + @Nullable Integer bioformats2rawLayout, + @Nullable SceneMetadata scene, + @Nullable PlateMetadata plate, + @Nullable WellMetadata well, + @Nullable List labels, + @Nullable ImageLabel imageLabel, + @Nullable List series + ) { + this(version, multiscales, omero, bioformats2rawLayout, scene, plate, well, labels, imageLabel, series, null); + } + + public OmeMetadata( + String version, + @Nullable List multiscales, + @Nullable OmeroMetadata omero, + @Nullable Integer bioformats2rawLayout, + @Nullable SceneMetadata scene, + @Nullable PlateMetadata plate, + @Nullable WellMetadata well + ) { + this(version, multiscales, omero, bioformats2rawLayout, scene, plate, well, null, null, null); } public OmeMetadata(String version, @Nullable List multiscales) { @@ -63,4 +115,21 @@ public OmeMetadata( ) { this(version, multiscales, omero, null, scene, null, null); } + + /** Returns a copy with {@code multiscales} replaced and all other fields preserved. */ + public OmeMetadata withMultiscales(@Nullable List multiscales) { + return new OmeMetadata(version, multiscales, omero, bioformats2rawLayout, scene, plate, well, + labels, imageLabel, series, extraFields); + } + + /** + * Members zarr-java does not model, such as the JSON-LD keys {@code @id} and {@code @type}. + * They are written back unchanged, so that a metadata rewrite does not drop them. + * + * @return the extra fields, never {@code null} + */ + @JsonAnyGetter + public Map extraFields() { + return extraFields; + } } diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/AffineCoordinateTransformation.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/AffineCoordinateTransformation.java index 5b543a0b..f117bc09 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/AffineCoordinateTransformation.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/AffineCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.List; @@ -13,8 +12,8 @@ public final class AffineCoordinateTransformation extends BaseCoordinateTransfor @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public AffineCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("affine") List> affine, @Nullable @JsonProperty("path") String path diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/BaseCoordinateTransformation.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/BaseCoordinateTransformation.java index ae0bd284..375c0dd0 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/BaseCoordinateTransformation.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/BaseCoordinateTransformation.java @@ -4,14 +4,14 @@ abstract class BaseCoordinateTransformation implements CoordinateTransformation { public final String type; - @Nullable public final String input; - @Nullable public final String output; + @Nullable public final CoordinateSystemRef input; + @Nullable public final CoordinateSystemRef output; @Nullable public final String name; protected BaseCoordinateTransformation( String type, - @Nullable String input, - @Nullable String output, + @Nullable CoordinateSystemRef input, + @Nullable CoordinateSystemRef output, @Nullable String name ) { this.type = type; @@ -26,12 +26,12 @@ public String getType() { } @Override - public String getInput() { + public CoordinateSystemRef getInput() { return input; } @Override - public String getOutput() { + public CoordinateSystemRef getOutput() { return output; } diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/BijectionCoordinateTransformation.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/BijectionCoordinateTransformation.java index a83f5daa..2102fd9d 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/BijectionCoordinateTransformation.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/BijectionCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; @@ -12,8 +11,8 @@ public final class BijectionCoordinateTransformation extends BaseCoordinateTrans @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public BijectionCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("forward") CoordinateTransformation forward, @Nullable @JsonProperty("inverse") CoordinateTransformation inverse diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/ByDimensionCoordinateTransformation.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/ByDimensionCoordinateTransformation.java index 374d81b7..75e74e6a 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/ByDimensionCoordinateTransformation.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/ByDimensionCoordinateTransformation.java @@ -1,8 +1,8 @@ package dev.zarr.conventions.ome.v0_6.metadata.transform; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.List; @@ -12,8 +12,8 @@ public final class ByDimensionCoordinateTransformation extends BaseCoordinateTra @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public ByDimensionCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("transformations") List transformations ) { @@ -22,14 +22,18 @@ public ByDimensionCoordinateTransformation( } public static final class ByDimensionTransformation { - @Nullable public final List inputAxes; - @Nullable public final List outputAxes; + @Nullable + @JsonProperty("inputAxes") + public final List inputAxes; + @Nullable + @JsonProperty("outputAxes") + public final List outputAxes; @Nullable public final CoordinateTransformation transformation; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public ByDimensionTransformation( - @Nullable @JsonProperty("input_axes") List inputAxes, - @Nullable @JsonProperty("output_axes") List outputAxes, + @Nullable @JsonProperty("inputAxes") @JsonAlias("input_axes") List inputAxes, + @Nullable @JsonProperty("outputAxes") @JsonAlias("output_axes") List outputAxes, @Nullable @JsonProperty("transformation") CoordinateTransformation transformation ) { this.inputAxes = inputAxes; diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/CoordinateSystemRef.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/CoordinateSystemRef.java new file mode 100644 index 00000000..c47f746c --- /dev/null +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/CoordinateSystemRef.java @@ -0,0 +1,153 @@ +package dev.zarr.conventions.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.Objects; + +/** + * Reference to a coordinate system, used as the {@code input} / {@code output} of an OME-Zarr v0.6 + * coordinate transformation. + * + *

Serialized as a JSON object {@code {"name": ..., "path": ...}}; both fields are optional and + * omitted when {@code null}. Which fields are required depends on the context + * (see the OME-Zarr 0.6 "coordinateTransformations" constraints): + *

    + *
  • {@code multiscales > datasets}: input {@code {"path": ""}}, + * output {@code {"name": ""}}
  • + *
  • {@code multiscales > coordinateTransformations}: {@code {"name": ...}}, optionally with a + * {@code path} to a child labels group
  • + *
  • {@code scene > coordinateTransformations}: {@code {"name": ..., "path": ""}}
  • + *
+ * A {@code null} or omitted {@code path} refers to a coordinate system in the same {@code zarr.json}. + * + *

Legacy string form. Pre-release 0.6 drafts stored references as plain strings. For + * backwards compatibility a bare JSON string is still accepted on read and interpreted as a + * coordinate system {@code name}, except for the {@code input} of a top-level + * {@code multiscales > datasets} transformation, where it is interpreted as the dataset + * {@code path} (see {@link dev.zarr.conventions.ome.v0_6.metadata.Dataset}). + * Strings of the form {@code "#"} (as written by earlier zarr-java versions) are split + * into path and name, with {@code "."} meaning "no path". Writing always emits the object form. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonDeserialize(using = CoordinateSystemRef.Deserializer.class) +public final class CoordinateSystemRef { + + @Nullable + @JsonProperty("name") + public final String name; + @Nullable + @JsonProperty("path") + public final String path; + + public CoordinateSystemRef(@Nullable String name, @Nullable String path) { + this.name = name; + this.path = path; + } + + /** Reference to a named coordinate system in the same {@code zarr.json}. */ + public static CoordinateSystemRef ofName(String name) { + return new CoordinateSystemRef(name, null); + } + + /** Reference by path only (e.g. the input of a dataset transformation: the array at {@code path}). */ + public static CoordinateSystemRef ofPath(String path) { + return new CoordinateSystemRef(null, path); + } + + /** Reference to the coordinate system {@code name} defined in the Zarr group at {@code path}. */ + public static CoordinateSystemRef of(@Nullable String name, @Nullable String path) { + return new CoordinateSystemRef(name, path); + } + + /** + * Parses the legacy (pre-release 0.6) string form of a reference: a bare string is a name; + * {@code "#"} is split into path and name ({@code "."} or empty path means no path). + */ + public static CoordinateSystemRef fromLegacyString(String value) { + int hash = value.indexOf('#'); + if (hash < 0) { + return ofName(value); + } + String p = value.substring(0, hash); + String n = value.substring(hash + 1); + if (p.isEmpty() || ".".equals(p)) { + p = null; + } + return new CoordinateSystemRef(n.isEmpty() ? null : n, p); + } + + @Nullable + public String getName() { + return name; + } + + @Nullable + public String getPath() { + return path; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CoordinateSystemRef)) { + return false; + } + CoordinateSystemRef that = (CoordinateSystemRef) o; + return Objects.equals(name, that.name) && Objects.equals(path, that.path); + } + + @Override + public int hashCode() { + return Objects.hash(name, path); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("{"); + if (name != null) { + sb.append("name=").append(name); + } + if (path != null) { + if (name != null) { + sb.append(", "); + } + sb.append("path=").append(path); + } + return sb.append('}').toString(); + } + + static final class Deserializer extends StdDeserializer { + Deserializer() { + super(CoordinateSystemRef.class); + } + + @Override + public CoordinateSystemRef deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonToken token = p.currentToken(); + if (token == JsonToken.VALUE_STRING) { + return fromLegacyString(p.getValueAsString()); + } + if (token == JsonToken.START_OBJECT) { + JsonNode node = p.readValueAsTree(); + return new CoordinateSystemRef(textOrNull(node.get("name")), textOrNull(node.get("path"))); + } + return (CoordinateSystemRef) ctxt.handleUnexpectedToken(CoordinateSystemRef.class, p); + } + + @Nullable + private static String textOrNull(@Nullable JsonNode node) { + return node != null && node.isTextual() ? node.asText() : null; + } + } +} diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/CoordinateSystemRefSerde.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/CoordinateSystemRefSerde.java deleted file mode 100644 index e087aa76..00000000 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/CoordinateSystemRefSerde.java +++ /dev/null @@ -1,41 +0,0 @@ -package dev.zarr.conventions.ome.v0_6.metadata.transform; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; - -import java.io.IOException; - -final class CoordinateSystemRefSerde { - private CoordinateSystemRefSerde() { - } - - static final class Deserializer extends StdDeserializer { - Deserializer() { - super(String.class); - } - - @Override - public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - JsonToken token = p.currentToken(); - if (token == JsonToken.VALUE_STRING) { - return p.getValueAsString(); - } - if (token == JsonToken.START_OBJECT) { - JsonNode node = p.readValueAsTree(); - JsonNode pathNode = node.get("path"); - JsonNode nameNode = node.get("name"); - String path = pathNode != null && pathNode.isTextual() ? pathNode.asText() : null; - String name = nameNode != null && nameNode.isTextual() ? nameNode.asText() : null; - if (name == null) { - return path; - } - String canonicalPath = (path == null || path.isEmpty()) ? "." : path; - return canonicalPath + "#" + name; - } - return (String) ctxt.handleUnexpectedToken(String.class, p); - } - } -} diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/CoordinateTransformation.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/CoordinateTransformation.java index f83a6375..b623d5be 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/CoordinateTransformation.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/CoordinateTransformation.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import javax.annotation.Nullable; import java.util.List; @JsonTypeInfo( @@ -16,6 +18,7 @@ @JsonSubTypes.Type(value = IdentityCoordinateTransformation.class, name = "identity"), @JsonSubTypes.Type(value = SequenceCoordinateTransformation.class, name = "sequence"), @JsonSubTypes.Type(value = MapAxisCoordinateTransformation.class, name = "mapAxis"), + @JsonSubTypes.Type(value = ProjectAxisCoordinateTransformation.class, name = "projectAxis"), @JsonSubTypes.Type(value = AffineCoordinateTransformation.class, name = "affine"), @JsonSubTypes.Type(value = RotationCoordinateTransformation.class, name = "rotation"), @JsonSubTypes.Type(value = DisplacementsCoordinateTransformation.class, name = "displacements"), @@ -25,19 +28,43 @@ }) public interface CoordinateTransformation { String getType(); - String getInput(); - String getOutput(); + @Nullable CoordinateSystemRef getInput(); + @Nullable CoordinateSystemRef getOutput(); String getName(); - static CoordinateTransformation scale(List scale, String input, String output) { + /** + * Scale transformation from {@code input} to {@code output}. For a {@code multiscales > datasets} + * entry use {@code CoordinateSystemRef.ofPath(datasetPath)} as input and + * {@code CoordinateSystemRef.ofName(intrinsicName)} as output. + */ + static CoordinateTransformation scale( + List scale, @Nullable CoordinateSystemRef input, @Nullable CoordinateSystemRef output) { return new ScaleCoordinateTransformation(input, output, null, scale, null); } - static CoordinateTransformation translation(List translation, String input, String output) { + /** Scale transformation without input/output, e.g. for use inside a {@code sequence}. */ + static CoordinateTransformation scale(List scale) { + return scale(scale, null, null); + } + + static CoordinateTransformation translation( + List translation, @Nullable CoordinateSystemRef input, @Nullable CoordinateSystemRef output) { return new TranslationCoordinateTransformation(input, output, null, translation, null); } - static CoordinateTransformation identity(String input, String output) { + /** Translation transformation without input/output, e.g. for use inside a {@code sequence}. */ + static CoordinateTransformation translation(List translation) { + return translation(translation, null, null); + } + + static CoordinateTransformation identity(@Nullable CoordinateSystemRef input, @Nullable CoordinateSystemRef output) { return new IdentityCoordinateTransformation(input, output, null, null); } + + static CoordinateTransformation sequence( + List transformations, + @Nullable CoordinateSystemRef input, + @Nullable CoordinateSystemRef output) { + return new SequenceCoordinateTransformation(input, output, null, transformations); + } } diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/CoordinatesCoordinateTransformation.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/CoordinatesCoordinateTransformation.java index 29727846..fa4f5ee7 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/CoordinatesCoordinateTransformation.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/CoordinatesCoordinateTransformation.java @@ -2,21 +2,33 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; public final class CoordinatesCoordinateTransformation extends BaseCoordinateTransformation { @Nullable public final String path; + /** Interpolation method for the {@code path} array, e.g. {@code nearest}, {@code linear} (default), {@code bspline-cubic}. */ + @Nullable public final String interpolation; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public CoordinatesCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, - @Nullable @JsonProperty("path") String path + @Nullable @JsonProperty("path") String path, + @Nullable @JsonProperty("interpolation") String interpolation ) { super("coordinates", input, output, name); this.path = path; + this.interpolation = interpolation; + } + + public CoordinatesCoordinateTransformation( + @Nullable CoordinateSystemRef input, + @Nullable CoordinateSystemRef output, + @Nullable String name, + @Nullable String path + ) { + this(input, output, name, path, null); } } diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/DisplacementsCoordinateTransformation.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/DisplacementsCoordinateTransformation.java index be15b8f1..2d2fabdd 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/DisplacementsCoordinateTransformation.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/DisplacementsCoordinateTransformation.java @@ -2,21 +2,33 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; public final class DisplacementsCoordinateTransformation extends BaseCoordinateTransformation { @Nullable public final String path; + /** Interpolation method for the {@code path} array, e.g. {@code nearest}, {@code linear} (default), {@code bspline-cubic}. */ + @Nullable public final String interpolation; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public DisplacementsCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, - @Nullable @JsonProperty("path") String path + @Nullable @JsonProperty("path") String path, + @Nullable @JsonProperty("interpolation") String interpolation ) { super("displacements", input, output, name); this.path = path; + this.interpolation = interpolation; + } + + public DisplacementsCoordinateTransformation( + @Nullable CoordinateSystemRef input, + @Nullable CoordinateSystemRef output, + @Nullable String name, + @Nullable String path + ) { + this(input, output, name, path, null); } } diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/GenericCoordinateTransformation.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/GenericCoordinateTransformation.java index b7d02623..110058c0 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/GenericCoordinateTransformation.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/GenericCoordinateTransformation.java @@ -1,9 +1,10 @@ package dev.zarr.conventions.ome.v0_6.metadata.transform; -import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.LinkedHashMap; @@ -16,13 +17,14 @@ * rather than rejecting the entire OME payload. */ public final class GenericCoordinateTransformation extends BaseCoordinateTransformation { + @JsonIgnore public final Map raw = new LinkedHashMap<>(); @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public GenericCoordinateTransformation( @JsonProperty(value = "type", required = true) String type, - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name ) { super(type, input, output, name); @@ -32,4 +34,9 @@ public GenericCoordinateTransformation( public void capture(String key, Object value) { raw.put(key, value); } + + @JsonAnyGetter + Map rawProperties() { + return raw; + } } diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/IdentityCoordinateTransformation.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/IdentityCoordinateTransformation.java index 859b6f6b..9d2ac1ae 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/IdentityCoordinateTransformation.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/IdentityCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; @@ -10,14 +9,14 @@ public final class IdentityCoordinateTransformation extends dev.zarr.conventions.ome.metadata.transform.IdentityCoordinateTransformation implements CoordinateTransformation { - @Nullable public final String input; - @Nullable public final String output; + @Nullable public final CoordinateSystemRef input; + @Nullable public final CoordinateSystemRef output; @Nullable public final String name; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public IdentityCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("path") String path ) { @@ -33,12 +32,12 @@ public String getType() { } @Override - public String getInput() { + public CoordinateSystemRef getInput() { return input; } @Override - public String getOutput() { + public CoordinateSystemRef getOutput() { return output; } diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/MapAxisCoordinateTransformation.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/MapAxisCoordinateTransformation.java index e5a5fcf9..d03026dc 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/MapAxisCoordinateTransformation.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/MapAxisCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.List; @@ -13,8 +12,8 @@ public final class MapAxisCoordinateTransformation extends BaseCoordinateTransfo @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public MapAxisCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("mapAxis") List mapAxis, @Nullable @JsonProperty("transformation") CoordinateTransformation transformation diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/ProjectAxisCoordinateTransformation.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/ProjectAxisCoordinateTransformation.java new file mode 100644 index 00000000..7a40aa03 --- /dev/null +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/ProjectAxisCoordinateTransformation.java @@ -0,0 +1,31 @@ +package dev.zarr.conventions.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import java.util.List; + +/** + * Projects input coordinates from N to M dimensions by dropping input dimensions and/or + * creating new output dimensions. + */ +public final class ProjectAxisCoordinateTransformation extends BaseCoordinateTransformation { + /** Indices of the input coordinate vector at which dimensions are dropped. */ + @Nullable public final List droppedInputs; + /** Indices of the output coordinate vector at which new dimensions are added. */ + @Nullable public final List createdOutputs; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public ProjectAxisCoordinateTransformation( + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, + @Nullable @JsonProperty("name") String name, + @Nullable @JsonProperty("droppedInputs") List droppedInputs, + @Nullable @JsonProperty("createdOutputs") List createdOutputs + ) { + super("projectAxis", input, output, name); + this.droppedInputs = droppedInputs; + this.createdOutputs = createdOutputs; + } +} diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/RotationCoordinateTransformation.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/RotationCoordinateTransformation.java index 55d60e47..112a6687 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/RotationCoordinateTransformation.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/RotationCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.List; @@ -13,8 +12,8 @@ public final class RotationCoordinateTransformation extends BaseCoordinateTransf @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public RotationCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("rotation") List> rotation, @Nullable @JsonProperty("path") String path diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/ScaleCoordinateTransformation.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/ScaleCoordinateTransformation.java index 69b97a80..5d305c38 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/ScaleCoordinateTransformation.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/ScaleCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.List; @@ -11,14 +10,14 @@ public final class ScaleCoordinateTransformation extends dev.zarr.conventions.ome.metadata.transform.ScaleCoordinateTransformation implements CoordinateTransformation { - @Nullable public final String input; - @Nullable public final String output; + @Nullable public final CoordinateSystemRef input; + @Nullable public final CoordinateSystemRef output; @Nullable public final String name; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public ScaleCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("scale") List scale, @Nullable @JsonProperty("path") String path @@ -35,12 +34,12 @@ public String getType() { } @Override - public String getInput() { + public CoordinateSystemRef getInput() { return input; } @Override - public String getOutput() { + public CoordinateSystemRef getOutput() { return output; } diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/SequenceCoordinateTransformation.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/SequenceCoordinateTransformation.java index d5779648..4a9809b8 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/SequenceCoordinateTransformation.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/SequenceCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.List; @@ -12,8 +11,8 @@ public final class SequenceCoordinateTransformation extends BaseCoordinateTransf @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public SequenceCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("transformations") List transformations ) { diff --git a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/TranslationCoordinateTransformation.java b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/TranslationCoordinateTransformation.java index c6eae2d4..75346679 100644 --- a/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/TranslationCoordinateTransformation.java +++ b/src/main/java/dev/zarr/conventions/ome/v0_6/metadata/transform/TranslationCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.List; @@ -11,14 +10,14 @@ public final class TranslationCoordinateTransformation extends dev.zarr.conventions.ome.metadata.transform.TranslationCoordinateTransformation implements CoordinateTransformation { - @Nullable public final String input; - @Nullable public final String output; + @Nullable public final CoordinateSystemRef input; + @Nullable public final CoordinateSystemRef output; @Nullable public final String name; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public TranslationCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("translation") List translation, @Nullable @JsonProperty("path") String path @@ -35,12 +34,12 @@ public String getType() { } @Override - public String getInput() { + public CoordinateSystemRef getInput() { return input; } @Override - public String getOutput() { + public CoordinateSystemRef getOutput() { return output; } diff --git a/src/test/java/dev/zarr/conventions/ome/OmeObjectMappersTest.java b/src/test/java/dev/zarr/conventions/ome/OmeObjectMappersTest.java index e6fde49c..88c4e64b 100644 --- a/src/test/java/dev/zarr/conventions/ome/OmeObjectMappersTest.java +++ b/src/test/java/dev/zarr/conventions/ome/OmeObjectMappersTest.java @@ -197,8 +197,9 @@ void v2MapperParsesUnknownV06TransformAsGenericAndPreservesRawFields() { (dev.zarr.conventions.ome.v0_6.metadata.transform.GenericCoordinateTransformation) entry.datasets.get(0).coordinateTransformations.get(0); assertEquals("customNonLinear", generic.type); - assertEquals("s0", generic.input); - assertEquals("physical", generic.output); + // legacy bare-string refs: dataset input -> path, output -> name + assertEquals(dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef.ofPath("s0"), generic.input); + assertEquals(dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef.ofName("physical"), generic.output); assertEquals("custom-stage", generic.name); assertEquals(Arrays.asList(1, 4, 9), generic.raw.get("lut")); assertEquals(extension, generic.raw.get("extension")); @@ -336,8 +337,10 @@ void v3MapperParsesV06SceneMetadataRefsAndNestedTransforms() { dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation parsedTranslation = seq.transformations.get(0); assertEquals("translation", parsedTranslation.getType()); - assertEquals("imgA#physical", parsedTranslation.getInput()); - assertEquals(".#world", parsedTranslation.getOutput()); + assertEquals(dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef.of("physical", "imgA"), + parsedTranslation.getInput()); + assertEquals(dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef.ofName("world"), + parsedTranslation.getOutput()); dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation parsedByDim = seq.transformations.get(1); diff --git a/src/test/java/dev/zarr/conventions/ome/OmeZarrBaseTest.java b/src/test/java/dev/zarr/conventions/ome/OmeZarrBaseTest.java index d1dfbc3c..f354e833 100644 --- a/src/test/java/dev/zarr/conventions/ome/OmeZarrBaseTest.java +++ b/src/test/java/dev/zarr/conventions/ome/OmeZarrBaseTest.java @@ -40,6 +40,21 @@ protected StoreHandle storeHandle(Path path) throws Exception { return new FilesystemStore(path).resolve(); } + /** + * Parses {@code omeJson} (the value of {@code attributes.ome}) into {@code cls} with the OME + * reader, serializes it back with the writer used for zarr.json and asserts that the JSON trees + * are equal, i.e. nothing was dropped or renamed. Returns the parsed object. + */ + protected static T assertOmeRoundTrip(String omeJson, Class cls) throws Exception { + com.fasterxml.jackson.databind.ObjectMapper reader = OmeObjectMappers.makeV3Mapper(); + com.fasterxml.jackson.databind.JsonNode expected = reader.readTree(omeJson); + T parsed = reader.treeToValue(expected, cls); + com.fasterxml.jackson.databind.JsonNode actual = + dev.zarr.zarrjava.v3.Node.makeObjectMapper().valueToTree(parsed); + assertEquals(expected, actual); + return parsed; + } + // ── unified interface contract tests ───────────────────────────────────── @Test diff --git a/src/test/java/dev/zarr/conventions/ome/OmeZarrSceneV06Test.java b/src/test/java/dev/zarr/conventions/ome/OmeZarrSceneV06Test.java index 75a81f68..1f37429e 100644 --- a/src/test/java/dev/zarr/conventions/ome/OmeZarrSceneV06Test.java +++ b/src/test/java/dev/zarr/conventions/ome/OmeZarrSceneV06Test.java @@ -1,6 +1,7 @@ package dev.zarr.conventions.ome; import dev.zarr.zarrjava.store.FilesystemStore; +import dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef; import dev.zarr.zarrjava.store.StoreHandle; import org.junit.jupiter.api.Test; @@ -57,8 +58,8 @@ void openSceneAndNavigateImages() throws Exception { dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation top = scene.getSceneMetadata().coordinateTransformations.get(0); assertEquals("bijection", top.getType()); - assertEquals("JRC2018F#physical", top.getInput()); - assertEquals("FCWB#physical", top.getOutput()); + assertEquals(CoordinateSystemRef.of("physical", "JRC2018F"), top.getInput()); + assertEquals(CoordinateSystemRef.of("physical", "FCWB"), top.getOutput()); assertTrue(scene.listImageNodes().contains("FCWB")); assertTrue(scene.listImageNodes().contains("JRC2018F")); @@ -91,7 +92,7 @@ void createAndReopenScene() throws Exception { dev.zarr.conventions.ome.v0_6.metadata.SceneMetadata sceneMetadata = new dev.zarr.conventions.ome.v0_6.metadata.SceneMetadata( Collections.singletonList(new dev.zarr.conventions.ome.v0_6.metadata.transform.TranslationCoordinateTransformation( - "imageA#physical", ".#world", "imageA to world", Arrays.asList(1.0, 2.0), null)), + CoordinateSystemRef.of("physical", "imageA"), CoordinateSystemRef.ofName("world"), "imageA to world", Arrays.asList(1.0, 2.0), null)), Collections.singletonList(new dev.zarr.conventions.ome.v0_6.metadata.CoordinateSystem( "world", Arrays.asList(y, x))) ); @@ -105,7 +106,7 @@ void createAndReopenScene() throws Exception { "s0", Collections.singletonList( dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation.scale( - Arrays.asList(1.0, 1.0), "s0", "physical")))), + Arrays.asList(1.0, 1.0), CoordinateSystemRef.ofPath("s0"), CoordinateSystemRef.ofName("physical"))))), null, Collections.singletonList(new dev.zarr.conventions.ome.v0_6.metadata.CoordinateSystem( "physical", Arrays.asList(y, x))), @@ -125,7 +126,7 @@ void createAndReopenScene() throws Exception { dev.zarr.conventions.ome.v0_6.Scene reopened = dev.zarr.conventions.ome.v0_6.Scene.openScene(root); assertEquals(Collections.singletonList("imageA"), reopened.listImageNodes()); assertEquals("world", reopened.getSceneMetadata().coordinateSystems.get(0).name); - assertEquals("imageA#physical", reopened.getSceneMetadata().coordinateTransformations.get(0).getInput()); + assertEquals(CoordinateSystemRef.of("physical", "imageA"), reopened.getSceneMetadata().coordinateTransformations.get(0).getInput()); dev.zarr.conventions.ome.v0_6.MultiscaleImage imageA = reopened.openImageNode("imageA"); assertEquals(1, imageA.getScaleLevelCount()); @@ -149,8 +150,8 @@ void openSceneExample1AffinePathBetweenInstruments() throws Exception { dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation ct = scene.getSceneMetadata().coordinateTransformations.get(0); assertEquals("affine", ct.getType()); - assertEquals("sampleA_instrument2#physical_instrument2", ct.getInput()); - assertEquals("sampleA_instrument1#physical_instrument1", ct.getOutput()); + assertEquals(CoordinateSystemRef.of("physical_instrument2", "sampleA_instrument2"), ct.getInput()); + assertEquals(CoordinateSystemRef.of("physical_instrument1", "sampleA_instrument1"), ct.getOutput()); assertTrue(ct instanceof dev.zarr.conventions.ome.v0_6.metadata.transform.AffineCoordinateTransformation); assertEquals("coordinateTransformations/sampleA_instrument2-to-instrument1", ((dev.zarr.conventions.ome.v0_6.metadata.transform.AffineCoordinateTransformation) ct).path); @@ -179,10 +180,10 @@ void openSceneExample2TwoAffineLinksViaInstrument2() throws Exception { scene.getSceneMetadata().coordinateTransformations.get(1); assertEquals("affine", t0.getType()); assertEquals("affine", t1.getType()); - assertEquals("instrument1#physical", t0.getInput()); - assertEquals("instrument2#physical", t0.getOutput()); - assertEquals("instrument3#physical", t1.getInput()); - assertEquals("instrument2#physical", t1.getOutput()); + assertEquals(CoordinateSystemRef.of("physical", "instrument1"), t0.getInput()); + assertEquals(CoordinateSystemRef.of("physical", "instrument2"), t0.getOutput()); + assertEquals(CoordinateSystemRef.of("physical", "instrument3"), t1.getInput()); + assertEquals(CoordinateSystemRef.of("physical", "instrument2"), t1.getOutput()); assertTrue(t0 instanceof dev.zarr.conventions.ome.v0_6.metadata.transform.AffineCoordinateTransformation); assertTrue(((dev.zarr.conventions.ome.v0_6.metadata.transform.AffineCoordinateTransformation) t0).affine.size() > 0); diff --git a/src/test/java/dev/zarr/conventions/ome/OmeZarrV05Test.java b/src/test/java/dev/zarr/conventions/ome/OmeZarrV05Test.java index 9d592c39..2ec7a019 100644 --- a/src/test/java/dev/zarr/conventions/ome/OmeZarrV05Test.java +++ b/src/test/java/dev/zarr/conventions/ome/OmeZarrV05Test.java @@ -24,7 +24,9 @@ import java.net.URI; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @@ -188,8 +190,11 @@ void writeLabelsRoundTrip() throws Exception { StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v05_labels")); dev.zarr.conventions.ome.v0_5.MultiscaleImage.create(handle, new MultiscalesEntry(axes, Collections.emptyList())); + Map labelsOme = new LinkedHashMap<>(); + labelsOme.put("version", "0.5"); + labelsOme.put("labels", Arrays.asList("nuclei")); Attributes labelsAttrs = new Attributes(); - labelsAttrs.put("labels", Arrays.asList("nuclei")); + labelsAttrs.put("ome", labelsOme); dev.zarr.zarrjava.v3.Group.create(handle.resolve("labels"), labelsAttrs); dev.zarr.conventions.ome.v0_5.MultiscaleImage nuclei = dev.zarr.conventions.ome.v0_5.MultiscaleImage.create( @@ -203,6 +208,75 @@ void writeLabelsRoundTrip() throws Exception { assertEquals(Arrays.asList("z", "y"), reopened.openLabel("nuclei").getAxisNames()); } + @Test + void labelsListLegacyLayout() throws Exception { + // Older files put "labels" directly under attributes instead of attributes.ome. + List axes = Arrays.asList( + new Axis("y", "space", "micrometer"), + new Axis("x", "space", "micrometer")); + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v05_labels_legacy")); + dev.zarr.conventions.ome.v0_5.MultiscaleImage.create(handle, new MultiscalesEntry(axes, Collections.emptyList())); + Attributes labelsAttrs = new Attributes(); + labelsAttrs.put("labels", Arrays.asList("cells", "nuclei")); + dev.zarr.zarrjava.v3.Group.create(handle.resolve("labels"), labelsAttrs); + + assertEquals(Arrays.asList("cells", "nuclei"), MultiscaleImage.open(handle).getLabels()); + } + + @Test + void labelsGroupAndImageLabelRoundTrip() throws Exception { + String labelsGroupJson = "{\"version\":\"0.5\",\"labels\":[\"nuclei\"]}"; + String labelImageJson = "{\"version\":\"0.5\"," + + "\"multiscales\":[{\"axes\":[{\"name\":\"y\"},{\"name\":\"x\"}]," + + "\"datasets\":[{\"path\":\"0\",\"coordinateTransformations\":[{\"type\":\"scale\",\"scale\":[1.0,1.0]}]}]}]," + + "\"image-label\":{" + + "\"colors\":[{\"label-value\":0,\"rgba\":[0,0,128,128]},{\"label-value\":1,\"rgba\":[0,128,0,128],\"note\":\"x\"}]," + + "\"properties\":[{\"label-value\":0,\"class\":\"background\"},{\"label-value\":1,\"area (pixels)\":1650,\"cell type\":\"neuron\"}]," + + "\"source\":{\"image\":\"../../\"}}}"; + assertOmeRoundTrip(labelsGroupJson, dev.zarr.conventions.ome.metadata.OmeMetadata.class); + dev.zarr.conventions.ome.metadata.OmeMetadata parsed = + assertOmeRoundTrip(labelImageJson, dev.zarr.conventions.ome.metadata.OmeMetadata.class); + assertEquals(2, parsed.imageLabel.colors.size()); + assertEquals(1, parsed.imageLabel.colors.get(1).labelValue); + assertEquals("x", parsed.imageLabel.colors.get(1).getAdditionalProperties().get("note")); + assertEquals("neuron", parsed.imageLabel.properties.get(1).getAdditionalProperties().get("cell type")); + assertEquals("../../", parsed.imageLabel.source.image); + } + + @Test + void imageLabelSourceDefaultNotWritten() throws Exception { + String json = "{\"version\":\"0.5\",\"image-label\":{\"source\":{}}}"; + dev.zarr.conventions.ome.metadata.OmeMetadata parsed = + assertOmeRoundTrip(json, dev.zarr.conventions.ome.metadata.OmeMetadata.class); + assertNull(parsed.imageLabel.source.image); + assertEquals("../../", parsed.imageLabel.source.resolveImage()); + } + + @Test + void imageLabelPreservedOnCreateScaleLevel() throws Exception { + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v05_image_label_preserved")); + dev.zarr.conventions.ome.v0_5.MultiscaleImage.create(handle, new MultiscalesEntry( + Arrays.asList(new Axis("y", "space", null), new Axis("x", "space", null)), Collections.emptyList())); + // Add image-label out of band, then reopen and append a scale level through the API. + Attributes attrs = dev.zarr.zarrjava.v3.Group.open(handle).metadata.attributes; + @SuppressWarnings("unchecked") + Map ome = (Map) attrs.get("ome"); + ome.put("image-label", Collections.singletonMap("colors", + Collections.singletonList(Collections.singletonMap("label-value", 3)))); + dev.zarr.zarrjava.v3.Group.open(handle).setAttributes(attrs); + + dev.zarr.conventions.ome.v0_5.MultiscaleImage image = + dev.zarr.conventions.ome.v0_5.MultiscaleImage.openMultiscaleImage(handle); + image.createScaleLevel("0", + Array.metadataBuilder().withShape(4, 4).withChunkShape(4, 4).withDataType(DataType.UINT8).build(), + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))); + + dev.zarr.conventions.ome.v0_5.MultiscaleImage reopened = + dev.zarr.conventions.ome.v0_5.MultiscaleImage.openMultiscaleImage(handle); + assertNotNull(reopened.getImageLabel()); + assertEquals(3, reopened.getImageLabel().colors.get(0).labelValue); + } + @Test void writePlateRoundTrip() throws Exception { PlateMetadata meta = new PlateMetadata( diff --git a/src/test/java/dev/zarr/conventions/ome/OmeZarrV06CoordinateSystemRefTest.java b/src/test/java/dev/zarr/conventions/ome/OmeZarrV06CoordinateSystemRefTest.java new file mode 100644 index 00000000..d9aafc47 --- /dev/null +++ b/src/test/java/dev/zarr/conventions/ome/OmeZarrV06CoordinateSystemRefTest.java @@ -0,0 +1,288 @@ +package dev.zarr.conventions.ome; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.ZarrTest; +import dev.zarr.conventions.ome.metadata.Axis; +import dev.zarr.conventions.ome.v0_6.metadata.CoordinateSystem; +import dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata; +import dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef; +import dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation; +import dev.zarr.conventions.ome.v0_6.metadata.transform.SequenceCoordinateTransformation; +import dev.zarr.zarrjava.store.FilesystemStore; +import dev.zarr.zarrjava.store.StoreHandle; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** OME-Zarr 0.6 coordinate system references ({@code input}/{@code output} as {@code {name, path}} objects). */ +public class OmeZarrV06CoordinateSystemRefTest extends ZarrTest { + + private static final String SPEC_MULTISCALES_JSON = "{" + + "\"version\": \"0.6\"," + + "\"multiscales\": [{" + + " \"name\": \"example\"," + + " \"coordinateSystems\": [" + + " {\"name\": \"physical\", \"axes\": [" + + " {\"name\": \"y\", \"type\": \"space\", \"unit\": \"micrometer\"}," + + " {\"name\": \"x\", \"type\": \"space\", \"unit\": \"micrometer\"}]}," + + " {\"name\": \"output\", \"axes\": [" + + " {\"name\": \"y\", \"type\": \"space\", \"unit\": \"micrometer\"}," + + " {\"name\": \"x\", \"type\": \"space\", \"unit\": \"micrometer\"}]}" + + " ]," + + " \"datasets\": [" + + " {\"path\": \"s0\", \"coordinateTransformations\": [{" + + " \"type\": \"scale\", \"scale\": [0.5, 0.5]," + + " \"input\": {\"path\": \"s0\"}, \"output\": {\"name\": \"physical\"}}]}," + + " {\"path\": \"s1\", \"coordinateTransformations\": [{" + + " \"type\": \"sequence\"," + + " \"input\": {\"path\": \"s1\"}, \"output\": {\"name\": \"physical\"}," + + " \"transformations\": [" + + " {\"type\": \"scale\", \"scale\": [1.0, 1.0]}," + + " {\"type\": \"translation\", \"translation\": [0.25, 0.25]}]}]}" + + " ]," + + " \"coordinateTransformations\": [" + + " {\"type\": \"identity\", \"input\": {\"name\": \"physical\"}, \"output\": {\"name\": \"output\"}}," + + " {\"type\": \"scale\", \"scale\": [2.0, 2.0]," + + " \"input\": {\"name\": \"physical\"}, \"output\": {\"name\": \"physical\", \"path\": \"labels/cells\"}}" + + " ]" + + "}]}"; + + private static final String SPEC_SCENE_JSON = "{" + + "\"version\": \"0.6\"," + + "\"scene\": {" + + " \"coordinateSystems\": [{\"name\": \"world\", \"axes\": [" + + " {\"name\": \"y\", \"type\": \"space\", \"unit\": \"micrometer\"}," + + " {\"name\": \"x\", \"type\": \"space\", \"unit\": \"micrometer\"}]}]," + + " \"coordinateTransformations\": [" + + " {\"type\": \"translation\", \"name\": \"A to B\", \"translation\": [1.0, 2.0]," + + " \"input\": {\"name\": \"physical\", \"path\": \"imageA\"}," + + " \"output\": {\"name\": \"physical\", \"path\": \"imageB\"}}," + + " {\"type\": \"identity\"," + + " \"input\": {\"name\": \"physical\", \"path\": \"imageB\"}," + + " \"output\": {\"name\": \"world\"}}" + + " ]" + + "}}"; + + private static final ObjectMapper JSON = new ObjectMapper(); + + private static JsonNode roundTrip(String json) throws Exception { + OmeMetadata parsed = OmeObjectMappers.makeV3Mapper().readValue(json, OmeMetadata.class); + return dev.zarr.zarrjava.v3.Node.makeObjectMapper().valueToTree(parsed); + } + + private static JsonNode readOmeAttributes(Path groupPath) throws Exception { + return JSON.readTree(Files.readAllBytes(groupPath.resolve("zarr.json"))).get("attributes").get("ome"); + } + + @Test + void specMultiscalesJsonRoundTrips() throws Exception { + assertEquals(JSON.readTree(SPEC_MULTISCALES_JSON), roundTrip(SPEC_MULTISCALES_JSON)); + } + + @Test + void specSceneJsonRoundTrips() throws Exception { + assertEquals(JSON.readTree(SPEC_SCENE_JSON), roundTrip(SPEC_SCENE_JSON)); + } + + @Test + void specRefsAreParsedIntoNameAndPath() throws Exception { + OmeMetadata parsed = OmeObjectMappers.makeV3Mapper().readValue(SPEC_SCENE_JSON, OmeMetadata.class); + CoordinateTransformation t = parsed.scene.coordinateTransformations.get(0); + assertEquals(CoordinateSystemRef.of("physical", "imageA"), t.getInput()); + assertEquals(CoordinateSystemRef.of("physical", "imageB"), t.getOutput()); + assertEquals(CoordinateSystemRef.ofName("world"), parsed.scene.coordinateTransformations.get(1).getOutput()); + + OmeMetadata ms = OmeObjectMappers.makeV3Mapper().readValue(SPEC_MULTISCALES_JSON, OmeMetadata.class); + CoordinateTransformation ds = ms.multiscales.get(0).datasets.get(0).coordinateTransformations.get(0); + assertEquals(CoordinateSystemRef.ofPath("s0"), ds.getInput()); + assertEquals(CoordinateSystemRef.ofName("physical"), ds.getOutput()); + } + + @Test + void sceneFileWrittenAndReopenedKeepsObjectRefsAndResolvesGraph() throws Exception { + Path out = TESTOUTPUT.resolve("ome_v06_scene_object_refs"); + StoreHandle root = new FilesystemStore(out).resolve(); + OmeMetadata parsed = OmeObjectMappers.makeV3Mapper().readValue(SPEC_SCENE_JSON, OmeMetadata.class); + dev.zarr.conventions.ome.v0_6.Scene.createScene(root, parsed.scene); + for (String image : Arrays.asList("imageA", "imageB")) { + dev.zarr.conventions.ome.v0_6.MultiscaleImage.create(root.resolve(image), physicalEntry()); + } + + assertEquals(JSON.readTree(SPEC_SCENE_JSON), readOmeAttributes(out)); + + dev.zarr.conventions.ome.v0_6.Scene scene = dev.zarr.conventions.ome.v0_6.Scene.openScene(root); + dev.zarr.conventions.ome.v0_6.SceneTransformationGraph graph = scene.getCoordinateTransformationGraph(); + assertTrue(graph.warnings.isEmpty(), graph.warnings.toString()); + assertEquals(3, graph.nodes.size()); + assertEquals(2, graph.edges.size()); + assertEquals("imageA#physical", graph.edges.get(0).inputNodeId); + assertEquals("imageB#physical", graph.edges.get(0).outputNodeId); + assertEquals(".#world", graph.edges.get(1).outputNodeId); + } + + @Test + void createScaleLevelWritesDatasetInputPathAndIntrinsicOutputName() throws Exception { + Path out = TESTOUTPUT.resolve("ome_v06_create_scale_level_refs"); + StoreHandle handle = new FilesystemStore(out).resolve(); + dev.zarr.conventions.ome.v0_6.MultiscaleImage image = + dev.zarr.conventions.ome.v0_6.MultiscaleImage.create(handle, physicalEntry()); + image.createScaleLevel("s0", arrayMetadata(16), + Collections.singletonList(dev.zarr.conventions.ome.metadata.transform.CoordinateTransformation.scale( + Arrays.asList(1.0, 1.0)))); + image.createScaleLevel("s1", arrayMetadata(8), + Arrays.asList( + dev.zarr.conventions.ome.metadata.transform.CoordinateTransformation.scale( + Arrays.asList(2.0, 2.0)), + dev.zarr.conventions.ome.metadata.transform.CoordinateTransformation.translation( + Arrays.asList(0.5, 0.5)))); + + JsonNode datasets = readOmeAttributes(out).get("multiscales").get(0).get("datasets"); + JsonNode s0 = datasets.get(0).get("coordinateTransformations"); + assertEquals(1, s0.size()); + assertEquals("scale", s0.get(0).get("type").asText()); + assertEquals(JSON.readTree("{\"path\": \"s0\"}"), s0.get(0).get("input")); + assertEquals(JSON.readTree("{\"name\": \"physical\"}"), s0.get(0).get("output")); + + // multiple transformations are wrapped in a sequence; nested ones carry no input/output + JsonNode s1 = datasets.get(1).get("coordinateTransformations"); + assertEquals(1, s1.size()); + assertEquals("sequence", s1.get(0).get("type").asText()); + assertEquals(JSON.readTree("{\"path\": \"s1\"}"), s1.get(0).get("input")); + assertEquals(JSON.readTree("{\"name\": \"physical\"}"), s1.get(0).get("output")); + JsonNode nested = s1.get(0).get("transformations"); + assertEquals(2, nested.size()); + for (JsonNode n : nested) { + assertFalse(n.has("input")); + assertFalse(n.has("output")); + } + + // version-independent view keeps the flat [scale, translation] list + MultiscaleImage reopened = MultiscaleImage.open(handle); + List generic = + reopened.getMultiscaleNode(0).datasets.get(1).coordinateTransformations; + assertEquals(2, generic.size()); + assertEquals("scale", generic.get(0).type); + assertEquals("translation", generic.get(1).type); + } + + @Test + void createScaleLevelUsesExistingDatasetOutputName() throws Exception { + Path out = TESTOUTPUT.resolve("ome_v06_create_scale_level_existing_output"); + StoreHandle handle = new FilesystemStore(out).resolve(); + List axes = yx(); + dev.zarr.conventions.ome.v0_6.metadata.MultiscalesEntry entry = + new dev.zarr.conventions.ome.v0_6.metadata.MultiscalesEntry( + Collections.singletonList(new dev.zarr.conventions.ome.v0_6.metadata.Dataset("s0", + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0), + CoordinateSystemRef.ofPath("s0"), CoordinateSystemRef.ofName("intrinsic"))))), + Arrays.asList(new CoordinateSystem("other", axes), new CoordinateSystem("intrinsic", axes)), + "multiscales"); + dev.zarr.conventions.ome.v0_6.MultiscaleImage image = + dev.zarr.conventions.ome.v0_6.MultiscaleImage.create(handle, entry); + image.createScaleLevel("s1", arrayMetadata(8), + Collections.singletonList(dev.zarr.conventions.ome.metadata.transform.CoordinateTransformation.scale( + Arrays.asList(2.0, 2.0)))); + + CoordinateTransformation written = image.getMultiscalesEntry(0).datasets.get(1).coordinateTransformations.get(0); + assertEquals(CoordinateSystemRef.ofPath("s1"), written.getInput()); + assertEquals(CoordinateSystemRef.ofName("intrinsic"), written.getOutput()); + } + + @Test + void createScaleLevelWithoutCoordinateSystemsFails() throws Exception { + StoreHandle handle = new FilesystemStore(TESTOUTPUT.resolve("ome_v06_create_scale_level_no_cs")).resolve(); + dev.zarr.conventions.ome.v0_6.MultiscaleImage image = + dev.zarr.conventions.ome.v0_6.MultiscaleImage.create(handle, + new dev.zarr.conventions.ome.v0_6.metadata.MultiscalesEntry( + Collections.emptyList(), + null, "multiscales")); + ZarrException ex = assertThrows(ZarrException.class, () -> image.createScaleLevel("s0", arrayMetadata(8), + Collections.singletonList(dev.zarr.conventions.ome.metadata.transform.CoordinateTransformation.scale( + Arrays.asList(1.0, 1.0))))); + assertTrue(ex.getMessage().contains("intrinsic coordinate system")); + } + + @Test + void legacyStringFixtureOpensAndIsWrittenBackAsObjects() throws Exception { + // written in the pre-release string form ("input": "s0", "output": "physical") of 0.6.dev3 + Path fixture = TESTDATA.resolve("ome/v0.6_legacy/scale_multiscale.zarr"); + JsonNode raw = readOmeAttributes(fixture).get("multiscales").get(0).get("datasets").get(0) + .get("coordinateTransformations").get(0); + assertTrue(raw.get("input").isTextual()); + + dev.zarr.conventions.ome.v0_6.MultiscaleImage image = + (dev.zarr.conventions.ome.v0_6.MultiscaleImage) MultiscaleImage.open( + new FilesystemStore(fixture).resolve()); + CoordinateTransformation ct = image.getMultiscalesEntry(0).datasets.get(0).coordinateTransformations.get(0); + assertEquals(CoordinateSystemRef.ofPath("s0"), ct.getInput()); + assertEquals(CoordinateSystemRef.ofName("physical"), ct.getOutput()); + + OmeMetadata reparsed = OmeObjectMappers.makeV3Mapper().convertValue( + readOmeAttributes(fixture), OmeMetadata.class); + JsonNode rewritten = dev.zarr.zarrjava.v3.Node.makeObjectMapper().valueToTree(reparsed) + .get("multiscales").get(0).get("datasets").get(0).get("coordinateTransformations").get(0); + assertEquals(JSON.readTree("{\"path\": \"s0\"}"), rewritten.get("input")); + assertEquals(JSON.readTree("{\"name\": \"physical\"}"), rewritten.get("output")); + } + + @Test + void legacyStringRefsOutsideDatasetsAreNames() throws Exception { + String json = "{\"version\": \"0.6\", \"multiscales\": [{" + + "\"coordinateSystems\": []," + + "\"datasets\": []," + + "\"coordinateTransformations\": [{\"type\": \"identity\", \"input\": \"physical\", \"output\": \"array\"}]" + + "}], \"scene\": {\"coordinateTransformations\": [" + + "{\"type\": \"identity\", \"input\": \"imgA#physical\", \"output\": \".#world\"}]}}"; + OmeMetadata parsed = OmeObjectMappers.makeV3Mapper().readValue(json, OmeMetadata.class); + CoordinateTransformation ms = parsed.multiscales.get(0).coordinateTransformations.get(0); + assertEquals(CoordinateSystemRef.ofName("physical"), ms.getInput()); + assertEquals(CoordinateSystemRef.ofName("array"), ms.getOutput()); + // "#" strings written by earlier zarr-java versions + CoordinateTransformation scene = parsed.scene.coordinateTransformations.get(0); + assertEquals(CoordinateSystemRef.of("physical", "imgA"), scene.getInput()); + assertEquals(CoordinateSystemRef.ofName("world"), scene.getOutput()); + } + + @Test + void sequenceFactoryAndNestedTransformsHaveNoRefs() { + CoordinateTransformation seq = CoordinateTransformation.sequence( + Arrays.asList(CoordinateTransformation.scale(Arrays.asList(2.0, 2.0)), + CoordinateTransformation.translation(Arrays.asList(0.5, 0.5))), + CoordinateSystemRef.ofPath("s1"), CoordinateSystemRef.ofName("physical")); + JsonNode tree = dev.zarr.zarrjava.v3.Node.makeObjectMapper().valueToTree(seq); + assertEquals(JSON.createObjectNode().put("path", "s1"), tree.get("input")); + assertEquals(JSON.createObjectNode().put("name", "physical"), tree.get("output")); + for (JsonNode n : tree.get("transformations")) { + assertFalse(n.has("input")); + assertFalse(n.has("output")); + } + assertEquals(2, ((SequenceCoordinateTransformation) seq).transformations.size()); + } + + private static List yx() { + return Arrays.asList(new Axis("y", "space", "micrometer"), new Axis("x", "space", "micrometer")); + } + + private static dev.zarr.conventions.ome.v0_6.metadata.MultiscalesEntry physicalEntry() { + return new dev.zarr.conventions.ome.v0_6.metadata.MultiscalesEntry( + Collections.emptyList(), + Collections.singletonList(new CoordinateSystem("physical", yx())), + "multiscales"); + } + + private static dev.zarr.zarrjava.v3.ArrayMetadata arrayMetadata(int size) throws ZarrException { + return dev.zarr.zarrjava.v3.Array.metadataBuilder() + .withShape(size, size) + .withChunkShape(8, 8) + .withDataType(dev.zarr.zarrjava.v3.DataType.UINT16) + .build(); + } +} diff --git a/src/test/java/dev/zarr/conventions/ome/OmeZarrV06RoundTripTest.java b/src/test/java/dev/zarr/conventions/ome/OmeZarrV06RoundTripTest.java new file mode 100644 index 00000000..5a9bb62d --- /dev/null +++ b/src/test/java/dev/zarr/conventions/ome/OmeZarrV06RoundTripTest.java @@ -0,0 +1,162 @@ +package dev.zarr.conventions.ome; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.zarr.zarrjava.ZarrTest; +import dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Reads every OME-Zarr v0.6 metadata document we have test data for and checks that writing it + * back yields the same JSON. + */ +public class OmeZarrV06RoundTripTest extends ZarrTest { + + private static final Path RFC5_EXAMPLES = TESTDATA.resolve("ome/v0.6/examples"); + private static final Path NGFF_SPEC_TESTS = TESTDATA.resolve("ome/v0.6/ngff-spec/tests/attributes"); + private static final Path SCENE_EXAMPLES = TESTDATA.resolve("ome/v0.6_scene"); + + private static final ObjectMapper RAW = new ObjectMapper(); + + static Stream rfc5Examples() throws IOException { + List out = new ArrayList<>(); + for (Path zarrJson : zarrJsonFiles(RFC5_EXAMPLES)) { + JsonNode node = RAW.readTree(zarrJson.toFile()); + // Displacement/coordinate field arrays in these examples still carry + // ome.coordinateSystems directly on the array; final v0.6 stores fields as + // multiscales groups, so only groups are OME documents here. + if (!"group".equals(node.path("node_type").asText()) || !node.path("attributes").has("ome")) { + continue; + } + out.add(Arguments.of(RFC5_EXAMPLES.relativize(zarrJson.getParent()).toString(), + node.path("attributes").get("ome"))); + } + return out.stream(); + } + + /** The {@code valid} attribute documents of the official ngff-spec conformance suite. */ + static Stream ngffSpecValidAttributes() throws IOException { + List out = new ArrayList<>(); + for (String flavor : new String[]{"spec", "strict"}) { + Path validDir = NGFF_SPEC_TESTS.resolve(flavor).resolve("valid"); + try (Stream files = Files.walk(validDir)) { + for (Path file : files.filter(p -> p.toString().endsWith(".json")).sorted().collect(Collectors.toList())) { + out.add(Arguments.of(NGFF_SPEC_TESTS.relativize(file).toString(), + RAW.readTree(file.toFile()).get("ome"))); + } + } + } + return out.stream(); + } + + static Stream sceneExamples() throws IOException { + List out = new ArrayList<>(); + for (Path zarrJson : zarrJsonFiles(SCENE_EXAMPLES)) { + JsonNode attributes = RAW.readTree(zarrJson.toFile()).path("attributes"); + if (attributes.has("ome")) { + out.add(Arguments.of(SCENE_EXAMPLES.relativize(zarrJson.getParent()).toString(), attributes.get("ome"))); + } + } + return out.stream(); + } + + /** Members outside the spec that must survive a rewrite as well. */ + static Stream extensionMembers() { + String image = "{\"version\":\"0.6\",\"multiscales\":[{\"name\":\"image\"," + + "\"coordinateSystems\":[{\"name\":\"physical\",\"axes\":[{\"name\":\"y\",\"type\":\"space\"}," + + "{\"name\":\"x\",\"type\":\"space\"}]}]," + + "\"datasets\":[{\"path\":\"s0\",\"coordinateTransformations\":[{\"type\":\"scale\"," + + "\"input\":{\"path\":\"s0\"},\"output\":{\"name\":\"physical\"},\"scale\":[1,1]}]}]%s}]%s}"; + return Stream.of( + Arguments.of("vendor transform type", String.format(image, + ",\"coordinateTransformations\":[{\"type\":\"vendorWarp\",\"input\":{\"name\":\"physical\"}," + + "\"output\":{\"name\":\"warped\"},\"strength\":3.5,\"vendor\":{\"id\":7}}]", "")), + Arguments.of("omero wavelength_id", String.format(image, "", + ",\"omero\":{\"channels\":[{\"label\":\"H2B\",\"wavelength_id\":\"H2B-mCherry\"}]}"))); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("extensionMembers") + void roundTripExtensionMembers(String name, String ome) throws Exception { + assertRoundTrip(RAW.readTree(ome)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("rfc5Examples") + void roundTripRfc5Examples(String name, JsonNode ome) throws Exception { + assertRoundTrip(ome); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("ngffSpecValidAttributes") + void roundTripNgffSpecConformanceSuite(String name, JsonNode ome) throws Exception { + assertRoundTrip(ome); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("sceneExamples") + void roundTripSceneExamples(String name, JsonNode ome) throws Exception { + assertRoundTrip(ome); + } + + static void assertRoundTrip(JsonNode ome) throws Exception { + ObjectMapper mapper = OmeObjectMappers.makeV3Mapper(); + OmeMetadata parsed = mapper.treeToValue(ome, OmeMetadata.class); + JsonNode written = mapper.valueToTree(parsed); + List differences = new ArrayList<>(); + diff("", ome, written, differences); + if (!differences.isEmpty()) { + fail("Written metadata differs from the original:\n " + String.join("\n ", differences)); + } + // the written document must also read back to the same JSON + assertTrue(written.equals(mapper.valueToTree(mapper.treeToValue(written, OmeMetadata.class)))); + } + + private static void diff(String path, JsonNode expected, JsonNode actual, List out) { + if (expected.isNumber() && actual.isNumber()) { + if (expected.doubleValue() != actual.doubleValue()) { + out.add(path + ": expected " + expected + " but was " + actual); + } + } else if (expected.isObject() && actual.isObject()) { + for (Map.Entry field : expected.properties()) { + String fieldPath = path + "/" + field.getKey(); + if (actual.has(field.getKey())) { + diff(fieldPath, field.getValue(), actual.get(field.getKey()), out); + } else { + out.add(fieldPath + ": missing in written metadata"); + } + } + for (Map.Entry field : actual.properties()) { + if (!expected.has(field.getKey())) { + out.add(path + "/" + field.getKey() + ": added in written metadata: " + field.getValue()); + } + } + } else if (expected.isArray() && actual.isArray() && expected.size() == actual.size()) { + for (int i = 0; i < expected.size(); i++) { + diff(path + "[" + i + "]", expected.get(i), actual.get(i), out); + } + } else if (!expected.equals(actual)) { + out.add(path + ": expected " + expected + " but was " + actual); + } + } + + private static List zarrJsonFiles(Path root) throws IOException { + try (Stream files = Files.walk(root)) { + return files.filter(p -> p.getFileName().toString().equals("zarr.json")).sorted().collect(Collectors.toList()); + } + } +} diff --git a/src/test/java/dev/zarr/conventions/ome/OmeZarrV06Test.java b/src/test/java/dev/zarr/conventions/ome/OmeZarrV06Test.java index 54339dbc..296c3506 100644 --- a/src/test/java/dev/zarr/conventions/ome/OmeZarrV06Test.java +++ b/src/test/java/dev/zarr/conventions/ome/OmeZarrV06Test.java @@ -10,6 +10,7 @@ import dev.zarr.conventions.ome.metadata.WellMetadata; import dev.zarr.conventions.ome.metadata.WellRef; import dev.zarr.conventions.ome.v0_6.metadata.CoordinateSystem; +import dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateSystemRef; import dev.zarr.zarrjava.store.StoreHandle; import org.junit.jupiter.api.Test; @@ -87,8 +88,8 @@ void datasetsPathsAndTransformations() throws Exception { dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation ct = entry.datasets.get(0).coordinateTransformations.get(0); assertEquals("scale", ct.getType()); - assertEquals("s0", ct.getInput()); - assertEquals("physical", ct.getOutput()); + assertEquals(CoordinateSystemRef.ofPath("s0"), ct.getInput()); + assertEquals(CoordinateSystemRef.ofName("physical"), ct.getOutput()); assertInstanceOf(dev.zarr.conventions.ome.v0_6.metadata.transform.ScaleCoordinateTransformation.class, ct); dev.zarr.conventions.ome.v0_6.metadata.transform.ScaleCoordinateTransformation scaleCt = (dev.zarr.conventions.ome.v0_6.metadata.transform.ScaleCoordinateTransformation) ct; @@ -174,7 +175,7 @@ void readV06WithOmeroMetadata() throws Exception { "s0", java.util.Collections.singletonList( dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation.scale( - Arrays.asList(1.0, 1.0), "s0", "physical")))); + Arrays.asList(1.0, 1.0), CoordinateSystemRef.ofPath("s0"), CoordinateSystemRef.ofName("physical"))))); dev.zarr.conventions.ome.v0_6.metadata.MultiscalesEntry ms = new dev.zarr.conventions.ome.v0_6.metadata.MultiscalesEntry( null, @@ -245,7 +246,7 @@ void readV06WithBioformats2rawLayout() throws Exception { "s0", java.util.Collections.singletonList( dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation.scale( - Arrays.asList(1.0, 1.0), "s0", "physical")))); + Arrays.asList(1.0, 1.0), CoordinateSystemRef.ofPath("s0"), CoordinateSystemRef.ofName("physical"))))); dev.zarr.conventions.ome.v0_6.metadata.MultiscalesEntry ms = new dev.zarr.conventions.ome.v0_6.metadata.MultiscalesEntry( null, @@ -326,4 +327,221 @@ void hcsV06PlateWellDispatchAndNavigation() throws Exception { Collections.singletonList(image.getMultiscaleNode(0).datasets.get(0).path)); assertEquals(Arrays.asList("y", "x"), image.getAxisNames()); } + + // ── metadata preservation (read -> write keeps everything) ─────────────── + + private static final String V06_AXES_YX = + "[{\"name\":\"y\",\"type\":\"space\",\"unit\":\"micrometer\"},{\"name\":\"x\",\"type\":\"space\",\"unit\":\"micrometer\"}]"; + + private static String v06ImageJson(String extraMultiscaleKeys, String extraOmeKeys) { + return "{\"version\":\"0.6\",\"multiscales\":[{" + + "\"coordinateSystems\":[{\"name\":\"physical\",\"axes\":" + V06_AXES_YX + "}]," + + "\"datasets\":[{\"path\":\"s0\",\"coordinateTransformations\":[{\"type\":\"scale\",\"scale\":[0.5,0.5]}]}]" + + extraMultiscaleKeys + "}]" + extraOmeKeys + "}"; + } + + @Test + void imageLabelRoundTrip() throws Exception { + String json = v06ImageJson("", ",\"image-label\":{" + + "\"colors\":[{\"label-value\":0,\"rgba\":[0,0,128,128]},{\"label-value\":1,\"rgba\":[0,128,0,128]}]," + + "\"properties\":[{\"label-value\":0,\"area (pixels)\":1200,\"class\":\"intercellular space\"}," + + "{\"label-value\":1,\"area (pixels)\":1650,\"class\":\"cell\",\"cell type\":\"neuron\"}]," + + "\"source\":{\"image\":\"../../\"}}"); + dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata parsed = + assertOmeRoundTrip(json, dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata.class); + dev.zarr.conventions.ome.metadata.ImageLabel imageLabel = parsed.imageLabel; + assertNotNull(imageLabel); + assertEquals(Arrays.asList(0, 128, 0, 128), imageLabel.colors.get(1).rgba); + assertEquals(1650, imageLabel.properties.get(1).getAdditionalProperties().get("area (pixels)")); + assertEquals("neuron", imageLabel.properties.get(1).getAdditionalProperties().get("cell type")); + assertEquals("../../", imageLabel.source.image); + } + + @Test + void labelsGroupAndSeriesRoundTrip() throws Exception { + dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata labels = assertOmeRoundTrip( + "{\"version\":\"0.6\",\"labels\":[\"cell_segmentation\"]}", + dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata.class); + assertEquals(Collections.singletonList("cell_segmentation"), labels.labels); + dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata series = assertOmeRoundTrip( + "{\"version\":\"0.6\",\"series\":[\"0\",\"1\"]}", + dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata.class); + assertEquals(Arrays.asList("0", "1"), series.series); + } + + @Test + void axisLongNameRoundTrip() throws Exception { + String axes = "[{\"name\":\"t\",\"type\":\"time\",\"unit\":\"second\",\"discrete\":false,\"longName\":\"Time\"}," + + "{\"name\":\"x\",\"type\":\"space\"}]"; + String json = "{\"version\":\"0.6\",\"multiscales\":[{" + + "\"coordinateSystems\":[{\"name\":\"physical\",\"axes\":" + axes + "}]," + + "\"datasets\":[{\"path\":\"s0\",\"coordinateTransformations\":[{\"type\":\"identity\"}]}]}]}"; + dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata parsed = + assertOmeRoundTrip(json, dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata.class); + assertEquals("Time", parsed.multiscales.get(0).coordinateSystems.get(0).axes.get(0).longName); + } + + @Test + void axisLegacyLongNameIsReadAndWrittenAsLongName() throws Exception { + com.fasterxml.jackson.databind.ObjectMapper reader = OmeObjectMappers.makeV3Mapper(); + dev.zarr.conventions.ome.metadata.Axis axis = reader.readValue( + "{\"name\":\"t\",\"long_name\":\"Time\"}", dev.zarr.conventions.ome.metadata.Axis.class); + assertEquals("Time", axis.longName); + com.fasterxml.jackson.databind.JsonNode written = + dev.zarr.zarrjava.v3.Node.makeObjectMapper().valueToTree(axis); + assertEquals(reader.readTree("{\"name\":\"t\",\"longName\":\"Time\"}"), written); + } + + @Test + void displacementsAndCoordinatesInterpolationRoundTrip() throws Exception { + String json = v06ImageJson(",\"coordinateTransformations\":[" + + "{\"type\":\"displacements\",\"path\":\"dfield\",\"interpolation\":\"linear\",\"name\":\"d\"}," + + "{\"type\":\"coordinates\",\"path\":\"cfield\",\"interpolation\":\"nearest\"}]", ""); + dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata parsed = + assertOmeRoundTrip(json, dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata.class); + List cts = + parsed.multiscales.get(0).coordinateTransformations; + assertEquals("linear", + ((dev.zarr.conventions.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation) cts.get(0)).interpolation); + assertEquals("nearest", + ((dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinatesCoordinateTransformation) cts.get(1)).interpolation); + } + + @Test + void displacementsInterpolationFromFixture() throws Exception { + dev.zarr.conventions.ome.v0_6.MultiscaleImage image = + dev.zarr.conventions.ome.v0_6.MultiscaleImage.openMultiscaleImage( + storeHandle(TESTDATA.resolve("ome/v0.6/examples/2d/nonlinear/displacements.zarr"))); + dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation ct = + image.getMultiscalesEntry(0).coordinateTransformations.get(0); + assertInstanceOf(dev.zarr.conventions.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation.class, ct); + assertEquals("linear", + ((dev.zarr.conventions.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation) ct).interpolation); + } + + @Test + void projectAxisRoundTrip() throws Exception { + String json = v06ImageJson(",\"coordinateTransformations\":[" + + "{\"type\":\"projectAxis\",\"droppedInputs\":[0],\"createdOutputs\":[0,1],\"name\":\"p\"}]", ""); + dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata parsed = + assertOmeRoundTrip(json, dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata.class); + dev.zarr.conventions.ome.v0_6.metadata.transform.CoordinateTransformation ct = + parsed.multiscales.get(0).coordinateTransformations.get(0); + assertInstanceOf(dev.zarr.conventions.ome.v0_6.metadata.transform.ProjectAxisCoordinateTransformation.class, ct); + dev.zarr.conventions.ome.v0_6.metadata.transform.ProjectAxisCoordinateTransformation p = + (dev.zarr.conventions.ome.v0_6.metadata.transform.ProjectAxisCoordinateTransformation) ct; + assertEquals(Collections.singletonList(0), p.droppedInputs); + assertEquals(Arrays.asList(0, 1), p.createdOutputs); + } + + @Test + void byDimensionWritesCamelCaseAndAcceptsSnakeCase() throws Exception { + String camel = v06ImageJson(",\"coordinateTransformations\":[{\"type\":\"byDimension\",\"transformations\":[" + + "{\"inputAxes\":[1],\"outputAxes\":[1],\"transformation\":{\"type\":\"scale\",\"scale\":[2.0]}}]}]", ""); + assertOmeRoundTrip(camel, dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata.class); + + // The dev-draft fixture still uses input_axes/output_axes. + dev.zarr.conventions.ome.v0_6.MultiscaleImage image = + dev.zarr.conventions.ome.v0_6.MultiscaleImage.openMultiscaleImage( + storeHandle(TESTDATA.resolve("ome/v0.6/examples/2d/axis_dependent/byDimension.zarr"))); + dev.zarr.conventions.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation byDim = + (dev.zarr.conventions.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation) + image.getMultiscalesEntry(0).coordinateTransformations.get(0); + assertEquals(Collections.singletonList(1), byDim.transformations.get(0).inputAxes); + assertEquals(Collections.singletonList(1), byDim.transformations.get(0).outputAxes); + com.fasterxml.jackson.databind.JsonNode written = + dev.zarr.zarrjava.v3.Node.makeObjectMapper().valueToTree(byDim.transformations.get(0)); + assertTrue(written.has("inputAxes")); + assertTrue(written.has("outputAxes")); + assertFalse(written.has("input_axes")); + assertFalse(written.has("output_axes")); + } + + @Test + void unifiedTransformsKeepInterpolationAndProjectAxis() throws Exception { + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v06_unified_transform_fields")); + List axes = Arrays.asList( + new dev.zarr.conventions.ome.metadata.Axis("y", "space", "micrometer"), + new dev.zarr.conventions.ome.metadata.Axis("x", "space", "micrometer")); + dev.zarr.conventions.ome.v0_6.MultiscaleImage image = + dev.zarr.conventions.ome.v0_6.MultiscaleImage.create(handle, + new dev.zarr.conventions.ome.v0_6.metadata.MultiscalesEntry( + null, Collections.emptyList(), null, + Collections.singletonList(new CoordinateSystem("physical", axes)), "ms", null, null)); + + dev.zarr.conventions.ome.metadata.transform.GenericCoordinateTransformation displacements = + new dev.zarr.conventions.ome.metadata.transform.GenericCoordinateTransformation("displacements"); + displacements.raw.put("path", "dfield"); + displacements.raw.put("interpolation", "nearest"); + dev.zarr.conventions.ome.metadata.transform.GenericCoordinateTransformation project = + new dev.zarr.conventions.ome.metadata.transform.GenericCoordinateTransformation("projectAxis"); + project.raw.put("droppedInputs", Collections.singletonList(0)); + project.raw.put("createdOutputs", Arrays.asList(0, 1)); + image.createScaleLevel("s0", + dev.zarr.zarrjava.v3.Array.metadataBuilder() + .withShape(4, 4).withChunkShape(4, 4) + .withDataType(dev.zarr.zarrjava.v3.DataType.UINT8).build(), + Arrays.asList(displacements, project)); + + dev.zarr.conventions.ome.v0_6.MultiscaleImage reopened = + dev.zarr.conventions.ome.v0_6.MultiscaleImage.openMultiscaleImage(handle); + // several transformations are written as one sequence, as v0.6 allows one per dataset + List cts = + ((dev.zarr.conventions.ome.v0_6.metadata.transform.SequenceCoordinateTransformation) + reopened.getMultiscalesEntry(0).datasets.get(0).coordinateTransformations.get(0)).transformations; + assertEquals("nearest", + ((dev.zarr.conventions.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation) cts.get(0)).interpolation); + assertEquals(Arrays.asList(0, 1), + ((dev.zarr.conventions.ome.v0_6.metadata.transform.ProjectAxisCoordinateTransformation) cts.get(1)).createdOutputs); + + // The unified view exposes the same fields. + List unified = + reopened.getMultiscaleNode(0).datasets.get(0).coordinateTransformations; + assertEquals("nearest", + ((dev.zarr.conventions.ome.metadata.transform.GenericCoordinateTransformation) unified.get(0)).raw.get("interpolation")); + assertEquals("projectAxis", unified.get(1).type); + assertEquals(Collections.singletonList(0), + ((dev.zarr.conventions.ome.metadata.transform.GenericCoordinateTransformation) unified.get(1)).raw.get("droppedInputs")); + } + + @Test + void labelsListAndImageLabelSpecLayout() throws Exception { + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v06_labels_spec")); + writeGroupZarrJson(handle, v06ImageJson("", "")); + writeGroupZarrJson(handle.resolve("labels"), "{\"version\":\"0.6\",\"labels\":[\"cell_segmentation\"]}"); + writeGroupZarrJson(handle.resolve("labels", "cell_segmentation"), v06ImageJson("", + ",\"image-label\":{\"colors\":[{\"label-value\":1,\"rgba\":[0,128,0,128]}]}")); + + MultiscaleImage image = MultiscaleImage.open(handle); + assertEquals(Collections.singletonList("cell_segmentation"), image.getLabels()); + MultiscaleImage label = image.openLabel("cell_segmentation"); + assertInstanceOf(dev.zarr.conventions.ome.v0_6.MultiscaleImage.class, label); + dev.zarr.conventions.ome.metadata.ImageLabel imageLabel = + ((dev.zarr.conventions.ome.v0_6.MultiscaleImage) label).getImageLabel(); + assertNotNull(imageLabel); + assertEquals(1, imageLabel.colors.get(0).labelValue); + assertNull(imageLabel.source); + } + + private static void writeGroupZarrJson(StoreHandle handle, String omeJson) { + String zarrJson = "{\"zarr_format\":3,\"node_type\":\"group\",\"attributes\":{\"ome\":" + omeJson + "}}"; + handle.resolve("zarr.json").set( + java.nio.ByteBuffer.wrap(zarrJson.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + } + + @Test + void axesComeFromIntrinsicCoordinateSystem() throws Exception { + // From the ngff-spec conformance suite: the intrinsic system "physical" is listed second. + com.fasterxml.jackson.databind.JsonNode ome = new com.fasterxml.jackson.databind.ObjectMapper().readTree( + TESTDATA.resolve("ome/v0.6/ngff-spec/tests/attributes/spec/valid/transforms/projectAxis.json").toFile()).get("ome"); + dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata parsed = OmeObjectMappers.makeV3Mapper() + .treeToValue(ome, dev.zarr.conventions.ome.v0_6.metadata.OmeMetadata.class); + dev.zarr.conventions.ome.v0_6.metadata.MultiscalesEntry entry = parsed.multiscales.get(0); + assertEquals("world", entry.coordinateSystems.get(0).name); + assertEquals("physical", entry.intrinsicCoordinateSystem().name); + + StoreHandle handle = storeHandle(TESTOUTPUT.resolve("ome_v06_intrinsic_axes")); + dev.zarr.conventions.ome.v0_6.MultiscaleImage.create(handle, entry); + assertEquals(Arrays.asList("y", "x"), MultiscaleImage.open(handle).getAxisNames()); + } } diff --git a/testdata/ome/v0.5/labels/zarr.json b/testdata/ome/v0.5/labels/zarr.json index 4215aab9..83de02b7 100644 --- a/testdata/ome/v0.5/labels/zarr.json +++ b/testdata/ome/v0.5/labels/zarr.json @@ -1,8 +1,11 @@ { "attributes": { - "labels": [ - "nuclei" - ] + "ome": { + "version": "0.5", + "labels": [ + "nuclei" + ] + } }, "zarr_format": 3, "node_type": "group" diff --git a/testdata/ome/v0.6/examples b/testdata/ome/v0.6/examples index 8eb63f34..a5506161 160000 --- a/testdata/ome/v0.6/examples +++ b/testdata/ome/v0.6/examples @@ -1 +1 @@ -Subproject commit 8eb63f3401eaa73633ad9150fef9a2e971f857e6 +Subproject commit a550616136083561982ffcdda531e18a7837e326 diff --git a/testdata/ome/v0.6/ngff-spec b/testdata/ome/v0.6/ngff-spec new file mode 160000 index 00000000..89da1da5 --- /dev/null +++ b/testdata/ome/v0.6/ngff-spec @@ -0,0 +1 @@ +Subproject commit 89da1da5b0491f83b7cdde7d123aef763a457029 diff --git a/testdata/ome/v0.6_legacy/scale_multiscale.zarr/zarr.json b/testdata/ome/v0.6_legacy/scale_multiscale.zarr/zarr.json new file mode 100644 index 00000000..cc31ad44 --- /dev/null +++ b/testdata/ome/v0.6_legacy/scale_multiscale.zarr/zarr.json @@ -0,0 +1,37 @@ +{ + "zarr_format": 3, + "node_type": "group", + "attributes": { + "ome": { + "version": "0.6.dev3", + "multiscales": [ + { + "name": "multiscales", + "coordinateSystems": [ + { + "name": "physical", + "axes": [ + {"name": "y", "type": "space", "unit": "micrometer"}, + {"name": "x", "type": "space", "unit": "micrometer"} + ] + } + ], + "datasets": [ + { + "path": "s0", + "coordinateTransformations": [ + {"type": "scale", "input": "s0", "output": "physical", "scale": [1.0, 1.0]} + ] + }, + { + "path": "s1", + "coordinateTransformations": [ + {"type": "scale", "input": "s1", "output": "physical", "scale": [2.0, 2.0]} + ] + } + ] + } + ] + } + } +} diff --git a/testdata/ome/v0.6_scene/example1_instrument_registration.zarr/sampleA_instrument1/zarr.json b/testdata/ome/v0.6_scene/example1_instrument_registration.zarr/sampleA_instrument1/zarr.json index 8f2d8bdb..13d32b36 100644 --- a/testdata/ome/v0.6_scene/example1_instrument_registration.zarr/sampleA_instrument1/zarr.json +++ b/testdata/ome/v0.6_scene/example1_instrument_registration.zarr/sampleA_instrument1/zarr.json @@ -3,7 +3,7 @@ "node_type": "group", "attributes": { "ome": { - "version": "0.6.dev3", + "version": "0.6", "multiscales": [ { "name": "multiscales", @@ -11,9 +11,21 @@ { "name": "physical_instrument1", "axes": [ - { "name": "z", "type": "space", "unit": "micrometer" }, - { "name": "y", "type": "space", "unit": "micrometer" }, - { "name": "x", "type": "space", "unit": "micrometer" } + { + "name": "z", + "type": "space", + "unit": "micrometer" + }, + { + "name": "y", + "type": "space", + "unit": "micrometer" + }, + { + "name": "x", + "type": "space", + "unit": "micrometer" + } ] } ], @@ -23,9 +35,17 @@ "coordinateTransformations": [ { "type": "scale", - "input": "s0", - "output": "physical_instrument1", - "scale": [1.0, 1.0, 1.0] + "input": { + "path": "s0" + }, + "output": { + "name": "physical_instrument1" + }, + "scale": [ + 1.0, + 1.0, + 1.0 + ] } ] } diff --git a/testdata/ome/v0.6_scene/example1_instrument_registration.zarr/sampleA_instrument2/zarr.json b/testdata/ome/v0.6_scene/example1_instrument_registration.zarr/sampleA_instrument2/zarr.json index 5188d51a..aab376b1 100644 --- a/testdata/ome/v0.6_scene/example1_instrument_registration.zarr/sampleA_instrument2/zarr.json +++ b/testdata/ome/v0.6_scene/example1_instrument_registration.zarr/sampleA_instrument2/zarr.json @@ -3,7 +3,7 @@ "node_type": "group", "attributes": { "ome": { - "version": "0.6.dev3", + "version": "0.6", "multiscales": [ { "name": "multiscales", @@ -11,9 +11,21 @@ { "name": "physical_instrument2", "axes": [ - { "name": "z", "type": "space", "unit": "micrometer" }, - { "name": "y", "type": "space", "unit": "micrometer" }, - { "name": "x", "type": "space", "unit": "micrometer" } + { + "name": "z", + "type": "space", + "unit": "micrometer" + }, + { + "name": "y", + "type": "space", + "unit": "micrometer" + }, + { + "name": "x", + "type": "space", + "unit": "micrometer" + } ] } ], @@ -23,9 +35,17 @@ "coordinateTransformations": [ { "type": "scale", - "input": "s0", - "output": "physical_instrument2", - "scale": [1.0, 1.0, 1.0] + "input": { + "path": "s0" + }, + "output": { + "name": "physical_instrument2" + }, + "scale": [ + 1.0, + 1.0, + 1.0 + ] } ] } diff --git a/testdata/ome/v0.6_scene/example1_instrument_registration.zarr/zarr.json b/testdata/ome/v0.6_scene/example1_instrument_registration.zarr/zarr.json index 748e6490..5a7c5c61 100644 --- a/testdata/ome/v0.6_scene/example1_instrument_registration.zarr/zarr.json +++ b/testdata/ome/v0.6_scene/example1_instrument_registration.zarr/zarr.json @@ -3,14 +3,20 @@ "node_type": "group", "attributes": { "ome": { - "version": "0.6.dev3", + "version": "0.6", "scene": { "coordinateTransformations": [ { "type": "affine", "path": "coordinateTransformations/sampleA_instrument2-to-instrument1", - "input": { "path": "sampleA_instrument2", "name": "physical_instrument2" }, - "output": { "path": "sampleA_instrument1", "name": "physical_instrument1" } + "input": { + "path": "sampleA_instrument2", + "name": "physical_instrument2" + }, + "output": { + "path": "sampleA_instrument1", + "name": "physical_instrument1" + } } ] } diff --git a/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/instrument1/zarr.json b/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/instrument1/zarr.json index 92763b45..2a29c919 100644 --- a/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/instrument1/zarr.json +++ b/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/instrument1/zarr.json @@ -3,7 +3,7 @@ "node_type": "group", "attributes": { "ome": { - "version": "0.6.dev3", + "version": "0.6", "multiscales": [ { "name": "multiscales", @@ -11,9 +11,21 @@ { "name": "physical", "axes": [ - { "name": "z", "type": "space", "unit": "micrometer" }, - { "name": "y", "type": "space", "unit": "micrometer" }, - { "name": "x", "type": "space", "unit": "micrometer" } + { + "name": "z", + "type": "space", + "unit": "micrometer" + }, + { + "name": "y", + "type": "space", + "unit": "micrometer" + }, + { + "name": "x", + "type": "space", + "unit": "micrometer" + } ] } ], @@ -23,9 +35,17 @@ "coordinateTransformations": [ { "type": "scale", - "input": "s0", - "output": "physical", - "scale": [1.0, 1.0, 1.0] + "input": { + "path": "s0" + }, + "output": { + "name": "physical" + }, + "scale": [ + 1.0, + 1.0, + 1.0 + ] } ] } diff --git a/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/instrument2/zarr.json b/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/instrument2/zarr.json index 92763b45..2a29c919 100644 --- a/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/instrument2/zarr.json +++ b/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/instrument2/zarr.json @@ -3,7 +3,7 @@ "node_type": "group", "attributes": { "ome": { - "version": "0.6.dev3", + "version": "0.6", "multiscales": [ { "name": "multiscales", @@ -11,9 +11,21 @@ { "name": "physical", "axes": [ - { "name": "z", "type": "space", "unit": "micrometer" }, - { "name": "y", "type": "space", "unit": "micrometer" }, - { "name": "x", "type": "space", "unit": "micrometer" } + { + "name": "z", + "type": "space", + "unit": "micrometer" + }, + { + "name": "y", + "type": "space", + "unit": "micrometer" + }, + { + "name": "x", + "type": "space", + "unit": "micrometer" + } ] } ], @@ -23,9 +35,17 @@ "coordinateTransformations": [ { "type": "scale", - "input": "s0", - "output": "physical", - "scale": [1.0, 1.0, 1.0] + "input": { + "path": "s0" + }, + "output": { + "name": "physical" + }, + "scale": [ + 1.0, + 1.0, + 1.0 + ] } ] } diff --git a/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/instrument3/zarr.json b/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/instrument3/zarr.json index 92763b45..2a29c919 100644 --- a/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/instrument3/zarr.json +++ b/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/instrument3/zarr.json @@ -3,7 +3,7 @@ "node_type": "group", "attributes": { "ome": { - "version": "0.6.dev3", + "version": "0.6", "multiscales": [ { "name": "multiscales", @@ -11,9 +11,21 @@ { "name": "physical", "axes": [ - { "name": "z", "type": "space", "unit": "micrometer" }, - { "name": "y", "type": "space", "unit": "micrometer" }, - { "name": "x", "type": "space", "unit": "micrometer" } + { + "name": "z", + "type": "space", + "unit": "micrometer" + }, + { + "name": "y", + "type": "space", + "unit": "micrometer" + }, + { + "name": "x", + "type": "space", + "unit": "micrometer" + } ] } ], @@ -23,9 +35,17 @@ "coordinateTransformations": [ { "type": "scale", - "input": "s0", - "output": "physical", - "scale": [1.0, 1.0, 1.0] + "input": { + "path": "s0" + }, + "output": { + "name": "physical" + }, + "scale": [ + 1.0, + 1.0, + 1.0 + ] } ] } diff --git a/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/zarr.json b/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/zarr.json index 58811cb3..4e5e9353 100644 --- a/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/zarr.json +++ b/testdata/ome/v0.6_scene/example2_multi_instrument_chain.zarr/zarr.json @@ -3,27 +3,69 @@ "node_type": "group", "attributes": { "ome": { - "version": "0.6.dev3", + "version": "0.6", "scene": { "coordinateTransformations": [ { "type": "affine", - "input": { "path": "instrument1", "name": "physical" }, - "output": { "path": "instrument2", "name": "physical" }, + "input": { + "path": "instrument1", + "name": "physical" + }, + "output": { + "path": "instrument2", + "name": "physical" + }, "affine": [ - [1.0, 0.0, 0.0, 10.0], - [0.0, 1.0, 0.0, 5.0], - [0.0, 0.0, 1.0, -2.0] + [ + 1.0, + 0.0, + 0.0, + 10.0 + ], + [ + 0.0, + 1.0, + 0.0, + 5.0 + ], + [ + 0.0, + 0.0, + 1.0, + -2.0 + ] ] }, { "type": "affine", - "input": { "path": "instrument3", "name": "physical" }, - "output": { "path": "instrument2", "name": "physical" }, + "input": { + "path": "instrument3", + "name": "physical" + }, + "output": { + "path": "instrument2", + "name": "physical" + }, "affine": [ - [0.9, 0.1, 0.0, 0.0], - [0.0, 1.1, 0.0, 1.0], - [0.0, 0.0, 1.2, 3.0] + [ + 0.9, + 0.1, + 0.0, + 0.0 + ], + [ + 0.0, + 1.1, + 0.0, + 1.0 + ], + [ + 0.0, + 0.0, + 1.2, + 3.0 + ] ] } ]