Skip to content

Commit

Permalink
feat: Methods for axis permutation on metadata objects (#19)
Browse files Browse the repository at this point in the history
* pref: flip both f-order and c-order metadata
* add reverseDimensions helper method
* feat: axis utils more methods for permutation
* feat: methods for permuting metadata axes
* feat: can detect "all default" metadata
* adds N5DefaultSingleScaleMetadata
* feat: another permuteImageAndMetadataForImagePlus helper method
* test: build axis permuted cosem data
* fix: permutation of cosem metadata
  • Loading branch information
bogovicj authored May 15, 2024
1 parent bb1e961 commit 56e39bd
Show file tree
Hide file tree
Showing 14 changed files with 830 additions and 106 deletions.
5 changes: 2 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,11 @@
<releaseProfiles>sign,deploy-to-scijava</releaseProfiles>

<n5.version>3.2.0</n5.version>
<n5-aws-s3.version>4.1.2</n5-aws-s3.version>
<n5-hdf5.version>2.2.0</n5-hdf5.version>
<n5-imglib2.version>7.0.0</n5-imglib2.version>
<n5-google-cloud.version>4.1.0</n5-google-cloud.version>

<n5-zarr.version>1.3.1</n5-zarr.version>
<n5-aws-s3.version>4.1.1</n5-aws-s3.version>

<jackson-jq.version>1.0.0-preview.20191208</jackson-jq.version>
<alphanumeric-comparator.version>1.4.1</alphanumeric-comparator.version>
Expand Down Expand Up @@ -229,7 +228,7 @@
<version>${jaxb-api.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencies>


<repositories>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,17 +21,21 @@
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;
import net.imglib2.realtransform.ScaleAndTranslation;
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];
Expand Down Expand Up @@ -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]));
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -307,4 +316,208 @@ else if (scale.length == 3)
return null;
}

@SuppressWarnings("unchecked")
public static <M extends N5DatasetMetadata> 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 extends N5Metadata> 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());

}

}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -44,6 +43,8 @@ public class N5GenericSingleScaleMetadataParser implements N5MetadataParser<N5Si
public boolean unitKeyStrict = false;
public boolean downsamplingFactorsKeyStrict = false;

private boolean allDefault;

public N5GenericSingleScaleMetadataParser() {
minKey = DEFAULT_MIN;
maxKey = DEFAULT_MAX;
Expand Down Expand Up @@ -183,6 +184,7 @@ public N5GenericSingleScaleMetadataParser build() {
@Override
public Optional<N5SingleScaleMetadata> parseMetadata(final N5Reader n5, final N5TreeNode node) {

allDefault = true;
try {
final DatasetAttributes attributes = n5.getDatasetAttributes(node.getPath());
if (attributes == null)
Expand Down Expand Up @@ -226,8 +228,13 @@ public Optional<N5SingleScaleMetadata> 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) {
Expand All @@ -244,14 +251,19 @@ private static <T> Optional<T> getAttributeOptional(final N5Reader n5, final Str
}
}

private static <T> T getAttribute(final N5Reader n5, final String path, final String key, final Class<T> clazz,
private <T> T getAttribute(final N5Reader n5, final String path, final String key, final Class<T> clazz,
final boolean strict, final Predicate<T> filter,
final Supplier<T> defaultValue) {

final Optional<T> 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();
});
}
}
Loading

0 comments on commit 56e39bd

Please sign in to comment.