diff --git a/src/main/java/bdv/img/catmaid/TiledImageLoader.java b/src/main/java/bdv/img/catmaid/TiledImageLoader.java
new file mode 100644
index 0000000000000000000000000000000000000000..c7bdb703e771f6d9dc3dd2fd888886127ffe69e2
--- /dev/null
+++ b/src/main/java/bdv/img/catmaid/TiledImageLoader.java
@@ -0,0 +1,352 @@
+/*
+ * #%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.catmaid;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.List;
+
+import bdv.AbstractViewerSetupImgLoader;
+import bdv.ViewerImgLoader;
+import bdv.ViewerSetupImgLoader;
+import bdv.cache.CacheHints;
+import bdv.cache.LoadingStrategy;
+import bdv.img.cache.CacheArrayLoader;
+import bdv.img.cache.CachedCellImg;
+import bdv.img.cache.VolatileGlobalCellCache;
+import bdv.img.cache.VolatileImgCells;
+import bdv.img.cache.VolatileImgCells.CellCache;
+import mpicbg.spim.data.generic.sequence.ImgLoaderHint;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.Volatile;
+import net.imglib2.img.NativeImg;
+import net.imglib2.img.basictypeaccess.volatiles.VolatileAccess;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.NativeType;
+import net.imglib2.util.Fraction;
+
+public class TiledImageLoader< A extends VolatileAccess, T extends NativeType< T >, V extends Volatile< T > & NativeType< V > > implements ViewerImgLoader
+{
+	protected final int numScales;
+
+	protected final double[][] mipmapResolutions;
+
+	protected final AffineTransform3D[] mipmapTransforms;
+
+	protected final long[][] imageDimensions;
+
+	protected final int[][] blockDimensions;
+
+	protected VolatileGlobalCellCache cache;
+
+	final protected HashMap< Integer, TiledSetupImageLoader > setupLoaders = new HashMap<>();
+
+	final static private int[][] blockDimensions(
+			final int tileWidth,
+			final int tileHeight,
+			final int numScales )
+	{
+		final int[][] blockDimensions = new int[ numScales ][];
+		for ( int i = 0; i < numScales; ++i )
+			blockDimensions[ i ] = new int[]{ tileWidth, tileHeight, 1 };
+
+		return blockDimensions;
+	}
+
+	public TiledImageLoader(
+			final List< CacheArrayLoader< A > > loaders,
+			final T type,
+			final V vType,
+			final long width,
+			final long height,
+			final long depth,
+			final double zScale,
+			final int tileWidth,
+			final int tileHeight,
+			final int[][] blockDimensions,
+			final boolean topLeft )
+	{
+		this.numScales = blockDimensions.length;
+
+		mipmapResolutions = new double[ numScales ][];
+		imageDimensions = new long[ numScales ][];
+		mipmapTransforms = new AffineTransform3D[ numScales ];
+		final int[] zScales = new int[ numScales ];
+		this.blockDimensions = new int[ numScales ][];
+		for ( int l = 0; l < numScales; ++l )
+		{
+			final int sixy = 1 << l;
+			final int siz = Math.max( 1, ( int )Math.round( sixy / zScale ) );
+
+			mipmapResolutions[ l ] = new double[] { sixy, sixy, siz };
+			imageDimensions[ l ] = new long[] { width >> l, height >> l, depth / siz };
+			this.blockDimensions[ l ] = blockDimensions[ l ].clone();
+			zScales[ l ] = siz;
+
+			final AffineTransform3D mipmapTransform = new AffineTransform3D();
+
+			mipmapTransform.set( sixy, 0, 0 );
+			mipmapTransform.set( sixy, 1, 1 );
+			mipmapTransform.set( zScale * siz, 2, 2 );
+
+			if ( topLeft )
+			{
+				mipmapTransform.set( 0.5 * ( sixy - 1 ), 0, 3 );
+				mipmapTransform.set( 0.5 * ( sixy - 1 ), 1, 3 );
+			}
+			mipmapTransform.set( 0.5 * ( zScale * siz - 1 ), 2, 3 );
+
+			mipmapTransforms[ l ] = mipmapTransform;
+		}
+
+		for ( int i = 0; i < loaders.size(); ++i )
+			setupLoaders.put( i, new TiledSetupImageLoader( loaders.get( i ), type, vType, i ) );
+
+		cache = new VolatileGlobalCellCache( numScales, 10 );
+	}
+
+	public TiledImageLoader(
+			final List< CacheArrayLoader< A > > loaders,
+			final T type,
+			final V vType,
+			final long width,
+			final long height,
+			final long depth,
+			final double zScale,
+			final int tileWidth,
+			final int tileHeight,
+			final int[][] blockDimensions )
+	{
+		this( loaders, type, vType, width, height, depth, zScale, tileWidth, tileHeight, blockDimensions, true );
+	}
+
+	public TiledImageLoader(
+			final List< CacheArrayLoader< A > > loaders,
+			final T type,
+			final V vType,
+			final long width,
+			final long height,
+			final long depth,
+			final double zScale,
+			final int numScales,
+			final int tileWidth,
+			final int tileHeight,
+			final int blockWidth,
+			final int blockHeight,
+			final boolean topLeft )
+	{
+		this( loaders, type, vType, width, height, depth, zScale, tileWidth, tileHeight, blockDimensions( blockWidth, blockHeight, numScales ), topLeft );
+	}
+
+	public TiledImageLoader(
+			final List< CacheArrayLoader< A > > loaders,
+			final T type,
+			final V vType,
+			final long width,
+			final long height,
+			final long depth,
+			final double zScale,
+			final int numScales,
+			final int tileWidth,
+			final int tileHeight,
+			final int blockWidth,
+			final int blockHeight )
+	{
+		this( loaders, type, vType, width, height, depth, zScale, tileWidth, tileHeight, blockDimensions( blockWidth, blockHeight, numScales ), true );
+	}
+
+	public TiledImageLoader(
+			final List< CacheArrayLoader< A > > loaders,
+			final T type,
+			final V vType,
+			final long width,
+			final long height,
+			final long depth,
+			final double zScale,
+			final int numScales,
+			final int tileWidth,
+			final int tileHeight,
+			final boolean topLeft )
+	{
+		this( loaders, type, vType, width, height, depth, zScale, numScales, tileWidth, tileHeight, tileWidth, tileHeight, topLeft );
+	}
+
+	public TiledImageLoader(
+			final List< CacheArrayLoader< A > > loaders,
+			final T type,
+			final V vType,
+			final long width,
+			final long height,
+			final long depth,
+			final double zScale,
+			final int numScales,
+			final int tileWidth,
+			final int tileHeight )
+	{
+		this( loaders, type, vType, width, height, depth, zScale, numScales, tileWidth, tileHeight, true );
+	}
+
+	final static public int getNumScales( long width, long height, final long tileWidth, final long tileHeight )
+	{
+		int i = 1;
+
+		while ( ( width >>= 1 ) > tileWidth && ( height >>= 1 ) > tileHeight )
+			++i;
+
+		return i;
+	}
+
+	/**
+	 * (Almost) create a {@link CachedCellImg} backed by the cache.
+	 * The created image needs a {@link NativeImg#setLinkedType(net.imglib2.type.Type) linked type} before it can be used.
+	 */
+	protected < N extends NativeType< N > > CachedCellImg< N, A > prepareCachedImage(
+			final CacheArrayLoader< A > loader,
+			final int timepointId,
+			final int setupId,
+			final int level,
+			final LoadingStrategy loadingStrategy )
+	{
+		final long[] dimensions = imageDimensions[ level ];
+
+		final int priority = numScales - 1 - level;
+		final CacheHints cacheHints = new CacheHints( loadingStrategy, priority, false );
+		final CellCache< A > c = cache.new VolatileCellCache<>( timepointId, setupId, level, cacheHints, loader );
+		final VolatileImgCells< A > cells = new VolatileImgCells<>( c, new Fraction(), dimensions, blockDimensions[ level ] );
+		final CachedCellImg< N, A > img = new CachedCellImg<>( cells );
+		return img;
+	}
+
+	@Override
+	public VolatileGlobalCellCache getCacheControl()
+	{
+		return cache;
+	}
+
+	@Override
+	public ViewerSetupImgLoader< ?, ? > getSetupImgLoader( final int setupId )
+	{
+		return setupLoaders.get( setupId );
+	}
+
+	/**
+	 * Reflection hack because there is no T NativeType<T>.create(NativeImg<?, A>) method in ImgLib2
+	 * Note that for this method to be introduced, NativeType would need an additional generic parameter A
+	 * that specifies the accepted family of access objects that can be used in the NativeImg... big change
+	 *
+	 * @throws SecurityException
+	 * @throws NoSuchMethodException
+	 * @throws InvocationTargetException
+	 * @throws IllegalArgumentException
+	 * @throws IllegalAccessException
+	 * @throws InstantiationException
+	 */
+	@SuppressWarnings( { "rawtypes", "unchecked" } )
+	protected void linkType( final NativeType t, final CachedCellImg img ) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
+	{
+		final Constructor constructor = t.getClass().getDeclaredConstructor( NativeImg.class );
+		if ( constructor != null )
+		{
+			final NativeType linkedType = ( NativeType )constructor.newInstance( img );
+			img.setLinkedType( linkedType );
+		}
+	}
+
+	public void setCache( final VolatileGlobalCellCache cache )
+	{
+		this.cache = cache;
+	}
+
+	public class TiledSetupImageLoader extends AbstractViewerSetupImgLoader< T, V >
+	{
+		final protected int setupId;
+
+		final protected CacheArrayLoader< A > loader;
+
+		public TiledSetupImageLoader( final CacheArrayLoader< A > loader, final T type, final V volatileType, final int setupId )
+		{
+			super( type, volatileType );
+
+			this.loader = loader;
+			this.setupId = setupId;
+		}
+
+		@Override
+		public RandomAccessibleInterval< T > getImage( final int timepointId, final int level, final ImgLoaderHint... hints )
+		{
+			try
+			{
+				final CachedCellImg< T, A >  img = prepareCachedImage( loader, timepointId, setupId, level, LoadingStrategy.BLOCKING );
+				linkType( type, img );
+				return img;
+			}
+			catch ( final Exception e )
+			{
+				e.printStackTrace( System.err );
+				return null;
+			}
+		}
+
+		@Override
+		public RandomAccessibleInterval< V > getVolatileImage( final int timepointId, final int level, final ImgLoaderHint... hints )
+		{
+			try
+			{
+				final CachedCellImg< V, A > img = prepareCachedImage( loader, timepointId, setupId, level, LoadingStrategy.VOLATILE );
+				linkType( volatileType, img );
+				return img;
+			}
+			catch ( final Exception e )
+			{
+				e.printStackTrace( System.err );
+				return null;
+			}
+		}
+
+		@Override
+		public double[][] getMipmapResolutions()
+		{
+			return mipmapResolutions;
+		}
+
+		@Override
+		public AffineTransform3D[] getMipmapTransforms()
+		{
+			return mipmapTransforms;
+		}
+
+		@Override
+		public int numMipmapLevels()
+		{
+			return numScales;
+		}
+	}
+}
diff --git a/src/main/java/bdv/img/catmaid/VolatileFloatArrayTileLoader.java b/src/main/java/bdv/img/catmaid/VolatileFloatArrayTileLoader.java
new file mode 100644
index 0000000000000000000000000000000000000000..d999735e4ab53bbe35b1a5241abf1250c66388e9
--- /dev/null
+++ b/src/main/java/bdv/img/catmaid/VolatileFloatArrayTileLoader.java
@@ -0,0 +1,227 @@
+/*
+ * #%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.catmaid;
+
+import bdv.img.cache.CacheArrayLoader;
+import ij.ImagePlus;
+import ij.io.Opener;
+import ij.process.Blitter;
+import ij.process.FloatProcessor;
+import net.imglib2.img.basictypeaccess.volatiles.array.VolatileFloatArray;
+
+public class VolatileFloatArrayTileLoader implements CacheArrayLoader< VolatileFloatArray >
+{
+	private VolatileFloatArray theEmptyArray;
+
+	private final String urlFormat;
+
+	private final int tileWidth;
+
+	private final int tileHeight;
+
+	final private int[] zScales;
+
+	/**
+	 * <p>Create a {@link CacheArrayLoader} for a tiled image source.  Tiles are
+	 * addressed, in this order, by their</p>
+	 * <ul>
+	 * <li>scale level,</li>
+	 * <li>scale,</li>
+	 * <li>x,</li>
+	 * <li>y,</li>
+	 * <li>z,</li>
+	 * <li>tile width,</li>
+	 * <li>tile height,</li>
+	 * <li>tile row, and</li>
+	 * <li>tile column.</li>
+	 * </ul>
+	 * <p><code>urlFormat</code> specifies how these parameters are used
+	 * to generate a URL referencing the tile.  Examples:</p>
+	 *
+	 * <dl>
+	 * <dt>"http://catmaid.org/my-data/xy/%5$d/%8$d_%9$d_%1$d.jpg"</dt>
+	 * <dd>CATMAID DefaultTileSource (type 1)</dd>
+	 * <dt>"http://catmaid.org/my-data/xy/?x=%3$d&amp;y=%4$d&amp;width=%6d&amp;height=%7$d&amp;row=%8$d&amp;col=%9$d&amp;scale=%2$f&amp;z=%4$d"</dt>
+     * <dd>CATMAID RequestTileSource (type 2)</dd>
+	 * <dt>"http://catmaid.org/my-data/xy/%1$d/%5$d/%8$d/%9$d.jpg"</dt>
+	 * <dd>CATMAID LargeDataTileSource (type 5)</dd>
+	 * </dl>
+	 *
+	 * @param urlFormat
+	 * @param tileWidth
+	 * @param tileHeight
+	 */
+	public VolatileFloatArrayTileLoader( final String urlFormat, final int tileWidth, final int tileHeight, final int[] zScales )
+	{
+		theEmptyArray = new VolatileFloatArray( tileWidth * tileHeight, false );
+		this.urlFormat = urlFormat;
+		this.tileWidth = tileWidth;
+		this.tileHeight = tileHeight;
+		this.zScales = zScales;
+	}
+
+	@Override
+	public int getBytesPerElement()
+	{
+		return 4;
+	}
+
+	final private void loadSliceArray(
+			final float[] slice,
+			final int level,
+			final double scale,
+			final long c0,
+			final long r0,
+			final long x0,
+			final long y0,
+			final long z,
+			final long xm,
+			final long ym,
+			final long[] min,
+			final int w,
+			final int h ) throws InterruptedException
+	{
+		final FloatProcessor ip = new FloatProcessor( w, h, slice );
+
+		for (
+				long c = c0, x = x0;
+				x < xm;
+				++c, x += tileWidth )
+		{
+			for (
+					long r = r0, y = y0;
+					y < ym;
+					++r, y += tileHeight )
+			{
+				final String urlString = String.format( urlFormat, level, scale, x, y, z, tileWidth, tileHeight, r, c );
+//				System.out.println( "r=" + r + " c=" + c + " : " + urlString );
+				final ImagePlus tile;
+				// ij.io.Opener cannot handle "file:" URLs...
+				if ( urlString.startsWith( "file:" ) )
+					tile = new Opener().openImage( urlString.substring( 5 ) );
+				else
+					tile = new Opener().openURL( urlString );
+				if ( tile == null )
+					System.out.println( "failed loading r=" + r + " c=" + c );
+				else
+				{
+					final FloatProcessor tp = tile.getProcessor().convertToFloatProcessor();
+					ip.copyBits( tp, ( int )( x - min[ 0 ] ), ( int )( y - min[ 1 ] ), Blitter.COPY );
+				}
+			}
+		}
+	}
+
+
+	final private void averageSlice(
+			final float[] slice,
+			final int level,
+			final double scale,
+			final long c0,
+			final long r0,
+			final long x0,
+			final long y0,
+			final long xm,
+			final long ym,
+			final long[] min,
+			final int w,
+			final int h ) throws InterruptedException
+	{
+		final double[] fs = new double[ slice.length ];
+		for ( int z = ( int ) min[ 2 ] * zScales[ level ], dz = 0; dz < zScales[ level ]; ++dz )
+		{
+			loadSliceArray( slice, level, scale, c0, r0, x0, y0, z + dz, xm, ym, min, w, h );
+			for ( int i = 0; i < slice.length; ++i )
+				fs[ i ] += slice[ i ];
+		}
+		for ( int i = 0; i < slice.length; ++i )
+			slice[ i ] = ( float )fs[ i ];
+	}
+
+
+	@Override
+	public VolatileFloatArray loadArray(
+			 final int timepoint,
+			 final int setup,
+			 final int level,
+			 final int[] dimensions,
+			 final long[] min ) throws InterruptedException
+	{
+		final int w = dimensions[ 0 ];
+		final int h = dimensions[ 1 ];
+		final long xm = min[ 0 ] + w;
+		final long ym = min[ 1 ] + h;
+		final double scale = 1.0 / Math.pow(2.0, level);
+		final float[] slice = new float[ w * h ];
+
+		final long c0 = min[ 0 ] / tileWidth;
+		final long r0 = min[ 1 ] / tileHeight;
+		final long x0 = c0 * tileWidth;
+		final long y0 = r0 * tileHeight;
+
+		final float[] data;
+		if ( dimensions[ 2 ] > 1 )
+		{
+			data = new float[ w * h * dimensions[ 2 ] ];
+			final long[] zMin = min.clone();
+			for ( int z = 0; z < dimensions[ 2 ]; ++z )
+			{
+				zMin[ 2 ] = min[ 2 ] + z;
+				if ( zScales[ level ] > 1 )
+					averageSlice( slice, level, scale, c0, r0, x0, y0, xm, ym, zMin, w, h );
+				else
+					loadSliceArray( slice, level, scale, c0, r0, x0, y0, zMin[ 2 ], xm, ym, zMin, w, h );
+
+				System.arraycopy( slice, 0, data, z * slice.length, slice.length );
+			}
+		}
+		else
+		{
+			data = slice;
+			if ( zScales[ level ] > 1 )
+				averageSlice( slice, level, scale, c0, r0, x0, y0, xm, ym, min, w, h );
+			else
+				loadSliceArray( slice, level, scale, c0, r0, x0, y0, min[ 2 ], xm, ym, min, w, h );
+		}
+
+		return new VolatileFloatArray( data, true );
+	}
+
+	@Override
+	public VolatileFloatArray emptyArray( final int[] dimensions )
+	{
+		int numEntities = 1;
+		for ( int i = 0; i < dimensions.length; ++i )
+			numEntities *= dimensions[ i ];
+		if ( theEmptyArray.getCurrentStorageArray().length < numEntities )
+			theEmptyArray = new VolatileFloatArray( numEntities, false );
+		return theEmptyArray;
+	}
+}
diff --git a/src/main/java/bdv/img/catmaid/VolatileUnsignedByteArrayTileLoader.java b/src/main/java/bdv/img/catmaid/VolatileUnsignedByteArrayTileLoader.java
new file mode 100644
index 0000000000000000000000000000000000000000..f2bcd6fbab68bc8912ac6393e2680d71a4186434
--- /dev/null
+++ b/src/main/java/bdv/img/catmaid/VolatileUnsignedByteArrayTileLoader.java
@@ -0,0 +1,229 @@
+/*
+ * #%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.catmaid;
+
+import bdv.img.cache.CacheArrayLoader;
+import ij.ImagePlus;
+import ij.io.Opener;
+import ij.process.Blitter;
+import ij.process.ByteProcessor;
+import net.imglib2.img.basictypeaccess.volatiles.array.VolatileByteArray;
+
+public class VolatileUnsignedByteArrayTileLoader implements CacheArrayLoader< VolatileByteArray >
+{
+	private VolatileByteArray theEmptyArray;
+
+	private final String urlFormat;
+
+	private final int tileWidth;
+
+	private final int tileHeight;
+
+	final private int[] zScales;
+
+	/**
+	 * <p>Create a {@link CacheArrayLoader} for a tiled image source.  Tiles are
+	 * addressed, in this order, by their</p>
+	 * <ul>
+	 * <li>scale level,</li>
+	 * <li>scale,</li>
+	 * <li>x,</li>
+	 * <li>y,</li>
+	 * <li>z,</li>
+	 * <li>tile width,</li>
+	 * <li>tile height,</li>
+	 * <li>tile row, and</li>
+	 * <li>tile column.</li>
+	 * </ul>
+	 * <p><code>urlFormat</code> specifies how these parameters are used
+	 * to generate a URL referencing the tile.  Examples:</p>
+	 *
+	 * <dl>
+	 * <dt>"http://catmaid.org/my-data/xy/%5$d/%8$d_%9$d_%1$d.jpg"</dt>
+	 * <dd>CATMAID DefaultTileSource (type 1)</dd>
+	 * <dt>"http://catmaid.org/my-data/xy/?x=%3$d&amp;y=%4$d&amp;width=%6d&amp;height=%7$d&amp;row=%8$d&amp;col=%9$d&amp;scale=%2$f&amp;z=%4$d"</dt>
+     * <dd>CATMAID RequestTileSource (type 2)</dd>
+	 * <dt>"http://catmaid.org/my-data/xy/%1$d/%5$d/%8$d/%9$d.jpg"</dt>
+	 * <dd>CATMAID LargeDataTileSource (type 5)</dd>
+	 * </dl>
+	 *
+	 * @param urlFormat
+	 * @param tileWidth
+	 * @param tileHeight
+	 */
+	public VolatileUnsignedByteArrayTileLoader( final String urlFormat, final int tileWidth, final int tileHeight, final int[] zScales )
+	{
+		theEmptyArray = new VolatileByteArray( tileWidth * tileHeight, false );
+		this.urlFormat = urlFormat;
+		this.tileWidth = tileWidth;
+		this.tileHeight = tileHeight;
+		this.zScales = zScales;
+	}
+
+	@Override
+	public int getBytesPerElement()
+	{
+		return 1;
+	}
+
+	final private void loadSliceArray(
+			final byte[] slice,
+			final int level,
+			final double scale,
+			final long c0,
+			final long r0,
+			final long x0,
+			final long y0,
+			final long z,
+			final long xm,
+			final long ym,
+			final long[] min,
+			final int w,
+			final int h ) throws InterruptedException
+	{
+		final ByteProcessor ip = new ByteProcessor( w, h, slice );
+
+		for (
+				long c = c0, x = x0;
+				x < xm;
+				++c, x += tileWidth )
+		{
+			for (
+					long r = r0, y = y0;
+					y < ym;
+					++r, y += tileHeight )
+			{
+				final String urlString = String.format( urlFormat, level, scale, x, y, z, tileWidth, tileHeight, r, c );
+//				System.out.println( urlString );
+				final ImagePlus tile;
+				// ij.io.Opener cannot handle "file:" URLs...
+				if ( urlString.startsWith( "file:" ) )
+					tile = new Opener().openImage( urlString.substring( 5 ) );
+				else
+					tile = new Opener().openURL( urlString );
+				if ( tile == null )
+					System.out.println( "failed loading r=" + r + " c=" + c );
+				else
+				{
+					final ByteProcessor tp = tile.getProcessor().convertToByteProcessor();
+					ip.copyBits( tp, ( int )( x - min[ 0 ] ), ( int )( y - min[ 1 ] ), Blitter.COPY );
+				}
+			}
+		}
+	}
+
+
+	final private void averageSlice(
+			final byte[] slice,
+			final int level,
+			final double scale,
+			final long c0,
+			final long r0,
+			final long x0,
+			final long y0,
+			final long xm,
+			final long ym,
+			final long[] min,
+			final int w,
+			final int h ) throws InterruptedException
+	{
+		final double[] fs = new double[ slice.length ];
+		for ( int z = ( int ) min[ 2 ] * zScales[ level ], dz = 0; dz < zScales[ level ]; ++dz )
+		{
+			loadSliceArray( slice, level, scale, c0, r0, x0, y0, z + dz, xm, ym, min, w, h );
+			for ( int i = 0; i < slice.length; ++i )
+				fs[ i ] += slice[ i ];
+		}
+		for ( int i = 0; i < slice.length; ++i )
+			slice[ i ] = ( byte )Math.round( fs[ i ] );
+	}
+
+
+	@Override
+	public VolatileByteArray loadArray(
+			 final int timepoint,
+			 final int setup,
+			 final int level,
+			 final int[] dimensions,
+			 final long[] min ) throws InterruptedException
+	{
+		final int w = dimensions[ 0 ];
+		final int h = dimensions[ 1 ];
+		final long xm = min[ 0 ] + w;
+		final long ym = min[ 1 ] + h;
+		final double scale = 1.0 / Math.pow(2.0, level);
+		final byte[] slice = new byte[ w * h ];
+
+		final long c0 = min[ 0 ] / tileWidth;
+		final long r0 = min[ 1 ] / tileHeight;
+		final long x0 = c0 * tileWidth;
+		final long y0 = r0 * tileHeight;
+
+		final byte[] data;
+		if ( dimensions[ 2 ] > 1 )
+		{
+			data = new byte[ w * h * dimensions[ 2 ] ];
+			final long[] zMin = min.clone();
+			for ( int z = 0; z < dimensions[ 2 ]; ++z )
+			{
+				zMin[ 2 ] = min[ 2 ] + z;
+				if ( zScales[ level ] > 1 )
+					averageSlice( slice, level, scale, c0, r0, x0, y0, xm, ym, zMin, w, h );
+				else
+					loadSliceArray( slice, level, scale, c0, r0, x0, y0, zMin[ 2 ], xm, ym, zMin, w, h );
+
+				System.arraycopy( slice, 0, data, z * slice.length, slice.length );
+			}
+		}
+		else
+		{
+			data = slice;
+			if ( zScales[ level ] > 1 )
+				averageSlice( slice, level, scale, c0, r0, x0, y0, xm, ym, min, w, h );
+			else
+				loadSliceArray( slice, level, scale, c0, r0, x0, y0, min[ 2 ], xm, ym, min, w, h );
+		}
+
+//		new ImagePlus( "", new ByteProcessor( w, h, data) ).show();
+
+		return new VolatileByteArray( data, true );
+	}
+
+	@Override
+	public VolatileByteArray emptyArray( final int[] dimensions )
+	{
+		int numEntities = 1;
+		for ( int i = 0; i < dimensions.length; ++i )
+			numEntities *= dimensions[ i ];
+		if ( theEmptyArray.getCurrentStorageArray().length < numEntities )
+			theEmptyArray = new VolatileByteArray( numEntities, false );
+		return theEmptyArray;
+	}
+}
diff --git a/src/main/java/bdv/img/catmaid/XmlIoTiledImageLoader.java b/src/main/java/bdv/img/catmaid/XmlIoTiledImageLoader.java
new file mode 100644
index 0000000000000000000000000000000000000000..2b3ece4281810a658bf757811c38c8d80d2cee04
--- /dev/null
+++ b/src/main/java/bdv/img/catmaid/XmlIoTiledImageLoader.java
@@ -0,0 +1,135 @@
+/*
+ * #%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.catmaid;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jdom2.Element;
+
+import bdv.img.cache.CacheArrayLoader;
+import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription;
+import mpicbg.spim.data.generic.sequence.ImgLoaderIo;
+import mpicbg.spim.data.generic.sequence.XmlIoBasicImgLoader;
+import net.imglib2.Volatile;
+import net.imglib2.type.NativeType;
+import net.imglib2.type.numeric.integer.UnsignedByteType;
+import net.imglib2.type.numeric.real.FloatType;
+import net.imglib2.type.volatiles.VolatileFloatType;
+import net.imglib2.type.volatiles.VolatileUnsignedByteType;
+
+@SuppressWarnings( "rawtypes" )
+@ImgLoaderIo( format = "tiled", type = TiledImageLoader.class )
+public class XmlIoTiledImageLoader
+		implements XmlIoBasicImgLoader< TiledImageLoader >
+{
+	@Override
+	public Element toXml( final TiledImageLoader imgLoader, final File basePath )
+	{
+		throw new UnsupportedOperationException( "not implemented" );
+	}
+
+	@SuppressWarnings( { "unchecked" } )
+	@Override
+	public TiledImageLoader fromXml( final Element elem, final File basePath, final AbstractSequenceDescription< ?, ?, ? > sequenceDescription )
+	{
+		final long width = Long.parseLong( elem.getChildText( "width" ) );
+		final long height = Long.parseLong( elem.getChildText( "height" ) );
+		final long depth = Long.parseLong( elem.getChildText( "depth" ) );
+
+		final double resXY = Double.parseDouble( elem.getChildText( "resXY" ) );
+		final double resZ = Double.parseDouble( elem.getChildText( "resZ" ) );
+		final double zScale = resZ / resXY;
+
+		final List< String > urlFormats = new ArrayList<>();
+		elem.getChildren( "urlFormat" ).forEach( a -> urlFormats.add( a.getText() ) );
+
+		final int tileWidth = Integer.parseInt( elem.getChildText( "tileWidth" ) );
+		final int tileHeight = Integer.parseInt( elem.getChildText( "tileHeight" ) );
+
+		final String blockWidthString = elem.getChildText( "blockWidth" );
+		final String blockHeightString = elem.getChildText( "blockHeight" );
+		final String blockDepthString = elem.getChildText( "blockDepth" );
+
+		final int blockWidth = blockWidthString == null ? tileWidth : Integer.parseInt( blockWidthString );
+		final int blockHeight = blockHeightString == null ? tileHeight : Integer.parseInt( blockHeightString );
+		final int blockDepth = blockDepthString == null ? 1 : Integer.parseInt( blockDepthString );
+
+		System.out.println( String.format( "Block size = (%d, %d, %d)", blockWidth, blockHeight, blockDepth ) );
+
+		final String numScalesString = elem.getChildText( "numScales" );
+		int numScales;
+		if ( numScalesString == null )
+			numScales = CatmaidImageLoader.getNumScales( width, height, tileWidth, tileHeight );
+		else
+			numScales = Integer.parseInt( numScalesString );
+
+		final int[][] blockSize = new int[ numScales ][];
+		final int[] zScales = new int[ numScales ];
+		for ( int i = 0; i < numScales; ++i )
+		{
+			blockSize[ i ] = new int[]{ blockWidth, blockHeight, blockDepth };
+			final int sixy = 1 << i;
+			final int siz = Math.max( 1, ( int )Math.round( sixy / zScale ) );
+			zScales[ i ] = siz;
+		}
+
+		final String type = elem.getChildText( "type" );
+
+		final ArrayList< CacheArrayLoader< ? > > arrayLoaders = new ArrayList<>();
+		final NativeType t;
+		final NativeType v;
+		switch ( type )
+		{
+		case "float32":
+			urlFormats.forEach( urlFormat -> arrayLoaders.add( new VolatileFloatArrayTileLoader( urlFormat, tileWidth, tileHeight, zScales ) ) );
+			t = new FloatType();
+			v = new VolatileFloatType();
+			break;
+		default:
+			urlFormats.forEach( urlFormat -> arrayLoaders.add( new VolatileUnsignedByteArrayTileLoader( urlFormat, tileWidth, tileHeight, zScales ) ) );
+			t = new UnsignedByteType();
+			v =new VolatileUnsignedByteType();
+		}
+
+		return new TiledImageLoader(
+				arrayLoaders,
+				t,
+				( Volatile & NativeType )v,
+				width,
+				height,
+				depth,
+				resZ / resXY,
+				tileWidth,
+				tileHeight,
+				blockSize );
+	}
+}
diff --git a/src/main/java/bdv/spimdata/legacy/XmlIoSpimDataMinimalLegacy.java b/src/main/java/bdv/spimdata/legacy/XmlIoSpimDataMinimalLegacy.java
index 7f0ffefcea08cc066abd4488e48f7cb5a4641c88..e30b94a93b1ef9f381144ca5fca4b33d0b3f61b0 100644
--- a/src/main/java/bdv/spimdata/legacy/XmlIoSpimDataMinimalLegacy.java
+++ b/src/main/java/bdv/spimdata/legacy/XmlIoSpimDataMinimalLegacy.java
@@ -7,13 +7,13 @@
  * %%
  * 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
@@ -40,6 +40,7 @@ import java.util.Map;
 import org.jdom2.Element;
 
 import bdv.img.catmaid.XmlIoCatmaidImageLoader;
+import bdv.img.catmaid.XmlIoTiledImageLoader;
 import bdv.img.hdf5.Hdf5ImageLoader;
 import bdv.img.hdf5.Partition;
 import bdv.img.openconnectome.XmlIoOpenConnectomeImageLoader;
@@ -177,6 +178,10 @@ public class XmlIoSpimDataMinimalLegacy
 		{
 			return new XmlIoCatmaidImageLoader().fromXml( elem, basePath, sequenceDescription );
 		}
+		else if ( classn.equals( "bdv.img.catmaid.TiledImageLoader" ) )
+		{
+			return new XmlIoTiledImageLoader().fromXml( elem, basePath, sequenceDescription );
+		}
 		else if ( classn.equals( "bdv.img.openconnectome.OpenConnectomeImageLoader" ) )
 		{
 			return new XmlIoOpenConnectomeImageLoader().fromXml( elem, basePath, sequenceDescription );