From 5f3d6d5c086e3a8afebe576358af6878de601863 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Thu, 24 Sep 2026 14:45:40 +0200 Subject: [PATCH] OME: preserve labels, image-label, longName, interpolation, projectAxis and series metadata - getLabels() reads attributes.ome.labels for zarr v3 labels groups (spec 0.5/0.6), falling back to the legacy attributes.labels; fix the v0.5 labels fixture to the spec layout. - Axis serializes longName (spec) and still accepts long_name on read. - displacements/coordinates transforms keep their interpolation field. - Model labels, image-label and series in the v0.5 and v0.6 OmeMetadata (new shared ImageLabel type; unknown keys in colors/properties are kept). createScaleLevel no longer drops ome fields in v0.5. - Add the projectAxis transform (droppedInputs/createdOutputs). - byDimension items use inputAxes/outputAxes, accepting the snake_case aliases on read. Co-Authored-By: Claude Opus 5.5 --- .../experimental/ome/MultiscaleImage.java | 15 +- .../experimental/ome/metadata/Axis.java | 6 +- .../experimental/ome/metadata/ImageLabel.java | 125 +++++++++++ .../ome/metadata/OmeMetadata.java | 35 ++- .../ome/v0_5/MultiscaleImage.java | 10 +- .../ome/v0_6/MultiscaleImage.java | 57 +++-- .../ome/v0_6/metadata/OmeMetadata.java | 35 ++- .../ByDimensionCoordinateTransformation.java | 13 +- .../transform/CoordinateTransformation.java | 1 + .../CoordinatesCoordinateTransformation.java | 15 +- ...DisplacementsCoordinateTransformation.java | 15 +- .../ProjectAxisCoordinateTransformation.java | 32 +++ .../experimental/ome/OmeZarrBaseTest.java | 15 ++ .../experimental/ome/OmeZarrV05Test.java | 76 ++++++- .../experimental/ome/OmeZarrV06Test.java | 199 ++++++++++++++++++ testdata/ome/v0.5/labels/zarr.json | 9 +- 16 files changed, 626 insertions(+), 32 deletions(-) create mode 100644 src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/ImageLabel.java create mode 100644 src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ProjectAxisCoordinateTransformation.java diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/MultiscaleImage.java index eb25d178..b3b7c10f 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/MultiscaleImage.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/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/zarrjava/experimental/ome/metadata/Axis.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/Axis.java index 0b24c9d6..a5dcb85a 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/Axis.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/Axis.java @@ -1,5 +1,6 @@ package dev.zarr.zarrjava.experimental.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/zarrjava/experimental/ome/metadata/ImageLabel.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/ImageLabel.java new file mode 100644 index 00000000..0ae15284 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/ImageLabel.java @@ -0,0 +1,125 @@ +package dev.zarr.zarrjava.experimental.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/zarrjava/experimental/ome/metadata/OmeMetadata.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/OmeMetadata.java index 4e8920a3..428349d9 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/metadata/OmeMetadata.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/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/zarrjava/experimental/ome/v0_5/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_5/MultiscaleImage.java index 3f453758..0efa439f 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_5/MultiscaleImage.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/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.zarrjava.experimental.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/zarrjava/experimental/ome/v0_6/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/MultiscaleImage.java index 327f7187..bd30c765 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/MultiscaleImage.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/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.zarrjava.experimental.ome.metadata.ImageLabel getImageLabel() { + return omeMetadata.imageLabel; + } + OmeMetadata getRawOmeMetadata() { return omeMetadata; } @@ -164,6 +172,11 @@ public void createScaleLevel( null, null, null, castIntList(raw.get("mapAxis")), castV06Transform(raw.get("transformation")))); continue; } + if ("projectAxis".equals(type)) { + v06Transforms.add(new dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation( null, null, null, castMatrix(raw.get("affine")), rawPath)); @@ -176,12 +189,12 @@ public void createScaleLevel( } if ("displacements".equals(type)) { v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation( - null, null, null, rawPath)); + null, null, null, rawPath, castString(raw.get("interpolation")))); continue; } if ("coordinates".equals(type)) { v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinatesCoordinateTransformation( - null, null, null, rawPath)); + null, null, null, rawPath, castString(raw.get("interpolation")))); continue; } if ("bijection".equals(type)) { @@ -218,14 +231,7 @@ public void createScaleLevel( MultiscalesEntry updated = current.withDataset(new dev.zarr.zarrjava.experimental.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)); } @@ -294,6 +300,19 @@ private static CoordinateTransformation mapTransform( } return generic; } + if (ct instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ProjectAxisCoordinateTransformation) { + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ProjectAxisCoordinateTransformation t = + (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ProjectAxisCoordinateTransformation) ct; + dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation generic = + new dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation) { dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation t = (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation) ct; @@ -322,6 +341,9 @@ private static CoordinateTransformation mapTransform( dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation generic = new dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinatesCoordinateTransformation) { @@ -330,6 +352,9 @@ private static CoordinateTransformation mapTransform( dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation generic = new dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.v0_6.metadata.transform.BijectionCoordinateTransformation) { @@ -354,8 +379,8 @@ private static CoordinateTransformation mapTransform( List> transformed = new ArrayList<>(); for (dev.zarr.zarrjava.experimental.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 +436,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 +518,8 @@ private static List map = (Map) item; out.add(new dev.zarr.zarrjava.experimental.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/zarrjava/experimental/ome/v0_6/metadata/OmeMetadata.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/OmeMetadata.java index 0040ae91..521b828c 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/OmeMetadata.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/OmeMetadata.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import dev.zarr.zarrjava.experimental.ome.metadata.ImageLabel; import dev.zarr.zarrjava.experimental.ome.metadata.OmeroMetadata; import dev.zarr.zarrjava.experimental.ome.metadata.PlateMetadata; import dev.zarr.zarrjava.experimental.ome.metadata.WellMetadata; @@ -23,6 +24,14 @@ 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; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public OmeMetadata( @@ -32,7 +41,10 @@ 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 ) { this.version = version; this.multiscales = multiscales; @@ -41,6 +53,21 @@ public OmeMetadata( this.scene = scene; 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 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 +90,10 @@ 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); + } } diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ByDimensionCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ByDimensionCoordinateTransformation.java index 03e44183..c1c780fc 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ByDimensionCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ByDimensionCoordinateTransformation.java @@ -1,5 +1,6 @@ package dev.zarr.zarrjava.experimental.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; @@ -22,14 +23,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/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateTransformation.java index 4463d185..d9c2e08d 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateTransformation.java @@ -16,6 +16,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"), diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinatesCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinatesCoordinateTransformation.java index 00e1e254..6f31b21f 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinatesCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinatesCoordinateTransformation.java @@ -8,15 +8,28 @@ 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("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 String input, + @Nullable String output, + @Nullable String name, + @Nullable String path + ) { + this(input, output, name, path, null); } } diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/DisplacementsCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/DisplacementsCoordinateTransformation.java index 953ea841..5eabffdb 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/DisplacementsCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/DisplacementsCoordinateTransformation.java @@ -8,15 +8,28 @@ 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("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 String input, + @Nullable String output, + @Nullable String name, + @Nullable String path + ) { + this(input, output, name, path, null); } } diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ProjectAxisCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ProjectAxisCoordinateTransformation.java new file mode 100644 index 00000000..bee019b9 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ProjectAxisCoordinateTransformation.java @@ -0,0 +1,32 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; + +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; + +/** + * 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") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, + @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String 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/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrBaseTest.java b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrBaseTest.java index fcfea4c6..c48a3f3d 100644 --- a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrBaseTest.java +++ b/src/test/java/dev/zarr/zarrjava/experimental/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/zarrjava/experimental/ome/OmeZarrV05Test.java b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV05Test.java index fd820c88..e8f37307 100644 --- a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV05Test.java +++ b/src/test/java/dev/zarr/zarrjava/experimental/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.zarrjava.experimental.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.zarrjava.experimental.ome.v0_5.MultiscaleImage nuclei = dev.zarr.zarrjava.experimental.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.zarrjava.experimental.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.zarrjava.experimental.ome.metadata.OmeMetadata.class); + dev.zarr.zarrjava.experimental.ome.metadata.OmeMetadata parsed = + assertOmeRoundTrip(labelImageJson, dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.metadata.OmeMetadata parsed = + assertOmeRoundTrip(json, dev.zarr.zarrjava.experimental.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.zarrjava.experimental.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.zarrjava.experimental.ome.v0_5.MultiscaleImage image = + dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.v0_5.MultiscaleImage reopened = + dev.zarr.zarrjava.experimental.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/zarrjava/experimental/ome/OmeZarrV06Test.java b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV06Test.java index 779d0d4e..71e91b50 100644 --- a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV06Test.java +++ b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV06Test.java @@ -326,4 +326,203 @@ 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.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata parsed = + assertOmeRoundTrip(json, dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata.class); + dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata labels = assertOmeRoundTrip( + "{\"version\":\"0.6\",\"labels\":[\"cell_segmentation\"]}", + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata.class); + assertEquals(Collections.singletonList("cell_segmentation"), labels.labels); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata series = assertOmeRoundTrip( + "{\"version\":\"0.6\",\"series\":[\"0\",\"1\"]}", + dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata parsed = + assertOmeRoundTrip(json, dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.metadata.Axis axis = reader.readValue( + "{\"name\":\"t\",\"long_name\":\"Time\"}", dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata parsed = + assertOmeRoundTrip(json, dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata.class); + List cts = + parsed.multiscales.get(0).coordinateTransformations; + assertEquals("linear", + ((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation) cts.get(0)).interpolation); + assertEquals("nearest", + ((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinatesCoordinateTransformation) cts.get(1)).interpolation); + } + + @Test + void displacementsInterpolationFromFixture() throws Exception { + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage image = + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.openMultiscaleImage( + storeHandle(TESTDATA.resolve("ome/v0.6/examples/2d/nonlinear/displacements.zarr"))); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation ct = + image.getMultiscalesEntry(0).coordinateTransformations.get(0); + assertInstanceOf(dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation.class, ct); + assertEquals("linear", + ((dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata parsed = + assertOmeRoundTrip(json, dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata.class); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation ct = + parsed.multiscales.get(0).coordinateTransformations.get(0); + assertInstanceOf(dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ProjectAxisCoordinateTransformation.class, ct); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ProjectAxisCoordinateTransformation p = + (dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata.class); + + // The dev-draft fixture still uses input_axes/output_axes. + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage image = + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.openMultiscaleImage( + storeHandle(TESTDATA.resolve("ome/v0.6/examples/2d/axis_dependent/byDimension.zarr"))); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation byDim = + (dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.metadata.Axis("y", "space", "micrometer"), + new dev.zarr.zarrjava.experimental.ome.metadata.Axis("x", "space", "micrometer")); + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage image = + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.create(handle, + new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry( + null, Collections.emptyList(), null, + Collections.singletonList(new CoordinateSystem("physical", axes)), "ms", null, null)); + + dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation displacements = + new dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation("displacements"); + displacements.raw.put("path", "dfield"); + displacements.raw.put("interpolation", "nearest"); + dev.zarr.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation project = + new dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.v0_6.MultiscaleImage reopened = + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.openMultiscaleImage(handle); + List cts = + reopened.getMultiscalesEntry(0).datasets.get(0).coordinateTransformations; + assertEquals("nearest", + ((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation) cts.get(0)).interpolation); + assertEquals(Arrays.asList(0, 1), + ((dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.metadata.transform.GenericCoordinateTransformation) unified.get(0)).raw.get("interpolation")); + assertEquals("projectAxis", unified.get(1).type); + assertEquals(Collections.singletonList(0), + ((dev.zarr.zarrjava.experimental.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.zarrjava.experimental.ome.v0_6.MultiscaleImage.class, label); + dev.zarr.zarrjava.experimental.ome.metadata.ImageLabel imageLabel = + ((dev.zarr.zarrjava.experimental.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))); + } } 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"