diff --git a/pom.xml b/pom.xml
index 048230adafa8ae31e8aae32238b32a99bf7ee8e4..2baec57550e574e3e8a79de560437ff8f04a2d3b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -197,6 +197,18 @@
<artifactId>scijava-listeners</artifactId>
<version>1.0.0-beta-2</version>
</dependency>
+
+ <dependency>
+ <groupId>org.janelia.saalfeldlab</groupId>
+ <artifactId>n5-imglib2</artifactId>
+ <version>3.4.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.janelia.saalfeldlab</groupId>
+ <artifactId>n5</artifactId>
+ <version>2.1.1</version>
+ </dependency>
+
<!-- test dependencies -->
<dependency>
<groupId>junit</groupId>
diff --git a/src/main/java/bdv/export/n5/WriteSequenceToN5.java b/src/main/java/bdv/export/n5/WriteSequenceToN5.java
new file mode 100644
index 0000000000000000000000000000000000000000..005ef0389156af8378bd3aed4a13f7088c9efed0
--- /dev/null
+++ b/src/main/java/bdv/export/n5/WriteSequenceToN5.java
@@ -0,0 +1,324 @@
+package bdv.export.n5;
+
+import bdv.export.ExportMipmapInfo;
+import bdv.export.ExportScalePyramid;
+import bdv.export.ProgressWriter;
+import bdv.export.ProgressWriterNull;
+import bdv.export.SubTaskProgressWriter;
+import bdv.export.ExportScalePyramid.AfterEachPlane;
+import bdv.export.ExportScalePyramid.LoopbackHeuristic;
+import bdv.img.cache.SimpleCacheArrayLoader;
+import bdv.img.n5.N5ImageLoader;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription;
+import mpicbg.spim.data.generic.sequence.BasicImgLoader;
+import mpicbg.spim.data.generic.sequence.BasicSetupImgLoader;
+import mpicbg.spim.data.generic.sequence.BasicViewSetup;
+import mpicbg.spim.data.sequence.TimePoint;
+import mpicbg.spim.data.sequence.ViewId;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.cache.img.ReadOnlyCachedCellImgFactory;
+import net.imglib2.img.cell.Cell;
+import net.imglib2.img.cell.CellGrid;
+import net.imglib2.type.NativeType;
+import net.imglib2.type.numeric.RealType;
+import net.imglib2.util.Cast;
+import org.janelia.saalfeldlab.n5.ByteArrayDataBlock;
+import org.janelia.saalfeldlab.n5.Compression;
+import org.janelia.saalfeldlab.n5.DataBlock;
+import org.janelia.saalfeldlab.n5.DataType;
+import org.janelia.saalfeldlab.n5.DatasetAttributes;
+import org.janelia.saalfeldlab.n5.DoubleArrayDataBlock;
+import org.janelia.saalfeldlab.n5.FloatArrayDataBlock;
+import org.janelia.saalfeldlab.n5.IntArrayDataBlock;
+import org.janelia.saalfeldlab.n5.LongArrayDataBlock;
+import org.janelia.saalfeldlab.n5.N5FSWriter;
+import org.janelia.saalfeldlab.n5.N5Writer;
+import org.janelia.saalfeldlab.n5.ShortArrayDataBlock;
+import org.janelia.saalfeldlab.n5.imglib2.N5Utils;
+
+import static bdv.img.n5.BdvN5Format.DATA_TYPE_KEY;
+import static bdv.img.n5.BdvN5Format.DOWNSAMPLING_FACTORS_KEY;
+import static bdv.img.n5.BdvN5Format.getPathName;
+import static net.imglib2.cache.img.ReadOnlyCachedCellImgOptions.options;
+
+/**
+ * @author Tobias Pietzsch
+ * @author John Bogovic
+ */
+public class WriteSequenceToN5
+{
+
+ /**
+ * Create a n5 group containing image data from all views and all
+ * timepoints in a chunked, mipmaped representation.
+ *
+ * @param seq
+ * description of the sequence to be stored as hdf5. (The
+ * {@link AbstractSequenceDescription} contains the number of
+ * setups and timepoints as well as an {@link BasicImgLoader}
+ * that provides the image data, Registration information is not
+ * needed here, that will go into the accompanying xml).
+ * @param perSetupMipmapInfo
+ * this maps from setup {@link BasicViewSetup#getId() id} to
+ * {@link ExportMipmapInfo} for that setup. The
+ * {@link ExportMipmapInfo} contains for each mipmap level, the
+ * subsampling factors and subdivision block sizes.
+ * @param compression
+ * whether to compress the data with the HDF5 DEFLATE filter.
+ * @param n5File
+ * n5 root.
+ * @param loopbackHeuristic
+ * heuristic to decide whether to create each resolution level by
+ * reading pixels from the original image or by reading back a
+ * finer resolution level already written to the hdf5. may be
+ * null (in this case always use the original image).
+ * @param afterEachPlane
+ * this is called after each "plane of chunks" is written, giving
+ * the opportunity to clear caches, etc.
+ * @param numCellCreatorThreads
+ * The number of threads that will be instantiated to generate
+ * cell data. Must be at least 1. (In addition the cell creator
+ * threads there is one writer thread that saves the generated
+ * data to HDF5.)
+ * @param progressWriter
+ * completion ratio and status output will be directed here.
+ */
+ public static void writeN5File(
+ final AbstractSequenceDescription< ?, ?, ? > seq,
+ final Map< Integer, ExportMipmapInfo > perSetupMipmapInfo,
+ final Compression compression,
+ final File n5File,
+ final LoopbackHeuristic loopbackHeuristic,
+ final AfterEachPlane afterEachPlane,
+ final int numCellCreatorThreads,
+ ProgressWriter progressWriter ) throws IOException
+ {
+ if ( progressWriter == null )
+ progressWriter = new ProgressWriterNull();
+ progressWriter.setProgress( 0 );
+
+ final BasicImgLoader imgLoader = seq.getImgLoader();
+
+ for ( final BasicViewSetup setup : seq.getViewSetupsOrdered() )
+ {
+ final Object type = imgLoader.getSetupImgLoader( setup.getId() ).getImageType();
+ if ( !( type instanceof RealType &&
+ type instanceof NativeType &&
+ N5Utils.dataType( Cast.unchecked( type ) ) != null ) )
+ throw new IllegalArgumentException( "Unsupported pixel type: " + type.getClass().getSimpleName() );
+ }
+
+ final List< Integer > timepointIds = seq.getTimePoints().getTimePointsOrdered().stream()
+ .map( TimePoint::getId )
+ .collect( Collectors.toList() );
+ final List< Integer > setupIds = seq.getViewSetupsOrdered().stream()
+ .map( BasicViewSetup::getId )
+ .collect( Collectors.toList() );
+
+ N5Writer n5 = new N5FSWriter( n5File.getAbsolutePath() );
+
+ // write Mipmap descriptions
+ for ( final int setupId : setupIds )
+ {
+ final String pathName = getPathName( setupId );
+ final int[][] downsamplingFactors = perSetupMipmapInfo.get( setupId ).getExportResolutions();
+ final DataType dataType = N5Utils.dataType( Cast.unchecked( imgLoader.getSetupImgLoader( setupId ).getImageType() ) );
+ n5.createGroup( pathName );
+ n5.setAttribute( pathName, DOWNSAMPLING_FACTORS_KEY, downsamplingFactors );
+ n5.setAttribute( pathName, DATA_TYPE_KEY, dataType );
+ }
+
+
+ // calculate number of tasks for progressWriter
+ int numTasks = 0; // first task is for writing mipmap descriptions etc...
+ for ( final int timepointIdSequence : timepointIds )
+ for ( final int setupIdSequence : setupIds )
+ if ( seq.getViewDescriptions().get( new ViewId( timepointIdSequence, setupIdSequence ) ).isPresent() )
+ numTasks++;
+ int numCompletedTasks = 0;
+
+ final ExecutorService executorService = Executors.newFixedThreadPool( numCellCreatorThreads );
+ try
+ {
+ // write image data for all views
+ final int numTimepoints = timepointIds.size();
+ int timepointIndex = 0;
+ for ( final int timepointId : timepointIds )
+ {
+ progressWriter.out().printf( "proccessing timepoint %d / %d\n", ++timepointIndex, numTimepoints );
+
+ // assemble the viewsetups that are present in this timepoint
+ final ArrayList< Integer > setupsTimePoint = new ArrayList<>();
+ for ( final int setupId : setupIds )
+ if ( seq.getViewDescriptions().get( new ViewId( timepointId, setupId ) ).isPresent() )
+ setupsTimePoint.add( setupId );
+
+ final int numSetups = setupsTimePoint.size();
+ int setupIndex = 0;
+ for ( final int setupId : setupsTimePoint )
+ {
+ progressWriter.out().printf( "proccessing setup %d / %d\n", ++setupIndex, numSetups );
+
+ final ExportMipmapInfo mipmapInfo = perSetupMipmapInfo.get( setupId );
+ final double startCompletionRatio = ( double ) numCompletedTasks++ / numTasks;
+ final double endCompletionRatio = ( double ) numCompletedTasks / numTasks;
+ final ProgressWriter subProgressWriter = new SubTaskProgressWriter( progressWriter, startCompletionRatio, endCompletionRatio );
+ writeScalePyramid(
+ n5, compression,
+ imgLoader, setupId, timepointId, mipmapInfo,
+ executorService, numCellCreatorThreads,
+ loopbackHeuristic, afterEachPlane, subProgressWriter );
+ }
+ }
+ }
+ finally
+ {
+ executorService.shutdown();
+ }
+
+ progressWriter.setProgress( 1.0 );
+ }
+
+ static < T extends RealType< T > & NativeType< T > > void writeScalePyramid(
+ final N5Writer n5,
+ final Compression compression,
+ final BasicImgLoader imgLoader,
+ final int setupId,
+ final int timepointId,
+ final ExportMipmapInfo mipmapInfo,
+ final ExecutorService executorService,
+ final int numThreads,
+ final LoopbackHeuristic loopbackHeuristic,
+ final AfterEachPlane afterEachPlane,
+ ProgressWriter progressWriter ) throws IOException
+ {
+ final BasicSetupImgLoader< T > setupImgLoader = Cast.unchecked( imgLoader.getSetupImgLoader( setupId ) );
+ final RandomAccessibleInterval< T > img = setupImgLoader.getImage( timepointId );
+ final T type = setupImgLoader.getImageType();
+ final N5DatasetIO< T > io = new N5DatasetIO<>( n5, compression, setupId, timepointId, type );
+ ExportScalePyramid.writeScalePyramid(
+ img, type, mipmapInfo, io,
+ executorService, numThreads,
+ loopbackHeuristic, afterEachPlane, progressWriter );
+ }
+
+ static class N5Dataset
+ {
+ final String pathName;
+ final DatasetAttributes attributes;
+
+ public N5Dataset( final String pathName, final DatasetAttributes attributes )
+ {
+ this.pathName = pathName;
+ this.attributes = attributes;
+ }
+ }
+
+ static class N5DatasetIO< T extends RealType< T > & NativeType< T > > implements ExportScalePyramid.DatasetIO< N5Dataset, T >
+ {
+ private final N5Writer n5;
+ private final Compression compression;
+ private final int setupId;
+ private final int timepointId;
+ private final DataType dataType;
+ private final T type;
+ private final Function< ExportScalePyramid.Block< T >, DataBlock< ? > > getDataBlock;
+
+ public N5DatasetIO( final N5Writer n5, final Compression compression, final int setupId, final int timepointId, final T type )
+ {
+ this.n5 = n5;
+ this.compression = compression;
+ this.setupId = setupId;
+ this.timepointId = timepointId;
+ this.dataType = N5Utils.dataType( type );
+ this.type = type;
+
+ switch ( dataType )
+ {
+ case UINT8:
+ getDataBlock = b -> new ByteArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) );
+ break;
+ case UINT16:
+ getDataBlock = b -> new ShortArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) );
+ break;
+ case UINT32:
+ getDataBlock = b -> new IntArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) );
+ break;
+ case UINT64:
+ getDataBlock = b -> new LongArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) );
+ break;
+ case INT8:
+ getDataBlock = b -> new ByteArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) );
+ break;
+ case INT16:
+ getDataBlock = b -> new ShortArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) );
+ break;
+ case INT32:
+ getDataBlock = b -> new IntArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) );
+ break;
+ case INT64:
+ getDataBlock = b -> new LongArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) );
+ break;
+ case FLOAT32:
+ getDataBlock = b -> new FloatArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) );
+ break;
+ case FLOAT64:
+ getDataBlock = b -> new DoubleArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) );
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ @Override
+ public N5Dataset createDataset( final int level, final long[] dimensions, final int[] blockSize ) throws IOException
+ {
+ final String pathName = getPathName( setupId, timepointId, level );
+ n5.createDataset( pathName, dimensions, blockSize, dataType, compression );
+ final DatasetAttributes attributes = n5.getDatasetAttributes( pathName );
+ return new N5Dataset( pathName, attributes );
+ }
+
+ @Override
+ public void writeBlock( final N5Dataset dataset, final ExportScalePyramid.Block< T > dataBlock ) throws IOException
+ {
+ n5.writeBlock( dataset.pathName, dataset.attributes, getDataBlock.apply( dataBlock ) );
+ }
+
+ @Override
+ public void flush( final N5Dataset dataset )
+ {}
+
+ @Override
+ public RandomAccessibleInterval< T > getImage( final int level ) throws IOException
+ {
+ final String pathName = getPathName( setupId, timepointId, level );
+ final DatasetAttributes attributes = n5.getDatasetAttributes( pathName );
+ final long[] dimensions = attributes.getDimensions();
+ final int[] cellDimensions = attributes.getBlockSize();
+ final CellGrid grid = new CellGrid( dimensions, cellDimensions );
+ final SimpleCacheArrayLoader< ? > cacheArrayLoader = N5ImageLoader.createCacheArrayLoader( n5, pathName );
+ return new ReadOnlyCachedCellImgFactory().createWithCacheLoader(
+ dimensions, type,
+ key -> {
+ final int n = grid.numDimensions();
+ final long[] cellMin = new long[ n ];
+ final int[] cellDims = new int[ n ];
+ final long[] cellGridPosition = new long[ n ];
+ grid.getCellDimensions( key, cellMin, cellDims );
+ grid.getCellGridPositionFlat( key, cellGridPosition );
+ return new Cell<>( cellDims, cellMin, cacheArrayLoader.loadArray( cellGridPosition ) );
+ },
+ options().cellDimensions( cellDimensions ) );
+ }
+ }
+}
diff --git a/src/main/java/bdv/img/n5/BdvN5Format.java b/src/main/java/bdv/img/n5/BdvN5Format.java
new file mode 100644
index 0000000000000000000000000000000000000000..f4d4d69b5b833646a61682253f52ee583d36b420
--- /dev/null
+++ b/src/main/java/bdv/img/n5/BdvN5Format.java
@@ -0,0 +1,22 @@
+package bdv.img.n5;
+
+public class BdvN5Format
+{
+ public static final String DOWNSAMPLING_FACTORS_KEY = "downsamplingFactors";
+ public static final String DATA_TYPE_KEY = "dataType";
+
+ public static String getPathName( final int setupId )
+ {
+ return String.format( "setup%02d", setupId );
+ }
+
+ public static String getPathName( final int setupId, final int timepointId )
+ {
+ return String.format( "setup%02d/timepoint%05d", setupId, timepointId );
+ }
+
+ public static String getPathName( final int setupId, final int timepointId, final int level )
+ {
+ return String.format( "setup%02d/timepoint%05d/s%d", setupId, timepointId, level );
+ }
+}
diff --git a/src/main/java/bdv/img/n5/N5ImageLoader.java b/src/main/java/bdv/img/n5/N5ImageLoader.java
new file mode 100644
index 0000000000000000000000000000000000000000..d3031f626c404f20f973bf14941299ac1fa173ee
--- /dev/null
+++ b/src/main/java/bdv/img/n5/N5ImageLoader.java
@@ -0,0 +1,391 @@
+/*
+ * #%L
+ * BigDataViewer core classes with minimal dependencies
+ * %%
+ * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch,
+ * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package bdv.img.n5;
+
+import bdv.AbstractViewerSetupImgLoader;
+import bdv.ViewerImgLoader;
+import bdv.cache.CacheControl;
+import bdv.img.cache.SimpleCacheArrayLoader;
+import bdv.img.cache.VolatileGlobalCellCache;
+import bdv.util.ConstantRandomAccessible;
+import bdv.util.MipmapTransforms;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.function.Function;
+import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription;
+import mpicbg.spim.data.generic.sequence.BasicViewSetup;
+import mpicbg.spim.data.generic.sequence.ImgLoaderHint;
+import mpicbg.spim.data.sequence.MultiResolutionImgLoader;
+import mpicbg.spim.data.sequence.MultiResolutionSetupImgLoader;
+import mpicbg.spim.data.sequence.VoxelDimensions;
+import net.imglib2.Dimensions;
+import net.imglib2.FinalDimensions;
+import net.imglib2.FinalInterval;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.Volatile;
+import net.imglib2.cache.queue.BlockingFetchQueues;
+import net.imglib2.cache.queue.FetcherThreads;
+import net.imglib2.cache.volatiles.CacheHints;
+import net.imglib2.cache.volatiles.LoadingStrategy;
+import net.imglib2.img.basictypeaccess.volatiles.array.VolatileByteArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.VolatileDoubleArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.VolatileFloatArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.VolatileIntArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.VolatileLongArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.VolatileShortArray;
+import net.imglib2.img.cell.CellGrid;
+import net.imglib2.img.cell.CellImg;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.NativeType;
+import net.imglib2.type.numeric.integer.ByteType;
+import net.imglib2.type.numeric.integer.IntType;
+import net.imglib2.type.numeric.integer.LongType;
+import net.imglib2.type.numeric.integer.ShortType;
+import net.imglib2.type.numeric.integer.UnsignedByteType;
+import net.imglib2.type.numeric.integer.UnsignedIntType;
+import net.imglib2.type.numeric.integer.UnsignedLongType;
+import net.imglib2.type.numeric.integer.UnsignedShortType;
+import net.imglib2.type.numeric.real.DoubleType;
+import net.imglib2.type.numeric.real.FloatType;
+import net.imglib2.type.volatiles.VolatileByteType;
+import net.imglib2.type.volatiles.VolatileDoubleType;
+import net.imglib2.type.volatiles.VolatileFloatType;
+import net.imglib2.type.volatiles.VolatileIntType;
+import net.imglib2.type.volatiles.VolatileLongType;
+import net.imglib2.type.volatiles.VolatileShortType;
+import net.imglib2.type.volatiles.VolatileUnsignedByteType;
+import net.imglib2.type.volatiles.VolatileUnsignedIntType;
+import net.imglib2.type.volatiles.VolatileUnsignedLongType;
+import net.imglib2.type.volatiles.VolatileUnsignedShortType;
+import net.imglib2.util.Cast;
+import net.imglib2.view.Views;
+import org.janelia.saalfeldlab.n5.DataBlock;
+import org.janelia.saalfeldlab.n5.DataType;
+import org.janelia.saalfeldlab.n5.DatasetAttributes;
+import org.janelia.saalfeldlab.n5.N5FSReader;
+import org.janelia.saalfeldlab.n5.N5Reader;
+
+import static bdv.img.n5.BdvN5Format.DATA_TYPE_KEY;
+import static bdv.img.n5.BdvN5Format.DOWNSAMPLING_FACTORS_KEY;
+import static bdv.img.n5.BdvN5Format.getPathName;
+
+public class N5ImageLoader implements ViewerImgLoader, MultiResolutionImgLoader
+{
+ private final File n5File;
+
+ // TODO: it would be good if this would not be needed
+ // find available setups from the n5
+ private final AbstractSequenceDescription< ?, ?, ? > seq;
+
+ /**
+ * Maps setup id to {@link SetupImgLoader}.
+ */
+ private final Map< Integer, SetupImgLoader > setupImgLoaders = new HashMap<>();
+
+ public N5ImageLoader( final File n5File, final AbstractSequenceDescription< ?, ?, ? > sequenceDescription )
+ {
+ this.n5File = n5File;
+ this.seq = sequenceDescription;
+ }
+
+ public File getN5File()
+ {
+ return n5File;
+ }
+
+ private volatile boolean isOpen = false;
+ private FetcherThreads fetchers;
+ private VolatileGlobalCellCache cache;
+ private N5Reader n5;
+
+ private void open()
+ {
+ if ( !isOpen )
+ {
+ synchronized ( this )
+ {
+ if ( isOpen )
+ return;
+
+ try
+ {
+ this.n5 = new N5FSReader( n5File.getAbsolutePath() );
+
+ int maxNumLevels = 0;
+ final List< ? extends BasicViewSetup > setups = seq.getViewSetupsOrdered();
+ for ( final BasicViewSetup setup : setups )
+ {
+ final int setupId = setup.getId();
+ final SetupImgLoader setupImgLoader = createSetupImgLoader( setupId );
+ setupImgLoaders.put( setupId, setupImgLoader );
+ maxNumLevels = Math.max( maxNumLevels, setupImgLoader.numMipmapLevels() );
+ }
+
+ final int numFetcherThreads = 1;
+ final BlockingFetchQueues< Callable< ? > > queue = new BlockingFetchQueues<>( maxNumLevels, numFetcherThreads );
+ fetchers = new FetcherThreads( queue, numFetcherThreads );
+ cache = new VolatileGlobalCellCache( queue );
+ }
+ catch ( IOException e )
+ {
+ throw new RuntimeException( e );
+ }
+
+ isOpen = true;
+ }
+ }
+ }
+
+ /**
+ * Clear the cache. Images that were obtained from
+ * this loader before {@link #close()} will stop working. Requesting images
+ * after {@link #close()} will cause the n5 to be reopened (with a
+ * new cache).
+ */
+ public void close()
+ {
+ if ( isOpen )
+ {
+ synchronized ( this )
+ {
+ if ( !isOpen )
+ return;
+ fetchers.shutdown();
+ cache.clearCache();
+ isOpen = false;
+ }
+ }
+ }
+
+ @Override
+ public SetupImgLoader getSetupImgLoader( final int setupId )
+ {
+ open();
+ return setupImgLoaders.get( setupId );
+ }
+
+ private < T extends NativeType< T >, V extends Volatile< T > & NativeType< V > > SetupImgLoader< T, V > createSetupImgLoader( final int setupId ) throws IOException
+ {
+ final String pathName = getPathName( setupId );
+ final DataType dataType = n5.getAttribute( pathName, DATA_TYPE_KEY, DataType.class );
+ switch ( dataType )
+ {
+ case UINT8:
+ return Cast.unchecked( new SetupImgLoader<>( setupId, new UnsignedByteType(), new VolatileUnsignedByteType() ) );
+ case UINT16:
+ return Cast.unchecked( new SetupImgLoader<>( setupId, new UnsignedShortType(), new VolatileUnsignedShortType() ) );
+ case UINT32:
+ return Cast.unchecked( new SetupImgLoader<>( setupId, new UnsignedIntType(), new VolatileUnsignedIntType() ) );
+ case UINT64:
+ return Cast.unchecked( new SetupImgLoader<>( setupId, new UnsignedLongType(), new VolatileUnsignedLongType() ) );
+ case INT8:
+ return Cast.unchecked( new SetupImgLoader<>( setupId, new ByteType(), new VolatileByteType() ) );
+ case INT16:
+ return Cast.unchecked( new SetupImgLoader<>( setupId, new ShortType(), new VolatileShortType() ) );
+ case INT32:
+ return Cast.unchecked( new SetupImgLoader<>( setupId, new IntType(), new VolatileIntType() ) );
+ case INT64:
+ return Cast.unchecked( new SetupImgLoader<>( setupId, new LongType(), new VolatileLongType() ) );
+ case FLOAT32:
+ return Cast.unchecked( new SetupImgLoader<>( setupId, new FloatType(), new VolatileFloatType() ) );
+ case FLOAT64:
+ return Cast.unchecked( new SetupImgLoader<>( setupId, new DoubleType(), new VolatileDoubleType() ) );
+ }
+ return null;
+ }
+
+ @Override
+ public CacheControl getCacheControl()
+ {
+ open();
+ return cache;
+ }
+
+ public class SetupImgLoader< T extends NativeType< T >, V extends Volatile< T > & NativeType< V > >
+ extends AbstractViewerSetupImgLoader< T, V >
+ implements MultiResolutionSetupImgLoader< T >
+ {
+ private final int setupId;
+
+ private final double[][] mipmapResolutions;
+
+ private final AffineTransform3D[] mipmapTransforms;
+
+ public SetupImgLoader( final int setupId, final T type, final V volatileType ) throws IOException
+ {
+ super( type, volatileType );
+ this.setupId = setupId;
+ final String pathName = getPathName( setupId );
+ mipmapResolutions = n5.getAttribute( pathName, DOWNSAMPLING_FACTORS_KEY, double[][].class );
+ mipmapTransforms = new AffineTransform3D[ mipmapResolutions.length ];
+ for ( int level = 0; level < mipmapResolutions.length; level++ )
+ mipmapTransforms[ level ] = MipmapTransforms.getMipmapTransformDefault( mipmapResolutions[ level ] );
+ }
+
+ @Override
+ public RandomAccessibleInterval< V > getVolatileImage( final int timepointId, final int level, final ImgLoaderHint... hints )
+ {
+ return prepareCachedImage( timepointId, level, LoadingStrategy.BUDGETED, volatileType );
+ }
+
+ @Override
+ public RandomAccessibleInterval< T > getImage( final int timepointId, final int level, final ImgLoaderHint... hints )
+ {
+ return prepareCachedImage( timepointId, level, LoadingStrategy.BLOCKING, type );
+ }
+
+ @Override
+ public Dimensions getImageSize( final int timepointId, final int level )
+ {
+ try
+ {
+ final String pathName = getPathName( setupId, timepointId, level );
+ final DatasetAttributes attributes = n5.getDatasetAttributes( pathName );
+ return new FinalDimensions( attributes.getDimensions() );
+ }
+ catch( Exception e )
+ {
+ return null;
+ }
+ }
+
+ @Override
+ public double[][] getMipmapResolutions()
+ {
+ return mipmapResolutions;
+ }
+
+ @Override
+ public AffineTransform3D[] getMipmapTransforms()
+ {
+ return mipmapTransforms;
+ }
+
+ @Override
+ public int numMipmapLevels()
+ {
+ return mipmapResolutions.length;
+ }
+
+ @Override
+ public VoxelDimensions getVoxelSize( final int timepointId )
+ {
+ return null;
+ }
+
+ /**
+ * Create a {@link CellImg} backed by the cache.
+ */
+ private < T extends NativeType< T > > RandomAccessibleInterval< T > prepareCachedImage( final int timepointId, final int level, final LoadingStrategy loadingStrategy, final T type )
+ {
+ try
+ {
+ final String pathName = getPathName( setupId, timepointId, level );
+ final DatasetAttributes attributes = n5.getDatasetAttributes( pathName );
+ final long[] dimensions = attributes.getDimensions();
+ final int[] cellDimensions = attributes.getBlockSize();
+ final CellGrid grid = new CellGrid( dimensions, cellDimensions );
+
+ final int priority = numMipmapLevels() - 1 - level;
+ final CacheHints cacheHints = new CacheHints( loadingStrategy, priority, false );
+
+ final SimpleCacheArrayLoader< ? > loader = createCacheArrayLoader( n5, pathName );
+ return cache.createImg( grid, timepointId, setupId, level, cacheHints, loader, type );
+ }
+ catch ( IOException e )
+ {
+ System.err.println( String.format(
+ "image data for timepoint %d setup %d level %d could not be found.",
+ timepointId, setupId, level ) );
+ return Views.interval(
+ new ConstantRandomAccessible<>( type.createVariable(), 3 ),
+ new FinalInterval( 1, 1, 1 ) );
+ }
+ }
+ }
+
+ private static class N5CacheArrayLoader< A > implements SimpleCacheArrayLoader< A >
+ {
+ private final N5Reader n5;
+ private final String pathName;
+ private final DatasetAttributes attributes;
+ private final Function< DataBlock< ? >, A > createArray;
+
+ N5CacheArrayLoader( final N5Reader n5, final String pathName, final DatasetAttributes attributes, final Function< DataBlock< ? >, A > createArray )
+ {
+ this.n5 = n5;
+ this.pathName = pathName;
+ this.attributes = attributes;
+ this.createArray = createArray;
+ }
+
+ @Override
+ public A loadArray( final long[] gridPosition ) throws IOException
+ {
+ return createArray.apply( n5.readBlock( pathName, attributes, gridPosition ) );
+ }
+ }
+
+ public static SimpleCacheArrayLoader< ? > createCacheArrayLoader( final N5Reader n5, final String pathName ) throws IOException
+ {
+ final DatasetAttributes attributes = n5.getDatasetAttributes( pathName );
+ switch ( attributes.getDataType() )
+ {
+ case UINT8:
+ case INT8:
+ return new N5CacheArrayLoader<>( n5, pathName, attributes,
+ dataBlock -> new VolatileByteArray( Cast.unchecked( dataBlock.getData() ), true ) );
+ case UINT16:
+ case INT16:
+ return new N5CacheArrayLoader<>( n5, pathName, attributes,
+ dataBlock -> new VolatileShortArray( Cast.unchecked( dataBlock.getData() ), true ) );
+ case UINT32:
+ case INT32:
+ return new N5CacheArrayLoader<>( n5, pathName, attributes,
+ dataBlock -> new VolatileIntArray( Cast.unchecked( dataBlock.getData() ), true ) );
+ case UINT64:
+ case INT64:
+ return new N5CacheArrayLoader<>( n5, pathName, attributes,
+ dataBlock -> new VolatileLongArray( Cast.unchecked( dataBlock.getData() ), true ) );
+ case FLOAT32:
+ return new N5CacheArrayLoader<>( n5, pathName, attributes,
+ dataBlock -> new VolatileFloatArray( Cast.unchecked( dataBlock.getData() ), true ) );
+ case FLOAT64:
+ return new N5CacheArrayLoader<>( n5, pathName, attributes,
+ dataBlock -> new VolatileDoubleArray( Cast.unchecked( dataBlock.getData() ), true ) );
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+}
diff --git a/src/main/java/bdv/img/n5/XmlIoN5ImageLoader.java b/src/main/java/bdv/img/n5/XmlIoN5ImageLoader.java
new file mode 100644
index 0000000000000000000000000000000000000000..10e40feb68c4cce6a028e684a168a189df97c7d9
--- /dev/null
+++ b/src/main/java/bdv/img/n5/XmlIoN5ImageLoader.java
@@ -0,0 +1,62 @@
+/*
+ * #%L
+ * BigDataViewer core classes with minimal dependencies
+ * %%
+ * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch,
+ * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package bdv.img.n5;
+
+import java.io.File;
+import mpicbg.spim.data.XmlHelpers;
+import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription;
+import mpicbg.spim.data.generic.sequence.ImgLoaderIo;
+import mpicbg.spim.data.generic.sequence.XmlIoBasicImgLoader;
+import org.jdom2.Element;
+
+import static mpicbg.spim.data.XmlHelpers.loadPath;
+import static mpicbg.spim.data.XmlKeys.IMGLOADER_FORMAT_ATTRIBUTE_NAME;
+
+@ImgLoaderIo( format = "bdv.n5", type = N5ImageLoader.class )
+public class XmlIoN5ImageLoader implements XmlIoBasicImgLoader< N5ImageLoader >
+{
+ @Override
+ public Element toXml( final N5ImageLoader imgLoader, final File basePath )
+ {
+ final Element elem = new Element( "ImageLoader" );
+ elem.setAttribute( IMGLOADER_FORMAT_ATTRIBUTE_NAME, "bdv.n5" );
+ elem.setAttribute( "version", "1.0" );
+ elem.addContent( XmlHelpers.pathElement( "n5", imgLoader.getN5File(), basePath ) );
+ return elem;
+ }
+
+ @Override
+ public N5ImageLoader fromXml( final Element elem, final File basePath, final AbstractSequenceDescription< ?, ?, ? > sequenceDescription )
+ {
+// final String version = elem.getAttributeValue( "version" );
+ final File path = loadPath( elem, "n5", basePath );
+ return new N5ImageLoader( path, sequenceDescription );
+ }
+}