Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions USERGUIDE-OME-ZARR.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": <level path>}`, output `{"name": <intrinsic>}`, 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
Expand Down
15 changes: 12 additions & 3 deletions src/main/java/dev/zarr/conventions/ome/MultiscaleImage.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,24 @@ default List<String> getAxisNames() throws ZarrException {
default List<String> 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<String> result = new ArrayList<>();
for (com.fasterxml.jackson.databind.JsonNode item : labelsNode) {
result.add(item.asText());
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/dev/zarr/conventions/ome/metadata/Axis.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)
Expand All @@ -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;
Expand Down
125 changes: 125 additions & 0 deletions src/main/java/dev/zarr/conventions/ome/metadata/ImageLabel.java
Original file line number Diff line number Diff line change
@@ -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<Color> colors;
@Nullable
public final List<Property> 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<Color> colors,
@Nullable @JsonProperty("properties") List<Property> 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<Color> colors,
@Nullable List<Property> 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<Integer> rgba;
private final Map<String, Object> additionalProperties = new LinkedHashMap<>();

@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public Color(
@JsonProperty(value = "label-value", required = true) int labelValue,
@Nullable @JsonProperty("rgba") List<Integer> rgba
) {
this.labelValue = labelValue;
this.rgba = rgba;
}

/** Keys other than {@code label-value} and {@code rgba}. */
@JsonAnyGetter
public Map<String, Object> 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<String, Object> 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<String, Object> 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;
}
}
}
35 changes: 34 additions & 1 deletion src/main/java/dev/zarr/conventions/ome/metadata/OmeMetadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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<String> series;

@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public OmeMetadata(
Expand All @@ -31,18 +41,41 @@ 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<String> labels,
@Nullable @JsonProperty("image-label") ImageLabel imageLabel,
@Nullable @JsonProperty("series") List<String> series
) {
this.version = version;
this.multiscales = multiscales;
this.omero = omero;
this.bioformats2rawLayout = bioformats2rawLayout;
this.plate = plate;
this.well = well;
this.labels = labels;
this.imageLabel = imageLabel;
this.series = series;
}

public OmeMetadata(
String version,
@Nullable List<MultiscalesEntry> 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<MultiscalesEntry> 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<MultiscalesEntry> multiscales) {
return new OmeMetadata(version, multiscales, omero, bioformats2rawLayout, plate, well,
labels, imageLabel, series);
}
}
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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<String, Object> extraFields;

@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public OmeroChannel(
Expand All @@ -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<String, Object> extraFields
) {
this.active = active;
this.coefficient = coefficient;
Expand All @@ -34,5 +41,31 @@ public OmeroChannel(
this.inverted = inverted;
this.label = label;
this.window = window;
this.extraFields = extraFields == null
? Collections.<String, Object>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<String, Object> extraFields() {
return extraFields;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -107,7 +115,7 @@ public void createScaleLevel(
MultiscalesEntry updated = current.withDataset(new Dataset(path, coordinateTransformations));
List<MultiscalesEntry> updatedList = new java.util.ArrayList<>(omeMetadata.multiscales);
updatedList.set(0, updated);
omeMetadata = new OmeMetadata(omeMetadata.version, updatedList);
omeMetadata = omeMetadata.withMultiscales(updatedList);
setAttributes(omeAttributes(omeMetadata));
}
}
Loading
Loading