Skip to content

Report Modulo dimensions as separate axes (RFC-3) - #330

Open
melissalinkert wants to merge 13 commits into
glencoesoftware:masterfrom
melissalinkert:rfc-3
Open

Report Modulo dimensions as separate axes (RFC-3)#330
melissalinkert wants to merge 13 commits into
glencoesoftware:masterfrom
melissalinkert:rfc-3

Conversation

@melissalinkert

Copy link
Copy Markdown
Member

See https://ngff.openmicroscopy.org/rfc/3/#proposal, fixes #328.

Minimally tested with https://downloads.openmicroscopy.org/images/OME-TIFF/2016-06/modulo/, but needs unit tests which I need to think about a bit more as that doesn't appear to be possible with just .fake files. I think I also want to get the corresponding raw2ometiff changes in place before assigning for review, since it will be much easier to test when the input data can be directly compared with an output OME-TIFF.

@melissalinkert melissalinkert added this to the 0.13.0 milestone Jun 24, 2026
@melissalinkert
melissalinkert marked this pull request as ready for review June 25, 2026 22:03
@melissalinkert

Copy link
Copy Markdown
Member Author

@sbesson mentioned a discussion with @jni around providing test data for RFC-3. We're likely to start seriously testing this functionality in the next few weeks (once corresponding raw2ometiff work is done). If there are datasets we should include in testing, it would be helpful to collect them under https://zenodo.org/communities/bio-formats.

@sbesson

sbesson commented Jul 1, 2026

Copy link
Copy Markdown
Member

For reference, representative samples were discussed in the context of an imagesc.zulip thread.

Seconding Melissa's request and as discussed during the OME-Zarr 1.0 meeting in Heidelberg a few weeks ago, public and permissively licensed datasets are essential to support the upcoming OME-Zarr 1.0 work and allow the generation of derived OME-Zarr samples under similar use, modification and distribution terms. In the thread above, the FLIM ICS datasets would qualify as they have been placed in the public domain under CC0.

@jni

jni commented Jul 4, 2026

Copy link
Copy Markdown

Hello team! Please see clbarnes/ome-zarr-rfc3-data#1 for some real datasets. One of those (EBSD) is just "I got permission from the authors to share it", but unlicensed. I'll double check with them now whether I can make it more public and how they would like it cited if -BY. The other (FLIM, BioImage Archive S-BIAD1967) is CC-BY 4.0.

@jni

jni commented Jul 6, 2026

Copy link
Copy Markdown

I've confirmed with the creators of the EBSD dataset that they are happy with CC-BY 4.0. Here is the attribution string:

EBSD scan images of commercially pure (CP) Titanium, provided by Jie Luo, Ethan Sprague, and Michael Preuss, Monash University, Department of Materials Science, 2025.

@melissalinkert

Copy link
Copy Markdown
Member Author

Thanks @jni!

@sbesson sbesson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RFC-3 proposes to update the OME-Zarr specification to relax the requirement to have between 2 and 5 dimensions as well as the constraints on the axis naming and ordering.

In the context of bioformats2raw, the most amenable use case is the extension of the OME model to support 6, 7 or 8 dimensions via the modulo annotation - see https://ome-model.readthedocs.io/en/stable/developers/6d-7d-and-8d-storage.html. This extension is implemented across several reader of Bio-Formats e.g. to deal with FLIM file formats which are one of the motivations behind the RFC - see https://ngff.openmicroscopy.org/rfc/3/index.html#fluorescence-lifetime-imaging-microscopy-flim.

To test this change, I collected all the public samples of the curated QA repository that are read using this extension. Each sample has been:

  • converted to OME-Zarr 0.5 using a build of bioformats2raw with this PR
  • converted to OME-Zarr 1.0-DEV using a build of bioformats2raw with this PR
  • converted from OME-Zarr 1.0-DEV to OME-TIFF using a build of raw2ometiff with glencoesoftware/raw2ometiff#156 included
  • converted to OME-TIFF using Bio-Formats 8.5.0 and bfconvert

The following generation script has been used

#! /bin/sh

# Generates OME-Zarr 0.5 and 1.0-DEV (with RFC-3) from a series of
# public sample files implementing the Modulo extension of the OME model
# The following application are expected alongside this script and the source files
# - the command-line Bio-Formats 8.5.0  utilities
# - a build of bioformats2raw with https://github.com/glencoesoftware/bioformats2raw/pull/330
# - a build of raw2ometiff with https://github.com/glencoesoftware/raw2ometiff/pull/156

set -e
set -x

BASE_PATH=~/Desktop/RFC-3
BF_MAX_MEM=8g 

# Clean the generated folders
rm -rfv $BASE_PATH/ome-zarr
rm -rfv $BASE_PATH/ome-tiff
for format in ics oir ome-tiff sdt spc; do
    mkdir -p $BASE_PATH/ome-zarr/0.5/$format
    mkdir -p $BASE_PATH/ome-zarr/1.0-DEV/$format
    mkdir -p $BASE_PATH/ome-tiff/raw2ometiff/$format
    mkdir -p $BASE_PATH/ome-tiff/bfconvert/$format
done

convert () {
  echo "Converting $1"
  filename="${1%.*}"
  ./bioformats2raw-0.13.0-SNAPSHOT/bin/bioformats2raw $BASE_PATH/sources/$1 $BASE_PATH/ome-zarr/0.5/$filename.zarr --ngff-version 0.5 -p
  ./bioformats2raw-0.13.0-SNAPSHOT/bin/bioformats2raw $BASE_PATH/sources/$1 $BASE_PATH/ome-zarr/1.0-DEV/$filename.zarr --ngff-version 1.0-DEV -p
  ./raw2ometiff-0.11.0-SNAPSHOT/bin/raw2ometiff $BASE_PATH/ome-zarr/1.0-DEV/$filename.zarr $BASE_PATH/ome-tiff/raw2ometiff/$filename.ome.tiff -p
  ./bftools/bfconvert $BASE_PATH/sources/$1 $BASE_PATH/ome-tiff/bfconvert/$filename.ome.tiff -compression LZW
}

# https://downloads.openmicroscopy.org/images/ICS/gh-4197/
convert ics/20240612_training_test-tiles_01_stitched.ics

# https://downloads.openmicroscopy.org/images/SPC-FIFO/biofisika/
convert spc/conv-256x256.spc

# https://downloads.openmicroscopy.org/images/Olympus-OIR/gh-4205/zenodo-12773657/
for filename in DAPI_mCherry_4T_5Lambda-420-630-w10nm-s50nm DAPI_mCherry_4Z_5Lambda-420-630-w10nm-s50nm DAPI-mCherry_3T_4Z_5Lambda-420-630-w10nm-s50nm; do
    convert oir/$filename.oir
done

# https://downloads.openmicroscopy.org/images/OME-TIFF/2016-06/modulo/
for filename in FLIM-ModuloAlongC FLIM-ModuloAlongT-TSCPC LAMBDA-ModuloAlongZ-ModuloAlongT SPIM-ModuloAlongZ; do
    convert ome-tiff/$filename.ome.tiff
done

# https://downloads.openmicroscopy.org/images/SDT/gh-4198/
convert sdt/FocalCheck_A1_20x_8xzoom_800nm.sdt

and all sources and secondary data (OME-TIFF & OME-Zarr) has been uploaded to the public gs-public-zarr-dev AWS S3 bucket under the RFC-3 prefix - see https://gs-public-zarr-dev.s3.amazonaws.com/index.html#RFC-3. Note this is a development bucket and all data stored there is not expected to be persistent and will be cleaned up eventually.

This raises the question of the validation of this output. The OME NGFF validator can be used to point at some of these samples e.g. https://ome.github.io/ome-ngff-validator/?source=https://gs-public-zarr-dev.s3.amazonaws.com/RFC-3/ome-zarr/1.0-DEV/oir/DAPI-mCherry_3T_4Z_5Lambda-420-630-w10nm-s50nm.zarr/0/ or https://ome.github.io/ome-ngff-validator/?source=https://gs-public-zarr-dev.s3.amazonaws.com/RFC-3/ome-zarr/1.0-DEV/ome-tiff/LAMBDA-ModuloAlongZ-ModuloAlongT.ome.zarr/0. The absence of schema makes it hard to validate the metadata. At minimum, the data is chunked alongside more than 5 dimensions and the individual chunks can be loaded.

Next steps will be to:

  • generate additional variants of these datasets using compact dimensions (no singleton) and sharding
  • validate the OME-TIFF filesets and compare them to the ground-truth generated by bftools

@@ -1247,7 +1248,8 @@ public SupportedVersions getNGFFVersion() {
* @return true if Zarr v3 data should be written
*/
public boolean getV3() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given we will need to keep maintaining this everytime we had a new versions, I wonder if this should be moved to SupportedVersions so that we have an API to retrieve the underlying Zarr versions associated with each version of the OME-Zarr specification.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See 131e394.

int[] offset = new int[axes.size()];
Arrays.fill(offset, 0);

boolean useModulo = getNGFFVersion() == SupportedVersions.NGFF_DEV;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, as we add new versions, this will need to be kept up-to-date. Almost feels like we need an API of the like of supportsRFC3(value) for each version.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

131e394 has one approach to this, but open to other ideas.

@sbesson sbesson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retesting the last commit, I came across an error while using the new development version and the --compact option to reduce the number of singleton dimensions

sbesson@Sebastien-GS-MacBook-Pro-2025 RFC-3 % ./bioformats2raw-0.13.0-SNAPSHOT/bin/bioformats2raw /Users/sbesson/Desktop/RFC-3/sources/ics/20240612_training_test-tiles_01_stitched.ics /Users/sbesson/Desktop/RFC-3/ome-zarr/1.0-DEV-compact/ics/20240612_training_test-tiles_01_stitched.zarr --ngff-version 1.0-DEV --compact -p
2026-08-04 11:15:57,482 [main] WARN  o.x.m.e.h.ExperimentTypeEnumHandler - Unknown ExperimentType value 'null' will be stored as "Other"
2026-08-04 11:15:57,512 [main] WARN  o.x.m.e.h.ExperimentTypeEnumHandler - Unknown ExperimentType value 'null' will be stored as "Other"
2026-08-04 11:15:57,545 [main] WARN  o.x.m.e.h.ExperimentTypeEnumHandler - Unknown ExperimentType value 'null' will be stored as "Other"
2026-08-04 11:15:57,552 [main] WARN  o.x.m.e.h.ExperimentTypeEnumHandler - Unknown ExperimentType value 'null' will be stored as "Other"
2026-08-04 11:15:57,560 [main] WARN  o.x.m.e.h.ExperimentTypeEnumHandler - Unknown ExperimentType value 'null' will be stored as "Other"
[0/0]  62% [=====================================================================================                                                   ] 541/864 (0:00:03 / 0:00:01) 
2026-08-04 11:16:01,184 [pool-1-thread-2] ERROR c.g.bioformats2raw.Converter - Failure processing chunk; resolution=0 plane=28 xx=2048 yy=2048 zz=4 width=1024 height=277 depth=1
java.lang.RuntimeException: dev.zarr.zarrjava.ZarrException: Error in decoding blosc.
	at dev.zarr.zarrjava.v3.codec.core.ShardingIndexedCodec.lambda$decodeInternal$3(ShardingIndexedCodec.java:238)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
	at java.base/java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:290)
	at java.base/java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:746)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
	at java.base/java.util.concurrent.ForkJoinTask.doInvoke(ForkJoinTask.java:408)
	at java.base/java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:736)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateParallel(ForEachOps.java:159)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
	at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:661)
	at dev.zarr.zarrjava.v3.codec.core.ShardingIndexedCodec.decodeInternal(ShardingIndexedCodec.java:212)
	at dev.zarr.zarrjava.v3.codec.core.ShardingIndexedCodec.decode(ShardingIndexedCodec.java:101)
	at dev.zarr.zarrjava.core.codec.CodecPipeline.decode(CodecPipeline.java:122)
	at dev.zarr.zarrjava.core.Array.readChunk(Array.java:181)
	at dev.zarr.zarrjava.core.Array.lambda$write$0(Array.java:116)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
	at java.base/java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:290)
	at java.base/java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:746)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
	at java.base/java.util.concurrent.ForkJoinTask.doInvoke(ForkJoinTask.java:408)
	at java.base/java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:736)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateParallel(ForEachOps.java:159)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
	at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:661)
	at dev.zarr.zarrjava.core.Array.write(Array.java:99)
	at dev.zarr.zarrjava.core.Array.write(Array.java:309)
	at com.glencoesoftware.bioformats2raw.Converter.writeBytes(Converter.java:2015)
	at com.glencoesoftware.bioformats2raw.Converter.processChunk(Converter.java:2495)
	at com.glencoesoftware.bioformats2raw.Converter.lambda$saveResolutions$6(Converter.java:2727)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:829)
Caused by: dev.zarr.zarrjava.ZarrException: Error in decoding blosc.
	at dev.zarr.zarrjava.core.codec.core.BloscCodec.decode(BloscCodec.java:26)
	at dev.zarr.zarrjava.core.codec.CodecPipeline.decode(CodecPipeline.java:114)
	at dev.zarr.zarrjava.v3.codec.core.ShardingIndexedCodec.lambda$decodeInternal$3(ShardingIndexedCodec.java:231)
	... 39 common frames omitted
Caused by: java.lang.RuntimeException: Buffer does not contain valid blosc-encoded contents
	at com.scalableminds.bloscjava.Blosc._decompress(Native Method)
	at com.scalableminds.bloscjava.Blosc.decompress(Blosc.java:141)
	at com.scalableminds.bloscjava.Blosc.decompress(Blosc.java:151)
	at dev.zarr.zarrjava.core.codec.core.BloscCodec.decode(BloscCodec.java:24)
	... 41 common frames omitted
2026-08-04 11:16:01,184 [pool-1-thread-1] ERROR c.g.bioformats2raw.Converter - Failure processing chunk; resolution=0 plane=20 xx=2048 yy=2048 zz=4 width=1024 height=277 depth=1
java.lang.RuntimeException: dev.zarr.zarrjava.ZarrException: Error in decoding blosc.
	at dev.zarr.zarrjava.v3.codec.core.ShardingIndexedCodec.lambda$decodeInternal$3(ShardingIndexedCodec.java:238)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
	at java.base/java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:290)
	at java.base/java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:746)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
	at java.base/java.util.concurrent.ForkJoinTask.doInvoke(ForkJoinTask.java:408)
	at java.base/java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:736)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateParallel(ForEachOps.java:159)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
	at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:661)
	at dev.zarr.zarrjava.v3.codec.core.ShardingIndexedCodec.decodeInternal(ShardingIndexedCodec.java:212)
	at dev.zarr.zarrjava.v3.codec.core.ShardingIndexedCodec.decode(ShardingIndexedCodec.java:101)
	at dev.zarr.zarrjava.core.codec.CodecPipeline.decode(CodecPipeline.java:122)
	at dev.zarr.zarrjava.core.Array.readChunk(Array.java:181)
	at dev.zarr.zarrjava.core.Array.lambda$write$0(Array.java:116)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
	at java.base/java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:290)
	at java.base/java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:746)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
	at java.base/java.util.concurrent.ForkJoinTask.doInvoke(ForkJoinTask.java:408)
	at java.base/java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:736)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateParallel(ForEachOps.java:159)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
	at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:661)
	at dev.zarr.zarrjava.core.Array.write(Array.java:99)
	at dev.zarr.zarrjava.core.Array.write(Array.java:309)
	at com.glencoesoftware.bioformats2raw.Converter.writeBytes(Converter.java:2015)
	at com.glencoesoftware.bioformats2raw.Converter.processChunk(Converter.java:2495)
	at com.glencoesoftware.bioformats2raw.Converter.lambda$saveResolutions$6(Converter.java:2727)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:829)
Caused by: dev.zarr.zarrjava.ZarrException: Error in decoding blosc.
	at dev.zarr.zarrjava.core.codec.core.BloscCodec.decode(BloscCodec.java:26)
	at dev.zarr.zarrjava.core.codec.CodecPipeline.decode(CodecPipeline.java:114)
	at dev.zarr.zarrjava.v3.codec.core.ShardingIndexedCodec.lambda$decodeInternal$3(ShardingIndexedCodec.java:231)
	... 39 common frames omitted
Caused by: java.lang.RuntimeException: Buffer does not contain valid blosc-encoded contents
	at com.scalableminds.bloscjava.Blosc._decompress(Native Method)
	at com.scalableminds.bloscjava.Blosc.decompress(Blosc.java:141)
	at com.scalableminds.bloscjava.Blosc.decompress(Blosc.java:151)
	at dev.zarr.zarrjava.core.codec.core.BloscCodec.decode(BloscCodec.java:24)

NGFF_05("0.5");
NGFF_01("0.1", 2, null),
NGFF_04("0.4", 2, null),
NGFF_05("0.5", 3, null),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For completeness, proposing

Suggested change
NGFF_05("0.5", 3, null),
NGFF_05("0.5", 3, new Integer[] {2}),

NGFF_01("0.1", 2, null),
NGFF_04("0.4", 2, null),
NGFF_05("0.5", 3, null),
NGFF_DEV("1.0-DEV", 3, new Integer[] {3});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
NGFF_DEV("1.0-DEV", 3, new Integer[] {3});
NGFF_DEV("1.0-DEV", 3, new Integer[] {2, 3});

as per above

@melissalinkert

Copy link
Copy Markdown
Member Author

I haven't so far been able to reproduce the error with --compact, but have added tests that use --compact with modulo data. Do you see the same error with --max-workers 1?

@erindiel

erindiel commented Aug 6, 2026

Copy link
Copy Markdown
Member

I compared the data generated by @sbesson in gs-public-zarr-dev by importing into OMERO Plus, which uses the omero-zarr-pixel-buffer for reading OME-Zarr data. NGFF 0.5 is supported, but the NGFF 1.0-DEV produced by this PR is not; however, when NGFF 1.0-DEV data is converted to OME-TIFF, this can be imported.

The following sources were compared to their raw2ometiff converted counterpart OME-TIFF (via NGFF 1.0-DEV):

2026-07-30 12:42:58   36165076 RFC-3/sources/oir/DAPI-mCherry_3T_4Z_5Lambda-420-630-w10nm-s50nm.oir
2026-07-30 12:43:17   14603409 RFC-3/sources/oir/DAPI_mCherry_4T_5Lambda-420-630-w10nm-s50nm.oir
2026-07-30 12:43:19   14595535 RFC-3/sources/oir/DAPI_mCherry_4Z_5Lambda-420-630-w10nm-s50nm.oir
2026-07-30 12:43:24     461842 RFC-3/sources/ome-tiff/FLIM-ModuloAlongC.ome.tiff
2026-07-30 12:43:24    1221534 RFC-3/sources/ome-tiff/FLIM-ModuloAlongT-TSCPC.ome.tiff
2026-07-30 12:43:26    2108703 RFC-3/sources/ome-tiff/LAMBDA-ModuloAlongZ-ModuloAlongT.ome.tiff
2026-07-30 12:43:26    7225903 RFC-3/sources/ome-tiff/SPIM-ModuloAlongZ.ome.tiff
2026-07-30 12:43:27   30039135 RFC-3/sources/sdt/FocalCheck_A1_20x_8xzoom_800nm.sdt

The following were checked for consistency:

  • pixel type
  • XYZTC dimensions
  • channel names
  • per-channel min/max values
  • pixel sizes (XYZ)
  • channel colors
  • visual comparison of pixels in the viewer

Because RFC-3/sources/ics/20240612_training_test-tiles_01_stitched.ics does not import successfully into OMERO Plus due to lack of resolution pyramid, I compared the raw2ometiff OME-TIFF output to the OME-Zarr (NGFF 0.5).

No issues were found in the consistency checks above.

@melissalinkert

Copy link
Copy Markdown
Member Author

I was eventually able to reproduce an error with 20240612_training_test-tiles_01_stitched.ics when --compact is used. This is very specific to writing whole shards in the case where one of the two dimensions (in C, Z, or T) has length 1 and is thus compacted away.

In this test data, channel remains but the parent c has length 1 so is removed. This meant that the channel axis offset was always set to 0, which meant there were in fact multiple threads trying to write to the same shard.

I was a little tempted to instead forbid removing only one of the two axes when a modulo dimension is present; either both have length 1 and are removed, or at least one has length > 1 so they both stay. That's maybe a little easier to think about, but isn't really in the spirit of what --compact is supposed to do.

@sbesson

sbesson commented Aug 12, 2026

Copy link
Copy Markdown
Member

I was a little tempted to instead forbid removing only one of the two axes when a modulo dimension is present; either both have length 1 and are removed, or at least one has length > 1 so they both stay. That's maybe a little easier to think about, but isn't really in the spirit of what --compact is supposed to do.

I agree this is a edge case. The last commits fix the issue but then running through the raw2ometiff companion, I get

+ ./raw2ometiff-0.11.0-SNAPSHOT/bin/raw2ometiff /Users/sbesson/Desktop/RFC-3/ome-zarr/0.9.dev1/compact/ics/20240612_training_test-tiles_01_stitched.zarr /Users/sbesson/Desktop/RFC-3/ome-tiff/raw2ometiff/0.9.dev1-compact/ics/20240612_training_test-tiles_01_stitched.ome.tiff -p
Exception in thread "main" picocli.CommandLine$ExecutionException: Error while calling command (com.glencoesoftware.pyramid.PyramidFromDirectoryWriter@41294f8): java.lang.RuntimeException: loci.formats.FormatException: C axis expected but not defined
	at picocli.CommandLine.executeUserObject(CommandLine.java:2054)
	at picocli.CommandLine.access$1500(CommandLine.java:148)
	at picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2465)
	at picocli.CommandLine$RunLast.handle(CommandLine.java:2457)
	at picocli.CommandLine$RunLast.handle(CommandLine.java:2419)
	at picocli.CommandLine$AbstractParseResultHandler.handleParseResult(CommandLine.java:2268)
	at picocli.CommandLine.parseWithHandlers(CommandLine.java:2668)
	at picocli.CommandLine.parseWithHandler(CommandLine.java:2603)
	at picocli.CommandLine.call(CommandLine.java:2879)
	at com.glencoesoftware.pyramid.PyramidFromDirectoryWriter.main(PyramidFromDirectoryWriter.java:569)
Caused by: java.lang.RuntimeException: loci.formats.FormatException: C axis expected but not defined
	at com.glencoesoftware.pyramid.PyramidFromDirectoryWriter.call(PyramidFromDirectoryWriter.java:644)
	at com.glencoesoftware.pyramid.PyramidFromDirectoryWriter.call(PyramidFromDirectoryWriter.java:110)
	at picocli.CommandLine.executeUserObject(CommandLine.java:2045)
	... 9 more
Caused by: loci.formats.FormatException: C axis expected but not defined
	at com.glencoesoftware.pyramid.PyramidSeries.setupResolution(PyramidSeries.java:200)
	at com.glencoesoftware.pyramid.PyramidSeries.describePyramid(PyramidSeries.java:103)
	at com.glencoesoftware.pyramid.PyramidFromDirectoryWriter.initialize(PyramidFromDirectoryWriter.java:1370)
	at com.glencoesoftware.pyramid.PyramidFromDirectoryWriter.call(PyramidFromDirectoryWriter.java:630)
	... 11 more

I assume if we decide to allow one of the modulo dimensions to be dropped if length is 1, we will need to handle this special case in raw2ometiff ?

@melissalinkert

Copy link
Copy Markdown
Member Author

I assume if we decide to allow one of the modulo dimensions to be dropped if length is 1, we will need to handle this special case in raw2ometiff ?

Yeah, that error is coming from the logic that is supposed to check that the OME-XML dimensions and the Zarr array dimensions all match. SizeC obviously is defined in OME-XML, but C isn't an axis in the Zarr metadata (as expected). If we're OK with how --compact behaves for this data now, then I'll update glencoesoftware/raw2ometiff#156 separately (including more test cases).

@sbesson

sbesson commented Aug 13, 2026

Copy link
Copy Markdown
Member

Thanks, I went through another round of generation of 0.9.dev1 OME-Zarr samples using the state of this PR and uploaded them to s3://gs-public-zarr-dev/RFC-3/ome-zarr - see e.g. https://ome.github.io/ome-ngff-validator/?source=https://gs-public-zarr-dev.s3.amazonaws.com/RFC-3/ome-zarr/0.9.dev1/compact/ics/20240612_training_test-tiles_01_stitched.zarr

I will validate these samples more closely tomorrow but my initial vote would be to keep the --compact behavior consistent and try to handle these special scenarios in glencoesoftware/raw2ometiff#156. If that causes an issue (e.g. because we are unable to identify alongside which dimension the modulo annotation should be), we can revisit our approach.

@melissalinkert

Copy link
Copy Markdown
Member Author

With the current state of this PR and glencoesoftware/raw2ometiff#156, 20240612_training_test-tiles_01_stitched.ics still fails to convert due to:

https://github.com/ome/bioformats/blob/develop/components/formats-api/src/loci/formats/Modulo.java#L96

The type on the Modulo object associated with C is set to Channel, but this gets overridden to other when constructing the ModuloAlongC annotation in the OME-XML. I think this is expected based on https://ome-model.readthedocs.io/en/latest/developers/6d-7d-and-8d-storage.html.

When writing the Zarr metadata, the actual Modulo object is used. This means the Zarr metadata (with --compact --ngff-version 0.9.dev1) has an axis named channel and no axis named C (expected), but the corresponding ModuloAlongC in OME/METADATA.ome.xml is:

        <Modulo namespace="http://www.openmicroscopy.org/Schemas/Additions/2011-09">
          <ModuloAlongC End="3.0" Start="0.0" Step="1.0" Type="other" TypeDescription="TCSPC"/>
        </Modulo>

raw2ometiff uses the Type on the ModuloAlong* to identify the correct axis in the Zarr metadata. I guess the simplest option would be to update this PR to set the axis name to the Type attribute of the ModuloAlong* annotation, instead of using the type on the Modulo objects directly from the reader. However, that does mean that the original type information is lost during conversion.

@melissalinkert

Copy link
Copy Markdown
Member Author

With 67f9da3, the compact .ics test above results in axes:

        "axes" : [ {
          "name" : "channel",
          "type" : "channel"
        }, {
          "unit" : "micrometer",
          "name" : "z",
          "type" : "space"
        }, {
          "unit" : "micrometer",
          "name" : "y",
          "type" : "space"
        }, {
          "unit" : "micrometer",
          "name" : "x",
          "type" : "space"
        } ],

As discussed separately today, that means the original type attribute on the Modulo object is preserved in the name attribute in Zarr. The type attribute in Zarr then matches the type attribute on the parent axis, i.e. if --compact was not used there would have been two axes with type set to channel in the Zarr metadata. This allows raw2ometiff to more easily determine which axis is the modulo axis for Z, C, or T, particularly in the case where --compact is used.

@sbesson sbesson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the latest state of this PR and glencoesoftware/raw2ometiff#156, I regenerated a complete set of datasets from the same sources as #330 (review) and going through the following conversion workflows:

  • original file format -> OME-Zarr 0.5 (default) -> OME-TIFF
  • original file format -> OME-Zarr 0.5 (--compact) -> OME-TIFF
  • original file format -> OME-Zarr 0.9.dev1 -> OME-TIFF
  • original file format -> OME-Zarr 0.9.dev1 -> OME-TIFF
  • original file format -> OME-TIFF (via bftools)

All datasets have been uploaded to s3://gs-public-zarr-dev/RFC-3 (see https://gs-public-zarr-dev.s3.amazonaws.com/index.html#RFC-3/) and an initial look at these datasets through the validator looks as expected.

I will perform a round of more in-depth validation overthe next few days. Note also progress is happening on the specification and schemas in ome/ngff-spec#190. There is an outstanding question about the dev version number that might affect this PR and the samples.

@sbesson

sbesson commented Aug 20, 2026

Copy link
Copy Markdown
Member

To simplify the dissemination and validation process, the table below contains the list of all generated OME-Zarr datasets (versions 0.5 and 0.9.dev.1, with and without --compact) that can be accessed via the OME validator

--ngff-version=0.5 --ngff-version=0.5 --compact --ngff-version=0.9.dev1 --ngff-version=0.9.dev1 --compact



20240612_training_test-tiles_01_stitched.ics
validator validator validator validator

conv-256x256.spc
validator validator validator validator

DAPI_mCherry_4T_5Lambda-420-630-w10nm-s50nm.oir
validator validator validator validator

DAPI_mCherry_4Z_5Lambda-420-630-w10nm-s50nm.oir
validator validator validator validator

DAPI-mCherry_3T_4Z_5Lambda-420-630-w10nm-s50nm.oir
validator validator validator validator

FLIM-ModuloAlongC.ome.tiff
validator validator validator validator

FLIM-ModuloAlongT-TSCPC.ome.tiff
validator validator validator validator

LAMBDA-ModuloAlongZ-ModuloAlongT.ome.tiff
validator validator validator validator

SPIM-ModuloAlongZ.ome.tiff
validator validator validator validator

FocalCheck_A1_20x_8xzoom_800nm.sdt
validator validator validator validator

@melissalinkert

Copy link
Copy Markdown
Member Author

Looking more carefully at the changes in ome/ngff-spec#190, I see that includes RFC-5, which introduces a required coordinateSystems array containing the axes. If I read correctly, I expect we'll need to update to use coordinateSystems in order to pass validation.

See also https://ngff.openmicroscopy.org/rfc/5/index.html#multiscales-metadata

@sbesson

sbesson commented Aug 20, 2026

Copy link
Copy Markdown
Member

@melissalinkert

Copy link
Copy Markdown
Member Author

See Converter starting at line 2234 - the step attribute on Modulo is what determines the order, i.e. it should reflect the native order of the original data.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add implementation for RFC-3

4 participants