diff --git a/.gitignore b/.gitignore index 8502caedf..4959fdacb 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ # IntelliJ # .idea/ SPIM_Registration.iml + +.claude \ No newline at end of file diff --git a/pom.xml b/pom.xml index 11927a283..b5c633db4 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ net.preibisch multiview-reconstruction - 9.0.0-SNAPSHOT + 9.0.1-SNAPSHOT Multiview Reconstruction Software for the reconstruction of multi-view microscopic acquisitions @@ -98,6 +98,10 @@ like Selective Plane Illumination Microscopy (SPIM) Data. gpl_v2 Multiview Reconstruction developers. + 17 + https://javadoc.scijava.org/Java11/ + https://javadoc.scijava.org/JavaFX11/ + true sign,deploy-to-scijava @@ -119,6 +123,8 @@ like Selective Plane Illumination Microscopy (SPIM) Data. 1.0.0-beta-20 4.0.5 0.18.3 + 10.6.11 + 19.0.2.1 0.3.1 1.6.6 @@ -318,6 +324,11 @@ like Selective Plane Illumination Microscopy (SPIM) Data. ijp-kheops ${ijp-kheops.version} + + ch.epfl.biop + quick-start-czi-reader + 0.3.0 + com.google.code.gson gson @@ -361,6 +372,23 @@ like Selective Plane Illumination Microscopy (SPIM) Data. slf4j-api + + + org.openjfx + javafx-base + ${scijava.natives.classifier.javafx} + + + org.openjfx + javafx-controls + ${scijava.natives.classifier.javafx} + + + org.openjfx + javafx-swing + ${scijava.natives.classifier.javafx} + + org.junit.jupiter @@ -405,7 +433,7 @@ like Selective Plane Illumination Microscopy (SPIM) Data. - + fatjar @@ -455,6 +483,28 @@ like Selective Plane Illumination Microscopy (SPIM) Data. + + with-deps + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-dependencies + package + copy-dependencies + + ${project.build.directory}/deps + runtime + + + + + + + diff --git a/src/main/java/net/preibisch/mvrecon/fiji/datasetmanager/SmartSPIM.java b/src/main/java/net/preibisch/mvrecon/fiji/datasetmanager/SmartSPIM.java index f810e8a19..c92368572 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/datasetmanager/SmartSPIM.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/datasetmanager/SmartSPIM.java @@ -227,7 +227,7 @@ public static boolean populateImageSize( final SmartSPIMMetaData metadata, final IOFunctions.println( "file names are not equal. Stopping." ); return false; } - + } return true; diff --git a/src/main/java/net/preibisch/mvrecon/fiji/plugin/resave/Resave_N5Api.java b/src/main/java/net/preibisch/mvrecon/fiji/plugin/resave/Resave_N5Api.java index 47366a303..4266f6f0b 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/plugin/resave/Resave_N5Api.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/plugin/resave/Resave_N5Api.java @@ -39,6 +39,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; +import mpicbg.spim.data.sequence.VoxelDimensions; import org.bigdataviewer.n5.N5CloudImageLoader; import org.janelia.saalfeldlab.n5.Compression; import org.janelia.saalfeldlab.n5.DataType; @@ -192,13 +193,15 @@ else if ( n5Params.format == StorageFormat.HDF5 ) } else { + VoxelDimensions vx = data.getSequenceDescription().getViewDescription( viewId ).getViewSetup().getVoxelSize(); // 5d OME-ZARR with dimension=1 in c and t mrInfo = N5ApiTools.setupBdvDatasetsOMEZARR_ResaveRaw( n5Writer, viewId, dataTypes.get( viewId.getViewSetupId() ), dimensions.get( viewId.getViewSetupId() ), - //data.getSequenceDescription().getViewDescription( viewId ).getViewSetup().getVoxelSize().dimensionsAsDoubleArray(), + vx.dimensionsAsDoubleArray(), // resolutionS0 + vx.unit(), compression, blockSize, downsamplings, diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/explorer/popup/DisplayRawImagesPopup.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/explorer/popup/DisplayRawImagesPopup.java index 753c523da..61f25e725 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/explorer/popup/DisplayRawImagesPopup.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/explorer/popup/DisplayRawImagesPopup.java @@ -29,7 +29,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.stream.Collectors; import javax.swing.JMenu; import javax.swing.JMenuItem; diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/AllenOMEZarrProperties.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/AllenOMEZarrProperties.java index 65eb8beb2..8c403936f 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/AllenOMEZarrProperties.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/AllenOMEZarrProperties.java @@ -23,64 +23,52 @@ package net.preibisch.mvrecon.fiji.spimdata.imgloaders; import java.util.Arrays; -import java.util.HashMap; import java.util.Map; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5Reader; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMetadata; import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMultiScaleMetadata; import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMultiScaleMetadata.OmeNgffDataset; import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.coordinateTransformations.CoordinateTransformation; import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.coordinateTransformations.ScaleCoordinateTransformation; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.coordinateTransformations.TranslationCoordinateTransformation; import bdv.img.n5.N5Properties; import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription; import mpicbg.spim.data.sequence.TimePoint; import mpicbg.spim.data.sequence.ViewId; +import net.preibisch.legacy.io.IOFunctions; import net.preibisch.mvrecon.fiji.spimdata.imgloaders.AllenOMEZarrLoader.OMEZARREntry; public class AllenOMEZarrProperties implements N5Properties { private final AbstractSequenceDescription< ?, ?, ? > sequenceDescription; - private final Map< ViewId, OMEZARREntry > viewIdToPath; + // mapping of viewIDs to corresponding OME-ZARRs + private final Map< ViewId, OMEZARREntry > viewIdToOmeZarrPath; - // Cache for actual dataset paths from multiscales metadata: setupId -> (level -> datasetPath) - private final Map< Integer, String[] > levelPathCache = new HashMap<>(); + // only warn once per opened XML if TranslationCoordinateTransformation is missing + private boolean warnedMissingTranslation = false; public AllenOMEZarrProperties( final AbstractSequenceDescription< ?, ?, ? > sequenceDescription, - final Map< ViewId, OMEZARREntry > viewIdToPath ) + final Map< ViewId, OMEZARREntry > viewIdToOmeZarrPath) { this.sequenceDescription = sequenceDescription; - this.viewIdToPath = viewIdToPath; + this.viewIdToOmeZarrPath = viewIdToOmeZarrPath; } private String getPath( final int setupId, final int timepointId ) { - return viewIdToPath.get( new ViewId( timepointId, setupId ) ).getPath(); + return viewIdToOmeZarrPath.get( new ViewId( timepointId, setupId ) ).getPath(); } @Override public String getDatasetPath( final N5Reader n5, final int setupId, final int timepointId, final int level ) { - // TODO: use the N5Reader @goinac @schweinfurthl, Claude wanted to add: - /* - // Ensure the level-path cache is populated from the multiscales metadata, - // otherwise we would fall back to numeric levels which are wrong for v0.5 nested paths - if ( levelPathCache.get( setupId ) == null ) - getMipMapResolutions( this, n5, setupId ); - */ - - // Check if we have cached the actual paths from multiscales metadata - final String[] cachedPaths = levelPathCache.get( setupId ); - if ( cachedPaths != null && level < cachedPaths.length ) - { - return getPath( setupId, timepointId ) + "/" + cachedPaths[ level ]; - } - // Fallback to numeric level (for v0.4 or if cache not populated) - return String.format( getPath( setupId, timepointId ) + "/%d", level ); + return getMultiscaleDatasetPathOrDefault( n5, timepointId, setupId, level ); } @Override @@ -98,7 +86,7 @@ public double[][] getMipmapResolutions( final N5Reader n5, final int setupId ) @Override public long[] getDimensions( final N5Reader n5, final int setupId, final int timepointId, final int level ) { - final String path = getDatasetPath( n5, setupId, timepointId, level ); + final String path = getMultiscaleDatasetPathOrDefault(n5, timepointId, setupId, level); final long[] dimensions = n5.getDatasetAttributes( path ).getDimensions(); // dataset dimensions is 5D, remove the channel and time dimensions return Arrays.copyOf( dimensions, 3 ); @@ -126,17 +114,13 @@ private static int getFirstAvailableTimepointId( final AbstractSequenceDescripti private static DataType getDataType( final AllenOMEZarrProperties n5properties, final N5Reader n5, final int setupId ) { - // we need to make sure the cache is populated, otherwise getDatasetPath(...) might return the wrong path - if ( n5properties.levelPathCache.get( setupId ) == null ) - getMipMapResolutions( n5properties, n5, setupId ); - final int timePointId = getFirstAvailableTimepointId( n5properties.sequenceDescription, setupId ); // If all timepoints are missing, return a default data type if ( timePointId < 0 ) return DataType.UINT16; - final String path = n5properties.getDatasetPath( n5, setupId, timePointId, 0 ); + final String path = n5properties.getMultiscaleDatasetPathOrDefault(n5, timePointId, setupId, 0); final DatasetAttributes attributes = n5.getDatasetAttributes( path ); if ( attributes == null ) @@ -154,61 +138,153 @@ private static double[][] getMipMapResolutions( final AllenOMEZarrProperties n5p return new double[][] { { 1.0, 1.0, 1.0 } }; // multiresolution pyramid + final OmeNgffMultiScaleMetadata multiScaleMetadata = n5properties.getViewSetupMultiscaleMetadata(n5, timePointId, setupId); + final double[][] mipMapResolutions = new double[ multiScaleMetadata.datasets.length ][ 3 ]; + double[] scaleS0 = null; //org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMetadata // for this to work you need to register an adapter in the N5Factory class // final GsonBuilder builder = new GsonBuilder().registerTypeAdapter( CoordinateTransformation.class, new CoordinateTransformationAdapter() ); final String path = n5properties.getPath( setupId, timePointId ); - // Try v0.4 structure first (multiscales at root), then v0.5/Zarr v3 structure (nested under ome) - OmeNgffMultiScaleMetadata[] multiscales = n5.getAttribute( path, "multiscales", OmeNgffMultiScaleMetadata[].class ); - - if ( multiscales == null || multiscales.length == 0 ) - multiscales = n5.getAttribute( path, "ome/multiscales", OmeNgffMultiScaleMetadata[].class ); + // iterate over all resolution levels for scale + for ( int s = 0; s < multiScaleMetadata.datasets.length; ++s ) + { + final OmeNgffDataset ds = multiScaleMetadata.datasets[ s ]; - if ( multiscales == null || multiscales.length == 0 ) - throw new RuntimeException( "Could not parse OME-ZARR multiscales object (tried 'multiscales' and 'ome/multiscales'). stopping." ); + for ( final CoordinateTransformation< ? > c : ds.coordinateTransformations ) + { + if ( c instanceof ScaleCoordinateTransformation ) + { + final ScaleCoordinateTransformation scale = ( ScaleCoordinateTransformation ) c; - if ( multiscales.length != 1 ) - System.out.println( "This dataset has " + multiscales.length + " objects, we expected 1. Picking the first one." ); + if ( scaleS0 == null ) + scaleS0 = scale.getScale().clone(); - //System.out.println( "AllenOMEZarrLoader.getMipmapResolutions() for " + setupId + " using " + n5properties.getPath( setupId, timePointId ) + ": found " + multiscales[ 0 ].datasets.length + " multi-resolution levels." ); + for ( int d = 0; d < mipMapResolutions[ s ].length; ++d ) + { + mipMapResolutions[ s ][ d ] = scale.getScale()[ d ] / scaleS0[ d ]; + mipMapResolutions[ s ][ d ] = Math.round(mipMapResolutions[ s ][ d ]*10000)/10000d; // round to the 5th digit + } + } + } + } - double[][] mipMapResolutions = new double[ multiscales[ 0 ].datasets.length ][ 3 ]; - double[] firstScale = null; + // iterate over all resolution levels for translation (to make sure scaleS0 is assigned if it existed) + // OME-Zarr 0.4: physical = pixel * scale + translation + // For averaging downsampling by factor r: expected relative shift = (r-1)/2 in s0 pixels + // For non-averaging (strided) downsampling: expected relative shift = 0 + double[] translationS0 = null; + Boolean isAveraging = null; // determined from first downsampled level with r>1, then verified for subsequent levels - // Cache the actual dataset paths from metadata (they may not be just "0", "1", "2") - final String[] levelPaths = new String[ multiscales[ 0 ].datasets.length ]; + //System.out.println( "\nsetup " + setupId ); - for ( int i = 0; i < multiscales[ 0 ].datasets.length; ++i ) + for ( int s = 0; s < multiScaleMetadata.datasets.length; ++s ) { - final OmeNgffDataset ds = multiscales[ 0 ].datasets[ i ]; + final OmeNgffDataset ds = multiScaleMetadata.datasets[ s ]; - // Store the actual path for this resolution level - levelPaths[ i ] = ds.path; + boolean foundTranslation = false; for ( final CoordinateTransformation< ? > c : ds.coordinateTransformations ) { - if ( c instanceof ScaleCoordinateTransformation ) + if ( c instanceof TranslationCoordinateTransformation ) { - final ScaleCoordinateTransformation s = ( ScaleCoordinateTransformation ) c; + final TranslationCoordinateTransformation t = ( TranslationCoordinateTransformation ) c; + foundTranslation = true; + + if ( scaleS0 == null ) + throw new IllegalStateException( "Expected first scale to be set before the translation for level " + s + " dataset is processed" ); + + //System.out.println( "s=" + s + ", translation: " + Arrays.toString( t.getTranslation() )); - if ( firstScale == null ) - firstScale = s.getScale().clone(); + // capture s0's translation + if ( translationS0 == null ) + { + translationS0 = t.getTranslation().clone(); + // s0: no validation needed, just capture + break; + } - for ( int d = 0; d < mipMapResolutions[ i ].length; ++d ) + for ( int d = 0; d < mipMapResolutions[ s ].length; ++d ) { - mipMapResolutions[ i ][ d ] = s.getScale()[ d ] / firstScale[ d ]; - mipMapResolutions[ i ][ d ] = Math.round(mipMapResolutions[ i ][ d ]*10000)/10000d; // round to the 5th digit + final double r = mipMapResolutions[ s ][ d ]; + + // skip dimensions that are not downsampled (r=1), both modes have shift=0 + if ( Math.abs( r - 1.0 ) < 0.01 ) + continue; + + // relative pixel translation: (translation_s - translationS0) / scaleS0 + final double relPxTranslation = ( t.getTranslation()[ d ] - translationS0[ d ] ) / scaleS0[ d ]; + final double expectedAveraging = ( r - 1.0 ) / 2.0; // 0.5, 1.5, 3.5, 7.5, ... + + final boolean matchesAveraging = Math.abs( relPxTranslation - expectedAveraging ) < 0.01; + final boolean matchesNonAveraging = Math.abs( relPxTranslation ) < 0.01; + + //System.out.println( "s=" + s + ", d=" + d + ", relPxTranslation=" + relPxTranslation + " @ scale=" + r ); + + if ( isAveraging == null ) + { + // determine mode from first downsampled dimension + if ( matchesAveraging ) + isAveraging = true; + else if ( matchesNonAveraging ) + throw new IllegalStateException( "Non-averaging downsampling detected (translation=0 for level " + s + " dim " + d + "), which is currently not supported." ); + else + throw new IllegalStateException( "Unsupported translation for level " + s + " dim " + d + ": relative pixel translation=" + relPxTranslation + " (expected " + expectedAveraging + " for averaging or 0.0 for non-averaging)." ); + } + else + { + // verify consistency with detected mode + final double expected = isAveraging ? expectedAveraging : 0.0; + if ( Math.abs( relPxTranslation - expected ) >= 0.01 ) + throw new IllegalStateException( "Inconsistent translation for level " + s + " dim " + d + ": relative pixel translation=" + relPxTranslation + ", expected " + expected + " based on detected " + ( isAveraging ? "averaging" : "non-averaging" ) + " downsampling." ); + } } - //System.out.println( "AllenOMEZarrLoader.getMipmapResolutions(), level " + i + ": " + Arrays.toString( s.getScale() ) + " >> " + Arrays.toString( mipMapResolutions[ i ] ) ); } } - } - // Cache the level paths for this setup - n5properties.levelPathCache.put( setupId, levelPaths ); + if ( !foundTranslation && s > 0 && !n5properties.warnedMissingTranslation ) + { + IOFunctions.println( "WARNING: No TranslationCoordinateTransformation found, assuming half-pixel shifts for averaging-based downsampling." ); + n5properties.warnedMissingTranslation = true; + } + } return mipMapResolutions; } + + private String getMultiscaleDatasetPathOrDefault( N5Reader n5, int timepointId, int setupId, int level ) + { + OmeNgffMultiScaleMetadata omeNgffMultiScaleMetadata = getViewSetupMultiscaleMetadata(n5, timepointId, setupId); + + String viewSetupPath = getPath( setupId, timepointId ); + // get the first scale path from the metadata + String datasetPath = omeNgffMultiScaleMetadata.datasets[level].path; + return String.format( "%s/%s", viewSetupPath, datasetPath); + } + + // retrieve and cache the multiscale metadata + private OmeNgffMultiScaleMetadata getViewSetupMultiscaleMetadata(N5Reader n5, int timePointId, int setupId) { + String datasetPath = getPath( setupId, timePointId ); + + System.out.println( "Getting multiscale metadata for " + datasetPath ); + OmeNgffMetadata omeMetadata = n5.getAttribute(datasetPath, "ome", OmeNgffMetadata.class); + + final OmeNgffMultiScaleMetadata[] multiscales; + if (omeMetadata != null) { + System.out.println( "Found OME metadata at " + datasetPath + ": " + omeMetadata); + multiscales = omeMetadata.multiscales; + } else { + multiscales = n5.getAttribute( datasetPath, "multiscales", OmeNgffMultiScaleMetadata[].class ); + System.out.println( "Retrieved OME multiscales at " + datasetPath + ": " + multiscales); + } + + if ( multiscales == null || multiscales.length == 0 ) + throw new IllegalStateException( "Could not parse OME-ZARR multiscales object. stopping." ); + + if ( multiscales.length > 1 ) + System.out.println( "This dataset has " + multiscales.length + " objects, we expected 1. Picking the first one." ); + + return multiscales[0]; + } } diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/LegacyFileMapImgLoaderLOCI.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/LegacyFileMapImgLoaderLOCI.java index 0e8f63874..479d36f30 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/LegacyFileMapImgLoaderLOCI.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/LegacyFileMapImgLoaderLOCI.java @@ -192,7 +192,7 @@ protected < T extends RealType< T > & NativeType< T > > RandomAccessibleInterval // use a new ImageReader since we might be loading multi-threaded and BioFormats is not thread-save // use Memoizer to cache ReaderState for each File on disk // see: https://www-legacy.openmicroscopy.org/site/support/bio-formats5.1/developers/matlab-dev.html#reader-performance - IFormatReader reader = null; + IFormatReader reader; if (zGrouped) { final FileStitcher fs = new FileStitcher(true); @@ -239,8 +239,14 @@ protected < T extends RealType< T > & NativeType< T > > RandomAccessibleInterval final int height = dim[ 1 ]; final int depth = dim[ 2 ]; final int numPx = width * height; - - final byte[] b = new byte[ numPx * reader.getBitsPerPixel() / 8 ]; + final int bitsPerPixels = reader.getBitsPerPixel(); + final int bytesPerPixel = BioformatsReaderUtils.getBytesPerPixel( reader ); + IOFunctions.println(String.format( + "Reader=%s Create %d buffer for (%d, %d, %d) image with pixelType %d, %d bits/pixel, %d bytes/pixel", + reader.getClass().getName(), (numPx * bytesPerPixel), width, height, depth, pixelType, bitsPerPixels, bytesPerPixel) + ); + final int bufferSize = Math.multiplyExact( numPx, bytesPerPixel ); + final byte[] b = new byte[ bufferSize ]; try { @@ -539,8 +545,9 @@ public static > long getMaxNonzero(RandomAccessibleInter public static long getMaxNonzero(IFormatReader reader) { - final int siz = reader.getBitsPerPixel() / 8 * reader.getRGBChannelCount() * reader.getSizeX() - * reader.getSizeY(); + final int siz = Math.multiplyExact( + BioformatsReaderUtils.getPlaneSizeInBytes( reader ), + reader.getRGBChannelCount() ); final byte[] buffer = new byte[siz]; final int nC = reader.getSizeC() == reader.getRGBChannelCount() ? 1 : reader.getSizeC(); diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/OMEZarrAttributes.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/OMEZarrAttributes.java index dfed47383..6fe9f0c8e 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/OMEZarrAttributes.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/OMEZarrAttributes.java @@ -27,21 +27,17 @@ import java.util.Arrays; import java.util.function.Function; +import net.preibisch.mvrecon.process.interestpointregistration.TransformationTools; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5Reader; import org.janelia.saalfeldlab.n5.universe.N5Factory; import org.janelia.saalfeldlab.n5.universe.StorageFormat; import org.janelia.saalfeldlab.n5.universe.metadata.axes.Axis; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMetadata; import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMultiScaleMetadata; import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMultiScaleMetadata.OmeNgffDataset; -import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMultiScaleMetadata.OmeNgffDownsamplingMetadata; -import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.coordinateTransformations.CoordinateTransformation; -import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.coordinateTransformations.ScaleCoordinateTransformation; -import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.coordinateTransformations.TranslationCoordinateTransformation; -import mpicbg.spim.data.sequence.VoxelDimensions; import net.imglib2.realtransform.AffineTransform3D; -import net.preibisch.mvrecon.process.interestpointregistration.TransformationTools; import util.URITools; public class OMEZarrAttributes @@ -52,112 +48,125 @@ public class OMEZarrAttributes * @param numResolutionLevels - number of multiresolution levels (e.g. s0, s1, s2 would be 3), always includes full res * @return */ - public static OmeNgffMultiScaleMetadata[] createOMEZarrMetadata( + public static OmeNgffMetadata createOMEZarrMetadata( final int n, - final String name, // I also saw "/" + final String name, + final String version, final double[] resolutionS0, - final String unitXYZ, // e.g micrometer + final String unitXYZ, final int numResolutionLevels, final Function levelToName, final Function levelToMipmapTransform ) { - // TODO: make sure the unit is supported by OME-ZARR, if not replace it because otherwise readers will fail - // TODO: e.g. um -> micrometer - // TODO: etc. - // TODO: can you find out what the correct unit for 'unit unknown' is, because that is what I would replace it with, otherwise micrometer - // TOOD: then please also change in TransformationTools.computeCalibration - - final OmeNgffMultiScaleMetadata[] meta = new OmeNgffMultiScaleMetadata[ 1 ]; - - // dataset name and co - final String path = ""; - final String type = null; - - // axis descriptions - // TODO: seem to be TCZYX order + // axis descriptions (TCZYX order, same as v0.4) final Axis[] axes = new Axis[ n ]; - int index = 0; + int axisIndex = 0; if ( n >= 5 ) - axes[ index++ ] = new Axis( "time", "t", "millisecond" ); - + axes[ axisIndex++ ] = new Axis( "time", "t", "millisecond" ); if ( n >= 4 ) - axes[ index++ ] = new Axis( "channel", "c", null ); - - axes[ index++ ] = new Axis( "space", "z", unitXYZ ); - axes[ index++ ] = new Axis( "space", "y", unitXYZ ); - axes[ index++ ] = new Axis( "space", "x", unitXYZ ); + axes[ axisIndex++ ] = new Axis( "channel", "c", null ); + final String spatialUnit = adaptSpatialUnit( unitXYZ ); + axes[ axisIndex++ ] = new Axis( "space", "z", spatialUnit ); + axes[ axisIndex++ ] = new Axis( "space", "y", spatialUnit ); + axes[ axisIndex++ ] = new Axis( "space", "x", spatialUnit ); - // multiresolution-pyramid - // TODO: seem to be in XYZCT order (but in the file it seems reversed) - final OmeNgffDataset[] datasets = new OmeNgffDataset[ numResolutionLevels ]; + // per-level scales / translations / paths + final String[] scalePaths = new String[ numResolutionLevels ]; + final double[][] scales = new double[ numResolutionLevels ][ n ]; + final double[][] translations = new double[ numResolutionLevels ][ n ]; for ( int s = 0; s < numResolutionLevels; ++s ) { - datasets[ s ] = new OmeNgffDataset(); - - datasets[ s ].path = levelToName.apply( s ); + scalePaths[ s ] = levelToName.apply( s ); final AffineTransform3D m = levelToMipmapTransform.apply( s ); - final double[] translation = new double[ n ]; - final double[] scale = new double[ n ]; - for ( int d = 0; d < 3; ++d ) { - translation[ d ] = m.getTranslation()[ d ]; - scale[ d ] = resolutionS0[ d ] * m.get( d, d ); + translations[ s ][ d ] = resolutionS0[ d ] * m.getTranslation()[ d ]; + scales[ s ][ d ] = resolutionS0[ d ] * m.get( d, d ); } // if 4d and 5d, add 1's for C and T for ( int d = 3; d < n; ++d ) { - translation[ d ] = 0.0; - scale[ d ] = 1.0; + translations[ s ][ d ] = 0.0; + scales[ s ][ d ] = 1.0; } - - datasets[ s ].coordinateTransformations = new CoordinateTransformation[ 2 ]; - datasets[ s ].coordinateTransformations[ 0 ] = new ScaleCoordinateTransformation( scale ); // TODO: check - datasets[ s ].coordinateTransformations[ 1 ] = new TranslationCoordinateTransformation( translation ); // TODO: check } - - final double[] scale = new double[ n ]; - - for ( int d = 0; d < n; ++d ) - scale[ d ] = 1.0; - - // just saw these being null everywhere - final DatasetAttributes[] childrenAttributes = null; - final CoordinateTransformation[] coordinateTransformations = new CoordinateTransformation[] { new ScaleCoordinateTransformation( scale ) }; // I also saw null - final OmeNgffDownsamplingMetadata metadata = null; - - meta[ 0 ] = new OmeNgffMultiScaleMetadata( n, path, name, type, "0.4", axes, datasets, coordinateTransformations, childrenAttributes, metadata ); - - return meta; + return OmeNgffMetadata.buildForWriting(n, name, version, axes, scalePaths, scales, translations); } - - // Note: TransformationTools.computeAverageCalibration does this reasonably correct - /* - public static double[] getResolutionS0( final VoxelDimensions vx, final double anisoF, final double downsamplingF ) + public static double[] getResolutionS0( final double[] cal, final double anisoF, final double downsamplingF ) { - final double[] resolutionS0 = vx.dimensionsAsDoubleArray(); + double[] resolutionS0 = Arrays.copyOf( cal, cal.length ); - // not preserving anisotropy - if ( Double.isNaN( anisoF ) ) - resolutionS0[ 2 ] = resolutionS0[ 0 ]; + if ( !Double.isNaN( anisoF ) ) { + // preserving anisotropy + resolutionS0[2] = cal[2] * anisoF; + } // downsampling if ( !Double.isNaN( downsamplingF ) ) Arrays.setAll( resolutionS0, d -> resolutionS0[ d ] * downsamplingF ); - // TODO: this is a hack so the export downsampling pyramid is working - Arrays.setAll( resolutionS0, d -> 1 ); - return resolutionS0; } - */ + + /** + * Adapt various space unit namings to the units supported by Neuroglancer. + * OME NGFF spec does not have any restrictions on units but Neuroglancer only supports the ones that end in meter or the US customary units. + * @param unit + * @return + */ + private static String adaptSpatialUnit(String unit) + { + if ( unit == null ) + return "micrometer"; + + switch ( unit.toLowerCase() ) { + case "angstrom": + case "ångström": + case "ångströms": + return "angstrom"; + case "nm": + case "nanometers": + case "nanometer": + return "nanometer"; + case "mm": + case "millimeters": + case "millimeter": + return "millimeter"; + case "m": + case "meters": + case "meter": + return "meter"; + case "km": + case "kilometer": + case "kilometers": + return "kilometer"; + case "inch": + case "inches": + return "inch"; + case "foot": + case "feet": + return "foot"; + case "yard": + case "yards": + return "yard"; + case "mile": + case "miles": + return "mile"; + case "um": + case "μm": + case "microns": + case "micron": + default: + return "micrometer"; + } + } public static void loadOMEZarr( final N5Reader n5, final String dataset ) { diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/filemap2/VirtualRAIFactoryLOCI.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/filemap2/VirtualRAIFactoryLOCI.java index e83761e67..7ea88a638 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/filemap2/VirtualRAIFactoryLOCI.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/filemap2/VirtualRAIFactoryLOCI.java @@ -53,6 +53,7 @@ import net.imglib2.type.numeric.integer.UnsignedShortType; import net.imglib2.type.numeric.real.FloatType; import net.imglib2.util.Cast; +import net.preibisch.mvrecon.fiji.spimdata.imgloaders.util.BioformatsReaderUtils; class VirtualRAIFactoryLOCI { @@ -178,7 +179,7 @@ private static ByteBuffer readIntoBuffer( setReaderFileAndSeriesIfNecessary( reader, file, series ); // System.out.println( "reading z plane " + z + " from series " + series + " in file " + file.getAbsolutePath() ); - final int planeSize = ( reader.getBitsPerPixel() / 8 ) * reader.getSizeX() * reader.getSizeY(); + final int planeSize = BioformatsReaderUtils.getPlaneSizeInBytes( reader ); final int size = planeSize * reader.getRGBChannelCount(); final byte[] buffer = new byte[ size ]; diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/util/BioformatsReaderUtils.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/util/BioformatsReaderUtils.java index 405e5883f..f747cd6ba 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/util/BioformatsReaderUtils.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/util/BioformatsReaderUtils.java @@ -24,6 +24,8 @@ import java.util.HashMap; +import ch.epfl.biop.formats.in.ZeissQuickStartCZIReader; +import loci.formats.FormatTools; import loci.formats.IFormatReader; import loci.formats.ImageReader; import loci.formats.in.ZeissCZIReader; @@ -33,6 +35,7 @@ public class BioformatsReaderUtils { static HashMap, BioformatsReaderSetupHook> setupHooks = new HashMap<>(); static { setupHooks.put(ZeissCZIReader.class, CZIReaderSetupHook.getInstance()); + setupHooks.put(ZeissQuickStartCZIReader.class, CZIReaderSetupHook.getInstance()); } public static ImageReader createImageReaderWithSetupHooks() @@ -46,4 +49,30 @@ public static ImageReader createImageReaderWithSetupHooks() return reader; } + + public static int getBytesPerPixel(final IFormatReader reader) + { + final int bytesPerPixel = FormatTools.getBytesPerPixel(reader.getPixelType()); + + if (bytesPerPixel <= 0) + { + final int bitsPerPixel = reader.getBitsPerPixel(); + + if (bitsPerPixel > 0 && bitsPerPixel % 8 == 0) + return bitsPerPixel / 8; + + throw new IllegalArgumentException( + "Could not determine bytes per pixel for pixelType=" + reader.getPixelType() + + ", bitsPerPixel=" + bitsPerPixel); + } + + return bytesPerPixel; + } + + public static int getPlaneSizeInBytes(final IFormatReader reader) + { + return Math.multiplyExact( + Math.multiplyExact(getBytesPerPixel(reader), reader.getSizeX()), + reader.getSizeY()); + } } diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/util/CZIReaderSetupHook.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/util/CZIReaderSetupHook.java index 4b1121192..2d38e7907 100644 --- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/util/CZIReaderSetupHook.java +++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/util/CZIReaderSetupHook.java @@ -22,6 +22,7 @@ */ package net.preibisch.mvrecon.fiji.spimdata.imgloaders.util; +import ch.epfl.biop.formats.in.ZeissQuickStartCZIReader; import loci.formats.IFormatReader; import loci.formats.in.DynamicMetadataOptions; import loci.formats.in.MetadataOptions; @@ -56,22 +57,47 @@ public void setRelativePositions(boolean relativePositions) @Override public void runSetup(IFormatReader reader) { - if (!(reader instanceof ZeissCZIReader)) { - return; + if (reader instanceof ZeissCZIReader) { + setupZeissCZIReader(reader); + } else if (reader instanceof ZeissQuickStartCZIReader) { + setupQuickCZIReader(reader); } + } + + private void setupZeissCZIReader(final IFormatReader reader) + { + configureCZIReader( + reader, + ZeissCZIReader.ALLOW_AUTOSTITCHING_KEY, + ZeissCZIReader.RELATIVE_POSITIONS_KEY + ); + } + private void setupQuickCZIReader(final IFormatReader reader) + { + configureCZIReader( + reader, + ZeissQuickStartCZIReader.ALLOW_AUTOSTITCHING_KEY, + ZeissQuickStartCZIReader.RELATIVE_POSITIONS_KEY + ); + } + + private void configureCZIReader( + final IFormatReader reader, + final String autoStitchKey, + final String relativePositionsKey) + { // disable auto stitching, following solutions by @CellKai and @NicoKiaru in // https://forum.image.sc/t/change-in-czi-tile-info-metadata-after-upgrade-to-zeiss-lightsheet-7/49414/15 - MetadataOptions options = reader.getMetadataOptions(); + final MetadataOptions options = reader.getMetadataOptions(); if (options instanceof DynamicMetadataOptions) { - ((DynamicMetadataOptions) options).setBoolean( - ZeissCZIReader.ALLOW_AUTOSTITCHING_KEY, allowAutostitch); - ((DynamicMetadataOptions) options).setBoolean( - ZeissCZIReader.RELATIVE_POSITIONS_KEY, relativePositions); + final DynamicMetadataOptions dynamicOptions = (DynamicMetadataOptions) options; + dynamicOptions.setBoolean(autoStitchKey, allowAutostitch); + dynamicOptions.setBoolean(relativePositionsKey, relativePositions); + reader.setMetadataOptions(dynamicOptions); } else { - System.err.println("WARNING: could not set CZI autostitching option."); + System.err.println("WARNING: could not set CZI metadata options for " + reader.getClass().getSimpleName() + "."); } - reader.setMetadataOptions(options); } } diff --git a/src/main/java/net/preibisch/mvrecon/process/export/ExportN5Api.java b/src/main/java/net/preibisch/mvrecon/process/export/ExportN5Api.java index 251656495..c68d86a68 100644 --- a/src/main/java/net/preibisch/mvrecon/process/export/ExportN5Api.java +++ b/src/main/java/net/preibisch/mvrecon/process/export/ExportN5Api.java @@ -47,7 +47,7 @@ import org.janelia.saalfeldlab.n5.hdf5.N5HDF5Writer; import org.janelia.saalfeldlab.n5.imglib2.N5Utils; import org.janelia.saalfeldlab.n5.universe.StorageFormat; -import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMultiScaleMetadata; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMetadata; import bdv.export.ExportMipmapInfo; import bdv.export.ProposeMipmaps; @@ -256,24 +256,42 @@ else if ( storageType == StorageFormat.N5 || storageType == StorageFormat.ZARR | final Function levelToMipmapTransform = (level) -> MipmapTransforms.getMipmapTransformDefault( mrInfoZarr[level].absoluteDownsamplingDouble() ); - IOFunctions.println( "Resolution of level 0: " + Util.printCoordinates( cal ) + " " + unit ); //vx.unit() might not be OME-ZARR compatible + // at this point the pixel size has already been set + // - here we adjust the S0 resolution based on selected downsampling and anisotropy before exporting to N5 + double[] resolutionS0 = OMEZarrAttributes.getResolutionS0( cal, anisoF, downsamplingF ); - // create metadata - final OmeNgffMultiScaleMetadata[] meta = OMEZarrAttributes.createOMEZarrMetadata( - 5, // int n - "/", // String name, I also saw "/" - cal, // double[] resolutionS0, - unit, //"micrometer", //vx.unit() might not be OME-ZARR compatible // String unitXYZ, // e.g micrometer - mrInfoZarr.length, // int numResolutionLevels, - levelToName, - levelToMipmapTransform ); + IOFunctions.println( "Calibration: " + Util.printCoordinates( cal ) + " micrometer; resolution at S0: " + Util.printCoordinates( resolutionS0 ) + " " + unit); - // save metadata - - //org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMetadata - // for this to work you need to register an adapter in the N5Factory class - // final GsonBuilder builder = new GsonBuilder().registerTypeAdapter( CoordinateTransformation.class, new CoordinateTransformationAdapter() ); - driverVolumeWriter.setAttribute( "/", "multiscales", meta ); + // create metadata + if (storageType == StorageFormat.ZARR2) { + final OmeNgffMetadata meta = OMEZarrAttributes.createOMEZarrMetadata( + 5, // int n + "/", // String name, I also saw "/" + "0.4", + resolutionS0, // double[] resolutionS0, + unit, //"micrometer", //vx.unit() might not be OME-ZARR compatible // String unitXYZ, // e.g micrometer + mrInfoZarr.length, // int numResolutionLevels, + levelToName, + levelToMipmapTransform ); + + // save metadata + driverVolumeWriter.setAttribute( "/", "multiscales", meta.multiscales ); + } else { + // ZARR3 + final OmeNgffMetadata meta = OMEZarrAttributes.createOMEZarrMetadata( + 5, // int n + "", // String name, I also saw "/" + "0.5", + resolutionS0, // double[] resolutionS0, + unit, //"micrometer", //vx.unit() might not be OME-ZARR compatible // String unitXYZ, // e.g micrometer + mrInfoZarr.length, // int numResolutionLevels, + levelToName, + levelToMipmapTransform ); + // save metadata + driverVolumeWriter.setAttribute( "/", "ome", meta ); + // this is hacky until OmeNgffMetadata gets fixed to output version + driverVolumeWriter.setAttribute( "/", "ome/version", meta.version ); + } } } else @@ -375,24 +393,28 @@ else if ( storageType == StorageFormat.ZARR || storageType == StorageFormat.ZARR final Function levelToMipmapTransform = (level) -> MipmapTransforms.getMipmapTransformDefault( mrInfo[level].absoluteDownsamplingDouble() ); - IOFunctions.println( "Resolution of level 0: " + Util.printCoordinates( cal ) + " micrometer" ); + double[] resolutionS0 = OMEZarrAttributes.getResolutionS0( cal, anisoF, downsamplingF ); + + IOFunctions.println( "Calibration: " + Util.printCoordinates( cal ) + " micrometer; resolution at S0: " + Util.printCoordinates( resolutionS0 ) + " " + unit); // create metadata - final OmeNgffMultiScaleMetadata[] meta = OMEZarrAttributes.createOMEZarrMetadata( + final OmeNgffMetadata meta = OMEZarrAttributes.createOMEZarrMetadata( 3, // int n omeZarrSubContainer, // String name, I also saw "/" - cal, // double[] resolutionS0, - unit, //"micrometer", //vx.unit() might not be OME-ZARR compatible // String unitXYZ, // e.g micrometer + storageType == StorageFormat.ZARR2 ? "0.4" : "0.5", + resolutionS0, // double[] resolutionS0, + unit, // might not be OME-ZARR compatible // String unitXYZ, // e.g micrometer mrInfo.length, // int numResolutionLevels, (level) -> "/" + level, levelToMipmapTransform ); // save metadata - - //org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMetadata - // for this to work you need to register an adapter in the N5Factory class - // final GsonBuilder builder = new GsonBuilder().registerTypeAdapter( CoordinateTransformation.class, new CoordinateTransformationAdapter() ); - driverVolumeWriter.setAttribute( omeZarrSubContainer, "multiscales", meta ); + if (storageType == StorageFormat.ZARR2) + driverVolumeWriter.setAttribute( omeZarrSubContainer, "multiscales", meta.multiscales ); + else { + driverVolumeWriter.setAttribute( omeZarrSubContainer, "ome", meta ); + driverVolumeWriter.setAttribute( omeZarrSubContainer, "ome/version", meta.version ); + } omeZarrEntry = new OMEZARREntry( mrInfo[ 0 ].dataset.substring(0, mrInfo[ 0 ].dataset.lastIndexOf( "/" ) ), diff --git a/src/main/java/net/preibisch/mvrecon/process/interestpointregistration/TransformationTools.java b/src/main/java/net/preibisch/mvrecon/process/interestpointregistration/TransformationTools.java index 2543905b8..ab1c91ee8 100644 --- a/src/main/java/net/preibisch/mvrecon/process/interestpointregistration/TransformationTools.java +++ b/src/main/java/net/preibisch/mvrecon/process/interestpointregistration/TransformationTools.java @@ -334,13 +334,13 @@ public static Pair< double[], String > computeAverageCalibration( else if ( unit.equalsIgnoreCase( transformedCal.getB() ) ) unit = transformedCal.getB(); else - unit = "inconsisistent"; + unit = "inconsistent"; System.out.println( "Calibration (transformed): " + Util.printCoordinates( transformedCal.getA() ) + " " + transformedCal.getB() ); } if ( count == 0 ) - return new ValuePair<>( new double[] { 1, 1, 1 }, "px" ); + return new ValuePair<>( new double[] { 1, 1, 1 }, "micrometer" ); else return new ValuePair<>( new double[] { avgCalX / (double)count, avgCalY / (double)count, avgCalZ / (double)count }, unit ); } diff --git a/src/main/java/net/preibisch/mvrecon/process/n5api/N5ApiTools.java b/src/main/java/net/preibisch/mvrecon/process/n5api/N5ApiTools.java index 0aa5db7f8..e16e05da5 100644 --- a/src/main/java/net/preibisch/mvrecon/process/n5api/N5ApiTools.java +++ b/src/main/java/net/preibisch/mvrecon/process/n5api/N5ApiTools.java @@ -43,10 +43,10 @@ import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5Writer; import org.janelia.saalfeldlab.n5.RawCompression; +import org.janelia.saalfeldlab.n5.codec.checksum.Crc32cChecksumCodec; import org.janelia.saalfeldlab.n5.imglib2.N5Utils; import org.janelia.saalfeldlab.n5.universe.StorageFormat; -import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMultiScaleMetadata; -import org.janelia.saalfeldlab.n5.zarr.v3.ZarrV3Compressor; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMetadata; import org.janelia.saalfeldlab.n5.zarr.v3.ZarrV3DatasetAttributes; import bdv.export.ExportMipmapInfo; @@ -293,16 +293,13 @@ public static MultiResolutionLevelInfo[] setupMultiResolutionPyramid( if ( useSharding ) { - // Use Zarr v3 sharding via ZarrV3DatasetAttributes constructor - // Convert Compression to DataCodecInfo (only Blosc and Zstandard officially supported) - final ZarrV3Compressor codec = ZarrV3Compressor.fromCompression( compression ); - final DatasetAttributes attributes = new ZarrV3DatasetAttributes( - dimensionsS0, // shape - shardSize, // shard dimensions - blockSize, // inner chunk size within shards - dataType, - codec ); // compression codec (can be null for no compression) - + // Use Zarr v3 sharding via ZarrV3DatasetAttributes builder + final DatasetAttributes attributes = ZarrV3DatasetAttributes.builder(dimensionsS0, dataType) + .blockSize(shardSize) // shard dimensions + .chunkSize(blockSize) // inner chunk size within shards + .compression(compression) + .shardIndexDataCodecInfos(new Crc32cChecksumCodec()) + .build(); driverVolumeWriter.createDataset( viewIdToDataset.apply( viewId, 0 ), attributes ); } else @@ -334,14 +331,13 @@ public static MultiResolutionLevelInfo[] setupMultiResolutionPyramid( if ( useSharding ) { - final ZarrV3Compressor codec = ZarrV3Compressor.fromCompression( compression ); - final DatasetAttributes attributes = new ZarrV3DatasetAttributes( - dim, // shape - shardSize, // shard dimensions - blockSize, // inner chunk size within shards - dataType, - codec ); // compression codec - + // Use Zarr v3 sharding via ZarrV3DatasetAttributes builder + final DatasetAttributes attributes = ZarrV3DatasetAttributes.builder(dim, dataType) + .blockSize(shardSize) // shard dimensions + .chunkSize(blockSize) // inner chunk size within shards + .compression(compression) + .shardIndexDataCodecInfos(new Crc32cChecksumCodec()) + .build(); driverVolumeWriter.createDataset( datasetLevel, attributes ); } else @@ -456,7 +452,8 @@ public static MultiResolutionLevelInfo[] setupBdvDatasetsOMEZARR_ResaveRaw( final ViewId viewId, final DataType dataType, final long[] dimensions, - //final double[] resolutionS0, // TODO: this is a hack (uses 1,1,1) so the export downsampling pyramid is working + final double[] resolutionS0, + final String resolutionUnit, final Compression compression, final int[] blockSize, int[][] downsamplings, @@ -497,16 +494,13 @@ public static MultiResolutionLevelInfo[] setupBdvDatasetsOMEZARR_ResaveRaw( final Function levelToMipmapTransform = (level) -> MipmapTransforms.getMipmapTransformDefault( mrInfo[level].absoluteDownsamplingDouble() ); - // extract the resolution of the s0 export - //final VoxelDimensions vx = fusionGroup.iterator().next().getViewSetup().getVoxelSize(); - //final double[] resolutionS0 = OMEZarrAttributes.getResolutionS0( vx, anisoF, downsamplingF ); - // create metadata - final OmeNgffMultiScaleMetadata[] meta = OMEZarrAttributes.createOMEZarrMetadata( + final OmeNgffMetadata meta = OMEZarrAttributes.createOMEZarrMetadata( 5, // int n "/", // String name, I also saw "/" - new double[] { 1, 1, 1 }, //resolutionS0, // double[] resolutionS0, - "micrometer", //vx.unit() might not be OME-ZARR compatible // String unitXYZ, // e.g micrometer + "0.5", + resolutionS0, // double[] resolutionS0, + resolutionUnit, //vx.unit() might not be OME-ZARR compatible // String unitXYZ, // e.g micrometer mrInfo.length, // int numResolutionLevels, levelToName, levelToMipmapTransform ); @@ -516,7 +510,8 @@ public static MultiResolutionLevelInfo[] setupBdvDatasetsOMEZARR_ResaveRaw( //org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMetadata // for this to work you need to register an adapter in the N5Factory class // final GsonBuilder builder = new GsonBuilder().registerTypeAdapter( CoordinateTransformation.class, new CoordinateTransformationAdapter() ); - driverVolumeWriter.setAttribute( baseDataset, "multiscales", meta ); + driverVolumeWriter.setAttribute( baseDataset, "ome", meta ); + driverVolumeWriter.setAttribute( baseDataset, "ome/version", meta.version ); return mrInfo; } @@ -619,8 +614,9 @@ public static < T extends NativeType< T > & RealType< T > > void writeDownsample // For sharded datasets, we must write complete shards including empty blocks // because sparse shard reading is not fully supported yet - //N5Utils.saveNonEmptyBlock(sourceGridBlock, n5, dataset, gridBlock[2], previousScale.getType().createVariable() ); - N5Utils.saveBlock(sourceGridBlock, n5, dataset, gridBlock[2]); + // using saveNonEmptyBlock again - need to test if this is supported with sharding + N5Utils.saveNonEmptyBlock(sourceGridBlock, n5, dataset, gridBlock[2], previousScale.getType().createVariable() ); +// N5Utils.saveBlock(sourceGridBlock, n5, dataset, gridBlock[2]); } public static < T extends NativeType< T > & RealType< T > > void writeDownsampledBlock5dOMEZARR( @@ -661,15 +657,17 @@ public static < T extends NativeType< T > & RealType< T > > void writeDownsample final long[] dimensionsRaw = attrs.getDimensions(); final long[] dimensions = new long[] { dimensionsRaw[ 0 ], dimensionsRaw[ 1 ], dimensionsRaw[ 2 ] }; - final RandomAccessibleInterval< T > downsampled3d = BlockAlgoUtils.cellImg( blocks, dimensions, new int[] { 64 } ); + final int[] cellSize = new int[] { mrInfo.blockSize[0], mrInfo.blockSize[1], mrInfo.blockSize[2] }; + final RandomAccessibleInterval downsampled3d = BlockAlgoUtils.cellImg( blocks, dimensions, cellSize ); // the same information is returned no matter which index is queried in C and T final RandomAccessible< T > downsampled5d = Views.addDimension( Views.addDimension( downsampled3d ) ); final RandomAccessibleInterval sourceGridBlock = Views.offsetInterval(downsampled5d, blockOffset, blockSize); - //N5Utils.saveNonEmptyBlock(sourceGridBlock, n5, dataset, gridOffset, previousScale.getType().createVariable()); - N5Utils.saveBlock(sourceGridBlock, n5, dataset, gridOffset ); + // using saveNonEmptyBlock again - need to test if this is supported with sharding + N5Utils.saveNonEmptyBlock( sourceGridBlock, n5, dataset, gridOffset, downsampled5d.getType().createVariable() ); +// N5Utils.saveBlock(sourceGridBlock, n5, dataset, gridOffset ); } public static List assembleJobs( final MultiResolutionLevelInfo mrInfo ) @@ -686,7 +684,8 @@ public static List assembleJobs( final ViewId viewId, final MultiResolutionLevelInfo mrInfo ) { - return assembleJobs( viewId, mrInfo.dimensions, mrInfo.blockSize, mrInfo.blockSize ); + return assembleJobs( viewId, mrInfo.dimensions, mrInfo.blockSize, + mrInfo.shardSize != null ? mrInfo.shardSize : mrInfo.blockSize ); } public static List assembleJobs( @@ -809,8 +808,9 @@ public static > void resaveS0Block( } final RandomAccessibleInterval< T > sourceGridBlock = Views.offsetInterval( image, blockOffset, blockSize ); - //N5Utils.saveNonEmptyBlock( sourceGridBlock, n5, dataset, gridOffset, image.getType().createVariable() ); - N5Utils.saveBlock( sourceGridBlock, n5, dataset, gridOffset ); + // using saveNonEmptyBlock again - need to test if this is supported with sharding + N5Utils.saveNonEmptyBlock( sourceGridBlock, n5, dataset, gridOffset, image.getType().createVariable() ); +// N5Utils.saveBlock( sourceGridBlock, n5, dataset, gridOffset ); System.out.println( "ViewId " + Group.pvid( viewId ) + ", written block: offset=" + Util.printCoordinates( blockOffset ) + ", dimension=" + Util.printCoordinates( blockSize ) ); } diff --git a/src/main/java/util/URITools.java b/src/main/java/util/URITools.java index 2ab710626..24b927ae4 100644 --- a/src/main/java/util/URITools.java +++ b/src/main/java/util/URITools.java @@ -36,10 +36,10 @@ import java.io.PrintWriter; import java.net.URI; import java.net.URISyntaxException; -import java.nio.file.FileSystems; import java.util.Date; import java.util.regex.Pattern; +import com.google.gson.JsonObject; import org.janelia.saalfeldlab.googlecloud.GoogleCloudUtils; import org.janelia.saalfeldlab.n5.FileSystemKeyValueAccess; import org.janelia.saalfeldlab.n5.GsonKeyValueN5Reader; @@ -51,7 +51,6 @@ import org.janelia.saalfeldlab.n5.N5Writer; import org.janelia.saalfeldlab.n5.hdf5.N5HDF5Reader; import org.janelia.saalfeldlab.n5.hdf5.N5HDF5Writer; -import org.janelia.saalfeldlab.n5.s3.AmazonS3Utils; import org.janelia.saalfeldlab.n5.universe.N5Factory; import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; import software.amazon.awssdk.regions.Region; diff --git a/src/test/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/ExampleOMEZarrXMLGeneration.java b/src/test/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/ExampleOMEZarrXMLGeneration.java index 7b04fd473..b5b30b0f9 100644 --- a/src/test/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/ExampleOMEZarrXMLGeneration.java +++ b/src/test/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/ExampleOMEZarrXMLGeneration.java @@ -37,6 +37,7 @@ import org.janelia.saalfeldlab.n5.N5Writer; import org.janelia.saalfeldlab.n5.imglib2.N5Utils; import org.janelia.saalfeldlab.n5.universe.StorageFormat; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMetadata; import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMultiScaleMetadata; import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.coordinateTransformations.ScaleCoordinateTransformation; import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.coordinateTransformations.TranslationCoordinateTransformation; @@ -190,19 +191,19 @@ private static void createZarr( N5Writer writer, String datasetBaseName, int siz // Add OME-ZARR multiscales metadata to the container root if ( datasetBaseName.isEmpty() ) { - org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.OmeNgffMultiScaleMetadata[] metadata = - OMEZarrAttributes.createOMEZarrMetadata( + OmeNgffMetadata metadata = OMEZarrAttributes.createOMEZarrMetadata( is3D ? 3 : 4, "/", - is3D ? new double[] { voxelSizeX, voxelSizeY, voxelSizeZ } : - new double[] { voxelSizeX, voxelSizeY, voxelSizeZ, 1.0 }, + "0.4", + is3D ? new double[]{voxelSizeX, voxelSizeY, voxelSizeZ} : + new double[]{voxelSizeX, voxelSizeY, voxelSizeZ, 1.0}, "um", 1, // single resolution level - level -> String.valueOf( level ), + String::valueOf, level -> new AffineTransform3D() - ); + ); - writer.setAttribute( "/", "multiscales", metadata ); + writer.setAttribute( "/", "multiscales", metadata.multiscales ); } System.out.println( " ✓ Created " + (is3D ? "3D" : "4D") + " with dimensions: " + java.util.Arrays.toString( dimensions ) );