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
+}