diff --git a/src/main/java/bdv/AbstractSpimSource.java b/src/main/java/bdv/AbstractSpimSource.java
index ba8e1b93ed17032580371c1c890e55774753434e..c981e1039da5f2bac258e1e46449e7348ff5f2ab 100644
--- a/src/main/java/bdv/AbstractSpimSource.java
+++ b/src/main/java/bdv/AbstractSpimSource.java
@@ -1,6 +1,7 @@
 package bdv;
 
 import mpicbg.spim.data.SequenceDescription;
+import mpicbg.spim.data.View;
 import net.imglib2.RandomAccessible;
 import net.imglib2.RandomAccessibleInterval;
 import net.imglib2.RealRandomAccessible;
@@ -9,6 +10,7 @@ import net.imglib2.interpolation.randomaccess.NLinearInterpolatorFactory;
 import net.imglib2.interpolation.randomaccess.NearestNeighborInterpolatorFactory;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.type.numeric.NumericType;
+import net.imglib2.view.Views;
 import bdv.viewer.Interpolation;
 import bdv.viewer.Source;
 
@@ -59,7 +61,40 @@ public abstract class AbstractSpimSource< T extends NumericType< T > > implement
 		interpolatorFactories[ iNLinearMethod ] = new NLinearInterpolatorFactory< T >();
 	}
 
-	protected abstract void loadTimepoint( final int timepoint );
+	protected void loadTimepoint( final int timepoint )
+	{
+		currentTimepoint = timepoint;
+		if ( isPresent( timepoint ) )
+		{
+			final T zero = getType().createVariable();
+			zero.setZero();
+			final View view = sequenceViews.getView( timepoint, setup );
+			final AffineTransform3D reg = view.getModel();
+			for ( int level = 0; level < currentSources.length; level++ )
+			{
+				final AffineTransform3D mipmapTransform = getMipmapTransforms( setup )[ level ];
+				currentSourceTransforms[ level ].set( reg );
+				currentSourceTransforms[ level ].concatenate( mipmapTransform );
+				currentSources[ level ] = getImage( view, level );
+				for ( int method = 0; method < numInterpolationMethods; ++method )
+					currentInterpolatedSources[ level ][ method ] = Views.interpolate( Views.extendValue( currentSources[ level ], zero ), interpolatorFactories[ method ] );
+			}
+		}
+		else
+		{
+			for ( int level = 0; level < currentSources.length; level++ )
+			{
+				currentSourceTransforms[ level ].identity();
+				currentSources[ level ] = null;
+				for ( int method = 0; method < numInterpolationMethods; ++method )
+					currentInterpolatedSources[ level ][ method ] = null;
+			}
+		}
+	}
+
+	protected abstract AffineTransform3D[] getMipmapTransforms( final int setup );
+
+	protected abstract RandomAccessibleInterval< T > getImage( final View view, final int level );
 
 	@Override
 	public boolean isPresent( final int t )
diff --git a/src/main/java/bdv/BigDataViewer.java b/src/main/java/bdv/BigDataViewer.java
index f0b779f25ca3577f4521c46dd0cd31f0a00cf09a..9c86ff48d0779cb8f8ca250638896d56feed89bd 100644
--- a/src/main/java/bdv/BigDataViewer.java
+++ b/src/main/java/bdv/BigDataViewer.java
@@ -31,7 +31,6 @@ import org.jdom2.output.XMLOutputter;
 
 import bdv.export.ProgressWriter;
 import bdv.export.ProgressWriterConsole;
-import bdv.img.openconnectome.OpenConnectomeImageLoader;
 import bdv.tools.HelpDialog;
 import bdv.tools.InitializeViewerState;
 import bdv.tools.RecordMovieDialog;
@@ -173,7 +172,7 @@ public class BigDataViewer
 		initSetups( loader, converterSetups, sources );
 
 		viewerFrame = new ViewerFrame( width, height, sources, seq.numTimepoints(),
-				( ( OpenConnectomeImageLoader ) seq.imgLoader ).getCache() );
+				( ( ViewerImgLoader< ?, ? > ) seq.imgLoader ).getCache() );
 		viewer = viewerFrame.getViewerPanel();
 
 		for ( final ConverterSetup cs : converterSetups )
diff --git a/src/main/java/bdv/SpimSource.java b/src/main/java/bdv/SpimSource.java
index 70b34d0655c43308680cca8e9f692e06d4004e18..801db3445fa1f911f143493cfb3c13fb0c9b968d 100644
--- a/src/main/java/bdv/SpimSource.java
+++ b/src/main/java/bdv/SpimSource.java
@@ -2,9 +2,9 @@ package bdv;
 
 import mpicbg.spim.data.SequenceDescription;
 import mpicbg.spim.data.View;
+import net.imglib2.RandomAccessibleInterval;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.type.numeric.NumericType;
-import net.imglib2.view.Views;
 
 public class SpimSource< T extends NumericType< T > > extends AbstractSpimSource< T >
 {
@@ -20,40 +20,20 @@ public class SpimSource< T extends NumericType< T > > extends AbstractSpimSource
 	}
 
 	@Override
-	protected void loadTimepoint( final int timepoint )
+	public T getType()
 	{
-		currentTimepoint = timepoint;
-		if ( isPresent( timepoint ) )
-		{
-			final T zero = imgLoader.getImageType().createVariable();
-			zero.setZero();
-			final View view = sequenceViews.getView( timepoint, setup );
-			final AffineTransform3D reg = view.getModel();
-			for ( int level = 0; level < currentSources.length; level++ )
-			{
-				final AffineTransform3D mipmapTransform = imgLoader.getMipmapTransforms( setup )[ level ];
-				currentSourceTransforms[ level ].set( reg );
-				currentSourceTransforms[ level ].concatenate( mipmapTransform );
-				currentSources[ level ] = imgLoader.getImage( view, level );
-				for ( int method = 0; method < numInterpolationMethods; ++method )
-					currentInterpolatedSources[ level ][ method ] = Views.interpolate( Views.extendValue( currentSources[ level ], zero ), interpolatorFactories[ method ] );
-			}
-		}
-		else
-		{
-			for ( int level = 0; level < currentSources.length; level++ )
-			{
-				currentSourceTransforms[ level ].identity();
-				currentSources[ level ] = null;
-				for ( int method = 0; method < numInterpolationMethods; ++method )
-					currentInterpolatedSources[ level ][ method ] = null;
-			}
-		}
+		return imgLoader.getImageType();
 	}
 
 	@Override
-	public T getType()
+	protected RandomAccessibleInterval< T > getImage( final View view, final int level )
 	{
-		return imgLoader.getImageType();
+		return imgLoader.getImage( view, level );
+	}
+
+	@Override
+	protected AffineTransform3D[] getMipmapTransforms( final int setup )
+	{
+		return imgLoader.getMipmapTransforms( setup );
 	}
 }
diff --git a/src/main/java/bdv/VolatileSpimSource.java b/src/main/java/bdv/VolatileSpimSource.java
index d2dfb10d279230c222817119966157e6332ee331..7739d9e8753db9623481711d427cf4cdd24eaffb 100644
--- a/src/main/java/bdv/VolatileSpimSource.java
+++ b/src/main/java/bdv/VolatileSpimSource.java
@@ -2,11 +2,10 @@ package bdv;
 
 import mpicbg.spim.data.SequenceDescription;
 import mpicbg.spim.data.View;
+import net.imglib2.RandomAccessibleInterval;
 import net.imglib2.Volatile;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.type.numeric.NumericType;
-import net.imglib2.type.numeric.RealType;
-import net.imglib2.view.Views;
 
 public class VolatileSpimSource< T extends NumericType< T >, V extends Volatile< T > & NumericType< V >  > extends AbstractSpimSource< V >
 {
@@ -24,38 +23,6 @@ public class VolatileSpimSource< T extends NumericType< T >, V extends Volatile<
 		loadTimepoint( 0 );
 	}
 
-	@Override
-	protected void loadTimepoint( final int timepoint )
-	{
-		currentTimepoint = timepoint;
-		if ( isPresent( timepoint ) )
-		{
-			final V zero = imgLoader.getVolatileImageType().createVariable();
-			zero.setZero();
-			final View view = sequenceViews.getView( timepoint, setup );
-			final AffineTransform3D reg = view.getModel();
-			for ( int level = 0; level < currentSources.length; level++ )
-			{
-				final AffineTransform3D mipmapTransform = imgLoader.getMipmapTransforms( setup )[ level ];
-				currentSourceTransforms[ level ].set( reg );
-				currentSourceTransforms[ level ].concatenate( mipmapTransform );
-				currentSources[ level ] = imgLoader.getVolatileImage( view, level );
-				for ( int method = 0; method < numInterpolationMethods; ++method )
-					currentInterpolatedSources[ level ][ method ] = Views.interpolate( Views.extendValue( currentSources[ level ], zero ), interpolatorFactories[ method ] );
-			}
-		}
-		else
-		{
-			for ( int level = 0; level < currentSources.length; level++ )
-			{
-				currentSourceTransforms[ level ].identity();
-				currentSources[ level ] = null;
-				for ( int method = 0; method < numInterpolationMethods; ++method )
-					currentInterpolatedSources[ level ][ method ] = null;
-			}
-		}
-	}
-
 	@Override
 	public V getType()
 	{
@@ -66,4 +33,16 @@ public class VolatileSpimSource< T extends NumericType< T >, V extends Volatile<
 	{
 		return nonVolatileSource;
 	}
+
+	@Override
+	protected RandomAccessibleInterval< V > getImage( final View view, final int level )
+	{
+		return imgLoader.getVolatileImage( view, level );
+	}
+
+	@Override
+	protected AffineTransform3D[] getMipmapTransforms( final int setup )
+	{
+		return imgLoader.getMipmapTransforms( setup );
+	}
 }
diff --git a/src/main/java/bdv/img/catmaid/CatmaidImageLoader.java b/src/main/java/bdv/img/catmaid/CatmaidImageLoader.java
index 5f56ac92dbcb91544be242dd16dad7a74bce3c77..eeaac44bb89c5264198fde11c421199fb6333f46 100644
--- a/src/main/java/bdv/img/catmaid/CatmaidImageLoader.java
+++ b/src/main/java/bdv/img/catmaid/CatmaidImageLoader.java
@@ -43,11 +43,11 @@ public class CatmaidImageLoader extends AbstractViewerImgLoader< ARGBType, Volat
 
 	private double[][] mipmapResolutions;
 
+	private AffineTransform3D[] mipmapTransforms;
+
 	private long[][] imageDimensions;
 
 	private int[][] blockDimensions;
-	
-	private AffineTransform3D[] mipmapTransforms;
 
 	private VolatileGlobalCellCache< VolatileIntArray > cache;
 
@@ -163,7 +163,7 @@ public class CatmaidImageLoader extends AbstractViewerImgLoader< ARGBType, Volat
 	}
 
 	@Override
-	public AffineTransform3D[] getMipmapTransforms( int setup )
+	public AffineTransform3D[] getMipmapTransforms( final int setup )
 	{
 		return mipmapTransforms;
 	}
diff --git a/src/main/java/bdv/img/hdf5/Hdf5ImageLoader.java b/src/main/java/bdv/img/hdf5/Hdf5ImageLoader.java
index 526891d2fc8af7b4694f6886620071d11d7161f0..f399244473b0e86a6d10afe0296762521f1e2653 100644
--- a/src/main/java/bdv/img/hdf5/Hdf5ImageLoader.java
+++ b/src/main/java/bdv/img/hdf5/Hdf5ImageLoader.java
@@ -30,6 +30,7 @@ import bdv.img.cache.VolatileGlobalCellCache;
 import bdv.img.cache.VolatileGlobalCellCache.LoadingStrategy;
 import bdv.img.cache.VolatileImgCells;
 import bdv.img.cache.VolatileImgCells.CellCache;
+import bdv.util.MipmapTransforms;
 import ch.systemsx.cisd.hdf5.HDF5DataSetInformation;
 import ch.systemsx.cisd.hdf5.HDF5Factory;
 import ch.systemsx.cisd.hdf5.IHDF5Reader;
@@ -140,16 +141,7 @@ public class Hdf5ImageLoader extends AbstractViewerImgLoader< UnsignedShortType,
 
 			final AffineTransform3D[] mipmapTransforms = new AffineTransform3D[ mipmapResolutions.length ];
 			for ( int level = 0; level < mipmapResolutions.length; level++ )
-			{
-				final AffineTransform3D mipmapTransform = new AffineTransform3D();
-				final double[] resolution = mipmapResolutions[ level ];
-				for ( int d = 0; d < 3; ++d )
-				{
-					mipmapTransform.set( resolution[ d ], d, d );
-					mipmapTransform.set( 0.5 * ( resolution[ d ] - 1 ), d, 3 );
-				}
-				mipmapTransforms[ level ] = mipmapTransform;
-			}
+				mipmapTransforms[ level ] = MipmapTransforms.getMipmapTransformDefault( mipmapResolutions[ level ] );
 			perSetupMipmapTransforms.add( mipmapTransforms );
 
 			final int[][] subdivisions = hdf5Reader.readIntMatrix( getSubdivisionsPath( setup ) );
diff --git a/src/main/java/bdv/img/remote/RemoteImageLoader.java b/src/main/java/bdv/img/remote/RemoteImageLoader.java
index c6d937387e3911be48c38ff2b2fcc87c5f21ed89..9046b78247f0e893fbf7b2571d558de2dd53075a 100644
--- a/src/main/java/bdv/img/remote/RemoteImageLoader.java
+++ b/src/main/java/bdv/img/remote/RemoteImageLoader.java
@@ -28,6 +28,7 @@ import bdv.img.cache.VolatileGlobalCellCache;
 import bdv.img.cache.VolatileGlobalCellCache.LoadingStrategy;
 import bdv.img.cache.VolatileImgCells;
 import bdv.img.cache.VolatileImgCells.CellCache;
+import bdv.util.MipmapTransforms;
 
 import com.google.gson.Gson;
 
@@ -67,16 +68,7 @@ public class RemoteImageLoader extends AbstractViewerImgLoader< UnsignedShortTyp
 			final double[][] mipmapResolutions = metadata.perSetupMipmapResolutions.get( setup );
 			final AffineTransform3D[] mipmapTransforms = new AffineTransform3D[ mipmapResolutions.length ];
 			for ( int level = 0; level < mipmapResolutions.length; level++ )
-			{
-				final AffineTransform3D mipmapTransform = new AffineTransform3D();
-				final double[] resolution = mipmapResolutions[ level ];
-				for ( int d = 0; d < 3; ++d )
-				{
-					mipmapTransform.set( resolution[ d ], d, d );
-					mipmapTransform.set( 0.5 * ( resolution[ d ] - 1 ), d, 3 );
-				}
-				mipmapTransforms[ level ] = mipmapTransform;
-			}
+				mipmapTransforms[ level ] = MipmapTransforms.getMipmapTransformDefault( mipmapResolutions[ level ] );
 			perSetupMipmapTransforms.add( mipmapTransforms );
 		}
 		cellsDimensions = metadata.createCellsDimensions();
diff --git a/src/main/java/bdv/util/MipmapTransforms.java b/src/main/java/bdv/util/MipmapTransforms.java
new file mode 100644
index 0000000000000000000000000000000000000000..cb4dd53bae279d7851906a268f17d793960d6ff3
--- /dev/null
+++ b/src/main/java/bdv/util/MipmapTransforms.java
@@ -0,0 +1,32 @@
+package bdv.util;
+
+import net.imglib2.realtransform.AffineTransform3D;
+
+public class MipmapTransforms
+{
+	/**
+	 * Compute the transformation (scale and shift) that maps from coordinates
+	 * in a down-scaled image to coordinates in the original image. This assumes
+	 * that each down-scaled pixel is the average of a block of blocks of pixels
+	 * in the original image. For down-scaling by a factor of 2, pixel (0,0,0)
+	 * in the down-scaled image is the average of the 8 pixel block from (0,0,0)
+	 * to (1,1,1) in the original image.
+	 *
+	 * @param resolution
+	 *            the down-scaling factors in each dimension. {4,4,2} means
+	 *            every pixel in the down-scaled image corresponds to a 4x4x2
+	 *            pixel block in the original image.
+	 * @return transformation from down-scaled image to original image.
+	 */
+	public static AffineTransform3D getMipmapTransformDefault( final double[] resolution )
+	{
+		assert resolution.length == 3;
+		final AffineTransform3D mipmapTransform = new AffineTransform3D();
+		for ( int d = 0; d < 3; ++d )
+		{
+			mipmapTransform.set( resolution[ d ], d, d );
+			mipmapTransform.set( 0.5 * ( resolution[ d ] - 1 ), d, 3 );
+		}
+		return mipmapTransform;
+	}
+}
diff --git a/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java b/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java
index e794a060b02b0cb7f7099aa9843ec723806f6c24..7e5aa240e07cee3326b6ed4a9138771c69575d41 100644
--- a/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java
+++ b/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java
@@ -371,12 +371,12 @@ public class MultiResolutionRenderer
 					( renderImages[ 0 ][ 0 ].dimension( 0 ) != screenImages[ 0 ][ 0 ].dimension( 0 ) ||
 					  renderImages[ 0 ][ 0 ].dimension( 1 ) != screenImages[ 0 ][ 0 ].dimension( 1 ) ) ) )
 		{
-			renderImages = new ARGBScreenImage[ screenScales.length ][ numVisibleSources ];
+			renderImages = new ARGBScreenImage[ screenScales.length ][ n ];
 			for ( int i = 0; i < screenScales.length; ++i )
 			{
 				final int w = ( int ) screenImages[ i ][ 0 ].dimension( 0 );
 				final int h = ( int ) screenImages[ i ][ 0 ].dimension( 1 );
-				for ( int j = 0; j < numVisibleSources; ++j )
+				for ( int j = 0; j < n; ++j )
 				{
 					renderImages[ i ][ j ] = ( i == 0 ) ?
 						new ARGBScreenImage( w, h ) :