diff --git a/pom.xml b/pom.xml index 614ee47..d289a74 100644 --- a/pom.xml +++ b/pom.xml @@ -112,12 +112,11 @@ sign,deploy-to-scijava 3.2.0 + 4.1.2 2.2.0 7.0.0 4.1.0 - 1.3.1 - 4.1.1 1.0.0-preview.20191208 1.4.1 @@ -229,7 +228,7 @@ ${jaxb-api.version} test - + diff --git a/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/MetadataUtils.java b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/MetadataUtils.java index e322a35..ea33513 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/MetadataUtils.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/MetadataUtils.java @@ -3,11 +3,16 @@ import java.net.URISyntaxException; import java.util.Arrays; import java.util.HashMap; +import java.util.Optional; +import org.apache.commons.lang3.ArrayUtils; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5URI; import org.janelia.saalfeldlab.n5.universe.N5TreeNode; +import org.janelia.saalfeldlab.n5.universe.metadata.N5CosemMetadata.CosemTransform; import org.janelia.saalfeldlab.n5.universe.metadata.axes.Axis; +import org.janelia.saalfeldlab.n5.universe.metadata.axes.AxisUtils; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.NgffSingleScaleAxesMetadata; import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.coordinateTransformations.CoordinateTransformation; import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.coordinateTransformations.ScaleCoordinateTransformation; import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.coordinateTransformations.TranslationCoordinateTransformation; @@ -16,6 +21,7 @@ import com.google.gson.JsonNull; import net.imglib2.realtransform.AffineGet; +import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.realtransform.Scale; import net.imglib2.realtransform.Scale2D; import net.imglib2.realtransform.Scale3D; @@ -23,10 +29,13 @@ import net.imglib2.realtransform.Translation; import net.imglib2.realtransform.Translation2D; import net.imglib2.realtransform.Translation3D; -import net.imglib2.util.Pair; public class MetadataUtils { + // duplicate variables in N5ScalePyramidExporter in n5-ij + public static final String DOWN_SAMPLE = "Sample"; + public static final String DOWN_AVERAGE = "Average"; + public static double[] mul(final double[] a, final double[] b) { final double[] out = new double[a.length]; @@ -116,7 +125,7 @@ public static ScaleAndTranslation scaleTranslationFromCoordinateTransformations( if (cts == null || cts.length == 0) return null; - ScaleAndTranslation out = coordinateTransformToScaleAndTranslation(cts[0]); + final ScaleAndTranslation out = coordinateTransformToScaleAndTranslation(cts[0]); for (int i = 1; i < cts.length; i++) { out.preConcatenate(coordinateTransformToScaleAndTranslation(cts[i])); } @@ -218,7 +227,7 @@ public static String normalizeGroupPath(final String path) { } /** - * Returns a relative group path from the child absolute path group path child + * Returns a relative group path from the child absolute path group path child * the parent absolute group path. * * If the child path is not a descendent of parent, child will be returned. @@ -307,4 +316,208 @@ else if (scale.length == 3) return null; } + @SuppressWarnings("unchecked") + public static M metadataForThisScale(final String newPath, + final M baseMetadata, + final String downsampleMethod, + final double[] baseResolution, + final double[] absoluteDownsamplingFactors, + final double[] absoluteScale, + final double[] absoluteTranslation) { + + if (baseMetadata == null) + return null; + + /** + * if metadata is N5SingleScaleMetadata and not a subclass of it then this is using N5Viewer + * metadata which does not have an offset + */ + if (baseMetadata.getClass().equals(N5SingleScaleMetadata.class)) { + return (M)buildN5VMetadata(newPath, (N5SingleScaleMetadata)baseMetadata, downsampleMethod, baseResolution, absoluteDownsamplingFactors); + } else if (baseMetadata instanceof N5CosemMetadata) { + return (M)buildCosemMetadata(newPath, (N5CosemMetadata)baseMetadata, absoluteScale, absoluteTranslation); + + } else if (baseMetadata instanceof NgffSingleScaleAxesMetadata) { + return (M)buildNgffMetadata(newPath, (NgffSingleScaleAxesMetadata)baseMetadata, absoluteScale, absoluteTranslation); + } else + return baseMetadata; + } + + public static N5SingleScaleMetadata buildN5VMetadata( + final String path, + final N5SingleScaleMetadata baseMetadata, + final String downsampleMethod, + final double[] baseResolution, + final double[] downsamplingFactors) { + + /** + * N5Viewer metadata doesn't have a way to directly represent offset. Rather, the half-pixel + * offsets that averaging downsampling introduces are assumed when downsampling factors are + * not equal to ones. + * + * As a result, we use downsampling factors with average downsampling, but set the factors + * to one otherwise. + */ + final int nd = baseResolution.length > 3 ? 3 : baseResolution.length; + final double[] resolution = new double[nd]; + final double[] factors = new double[nd]; + + if (downsampleMethod.equals(DOWN_AVERAGE)) { + System.arraycopy(baseResolution, 0, resolution, 0, nd); + System.arraycopy(downsamplingFactors, 0, factors, 0, nd); + } else { + for (int i = 0; i < nd; i++) + resolution[i] = baseResolution[i] * downsamplingFactors[i]; + + Arrays.fill(factors, 1); + } + + final AffineTransform3D transform = new AffineTransform3D(); + for (int i = 0; i < nd; i++) + transform.set(resolution[i], i, i); + + return new N5SingleScaleMetadata( + path, + transform, + factors, + resolution, + baseMetadata.getOffset(), + baseMetadata.unit(), + baseMetadata.getAttributes(), + baseMetadata.minIntensity(), + baseMetadata.maxIntensity(), + baseMetadata.isLabelMultiset()); + + } + + public static N5CosemMetadata buildCosemMetadata( + final String path, + final N5CosemMetadata baseMetadata, + final double[] absoluteResolution, + final double[] absoluteTranslation) { + + final double[] resolution = new double[absoluteResolution.length]; + System.arraycopy(absoluteResolution, 0, resolution, 0, absoluteResolution.length); + + final double[] translation = new double[absoluteTranslation.length]; + System.arraycopy(absoluteTranslation, 0, translation, 0, absoluteTranslation.length); + + return new N5CosemMetadata( + path, + new CosemTransform( + baseMetadata.getCosemTransform().axes, + resolution, + translation, + baseMetadata.getCosemTransform().units), + baseMetadata.getAttributes()); + } + + public static NgffSingleScaleAxesMetadata buildNgffMetadata( + final String path, + final NgffSingleScaleAxesMetadata baseMetadata, + final double[] absoluteResolution, + final double[] absoluteTranslation) { + + final double[] resolution = new double[absoluteResolution.length]; + System.arraycopy(absoluteResolution, 0, resolution, 0, absoluteResolution.length); + + final double[] translation = new double[absoluteTranslation.length]; + System.arraycopy(absoluteTranslation, 0, translation, 0, absoluteTranslation.length); + + return new NgffSingleScaleAxesMetadata( + path, + resolution, + translation, + baseMetadata.getAxes(), + baseMetadata.getAttributes()); + } + + @SuppressWarnings("unchecked") + public static M permuteSpatialMetadata(final M metadata, final int[] axisPermutation) { + + if (metadata == null) + return null; + + /** + * if metadata is N5SingleScaleMetadata and not a subclass of it then this is using N5Viewer + * metadata which does not have an offset + */ + if (metadata.getClass().equals(N5SingleScaleMetadata.class)) { + return (M)permuteN5vMetadata((N5SingleScaleMetadata)metadata, axisPermutation); + } else if (metadata instanceof N5CosemMetadata) { + return (M)permuteCosemMetadata((N5CosemMetadata)metadata, axisPermutation); + } else if (metadata instanceof NgffSingleScaleAxesMetadata) { + return (M)permuteNgffMetadata((NgffSingleScaleAxesMetadata)metadata, axisPermutation); + } else + return metadata; + } + + public static NgffSingleScaleAxesMetadata permuteNgffMetadata(final NgffSingleScaleAxesMetadata metadata, int[] axisPermutation) { + + final Axis[] axes = metadata.getAxes(); + final Axis[] axesPermuted = new Axis[axes.length]; + for (int i = 0; i < axes.length; i++) + axesPermuted[i] = axes[i]; + + AxisUtils.permute(axesPermuted, axesPermuted, axisPermutation); + + return new NgffSingleScaleAxesMetadata( + metadata.getPath(), + AxisUtils.permute(metadata.getScale(), axisPermutation), + AxisUtils.permute(metadata.getTranslation(), axisPermutation), + axesPermuted, + metadata.getAttributes()); + } + + public static N5CosemMetadata permuteCosemMetadata(final N5CosemMetadata metadata, int[] axisPermutation) { + + final double[] oldScales = ArrayUtils.clone(metadata.getCosemTransform().scale); + ArrayUtils.reverse(oldScales); + final double[] newScales = AxisUtils.permute(oldScales, axisPermutation); + ArrayUtils.reverse(newScales); + + final double[] oldTranslation = ArrayUtils.clone(metadata.getCosemTransform().translate); + ArrayUtils.reverse(oldTranslation); + final double[] newTranslation = AxisUtils.permute(oldTranslation, axisPermutation); + ArrayUtils.reverse(newTranslation); + + final String[] newAxes = ArrayUtils.clone(metadata.getCosemTransform().axes); + ArrayUtils.reverse(newAxes); + AxisUtils.permute(newAxes, newAxes, axisPermutation); + ArrayUtils.reverse(newAxes); + + final String[] newUnits = ArrayUtils.clone(metadata.getCosemTransform().units); + ArrayUtils.reverse(newUnits); + AxisUtils.permute(newUnits, newUnits, axisPermutation); + ArrayUtils.reverse(newUnits); + + return new N5CosemMetadata( + metadata.getPath(), + new CosemTransform(newAxes, newScales, newTranslation, newUnits), + metadata.getAttributes()); + } + + + public static N5SingleScaleMetadata permuteN5vMetadata(final N5SingleScaleMetadata metadata, int[] axisPermutation) { + + final double[] newScales = AxisUtils.permute(metadata.getPixelResolution(), axisPermutation); + final double[] newOffset = AxisUtils.permute(metadata.getOffset(), axisPermutation); + final double[] newFactors = AxisUtils.permute(metadata.getDownsamplingFactors(), axisPermutation); + + final AffineTransform3D offsetTform = new AffineTransform3D(); + offsetTform.translate(newOffset); + + final AffineTransform3D transform = N5SingleScaleMetadataParser.buildTransform(newFactors, newScales, Optional.of(offsetTform)); + + return new N5SingleScaleMetadata( + metadata.getPath(), + transform, + newFactors, + newScales, + newOffset, + metadata.unit(), + metadata.getAttributes()); + + } + } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/N5DefaultSingleScaleMetadata.java b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/N5DefaultSingleScaleMetadata.java new file mode 100644 index 0000000..cb9be0e --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/N5DefaultSingleScaleMetadata.java @@ -0,0 +1,31 @@ +package org.janelia.saalfeldlab.n5.universe.metadata; + +import org.janelia.saalfeldlab.n5.DatasetAttributes; + +import net.imglib2.realtransform.AffineTransform3D; + +/** + * This class merely serves as a marker that all its values are default values. See + * {@link N5GenericSingleScaleMetadataParser}. + */ +public class N5DefaultSingleScaleMetadata extends N5SingleScaleMetadata { + + public N5DefaultSingleScaleMetadata(String path, AffineTransform3D transform, double[] downsamplingFactors, double[] pixelResolution, double[] offset, + String unit, DatasetAttributes attributes, Double minIntensity, Double maxIntensity, boolean isLabelMultiset) { + + super(path, transform, downsamplingFactors, pixelResolution, offset, unit, attributes, minIntensity, maxIntensity, isLabelMultiset); + } + + public N5DefaultSingleScaleMetadata(String path, AffineTransform3D transform, double[] downsamplingFactors, double[] pixelResolution, double[] offset, + String unit, DatasetAttributes attributes, boolean isLabelMultiset) { + + super(path, transform, downsamplingFactors, pixelResolution, offset, unit, attributes, isLabelMultiset); + } + + public N5DefaultSingleScaleMetadata(String path, AffineTransform3D transform, double[] downsamplingFactors, double[] pixelResolution, double[] offset, + String unit, DatasetAttributes attributes) { + + super(path, transform, downsamplingFactors, pixelResolution, offset, unit, attributes); + } + +} diff --git a/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/N5GenericSingleScaleMetadataParser.java b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/N5GenericSingleScaleMetadataParser.java index 68a947e..499051f 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/N5GenericSingleScaleMetadataParser.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/N5GenericSingleScaleMetadataParser.java @@ -1,6 +1,5 @@ package org.janelia.saalfeldlab.n5.universe.metadata; -import java.util.Map; import java.util.Optional; import java.util.function.Predicate; import java.util.function.Supplier; @@ -44,6 +43,8 @@ public class N5GenericSingleScaleMetadataParser implements N5MetadataParser parseMetadata(final N5Reader n5, final N5TreeNode node) { + allDefault = true; try { final DatasetAttributes attributes = n5.getDatasetAttributes(node.getPath()); if (attributes == null) @@ -226,8 +228,13 @@ public Optional parseMetadata(final N5Reader n5, final N5 transform.set(offset[i], i, 3); } - final N5SingleScaleMetadata metadata = new N5SingleScaleMetadata(path, transform, downsamplingFactors, - resolution, offset, unit, attributes, min, max, isLabelMultiset); + final N5SingleScaleMetadata metadata; + if (allDefault) + metadata = new N5DefaultSingleScaleMetadata(path, transform, downsamplingFactors, + resolution, offset, unit, attributes, min, max, isLabelMultiset); + else + metadata = new N5SingleScaleMetadata(path, transform, downsamplingFactors, + resolution, offset, unit, attributes, min, max, isLabelMultiset); return Optional.of(metadata); } catch (final N5Exception e) { @@ -244,14 +251,19 @@ private static Optional getAttributeOptional(final N5Reader n5, final Str } } - private static T getAttribute(final N5Reader n5, final String path, final String key, final Class clazz, + private T getAttribute(final N5Reader n5, final String path, final String key, final Class clazz, final boolean strict, final Predicate filter, final Supplier defaultValue) { - final Optional optAttr = getAttributeOptional( n5, path, key, clazz ).filter(filter); - if (strict) - return optAttr.orElseThrow(() -> new N5Exception("Missing or invalid attribute for key: " + key )); - else - return optAttr.orElseGet( defaultValue ); + return getAttributeOptional(n5, path, key, clazz).filter(filter).map(x -> { + // this will be triggered if there exists a value, therefore not all values are default + allDefault = false; + return x; + }).orElseGet(() -> { + if (strict) + throw new N5Exception("Missing or invalid attribute for key: " + key); + + return defaultValue.get(); + }); } } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/axes/AxisUtils.java b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/axes/AxisUtils.java index 77630e4..dce8e33 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/axes/AxisUtils.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/axes/AxisUtils.java @@ -3,15 +3,28 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.OptionalInt; import java.util.Set; +import java.util.TreeSet; +import java.util.function.Predicate; import java.util.stream.IntStream; import java.util.stream.Stream; +import org.janelia.saalfeldlab.n5.universe.metadata.MetadataUtils; +import org.janelia.saalfeldlab.n5.universe.metadata.N5Metadata; import org.janelia.saalfeldlab.n5.universe.metadata.N5SpatialDatasetMetadata; +import org.janelia.saalfeldlab.n5.universe.metadata.SpatialMetadata; +import org.janelia.saalfeldlab.n5.universe.metadata.SpatialModifiable; import net.imglib2.RandomAccessibleInterval; +import net.imglib2.realtransform.AffineGet; +import net.imglib2.realtransform.AffineTransform; +import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.transform.integer.MixedTransform; +import net.imglib2.util.Pair; +import net.imglib2.util.ValuePair; import net.imglib2.view.IntervalView; import net.imglib2.view.MixedTransformView; import net.imglib2.view.Views; @@ -34,6 +47,7 @@ public class AxisUtils { public static String SPACE_UNIT = "um"; public static String TIME_UNIT = "s"; + /** * Finds and returns a permutation p such that source[p[i]] equals target[i] * @@ -70,6 +84,56 @@ public static List permute(final List in, final int[] p) { return out; } + /** + * Permutes an input array into a destination array. The input and destination may be the same + * instance. + * + * @param + * the type + * @param in + * the input array + * @param dest + * the destination array + * @param p + * the permutation + */ + public static void permute(final T[] in, final T[] dest, final int[] p) { + + final ArrayList tmp = new ArrayList(in.length); + for (int i = 0; i < in.length; i++) + tmp.add(in[i]); + + for (int i = 0; i < p.length; i++) + dest[i] = tmp.get(p[i]); + } + + public static long[] permute(final long[] in, final int[] p) { + + final long[] out = new long[p.length]; + for (int i = 0; i < p.length; i++) + out[i] = in[p[i]]; + + return out; + } + + public static int[] permute(final int[] in, final int[] p) { + + final int[] out = new int[p.length]; + for (int i = 0; i < p.length; i++) + out[i] = in[p[i]]; + + return out; + } + + public static double[] permute(final double[] in, final int[] p) { + + final double[] out = new double[p.length]; + for (int i = 0; i < p.length; i++) + out[i] = in[p[i]]; + + return out; + } + public static Axis[] buildAxes( final String... labels ) { return Arrays.stream(labels).map( x -> { @@ -86,7 +150,10 @@ public static Axis[] buildAxes( final String... labels ) * @return the permutation */ public static int[] findImagePlusPermutation( final AxisMetadata axisMetadata ) { - return findImagePlusPermutation( axisMetadata.getAxisLabels()); + + // TODO should use axis types, not just labels. + // and should consider what to do if an unknown label exists + return findImagePlusPermutation(axisMetadata.getAxisLabels()); } /** @@ -95,7 +162,7 @@ public static int[] findImagePlusPermutation( final Ax * @param axisLabels the axis labels * @return the permutation array */ - public static int[] findImagePlusPermutation( final String[] axisLabels ) { + public static int[] findImagePlusPermutation(final String[] axisLabels) { final int[] p = new int[ 5 ]; p[0] = indexOf( axisLabels, "x" ); @@ -106,21 +173,72 @@ public static int[] findImagePlusPermutation( final String[] axisLabels ) { return p; } + public static int[] findImagePlusSpatialPermutation(final int[] p) { + + final OptionalInt minOpt = Arrays.stream(p).min(); + if (minOpt.isPresent()) { + final int min = minOpt.getAsInt(); + return Arrays.stream(p).map(x -> x - min).toArray(); + } else + return p; + } + + /** + * Converters an array of integers to a normalized array of integers such that the smallest + * integer is mapped to 0, the second smallest to 1 ... and the largest is mapped to N-1, where + * N is the number of unique integers in the array. + * + * @param indexes + * the indexes + * + * @return normalized indexes + */ + public static int[] normalizeIndexes(final int[] indexes) { + + final TreeSet set = new TreeSet(); + for (final int i : indexes) + set.add(i); + + // can't get index from a tree set, use this sad, not scalable workaround for now + final int[] sortedUniqueIndexes = new int[set.size()]; + final Iterator it = set.iterator(); + int i = 0; + while ( it.hasNext()) + sortedUniqueIndexes[i++] = it.next(); + + final int[] out = new int[indexes.length]; + for (i = 0; i < out.length; i++ ) + out[i] = Arrays.binarySearch(sortedUniqueIndexes, indexes[i]); + + return out; + } + /** * Replaces "-1"s in the input permutation array * with the largest value. * * @param p the permutation */ - public static void fillPermutation( final int[] p ) { + public static void fillPermutation(final int[] p) { int j = Arrays.stream(p).max().getAsInt() + 1; for (int i = 0; i < p.length; i++) if (p[i] < 0) p[i] = j++; } - public static boolean isIdentityPermutation( final int[] p ) - { + public static AffineGet axisPermutationTransform(final int[] p) { + + final int N = p.length; + final int[] normalP = normalizeIndexes(p); + final double[] affineParams = new double[N * (N + 1)]; + for (int i = 0; i < normalP.length; i++) + affineParams[normalP[i] + (N + 1) * i] = 1.0; + + return new AffineTransform(affineParams); + } + + public static boolean isIdentityPermutation( final int[] p ) { + for( int i = 0; i < p.length; i++ ) if( p[i] != i ) return false; @@ -128,9 +246,9 @@ public static boolean isIdentityPermutation( final int[] p ) return true; } - public static RandomAccessibleInterval permuteForImagePlus( + public static RandomAccessibleInterval permuteForImagePlus( final RandomAccessibleInterval img, - final AxisMetadata meta ) { + final M meta) { final int[] p = findImagePlusPermutation( meta ); fillPermutation( p ); @@ -147,7 +265,89 @@ public static RandomAccessibleInterval permuteForImagePlus( return permute(imgTmp, invertPermutation(p)); } - private static final int indexOf( final T[] arr, final T tgt ) { + public static M permuteForImagePlus(int[] spatialPermutation, final M meta) { + + if (isIdentityPermutation(spatialPermutation)) + return meta; + + if (meta instanceof SpatialMetadata && meta instanceof SpatialModifiable) { + + final AffineTransform3D tform = ((SpatialMetadata)meta).spatialTransform3d().copy(); + final AffineTransform3D tformInv = ((SpatialMetadata)meta).spatialTransform3d().inverse().copy(); + + final AffineGet permTform = AxisUtils.axisPermutationTransform(spatialPermutation); + tform.concatenate(permTform).preConcatenate(permTform.inverse()); // exchange rows and + tform.concatenate(tformInv); + + final M out = (M)(((SpatialModifiable)meta).modifySpatialTransform(meta.getPath(), tform)); + return out; + } + + return meta; + } + + public static Pair, M> permuteImageAndMetadataForImagePlus( + final RandomAccessibleInterval img, final M meta) { + + if (meta != null && meta instanceof AxisMetadata) { + + final int[] p = AxisUtils.findImagePlusPermutation((AxisMetadata)meta); + AxisUtils.fillPermutation(p); + + RandomAccessibleInterval imgTmp = img; + while (imgTmp.numDimensions() < 5) + imgTmp = Views.addDimension(imgTmp, 0, 0); + + if (AxisUtils.isIdentityPermutation(p)) + return new ValuePair<>(imgTmp, meta); + + // do the permutation + final RandomAccessibleInterval imgOut = permute(imgTmp, invertPermutation(p)); + final int[] spatialPermutation = new int[]{p[0], p[1], p[3]}; + @SuppressWarnings("unchecked") + final M permutedMeta = (M)permuteForImagePlus(spatialPermutation, (A)meta); + + return new ValuePair<>(imgOut, permutedMeta); + } + + return new ValuePair<>(img, meta); + } + + public static Pair, M> permuteImageAndMetadataForImagePlus( + final int[] p, final RandomAccessibleInterval img, final M meta) { + + // store the permutation for metadata + final int[] metadataPermutation = Arrays.stream(p).filter(x -> x >= 0).toArray(); + + // pad the image permutation + AxisUtils.fillPermutation(p); + + RandomAccessibleInterval imgTmp = img; + while (imgTmp.numDimensions() < 5) + imgTmp = Views.addDimension(imgTmp, 0, 0); + + RandomAccessibleInterval imgOut; + M datasetMeta; + if (AxisUtils.isIdentityPermutation(p)) { + imgOut = imgTmp; + datasetMeta = meta; + } else { + imgOut = AxisUtils.permute(imgTmp, AxisUtils.invertPermutation(p)); + datasetMeta = (M)MetadataUtils.permuteSpatialMetadata(meta, metadataPermutation); + } + + return new ValuePair<>(imgOut, datasetMeta); + } + + public static RandomAccessibleInterval reverseDimensions(final RandomAccessibleInterval img) { + + final int nd = img.numDimensions(); + final int[] p = IntStream.iterate(nd - 1, x -> x - 1).limit(nd).toArray(); + // reversing is its own permutation, so can skip the invert step + return permute(img, p); + } + + private static final int indexOf(final T[] arr, final T tgt) { for( int i = 0; i < arr.length; i++ ) { if( arr[i].equals(tgt)) return i; @@ -165,22 +365,21 @@ private static final int indexOf( final T[] arr, final T tgt ) { * @param p the permutation * @return the permuted source */ - public static final < T > IntervalView< T > permute( final RandomAccessibleInterval< T > source, final int[] p ) - { + public static final IntervalView permute(final RandomAccessibleInterval source, final int[] p) { + final int n = source.numDimensions(); - final long[] min = new long[ n ]; - final long[] max = new long[ n ]; - for ( int i = 0; i < n; ++i ) - { - min[ p[ i ] ] = source.min( i ); - max[ p[ i ] ] = source.max( i ); + final long[] min = new long[n]; + final long[] max = new long[n]; + for (int i = 0; i < n; ++i) { + min[p[i]] = source.min(i); + max[p[i]] = source.max(i); } - final MixedTransform t = new MixedTransform( n, n ); - t.setComponentMapping( p ); + final MixedTransform t = new MixedTransform(n, n); + t.setComponentMapping(p); - final IntervalView out = Views.interval( new MixedTransformView< T >( source, t ), min, max ); + final IntervalView out = Views.interval(new MixedTransformView(source, t), min, max); return out; } @@ -193,6 +392,11 @@ public static int[] invertPermutation( final int[] p ) return inv; } + public static int[] indexes(final Axis[] axes, final Predicate predicate) { + + return IntStream.range(0, axes.length).filter(i -> predicate.test(axes[i])).toArray(); + } + public static Axis[] defaultAxes( final int N ) { return IntStream.range(0, N).mapToObj(i -> { diff --git a/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/NgffSingleScaleAxesMetadata.java b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/NgffSingleScaleAxesMetadata.java index 4ac44be..ddac4ca 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/NgffSingleScaleAxesMetadata.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/NgffSingleScaleAxesMetadata.java @@ -54,15 +54,28 @@ public NgffSingleScaleAxesMetadata(final String path, this.path = MetadataUtils.normalizeGroupPath(path); - this.scale = scale; - this.translation = translation; + this.scale = scale != null ? scale : ones(axes.length); + this.translation = translation != null ? translation : new double[axes.length]; this.axes = axes; this.datasetAttributes = datasetAttributes; - coordinateTransformations = MetadataUtils.buildScaleTranslationTransformList(scale, translation); + coordinateTransformations = MetadataUtils.buildScaleTranslationTransformList(this.scale, this.translation); + if (Arrays.stream(axes).allMatch(x -> x.getType().equals(Axis.SPACE))) { + this.transform = MetadataUtils.scaleTranslationTransforms(this.scale, this.translation); + } else { + final int[] spaceIndexes = AxisUtils.indexes(axes, x -> x.getType().equals(Axis.SPACE)); + final double[] spaceScale = AxisUtils.permute(this.scale, spaceIndexes); + final double[] spaceTranslation = AxisUtils.permute(this.translation, spaceIndexes); + this.transform = MetadataUtils.scaleTranslationTransforms(spaceScale, spaceTranslation); + } + } + + private static double[] ones(final int N) { - this.transform = MetadataUtils.scaleTranslationTransforms(scale, translation); + final double[] ones = new double[N]; + Arrays.fill(ones, 1); + return ones; } @Override diff --git a/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/OmeNgffMetadataParser.java b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/OmeNgffMetadataParser.java index 44dcc18..0364973 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/OmeNgffMetadataParser.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/OmeNgffMetadataParser.java @@ -19,7 +19,6 @@ import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.coordinateTransformations.CoordinateTransformation; import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.coordinateTransformations.CoordinateTransformationAdapter; import org.janelia.saalfeldlab.n5.zarr.ZarrDatasetAttributes; -import org.janelia.saalfeldlab.n5.zarr.ZarrKeyValueReader; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -94,11 +93,8 @@ public Optional parseMetadata(final N5Reader n5, final N5TreeNo attrs[i] = dsetMeta[i].getAttributes(); } - // if zarr is used for storage, and arrays are stored in F-order, axes should not be reversed - // reverse Axes if C-order where "row major" == C-order in this context. - boolean reverseAxes = true; - if( n5 instanceof ZarrKeyValueReader) - reverseAxes = cOrder(dsetMeta[0].getAttributes()); + // maybe axes can be flipped first? + ArrayUtils.reverse(ms.axes); final NgffSingleScaleAxesMetadata[] msChildrenMeta = OmeNgffMultiScaleMetadata.buildMetadata( nd, node.getPath(), ms.datasets, attrs, ms.coordinateTransformations, ms.metadata, ms.axes); @@ -106,10 +102,10 @@ public Optional parseMetadata(final N5Reader n5, final N5TreeNo MetadataUtils.updateChildrenMetadata(node, msChildrenMeta, false); // axes need to be flipped after the child is created - if (reverseAxes) - ArrayUtils.reverse(ms.axes); + // is this actually true? + // ArrayUtils.reverse(ms.axes); - multiscales[j] = new OmeNgffMultiScaleMetadata( ms, msChildrenMeta ); + multiscales[j] = new OmeNgffMultiScaleMetadata(ms, msChildrenMeta); } return Optional.of(new OmeNgffMetadata(node.getPath(), multiscales)); diff --git a/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/OmeNgffMultiScaleMetadata.java b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/OmeNgffMultiScaleMetadata.java index 47964ea..f431854 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/OmeNgffMultiScaleMetadata.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/OmeNgffMultiScaleMetadata.java @@ -99,8 +99,6 @@ public OmeNgffMultiScaleMetadata(final int nd, final String path, final String n final boolean buildDatasetsFromChildren) { super( MetadataUtils.normalizeGroupPath(path), buildMetadata(nd, path, datasets, childrenAttributes, coordinateTransformations, metadata, axes)); - if (!allSameAxisOrder(childrenAttributes)) - throw new RuntimeException("All ome-zarr arrays must have same array order"); this.name = name; this.type = type; @@ -280,7 +278,7 @@ public static class OmeNgffDownsamplingMetadata public JsonObject kwargs; } - public static boolean cOrder( final DatasetAttributes datasetAttributes ) { + public static boolean cOrder(final DatasetAttributes datasetAttributes) { if (datasetAttributes instanceof ZarrDatasetAttributes) { final ZarrDatasetAttributes zattrs = (ZarrDatasetAttributes)datasetAttributes; @@ -289,7 +287,16 @@ public static boolean cOrder( final DatasetAttributes datasetAttributes ) { return false; } - public static T[] reverseIfCorder( final DatasetAttributes datasetAttributes, final T[] arr ) { + public static boolean fOrder(final DatasetAttributes datasetAttributes) { + + if (datasetAttributes instanceof ZarrDatasetAttributes) { + final ZarrDatasetAttributes zattrs = (ZarrDatasetAttributes)datasetAttributes; + return !zattrs.isRowMajor(); + } + return false; + } + + public static T[] reverseIfCorder(final DatasetAttributes datasetAttributes, final T[] arr) { if (datasetAttributes == null || arr == null) return arr; @@ -302,7 +309,7 @@ public static T[] reverseIfCorder( final DatasetAttributes datasetAttributes return arr; } - public static T[] reverseIfCorder( final boolean cOrder, final T[] arr ) { + public static T[] reverseIfCorder(final boolean cOrder, final T[] arr) { if (arr == null) return arr; @@ -317,11 +324,11 @@ public static T[] reverseIfCorder( final boolean cOrder, final T[] arr ) { public static double[] reverseIfCorder(final DatasetAttributes[] datasetAttributes, final boolean cOrder, final double[] arr) { - if( arr == null ) + if (arr == null) return null; if (datasetAttributes == null || datasetAttributes.length == 0) - return reverseIfCorder(cOrder, arr) ; + return reverseIfCorder(cOrder, arr); if (datasetAttributes[0] instanceof ZarrDatasetAttributes) { @@ -355,14 +362,15 @@ public static double[] reverseIfCorder(final boolean cOrder, final double[] arr) return arr; } - public static double[] reverseCopy(final double[] arr) { + final double[] arrCopy = Arrays.copyOf(arr, arr.length); ArrayUtils.reverse(arrCopy); return arrCopy; } public static T[] reverseCopy(final T[] arr) { + final T[] arrCopy = Arrays.copyOf(arr, arr.length); ArrayUtils.reverse(arrCopy); return arrCopy; @@ -370,7 +378,7 @@ public static T[] reverseCopy(final T[] arr) { public static boolean allSameAxisOrder(final DatasetAttributes[] multiscaleDatasetAttributes) { - if( multiscaleDatasetAttributes == null ) + if (multiscaleDatasetAttributes == null) return true; boolean unknown = true; @@ -382,7 +390,7 @@ public static boolean allSameAxisOrder(final DatasetAttributes[] multiscaleDatas if (unknown) { cOrder = zattrs.isRowMajor(); unknown = true; - } else if ( cOrder != zattrs.isRowMajor() ){ + } else if (cOrder != zattrs.isRowMajor()) { return false; } } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/OmeNgffMultiScaleMetadataMutable.java b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/OmeNgffMultiScaleMetadataMutable.java index 595decc..f0ceb84 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/OmeNgffMultiScaleMetadataMutable.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/OmeNgffMultiScaleMetadataMutable.java @@ -1,12 +1,9 @@ package org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04; -import java.net.URISyntaxException; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import org.janelia.saalfeldlab.n5.DatasetAttributes; -import org.janelia.saalfeldlab.n5.N5URI; import org.janelia.saalfeldlab.n5.universe.metadata.MetadataUtils; import org.janelia.saalfeldlab.n5.universe.metadata.axes.Axis; diff --git a/src/test/java/org/janelia/saalfeldlab/n5/universe/metadata/AxisMetadataTests.java b/src/test/java/org/janelia/saalfeldlab/n5/universe/metadata/AxisMetadataTests.java index f267b0b..4b7e6ac 100644 --- a/src/test/java/org/janelia/saalfeldlab/n5/universe/metadata/AxisMetadataTests.java +++ b/src/test/java/org/janelia/saalfeldlab/n5/universe/metadata/AxisMetadataTests.java @@ -1,5 +1,10 @@ package org.janelia.saalfeldlab.n5.universe.metadata; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Optional; + import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5FSReader; import org.janelia.saalfeldlab.n5.N5FSWriter; @@ -16,11 +21,6 @@ import com.google.gson.Gson; -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.util.Optional; - public class AxisMetadataTests { @@ -41,9 +41,9 @@ public void before() { final String n5Root = "src/test/resources/canonical.n5"; n5rootF = new File(n5Root); - - URL configUrl = TransformTests.class.getResource( "/n5.jq" ); - File baseDir = new File( configUrl.getFile() ).getParentFile(); + + final URL configUrl = TransformTests.class.getResource( "/n5.jq" ); + final File baseDir = new File( configUrl.getFile() ).getParentFile(); containerDir = new File( baseDir, "canonical.n5" ); try { @@ -51,7 +51,7 @@ public void before() { n5 = new N5FSReader( n5rootF.getCanonicalPath(), JqUtils.gsonBuilder(null)); n5w = new N5FSWriter( containerDir.getCanonicalPath(), JqUtils.gsonBuilder(null)); - }catch( IOException e ) { + }catch( final IOException e ) { e.printStackTrace(); } @@ -77,12 +77,12 @@ public void parseTest() throws IOException { final CanonicalMetadataParser parser = new CanonicalMetadataParser(); Assert.assertTrue("affine dataset exists", n5.exists("affine")); - Optional metaOpt = parser.parseMetadata(n5, "affine"); + final Optional metaOpt = parser.parseMetadata(n5, "affine"); Assert.assertTrue("canonical metadata exists", metaOpt.isPresent() ); - + final CanonicalMetadata metaRaw = metaOpt.get(); Assert.assertTrue("is CanonicalSpatialDatasetMetadata ", (metaRaw instanceof CanonicalSpatialDatasetMetadata )); - CanonicalSpatialDatasetMetadata sdMeta = (CanonicalSpatialDatasetMetadata)metaRaw; + final CanonicalSpatialDatasetMetadata sdMeta = (CanonicalSpatialDatasetMetadata)metaRaw; // test intensity Assert.assertEquals("min intensity", 12.0, sdMeta.minIntensity(), eps ); @@ -95,25 +95,25 @@ public void parseTest() throws IOException { Assert.assertArrayEquals("affineData", expectedAffineData, affineData, eps); - Optional msMetaOpt = parser.parseMetadata(n5, "multiscaleAffine"); + final Optional msMetaOpt = parser.parseMetadata(n5, "multiscaleAffine"); Assert.assertTrue("canonical ms metadata exists", msMetaOpt.isPresent() ); - + System.out.println( msMetaOpt ); } - + @Test public void readWriteTest() throws IOException { final Gson gson = JqUtils.buildGson(n5w); - CanonicalMetadataParser parser = new CanonicalMetadataParser(); - Optional meta = parser.parseMetadata(n5, "axes/xyz"); + final CanonicalMetadataParser parser = new CanonicalMetadataParser(); + final Optional meta = parser.parseMetadata(n5, "axes/xyz"); System.out.println( meta.get() ); } private CanonicalSpatialDatasetMetadata makeMeta(double[] affine, DatasetAttributes attrs, Axis[] axes) { return new CanonicalSpatialDatasetMetadata("", - new SpatialMetadataCanonical("", new AffineSpatialTransform(affine), "mm", axes), + new SpatialMetadataCanonical("", new AffineSpatialTransform(affine), "mm", axes), attrs); } diff --git a/src/test/java/org/janelia/saalfeldlab/n5/universe/metadata/NgffTests.java b/src/test/java/org/janelia/saalfeldlab/n5/universe/metadata/NgffTests.java index d3f287f..136bc11 100644 --- a/src/test/java/org/janelia/saalfeldlab/n5/universe/metadata/NgffTests.java +++ b/src/test/java/org/janelia/saalfeldlab/n5/universe/metadata/NgffTests.java @@ -2,49 +2,262 @@ import static org.junit.Assert.fail; +import java.util.Arrays; +import java.util.HashMap; +import java.util.stream.IntStream; + +import org.apache.commons.lang3.ArrayUtils; +import org.janelia.saalfeldlab.n5.Compression; +import org.janelia.saalfeldlab.n5.DataType; +import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.N5FSReader; +import org.janelia.saalfeldlab.n5.N5Writer; +import org.janelia.saalfeldlab.n5.RawCompression; +import org.janelia.saalfeldlab.n5.universe.N5DatasetDiscoverer; +import org.janelia.saalfeldlab.n5.universe.N5TreeNode; +import org.janelia.saalfeldlab.n5.universe.metadata.N5CosemMetadata.CosemTransform; import org.janelia.saalfeldlab.n5.universe.metadata.NgffMultiScaleGroupAttributes.MultiscaleDataset; +import org.janelia.saalfeldlab.n5.universe.metadata.axes.Axis; +import org.janelia.saalfeldlab.n5.universe.metadata.axes.AxisUtils; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.NgffSingleScaleAxesMetadata; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMultiScaleMetadata; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMultiScaleMetadata.OmeNgffDataset; +import org.janelia.saalfeldlab.n5.zarr.ZarrKeyValueWriter; import org.junit.Assert; import org.junit.Before; import org.junit.Test; public class NgffTests { - + + // indexes + public static final char X = 'x'; + public static final char Y = 'y'; + public static final char Z = 'z'; + public static final char C = 'c'; + public static final char T = 't'; + + // indexes + public static final int IX = 0; + public static final int IY = 1; + public static final int IZ = 2; + public static final int IC = 3; + public static final int IT = 4; + + // size per dimension + public static final int NX = 6; + public static final int NY = 5; + public static final int NC = 2; + public static final int NZ = 4; + public static final int NT = 3; + + // resolution per dimension + public static final double RX = 6; + public static final double RY = 5; + public static final double RC = 2; + public static final double RZ = 4; + public static final double RT = 3; + + // translation per dimension + public static final double TX = 60; + public static final double TY = 50; + public static final double TC = 20; + public static final double TZ = 40; + public static final double TT = 30; + + public static final long[] DEFAULT_DIMENSIONS = new long[]{NX, NY, NC, NZ, NT}; + public static final char[] DEFAULT_AXES = new char[]{X, Y, C, Z, T}; + public static final String[] DEFAULT_AXES_S = charToString(DEFAULT_AXES); + public static final double[] DEFAULT_RESOLUTION = new double[]{RX, RY, RC, RZ, RT}; + public static final double[] DEFAULT_TRANSLATION = new double[]{TX, TY, TC, TZ, TT}; + private N5FSReader n5; - @Before - public void setUp() throws N5Exception { + public static String[] charToString(char[] arr) { + + final String[] out = new String[arr.length]; + for (int i = 0; i < arr.length; i++) + out[i] = String.valueOf(arr[i]); + return out; + } + + @Before + public void setUp() throws N5Exception { final String n5Root = "src/test/resources/ngff.n5"; n5 = new N5FSReader(n5Root); - } + } - @Test - public void testNgffGroupAttributeParsing() { + @Test + public void testNgffGroupAttributeParsing() { - final double eps = 1e-9; - try { - NgffMultiScaleGroupAttributes[] multiscales = n5.getAttribute("ngff_grpAttributes", "multiscales", NgffMultiScaleGroupAttributes[].class ); + final double eps = 1e-9; + try { + final NgffMultiScaleGroupAttributes[] multiscales = n5.getAttribute("ngff_grpAttributes", "multiscales", NgffMultiScaleGroupAttributes[].class); Assert.assertEquals("one set of multiscales", 1, multiscales.length); - - MultiscaleDataset[] datasets = multiscales[0].datasets; + + final MultiscaleDataset[] datasets = multiscales[0].datasets; Assert.assertEquals("num levels", 6, datasets.length); double scale = 4; for (int i = 0; i < datasets.length; i++) { - String pathName = String.format("s%d", i); + final String pathName = String.format("s%d", i); Assert.assertEquals("path name " + i, pathName, datasets[i].path); Assert.assertEquals("scale " + i, scale, datasets[i].transform.scale[2], eps); scale *= 2; } - } catch (N5Exception e) { + } catch (final N5Exception e) { fail("Ngff parsing failed"); e.printStackTrace(); } - } + } + + public static OmeNgffMultiScaleMetadata parse(final N5Writer zarr, final String base) { + + final N5TreeNode root = N5DatasetDiscoverer.discover(zarr); + return (OmeNgffMultiScaleMetadata)root.getDescendant(base).map(n -> n.getMetadata()).orElse(null); + } + + public static OmeNgffMultiScaleMetadata buildPermutedAxesMetadata(final int[] permutation, final DatasetAttributes dsetAttrs) { + + return buildPermutedAxesMetadata(permutation, true, dsetAttrs); + } + + public static OmeNgffMultiScaleMetadata buildPermutedAxesMetadata(final int[] permutation, final boolean cOrder, final DatasetAttributes dsetAttrs) { + + final HashMap axisResolution = new HashMap<>(); + final HashMap axisTranslation = new HashMap<>(); + for (int i = 0; i < DEFAULT_AXES.length; i++) { + axisResolution.put(DEFAULT_AXES_S[i], DEFAULT_RESOLUTION[i]); + axisTranslation.put(DEFAULT_AXES_S[i], DEFAULT_TRANSLATION[i]); + } + + final String[] axesLabels = new String[permutation.length]; + AxisUtils.permute(DEFAULT_AXES_S, axesLabels, permutation); + + final double[] resolution = AxisUtils.permute(DEFAULT_RESOLUTION, permutation); + final double[] translation = AxisUtils.permute(DEFAULT_TRANSLATION, permutation); + + final NgffSingleScaleAxesMetadata s0Meta = new NgffSingleScaleAxesMetadata("s0", resolution, translation, dsetAttrs); + final OmeNgffDataset[] dsets = new OmeNgffDataset[] { new OmeNgffDataset() }; + dsets[0].path = s0Meta.getPath(); + dsets[0].coordinateTransformations = s0Meta.getCoordinateTransformations(); + + + final Axis[] axes = AxisUtils.defaultAxes(axesLabels); + + if (cOrder) { + ArrayUtils.reverse(resolution); + ArrayUtils.reverse(translation); + ArrayUtils.reverse(axes); + } + + final int nd = axes.length; + return new OmeNgffMultiScaleMetadata( + nd, "", "test", "type", "0.4", + axes, + dsets, + new DatasetAttributes[] { dsetAttrs }, + null, null); + + } + + public static CosemTransform buildPermutedAxesCosemMetadata( + final int[] permutation, final boolean cOrder, final DatasetAttributes dsetAttrs) { + + final HashMap axisResolution = new HashMap<>(); + final HashMap axisTranslation = new HashMap<>(); + for (int i = 0; i < DEFAULT_AXES.length; i++) { + axisResolution.put(DEFAULT_AXES_S[i], DEFAULT_RESOLUTION[i]); + axisTranslation.put(DEFAULT_AXES_S[i], DEFAULT_TRANSLATION[i]); + } + + final String[] axesLabels = new String[permutation.length]; + AxisUtils.permute(DEFAULT_AXES_S, axesLabels, permutation); + + final double[] resolution = AxisUtils.permute(DEFAULT_RESOLUTION, permutation); + final double[] translation = AxisUtils.permute(DEFAULT_TRANSLATION, permutation); + + final NgffSingleScaleAxesMetadata s0Meta = new NgffSingleScaleAxesMetadata("s0", resolution, translation, dsetAttrs); + final OmeNgffDataset[] dsets = new OmeNgffDataset[]{new OmeNgffDataset()}; + dsets[0].path = s0Meta.getPath(); + dsets[0].coordinateTransformations = s0Meta.getCoordinateTransformations(); + + final Axis[] axes = AxisUtils.defaultAxes(axesLabels); + final String[] units = IntStream.range(0, axes.length).mapToObj(x -> "").toArray(n -> { + return new String[n]; + }); + + if (cOrder) { + ArrayUtils.reverse(resolution); + ArrayUtils.reverse(translation); + ArrayUtils.reverse(axesLabels); + } + + return new N5CosemMetadata.CosemTransform(axesLabels, resolution, translation, units); + } + + public static void writePermutedAxes(final N5Writer zarr, final String base, final boolean cOrder, final int[] permutation) { + + + final long[] dims = AxisUtils.permute(DEFAULT_DIMENSIONS, permutation); + final int[] blkSize = Arrays.stream(dims).mapToInt(x -> (int)x).toArray(); + + final String dsetPath = base + "/s0"; + createDataset(zarr, cOrder, dsetPath, dims, blkSize, DataType.UINT8, new RawCompression()); + + final DatasetAttributes dsetAttrs = zarr.getDatasetAttributes(dsetPath); + + final OmeNgffMultiScaleMetadata meta = NgffTests.buildPermutedAxesMetadata(permutation, true, dsetAttrs); + zarr.setAttribute(base, "multiscales", new OmeNgffMultiScaleMetadata[]{meta}); + } + + public static void createDataset( + final N5Writer zarr, + final boolean cOrder, + final String datasetPath, + final long[] dimensions, + final int[] blockSize, + final DataType dataType, + final Compression compression) throws N5Exception { + + assert zarr instanceof ZarrKeyValueWriter; + + if (cOrder) { + zarr.createDataset(datasetPath, dimensions, blockSize, dataType, compression); + } + else { + final long[] dimsRev = ArrayUtils.clone(dimensions); + ArrayUtils.reverse(dimsRev); + + final int[] blkSizeRev = ArrayUtils.clone(blockSize); + ArrayUtils.reverse(blkSizeRev); + + zarr.createDataset(datasetPath, dimsRev, blkSizeRev, dataType, compression); + zarr.setAttribute(datasetPath, "order", "F"); + } + + } + + public static int[] permutationFromName(final String name) { + + final String nameNorm = name.toLowerCase(); + final String[] axesOrder = nameNorm.split("_"); + final char[] axes = axesOrder[0].toCharArray(); + + final int[] p = new int[axes.length]; + for (int i = 0; i < axes.length; i++) { + p[i] = ArrayUtils.indexOf(DEFAULT_AXES, axes[i]); + } + return p; + } + + public static boolean isCOrderFromName(final String name) { + + return name.toLowerCase().split("_")[1].equals("c"); + } } diff --git a/src/test/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/NgffAxisTests.java b/src/test/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/NgffAxisTests.java index 70cdfd0..3b8e559 100644 --- a/src/test/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/NgffAxisTests.java +++ b/src/test/java/org/janelia/saalfeldlab/n5/universe/metadata/ome/ngff/v04/NgffAxisTests.java @@ -14,12 +14,17 @@ import org.janelia.saalfeldlab.n5.universe.N5TreeNode; import org.janelia.saalfeldlab.n5.universe.metadata.axes.Axis; import org.janelia.saalfeldlab.n5.universe.metadata.axes.AxisUtils; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.coordinateTransformations.CoordinateTransformation; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.coordinateTransformations.ScaleCoordinateTransformation; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.coordinateTransformations.TranslationCoordinateTransformation; import org.junit.Test; import net.imglib2.realtransform.AffineTransform3D; public class NgffAxisTests { + private static double EPS = 1e-6; + @Test public void testSpatial3D() { @@ -97,24 +102,30 @@ public void testSpatial3D() { @Test public void testAxisOrderStorageOrder() { - URI rootF = Paths.get("src", "test", "resources", "metadata.zarr").toUri(); + final URI rootF = Paths.get("src", "test", "resources", "metadata.zarr").toUri(); final N5Reader zarr = new N5Factory().openReader(rootF.toString()); final String[] names = new String[]{"c", "x", "y", "z"}; + ArrayUtils.reverse(names); + + // the expected scales and translations are reversed versions + // of the arrays appearing in the JSON + final double[] expectedScales = new double[]{13, 12, 11, 1}; + final double[] expectedTranslations = new double[]{3, 2, 1, 0}; final OmeNgffMetadataParser parser = new OmeNgffMetadataParser(); - // no not flip when f-Order + // flip when f-Order final N5TreeNode fOrderNode = CoordinateTransformParsingTest.setupNode(zarr, "fOrder", "1"); - axisOrderTest(parser.parseMetadata(zarr, fOrderNode), names); + axisOrderTest(parser.parseMetadata(zarr, fOrderNode), names, expectedScales, expectedTranslations); - // flip when c-Order - ArrayUtils.reverse(names); + // and flip when c-Order final N5TreeNode cOrderNode = CoordinateTransformParsingTest.setupNode(zarr, "cOrder", "1"); - axisOrderTest(parser.parseMetadata(zarr, cOrderNode), names); + axisOrderTest(parser.parseMetadata(zarr, cOrderNode), names, expectedScales, expectedTranslations); } - private void axisOrderTest(final Optional metaOpt, final String[] expectedNames) { + private void axisOrderTest(final Optional metaOpt, final String[] expectedNames, + final double[] expectedScales, final double[] expectedTranslations) { assertTrue("ss not parsable", metaOpt.isPresent()); @@ -124,6 +135,15 @@ private void axisOrderTest(final Optional metaOpt, final String final Axis[] axes = meta.multiscales[0].axes; final String[] names = Arrays.stream(axes).map(a -> a.getName()).toArray(N -> new String[N]); assertArrayEquals("names don't match", expectedNames, names); + + final CoordinateTransformation[] cts = meta.multiscales[0].datasets[0].coordinateTransformations; + assertTrue("first coordinate transform not scale", cts[0] instanceof ScaleCoordinateTransformation); + final ScaleCoordinateTransformation ct0 = (ScaleCoordinateTransformation)cts[0]; + assertArrayEquals("scales don't match", expectedScales, ct0.getScale(), EPS); + + assertTrue("second coordinate transform not translation", cts[1] instanceof TranslationCoordinateTransformation); + final TranslationCoordinateTransformation ct1 = (TranslationCoordinateTransformation)cts[1]; + assertArrayEquals("translations don't match", expectedTranslations, ct1.getTranslation(), EPS); } } diff --git a/src/test/resources/metadata.zarr/cOrder/.zattrs b/src/test/resources/metadata.zarr/cOrder/.zattrs index f0bf7db..fb9e231 100644 --- a/src/test/resources/metadata.zarr/cOrder/.zattrs +++ b/src/test/resources/metadata.zarr/cOrder/.zattrs @@ -31,9 +31,18 @@ "type": "scale", "scale": [ 1.0, - 11.239999771118164, - 11.239999771118164, - 28.0 + 11.0, + 12.0, + 13.0 + ] + }, + { + "type": "translation", + "translation": [ + 0.0, + 1.0, + 2.0, + 3.0 ] } ] diff --git a/src/test/resources/metadata.zarr/fOrder/.zattrs b/src/test/resources/metadata.zarr/fOrder/.zattrs index f0bf7db..e522b3c 100644 --- a/src/test/resources/metadata.zarr/fOrder/.zattrs +++ b/src/test/resources/metadata.zarr/fOrder/.zattrs @@ -31,9 +31,18 @@ "type": "scale", "scale": [ 1.0, - 11.239999771118164, - 11.239999771118164, - 28.0 + 11.0, + 12.0, + 13.0 + ] + }, + { + "type": "translation", + "translation": [ + 0.0, + 1.0, + 2.0, + 3.0 ] } ] @@ -41,4 +50,4 @@ ] } ] -} \ No newline at end of file +}