Skip to content

Commit c8bfd66

Browse files
authored
Add OME-Zarr support (#66)
* Add ngff-rfc5 coordinate transformation examples as submodule at 8eb63f3 * add ome testdata for v0.5 and v0.4 * add ome v0.4 and v0.5 implementation * add ome v0.4 and v0.5 tests * add omero, bioformats2raw, labels, hcs * add tests for omero, bioformats2raw, labels, and hcs * add ome v1.0 and v0.6 * reduce code duplication and better unified interface between versions (via MultiscalesEntry) * allow unknown parameters for MultiscalesEntry * add zstd codec for v2 * version to SNAPSHOT for compability with ome-zarr-fiji-java PR * update user guide to include OME-Zarr section and create dedicated OME-Zarr guide * rename tests * add zstd to ZarrPythonTests.testWriteV2 * add OmeZarrUserGuideExamplesTest * hardened OME metadata deserialization by ignoring unknown fields * add test to read ome sample data from s3 * add hint to stores in USERGUIDE-OME-ZARR.md * warn on unknown metadata fields * add v0.4zarr3 * add optional omero fields id, version, name * add omero to v0.6 * typing of Omero Metadata * better representation of transformations * better representation of transformations * test object mappers * remove ome versions v0.4-zarr and v1.0 * add scene, plate, well, and bioformats2raw to v0.6 * refactor scene to use existing transformations * test scene * move the namespace of ome to dev.zarr.zarrjava.experimental.ome * fix openSceneExample2TwoAffineLinksViaInstrument2 * refactor transformation inheritance across versions to allow for better type checks
1 parent 134adb8 commit c8bfd66

136 files changed

Lines changed: 6512 additions & 34 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ jobs:
2828

2929
steps:
3030
- uses: actions/checkout@v5
31+
with:
32+
submodules: true
3133

3234
- name: Set up JDK
3335
uses: actions/setup-java@v4

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "testdata/ome/v0.6/examples"]
2+
path = testdata/ome/v0.6/examples
3+
url = https://github.com/jo-mueller/ngff-rfc5-coordinate-transformation-examples

USERGUIDE-OME-ZARR.md

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# OME-Zarr Guide for zarr-java
2+
3+
## Scope and supported versions
4+
5+
`dev.zarr.zarrjava.experimental.ome` supports:
6+
7+
- v0.4 (Zarr v2 layout)
8+
- v0.5 (Zarr v3 layout)
9+
- v0.6 / RFC-5
10+
11+
## Primary entry points
12+
13+
Use these static open methods:
14+
15+
- `MultiscaleImage.open(StoreHandle)` for multiscale images (auto-detects v0.4/v0.5/v0.6 image nodes)
16+
- `Plate.open(StoreHandle)` for HCS plates (v0.4/v0.5)
17+
- `Well.open(StoreHandle)` for HCS wells (v0.4/v0.5)
18+
19+
### StoreHandle and stores
20+
21+
OME-Zarr APIs are store-agnostic: pass any `StoreHandle` (filesystem, S3, HTTP, ZIP, memory) to `open(...)`.
22+
See storage backend setup in [`USERGUIDE.md#storage-backends`](USERGUIDE.md#storage-backends).
23+
24+
```java
25+
StoreHandle s3 = new S3Store(client, "idr", "zarr/v0.5/idr0083").resolve("9822152.zarr");
26+
MultiscaleImage image = MultiscaleImage.open(s3);
27+
```
28+
29+
## Essential methods
30+
31+
### MultiscaleImage
32+
33+
Metadata:
34+
35+
- `getMultiscaleNode(int i)` → normalized `ome.metadata.MultiscalesEntry`
36+
- `getAxisNames()` → axis names from multiscale `0`
37+
- `getScaleLevelCount()` → number of datasets/levels in multiscale `0`
38+
- `getLabels()` / `openLabel(String)` → labels subgroup helpers
39+
40+
Array access:
41+
42+
- `openScaleLevel(int i)``dev.zarr.zarrjava.core.Array`
43+
- then call `read()` or `read(offset, shape)` on that array
44+
- typical viewer flow: read axes + scale count first, then select a level by `i`
45+
46+
### Plate and Well (HCS)
47+
48+
Metadata:
49+
50+
- `Plate.getPlateMetadata()`
51+
- `Well.getWellMetadata()`
52+
53+
Navigation:
54+
55+
- `Plate.openWell(String rowColPath)` (for example `"A/1"`)
56+
- `Well.openImage(String path)` (for example `"0"`)
57+
58+
## Version-specific typed metadata
59+
60+
If you need the raw version-specific metadata model instead of normalized `MultiscalesEntry`:
61+
62+
- Cast to `MultiscalesMetadataImage<?>` and call `getMultiscalesEntry(i)`
63+
64+
65+
## v0.6 Scene metadata
66+
67+
Scene roots (groups with `ome.scene`) are supported via `dev.zarr.zarrjava.experimental.ome.v0_6.Scene`:
68+
69+
- `Scene.openScene(StoreHandle)` / `Scene.open(StoreHandle)`
70+
- `Scene.createScene(StoreHandle, SceneMetadata)` / `Scene.create(...)`
71+
- `listImageNodes()` and `openImageNode(String)` for sibling multiscale images
72+
- `getCoordinateTransformationGraph()` for lightweight metadata graph inspection
73+
74+
Notes:
75+
- Parsing is permissive and explicit (no strict full-spec validation).
76+
- Scene-level references (`input`/`output`) are resolved against scene-root coordinate systems and child image coordinate systems for graph inspection.
77+
- Path-based transform assets can be normalized with `Scene.normalizeCoordinateTransformPath(...)` and grouped under `coordinateTransformations/` via `createCoordinateTransformationsGroup()`.
78+
79+
## Read example
80+
81+
```java
82+
import dev.zarr.zarrjava.experimental.ome.MultiscaleImage;
83+
import dev.zarr.zarrjava.experimental.ome.Plate;
84+
import dev.zarr.zarrjava.experimental.ome.Well;
85+
import dev.zarr.zarrjava.store.FilesystemStore;
86+
import dev.zarr.zarrjava.store.StoreHandle;
87+
88+
StoreHandle imageHandle = new FilesystemStore("/data/ome/image.zarr").resolve();
89+
MultiscaleImage image = MultiscaleImage.open(imageHandle);
90+
91+
int scaleCount = image.getScaleLevelCount();
92+
java.util.List<String> axisNames = image.getAxisNames();
93+
dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry entry0 = image.getMultiscaleNode(0);
94+
95+
dev.zarr.zarrjava.core.Array s0 = image.openScaleLevel(0);
96+
ucar.ma2.Array full = s0.read();
97+
ucar.ma2.Array subset = s0.read(new long[]{0, 0, 0, 0, 0}, new long[]{1, 1, 4, 8, 8});
98+
99+
java.util.List<String> labels = image.getLabels();
100+
if (!labels.isEmpty()) {
101+
MultiscaleImage label = image.openLabel(labels.get(0));
102+
}
103+
104+
StoreHandle plateHandle = new FilesystemStore("/data/ome/plate.zarr").resolve();
105+
Plate plate = Plate.open(plateHandle);
106+
Well well = plate.openWell("A/1");
107+
MultiscaleImage wellImage = well.openImage("0");
108+
```
109+
110+
## Write example
111+
112+
Creation is version-specific, but the pattern is the same: create node with version metadata, then append levels/datasets with scale transforms. For example, for v0.5:
113+
114+
```java
115+
import dev.zarr.zarrjava.experimental.ome.metadata.Axis;
116+
import dev.zarr.zarrjava.experimental.ome.metadata.CoordinateTransformation;
117+
import dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry;
118+
import dev.zarr.zarrjava.store.FilesystemStore;
119+
import dev.zarr.zarrjava.store.StoreHandle;
120+
import dev.zarr.zarrjava.v3.Array;
121+
import dev.zarr.zarrjava.v3.DataType;
122+
123+
import java.util.Arrays;
124+
import java.util.Collections;
125+
126+
StoreHandle out = new FilesystemStore("/tmp/ome_v05.zarr").resolve();
127+
MultiscalesEntry ms = new MultiscalesEntry(
128+
Arrays.asList(new Axis("y", "space", "micrometer"), new Axis("x", "space", "micrometer")),
129+
Collections.<Dataset>emptyList());
130+
);
131+
dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage written = dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage.create(out, ms);
132+
133+
written.createScaleLevel(
134+
"s0",
135+
Array.metadataBuilder().withShape(1024, 1024).withChunkShape(256, 256).withDataType(DataType.UINT16).build(),
136+
Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0)))
137+
);
138+
written.createScaleLevel(
139+
"s1",
140+
Array.metadataBuilder().withShape(512, 512).withChunkShape(256, 256).withDataType(DataType.UINT16).build(),
141+
Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(2.0, 2.0)))
142+
);
143+
```
144+
145+
## Write entry points by version
146+
147+
- `ome.v0_4.MultiscaleImage.create(...)`
148+
- `ome.v0_5.MultiscaleImage.create(...)`
149+
- `ome.v0_6.MultiscaleImage.create(...)`
150+
151+
Use the corresponding metadata classes for each version package.

USERGUIDE.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
7. [Storage Backends](#storage-backends)
1212
8. [Compression and Codecs](#compression-and-codecs)
1313
9. [Advanced Topics](#advanced-topics)
14-
10. [Examples](#examples)
15-
11. [Troubleshooting](#troubleshooting)
14+
10. [OME-Zarr](#ome-zarr-v04-v05-v06)
15+
11. [Examples](#examples)
16+
12. [Troubleshooting](#troubleshooting)
1617
---
1718
## Introduction
1819
zarr-java is a Java implementation of the [Zarr specification](https://zarr.dev/) for chunked, compressed, N-dimensional arrays. It supports both Zarr version 2 and version 3 formats, providing a unified API for working with large scientific datasets.
@@ -653,7 +654,6 @@ try {
653654
- `"No Zarr array found at the specified location"` - Check path and ensure `.zarray` (v2) or `zarr.json` (v3) exists
654655
- `"Requested data is outside of the array's domain"` - Verify that `offset + shape <= array.shape`
655656
- `"Failed to read from store"` - Check network connectivity, file permissions, or storage availability
656-
---
657657

658658
### Best Practices
659659
1. **Chunk sizes for Best Performance**:
@@ -677,7 +677,14 @@ try {
677677
// For balanced 3D access
678678
.withChunkShape(100, 100, 100) // Balanced for all dimensions
679679
```
680-
680+
681+
## OME-Zarr (v0.4, v0.5, v0.6)
682+
683+
For a focused OME-Zarr API guide (metadata access, array access, version behavior, and concise examples),
684+
see:
685+
686+
- [`USERGUIDE-OME-ZARR.md`](USERGUIDE-OME-ZARR.md)
687+
681688
## Examples
682689
### Complete Example: Creating a 3D Dataset
683690
```java

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>dev.zarr</groupId>
88
<artifactId>zarr-java</artifactId>
9-
<version>0.1.0</version>
9+
<version>0.1.1-SNAPSHOT</version>
1010

1111
<name>zarr-java</name>
1212

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package dev.zarr.zarrjava.core.codec.core;
2+
3+
import com.github.luben.zstd.Zstd;
4+
import com.github.luben.zstd.ZstdCompressCtx;
5+
import dev.zarr.zarrjava.ZarrException;
6+
import dev.zarr.zarrjava.core.codec.BytesBytesCodec;
7+
import dev.zarr.zarrjava.utils.Utils;
8+
9+
import java.nio.ByteBuffer;
10+
11+
public abstract class ZstdCodec extends BytesBytesCodec {
12+
13+
@Override
14+
public ByteBuffer decode(ByteBuffer compressedBytes) throws ZarrException {
15+
byte[] compressedArray = Utils.toArray(compressedBytes);
16+
long originalSize = Zstd.getFrameContentSize(compressedArray);
17+
if (originalSize < 0) {
18+
throw new ZarrException("Failed to get decompressed zstd size.");
19+
}
20+
byte[] decompressed = Zstd.decompress(compressedArray, (int) originalSize);
21+
return ByteBuffer.wrap(decompressed);
22+
}
23+
24+
protected ByteBuffer encodeInternal(int level, boolean checksum, ByteBuffer chunkBytes)
25+
throws ZarrException {
26+
byte[] arr = Utils.toArray(chunkBytes);
27+
byte[] compressed;
28+
try (ZstdCompressCtx ctx = new ZstdCompressCtx()) {
29+
ctx.setLevel(level);
30+
ctx.setChecksum(checksum);
31+
compressed = ctx.compress(arr);
32+
}
33+
return ByteBuffer.wrap(compressed);
34+
}
35+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package dev.zarr.zarrjava.experimental.ome;
2+
3+
import dev.zarr.zarrjava.ZarrException;
4+
import dev.zarr.zarrjava.core.Node;
5+
import dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry;
6+
import dev.zarr.zarrjava.store.StoreHandle;
7+
import dev.zarr.zarrjava.utils.Utils;
8+
9+
import java.io.IOException;
10+
import java.util.ArrayList;
11+
import java.util.Collections;
12+
import java.util.List;
13+
14+
/**
15+
* Unified interface for reading OME-Zarr multiscale images across Zarr format versions.
16+
*/
17+
public interface MultiscaleImage {
18+
19+
/**
20+
* Returns the store handle for this multiscale image node.
21+
*/
22+
StoreHandle getStoreHandle();
23+
24+
/**
25+
* Returns a {@link MultiscalesEntry} view of multiscale {@code i}, normalized to the shared
26+
* metadata type. All axis and dataset information is accessible from the returned entry.
27+
*/
28+
MultiscalesEntry getMultiscaleNode(int i) throws ZarrException;
29+
30+
/**
31+
* Opens the scale level array at index {@code i} within the first multiscale entry.
32+
*/
33+
dev.zarr.zarrjava.core.Array openScaleLevel(int i) throws IOException, ZarrException;
34+
35+
/**
36+
* Returns the number of scale levels in the first multiscale entry.
37+
*/
38+
int getScaleLevelCount() throws ZarrException;
39+
40+
/**
41+
* Returns the axis names of the first multiscale entry.
42+
*/
43+
default List<String> getAxisNames() throws ZarrException {
44+
MultiscalesEntry entry = getMultiscaleNode(0);
45+
List<String> names = new ArrayList<>();
46+
for (dev.zarr.zarrjava.experimental.ome.metadata.Axis axis : entry.axes) {
47+
names.add(axis.name);
48+
}
49+
return names;
50+
}
51+
52+
/**
53+
* Returns all label names from the {@code labels/} sub-group, or an empty list if none exist.
54+
*/
55+
default List<String> getLabels() throws IOException, ZarrException {
56+
StoreHandle labelsHandle = getStoreHandle().resolve("labels");
57+
58+
// Try v0.5: labels/zarr.json with {"attributes": {"labels": [...]}}
59+
StoreHandle zarrJson = labelsHandle.resolve(Node.ZARR_JSON);
60+
if (zarrJson.exists()) {
61+
com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v3.Node.makeObjectMapper();
62+
byte[] bytes = Utils.toArray(zarrJson.readNonNull());
63+
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes);
64+
com.fasterxml.jackson.databind.JsonNode attrs = root.get("attributes");
65+
if (attrs != null && attrs.has("labels")) {
66+
com.fasterxml.jackson.databind.JsonNode labelsNode = attrs.get("labels");
67+
List<String> result = new ArrayList<>();
68+
for (com.fasterxml.jackson.databind.JsonNode item : labelsNode) {
69+
result.add(item.asText());
70+
}
71+
return result;
72+
}
73+
}
74+
75+
// Try v0.4: labels/.zattrs with {"labels": [...]}
76+
StoreHandle zattrs = labelsHandle.resolve(Node.ZATTRS);
77+
if (zattrs.exists()) {
78+
com.fasterxml.jackson.databind.ObjectMapper mapper = dev.zarr.zarrjava.v2.Node.makeObjectMapper();
79+
byte[] bytes = Utils.toArray(zattrs.readNonNull());
80+
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes);
81+
if (root.has("labels")) {
82+
com.fasterxml.jackson.databind.JsonNode labelsNode = root.get("labels");
83+
List<String> result = new ArrayList<>();
84+
for (com.fasterxml.jackson.databind.JsonNode item : labelsNode) {
85+
result.add(item.asText());
86+
}
87+
return result;
88+
}
89+
}
90+
91+
return Collections.emptyList();
92+
}
93+
94+
/**
95+
* Opens the named label image from the {@code labels/} sub-group.
96+
*/
97+
default MultiscaleImage openLabel(String name) throws IOException, ZarrException {
98+
return MultiscaleImage.open(getStoreHandle().resolve("labels").resolve(name));
99+
}
100+
101+
/**
102+
* Opens an OME-Zarr multiscale image at the given store handle, auto-detecting the Zarr version.
103+
*
104+
* <p>Tries v0.6 (zarr.json with version "0.6"), then v0.5 (zarr.json with "ome" key), then v0.4 (.zattrs with "multiscales" key).
105+
*/
106+
static MultiscaleImage open(StoreHandle storeHandle) throws IOException, ZarrException {
107+
// Try version >= 0.5: zarr.json with "ome" key
108+
StoreHandle zarrJson = storeHandle.resolve(Node.ZARR_JSON);
109+
if (zarrJson.exists()) {
110+
com.fasterxml.jackson.databind.ObjectMapper mapper = OmeObjectMappers.makeV3Mapper();
111+
byte[] bytes = Utils.toArray(zarrJson.readNonNull());
112+
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes);
113+
com.fasterxml.jackson.databind.JsonNode attrs = root.get("attributes");
114+
if (attrs != null && attrs.has("ome")) {
115+
com.fasterxml.jackson.databind.JsonNode omeNode = attrs.get("ome");
116+
String version = omeNode.has("version") ? omeNode.get("version").asText() : "";
117+
if (version.startsWith("0.6")) {
118+
return dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.openMultiscaleImage(storeHandle);
119+
}
120+
return dev.zarr.zarrjava.experimental.ome.v0_5.MultiscaleImage.openMultiscaleImage(storeHandle);
121+
}
122+
}
123+
124+
// Try v0.4: .zattrs with "multiscales" key
125+
StoreHandle zattrs = storeHandle.resolve(Node.ZATTRS);
126+
if (zattrs.exists()) {
127+
com.fasterxml.jackson.databind.ObjectMapper mapper = OmeObjectMappers.makeV2Mapper();
128+
byte[] bytes = Utils.toArray(zattrs.readNonNull());
129+
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(bytes);
130+
if (root.has("multiscales")) {
131+
return dev.zarr.zarrjava.experimental.ome.v0_4.MultiscaleImage.openMultiscaleImage(storeHandle);
132+
}
133+
}
134+
135+
throw new ZarrException("No OME-Zarr multiscale metadata found at " + storeHandle);
136+
}
137+
}

0 commit comments

Comments
 (0)