Skip to content

Commit d74e779

Browse files
Initial Zarr v3 support
1 parent 46b3e82 commit d74e779

File tree

3 files changed

+341
-118
lines changed

3 files changed

+341
-118
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ repositories {
3131
dependencies {
3232
implementation 'net.java.dev.jna:jna:5.10.0'
3333
implementation 'dev.zarr:jzarr:0.4.2'
34+
implementation 'dev.zarr:zarr-java:0.0.4'
3435
implementation 'info.picocli:picocli:4.7.5'
3536
implementation 'me.tongfei:progressbar:0.9.0'
3637
implementation 'ome:formats-bsd:8.3.0'

src/main/java/com/glencoesoftware/pyramid/PyramidFromDirectoryWriter.java

Lines changed: 186 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package com.glencoesoftware.pyramid;
99

1010
import java.io.IOException;
11+
import java.nio.ByteBuffer;
1112
import java.nio.ByteOrder;
1213
import java.nio.file.Files;
1314
import java.nio.file.Path;
@@ -69,9 +70,6 @@
6970
import org.perf4j.StopWatch;
7071
import org.perf4j.slf4j.Slf4JStopWatch;
7172

72-
import com.bc.zarr.DataType;
73-
import com.bc.zarr.ZarrArray;
74-
import com.bc.zarr.ZarrGroup;
7573
import com.glencoesoftware.bioformats2raw.IProgressListener;
7674
import com.glencoesoftware.bioformats2raw.NoOpProgressListener;
7775
import com.glencoesoftware.bioformats2raw.ProgressBarListener;
@@ -81,6 +79,20 @@
8179
import picocli.CommandLine.Option;
8280
import picocli.CommandLine.Parameters;
8381

82+
// Zarr v2
83+
84+
import com.bc.zarr.DataType;
85+
import com.bc.zarr.ZarrArray;
86+
import com.bc.zarr.ZarrGroup;
87+
88+
// Zarr v3
89+
90+
import dev.zarr.zarrjava.ZarrException;
91+
import dev.zarr.zarrjava.store.FilesystemStore;
92+
import dev.zarr.zarrjava.utils.Utils;
93+
import dev.zarr.zarrjava.v3.Array;
94+
import dev.zarr.zarrjava.v3.Group;
95+
8496
/**
8597
* Writes a pyramid OME-TIFF file or Bio-Formats 5.9.x "Faas" TIFF file.
8698
* Image tiles are read from files within a specific folder structure:
@@ -142,8 +154,13 @@ public class PyramidFromDirectoryWriter implements Callable<Void> {
142154

143155
String imageFile = null;
144156

157+
// used for reading v2 data
145158
private ZarrGroup reader = null;
146159

160+
// used for reading v3 data
161+
private Group v3Reader = null;
162+
private FilesystemStore v3Store = null;
163+
147164
/** Writer metadata. */
148165
OMEPyramidStore metadata;
149166
OMEPyramidStore binaryOnly;
@@ -672,6 +689,38 @@ private byte[] getInputTileBytes(PyramidSeries s, int resolution,
672689
y * descriptor.tileSizeY, x * descriptor.tileSizeX);
673690
int[] shape = s.getArray(1, 1, 1, realHeight, realWidth);
674691

692+
if (isV3()) {
693+
return readV3Tile(s, descriptor, pos, shape, gridPosition);
694+
}
695+
return readV2Tile(s, descriptor, pos, shape, gridPosition);
696+
}
697+
698+
private byte[] readV3Tile(PyramidSeries s, ResolutionDescriptor descriptor,
699+
int[] pos, int[] shape, int[] gridPosition)
700+
throws FormatException, IOException
701+
{
702+
Array block = getZarrV3Array(descriptor.path);
703+
if (block == null) {
704+
throw new FormatException("Could not find block = " + descriptor.path +
705+
", position = [" + pos[0] + ", " + pos[1] + ", " + pos[2] + "]");
706+
}
707+
try {
708+
ucar.ma2.Array tile = block.read(Utils.toLongArray(gridPosition), shape);
709+
ByteBuffer buf = tile.getDataAsByteBuffer(
710+
s.littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
711+
byte[] bytes = new byte[buf.remaining()];
712+
buf.get(bytes);
713+
return bytes;
714+
}
715+
catch (ZarrException e) {
716+
throw new FormatException(e);
717+
}
718+
}
719+
720+
private byte[] readV2Tile(PyramidSeries s, ResolutionDescriptor descriptor,
721+
int[] pos, int[] shape, int[] gridPosition)
722+
throws FormatException, IOException
723+
{
675724
ZarrArray block = reader.openArray(descriptor.path);
676725

677726
if (block == null) {
@@ -769,10 +818,18 @@ private void populateMetadata() throws IOException {
769818
metadata.setWellColumn(new NonNegativeInteger(colIndex), 0, i);
770819
metadata.setWellRow(new NonNegativeInteger(rowIndex), 0, i);
771820

772-
ZarrGroup wellGroup = getZarrGroup(well);
821+
Map<String, Object> wellGroupAttrs = null;
822+
if (isV3()) {
823+
Group v3WellGroup = getZarrGroupV3(well);
824+
wellGroupAttrs = v3WellGroup.metadata.attributes;
825+
}
826+
else {
827+
ZarrGroup wellGroup = getZarrGroup(well);
828+
wellGroupAttrs = wellGroup.getAttributes();
829+
}
773830

774831
Map<String, Object> wellAttr =
775-
(Map<String, Object>) wellGroup.getAttributes().get("well");
832+
(Map<String, Object>) wellGroupAttrs.get("well");
776833
List<Map<String, Object>> images =
777834
(List<Map<String, Object>>) wellAttr.get("images");
778835
for (int img=0; img<images.size(); img++) {
@@ -864,6 +921,31 @@ private PixelType getPixelType(DataType type) {
864921
}
865922
}
866923

924+
private PixelType getV3PixelType(dev.zarr.zarrjava.v3.DataType type) {
925+
switch (type) {
926+
case BOOL:
927+
return PixelType.BIT;
928+
case INT8:
929+
return PixelType.INT8;
930+
case INT16:
931+
return PixelType.INT16;
932+
case INT32:
933+
return PixelType.INT32;
934+
case UINT8:
935+
return PixelType.UINT8;
936+
case UINT16:
937+
return PixelType.UINT16;
938+
case UINT32:
939+
return PixelType.UINT32;
940+
case FLOAT32:
941+
return PixelType.FLOAT;
942+
case FLOAT64:
943+
return PixelType.DOUBLE;
944+
default:
945+
throw new IllegalArgumentException("Unsupported pixel type: " + type);
946+
}
947+
}
948+
867949
private ZarrGroup getZarrGroup(String path) throws IOException {
868950
return ZarrGroup.open(inputDirectory.resolve(path).toString());
869951
}
@@ -872,12 +954,33 @@ private int getSubgroupCount(String path) throws IOException {
872954
return getZarrGroup(path).getGroupKeys().size();
873955
}
874956

957+
private Group getZarrGroupV3(String... path) throws IOException {
958+
return Group.open(v3Store.resolve(path));
959+
}
960+
961+
private Array getZarrV3Array(String... path)
962+
throws FormatException, IOException
963+
{
964+
try {
965+
return Array.open(v3Store.resolve(path));
966+
}
967+
catch (ZarrException e) {
968+
throw new FormatException(e);
969+
}
970+
}
971+
875972
/**
876973
* Calculate the number of series.
877974
*
878975
* @return number of series
879976
*/
880977
private int getSeriesCount() throws IOException {
978+
if (isV3()) {
979+
// TODO: handle plate data
980+
return (int) v3Reader.storeHandle.list()
981+
.filter(key -> !key.equals("OME") && !key.equals("zarr.json"))
982+
.count();
983+
}
881984
Set<String> groupKeys = reader.getGroupKeys();
882985
groupKeys.remove("OME");
883986
int groupKeyCount = groupKeys.size();
@@ -904,18 +1007,35 @@ private int getSeriesCount() throws IOException {
9041007
* @param s current series
9051008
*/
9061009
private void findNumberOfResolutions(PyramidSeries s) throws IOException {
907-
ZarrGroup seriesGroup = getZarrGroup(s.path);
908-
if (seriesGroup == null) {
909-
throw new IOException("Expected series " + s.index + " not found");
1010+
int arrayKeys = 0;
1011+
List<Map<String, Object>> multiscales = null;
1012+
1013+
if (isV3()) {
1014+
Group v3Series = getZarrGroupV3(s.path);
1015+
if (v3Series == null) {
1016+
throw new IOException("Expected series " + s.index + " not found");
1017+
}
1018+
1019+
Map<String, Object> ome =
1020+
(Map<String, Object>) v3Series.metadata.attributes.get("ome");
1021+
multiscales = (List<Map<String, Object>>) ome.get("multiscales");
1022+
}
1023+
else {
1024+
ZarrGroup seriesGroup = getZarrGroup(s.path);
1025+
if (seriesGroup == null) {
1026+
throw new IOException("Expected series " + s.index + " not found");
1027+
}
1028+
arrayKeys = seriesGroup.getArrayKeys().size();
1029+
1030+
Map<String, Object> seriesAttributes = seriesGroup.getAttributes();
1031+
multiscales =
1032+
(List<Map<String, Object>>) seriesAttributes.get("multiscales");
9101033
}
9111034

9121035
// use multiscales metadata if it exists, to distinguish between
9131036
// resolutions and labels
9141037
// if no multiscales metadata (older dataset?), assume no labels
9151038
// and just use the path listing length
916-
Map<String, Object> seriesAttributes = seriesGroup.getAttributes();
917-
List<Map<String, Object>> multiscales =
918-
(List<Map<String, Object>>) seriesAttributes.get("multiscales");
9191039
if (multiscales != null && multiscales.size() > 0) {
9201040
List<Map<String, Object>> datasets =
9211041
(List<Map<String, Object>>) multiscales.get(0).get("datasets");
@@ -925,7 +1045,7 @@ private void findNumberOfResolutions(PyramidSeries s) throws IOException {
9251045
}
9261046

9271047
if (s.numberOfResolutions == 0) {
928-
s.numberOfResolutions = seriesGroup.getArrayKeys().size();
1048+
s.numberOfResolutions = arrayKeys;
9291049
}
9301050
}
9311051

@@ -938,11 +1058,18 @@ public void initialize()
9381058
{
9391059
createReader();
9401060

941-
if (reader == null) {
1061+
if (reader == null && v3Store == null) {
9421062
throw new FormatException("Could not create a reader");
9431063
}
9441064

945-
Map<String, Object> attributes = reader.getAttributes();
1065+
Map<String, Object> attributes = null;
1066+
if (isV3()) {
1067+
attributes = v3Reader.metadata.attributes;
1068+
attributes = (Map<String, Object>) attributes.get("ome");
1069+
}
1070+
else {
1071+
attributes = reader.getAttributes();
1072+
}
9461073
Integer layoutVersion = (Integer) attributes.get("bioformats2raw.layout");
9471074
if (layoutVersion == null) {
9481075
LOG.warn("Layout version not recorded; may be unsupported");
@@ -1095,12 +1222,30 @@ else if (imageFile != null && !imageFile.isEmpty()) {
10951222
s.dimensionLengths[s.dimensionOrder.indexOf("C") - 2] = s.c;
10961223

10971224
// make sure that OME-XML and first resolution array have same dimensions
1098-
ZarrGroup imgGroup = getZarrGroup(s.path);
1099-
ZarrArray imgArray = imgGroup.openArray("0");
1100-
int[] dims = imgArray.getShape();
1225+
Map<String, Object> firstResAttrs = null;
1226+
int[] dims = null;
1227+
boolean bigEndian = false;
1228+
PixelType type = null;
1229+
1230+
if (isV3()) {
1231+
Group v3ImgGroup = getZarrGroupV3(s.path);
1232+
firstResAttrs =
1233+
(Map<String, Object>) v3ImgGroup.metadata.attributes.get("ome");
1234+
Array v3Array = getZarrV3Array(s.path, "0");
1235+
dims = Utils.toIntArray(v3Array.metadata.shape);
1236+
type = getV3PixelType(v3Array.metadata.dataType);
1237+
}
1238+
else {
1239+
ZarrGroup imgGroup = getZarrGroup(s.path);
1240+
ZarrArray imgArray = imgGroup.openArray("0");
1241+
dims = imgArray.getShape();
1242+
firstResAttrs = imgGroup.getAttributes();
1243+
bigEndian = imgArray.getByteOrder() == ByteOrder.BIG_ENDIAN;
1244+
type = getPixelType(imgArray.getDataType());
1245+
}
11011246

11021247
List<Map<String, Object>> imgMultiscales =
1103-
(List<Map<String, Object>>) imgGroup.getAttributes().get("multiscales");
1248+
(List<Map<String, Object>>) firstResAttrs.get("multiscales");
11041249
List<Map<String, Object>> imgAxes = null;
11051250
int channelIndex = -1;
11061251
if (imgMultiscales != null) {
@@ -1146,15 +1291,13 @@ else if (s.c < dims[channelIndex]) {
11461291
s.planeCount = s.z * s.t;
11471292

11481293
// Zarr format allows both little and big endian order
1149-
boolean bigEndian = imgArray.getByteOrder() == ByteOrder.BIG_ENDIAN;
11501294
s.littleEndian = !bigEndian;
11511295
if (bigEndian != metadata.getPixelsBigEndian(seriesIndex)) {
11521296
LOG.debug("Setting BigEndian={} for series {}", bigEndian, seriesIndex);
11531297
metadata.setPixelsBigEndian(bigEndian, seriesIndex);
11541298
}
11551299

11561300
// make sure pixel types are consistent between Zarr and OME metadata
1157-
PixelType type = getPixelType(imgArray.getDataType());
11581301
s.pixelType = FormatTools.pixelTypeFromString(type.getValue());
11591302
if (type != metadata.getPixelsType(seriesIndex)) {
11601303
LOG.debug("Setting PixelsType = {} for series {}", type, seriesIndex);
@@ -1208,7 +1351,12 @@ else if (rgb) {
12081351
}
12091352

12101353
s.planeCount *= effectiveChannels;
1211-
s.describePyramid(reader, metadata);
1354+
if (isV3()) {
1355+
s.describePyramidV3(v3Store, metadata);
1356+
}
1357+
else {
1358+
s.describePyramid(reader, metadata);
1359+
}
12121360

12131361
metadata.setTiffDataIFD(new NonNegativeInteger(totalPlanes), s.index, 0);
12141362

@@ -1917,10 +2065,26 @@ private void createReader() throws IOException {
19172065
LOG.debug("attempting to open {}", zarr);
19182066
if (Files.exists(zarr)) {
19192067
LOG.debug(" zarr directory exists");
1920-
reader = ZarrGroup.open(zarr.toString());
2068+
try {
2069+
reader = ZarrGroup.open(zarr.toString());
2070+
}
2071+
catch (IOException e) {
2072+
LOG.debug("Could not open as v2", e);
2073+
}
2074+
2075+
// couldn't open or no attributes implies we should try
2076+
// reading as v3 instead of v2
2077+
if (reader == null || reader.getAttributes().size() == 0) {
2078+
v3Store = new FilesystemStore(zarr);
2079+
v3Reader = Group.open(v3Store.resolve());
2080+
}
19212081
}
19222082
}
19232083

2084+
private boolean isV3() {
2085+
return v3Store != null;
2086+
}
2087+
19242088
/**
19252089
* Set up the root logger, turning on debug logging if appropriate.
19262090
*/

0 commit comments

Comments
 (0)