@@ -95,6 +95,14 @@ public class ZarrPixelBuffer implements PixelBuffer {
9595 /** Array path vs. ZarrArray cache */
9696 private final AsyncLoadingCache <Path , ZarrArray > zarrArrayCache ;
9797
98+ /** Supported axes, X and Y are essential */
99+ public enum Axis {
100+ X , Y , Z , C , T ;
101+ }
102+
103+ /** Maps axes to their corresponding indexes */
104+ private Map <Axis , Integer > axesOrder ;
105+
98106 /**
99107 * Default constructor
100108 * @param pixels Pixels metadata for the pixel buffer
@@ -122,12 +130,14 @@ public ZarrPixelBuffer(Pixels pixels, Path root, Integer maxPlaneWidth,
122130 if (!rootGroupAttributes .containsKey ("multiscales" )) {
123131 throw new IllegalArgumentException ("Missing multiscales metadata!" );
124132 }
133+ this .axesOrder = getAxesOrder ();
125134 this .resolutionLevels = this .getResolutionLevels ();
126135 setResolutionLevel (this .resolutionLevels - 1 );
127136 if (this .resolutionLevel < 0 ) {
128137 throw new IllegalArgumentException (
129138 "This Zarr file has no pixel data" );
130139 }
140+
131141 this .maxPlaneWidth = maxPlaneWidth ;
132142 this .maxPlaneHeight = maxPlaneHeight ;
133143
@@ -196,25 +206,30 @@ private long length(int[] shape) {
196206 private void read (byte [] buffer , int [] shape , int [] offset )
197207 throws IOException {
198208 // Check planar read size (sizeX and sizeY only)
199- checkReadSize (Arrays . copyOfRange ( shape , 3 , 5 ) );
200-
209+ checkReadSize (new int [] { shape [ axesOrder . get ( Axis . X )], shape [ axesOrder . get ( Axis . Y )]} );
210+
201211 // if reading from a resolution downsampled in Z,
202212 // adjust the shape/offset for the Z coordinate only
203213 // this ensures that the correct Zs are read from the correct offsets
204214 // since the requested shape/offset may not match the underlying array
205215 int planes = 1 ;
206- int originalZIndex = offset [2 ];
216+ int originalZIndex = 1 ;
217+ if (axesOrder .containsKey (Axis .Z )) {
218+ originalZIndex = offset [axesOrder .get (Axis .Z )];
219+ }
207220 if (getSizeZ () != getTrueSizeZ ()) {
208- offset [2 ] = zIndexMap .get (originalZIndex );
209- planes = shape [2 ];
210- shape [2 ] = 1 ;
221+ offset [axesOrder . get ( Axis . Z ) ] = zIndexMap .get (originalZIndex );
222+ planes = shape [axesOrder . get ( Axis . Z ) ];
223+ shape [axesOrder . get ( Axis . Z ) ] = 1 ;
211224 }
212225
213226 try {
214227 ByteBuffer asByteBuffer = ByteBuffer .wrap (buffer );
215228 DataType dataType = array .getDataType ();
216229 for (int z =0 ; z <planes ; z ++) {
217- offset [2 ] = zIndexMap .get (originalZIndex + z );
230+ if (axesOrder .containsKey (Axis .Z )) {
231+ offset [axesOrder .get (Axis .Z )] = zIndexMap .get (originalZIndex + z );
232+ }
218233 switch (dataType ) {
219234 case u1 :
220235 case i1 :
@@ -296,7 +311,7 @@ public int[][] getChunks() throws IOException {
296311 }
297312
298313 /**
299- * Retrieves the datasets metadat of the first multiscale from the root
314+ * Retrieves the datasets metadata of the first multiscale from the root
300315 * group attributes.
301316 * @return See above.
302317 * @see #getRootGroupAttributes()
@@ -307,6 +322,37 @@ public List<Map<String, String>> getDatasets() {
307322 getMultiscalesMetadata ().get (0 ).get ("datasets" );
308323 }
309324
325+ /**
326+ * Retrieves the axes metadata of the first multiscale
327+ * @return See above.
328+ */
329+ public Map <Axis , Integer > getAxesOrder () {
330+ HashMap <Axis , Integer > order = new HashMap <>();
331+ List <Map <String , Object >> axesData = (List <Map <String , Object >>) getMultiscalesMetadata ().get (0 ).get ("axes" );
332+ if (axesData == null ) {
333+ log .warn ("No axes metadata found, defaulting to standard axes TCZYX" );
334+ order .put (Axis .T , 0 );
335+ order .put (Axis .C , 1 );
336+ order .put (Axis .Z , 2 );
337+ order .put (Axis .Y , 3 );
338+ order .put (Axis .X , 4 );
339+ } else {
340+ for (int i =0 ; i <axesData .size (); i ++) {
341+ Map <String , Object > axis = axesData .get (i );
342+ String name = axis .get ("name" ).toString ().toUpperCase ();
343+ try {
344+ order .put (Axis .valueOf (name ), i );
345+ } catch (IllegalArgumentException e ) {
346+ throw new IllegalArgumentException ("Invalid axis name (only T,C,Z,Y,X are supported): " + name );
347+ }
348+ }
349+ }
350+ if (!order .containsKey (Axis .X ) || !order .containsKey (Axis .Y )) {
351+ throw new IllegalArgumentException ("Missing X or Y axis!" );
352+ }
353+ return order ;
354+ }
355+
310356 /**
311357 * Retrieves the multiscales metadata from the root group attributes.
312358 * @return See above.
@@ -517,8 +563,33 @@ public byte[] getTileDirect(
517563 checkBounds (x , y , z , c , t );
518564 //Check check bottom-right of tile in bounds
519565 checkBounds (x + w - 1 , y + h - 1 , z , c , t );
520- int [] shape = new int [] { 1 , 1 , 1 , h , w };
521- int [] offset = new int [] { t , c , z , y , x };
566+
567+ int [] shape = new int [axesOrder .size ()];
568+ if (axesOrder .containsKey (Axis .T )) {
569+ shape [axesOrder .get (Axis .T )] = 1 ;
570+ }
571+ if (axesOrder .containsKey (Axis .C )) {
572+ shape [axesOrder .get (Axis .C )] = 1 ;
573+ }
574+ if (axesOrder .containsKey (Axis .Z )) {
575+ shape [axesOrder .get (Axis .Z )] = 1 ;
576+ }
577+ shape [axesOrder .get (Axis .Y )] = h ;
578+ shape [axesOrder .get (Axis .X )] = w ;
579+
580+ int [] offset = new int [axesOrder .size ()];
581+ if (axesOrder .containsKey (Axis .T )) {
582+ offset [axesOrder .get (Axis .T )] = t ;
583+ }
584+ if (axesOrder .containsKey (Axis .C )) {
585+ offset [axesOrder .get (Axis .C )] = c ;
586+ }
587+ if (axesOrder .containsKey (Axis .Z )) {
588+ offset [axesOrder .get (Axis .Z )] = z ;
589+ }
590+ offset [axesOrder .get (Axis .Y )] = y ;
591+ offset [axesOrder .get (Axis .X )] = x ;
592+
522593 read (buffer , shape , offset );
523594 return buffer ;
524595 } catch (Exception e ) {
@@ -618,8 +689,33 @@ public byte[] getStackDirect(Integer c, Integer t, byte[] buffer)
618689 checkBounds (x , y , z , c , t );
619690 //Check check bottom-right of tile in bounds
620691 checkBounds (x + w - 1 , y + h - 1 , z , c , t );
621- int [] shape = new int [] { 1 , 1 , getSizeZ (), h , w };
622- int [] offset = new int [] { t , c , z , y , x };
692+
693+ int [] shape = new int [axesOrder .size ()];
694+ if (axesOrder .containsKey (Axis .T )) {
695+ shape [axesOrder .get (Axis .T )] = 1 ;
696+ }
697+ if (axesOrder .containsKey (Axis .C )) {
698+ shape [axesOrder .get (Axis .C )] = 1 ;
699+ }
700+ if (axesOrder .containsKey (Axis .Z )) {
701+ shape [axesOrder .get (Axis .Z )] = getSizeZ ();
702+ }
703+ shape [axesOrder .get (Axis .Y )] = h ;
704+ shape [axesOrder .get (Axis .X )] = w ;
705+
706+ int [] offset = new int [axesOrder .size ()];
707+ if (axesOrder .containsKey (Axis .T )) {
708+ offset [axesOrder .get (Axis .T )] = t ;
709+ }
710+ if (axesOrder .containsKey (Axis .C )) {
711+ offset [axesOrder .get (Axis .C )] = c ;
712+ }
713+ if (axesOrder .containsKey (Axis .Z )) {
714+ offset [axesOrder .get (Axis .Z )] = z ;
715+ }
716+ offset [axesOrder .get (Axis .Y )] = y ;
717+ offset [axesOrder .get (Axis .X )] = x ;
718+
623719 read (buffer , shape , offset );
624720 return buffer ;
625721 }
@@ -647,8 +743,33 @@ public byte[] getTimepointDirect(Integer t, byte[] buffer)
647743 checkBounds (x , y , z , c , t );
648744 //Check check bottom-right of tile in bounds
649745 checkBounds (x + w - 1 , y + h - 1 , z , c , t );
650- int [] shape = new int [] { 1 , getSizeC (), getSizeZ (), h , w };
651- int [] offset = new int [] { t , c , z , y , x };
746+
747+ int [] shape = new int [axesOrder .size ()];
748+ if (axesOrder .containsKey (Axis .T )) {
749+ shape [axesOrder .get (Axis .T )] = 1 ;
750+ }
751+ if (axesOrder .containsKey (Axis .C )) {
752+ shape [axesOrder .get (Axis .C )] = getSizeC ();
753+ }
754+ if (axesOrder .containsKey (Axis .Z )) {
755+ shape [axesOrder .get (Axis .Z )] = getSizeZ ();
756+ }
757+ shape [axesOrder .get (Axis .Y )] = h ;
758+ shape [axesOrder .get (Axis .X )] = w ;
759+
760+ int [] offset = new int [axesOrder .size ()];
761+ if (axesOrder .containsKey (Axis .T )) {
762+ offset [axesOrder .get (Axis .T )] = t ;
763+ }
764+ if (axesOrder .containsKey (Axis .C )) {
765+ offset [axesOrder .get (Axis .C )] = c ;
766+ }
767+ if (axesOrder .containsKey (Axis .Z )) {
768+ offset [axesOrder .get (Axis .Z )] = z ;
769+ }
770+ offset [axesOrder .get (Axis .Y )] = y ;
771+ offset [axesOrder .get (Axis .X )] = x ;
772+
652773 read (buffer , shape , offset );
653774 return buffer ;
654775 }
@@ -757,35 +878,47 @@ public long getId() {
757878
758879 @ Override
759880 public int getSizeX () {
760- return array .getShape ()[4 ];
881+ return array .getShape ()[axesOrder . get ( Axis . X ) ];
761882 }
762883
763884 @ Override
764885 public int getSizeY () {
765- return array .getShape ()[3 ];
886+ return array .getShape ()[axesOrder . get ( Axis . Y ) ];
766887 }
767888
768889 @ Override
769890 public int getSizeZ () {
770- // this is expected to be the Z size of the full resolution array
771- return zIndexMap .size ();
891+ if (axesOrder .containsKey (Axis .Z )) {
892+ // this is expected to be the Z size of the full resolution array
893+ return zIndexMap .size ();
894+ }
895+ return 1 ;
772896 }
773897
774898 /**
775899 * @return Z size of the current underlying Zarr array
776900 */
777901 private int getTrueSizeZ () {
778- return array .getShape ()[2 ];
902+ if (axesOrder .containsKey (Axis .Z )) {
903+ return array .getShape ()[axesOrder .get (Axis .Z )];
904+ }
905+ return 1 ;
779906 }
780907
781908 @ Override
782909 public int getSizeC () {
783- return array .getShape ()[1 ];
910+ if (axesOrder .containsKey (Axis .C )) {
911+ return array .getShape ()[axesOrder .get (Axis .C )];
912+ }
913+ return 1 ;
784914 }
785915
786916 @ Override
787917 public int getSizeT () {
788- return array .getShape ()[0 ];
918+ if (axesOrder .containsKey (Axis .T )) {
919+ return array .getShape ()[axesOrder .get (Axis .T )];
920+ }
921+ return 1 ;
789922 }
790923
791924 @ Override
@@ -813,6 +946,7 @@ public void setResolutionLevel(int resolutionLevel) {
813946 throw new IllegalArgumentException (
814947 "This Zarr file has no pixel data" );
815948 }
949+
816950 if (zIndexMap == null ) {
817951 zIndexMap = new HashMap <Integer , Integer >();
818952 }
@@ -825,26 +959,29 @@ public void setResolutionLevel(int resolutionLevel) {
825959
826960 ZarrArray fullResolutionArray = zarrArrayCache .get (
827961 root .resolve ("0" )).get ();
828-
829- // map each Z index in the full resolution array
830- // to a Z index in the subresolution array
831- // if no Z downsampling, this is just an identity map
832- int fullResZ = fullResolutionArray .getShape ()[2 ];
833- int arrayZ = array .getShape ()[2 ];
834- for (int z =0 ; z <fullResZ ; z ++) {
835- zIndexMap .put (z , Math .round (z * arrayZ / fullResZ ));
962+
963+ if (axesOrder .containsKey (Axis .Z )) {
964+ // map each Z index in the full resolution array
965+ // to a Z index in the subresolution array
966+ // if no Z downsampling, this is just an identity map
967+ int fullResZ = fullResolutionArray .getShape ()[axesOrder .get (Axis .Z )];
968+ int arrayZ = array .getShape ()[axesOrder .get (Axis .Z )];
969+ for (int z =0 ; z <fullResZ ; z ++) {
970+ zIndexMap .put (z , Math .round (z * arrayZ / fullResZ ));
971+ }
836972 }
837973 } catch (Exception e ) {
838974 // FIXME: Throw the right exception
839975 throw new RuntimeException (e );
840976 }
977+
841978 }
842979
843980 @ Override
844981 public Dimension getTileSize () {
845982 try {
846983 int [] chunks = getChunks ()[resolutionLevel ];
847- return new Dimension (chunks [4 ], chunks [3 ]);
984+ return new Dimension (chunks [axesOrder . get ( Axis . X ) ], chunks [axesOrder . get ( Axis . Y ) ]);
848985 } catch (Exception e ) {
849986 // FIXME: Throw the right exception
850987 throw new RuntimeException (e );
0 commit comments