-
Notifications
You must be signed in to change notification settings - Fork 10
Extended axes support #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 24 commits
63c9936
71c93bb
dfe6319
38838db
d8a7e3c
7fef6bf
b6dc260
7d9f338
a7cd5c9
0020bb6
e4eb3e9
ed93541
8995b11
56df523
04fe46e
de1096e
51119b5
0a30109
d000619
bd95004
4d9ef53
c950c9c
ff75695
a0b3c47
6945b90
bd1bac4
57bffda
96afc5a
40c09cc
6194831
3868956
195ec0f
beeacff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -95,6 +95,14 @@ public class ZarrPixelBuffer implements PixelBuffer { | |
| /** Array path vs. ZarrArray cache */ | ||
| private final AsyncLoadingCache<Path, ZarrArray> zarrArrayCache; | ||
|
|
||
| /** Supported axes, X and Y are essential */ | ||
| public enum Axis { | ||
| X, Y, Z, C, T; | ||
| } | ||
|
|
||
| /** Maps axes to their corresponding indexes */ | ||
| private Map<Axis, Integer> axesOrder; | ||
|
|
||
| /** | ||
| * Default constructor | ||
| * @param pixels Pixels metadata for the pixel buffer | ||
|
|
@@ -122,12 +130,14 @@ public ZarrPixelBuffer(Pixels pixels, Path root, Integer maxPlaneWidth, | |
| if (!rootGroupAttributes.containsKey("multiscales")) { | ||
| throw new IllegalArgumentException("Missing multiscales metadata!"); | ||
| } | ||
| this.axesOrder = getAxesOrder(); | ||
| this.resolutionLevels = this.getResolutionLevels(); | ||
| setResolutionLevel(this.resolutionLevels - 1); | ||
| if (this.resolutionLevel < 0) { | ||
| throw new IllegalArgumentException( | ||
| "This Zarr file has no pixel data"); | ||
| } | ||
|
|
||
| this.maxPlaneWidth = maxPlaneWidth; | ||
| this.maxPlaneHeight = maxPlaneHeight; | ||
|
|
||
|
|
@@ -196,25 +206,30 @@ private long length(int[] shape) { | |
| private void read(byte[] buffer, int[] shape, int[] offset) | ||
| throws IOException { | ||
| // Check planar read size (sizeX and sizeY only) | ||
| checkReadSize(Arrays.copyOfRange(shape, 3, 5)); | ||
|
|
||
| checkReadSize(new int[] {shape[axesOrder.get(Axis.X)], shape[axesOrder.get(Axis.Y)]}); | ||
| // if reading from a resolution downsampled in Z, | ||
| // adjust the shape/offset for the Z coordinate only | ||
| // this ensures that the correct Zs are read from the correct offsets | ||
| // since the requested shape/offset may not match the underlying array | ||
| int planes = 1; | ||
| int originalZIndex = offset[2]; | ||
| int originalZIndex = 1; | ||
| if (axesOrder.containsKey(Axis.Z)) { | ||
| originalZIndex = offset[axesOrder.get(Axis.Z)]; | ||
| } | ||
| if (getSizeZ() != getTrueSizeZ()) { | ||
| offset[2] = zIndexMap.get(originalZIndex); | ||
| planes = shape[2]; | ||
| shape[2] = 1; | ||
| offset[axesOrder.get(Axis.Z)] = zIndexMap.get(originalZIndex); | ||
| planes = shape[axesOrder.get(Axis.Z)]; | ||
| shape[axesOrder.get(Axis.Z)] = 1; | ||
| } | ||
|
|
||
| try { | ||
| ByteBuffer asByteBuffer = ByteBuffer.wrap(buffer); | ||
| DataType dataType = array.getDataType(); | ||
| for (int z=0; z<planes; z++) { | ||
| offset[2] = zIndexMap.get(originalZIndex + z); | ||
| if (axesOrder.containsKey(Axis.Z)) { | ||
| offset[axesOrder.get(Axis.Z)] = zIndexMap.get(originalZIndex + z); | ||
| } | ||
| switch (dataType) { | ||
| case u1: | ||
| case i1: | ||
|
|
@@ -296,7 +311,7 @@ public int[][] getChunks() throws IOException { | |
| } | ||
|
|
||
| /** | ||
| * Retrieves the datasets metadat of the first multiscale from the root | ||
| * Retrieves the datasets metadata of the first multiscale from the root | ||
| * group attributes. | ||
| * @return See above. | ||
| * @see #getRootGroupAttributes() | ||
|
|
@@ -307,6 +322,37 @@ public List<Map<String, String>> getDatasets() { | |
| getMultiscalesMetadata().get(0).get("datasets"); | ||
| } | ||
|
|
||
| /** | ||
| * Retrieves the axes metadata of the first multiscale | ||
| * @return See above. | ||
| */ | ||
| public Map<Axis, Integer> getAxesOrder() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like this should either be |
||
| HashMap<Axis, Integer> order = new HashMap<>(); | ||
| List<Map<String, Object>> axesData = (List<Map<String, Object>>) getMultiscalesMetadata().get(0).get("axes"); | ||
| if (axesData == null) { | ||
| log.warn("No axes metadata found, defaulting to standard axes TCZYX"); | ||
| order.put(Axis.T, 0); | ||
| order.put(Axis.C, 1); | ||
| order.put(Axis.Z, 2); | ||
| order.put(Axis.Y, 3); | ||
| order.put(Axis.X, 4); | ||
| } else { | ||
| for (int i=0; i<axesData.size(); i++) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For formatting, do we want spaces between the values and operators here?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kkoz should we review whether/how to enforce formatting on this repository as a follow up of this work? My feeling is that if we go down this route, we should encode the decisions under the form of checkstyle constraints similarly to https://github.com/glencoesoftware/bioformats2raw/blob/97f8a26dd120d854d3e03ab70f1c481b524ff025/config/checkstyle/checkstyle.xml and let Gradle lint the code as part of the build.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. I was under the impression that there was already an OME style somewhere, but after poking around a bit I couldn't find it. Automatically applying consistent formatting in all repos would definitely be a plus.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://ome-contributing.readthedocs.io/en/latest/code-contributions.html would be the most generic guidelines and Bio-Formats has some additional instructions https://bio-formats.readthedocs.io/en/latest/developers/code-formatting.html For OMERO, there are no written guidelines but https://github.com/ome/openmicroscopy/tree/develop/docs/styles will contain the historical style definition. |
||
| Map<String, Object> axis = axesData.get(i); | ||
| String name = axis.get("name").toString().toUpperCase(); | ||
| try { | ||
| order.put(Axis.valueOf(name), i); | ||
| } catch (IllegalArgumentException e) { | ||
| throw new IllegalArgumentException("Invalid axis name (only T,C,Z,Y,X are supported): " + name); | ||
| } | ||
| } | ||
| } | ||
| if (!order.containsKey(Axis.X) || !order.containsKey(Axis.Y)) { | ||
| throw new IllegalArgumentException("Missing X or Y axis!"); | ||
| } | ||
| return order; | ||
| } | ||
|
|
||
| /** | ||
| * Retrieves the multiscales metadata from the root group attributes. | ||
| * @return See above. | ||
|
|
@@ -517,8 +563,33 @@ public byte[] getTileDirect( | |
| checkBounds(x, y, z, c, t); | ||
| //Check check bottom-right of tile in bounds | ||
| checkBounds(x + w - 1, y + h - 1, z, c, t); | ||
| int[] shape = new int[] { 1, 1, 1, h, w }; | ||
| int[] offset = new int[] { t, c, z, y, x }; | ||
|
|
||
| int[] shape = new int[axesOrder.size()]; | ||
| if (axesOrder.containsKey(Axis.T)) { | ||
| shape[axesOrder.get(Axis.T)] = 1; | ||
| } | ||
| if (axesOrder.containsKey(Axis.C)) { | ||
| shape[axesOrder.get(Axis.C)] = 1; | ||
| } | ||
| if (axesOrder.containsKey(Axis.Z)) { | ||
| shape[axesOrder.get(Axis.Z)] = 1; | ||
| } | ||
| shape[axesOrder.get(Axis.Y)] = h; | ||
| shape[axesOrder.get(Axis.X)] = w; | ||
|
|
||
| int[] offset = new int[axesOrder.size()]; | ||
| if (axesOrder.containsKey(Axis.T)) { | ||
| offset[axesOrder.get(Axis.T)] = t; | ||
| } | ||
| if (axesOrder.containsKey(Axis.C)) { | ||
| offset[axesOrder.get(Axis.C)] = c; | ||
| } | ||
| if (axesOrder.containsKey(Axis.Z)) { | ||
| offset[axesOrder.get(Axis.Z)] = z; | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be worth condensing lines 566-589, so that for each of the 3 axes there is something like: That would reduce the number of |
||
| offset[axesOrder.get(Axis.Y)] = y; | ||
| offset[axesOrder.get(Axis.X)] = x; | ||
|
|
||
| read(buffer, shape, offset); | ||
| return buffer; | ||
| } catch (Exception e) { | ||
|
|
@@ -618,8 +689,33 @@ public byte[] getStackDirect(Integer c, Integer t, byte[] buffer) | |
| checkBounds(x, y, z, c, t); | ||
| //Check check bottom-right of tile in bounds | ||
| checkBounds(x + w - 1, y + h - 1, z, c, t); | ||
| int[] shape = new int[] { 1, 1, getSizeZ(), h, w }; | ||
| int[] offset = new int[] { t, c, z, y, x }; | ||
|
|
||
| int[] shape = new int[axesOrder.size()]; | ||
| if (axesOrder.containsKey(Axis.T)) { | ||
| shape[axesOrder.get(Axis.T)] = 1; | ||
| } | ||
| if (axesOrder.containsKey(Axis.C)) { | ||
| shape[axesOrder.get(Axis.C)] = 1; | ||
| } | ||
| if (axesOrder.containsKey(Axis.Z)) { | ||
| shape[axesOrder.get(Axis.Z)] = getSizeZ(); | ||
| } | ||
| shape[axesOrder.get(Axis.Y)] = h; | ||
| shape[axesOrder.get(Axis.X)] = w; | ||
|
|
||
| int[] offset = new int[axesOrder.size()]; | ||
| if (axesOrder.containsKey(Axis.T)) { | ||
| offset[axesOrder.get(Axis.T)] = t; | ||
| } | ||
| if (axesOrder.containsKey(Axis.C)) { | ||
| offset[axesOrder.get(Axis.C)] = c; | ||
| } | ||
| if (axesOrder.containsKey(Axis.Z)) { | ||
| offset[axesOrder.get(Axis.Z)] = z; | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As above, this could be condensed to a single |
||
| offset[axesOrder.get(Axis.Y)] = y; | ||
| offset[axesOrder.get(Axis.X)] = x; | ||
|
|
||
| read(buffer, shape, offset); | ||
| return buffer; | ||
| } | ||
|
|
@@ -647,8 +743,33 @@ public byte[] getTimepointDirect(Integer t, byte[] buffer) | |
| checkBounds(x, y, z, c, t); | ||
| //Check check bottom-right of tile in bounds | ||
| checkBounds(x + w - 1, y + h - 1, z, c, t); | ||
| int[] shape = new int[] { 1, getSizeC(), getSizeZ(), h, w }; | ||
| int[] offset = new int[] { t, c, z, y, x }; | ||
|
|
||
| int[] shape = new int[axesOrder.size()]; | ||
| if (axesOrder.containsKey(Axis.T)) { | ||
| shape[axesOrder.get(Axis.T)] = 1; | ||
| } | ||
| if (axesOrder.containsKey(Axis.C)) { | ||
| shape[axesOrder.get(Axis.C)] = getSizeC(); | ||
| } | ||
| if (axesOrder.containsKey(Axis.Z)) { | ||
| shape[axesOrder.get(Axis.Z)] = getSizeZ(); | ||
| } | ||
| shape[axesOrder.get(Axis.Y)] = h; | ||
| shape[axesOrder.get(Axis.X)] = w; | ||
|
|
||
| int[] offset = new int[axesOrder.size()]; | ||
| if (axesOrder.containsKey(Axis.T)) { | ||
| offset[axesOrder.get(Axis.T)] = t; | ||
| } | ||
| if (axesOrder.containsKey(Axis.C)) { | ||
| offset[axesOrder.get(Axis.C)] = c; | ||
| } | ||
| if (axesOrder.containsKey(Axis.Z)) { | ||
| offset[axesOrder.get(Axis.Z)] = z; | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here as for lines 566-589 and 693-715. |
||
| offset[axesOrder.get(Axis.Y)] = y; | ||
| offset[axesOrder.get(Axis.X)] = x; | ||
|
|
||
| read(buffer, shape, offset); | ||
| return buffer; | ||
| } | ||
|
|
@@ -757,35 +878,47 @@ public long getId() { | |
|
|
||
| @Override | ||
| public int getSizeX() { | ||
| return array.getShape()[4]; | ||
| return array.getShape()[axesOrder.get(Axis.X)]; | ||
| } | ||
|
|
||
| @Override | ||
| public int getSizeY() { | ||
| return array.getShape()[3]; | ||
| return array.getShape()[axesOrder.get(Axis.Y)]; | ||
| } | ||
|
|
||
| @Override | ||
| public int getSizeZ() { | ||
| // this is expected to be the Z size of the full resolution array | ||
| return zIndexMap.size(); | ||
| if (axesOrder.containsKey(Axis.Z)) { | ||
| // this is expected to be the Z size of the full resolution array | ||
| return zIndexMap.size(); | ||
| } | ||
| return 1; | ||
| } | ||
|
|
||
| /** | ||
| * @return Z size of the current underlying Zarr array | ||
| */ | ||
| private int getTrueSizeZ() { | ||
| return array.getShape()[2]; | ||
| if (axesOrder.containsKey(Axis.Z)) { | ||
| return array.getShape()[axesOrder.get(Axis.Z)]; | ||
| } | ||
| return 1; | ||
| } | ||
|
|
||
| @Override | ||
| public int getSizeC() { | ||
| return array.getShape()[1]; | ||
| if (axesOrder.containsKey(Axis.C)) { | ||
| return array.getShape()[axesOrder.get(Axis.C)]; | ||
| } | ||
| return 1; | ||
| } | ||
|
|
||
| @Override | ||
| public int getSizeT() { | ||
| return array.getShape()[0]; | ||
| if (axesOrder.containsKey(Axis.T)) { | ||
| return array.getShape()[axesOrder.get(Axis.T)]; | ||
| } | ||
| return 1; | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -813,6 +946,7 @@ public void setResolutionLevel(int resolutionLevel) { | |
| throw new IllegalArgumentException( | ||
| "This Zarr file has no pixel data"); | ||
| } | ||
|
|
||
| if (zIndexMap == null) { | ||
| zIndexMap = new HashMap<Integer, Integer>(); | ||
| } | ||
|
|
@@ -825,26 +959,29 @@ public void setResolutionLevel(int resolutionLevel) { | |
|
|
||
| ZarrArray fullResolutionArray = zarrArrayCache.get( | ||
| root.resolve("0")).get(); | ||
|
|
||
| // map each Z index in the full resolution array | ||
| // to a Z index in the subresolution array | ||
| // if no Z downsampling, this is just an identity map | ||
| int fullResZ = fullResolutionArray.getShape()[2]; | ||
| int arrayZ = array.getShape()[2]; | ||
| for (int z=0; z<fullResZ; z++) { | ||
| zIndexMap.put(z, Math.round(z * arrayZ / fullResZ)); | ||
|
|
||
| if (axesOrder.containsKey(Axis.Z)) { | ||
| // map each Z index in the full resolution array | ||
| // to a Z index in the subresolution array | ||
| // if no Z downsampling, this is just an identity map | ||
| int fullResZ = fullResolutionArray.getShape()[axesOrder.get(Axis.Z)]; | ||
| int arrayZ = array.getShape()[axesOrder.get(Axis.Z)]; | ||
| for (int z=0; z<fullResZ; z++) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same formatting comment here
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's true, I've not set any particular formatting style in my IDE, sorry. Happy to use the bioformats2raw one directly! |
||
| zIndexMap.put(z, Math.round(z * arrayZ / fullResZ)); | ||
| } | ||
| } | ||
| } catch (Exception e) { | ||
| // FIXME: Throw the right exception | ||
| throw new RuntimeException(e); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| @Override | ||
| public Dimension getTileSize() { | ||
| try { | ||
| int[] chunks = getChunks()[resolutionLevel]; | ||
| return new Dimension(chunks[4], chunks[3]); | ||
| return new Dimension(chunks[axesOrder.get(Axis.X)], chunks[axesOrder.get(Axis.Y)]); | ||
| } catch (Exception e) { | ||
| // FIXME: Throw the right exception | ||
| throw new RuntimeException(e); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since these require
axesOrderto containAxis.Z, should they be inside the conditional above?