Repository: mobie/mobie-viewer-fiji (language: python)

Issue:
Refactor file chooser and move clear button
fixes https://github.com/mobie/mobie-viewer-fiji/issues/639 and https://github.com/mobie/mobie-viewer-fiji/issues/633

- refactors use of JFileChooser to always use the workaround from @tischi (I had to add ```new Thread()``` in a few more places to stop the file choosers hanging on windows)
- changes to SNAPSHOT version
- moves clear button down below location (while I was working on the file choosers, I found a bug where loading/saving views leads to multiple copies of the clear button - so I thought I'd just go ahead and move it)
![Screenshot 2022-03-11 115135](https://user-images.githubusercontent.com/24316371/157854550-45978928-a295-4255-bcd7-897051321a8a.png)


ViewsSaver API
@K-Meech 

To automatically save views I would need three new public methods in `ViewsSaver`:

```
public boolean saveViewToProject( View view, String viewName, boolean overwrite ); // without UI, projection location should be automatically determined from moBIE.

public boolean saveViewToFileSystem( View view, String viewName, String path, boolean overwrite  ); // without UI

public boolean saveViewToFileSystem( View view, String viewName, boolean overwrite  ); // pops up UI to choose the file path
```

Could you please add those?

They should return `false` if `overwrite` was `false` and the `viewName` exists already and they could thus not save it.

Sounds good?





We cannot always close the ImgLoader

```
MoBIE.closeSourceAndConverter( SourceAndConverter< ? > sourceAndConverter )
	{
		SourceAndConverterServices.getBdvDisplayService().removeFromAllBdvs( sourceAndConverter );
		String sourceName = sourceAndConverter.getSpimSource().getName();
		final ImgLoader imgLoader = sourceNameToImgLoader.get( sourceName );
		if ( imgLoader instanceof N5ImageLoader )
		{
			( ( N5ImageLoader ) imgLoader ).close();
		} else if ( imgLoader instanceof N5OMEZarrImageLoader ) {
            ( ( N5OMEZarrImageLoader ) imgLoader ).close();
        }

		sourceNameToImgLoader.remove( sourceName );
		SourceAndConverterServices.getSourceAndConverterService().remove( sourceAndConverter );

		// TODO - when we support more image formats e.g. OME-ZARR, we should explicitly close their imgloaders here too
	}
```

If one `imgLoader` serves several `sourceAndConverter` (such as for example when creating a `MaskedSource` from a `Source`) than this is not correct and can lead to the disappearance of that other source from BDV.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build-main.yml b/.github/workflows/build-main.yml
index af22aa45e..f8941c36b 100755
--- a/.github/workflows/build-main.yml
+++ b/.github/workflows/build-main.yml
@@ -1,7 +1,7 @@
 name: build
 
 on:
-  release:
+  push:
     branches:
       - main
       - develop
@@ -36,8 +36,9 @@ jobs:
         run: .github/setup.sh
       - name: Install blosc
         run: sudo apt install libblosc-dev
-      - run: sudo apt-get install xvfb
-      - name: Execute the build
+      - name: Install xvfb
+        run: sudo apt-get install xvfb
+      - name: Execute the build using xvfb
         run: xvfb-run --auto-servernum .github/build.sh
         env:
           GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }}
diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml
index 294c8727b..878a4b7fa 100755
--- a/.github/workflows/build-pr.yml
+++ b/.github/workflows/build-pr.yml
@@ -32,5 +32,9 @@ jobs:
           distribution: 'zulu'
       - name: Set up CI environment
         run: .github/setup.sh
-      - name: Execute the build
-        run: .github/build.sh
+      - name: Install blosc
+        run: sudo apt install libblosc-dev
+      - name: Install xvfb
+        run: sudo apt-get install xvfb
+      - name: Execute the build using xvfb
+        run: xvfb-run --auto-servernum .github/build.sh
diff --git a/pom.xml b/pom.xml
index 91676f7a1..f9ffc062d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,7 +9,7 @@
 
 	<groupId>org.embl.mobie</groupId>
 	<artifactId>mobie-viewer-fiji</artifactId>
-	<version>2.0.7</version>
+	<version>2.1.7-SNAPSHOT</version>
 
 	<!-- mvn clean install -Dscijava.app.directory=/Users/tischer/Desktop/Fiji/Fiji-MoBIE-beta.app -->
 	<!-- mvn clean install -Dscijava.app.directory=/Users/tischer/Desktop/Fiji/Fiji-MoBIE.app -->
@@ -60,8 +60,8 @@
 		</mailingList>
 	</mailingLists>
 	<scm>
-		<connection>scm:git:git://github.com/mobie/mobie-viewer-fiji</connection>
-		<developerConnection>scm:git:git@github.com/mobie/mobie-viewer-fiji</developerConnection>
+		<connection>scm:git:https://github.com/mobie/mobie-viewer-fiji</connection>
+		<developerConnection>scm:git:git@github.com:mobie/mobie-viewer-fiji</developerConnection>
 		<tag>HEAD</tag>
 		<url>https://github.com/mobie/mobie-viewer-fiji</url>
 	</scm>
@@ -70,7 +70,8 @@
 		<url>https://github.com/mobie/mobie-viewer-fiji/issues</url>
 	</issueManagement>
 	<ciManagement>
-		<system>None</system>
+		<system>Github Actions</system>
+		<url>https://github.com/mobie/mobie-viewer-fiji/actions</url>
 	</ciManagement>
 	<properties>
 		<package-name>de.embl.cba.mobie</package-name>
@@ -79,7 +80,7 @@
 		<enforcer.skip>true</enforcer.skip>
 
 		<!-- Java 8	Update Site	-->
-		<imglib2.version>5.12.0</imglib2.version>
+<!--		<imglib2.version>5.12.0</imglib2.version>-->
 		<imglib2-cache.version>1.0.0-beta-16</imglib2-cache.version>
 		<bigdataviewer-core.version>10.2.1</bigdataviewer-core.version>
 		<bigdataviewer-vistools.version>1.0.0-beta-29</bigdataviewer-vistools.version>
@@ -89,11 +90,12 @@
 		<spim_data.version>2.2.5</spim_data.version>
 
 		<!-- BIOP -->
-		<bigdataviewer-playground.version>0.3.13</bigdataviewer-playground.version>
+		<bigdataviewer-playground.version>0.4.1</bigdataviewer-playground.version>
 
 		<!-- EMBL CBA -->
 		<imagej-utils.version>0.6.4</imagej-utils.version>
-		<mobie-io.version>1.0.8</mobie-io.version>
+
+		<mobie-io.version>1.2.8</mobie-io.version>
 
 		<!-- Version 1.6.0-scijava-3-SNAPSHOT of j3dcore addresses a minor issue
 		https://github.com/fiji/3D_Viewer/issues/26
diff --git a/src/main/java/org/embl/mobie/viewer/MoBIE.java b/src/main/java/org/embl/mobie/viewer/MoBIE.java
index 6dfd44f39..0746202e4 100644
--- a/src/main/java/org/embl/mobie/viewer/MoBIE.java
+++ b/src/main/java/org/embl/mobie/viewer/MoBIE.java
@@ -1,27 +1,29 @@
 package org.embl.mobie.viewer;
 
-import bdv.util.volatiles.SharedQueue;
 import bdv.img.n5.N5ImageLoader;
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
 import de.embl.cba.bdv.utils.Logger;
+import mpicbg.spim.data.SpimDataException;
+import org.embl.mobie.io.ImageDataFormat;
+import org.embl.mobie.io.SpimDataOpener;
 import org.embl.mobie.io.ome.zarr.loaders.N5OMEZarrImageLoader;
+import org.embl.mobie.io.util.FileAndUrlUtils;
+import org.embl.mobie.io.util.S3Utils;
 import org.embl.mobie.viewer.display.SegmentationSourceDisplay;
 import org.embl.mobie.viewer.display.AnnotatedIntervalDisplay;
 import org.embl.mobie.viewer.annotate.AnnotatedIntervalCreator;
 import org.embl.mobie.viewer.annotate.AnnotatedIntervalTableRow;
+import org.embl.mobie.viewer.plugins.platybrowser.GeneSearchCommand;
 import org.embl.mobie.viewer.serialize.DatasetJsonParser;
 import org.embl.mobie.viewer.serialize.ProjectJsonParser;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import org.embl.mobie.viewer.source.ImageSource;
 import org.embl.mobie.viewer.source.SegmentationSource;
-import org.embl.mobie.viewer.source.SpimDataOpener;
 import org.embl.mobie.viewer.table.TableDataFormat;
 import org.embl.mobie.viewer.ui.UserInterface;
 import org.embl.mobie.viewer.ui.WindowArrangementHelper;
 import org.embl.mobie.viewer.view.View;
 import org.embl.mobie.viewer.view.ViewManager;
-import de.embl.cba.tables.FileAndUrlUtils;
 import de.embl.cba.tables.TableColumns;
 import de.embl.cba.tables.TableRows;
 import de.embl.cba.tables.github.GitHubUtils;
@@ -30,6 +32,7 @@
 import mpicbg.spim.data.SpimData;
 import mpicbg.spim.data.sequence.ImgLoader;
 import sc.fiji.bdvpg.PlaygroundPrefs;
+import sc.fiji.bdvpg.scijava.services.SourceAndConverterService;
 import sc.fiji.bdvpg.services.SourceAndConverterServices;
 import sc.fiji.bdvpg.sourceandconverter.importer.SourceAndConverterFromSpimDataCreator;
 
@@ -38,16 +41,14 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 
 public class MoBIE
 {
 	static { net.imagej.patcher.LegacyInjector.preinit(); }
 
-	public static final int N_THREADS = Runtime.getRuntime().availableProcessors() - 1;
-	public static final SharedQueue sharedQueue = new SharedQueue( N_THREADS );
 	public static final String PROTOTYPE_DISPLAY_VALUE = "01234567890123456789";
-	public static final ExecutorService executorService = Executors.newFixedThreadPool( N_THREADS );
+
 	private final String projectName;
 	private MoBIESettings settings;
 	private String datasetName;
@@ -59,7 +60,8 @@ public class MoBIE
 	private String imageRoot;
 	private String tableRoot;
 	private HashMap< String, ImgLoader > sourceNameToImgLoader;
-	private Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter;
+	private Map< String, SourceAndConverter< ? > > sourceNameToTransformedSourceAndConverter;
+	private ArrayList< String > projectCommands = new ArrayList<>();;
 
 	public MoBIE( String projectRoot ) throws IOException
 	{
@@ -70,19 +72,50 @@ public MoBIE( String projectLocation, MoBIESettings settings ) throws IOExceptio
 	{
 		IJ.log("MoBIE");
 		this.settings = settings.projectLocation( projectLocation );
+		setS3Credentials( settings );
 		setProjectImageAndTableRootLocations( );
-		projectName = Utils.getName( projectLocation );
+		registerProjectPlugins( settings.values.getProjectLocation() );
+		projectName = MoBIEUtils.getName( projectLocation );
 		PlaygroundPrefs.setSourceAndConverterUIVisibility( false );
 		project = new ProjectJsonParser().parseProject( FileAndUrlUtils.combinePath( projectRoot,  "project.json" ) );
 		this.settings = setImageDataFormat( projectLocation );
 		openDataset();
 	}
 
+	// TODO: probably such plugins should rather come with the MoBIESettings
+	//  such that additional commands could be registered without
+	//  changing the core code
+	private void registerProjectPlugins( String projectLocation )
+	{
+		if( projectLocation.contains( "platybrowser" ) )
+		{
+			GeneSearchCommand.setMoBIE( this );
+			projectCommands.add( SourceAndConverterService.getCommandName(  GeneSearchCommand.class ) );
+		}
+	}
+
+	private void setS3Credentials( MoBIESettings settings )
+	{
+		if ( settings.values.getS3AccessAndSecretKey() != null )
+		{
+			S3Utils.setS3AccessAndSecretKey( settings.values.getS3AccessAndSecretKey() );
+		}
+	}
+
 	private MoBIESettings setImageDataFormat( String projectLocation )
 	{
-		if ( settings.values.getImageDataFormat() == null )
+		final ImageDataFormat imageDataFormat = settings.values.getImageDataFormat();
+
+		if ( imageDataFormat != null )
+		{
+			if ( ! project.getImageDataFormats().contains( imageDataFormat ) )
+			{
+				throw new RuntimeException( "The requested image data format " + imageDataFormat + " is not supported by the project: " + projectLocation );
+			}
+		}
+		else // automatically determine the correct image format
 		{
-			final List<ImageDataFormat> imageDataFormats = project.getImageDataFormats();
+			final List< ImageDataFormat > imageDataFormats = project.getImageDataFormats();
 			if ( projectLocation.startsWith( "http" ) )
 			{
 				if ( imageDataFormats.contains( ImageDataFormat.OmeZarrS3 ) )
@@ -120,8 +153,8 @@ public static void mergeAnnotatedIntervalTable(List<AnnotatedIntervalTableRow> i
 		// deal with the fact that the grid ids are sometimes
 		// stored as 1 and sometimes as 1.0
 		// after below operation they all will be 1.0, 2.0, ...
-		Utils.toDoubleStrings( gridIdColumn );
-		Utils.toDoubleStrings( columns.get( TableColumnNames.ANNOTATION_ID ) );
+		MoBIEUtils.toDoubleStrings( gridIdColumn );
+		MoBIEUtils.toDoubleStrings( columns.get( TableColumnNames.ANNOTATION_ID ) );
 
 		final Map< String, List< String > > newColumns = TableColumns.createColumnsForMergingExcludingReferenceColumns( referenceColumns, columns );
 
@@ -173,34 +206,39 @@ private void setProjectImageAndTableRootLocations( )
 		}
 	}
 
+	/*
+	 * Opens "raw" SourceAndConverters.
+	 * Note that they do not yet contain all source transforms that may be applied by a view.
+	 * However, sourceAndConverters obtained via the getSourceAndConverter method
+	 * are containing all the sourceTransforms.
+	 * This can be confusing...
+	 */
 	public Map< String, SourceAndConverter< ? > > openSourceAndConverters( Collection< String > sources )
 	{
 		final long start = System.currentTimeMillis();
 
-		Map< String, SourceAndConverter< ? > > sourceAndConverters = new ConcurrentHashMap< >();
+		Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverters = new ConcurrentHashMap< >();
 
-		final ExecutorService executorService = Executors.newFixedThreadPool( N_THREADS );
+		final ArrayList< Future< ? > > futures = ThreadUtils.getFutures();
 		for ( String sourceName : sources )
 		{
-			executorService.execute( () -> {
-				sourceAndConverters.put( sourceName, openSourceAndConverter( sourceName ) );
-			} );
+			futures.add(
+					ThreadUtils.ioExecutorService.submit( () -> { sourceNameToSourceAndConverters.put( sourceName, openSourceAndConverter( sourceName ) ); }
+				) );
 		}
+		ThreadUtils.waitUntilFinished( futures );
 
-		Utils.waitUntilFinishedAndShutDown( executorService );
+		IJ.log( "Fetched " + sourceNameToSourceAndConverters.size() + " image(s)  in " + (System.currentTimeMillis() - start) + " ms, using " + ThreadUtils.N_IO_THREADS + " thread(s).");
 
-		System.out.println( "Fetched " + sourceAndConverters.size() + " image source(s) in " + (System.currentTimeMillis() - start) + " ms, using " + N_THREADS + " thread(s).");
-
-		return sourceAndConverters;
+		return sourceNameToSourceAndConverters;
 	}
 
 	private void openDataset( String datasetName ) throws IOException
 	{
 		sourceNameToImgLoader = new HashMap<>();
-		sourceNameToSourceAndConverter = new ConcurrentHashMap<>();
+		sourceNameToTransformedSourceAndConverter = new ConcurrentHashMap<>();
 		setDatasetName( datasetName );
 		dataset = new DatasetJsonParser().parseDataset( getDatasetPath( "dataset.json" ) );
-
 		userInterface = new UserInterface( this );
 		viewManager = new ViewManager( this, userInterface, dataset.is2D, dataset.timepoints );
 		final View view = dataset.views.get( settings.values.getView() );
@@ -286,18 +324,48 @@ public synchronized ImageSource getSource( String sourceName )
 	{
 		final ImageSource imageSource = getSource( sourceName );
 		final String imagePath = getImagePath( imageSource );
-        IJ.log( "Opening image:\n" + imagePath );
-        final ImageDataFormat imageDataFormat = settings.values.getImageDataFormat();
-        SpimData spimData = new SpimDataOpener().openSpimData( imagePath, imageDataFormat, sharedQueue );
-        sourceNameToImgLoader.put( sourceName, spimData.getSequenceDescription().getImgLoader() );
+		IJ.log( "Opening image:\n" + imagePath );
+		final ImageDataFormat imageDataFormat = settings.values.getImageDataFormat();
 
-        final SourceAndConverterFromSpimDataCreator creator = new SourceAndConverterFromSpimDataCreator( spimData );
-        SourceAndConverter<?> sourceAndConverter = creator.getSetupIdToSourceAndConverter().values().iterator().next();
+		try
+		{
+			SpimData spimData = tryOpenSpimData( imagePath, imageDataFormat );
+			sourceNameToImgLoader.put( sourceName, spimData.getSequenceDescription().getImgLoader() );
 
-        return sourceAndConverter;
+			final SourceAndConverterFromSpimDataCreator creator = new SourceAndConverterFromSpimDataCreator( spimData );
+			SourceAndConverter< ? > sourceAndConverter = creator.getSetupIdToSourceAndConverter().values().iterator().next();
+			return sourceAndConverter;
+		}
+		catch ( Exception e )
+		{
+			System.err.println( "Error opening: " + imagePath );
+			e.printStackTrace();
+			throw new RuntimeException();
+		}
     }
 
-    public void setDataset( String dataset )
+	private SpimData tryOpenSpimData( String imagePath, ImageDataFormat imageDataFormat )
+	{
+		try
+		{
+			if ( imageDataFormat.equals( ImageDataFormat.BdvOmeZarrS3) ||
+					imageDataFormat.equals( ImageDataFormat.BdvOmeZarr) )
+			{
+				// TODO enable shared queues
+				return ( SpimData ) new SpimDataOpener().openSpimData( imagePath, imageDataFormat );
+			}
+			else
+			{
+				return ( SpimData ) new SpimDataOpener().openSpimData( imagePath, imageDataFormat, ThreadUtils.sharedQueue );
+			}
+		}
+		catch ( SpimDataException e )
+		{
+			throw new RuntimeException( e );
+		}
+	}
+
+	public void setDataset( String dataset )
     {
         setDatasetName( dataset );
         viewManager.close();
@@ -354,13 +422,13 @@ private String createPath( String root, String[] files )
 		return location;
 	}
 
-	private List< TableRowImageSegment > loadImageSegmentsTable( String sourceName, String tableName, String displaySourceName )
+	private List< TableRowImageSegment > loadImageSegmentsTable( String sourceName, String tableName )
 	{
 		final SegmentationSource tableSource = ( SegmentationSource ) getSource( sourceName );
 
 		final String defaultTablePath = getTablePath( tableSource, tableName );
 
-		final List< TableRowImageSegment > segments = Utils.createAnnotatedImageSegmentsFromTableFile( defaultTablePath, displaySourceName );
+		final List< TableRowImageSegment > segments = MoBIEUtils.createAnnotatedImageSegmentsFromTableFile( defaultTablePath, sourceName );
 
 		return segments;
 	}
@@ -378,54 +446,60 @@ private List< Map< String, List< String > > > loadAdditionalTables( List<String>
 		final List< Map< String, List< String > > > additionalTables = new CopyOnWriteArrayList<>();
 
 		final long start = System.currentTimeMillis();
-		final ExecutorService executorService = Executors.newFixedThreadPool( N_THREADS );
-
+		final ExecutorService executorService = ThreadUtils.ioExecutorService;
+		final ArrayList< Future< ? > > futures = ThreadUtils.getFutures();
 		for ( String sourceName : sources )
 		{
-			executorService.execute( () -> {
-				Map< String, List< String > > columns =
-						loadAdditionalTable( sourceName, getTablePath( ( SegmentationSource ) getSource( sourceName ), table ) );
+			futures.add(
+				executorService.submit( () -> {
+					Map< String, List< String > > columns = loadAdditionalTable( sourceName, getTablePath( ( SegmentationSource ) getSource( sourceName ), table ) );
 				additionalTables.add( columns );
-			} );
+				} )
+			);
 		}
+		ThreadUtils.waitUntilFinished( futures );
 
-		Utils.waitUntilFinishedAndShutDown( executorService );
-
-		System.out.println( "Fetched " + sources.size() + " table(s) in " + (System.currentTimeMillis() - start) + " ms, using " + N_THREADS + " thread(s).");
+		System.out.println( "Fetched " + sources.size() + " table(s) in " + (System.currentTimeMillis() - start) + " ms, using " + ThreadUtils.N_IO_THREADS + " thread(s).");
 
 		return additionalTables;
 	}
 
+	public Map< String, SourceAndConverter< ? > > getSourceNameToTransformedSourceAndConverter()
+	{
+		return sourceNameToTransformedSourceAndConverter;
+	}
 
-	private ArrayList< List< TableRowImageSegment > > loadPrimarySegmentsTables(SegmentationSourceDisplay segmentationDisplay, String table )
+	private ArrayList< List< TableRowImageSegment > > loadPrimarySegmentsTables( SegmentationSourceDisplay segmentationDisplay, String table )
 	{
 		final List< String > segmentationDisplaySources = segmentationDisplay.getSources();
-		final ConcurrentHashMap< String, Set< Source > > sourceNameToRootSources = new ConcurrentHashMap();
+		final ConcurrentHashMap< String, Set< Source< ? > > > sourceNameToRootSources = new ConcurrentHashMap();
 
 		for ( String sourceName : segmentationDisplaySources )
 		{
-			Set< Source > rootSourceNames = ConcurrentHashMap.newKeySet();
-			Utils.fetchRootSources( getSourceAndConverter( sourceName ).getSpimSource(), rootSourceNames );
-			sourceNameToRootSources.put( sourceName, rootSourceNames );
+			Set< Source< ? > > rootSources = ConcurrentHashMap.newKeySet();
+			MoBIEUtils.fetchRootSources( getTransformedSourceAndConverter( sourceName ).getSpimSource(), rootSources );
+			sourceNameToRootSources.put( sourceName, rootSources );
 		}
 
-		// TODO: make parallel
+
 		final ArrayList< List< TableRowImageSegment > > primaryTables = new ArrayList<>();
+		final ArrayList< Future< ? > > futures = ThreadUtils.getFutures();
 		for ( String displayedSourceName : segmentationDisplaySources )
 		{
-			final Set< Source > rootSources = sourceNameToRootSources.get( displayedSourceName );
+			final Set< Source< ? > > rootSources = sourceNameToRootSources.get( displayedSourceName );
 			for ( Source rootSource : rootSources )
 			{
-				addPrimaryTable( table, primaryTables, rootSource.getName(), displayedSourceName );
+				futures.add( ThreadUtils.ioExecutorService.submit( () -> 				loadAndAddPrimaryTable( table, primaryTables, rootSource.getName() ) ) );
 			}
 		}
+		ThreadUtils.waitUntilFinished( futures );
 
 		return primaryTables;
 	}
 
-	private void addPrimaryTable( String tableName, ArrayList< List< TableRowImageSegment > > primaryTables, String tableSourceName, String displaySourceName )
+	private void loadAndAddPrimaryTable( String tableName, ArrayList< List< TableRowImageSegment > > primaryTables, String tableSourceName )
 	{
-		final List< TableRowImageSegment > primaryTable = loadImageSegmentsTable( tableSourceName, tableName, displaySourceName );
+		final List< TableRowImageSegment > primaryTable = loadImageSegmentsTable( tableSourceName, tableName );
 		primaryTables.add( primaryTable );
 	}
 
@@ -440,8 +514,8 @@ private Map< String, List< String > > createColumnsForMerging( List< TableRowIma
 		// deal with the fact that the label ids are sometimes
 		// stored as 1 and sometimes as 1.0
 		// after below operation they all will be 1.0, 2.0, ...
-		Utils.toDoubleStrings( segmentIdColumn );
-		Utils.toDoubleStrings( newColumns.get( TableColumnNames.SEGMENT_LABEL_ID ) );
+		MoBIEUtils.toDoubleStrings( segmentIdColumn );
+		MoBIEUtils.toDoubleStrings( newColumns.get( TableColumnNames.SEGMENT_LABEL_ID ) );
 
 		final Map< String, List< String > > columnsForMerging = TableColumns.createColumnsForMergingExcludingReferenceColumns( referenceColumns, newColumns );
 
@@ -510,7 +584,7 @@ public List< AnnotatedIntervalTableRow > loadAnnotatedIntervalTables( AnnotatedI
 		for ( String table : annotationDisplay.getTables() )
 		{
 			String tablePath = getTablePath( annotationDisplay.getTableDataFolder( TableDataFormat.TabDelimitedFile ), table );
-			tablePath = Utils.resolveTablePath( tablePath );
+			tablePath = MoBIEUtils.resolveTablePath( tablePath );
 			Logger.log( "Opening table:\n" + tablePath );
 			tables.add( TableColumns.stringColumnsFromTableFile( tablePath ) );
 		}
@@ -518,7 +592,7 @@ public List< AnnotatedIntervalTableRow > loadAnnotatedIntervalTables( AnnotatedI
 		// create primary AnnotatedIntervalTableRow table
 		final Map< String, List< String > > referenceTable = tables.get( 0 );
 		// TODO: The AnnotatedIntervalCreator does not need the sources, but just the source's real intervals
-		final AnnotatedIntervalCreator annotatedIntervalCreator = new AnnotatedIntervalCreator( referenceTable, annotationDisplay.getAnnotationIdToSources(), (String sourceName ) -> this.getSourceAndConverter( sourceName )  );
+		final AnnotatedIntervalCreator annotatedIntervalCreator = new AnnotatedIntervalCreator( referenceTable, annotationDisplay.getAnnotationIdToSources(), (String sourceName ) -> this.getTransformedSourceAndConverter( sourceName )  );
 		final List< AnnotatedIntervalTableRow > intervalTableRows = annotatedIntervalCreator.getAnnotatedIntervalTableRows();
 
 		final List< Map< String, List< String > > > additionalTables = tables.subList( 1, tables.size() );
@@ -531,22 +605,25 @@ public List< AnnotatedIntervalTableRow > loadAnnotatedIntervalTables( AnnotatedI
 		return intervalTableRows;
 	}
 
-	public void closeSourceAndConverter( SourceAndConverter< ? > sourceAndConverter )
+	public void closeSourceAndConverter( SourceAndConverter< ? > sourceAndConverter, boolean closeImgLoader )
 	{
 		SourceAndConverterServices.getBdvDisplayService().removeFromAllBdvs( sourceAndConverter );
 		String sourceName = sourceAndConverter.getSpimSource().getName();
-		final ImgLoader imgLoader = sourceNameToImgLoader.get( sourceName );
-		if ( imgLoader instanceof N5ImageLoader )
+
+		if ( closeImgLoader )
 		{
-			( ( N5ImageLoader ) imgLoader ).close();
-		} else if ( imgLoader instanceof N5OMEZarrImageLoader ) {
-            ((N5OMEZarrImageLoader) imgLoader).close();
-        }
+			final ImgLoader imgLoader = sourceNameToImgLoader.get( sourceName );
+			if ( imgLoader instanceof N5ImageLoader )
+			{
+				( ( N5ImageLoader ) imgLoader ).close();
+			} else if ( imgLoader instanceof N5OMEZarrImageLoader )
+			{
+				( ( N5OMEZarrImageLoader ) imgLoader ).close();
+			}
+		}
 
 		sourceNameToImgLoader.remove( sourceName );
 		SourceAndConverterServices.getSourceAndConverterService().remove( sourceAndConverter );
-
-		// TODO - when we support more image formats e.g. OME-ZARR, we should explicitly close their imgloaders here too
 	}
 
     public synchronized String getImagePath(ImageSource source) {
@@ -568,13 +645,18 @@ public synchronized String getImagePath(ImageSource source) {
         }
     }
 
-	public void addSourceAndConverters( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverters )
+	public void addTransformedSourceAndConverters( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverters )
+	{
+		sourceNameToTransformedSourceAndConverter.putAll( sourceNameToSourceAndConverters );
+	}
+
+	public SourceAndConverter< ? > getTransformedSourceAndConverter( String sourceName )
 	{
-		this.sourceNameToSourceAndConverter.putAll( sourceNameToSourceAndConverters );
+		return sourceNameToTransformedSourceAndConverter.get( sourceName );
 	}
 
-	public SourceAndConverter< ? > getSourceAndConverter( String sourceName )
+	public ArrayList< String > getProjectCommands()
 	{
-		return this.sourceNameToSourceAndConverter.get( sourceName );
+		return projectCommands;
 	}
 }
diff --git a/src/main/java/org/embl/mobie/viewer/MoBIEInfo.java b/src/main/java/org/embl/mobie/viewer/MoBIEInfo.java
index debdffd91..fc3e2b7e7 100644
--- a/src/main/java/org/embl/mobie/viewer/MoBIEInfo.java
+++ b/src/main/java/org/embl/mobie/viewer/MoBIEInfo.java
@@ -1,7 +1,7 @@
 package org.embl.mobie.viewer;
 
 import bdv.tools.HelpDialog;
-import de.embl.cba.tables.FileAndUrlUtils;
+import org.embl.mobie.io.util.FileAndUrlUtils;
 import de.embl.cba.tables.Help;
 import ij.IJ;
 
diff --git a/src/main/java/org/embl/mobie/viewer/MoBIESettings.java b/src/main/java/org/embl/mobie/viewer/MoBIESettings.java
index c065005dc..5d89bb228 100644
--- a/src/main/java/org/embl/mobie/viewer/MoBIESettings.java
+++ b/src/main/java/org/embl/mobie/viewer/MoBIESettings.java
@@ -1,6 +1,6 @@
 package org.embl.mobie.viewer;
 
-import org.embl.mobie.viewer.source.ImageDataFormat;
+import org.embl.mobie.io.ImageDataFormat;
 
 public class MoBIESettings
 {
@@ -65,8 +65,15 @@ public MoBIESettings publicationURL( String publicationURL )
 		return this;
 	}
 
+	public MoBIESettings s3AccessAndSecretKey( String[] s3AccessAndSecretKey )
+	{
+		this.values.s3AccessAndSecretKey = s3AccessAndSecretKey;
+		return this;
+	}
+
 	public static class Values
 	{
+		public String[] s3AccessAndSecretKey;
 		private String publicationURL;
 		private String dataset;
 		private String projectBranch = "main"; // project and images
@@ -123,5 +130,10 @@ public String getView()
 		{
 			return view;
 		}
+
+		public String[] getS3AccessAndSecretKey()
+		{
+			return s3AccessAndSecretKey;
+		}
 	}
 }
diff --git a/src/main/java/org/embl/mobie/viewer/Utils.java b/src/main/java/org/embl/mobie/viewer/MoBIEUtils.java
similarity index 76%
rename from src/main/java/org/embl/mobie/viewer/Utils.java
rename to src/main/java/org/embl/mobie/viewer/MoBIEUtils.java
index ee9f6e82f..8d79b225a 100644
--- a/src/main/java/org/embl/mobie/viewer/Utils.java
+++ b/src/main/java/org/embl/mobie/viewer/MoBIEUtils.java
@@ -6,47 +6,45 @@
 import bdv.util.ResampledSource;
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
-import de.embl.cba.bdv.utils.BdvUtils;
-import de.embl.cba.tables.FileAndUrlUtils;
+import bdv.viewer.ViewerPanel;
+import org.embl.mobie.io.util.FileAndUrlUtils;
 import de.embl.cba.tables.TableColumns;
 import de.embl.cba.tables.imagesegment.SegmentProperty;
 import de.embl.cba.tables.imagesegment.SegmentUtils;
 import de.embl.cba.tables.tablerow.TableRowImageSegment;
 import ij.IJ;
 import ij.gui.GenericDialog;
-import net.imglib2.*;
+import net.imglib2.FinalRealInterval;
+import net.imglib2.RealPoint;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.realtransform.Scale3D;
+import org.embl.mobie.viewer.playground.BdvPlaygroundUtils;
+import org.embl.mobie.viewer.source.LabelSource;
 import org.embl.mobie.viewer.transform.MergedGridSource;
+import org.embl.mobie.viewer.transform.TransformHelper;
 
 import javax.swing.*;
 import javax.swing.filechooser.FileNameExtensionFilter;
-import java.io.*;
+import java.io.File;
 import java.net.URI;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 
-import static de.embl.cba.bdv.utils.BdvUtils.getBdvWindowCenter;
-import static org.embl.mobie.viewer.ui.SwingHelper.selectionDialog;
 import static de.embl.cba.tables.imagesegment.SegmentUtils.BB_MAX_Z;
 import static de.embl.cba.tables.imagesegment.SegmentUtils.BB_MIN_Z;
+import static org.embl.mobie.viewer.ui.SwingHelpers.selectionDialog;
 
-public abstract class Utils
+public abstract class MoBIEUtils
 {
 	static { net.imagej.patcher.LegacyInjector.preinit(); }
 
-	public static void waitUntilFinishedAndShutDown( ExecutorService executorService )
-	{
-		executorService.shutdown();
-		try {
-			executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
-		} catch (InterruptedException e) {
-		}
-	}
-
 	public static int[] asInts( long[] longs) {
 		int[] ints = new int[longs.length];
 
@@ -70,11 +68,11 @@ public static long[] asLongs( int[] ints) {
 	}
 
 	/**
-	 * Recursively fetch the names of all root sources
+	 * Recursively fetch all root sources
 	 * @param source
 	 * @param rootSources
 	 */
-	public static void fetchRootSources( Source< ? > source, Set< Source > rootSources )
+	public static void fetchRootSources( Source< ? > source, Set< Source< ? > > rootSources )
 	{
 		if ( source instanceof SpimSource )
 		{
@@ -86,6 +84,12 @@ else if ( source instanceof TransformedSource )
 
 			fetchRootSources( wrappedSource, rootSources );
 		}
+		else if (  source instanceof LabelSource )
+		{
+			final Source< ? > wrappedSource = (( LabelSource ) source).getWrappedSource();
+
+			fetchRootSources( wrappedSource, rootSources );
+		}
 		else if (  source instanceof MergedGridSource )
 		{
 			final MergedGridSource< ? > mergedGridSource = ( MergedGridSource ) source;
@@ -147,8 +151,7 @@ private static String chooseCommonFileName( List<String> directories, String obj
 
 	public static FileLocation loadFromProjectOrFileSystemDialog() {
 		final GenericDialog gd = new GenericDialog("Choose source");
-		gd.addChoice("Load from", new String[]{FileLocation.Project.toString(),
-				FileLocation.FileSystem.toString()}, FileLocation.Project.toString());
+		gd.addChoice("Load from", new String[]{FileLocation.Project.toString(), FileLocation.FileSystem.toString()}, FileLocation.Project.toString());
 		gd.showDialog();
 		if (gd.wasCanceled()) return null;
 		return FileLocation.valueOf(gd.getNextChoice());
@@ -188,59 +191,72 @@ public static String selectPathFromProject( String directory, String objectName
 
 	public static File lastSelectedDir;
 
-	private static void setLastSelectedDir( String filePath ) {
-		File selectedFile = new File( filePath );
-		if ( selectedFile.isDirectory() ) {
-			lastSelectedDir = selectedFile;
-		} else {
-			lastSelectedDir = selectedFile.getParentFile();
+	// objectName is used for the dialog labels e.g. 'table', 'bookmark' etc...
+	public static String selectFilePath( String fileExtension, String objectName, boolean open ) {
+		final JFileChooser jFileChooser = new JFileChooser( lastSelectedDir );
+		if ( fileExtension != null ) {
+			jFileChooser.setFileFilter(new FileNameExtensionFilter(fileExtension, fileExtension));
 		}
+		jFileChooser.setDialogTitle( "Select " + objectName );
+		return selectPath( jFileChooser, open );
 	}
 
 	// objectName is used for the dialog labels e.g. 'table', 'bookmark' etc...
-	public static String selectOpenPathFromFileSystem( String objectName, String fileExtension ) {
+	public static String selectDirectoryPath( String objectName, boolean open ) {
 		final JFileChooser jFileChooser = new JFileChooser( lastSelectedDir );
-		jFileChooser.setFileFilter(new FileNameExtensionFilter( fileExtension, fileExtension ));
-		return selectOpenPathFromFileSystem( objectName, jFileChooser );
+		jFileChooser.setFileSelectionMode( JFileChooser.DIRECTORIES_ONLY );
+		jFileChooser.setDialogTitle( "Select " + objectName );
+		return selectPath( jFileChooser, open );
 	}
 
-	public static String selectOpenPathFromFileSystem( String objectName ) {
-		final JFileChooser jFileChooser = new JFileChooser( lastSelectedDir );
-		return selectOpenPathFromFileSystem( objectName, jFileChooser );
+	private static String selectPath( JFileChooser jFileChooser, boolean open ) {
+		final AtomicBoolean isDone = new AtomicBoolean( false );
+		final String[] path = new String[ 1 ];
+		Runnable r = () -> {
+			if ( open ) {
+				path[0] = selectOpenPathFromFileSystem( jFileChooser);
+			} else {
+				path[0] = selectSavePathFromFileSystem( jFileChooser );
+			}
+			isDone.set( true );
+		};
+
+		SwingUtilities.invokeLater(r);
+
+		while ( ! isDone.get() ){
+			try {
+				Thread.sleep( 100 );
+			} catch ( InterruptedException e )
+			{ e.printStackTrace(); }
+		};
+		return path[ 0 ];
 	}
 
-	public static String selectOpenPathFromFileSystem( String objectName, JFileChooser jFileChooser ) {
+	private static void setLastSelectedDir( String filePath ) {
+		File selectedFile = new File( filePath );
+		if ( selectedFile.isDirectory() ) {
+			lastSelectedDir = selectedFile;
+		} else {
+			lastSelectedDir = selectedFile.getParentFile();
+		}
+	}
+
+	public static String selectOpenPathFromFileSystem( JFileChooser jFileChooser ) {
 		String filePath = null;
-		jFileChooser.setDialogTitle( "Select " + objectName );
 		if (jFileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
 			filePath = jFileChooser.getSelectedFile().getAbsolutePath();
 			setLastSelectedDir( filePath );
 		}
-
 		return filePath;
 	}
 
-	public static String selectOpenDirFromFileSystem( String objectName ) {
-		final JFileChooser jFileChooser = new JFileChooser( lastSelectedDir );
-		return selectOpenDirFromFileSystem( objectName, jFileChooser );
-	}
-
-	public static String selectOpenDirFromFileSystem( String objectName, JFileChooser jFileChooser ) {
-		jFileChooser.setFileSelectionMode( JFileChooser.DIRECTORIES_ONLY );
-		return selectOpenPathFromFileSystem( objectName, jFileChooser );
-	}
-
-	// objectName is used for the dialog labels e.g. 'table', 'bookmark' etc...
-	public static String selectSavePathFromFileSystem( String fileExtension )
+	public static String selectSavePathFromFileSystem( JFileChooser jFileChooser )
 	{
 		String filePath = null;
-		final JFileChooser jFileChooser = new JFileChooser( lastSelectedDir );
-		jFileChooser.setFileFilter(new FileNameExtensionFilter( fileExtension, fileExtension ));
 		if (jFileChooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
 			filePath = jFileChooser.getSelectedFile().getAbsolutePath();
 			setLastSelectedDir( filePath );
 		}
-
 		return filePath;
 	}
 
@@ -350,30 +366,12 @@ public static Map< SegmentProperty, List< String > > createSegmentPropertyToColu
 
 	public static String createNormalisedViewerTransformString( BdvHandle bdv, double[] position )
 	{
-		final AffineTransform3D view = createNormalisedViewerTransform( bdv, position );
+		final AffineTransform3D view = TransformHelper.createNormalisedViewerTransform( bdv.getViewerPanel(), position );
 		final String replace = view.toString().replace( "3d-affine: (", "" ).replace( ")", "" );
 		final String collect = Arrays.stream( replace.split( "," ) ).map( x -> "n" + x.trim() ).collect( Collectors.joining( "," ) );
 		return collect;
 	}
 
-	public static AffineTransform3D createNormalisedViewerTransform( BdvHandle bdv, double[] position )
-	{
-		final AffineTransform3D view = new AffineTransform3D();
-		bdv.getViewerPanel().state().getViewerTransform( view );
-
-		// translate position to upper left corner of the Window (0,0)
-		final AffineTransform3D translate = new AffineTransform3D();
-		translate.translate( position );
-		view.preConcatenate( translate.inverse() );
-
-		// divide by window width
-		final int bdvWindowWidth = BdvUtils.getBdvWindowWidth( bdv );
-		final Scale3D scale = new Scale3D( 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth );
-		view.preConcatenate( scale );
-
-		return view;
-	}
-
 	public static double[] getMousePosition( BdvHandle bdv )
 	{
 		final RealPoint realPoint = new RealPoint( 2 );
@@ -383,22 +381,6 @@ public static double[] getMousePosition( BdvHandle bdv )
 		return doubles;
 	}
 
-	public static AffineTransform3D createUnnormalizedViewerTransform( AffineTransform3D normalisedTransform, BdvHandle bdv )
-	{
-		final AffineTransform3D transform = normalisedTransform.copy();
-
-		final int bdvWindowWidth = BdvUtils.getBdvWindowWidth( bdv );
-		final Scale3D scale = new Scale3D( 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth );
-		transform.preConcatenate( scale.inverse() );
-
-		AffineTransform3D translate = new AffineTransform3D();
-		translate.translate( getBdvWindowCenter( bdv ) );
-
-		transform.preConcatenate( translate );
-
-		return transform;
-	}
-
 	public static AffineTransform3D asAffineTransform3D( double[] doubles )
 	{
 		final AffineTransform3D view = new AffineTransform3D( );
diff --git a/src/main/java/org/embl/mobie/viewer/Project.java b/src/main/java/org/embl/mobie/viewer/Project.java
index c50920aa3..bd534c3f7 100644
--- a/src/main/java/org/embl/mobie/viewer/Project.java
+++ b/src/main/java/org/embl/mobie/viewer/Project.java
@@ -1,13 +1,13 @@
 package org.embl.mobie.viewer;
 
-import org.embl.mobie.viewer.source.ImageDataFormat;
+import org.embl.mobie.io.ImageDataFormat;
 
 import java.util.List;
 
 public class Project
 {
 	private List< String > datasets;
-	private List<ImageDataFormat> imageDataFormats;
+	private List< ImageDataFormat > imageDataFormats;
 	private String defaultDataset;
 	private String specVersion;
 
@@ -32,7 +32,7 @@ public String getSpecVersion()
 
 	public void setSpecVersion( String specVersion ) { this.specVersion = specVersion; }
 
-	public void setImageDataFormats(List<ImageDataFormat> imageDataFormats) {
+	public void setImageDataFormats( List< ImageDataFormat > imageDataFormats) {
 		this.imageDataFormats = imageDataFormats;
 	}
 
diff --git a/src/main/java/org/embl/mobie/viewer/SourceNameEncoder.java b/src/main/java/org/embl/mobie/viewer/SourceNameEncoder.java
new file mode 100644
index 000000000..606e3b9f3
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/SourceNameEncoder.java
@@ -0,0 +1,77 @@
+package org.embl.mobie.viewer;
+
+import net.imglib2.type.numeric.integer.UnsignedIntType;
+import net.imglib2.type.volatiles.VolatileUnsignedIntType;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class SourceNameEncoder
+{
+	private static Map< String, Long > nameToLong = new HashMap< String, Long >();
+	private static Map< Long, String > longToName = new HashMap< Long, String >();
+	private static long count = 0;
+
+	public static Integer valueBits = 16;
+
+	public static synchronized void addNames( Collection< String > names )
+	{
+		for ( String name : names )
+		{
+			addName( name );
+		}
+	}
+
+	public static void addName( String name )
+	{
+		if ( ! nameToLong.containsKey( name ) )
+		{
+			nameToLong.put( name, count );
+			longToName.put( count, name );
+			count++;
+		}
+	}
+
+	public static String getName( final UnsignedIntType unsignedIntType )
+	{
+		final long imageIndex = unsignedIntType.get() >> valueBits;
+		final String name = longToName.get( imageIndex );
+		return name;
+	}
+
+	public static String getName( final VolatileUnsignedIntType unsignedIntType )
+	{
+		return getName( unsignedIntType.get().get() );
+	}
+
+	public static String getName( final long l )
+	{
+		final long imageIndex = l >> valueBits;
+		final String name = longToName.get( imageIndex );
+		return name;
+	}
+
+	public static long getValue( final UnsignedIntType unsignedIntType )
+	{
+		return getValue( unsignedIntType.get() );
+	}
+
+	public static long getValue( final VolatileUnsignedIntType unsignedIntType )
+	{
+		return getValue( unsignedIntType.get().get() );
+	}
+
+	public static long getValue( final long l )
+	{
+		final long value = l & 0x0000FFFF;
+		return value;
+	}
+
+	public static void encodeName( final UnsignedIntType value, final String name )
+	{
+		final long l = value.get();
+		final long encoded = l + ( nameToLong.get( name ) << valueBits );
+		value.set( encoded );
+	}
+}
diff --git a/src/main/java/org/embl/mobie/viewer/ThreadUtils.java b/src/main/java/org/embl/mobie/viewer/ThreadUtils.java
new file mode 100644
index 000000000..7cba8a84a
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/ThreadUtils.java
@@ -0,0 +1,45 @@
+package org.embl.mobie.viewer;
+
+import bdv.util.volatiles.SharedQueue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+public class ThreadUtils
+{
+	public static final int N_FETCHER_THREADS = Runtime.getRuntime().availableProcessors() - 1;
+	public static final SharedQueue sharedQueue = new SharedQueue( N_FETCHER_THREADS );
+
+	public static final int N_IO_THREADS = 8;
+	public static final ExecutorService ioExecutorService = Executors.newFixedThreadPool( N_IO_THREADS );
+
+	public static final int N_THREADS = Runtime.getRuntime().availableProcessors() - 1;;
+	public static final ExecutorService executorService = Executors.newFixedThreadPool( N_THREADS );
+
+	public static void waitUntilFinished( List< Future< ? > > futures )
+	{
+		for ( Future< ? > future : futures )
+		{
+			try
+			{
+				future.get();
+			} catch ( InterruptedException e )
+			{
+				e.printStackTrace();
+			} catch ( ExecutionException e )
+			{
+				e.printStackTrace();
+			}
+		}
+	}
+
+	public static ArrayList< Future< ? > > getFutures()
+	{
+		return new ArrayList<>();
+	}
+}
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/BdvCircleOverlay.java b/src/main/java/org/embl/mobie/viewer/bdv/BdvCircleOverlay.java
new file mode 100644
index 000000000..d171f843a
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/bdv/BdvCircleOverlay.java
@@ -0,0 +1,84 @@
+package org.embl.mobie.viewer.bdv;
+
+import bdv.util.Affine3DHelpers;
+import bdv.util.BdvOverlay;
+import net.imglib2.RealPoint;
+import net.imglib2.realtransform.AffineTransform3D;
+
+import java.awt.*;
+import java.util.ArrayList;
+import java.util.List;
+
+public class BdvCircleOverlay extends BdvOverlay
+{
+	final List< RealPoint > points;
+	private final double radius;
+
+	public BdvCircleOverlay( double[] location, double radius )
+	{
+		super();
+		this.points = new ArrayList< RealPoint >();
+		this.points.add( new RealPoint( location ) );
+		this.radius = radius;
+	}
+
+	public BdvCircleOverlay( RealPoint point, double radius )
+	{
+		super();
+		this.points = new ArrayList< RealPoint >();
+		this.points.add( point );
+		this.radius = radius;
+	}
+
+	public BdvCircleOverlay( List< RealPoint > points, double radius )
+	{
+		super();
+		this.points = points;
+		this.radius = radius;
+	}
+
+	public void addCircle( double[] point )
+	{
+		this.points.add( new RealPoint( point ) );
+	}
+
+	@Override
+	protected void draw( final Graphics2D g )
+	{
+		final AffineTransform3D transform = new AffineTransform3D();
+		getCurrentTransform3D( transform );
+
+		final double[] globalPosition = new double[ 3 ];
+		final double[] viewPosition = new double[ 3 ];
+		final double[] scale = new double[ 3 ];
+		final int[] viewDiameter = new int[ 2 ];
+		for ( final RealPoint point : points )
+		{
+			point.localize( globalPosition );
+			transform.apply( globalPosition, viewPosition );
+			for ( int d = 0; d < 3; d++ )
+				scale[ d ] = Affine3DHelpers.extractScale( transform, d );
+
+			final double depth = Math.abs( viewPosition[ 2 ] ) / scale[ 2 ];
+			setViewDiameter( depth, scale, viewDiameter );
+			if ( viewDiameter[ 0 ] == 0 || viewDiameter[ 1 ] == 0 )
+				return;
+			final int x = ( int ) ( viewPosition[ 0 ] - 0.5 * viewDiameter [ 0 ] );
+			final int y = ( int ) ( viewPosition[ 1 ] - 0.5 * viewDiameter [ 1 ] );
+			final Color color = new Color( 255, 0, 255, 150 );
+			g.setColor( color );
+			g.drawOval( x, y, viewDiameter[ 0 ], viewDiameter[ 1 ] );
+		}
+	}
+
+	private void setViewDiameter( final double depth, double[] scaleXY, int[] viewerDiameter )
+	{
+		for ( int d = 0; d < 2; d++ )
+		{
+			if ( depth > radius )
+				viewerDiameter[ d ] = 0;
+			else
+				viewerDiameter[ d ] = (int) Math.round( scaleXY[ d ] *  2 * Math.sqrt( radius * radius - depth * depth ) );
+		}
+	}
+}
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/BdvGlobalMousePositionProvider.java b/src/main/java/org/embl/mobie/viewer/bdv/BdvGlobalMousePositionProvider.java
new file mode 100644
index 000000000..c751568d7
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/bdv/BdvGlobalMousePositionProvider.java
@@ -0,0 +1,34 @@
+package org.embl.mobie.viewer.bdv;
+
+import bdv.util.BdvHandle;
+import net.imglib2.RealPoint;
+
+public class BdvGlobalMousePositionProvider
+{
+	private final RealPoint realPoint;
+	private final int timePoint;
+
+	public BdvGlobalMousePositionProvider( BdvHandle bdvHandle )
+	{
+		realPoint = new RealPoint( 3 );
+		bdvHandle.getBdvHandle().getViewerPanel().getGlobalMouseCoordinates( realPoint );
+		timePoint = bdvHandle.getViewerPanel().state().getCurrentTimepoint();
+	}
+
+	public RealPoint getPositionAsRealPoint()
+	{
+		return realPoint;
+	}
+
+	public double[] getPositionAsDoubles()
+	{
+		double[] doubles = new double[ 3 ];
+		realPoint.localize( doubles );
+		return doubles;
+	}
+
+	public int getTimePoint()
+	{
+		return timePoint;
+	}
+}
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/BdvMousePositionProvider.java b/src/main/java/org/embl/mobie/viewer/bdv/BdvMousePositionProvider.java
deleted file mode 100644
index ce174e117..000000000
--- a/src/main/java/org/embl/mobie/viewer/bdv/BdvMousePositionProvider.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.embl.mobie.viewer.bdv;
-
-import bdv.util.BdvHandle;
-import net.imglib2.RealPoint;
-
-public class BdvMousePositionProvider
-{
-	private final RealPoint position;
-	private final int timePoint;
-
-	public BdvMousePositionProvider( BdvHandle bdvHandle )
-	{
-		// Gets mouse location in space (global 3D coordinates) and time
-		position = new RealPoint( 3 );
-		bdvHandle.getBdvHandle().getViewerPanel().getGlobalMouseCoordinates( position );
-		timePoint = bdvHandle.getViewerPanel().state().getCurrentTimepoint();
-	}
-
-	public RealPoint getPosition()
-	{
-		return position;
-	}
-
-	public int getTimePoint()
-	{
-		return timePoint;
-	}
-}
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/BdvPointOverlay.java b/src/main/java/org/embl/mobie/viewer/bdv/BdvPointOverlay.java
deleted file mode 100644
index 3e9390032..000000000
--- a/src/main/java/org/embl/mobie/viewer/bdv/BdvPointOverlay.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package org.embl.mobie.viewer.bdv;
-
-import bdv.util.BdvOverlay;
-import net.imglib2.RealPoint;
-import net.imglib2.realtransform.AffineTransform3D;
-
-import java.awt.*;
-import java.util.ArrayList;
-import java.util.List;
-
-
-public class BdvPointOverlay extends BdvOverlay
-{
-	final List< RealPoint > points;
-	private final double depthOfField;
-
-	public BdvPointOverlay( double[] point, double depthOfField )
-	{
-		super();
-		this.points = new ArrayList< RealPoint >();
-		this.points.add( new RealPoint( point ) );
-		this.depthOfField = depthOfField;
-	}
-
-	public BdvPointOverlay( RealPoint point, double depthOfField )
-	{
-		super();
-		this.points = new ArrayList< RealPoint >();
-		this.points.add( point );
-		this.depthOfField = depthOfField;
-	}
-
-	public BdvPointOverlay( List< RealPoint > points, double depthOfField )
-	{
-		super();
-		this.points = points;
-		this.depthOfField = depthOfField;
-	}
-
-	public void addPoint( double[] point )
-	{
-		this.points.add( new RealPoint( point ) );
-	}
-
-	@Override
-	protected void draw( final Graphics2D g )
-	{
-		final AffineTransform3D t = new AffineTransform3D();
-		getCurrentTransform3D( t );
-
-		final double[] lPos = new double[ 3 ];
-		final double[] gPos = new double[ 3 ];
-		for ( final RealPoint p : points )
-		{
-			p.localize( lPos );
-			t.apply( lPos, gPos );
-			final int size = getSize( gPos[ 2 ] );
-			final int x = ( int ) ( gPos[ 0 ] - 0.5 * size );
-			final int y = ( int ) ( gPos[ 1 ] - 0.5 * size );
-			g.setColor( getColor( gPos[ 2 ] ) );
-			g.fillOval( x, y, size, size );
-		}
-	}
-
-	private Color getColor( final double depth )
-	{
-		int alpha = 200 - ( int ) Math.round( Math.abs( depth ) );
-
-		if ( alpha < 150 )
-			alpha = 150;
-
-		return new Color( 255, 0, 255, alpha );
-	}
-
-	private int getSize( final double depth )
-	{
-		return ( int ) Math.max( 10, 30 - Math.round( Math.abs( depth ) / depthOfField  ) );
-	}
-
-}
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/MobieSerializableBdvOptions.java b/src/main/java/org/embl/mobie/viewer/bdv/MobieSerializableBdvOptions.java
index 39fd32918..d1bc1a091 100644
--- a/src/main/java/org/embl/mobie/viewer/bdv/MobieSerializableBdvOptions.java
+++ b/src/main/java/org/embl/mobie/viewer/bdv/MobieSerializableBdvOptions.java
@@ -26,8 +26,7 @@ public class MobieSerializableBdvOptions {
 
     public AxisOrder axisOrder = AxisOrder.DEFAULT;
 
-    // Extra for the playground
-    public boolean interpolate = false;
+    public boolean interpolate = true;
 
     public int numTimePoints = 1;
 
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/ScreenShotMaker.java b/src/main/java/org/embl/mobie/viewer/bdv/ScreenShotMaker.java
index a85846b40..9cd8e3795 100644
--- a/src/main/java/org/embl/mobie/viewer/bdv/ScreenShotMaker.java
+++ b/src/main/java/org/embl/mobie/viewer/bdv/ScreenShotMaker.java
@@ -55,6 +55,7 @@
 import net.imglib2.view.Views;
 import sc.fiji.bdvpg.bdv.BdvHandleHelper;
 import sc.fiji.bdvpg.scijava.services.SourceAndConverterBdvDisplayService;
+import sc.fiji.bdvpg.services.ISourceAndConverterService;
 import sc.fiji.bdvpg.services.SourceAndConverterServices;
 
 import java.awt.*;
@@ -77,35 +78,32 @@ public class ScreenShotMaker
     static { net.imagej.patcher.LegacyInjector.preinit(); }
 
     private final BdvHandle bdvHandle;
-    private double physicalPixelSpacingInXY = 1;
+    private final ISourceAndConverterService sacService;
+    private double samplingXY = 1;
     private String physicalUnit = "Pixels";
     private boolean sourceInteractionWithViewerPlaneOnly2D = false; // TODO: maybe remove in the future
-    ImagePlus screenShot = null;
-    private CompositeImage rawImageData = null;
-    private final SourceAndConverterBdvDisplayService displayService;
-    //private final ISourceAndConverterService sacService;
-    private long captureWidth;
-    private long captureHeight;
+    ImagePlus rgbImagePlus = null;
+    private CompositeImage compositeImagePlus = null;
+    private long[] captureImageSizeInPixels = new long[2];
 
     public ScreenShotMaker( BdvHandle bdvHandle) {
         this.bdvHandle = bdvHandle;
-        this.displayService = SourceAndConverterServices.getBdvDisplayService();
-        //this.sacService = SourceAndConverterServices.getSourceAndConverterService();
+        this.sacService = SourceAndConverterServices.getSourceAndConverterService();
     }
 
     public void setPhysicalPixelSpacingInXY(double spacing, String unit) {
-        this.screenShot = null;
-        this.physicalPixelSpacingInXY = spacing;
+        this.rgbImagePlus = null;
+        this.samplingXY = spacing;
         this.physicalUnit = unit;
     }
 
     public void setSourceInteractionWithViewerPlaneOnly2D(boolean sourceInteractionWithViewerPlaneOnly2D) {
-        this.screenShot = null;
+        this.rgbImagePlus = null;
         this.sourceInteractionWithViewerPlaneOnly2D = sourceInteractionWithViewerPlaneOnly2D;
     }
 
     private void process() {
-        if (screenShot != null) {
+        if ( rgbImagePlus != null) {
             return;
         }
         createScreenShot();
@@ -113,28 +111,46 @@ private void process() {
 
     public ImagePlus getRgbScreenShot() {
         process();
-        return screenShot;
+        return rgbImagePlus;
     }
 
     public CompositeImage getRawScreenShot()
     {
         process();
-        return rawImageData;
+        return compositeImagePlus;
     }
 
-    private void createScreenShot()
+    public static long[] getCaptureImageSizeInPixels( BdvHandle bdvHandle, double samplingXY )
     {
-        final AffineTransform3D viewerTransform = new AffineTransform3D();
-        bdvHandle.getViewerPanel().state().getViewerTransform( viewerTransform );
-
         final double viewerVoxelSpacing = getViewerVoxelSpacing( bdvHandle );
-        double dxy = physicalPixelSpacingInXY / viewerVoxelSpacing;
 
+        final double[] bdvWindowPhysicalSize = getBdvWindowPhysicalSize( bdvHandle, viewerVoxelSpacing );
+
+        final long[] capturePixelSize = new long[ 2 ];
+        for ( int d = 0; d < 2; d++ )
+        {
+            capturePixelSize[ d ] = ( long ) ( Math.ceil( bdvWindowPhysicalSize[ d ] / samplingXY ) );
+        }
+
+        return capturePixelSize;
+    }
+
+    private static double[] getBdvWindowPhysicalSize( BdvHandle bdvHandle, double viewerVoxelSpacing )
+    {
+        final double[] bdvWindowPhysicalSize = new double[ 2 ];
         final int w = bdvHandle.getViewerPanel().getWidth();
         final int h = bdvHandle.getViewerPanel().getHeight();
+        bdvWindowPhysicalSize[ 0 ] = w * viewerVoxelSpacing;
+        bdvWindowPhysicalSize[ 1 ] = h * viewerVoxelSpacing;
+        return bdvWindowPhysicalSize;
+    }
+
+    private void createScreenShot()
+    {
+        final AffineTransform3D viewerTransform = new AffineTransform3D();
+        bdvHandle.getViewerPanel().state().getViewerTransform( viewerTransform );
 
-        captureWidth = ( long ) Math.ceil( w / dxy );
-        captureHeight = ( long ) Math.ceil( h / dxy );
+        captureImageSizeInPixels = getCaptureImageSizeInPixels( bdvHandle, samplingXY );
 
         final ArrayList< RandomAccessibleInterval< UnsignedShortType > > rawCaptures = new ArrayList<>();
         final ArrayList< RandomAccessibleInterval< ARGBType > > argbSources = new ArrayList<>();
@@ -159,14 +175,14 @@ private void createScreenShot()
         for ( SourceAndConverter< ?  > sac : sacs )
         {
             final RandomAccessibleInterval< UnsignedShortType > rawCapture
-                    = ArrayImgs.unsignedShorts( captureWidth, captureHeight );
+                    = ArrayImgs.unsignedShorts( captureImageSizeInPixels[ 0 ], captureImageSizeInPixels[ 1 ] );
             final RandomAccessibleInterval< ARGBType > argbCapture
-                    = ArrayImgs.argbs( captureWidth, captureHeight );
+                    = ArrayImgs.argbs( captureImageSizeInPixels[ 0 ], captureImageSizeInPixels[ 1 ]  );
 
             Source< ? > source = sac.getSpimSource();
             final Converter converter = sac.getConverter();
 
-            final int level = getLevel( source, physicalPixelSpacingInXY );
+            final int level = getLevel( source, samplingXY );
             final AffineTransform3D sourceTransform =
                     BdvHandleHelper.getSourceTransform( source, t, level );
 
@@ -174,9 +190,10 @@ private void createScreenShot()
             viewerToSourceTransform.preConcatenate( viewerTransform.inverse() );
             viewerToSourceTransform.preConcatenate( sourceTransform.inverse() );
 
-            // TODO: Once we have a logic for segmentation images, make this choice depend on this
-            boolean interpolate = true;
+            final double canvasStepSize = samplingXY / getViewerVoxelSpacing( bdvHandle );
 
+            // TODO: Once we have a logic for segmentation images, make this choice depend on this
+            final boolean interpolate = true;
             Grids.collectAllContainedIntervals(
                     Intervals.dimensionsAsLongArray( argbCapture ),
                     new int[]{100, 100}).parallelStream().forEach( interval ->
@@ -200,6 +217,7 @@ private void createScreenShot()
 
                 final ARGBType argbType = new ARGBType();
 
+                // iterate through the target image in pixel units
                 while ( rawCaptureCursor.hasNext() )
                 {
                     rawCaptureCursor.fwd();
@@ -208,10 +226,10 @@ private void createScreenShot()
                     argbCaptureAccess.setPosition( rawCaptureCursor );
 
                     // canvasPosition is the position on the canvas, in calibrated units
-                    // dxy is the step size that is needed to get the desired resolution in the
-                    // output image
-                    canvasPosition[ 0 ] *= dxy;
-                    canvasPosition[ 1 ] *= dxy;
+                    // dxy is the step size that is needed to get
+                    // the desired resolution in the output image
+                    canvasPosition[ 0 ] *= canvasStepSize;
+                    canvasPosition[ 1 ] *= canvasStepSize;
 
                     viewerToSourceTransform.apply( canvasPosition, sourceRealPosition );
 
@@ -223,19 +241,19 @@ private void createScreenShot()
             rawCaptures.add( rawCapture );
             argbSources.add( argbCapture );
             // colors.add( getSourceColor( bdv, sourceIndex ) ); Not used, show GrayScale
-            displayRanges.add( BdvHandleHelper.getDisplayRange( displayService.getConverterSetup( sac ) ) );
+            displayRanges.add( BdvHandleHelper.getDisplayRange( sacService.getConverterSetup( sac ) ) );
         }
 
         final double[] voxelSpacing = new double[ 3 ];
         for ( int d = 0; d < 2; d++ )
-            voxelSpacing[ d ] = physicalPixelSpacingInXY;
+            voxelSpacing[ d ] = samplingXY;
 
-        voxelSpacing[ 2 ] = viewerVoxelSpacing; // TODO: What to put here?
+        voxelSpacing[ 2 ] = getViewerVoxelSpacing( bdvHandle ); // TODO: What to put here?
 
         if ( rawCaptures.size() > 0 )
         {
-            screenShot = createImagePlus( physicalUnit, argbSources, voxelSpacing, sacs );
-            rawImageData  = createCompositeImage( voxelSpacing, physicalUnit, rawCaptures, colors, displayRanges );
+            rgbImagePlus = createImagePlus( physicalUnit, argbSources, voxelSpacing, sacs );
+            compositeImagePlus = createCompositeImage( voxelSpacing, physicalUnit, rawCaptures, colors, displayRanges );
         }
     }
 
@@ -311,7 +329,7 @@ private ImagePlus createImagePlus(
             double[] voxelSpacing,
             List< SourceAndConverter< ? > > sacs )
     {
-        final RandomAccessibleInterval< ARGBType > argbTarget = ArrayImgs.argbs( captureWidth, captureHeight );
+        final RandomAccessibleInterval< ARGBType > argbTarget = ArrayImgs.argbs( captureImageSizeInPixels[ 0 ], captureImageSizeInPixels[ 1 ]  );
 
         project( argbSources, argbTarget, sacs );
 
@@ -388,7 +406,7 @@ private Cursor< ARGBType >[] getCursors( ArrayList< RandomAccessibleInterval< AR
 
     private ImagePlus asImagePlus( RandomAccessibleInterval< ARGBType > argbCapture, String physicalUnit, double[] voxelSpacing )
     {
-        final ImagePlus rgbImage = ImageJFunctions.wrap( argbCapture, "View Capture RGB" );
+        final ImagePlus rgbImage = ImageJFunctions.wrap( argbCapture, "RGB" );
 
         IJ.run( rgbImage,
                 "Properties...",
@@ -409,7 +427,7 @@ public static CompositeImage createCompositeImage(
     {
         final RandomAccessibleInterval< UnsignedShortType > stack = Views.stack( rais );
 
-        final ImagePlus imp = ImageJFunctions.wrap( stack, "View Capture Raw" );
+        final ImagePlus imp = ImageJFunctions.wrap( stack, "Multi-Channel" );
 
         // duplicate: otherwise it is virtual and cannot be modified
         final ImagePlus dup = new Duplicator().run( imp );
@@ -434,7 +452,7 @@ public static CompositeImage createCompositeImage(
             compositeImage.setDisplayRange( range[ 0 ], range[ 1 ] );
         }
 
-        compositeImage.setTitle( "View Capture Raw" );
+        compositeImage.setTitle( "Multi-Channel" );
         return compositeImage;
     }
 
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/SourcesAtMousePositionSupplier.java b/src/main/java/org/embl/mobie/viewer/bdv/SourcesAtMousePositionSupplier.java
index ff9b5927f..2dfa6740c 100644
--- a/src/main/java/org/embl/mobie/viewer/bdv/SourcesAtMousePositionSupplier.java
+++ b/src/main/java/org/embl/mobie/viewer/bdv/SourcesAtMousePositionSupplier.java
@@ -1,6 +1,7 @@
 package org.embl.mobie.viewer.bdv;
 
 import bdv.util.BdvHandle;
+import bdv.util.PlaceHolderSource;
 import bdv.viewer.SourceAndConverter;
 import sc.fiji.bdvpg.services.SourceAndConverterServices;
 import sc.fiji.bdvpg.sourceandconverter.SourceAndConverterHelper;
@@ -24,11 +25,12 @@ public SourcesAtMousePositionSupplier( BdvHandle bdvHandle, boolean is2D )
 	@Override
 	public Collection< SourceAndConverter< ? > > get()
 	{
-		final BdvMousePositionProvider positionProvider = new BdvMousePositionProvider( bdvHandle );
+		final BdvGlobalMousePositionProvider positionProvider = new BdvGlobalMousePositionProvider( bdvHandle );
 
 		final List< SourceAndConverter< ? > > sourceAndConverters = SourceAndConverterServices.getBdvDisplayService().getSourceAndConverterOf( bdvHandle )
 				.stream()
-				.filter( sac -> SourceAndConverterHelper.isPositionWithinSourceInterval( sac, positionProvider.getPosition(), positionProvider.getTimePoint(), is2D ) )
+				.filter( sac -> ! ( sac.getSpimSource() instanceof PlaceHolderSource ) )
+				.filter( sac -> SourceAndConverterHelper.isPositionWithinSourceInterval( sac, positionProvider.getPositionAsRealPoint(), positionProvider.getTimePoint(), is2D ) )
 				.filter( sac -> SourceAndConverterServices.getBdvDisplayService().isVisible( sac, bdvHandle ) )
 				.collect( Collectors.toList() );
 
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/ViewerTransformLogger.java b/src/main/java/org/embl/mobie/viewer/bdv/ViewerTransformLogger.java
index d9ee6d4d1..ad516fdf6 100644
--- a/src/main/java/org/embl/mobie/viewer/bdv/ViewerTransformLogger.java
+++ b/src/main/java/org/embl/mobie/viewer/bdv/ViewerTransformLogger.java
@@ -2,14 +2,16 @@
 
 import bdv.util.BdvHandle;
 import com.google.gson.Gson;
+import de.embl.cba.bdv.utils.BdvUtils;
 import de.embl.cba.bdv.utils.Logger;
-import org.embl.mobie.viewer.playground.PlaygroundUtils;
-import org.embl.mobie.viewer.Utils;
+import org.embl.mobie.viewer.playground.BdvPlaygroundUtils;
 import org.embl.mobie.viewer.serialize.JsonHelper;
 import org.embl.mobie.viewer.transform.AffineViewerTransform;
+import org.embl.mobie.viewer.transform.NormalVectorViewerTransform;
 import org.embl.mobie.viewer.transform.NormalizedAffineViewerTransform;
 import org.embl.mobie.viewer.transform.PositionViewerTransform;
 import net.imglib2.realtransform.AffineTransform3D;
+import org.embl.mobie.viewer.transform.TransformHelper;
 import org.scijava.plugin.Parameter;
 import org.scijava.plugin.Plugin;
 import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
@@ -17,7 +19,7 @@
 @Plugin( type = BdvPlaygroundActionCommand.class, name = ViewerTransformLogger.NAME, menuPath = ViewerTransformLogger.NAME )
 public class ViewerTransformLogger implements BdvPlaygroundActionCommand
 {
-	public static final String NAME = "BDV - Log Current View";
+	public static final String NAME = "Log Current View";
 
 	@Parameter
 	BdvHandle bdv;
@@ -28,7 +30,7 @@ public void run()
 		new Thread( () -> {
 
 			final int timepoint = bdv.getViewerPanel().state().getCurrentTimepoint();
-			final double[] position = PlaygroundUtils.getWindowCentreInCalibratedUnits( bdv );
+			final double[] position = BdvPlaygroundUtils.getWindowCentreInCalibratedUnits( bdv );
 
 			// position
 			final PositionViewerTransform positionViewerTransform = new PositionViewerTransform( position, timepoint );
@@ -39,10 +41,13 @@ public void run()
 			final AffineViewerTransform affineViewerTransform = new AffineViewerTransform( affineTransform3D.getRowPackedCopy(), timepoint );
 
 			// normalized affine
-			final AffineTransform3D normalisedViewerTransform = Utils.createNormalisedViewerTransform( bdv,
-					PlaygroundUtils.getWindowCentreInPixelUnits( bdv ) );
+			final AffineTransform3D normalisedViewerTransform = TransformHelper.createNormalisedViewerTransform( bdv.getViewerPanel() );
 			final NormalizedAffineViewerTransform normalizedAffineViewerTransform = new NormalizedAffineViewerTransform( normalisedViewerTransform.getRowPackedCopy(), timepoint );
 
+			// normal vector
+			double[] currentNormalVector = BdvUtils.getCurrentViewNormalVector( bdv );
+			final NormalVectorViewerTransform normalVectorViewerTransform = new NormalVectorViewerTransform( currentNormalVector, timepoint );
+
 			// print
 			final Gson gson = JsonHelper.buildGson( false );
 			Logger.log( "# Current view " );
@@ -52,6 +57,7 @@ public void run()
 			Logger.log( gson.toJson( positionViewerTransform ) );
 			Logger.log( gson.toJson( affineViewerTransform ) );
 			Logger.log( gson.toJson( normalizedAffineViewerTransform ) );
+			Logger.log( gson.toJson( normalVectorViewerTransform ) );
 
 		} ).start();
 	}
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/view/AnnotatedIntervalSliceView.java b/src/main/java/org/embl/mobie/viewer/bdv/view/AnnotatedIntervalSliceView.java
index 5e0e76a0e..01dd989cf 100644
--- a/src/main/java/org/embl/mobie/viewer/bdv/view/AnnotatedIntervalSliceView.java
+++ b/src/main/java/org/embl/mobie/viewer/bdv/view/AnnotatedIntervalSliceView.java
@@ -62,16 +62,11 @@ private void show( )
 		bdvHandle.getViewerPanel().addTimePointListener(  converter );
 	}
 
-	public void close()
+	public void close( boolean closeImgLoader )
 	{
 		for ( SourceAndConverter< ? > sourceAndConverter : display.sourceNameToSourceAndConverter.values() )
 		{
-			SourceAndConverterServices.getBdvDisplayService().removeFromAllBdvs( sourceAndConverter );
-		}
-
-		for ( SourceAndConverter< ? > sourceAndConverter : display.sourceNameToSourceAndConverter.values() )
-		{
-			moBIE.closeSourceAndConverter( sourceAndConverter );
+			moBIE.closeSourceAndConverter( sourceAndConverter, closeImgLoader );
 		}
 		display.sourceNameToSourceAndConverter.clear();
 	};
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/view/ImageSliceView.java b/src/main/java/org/embl/mobie/viewer/bdv/view/ImageSliceView.java
index 8c9deded1..69c8bab1f 100644
--- a/src/main/java/org/embl/mobie/viewer/bdv/view/ImageSliceView.java
+++ b/src/main/java/org/embl/mobie/viewer/bdv/view/ImageSliceView.java
@@ -16,8 +16,6 @@
 import net.imglib2.converter.Converter;
 import net.imglib2.type.numeric.ARGBType;
 import net.imglib2.type.numeric.RealType;
-import sc.fiji.bdvpg.scijava.services.SourceAndConverterBdvDisplayService;
-import sc.fiji.bdvpg.scijava.services.SourceAndConverterService;
 import sc.fiji.bdvpg.services.SourceAndConverterServices;
 import sc.fiji.bdvpg.sourceandconverter.display.ColorChanger;
 import sc.fiji.bdvpg.sourceandconverter.display.ConverterChanger;
@@ -31,11 +29,9 @@ public class ImageSliceView
 {
 	static { net.imagej.patcher.LegacyInjector.preinit(); }
 
-	private final SourceAndConverterBdvDisplayService displayService;
 	private final MoBIE moBIE;
 	private final ImageSourceDisplay display;
 	private final BdvHandle bdvHandle;
-	private final SourceAndConverterService sacService;
 
 	public ImageSliceView( MoBIE moBIE, ImageSourceDisplay display, BdvHandle bdvHandle )
 	{
@@ -43,9 +39,6 @@ public ImageSliceView( MoBIE moBIE, ImageSourceDisplay display, BdvHandle bdvHan
 		this.display = display;
 		this.bdvHandle = bdvHandle;
 
-		displayService = SourceAndConverterServices.getBdvDisplayService();
-		sacService = ( SourceAndConverterService ) SourceAndConverterServices.getSourceAndConverterService();
-
 		show();
 	}
 
@@ -54,7 +47,7 @@ private void show( )
 		// show
 		Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter = new HashMap<>();
 		for ( String name : display.getSources() ) {
-			sourceNameToSourceAndConverter.put( name, moBIE.getSourceAndConverter( name ) );
+			sourceNameToSourceAndConverter.put( name, moBIE.getTransformedSourceAndConverter( name ) );
 		}
 
 		display.sourceNameToSourceAndConverter = new HashMap<>();
@@ -74,10 +67,10 @@ private void show( )
 				SourceAndConverterServices.getSourceAndConverterService().setMetadata( sourceAndConverter, BlendingMode.BLENDING_MODE, display.getBlendingMode() );
 
 			// show
-			displayService.show( bdvHandle, display.isVisible(), sourceAndConverter );
+			SourceAndConverterServices.getBdvDisplayService().show( bdvHandle, display.isVisible(), sourceAndConverter );
 
 			// adapt contrast limits
-			final ConverterSetup converterSetup = displayService.getConverterSetup( sourceAndConverter );
+			final ConverterSetup converterSetup = SourceAndConverterServices.getSourceAndConverterService().getConverterSetup( sourceAndConverter );
 			converterSetup.setDisplayRange( display.getContrastLimits()[ 0 ], display.getContrastLimits()[ 1 ] );
 
 			// register	the actually displayed sac (for serialisation)
@@ -127,11 +120,11 @@ private void adaptImageColor( SourceAndConverter< ? > sourceAndConverter )
 		}
 	}
 
-	public void close( )
+	public void close( boolean closeImgLoader )
 	{
 		for ( SourceAndConverter< ? > sourceAndConverter : display.sourceNameToSourceAndConverter.values() )
 		{
-			moBIE.closeSourceAndConverter( sourceAndConverter );
+			moBIE.closeSourceAndConverter( sourceAndConverter, closeImgLoader );
 		}
 		display.sourceNameToSourceAndConverter.clear();
 	}
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/view/SegmentationSliceView.java b/src/main/java/org/embl/mobie/viewer/bdv/view/SegmentationSliceView.java
index 79f2e5aa7..25a6c9ca4 100644
--- a/src/main/java/org/embl/mobie/viewer/bdv/view/SegmentationSliceView.java
+++ b/src/main/java/org/embl/mobie/viewer/bdv/view/SegmentationSliceView.java
@@ -3,7 +3,6 @@
 import bdv.util.BdvHandle;
 import bdv.viewer.SourceAndConverter;
 import bdv.viewer.TimePointListener;
-import org.embl.mobie.io.n5.source.LabelSource;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.bdv.render.BlendingMode;
 import org.embl.mobie.viewer.color.OpacityAdjuster;
@@ -14,6 +13,8 @@
 import de.embl.cba.tables.select.SelectionListener;
 import mpicbg.spim.data.sequence.VoxelDimensions;
 import net.imglib2.realtransform.AffineTransform3D;
+import org.embl.mobie.viewer.source.LabelSource;
+import org.embl.mobie.viewer.transform.MergedGridSource;
 import sc.fiji.bdvpg.bdv.BdvHandleHelper;
 import sc.fiji.bdvpg.bdv.navigate.ViewerTransformChanger;
 import sc.fiji.bdvpg.scijava.services.SourceAndConverterBdvDisplayService;
@@ -48,7 +49,7 @@ private void show( )
 
 		Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter = new HashMap<>();
 		for ( String name : display.getSources() ) {
-			sourceNameToSourceAndConverter.put( name, moBIE.getSourceAndConverter( name ) );
+			sourceNameToSourceAndConverter.put( name, moBIE.getTransformedSourceAndConverter( name ) );
 		}
 
 		// convert to labelSource
@@ -81,10 +82,8 @@ private void show( )
 		for ( String name: sourceNameToSourceAndConverter.keySet() )
 		{
 			SourceAndConverter< ? > sourceAndConverter = sourceNameToSourceAndConverter.get( name );
-			LabelConverter< S > labelConverter = new LabelConverter(
-					display.segmentAdapter,
-					sourceAndConverter.getSpimSource().getName(),
-					display.coloringModel );
+
+			LabelConverter< S > labelConverter = getLabelConverter( sourceAndConverter );
 
 			SourceAndConverter< ? > sourceAndLabelConverter = asSourceAndLabelConverter( sourceAndConverter, labelConverter );
 
@@ -94,6 +93,28 @@ private void show( )
 		return sourceNameToLabelSourceAndConverter;
 	}
 
+	private LabelConverter< S > getLabelConverter( SourceAndConverter< ? > sourceAndConverter )
+	{
+		if ( MergedGridSource.instanceOf( sourceAndConverter ) )
+		{
+			// The source name is not the one from which the
+			// image segments should be fetched.
+			// Thus, the constructor where the source name
+			// is determined from encoding in the label is chosen.
+
+			return new LabelConverter(
+					display.segmentAdapter,
+					display.coloringModel );
+		}
+		else
+		{
+			return new LabelConverter(
+					display.segmentAdapter,
+					sourceAndConverter.getSpimSource().getName(),
+					display.coloringModel );
+		}
+	}
+
 	private SourceAndConverter asSourceAndLabelConverter( SourceAndConverter< ? > sourceAndConverter, LabelConverter labelConverter )
 	{
 		LabelSource volatileLabelSource = new LabelSource( sourceAndConverter.asVolatile().getSpimSource() );
@@ -102,16 +123,11 @@ private SourceAndConverter asSourceAndLabelConverter( SourceAndConverter< ? > so
 		return new SourceAndConverter( labelSource, labelConverter, volatileSourceAndConverter );
 	}
 
-	public void close()
+	public void close( boolean closeImgLoader )
 	{
 		for ( SourceAndConverter< ? > sourceAndConverter : display.sourceNameToSourceAndConverter.values() )
 		{
-			SourceAndConverterServices.getBdvDisplayService().removeFromAllBdvs( sourceAndConverter );
-		}
-
-		for ( SourceAndConverter< ? > sourceAndConverter : display.sourceNameToSourceAndConverter.values() )
-		{
-			moBIE.closeSourceAndConverter( sourceAndConverter );
+			moBIE.closeSourceAndConverter( sourceAndConverter, closeImgLoader );
 		}
 		display.sourceNameToSourceAndConverter.clear();
 	};
@@ -151,7 +167,7 @@ public synchronized void focusEvent( S selection )
 	private void adaptPosition( double[] position, String sourceName )
 	{
 		// get source transform
-		final SourceAndConverter< ? > sourceAndConverter = display.sourceNameToSourceAndConverter.get( sourceName );
+		final SourceAndConverter< ? > sourceAndConverter = moBIE.getTransformedSourceAndConverter( sourceName );
 		AffineTransform3D sourceTransform = new AffineTransform3D();
 		sourceAndConverter.getSpimSource().getSourceTransform( 0,0, sourceTransform );
 
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/view/SliceViewer.java b/src/main/java/org/embl/mobie/viewer/bdv/view/SliceViewer.java
index 91a2d9bed..b9b362bc4 100644
--- a/src/main/java/org/embl/mobie/viewer/bdv/view/SliceViewer.java
+++ b/src/main/java/org/embl/mobie/viewer/bdv/view/SliceViewer.java
@@ -6,11 +6,15 @@
 import org.embl.mobie.viewer.bdv.MobieSerializableBdvOptions;
 import org.embl.mobie.viewer.bdv.SourcesAtMousePositionSupplier;
 import org.embl.mobie.viewer.bdv.ViewerTransformLogger;
+import org.embl.mobie.viewer.command.ImagePlusExportCommand;
+import org.embl.mobie.viewer.command.ManualRegistrationCommand;
+import org.embl.mobie.viewer.command.BigWarpRegistrationCommand;
+import org.embl.mobie.viewer.command.ImageVolumeRenderingConfiguratorCommand;
 import org.embl.mobie.viewer.command.NonSelectedSegmentsOpacityAdjusterCommand;
 import org.embl.mobie.viewer.command.SegmentsVolumeRenderingConfiguratorCommand;
 import org.embl.mobie.viewer.command.SelectedSegmentsColorConfiguratorCommand;
 import org.embl.mobie.viewer.command.SourceAndConverterBlendingModeChangerCommand;
-import org.embl.mobie.viewer.playground.ScreenShotMakerCommand;
+import org.embl.mobie.viewer.command.ScreenShotMakerCommand;
 import org.embl.mobie.viewer.segment.BdvSegmentSelector;
 import org.embl.mobie.viewer.command.RandomColorSeedChangerCommand;
 import org.embl.mobie.viewer.view.ViewManager;
@@ -33,21 +37,23 @@ public class SliceViewer implements Supplier< BdvHandle >
 	public static final String UNDO_SEGMENT_SELECTIONS = "Undo Segment Selections [ Ctrl Shift N ]";
 	public static final String CHANGE_RANDOM_COLOR_SEED = "Change Random Color Seed";
 	public static final String LOAD_ADDITIONAL_VIEWS = "Load Additional Views";
-	public static final String SAVE_CURRENT_SETTINGS_AS_VIEW = "Save Current Settings As View";
+	public static final String SAVE_CURRENT_SETTINGS_AS_VIEW = "Save Current View";
 	private final SourceAndConverterBdvDisplayService sacDisplayService;
 	private BdvHandle bdvHandle;
 	private final boolean is2D;
 	private final ViewManager viewManager;
 	private final int timepoints;
+	private final ArrayList< String > projectCommands;
 
 	private SourceAndConverterContextMenuClickBehaviour contextMenu;
 	private final SourceAndConverterService sacService;
 
-	public SliceViewer( boolean is2D, ViewManager viewManager, int timepoints )
+	public SliceViewer( boolean is2D, ViewManager viewManager, int timepoints, ArrayList< String > projectCommands )
 	{
 		this.is2D = is2D;
 		this.viewManager = viewManager;
 		this.timepoints = timepoints;
+		this.projectCommands = projectCommands;
 
 		sacService = ( SourceAndConverterService ) SourceAndConverterServices.getSourceAndConverterService();
 		sacDisplayService = SourceAndConverterServices.getBdvDisplayService();
@@ -78,10 +84,11 @@ private void installContextMenuAndKeyboardShortCuts( )
 			segmentBdvSelector.clearSelection();
 		} );
 
-		sacService.registerAction( sacService.getCommandName( RandomColorSeedChangerCommand.class ), sourceAndConverters -> {
-			// TODO: Maybe only do this for the sacs at the mouse position
-			RandomColorSeedChangerCommand.incrementRandomColorSeed( sourceAndConverters );
-		} );
+		// TODO: why is this needed here (and not just in below list?)
+//		sacService.registerAction( sacService.getCommandName( RandomColorSeedChangerCommand.class ), sourceAndConverters -> {
+//			// TODO: Maybe only do this for the sacs at the mouse position
+//			RandomColorSeedChangerCommand.incrementRandomColorSeed( sourceAndConverters );
+//		} );
 
 		sacService.registerAction( LOAD_ADDITIONAL_VIEWS, sourceAndConverters -> {
 			// TODO: Maybe only do this for the sacs at the mouse position
@@ -95,20 +102,31 @@ private void installContextMenuAndKeyboardShortCuts( )
 
 		final Set< String > actionsKeys = sacService.getActionsKeys();
 
-		final String[] actions = {
-				sacService.getCommandName( ScreenShotMakerCommand.class ),
-				sacService.getCommandName( ViewerTransformLogger.class ),
-				sacService.getCommandName( SourceAndConverterBlendingModeChangerCommand.class ),
-				sacService.getCommandName( RandomColorSeedChangerCommand.class ),
-				sacService.getCommandName( NonSelectedSegmentsOpacityAdjusterCommand.class ),
-				sacService.getCommandName( SelectedSegmentsColorConfiguratorCommand.class ),
-				sacService.getCommandName( SegmentsVolumeRenderingConfiguratorCommand.class ),
-				UNDO_SEGMENT_SELECTIONS,
-				LOAD_ADDITIONAL_VIEWS,
-				SAVE_CURRENT_SETTINGS_AS_VIEW
-		};
-
-		contextMenu = new SourceAndConverterContextMenuClickBehaviour( bdvHandle, new SourcesAtMousePositionSupplier( bdvHandle, is2D ), actions );
+		final ArrayList< String > actions = new ArrayList< String >();
+		actions.add( sacService.getCommandName( ScreenShotMakerCommand.class ) );
+		actions.add( sacService.getCommandName( ImagePlusExportCommand.class ) );
+		actions.add( sacService.getCommandName( ViewerTransformLogger.class ) );
+		actions.add( sacService.getCommandName( BigWarpRegistrationCommand.class ) );
+		actions.add( sacService.getCommandName( ManualRegistrationCommand.class ) );
+		actions.add( sacService.getCommandName( SourceAndConverterBlendingModeChangerCommand.class ) );
+		actions.add( sacService.getCommandName( RandomColorSeedChangerCommand.class ) );
+		actions.add( sacService.getCommandName( NonSelectedSegmentsOpacityAdjusterCommand.class ) );
+		actions.add( sacService.getCommandName( SelectedSegmentsColorConfiguratorCommand.class ) );
+		actions.add( sacService.getCommandName( SegmentsVolumeRenderingConfiguratorCommand.class ) );
+		actions.add( sacService.getCommandName( ImageVolumeRenderingConfiguratorCommand.class ) );
+		actions.add( UNDO_SEGMENT_SELECTIONS );
+		actions.add( LOAD_ADDITIONAL_VIEWS );
+		actions.add( SAVE_CURRENT_SETTINGS_AS_VIEW );
+
+		if ( projectCommands != null )
+		{
+			for ( String commandName : projectCommands )
+			{
+				actions.add( commandName );
+			}
+		}
+
+		contextMenu = new SourceAndConverterContextMenuClickBehaviour( bdvHandle, new SourcesAtMousePositionSupplier( bdvHandle, is2D ), actions.toArray( new String[0] ) );
 
 		Behaviours behaviours = new Behaviours( new InputTriggerConfig() );
 		behaviours.behaviour( contextMenu, "Context menu", "button3", "shift P");
diff --git a/src/main/java/org/embl/mobie/viewer/color/LabelConverter.java b/src/main/java/org/embl/mobie/viewer/color/LabelConverter.java
index 6b97b512a..32d0f780f 100644
--- a/src/main/java/org/embl/mobie/viewer/color/LabelConverter.java
+++ b/src/main/java/org/embl/mobie/viewer/color/LabelConverter.java
@@ -29,6 +29,9 @@
 package org.embl.mobie.viewer.color;
 
 import bdv.viewer.TimePointListener;
+import net.imglib2.type.numeric.integer.UnsignedIntType;
+import net.imglib2.type.volatiles.VolatileUnsignedIntType;
+import org.embl.mobie.viewer.SourceNameEncoder;
 import org.embl.mobie.viewer.segment.SegmentAdapter;
 import de.embl.cba.tables.imagesegment.ImageSegment;
 import net.imglib2.Volatile;
@@ -45,6 +48,15 @@ public class LabelConverter< S extends ImageSegment > implements Converter< Real
 	private int timePointIndex = 0;
 	private double opacity = 1.0;
 
+	public LabelConverter(
+			SegmentAdapter< S > segmentAdapter,
+			MoBIEColoringModel< S > coloringModel )
+	{
+		this.segmentAdapter = segmentAdapter;
+		this.imageId = null; // No imageId given => decode from pixel value
+		this.coloringModel = coloringModel;
+	}
+
 	public LabelConverter(
 			SegmentAdapter< S > segmentAdapter,
 			String imageId,
@@ -60,33 +72,47 @@ public void convert( RealType label, ARGBType color )
 	{
 		if ( label instanceof Volatile )
 		{
-			if ( ! ( ( Volatile ) label ).isValid() )
+			if ( !( ( Volatile ) label ).isValid() )
 			{
 				color.set( 0 );
 				return;
 			}
 		}
 
-		if ( label.getRealDouble() == 0 )
+		if ( imageId == null )
 		{
-			color.set( 0 );
-			return;
-		}
+			final long labelId = SourceNameEncoder.getValue( ( VolatileUnsignedIntType ) label );
 
-		final S imageSegment = segmentAdapter.getSegment( label.getRealDouble(), timePointIndex, imageId );
+			if ( labelId == 0 )
+			{
+				color.set( 0 );
+				return;
+			}
 
-		if ( imageSegment == null )
-		{
-			color.set( 0 );
-			return;
+			final String imageId = SourceNameEncoder.getName( ( VolatileUnsignedIntType ) label );
+			S segment = segmentAdapter.getSegment( labelId, timePointIndex, imageId );
+			setColorBySegment( color, segment );
 		}
 		else
 		{
-			coloringModel.convert( imageSegment, color );
-			final int alpha = ARGBType.alpha( color.get() );
-			color.mul( alpha / 255.0 );
+			final double labelId = label.getRealDouble();
+
+			if ( labelId == 0 )
+			{
+				color.set( 0 );
+				return;
+			}
+
+			final S segment = segmentAdapter.getSegment( labelId, timePointIndex, imageId );
+			setColorBySegment( color, segment );
 		}
+	}
 
+	private void setColorBySegment( ARGBType color, S imageSegment )
+	{
+		coloringModel.convert( imageSegment, color );
+		final int alpha = ARGBType.alpha( color.get() );
+		color.mul( alpha / 255.0 );
 		color.mul( opacity );
 	}
 
diff --git a/src/main/java/org/embl/mobie/viewer/color/LazyLabelConverter.java b/src/main/java/org/embl/mobie/viewer/color/LazyLabelConverter.java
new file mode 100644
index 000000000..9fe5d04c4
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/color/LazyLabelConverter.java
@@ -0,0 +1,114 @@
+/*-
+ * #%L
+ * TODO
+ * %%
+ * Copyright (C) 2018 - 2020 EMBL
+ * %%
+ * 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 org.embl.mobie.viewer.color;
+
+import de.embl.cba.bdv.utils.lut.GlasbeyARGBLut;
+import de.embl.cba.tables.color.LazyCategoryColoringModel;
+import net.imglib2.Volatile;
+import net.imglib2.converter.Converter;
+import net.imglib2.display.ColorConverter;
+import net.imglib2.type.numeric.ARGBType;
+import net.imglib2.type.numeric.RealType;
+
+public class LazyLabelConverter implements Converter<RealType<?>, ARGBType>, ColorConverter {
+    private final LazyCategoryColoringModel<Double> coloringModel;
+    private ARGBType singleColor;
+    private double max = 500;
+
+    public LazyLabelConverter() {
+        this.coloringModel = new LazyCategoryColoringModel<>(new GlasbeyARGBLut(255));
+    }
+
+    @Override
+    public void convert(RealType label, ARGBType color) {
+        if (label instanceof Volatile) {
+            Volatile<?> volatileLabel = (Volatile<?>) label;
+            if (!(volatileLabel).isValid()) {
+                color.set(0);
+                return;
+            }
+        }
+
+        final double realDouble = label.getRealDouble();
+
+        if (realDouble == 0) {
+            color.set(0);
+            return;
+        }
+
+        if (singleColor != null) {
+            color.set(singleColor.get());
+            return;
+        }
+
+        coloringModel.convert(realDouble, color);
+        color.mul(100 / max); // changes the intensity
+    }
+
+    public LazyCategoryColoringModel<Double> getColoringModel() {
+        return coloringModel;
+    }
+
+    public void setSingleColor(ARGBType argbType) {
+        singleColor = argbType;
+    }
+
+    @Override
+    public ARGBType getColor() {
+        return null;
+    }
+
+    @Override
+    public void setColor(ARGBType c) {
+    }
+
+    @Override
+    public boolean supportsColor() {
+        return false;
+    }
+
+    @Override
+    public double getMin() {
+        return 0;
+    }
+
+    @Override
+    public void setMin(double min) {
+    }
+
+    @Override
+    public double getMax() {
+        return max;
+    }
+
+    @Override
+    public void setMax(double max) {
+        this.max = max;
+    }
+}
diff --git a/src/main/java/org/embl/mobie/viewer/command/BigWarpRegistrationCommand.java b/src/main/java/org/embl/mobie/viewer/command/BigWarpRegistrationCommand.java
new file mode 100644
index 000000000..bb996ce25
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/command/BigWarpRegistrationCommand.java
@@ -0,0 +1,151 @@
+package org.embl.mobie.viewer.command;
+
+import bdv.gui.TransformTypeSelectDialog;
+import bdv.tools.brightness.ConverterSetup;
+import bdv.tools.transformation.TransformedSource;
+import bdv.util.BdvHandle;
+import bdv.viewer.BigWarpViewerPanel;
+import bdv.viewer.SourceAndConverter;
+import bdv.viewer.TransformListener;
+import bigwarp.BigWarp;
+import bigwarp.transforms.BigWarpTransform;
+import ij.gui.NonBlockingGenericDialog;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.realtransform.InvertibleRealTransform;
+import org.embl.mobie.viewer.transform.TransformHelper;
+import org.scijava.plugin.Parameter;
+import org.scijava.plugin.Plugin;
+import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
+import sc.fiji.bdvpg.scijava.services.SourceAndConverterBdvDisplayService;
+import sc.fiji.bdvpg.services.ISourceAndConverterService;
+import sc.fiji.bdvpg.services.SourceAndConverterServices;
+import sc.fiji.bdvpg.sourceandconverter.register.BigWarpLauncher;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = CommandConstants.CONTEXT_MENU_ITEMS_ROOT + "Transform>Registration - BigWarp")
+public class BigWarpRegistrationCommand implements BdvPlaygroundActionCommand, TransformListener< InvertibleRealTransform >
+{
+	@Parameter
+	BdvHandle bdvHandle;
+
+	@Parameter(label = "Fixed Source(s)")
+	SourceAndConverter[] fixedSources;
+
+	@Parameter(label = "Moving Source(s)")
+	SourceAndConverter[] movingSources;
+
+	private BigWarp bigWarp;
+	private Map< SourceAndConverter< ? >, AffineTransform3D > sacToOriginalFixedTransform;
+	private List< SourceAndConverter > movingSacs;
+	private List< SourceAndConverter > fixedSacs;
+
+	private ISourceAndConverterService sacService;
+	private SourceAndConverterBdvDisplayService bdvDisplayService;
+
+	@Override
+	public void run()
+	{
+		// FIXME put all of this into a new thread?
+
+		sacService = SourceAndConverterServices.getSourceAndConverterService();
+		bdvDisplayService = SourceAndConverterServices.getBdvDisplayService();
+
+		movingSacs = Arrays.stream( movingSources ).collect( Collectors.toList() );
+		fixedSacs = Arrays.stream( fixedSources ).collect( Collectors.toList() );
+
+		storeOriginalTransforms( movingSacs );
+
+		List< ConverterSetup > converterSetups = Arrays.stream( movingSources ).map( src -> sacService.getConverterSetup(src)).collect( Collectors.toList() );
+		converterSetups.addAll( Arrays.stream( fixedSources ).map( src -> sacService.getConverterSetup( src) ).collect( Collectors.toList() ) );
+
+		BigWarpLauncher bigWarpLauncher = new BigWarpLauncher( movingSacs, fixedSacs, "Big Warp", converterSetups);
+		bigWarpLauncher.run();
+
+		bdvDisplayService.pairClosing( bigWarpLauncher.getBdvHandleQ(), bigWarpLauncher.getBdvHandleP() );
+
+		bigWarp = bigWarpLauncher.getBigWarp();
+
+		final AffineTransform3D normalisedViewerTransform = TransformHelper.createNormalisedViewerTransform( bdvHandle.getViewerPanel() );
+		applyViewerTransform( normalisedViewerTransform, bigWarp.getViewerFrameQ().getViewerPanel() );
+		applyViewerTransform( normalisedViewerTransform, bigWarp.getViewerFrameP().getViewerPanel() );
+		bigWarp.setTransformType( TransformTypeSelectDialog.AFFINE );
+		bigWarp.addTransformListener( this );
+
+		new Thread( () -> showDialog() ).start();
+	}
+
+	private void applyViewerTransform( AffineTransform3D normalisedViewerTransform, BigWarpViewerPanel viewerPanel )
+	{
+		viewerPanel.state().setViewerTransform( TransformHelper.createUnnormalizedViewerTransform( normalisedViewerTransform, viewerPanel ) );
+	}
+
+	private void showDialog()
+	{
+		final NonBlockingGenericDialog dialog = new NonBlockingGenericDialog( "Registration - BigWarp" );
+		dialog.addMessage( "Landmark based affine, similarity, rigid and translation transformations.\nPlease read the BigWarp help.\n" + "Press [ OK ] to close BigWarp and apply the current registration in MoBIE.");
+		dialog.showDialog();
+		if ( dialog.wasCanceled() )
+		{
+			resetMovingTransforms();
+			bigWarp.closeAll();
+		}
+		else
+		{
+			setMovingTransforms();
+			bigWarp.closeAll();
+		}
+	}
+
+	private void resetMovingTransforms()
+	{
+		for ( SourceAndConverter movingSac : movingSacs )
+		{
+			( ( TransformedSource) movingSac.getSpimSource() ).setFixedTransform( sacToOriginalFixedTransform.get( movingSac ) );
+		}
+		bdvHandle.getViewerPanel().requestRepaint();
+	}
+
+	private void storeOriginalTransforms( List< SourceAndConverter > movingSacs )
+	{
+		sacToOriginalFixedTransform = new HashMap<>();
+		for ( SourceAndConverter movingSac : movingSacs )
+		{
+			final AffineTransform3D fixedTransform = new AffineTransform3D();
+			( ( TransformedSource ) movingSac.getSpimSource()).getFixedTransform( fixedTransform );
+			sacToOriginalFixedTransform.put( movingSac, fixedTransform );
+		}
+	}
+
+	@Override
+	public void transformChanged( InvertibleRealTransform transform )
+	{
+		final String transformType = bigWarp.getTransformType();
+		if ( transformType.equals( TransformTypeSelectDialog.TPS ) )
+		{
+			System.err.println( TransformTypeSelectDialog.TPS + "is currently not supported by MoBIE please choose any of the other transform types by selecting one of the BigWarp windows and pressing F2.");
+		}
+		else
+		{
+			//setMovingTransforms();
+		}
+	}
+
+	private void setMovingTransforms()
+	{
+		final BigWarpTransform bwTransform = bigWarp.getBwTransform();
+		final AffineTransform3D bwAffineTransform = bwTransform.affine3d();
+		for ( SourceAndConverter< ? > movingSource : movingSources )
+		{
+			final AffineTransform3D combinedTransform = sacToOriginalFixedTransform.get( movingSource ).copy();
+			combinedTransform.preConcatenate( bwAffineTransform.copy().inverse() );
+			final TransformedSource< ? > transformedSource = ( TransformedSource< ? > ) movingSource.getSpimSource();
+			transformedSource.setFixedTransform( combinedTransform );
+		}
+		bdvHandle.getViewerPanel().requestRepaint();
+	}
+}
diff --git a/src/main/java/org/embl/mobie/viewer/command/CommandConstants.java b/src/main/java/org/embl/mobie/viewer/command/CommandConstants.java
new file mode 100644
index 000000000..b1813b74b
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/command/CommandConstants.java
@@ -0,0 +1,7 @@
+package org.embl.mobie.viewer.command;
+
+public abstract class CommandConstants
+{
+	public static final String MOBIE_PLUGIN_ROOT = "Plugins>MoBIE>";
+	public static final String CONTEXT_MENU_ITEMS_ROOT = "Plugins>Context Menu Items>Do not use>MoBIE>";
+}
diff --git a/src/main/java/org/embl/mobie/viewer/command/CreateNewMoBIEProjectCommand.java b/src/main/java/org/embl/mobie/viewer/command/CreateNewMoBIEProjectCommand.java
index 45b991cdf..885d32aa3 100644
--- a/src/main/java/org/embl/mobie/viewer/command/CreateNewMoBIEProjectCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/CreateNewMoBIEProjectCommand.java
@@ -9,12 +9,12 @@
 import java.io.File;
 import java.io.IOException;
 
-import static org.embl.mobie.viewer.ui.UserInterfaceHelper.*;
+import static org.embl.mobie.viewer.ui.UserInterfaceHelpers.*;
 
-@Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Create>Create New MoBIE Project..." )
+@Plugin(type = Command.class, menuPath = CommandConstants.MOBIE_PLUGIN_ROOT + "Create>Create New MoBIE Project..." )
 public class CreateNewMoBIEProjectCommand implements Command {
 
-	  static { net.imagej.patcher.LegacyInjector.preinit(); }
+    static { net.imagej.patcher.LegacyInjector.preinit(); }
 
     @Parameter( label= "Choose a project name:")
     public String projectName;
diff --git a/src/main/java/org/embl/mobie/viewer/command/EditMoBIEProjectCommand.java b/src/main/java/org/embl/mobie/viewer/command/EditMoBIEProjectCommand.java
index 720d8fca0..2b649e765 100644
--- a/src/main/java/org/embl/mobie/viewer/command/EditMoBIEProjectCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/EditMoBIEProjectCommand.java
@@ -11,7 +11,7 @@
 
 import static org.scijava.ItemVisibility.MESSAGE;
 
-@Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Create>Edit MoBIE Project..." )
+@Plugin(type = Command.class, menuPath = CommandConstants.MOBIE_PLUGIN_ROOT + "Create>Edit MoBIE Project..." )
 public class EditMoBIEProjectCommand implements Command
 {
 	  static { net.imagej.patcher.LegacyInjector.preinit(); }
diff --git a/src/main/java/org/embl/mobie/viewer/command/ImagePlusExportCommand.java b/src/main/java/org/embl/mobie/viewer/command/ImagePlusExportCommand.java
new file mode 100644
index 000000000..56ace9750
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/command/ImagePlusExportCommand.java
@@ -0,0 +1,191 @@
+package org.embl.mobie.viewer.command;
+
+import bdv.tools.transformation.TransformedSource;
+import bdv.util.Affine3DHelpers;
+import bdv.viewer.Source;
+import bdv.viewer.SourceAndConverter;
+import ij.IJ;
+import ij.ImagePlus;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.img.display.imagej.ImageJFunctions;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.numeric.NumericType;
+import net.imglib2.view.IntervalView;
+import net.imglib2.view.Views;
+import org.embl.mobie.viewer.MoBIEUtils;
+import org.scijava.plugin.Parameter;
+import org.scijava.plugin.Plugin;
+import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = CommandConstants.CONTEXT_MENU_ITEMS_ROOT + "Open " + ImagePlusExportCommand.RAW + " Data" )
+public class ImagePlusExportCommand< T extends NumericType< T > > implements BdvPlaygroundActionCommand
+{
+	public static final String RAW = "Raw"; // aka "Array" or "Voxel Grid", ... (not sure yet...)
+
+	@Parameter( label = "Source(s)" )
+	public SourceAndConverter[] sourceAndConverterArray;
+
+	@Parameter( label = "Maximum number of voxels [10^6]" )
+	public int maxNumMegaVoxels = 1;
+
+	@Override
+	public void run()
+	{
+		final List< SourceAndConverter< T > > sourceAndConverters = getSacs();
+
+		for ( SourceAndConverter< T > sourceAndConverter : sourceAndConverters )
+		{
+			exportAsImagePlus( sourceAndConverter, maxNumMegaVoxels * 1000000L );
+		}
+	}
+
+	private void exportAsImagePlus( SourceAndConverter< T > sourceAndConverter, long maxNumVoxels )
+	{
+		final Source< T > source = sourceAndConverter.getSpimSource();
+		final Source< T > rootSource = getRootSource( source );
+
+		if ( rootSource == null )
+		{
+			IJ.log( source.getName() + ": Consists of multiple sources and export to ImagePlus is not yet supported.");
+			return;
+		}
+
+		IJ.log(source.getName() + ": " + RAW + " data = " + rootSource.getName() );
+
+		int exportLevel = getExportLevel( source, rootSource, maxNumVoxels );
+
+		if ( exportLevel == -1 )
+		{
+			IJ.log(source.getName() + " is too big at all resolution levels and thus cannot be exported.");
+			return;
+		}
+
+		long[] dimensions = source.getSource( 0, exportLevel ).dimensionsAsLongArray();
+
+		IJ.log( source.getName() + ": Exporting at resolution level = " + exportLevel );
+		IJ.log( source.getName() + ": [nx, yz, nz] = " + Arrays.toString( dimensions ) );
+
+		final AffineTransform3D sourceTransform = new AffineTransform3D();
+		source.getSourceTransform( 0, exportLevel, sourceTransform );
+
+		final AffineTransform3D rootSourceTransform = new AffineTransform3D();
+		rootSource.getSourceTransform( 0, exportLevel, rootSourceTransform );
+
+		double[] sourceScale = new double[ 3 ];
+		double[] rootSourceScale = new double[ 3 ];
+
+		for ( int d = 0; d < 3; d++ )
+		{
+			sourceScale[ d ] = Affine3DHelpers.extractScale( sourceTransform, d );
+			rootSourceScale[ d ] = Affine3DHelpers.extractScale( rootSourceTransform, d );
+		}
+
+		IJ.log( source.getName() + ": Scale = " + Arrays.toString( sourceScale ) );
+		IJ.log( source.getName() + ": Transform = " + sourceTransform );
+		IJ.log( source.getName() + ": " + RAW + " data scale = " + Arrays.toString( rootSourceScale ) );
+		IJ.log( source.getName() + ": " + RAW + " data transform = " + rootSourceTransform );
+
+		final ImagePlus imagePlus = getImagePlus( rootSource, exportLevel );
+		imagePlus.getCalibration().setUnit( rootSource.getVoxelDimensions().unit() );
+		imagePlus.getCalibration().pixelWidth = rootSourceScale[ 0 ];
+		imagePlus.getCalibration().pixelHeight = rootSourceScale[ 1 ];
+		imagePlus.getCalibration().pixelDepth = rootSourceScale[ 2 ];
+
+		imagePlus.show();
+
+		IJ.log(source.getName() + ": Export done!" );
+	}
+
+	private int getExportLevel( Source< T > source, Source< T > rootSource, long maxNumPixels )
+	{
+		final int numMipmapLevels = rootSource.getNumMipmapLevels();
+
+		for ( int level = 0; level < numMipmapLevels; level++ )
+		{
+			long[] dimensions = source.getSource( 0, level ).dimensionsAsLongArray();
+
+			final boolean javaIndexingOK = dimensions[ 0 ] * dimensions[ 1 ] < Integer.MAX_VALUE - 1;
+
+			final boolean sizeOK = dimensions[ 0 ] * dimensions[ 1 ] * dimensions[ 2 ] < maxNumPixels;
+
+			if( javaIndexingOK && sizeOK )
+				return level;
+		}
+		return -1;
+	}
+
+	private Source< T > getRootSource( Source< T > source )
+	{
+		final Set< Source< ? > > rootSources = new HashSet<>();
+		MoBIEUtils.fetchRootSources( source, rootSources );
+		if ( rootSources.size() > 1 )
+		{
+			return null;
+		}
+		final Source< T > rootSource = ( Source< T > ) rootSources.iterator().next();
+		return rootSource;
+	}
+
+	private ImagePlus getImagePlus( Source< T > source, int level )
+	{
+		final RandomAccessibleInterval< T > raiXYZT = getRAIXYZT( source, level );
+		final IntervalView< T > raiXYZTC = Views.addDimension( raiXYZT, 0, 0 );
+		final IntervalView< T > raiXYCZT = Views.permute( Views.permute( raiXYZTC, 4, 3 ), 3, 2);
+		final ImagePlus imagePlus = ImageJFunctions.wrap( raiXYCZT, source.getName() );
+		return imagePlus;
+	}
+
+	private List< SourceAndConverter< T > > getSacs()
+	{
+		final List< SourceAndConverter< T > > sourceAndConverters = new ArrayList<>();
+		for ( SourceAndConverter< ? > sourceAndConverter : sourceAndConverterArray )
+		{
+			sourceAndConverters.add( ( SourceAndConverter< T > ) sourceAndConverter );
+		}
+		return sourceAndConverters;
+	}
+
+	private RandomAccessibleInterval< T > getRAIXYZT( Source< T > source, int level )
+	{
+		int numTimepoints = 0;
+		while ( source.isPresent( numTimepoints ) )
+		{
+			numTimepoints++;
+			if ( numTimepoints >= Integer.MAX_VALUE )
+			{
+				throw new RuntimeException("The source " + source.getName() + " appears to contain more than " + Integer.MAX_VALUE + " time points; maybe something is wrong?");
+			}
+		}
+
+		return getRAIXYZT( source, level, numTimepoints );
+	}
+
+	private RandomAccessibleInterval< T > getRAIXYZT( Source< T > source, int level, int numTimepoints )
+	{
+		final ArrayList< RandomAccessibleInterval< T > > rais = new ArrayList<>();
+		for ( int t = 0; t < numTimepoints; t++ )
+		{
+			rais.add( source.getSource( t, level ) );
+		}
+		return Views.stack( rais );
+	}
+
+	private HashMap< SourceAndConverter< T >, AffineTransform3D > fetchTransforms( List< SourceAndConverter< T > > movingSacs )
+	{
+		final HashMap< SourceAndConverter< T >, AffineTransform3D > sacToTransform = new HashMap<>();
+		for ( SourceAndConverter movingSac : movingSacs )
+		{
+			final AffineTransform3D fixedTransform = new AffineTransform3D();
+			( ( TransformedSource ) movingSac.getSpimSource()).getFixedTransform( fixedTransform );
+			sacToTransform.put( movingSac, fixedTransform );
+		}
+		return sacToTransform;
+	}
+}
diff --git a/src/main/java/org/embl/mobie/viewer/command/ImageVolumeRenderingConfiguratorCommand.java b/src/main/java/org/embl/mobie/viewer/command/ImageVolumeRenderingConfiguratorCommand.java
new file mode 100644
index 000000000..3803eb68c
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/command/ImageVolumeRenderingConfiguratorCommand.java
@@ -0,0 +1,57 @@
+package org.embl.mobie.viewer.command;
+
+import bdv.viewer.SourceAndConverter;
+import org.embl.mobie.viewer.volume.ImageVolumeViewer;
+import org.embl.mobie.viewer.volume.SegmentsVolumeViewer;
+import org.scijava.plugin.Parameter;
+import org.scijava.plugin.Plugin;
+import sc.fiji.bdvpg.scijava.ScijavaBdvDefaults;
+import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
+import sc.fiji.bdvpg.scijava.services.SourceAndConverterService;
+import sc.fiji.bdvpg.services.SourceAndConverterServices;
+
+import static org.scijava.ItemVisibility.MESSAGE;
+
+@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = CommandConstants.CONTEXT_MENU_ITEMS_ROOT + "Display>Configure Image Volume Rendering")
+public class ImageVolumeRenderingConfiguratorCommand implements BdvPlaygroundActionCommand
+{
+	@Parameter
+	SourceAndConverter[] sourceAndConverters;
+
+	@Parameter ( label = "Resolution X", style="format:#.000")
+	double sx;
+
+	@Parameter ( label = "Resolution Y", style="format:#.000")
+	double sy;
+
+	@Parameter ( label = "Resolution Z", style="format:#.000")
+	double sz;
+
+	@Parameter ( visibility = MESSAGE )
+	String msg = "( Resolution units: see BigDataViewer scale bar )";
+
+	@Parameter ( label = "Repaint images")
+	boolean repaint;
+
+	@Override
+	public void run()
+	{
+		setVoxelSpacing( sourceAndConverters, new double[]{ sx, sy, sz }, repaint );
+	}
+
+	public static void setVoxelSpacing( SourceAndConverter[] sourceAndConverters, double[] voxelSpacing, boolean repaint )
+	{
+		final SourceAndConverterService sacService = ( SourceAndConverterService ) SourceAndConverterServices.getSourceAndConverterService();
+
+		for ( SourceAndConverter sourceAndConverter : sourceAndConverters )
+		{
+			final ImageVolumeViewer volumeViewer = ( ImageVolumeViewer ) sacService.getMetadata( sourceAndConverter, ImageVolumeViewer.class.getName() );
+			if ( volumeViewer != null )
+			{
+				volumeViewer.setVoxelSpacing( voxelSpacing );
+				if ( repaint )
+					volumeViewer.updateView();
+			}
+		}
+	}
+}
diff --git a/src/main/java/org/embl/mobie/viewer/command/ManualRegistrationCommand.java b/src/main/java/org/embl/mobie/viewer/command/ManualRegistrationCommand.java
new file mode 100644
index 000000000..ac1d30bdf
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/command/ManualRegistrationCommand.java
@@ -0,0 +1,60 @@
+package org.embl.mobie.viewer.command;
+
+import bdv.tools.transformation.TransformedSource;
+import bdv.viewer.SourceAndConverter;
+import ij.gui.NonBlockingGenericDialog;
+import net.imglib2.realtransform.AffineTransform3D;
+import org.scijava.plugin.Plugin;
+import sc.fiji.bdvpg.scijava.ScijavaBdvDefaults;
+import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
+
+import javax.swing.*;
+
+@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = CommandConstants.CONTEXT_MENU_ITEMS_ROOT + "Transform>Registration - Manual")
+public class ManualRegistrationCommand implements BdvPlaygroundActionCommand
+{
+
+	private AffineTransform3D originalFixedTransform;
+
+	@Override
+	public void run()
+	{
+		new Thread( () -> showDialog() ).start();
+	}
+
+	private void showDialog()
+	{
+		final NonBlockingGenericDialog dialog = new NonBlockingGenericDialog( "Registration - Manual" );
+		dialog.hideCancelButton();
+		dialog.addMessage( "Manual translation, rotation and scaling transformations.\n\n" +
+				"- Select the BDV window and press P and select the source to be transformed as the current source\n" +
+				"- Press T to start the manual transform mode\n" +
+				"  - While in manual transform mode the mouse and keyboard actions that normally change the view will now transform the current source\n" +
+				"  - For example, right mouse button and mouse drag will translate the source\n\n" +
+				"Press [ T ] again to fix the transformation\n" +
+				"Press [ ESC ] to abort the transformation");
+
+		dialog.showDialog();
+		if ( dialog.wasCanceled() )
+		{
+			//resetMovingTransform();
+		}
+		else
+		{
+			// accept
+		}
+	}
+
+	private void storeOriginalTransform(  )
+	{
+//		originalFixedTransform = new AffineTransform3D();
+//		( ( TransformedSource ) .getSpimSource()).getFixedTransform( originalFixedTransform );
+	}
+
+	private void resetMovingTransform()
+	{
+//		( ( TransformedSource) movingSac.getSpimSource() ).setFixedTransform( sacToOriginalFixedTransform.get( movingSac ) );
+//		}
+//		bdvHandle.getViewerPanel().requestRepaint();
+	}
+}
diff --git a/src/main/java/org/embl/mobie/viewer/command/NonSelectedSegmentsOpacityAdjusterCommand.java b/src/main/java/org/embl/mobie/viewer/command/NonSelectedSegmentsOpacityAdjusterCommand.java
index dc0ec410d..d00ff8f04 100644
--- a/src/main/java/org/embl/mobie/viewer/command/NonSelectedSegmentsOpacityAdjusterCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/NonSelectedSegmentsOpacityAdjusterCommand.java
@@ -8,7 +8,7 @@
 import sc.fiji.bdvpg.scijava.ScijavaBdvDefaults;
 import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
 
-@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = ScijavaBdvDefaults.RootMenu+"Sources>Display>Change Opacity Of Non-selected Segments")
+@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = CommandConstants.CONTEXT_MENU_ITEMS_ROOT + "Display>Change Opacity Of Non-selected Segments")
 public class NonSelectedSegmentsOpacityAdjusterCommand implements BdvPlaygroundActionCommand
 {
 	@Parameter
diff --git a/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEProjectAdvancedCommand.java b/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEProjectAdvancedCommand.java
index b2f9b5187..cb5aab70f 100644
--- a/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEProjectAdvancedCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEProjectAdvancedCommand.java
@@ -1,15 +1,15 @@
 package org.embl.mobie.viewer.command;
 
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import org.scijava.command.Command;
 import org.scijava.plugin.Parameter;
 import org.scijava.plugin.Plugin;
 
 import java.io.IOException;
 
-@Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Open>Advanced>Open MoBIE Project Expert Mode..." )
+@Plugin(type = Command.class, menuPath = CommandConstants.MOBIE_PLUGIN_ROOT + "Open>Advanced>Open MoBIE Project Expert Mode..." )
 public class OpenMoBIEProjectAdvancedCommand implements Command
 {
 	@Parameter ( label = "Project Location" )
diff --git a/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEProjectBranchCommand.java b/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEProjectBranchCommand.java
index ae66f07db..0ab7dff29 100644
--- a/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEProjectBranchCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEProjectBranchCommand.java
@@ -8,7 +8,7 @@
 
 import java.io.IOException;
 
-@Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Open>Advanced>Open MoBIE Project Branch..." )
+@Plugin(type = Command.class, menuPath = CommandConstants.MOBIE_PLUGIN_ROOT + "Open>Advanced>Open MoBIE Project Branch..." )
 public class OpenMoBIEProjectBranchCommand implements Command
 {
 	@Parameter ( label = "Project Location" )
diff --git a/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEProjectCommand.java b/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEProjectCommand.java
index 38ed9ed14..6a2bfc961 100644
--- a/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEProjectCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEProjectCommand.java
@@ -9,7 +9,7 @@
 import java.io.IOException;
 
 
-@Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Open>Open MoBIE Project..." )
+@Plugin(type = Command.class, menuPath = CommandConstants.MOBIE_PLUGIN_ROOT + "Open>Open MoBIE Project..." )
 public class OpenMoBIEProjectCommand implements Command
 {
 	@Parameter ( label = "Project Location" )
diff --git a/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEProjectWithS3CredentialsCommand.java b/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEProjectWithS3CredentialsCommand.java
new file mode 100644
index 000000000..ff8d21eb1
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEProjectWithS3CredentialsCommand.java
@@ -0,0 +1,39 @@
+package org.embl.mobie.viewer.command;
+
+import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.MoBIESettings;
+import org.scijava.command.Command;
+import org.scijava.plugin.Parameter;
+import org.scijava.plugin.Plugin;
+
+import java.io.IOException;
+
+@Plugin(type = Command.class, menuPath = CommandConstants.MOBIE_PLUGIN_ROOT + "Open>Advanced>Open MoBIE Project With S3 Credentials..." )
+public class OpenMoBIEProjectWithS3CredentialsCommand implements Command
+{
+	@Parameter ( label = "S3 Project Location" )
+	public String projectLocation = "https://s3.embl.de/comulis";
+
+	@Parameter ( label = "S3 Access Key" )
+	public String s3AccessKey = "";
+
+	@Parameter ( label = "S3 Secret Key", persist = false )
+	public String s3SecretKey = "";
+
+	@Override
+	public void run()
+	{
+		try
+		{
+			new MoBIE(
+					projectLocation,
+					MoBIESettings.settings()
+							.s3AccessAndSecretKey( new String[]{ s3AccessKey, s3SecretKey } )
+			);
+		}
+		catch ( IOException e )
+		{
+			e.printStackTrace();
+		}
+	}
+}
diff --git a/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEPublishedProjectCommand.java b/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEPublishedProjectCommand.java
index 465c35ae9..0442b12db 100644
--- a/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEPublishedProjectCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/OpenMoBIEPublishedProjectCommand.java
@@ -12,7 +12,7 @@
 import java.util.HashMap;
 
 
-@Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Open>Open Published MoBIE Project..." )
+@Plugin(type = Command.class, menuPath = CommandConstants.MOBIE_PLUGIN_ROOT + "Open>Open Published MoBIE Project..." )
 public class OpenMoBIEPublishedProjectCommand implements Command
 {
 	static { net.imagej.patcher.LegacyInjector.preinit(); }
diff --git a/src/main/java/org/embl/mobie/viewer/command/OpenOMEZARRCommand.java b/src/main/java/org/embl/mobie/viewer/command/OpenOMEZARRCommand.java
index 3d28ebc08..ca9239895 100644
--- a/src/main/java/org/embl/mobie/viewer/command/OpenOMEZARRCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/OpenOMEZARRCommand.java
@@ -1,8 +1,9 @@
 package org.embl.mobie.viewer.command;
 
 import mpicbg.spim.data.SpimData;
-import org.embl.mobie.io.ome.zarr.OMEZarrViewer;
 import org.embl.mobie.io.ome.zarr.openers.OMEZarrOpener;
+import org.embl.mobie.io.util.S3Utils;
+import org.embl.mobie.viewer.view.OMEZarrViewer;
 import org.scijava.command.Command;
 import org.scijava.plugin.Parameter;
 import org.scijava.plugin.Plugin;
diff --git a/src/main/java/org/embl/mobie/viewer/command/OpenOMEZARRFromS3AdvancedCommand.java b/src/main/java/org/embl/mobie/viewer/command/OpenOMEZARRFromS3AdvancedCommand.java
deleted file mode 100644
index 0933da60b..000000000
--- a/src/main/java/org/embl/mobie/viewer/command/OpenOMEZARRFromS3AdvancedCommand.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.embl.mobie.viewer.command;
-
-import org.embl.mobie.io.ome.zarr.openers.OMEZarrS3Opener;
-import org.scijava.command.Command;
-import org.scijava.plugin.Parameter;
-import org.scijava.plugin.Plugin;
-
-import java.io.IOException;
-
-@Plugin(type = Command.class, menuPath = "Plugins>BigDataViewer>OME ZARR>Open OME ZARR From S3 (Advanced)..." )
-public class OpenOMEZARRFromS3AdvancedCommand extends OpenOMEZARRFromS3Command
-{
-    @Parameter ( label = "Log chunks loading" )
-    public boolean logChunkLoading = true;
-
-    @Override
-    public void run()
-    {
-        try
-        {
-            OMEZarrS3Opener.setLogChunkLoading( logChunkLoading );
-            openAndShow( s3URL );
-        }
-        catch ( IOException e )
-        {
-            e.printStackTrace();
-        }
-    }
-}
diff --git a/src/main/java/org/embl/mobie/viewer/command/OpenOMEZARRFromS3Command.java b/src/main/java/org/embl/mobie/viewer/command/OpenOMEZARRFromS3Command.java
index 3d430dde7..083a305df 100644
--- a/src/main/java/org/embl/mobie/viewer/command/OpenOMEZARRFromS3Command.java
+++ b/src/main/java/org/embl/mobie/viewer/command/OpenOMEZARRFromS3Command.java
@@ -1,20 +1,23 @@
 package org.embl.mobie.viewer.command;
 
 import mpicbg.spim.data.SpimData;
-import org.embl.mobie.io.ome.zarr.OMEZarrViewer;
 import org.embl.mobie.io.ome.zarr.openers.OMEZarrS3Opener;
+import org.embl.mobie.viewer.view.OMEZarrViewer;
 import org.scijava.command.Command;
 import org.scijava.plugin.Parameter;
 import org.scijava.plugin.Plugin;
 
 import java.io.IOException;
 
-
 @Plugin(type = Command.class, menuPath = "Plugins>BigDataViewer>OME ZARR>Open OME ZARR From S3...")
 public class OpenOMEZARRFromS3Command implements Command {
+
     @Parameter(label = "S3 URL")
     public String s3URL = "https://s3.embl.de/i2k-2020/em-raw.ome.zarr";
 
+    @Parameter ( label = "Log chunk loading" )
+    public boolean logChunkLoading = false;
+
     protected static void openAndShow(String s3URL) throws IOException {
         SpimData spimData = OMEZarrS3Opener.readURL(s3URL);
         final OMEZarrViewer viewer = new OMEZarrViewer(spimData);
@@ -24,6 +27,7 @@ protected static void openAndShow(String s3URL) throws IOException {
     @Override
     public void run() {
         try {
+            OMEZarrS3Opener.setLogging( logChunkLoading );
             openAndShow(s3URL);
         } catch (IOException e) {
             e.printStackTrace();
diff --git a/src/main/java/org/embl/mobie/viewer/command/OpenOMEZARRFromS3WithCredentialsCommand.java b/src/main/java/org/embl/mobie/viewer/command/OpenOMEZARRFromS3WithCredentialsCommand.java
new file mode 100644
index 000000000..c99daed12
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/command/OpenOMEZARRFromS3WithCredentialsCommand.java
@@ -0,0 +1,32 @@
+package org.embl.mobie.viewer.command;
+
+import mpicbg.spim.data.SpimData;
+import org.embl.mobie.io.ome.zarr.openers.OMEZarrS3Opener;
+import org.embl.mobie.io.util.S3Utils;
+import org.embl.mobie.viewer.view.OMEZarrViewer;
+import org.scijava.command.Command;
+import org.scijava.plugin.Parameter;
+import org.scijava.plugin.Plugin;
+
+import java.io.IOException;
+
+@Plugin(type = Command.class, menuPath = "Plugins>BigDataViewer>OME ZARR>" +
+        "Open OME ZARR From S3 with Credentials...")
+public class OpenOMEZARRFromS3WithCredentialsCommand extends OpenOMEZARRFromS3Command {
+
+    @Parameter ( label = "S3 Access Key", persist = false )
+    public String s3AccessKey = "";
+
+    @Parameter ( label = "S3 Secret Key", persist = false )
+    public String s3SecretKey = "";
+
+    @Override
+    public void run() {
+        try {
+            S3Utils.setS3AccessAndSecretKey( new String[]{ s3AccessKey, s3SecretKey} );
+            openAndShow(s3URL);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/src/main/java/org/embl/mobie/viewer/command/OpenPlatyBrowserCommand.java b/src/main/java/org/embl/mobie/viewer/command/OpenPlatyBrowserCommand.java
index f17c906ad..d9b4e0efb 100644
--- a/src/main/java/org/embl/mobie/viewer/command/OpenPlatyBrowserCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/OpenPlatyBrowserCommand.java
@@ -1,15 +1,15 @@
 package org.embl.mobie.viewer.command;
 
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import org.scijava.command.Command;
 import org.scijava.plugin.Plugin;
 
 import java.io.IOException;
 
 
-@Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Open>Open PlatyBrowser")
+@Plugin(type = Command.class, menuPath = CommandConstants.MOBIE_PLUGIN_ROOT + "Open>Open PlatyBrowser")
 public class OpenPlatyBrowserCommand implements Command
 {
 	@Override
diff --git a/src/main/java/org/embl/mobie/viewer/command/RandomColorSeedChangerCommand.java b/src/main/java/org/embl/mobie/viewer/command/RandomColorSeedChangerCommand.java
index 44c320d15..a6a79f088 100644
--- a/src/main/java/org/embl/mobie/viewer/command/RandomColorSeedChangerCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/RandomColorSeedChangerCommand.java
@@ -7,7 +7,7 @@
 import sc.fiji.bdvpg.scijava.ScijavaBdvDefaults;
 import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
 
-@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = ScijavaBdvDefaults.RootMenu+"Sources>Display>Change Random Coloring Seed [ Ctrl L ]")
+@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = CommandConstants.CONTEXT_MENU_ITEMS_ROOT + "Display>Change Random Coloring Seed [ Ctrl L ]")
 public class RandomColorSeedChangerCommand implements BdvPlaygroundActionCommand
 {
 	// TODO: Maybe implement a setter (the Categorical coloring models have the method for this already)
diff --git a/src/main/java/org/embl/mobie/viewer/command/ScreenShotMakerCommand.java b/src/main/java/org/embl/mobie/viewer/command/ScreenShotMakerCommand.java
new file mode 100644
index 000000000..5c36df24b
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/command/ScreenShotMakerCommand.java
@@ -0,0 +1,97 @@
+/*-
+ * #%L
+ * BigDataViewer-Playground
+ * %%
+ * Copyright (C) 2019 - 2021 Nicolas Chiaruttini, EPFL - Robert Haase, MPI CBG - Christian Tischer, EMBL
+ * %%
+ * 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 org.embl.mobie.viewer.command;
+
+import bdv.util.BdvHandle;
+import org.embl.mobie.viewer.bdv.ScreenShotMaker;
+import org.scijava.Initializable;
+import org.scijava.command.DynamicCommand;
+import org.scijava.module.MutableModuleItem;
+import org.scijava.plugin.Parameter;
+import org.scijava.plugin.Plugin;
+import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
+
+import static org.scijava.ItemVisibility.MESSAGE;
+
+@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = CommandConstants.CONTEXT_MENU_ITEMS_ROOT + "Take Screenshot")
+public class ScreenShotMakerCommand extends DynamicCommand implements BdvPlaygroundActionCommand, Initializable
+{
+    public static final String CAPTURE_SIZE_PIXELS = "Capture size [pixels]: ";
+
+    @Parameter
+    public BdvHandle bdvh;
+
+    @Parameter(label="Screenshot Sampling [UNIT]", callback = "showNumPixels", min = "0.0", style="format:#.00000", stepSize = "0.01")
+    public Double targetSamplingInXY = 1D;
+
+    @Parameter(label="Show RGB Image")
+    public boolean showRGB = true;
+
+    @Parameter(label="Show Multi-Channel Image")
+    public boolean showMultiChannel = true;
+
+    @Parameter( visibility = MESSAGE, required = false )
+    String message = CAPTURE_SIZE_PIXELS +"";
+
+    @Parameter( visibility = MESSAGE, required = false )
+    String scaleBarMessage = "Add Scale Bar: [ Analyze > Tools > Scale Bar... ]";
+
+    private String pixelUnit = "Pixels";
+
+    @Override
+    public void run() {
+        ScreenShotMaker screenShotMaker = new ScreenShotMaker( bdvh );
+        screenShotMaker.setPhysicalPixelSpacingInXY( targetSamplingInXY, pixelUnit );
+
+        if( showRGB )
+            screenShotMaker.getRgbScreenShot().show();
+
+        if( showMultiChannel )
+            screenShotMaker.getRawScreenShot().show();
+    }
+
+    @Override
+    public void initialize() {
+
+        // adapt pixel unit
+        //
+        final MutableModuleItem< Double > pixelSizeItem = //
+                getInfo().getMutableInput("targetSamplingInXY", Double.class);
+        pixelUnit = bdvh.getViewerPanel().state().getCurrentSource().getSpimSource().getVoxelDimensions().unit();
+        pixelSizeItem.setLabel( pixelSizeItem.getLabel().replace( "UNIT", pixelUnit ) );
+    }
+
+    // callback
+    private void showNumPixels()
+    {
+        final long[] sizeInPixels = ScreenShotMaker.getCaptureImageSizeInPixels( bdvh, targetSamplingInXY );
+        final MutableModuleItem< String > message = getInfo().getMutableInput("message", String.class);
+        message.setValue( this, CAPTURE_SIZE_PIXELS + sizeInPixels[ 0 ] + ", " + sizeInPixels[ 1 ] );
+    }
+}
diff --git a/src/main/java/org/embl/mobie/viewer/command/SegmentsVolumeRenderingConfiguratorCommand.java b/src/main/java/org/embl/mobie/viewer/command/SegmentsVolumeRenderingConfiguratorCommand.java
index 255040810..20f6b7baa 100644
--- a/src/main/java/org/embl/mobie/viewer/command/SegmentsVolumeRenderingConfiguratorCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/SegmentsVolumeRenderingConfiguratorCommand.java
@@ -9,22 +9,27 @@
 import sc.fiji.bdvpg.scijava.services.SourceAndConverterService;
 import sc.fiji.bdvpg.services.SourceAndConverterServices;
 
-@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = ScijavaBdvDefaults.RootMenu+"Sources>Display>Configure Segments Volume Rendering")
+import static org.scijava.ItemVisibility.MESSAGE;
+
+@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = CommandConstants.CONTEXT_MENU_ITEMS_ROOT + "Display>Configure Segments Volume Rendering")
 public class SegmentsVolumeRenderingConfiguratorCommand implements BdvPlaygroundActionCommand
 {
 	@Parameter
 	SourceAndConverter[] sourceAndConverters;
 
-	@Parameter ( label = "Resolution X [um]")
+	@Parameter ( label = "Resolution X", style="format:#.000" )
 	double sx;
 
-	@Parameter ( label = "Resolution Y [um]")
+	@Parameter ( label = "Resolution Y", style="format:#.000" )
 	double sy;
 
-	@Parameter ( label = "Resolution Z [um]")
+	@Parameter ( label = "Resolution Z", style="format:#.000" )
 	double sz;
 
-	@Parameter ( label = "Repaint all segments")
+	@Parameter ( visibility = MESSAGE )
+	String msg = "( Resolution units: see BigDataViewer scale bar )";
+
+	@Parameter ( label = "Repaint segments")
 	boolean repaint;
 
 	@Override
@@ -39,7 +44,7 @@ public static void setVoxelSpacing( SourceAndConverter[] sourceAndConverters, do
 
 		for ( SourceAndConverter sourceAndConverter : sourceAndConverters )
 		{
-			final SegmentsVolumeViewer volumeViewer = ( SegmentsVolumeViewer ) sacService.getMetadata( sourceAndConverter, SegmentsVolumeViewer.VOLUME_VIEW );
+			final SegmentsVolumeViewer volumeViewer = ( SegmentsVolumeViewer ) sacService.getMetadata( sourceAndConverter, SegmentsVolumeViewer.class.getName() );
 			if ( volumeViewer != null )
 			{
 				volumeViewer.setVoxelSpacing( voxelSpacing );
diff --git a/src/main/java/org/embl/mobie/viewer/command/SelectedSegmentsColorConfiguratorCommand.java b/src/main/java/org/embl/mobie/viewer/command/SelectedSegmentsColorConfiguratorCommand.java
index 00294567e..16b80c4e9 100644
--- a/src/main/java/org/embl/mobie/viewer/command/SelectedSegmentsColorConfiguratorCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/SelectedSegmentsColorConfiguratorCommand.java
@@ -10,7 +10,7 @@
 import sc.fiji.bdvpg.scijava.ScijavaBdvDefaults;
 import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
 
-@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = ScijavaBdvDefaults.RootMenu+"Sources>Display>Configure Coloring Of Selected Segments")
+@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = CommandConstants.CONTEXT_MENU_ITEMS_ROOT + "Display>Configure Coloring Of Selected Segments")
 public class SelectedSegmentsColorConfiguratorCommand implements BdvPlaygroundActionCommand
 {
 	public static final String DEFAULT_COLOR = "Default color";
diff --git a/src/main/java/org/embl/mobie/viewer/command/SourceAndConverterBlendingModeChangerCommand.java b/src/main/java/org/embl/mobie/viewer/command/SourceAndConverterBlendingModeChangerCommand.java
index 4f944d169..d5e720045 100644
--- a/src/main/java/org/embl/mobie/viewer/command/SourceAndConverterBlendingModeChangerCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/SourceAndConverterBlendingModeChangerCommand.java
@@ -36,7 +36,7 @@
 import sc.fiji.bdvpg.scijava.ScijavaBdvDefaults;
 import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
 
-@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = ScijavaBdvDefaults.RootMenu+"Sources>Display>Set Blending Mode")
+@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = CommandConstants.CONTEXT_MENU_ITEMS_ROOT + "Display>Set Blending Mode")
 public class SourceAndConverterBlendingModeChangerCommand implements BdvPlaygroundActionCommand {
 
     @Parameter(label = "Blending Mode", choices = { BlendingMode.SUM, BlendingMode.SUM_OCCLUDING })
diff --git a/src/main/java/org/embl/mobie/viewer/command/WriteOMEZARRCommand.java b/src/main/java/org/embl/mobie/viewer/command/WriteOMEZARRCommand.java
index 87d0557b7..ecc0a3931 100644
--- a/src/main/java/org/embl/mobie/viewer/command/WriteOMEZARRCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/WriteOMEZARRCommand.java
@@ -1,18 +1,20 @@
 package org.embl.mobie.viewer.command;
 
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.io.n5.util.DownsampleBlock;
 import org.embl.mobie.io.ome.zarr.writers.imgplus.WriteImgPlusToN5OmeZarr;
 import org.embl.mobie.viewer.projectcreator.ui.ManualExportPanel;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import ij.ImagePlus;
 import net.imglib2.realtransform.AffineTransform3D;
+import org.janelia.saalfeldlab.n5.Compression;
 import org.janelia.saalfeldlab.n5.GzipCompression;
+import org.scijava.ItemVisibility;
 import org.scijava.command.Command;
 import org.scijava.plugin.Parameter;
 import org.scijava.plugin.Plugin;
 
 import java.io.File;
-import static org.embl.mobie.viewer.ui.UserInterfaceHelper.tidyString;
+import static org.embl.mobie.viewer.ui.UserInterfaceHelpers.tidyString;
 
 @Plugin(type = Command.class, menuPath = "Plugins>BigDataViewer>OME ZARR>Export Current Image To OME-ZARR...")
 public class WriteOMEZARRCommand implements Command {
@@ -22,6 +24,9 @@ public class WriteOMEZARRCommand implements Command {
     @Parameter
     public ImagePlus currentImage;
 
+    @Parameter (visibility = ItemVisibility.MESSAGE, required = false)
+    public String message = "Make sure your voxel size, and unit,\n are set properly under Image > Properties...";
+
     @Parameter(label="Image name:")
     public String imageName;
 
@@ -47,16 +52,23 @@ public void run() {
         }
 
         // affine transforms are currently not supported in the ome-zarr spec, so here we just generate a default
-        // identity affine
+        // identity affine - it will automatically write the image scaling anyway
         AffineTransform3D sourceTransform = new AffineTransform3D();
 
         if ( name != null ) {
             if ( useDefaults ) {
                 new WriteImgPlusToN5OmeZarr().export(currentImage, filePath, sourceTransform, method,
-                        new GzipCompression(), new String[]{imageName});
+                        new GzipCompression() );
             } else {
-                new ManualExportPanel( currentImage, filePath, sourceTransform, method, imageName,
-                        ImageDataFormat.OmeZarr ).getManualExportParameters();
+                ManualExportPanel manualExportPanel = new ManualExportPanel( ImageDataFormat.OmeZarr );
+                int[][] resolutions = manualExportPanel.getResolutions();
+                int[][] subdivisions = manualExportPanel.getSubdivisions();
+                Compression compression = manualExportPanel.getCompression();
+
+                if ( resolutions != null && subdivisions != null && compression != null ) {
+                    new WriteImgPlusToN5OmeZarr().export(currentImage, resolutions, subdivisions, filePath, sourceTransform,
+                            method, compression );
+                }
             }
         }
     }
diff --git a/src/main/java/org/embl/mobie/viewer/display/AbstractSourceDisplay.java b/src/main/java/org/embl/mobie/viewer/display/AbstractSourceDisplay.java
index 5a22e4c1f..2d4df8724 100644
--- a/src/main/java/org/embl/mobie/viewer/display/AbstractSourceDisplay.java
+++ b/src/main/java/org/embl/mobie/viewer/display/AbstractSourceDisplay.java
@@ -22,10 +22,7 @@ public double getOpacity()
 	{
 		return opacity;
 	}
-	public boolean isVisible()
-	{
-		return visible;
-	}
+	public boolean isVisible() { return visible; }
 
 	// Runtime
 	public transient Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter;
diff --git a/src/main/java/org/embl/mobie/viewer/display/AnnotatedIntervalDisplay.java b/src/main/java/org/embl/mobie/viewer/display/AnnotatedIntervalDisplay.java
index 1669c9a4e..ec78b36ce 100644
--- a/src/main/java/org/embl/mobie/viewer/display/AnnotatedIntervalDisplay.java
+++ b/src/main/java/org/embl/mobie/viewer/display/AnnotatedIntervalDisplay.java
@@ -8,7 +8,7 @@
 
 import java.util.*;
 
-public class AnnotatedIntervalDisplay extends AnnotatedRegionDisplay<AnnotatedIntervalTableRow>
+public class AnnotatedIntervalDisplay extends AnnotatedRegionDisplay< AnnotatedIntervalTableRow >
 {
 	// Serialization
 	protected Map< String, List< String > > sources;
diff --git a/src/main/java/org/embl/mobie/viewer/display/ImageSourceDisplay.java b/src/main/java/org/embl/mobie/viewer/display/ImageSourceDisplay.java
index b3d6a8815..81134348d 100644
--- a/src/main/java/org/embl/mobie/viewer/display/ImageSourceDisplay.java
+++ b/src/main/java/org/embl/mobie/viewer/display/ImageSourceDisplay.java
@@ -6,9 +6,11 @@
 import org.embl.mobie.viewer.bdv.render.BlendingMode;
 import org.embl.mobie.viewer.color.opacity.AdjustableOpacityColorConverter;
 import net.imglib2.display.ColorConverter;
+import org.embl.mobie.viewer.volume.ImageVolumeViewer;
 import sc.fiji.bdvpg.services.SourceAndConverterServices;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 public class ImageSourceDisplay extends AbstractSourceDisplay
@@ -22,6 +24,7 @@ public class ImageSourceDisplay extends AbstractSourceDisplay
 
 	// Runtime
 	public transient ImageSliceView imageSliceView;
+	public transient ImageVolumeViewer imageVolumeViewer;
 
 	// Getters for serialised fields
 	public String getColor()
@@ -39,6 +42,11 @@ public BlendingMode getBlendingMode()
 		return blendingMode;
 	}
 
+	public boolean showImagesIn3d()
+	{
+		return showImagesIn3d;
+	}
+
 	@Override
 	public List< String > getSources()
 	{
@@ -70,28 +78,38 @@ public ImageSourceDisplay( ImageSourceDisplay imageDisplay )
 		this.sources.addAll( imageDisplay.sourceNameToSourceAndConverter.keySet() );
 
 		final SourceAndConverter< ? > sourceAndConverter = imageDisplay.sourceNameToSourceAndConverter.values().iterator().next();
-		final ConverterSetup converterSetup = SourceAndConverterServices.getBdvDisplayService().getConverterSetup( sourceAndConverter );
+
+		setDisplaySettings( sourceAndConverter );
+
+		// TODO - show images in 3d (currently not supported in viewer)
+	}
+
+	public ImageSourceDisplay( SourceAndConverter< ? > sourceAndConverter )
+	{
+		sources = Arrays.asList( sourceAndConverter.getSpimSource().getName() );
+		setDisplaySettings( sourceAndConverter );
+	}
+
+	private void setDisplaySettings( SourceAndConverter< ? > sourceAndConverter )
+	{
+		final ConverterSetup converterSetup = SourceAndConverterServices.getSourceAndConverterService().getConverterSetup( sourceAndConverter );
 
 		if( sourceAndConverter.getConverter() instanceof AdjustableOpacityColorConverter )
 		{
-			this.opacity = ( ( AdjustableOpacityColorConverter ) sourceAndConverter.getConverter() ).getOpacity();
+			opacity = ( ( AdjustableOpacityColorConverter ) sourceAndConverter.getConverter() ).getOpacity();
 		}
 
 		if ( sourceAndConverter.getConverter() instanceof ColorConverter)
 		{
 			// needs to be of form r=(\\d+),g=(\\d+),b=(\\d+),a=(\\d+)"
-			String colorString = ( ( ColorConverter ) sourceAndConverter.getConverter() ).getColor().toString();
-			colorString = colorString.replaceAll("[()]", "");
-			this.color = colorString;
+			color = ( ( ColorConverter ) sourceAndConverter.getConverter() ).getColor().toString();
+			color = color.replaceAll("[()]", "");
 		}
 
-		double[] contrastLimits = new double[2];
+		contrastLimits = new double[2];
 		contrastLimits[0] = converterSetup.getDisplayRangeMin();
 		contrastLimits[1] = converterSetup.getDisplayRangeMax();
-		this.contrastLimits = contrastLimits;
-
-		this.blendingMode = (BlendingMode) SourceAndConverterServices.getSourceAndConverterService().getMetadata( sourceAndConverter, BlendingMode.BLENDING_MODE );
 
-		// TODO - show images in 3d (currently not supported in viewer)
+		blendingMode = (BlendingMode) SourceAndConverterServices.getSourceAndConverterService().getMetadata( sourceAndConverter, BlendingMode.BLENDING_MODE );
 	}
 }
diff --git a/src/main/java/org/embl/mobie/viewer/mesh/MeshCreator.java b/src/main/java/org/embl/mobie/viewer/mesh/MeshCreator.java
index a7fb164bd..54be39cb5 100644
--- a/src/main/java/org/embl/mobie/viewer/mesh/MeshCreator.java
+++ b/src/main/java/org/embl/mobie/viewer/mesh/MeshCreator.java
@@ -5,9 +5,9 @@
 import de.embl.cba.bdv.utils.objects3d.FloodFill;
 import de.embl.cba.tables.Logger;
 import de.embl.cba.tables.Utils;
-import de.embl.cba.tables.ij3d.UniverseUtils;
 import de.embl.cba.tables.imagesegment.ImageSegment;
 import de.embl.cba.tables.mesh.MeshExtractor;
+import ij.IJ;
 import isosurface.MeshEditor;
 import net.imglib2.FinalInterval;
 import net.imglib2.FinalRealInterval;
@@ -17,13 +17,12 @@
 import net.imglib2.type.numeric.RealType;
 import net.imglib2.util.Intervals;
 import net.imglib2.view.Views;
+import org.embl.mobie.viewer.playground.BdvPlaygroundUtils;
 import org.scijava.vecmath.Point3f;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 
-import static de.embl.cba.tables.Utils.getVoxelSpacings;
-
 public class MeshCreator < S extends ImageSegment >
 {
 	private int meshSmoothingIterations;
@@ -82,9 +81,7 @@ private float[] createMesh( ImageSegment segment, double[] voxelSpacing, Source<
 
 		if ( meshCoordinates.length == 0 )
 		{
-			Logger.warn( "Could not find any pixels for segment with label " + segment.labelId()
-					+ "\nwithin bounding box " + boundingBox );
-			return null;
+			throw new RuntimeException("Mesh has zero pixels.");
 		}
 
 		return meshCoordinates;
@@ -104,14 +101,11 @@ private CustomTriangleMesh createCustomTriangleMesh( ImageSegment segment, doubl
 			try
 			{
 				final float[] mesh = createMesh( segment, voxelSpacing, source );
-				if ( mesh == null )
-				{
-					throw new RuntimeException( "Could not create mesh for segment " + segment.labelId() + " at time point " + segment.timePoint() );
-				}
 				segment.setMesh( mesh );
 			}
 			catch ( Exception e )
 			{
+				IJ.showMessage("Could not create mesh for segment " + segment.labelId() + " at time point " + segment.timePoint() + "\nIt could be that the segment could not be found at the given resolution; please try again with an increased resolution." );
 				e.printStackTrace();
 				throw new RuntimeException( "Could not create mesh for segment " + segment.labelId() + " at time point " + segment.timePoint() );
 			}
@@ -141,73 +135,44 @@ private static CustomTriangleMesh asCustomTriangleMesh( final float[] meshCoordi
 
 	private Integer getLevel( ImageSegment segment, Source< ? > labelSource, double[] voxelSpacing )
 	{
-		Integer level;
-
-		if ( voxelSpacing != null )
+		if ( voxelSpacing != null ) // user determined resolution
 		{
-			level = getLevel( labelSource, voxelSpacing );
+			return BdvPlaygroundUtils.getLevel( labelSource, voxelSpacing );
 		}
-		else // auto-resolution
+		else // auto-resolution, uses maxNumSegmentVoxels
 		{
 			if ( segment.boundingBox() == null )
 			{
-				Logger.error( "3D View:\n" +
-						"Automated resolution level selection is enabled, but the segment has no bounding box.\n" +
-						"This combination is currently not possible." );
-				level = null;
+				Logger.error( "3D View:\nAutomated resolution level selection is enabled, but the segment has no bounding box.\nThis combination is currently not supported." );
+				throw new RuntimeException();
 			}
 			else
 			{
-				final ArrayList< double[] > voxelSpacings = Utils.getVoxelSpacings( labelSource );
-
-				final int numLevels = voxelSpacings.size();
-
-				for ( level = 0; level < numLevels; level++ )
-				{
-					FinalInterval boundingBox = getIntervalInVoxelUnits( segment.boundingBox(), voxelSpacings.get( level ) );
+				int level = getLevel( segment, labelSource );
 
-					final long numElements = Intervals.numElements( boundingBox );
-
-					if ( numElements <= maxNumSegmentVoxels )
-						break;
-				}
-
-				if ( level == numLevels ) level = numLevels - 1;
+				return level;
 			}
 		}
-
-		return level;
 	}
 
-	private static int getLevel( Source< ? > source, double[] requestedVoxelSpacing )
+	private int getLevel( ImageSegment segment, Source< ? > labelSource )
 	{
-		ArrayList< double[] > voxelSpacings = getVoxelSpacings( source );
-		return getLevel( voxelSpacings, requestedVoxelSpacing );
-	}
+		final ArrayList< double[] > voxelSpacings = Utils.getVoxelSpacings( labelSource );
 
-	private static int getLevel( ArrayList< double[] > sourceVoxelSpacings, double[] requestedVoxelSpacing )
-	{
-		int level;
-		int numLevels = sourceVoxelSpacings.size();
-		final int numDimensions = sourceVoxelSpacings.get( 0 ).length;
+		final int numLevels = voxelSpacings.size();
 
+		int level;
 		for ( level = 0; level < numLevels; level++ )
 		{
-			boolean allLargerOrEqual = true;
-			for ( int d = 0; d < numDimensions; d++ )
-			{
-				if ( sourceVoxelSpacings.get( level )[ d ] < requestedVoxelSpacing[ d ] )
-				{
-					allLargerOrEqual = false;
-					continue;
-				}
-			}
+			FinalInterval boundingBox = getIntervalInVoxelUnits( segment.boundingBox(), voxelSpacings.get( level ) );
 
-			if ( allLargerOrEqual ) break;
+			final long numElements = Intervals.numElements( boundingBox );
+
+			if ( numElements <= maxNumSegmentVoxels )
+				break;
 		}
 
 		if ( level == numLevels ) level = numLevels - 1;
-
 		return level;
 	}
 
diff --git a/src/main/java/org/embl/mobie/viewer/playground/BdvPlaygroundUtils.java b/src/main/java/org/embl/mobie/viewer/playground/BdvPlaygroundUtils.java
new file mode 100644
index 000000000..4b65f6307
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/playground/BdvPlaygroundUtils.java
@@ -0,0 +1,90 @@
+package org.embl.mobie.viewer.playground;
+
+import bdv.util.BdvHandle;
+import bdv.viewer.Source;
+import bdv.viewer.ViewerPanel;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.util.Intervals;
+
+import java.util.ArrayList;
+
+import static de.embl.cba.tables.Utils.getVoxelSpacings;
+
+
+/**
+ * These helper functions either exist already in bdv-playground,
+ * but in a too recent version, or should be moved to bdv-playground.
+ */
+public class BdvPlaygroundUtils
+{
+	public static double[] getWindowCentreInPixelUnits( ViewerPanel viewerPanel )
+	{
+		final double[] windowCentreInPixelUnits = new double[ 3 ];
+		windowCentreInPixelUnits[ 0 ] = viewerPanel.getDisplay().getWidth() / 2.0;
+		windowCentreInPixelUnits[ 1 ] = viewerPanel.getDisplay().getHeight() / 2.0;
+		return windowCentreInPixelUnits;
+	}
+
+	public static double[] getWindowCentreInCalibratedUnits( BdvHandle bdvHandle )
+	{
+		final double[] centreInPixelUnits = getWindowCentreInPixelUnits( bdvHandle.getViewerPanel() );
+		final AffineTransform3D affineTransform3D = new AffineTransform3D();
+		bdvHandle.getViewerPanel().state().getViewerTransform( affineTransform3D );
+		final double[] centreInCalibratedUnits = new double[ 3 ];
+		affineTransform3D.inverse().apply( centreInPixelUnits, centreInCalibratedUnits );
+		return centreInCalibratedUnits;
+	}
+
+	public static int getLevel( Source< ? > source, long maxNumVoxels )
+	{
+		final ArrayList< double[] > voxelSpacings = getVoxelSpacings( source );
+
+		final int numLevels = voxelSpacings.size();
+
+		int level;
+
+		for ( level = 0; level < numLevels; level++ )
+		{
+			final long numElements = Intervals.numElements( source.getSource( 0, level ) );
+
+			if ( numElements <= maxNumVoxels )
+				break;
+		}
+
+		if ( level == numLevels ) level = numLevels - 1;
+
+		return level;
+	}
+
+	public static int getLevel( Source< ? > source, double[] requestedVoxelSpacing )
+	{
+		ArrayList< double[] > voxelSpacings = getVoxelSpacings( source );
+		return getLevel( voxelSpacings, requestedVoxelSpacing );
+	}
+
+	public static int getLevel( ArrayList< double[] > sourceVoxelSpacings, double[] requestedVoxelSpacing )
+	{
+		int level;
+		int numLevels = sourceVoxelSpacings.size();
+		final int numDimensions = sourceVoxelSpacings.get( 0 ).length;
+
+		for ( level = 0; level < numLevels; level++ )
+		{
+			boolean allLargerOrEqual = true;
+			for ( int d = 0; d < numDimensions; d++ )
+			{
+				if ( sourceVoxelSpacings.get( level )[ d ] < requestedVoxelSpacing[ d ] )
+				{
+					allLargerOrEqual = false;
+					continue;
+				}
+			}
+
+			if ( allLargerOrEqual ) break;
+		}
+
+		if ( level == numLevels ) level = numLevels - 1;
+
+		return level;
+	}
+}
diff --git a/src/main/java/org/embl/mobie/viewer/playground/PlaygroundUtils.java b/src/main/java/org/embl/mobie/viewer/playground/PlaygroundUtils.java
deleted file mode 100644
index 4d6e4dff1..000000000
--- a/src/main/java/org/embl/mobie/viewer/playground/PlaygroundUtils.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.embl.mobie.viewer.playground;
-
-import bdv.util.BdvHandle;
-import net.imglib2.realtransform.AffineTransform3D;
-
-
-/**
- * These helper functions either exist already in bdv-playground,
- * but in a too recent version, or should be moved to bdv-playground.
- */
-public class PlaygroundUtils
-{
-	public static double[] getWindowCentreInPixelUnits( BdvHandle bdvHandle )
-	{
-		final double[] windowCentreInPixelUnits = new double[ 3 ];
-		windowCentreInPixelUnits[ 0 ] = bdvHandle.getViewerPanel().getDisplay().getWidth() / 2.0;
-		windowCentreInPixelUnits[ 1 ] = bdvHandle.getViewerPanel().getDisplay().getHeight() / 2.0;
-		return windowCentreInPixelUnits;
-	}
-
-	public static double[] getWindowCentreInCalibratedUnits( BdvHandle bdvHandle )
-	{
-		final double[] centreInPixelUnits = getWindowCentreInPixelUnits( bdvHandle );
-		final AffineTransform3D affineTransform3D = new AffineTransform3D();
-		bdvHandle.getViewerPanel().state().getViewerTransform( affineTransform3D );
-		final double[] centreInCalibratedUnits = new double[ 3 ];
-		affineTransform3D.inverse().apply( centreInPixelUnits, centreInCalibratedUnits );
-		return centreInCalibratedUnits;
-	}
-
-}
diff --git a/src/main/java/org/embl/mobie/viewer/playground/ScreenShotMakerCommand.java b/src/main/java/org/embl/mobie/viewer/playground/ScreenShotMakerCommand.java
deleted file mode 100644
index 8a46c1a5c..000000000
--- a/src/main/java/org/embl/mobie/viewer/playground/ScreenShotMakerCommand.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*-
- * #%L
- * BigDataViewer-Playground
- * %%
- * Copyright (C) 2019 - 2021 Nicolas Chiaruttini, EPFL - Robert Haase, MPI CBG - Christian Tischer, EMBL
- * %%
- * 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 org.embl.mobie.viewer.playground;
-
-import bdv.util.BdvHandle;
-import org.embl.mobie.viewer.bdv.ScreenShotMaker;
-import org.scijava.plugin.Parameter;
-import org.scijava.plugin.Plugin;
-import sc.fiji.bdvpg.scijava.ScijavaBdvDefaults;
-import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
-
-/**
- * ScreenShotMakerCommand
- * <p>
- * <p>
- * <p>
- * Author: @haesleinhuepf
- * 12 2019
- */
-
-@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = ScijavaBdvDefaults.RootMenu+"BDV>BDV - Screenshot",
-        description = "Creates a screenshot of a BDV view, the resolution can be chosen to upscale or downscale" +
-                " the image compared to the original window. A single RGB image resulting from the projection" +
-                " of all sources is displayed. Raw image data can also be exported in grayscale.")
-public class ScreenShotMakerCommand implements BdvPlaygroundActionCommand {
-
-    @Parameter
-    BdvHandle bdvh;
-
-    @Parameter(label="Target Pixel Size (in XY)")
-    public double targetPixelSizeInXY = 1;
-
-    @Parameter(label="Pixel Size Unit")
-    public String targetPixelUnit = "Pixels";
-
-    @Parameter(label="Show Raw Data")
-    public boolean showRawData = false;
-
-    @Override
-    public void run() {
-        ScreenShotMaker screenShotMaker = new ScreenShotMaker(bdvh);
-        screenShotMaker.setPhysicalPixelSpacingInXY(targetPixelSizeInXY, targetPixelUnit);
-        screenShotMaker.getRgbScreenShot().show();
-        if(showRawData) screenShotMaker.getRawScreenShot().show();
-    }
-}
diff --git a/src/main/java/org/embl/mobie/viewer/plot/ScatterPlotViewer.java b/src/main/java/org/embl/mobie/viewer/plot/ScatterPlotViewer.java
index 4a9d554a9..4c1eba03f 100644
--- a/src/main/java/org/embl/mobie/viewer/plot/ScatterPlotViewer.java
+++ b/src/main/java/org/embl/mobie/viewer/plot/ScatterPlotViewer.java
@@ -356,7 +356,7 @@ private ArrayList< T > searchWithinRadius(  )
 	private void showInBdv( FunctionRealRandomAccessible< ARGBType > randomAccessible, FinalInterval interval, String[] selectedColumns )
 	{
 		Prefs.showMultibox( false );
-		Prefs.showScaleBar( false );
+		Prefs.showScaleBar( true ); // This clashes with the main BDV...
 
 		final BdvOptions bdvOptions = BdvOptions.options().is2D().frameTitle( "Scatter plot" ).addTo( bdvHandle );
 
diff --git a/src/main/java/org/embl/mobie/viewer/plugins/platybrowser/GeneSearch.java b/src/main/java/org/embl/mobie/viewer/plugins/platybrowser/GeneSearch.java
new file mode 100644
index 000000000..575fa4805
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/plugins/platybrowser/GeneSearch.java
@@ -0,0 +1,338 @@
+package org.embl.mobie.viewer.plugins.platybrowser;
+
+import bdv.viewer.Source;
+import bdv.viewer.SourceAndConverter;
+import de.embl.cba.tables.TableUIs;
+import ij.IJ;
+import mpicbg.spim.data.sequence.VoxelDimensions;
+import net.imglib2.Cursor;
+import net.imglib2.RandomAccess;
+import net.imglib2.RandomAccessible;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.algorithm.neighborhood.HyperSphereShape;
+import net.imglib2.algorithm.neighborhood.Neighborhood;
+import net.imglib2.type.NativeType;
+import net.imglib2.type.numeric.RealType;
+import org.embl.mobie.io.ImageDataFormat;
+import org.embl.mobie.viewer.Dataset;
+import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.ThreadUtils;
+import org.embl.mobie.viewer.source.ImageSource;
+import org.embl.mobie.viewer.view.View;
+
+import javax.swing.*;
+import javax.swing.table.DefaultTableModel;
+import java.awt.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Future;
+
+import static org.embl.mobie.viewer.plugins.platybrowser.GeneSearch.GeneSearchUtils.getFractionOfNonZeroVoxels;
+import static org.embl.mobie.viewer.plugins.platybrowser.GeneSearch.GeneSearchUtils.sortByValue;
+
+public class GeneSearch
+{
+	private static final String PROSPR_UI_SELECTION_GROUP = "prospr";
+
+	private final double micrometerRadius;
+	private final double[] micrometerPosition;
+	private final MoBIE moBIE;
+	private Map< String, Double > localExpression;
+	private Collection< String > prosprSourceNames;
+	private static Map< String, SourceAndConverter< ? > > prosprSources;
+
+	public GeneSearch( double micrometerRadius,
+					   double[] micrometerPosition,
+					   MoBIE moBIE )
+	{
+		this.micrometerRadius = micrometerRadius;
+		this.micrometerPosition = micrometerPosition;
+		this.moBIE = moBIE;
+	}
+
+	public void searchGenes( )
+	{
+		prosprSourceNames = fetchProsprSourceNames();
+
+		// TODO: One should not open the raw sources but the views,
+		//  because there may be additional transformations in the views.
+		//  This could be done using those methods:
+		//  moBIE.getViewManager().openAndTransformViewSources( view );
+		//  Since the Prospr sources are not transformed, this does not matter (yet)...
+		if ( prosprSources == null )
+			prosprSources = moBIE.openSourceAndConverters( prosprSourceNames );
+
+		final Map< String, Double > geneExpressionLevels = runSearchAndGetLocalExpression( prosprSources );
+
+		GeneSearchUtils.addRowToGeneExpressionTable( micrometerPosition, micrometerRadius, geneExpressionLevels );
+
+		GeneSearchUtils.logGeneExpression( micrometerPosition, micrometerRadius, geneExpressionLevels );
+	}
+
+	private Collection< String> fetchProsprSourceNames()
+	{
+		final Map< String, Map< String, View > > groupingsToViews = moBIE.getUserInterface().getGroupingsToViews();
+		return groupingsToViews.get( PROSPR_UI_SELECTION_GROUP ).keySet();
+	}
+
+	private Map< String, Double > getExpressionLevelsSortedByValue()
+	{
+		Map< String, Double > localSortedExpression = sortByValue( localExpression );
+		removeGenesWithZeroExpression( localSortedExpression );
+		return localSortedExpression;
+	}
+
+	private Map< String, Double > runSearchAndGetLocalExpression( Map< String, SourceAndConverter< ? > > sources )
+	{
+		localExpression = new ConcurrentHashMap<>();
+
+		IJ.log( "# Gene search" );
+		final ArrayList< Future< ? > > futures = ThreadUtils.getFutures();
+		for ( String gene : sources.keySet() )
+		{
+			futures.add(
+				ThreadUtils.executorService.submit( () -> {
+					searchGene( sources.get( gene ) );
+			}));
+		}
+		ThreadUtils.waitUntilFinished( futures );
+
+		return localExpression;
+	}
+
+	private void searchGene( SourceAndConverter< ? > sourceAndConverter )
+	{
+		final Source< ? > source = sourceAndConverter.getSpimSource();
+
+		final RandomAccessibleInterval< ? > rai = source.getSource( 0, 0 );
+
+		final VoxelDimensions voxelDimensions = source.getVoxelDimensions();
+
+		final double fractionOfNonZeroVoxels = getFractionOfNonZeroVoxels(
+				( RandomAccessibleInterval ) rai,
+				micrometerPosition,
+				micrometerRadius,
+				voxelDimensions.dimension( 0 ) );
+
+		localExpression.put( source.getName(), fractionOfNonZeroVoxels );
+
+		IJ.log( "Gene Search: Fraction of non-zero voxels in search region: " + source.getName() + ": " + fractionOfNonZeroVoxels );
+	}
+
+	private void removeGenesWithZeroExpression( Map< String, Double > localSortedExpression)
+	{
+		ArrayList< String > sortedNames = new ArrayList( localSortedExpression.keySet() );
+
+		// remove entries with zero expression
+		for ( int i = sortedNames.size() - 1; i >= 0; --i )
+		{
+			String name = sortedNames.get( i );
+			double value = localSortedExpression.get( name ).doubleValue();
+
+			if ( value == 0.0 )
+			{
+				localSortedExpression.remove( name );
+			}
+		}
+	}
+
+	// TODO: It makes no sense to have this as an extra class => move all methods into GeneSearch
+	public static class GeneSearchUtils
+	{
+		private static Map< String, SourceAndConverter< ? > > prosprSources;
+
+		private static JTable table;
+		private static DefaultTableModel model;
+		private static ArrayList< String > prosprSourceNames;
+		private static MoBIE moBIE;
+
+		public static void logGeneExpression(
+				double[] micrometerPosition,
+				double micrometerRadius,
+				Map< String, Double > geneExpressionLevels )
+		{
+			IJ.log( "\n# Expression levels [fraction of search volume]" );
+			IJ.log( "Center position [um]: " + Arrays.toString(  micrometerPosition ) );
+			IJ.log( "Radius [um]: " + micrometerRadius );
+
+			final LinkedHashMap< String, Double > sortedExpressionLevels = new LinkedHashMap<>( );
+
+			geneExpressionLevels.entrySet()
+					.stream()
+					.sorted(Map.Entry.comparingByValue(( Comparator.reverseOrder())))
+					.forEachOrdered(x -> sortedExpressionLevels.put(x.getKey(), x.getValue()));
+
+			for ( String gene : sortedExpressionLevels.keySet() )
+			{
+				if ( sortedExpressionLevels.get( gene ) > 0.0 )
+					IJ.log( gene + ": " + String.format( "%.2f", sortedExpressionLevels.get( gene ) ) );
+			}
+		}
+
+		public static synchronized void addRowToGeneExpressionTable(
+				double[] micrometerPosition,
+				double micrometerRadius,
+				Map< String, Double > expressionLevels )
+		{
+
+			final ArrayList< String > sortedGeneNames = sort( expressionLevels.keySet() );
+
+			if ( table == null )
+			{
+				initGeneExpressionTable( sortedGeneNames );
+				showGeneExpressionTable();
+			}
+
+			final Double[] position = { micrometerPosition [ 0 ], micrometerPosition[ 1 ], micrometerPosition[ 2 ], 0.0 };
+			final Double[] parameters = { micrometerRadius };
+
+			final ArrayList< Double > sortedExpressionLevels = new ArrayList<>();
+			for ( String geneName : sortedGeneNames )
+				sortedExpressionLevels.add( expressionLevels.get( geneName ) );
+
+			model.addRow( combine( combine( position, parameters ), sortedExpressionLevels.toArray( new Double[ expressionLevels.size() ] )));
+		}
+
+		public static ArrayList< String > sort( Collection< String > strings )
+		{
+			final ArrayList< String > sorted = new ArrayList<>( strings );
+			Collections.sort( sorted, String.CASE_INSENSITIVE_ORDER  );
+			return sorted;
+		}
+
+		public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
+			final ArrayList< Map.Entry< K, V > > list = new ArrayList<>( map.entrySet() );
+			list.sort( Map.Entry.comparingByValue() );
+
+			Map<K, V> result = new LinkedHashMap<>();
+			for (Map.Entry<K, V> entry : list) {
+				result.put(entry.getKey(), entry.getValue());
+			}
+
+			return result;
+		}
+
+		public static void showGeneExpressionTable()
+		{
+			JFrame frame = new JFrame("Gene Expression");
+
+			JPanel panel = new JPanel();
+			panel.setLayout(new BorderLayout());
+
+			final JMenuBar jMenuBar = new JMenuBar();
+			final JMenu menu = new JMenu( "File" );
+			final JMenuItem saveAs = new JMenuItem( "Save as..." );
+			saveAs.addActionListener( e -> SwingUtilities.invokeLater( () -> TableUIs.saveTableUI( table ) ) );
+			menu.add( saveAs );
+			jMenuBar.add( menu );
+			frame.setJMenuBar( jMenuBar );
+
+			JScrollPane tableContainer = new JScrollPane(
+					table,
+					JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
+					JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED );
+
+			table.setAutoResizeMode( JTable.AUTO_RESIZE_OFF );
+
+			panel.add(tableContainer, BorderLayout.CENTER);
+			frame.getContentPane().add(panel);
+
+			frame.pack();
+			frame.setVisible(true);
+		}
+
+		public static void initGeneExpressionTable( Collection< String > geneNames )
+		{
+			final String[] position = { "X", "Y", "Z", "T" };
+			final String[] searchParameters = { "SearchRadius_um" };
+
+			String[] genes = geneNames.toArray( new String[ geneNames.size() ] );
+
+			model = new DefaultTableModel();
+			model.setColumnIdentifiers( combine( combine( position, searchParameters ), genes )  );
+			table = new JTable( model );
+		}
+
+		public static < T extends RealType< T > & NativeType< T > >
+		double getFractionOfNonZeroVoxels( final RandomAccessibleInterval< T > rai, double[] position, double radius, double calibration )
+		{
+			// TODO: add out-of-bounds strategy or is this handled by the Neighborhood?
+			final HyperSphereShape sphereShape = new HyperSphereShape( ( int ) Math.ceil( radius / calibration ) );
+			final RandomAccessible< Neighborhood< T > > nra = sphereShape.neighborhoodsRandomAccessible( rai );
+			final RandomAccess< Neighborhood< T > > neighborhoodRandomAccess = nra.randomAccess();
+			neighborhoodRandomAccess.setPosition( getPixelPosition( position, calibration ) );
+
+			final Neighborhood< T > neighborhood = neighborhoodRandomAccess.get();
+			final Cursor< T > cursor = neighborhood.cursor();
+
+			long numberOfNonZeroVoxels = 0;
+			long numberOfVoxels = 0;
+
+			while( cursor.hasNext() )
+			{
+				numberOfVoxels++;
+
+				final double realDouble = cursor.next().getRealDouble();
+
+				if ( realDouble != 0)
+				{
+					numberOfNonZeroVoxels++;
+				}
+			}
+
+			return 1.0 * numberOfNonZeroVoxels / numberOfVoxels;
+		}
+
+		private static long[] getPixelPosition( double[] position, double calibration )
+		{
+			long[] pixelPosition = new long[ position.length ];
+			for ( int d = 0; d < position.length; ++d )
+			{
+				pixelPosition[ d ] = (long) ( position[ d ] / calibration );
+			}
+			return pixelPosition;
+		}
+
+		public static String[] combine(String[] a, String[] b){
+			int length = a.length + b.length;
+			String[] result = new String[length];
+			System.arraycopy(a, 0, result, 0, a.length);
+			System.arraycopy(b, 0, result, a.length, b.length);
+			return result;
+		}
+
+		public static Object[] combine(Object[] a, Object[] b){
+			int length = a.length + b.length;
+			Object[] result = new Object[length];
+			System.arraycopy(a, 0, result, 0, a.length);
+			System.arraycopy(b, 0, result, a.length, b.length);
+			return result;
+		}
+
+		public static void setProsprSourceNames( ImageDataFormat imageDataFormat, Dataset dataset )
+		{
+			GeneSearchUtils.prosprSourceNames = new ArrayList<>();
+			for ( String sourceName : dataset.sources.keySet() )
+			{
+				final ImageSource imageSource = dataset.sources.get( sourceName ).get();
+				final String relativePath = imageSource.imageData.get( imageDataFormat ).relativePath;
+
+				if ( relativePath.contains( PROSPR_UI_SELECTION_GROUP ) )
+				{
+					prosprSourceNames.add( sourceName );
+				};
+			}
+		}
+
+		public static void setMoBIE( MoBIE moBIE )
+		{
+			GeneSearchUtils.moBIE = moBIE;
+		}
+	}
+}
diff --git a/src/main/java/org/embl/mobie/viewer/plugins/platybrowser/GeneSearchCommand.java b/src/main/java/org/embl/mobie/viewer/plugins/platybrowser/GeneSearchCommand.java
new file mode 100644
index 000000000..fec384579
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/plugins/platybrowser/GeneSearchCommand.java
@@ -0,0 +1,57 @@
+package org.embl.mobie.viewer.plugins.platybrowser;
+
+import bdv.util.BdvFunctions;
+import bdv.util.BdvHandle;
+import bdv.util.BdvOptions;
+import bdv.viewer.SourceAndConverter;
+import ij.IJ;
+import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.bdv.BdvGlobalMousePositionProvider;
+import org.embl.mobie.viewer.bdv.BdvCircleOverlay;
+import org.embl.mobie.viewer.command.CommandConstants;
+import org.scijava.plugin.Parameter;
+import org.scijava.plugin.Plugin;
+import sc.fiji.bdvpg.scijava.ScijavaBdvDefaults;
+import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
+
+import java.util.Arrays;
+
+@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = CommandConstants.CONTEXT_MENU_ITEMS_ROOT + "ProSPr Gene Search")
+public class GeneSearchCommand implements BdvPlaygroundActionCommand
+{
+	@Parameter
+	BdvHandle bdvHandle;
+
+	@Parameter
+	SourceAndConverter[] sourceAndConverters;
+
+	@Parameter ( label = "Search radius [micrometer]")
+	private double radius = 3.0;
+
+	private static MoBIE moBIE;
+
+	public static void setMoBIE( MoBIE moBIE )
+	{
+		GeneSearchCommand.moBIE = moBIE;
+	}
+
+	@Override
+	public void run()
+	{
+		new Thread( () ->
+		{
+			double[] position = new BdvGlobalMousePositionProvider( bdvHandle ).getPositionAsDoubles();
+
+			final BdvCircleOverlay bdvCircleOverlay = new BdvCircleOverlay( position, radius );
+			BdvFunctions.showOverlay( bdvCircleOverlay, "", BdvOptions.options().addTo( bdvHandle ) );
+
+			IJ.log( "Gene search at [um]: " + Arrays.toString( position ) );
+			IJ.log( "Gene search: In progress, please wait..." );
+			final GeneSearch geneSearch = new GeneSearch( radius, position, moBIE );
+			geneSearch.searchGenes();
+			IJ.log( "Gene search: Done!" );
+		}
+		).start();
+	}
+
+}
diff --git a/src/main/java/org/embl/mobie/viewer/project/PublishedProjectsCreator.java b/src/main/java/org/embl/mobie/viewer/project/PublishedProjectsCreator.java
index e6b892f9f..89160d772 100644
--- a/src/main/java/org/embl/mobie/viewer/project/PublishedProjectsCreator.java
+++ b/src/main/java/org/embl/mobie/viewer/project/PublishedProjectsCreator.java
@@ -1,5 +1,7 @@
 package org.embl.mobie.viewer.project;
 
+import bdv.SpimSource;
+
 import java.util.HashMap;
 
 public class PublishedProjectsCreator
@@ -11,26 +13,28 @@ public PublishedProjectsCreator()
 		publishedProjects = new HashMap<>();
 
 		final PublishedProject platybrowserProject = new PublishedProject();
-		platybrowserProject.name = "Vergara et al. (2020) \"PlatyBrowser\", bioRxiv";
+		platybrowserProject.name = "Vergara et al. (2021) PlatyBrowser, Cell";
 		platybrowserProject.location = "https://github.com/mobie/platybrowser-datasets";
 		platybrowserProject.pulicationURL = "https://www.biorxiv.org/content/10.1101/2020.02.26.961037v1";
-
 		publishedProjects.put( platybrowserProject.name, platybrowserProject );
 
 		final PublishedProject covidTomoProject = new PublishedProject();
-		covidTomoProject.name = "Cortese et al. (2020) covid tomograms, Cell Host & Microbe";
+		covidTomoProject.name = "Cortese et al. (2020) Covid tomograms, Cell Host & Microbe";
 		covidTomoProject.location = "https://github.com/mobie/covid-tomo-datasets";
 		covidTomoProject.pulicationURL = "https://www.sciencedirect.com/science/article/pii/S193131282030620X";
-
 		publishedProjects.put( covidTomoProject.name, covidTomoProject );
 
 		final PublishedProject covidFIBProject = new PublishedProject();
-		covidFIBProject.name = "Cortese et al. (2020) covid FIB-SEM, Cell Host & Microbe";
+		covidFIBProject.name = "Cortese et al. (2020) Covid FIB-SEM, Cell Host & Microbe";
 		covidFIBProject.location = "https://github.com/mobie/covid-em-datasets";
 		covidFIBProject.pulicationURL = "https://www.sciencedirect.com/science/article/pii/S193131282030620X";
-
 		publishedProjects.put( covidFIBProject.name, covidFIBProject );
 
+		final PublishedProject spongeProject = new PublishedProject();
+		spongeProject.name = "Musser et al. (2021) Sponge FIB-SEM, Science";
+		spongeProject.location = "https://github.com/mobie/sponge-fibsem-project";
+		spongeProject.pulicationURL = "https://www.science.org/doi/abs/10.1126/science.abj2949";
+		publishedProjects.put( spongeProject.name, spongeProject );
 	}
 
 	public HashMap< String, PublishedProject > getPublishedProjects()
diff --git a/src/main/java/org/embl/mobie/viewer/projectcreator/DatasetJsonCreator.java b/src/main/java/org/embl/mobie/viewer/projectcreator/DatasetJsonCreator.java
index d2265e03f..1a1fec119 100644
--- a/src/main/java/org/embl/mobie/viewer/projectcreator/DatasetJsonCreator.java
+++ b/src/main/java/org/embl/mobie/viewer/projectcreator/DatasetJsonCreator.java
@@ -1,5 +1,7 @@
 package org.embl.mobie.viewer.projectcreator;
 
+import net.imglib2.realtransform.AffineTransform3D;
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.TableColumnNames;
 import org.embl.mobie.viewer.Dataset;
 import org.embl.mobie.viewer.display.ImageSourceDisplay;
@@ -8,13 +10,16 @@
 import org.embl.mobie.viewer.serialize.DatasetJsonParser;
 import org.embl.mobie.viewer.source.*;
 import org.embl.mobie.viewer.table.TableDataFormat;
+import org.embl.mobie.viewer.transform.AffineSourceTransformer;
+import org.embl.mobie.viewer.transform.SourceTransformer;
 import org.embl.mobie.viewer.view.View;
-import de.embl.cba.tables.FileAndUrlUtils;
+import org.embl.mobie.io.util.FileAndUrlUtils;
 import de.embl.cba.tables.color.ColoringLuts;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import static org.embl.mobie.viewer.projectcreator.ProjectCreatorHelper.imageFormatToFolderName;
@@ -27,58 +32,63 @@ public DatasetJsonCreator( ProjectCreator projectCreator ) {
         this.projectCreator = projectCreator;
     }
 
-    public void addImageToDatasetJson( String imageName, String datasetName,
-                                  String uiSelectionGroup, boolean is2D, int nTimepoints,
-                                  ImageDataFormat imageDataFormat, double[] contrastLimits, String colour ) {
-        Dataset dataset = fetchDataset( datasetName, is2D, nTimepoints );
+    public void addDataset( String datasetName, boolean is2D ) {
+        Dataset dataset = new Dataset();
+        dataset.sources = new HashMap<>();
+        dataset.views = new HashMap<>();
+        dataset.is2D = is2D;
+        writeDatasetJson( datasetName, dataset );
+    }
+
+    public void addImage(String imageName, String datasetName,
+                         String uiSelectionGroup, int nTimepoints,
+                         ImageDataFormat imageDataFormat, double[] contrastLimits, String colour,
+                         boolean exclusive, AffineTransform3D sourceTransform ) {
+        Dataset dataset = fetchDataset( datasetName, nTimepoints );
 
         addNewImageSource( dataset, imageName, imageDataFormat );
         if ( uiSelectionGroup != null ) {
             // add a view with the same name as the image, and sensible defaults
-            addNewImageView( dataset, imageName, uiSelectionGroup, contrastLimits, colour );
+            addNewImageView( dataset, imageName, uiSelectionGroup, contrastLimits, colour, exclusive, sourceTransform );
         }
 
         // if there is no default view, make one with this image and sensible defaults
         if ( !dataset.views.containsKey("default")) {
-            addNewDefaultImageView( dataset, imageName, contrastLimits, colour );
+            addNewDefaultImageView( dataset, imageName, contrastLimits, colour, sourceTransform );
         }
 
         writeDatasetJson( datasetName, dataset );
-        projectCreator.getDatasetsCreator().addImageDataFormat( imageDataFormat );
+        projectCreator.getProjectJsonCreator().addImageDataFormat( imageDataFormat );
     }
 
-    public void addSegmentationToDatasetJson( String imageName, String datasetName, String uiSelectionGroup,
-                                              boolean is2D, int nTimepoints, ImageDataFormat imageDataFormat ) {
-        Dataset dataset = fetchDataset( datasetName, is2D, nTimepoints );
+    public void addSegmentation(String imageName, String datasetName, String uiSelectionGroup,
+                                int nTimepoints, ImageDataFormat imageDataFormat,
+                                boolean exclusive, AffineTransform3D sourceTransform ) {
+        Dataset dataset = fetchDataset( datasetName, nTimepoints );
 
         addNewSegmentationSource( dataset, imageName, imageDataFormat );
         if ( uiSelectionGroup != null ) {
             // add a view with the same name as the image, and sensible defaults
-            addNewSegmentationView( dataset, imageName, uiSelectionGroup );
+            addNewSegmentationView( dataset, imageName, uiSelectionGroup, exclusive, sourceTransform );
         }
 
         // if there is no default view, make one with this image and sensible defaults
         if ( !dataset.views.containsKey("default")) {
-            addNewDefaultSegmentationView( dataset, imageName );
+            addNewDefaultSegmentationView( dataset, imageName, sourceTransform );
         }
 
         writeDatasetJson( datasetName, dataset );
-        projectCreator.getDatasetsCreator().addImageDataFormat( imageDataFormat );
+        projectCreator.getProjectJsonCreator().addImageDataFormat( imageDataFormat );
     }
 
-    private Dataset fetchDataset( String datasetName, boolean is2D, int nTimepoints ) {
+    public void makeDataset2D( String datasetName, boolean is2D ) {
         Dataset dataset = projectCreator.getDataset( datasetName );
-        if ( dataset == null ) {
-            dataset = new Dataset();
-            dataset.sources = new HashMap<>();
-            dataset.views = new HashMap<>();
-            // start new datasets with is2D as true, then for the first 3D image added it can be set to false
-            dataset.is2D = true;
-        }
+        dataset.is2D = is2D;
+        writeDatasetJson( datasetName, dataset );
+    }
 
-        if ( !is2D ) {
-            dataset.is2D = false;
-        }
+    private Dataset fetchDataset( String datasetName, int nTimepoints ) {
+        Dataset dataset = projectCreator.getDataset( datasetName );
 
         if ( nTimepoints > dataset.timepoints ) {
             dataset.timepoints = nTimepoints;
@@ -88,7 +98,7 @@ private Dataset fetchDataset( String datasetName, boolean is2D, int nTimepoints
     }
 
     private void addNewImageSource( Dataset dataset, String imageName, ImageDataFormat imageDataFormat ) {
-        Map<ImageDataFormat, StorageLocation> imageDataLocations;
+        Map< ImageDataFormat, StorageLocation > imageDataLocations;
         ImageSource imageSource = new ImageSource();
         imageDataLocations = makeImageDataLocations( imageDataFormat, imageName );
         imageSource.imageData = imageDataLocations;
@@ -98,7 +108,7 @@ private void addNewImageSource( Dataset dataset, String imageName, ImageDataForm
     }
 
     private void addNewSegmentationSource( Dataset dataset, String imageName, ImageDataFormat imageDataFormat ) {
-        Map<ImageDataFormat, StorageLocation> imageDataLocations;
+        Map< ImageDataFormat, StorageLocation > imageDataLocations;
 
         SegmentationSource segmentationSource = new SegmentationSource();
         segmentationSource.tableData = new HashMap<>();
@@ -114,9 +124,9 @@ private void addNewSegmentationSource( Dataset dataset, String imageName, ImageD
         dataset.sources.put( imageName, sourceSupplier );
     }
 
-    private Map<ImageDataFormat, StorageLocation> makeImageDataLocations( ImageDataFormat imageDataFormat,
-                                                                          String imageName ) {
-        Map<ImageDataFormat, StorageLocation> imageDataLocations = new HashMap<>();
+    private Map< ImageDataFormat, StorageLocation > makeImageDataLocations( ImageDataFormat imageDataFormat,
+                                                                            String imageName ) {
+        Map< ImageDataFormat, StorageLocation > imageDataLocations = new HashMap<>();
         StorageLocation imageStorageLocation = new StorageLocation();
         if ( imageDataFormat == ImageDataFormat.OmeZarr ) {
             imageStorageLocation.relativePath = "images/" + imageFormatToFolderName( imageDataFormat ) +
@@ -131,28 +141,39 @@ private Map<ImageDataFormat, StorageLocation> makeImageDataLocations( ImageDataF
     }
 
     private void addNewImageView( Dataset dataset, String imageName, String uiSelectionGroup,
-                                  double[] contrastLimits, String colour ) {
-        View view = createImageView( imageName, uiSelectionGroup, false, contrastLimits, colour );
+                                  double[] contrastLimits, String colour, boolean exclusive,
+                                  AffineTransform3D sourceTransform ) {
+        View view = createImageView( imageName, uiSelectionGroup, exclusive, contrastLimits, colour, sourceTransform );
         dataset.views.put( imageName, view );
     }
 
-    private void addNewSegmentationView( Dataset dataset, String imageName, String uiSelectionGroup ) {
-        View view = createSegmentationView( imageName, uiSelectionGroup, false );
+    private void addNewSegmentationView( Dataset dataset, String imageName, String uiSelectionGroup, boolean exclusive,
+                                         AffineTransform3D sourceTransform ) {
+        View view = createSegmentationView( imageName, uiSelectionGroup, exclusive, sourceTransform );
         dataset.views.put( imageName, view );
     }
 
-    private void addNewDefaultImageView( Dataset dataset, String imageName, double[] contrastLimits, String colour ) {
-        View view = createImageView( imageName, "bookmark", true, contrastLimits, colour );
+    private void addNewDefaultImageView( Dataset dataset, String imageName, double[] contrastLimits, String colour,
+                                         AffineTransform3D sourceTransform ) {
+        View view = createImageView( imageName, "bookmark", true, contrastLimits, colour,
+                sourceTransform );
         dataset.views.put( "default", view );
     }
 
-    private void addNewDefaultSegmentationView( Dataset dataset, String imageName ) {
-        View view = createSegmentationView( imageName, "bookmark", true );
+    private void addNewDefaultSegmentationView( Dataset dataset, String imageName, AffineTransform3D sourceTransform ) {
+        View view = createSegmentationView( imageName, "bookmark", true, sourceTransform );
         dataset.views.put( "default", view );
     }
 
+    private List<SourceTransformer> createSourceTransformerList( AffineTransform3D sourceTransform, List<String> sources ) {
+        List<SourceTransformer> sourceTransformerList = new ArrayList<>();
+        SourceTransformer sourceTransformer = new AffineSourceTransformer(
+                "affine", sourceTransform.getRowPackedCopy(), sources );
+        sourceTransformerList.add( sourceTransformer );
+        return  sourceTransformerList;
+    }
     private View createImageView( String imageName, String uiSelectionGroup, boolean isExclusive,
-                                  double[] contrastLimits, String colour ) {
+                                  double[] contrastLimits, String colour, AffineTransform3D sourceTransform ) {
         ArrayList< SourceDisplay > sourceDisplays = new ArrayList<>();
         ArrayList<String> sources = new ArrayList<>();
         sources.add( imageName );
@@ -161,11 +182,19 @@ private View createImageView( String imageName, String uiSelectionGroup, boolean
                 colour, contrastLimits, null, false );
         sourceDisplays.add( imageSourceDisplay );
 
-        View view = new View( uiSelectionGroup, sourceDisplays, null, null, isExclusive );
+        View view;
+        if ( sourceTransform.isIdentity() ) {
+            view = new View(uiSelectionGroup, sourceDisplays, null, null, isExclusive);
+        } else {
+            List<SourceTransformer> sourceTransformerList = createSourceTransformerList( sourceTransform, sources );
+            view = new View( uiSelectionGroup, sourceDisplays, sourceTransformerList, null, isExclusive );
+        }
+
         return view;
     }
 
-    private View createSegmentationView( String imageName, String uiSelectionGroup, boolean isExclusive ) {
+    private View createSegmentationView( String imageName, String uiSelectionGroup, boolean isExclusive,
+                                         AffineTransform3D sourceTransform ) {
         ArrayList< SourceDisplay > sourceDisplays = new ArrayList<>();
         ArrayList<String> sources = new ArrayList<>();
         sources.add( imageName );
@@ -179,7 +208,13 @@ private View createSegmentationView( String imageName, String uiSelectionGroup,
                 tables, null, null );
         sourceDisplays.add( segmentationSourceDisplay );
 
-        View view = new View( uiSelectionGroup, sourceDisplays, null, null, isExclusive );
+        View view;
+        if ( sourceTransform.isIdentity() ) {
+            view = new View( uiSelectionGroup, sourceDisplays, null, null, isExclusive );
+        } else {
+            List<SourceTransformer> sourceTransformerList = createSourceTransformerList( sourceTransform, sources );
+            view = new View( uiSelectionGroup, sourceDisplays, sourceTransformerList, null, isExclusive );
+        }
         return view;
     }
 
diff --git a/src/main/java/org/embl/mobie/viewer/projectcreator/DatasetsCreator.java b/src/main/java/org/embl/mobie/viewer/projectcreator/DatasetsCreator.java
index 550a6cc3b..1cc94128f 100644
--- a/src/main/java/org/embl/mobie/viewer/projectcreator/DatasetsCreator.java
+++ b/src/main/java/org/embl/mobie/viewer/projectcreator/DatasetsCreator.java
@@ -1,14 +1,9 @@
 package org.embl.mobie.viewer.projectcreator;
 
-import org.embl.mobie.viewer.Project;
-import org.embl.mobie.viewer.serialize.ProjectJsonParser;
-import org.embl.mobie.viewer.source.ImageDataFormat;
-import de.embl.cba.tables.FileAndUrlUtils;
+import org.embl.mobie.io.util.FileAndUrlUtils;
 import ij.IJ;
 
 import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
 import java.util.List;
 
 public class DatasetsCreator {
@@ -17,11 +12,20 @@ public class DatasetsCreator {
 
     private ProjectCreator projectCreator;
 
+    /**
+     * Make a datasetsCreator - includes all functions for creating and modifying datasets in projects
+     * @param projectCreator projectCreator
+     */
     public DatasetsCreator(ProjectCreator projectCreator ) {
         this.projectCreator = projectCreator;
     }
 
-    public void addDataset ( String datasetName ) {
+    /**
+     * Create a new, empty dataset
+     * @param datasetName dataset name
+     * @param is2D whether dataset only contains 2D images
+     */
+    public void addDataset ( String datasetName, boolean is2D ) {
         List<String> currentDatasets = projectCreator.getProject().getDatasets();
         boolean contains = currentDatasets.contains(datasetName);
         if ( !contains ) {
@@ -37,13 +41,11 @@ public void addDataset ( String datasetName ) {
                 new File(datasetDir, "tables").mkdirs();
                 new File(FileAndUrlUtils.combinePath(datasetDir.getAbsolutePath(), "misc", "views")).mkdirs();
 
+                // update project json
+                projectCreator.getProjectJsonCreator().addDataset( datasetName );
 
-                // if this is the first dataset, then make this the default
-                if ( currentDatasets.size() == 0 ) {
-                    projectCreator.getProject().setDefaultDataset( datasetName );
-                }
-                currentDatasets.add( datasetName );
-                writeProjectJson();
+                // create dataset json
+                projectCreator.getDatasetJsonCreator().addDataset( datasetName, is2D );
             } else {
                 IJ.log( "Dataset creation failed - this name already exists" );
             }
@@ -52,6 +54,11 @@ public void addDataset ( String datasetName ) {
         }
     }
 
+    /**
+     * Rename an existing dataset
+     * @param oldName old dataset name
+     * @param newName new dataset name
+     */
     public void renameDataset( String oldName, String newName ) {
 
         if ( !newName.equals(oldName) ) {
@@ -66,17 +73,8 @@ public void renameDataset( String oldName, String newName ) {
 
                 if (oldDatasetDir.exists()) {
                     if (oldDatasetDir.renameTo(newDatasetDir)) {
-
-                        // update json
-                        if ( projectCreator.getProject().getDefaultDataset().equals(oldName) ) {
-                            projectCreator.getProject().setDefaultDataset( newName );
-                        }
-
-                        int indexOld = projectCreator.getProject().getDatasets().indexOf(oldName);
-                        projectCreator.getProject().getDatasets().set(indexOld, newName);
-
-                        writeProjectJson();
-
+                        // update project json
+                        projectCreator.getProjectJsonCreator().renameDataset( oldName, newName );
                     } else {
                         IJ.log("Rename directory failed");
                     }
@@ -90,37 +88,22 @@ public void renameDataset( String oldName, String newName ) {
 
     }
 
+    /**
+     * Make the dataset the default when MoBIE is opened
+     * @param datasetName dataset name
+     */
     public void makeDefaultDataset ( String datasetName ) {
-        projectCreator.getProject().setDefaultDataset( datasetName );
-        writeProjectJson();
-    }
-
-    public void addImageDataFormat( ImageDataFormat imageDataFormat ) {
-        Project project = projectCreator.getProject();
-        if ( project.getImageDataFormats() == null ) {
-            project.setImageDataFormats( new ArrayList<>() );
-        }
-
-        List<ImageDataFormat> imageDataFormats = project.getImageDataFormats();
-        if ( !imageDataFormats.contains( imageDataFormat ) ) {
-            imageDataFormats.add( imageDataFormat );
-            writeProjectJson();
-        }
+        projectCreator.getProjectJsonCreator().setDefaultDataset( datasetName );
     }
 
-    private void writeProjectJson() {
-        try {
-            new ProjectJsonParser().saveProject( projectCreator.getProject(), projectCreator.getProjectJson().getAbsolutePath() );
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
 
-        // whether the project writing succeeded or not, we now read the current state of the project
-        try {
-            projectCreator.reloadProject();
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
+    /**
+     * Make a dataset 2D or 3D
+     * @param datasetName dataset name
+     * @param is2D 2D or not
+     */
+    public void makeDataset2D( String datasetName, boolean is2D ) {
+        projectCreator.getDatasetJsonCreator().makeDataset2D( datasetName, is2D );
     }
 
 }
diff --git a/src/main/java/org/embl/mobie/viewer/projectcreator/ImagesCreator.java b/src/main/java/org/embl/mobie/viewer/projectcreator/ImagesCreator.java
index c44a67a31..bb397c16b 100644
--- a/src/main/java/org/embl/mobie/viewer/projectcreator/ImagesCreator.java
+++ b/src/main/java/org/embl/mobie/viewer/projectcreator/ImagesCreator.java
@@ -1,23 +1,18 @@
 package org.embl.mobie.viewer.projectcreator;
 
 import bdv.img.n5.N5ImageLoader;
-import bdv.spimdata.SpimDataMinimal;
-import bdv.spimdata.XmlIoSpimDataMinimal;
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
 import ij.process.LUT;
+import org.embl.mobie.io.ImageDataFormat;
+import org.embl.mobie.io.SpimDataOpener;
 import org.embl.mobie.io.n5.loaders.N5FSImageLoader;
 import org.embl.mobie.io.n5.util.DownsampleBlock;
 import org.embl.mobie.io.n5.writers.WriteImgPlusToN5;
 import org.embl.mobie.io.ome.zarr.loaders.N5OMEZarrImageLoader;
-import org.embl.mobie.io.ome.zarr.readers.N5OmeZarrReader;
-import org.embl.mobie.io.ome.zarr.writers.imgplus.WriteImgPlusToN5BdvOmeZarr;
 import org.embl.mobie.io.ome.zarr.writers.imgplus.WriteImgPlusToN5OmeZarr;
 
-import org.embl.mobie.viewer.projectcreator.ui.ManualExportPanel;
-import org.embl.mobie.viewer.source.ImageDataFormat;
-import org.embl.mobie.viewer.source.SpimDataOpener;
-import de.embl.cba.tables.FileAndUrlUtils;
+import org.embl.mobie.io.util.FileAndUrlUtils;
 import de.embl.cba.tables.Tables;
 import ij.IJ;
 import ij.ImagePlus;
@@ -26,8 +21,6 @@
 import mpicbg.spim.data.XmlIoSpimData;
 import mpicbg.spim.data.generic.base.Entity;
 import mpicbg.spim.data.generic.sequence.BasicImgLoader;
-import mpicbg.spim.data.registration.ViewRegistration;
-import mpicbg.spim.data.registration.ViewRegistrations;
 import mpicbg.spim.data.sequence.*;
 import net.imglib2.RandomAccessibleInterval;
 import net.imglib2.converter.RealTypeConverters;
@@ -38,13 +31,16 @@
 import net.imglib2.type.numeric.integer.IntType;
 import net.imglib2.type.numeric.real.FloatType;
 import org.apache.commons.io.FileUtils;
+import org.janelia.saalfeldlab.n5.Compression;
 import org.janelia.saalfeldlab.n5.GzipCompression;
 import sc.fiji.bdvpg.sourceandconverter.importer.SourceAndConverterFromSpimDataCreator;
 import mpicbg.spim.data.sequence.SequenceDescription;
 
 import javax.swing.*;
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -59,6 +55,10 @@ public class ImagesCreator {
 
     ProjectCreator projectCreator;
 
+    /**
+     * Make an imagesCreator - includes all functions for adding images to a project
+     * @param projectCreator projectCreator
+     */
     public ImagesCreator( ProjectCreator projectCreator ) {
         this.projectCreator = projectCreator;
     }
@@ -72,13 +72,18 @@ private String getDefaultLocalImagePath( String datasetName, String imageName, I
     }
 
     private String getDefaultLocalImageXmlPath( String datasetName, String imageName, ImageDataFormat imageDataFormat ) {
-        return FileAndUrlUtils.combinePath(projectCreator.getDataLocation().getAbsolutePath(), datasetName,
-                "images", imageFormatToFolderName( imageDataFormat ), imageName + ".xml");
+        return FileAndUrlUtils.combinePath(getDefaultLocalImageDirPath(datasetName, imageDataFormat),
+                imageName + ".xml");
     }
 
     private String getDefaultLocalImageZarrPath( String datasetName, String imageName, ImageDataFormat imageDataFormat ) {
-        return FileAndUrlUtils.combinePath(projectCreator.getDataLocation().getAbsolutePath(), datasetName,
-                "images", imageFormatToFolderName( imageDataFormat ), imageName + ".ome.zarr");
+        return FileAndUrlUtils.combinePath(getDefaultLocalImageDirPath(datasetName, imageDataFormat),
+                imageName + ".ome.zarr");
+    }
+
+    private String getDefaultLocalN5Path( String datasetName, String imageName ) {
+        return FileAndUrlUtils.combinePath( getDefaultLocalImageDirPath( datasetName, ImageDataFormat.BdvN5),
+                imageName + ".n5" );
     }
 
     private String getDefaultLocalImageDirPath( String datasetName, ImageDataFormat imageDataFormat ) {
@@ -87,17 +92,148 @@ private String getDefaultLocalImageDirPath( String datasetName, ImageDataFormat
     }
 
     private String getDefaultTableDirPath( String datasetName, String imageName ) {
-        return FileAndUrlUtils.combinePath( projectCreator.getDataLocation().getAbsolutePath(), datasetName, "tables", imageName );
+        return FileAndUrlUtils.combinePath( projectCreator.getDataLocation().getAbsolutePath(),
+                datasetName, "tables", imageName );
     }
 
-    public void addImage (ImagePlus imp, String imageName, String datasetName,
-                          ImageDataFormat imageDataFormat, ProjectCreator.ImageType imageType,
-                          AffineTransform3D sourceTransform, boolean useDefaultSettings, String uiSelectionGroup ) {
+    private String getDefaultTablePath( String datasetName, String imageName ) {
+        return FileAndUrlUtils.combinePath( getDefaultTableDirPath(datasetName, imageName), "default.tsv" );
+    }
 
+    public boolean imageExists( String datasetName, String imageName, ImageDataFormat imageDataFormat ) {
+        // either xml file path or zarr file path depending on imageDataFormat
+        String filePath = getDefaultLocalImagePath( datasetName, imageName, imageDataFormat );
+        return new File (filePath).exists();
+    }
+
+    /**
+     *  Same as
+     *  {@link #addImage(ImagePlus, String, String, ImageDataFormat, ProjectCreator.ImageType, AffineTransform3D,
+     *  String, boolean, int[][], int[][], Compression) }, but calculates reasonable defaults for resolutions,
+     *  subdivisions and compression settings.
+     *
+     * @see #addImage(ImagePlus, String, String, ImageDataFormat, ProjectCreator.ImageType, AffineTransform3D, String, boolean, int[][], int[][], Compression)
+     */
+    public void addImage ( ImagePlus imp, String imageName, String datasetName,
+                           ImageDataFormat imageDataFormat, ProjectCreator.ImageType imageType,
+                           AffineTransform3D sourceTransform, String uiSelectionGroup,
+                           boolean exclusive ) throws SpimDataException, IOException {
+        addImage( imp, imageName, datasetName, imageDataFormat, imageType, sourceTransform, uiSelectionGroup,
+                exclusive, null, null, null );
+
+    }
+
+    /**
+     *  Same as
+     *  {@link #addImage(ImagePlus, String, String, ImageDataFormat, ProjectCreator.ImageType, AffineTransform3D,
+     *  String, boolean, int[][], int[][], Compression) }, but assumes identity sourceTransform, and calculates
+     *  reasonable defaults for resolutions, subdivisions and compression settings.
+     *
+     * @see #addImage(ImagePlus, String, String, ImageDataFormat, ProjectCreator.ImageType, AffineTransform3D, String, boolean, int[][], int[][], Compression)
+     */
+    public void addImage ( ImagePlus imp, String imageName, String datasetName,
+                           ImageDataFormat imageDataFormat, ProjectCreator.ImageType imageType,
+                           String uiSelectionGroup, boolean exclusive ) throws SpimDataException, IOException {
+        addImage( imp, imageName, datasetName, imageDataFormat, imageType, new AffineTransform3D(), uiSelectionGroup,
+                exclusive, null, null, null );
+
+    }
+
+    /**
+     *  Same as
+     *  {@link #addImage(ImagePlus, String, String, ImageDataFormat, ProjectCreator.ImageType, AffineTransform3D,
+     *  String, boolean, int[][], int[][], Compression) }, but assumes identity sourceTransform
+     *
+     * @see #addImage(ImagePlus, String, String, ImageDataFormat, ProjectCreator.ImageType, AffineTransform3D, String, boolean, int[][], int[][], Compression)
+     */
+    public void addImage ( ImagePlus imp, String imageName, String datasetName,
+                           ImageDataFormat imageDataFormat, ProjectCreator.ImageType imageType,
+                           String uiSelectionGroup, boolean exclusive,
+                           int[][] resolutions, int[][] subdivisions, Compression compression ) throws SpimDataException, IOException {
+        addImage( imp, imageName, datasetName, imageDataFormat, imageType, new AffineTransform3D(), uiSelectionGroup,
+                exclusive, resolutions, subdivisions, compression );
+    }
+
+    /**
+     * Add an image to a MoBIE project. Make sure the ImagePlus scale and unit is set properly, so that imp.getCalibration()
+     * returns the correct values. Note that multi-channel images are not supported - you will need to
+     * split these channels and add each as its own image.
+     * @param imp image to add
+     * @param imageName image name
+     * @param datasetName dataset name
+     * @param imageDataFormat image format
+     * @param imageType Image or Segmentation - segmentations will additionally generate a table.
+     * @param sourceTransform Affine transform - this will be added to the image view, and will be added on top
+     *                        of the normal scaling coming from imp.getCalibration()
+     * @param uiSelectionGroup Name of MoBIE drop-down menu to place view in
+     * @param exclusive Whether to make the view exclusive.
+     * @param resolutions Resolution/downsampling levels to write e.g. new int[][]{ {1,1,1}, {2,2,2}, {4,4,4} }
+     *                    will write one full resolution level, then one 2x downsampled, and one 4x downsampled.
+     *                    The order is {x, y, z}.
+     * @param subdivisions Chunk size. Must have the same number of entries as 'resolutions'. e.g.
+     *                     new int[][]{ {64,64,64}, {64,64,64}, {64,64,64} }. The order is {x, y, z}.
+     * @param compression type of compression to use
+     * @throws SpimDataException
+     * @throws IOException
+     */
+    public void addImage ( ImagePlus imp, String imageName, String datasetName,
+                           ImageDataFormat imageDataFormat, ProjectCreator.ImageType imageType,
+                           AffineTransform3D sourceTransform, String uiSelectionGroup, boolean exclusive,
+                           int[][] resolutions, int[][] subdivisions, Compression compression ) throws IOException, SpimDataException {
         // either xml file path or zarr file path depending on imageDataFormat
         String filePath = getDefaultLocalImagePath( datasetName, imageName, imageDataFormat );
         File imageFile = new File(filePath);
 
+        if ( !isImageValid( imp.getNChannels(), imp.getCalibration().getUnit(),
+                projectCreator.getVoxelUnit(), false ) ) {
+            return;
+        }
+
+        if ( imp.getNDimensions() > 2 && projectCreator.getDataset( datasetName ).is2D ) {
+            throw new UnsupportedOperationException("Can't add a " + imp.getNDimensions() + "D image to a 2D dataset" );
+        }
+
+        if ( projectCreator.getVoxelUnit() == null ) {
+            projectCreator.setVoxelUnit( imp.getCalibration().getUnit() );
+        }
+
+        if ( imageFile.exists() ) {
+            IJ.log("Overwriting image " + imageName + " in dataset " + datasetName );
+            deleteImageFiles( datasetName, imageName, imageDataFormat );
+        }
+
+        DownsampleBlock.DownsamplingMethod downsamplingMethod = getDownsamplingMethod( imageType );
+
+        File imageDir = new File(imageFile.getParent());
+        if ( !imageDir.exists() ) {
+            imageDir.mkdirs();
+        }
+
+        if ( resolutions == null || subdivisions == null || compression == null ) {
+            writeDefaultImage( imp, filePath, downsamplingMethod, imageName, imageDataFormat );
+        } else {
+            writeDefaultImage( imp, filePath, downsamplingMethod, imageName, imageDataFormat,
+                    resolutions, subdivisions, compression );
+        }
+
+        // check image written successfully, before writing jsons
+        if ( imageFile.exists() ) {
+            if (imageType == ProjectCreator.ImageType.image) {
+                double[] contrastLimits = new double[]{imp.getDisplayRangeMin(), imp.getDisplayRangeMax()};
+                LUT lut = imp.getLuts()[0];
+                String colour = "r=" + lut.getRed(255) + ",g=" + lut.getGreen(255) + ",b=" +
+                        lut.getBlue(255) + ",a=" + lut.getAlpha(255);
+                updateTableAndJsonsForNewImage( imageName, datasetName, uiSelectionGroup,
+                        imp.getNFrames(), imageDataFormat, contrastLimits, colour, exclusive, sourceTransform );
+            } else {
+                updateTableAndJsonsForNewSegmentation(imageName, datasetName, uiSelectionGroup,
+                        imp.getNFrames(), imageDataFormat, exclusive, sourceTransform );
+            }
+        }
+
+    }
+
+    private DownsampleBlock.DownsamplingMethod getDownsamplingMethod( ProjectCreator.ImageType imageType ) {
         DownsampleBlock.DownsamplingMethod downsamplingMethod;
         switch( imageType ) {
             case image:
@@ -107,137 +243,213 @@ public void addImage (ImagePlus imp, String imageName, String datasetName,
                 downsamplingMethod = DownsampleBlock.DownsamplingMethod.Centre;
         }
 
-        if ( !imageFile.exists() ) {
+        return downsamplingMethod;
+    }
 
-            File imageDir = new File(imageFile.getParent());
-            if ( !imageDir.exists() ) {
-                imageDir.mkdirs();
-            }
+    private void writeDefaultImage( ImagePlus imp, String filePath,
+                                    DownsampleBlock.DownsamplingMethod downsamplingMethod,
+                                    String imageName, ImageDataFormat imageDataFormat ) {
 
-            if ( !useDefaultSettings ) {
-                new ManualExportPanel( imp, filePath, sourceTransform, downsamplingMethod, imageName, imageDataFormat).getManualExportParameters();
-            } else {
-                writeDefaultImage( imp, filePath, sourceTransform, downsamplingMethod, imageName, imageDataFormat );
-            }
+        // transform only including scaling from image
+        AffineTransform3D sourceTransform = ProjectCreatorHelper.generateDefaultAffine( imp );
+
+        // gzip compression by default
+        switch( imageDataFormat ) {
+            case BdvN5:
+                new WriteImgPlusToN5().export(imp, filePath, sourceTransform, downsamplingMethod,
+                        new GzipCompression(), new String[]{imageName} );
+                break;
+
+            case OmeZarr:
+                new WriteImgPlusToN5OmeZarr().export(imp, filePath, sourceTransform, downsamplingMethod,
+                        new GzipCompression() );
+                break;
+
+            default:
+                throw new UnsupportedOperationException();
 
-            // check image written successfully, before writing jsons
-            if ( imageFile.exists() ) {
-                boolean is2D;
-                if ( imp.getNDimensions() <= 2 ) {
-                    is2D = true;
-                } else {
-                    is2D = false;
-                }
-                try {
-                    if (imageType == ProjectCreator.ImageType.image) {
-                        double[] contrastLimits = new double[]{imp.getDisplayRangeMin(), imp.getDisplayRangeMax()};
-                        LUT lut = imp.getLuts()[0];
-                        String colour = "r=" + lut.getRed(255) + ",g=" + lut.getGreen(255) + ",b=" +
-                                lut.getBlue(255) + ",a=" + lut.getAlpha(255);
-                        updateTableAndJsonsForNewImage(imageName, datasetName, uiSelectionGroup, is2D,
-                                imp.getNFrames(), imageDataFormat, contrastLimits, colour );
-                    } else {
-                        updateTableAndJsonsForNewSegmentation(imageName, datasetName, uiSelectionGroup, is2D,
-                                imp.getNFrames(), imageDataFormat);
-                    }
-                } catch (SpimDataException e) {
-                    e.printStackTrace();
-                }
-            }
-        } else {
-            IJ.log( "Adding image to project failed - this image name already exists" );
         }
     }
 
-    private void writeDefaultImage( ImagePlus imp, String filePath, AffineTransform3D sourceTransform,
-                                   DownsampleBlock.DownsamplingMethod downsamplingMethod,
-                                   String imageName, ImageDataFormat imageDataFormat ) {
+    private void writeDefaultImage( ImagePlus imp, String filePath,
+                             DownsampleBlock.DownsamplingMethod downsamplingMethod,
+                             String imageName, ImageDataFormat imageDataFormat,
+                             int[][] resolutions, int[][] subdivisions, Compression compression ) {
+
+        // transform only including scaling from image
+        AffineTransform3D sourceTransform = ProjectCreatorHelper.generateDefaultAffine( imp );
 
-        // gzip compression by default
         switch( imageDataFormat ) {
             case BdvN5:
-                new WriteImgPlusToN5().export(imp, filePath, sourceTransform, downsamplingMethod,
-                        new GzipCompression(), new String[]{imageName} );
+                new WriteImgPlusToN5().export(imp, resolutions, subdivisions, filePath, sourceTransform,
+                        downsamplingMethod, compression, new String[]{imageName});
                 break;
 
-            case BdvOmeZarr:
-                new WriteImgPlusToN5BdvOmeZarr().export(imp, filePath, sourceTransform, downsamplingMethod,
-                        new GzipCompression(), new String[]{imageName} );
+            case OmeZarr:
+                new WriteImgPlusToN5OmeZarr().export(imp, resolutions, subdivisions, filePath, sourceTransform,
+                        downsamplingMethod, compression );
+                break;
+
+            default:
+                throw new UnsupportedOperationException();
+        }
+    }
+
+    private void deleteImageFiles( String datasetName, String imageName, ImageDataFormat imageDataFormat ) throws IOException {
+
+        File xmlFile = null;
+        File n5File = null;
+        File zarrFile = null;
+        switch( imageDataFormat ) {
+            case BdvN5:
+                xmlFile = new File( getDefaultLocalImageXmlPath( datasetName, imageName, imageDataFormat ) );
+                n5File = new File( getDefaultLocalN5Path( datasetName, imageName ) );
                 break;
 
             case OmeZarr:
-                new WriteImgPlusToN5OmeZarr().export(imp, filePath, sourceTransform, downsamplingMethod,
-                        new GzipCompression(), new String[]{imageName});
+                zarrFile = new File( getDefaultLocalImageZarrPath( datasetName, imageName, imageDataFormat ) );
                 break;
 
             default:
                 throw new UnsupportedOperationException();
 
         }
+
+        File tableFile = new File( getDefaultTablePath( datasetName, imageName ) );
+
+        // delete files
+        for ( File file : new File[] {xmlFile, tableFile}) {
+            if ( file != null && file.exists() ) {
+                Files.delete( file.toPath() );
+            }
+        }
+
+        // delete directories
+        for ( File file : new File[]{ zarrFile, n5File }) {
+            if ( file != null && file.exists() ) {
+                FileUtils.deleteDirectory( file );
+            }
+        }
     }
 
-    public void addBdvFormatImage ( File fileLocation, String datasetName, ProjectCreator.ImageType imageType,
-                                   ProjectCreator.AddMethod addMethod, String uiSelectionGroup,
-                                    ImageDataFormat imageDataFormat ) throws SpimDataException, IOException {
+    /**
+     * Add a bdv format image to a MoBIE project (e.g. N5 or OME-ZARR). Make sure the scale and unit is set properly
+     * in the file. Note that multi-channel images are not supported - you will need to split these channels and add
+     * each as its own image.
+     * @param fileLocation Location of image file to add - for n5, location of the xml,
+     *                     for ome-zarr, the location of the .ome.zarr directory.
+     * @param imageName image name
+     * @param datasetName dataset name
+     * @param imageType Image or Segmentation - segmentations will additionally generate a table.
+     * @param addMethod Add method - link (leave image as-is, and link to this location. Only supported for
+     *                  N5 and local projects), copy (copy image into project), or move (move image into project -
+     *                  be careful as this will delete the image from its original location!)
+     * @param uiSelectionGroup Name of MoBIE drop-down menu to place view in
+     * @param imageDataFormat image format
+     * @param exclusive Whether to make the view exclusive.
+     * @throws SpimDataException
+     * @throws IOException
+     */
+    public void addBdvFormatImage ( File fileLocation, String imageName, String datasetName,
+                                    ProjectCreator.ImageType imageType, ProjectCreator.AddMethod addMethod,
+                                    String uiSelectionGroup, ImageDataFormat imageDataFormat, boolean exclusive ) throws SpimDataException, IOException {
 
         if ( fileLocation.exists() ) {
+            SpimData spimData = ( SpimData ) new SpimDataOpener().openSpimData( fileLocation.getAbsolutePath(), imageDataFormat );
+            addBdvFormatImage( spimData, imageName, datasetName, imageType, addMethod, uiSelectionGroup,
+                    imageDataFormat, exclusive );
+        } else {
+            throw new FileNotFoundException(
+                    "Adding image to project failed - " + fileLocation.getAbsolutePath() + " does not exist" );
+        }
+    }
 
-            SpimData spimData = new SpimDataOpener().openSpimData( fileLocation.getAbsolutePath(), imageDataFormat );
-            String imageName = fileLocation.getName().split("\\.")[0];
-            File imageDirectory = new File( getDefaultLocalImageDirPath( datasetName, imageDataFormat ));
+    /**
+     * Add a bdv format image to a MoBIE project (e.g. N5 or OME-ZARR). Make sure the scale and unit is set properly
+     * in the file. Note that multi-channel images are not supported - you will need to split these channels and add
+     * each as its own image.
+     * @param spimData spimData of n5 or ome-zarr file
+     * @param imageName image name
+     * @param datasetName dataset name
+     * @param imageType Image or Segmentation - segmentations will additionally generate a table.
+     * @param addMethod Add method - link (leave image as-is, and link to this location. Only supported for
+     *                  N5 and local projects), copy (copy image into project), or move (move image into project -
+     *                  be careful as this will delete the image from its original location!)
+     * @param uiSelectionGroup Name of MoBIE drop-down menu to place view in
+     * @param imageDataFormat image format
+     * @param exclusive Whether to make the view exclusive.
+     * @throws SpimDataException
+     * @throws IOException
+     */
+    public void addBdvFormatImage ( SpimData spimData, String imageName, String datasetName,
+                                    ProjectCreator.ImageType imageType, ProjectCreator.AddMethod addMethod,
+                                    String uiSelectionGroup, ImageDataFormat imageDataFormat, boolean exclusive ) throws SpimDataException, IOException {
+
+        File imageDirectory = new File( getDefaultLocalImageDirPath( datasetName, imageDataFormat ));
+
+        int nChannels = spimData.getSequenceDescription().getViewSetupsOrdered().size();
+        String imageUnit = spimData.getSequenceDescription().getViewSetupsOrdered().get(0).getVoxelSize().unit();
+
+        if ( !isImageValid( nChannels, imageUnit, projectCreator.getVoxelUnit(), true ) ) {
+            return;
+        }
 
-            File newImageFile = null;
-            switch( imageDataFormat ) {
-                case BdvN5:
+        if ( !isSpimData2D(spimData) && projectCreator.getDataset( datasetName ).is2D ) {
+            throw new UnsupportedOperationException("Can't add a 3D image to a 2D dataset" );
+        }
 
-                case BdvOmeZarr:
-                    newImageFile = new File(imageDirectory, imageName + ".xml");
-                    // The view setup name must be the same as the image name
-                    spimData = fixSetupName( spimData, imageName );
-                    break;
+        if ( projectCreator.getVoxelUnit() == null ) {
+            projectCreator.setVoxelUnit( imageUnit );
+        }
 
-                case OmeZarr:
-                    newImageFile = new File(imageDirectory, imageName + ".ome.zarr" );
-                    break;
-            }
+        File newImageFile = null;
+        switch( imageDataFormat ) {
+            case BdvN5:
+                newImageFile = new File(imageDirectory, imageName + ".xml");
+                // The view setup name must be the same as the image name
+                spimData = fixSetupName( spimData, imageName );
+                break;
 
-            if ( !newImageFile.exists() ) {
+            case OmeZarr:
+                newImageFile = new File(imageDirectory, imageName + ".ome.zarr" );
+                break;
+        }
 
-                // make directory for that image file format, if doesn't exist already
-                File imageDir = new File( newImageFile.getParent() );
-                if ( !imageDir.exists() ) {
-                    imageDir.mkdirs();
-                }
+        if ( newImageFile.exists() ) {
+            IJ.log("Overwriting image " + imageName + " in dataset " + datasetName );
+            deleteImageFiles( datasetName, imageName, imageDataFormat );
+        }
 
-                switch (addMethod) {
-                    case link:
-                        // TODO - linking currently not supported for ome-zarr
-                        new XmlIoSpimData().save(spimData, newImageFile.getAbsolutePath());
-                        break;
-                    case copy:
-                        copyImage( imageDataFormat, spimData, imageDirectory, imageName);
-                        break;
-                    case move:
-                        moveImage( imageDataFormat, spimData, imageDirectory, imageName);
-                        break;
-                }
+        // make directory for that image file format, if doesn't exist already
+        File imageDir = new File( newImageFile.getParent() );
+        if ( !imageDir.exists() ) {
+            imageDir.mkdirs();
+        }
 
-                if (imageType == ProjectCreator.ImageType.image) {
-                    updateTableAndJsonsForNewImage(imageName, datasetName, uiSelectionGroup,
-                            isSpimData2D(spimData), getNTimepointsFromSpimData(spimData),
-                            imageDataFormat, new double[]{0.0, 255.0}, "white" );
-                } else {
-                    updateTableAndJsonsForNewSegmentation(imageName, datasetName, uiSelectionGroup,
-                            isSpimData2D(spimData), getNTimepointsFromSpimData(spimData), imageDataFormat);
-                }
+        switch (addMethod) {
+            case link:
+                // TODO - linking currently not supported for ome-zarr
+                spimData.setBasePath( imageDir );
+                new XmlIoSpimData().save(spimData, newImageFile.getAbsolutePath());
+                break;
+            case copy:
+                copyImage( imageDataFormat, spimData, imageDirectory, imageName);
+                break;
+            case move:
+                moveImage( imageDataFormat, spimData, imageDirectory, imageName);
+                break;
+        }
 
-                IJ.log( "Bdv format image " + imageName + " added to project" );
-            } else {
-                IJ.log("Adding image to project failed - this image name already exists");
-            }
+        if (imageType == ProjectCreator.ImageType.image) {
+            updateTableAndJsonsForNewImage( imageName, datasetName, uiSelectionGroup,
+                    getNTimepointsFromSpimData(spimData), imageDataFormat, new double[]{0.0, 255.0},
+                    "white", exclusive, new AffineTransform3D() );
         } else {
-            IJ.log( "Adding image to project failed - " + fileLocation.getAbsolutePath() + " does not exist" );
+            updateTableAndJsonsForNewSegmentation( imageName, datasetName, uiSelectionGroup,
+                    getNTimepointsFromSpimData(spimData), imageDataFormat, exclusive, new AffineTransform3D() );
         }
+
+        IJ.log( "Bdv format image " + imageName + " added to project" );
     }
 
     private ArrayList<Object[]> makeDefaultTableRowsForTimepoint( Source labelsSource, int timepoint, boolean addTimepointColumn ) {
@@ -296,7 +508,7 @@ private ArrayList<Object[]> makeDefaultTableRowsForTimepoint( Source labelsSourc
     // TODO - is this efficient for big images?
     private void addDefaultTableForImage ( String imageName, String datasetName, ImageDataFormat imageDataFormat ) {
         File tableFolder = new File( getDefaultTableDirPath( datasetName, imageName ) );
-        File defaultTable = new File( tableFolder, "default.tsv" );
+        File defaultTable = new File( getDefaultTablePath( datasetName, imageName ) );
         if ( !tableFolder.exists() ){
             tableFolder.mkdirs();
         }
@@ -307,7 +519,7 @@ private void addDefaultTableForImage ( String imageName, String datasetName, Ima
 
             // xml file or zarr file, depending on imageDataFormat
             String filePath = getDefaultLocalImagePath( datasetName, imageName, imageDataFormat );
-            SpimData spimData = new SpimDataOpener().openSpimData( filePath, imageDataFormat);
+            SpimData spimData = tryOpenSpimData( imageDataFormat, filePath );
             final SourceAndConverterFromSpimDataCreator creator = new SourceAndConverterFromSpimDataCreator( spimData );
             final SourceAndConverter<?> sourceAndConverter = creator.getSetupIdToSourceAndConverter().values().iterator().next();
             final Source labelsSource = sourceAndConverter.getSpimSource();
@@ -344,20 +556,33 @@ private void addDefaultTableForImage ( String imageName, String datasetName, Ima
         }
     }
 
+    private SpimData tryOpenSpimData( ImageDataFormat imageDataFormat, String filePath )
+    {
+        try
+        {
+            return ( SpimData ) new SpimDataOpener().openSpimData( filePath, imageDataFormat );
+        } catch ( SpimDataException e )
+        {
+           throw new RuntimeException( e );
+        }
+    }
+
     private void updateTableAndJsonsForNewImage ( String imageName, String datasetName, String uiSelectionGroup,
-                                                  boolean is2D, int nTimepoints, ImageDataFormat imageDataFormat,
-                                                  double[] contrastLimits, String colour ) throws SpimDataException {
+                                                  int nTimepoints, ImageDataFormat imageDataFormat,
+                                                  double[] contrastLimits, String colour,
+                                                  boolean exclusive, AffineTransform3D sourceTransform ) {
         DatasetJsonCreator datasetJsonCreator = projectCreator.getDatasetJsonCreator();
-        datasetJsonCreator.addImageToDatasetJson( imageName, datasetName, uiSelectionGroup, is2D, nTimepoints,
-                imageDataFormat, contrastLimits, colour );
+        datasetJsonCreator.addImage( imageName, datasetName, uiSelectionGroup, nTimepoints,
+                imageDataFormat, contrastLimits, colour, exclusive, sourceTransform );
     }
 
     private void updateTableAndJsonsForNewSegmentation( String imageName, String datasetName, String uiSelectionGroup,
-                                                        boolean is2D, int nTimepoints, ImageDataFormat imageDataFormat ) {
+                                                        int nTimepoints, ImageDataFormat imageDataFormat,
+                                                        boolean exclusive, AffineTransform3D sourceTransform ) {
         addDefaultTableForImage( imageName, datasetName, imageDataFormat );
         DatasetJsonCreator datasetJsonCreator = projectCreator.getDatasetJsonCreator();
-        datasetJsonCreator.addSegmentationToDatasetJson( imageName, datasetName, uiSelectionGroup, is2D, nTimepoints,
-                imageDataFormat );
+        datasetJsonCreator.addSegmentation( imageName, datasetName, uiSelectionGroup, nTimepoints,
+                imageDataFormat, exclusive, sourceTransform );
     }
 
     private void copyImage ( ImageDataFormat imageFormat, SpimData spimData,
@@ -372,12 +597,6 @@ private void copyImage ( ImageDataFormat imageFormat, SpimData spimData,
                 writeNewBdvXml( spimData, newImageFile, imageDirectory, imageName, imageFormat );
                 break;
 
-            case BdvOmeZarr:
-                newImageFile = new File(imageDirectory, imageName + ".ome.zarr" );
-                FileUtils.copyDirectory(imageLocation, newImageFile);
-                writeNewBdvXml( spimData, newImageFile, imageDirectory, imageName, imageFormat );
-                break;
-
             case OmeZarr:
                 newImageFile = new File(imageDirectory, imageName + ".ome.zarr" );
                 FileUtils.copyDirectory(imageLocation, newImageFile);
@@ -399,13 +618,6 @@ private void moveImage ( ImageDataFormat imageFormat, SpimData spimData,
                 writeNewBdvXml( spimData, newImageFile, imageDirectory, imageName, imageFormat );
                 break;
 
-            case BdvOmeZarr:
-                newImageFile = new File(imageDirectory, imageName + ".ome.zarr" );
-                closeImgLoader( spimData, imageFormat );
-                FileUtils.moveDirectory( imageLocation, newImageFile );
-                writeNewBdvXml( spimData, newImageFile, imageDirectory, imageName, imageFormat );
-                break;
-
             case OmeZarr:
                 newImageFile = new File(imageDirectory, imageName + ".ome.zarr" );
                 closeImgLoader( spimData, imageFormat );
@@ -426,8 +638,6 @@ private void closeImgLoader ( SpimData spimData, ImageDataFormat imageFormat ) {
                 }
                 break;
 
-            case BdvOmeZarr:
-
             case OmeZarr:
                 if (imgLoader instanceof N5OMEZarrImageLoader ) {
                     ( (N5OMEZarrImageLoader) imgLoader ).close();
@@ -471,19 +681,11 @@ private SpimData fixSetupName( SpimData spimData, String imageName ) {
     }
 
     private void writeNewBdvXml ( SpimData spimData, File imageFile, File saveDirectory, String imageName,
-                                  ImageDataFormat imageFormat ) throws SpimDataException, IOException {
+                                  ImageDataFormat imageFormat ) throws SpimDataException {
 
         ImgLoader imgLoader = null;
-        switch ( imageFormat ) {
-            case BdvN5:
-                imgLoader = new N5ImageLoader( imageFile, null);
-                break;
-
-            case BdvOmeZarr:
-                imgLoader = new N5OMEZarrImageLoader(
-                        new N5OmeZarrReader(imageFile.getAbsolutePath()), spimData.getSequenceDescription());
-                break;
-
+        if (imageFormat == ImageDataFormat.BdvN5) {
+            imgLoader = new N5ImageLoader(imageFile, null);
         }
 
         spimData.setBasePath( saveDirectory );
@@ -491,38 +693,4 @@ private void writeNewBdvXml ( SpimData spimData, File imageFile, File saveDirect
         new XmlIoSpimData().save(spimData, new File( saveDirectory, imageName + ".xml").getAbsolutePath() );
     }
 
-    private void addAffineTransformToXml ( String xmlPath, String affineTransform )  {
-        if ( affineTransform != null ) {
-            try {
-                SpimDataMinimal spimDataMinimal = new XmlIoSpimDataMinimal().load( xmlPath );
-                int numTimepoints = spimDataMinimal.getSequenceDescription().getTimePoints().size();
-                int numSetups = spimDataMinimal.getSequenceDescription().getViewSetupsOrdered().size();
-
-                AffineTransform3D sourceTransform = new AffineTransform3D();
-                String[] splitAffineTransform = affineTransform.split(" ");
-                double[] doubleAffineTransform = new double[splitAffineTransform.length];
-                for ( int i = 0; i < splitAffineTransform.length; i++ ) {
-                    doubleAffineTransform[i] = Double.parseDouble( splitAffineTransform[i] );
-                }
-                sourceTransform.set( doubleAffineTransform );
-
-                final ArrayList<ViewRegistration> registrations = new ArrayList<>();
-                for ( int t = 0; t < numTimepoints; ++t ) {
-                    for (int s = 0; s < numSetups; ++s) {
-                        registrations.add(new ViewRegistration(t, s, sourceTransform));
-                    }
-                }
-
-                SpimDataMinimal updatedSpimDataMinimial = new SpimDataMinimal(spimDataMinimal.getBasePath(),
-                        spimDataMinimal.getSequenceDescription(), new ViewRegistrations( registrations) );
-
-                new XmlIoSpimDataMinimal().save( updatedSpimDataMinimial, xmlPath);
-            } catch (SpimDataException e) {
-                IJ.log( "Error adding affine transform to xml file. Check xml manually.");
-                e.printStackTrace();
-            }
-
-        }
-    }
-
 }
diff --git a/src/main/java/org/embl/mobie/viewer/projectcreator/ProjectCreator.java b/src/main/java/org/embl/mobie/viewer/projectcreator/ProjectCreator.java
index 2b4afc55c..da23163a5 100644
--- a/src/main/java/org/embl/mobie/viewer/projectcreator/ProjectCreator.java
+++ b/src/main/java/org/embl/mobie/viewer/projectcreator/ProjectCreator.java
@@ -1,10 +1,17 @@
 package org.embl.mobie.viewer.projectcreator;
 
+import mpicbg.spim.data.SpimData;
+import mpicbg.spim.data.SpimDataException;
+import mpicbg.spim.data.sequence.VoxelDimensions;
+import org.embl.mobie.io.ImageDataFormat;
+import org.embl.mobie.io.SpimDataOpener;
 import org.embl.mobie.viewer.Dataset;
 import org.embl.mobie.viewer.Project;
 import org.embl.mobie.viewer.serialize.DatasetJsonParser;
 import org.embl.mobie.viewer.serialize.ProjectJsonParser;
-import de.embl.cba.tables.FileAndUrlUtils;
+import org.embl.mobie.io.util.FileAndUrlUtils;
+import org.embl.mobie.viewer.source.ImageSource;
+import org.embl.mobie.viewer.source.SourceSupplier;
 
 import java.io.File;
 import java.io.IOException;
@@ -24,10 +31,12 @@ public class ProjectCreator {
     private String currentDatasetName;
     private Dataset currentDataset;
     private Map<String, ArrayList<String> > currentGrouptoViews;
+    private String voxelUnit;
 
     private final DatasetsCreator datasetsCreator;
     private final ImagesCreator imagesCreator;
     private final DatasetJsonCreator datasetJsonCreator;
+    private final ProjectJsonCreator projectJsonCreator;
     private final RemoteMetadataCreator remoteMetadataCreator;
 
     public enum ImageType {
@@ -41,7 +50,11 @@ public enum AddMethod {
         move
     }
 
-    // data location is the folder that contains the projects.json and the individual dataset folders
+    /**
+     * Make a project creator, allowing creation / editing of projects
+     * @param dataLocation directory that contains the project.json and individual dataset directories
+     * @throws IOException
+     */
     public ProjectCreator(File dataLocation ) throws IOException {
         this.dataLocation = dataLocation;
         projectJson = new File( FileAndUrlUtils.combinePath(  dataLocation.getAbsolutePath(), "project.json") );
@@ -56,8 +69,15 @@ public ProjectCreator(File dataLocation ) throws IOException {
 
         this.datasetsCreator = new DatasetsCreator( this );
         this.datasetJsonCreator = new DatasetJsonCreator( this );
+        this.projectJsonCreator = new ProjectJsonCreator( this );
         this.imagesCreator = new ImagesCreator( this );
         this.remoteMetadataCreator = new RemoteMetadataCreator( this );
+
+        try {
+            readVoxelUnitFromImage();
+        } catch (SpimDataException e) {
+            e.printStackTrace();
+        }
     }
 
     public File getDataLocation() { return dataLocation; }
@@ -68,6 +88,10 @@ public Project getProject() {
 
     public File getProjectJson() { return projectJson; }
 
+    /**
+     * Reload project by parsing the project.json again
+     * @throws IOException
+     */
     public void reloadProject() throws IOException {
         this.project = new ProjectJsonParser().parseProject( projectJson.getAbsolutePath() );
     }
@@ -93,6 +117,11 @@ public Dataset getDataset( String datasetName ) {
         }
     }
 
+    /**
+     * Get names of uiSelectionGroups in this dataset
+     * @param datasetName dataset name
+     * @return names of uiSelectionGroups
+     */
     public String[] getGroups( String datasetName ) {
         if ( !datasetName.equals(currentDatasetName) ) {
             getDataset( datasetName );
@@ -106,6 +135,12 @@ public String[] getGroups( String datasetName ) {
         return groups;
     }
 
+    /**
+     * Get names of views in this dataset and uiSelectionGroup
+     * @param datasetName dataset name
+     * @param uiSelectionGroup uiSelectionGroup
+     * @return names of views
+     */
     public String[] getViews( String datasetName, String uiSelectionGroup ) {
         if ( !datasetName.equals(currentDatasetName) ) {
             getDataset( datasetName );
@@ -119,6 +154,18 @@ public String[] getViews( String datasetName, String uiSelectionGroup ) {
         return views;
     }
 
+    public String getVoxelUnit() {
+        return voxelUnit;
+    }
+
+    public void setVoxelUnit(String voxelUnit) {
+        this.voxelUnit = voxelUnit;
+    }
+
+    /**
+     * Reload current dataset by parsing the dataset.json again
+     * @throws IOException
+     */
     public void reloadCurrentDataset() throws IOException {
         if ( currentDatasetName != null ) {
             this.currentDataset = new DatasetJsonParser().parseDataset(currentDatasetJson.getAbsolutePath());
@@ -138,7 +185,37 @@ public DatasetJsonCreator getDatasetJsonCreator() {
         return datasetJsonCreator;
     }
 
+    public ProjectJsonCreator getProjectJsonCreator() { return projectJsonCreator; }
+
     public RemoteMetadataCreator getRemoteMetadataCreator() {
         return remoteMetadataCreator;
     }
+
+    private void readVoxelUnitFromImage() throws SpimDataException {
+        // open first image (if any exist) to determine voxel unit
+        if ( project.getDatasets().size() == 0 ) {
+            return;
+        }
+
+        for ( String datasetName: project.getDatasets() ) {
+            Dataset dataset = getDataset( datasetName );
+            if ( dataset != null && dataset.sources.size() > 0 ) {
+                for ( SourceSupplier sourceSupplier: dataset.sources.values() ) {
+                    ImageSource imageSource = sourceSupplier.get();
+                    // open one of the local images
+                    for (ImageDataFormat format : imageSource.imageData.keySet()) {
+                        if (!format.isRemote()) {
+                            String imagePath = FileAndUrlUtils.combinePath( dataLocation.getAbsolutePath(), datasetName,
+                                    imageSource.imageData.get(format).relativePath);
+                            SpimData spimData = (SpimData) new SpimDataOpener().openSpimData( imagePath, format );
+                            VoxelDimensions voxelDimensions = spimData.getSequenceDescription().
+                                    getViewSetupsOrdered().get(0).getVoxelSize();
+                            voxelUnit = voxelDimensions.unit();
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+    }
 }
diff --git a/src/main/java/org/embl/mobie/viewer/projectcreator/ProjectCreatorHelper.java b/src/main/java/org/embl/mobie/viewer/projectcreator/ProjectCreatorHelper.java
index 5f6116e52..44c0b8b64 100644
--- a/src/main/java/org/embl/mobie/viewer/projectcreator/ProjectCreatorHelper.java
+++ b/src/main/java/org/embl/mobie/viewer/projectcreator/ProjectCreatorHelper.java
@@ -1,16 +1,14 @@
 package org.embl.mobie.viewer.projectcreator;
 
 import bdv.img.n5.N5ImageLoader;
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.io.n5.loaders.N5FSImageLoader;
-import org.embl.mobie.io.n5.loaders.N5S3ImageLoader;
 import org.embl.mobie.io.ome.zarr.loaders.N5OMEZarrImageLoader;
-import org.embl.mobie.io.ome.zarr.loaders.N5S3OMEZarrImageLoader;
 import org.embl.mobie.io.ome.zarr.readers.N5OmeZarrReader;
 import org.embl.mobie.viewer.Dataset;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import org.embl.mobie.viewer.view.View;
 import org.embl.mobie.viewer.view.additionalviews.AdditionalViews;
-import de.embl.cba.tables.FileAndUrlUtils;
+import org.embl.mobie.io.util.FileAndUrlUtils;
 import ij.IJ;
 import ij.ImagePlus;
 import ij.gui.GenericDialog;
@@ -18,50 +16,29 @@
 import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription;
 import mpicbg.spim.data.generic.sequence.BasicImgLoader;
 import mpicbg.spim.data.generic.sequence.BasicViewSetup;
-import mpicbg.spim.data.sequence.FinalVoxelDimensions;
-import net.imglib2.FinalDimensions;
 import net.imglib2.realtransform.AffineTransform3D;
+import ucar.units.*;
 
 import java.io.File;
+import java.text.DecimalFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 
-import static org.embl.mobie.viewer.ui.UserInterfaceHelper.tidyString;
+import static org.embl.mobie.viewer.ui.UserInterfaceHelpers.tidyString;
 
 public class ProjectCreatorHelper {
 
     static { net.imagej.patcher.LegacyInjector.preinit(); }
 
-    public static boolean isImageSuitable(ImagePlus imp) {
-        // check the image type
-        switch (imp.getType()) {
-            case ImagePlus.GRAY8:
-            case ImagePlus.GRAY16:
-            case ImagePlus.GRAY32:
-                break;
-            default:
-                IJ.showMessage("Only 8, 16, 32-bit images are supported currently!");
-                return false;
-        }
-
-        // check the image dimensionality
-        if (imp.getNDimensions() < 2) {
-            IJ.showMessage("Image must be at least 2-dimensional!");
-            return false;
-        }
-
-        return true;
-    }
-
     public static boolean isValidAffine(String affine) {
-        if (!affine.matches("^[0-9. ]+$")) {
-            IJ.log("Invalid affine transform - must contain only numbers and spaces");
+        if (!affine.matches("^[0-9., ]+$")) {
+            IJ.log("Invalid affine transform - must contain only numbers, commas and spaces");
             return false;
         }
 
-        String[] splitAffine = affine.split(" ");
+        String[] splitAffine = affine.split(",");
         if (splitAffine.length != 12) {
             IJ.log("Invalid affine transform - must be of length 12");
             return false;
@@ -73,7 +50,9 @@ public static boolean isValidAffine(String affine) {
     public static AffineTransform3D parseAffineString(String affine) {
         if (isValidAffine(affine)) {
             AffineTransform3D sourceTransform = new AffineTransform3D();
-            String[] splitAffineTransform = affine.split(" ");
+            // remove spaces
+            affine = affine.replaceAll("\\s","");
+            String[] splitAffineTransform = affine.split(",");
             double[] doubleAffineTransform = new double[splitAffineTransform.length];
             for (int i = 0; i < splitAffineTransform.length; i++) {
                 doubleAffineTransform[i] = Double.parseDouble(splitAffineTransform[i]);
@@ -85,79 +64,18 @@ public static AffineTransform3D parseAffineString(String affine) {
         }
     }
 
-    public static String generateDefaultAffine(ImagePlus imp) {
+    public static AffineTransform3D generateDefaultAffine(ImagePlus imp) {
         final double pixelWidth = imp.getCalibration().pixelWidth;
         final double pixelHeight = imp.getCalibration().pixelHeight;
         final double pixelDepth = imp.getCalibration().pixelDepth;
 
-        String defaultAffine = pixelWidth + " 0.0 0.0 0.0 0.0 " + pixelHeight + " 0.0 0.0 0.0 0.0 " + pixelDepth + " 0.0";
-        return defaultAffine;
-    }
-
-    public static FinalVoxelDimensions getVoxelSize(ImagePlus imp) {
-        final double pw = imp.getCalibration().pixelWidth;
-        final double ph = imp.getCalibration().pixelHeight;
-        final double pd = imp.getCalibration().pixelDepth;
-        String punit = imp.getCalibration().getUnit();
-        if (punit == null || punit.isEmpty())
-            punit = "px";
-        final FinalVoxelDimensions voxelSize = new FinalVoxelDimensions(punit, pw, ph, pd);
-        return voxelSize;
-    }
-
-    public static FinalDimensions getSize(ImagePlus imp) {
-        final int w = imp.getWidth();
-        final int h = imp.getHeight();
-        final int d = imp.getNSlices();
-        final FinalDimensions size = new FinalDimensions(w, h, d);
-        return size;
-    }
-
-    public static File getSeqFileFromPath(String seqFilename) {
-        final File seqFile = new File(seqFilename);
-        final File parent = seqFile.getParentFile();
-        if (parent == null || !parent.exists() || !parent.isDirectory()) {
-            IJ.showMessage("Invalid export filename " + seqFilename);
-            return null;
-        }
-        return seqFile;
-    }
-
-    public static AffineTransform3D generateSourceTransform(FinalVoxelDimensions voxelSize) {
-        // create SourceTransform from the images calibration
-        final AffineTransform3D sourceTransform = new AffineTransform3D();
-        sourceTransform.set(voxelSize.dimension(0), 0, 0, 0, 0, voxelSize.dimension(1),
-                0, 0, 0, 0, voxelSize.dimension(2), 0);
-        return sourceTransform;
-    }
-
-    public static File getN5FileFromXmlPath(String xmlPath) {
-        final String n5Filename = xmlPath.substring(0, xmlPath.length() - 4) + ".n5";
-        return new File(n5Filename);
-    }
-
-    public static File getOmeZarrFileFromXmlPath(String xmlPath) {
-        final String omeZarrFileName = xmlPath.substring(0, xmlPath.length() - 4) + ".ome.zarr";
-        return new File(omeZarrFileName);
-    }
-
-    public static ImageDataFormat getImageFormatFromSpimDataMinimal( SpimData spimData ) {
-        ImageDataFormat imageFormat = null;
-        BasicImgLoader imgLoader = spimData.getSequenceDescription().getImgLoader();
-        if ( imgLoader instanceof N5FSImageLoader | imgLoader instanceof N5ImageLoader ) {
-            imageFormat = ImageDataFormat.BdvN5;
-        } else if ( imgLoader instanceof N5S3ImageLoader ) {
-            imageFormat = ImageDataFormat.BdvN5S3;
-        } else if ( imgLoader instanceof N5OMEZarrImageLoader ) {
-            imageFormat = ImageDataFormat.OmeZarr;
-        } else if ( imgLoader instanceof N5S3OMEZarrImageLoader ) {
-            imageFormat = ImageDataFormat.OmeZarrS3;
-        }
+        AffineTransform3D defaultAffine = new AffineTransform3D();
+        defaultAffine.scale( pixelWidth, pixelHeight, pixelDepth );
 
-        return imageFormat;
+        return defaultAffine;
     }
 
-    public static File getImageLocationFromSequenceDescription(AbstractSequenceDescription seq, ImageDataFormat imageFormat ) {
+    public static File getImageLocationFromSequenceDescription( AbstractSequenceDescription seq, ImageDataFormat imageFormat ) {
         File imageLocation = null;
 
         // get image loader to find absolute image location
@@ -264,7 +182,7 @@ public static String makeNewUiSelectionGroup( String[] currentUiSelectionGroups
     }
 
     public static String chooseNewSelectionGroupNameDialog() {
-        final GenericDialog gd = new GenericDialog("Choose ui selection group Name:");
+        final GenericDialog gd = new GenericDialog("Choose ui selection group name:");
 
         gd.addStringField("New ui selection group name:", "", 25 );
         gd.showDialog();
@@ -279,4 +197,62 @@ public static String chooseNewSelectionGroupNameDialog() {
     public static String imageFormatToFolderName( ImageDataFormat imageFormat ) {
         return imageFormat.toString().replaceAll("\\.", "-");
     }
+
+    public static String getVoxelSizeString( ImagePlus imp ) {
+        DecimalFormat df = new DecimalFormat("#.###");
+        String voxelString =  "Voxel size: " + df.format( imp.getCalibration().pixelWidth ) + ", " +
+                df.format( imp.getCalibration().pixelHeight ) + ", " + df.format( imp.getCalibration().pixelDepth ) +
+                " " + imp.getCalibration().getUnit();
+
+        return voxelString;
+    }
+
+    private static String replaceMu( String string ) {
+        // Convert the mu symbol into "u" - as udunits can't handle this
+        return  string.replace("\u00B5", "u");
+    }
+
+    public static boolean unitsEqual( String unit1, String unit2 ) throws PrefixDBException, UnitSystemException, SpecificationException, UnitDBException {
+        // check with udunits that units are equivalent
+        UnitFormat unitFormatter = UnitFormatManager.instance();
+        Unit parsedUnit1 = unitFormatter.parse(replaceMu(unit1));
+        Unit parsedUnit2 = unitFormatter.parse(replaceMu(unit2));
+
+        return parsedUnit1.getCanonicalString().equals(parsedUnit2.getCanonicalString());
+    }
+
+    public static boolean isImageValid( int nChannels, String imageUnit, String projectUnit, boolean bdvFormat ) {
+        // reject multi-channel images
+        if ( nChannels > 1 ) {
+            // for bdv format images, don't print the fiji shortcuts
+            String channelMessage = "Multi-channel images are not supported. \n Please split the channels";
+            if ( !bdvFormat ) {
+                IJ.log( channelMessage + " [ Image > Color > Split Channels], and add each separately.");
+            } else {
+                IJ.log(channelMessage + ", and add each separately.");
+            }
+            return false;
+        }
+
+        // reject images with a different unit to the rest of the project
+        try {
+            if ( projectUnit != null && !unitsEqual( imageUnit, projectUnit) ) {
+                String unitMessage = "Image has a different unit (" + imageUnit + ") to the rest of the project (" +
+                        projectUnit + "). \n Please set your image unit";
+                if (! bdvFormat ) {
+                    IJ.log( unitMessage + " under [ Image > Properties... ] to match the project.");
+                } else {
+                    IJ.log( unitMessage + " to match the project." );
+                }
+                return false;
+            }
+        } catch (PrefixDBException | UnitSystemException | SpecificationException | UnitDBException e) {
+            IJ.log("Couldn't parse unit.");
+            e.printStackTrace();
+            return false;
+        }
+
+        return true;
+    }
+
 }
diff --git a/src/main/java/org/embl/mobie/viewer/projectcreator/ProjectJsonCreator.java b/src/main/java/org/embl/mobie/viewer/projectcreator/ProjectJsonCreator.java
new file mode 100644
index 000000000..f373c8d8e
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/projectcreator/ProjectJsonCreator.java
@@ -0,0 +1,80 @@
+package org.embl.mobie.viewer.projectcreator;
+
+import org.embl.mobie.io.ImageDataFormat;
+import org.embl.mobie.viewer.Project;
+import org.embl.mobie.viewer.serialize.ProjectJsonParser;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ProjectJsonCreator {
+    ProjectCreator projectCreator;
+
+    public ProjectJsonCreator( ProjectCreator projectCreator ) {
+        this.projectCreator = projectCreator;
+    }
+
+    public void addImageDataFormat( ImageDataFormat imageDataFormat ) {
+        Project project = projectCreator.getProject();
+        if ( project.getImageDataFormats() == null ) {
+            project.setImageDataFormats( new ArrayList<>() );
+        }
+
+        List< ImageDataFormat > imageDataFormats = project.getImageDataFormats();
+        if ( !imageDataFormats.contains( imageDataFormat ) ) {
+            imageDataFormats.add( imageDataFormat );
+            writeProjectJson();
+        }
+    }
+
+    public void removeImageDataFormat( ImageDataFormat imageDataFormat ) {
+        List<ImageDataFormat> imageDataFormats = projectCreator.getProject().getImageDataFormats();
+        if ( imageDataFormats.contains( imageDataFormat ) ) {
+            imageDataFormats.remove( imageDataFormat );
+            writeProjectJson();
+        }
+    }
+
+    public void addDataset( String datasetName ) {
+        List<String> currentDatasets = projectCreator.getProject().getDatasets();
+
+        // if this is the first dataset, then make this the default
+        if ( currentDatasets.size() == 0 ) {
+            projectCreator.getProject().setDefaultDataset( datasetName );
+        }
+        currentDatasets.add( datasetName );
+        writeProjectJson();
+    }
+
+    public void renameDataset( String oldName, String newName ) {
+        if ( projectCreator.getProject().getDefaultDataset().equals(oldName) ) {
+            projectCreator.getProject().setDefaultDataset( newName );
+        }
+
+        int indexOld = projectCreator.getProject().getDatasets().indexOf(oldName);
+        projectCreator.getProject().getDatasets().set(indexOld, newName);
+
+        writeProjectJson();
+    }
+
+    public void setDefaultDataset( String datasetName ) {
+        projectCreator.getProject().setDefaultDataset( datasetName );
+        writeProjectJson();
+    }
+
+    public void writeProjectJson() {
+        try {
+            new ProjectJsonParser().saveProject( projectCreator.getProject(), projectCreator.getProjectJson().getAbsolutePath() );
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        // whether the project writing succeeded or not, we now read the current state of the project
+        try {
+            projectCreator.reloadProject();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/src/main/java/org/embl/mobie/viewer/projectcreator/RemoteMetadataCreator.java b/src/main/java/org/embl/mobie/viewer/projectcreator/RemoteMetadataCreator.java
index b509be839..e05f4b09e 100644
--- a/src/main/java/org/embl/mobie/viewer/projectcreator/RemoteMetadataCreator.java
+++ b/src/main/java/org/embl/mobie/viewer/projectcreator/RemoteMetadataCreator.java
@@ -1,13 +1,14 @@
 package org.embl.mobie.viewer.projectcreator;
 
+import mpicbg.spim.data.generic.AbstractSpimData;
+import org.embl.mobie.io.ImageDataFormat;
+import org.embl.mobie.io.SpimDataOpener;
 import org.embl.mobie.io.n5.loaders.xml.XmlIoN5S3ImageLoader;
 import org.embl.mobie.io.ome.zarr.loaders.xml.XmlN5S3OmeZarrImageLoader;
 import org.embl.mobie.viewer.Dataset;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import org.embl.mobie.viewer.source.ImageSource;
-import org.embl.mobie.viewer.source.SpimDataOpener;
 import org.embl.mobie.viewer.source.StorageLocation;
-import de.embl.cba.tables.FileAndUrlUtils;
+import org.embl.mobie.io.util.FileAndUrlUtils;
 import ij.IJ;
 import mpicbg.spim.data.SpimData;
 import mpicbg.spim.data.SpimDataException;
@@ -36,6 +37,10 @@ public class RemoteMetadataCreator {
     ImageDataFormat remoteImageDataFormat;
     ImageDataFormat localImageDataFormat;
 
+    /**
+     * Make a remoteMetadataCreator - includes all functions for creation/modification of remote metadata
+     * @param projectCreator projectCreator
+     */
     public RemoteMetadataCreator( ProjectCreator projectCreator ) {
         this.projectCreator = projectCreator;
     }
@@ -50,6 +55,8 @@ private void deleteAllRemoteMetadata() throws IOException {
                 projectCreator.getDatasetJsonCreator().writeDatasetJson( datasetName, dataset );
             }
         }
+
+        projectCreator.getProjectJsonCreator().removeImageDataFormat( remoteImageDataFormat );
     }
 
     private void deleteRemoteMetadataForImage( String datasetName, String imageName ) throws IOException {
@@ -72,7 +79,7 @@ private void deleteRemoteMetadataForImage( String datasetName, String imageName
         }
     }
 
-    public String getRelativeKey( SpimData spimData, String datasetName, String imageName,
+    private String getRelativeKey( SpimData spimData, String datasetName, String imageName,
                                   ImageDataFormat imageFormat ) throws IOException {
         // check image is within the project folder (if people 'link' to bdv format images they may be outside)
         Path imagePath = Paths.get( ProjectCreatorHelper.getImageLocationFromSequenceDescription( spimData.getSequenceDescription(),
@@ -91,7 +98,7 @@ public String getRelativeKey( SpimData spimData, String datasetName, String imag
         return FilenameUtils.separatorsToUnix( relativeKey.toString() );
     }
 
-    public Element createImageLoaderXmlElement ( SpimData spimData,
+    private Element createImageLoaderXmlElement ( SpimData spimData,
                                                  ImageDataFormat imageFormat,
                                                  String datasetName, String imageName ) throws IOException {
         Element element = null;
@@ -108,8 +115,7 @@ public Element createImageLoaderXmlElement ( SpimData spimData,
         return element;
     }
 
-    public void saveXml( final SpimData spimData, String datasetName, String imagename,
-                         final String xmlFile, ImageDataFormat imageFormat ) throws SpimDataException, IOException {
+    private void saveXml( final SpimData spimData, String datasetName, String imagename, final String xmlFile, ImageDataFormat imageFormat ) throws SpimDataException, IOException {
         XmlIoSpimData io = new XmlIoSpimData();
         final File xmlFileDirectory = new File( xmlFile ).getParentFile();
         final Document doc = new Document( io.toXml( spimData, xmlFileDirectory ) );
@@ -143,7 +149,7 @@ private void addRemoteMetadataForImage( String datasetName, String imageName ) t
                     datasetName, imageSource.imageData.get(localImageDataFormat).relativePath);
 
             String remoteXmlLocation = FileAndUrlUtils.combinePath(projectCreator.getDataLocation().getAbsolutePath(),
-                    datasetName, "images", ProjectCreatorHelper.imageFormatToFolderName(remoteImageDataFormat));
+                    datasetName, "images", ProjectCreatorHelper.imageFormatToFolderName( remoteImageDataFormat ));
 
             // make directory for that image file format, if doesn't exist already
             File remoteDir = new File( remoteXmlLocation );
@@ -151,9 +157,9 @@ private void addRemoteMetadataForImage( String datasetName, String imageName ) t
                 remoteDir.mkdirs();
             }
 
-            SpimData spimData = new SpimDataOpener().openSpimData(localXmlLocation, localImageDataFormat);
+            AbstractSpimData spimData = new SpimDataOpener().openSpimData(localXmlLocation, localImageDataFormat);
             spimData.setBasePath(new File(remoteXmlLocation));
-            saveXml(spimData, datasetName, imageName,
+            saveXml( ( SpimData ) spimData, datasetName, imageName,
                     new File(remoteXmlLocation, imageName + ".xml").getAbsolutePath(),
                     remoteImageDataFormat);
 
@@ -164,7 +170,8 @@ private void addRemoteMetadataForImage( String datasetName, String imageName ) t
             // give absolute s3 path to ome.zarr file
             StorageLocation storageLocation = new StorageLocation();
             String relativePath = imageSource.imageData.get(localImageDataFormat).relativePath;
-            storageLocation.s3Address = serviceEndpoint + bucketName + "/data/" + datasetName + "/" + relativePath;
+            storageLocation.s3Address = serviceEndpoint + bucketName + "/" + datasetName + "/" + relativePath;
+            storageLocation.signingRegion = signingRegion;
             imageSource.imageData.put( remoteImageDataFormat, storageLocation );
         }
 
@@ -189,10 +196,20 @@ private void addAllRemoteMetadata() throws SpimDataException, IOException {
                 addRemoteMetadataForDataset( datasetName );
             }
         }
+
+        IJ.log( "Adding metadata to project json." );
+        projectCreator.getProjectJsonCreator().addImageDataFormat( remoteImageDataFormat );
+        IJ.log( "Done." );
     }
 
-    public void createRemoteMetadata( String signingRegion, String serviceEndpoint, String bucketName,
-                                      ImageDataFormat imageDataFormat ) {
+    /**
+     * Add remote metadata. Note this will overwrite any existing remote metadata for the given image format.
+     * @param signingRegion signing region e.g. us-west-2
+     * @param serviceEndpoint service endpoint e.g. https://s3.embl.de
+     * @param bucketName bucket name
+     * @param imageDataFormat image format
+     */
+    public void createRemoteMetadata( String signingRegion, String serviceEndpoint, String bucketName, ImageDataFormat imageDataFormat ) {
 
         if ( !imageDataFormat.isRemote() ) {
             IJ.log( "Creating remote metadata aborted - provided image data format is not remote." );
@@ -203,6 +220,10 @@ public void createRemoteMetadata( String signingRegion, String serviceEndpoint,
         this.bucketName = bucketName;
         this.remoteImageDataFormat = imageDataFormat;
 
+        if ( this.signingRegion.equals("") ) {
+            this.signingRegion = null;
+        }
+
         if ( !this.serviceEndpoint.endsWith("/") ) {
             this.serviceEndpoint = this.serviceEndpoint + "/";
         }
diff --git a/src/main/java/org/embl/mobie/viewer/projectcreator/ui/ManualExportPanel.java b/src/main/java/org/embl/mobie/viewer/projectcreator/ui/ManualExportPanel.java
index 8dbdb229a..4c7475fab 100644
--- a/src/main/java/org/embl/mobie/viewer/projectcreator/ui/ManualExportPanel.java
+++ b/src/main/java/org/embl/mobie/viewer/projectcreator/ui/ManualExportPanel.java
@@ -1,17 +1,11 @@
 package org.embl.mobie.viewer.projectcreator.ui;
 
 import bdv.ij.util.PluginHelper;
-import org.embl.mobie.io.n5.util.DownsampleBlock;
-import org.embl.mobie.io.n5.writers.WriteImgPlusToN5;
-import org.embl.mobie.io.ome.zarr.writers.imgplus.WriteImgPlusToN5BdvOmeZarr;
-import org.embl.mobie.io.ome.zarr.writers.imgplus.WriteImgPlusToN5OmeZarr;
-import org.embl.mobie.viewer.source.ImageDataFormat;
+import org.embl.mobie.io.ImageDataFormat;
 
 import fiji.util.gui.GenericDialogPlus;
 import ij.IJ;
-import ij.ImagePlus;
 import ij.gui.GenericDialog;
-import net.imglib2.realtransform.AffineTransform3D;
 import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
 import org.janelia.saalfeldlab.n5.*;
 
@@ -22,30 +16,16 @@ public class ManualExportPanel {
 
     static { net.imagej.patcher.LegacyInjector.preinit(); }
 
-    ImagePlus imp;
-    String filePath;
-    AffineTransform3D sourceTransform;
-    DownsampleBlock.DownsamplingMethod downsamplingMethod;
-    String imageName;
-    ImageDataFormat imageDataFormat;
+    private int[][] subdivisions;
+    private int[][] resolutions;
+    private Compression compression;
 
     static String lastSubsampling = "{ {1,1,1} }";
     static String lastChunkSizes = "{ {64,64,64} }";
     static int lastCompressionChoice = 0;
     static boolean lastCompressionDefaultSettings = true;
 
-    public ManualExportPanel(ImagePlus imp, String filePath, AffineTransform3D sourceTransform,
-                             DownsampleBlock.DownsamplingMethod downsamplingMethod, String imageName,
-                             ImageDataFormat imageDataFormat ) {
-        this.imp = imp;
-        this.filePath = filePath;
-        this.sourceTransform = sourceTransform;
-        this.downsamplingMethod = downsamplingMethod;
-        this.imageName = imageName;
-        this.imageDataFormat = imageDataFormat;
-    }
-
-    public void getManualExportParameters() {
+    public ManualExportPanel( ImageDataFormat imageDataFormat ) {
 
         final GenericDialog manualSettings = new GenericDialog( "Manual Settings for " +
                 imageDataFormat.toString() );
@@ -69,6 +49,9 @@ public void getManualExportParameters() {
         if ( !manualSettings.wasCanceled() ) {
             lastSubsampling = manualSettings.getNextString();
             lastChunkSizes = manualSettings.getNextString();
+
+            subdivisions = PluginHelper.parseResolutionsString( lastChunkSizes );
+            resolutions = PluginHelper.parseResolutionsString( lastSubsampling );
             if ( imageDataFormat == ImageDataFormat.BdvN5 ) {
                 lastCompressionChoice = manualSettings.getNextChoiceIndex();
                 lastCompressionDefaultSettings = manualSettings.getNextBoolean();
@@ -76,18 +59,27 @@ public void getManualExportParameters() {
                 lastCompressionChoice = 2;
                 lastCompressionDefaultSettings = true;
             }
-
-            parseInputAndWriteImage();
+            parseCompression();
+        } else {
+            subdivisions = null;
+            resolutions = null;
+            compression = null;
         }
+    }
 
+    public int[][] getSubdivisions() {
+        return subdivisions;
     }
 
-    private void parseInputAndWriteImage() {
-        // parse mipmap resolutions and cell sizes
-        final int[][] resolutions = PluginHelper.parseResolutionsString( lastSubsampling );
-        final int[][] subdivisions = PluginHelper.parseResolutionsString( lastChunkSizes );
+    public int[][] getResolutions() {
+        return resolutions;
+    }
+
+    public Compression getCompression() {
+        return compression;
+    }
 
-        final Compression compression;
+    private void parseCompression(){
         switch ( lastCompressionChoice )
         {
             default:
@@ -115,32 +107,6 @@ private void parseInputAndWriteImage() {
                         : getXzSettings();
                 break;
         }
-        if ( compression == null )
-            return;
-
-        writeImage( resolutions, subdivisions, compression );
-    }
-
-    private void writeImage( int[][] resolutions, int[][] subdivisions, Compression compression ) {
-        switch( imageDataFormat ) {
-            case BdvN5:
-                new WriteImgPlusToN5().export(imp, resolutions, subdivisions, filePath, sourceTransform,
-                        downsamplingMethod, compression, new String[]{imageName});
-                break;
-
-            case BdvOmeZarr:
-                new WriteImgPlusToN5BdvOmeZarr().export(imp, resolutions, subdivisions, filePath, sourceTransform,
-                        downsamplingMethod, compression, new String[]{imageName});
-                break;
-
-            case OmeZarr:
-                new WriteImgPlusToN5OmeZarr().export(imp, resolutions, subdivisions, filePath, sourceTransform,
-                        downsamplingMethod, compression, new String[]{imageName});
-                break;
-
-            default:
-                throw new UnsupportedOperationException();
-        }
     }
 
     static int lastBzip2BlockSize = BZip2CompressorOutputStream.MAX_BLOCKSIZE;
diff --git a/src/main/java/org/embl/mobie/viewer/projectcreator/ui/ProjectsCreatorPanel.java b/src/main/java/org/embl/mobie/viewer/projectcreator/ui/ProjectsCreatorPanel.java
index c74b28288..f5fffe79b 100644
--- a/src/main/java/org/embl/mobie/viewer/projectcreator/ui/ProjectsCreatorPanel.java
+++ b/src/main/java/org/embl/mobie/viewer/projectcreator/ui/ProjectsCreatorPanel.java
@@ -1,13 +1,17 @@
 package org.embl.mobie.viewer.projectcreator.ui;
 
+import mpicbg.spim.data.SpimData;
+import org.apache.commons.io.FilenameUtils;
+import org.embl.mobie.io.ImageDataFormat;
+import org.embl.mobie.io.SpimDataOpener;
 import org.embl.mobie.viewer.Dataset;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.Project;
-import org.embl.mobie.viewer.Utils;
+import org.embl.mobie.viewer.MoBIEUtils;
 import org.embl.mobie.viewer.command.OpenMoBIEProjectCommand;
+import org.embl.mobie.viewer.projectcreator.ImagesCreator;
 import org.embl.mobie.viewer.projectcreator.ProjectCreator;
-import org.embl.mobie.viewer.source.ImageDataFormat;
-import de.embl.cba.tables.FileAndUrlUtils;
+import org.embl.mobie.io.util.FileAndUrlUtils;
 import de.embl.cba.tables.SwingUtils;
 import ij.IJ;
 import ij.ImagePlus;
@@ -15,8 +19,9 @@
 import mpicbg.spim.data.SpimDataException;
 import net.imglib2.realtransform.AffineTransform3D;
 import org.embl.mobie.viewer.projectcreator.ProjectCreatorHelper;
-import org.embl.mobie.viewer.ui.SwingHelper;
-import org.embl.mobie.viewer.ui.UserInterfaceHelper;
+import org.embl.mobie.viewer.ui.SwingHelpers;
+import org.embl.mobie.viewer.ui.UserInterfaceHelpers;
+import org.janelia.saalfeldlab.n5.Compression;
 
 import javax.swing.*;
 import java.awt.*;
@@ -27,6 +32,8 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import static org.embl.mobie.viewer.projectcreator.ProjectCreatorHelper.*;
+
 public class ProjectsCreatorPanel extends JFrame {
 
     static { net.imagej.patcher.LegacyInjector.preinit(); }
@@ -37,6 +44,20 @@ public class ProjectsCreatorPanel extends JFrame {
     private JComboBox<String> groupsComboBox;
     private JComboBox<String> viewsComboBox;
 
+    private static ProjectCreator.ImageType imageType = ProjectCreator.ImageType.image;
+    private static ImageDataFormat imageDataFormat = ImageDataFormat.BdvN5;
+    private static ProjectCreator.AddMethod addMethod = ProjectCreator.AddMethod.link;
+    private static boolean useDefaultExportSettings = true;
+    private static boolean exclusive = false;
+    private static boolean useFileNameAsImageName = true;
+    private static String uiSelectionGroup = "Make New Ui Selection Group";
+    private static boolean is2D = false;
+
+    private final String[] imageFormats = new String[]{ ImageDataFormat.BdvN5.toString(),
+            ImageDataFormat.OmeZarr.toString() };
+    private final String[] imageTypes = new String[]{ ProjectCreator.ImageType.image.toString(), ProjectCreator.ImageType.segmentation.toString() };
+
+
     public ProjectsCreatorPanel ( File projectLocation ) throws IOException {
 
         // account for projects with and without the top 'data' directory
@@ -72,8 +93,8 @@ public ProjectCreator getProjectsCreator() {
     private void addDatasetPanel() {
         final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
 
-        final JButton addButton = SwingHelper.createButton("Add");
-        final JButton editButton = SwingHelper.createButton("Edit");
+        final JButton addButton = SwingHelpers.createButton("Add");
+        final JButton editButton = SwingHelpers.createButton("Edit");
 
         createDatasetComboBox();
         addButton.addActionListener( e ->
@@ -86,7 +107,7 @@ private void addDatasetPanel() {
             new Thread( () -> { editDatasetDialog(); } ).start();
         } );
 
-        horizontalLayoutPanel.add(SwingHelper.getJLabel("dataset", 60, 10));
+        horizontalLayoutPanel.add( SwingHelpers.getJLabel("dataset", 60, 10));
         horizontalLayoutPanel.add(datasetComboBox);
         horizontalLayoutPanel.add(addButton);
         horizontalLayoutPanel.add(editButton);
@@ -200,7 +221,7 @@ public void itemStateChanged(ItemEvent event) {
     private void addSourcesPanel() {
         final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
 
-        final JButton addButton = SwingHelper.createButton( "Add" );
+        final JButton addButton = SwingHelpers.createButton( "Add" );
         // for now we don't support editing any image properties, but this is likely to change in future,
         // so keep this code for now
         // final JButton editButton = createButton("Edit");
@@ -216,10 +237,10 @@ private void addSourcesPanel() {
         //     new Thread( () -> { editImageDialog(); } ).start();
         // } );
 
-        horizontalLayoutPanel.add(SwingHelper.getJLabel("source", 60, 10));
+        horizontalLayoutPanel.add( SwingHelpers.getJLabel("source", 60, 10));
         horizontalLayoutPanel.add(sourcesComboBox);
         horizontalLayoutPanel.add( addButton );
-        horizontalLayoutPanel.add( Box.createHorizontalStrut( SwingHelper.BUTTON_DIMENSION.width ) );
+        horizontalLayoutPanel.add( Box.createHorizontalStrut( SwingHelpers.BUTTON_DIMENSION.width ) );
         // horizontalLayoutPanel.add( editButton );
         horizontalLayoutPanel.setAlignmentX( Component.LEFT_ALIGNMENT );
 
@@ -233,14 +254,14 @@ private void addViewsPanel() {
         createGroupsCombobox();
         createViewsCombobox();
 
-        groupPanel.add(SwingHelper.getJLabel("group", 60, 10));
-        viewsPanel.add(SwingHelper.getJLabel("view", 60, 10));
+        groupPanel.add( SwingHelpers.getJLabel("group", 60, 10));
+        viewsPanel.add( SwingHelpers.getJLabel("view", 60, 10));
         groupPanel.add( groupsComboBox );
         viewsPanel.add( viewsComboBox );
-        groupPanel.add( Box.createHorizontalStrut( SwingHelper.BUTTON_DIMENSION.width ) );
-        groupPanel.add( Box.createHorizontalStrut( SwingHelper.BUTTON_DIMENSION.width ) );
-        viewsPanel.add( Box.createHorizontalStrut( SwingHelper.BUTTON_DIMENSION.width ) );
-        viewsPanel.add( Box.createHorizontalStrut( SwingHelper.BUTTON_DIMENSION.width ) );
+        groupPanel.add( Box.createHorizontalStrut( SwingHelpers.BUTTON_DIMENSION.width ) );
+        groupPanel.add( Box.createHorizontalStrut( SwingHelpers.BUTTON_DIMENSION.width ) );
+        viewsPanel.add( Box.createHorizontalStrut( SwingHelpers.BUTTON_DIMENSION.width ) );
+        viewsPanel.add( Box.createHorizontalStrut( SwingHelpers.BUTTON_DIMENSION.width ) );
         groupPanel.setAlignmentX( Component.LEFT_ALIGNMENT );
         viewsPanel.setAlignmentX( Component.LEFT_ALIGNMENT );
 
@@ -289,7 +310,7 @@ private boolean continueDialog( ImageDataFormat imageDataFormat ) {
 
     private void remoteMetadataSettingsDialog() {
         List<String> datasets = projectsCreator.getProject().getDatasets();
-        List<ImageDataFormat> imageDataFormats = projectsCreator.getProject().getImageDataFormats();
+        List< ImageDataFormat > imageDataFormats = projectsCreator.getProject().getImageDataFormats();
         if ( datasets == null || datasets.size() == 0 ) {
             IJ.log( "Remote metadata aborted - there are no datasets in your project!" );
             return;
@@ -324,7 +345,7 @@ private void remoteMetadataSettingsDialog() {
             formats[i] = remoteFormats.get(i).toString();
         }
         gd.addChoice("Image format:", formats, formats[0]);
-        gd.addStringField("Signing Region", "us-west-2", 20);
+        gd.addStringField("Signing Region", "", 20);
         gd.addStringField("Service endpoint", "https://...", 20);
         gd.addStringField("Bucket Name", "", 20);
 
@@ -389,7 +410,7 @@ private String selectUiSelectionGroupDialog( String datasetName ) {
             for (int i = 0; i < currentSelectionGroups.length; i++) {
                 choices[i + 1] = currentSelectionGroups[i];
             }
-            gd.addChoice("Ui Selection Group", choices, choices[0]);
+            gd.addChoice("Ui Selection Group", choices, uiSelectionGroup);
             gd.showDialog();
 
             if ( !gd.wasCanceled() ) {
@@ -415,56 +436,135 @@ private void updateComboBoxesForNewImage( String imageName, String uiSelectionGr
         }
     }
 
+    private boolean overwriteImageDialog() {
+        int result = JOptionPane.showConfirmDialog(null,
+                "This image name already exists. Overwrite image?", "Are you sure?",
+                JOptionPane.YES_NO_OPTION,
+                JOptionPane.QUESTION_MESSAGE);
+        if (result == JOptionPane.YES_OPTION) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private boolean changeDatasetDimensionDialog( String datasetName ) {
+        int result = JOptionPane.showConfirmDialog(null,
+                "This image is 3D, but the dataset (" + datasetName + ") is 2D. \n" +
+                        "Change the dataset to be 3D?", "Are you sure?",
+                JOptionPane.YES_NO_OPTION,
+                JOptionPane.QUESTION_MESSAGE);
+        if (result == JOptionPane.YES_OPTION) {
+            projectsCreator.getDatasetsCreator().makeDataset2D( datasetName, false );
+            return true;
+        } else {
+            IJ.log("Adding image aborted - can't add a 3D image to a 2D dataset" );
+            return false;
+        }
+    }
+
+    private String imageNameDialog( File imageFile ) {
+        final GenericDialog gd = new GenericDialog( "Choose image name..." );
+        gd.addStringField( "Image Name", imageFile.getName().split("\\.")[0], 35 );
+        gd.showDialog();
+
+        String imageName = null;
+        if ( !gd.wasCanceled() ) {
+            imageName = gd.getNextString();
+            // tidy up image name, remove any spaces
+            imageName = UserInterfaceHelpers.tidyString( imageName );
+        }
+        return imageName;
+    }
+
     public void addCurrentOpenImageDialog() {
         String datasetName = (String) datasetComboBox.getSelectedItem();
 
         if ( !datasetName.equals("") ) {
             ImagePlus currentImage = IJ.getImage();
-            String defaultAffineTransform = ProjectCreatorHelper.generateDefaultAffine( currentImage );
+
+            if ( !isImageValid( currentImage.getNChannels(), currentImage.getCalibration().getUnit(),
+                    projectsCreator.getVoxelUnit(), false ) ) {
+                return;
+            }
+
+            if ( currentImage.getNDimensions() > 2 && projectsCreator.getDataset( datasetName ).is2D ) {
+                if ( !changeDatasetDimensionDialog(datasetName) ) {
+                    return;
+                }
+            }
 
             final GenericDialog gd = new GenericDialog( "Add Current Image To MoBIE Project..." );
-            gd.addMessage( "Make sure your pixel size, and unit,\n are set properly under Image > Properties...");
-            gd.addStringField( "Image Name", "", 35 );
-            String[] imageTypes = new String[]{ ProjectCreator.ImageType.image.toString(),
-                    ProjectCreator.ImageType.segmentation.toString() };
-            gd.addChoice( "Image Type", imageTypes, imageTypes[0] );
-            // TODO - ImageDataFormat.OmeZarr removed from here for now. Add it back when transforms are supported
-            // so the voxel size can be maintained.
-            String[] imageFormats = new String[]{ ImageDataFormat.BdvN5.toString(),
-                    ImageDataFormat.BdvOmeZarr.toString() };
-            gd.addChoice( "Image format", imageFormats, imageFormats[0] );
-            gd.addStringField("Affine", defaultAffineTransform, 35 );
-            gd.addCheckbox("Use default export settings", true);
-            gd.addCheckbox( "Create view for this image", true );
+            gd.addMessage( "Make sure your voxel size, and unit,\n are set properly under Image > Properties...");
+            gd.addStringField( "Image Name", FilenameUtils.removeExtension(currentImage.getTitle()), 35 );
+            gd.addChoice( "Image Type", imageTypes, imageType.toString() );
+            gd.addChoice( "Image format", imageFormats, imageDataFormat.toString() );
+            gd.addCheckbox("Use default export settings", useDefaultExportSettings );
+            gd.addCheckbox("Make view exclusive", exclusive );
+
+            gd.addMessage( getVoxelSizeString( currentImage ) );
+
+            gd.addMessage("Additional view affine transform:");
+            gd.addStringField("Row 1", "1.0, 0.0, 0.0, 0.0", 25 );
+            gd.addStringField( "Row 2", "0.0, 1.0, 0.0, 0.0", 25 );
+            gd.addStringField( "Row 3", "0.0, 0.0, 1.0, 0.0", 25 );
 
             gd.showDialog();
 
             if ( !gd.wasCanceled() ) {
                 String imageName = gd.getNextString();
-                ProjectCreator.ImageType imageType = ProjectCreator.ImageType.valueOf( gd.getNextChoice() );
-                ImageDataFormat imageFormat = ImageDataFormat.fromString( gd.getNextChoice() );
-                String affineTransform = gd.getNextString().trim();
-                boolean useDefaultSettings = gd.getNextBoolean();
-                boolean createView = gd.getNextBoolean();
+                imageType = ProjectCreator.ImageType.valueOf( gd.getNextChoice() );
+                imageDataFormat = ImageDataFormat.fromString( gd.getNextChoice() );
+                useDefaultExportSettings = gd.getNextBoolean();
+                exclusive = gd.getNextBoolean();
 
-                // tidy up image name, remove any spaces
-                imageName = UserInterfaceHelper.tidyString( imageName );
+                String affineRow1 = gd.getNextString().trim();
+                String affineRow2 = gd.getNextString().trim();
+                String affineRow3 = gd.getNextString().trim();
+                String affineTransform = String.join(",", affineRow1, affineRow2, affineRow3 );
 
+                // tidy up image name, remove any spaces
+                imageName = UserInterfaceHelpers.tidyString( imageName );
                 AffineTransform3D sourceTransform = ProjectCreatorHelper.parseAffineString( affineTransform );
 
                 if ( imageName != null && sourceTransform != null ) {
-                    String uiSelectionGroup = null;
-                    if ( createView ) {
-                        uiSelectionGroup = selectUiSelectionGroupDialog( datasetName );
-                        if ( uiSelectionGroup != null ) {
-                            projectsCreator.getImagesCreator().addImage( currentImage, imageName,
-                                    datasetName, imageFormat, imageType, sourceTransform, useDefaultSettings, uiSelectionGroup );
-                            updateComboBoxesForNewImage( imageName, uiSelectionGroup );
-                        }
+                    ImagesCreator imagesCreator = projectsCreator.getImagesCreator();
+
+                    boolean overwriteImage = true;
+                    if ( imagesCreator.imageExists( datasetName, imageName, imageDataFormat ) ) {
+                        overwriteImage = overwriteImageDialog();
+                    }
+                    if ( !overwriteImage ) {
+                        return;
+                    }
+
+                    String chosenUiSelectionGroup = selectUiSelectionGroupDialog(datasetName);
+                    if ( chosenUiSelectionGroup == null ) {
+                        return;
                     } else {
-                        projectsCreator.getImagesCreator().addImage( currentImage, imageName,
-                                datasetName, imageFormat, imageType, sourceTransform, useDefaultSettings, uiSelectionGroup );
-                        updateComboBoxesForNewImage( imageName, uiSelectionGroup );
+                        uiSelectionGroup = chosenUiSelectionGroup;
+                    }
+
+                    try {
+                        if ( useDefaultExportSettings ) {
+                            imagesCreator.addImage(currentImage, imageName, datasetName, imageDataFormat,
+                                    imageType, sourceTransform, uiSelectionGroup, exclusive);
+                            updateComboBoxesForNewImage(imageName, uiSelectionGroup);
+                        } else {
+                            ManualExportPanel manualExportPanel = new ManualExportPanel( imageDataFormat );
+                            int[][] resolutions = manualExportPanel.getResolutions();
+                            int[][] subdivisions = manualExportPanel.getSubdivisions();
+                            Compression compression = manualExportPanel.getCompression();
+
+                            if ( resolutions != null && subdivisions != null && compression != null ) {
+                                imagesCreator.addImage( currentImage, imageName, datasetName, imageDataFormat, imageType,
+                                        sourceTransform, uiSelectionGroup, exclusive, resolutions, subdivisions,
+                                        compression );
+                                updateComboBoxesForNewImage( imageName, uiSelectionGroup );
+                            }
+                        }
+                    } catch (SpimDataException | IOException e) {
+                        e.printStackTrace();
                     }
                 }
             }
@@ -477,102 +577,149 @@ public void addCurrentOpenImageDialog() {
     public void addBdvFormatImageDialog() {
         String datasetName = (String) datasetComboBox.getSelectedItem();
 
-        if (!datasetName.equals("")) {
-            final GenericDialog gd = new GenericDialog("Add Bdv Format Image To Project...");
+        if (!datasetName.equals(""))
+        {
+            final GenericDialog gd = new GenericDialog( "Add Bdv Format Image To Project..." );
 
             gd.addMessage( "Note: You can only 'link' to images outside the project folder \n" +
-                    " for local projects. 'copy' or 'move' if you wish to upload to s3");
-
-            // TODO - ImageDataFormat.OmeZarr removed from here for now. Add it back when transforms are supported
-            // so the voxel size can be maintained.
-            String[] imageFormats = new String[]{ ImageDataFormat.BdvN5.toString(),
-                    ImageDataFormat.BdvOmeZarr.toString() };
-            gd.addChoice( "Image format", imageFormats, imageFormats[0] );
+                    " for local projects. 'copy' or 'move' if you wish to upload to s3" );
 
+            gd.addChoice( "Image format", imageFormats, imageDataFormat.toString() );
             String[] addMethods = new String[]{ ProjectCreator.AddMethod.link.toString(),
                     ProjectCreator.AddMethod.copy.toString(), ProjectCreator.AddMethod.move.toString() };
-            gd.addChoice("Add method:", addMethods, addMethods[0]);
-            String[] imageTypes = new String[]{ ProjectCreator.ImageType.image.toString(),
-                    ProjectCreator.ImageType.segmentation.toString() };
-            gd.addChoice("Image Type", imageTypes, imageTypes[0]);
-            gd.addCheckbox( "Create view for this image", true );
+            gd.addChoice( "Add method:", addMethods, addMethod.toString() );
+            gd.addChoice( "Image Type", imageTypes, imageType.toString() );
+            gd.addCheckbox( "Make view exclusive", exclusive );
+            gd.addCheckbox( "Use filename as image name", useFileNameAsImageName );
 
             gd.showDialog();
 
-            if (!gd.wasCanceled()) {
-                ImageDataFormat imageDataFormat = ImageDataFormat.fromString( gd.getNextChoice() );
-                ProjectCreator.AddMethod addMethod = ProjectCreator.AddMethod.valueOf( gd.getNextChoice() );
-                ProjectCreator.ImageType imageType = ProjectCreator.ImageType.valueOf( gd.getNextChoice() );
-                boolean createView = gd.getNextBoolean();
-
-                if ( imageDataFormat == ImageDataFormat.OmeZarr && addMethod == ProjectCreator.AddMethod.link ) {
-                    IJ.log( "link is currently unsupported for ome-zarr. Please choose copy or move instead for this" +
-                            "file format." );
-                    return;
-                }
+            if ( gd.wasCanceled() )
+            {
+                IJ.log( "Add image failed - create a dataset first" );
+                return;
+            }
 
-                // select the image file - either .xml or .ome.zarr
-                String filePath = null;
-                switch ( imageDataFormat ) {
-                    case BdvN5:
-                        filePath = Utils.selectOpenPathFromFileSystem("bdv .xml file", "xml");
-                        break;
-
-                    case BdvOmeZarr:
-                        filePath = Utils.selectOpenPathFromFileSystem("bdv .xml file", "xml");
-                        break;
-
-                    case OmeZarr:
-                        filePath = Utils.selectOpenDirFromFileSystem(".ome.zarr file" );
-                        // quick check that basic criteria for ome-zarr are met i.e. contains right files in top of dir
-                        if( !(new File( FileAndUrlUtils.combinePath(filePath, ".zgroup") ).exists() &&
-                                new File( FileAndUrlUtils.combinePath( filePath, ".zattrs")).exists() )) {
-                            IJ.log( "Add image failed - not a valid ome.zarr file." );
-                            return;
-                        }
-                        break;
+            imageDataFormat = ImageDataFormat.fromString( gd.getNextChoice() );
+            addMethod = ProjectCreator.AddMethod.valueOf( gd.getNextChoice() );
+            imageType = ProjectCreator.ImageType.valueOf( gd.getNextChoice() );
+            exclusive = gd.getNextBoolean();
+            useFileNameAsImageName = gd.getNextBoolean();
+
+            if ( imageDataFormat == ImageDataFormat.OmeZarr && addMethod == ProjectCreator.AddMethod.link )
+            {
+                IJ.log( "link is currently unsupported for ome-zarr. Please choose copy or move instead for this file format." );
+                return;
+            }
+
+            String filePath = getFilePath();
+
+            if ( filePath != null )
+            {
+                try
+                {
+                    addBdvFile( filePath, datasetName );
+                } catch ( SpimDataException e )
+                {
+                    e.printStackTrace();
                 }
+            }
+        }
+    }
 
-                if ( filePath != null ) {
+    private String getFilePath()
+    {
+        switch ( imageDataFormat )
+        {
+            case BdvN5:
+                return MoBIEUtils.selectFilePath( "xml", "bdv .xml file", true );
 
-                    File imageFile = new File( filePath );
-                    String imageName = imageFile.getName().split("\\.")[0];
+            case OmeZarr:
+                String filePath = MoBIEUtils.selectDirectoryPath( ".ome.zarr file", true );
 
-                    try {
-                        String uiSelectionGroup = null;
-                        if (createView) {
-                            uiSelectionGroup = selectUiSelectionGroupDialog(datasetName);
-                            if (uiSelectionGroup != null) {
-                                projectsCreator.getImagesCreator().addBdvFormatImage(imageFile, datasetName, imageType,
-                                        addMethod, uiSelectionGroup, imageDataFormat );
-                                updateComboBoxesForNewImage(imageName, uiSelectionGroup);
-                            }
-                        } else {
-                            projectsCreator.getImagesCreator().addBdvFormatImage(imageFile, datasetName, imageType,
-                                    addMethod, uiSelectionGroup, imageDataFormat );
-                            updateComboBoxesForNewImage(imageName, uiSelectionGroup);
-                        }
-                    } catch (SpimDataException | IOException e) {
-                        e.printStackTrace();
-                    }
+                // quick check that basic criteria for ome-zarr are met i.e. contains right files in top of dir
+                if ( ! isValidOMEZarr( filePath ) )
+                {
+                    IJ.log( "Add image failed - not a valid ome.zarr file." );
+                    return null;
                 }
+                else
+                {
+                    return filePath;
+                }
+            default:
+                return null;
+        }
+    }
+
+    private boolean isValidOMEZarr( String filePath )
+    {
+        return ( new File( FileAndUrlUtils.combinePath( filePath, ".zgroup" ) ).exists() && new File( FileAndUrlUtils.combinePath( filePath, ".zattrs" ) ).exists() );
+    }
+
+    private void addBdvFile( String filePath, String datasetName ) throws SpimDataException {
+        SpimData spimData = ( SpimData ) new SpimDataOpener().openSpimData( filePath, imageDataFormat );
+
+        int nChannels = spimData.getSequenceDescription().getViewSetupsOrdered().size();
+        String imageUnit = spimData.getSequenceDescription().getViewSetupsOrdered().get(0).getVoxelSize().unit();
+
+        if ( !isImageValid( nChannels, imageUnit, projectsCreator.getVoxelUnit(), true ) ) {
+            return;
+        }
+
+        if ( !isSpimData2D(spimData) && projectsCreator.getDataset( datasetName ).is2D ) {
+            if ( !changeDatasetDimensionDialog(datasetName) ) {
+                return;
             }
+        }
+
+        File imageFile = new File( filePath );
+        String imageName = imageFile.getName().split("\\.")[0];
+        if ( !useFileNameAsImageName ) {
+            imageName = imageNameDialog( imageFile );
+            if ( imageName == null ) {
+                return;
+            }
+        }
+
+        ImagesCreator imagesCreator = projectsCreator.getImagesCreator();
+        boolean overwriteImage = true;
+        if ( imagesCreator.imageExists( datasetName, imageName, imageDataFormat ) ) {
+            overwriteImage = overwriteImageDialog();
+        }
+        if ( !overwriteImage ) {
+            return;
+        }
+
+        String chosenUiSelectionGroup = selectUiSelectionGroupDialog(datasetName);
+        if ( chosenUiSelectionGroup == null ) {
+            return;
         } else {
-            IJ.log( "Add image failed - create a dataset first" );
+            uiSelectionGroup = chosenUiSelectionGroup;
+        }
+
+        try {
+            imagesCreator.addBdvFormatImage(spimData, imageName, datasetName, imageType,
+                    addMethod, uiSelectionGroup, imageDataFormat, exclusive);
+            updateComboBoxesForNewImage(imageName, uiSelectionGroup);
+        } catch (SpimDataException | IOException e) {
+            e.printStackTrace();
         }
     }
 
     public void addDatasetDialog () {
         final GenericDialog gd = new GenericDialog( "Create a new dataset" );
         gd.addStringField( "Name of dataset", "", 35 );
+        gd.addCheckbox("Limit images and display to only 2D?", is2D );
         gd.showDialog();
 
         if ( !gd.wasCanceled() ) {
             String datasetName = gd.getNextString();
-            datasetName = UserInterfaceHelper.tidyString( datasetName );
+            datasetName = UserInterfaceHelpers.tidyString( datasetName );
+            is2D = gd.getNextBoolean();
 
             if ( datasetName != null ) {
-                projectsCreator.getDatasetsCreator().addDataset(datasetName);
+                projectsCreator.getDatasetsCreator().addDataset( datasetName, is2D );
                 updateDatasetsComboBox( datasetName );
             }
         }
@@ -594,7 +741,7 @@ private void editDatasetDialog() {
 
             if (!gd.wasCanceled()) {
                 String newName = gd.getNextString();
-                newName = UserInterfaceHelper.tidyString( newName );
+                newName = UserInterfaceHelpers.tidyString( newName );
                 if ( newName != null ) {
                     projectsCreator.getDatasetsCreator().renameDataset( oldName, newName );
                     updateDatasetsComboBox( newName );
diff --git a/src/main/java/org/embl/mobie/viewer/segment/BdvSegmentSelector.java b/src/main/java/org/embl/mobie/viewer/segment/BdvSegmentSelector.java
index f1d1fef77..5cd3c85c0 100644
--- a/src/main/java/org/embl/mobie/viewer/segment/BdvSegmentSelector.java
+++ b/src/main/java/org/embl/mobie/viewer/segment/BdvSegmentSelector.java
@@ -1,25 +1,32 @@
 package org.embl.mobie.viewer.segment;
 
+import bdv.tools.transformation.TransformedSource;
 import bdv.util.BdvHandle;
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
-import org.embl.mobie.viewer.bdv.BdvMousePositionProvider;
+import org.embl.mobie.viewer.SourceNameEncoder;
+import org.embl.mobie.viewer.bdv.BdvGlobalMousePositionProvider;
 import org.embl.mobie.viewer.display.SegmentationSourceDisplay;
 import de.embl.cba.tables.tablerow.TableRowImageSegment;
 import net.imglib2.RandomAccess;
 import net.imglib2.RealPoint;
 import net.imglib2.type.numeric.RealType;
+import org.embl.mobie.viewer.source.LabelSource;
+import org.embl.mobie.viewer.transform.MergedGridSource;
 import sc.fiji.bdvpg.sourceandconverter.SourceAndConverterHelper;
 
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.function.Supplier;
 
 public class BdvSegmentSelector implements Runnable
 {
 	private BdvHandle bdvHandle;
 	private boolean is2D;
-	private Supplier< Collection<SegmentationSourceDisplay> > segmentationDisplaySupplier;
+	private Supplier< Collection< SegmentationSourceDisplay > > segmentationDisplaySupplier;
 
+	// A segmentationDisplaySupplier is used such that the segmentation images can
+	// change during runtime.
 	public BdvSegmentSelector( BdvHandle bdvHandle, boolean is2D, Supplier< Collection< SegmentationSourceDisplay > > segmentationDisplaySupplier )
 	{
 		this.bdvHandle = bdvHandle;
@@ -29,7 +36,7 @@ public BdvSegmentSelector( BdvHandle bdvHandle, boolean is2D, Supplier< Collecti
 
 	public synchronized void clearSelection()
 	{
-		final Collection< SegmentationSourceDisplay > segmentationDisplays = segmentationDisplaySupplier.get();
+		final Collection< SegmentationSourceDisplay > segmentationDisplays = getCurrent();
 
 		for ( SegmentationSourceDisplay segmentationDisplay : segmentationDisplays )
 		{
@@ -37,27 +44,53 @@ public synchronized void clearSelection()
 		}
 	}
 
+	private Collection< SegmentationSourceDisplay > getCurrent()
+	{
+		return segmentationDisplaySupplier.get();
+	}
+
 	private synchronized void toggleSelectionAtMousePosition()
 	{
-		final BdvMousePositionProvider positionProvider = new BdvMousePositionProvider( bdvHandle );
+		final BdvGlobalMousePositionProvider positionProvider = new BdvGlobalMousePositionProvider( bdvHandle );
 		final int timePoint = positionProvider.getTimePoint();
-		final RealPoint position = positionProvider.getPosition();
+		final RealPoint position = positionProvider.getPositionAsRealPoint();
 
 		final Collection< SegmentationSourceDisplay > segmentationDisplays = segmentationDisplaySupplier.get();
 
 		for ( SegmentationSourceDisplay segmentationDisplay : segmentationDisplays )
 		{
-			for ( SourceAndConverter< ? > sourceAndConverter : segmentationDisplay.sourceNameToSourceAndConverter.values() )
+			final Collection< SourceAndConverter< ? > > segmentationSourceAndConverters = segmentationDisplay.sourceNameToSourceAndConverter.values();
+
+			for ( SourceAndConverter< ? > sourceAndConverter : segmentationSourceAndConverters )
 			{
+				if ( ! bdvHandle.getViewerPanel().state().isSourceVisible( sourceAndConverter ) )
+				{
+					continue;
+				}
+
 				if ( SourceAndConverterHelper.isPositionWithinSourceInterval( sourceAndConverter, position, timePoint, is2D ) )
 				{
 					final Source< ? > source = sourceAndConverter.getSpimSource();
-					final double labelIndex = getPixelValue( timePoint, position, source );
-					if ( labelIndex == 0 ) return;
 
-					final TableRowImageSegment segment = segmentationDisplay.segmentAdapter.getSegment( labelIndex, timePoint, source.getName() );
+					final double pixelValue = getPixelValue( timePoint, position, source );
+					final String sourceName = getSourceName( source, pixelValue );
+					double labelIndex = getLabelIndex( source, pixelValue );
+
+					if ( labelIndex == 0 ) continue; // image background
+
+					final boolean containsSegment = segmentationDisplay.segmentAdapter.containsSegment( labelIndex, timePoint, sourceName );
+
+					if ( ! containsSegment )
+					{
+						// This happens when there is a segmentation without
+						// a segment table
+						continue;
+					}
+
+					final TableRowImageSegment segment = segmentationDisplay.segmentAdapter.getSegment( labelIndex, timePoint, sourceName );
 
 					segmentationDisplay.selectionModel.toggle( segment );
+
 					if ( segmentationDisplay.selectionModel.isSelected( segment ) )
 					{
 						segmentationDisplay.selectionModel.focus( segment );
@@ -67,13 +100,63 @@ private synchronized void toggleSelectionAtMousePosition()
 		}
 	}
 
+	private static double getLabelIndex( Source< ? > source, double pixelValue )
+	{
+		if ( MergedGridSource.instanceOf( source ) )
+		{
+			return SourceNameEncoder.getValue( Double.valueOf( pixelValue ).longValue() );
+		}
+		else
+		{
+			return pixelValue;
+		}
+	}
+
+	private static String getSourceName( Source< ? > source, double labelIndex )
+	{
+		if ( MergedGridSource.instanceOf( source ) )
+		{
+			return SourceNameEncoder.getName( Double.valueOf( labelIndex ).longValue() );
+		}
+		final String sourceName = source.getName();
+		return sourceName;
+	}
+
+	private Collection< SourceAndConverter< ? > > getContainedSourceAndConverters( SourceAndConverter< ? > sourceAndConverter )
+	{
+		final Collection< SourceAndConverter< ? > > containedSourceAndConverters = new HashSet<>();
+
+		Source< ? > source = sourceAndConverter.getSpimSource();
+
+		if ( source instanceof LabelSource )
+		{
+			source = ( ( LabelSource ) source ).getWrappedSource();
+		}
+
+		if ( source instanceof TransformedSource )
+		{
+			source = ( ( TransformedSource< ? > ) source ).getWrappedSource();
+		}
+
+		if ( source instanceof MergedGridSource )
+		{
+			containedSourceAndConverters.addAll( ( ( MergedGridSource ) source ).getContainedSourceAndConverters() );
+		}
+		else
+		{
+			// not a MergedGridSource, just return the initial source
+			containedSourceAndConverters.add( sourceAndConverter );
+		}
+
+		return containedSourceAndConverters;
+	}
+
 	private static double getPixelValue( int timePoint, RealPoint position, Source< ? > source )
 	{
 		final RandomAccess< RealType > randomAccess = ( RandomAccess< RealType > ) source.getSource( timePoint, 0 ).randomAccess();
 		final long[] positionInSource = SourceAndConverterHelper.getVoxelPositionInSource( source, position, timePoint, 0 );
 		randomAccess.setPosition( positionInSource );
-		final double labelIndex = randomAccess.get().getRealDouble();
-		return labelIndex;
+		return randomAccess.get().getRealDouble();
 	}
 
 	@Override
diff --git a/src/main/java/org/embl/mobie/viewer/segment/SegmentAdapter.java b/src/main/java/org/embl/mobie/viewer/segment/SegmentAdapter.java
index 1e4cd8758..b69df6855 100644
--- a/src/main/java/org/embl/mobie/viewer/segment/SegmentAdapter.java
+++ b/src/main/java/org/embl/mobie/viewer/segment/SegmentAdapter.java
@@ -11,6 +11,7 @@
 public class SegmentAdapter< T extends ImageSegment >
 {
 	private HashMap< LabelFrameAndImage, T > labelFrameAndImageToSegment;
+	private boolean isLazy = false;
 
 	/**
 	 * For lazy initialization
@@ -18,6 +19,7 @@ public class SegmentAdapter< T extends ImageSegment >
 	public SegmentAdapter()
 	{
 		labelFrameAndImageToSegment = new HashMap<>();
+		isLazy = true;
 	}
 
 	public SegmentAdapter( List< T > segments )
@@ -28,15 +30,17 @@ public SegmentAdapter( List< T > segments )
 			labelFrameAndImageToSegment.put( new LabelFrameAndImage( segment ), segment );
 	}
 
-	public T getSegment( double label, int t, String imageId )
+	public synchronized boolean containsSegment( double label, int t, String imageId )
 	{
 		final LabelFrameAndImage labelFrameAndImage = new LabelFrameAndImage( label, t, imageId  );
 
-		return getSegment( labelFrameAndImage );
+		return labelFrameAndImageToSegment.containsKey( labelFrameAndImage );
 	}
 
-	private T getSegment( LabelFrameAndImage labelFrameAndImage )
+	public synchronized T getSegmentCreateIfNotExist( double label, int t, String imageId )
 	{
+		final LabelFrameAndImage labelFrameAndImage = new LabelFrameAndImage( label, t, imageId  );
+
 		if ( ! labelFrameAndImageToSegment.containsKey( labelFrameAndImage ) )
 		{
 			final DefaultImageSegment defaultImageSegment = new DefaultImageSegment( labelFrameAndImage.getImage(), labelFrameAndImage.getLabel(), labelFrameAndImage.getFrame(), 0, 0, 0, null );
@@ -46,6 +50,24 @@ private T getSegment( LabelFrameAndImage labelFrameAndImage )
 		return labelFrameAndImageToSegment.get( labelFrameAndImage );
 	}
 
+	public synchronized T getSegment( double label, int t, String imageId )
+	{
+		if ( isLazy )
+		{
+			return getSegmentCreateIfNotExist( label, t, imageId );
+		}
+		else
+		{
+			final LabelFrameAndImage labelFrameAndImage = new LabelFrameAndImage( label, t, imageId );
+			return getSegment( labelFrameAndImage );
+		}
+	}
+
+	public synchronized T getSegment( LabelFrameAndImage labelFrameAndImage )
+	{
+		return labelFrameAndImageToSegment.get( labelFrameAndImage  );
+	}
+
 	// deserialize
 	public List< T > getSegments( List< String > serialisedSegments )
 	{
diff --git a/src/main/java/org/embl/mobie/viewer/serialize/AdditionalViewsJsonParser.java b/src/main/java/org/embl/mobie/viewer/serialize/AdditionalViewsJsonParser.java
index df5fa4bdd..9a4af3cf0 100644
--- a/src/main/java/org/embl/mobie/viewer/serialize/AdditionalViewsJsonParser.java
+++ b/src/main/java/org/embl/mobie/viewer/serialize/AdditionalViewsJsonParser.java
@@ -4,7 +4,7 @@
 import com.google.gson.reflect.TypeToken;
 import com.google.gson.stream.JsonWriter;
 import org.embl.mobie.viewer.view.additionalviews.AdditionalViews;
-import de.embl.cba.tables.FileAndUrlUtils;
+import org.embl.mobie.io.util.FileAndUrlUtils;
 
 import java.io.*;
 import java.lang.reflect.Type;
diff --git a/src/main/java/org/embl/mobie/viewer/serialize/DatasetJsonParser.java b/src/main/java/org/embl/mobie/viewer/serialize/DatasetJsonParser.java
index dd9f77191..0f136cec3 100644
--- a/src/main/java/org/embl/mobie/viewer/serialize/DatasetJsonParser.java
+++ b/src/main/java/org/embl/mobie/viewer/serialize/DatasetJsonParser.java
@@ -3,8 +3,9 @@
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
 import com.google.gson.stream.JsonWriter;
+import ij.IJ;
 import org.embl.mobie.viewer.Dataset;
-import de.embl.cba.tables.FileAndUrlUtils;
+import org.embl.mobie.io.util.FileAndUrlUtils;
 
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -26,14 +27,9 @@ public Dataset parseDataset( String path ) throws IOException
 	}
 
 	public void saveDataset( Dataset dataset, String path ) throws IOException {
-		Gson gson = JsonHelper.buildGson( false );
-		Type type = new TypeToken< Dataset >() {}.getType();
-
-		try (OutputStream outputStream = new FileOutputStream( path );
-			 final JsonWriter writer = new JsonWriter( new OutputStreamWriter(outputStream, "UTF-8")) ) {
-			writer.setIndent("  ");
-			gson.toJson(dataset, type, writer);
-		}
+		Gson gson = JsonHelper.buildGson( true );
+		final String json = gson.toJson( dataset ).replaceAll("\t", "  ");;
+		FileAndUrlUtils.write( path, json );
 	}
 
 	public String datasetToJsonString( Dataset dataset, boolean prettyPrinting ) {
diff --git a/src/main/java/org/embl/mobie/viewer/serialize/JsonHelper.java b/src/main/java/org/embl/mobie/viewer/serialize/JsonHelper.java
index d9bebc25e..4919a91bf 100644
--- a/src/main/java/org/embl/mobie/viewer/serialize/JsonHelper.java
+++ b/src/main/java/org/embl/mobie/viewer/serialize/JsonHelper.java
@@ -7,7 +7,7 @@
 import org.embl.mobie.viewer.display.SourceDisplay;
 import org.embl.mobie.viewer.transform.SourceTransformer;
 import org.embl.mobie.viewer.transform.ViewerTransform;
-import de.embl.cba.tables.FileAndUrlUtils;
+import org.embl.mobie.io.util.FileAndUrlUtils;
 
 import java.io.InputStream;
 import java.io.InputStreamReader;
diff --git a/src/main/java/org/embl/mobie/viewer/serialize/ProjectJsonParser.java b/src/main/java/org/embl/mobie/viewer/serialize/ProjectJsonParser.java
index be112d425..3e08a90c3 100644
--- a/src/main/java/org/embl/mobie/viewer/serialize/ProjectJsonParser.java
+++ b/src/main/java/org/embl/mobie/viewer/serialize/ProjectJsonParser.java
@@ -4,7 +4,7 @@
 import com.google.gson.reflect.TypeToken;
 import com.google.gson.stream.JsonWriter;
 import org.embl.mobie.viewer.Project;
-import de.embl.cba.tables.FileAndUrlUtils;
+import org.embl.mobie.io.util.FileAndUrlUtils;
 
 import java.io.FileOutputStream;
 import java.io.IOException;
diff --git a/src/main/java/org/embl/mobie/viewer/serialize/ViewerTransformAdapter.java b/src/main/java/org/embl/mobie/viewer/serialize/ViewerTransformAdapter.java
index 9953ece5c..005b4d132 100644
--- a/src/main/java/org/embl/mobie/viewer/serialize/ViewerTransformAdapter.java
+++ b/src/main/java/org/embl/mobie/viewer/serialize/ViewerTransformAdapter.java
@@ -2,6 +2,7 @@
 
 import com.google.gson.*;
 import org.embl.mobie.viewer.transform.AffineViewerTransform;
+import org.embl.mobie.viewer.transform.NormalVectorViewerTransform;
 import org.embl.mobie.viewer.transform.NormalizedAffineViewerTransform;
 import org.embl.mobie.viewer.transform.PositionViewerTransform;
 import org.embl.mobie.viewer.transform.TimepointViewerTransform;
@@ -12,7 +13,7 @@
 import java.util.TreeMap;
 
 // TODO: maybe we don't need the serializer?!
-public class ViewerTransformAdapter implements JsonSerializer<ViewerTransform>, JsonDeserializer< ViewerTransform >
+public class ViewerTransformAdapter implements JsonSerializer<ViewerTransform>, JsonDeserializer<ViewerTransform>
 {
 	private static Map<String, Class> nameToClass = new TreeMap<>();
 	private static Map<String, String> classToName = new TreeMap<>();
@@ -26,6 +27,8 @@ public class ViewerTransformAdapter implements JsonSerializer<ViewerTransform>,
 		classToName.put(PositionViewerTransform.class.getName(), "position");
 		nameToClass.put("timepoint", TimepointViewerTransform.class);
 		classToName.put(TimepointViewerTransform.class.getName(), "timepoint");
+		nameToClass.put("normalVector", NormalVectorViewerTransform.class);
+		classToName.put(NormalVectorViewerTransform.class.getName(), "normalVector");
 	}
 
 	@Override
diff --git a/src/main/java/org/embl/mobie/viewer/source/ImageDataFormat.java b/src/main/java/org/embl/mobie/viewer/source/ImageDataFormat.java
deleted file mode 100644
index 953a72924..000000000
--- a/src/main/java/org/embl/mobie/viewer/source/ImageDataFormat.java
+++ /dev/null
@@ -1,111 +0,0 @@
-package org.embl.mobie.viewer.source;
-
-import com.google.gson.annotations.SerializedName;
-
-public enum ImageDataFormat
-{
-	@SerializedName( "bdv.n5" )
-	BdvN5,
-	@SerializedName( "bdv.n5.s3" )
-	BdvN5S3,
-	@SerializedName( "openOrganelle.s3" )
-	OpenOrganelleS3,
-	@SerializedName("bdv.ome.zarr")
-	BdvOmeZarr,
-    @SerializedName("bdv.ome.zarr.s3")
-    BdvOmeZarrS3,
-	@SerializedName("ome.zarr")
-	OmeZarr,
-    @SerializedName("ome.zarr.s3")
-    OmeZarrS3;
-
-	// needed for SciJava Command UI, which does not support enums
-	public static final String BDVN5 = "BdvN5";
-	public static final String BDVN5S3 = "BdvN5S3";
-	public static final String OPENORGANELLES3 = "OpenOrganelleS3";
-	public static final String BDVOMEZARR = "BdvOmeZarr";
-	public static final String BDVOMEZARRS3 = "BdvOmeZarrS3";
-	public static final String OMEZARR = "OmeZarr";
-	public static final String OMEZARRS3 = "OmeZarrS3";
-
-	@Override
-	public String toString()
-	{
-		switch ( this )
-		{
-			case BdvN5:
-				return "bdv.n5";
-			case BdvN5S3:
-				return "bdv.n5.s3";
-			case OpenOrganelleS3:
-				return "openOrganelle.s3";
-			case BdvOmeZarr:
-				return "bdv.ome.zarr";
-			case OmeZarr:
-				return "ome.zarr";
-            case BdvOmeZarrS3:
-                return "bdv.ome.zarr.s3";
-            case OmeZarrS3:
-                return "ome.zarr.s3";
-			default:
-				throw new UnsupportedOperationException( "Unknown file format: " + this );
-		}
-	}
-
-	public static ImageDataFormat fromString( String string )
-	{
-		switch ( string )
-		{
-			case "bdv.n5":
-				return BdvN5;
-			case "bdv.n5.s3":
-				return BdvN5S3;
-			case "openOrganelle":
-				return OpenOrganelleS3;
-			case "bdv.ome.zarr":
-				return BdvOmeZarr;
-			case "ome.zarr":
-				return OmeZarr;
-            case "bdv.ome.zarr.s3":
-                return BdvOmeZarrS3;
-            case "ome.zarr.s3":
-                return OmeZarrS3;
-			default:
-				throw new UnsupportedOperationException( "Unknown file format: " + string );
-		}
-	}
-
-	public boolean isRemote() {
-		switch ( this )
-		{
-			case BdvN5S3:
-			case OmeZarrS3:
-			case BdvOmeZarrS3:
-			case OpenOrganelleS3:
-				return true;
-			case BdvN5:
-			case BdvOmeZarr:
-			case OmeZarr:
-			default:
-				return false;
-		}
-	}
-
-	public boolean hasXml() {
-		switch ( this )
-		{
-			case BdvN5S3:
-			case BdvOmeZarr:
-			case BdvN5:
-			case BdvOmeZarrS3:
-				return true;
-			case OmeZarr:
-			case OpenOrganelleS3:
-			case OmeZarrS3:
-			default:
-				return false;
-		}
-
-	}
-
-}
diff --git a/src/main/java/org/embl/mobie/viewer/source/ImageSource.java b/src/main/java/org/embl/mobie/viewer/source/ImageSource.java
index 3931903f8..1bac56057 100644
--- a/src/main/java/org/embl/mobie/viewer/source/ImageSource.java
+++ b/src/main/java/org/embl/mobie/viewer/source/ImageSource.java
@@ -1,5 +1,7 @@
 package org.embl.mobie.viewer.source;
 
+import org.embl.mobie.io.ImageDataFormat;
+
 import java.util.Map;
 
 public class ImageSource
diff --git a/src/main/java/org/embl/mobie/viewer/source/LabelSource.java b/src/main/java/org/embl/mobie/viewer/source/LabelSource.java
new file mode 100644
index 000000000..ea3eeac31
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/source/LabelSource.java
@@ -0,0 +1,108 @@
+package org.embl.mobie.viewer.source;
+
+import bdv.util.DefaultInterpolators;
+import bdv.viewer.Interpolation;
+import bdv.viewer.Source;
+import mpicbg.spim.data.sequence.VoxelDimensions;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.RealRandomAccessible;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.numeric.NumericType;
+import net.imglib2.type.numeric.RealType;
+import net.imglib2.view.Views;
+
+/**
+ * A {@link Source} that wraps another {@link Source}
+ * <p>
+ * This extra transformation is made to capture manual editing of the actual
+ * transform in the SpimViewer.
+ *
+ * @param <T> the type of the original source.
+ * @author Christian Tischer - Oct 2020
+ */
+public class LabelSource<T extends NumericType<T> & RealType<T>> implements Source<T> {
+    protected final Source<T> source;
+    private final DefaultInterpolators<T> interpolators;
+    private boolean showAsBoundaries;
+
+    public LabelSource(final Source<T> source) {
+        this.source = source;
+        this.interpolators = new DefaultInterpolators<>();
+    }
+
+    public void showAsBoundary(boolean showAsBoundaries, int boundaryWidth) {
+        this.showAsBoundaries = showAsBoundaries;
+    }
+
+    @Override
+    public boolean doBoundingBoxCulling() {
+        return source.doBoundingBoxCulling();
+    }
+
+    @Override
+    public synchronized void getSourceTransform(final int t, final int level, final AffineTransform3D transform) {
+        source.getSourceTransform(t, level, transform);
+    }
+
+    @Override
+    public boolean isPresent(final int t) {
+        return source.isPresent(t);
+    }
+
+    @Override
+    public RandomAccessibleInterval<T> getSource(final int t, final int level) {
+        return source.getSource(t, level);
+
+        // below code is not needed, because BDV (I think) always shows the interpolated source
+//		RandomAccessibleInterval< T > source = this.source.getSource( t, level );
+//
+//		if ( showAsBoundaries )
+//		{
+//			NeighborhoodNonZeroBoundariesConverter2< T > boundariesConverter = new NeighborhoodNonZeroBoundariesConverter2< T >( source );
+//			RandomAccessibleInterval boundaries = NeighborhoodViews.neighborhoodConvertedView(
+//					source,
+//					boundariesConverter,
+//					new HyperSphereShape( boundaryWidth ) );
+//			return boundaries;
+//		}
+//		else
+//		{
+//			return source;
+//		}
+    }
+
+    @Override
+    public RealRandomAccessible<T> getInterpolatedSource(final int t, final int level, final Interpolation method) {
+        // do not interpolate for label images, but always use NEARESTNEIGHBOR
+        if (showAsBoundaries) {
+            RandomAccessibleInterval<T> rai = getSource(t, level);
+            return Views.interpolate(Views.extendZero(rai), interpolators.get(Interpolation.NEARESTNEIGHBOR));
+        } else {
+            return source.getInterpolatedSource(t, level, Interpolation.NEARESTNEIGHBOR);
+        }
+    }
+
+    @Override
+    public T getType() {
+        return source.getType();
+    }
+
+    @Override
+    public String getName() {
+        return source.getName();
+    }
+
+    @Override
+    public VoxelDimensions getVoxelDimensions() {
+        return source.getVoxelDimensions();
+    }
+
+    @Override
+    public int getNumMipmapLevels() {
+        return source.getNumMipmapLevels();
+    }
+
+    public Source<T> getWrappedSource() {
+        return source;
+    }
+}
diff --git a/src/main/java/org/embl/mobie/viewer/source/Sources.java b/src/main/java/org/embl/mobie/viewer/source/Sources.java
new file mode 100644
index 000000000..0e12c4d38
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/source/Sources.java
@@ -0,0 +1,72 @@
+package org.embl.mobie.viewer.source;
+
+import bdv.util.BdvFunctions;
+import bdv.util.BdvHandle;
+import bdv.util.BdvStackSource;
+import bdv.viewer.SourceAndConverter;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.converter.Converter;
+import net.imglib2.img.display.imagej.ImageJFunctions;
+import net.imglib2.type.numeric.ARGBType;
+import net.imglib2.type.numeric.NumericType;
+import net.imglib2.type.numeric.RealType;
+import net.imglib2.util.Cast;
+import net.imglib2.view.IntervalView;
+import net.imglib2.view.Views;
+import org.embl.mobie.viewer.color.LazyLabelConverter;
+import org.scijava.ui.behaviour.ClickBehaviour;
+import org.scijava.ui.behaviour.io.InputTriggerConfig;
+import org.scijava.ui.behaviour.util.Behaviours;
+
+import java.lang.reflect.Method;
+
+public abstract class Sources {
+    public static <R extends NumericType<R> & RealType<R>> SourceAndConverter<R> replaceConverter(SourceAndConverter<?> source, Converter<RealType<?>, ARGBType> converter) {
+        LabelSource<?> labelVolatileSource = new LabelSource(source.asVolatile().getSpimSource());
+        SourceAndConverter<?> volatileSourceAndConverter = new SourceAndConverter(labelVolatileSource, converter);
+        LabelSource<?> labelSource = new LabelSource(source.getSpimSource());
+        return new SourceAndConverter(labelSource, converter, volatileSourceAndConverter);
+    }
+
+    public static <R extends NumericType<R> & RealType<R>> BdvStackSource<?> showAsLabelMask(BdvStackSource<?> bdvStackSource) {
+        LazyLabelConverter converter = new LazyLabelConverter();
+        SourceAndConverter<R> sac = replaceConverter(bdvStackSource.getSources().get(0), converter);
+        BdvHandle bdvHandle = bdvStackSource.getBdvHandle();
+        bdvStackSource.removeFromBdv();
+
+        // access by reflection, which feels quite OK as this will be public in future versions of bdv anyway:
+        // https://github.com/bigdataviewer/bigdataviewer-vistools/commit/8cad3edac6c563dc2d22abf71345655afa7f49cc
+        try {
+            Method method = BdvFunctions.class.getDeclaredMethod("addSpimDataSource", BdvHandle.class, SourceAndConverter.class, int.class);
+            method.setAccessible(true);
+            BdvStackSource<?> newBdvStackSource = (BdvStackSource<?>) method.invoke("addSpimDataSource", bdvHandle, sac, 1);
+
+            Behaviours behaviours = new Behaviours(new InputTriggerConfig());
+            behaviours.install(bdvHandle.getTriggerbindings(), "label source " + sac.getSpimSource().getName());
+            behaviours.behaviour((ClickBehaviour) (x, y) ->
+                            new Thread(() -> {
+                                converter.getColoringModel().incRandomSeed();
+                                bdvHandle.getViewerPanel().requestRepaint();
+                            }).start(),
+                    "shuffle random colors " + sac.getSpimSource().getName(),
+                    "ctrl L");
+
+            return newBdvStackSource;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+    }
+
+    /**
+     * TODO: add time
+     *
+     * @param bdvStackSource
+     * @param level
+     */
+    public static void viewAsHyperstack(BdvStackSource<?> bdvStackSource, int level) {
+        RandomAccessibleInterval<?> rai = bdvStackSource.getSources().get(0).getSpimSource().getSource(0, level);
+        IntervalView<?> permute = Views.permute(Views.addDimension(rai, 0, 0), 2, 3);
+        ImageJFunctions.wrap(Cast.unchecked(permute), "em").show();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/embl/mobie/viewer/source/SpimDataOpener.java b/src/main/java/org/embl/mobie/viewer/source/SpimDataOpener.java
deleted file mode 100644
index 3876bcaa7..000000000
--- a/src/main/java/org/embl/mobie/viewer/source/SpimDataOpener.java
+++ /dev/null
@@ -1,208 +0,0 @@
-package org.embl.mobie.viewer.source;
-
-import bdv.util.volatiles.SharedQueue;
-import de.embl.cba.bdv.utils.BdvUtils;
-import de.embl.cba.tables.FileAndUrlUtils;
-import mpicbg.spim.data.SpimData;
-import net.imglib2.util.Cast;
-import org.embl.mobie.io.n5.openers.N5Opener;
-import org.embl.mobie.io.n5.openers.N5S3Opener;
-import org.embl.mobie.io.ome.zarr.loaders.N5S3OMEZarrImageLoader;
-import org.embl.mobie.io.ome.zarr.loaders.xml.XmlN5OmeZarrImageLoader;
-import org.embl.mobie.io.ome.zarr.openers.OMEZarrOpener;
-import org.embl.mobie.io.ome.zarr.openers.OMEZarrS3Opener;
-import org.embl.mobie.io.openorganelle.OpenOrganelleS3Opener;
-import org.jdom2.Document;
-import org.jdom2.Element;
-import org.jdom2.JDOMException;
-import org.jdom2.input.SAXBuilder;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Paths;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.stream.Collectors;
-
-import static mpicbg.spim.data.XmlKeys.IMGLOADER_TAG;
-import static mpicbg.spim.data.XmlKeys.SEQUENCEDESCRIPTION_TAG;
-
-public class SpimDataOpener {
-
-    public SpimDataOpener() {}
-
-    public SpimData openSpimData( String imagePath, ImageDataFormat imageDataFormat ) {
-        SpimData spimData = null;
-        switch ( imageDataFormat ) {
-            case BdvN5:
-            case BdvN5S3:
-                spimData = BdvUtils.openSpimData( imagePath );
-                break;
-            case OmeZarr:
-                spimData = openOmeZarrData( imagePath );
-                break;
-            case OmeZarrS3:
-                spimData = openOmeZarrS3Data( imagePath );
-                break;
-            case BdvOmeZarrS3:
-                spimData = openBdvOmeZarrS3Data( imagePath );
-                break;
-            case BdvOmeZarr:
-                spimData = openBdvZarrData( imagePath );
-                break;
-            case OpenOrganelleS3:
-                spimData = openOpenOrganelleData( imagePath );
-        }
-
-        return spimData;
-    }
-
-    public SpimData openSpimData(String imagePath, ImageDataFormat imageDataFormat, SharedQueue sharedQueue ) {
-        SpimData spimData = null;
-        switch ( imageDataFormat ) {
-            case BdvN5:
-                spimData = openBDV( imagePath, sharedQueue );
-                break;
-            case BdvN5S3:
-                spimData = openBDVS3( imagePath, sharedQueue );
-                break;
-            case OmeZarr:
-                spimData = openOmeZarrData( imagePath, sharedQueue );
-                break;
-            case OmeZarrS3:
-                spimData = openOmeZarrS3Data( imagePath );
-                break;
-            case BdvOmeZarrS3:
-                spimData = openBdvOmeZarrS3Data( imagePath );
-                break;
-            case BdvOmeZarr:
-                spimData = openBdvZarrData( imagePath );
-                break;
-            case OpenOrganelleS3:
-                spimData = openOpenOrganelleData( imagePath );
-        }
-
-        return spimData;
-    }
-
-    private SpimData openBDV(String path, SharedQueue queue)
-    {
-        try {
-            return N5Opener.openFile( path, queue );
-        } catch ( IOException e ) {
-            e.printStackTrace();
-        }
-        return null;
-
-    }
-    private SpimData openBDVS3(String path, SharedQueue queue)
-    {
-        try {
-            return N5S3Opener.readURL( path, queue );
-        } catch ( IOException e ) {
-            e.printStackTrace();
-        }
-        return null;
-
-    }
-
-    private SpimData openOmeZarrData( String path )
-    {
-        try {
-            return OMEZarrOpener.openFile( path );
-        } catch ( IOException e ) {
-            e.printStackTrace();
-        }
-        return null;
-    }
-
-    private SpimData openOmeZarrData(String path, SharedQueue sharedQueue )
-    {
-        try {
-            return OMEZarrOpener.openFile( path, sharedQueue);
-        } catch ( IOException e ) {
-            e.printStackTrace();
-        }
-        return null;
-    }
-
-    private SpimData openOmeZarrS3Data( String path )
-    {
-        try {
-            return OMEZarrS3Opener.readURL( path );
-        } catch ( IOException e ) {
-            e.printStackTrace();
-        }
-        return null;
-    }
-
-    private SpimData openOpenOrganelleData( String path )
-    {
-        try {
-            return OpenOrganelleS3Opener.readURL( path );
-        } catch ( IOException e ) {
-            e.printStackTrace();
-        }
-        return null;
-    }
-
-    private SpimData openBdvOmeZarrS3Data( String path )
-    {
-        try {
-            final SAXBuilder sax = new SAXBuilder();
-            InputStream stream = FileAndUrlUtils.getInputStream(path);
-            final Document doc = sax.build(stream);
-            final Element imgLoaderElem = doc.getRootElement().getChild(SEQUENCEDESCRIPTION_TAG).getChild(IMGLOADER_TAG);
-            String bucketAndObject = imgLoaderElem.getChild( "BucketName").getText() + "/" + imgLoaderElem.getChild( "Key" ).getText();
-            final String[] split = bucketAndObject.split("/");
-            String bucket = split[0];
-            String object = Arrays.stream( split ).skip( 1 ).collect( Collectors.joining( "/") );
-            N5S3OMEZarrImageLoader imageLoader = new N5S3OMEZarrImageLoader(imgLoaderElem.getChild( "ServiceEndpoint" ).getText(), imgLoaderElem.getChild( "SigningRegion" ).getText(),bucket, object, ".");
-
-            SpimData spim = new SpimData(null, Cast.unchecked(imageLoader.getSequenceDescription()), imageLoader.getViewRegistrations());
-            SpimData sp1 = BdvUtils.openSpimData( path );
-            sp1.setBasePath(null);
-            sp1.getSequenceDescription().setImgLoader( spim.getSequenceDescription().getImgLoader() );
-            sp1.getSequenceDescription().getAllChannels().putAll( spim.getSequenceDescription().getAllChannels() );
-            return sp1;
-        } catch ( IOException | JDOMException e ) {
-            e.printStackTrace();
-        }
-        return null;
-    }
-
-    private SpimData openBdvZarrData(String path) {
-        try
-        {
-            final SAXBuilder sax = new SAXBuilder();
-            InputStream stream = FileAndUrlUtils.getInputStream(path);
-            final Document doc = sax.build(stream);
-            final Element imgLoaderElem = doc.getRootElement().getChild(SEQUENCEDESCRIPTION_TAG).getChild(IMGLOADER_TAG);
-            String imagesFile = XmlN5OmeZarrImageLoader.getDatasetsPathFromXml(imgLoaderElem, path);
-            if(imagesFile != null)
-            {
-                if ((imagesFile.equals(Paths.get(imagesFile).toString())))
-                {
-                    SpimData spim =  OMEZarrOpener.openFile( imagesFile );
-                    SpimData sp1 = BdvUtils.openSpimData( path );
-                    sp1.setBasePath( new File( imagesFile ) );
-                    sp1.getSequenceDescription().setImgLoader( spim.getSequenceDescription().getImgLoader() );
-                    sp1.getSequenceDescription().getAllChannels().putAll( spim.getSequenceDescription().getAllChannels() );
-                    return sp1;
-                } else
-                {
-                    SpimData spim = OMEZarrS3Opener.readURL( imagesFile );
-                    SpimData sp1 = BdvUtils.openSpimData( path );
-                    sp1.setBasePath(null);
-                    sp1.getSequenceDescription().setImgLoader( spim.getSequenceDescription().getImgLoader() );
-                    sp1.getSequenceDescription().getAllChannels().putAll( spim.getSequenceDescription().getAllChannels() );
-                    return sp1;
-                }
-            }
-        } catch (JDOMException | IOException e) {
-            e.printStackTrace();
-        }
-        return null;
-    }
-}
diff --git a/src/main/java/org/embl/mobie/viewer/source/StorageLocation.java b/src/main/java/org/embl/mobie/viewer/source/StorageLocation.java
index 5d025ca71..7832f86e1 100644
--- a/src/main/java/org/embl/mobie/viewer/source/StorageLocation.java
+++ b/src/main/java/org/embl/mobie/viewer/source/StorageLocation.java
@@ -4,4 +4,5 @@ public class StorageLocation
 {
 	public String relativePath;
 	public String s3Address;
+	public String signingRegion;
 }
diff --git a/src/main/java/org/embl/mobie/viewer/table/TableViewer.java b/src/main/java/org/embl/mobie/viewer/table/TableViewer.java
index afdc972de..5ffc4571b 100644
--- a/src/main/java/org/embl/mobie/viewer/table/TableViewer.java
+++ b/src/main/java/org/embl/mobie/viewer/table/TableViewer.java
@@ -30,6 +30,7 @@
 
 import de.embl.cba.bdv.utils.lut.GlasbeyARGBLut;
 import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.MoBIEUtils;
 import org.embl.mobie.viewer.annotate.AnnotatedIntervalTableRow;
 import org.embl.mobie.viewer.annotate.Annotator;
 import org.embl.mobie.viewer.color.MoBIEColoringModel;
@@ -52,10 +53,7 @@
 import javax.swing.table.DefaultTableCellRenderer;
 import javax.swing.table.TableModel;
 import java.awt.*;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.awt.event.WindowAdapter;
-import java.awt.event.WindowEvent;
+import java.awt.event.*;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -63,7 +61,7 @@
 import java.util.Map;
 import java.util.Set;
 
-import static org.embl.mobie.viewer.Utils.*;
+import static org.embl.mobie.viewer.MoBIEUtils.*;
 import static de.embl.cba.tables.color.CategoryTableRowColumnColoringModel.DARK_GREY;
 
 public class TableViewer< T extends TableRow > implements SelectionListener< T >, ColoringListener, TableRowListener
@@ -128,6 +126,16 @@ public TableViewer(
 
 		// TODO: reconsider
 		registerAsTableRowListener( tableRows );
+
+		configureJTable();
+
+		if ( selectionModel != null )
+			installSelectionModelNotification();
+
+		if ( coloringModel != null)
+			configureTableRowColoring();
+
+		createFrame();
 	}
 
 	public TableViewer< T > show()
@@ -140,7 +148,7 @@ public TableViewer< T > show()
 		if ( coloringModel != null)
 			configureTableRowColoring();
 
-		createAndShowMenu();
+		createFrame();
 
 		return this;
 	}
@@ -415,7 +423,7 @@ public void addAdditionalTable(String tablePath) {
 	}
 
 	private Map< String, List< String > > openTable( String tablePath ) {
-		String resolvedPath = org.embl.mobie.viewer.Utils.resolveTablePath( tablePath );
+		String resolvedPath = MoBIEUtils.resolveTablePath( tablePath );
 		Logger.info( "Opening table:\n" + resolvedPath );
 		return TableColumns.stringColumnsFromTableFile( resolvedPath );
 	}
@@ -448,7 +456,7 @@ private void loadColumnsFromProject()
 
 	private void loadColumnsFromFileSystem()
 	{
-		String path = selectOpenPathFromFileSystem( "Table" );
+		String path = selectFilePath( null, "Table", true );
 
 		if ( path != null ) {
 			new Thread( () -> {
@@ -483,25 +491,24 @@ private JMenuItem createLoadColumnsMenuItem()
 	{
 		final JMenuItem menuItem = new JMenuItem( "Load Columns..." );
 		menuItem.addActionListener( e ->
-				SwingUtilities.invokeLater( () ->
-				{
+				new Thread( () -> {
 					FileLocation fileLocation;
-					if ( sourceNameToTableDir.size() > 1 ) {
+					if (sourceNameToTableDir.size() > 1) {
 						// For multi-source tables, we only allow loading from the project
 						fileLocation = FileLocation.Project;
 					} else {
 						fileLocation = loadFromProjectOrFileSystemDialog();
+						if (fileLocation == null)
+							return;
 					}
 
-					if ( fileLocation == FileLocation.Project )
-					{
+
+					if (fileLocation == FileLocation.Project) {
 						loadColumnsFromProject();
-					}
-					else
-					{
+					} else {
 						loadColumnsFromFileSystem();
 					}
-				} ) );
+				}).start() );
 
 		return menuItem;
 	}
@@ -752,7 +759,7 @@ public void continueAnnotation( String columnName )
 		annotator.showDialog();
 	}
 
-	private void createAndShowMenu()
+	private void createFrame()
 	{
 		final JPanel panel = new JPanel( new GridLayout( 1, 0 ) );
 		JScrollPane scrollPane = new JScrollPane(
@@ -780,8 +787,11 @@ public void windowClosing( WindowEvent ev) {
 				frame.setVisible( false );
 			}
 		});
+	}
 
-		SwingUtilities.invokeLater( () -> frame.setVisible( true ) );
+	public void setVisible( boolean visible )
+	{
+		SwingUtilities.invokeLater( () -> frame.setVisible( visible ) );
 	}
 
 	public void addColumn( String column, Object defaultValue )
diff --git a/src/main/java/org/embl/mobie/viewer/transform/AffineSourceTransformer.java b/src/main/java/org/embl/mobie/viewer/transform/AffineSourceTransformer.java
index ffca86f68..bbbf09851 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/AffineSourceTransformer.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/AffineSourceTransformer.java
@@ -1,9 +1,12 @@
 package org.embl.mobie.viewer.transform;
 
+import bdv.tools.transformation.TransformedSource;
+import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
 import org.embl.mobie.viewer.playground.SourceAffineTransformer;
 import net.imglib2.realtransform.AffineTransform3D;
 
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 
@@ -18,9 +21,25 @@ public class AffineSourceTransformer extends AbstractSourceTransformer
 	private transient AffineTransform3D affineTransform3D;
 
 	public AffineSourceTransformer( String name, double[] parameters, List< String > sources ) {
+		this( name, parameters, sources, null );
+	}
+
+	public AffineSourceTransformer( String name, double[] parameters, List< String > sources, List< String > sourceNamesAfterTransform )
+	{
 		this.name = name;
 		this.parameters = parameters;
 		this.sources = sources;
+		this.sourceNamesAfterTransform = sourceNamesAfterTransform;
+	}
+
+	public AffineSourceTransformer( TransformedSource< ? > transformedSource )
+	{
+		AffineTransform3D fixedTransform = new AffineTransform3D();
+		transformedSource.getFixedTransform( fixedTransform );
+		name = "manualTransform";
+		parameters = fixedTransform.getRowPackedCopy();
+		sources	= Arrays.asList( transformedSource.getWrappedSource().getName() );
+		sourceNamesAfterTransform =	Arrays.asList( transformedSource.getName() );
 	}
 
 	@Override
diff --git a/src/main/java/org/embl/mobie/viewer/transform/MergedGridSource.java b/src/main/java/org/embl/mobie/viewer/transform/MergedGridSource.java
index 27b5ac2f9..d140cbc7c 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/MergedGridSource.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/MergedGridSource.java
@@ -1,10 +1,10 @@
 package org.embl.mobie.viewer.transform;
 
+import bdv.tools.transformation.TransformedSource;
 import bdv.util.DefaultInterpolators;
 import bdv.viewer.Interpolation;
 import bdv.viewer.Source;
-import net.imglib2.util.LinAlgHelpers;
-import org.embl.mobie.viewer.Utils;
+import bdv.viewer.SourceAndConverter;
 import mpicbg.spim.data.sequence.VoxelDimensions;
 import net.imglib2.Cursor;
 import net.imglib2.FinalInterval;
@@ -20,8 +20,12 @@
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.type.NativeType;
 import net.imglib2.type.numeric.NumericType;
+import net.imglib2.type.numeric.integer.UnsignedIntType;
 import net.imglib2.util.Util;
 import net.imglib2.view.Views;
+import org.embl.mobie.viewer.MoBIEUtils;
+import org.embl.mobie.viewer.SourceNameEncoder;
+import org.embl.mobie.viewer.source.LabelSource;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -29,6 +33,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
+import java.util.Set;
 
 public class MergedGridSource< T extends NativeType< T > & NumericType< T > > implements Source< T >
 {
@@ -40,16 +45,19 @@ public class MergedGridSource< T extends NativeType< T > & NumericType< T > > im
 	private final List< Source< T > > gridSources;
 	private final List< int[] > positions;
 	private final double relativeCellMargin;
+	private final boolean encodeSource;
 	private int currentTimepoint = 0;
 	private Map< String, long[] > sourceNameToVoxelTranslation;
 	private int[][] cellDimensions;
 	private double[] cellRealDimensions;
+	private Set< SourceAndConverter > containedSourceAndConverters;
 
-	public MergedGridSource( List< Source< T > > gridSources, List< int[] > positions, String mergedGridSourceName, double relativeCellMargin )
+	public MergedGridSource( List< Source< T > > gridSources, List< int[] > positions, String mergedGridSourceName, double relativeCellMargin, boolean encodeSource )
 	{
 		this.gridSources = gridSources;
 		this.positions = positions;
 		this.relativeCellMargin = relativeCellMargin;
+		this.encodeSource = encodeSource;
 		this.interpolators = new DefaultInterpolators<>();
 		this.referenceSource = gridSources.get( 0 );
 		this.mergedGridSourceName = mergedGridSourceName;
@@ -58,6 +66,21 @@ public MergedGridSource( List< Source< T > > gridSources, List< int[] > position
 		mergedRandomAccessibleIntervals = createMergedRandomAccessibleIntervals();
 	}
 
+	public static boolean instanceOf( SourceAndConverter< ? > sourceAndConverter )
+	{
+		final Source< ? > source = sourceAndConverter.getSpimSource();
+		return instanceOf( source );
+	}
+
+	public static boolean instanceOf( Source< ? > source )
+	{
+		if ( source instanceof LabelSource )  source = ( ( LabelSource ) source ).getWrappedSource();
+
+		final Source wrappedSource = ( ( TransformedSource ) source ).getWrappedSource();
+
+		return wrappedSource instanceof MergedGridSource || source instanceof MergedGridSource;
+	}
+
 	public List< Source< T > > getGridSources()
 	{
 		return gridSources;
@@ -93,6 +116,7 @@ private List< RandomAccessibleInterval< T > > createMergedRandomAccessibleInterv
 						cellLoader,
 						ReadOnlyCachedCellImgOptions.options().cellDimensions( cellDimensions[ level ] ) );
 
+
 			mergedRandomAccessibleIntervals.add( cachedCellImg );
 		}
 
@@ -101,7 +125,7 @@ private List< RandomAccessibleInterval< T > > createMergedRandomAccessibleInterv
 
 	private FinalInterval createInterval()
 	{
-		final long[] max = Utils.asLongs( cellDimensions[ 0 ] );
+		final long[] max = MoBIEUtils.asLongs( cellDimensions[ 0 ] );
 		for ( int d = 0; d < max.length; d++ )
 			max[ d ] -= 1;
 		final long[] min = new long[ 3 ];
@@ -155,7 +179,7 @@ private int[][] computeCellDimensions( int numMipmapLevels )
 		// positions.
 		final RandomAccessibleInterval< T > source = referenceSource.getSource( 0, 0 );
 		final long[] referenceSourceDimensions = source.dimensionsAsLongArray();
-		cellDimensions[ 0 ] = Utils.asInts( referenceSourceDimensions );
+		cellDimensions[ 0 ] = MoBIEUtils.asInts( referenceSourceDimensions );
 		for ( int d = 0; d < 2; d++ )
 		{
 			cellDimensions[ 0 ][ d ] *= ( 1 + 2.0 * relativeCellMargin );
@@ -242,6 +266,7 @@ private long[] computeTranslation( int[] cellDimensions, long[] cellMin, long[]
 		return translation;
 	}
 
+	// TODO: not used
 	public Map< String, long[] > getSourceNameToVoxelTranslation()
 	{
 		return sourceNameToVoxelTranslation;
@@ -310,6 +335,16 @@ public int getNumMipmapLevels()
 		return referenceSource.getNumMipmapLevels();
 	}
 
+	public void setContainedSourceAndConverters( Set< SourceAndConverter > containedSourceAndConverters )
+	{
+		this.containedSourceAndConverters = containedSourceAndConverters;
+	}
+
+	public Set< SourceAndConverter > getContainedSourceAndConverters()
+	{
+		return containedSourceAndConverters;
+	}
+
 	class RandomAccessibleIntervalCellLoader< T extends NativeType< T > > implements CellLoader< T >
 	{
 		private final Map< String, Source< T > > cellKeyToSource;
@@ -337,18 +372,31 @@ public void load( SingleCellArrayImg< T, ? > cell ) throws Exception
 				RandomAccessibleInterval< T > data = source.getSource( currentTimepoint, level );
 
 				// Create a view that is shifted to the cell position
-				final long[] offset = computeTranslation( Utils.asInts( cell.dimensionsAsLongArray() ), cell.minAsLongArray(), data.dimensionsAsLongArray() );
+				final long[] offset = computeTranslation( MoBIEUtils.asInts( cell.dimensionsAsLongArray() ), cell.minAsLongArray(), data.dimensionsAsLongArray() );
 				data = Views.translate( Views.zeroMin( data ), offset );
 
-
 				// copy RAI into cell
 				Cursor< T > sourceCursor = Views.iterable( data ).cursor();
 				RandomAccess< T > targetAccess = cell.randomAccess();
 
-				while ( sourceCursor.hasNext() )
+				if ( encodeSource )
+				{
+					final String name = source.getName();
+					while ( sourceCursor.hasNext() )
+					{
+						sourceCursor.fwd();
+						// copy the sourceCursor in order not to modify it by the source name encoding
+						targetAccess.setPositionAndGet( sourceCursor ).set( sourceCursor.get().copy() );
+						SourceNameEncoder.encodeName( ( UnsignedIntType ) targetAccess.get(), name );
+					}
+				}
+				else
 				{
-					sourceCursor.fwd();
-					targetAccess.setPositionAndGet( sourceCursor ).set( sourceCursor.get() );
+					while ( sourceCursor.hasNext() )
+					{
+						sourceCursor.fwd();
+						targetAccess.setPositionAndGet( sourceCursor ).set( sourceCursor.get() );
+					}
 				}
 			}
 		}
diff --git a/src/main/java/org/embl/mobie/viewer/transform/MergedGridSourceTransformer.java b/src/main/java/org/embl/mobie/viewer/transform/MergedGridSourceTransformer.java
index 35239dc2d..f67b6bef5 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/MergedGridSourceTransformer.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/MergedGridSourceTransformer.java
@@ -1,20 +1,24 @@
 package org.embl.mobie.viewer.transform;
 
+import bdv.tools.transformation.TransformedSource;
 import bdv.util.VolatileSource;
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
 import de.embl.cba.tables.Logger;
-import org.embl.mobie.viewer.MoBIE;
-import org.embl.mobie.viewer.Utils;
+import org.embl.mobie.viewer.MoBIEUtils;
 import net.imglib2.FinalRealInterval;
 import net.imglib2.converter.Converter;
 import net.imglib2.type.numeric.ARGBType;
+import org.embl.mobie.viewer.ThreadUtils;
 
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 import java.util.stream.Collectors;
 
 public class MergedGridSourceTransformer extends AbstractSourceTransformer
@@ -24,10 +28,12 @@ public class MergedGridSourceTransformer extends AbstractSourceTransformer
 	protected String mergedGridSourceName;
 	protected List< int[] > positions;
 	protected boolean centerAtOrigin = false;
+	protected boolean encodeSource = false;
 
 	// Runtime
 	private transient MergedGridSource< ? > mergedGridSource;
 	private transient double[] translationRealOffset;
+	private transient Set< SourceAndConverter > transformedSourceAndConverters;
 
 	@Override
 	public void transform( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter )
@@ -43,51 +49,42 @@ public void transform( Map< String, SourceAndConverter< ? > > sourceNameToSource
 
 		sourceNameToSourceAndConverter.put( mergedSourceAndConverter.getSpimSource().getName(), mergedSourceAndConverter );
 
-		// needed to know where the individual sources are in space:
-		transformGridSourcesIndividually( sourceNameToSourceAndConverter, gridSources );
+		// Transform (i.e. adapt the positions) all contained sources,
+		// because several parts of the code refer to them and their
+		// positions.
+		transformedSourceAndConverters = ConcurrentHashMap.newKeySet();
+		transformContainedSources( sourceNameToSourceAndConverter, gridSources );
+		mergedGridSource.setContainedSourceAndConverters( transformedSourceAndConverters );
 	}
 
-	private void transformGridSourcesIndividually( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, List< SourceAndConverter< ? > > gridSources )
+	private void transformContainedSources( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, List< SourceAndConverter< ? > > gridSources )
 	{
-		final long start = System.currentTimeMillis();
-
 		final ArrayList< SourceAndConverter< ? > > referenceSources = new ArrayList<>();
 		referenceSources.add( gridSources.get( 0 ) );
 
-		// final double[] gridCellRealDimensions = TransformedGridSourceTransformer.computeGridCellRealDimensions( referenceSources, TransformedGridSourceTransformer.RELATIVE_CELL_MARGIN );
-
 		final double[] gridCellRealDimensions = mergedGridSource.getCellRealDimensions();
 
-		// due to margin...
+		// account for grid margin
 		translationRealOffset = computeTranslationOffset( gridSources, gridCellRealDimensions );
 
-		final int nThreads = MoBIE.N_THREADS;
-		final ExecutorService executorService = Executors.newFixedThreadPool( nThreads );
-
 		final int numSources = gridSources.size();
+		final ArrayList< Future< ? > > futures = ThreadUtils.getFutures();
 		for ( int positionIndex = 0; positionIndex < numSources; positionIndex++ )
 		{
 			final int finalPositionIndex = positionIndex;
 
 			final ArrayList< String > sourceNamesAtGridPosition = getSourcesAtGridPosition( gridSources, finalPositionIndex );
 
-			// translate the source(s) at this grid position
-			// (in fact, here it can only be one source per grid position)
-			executorService.execute( () -> {
-				TransformedGridSourceTransformer.translate( sourceNameToSourceAndConverter, sourceNamesAtGridPosition, null, centerAtOrigin, gridCellRealDimensions[ 0 ] * positions.get( finalPositionIndex )[ 0 ] + translationRealOffset[ 0 ], gridCellRealDimensions[ 1 ] * positions.get( finalPositionIndex )[ 1 ] + translationRealOffset[ 1 ]);
-			} );
-
-			translateSourcesWithinMergedSources( sourceNameToSourceAndConverter, gridCellRealDimensions, executorService, finalPositionIndex, sourceNamesAtGridPosition );
-
+			futures.add( ThreadUtils.executorService.submit( () -> {
+				recursivelyTransformSources( sourceNameToSourceAndConverter, gridCellRealDimensions, finalPositionIndex, sourceNamesAtGridPosition );
+			} ) );
 		}
-		Utils.waitUntilFinishedAndShutDown( executorService );
-
-		// System.out.println( "Transformed " + sourceNameToSourceAndConverter.size() + " image source(s) in " + (System.currentTimeMillis() - start) + " ms, using " + nThreads + " thread(s)." );
+		ThreadUtils.waitUntilFinished( futures );
 	}
 
 	private double[] computeTranslationOffset( List< SourceAndConverter< ? > > gridSources, double[] gridCellRealDimensions )
 	{
-		final FinalRealInterval dataRealBounds = Utils.estimateBounds( gridSources.get( 0 ).getSpimSource() );
+		final FinalRealInterval dataRealBounds = MoBIEUtils.estimateBounds( gridSources.get( 0 ).getSpimSource() );
 
 		final double[] dataRealDimensions = new double[ 3 ];
 		for ( int d = 0; d < 3; d++ )
@@ -100,25 +97,46 @@ private double[] computeTranslationOffset( List< SourceAndConverter< ? > > gridS
 		return translationOffset;
 	}
 
-	private void translateSourcesWithinMergedSources( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, double[] gridCellRealDimensions, ExecutorService executorService, int finalPositionIndex, ArrayList< String > sourceNamesAtGridPosition )
+	private void recursivelyTransformSources( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, double[] gridCellRealDimensions, int finalPositionIndex, ArrayList< String > transformedSourceNames )
 	{
-		final ArrayList< String > baseSourceNames = new ArrayList<>();
-		for ( String sourceName : sourceNamesAtGridPosition )
-		{
-			final Source< ? > spimSource = sourceNameToSourceAndConverter.get( sourceName ).getSpimSource();
-			if ( spimSource instanceof MergedGridSource )
-			{
-				baseSourceNames.addAll( ( ( MergedGridSource< ? > ) spimSource ).getGridSources().stream().map( s -> s.getName() ).collect( Collectors.toList() ) ) ;
+		// transform the sources
+		final double translationX = gridCellRealDimensions[ 0 ] * positions.get( finalPositionIndex )[ 0 ] + translationRealOffset[ 0 ];
+		final double translationY = gridCellRealDimensions[ 1 ] * positions.get( finalPositionIndex )[ 1 ] + translationRealOffset[ 1 ];
 
-			}
+		TransformedGridSourceTransformer.translate( sourceNameToSourceAndConverter, transformedSourceNames, null, centerAtOrigin, translationX, translationY );
+		addTransformedSources( sourceNameToSourceAndConverter, transformedSourceNames );
+
+		// if there are any, also transform contained sources
+		final ArrayList< String > containedSourceNames = fetchContainedSourceNames( sourceNameToSourceAndConverter, transformedSourceNames );
+		if ( containedSourceNames.size() > 0 )
+		{
+			recursivelyTransformSources( sourceNameToSourceAndConverter, gridCellRealDimensions, finalPositionIndex, containedSourceNames );
 		}
+	}
 
-		if ( baseSourceNames.size() > 0 )
+	private void addTransformedSources( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, ArrayList< String > containedSourceNames )
+	{
+		final List< SourceAndConverter< ? > > sourceAndConverters = sourceNameToSourceAndConverter.values().stream().filter( sac -> containedSourceNames.contains( sac.getSpimSource().getName() ) ).collect( Collectors.toList() );
+		transformedSourceAndConverters.addAll( sourceAndConverters );
+	}
+
+	private ArrayList< String > fetchContainedSourceNames( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, ArrayList< String > sourceNames )
+	{
+		final ArrayList< String > containedSourceNames = new ArrayList<>();
+		for ( String sourceName : sourceNames )
 		{
-			executorService.execute( () -> {
-				TransformedGridSourceTransformer.translate( sourceNameToSourceAndConverter, baseSourceNames, null, centerAtOrigin, gridCellRealDimensions[ 0 ] * positions.get( finalPositionIndex )[ 0 ] + translationRealOffset[ 0 ], gridCellRealDimensions[ 1 ] * positions.get( finalPositionIndex )[ 1 ] + translationRealOffset[ 1 ]);
-			} );
+			Source< ? > source = sourceNameToSourceAndConverter.get( sourceName ).getSpimSource();
+			if ( source instanceof TransformedSource )
+			{
+				source = ( ( TransformedSource< ? > ) source ).getWrappedSource();
+			}
+
+			if ( source instanceof MergedGridSource )
+			{
+				containedSourceNames.addAll( ( ( MergedGridSource< ? > ) source ).getGridSources().stream().map( s -> s.getName() ).collect( Collectors.toList() ) ) ;
+			}
 		}
+		return containedSourceNames;
 	}
 
 	private ArrayList< String > getSourcesAtGridPosition( List< SourceAndConverter< ? > > gridSources, int finalPositionIndex )
@@ -136,9 +154,9 @@ public List< String > getSources()
 
 	private SourceAndConverter< ? > createMergedSourceAndConverter( List< Source< ? > > gridSources, Converter< ?, ARGBType > volatileConverter, Converter< ?, ARGBType > converter )
 	{
-		mergedGridSource = new MergedGridSource( gridSources, positions, mergedGridSourceName, TransformedGridSourceTransformer.RELATIVE_CELL_MARGIN );
+		mergedGridSource = new MergedGridSource( gridSources, positions, mergedGridSourceName, TransformedGridSourceTransformer.RELATIVE_CELL_MARGIN, encodeSource );
 
-		final VolatileSource< ?, ? > volatileMergedGridSource = new VolatileSource<>( mergedGridSource, MoBIE.sharedQueue );
+		final VolatileSource< ?, ? > volatileMergedGridSource = new VolatileSource<>( mergedGridSource, ThreadUtils.sharedQueue );
 
 		final SourceAndConverter< ? > volatileSourceAndConverter = new SourceAndConverter( volatileMergedGridSource, volatileConverter );
 
diff --git a/src/main/java/org/embl/mobie/viewer/transform/MoBIEViewerTransformChanger.java b/src/main/java/org/embl/mobie/viewer/transform/MoBIEViewerTransformChanger.java
index 00bff3e08..a16693310 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/MoBIEViewerTransformChanger.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/MoBIEViewerTransformChanger.java
@@ -2,9 +2,9 @@
 
 import bdv.util.*;
 import bdv.viewer.animate.SimilarityTransformAnimator;
-import org.embl.mobie.viewer.playground.PlaygroundUtils;
-import org.embl.mobie.viewer.bdv.BdvPointOverlay;
-import org.embl.mobie.viewer.Utils;
+import org.embl.mobie.viewer.playground.BdvPlaygroundUtils;
+import org.embl.mobie.viewer.bdv.BdvCircleOverlay;
+import org.embl.mobie.viewer.MoBIEUtils;
 import net.imglib2.realtransform.AffineTransform3D;
 
 import java.util.Arrays;
@@ -14,7 +14,7 @@ public abstract class MoBIEViewerTransformChanger
 	public static int animationDurationMillis = 3000;
 
 	private static BdvOverlaySource< BdvOverlay > pointOverlaySource;
-	private static BdvPointOverlay bdvPointOverlay;
+	private static BdvCircleOverlay bdvCircleOverlay;
 	private static boolean pointOverlaySourceIsActive;
 	private static boolean isPointOverlayEnabled;
 
@@ -31,17 +31,20 @@ else if ( viewerTransform instanceof TimepointViewerTransform )
 		{
 			adaptTimepoint( bdv, viewerTransform );
 		}
+		else if ( viewerTransform instanceof NormalVectorViewerTransform )
+		{
+			final AffineTransform3D transform = NormalVectorViewerTransform.createTransform( bdv, viewerTransform.getParameters() );
+			changeViewerTransform( bdv, transform, animationDurationMillis );
+			adaptTimepoint( bdv, viewerTransform );
+		}
 		else if ( viewerTransform instanceof AffineViewerTransform )
 		{
-			// TODO: changing time point and viewer transform at the same time
-			//  used to sometimes lead to hickups; check whether this is the case and think about a solution...
-			//  e.g., does changeBdvViewerTransform return already while the transformation is still going on?
-			changeViewerTransform( bdv, Utils.asAffineTransform3D( viewerTransform.getParameters() ), animationDurationMillis );
+			changeViewerTransform( bdv, MoBIEUtils.asAffineTransform3D( viewerTransform.getParameters() ), animationDurationMillis );
 			adaptTimepoint( bdv, viewerTransform );
 		}
 		else if ( viewerTransform instanceof NormalizedAffineViewerTransform )
 		{
-			final AffineTransform3D transform = Utils.createUnnormalizedViewerTransform( Utils.asAffineTransform3D( viewerTransform.getParameters() ), bdv );
+			final AffineTransform3D transform = TransformHelper.createUnnormalizedViewerTransform( MoBIEUtils.asAffineTransform3D( viewerTransform.getParameters() ), bdv.getBdvHandle().getViewerPanel() );
 			changeViewerTransform( bdv, transform, animationDurationMillis );
 			adaptTimepoint( bdv, viewerTransform );
 		}
@@ -63,18 +66,18 @@ public static void togglePointOverlay()
 
 	private static void addPointOverlay( Bdv bdv, double[] doubles )
 	{
-		if ( bdvPointOverlay == null )
+		if ( bdvCircleOverlay == null )
 		{
-			bdvPointOverlay = new BdvPointOverlay( doubles, 5.0 );
+			bdvCircleOverlay = new BdvCircleOverlay( doubles, 5.0 );
 			pointOverlaySource = BdvFunctions.showOverlay(
-					bdvPointOverlay,
+					bdvCircleOverlay,
 					"point-overlay-" + Arrays.toString( doubles ),
 					BdvOptions.options().addTo( bdv ) );
 			pointOverlaySourceIsActive = true;
 		}
 		else
 		{
-			bdvPointOverlay.addPoint( doubles );
+			bdvCircleOverlay.addCircle( doubles );
 		}
 	}
 
@@ -103,7 +106,7 @@ public static void moveToPosition( BdvHandle bdv, double[] xyz, long durationMil
 		}
 
 		newViewerTransform.translate( locationOfTargetCoordinatesInCurrentViewer );
-		final double[] bdvWindowCenter = PlaygroundUtils.getWindowCentreInPixelUnits( bdv );
+		final double[] bdvWindowCenter = BdvPlaygroundUtils.getWindowCentreInPixelUnits( bdv.getViewerPanel() );
 		newViewerTransform.translate( bdvWindowCenter );
 
 		if ( durationMillis <= 0 )
diff --git a/src/main/java/org/embl/mobie/viewer/transform/NormalVectorViewerTransform.java b/src/main/java/org/embl/mobie/viewer/transform/NormalVectorViewerTransform.java
new file mode 100644
index 000000000..3600c6493
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/transform/NormalVectorViewerTransform.java
@@ -0,0 +1,83 @@
+package org.embl.mobie.viewer.transform;
+
+import bdv.util.Affine3DHelpers;
+import bdv.util.Bdv;
+import de.embl.cba.bdv.utils.BdvUtils;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.util.LinAlgHelpers;
+
+import java.util.stream.DoubleStream;
+
+import static de.embl.cba.bdv.utils.BdvUtils.changeBdvViewerTransform;
+import static de.embl.cba.bdv.utils.BdvUtils.getBdvWindowCenter;
+import static de.embl.cba.bdv.utils.BdvUtils.quaternionToAffineTransform3D;
+
+public class NormalVectorViewerTransform implements ViewerTransform
+{
+	// Serialisation
+	private double[] normalVector; // required
+	private final Integer timepoint; // optional
+
+	public NormalVectorViewerTransform( double[] normalVector, int timepoint )
+	{
+		this.normalVector = normalVector;
+		this.timepoint = timepoint;
+	}
+
+	@Override
+	public double[] getParameters()
+	{
+		return normalVector;
+	}
+
+	@Override
+	public Integer getTimepoint()
+	{
+		return timepoint;
+	}
+
+	public static AffineTransform3D createTransform( Bdv bdv, double[] targetNormalVector )
+	{
+		double[] currentNormalVector = BdvUtils.getCurrentViewNormalVector( bdv );
+
+		AffineTransform3D currentViewerTransform = new AffineTransform3D();
+		bdv.getBdvHandle().getViewerPanel().state().getViewerTransform( currentViewerTransform );
+
+		LinAlgHelpers.normalize( targetNormalVector ); // just to be sure.
+
+		// determine rotation axis
+		double[] rotationAxis = new double[ 3 ];
+		LinAlgHelpers.cross( currentNormalVector, targetNormalVector, rotationAxis );
+		if ( LinAlgHelpers.length( rotationAxis ) > 0 ) LinAlgHelpers.normalize( rotationAxis );
+
+		// The rotation axis is in the coordinate system of the original data set => transform to viewer coordinate system
+		double[] qCurrentRotation = new double[ 4 ];
+		Affine3DHelpers.extractRotation( currentViewerTransform, qCurrentRotation );
+		final AffineTransform3D currentRotation = quaternionToAffineTransform3D( qCurrentRotation );
+
+		double[] rotationAxisInViewerSystem = new double[ 3 ];
+		currentRotation.apply( rotationAxis, rotationAxisInViewerSystem );
+
+		// determine rotation angle
+		double angle = - Math.acos( LinAlgHelpers.dot( currentNormalVector, targetNormalVector ) );
+
+		// construct rotation of angle around axis
+		double[] rotationQuaternion = new double[ 4 ];
+		LinAlgHelpers.quaternionFromAngleAxis( rotationAxisInViewerSystem, angle, rotationQuaternion );
+		final AffineTransform3D rotation = quaternionToAffineTransform3D( rotationQuaternion );
+
+		// apply transformation (rotating around current viewer centre position)
+		final AffineTransform3D translateCenterToOrigin = new AffineTransform3D();
+		translateCenterToOrigin.translate( DoubleStream.of( getBdvWindowCenter( bdv )).map( x -> -x ).toArray() );
+
+		final AffineTransform3D translateCenterBack = new AffineTransform3D();
+		translateCenterBack.translate( getBdvWindowCenter( bdv ) );
+
+		final AffineTransform3D affineTransform3D = currentViewerTransform.copy()
+				.preConcatenate( translateCenterToOrigin )
+				.preConcatenate( rotation )
+				.preConcatenate( translateCenterBack );
+
+		return affineTransform3D;
+	}
+}
diff --git a/src/main/java/org/embl/mobie/viewer/transform/NormalizedAffineViewerTransform.java b/src/main/java/org/embl/mobie/viewer/transform/NormalizedAffineViewerTransform.java
index f0313a88c..5e255a8cc 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/NormalizedAffineViewerTransform.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/NormalizedAffineViewerTransform.java
@@ -2,6 +2,7 @@
 
 public class NormalizedAffineViewerTransform implements ViewerTransform
 {
+	// Serialization
 	private double[] normalizedAffine;
 	private Integer timepoint;
 
diff --git a/src/main/java/org/embl/mobie/viewer/transform/PositionViewerTransform.java b/src/main/java/org/embl/mobie/viewer/transform/PositionViewerTransform.java
index 3b47a1b4d..14e612417 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/PositionViewerTransform.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/PositionViewerTransform.java
@@ -2,6 +2,7 @@
 
 public class PositionViewerTransform implements ViewerTransform
 {
+	// Serialization
 	private double[] position;
 	private Integer timepoint;
 
diff --git a/src/main/java/org/embl/mobie/viewer/transform/TimepointViewerTransform.java b/src/main/java/org/embl/mobie/viewer/transform/TimepointViewerTransform.java
index f1e83d61a..3735ca438 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/TimepointViewerTransform.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/TimepointViewerTransform.java
@@ -2,6 +2,7 @@
 
 public class TimepointViewerTransform implements ViewerTransform
 {
+	// Serialization
 	private Integer timepoint;
 
 	public TimepointViewerTransform( int timepoint )
diff --git a/src/main/java/org/embl/mobie/viewer/transform/TransformHelper.java b/src/main/java/org/embl/mobie/viewer/transform/TransformHelper.java
index 89ed34680..d8184cd6b 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/TransformHelper.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/TransformHelper.java
@@ -2,7 +2,10 @@
 
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
-import org.embl.mobie.viewer.Utils;
+import bdv.viewer.ViewerPanel;
+import net.imglib2.realtransform.Scale3D;
+import org.embl.mobie.viewer.MoBIEUtils;
+import org.embl.mobie.viewer.playground.BdvPlaygroundUtils;
 import org.embl.mobie.viewer.playground.SourceAffineTransformer;
 import net.imglib2.FinalRealInterval;
 import net.imglib2.RealInterval;
@@ -22,7 +25,7 @@ public static RealInterval unionRealInterval( List< ? extends Source< ? > > sour
 
 		for ( Source< ? > source : sources )
 		{
-			final FinalRealInterval bounds = Utils.estimateBounds( source );
+			final FinalRealInterval bounds = MoBIEUtils.estimateBounds( source );
 
 			if ( union == null )
 				union = bounds;
@@ -47,7 +50,7 @@ public static RealInterval unionRealInterval( List< ? extends Source< ? > > sour
 
 	public static double[] getCenter( SourceAndConverter< ? > sourceAndConverter )
 	{
-		final FinalRealInterval bounds = Utils.estimateBounds( sourceAndConverter.getSpimSource() );
+		final FinalRealInterval bounds = MoBIEUtils.estimateBounds( sourceAndConverter.getSpimSource() );
 		final double[] center = bounds.minAsDoubleArray();
 		final double[] max = bounds.maxAsDoubleArray();
 		for ( int d = 0; d < max.length; d++ )
@@ -92,4 +95,43 @@ public static double[] getMaximalSourceUnionRealDimensions( Map< String, SourceA
 
 		return maximalDimensions;
 	}
+
+	public static AffineTransform3D createNormalisedViewerTransform( ViewerPanel viewerPanel )
+	{
+		return createNormalisedViewerTransform( viewerPanel, BdvPlaygroundUtils.getWindowCentreInPixelUnits( viewerPanel ) );
+	}
+
+	public static AffineTransform3D createNormalisedViewerTransform( ViewerPanel viewerPanel, double[] position )
+	{
+		final AffineTransform3D view = new AffineTransform3D();
+		viewerPanel.state().getViewerTransform( view );
+
+		// translate position to upper left corner of the Window (0,0)
+		final AffineTransform3D translate = new AffineTransform3D();
+		translate.translate( position );
+		view.preConcatenate( translate.inverse() );
+
+		// divide by window width
+		final int bdvWindowWidth = viewerPanel.getDisplay().getWidth();;
+		final Scale3D scale = new Scale3D( 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth );
+		view.preConcatenate( scale );
+
+		return view;
+	}
+
+	public static AffineTransform3D createUnnormalizedViewerTransform( AffineTransform3D normalisedTransform, ViewerPanel viewerPanel )
+	{
+		final AffineTransform3D transform = normalisedTransform.copy();
+
+		final int bdvWindowWidth = viewerPanel.getDisplay().getWidth();
+		final Scale3D scale = new Scale3D( 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth );
+		transform.preConcatenate( scale.inverse() );
+
+		AffineTransform3D translate = new AffineTransform3D();
+		translate.translate( BdvPlaygroundUtils.getWindowCentreInPixelUnits( viewerPanel ) );
+
+		transform.preConcatenate( translate );
+
+		return transform;
+	}
 }
diff --git a/src/main/java/org/embl/mobie/viewer/transform/TransformedGridSourceTransformer.java b/src/main/java/org/embl/mobie/viewer/transform/TransformedGridSourceTransformer.java
index 3446716d8..1eaf7bb38 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/TransformedGridSourceTransformer.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/TransformedGridSourceTransformer.java
@@ -2,18 +2,16 @@
 
 import bdv.viewer.SourceAndConverter;
 import de.embl.cba.tables.Logger;
-import org.embl.mobie.viewer.MoBIE;
-import org.embl.mobie.viewer.Utils;
+import org.embl.mobie.viewer.ThreadUtils;
 import org.embl.mobie.viewer.playground.SourceAffineTransformer;
 import net.imglib2.realtransform.AffineTransform3D;
-import org.apache.commons.lang.ArrayUtils;
 
 import java.util.ArrayList;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 
 public class TransformedGridSourceTransformer extends AbstractSourceTransformer
 {
@@ -49,27 +47,20 @@ public List< String > getSources()
 
 	private void transform( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, double[] cellRealDimensions )
 	{
-		final long start = System.currentTimeMillis();
-
-		final int nThreads = MoBIE.N_THREADS;
-		final ExecutorService executorService = Executors.newFixedThreadPool( nThreads );
-
 		final int numGridPositions = sources.size();
 
+		final ArrayList< Future< ? > > futures = ThreadUtils.getFutures();
 		for ( int gridIndex = 0; gridIndex < numGridPositions; gridIndex++ )
 		{
 			int finalGridIndex = gridIndex;
-			executorService.execute( () -> {
+			futures.add( ThreadUtils.executorService.submit( () -> {
 				if ( sourceNamesAfterTransform != null )
 					translate( sourceNameToSourceAndConverter, sources.get( finalGridIndex ), sourceNamesAfterTransform.get( finalGridIndex ), centerAtOrigin, cellRealDimensions[ 0 ] * positions.get( finalGridIndex )[ 0 ], cellRealDimensions[ 1 ] * positions.get( finalGridIndex )[ 1 ] );
 				else
 					translate( sourceNameToSourceAndConverter, sources.get( finalGridIndex ), null, centerAtOrigin, cellRealDimensions[ 0 ] * positions.get( finalGridIndex )[ 0 ], cellRealDimensions[ 1 ] * positions.get( finalGridIndex )[ 1 ] );
-			} );
+			} ) );
 		}
-
-		Utils.waitUntilFinishedAndShutDown( executorService );
-
-		System.out.println( "Transformed " + sourceNameToSourceAndConverter.size() + " image source(s) in " + (System.currentTimeMillis() - start) + " ms, using " + nThreads + " thread(s)." );
+		ThreadUtils.waitUntilFinished( futures );
 	}
 
 	public static void translate( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, List< String > sourceNames, List< String > sourceNamesAfterTransform, boolean centerAtOrigin, double translationX, double translationY )
diff --git a/src/main/java/org/embl/mobie/viewer/transform/ViewerTransform.java b/src/main/java/org/embl/mobie/viewer/transform/ViewerTransform.java
index 8981806ff..cfb75de57 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/ViewerTransform.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/ViewerTransform.java
@@ -4,5 +4,5 @@ public interface ViewerTransform
 {
 	double[] getParameters();
 
-	Integer getTimepoint();
+	Integer getTimepoint(); // May return NULL in the implementation
 }
diff --git a/src/main/java/org/embl/mobie/viewer/ui/SwingHelper.java b/src/main/java/org/embl/mobie/viewer/ui/SwingHelpers.java
similarity index 83%
rename from src/main/java/org/embl/mobie/viewer/ui/SwingHelper.java
rename to src/main/java/org/embl/mobie/viewer/ui/SwingHelpers.java
index b693b7702..7d3e79936 100644
--- a/src/main/java/org/embl/mobie/viewer/ui/SwingHelper.java
+++ b/src/main/java/org/embl/mobie/viewer/ui/SwingHelpers.java
@@ -6,26 +6,14 @@
 import javax.swing.*;
 import java.awt.*;
 
-public class SwingHelper
+public class SwingHelpers
 {
-
 	static { net.imagej.patcher.LegacyInjector.preinit(); }
 
 	public static final int TEXT_FIELD_HEIGHT = 20;
 	public static final int COMBOBOX_WIDTH = 270;
 	public static final Dimension BUTTON_DIMENSION = new Dimension( 80, TEXT_FIELD_HEIGHT );
 
-	public static JPanel horizontalLayoutPanel()
-	{
-		JPanel panel = new JPanel();
-		panel.setLayout( new BoxLayout( panel, BoxLayout.LINE_AXIS ) );
-		panel.setBorder( BorderFactory.createEmptyBorder(0, 10, 10, 10) );
-		panel.add( Box.createHorizontalGlue() );
-		panel.setAlignmentX( Component.LEFT_ALIGNMENT );
-
-		return panel;
-	}
-
 	public static JLabel getJLabel( String text )
 	{
 		return getJLabel( text, 170, 10);
diff --git a/src/main/java/org/embl/mobie/viewer/ui/UserInterface.java b/src/main/java/org/embl/mobie/viewer/ui/UserInterface.java
index 803e520b6..0afbf96d1 100644
--- a/src/main/java/org/embl/mobie/viewer/ui/UserInterface.java
+++ b/src/main/java/org/embl/mobie/viewer/ui/UserInterface.java
@@ -13,44 +13,40 @@
 import java.util.Map;
 import java.util.Set;
 
-import static org.embl.mobie.viewer.ui.UserInterfaceHelper.*;
-
 public class UserInterface
 {
 	private final JPanel displaySettingsContainer;
 	private final JScrollPane displaySettingsScrollPane;
 	private final JFrame frame;
 	private final JPanel selectionContainer;
-	private final UserInterfaceHelper userInterfaceHelper;
+	private final UserInterfaceHelpers userInterfaceHelpers;
 	private Map< Object, JPanel > displayToPanel;
 	private JSplitPane splitPane;
 
 	public UserInterface( MoBIE moBIE )
 	{
 		MoBIELookAndFeelToggler.setMoBIELaf();
-		userInterfaceHelper = new UserInterfaceHelper( moBIE );
-
-		selectionContainer = userInterfaceHelper.createSelectionPanel();
-		displaySettingsContainer = userInterfaceHelper.createDisplaySettingsContainer();
-		displaySettingsScrollPane = userInterfaceHelper.createDisplaySettingsScrollPane( displaySettingsContainer );
-		JPanel displaySettingsPanel = userInterfaceHelper.createDisplaySettingsPanel( displaySettingsScrollPane );
+		userInterfaceHelpers = new UserInterfaceHelpers( moBIE );
+		selectionContainer = userInterfaceHelpers.createSelectionPanel();
+		displaySettingsContainer = userInterfaceHelpers.createDisplaySettingsContainer();
+		displaySettingsScrollPane = userInterfaceHelpers.createDisplaySettingsScrollPane( displaySettingsContainer );
+		JPanel displaySettingsPanel = userInterfaceHelpers.createDisplaySettingsPanel( displaySettingsScrollPane );
 		displayToPanel = new HashMap<>();
-
 		frame = createAndShowFrame( selectionContainer, displaySettingsPanel, moBIE.getProjectName() + "-" + moBIE.getDatasetName() );
 		MoBIELookAndFeelToggler.resetMoBIELaf();
 	}
 
-	private JFrame createAndShowFrame( JPanel actionPanel, JPanel displaySettingsPanel, String panelName )
+	private JFrame createAndShowFrame( JPanel selectionPanel, JPanel displaySettingsPanel, String panelName )
 	{
 		JFrame frame = new JFrame( "MoBIE: " + panelName );
 
 		splitPane = new JSplitPane();
 		splitPane.setOrientation( JSplitPane.VERTICAL_SPLIT );
-		final int actionPanelHeight = userInterfaceHelper.getActionPanelHeight();
+		final int actionPanelHeight = userInterfaceHelpers.getActionPanelHeight();
 
 
 		splitPane.setDividerLocation( actionPanelHeight );
-		splitPane.setTopComponent( actionPanel );
+		splitPane.setTopComponent( selectionPanel );
 		splitPane.setBottomComponent( displaySettingsPanel );
 		splitPane.setAutoscrolls( true );
 
@@ -79,17 +75,23 @@ private void refreshSelection()
 		selectionContainer.revalidate();
 		selectionContainer.repaint();
 		// update the location of the splitpane divider, so any new uiSelectionGroups are visible
-		final int actionPanelHeight = userInterfaceHelper.getActionPanelHeight();
+		final int actionPanelHeight = userInterfaceHelpers.getActionPanelHeight();
 		splitPane.setDividerLocation( actionPanelHeight );
 		frame.revalidate();
 		frame.repaint();
 	}
 
-	public void addViews( Map<String, View> views ) {
-		userInterfaceHelper.addViewsToSelectionPanel(views);
+	public void addViews( Map<String, View> views )
+	{
+		userInterfaceHelpers.addViewsToSelectionPanel( views );
 		refreshSelection();
 	}
 
+	public Map< String, Map< String, View > > getGroupingsToViews()
+	{
+		return userInterfaceHelpers.getGroupingsToViews();
+	}
+
 	public void addSourceDisplay( SourceDisplay sourceDisplay )
 	{
 		final JPanel panel = createDisplaySettingPanel( sourceDisplay );
@@ -100,15 +102,15 @@ private JPanel createDisplaySettingPanel( SourceDisplay sourceDisplay )
 	{
 		if ( sourceDisplay instanceof ImageSourceDisplay)
 		{
-			return userInterfaceHelper.createImageDisplaySettingsPanel( ( ImageSourceDisplay ) sourceDisplay );
+			return userInterfaceHelpers.createImageDisplaySettingsPanel( ( ImageSourceDisplay ) sourceDisplay );
 		}
 		else if ( sourceDisplay instanceof SegmentationSourceDisplay)
 		{
-			return userInterfaceHelper.createSegmentationDisplaySettingsPanel( ( SegmentationSourceDisplay ) sourceDisplay );
+			return userInterfaceHelpers.createSegmentationDisplaySettingsPanel( ( SegmentationSourceDisplay ) sourceDisplay );
 		}
 		else if ( sourceDisplay instanceof AnnotatedIntervalDisplay)
 		{
-			return userInterfaceHelper.createAnnotatedIntervalDisplaySettingsPanel( ( AnnotatedIntervalDisplay ) sourceDisplay );
+			return userInterfaceHelpers.createAnnotatedIntervalDisplaySettingsPanel( ( AnnotatedIntervalDisplay ) sourceDisplay );
 		}
 		else
 		{
@@ -148,7 +150,7 @@ public Window getWindow()
 
 	public String[] getUISelectionGroupNames() {
 
-		Set<String> groupings = userInterfaceHelper.getGroupings();
+		Set<String> groupings = userInterfaceHelpers.getGroupings();
 		String[] groupNames = new String[groupings.size()];
 		int i = 0;
 		for ( String groupName: groupings ) {
diff --git a/src/main/java/org/embl/mobie/viewer/ui/UserInterfaceHelper.java b/src/main/java/org/embl/mobie/viewer/ui/UserInterfaceHelpers.java
similarity index 84%
rename from src/main/java/org/embl/mobie/viewer/ui/UserInterfaceHelper.java
rename to src/main/java/org/embl/mobie/viewer/ui/UserInterfaceHelpers.java
index 9ed113e23..db025557d 100644
--- a/src/main/java/org/embl/mobie/viewer/ui/UserInterfaceHelper.java
+++ b/src/main/java/org/embl/mobie/viewer/ui/UserInterfaceHelpers.java
@@ -5,7 +5,6 @@
 import bdv.util.BdvHandle;
 import bdv.util.BoundedValueDouble;
 import bdv.viewer.SourceAndConverter;
-import com.formdev.flatlaf.FlatLightLaf;
 import com.google.gson.Gson;
 import de.embl.cba.bdv.utils.BdvUtils;
 import de.embl.cba.bdv.utils.BrightnessUpdateListener;
@@ -27,7 +26,7 @@
 import net.imglib2.display.ColorConverter;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.type.numeric.ARGBType;
-import org.embl.mobie.viewer.Utils;
+import org.embl.mobie.viewer.MoBIEUtils;
 import sc.fiji.bdvpg.bdv.navigate.ViewerTransformAdjuster;
 import sc.fiji.bdvpg.bdv.navigate.ViewerTransformChanger;
 import sc.fiji.bdvpg.services.SourceAndConverterServices;
@@ -43,15 +42,15 @@
 import java.util.*;
 import java.util.List;
 
-public class UserInterfaceHelper
+import static org.embl.mobie.viewer.ui.SwingHelpers.TEXT_FIELD_HEIGHT;
+
+public class UserInterfaceHelpers
 {
 	private static final Dimension PREFERRED_BUTTON_SIZE = new Dimension( 30, 30 );
 	private static final Dimension PREFERRED_CHECKBOX_SIZE = new Dimension( 40, 30 );
 	private static final Dimension PREFERRED_SPACE_SIZE = new Dimension( 10, 30 );
-	private static final String VIEW = "view";
 	private static final String MOVE = "move";
 	private static final String HELP = "show";
-	private static final String SWITCH = "switch";
 	private static final String LEVEL = "level";
 	private static final String ADD = "view";
 	public static final int SPACING = 20;
@@ -62,7 +61,7 @@ public class UserInterfaceHelper
 	private Map< String, Map< String, View> > groupingsToViews;
 	private Map< String, JComboBox > groupingsToComboBox;
 
-	public UserInterfaceHelper( MoBIE moBIE )
+	public UserInterfaceHelpers( MoBIE moBIE )
 	{
 		this.moBIE = moBIE;
 	}
@@ -96,41 +95,46 @@ public JPanel createDisplaySettingsPanel( JScrollPane displaySettingsScrollPane
 
 	public static void showBrightnessDialog(
 			String name,
-			List< ConverterSetup > converterSetups,
-			double rangeMin,
-			double rangeMax )
+			List< ConverterSetup > converterSetups )
 	{
 		JFrame frame = new JFrame( name );
 		frame.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
 
-		final double currentRangeMin = converterSetups.get( 0 ).getDisplayRangeMin();
-		final double currentRangeMax = converterSetups.get( 0 ).getDisplayRangeMax();
+		final double currentContrastLimitsMin = converterSetups.get( 0 ).getDisplayRangeMin();
+		final double currentContrastLimitsMax = converterSetups.get( 0 ).getDisplayRangeMax();
+		final double absCurrentRange = Math.abs( currentContrastLimitsMax - currentContrastLimitsMin );
+
+		final double rangeFactor = 1.0; // could be changed...
+
+		final double rangeMin = currentContrastLimitsMin - rangeFactor * absCurrentRange;
+		final double rangeMax = currentContrastLimitsMax + rangeFactor * absCurrentRange;
 
 		final BoundedValueDouble min =
 				new BoundedValueDouble(
 						rangeMin,
 						rangeMax,
-						currentRangeMin );
+						currentContrastLimitsMin );
 
 		final BoundedValueDouble max =
 				new BoundedValueDouble(
 						rangeMin,
 						rangeMax,
-						currentRangeMax );
+						currentContrastLimitsMax );
 
-		double spinnerStepSize = Math.abs( currentRangeMax - currentRangeMin ) / 100.0;
+
+		double spinnerStepSize = absCurrentRange / 100.0;
 
 		JPanel panel = new JPanel();
 		panel.setLayout( new BoxLayout( panel, BoxLayout.PAGE_AXIS ) );
 		final SliderPanelDouble minSlider =
 				new SliderPanelDouble( "Min", min, spinnerStepSize );
 		minSlider.setNumColummns( 7 );
-		minSlider.setDecimalFormat( "####E0" );
+		//minSlider.setDecimalFormat( "####E0" );
 
 		final SliderPanelDouble maxSlider =
 				new SliderPanelDouble( "Max", max, spinnerStepSize );
 		maxSlider.setNumColummns( 7 );
-		maxSlider.setDecimalFormat( "####E0" );
+		//maxSlider.setDecimalFormat( "####E0" );
 
 		final BrightnessUpdateListener brightnessUpdateListener = new BrightnessUpdateListener( min, max, minSlider, maxSlider, converterSetups );
 
@@ -192,7 +196,6 @@ public static void showOpacityDialog(
 		frame.setResizable( false );
 		frame.pack();
 		frame.setVisible( true );
-
 	}
 
 	public JPanel createAnnotatedIntervalDisplaySettingsPanel( AnnotatedIntervalDisplay display )
@@ -259,8 +262,10 @@ public JPanel createSelectionPanel()
 		panel.add( createDatasetSelectionPanel() );
 		panel.add( new JSeparator( SwingConstants.HORIZONTAL ) );
 		panel.add( createViewsSelectionPanel() );
+
 		panel.add( new JSeparator( SwingConstants.HORIZONTAL ) );
 		panel.add( createMoveToLocationPanel()  );
+		panel.add( createRemoveAllViewsPanel( moBIE ) );
 
 		return panel;
 	}
@@ -288,7 +293,7 @@ public JPanel createImageDisplaySettingsPanel( ImageSourceDisplay display )
 		// Checkboxes
 		panel.add( createSpace() );
 		panel.add( createSliceViewerVisibilityCheckbox( display.isVisible(), sourceAndConverters ) );
-		panel.add( createCheckboxPlaceholder() ); // TODO: createVolume...
+		panel.add( createImageVolumeViewerVisibilityCheckbox( display ) );
 		panel.add( createCheckboxPlaceholder() );
 		panel.add( createCheckboxPlaceholder() );
 
@@ -296,7 +301,7 @@ public JPanel createImageDisplaySettingsPanel( ImageSourceDisplay display )
 		// make the panel color listen to color changes of the sources
 		for ( SourceAndConverter< ? > sourceAndConverter : display.sourceNameToSourceAndConverter.values() )
 		{
-			SourceAndConverterServices.getBdvDisplayService().getConverterSetup( sourceAndConverter ).setupChangeListeners().add( setup -> {
+			SourceAndConverterServices.getSourceAndConverterService().getConverterSetup( sourceAndConverter ).setupChangeListeners().add( setup -> {
 				// color changed listener
 				setPanelColor( panel, setup.getColor() );
 			} );
@@ -336,7 +341,7 @@ public JPanel createSegmentationDisplaySettingsPanel( SegmentationSourceDisplay
 		if ( display.tableRows != null )
 		{
 			// segments 3D view
-			panel.add( createVolumeViewerVisibilityCheckbox( display ) );
+			panel.add( createSegmentsVolumeViewerVisibilityCheckbox( display ) );
 			// table view
 			panel.add( createWindowVisibilityCheckbox( display.showTable(), display.tableViewer.getWindow() ) );
 			// scatter plot view
@@ -366,7 +371,8 @@ public JPanel createViewsSelectionPanel( )
 		return viewSelectionPanel;
 	}
 
-	public void addViewsToSelectionPanel( Map< String, View > views ) {
+	public void addViewsToSelectionPanel( Map< String, View > views )
+	{
 		for ( String viewName : views.keySet() )
 		{
 			final View view = views.get( viewName );
@@ -423,6 +429,11 @@ public int compare(String s1, String s2) {
 		viewsSelectionPanelHeight = groupingsToViews.keySet().size() * 40;
 	}
 
+	public Map< String, Map< String, View > > getGroupingsToViews()
+	{
+		return groupingsToViews;
+	}
+
 	public int getViewsSelectionPanelHeight()
 	{
 		return viewsSelectionPanelHeight;
@@ -437,13 +448,38 @@ public Set<String> getGroupings() {
 		return groupingsToViews.keySet();
 	}
 
+	private JPanel createRemoveAllViewsPanel( MoBIE moBIE )
+	{
+		JPanel panel = new JPanel();
+		panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
+		panel.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
+		panel.add(Box.createHorizontalGlue());
+		panel.setAlignmentX(0.0F);
+
+		final JButton button = SwingHelpers.createButton( "clear", new Dimension( 80, TEXT_FIELD_HEIGHT ) );
+		button.setAlignmentX(Component.CENTER_ALIGNMENT);
+		button.addActionListener( e ->
+		{
+			SwingUtilities.invokeLater( () ->
+			{
+				new Thread( () ->
+				{
+					moBIE.getViewManager().removeAllSourceDisplays();
+				}).start();
+			} );
+		} );
+
+		panel.add( button );
+		return panel;
+	}
+
 	private JPanel createViewSelectionPanel(MoBIE moBIE, String panelName, Map< String, View > views )
 	{
 		final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
 
 		final JComboBox< String > comboBox = new JComboBox<>( views.keySet().toArray( new String[ 0 ] ) );
 
-		final JButton button = SwingHelper.createButton( ADD );
+		final JButton button = SwingHelpers.createButton( ADD );
 		button.addActionListener( e ->
 		{
 			SwingUtilities.invokeLater( () ->
@@ -458,9 +494,9 @@ private JPanel createViewSelectionPanel(MoBIE moBIE, String panelName, Map< Stri
 			} );
 		} );
 
-		SwingHelper.setComboBoxDimensions( comboBox );
+		SwingHelpers.setComboBoxDimensions( comboBox );
 
-		horizontalLayoutPanel.add( SwingHelper.getJLabel( panelName ) );
+		horizontalLayoutPanel.add( SwingHelpers.getJLabel( panelName ) );
 		horizontalLayoutPanel.add( comboBox );
 		horizontalLayoutPanel.add( button );
 
@@ -469,45 +505,14 @@ private JPanel createViewSelectionPanel(MoBIE moBIE, String panelName, Map< Stri
 		return horizontalLayoutPanel;
 	}
 
-	public JPanel createLevelingPanel( double[] levelingVector )
-	{
-		final double[] targetNormalVector = Arrays.copyOf( levelingVector, 3 );
-
-		final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
-
-		final JButton button = SwingHelper.createButton( LEVEL );
-		horizontalLayoutPanel.add( button );
-
-		// TODO: if below code is needed make an own Levelling class
-//		final JButton changeReference = new JButton( "Set new level vector" );
-//		horizontalLayoutPanel.add( changeReference );
-
-//		final JButton defaultReference = new JButton( "Set default level vector" );
-//		horizontalLayoutPanel.add( defaultReference );
-
-//		changeReference.addActionListener( e -> {
-//			targetNormalVector = BdvUtils.getCurrentViewNormalVector( bdv );
-//			Utils.logVector( "New reference normal vector: ", targetNormalVector );
-//		} );
-
-//		defaultReference.addActionListener( e -> {
-//			targetNormalVector = Arrays.copyOf( levelingVector, 3);
-//			Utils.logVector( "New reference normal vector (default): ", levelingVector );
-//		} );
-
-		button.addActionListener( e -> BdvUtils.levelCurrentView( moBIE.getViewManager().getSliceViewer().getBdvHandle(), targetNormalVector ) );
-
-		return horizontalLayoutPanel;
-	}
-
 	public JPanel createMoveToLocationPanel( )
 	{
 		final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
-		final JButton button = SwingHelper.createButton( MOVE );
+		final JButton button = SwingHelpers.createButton( MOVE );
 
 		final JTextField jTextField = new JTextField( "{\"position\":[120.5,115.3,201.5]}" );
-		jTextField.setPreferredSize( new Dimension( SwingHelper.COMBOBOX_WIDTH - 3, SwingHelper.TEXT_FIELD_HEIGHT ) );
-		jTextField.setMaximumSize( new Dimension( SwingHelper.COMBOBOX_WIDTH - 3, SwingHelper.TEXT_FIELD_HEIGHT ) );
+		jTextField.setPreferredSize( new Dimension( SwingHelpers.COMBOBOX_WIDTH - 3, TEXT_FIELD_HEIGHT ) );
+		jTextField.setMaximumSize( new Dimension( SwingHelpers.COMBOBOX_WIDTH - 3, TEXT_FIELD_HEIGHT ) );
 		button.addActionListener( e ->
 		{
 			final Gson gson = JsonHelper.buildGson( false );
@@ -515,7 +520,7 @@ public JPanel createMoveToLocationPanel( )
 			MoBIEViewerTransformChanger.changeViewerTransform( moBIE.getViewManager().getSliceViewer().getBdvHandle(), viewerTransform );
 		} );
 
-		horizontalLayoutPanel.add( SwingHelper.getJLabel( "location" ) );
+		horizontalLayoutPanel.add( SwingHelpers.getJLabel( "location" ) );
 		horizontalLayoutPanel.add( jTextField );
 		horizontalLayoutPanel.add( button );
 
@@ -526,12 +531,12 @@ public JPanel createInfoPanel( String projectLocation, String publicationURL )
 	{
 		final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
 
-		final JButton button = SwingHelper.createButton( HELP );
+		final JButton button = SwingHelpers.createButton( HELP );
 
 		final MoBIEInfo moBIEInfo = new MoBIEInfo( projectLocation, publicationURL );
 
 		final JComboBox< String > comboBox = new JComboBox<>( moBIEInfo.getInfoChoices() );
-		SwingHelper.setComboBoxDimensions( comboBox );
+		SwingHelpers.setComboBoxDimensions( comboBox );
 
 		button.addActionListener( e -> {
 			moBIEInfo.showInfo( ( String ) comboBox.getSelectedItem() );
@@ -552,7 +557,7 @@ public JPanel createInfoPanel( String projectLocation, String publicationURL )
 
 	public ImageIcon createMobieIcon( int size )
 	{
-		final URL resource = UserInterfaceHelper.class.getResource( "/mobie.jpeg" );
+		final URL resource = UserInterfaceHelpers.class.getResource( "/mobie.jpeg" );
 		final ImageIcon imageIcon = new ImageIcon( resource );
 		final Image scaledInstance = imageIcon.getImage().getScaledInstance( size, size, Image.SCALE_SMOOTH );
 		return new ImageIcon( scaledInstance );
@@ -564,7 +569,7 @@ public JPanel createDatasetSelectionPanel( )
 
 		final JComboBox< String > comboBox = new JComboBox<>( moBIE.getDatasets().toArray( new String[ 0 ] ) );
 
-		final JButton button = SwingHelper.createButton( ADD );
+		final JButton button = SwingHelpers.createButton( ADD );
 		button.addActionListener( e ->
 		{
 			SwingUtilities.invokeLater( () ->
@@ -575,9 +580,9 @@ public JPanel createDatasetSelectionPanel( )
 		} );
 
 		comboBox.setSelectedItem( moBIE.getDatasetName() );
-		SwingHelper.setComboBoxDimensions( comboBox );
+		SwingHelpers.setComboBoxDimensions( comboBox );
 
-		panel.add( SwingHelper.getJLabel( "dataset" ) );
+		panel.add( SwingHelpers.getJLabel( "dataset" ) );
 		panel.add( comboBox );
 		panel.add( button );
 
@@ -598,6 +603,40 @@ private static Component createCheckboxPlaceholder()
 		return Box.createRigidArea( PREFERRED_CHECKBOX_SIZE );
 	}
 
+	public static JCheckBox createSegmentsVolumeViewerVisibilityCheckbox( SegmentationSourceDisplay display )
+	{
+		JCheckBox checkBox = new JCheckBox( "V" );
+		checkBox.setSelected( display.showSelectedSegmentsIn3d() );
+		checkBox.setPreferredSize( PREFERRED_CHECKBOX_SIZE );
+
+		checkBox.addActionListener( new ActionListener()
+		{
+			@Override
+			public void actionPerformed( ActionEvent e )
+			{
+				new Thread( () -> {
+						display.segmentsVolumeViewer.showSegments( checkBox.isSelected() );
+				}).start();
+			}
+		} );
+
+		display.segmentsVolumeViewer.getListeners().add( new VisibilityListener()
+		{
+			@Override
+			public void visibility( boolean isVisible )
+			{
+				SwingUtilities.invokeLater( () ->
+				{
+					checkBox.setSelected( isVisible );
+				});
+			}
+		} );
+
+
+		return checkBox;
+	}
+
+
 	private static JCheckBox createSliceViewerVisibilityCheckbox(
 			boolean isVisible,
 			final List< SourceAndConverter< ? > > sourceAndConverters )
@@ -628,7 +667,7 @@ private static JCheckBox createWindowVisibilityCheckbox(
 		JCheckBox checkBox = new JCheckBox( "T" );
 		checkBox.setSelected( isVisible );
 		checkBox.setPreferredSize( PREFERRED_CHECKBOX_SIZE );
-		window.setVisible( checkBox.isSelected() );
+		window.setVisible( isVisible );
 		checkBox.addActionListener( e -> SwingUtilities.invokeLater( () -> window.setVisible( checkBox.isSelected() ) ) );
 
 		window.addWindowListener(
@@ -672,10 +711,10 @@ public void visibility( boolean isVisible )
 		return checkBox;
 	}
 
-	public static JCheckBox createVolumeViewerVisibilityCheckbox( SegmentationSourceDisplay display )
+	public static JCheckBox createImageVolumeViewerVisibilityCheckbox( ImageSourceDisplay display )
 	{
 		JCheckBox checkBox = new JCheckBox( "V" );
-		checkBox.setSelected( display.showSelectedSegmentsIn3d() );
+		checkBox.setSelected( display.showImagesIn3d() );
 		checkBox.setPreferredSize( PREFERRED_CHECKBOX_SIZE );
 
 		checkBox.addActionListener( new ActionListener()
@@ -684,19 +723,12 @@ public static JCheckBox createVolumeViewerVisibilityCheckbox( SegmentationSource
 			public void actionPerformed( ActionEvent e )
 			{
 				new Thread( () -> {
-					if ( checkBox.isSelected() )
-					{
-						display.segmentsVolumeViewer.showSegments( true );
-					}
-					else
-					{
-						display.segmentsVolumeViewer.showSegments( false );
-					}
+						display.imageVolumeViewer.showImages( checkBox.isSelected() );
 				}).start();
 			}
 		} );
 
-		display.segmentsVolumeViewer.getListeners().add( new VisibilityListener()
+		display.imageVolumeViewer.getListeners().add( new VisibilityListener()
 		{
 			@Override
 			public void visibility( boolean isVisible )
@@ -737,14 +769,12 @@ public static JButton createImageDisplayBrightnessButton( ImageSourceDisplay ima
 			final ArrayList< ConverterSetup > converterSetups = new ArrayList<>();
 			for ( SourceAndConverter< ? > sourceAndConverter : imageDisplay.sourceNameToSourceAndConverter.values() )
 			{
-				converterSetups.add( SourceAndConverterServices.getBdvDisplayService().getConverterSetup( sourceAndConverter ) );
+				converterSetups.add( SourceAndConverterServices.getSourceAndConverterService().getConverterSetup( sourceAndConverter ) );
 			}
 
-			UserInterfaceHelper.showBrightnessDialog(
+			UserInterfaceHelpers.showBrightnessDialog(
 					imageDisplay.getName(),
-					converterSetups,
-					0,   // TODO: determine somehow...
-					65535 );
+					converterSetups);
 		} );
 
 		return button;
@@ -769,7 +799,7 @@ public static JButton createOpacityButton( List< SourceAndConverter< ? > > sourc
 
 		button.addActionListener( e ->
 		{
-			UserInterfaceHelper.showOpacityDialog(
+			UserInterfaceHelpers.showOpacityDialog(
 					name,
 					sourceAndConverters,
 					bdvHandle );
@@ -829,7 +859,9 @@ private JButton createRemoveButton( SourceDisplay sourceDisplay )
 
 		removeButton.addActionListener( e ->
 		{
-			moBIE.getViewManager().removeSourceDisplay( sourceDisplay );
+			// remove the display but do not close the ImgLoader
+			// because some derived sources may currently be shown
+			moBIE.getViewManager().removeSourceDisplay( sourceDisplay, false );
 		} );
 
 		return removeButton;
@@ -840,12 +872,12 @@ public static String tidyString( String string ) {
 		String tidyString = string.replaceAll("\\s+","_");
 
 		if ( !string.equals(tidyString) ) {
-			Utils.log( "Spaces were removed from name, and replaced by _");
+			MoBIEUtils.log( "Spaces were removed from name, and replaced by _");
 		}
 
 		// check only contains alphanumerics, or _ -
 		if ( !tidyString.matches("^[a-zA-Z0-9_-]+$") ) {
-			Utils.log( "Names must only contain letters, numbers, _ or -. Please try again " +
+			MoBIEUtils.log( "Names must only contain letters, numbers, _ or -. Please try again " +
 					"with a different name.");
 			tidyString = null;
 		}
diff --git a/src/main/java/org/embl/mobie/viewer/ui/WindowArrangementHelper.java b/src/main/java/org/embl/mobie/viewer/ui/WindowArrangementHelper.java
index d317671b1..091e7b7b2 100644
--- a/src/main/java/org/embl/mobie/viewer/ui/WindowArrangementHelper.java
+++ b/src/main/java/org/embl/mobie/viewer/ui/WindowArrangementHelper.java
@@ -47,16 +47,16 @@ public static void setLogWindowPositionAndSize( Window reference )
 		final Frame log = WindowManager.getFrame( "Log" );
 		if (log != null) {
 			Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
-			final int logWindowHeight = screenSize.height - ( reference.getLocationOnScreen().y + reference.getHeight() + 2 * UserInterfaceHelper.SPACING );
+			final int logWindowHeight = screenSize.height - ( reference.getLocationOnScreen().y + reference.getHeight() + 2 * UserInterfaceHelpers.SPACING );
 			log.setSize( reference.getWidth(), logWindowHeight  );
-			log.setLocation( reference.getLocationOnScreen().x, reference.getLocationOnScreen().y + UserInterfaceHelper.SPACING + reference.getHeight() );
+			log.setLocation( reference.getLocationOnScreen().x, reference.getLocationOnScreen().y + UserInterfaceHelpers.SPACING + reference.getHeight() );
 		}
 	}
 
 	public static void rightAlignWindow( Window reference, Window window, boolean adjustWidth, boolean adjustHeight )
 	{
 		window.setLocation(
-				reference.getLocationOnScreen().x + reference.getWidth() + UserInterfaceHelper.SPACING,
+				reference.getLocationOnScreen().x + reference.getWidth() + UserInterfaceHelpers.SPACING,
 				reference.getLocationOnScreen().y );
 
 		if ( adjustWidth )
@@ -70,7 +70,7 @@ public static void bottomAlignWindow( Window reference, Window window )
 	{
 		window.setLocation(
 				reference.getLocationOnScreen().x,
-				reference.getLocationOnScreen().y + reference.getHeight() + UserInterfaceHelper.SPACING
+				reference.getLocationOnScreen().y + reference.getHeight() + UserInterfaceHelpers.SPACING
 		);
 	}
 }
diff --git a/src/main/java/org/embl/mobie/viewer/view/OMEZarrViewer.java b/src/main/java/org/embl/mobie/viewer/view/OMEZarrViewer.java
new file mode 100644
index 000000000..0da54318a
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/view/OMEZarrViewer.java
@@ -0,0 +1,55 @@
+package org.embl.mobie.viewer.view;
+
+import bdv.util.BdvFunctions;
+import bdv.util.BdvOptions;
+import bdv.util.BdvStackSource;
+import mpicbg.spim.data.SpimData;
+import mpicbg.spim.data.sequence.ViewSetup;
+import net.imglib2.Dimensions;
+import org.embl.mobie.viewer.source.Sources;
+
+import java.util.List;
+
+public class OMEZarrViewer
+{
+    private final SpimData spimData;
+
+    public OMEZarrViewer( SpimData spimData) {
+        this.spimData = spimData;
+    }
+
+    public void show() {
+        final List<ViewSetup> viewSetups = spimData.getSequenceDescription().getViewSetupsOrdered();
+
+        BdvOptions bdvOptions = getBdvOptions(viewSetups);
+
+        List<BdvStackSource<?>> sources = BdvFunctions.show(spimData, bdvOptions);
+
+        for (int i = 0; i < viewSetups.size(); i++) {
+            final String name = viewSetups.get(i).getChannel().getName();
+            if (name.contains("labels"))
+                Sources.showAsLabelMask(sources.get(i));
+        }
+    }
+
+    public BdvOptions getBdvOptions(List<ViewSetup> viewSetups) {
+        boolean is2D = is2D(viewSetups);
+        BdvOptions bdvOptions = new BdvOptions();
+
+        if (is2D) {
+            bdvOptions = bdvOptions.is2D();
+        }
+        return bdvOptions;
+    }
+
+    public boolean is2D(List<ViewSetup> viewSetups) {
+        for (ViewSetup viewSetup : viewSetups) {
+            final Dimensions size = viewSetup.getSize();
+            if (size.dimensionsAsLongArray().length > 2) {
+                if (size.dimension(2) > 1)
+                    return false;
+            }
+        }
+        return true;
+    }
+}
diff --git a/src/main/java/org/embl/mobie/viewer/view/ViewFromSourceAndConverterCreator.java b/src/main/java/org/embl/mobie/viewer/view/ViewFromSourceAndConverterCreator.java
new file mode 100644
index 000000000..3fefd1449
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/view/ViewFromSourceAndConverterCreator.java
@@ -0,0 +1,132 @@
+package org.embl.mobie.viewer.view;
+
+import bdv.SpimSource;
+import bdv.tools.brightness.ConverterSetup;
+import bdv.tools.transformation.TransformedSource;
+import bdv.util.ResampledSource;
+import bdv.viewer.Source;
+import bdv.viewer.SourceAndConverter;
+import net.imglib2.converter.Converter;
+import net.imglib2.display.ColorConverter;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.numeric.ARGBType;
+import org.embl.mobie.viewer.bdv.render.BlendingMode;
+import org.embl.mobie.viewer.color.LabelConverter;
+import org.embl.mobie.viewer.color.OpacityAdjuster;
+import org.embl.mobie.viewer.display.ImageSourceDisplay;
+import org.embl.mobie.viewer.display.SourceDisplay;
+import org.embl.mobie.viewer.source.LabelSource;
+import org.embl.mobie.viewer.transform.AffineSourceTransformer;
+import org.embl.mobie.viewer.transform.MergedGridSource;
+import org.embl.mobie.viewer.transform.SourceTransformer;
+import sc.fiji.bdvpg.services.SourceAndConverterServices;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ViewFromSourceAndConverterCreator
+{
+	private final SourceAndConverter sourceAndConverter;
+
+	// display settings
+	private double opacity;
+	private BlendingMode blendingMode;
+	private double[] contrastLimits;
+	private String color;
+
+	public ViewFromSourceAndConverterCreator( SourceAndConverter sourceAndConverter )
+	{
+		this.sourceAndConverter = sourceAndConverter;
+	}
+
+	public View getView()
+	{
+		final ArrayList< SourceDisplay > sourceDisplays = new ArrayList<>();
+		final ArrayList< SourceTransformer > sourceTransformers = new ArrayList<>();
+		final View view = new View( "uiSelectionGroup", sourceDisplays, sourceTransformers, false );
+
+		// recursively add all transformations
+		// FIXME: in fact this will be the wrong order.
+		addSourceTransformers( sourceAndConverter.getSpimSource(), sourceTransformers );
+
+		if ( sourceAndConverter.getConverter() instanceof LabelConverter )
+		{
+			throwError( sourceAndConverter.getConverter() );
+		}
+		else
+		{
+			sourceDisplays.add( new ImageSourceDisplay( sourceAndConverter ) );
+		}
+
+		return view;
+	}
+
+	private void createSourceDisplay( SourceAndConverter< ? > sourceAndConverter  )
+	{
+		final Converter< ?, ARGBType > converter = sourceAndConverter.getConverter();
+		final ConverterSetup converterSetup = SourceAndConverterServices.getSourceAndConverterService().getConverterSetup( sourceAndConverter );
+
+		if( converter instanceof OpacityAdjuster )
+			opacity = ( ( OpacityAdjuster ) converter ).getOpacity();
+
+		if ( converter instanceof ColorConverter )
+		{
+			// needs to be of form r=(\\d+),g=(\\d+),b=(\\d+),a=(\\d+)"
+			color = ( ( ColorConverter ) converter ).getColor().toString();
+			color = color.replaceAll("[()]", "");
+		}
+
+		contrastLimits = new double[2];
+		contrastLimits[0] = converterSetup.getDisplayRangeMin();
+		contrastLimits[1] = converterSetup.getDisplayRangeMax();
+
+		blendingMode = (BlendingMode) SourceAndConverterServices.getSourceAndConverterService().getMetadata( sourceAndConverter, BlendingMode.BLENDING_MODE );
+	}
+
+	private void addSourceTransformers( Source< ? > source, List< SourceTransformer > sourceTransformers )
+	{
+		if ( source instanceof SpimSource )
+		{
+			return;
+		}
+		else if ( source instanceof TransformedSource )
+		{
+			final TransformedSource transformedSource = ( TransformedSource ) source;
+
+			AffineTransform3D fixedTransform = new AffineTransform3D();
+			transformedSource.getFixedTransform( fixedTransform );
+			if ( ! fixedTransform.isIdentity() )
+			{
+				sourceTransformers.add( new AffineSourceTransformer( transformedSource ) );
+			}
+
+			final Source< ? > wrappedSource = transformedSource.getWrappedSource();
+
+			addSourceTransformers( wrappedSource, sourceTransformers );
+		}
+		else if (  source instanceof LabelSource )
+		{
+			final Source< ? > wrappedSource = (( LabelSource ) source).getWrappedSource();
+
+			addSourceTransformers( wrappedSource, sourceTransformers );
+		}
+		else if (  source instanceof MergedGridSource )
+		{
+			throwError( source );
+		}
+		else if (  source instanceof ResampledSource )
+		{
+			throwError( source );
+		}
+		else
+		{
+			throwError( source );
+		}
+	}
+
+	private void throwError( Object object )
+	{
+		throw new UnsupportedOperationException( "Cannot yet create a view from a " + object.getClass().getName() );
+	}
+
+}
diff --git a/src/main/java/org/embl/mobie/viewer/view/ViewManager.java b/src/main/java/org/embl/mobie/viewer/view/ViewManager.java
index c91601ca7..caa808830 100644
--- a/src/main/java/org/embl/mobie/viewer/view/ViewManager.java
+++ b/src/main/java/org/embl/mobie/viewer/view/ViewManager.java
@@ -12,9 +12,8 @@
 import ij.IJ;
 import net.imglib2.realtransform.AffineTransform3D;
 import org.apache.commons.lang.ArrayUtils;
-import org.embl.mobie.io.n5.source.LabelSource;
 import org.embl.mobie.viewer.MoBIE;
-import org.embl.mobie.viewer.Utils;
+import org.embl.mobie.viewer.SourceNameEncoder;
 import org.embl.mobie.viewer.annotate.AnnotatedIntervalAdapter;
 import org.embl.mobie.viewer.annotate.AnnotatedIntervalTableRow;
 import org.embl.mobie.viewer.bdv.view.AnnotatedIntervalSliceView;
@@ -23,10 +22,11 @@
 import org.embl.mobie.viewer.bdv.view.SliceViewer;
 import org.embl.mobie.viewer.color.MoBIEColoringModel;
 import org.embl.mobie.viewer.display.*;
-import org.embl.mobie.viewer.playground.PlaygroundUtils;
+import org.embl.mobie.viewer.playground.BdvPlaygroundUtils;
 import org.embl.mobie.viewer.playground.SourceAffineTransformer;
 import org.embl.mobie.viewer.plot.ScatterPlotViewer;
 import org.embl.mobie.viewer.segment.SegmentAdapter;
+import org.embl.mobie.viewer.source.LabelSource;
 import org.embl.mobie.viewer.source.SegmentationSource;
 import org.embl.mobie.viewer.table.TableDataFormat;
 import org.embl.mobie.viewer.table.TableViewer;
@@ -34,11 +34,13 @@
 import org.embl.mobie.viewer.transform.MoBIEViewerTransformChanger;
 import org.embl.mobie.viewer.transform.NormalizedAffineViewerTransform;
 import org.embl.mobie.viewer.transform.SourceTransformer;
+import org.embl.mobie.viewer.transform.TransformHelper;
 import org.embl.mobie.viewer.ui.MoBIELookAndFeelToggler;
 import org.embl.mobie.viewer.ui.UserInterface;
 import org.embl.mobie.viewer.ui.WindowArrangementHelper;
 import org.embl.mobie.viewer.view.additionalviews.AdditionalViewsLoader;
 import org.embl.mobie.viewer.view.saving.ViewsSaver;
+import org.embl.mobie.viewer.volume.ImageVolumeViewer;
 import org.embl.mobie.viewer.volume.SegmentsVolumeViewer;
 import org.embl.mobie.viewer.volume.UniverseManager;
 import sc.fiji.bdvpg.bdv.navigate.ViewerTransformAdjuster;
@@ -49,7 +51,7 @@
 import java.util.*;
 import java.util.stream.Collectors;
 
-import static org.embl.mobie.viewer.Utils.containsAtLeastOne;
+import static org.embl.mobie.viewer.MoBIEUtils.containsAtLeastOne;
 
 public class ViewManager
 {
@@ -67,23 +69,11 @@ public class ViewManager
 	private final AdditionalViewsLoader additionalViewsLoader;
 	private final ViewsSaver viewsSaver;
 
-	private View currentView;
-
-    public void setCurrentView( View currentView )
-    {
-        this.currentView = currentView;
-    }
-
     public List<SourceTransformer> getCurrentSourceTransformers()
     {
         return currentSourceTransformers;
     }
 
-    public View getCurrentView()
-    {
-        return currentView;
-    }
-
     public UserInterface getUserInterface()
     {
         return userInterface;
@@ -95,7 +85,7 @@ public ViewManager( MoBIE moBIE, UserInterface userInterface, boolean is2D, int
 		this.userInterface = userInterface;
 		currentSourceDisplays = new ArrayList<>();
 		currentSourceTransformers = new ArrayList<>();
-		sliceViewer = new SliceViewer( is2D, this, timepoints );
+		sliceViewer = new SliceViewer( is2D, this, timepoints, moBIE.getProjectCommands() );
 		universeManager = new UniverseManager();
 		bdvHandle = sliceViewer.get();
 		additionalViewsLoader = new AdditionalViewsLoader( moBIE );
@@ -145,7 +135,7 @@ private static void configureMoBIEColoringModel( AnnotatedRegionDisplay< ? > dis
 		}
 	}
 
-	public void showInTableViewer( SegmentationSourceDisplay display  )
+	public void initTableViewer( SegmentationSourceDisplay display  )
 	{
 		Map<String, String> sourceNameToTableDir = new HashMap<>();
 		for ( String source: display.getSources() )
@@ -160,7 +150,8 @@ public void showInTableViewer( SegmentationSourceDisplay display  )
 				sourceNameToTableDir.put( source, null );
 			}
 		}
-		display.tableViewer = new TableViewer<>( moBIE, display.tableRows, display.selectionModel, display.coloringModel, display.getName(), sourceNameToTableDir, false ).show();
+		display.tableViewer = new TableViewer<>( moBIE, display.tableRows, display.selectionModel, display.coloringModel, display.getName(), sourceNameToTableDir, false );
+		display.tableViewer.setVisible( display.showTable() );
 		display.selectionModel.listeners().add( display.tableViewer );
 		display.coloringModel.listeners().add( display.tableViewer );
 	}
@@ -248,8 +239,7 @@ public View getCurrentView( String uiSelectionGroup, boolean isExclusive, boolea
 
 		if ( includeViewerTransform )
 		{
-			AffineTransform3D normalisedViewTransform = Utils.createNormalisedViewerTransform( bdvHandle,
-					PlaygroundUtils.getWindowCentreInPixelUnits( bdvHandle ) );
+			AffineTransform3D normalisedViewTransform = TransformHelper.createNormalisedViewerTransform( bdvHandle.getViewerPanel() );
 
 			final NormalizedAffineViewerTransform transform = new NormalizedAffineViewerTransform( normalisedViewTransform.getRowPackedCopy(), bdvHandle.getViewerPanel().state().getCurrentTimepoint() );
 			return new View(uiSelectionGroup, viewSourceDisplays, viewSourceTransforms, transform, isExclusive);
@@ -269,14 +259,29 @@ public synchronized void show( View view )
 		{
 			removeAllSourceDisplays();
 		}
-		currentView = view;
 
+		openAndTransformViewSources( view );
+
+		// show the displays
+		MoBIELookAndFeelToggler.setMoBIELaf();
+		final List< SourceDisplay > sourceDisplays = view.getSourceDisplays();
+		for ( SourceDisplay sourceDisplay : sourceDisplays )
+			showSourceDisplay( sourceDisplay );
+		MoBIELookAndFeelToggler.resetMoBIELaf();
+
+		// adjust viewer transform
+		adjustViewerTransform( view );
+	}
+
+	public void openAndTransformViewSources( View view )
+	{
 		// fetch the names of all sources that are either shown or to be transformed
 		final Set< String > sources = fetchSources( view );
-		final Set< String > datasetSources = sources.stream().filter( s -> moBIE.getDataset().sources.containsKey( s ) ).collect( Collectors.toSet() );
+		SourceNameEncoder.addNames( sources );
+		final Set< String > rawSources = sources.stream().filter( s -> moBIE.getDataset().sources.containsKey( s ) ).collect( Collectors.toSet() );
 
 		// open all raw sources
-		Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverters = moBIE.openSourceAndConverters( datasetSources );
+		Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverters = moBIE.openSourceAndConverters( rawSources );
 
 		// create transformed sources
 		final List< SourceTransformer > sourceTransformers = view.getSourceTransforms();
@@ -290,23 +295,14 @@ public synchronized void show( View view )
 		// wrap all in a final transformed source. This is so any manual transformations can be
 		// retrieved separate from any from sourceTransformers.
 		for ( String sourceName : sourceNameToSourceAndConverters.keySet() ) {
-			SourceAndConverter<?> sourceAndConverter = new SourceAffineTransformer(
-					sourceNameToSourceAndConverters.get( sourceName ), new AffineTransform3D()).getSourceOut();
+			SourceAndConverter<?> sourceAndConverter = new SourceAffineTransformer( sourceNameToSourceAndConverters.get( sourceName ), new AffineTransform3D()).getSourceOut();
 			sourceNameToSourceAndConverters.put( sourceName, sourceAndConverter );
 		}
 
-		// register all available sources
-		moBIE.addSourceAndConverters( sourceNameToSourceAndConverters );
-
-		// show the displays
-		MoBIELookAndFeelToggler.setMoBIELaf();
-		final List< SourceDisplay > sourceDisplays = view.getSourceDisplays();
-		for ( SourceDisplay sourceDisplay : sourceDisplays )
-			showSourceDisplay( sourceDisplay );
-		MoBIELookAndFeelToggler.resetMoBIELaf();
-
-		// adjust viewer transform
-		adjustViewerTransform( view );
+		// register all available (transformed) sources in MoBIE
+		// this is where the source and segmentation displays will
+		// get the sources from
+		moBIE.addTransformedSourceAndConverters( sourceNameToSourceAndConverters );
 	}
 
 	public void adjustViewerTransform( View view )
@@ -365,7 +361,7 @@ else if ( sourceDisplay instanceof AnnotatedIntervalDisplay )
 		currentSourceDisplays.add( sourceDisplay );
 	}
 
-	public void removeAllSourceDisplays()
+	public synchronized void removeAllSourceDisplays()
 	{
 		// create a copy of the currently shown displays...
 		final ArrayList< SourceDisplay > currentSourceDisplays = new ArrayList<>( this.currentSourceDisplays ) ;
@@ -376,7 +372,8 @@ public void removeAllSourceDisplays()
 		{
 			// removes display from all viewers and
 			// also from the list of currently shown sourceDisplays
-			removeSourceDisplay( sourceDisplay );
+			// also close all ImgLoaders to free the cache
+			removeSourceDisplay( sourceDisplay, true );
 		}
 	}
 
@@ -384,6 +381,17 @@ private void showImageDisplay( ImageSourceDisplay imageDisplay )
 	{
 		imageDisplay.sliceViewer = sliceViewer;
 		imageDisplay.imageSliceView = new ImageSliceView( moBIE, imageDisplay, bdvHandle );
+		initImageVolumeViewer( imageDisplay );
+	}
+
+	// compare with initSegmentationVolumeViewer
+	private void initImageVolumeViewer( ImageSourceDisplay imageDisplay )
+	{
+		imageDisplay.imageVolumeViewer = new ImageVolumeViewer( imageDisplay.sourceNameToSourceAndConverter, universeManager );
+		for ( SourceAndConverter< ? > sourceAndConverter : imageDisplay.sourceNameToSourceAndConverter.values() )
+		{
+			sacService.setMetadata( sourceAndConverter, ImageVolumeViewer.class.getName(), imageDisplay.imageVolumeViewer );
+		}
 	}
 
 	private void showAnnotatedIntervalDisplay( AnnotatedIntervalDisplay annotationDisplay )
@@ -404,7 +412,7 @@ private void showAnnotatedIntervalDisplay( AnnotatedIntervalDisplay annotationDi
 		}
 
 		showInSliceViewer( annotationDisplay );
-		showInTableViewer( annotationDisplay );
+		initTableViewer( annotationDisplay );
 		initScatterPlotViewer( annotationDisplay );
 
 		SwingUtilities.invokeLater( () ->
@@ -413,7 +421,7 @@ private void showAnnotatedIntervalDisplay( AnnotatedIntervalDisplay annotationDi
 		} );
 	}
 
-	private void showInTableViewer( AnnotatedIntervalDisplay annotationDisplay )
+	private void initTableViewer( AnnotatedIntervalDisplay annotationDisplay )
 	{
 		HashMap<String, String> nameToTableDir = new HashMap<>();
 		nameToTableDir.put( annotationDisplay.getName(), annotationDisplay.getTableDataFolder( TableDataFormat.TabDelimitedFile ) );
@@ -449,7 +457,7 @@ private void showSegmentationDisplay( SegmentationSourceDisplay segmentationDisp
 
 		if ( segmentationDisplay.tableRows != null )
 		{
-			showInTableViewer( segmentationDisplay );
+			initTableViewer( segmentationDisplay );
 			initScatterPlotViewer( segmentationDisplay );
 
 			SwingUtilities.invokeLater( () ->
@@ -457,7 +465,7 @@ private void showSegmentationDisplay( SegmentationSourceDisplay segmentationDisp
 				WindowArrangementHelper.bottomAlignWindow( segmentationDisplay.sliceViewer.getWindow(), segmentationDisplay.tableViewer.getWindow() );
 			} );
 
-			initVolumeViewer( segmentationDisplay );
+			initSegmentationVolumeViewer( segmentationDisplay );
 		}
 	}
 
@@ -497,29 +505,29 @@ private void showInSliceViewer( AnnotatedIntervalDisplay annotatedIntervalDispla
 		annotatedIntervalDisplay.sliceView = new AnnotatedIntervalSliceView( moBIE, annotatedIntervalDisplay, bdvHandle );
 	}
 
-	private void initVolumeViewer( SegmentationSourceDisplay display )
+	private void initSegmentationVolumeViewer( SegmentationSourceDisplay segmentationDisplay )
 	{
-		display.segmentsVolumeViewer = new SegmentsVolumeViewer<>( display.selectionModel, display.coloringModel, display.sourceNameToSourceAndConverter.values(), universeManager );
-		Double[] resolution3dView = display.getResolution3dView();
+		segmentationDisplay.segmentsVolumeViewer = new SegmentsVolumeViewer<>( segmentationDisplay.selectionModel, segmentationDisplay.coloringModel, segmentationDisplay.sourceNameToSourceAndConverter.values(), universeManager );
+		Double[] resolution3dView = segmentationDisplay.getResolution3dView();
 		if ( resolution3dView != null ) {
-			display.segmentsVolumeViewer.setVoxelSpacing( ArrayUtils.toPrimitive(display.getResolution3dView()) );
+			segmentationDisplay.segmentsVolumeViewer.setVoxelSpacing( ArrayUtils.toPrimitive(segmentationDisplay.getResolution3dView()) );
 		}
-		display.segmentsVolumeViewer.showSegments( display.showSelectedSegmentsIn3d() );
-		display.coloringModel.listeners().add( display.segmentsVolumeViewer );
-		display.selectionModel.listeners().add( display.segmentsVolumeViewer );
+		segmentationDisplay.segmentsVolumeViewer.showSegments( segmentationDisplay.showSelectedSegmentsIn3d() );
+		segmentationDisplay.coloringModel.listeners().add( segmentationDisplay.segmentsVolumeViewer );
+		segmentationDisplay.selectionModel.listeners().add( segmentationDisplay.segmentsVolumeViewer );
 
-		for ( SourceAndConverter< ? > sourceAndConverter : display.sourceNameToSourceAndConverter.values() )
+		for ( SourceAndConverter< ? > sourceAndConverter : segmentationDisplay.sourceNameToSourceAndConverter.values() )
 		{
-			sacService.setMetadata( sourceAndConverter, SegmentsVolumeViewer.VOLUME_VIEW, display.segmentsVolumeViewer  );
+			sacService.setMetadata( sourceAndConverter, SegmentsVolumeViewer.class.getName(), segmentationDisplay.segmentsVolumeViewer );
 		}
 	}
 
-	public synchronized void removeSourceDisplay( SourceDisplay sourceDisplay )
+	public synchronized void removeSourceDisplay( SourceDisplay sourceDisplay, boolean closeImgLoader )
 	{
 		if ( sourceDisplay instanceof SegmentationSourceDisplay )
 		{
 			final SegmentationSourceDisplay segmentationDisplay = ( SegmentationSourceDisplay ) sourceDisplay;
-			segmentationDisplay.sliceView.close();
+			segmentationDisplay.sliceView.close( closeImgLoader );
 			if ( segmentationDisplay.tableRows != null )
 			{
 				segmentationDisplay.tableViewer.close();
@@ -530,13 +538,13 @@ public synchronized void removeSourceDisplay( SourceDisplay sourceDisplay )
 		else if ( sourceDisplay instanceof ImageSourceDisplay )
 		{
 			final ImageSourceDisplay imageDisplay = ( ImageSourceDisplay ) sourceDisplay;
-			imageDisplay.imageSliceView.close();
+			imageDisplay.imageSliceView.close( false );
 		}
 		else if ( sourceDisplay instanceof AnnotatedIntervalDisplay )
 		{
 			// TODO: Code duplication (sourceDisplay instanceof SegmentationSourceDisplay)
 			final AnnotatedIntervalDisplay annotatedIntervalDisplay = ( AnnotatedIntervalDisplay ) sourceDisplay;
-			annotatedIntervalDisplay.sliceView.close();
+			annotatedIntervalDisplay.sliceView.close( false );
 			annotatedIntervalDisplay.tableViewer.close();
 			annotatedIntervalDisplay.scatterPlotViewer.close();
 		}
diff --git a/src/main/java/org/embl/mobie/viewer/view/additionalviews/AdditionalViewsLoader.java b/src/main/java/org/embl/mobie/viewer/view/additionalviews/AdditionalViewsLoader.java
index c0ace8497..45c7a7d82 100644
--- a/src/main/java/org/embl/mobie/viewer/view/additionalviews/AdditionalViewsLoader.java
+++ b/src/main/java/org/embl/mobie/viewer/view/additionalviews/AdditionalViewsLoader.java
@@ -1,12 +1,10 @@
 package org.embl.mobie.viewer.view.additionalviews;
 
 import ij.IJ;
-import org.embl.mobie.viewer.MoBIESettings;
 import org.embl.mobie.viewer.MoBIE;
-import org.embl.mobie.viewer.Utils;
+import org.embl.mobie.viewer.MoBIEUtils;
 import org.embl.mobie.viewer.serialize.AdditionalViewsJsonParser;
 import org.embl.mobie.viewer.ui.MoBIELookAndFeelToggler;
-import org.embl.mobie.viewer.ui.UserInterfaceHelper;
 import org.embl.mobie.viewer.view.View;
 
 import java.io.IOException;
@@ -23,24 +21,27 @@ public AdditionalViewsLoader ( MoBIE moBIE ) {
     }
 
     public void loadAdditionalViewsDialog() {
-        try {
-            String selectedFilePath = null;
-            Utils.FileLocation fileLocation = Utils.loadFromProjectOrFileSystemDialog();
-            if ( fileLocation == Utils.FileLocation.Project ) {
-                selectedFilePath = Utils.selectPathFromProject( moBIE.getDatasetPath("misc", "views" ), "View" );
-            } else {
-                selectedFilePath = Utils.selectOpenPathFromFileSystem( "View" );
+        new Thread( () -> {
+            try {
+                String selectedFilePath = null;
+                MoBIEUtils.FileLocation fileLocation = MoBIEUtils.loadFromProjectOrFileSystemDialog();
+                if ( fileLocation == null )
+                    return;
+                if ( fileLocation == MoBIEUtils.FileLocation.Project ) {
+                    selectedFilePath = MoBIEUtils.selectPathFromProject( moBIE.getDatasetPath("misc", "views" ), "View" );
+                } else {
+                    selectedFilePath = MoBIEUtils.selectFilePath( "json", "View", true );
+                }
+
+                if (selectedFilePath != null) {
+                    MoBIELookAndFeelToggler.setMoBIELaf();
+                    loadViews( selectedFilePath );
+                    MoBIELookAndFeelToggler.resetMoBIELaf();
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
             }
-
-            if (selectedFilePath != null) {
-                MoBIELookAndFeelToggler.setMoBIELaf();
-                loadViews( selectedFilePath );
-                MoBIELookAndFeelToggler.resetMoBIELaf();
-            }
-
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
+        }).start();
     }
 
     public void loadViews( String selectedFilePath ) throws IOException
diff --git a/src/main/java/org/embl/mobie/viewer/view/saving/SelectExistingViewFrame.java b/src/main/java/org/embl/mobie/viewer/view/saving/SelectExistingViewDialog.java
similarity index 52%
rename from src/main/java/org/embl/mobie/viewer/view/saving/SelectExistingViewFrame.java
rename to src/main/java/org/embl/mobie/viewer/view/saving/SelectExistingViewDialog.java
index 1454a3938..863546306 100644
--- a/src/main/java/org/embl/mobie/viewer/view/saving/SelectExistingViewFrame.java
+++ b/src/main/java/org/embl/mobie/viewer/view/saving/SelectExistingViewDialog.java
@@ -3,12 +3,10 @@
 import org.embl.mobie.viewer.Dataset;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.ui.MoBIELookAndFeelToggler;
-import org.embl.mobie.viewer.view.View;
 import org.embl.mobie.viewer.view.additionalviews.AdditionalViews;
 import de.embl.cba.tables.SwingUtils;
-import ij.IJ;
 import org.embl.mobie.viewer.projectcreator.ProjectCreatorHelper;
-import org.embl.mobie.viewer.ui.SwingHelper;
+import org.embl.mobie.viewer.ui.SwingHelpers;
 
 import javax.swing.*;
 import java.awt.*;
@@ -16,58 +14,45 @@
 import java.awt.event.ItemListener;
 import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
-import java.io.File;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Map;
 
-import static org.embl.mobie.viewer.projectcreator.ProjectCreatorHelper.getGroupToViewsMap;
-import static org.embl.mobie.viewer.ui.SwingHelper.createButton;
-import static org.embl.mobie.viewer.ui.SwingHelper.getJLabel;
-import static org.embl.mobie.viewer.view.saving.ViewSavingHelpers.writeAdditionalViewsJson;
-import static org.embl.mobie.viewer.view.saving.ViewSavingHelpers.writeDatasetJson;
-
-public class SelectExistingViewFrame extends JFrame {
+public class SelectExistingViewDialog {
 
     static { net.imagej.patcher.LegacyInjector.preinit(); }
 
     private JComboBox<String> groupsComboBox;
     private JComboBox<String> viewsComboBox;
-    private AdditionalViews additionalViews;
-    private Dataset dataset;
-    private View view;
-    private String jsonPath;
-    private ViewsSaver.ProjectSaveLocation saveLocation;
+    private JDialog dialog;
+
     private Map<String, ArrayList<String>> groupToViewsMap;
+    private String selectedView;
 
     // writing to dataset json
-    public SelectExistingViewFrame( Dataset dataset, View view, String jsonPath ) {
-        MoBIELookAndFeelToggler.setMoBIELaf();
-        this.dataset = dataset;
-        this.view = view;
-        this.jsonPath = jsonPath;
+    public SelectExistingViewDialog(Dataset dataset ) {
         groupToViewsMap = ProjectCreatorHelper.getGroupToViewsMap(dataset);
-        createPanels();
     }
 
     // write to additional views json
-    public SelectExistingViewFrame( AdditionalViews additionalViews, View view, String jsonPath ) {
-        MoBIELookAndFeelToggler.setMoBIELaf();
-        this.additionalViews = additionalViews;
-        this.view = view;
-        this.jsonPath = jsonPath;
+    public SelectExistingViewDialog(AdditionalViews additionalViews ) {
         groupToViewsMap = ProjectCreatorHelper.getGroupToViewsMap(additionalViews);
+    }
+
+    public String getSelectedView() {
+        MoBIELookAndFeelToggler.setMoBIELaf();
         createPanels();
+        return selectedView;
     }
 
     private void createPanels() {
-        this.setTitle( "Choose an existing view..." );
-        this.getContentPane().setLayout( new BoxLayout(this.getContentPane(), BoxLayout.Y_AXIS ) );
-        this.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
+        dialog = new JDialog((Frame)null, true);
+        dialog.setTitle( "Choose an existing view..." );
+        dialog.getContentPane().setLayout( new BoxLayout(dialog.getContentPane(), BoxLayout.Y_AXIS ) );
+        dialog.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
         createComboBoxes();
         createAcceptPanel();
         // reset swing laf when finished
-        this.addWindowListener( new WindowAdapter()
+        dialog.addWindowListener( new WindowAdapter()
         {
             @Override
             public void windowClosing(WindowEvent e)
@@ -76,9 +61,9 @@ public void windowClosing(WindowEvent e)
                 e.getWindow().dispose();
             }
         });
-        this.pack();
-        this.setLocationRelativeTo(null);
-        this.setVisible( true );
+        dialog.pack();
+        dialog.setLocationRelativeTo(null);
+        dialog.setVisible( true );
     }
 
     private void createComboBoxes() {
@@ -90,53 +75,37 @@ private void createComboBoxes() {
         groupsComboBox.addItemListener( new SyncGroupAndViewComboBox() );
 
         JPanel groupPanel = SwingUtils.horizontalLayoutPanel();
-        groupPanel.add(SwingHelper.getJLabel("Ui selection group", 120, 10));
+        groupPanel.add( SwingHelpers.getJLabel("Ui selection group", 120, 10));
         groupPanel.add( groupsComboBox );
         JPanel viewPanel = SwingUtils.horizontalLayoutPanel();
-        viewPanel.add(SwingHelper.getJLabel("View name", 120, 10));
+        viewPanel.add( SwingHelpers.getJLabel("View name", 120, 10));
         viewPanel.add( viewsComboBox );
 
-        this.getContentPane().add( groupPanel );
-        this.getContentPane().add( viewPanel );
+        dialog.getContentPane().add( groupPanel );
+        dialog.getContentPane().add( viewPanel );
     }
 
     private void createAcceptPanel() {
         JPanel acceptPanel = SwingUtils.horizontalLayoutPanel();
-        JButton selectButton = SwingHelper.createButton("Select");
+        JButton selectButton = SwingHelpers.createButton("Select");
         selectButton.addActionListener( e ->
         {
             new Thread( () -> {
-                this.setVisible( false );
-                String selectedView = (String) viewsComboBox.getSelectedItem();
-                if ( dataset != null ) {
-                    try {
-                        writeDatasetJson( dataset, view, selectedView, jsonPath );
-                        IJ.log( selectedView + " overwritten in dataset.json" );
-                    } catch (IOException ioException) {
-                        ioException.printStackTrace();
-                    }
-                } else if ( additionalViews != null ) {
-                    try {
-                        writeAdditionalViewsJson( additionalViews, view, selectedView, jsonPath );
-                        IJ.log( selectedView + " overwritten in " + new File(jsonPath).getName() );
-                    } catch (IOException ioException) {
-                        ioException.printStackTrace();
-                    }
-                }
-                this.dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING));
-
+                selectedView = (String) viewsComboBox.getSelectedItem();
+                dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING));
             } ).start();
         } );
-        JButton cancelButton = SwingHelper.createButton("Cancel");
+        JButton cancelButton = SwingHelpers.createButton("Cancel");
         cancelButton.addActionListener( e ->
         {
             new Thread( () -> {
-                this.dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING));
+                selectedView = null;
+                dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING));
             } ).start();
         } );
         acceptPanel.add( selectButton );
         acceptPanel.add( cancelButton );
-        this.getContentPane().add( acceptPanel );
+        dialog.getContentPane().add( acceptPanel );
     }
 
 
diff --git a/src/main/java/org/embl/mobie/viewer/view/saving/ViewSavingHelpers.java b/src/main/java/org/embl/mobie/viewer/view/saving/ViewSavingHelpers.java
index 8e57d10c6..22303965f 100644
--- a/src/main/java/org/embl/mobie/viewer/view/saving/ViewSavingHelpers.java
+++ b/src/main/java/org/embl/mobie/viewer/view/saving/ViewSavingHelpers.java
@@ -24,8 +24,7 @@ public static void writeDatasetJson(Dataset dataset, View view, String viewName,
         }
     }
 
-    public static void writeAdditionalViewsJson( AdditionalViews additionalViews, View view, String viewName,
-                                                 String additionalViewsJsonPath ) throws IOException {
+    public static void writeAdditionalViewsJson( AdditionalViews additionalViews, View view, String viewName, String additionalViewsJsonPath ) throws IOException {
         if ( viewName != null ) {
             additionalViews.views.put(viewName, view);
 
diff --git a/src/main/java/org/embl/mobie/viewer/view/saving/ViewsSaver.java b/src/main/java/org/embl/mobie/viewer/view/saving/ViewsSaver.java
index dea22b044..bce847a64 100644
--- a/src/main/java/org/embl/mobie/viewer/view/saving/ViewsSaver.java
+++ b/src/main/java/org/embl/mobie/viewer/view/saving/ViewsSaver.java
@@ -11,17 +11,20 @@
 import ij.IJ;
 import ij.gui.GenericDialog;
 import org.apache.commons.io.FilenameUtils;
-import org.embl.mobie.viewer.Utils;
+import org.embl.mobie.viewer.MoBIEUtils;
 import org.embl.mobie.viewer.projectcreator.ProjectCreatorHelper;
-import org.embl.mobie.viewer.ui.UserInterfaceHelper;
+import org.embl.mobie.viewer.ui.UserInterfaceHelpers;
 
 import java.io.*;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.Map;
 
-import static de.embl.cba.tables.FileAndUrlUtils.getFileNames;
-import static de.embl.cba.tables.S3Utils.isS3;
-import static de.embl.cba.tables.github.GitHubUtils.isGithub;
+import static org.embl.mobie.io.github.GitHubUtils.isGithub;
+import static org.embl.mobie.io.util.FileAndUrlUtils.getFileNames;
+import static org.embl.mobie.io.util.S3Utils.isS3;
+import static org.embl.mobie.viewer.view.saving.ViewSavingHelpers.writeAdditionalViewsJson;
+import static org.embl.mobie.viewer.view.saving.ViewSavingHelpers.writeDatasetJson;
 
 public class ViewsSaver {
 
@@ -50,13 +53,13 @@ public void saveCurrentSettingsAsViewDialog() {
 
         String[] choices = new String[]{ "Save as new view", "Overwrite existing view" };
         gd.addChoice("Save method:", choices, choices[0] );
-        gd.addChoice("Save to", new String[]{ Utils.FileLocation.Project.toString(),
-                Utils.FileLocation.FileSystem.toString()}, Utils.FileLocation.Project.toString());
+        gd.addChoice("Save to", new String[]{ MoBIEUtils.FileLocation.Project.toString(),
+                MoBIEUtils.FileLocation.FileSystem.toString()}, MoBIEUtils.FileLocation.Project.toString());
         gd.showDialog();
 
         if (!gd.wasCanceled()) {
             String saveMethodString = gd.getNextChoice();
-            Utils.FileLocation fileLocation = Utils.FileLocation.valueOf(gd.getNextChoice());
+            MoBIEUtils.FileLocation fileLocation = MoBIEUtils.FileLocation.valueOf(gd.getNextChoice());
 
             SaveMethod saveMethod;
             if (saveMethodString.equals("Save as new view")) {
@@ -70,7 +73,7 @@ public void saveCurrentSettingsAsViewDialog() {
 
     }
 
-    public void viewSettingsDialog( SaveMethod saveMethod, Utils.FileLocation fileLocation ) {
+    public void viewSettingsDialog( SaveMethod saveMethod, MoBIEUtils.FileLocation fileLocation ) {
         final GenericDialog gd = new GenericDialog("View settings");
 
         if ( saveMethod == SaveMethod.saveAsNewView ) {
@@ -85,7 +88,7 @@ public void viewSettingsDialog( SaveMethod saveMethod, Utils.FileLocation fileLo
         }
         gd.addChoice("Ui Selection Group", choices, choices[0]);
 
-        if ( fileLocation == Utils.FileLocation.Project ) {
+        if ( fileLocation == MoBIEUtils.FileLocation.Project ) {
             String[] jsonChoices = new String[]{"dataset.json", "views.json"};
             gd.addChoice("Save location:", jsonChoices, jsonChoices[0]);
         }
@@ -99,7 +102,7 @@ public void viewSettingsDialog( SaveMethod saveMethod, Utils.FileLocation fileLo
 
             String viewName = null;
             if( saveMethod == SaveMethod.saveAsNewView ) {
-                viewName = UserInterfaceHelper.tidyString( gd.getNextString() );
+                viewName = UserInterfaceHelpers.tidyString( gd.getNextString() );
                 if ( viewName == null ) {
                     return;
                 }
@@ -107,7 +110,7 @@ public void viewSettingsDialog( SaveMethod saveMethod, Utils.FileLocation fileLo
 
             String uiSelectionGroup = gd.getNextChoice();
             ProjectSaveLocation projectSaveLocation = null;
-            if ( fileLocation == Utils.FileLocation.Project ) {
+            if ( fileLocation == MoBIEUtils.FileLocation.Project ) {
                 String projectSaveLocationString = gd.getNextChoice();
                 if ( projectSaveLocationString.equals("dataset.json") ) {
                     projectSaveLocation =  ProjectSaveLocation.datasetJson;
@@ -127,13 +130,13 @@ public void viewSettingsDialog( SaveMethod saveMethod, Utils.FileLocation fileLo
             View currentView = moBIE.getViewManager().getCurrentView(uiSelectionGroup, exclusive, includeViewerTransform);
 
             if ( uiSelectionGroup != null && currentView != null ) {
-                if ( fileLocation == Utils.FileLocation.Project && saveMethod == SaveMethod.saveAsNewView ) {
+                if ( fileLocation == MoBIEUtils.FileLocation.Project && saveMethod == SaveMethod.saveAsNewView ) {
                     saveNewViewToProject( currentView, viewName, projectSaveLocation );
-                } else if ( fileLocation == Utils.FileLocation.Project && saveMethod == SaveMethod.overwriteExistingView ) {
+                } else if ( fileLocation == MoBIEUtils.FileLocation.Project && saveMethod == SaveMethod.overwriteExistingView ) {
                     overwriteExistingViewInProject( currentView, projectSaveLocation );
-                } else if ( fileLocation == Utils.FileLocation.FileSystem && saveMethod == SaveMethod.saveAsNewView ) {
+                } else if ( fileLocation == MoBIEUtils.FileLocation.FileSystem && saveMethod == SaveMethod.saveAsNewView ) {
                     saveNewViewToFileSystem( currentView, viewName );
-                } else if ( fileLocation == Utils.FileLocation.FileSystem && saveMethod == SaveMethod.overwriteExistingView ) {
+                } else if ( fileLocation == MoBIEUtils.FileLocation.FileSystem && saveMethod == SaveMethod.overwriteExistingView ) {
                     overwriteExistingViewOnFileSystem( currentView );
                 }
             }
@@ -141,7 +144,7 @@ public void viewSettingsDialog( SaveMethod saveMethod, Utils.FileLocation fileLo
     }
 
     private String chooseFileSystemJson() {
-        String jsonPath = Utils.selectSavePathFromFileSystem( "json" );
+        String jsonPath = MoBIEUtils.selectFilePath( "json", "json file", false );
 
         if ( jsonPath != null && !jsonPath.endsWith(".json") ) {
                 jsonPath += ".json";
@@ -151,46 +154,45 @@ private String chooseFileSystemJson() {
     }
 
     private void saveNewViewToFileSystem( View view, String viewName ) {
-        String jsonPath = chooseFileSystemJson();
-        if ( jsonPath != null ) {
-            try {
-                saveNewViewToAdditionalViewsJson( view, viewName, jsonPath );
-            } catch (IOException e) {
-                e.printStackTrace();
+        new Thread( () -> {
+            String jsonPath = chooseFileSystemJson();
+            if ( jsonPath != null ) {
+                try {
+                    saveNewViewToAdditionalViewsJson( view, viewName, jsonPath );
+                    addViewToUi( viewName, view );
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
             }
-        }
+        }).start();
     }
 
     private void overwriteExistingViewOnFileSystem( View view ) {
-        String jsonPath = chooseFileSystemJson();
-        if ( jsonPath != null ) {
-            try {
-                overwriteExistingViewInAdditionalViewsJson( view, jsonPath );
-            } catch (IOException e) {
-                e.printStackTrace();
+        new Thread( () -> {
+            String jsonPath = chooseFileSystemJson();
+            if ( jsonPath != null ) {
+                try {
+                    overwriteExistingViewInAdditionalViewsJson( view, jsonPath );
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
             }
-        }
+        }).start();
     }
 
     private void saveNewViewToProject( View view, String viewName, ProjectSaveLocation projectSaveLocation ) {
-        if ( isS3(settings.values.getProjectLocation()) ) {
-            // TODO - support saving views to s3?
-            IJ.log("View saving aborted - saving directly to s3 is not yet supported!");
-        } else {
-
-            try {
-                if (projectSaveLocation == ProjectSaveLocation.datasetJson) {
-                    saveNewViewToDatasetJson( view, viewName );
-                } else {
-                    String viewJsonPath = chooseAdditionalViewsJson( true );
-                    if (viewJsonPath != null) {
-                        saveNewViewToAdditionalViewsJson( view, viewName, viewJsonPath);
-                    }
+        try {
+            if (projectSaveLocation == ProjectSaveLocation.datasetJson) {
+                saveViewToDatasetJson( view, viewName, false );
+            } else {
+                String viewJsonPath = chooseAdditionalViewsJson( true );
+                if (viewJsonPath != null) {
+                    saveNewViewToAdditionalViewsJson( view, viewName, viewJsonPath);
                 }
-            } catch (IOException e) {
-                e.printStackTrace();
             }
-
+            addViewToUi( viewName, view );
+        } catch (IOException e) {
+            e.printStackTrace();
         }
     }
 
@@ -216,17 +218,25 @@ private void overwriteExistingViewInProject( View view, ProjectSaveLocation proj
         }
     }
 
-    private void saveNewViewToDatasetJson( View view, String viewName ) throws IOException {
+    private void addViewToUi( String viewName, View view ) {
+        moBIE.getViews().put( viewName, view );
+
+        Map<String, View> views = new HashMap<>();
+        views.put( viewName, view );
+        moBIE.getUserInterface().addViews( views );
+    }
+
+    public void saveViewToDatasetJson( View view, String viewName, boolean overwrite ) throws IOException
+    {
         String datasetJsonPath = moBIE.getDatasetPath( "dataset.json");
         Dataset dataset = new DatasetJsonParser().parseDataset( datasetJsonPath );
 
-        if ( dataset.views.keySet().size() > 0 && dataset.views.containsKey( viewName ) ) {
-                IJ.log( "View saving aborted - this view name already exists!" );
-                return;
-        }
+        if ( ! overwrite )
+            if ( dataset.views.containsKey( viewName ) )
+                throw new IOException( "View saving aborted - this view name already exists!" );
 
         ViewSavingHelpers.writeDatasetJson( dataset, view, viewName, datasetJsonPath );
-        IJ.log( "New view, " + viewName + ", written to dataset.json" );
+        IJ.log( " View, " + viewName + ", written to dataset.json" );
     }
 
     private void overwriteExistingViewInDatasetJson( View view ) throws IOException {
@@ -234,7 +244,12 @@ private void overwriteExistingViewInDatasetJson( View view ) throws IOException
         Dataset dataset = new DatasetJsonParser().parseDataset( datasetJsonPath );
 
         if ( dataset.views.keySet().size() > 0 ) {
-            new SelectExistingViewFrame( dataset, view, datasetJsonPath );
+            String selectedView = new SelectExistingViewDialog( dataset ).getSelectedView();
+            if ( selectedView != null ) {
+                writeDatasetJson( dataset, view, selectedView, datasetJsonPath );
+                IJ.log( selectedView + " overwritten in dataset.json" );
+                addViewToUi( selectedView, view );
+            }
         } else {
             IJ.log( "View saving aborted - dataset.json contains no views" );
         }
@@ -256,7 +271,13 @@ private void overwriteExistingViewInAdditionalViewsJson( View view, String jsonP
         }
 
         AdditionalViews additionalViews = new AdditionalViewsJsonParser().getViews( jsonPath );
-        new SelectExistingViewFrame( additionalViews, view, jsonPath );
+        String selectedView = new SelectExistingViewDialog( additionalViews ).getSelectedView();
+
+        if ( selectedView != null ) {
+            writeAdditionalViewsJson( additionalViews, view, selectedView, jsonPath );
+            IJ.log( selectedView + " overwritten in " + new File(jsonPath).getName() );
+            addViewToUi( selectedView, view );
+        }
     }
 
     private void saveNewViewToAdditionalViewsJson( View view, String viewName, String jsonPath ) throws IOException {
@@ -331,7 +352,7 @@ private String makeNewViewFile( String[] existingViewFiles ) {
 
         // get rid of any spaces, warn for unusual characters in basename (without the .json)
         if ( viewFileName != null ) {
-            viewFileName = UserInterfaceHelper.tidyString( viewFileName);
+            viewFileName = UserInterfaceHelpers.tidyString( viewFileName);
         }
 
         if ( viewFileName != null ) {
diff --git a/src/main/java/org/embl/mobie/viewer/volume/ImageVolumeViewer.java b/src/main/java/org/embl/mobie/viewer/volume/ImageVolumeViewer.java
new file mode 100644
index 000000000..6bc62280f
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/volume/ImageVolumeViewer.java
@@ -0,0 +1,391 @@
+/*-
+ * #%L
+ * Various Java code for ImageJ
+ * %%
+ * Copyright (C) 2018 - 2021 EMBL
+ * %%
+ * 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 org.embl.mobie.viewer.volume;
+
+import bdv.viewer.Source;
+import bdv.viewer.SourceAndConverter;
+import de.embl.cba.tables.Logger;
+import de.embl.cba.tables.color.ColorUtils;
+import de.embl.cba.tables.imagesegment.ImageSegment;
+import de.embl.cba.util.CopyUtils;
+import ij.IJ;
+import ij.ImagePlus;
+import ij.Prefs;
+import ij3d.Content;
+import ij3d.ContentConstants;
+import ij3d.Image3DUniverse;
+import ij3d.UniverseListener;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.converter.RealUnsignedByteConverter;
+import net.imglib2.display.ColorConverter;
+import net.imglib2.img.display.imagej.ImageJFunctions;
+import net.imglib2.type.NativeType;
+import net.imglib2.type.numeric.ARGBType;
+import net.imglib2.type.numeric.RealType;
+import net.imglib2.type.numeric.integer.UnsignedByteType;
+import net.imglib2.type.numeric.integer.UnsignedShortType;
+import net.imglib2.util.Util;
+import net.imglib2.view.Views;
+import org.embl.mobie.viewer.VisibilityListener;
+import org.embl.mobie.viewer.mesh.MeshCreator;
+import org.embl.mobie.viewer.playground.BdvPlaygroundUtils;
+import org.scijava.java3d.View;
+import org.scijava.vecmath.Color3f;
+
+import java.awt.*;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import static de.embl.cba.bdv.utils.BdvUtils.getLevel;
+import static de.embl.cba.tables.Utils.getVoxelSpacings;
+
+public class ImageVolumeViewer
+{
+	static { net.imagej.patcher.LegacyInjector.preinit(); }
+
+	private final Map< String, SourceAndConverter< ? > > sourceAndConverters;
+	private final UniverseManager universeManager;
+
+	private ConcurrentHashMap< SourceAndConverter, Content > sacToContent;
+	private ConcurrentHashMap< Content, SourceAndConverter > contentToSac;
+	private boolean showImages;
+
+	private int meshSmoothingIterations;
+	private long maxNumVoxels;
+	private double[] voxelSpacing; // desired voxel spacings; null = auto => use maxNumVoxels
+
+	private float transparency = 0.0F;
+	private int currentTimePoint = 0;
+	private final MeshCreator< ImageSegment > meshCreator;
+	private List< VisibilityListener > listeners = new ArrayList<>(  );
+	private Window window;
+	private Image3DUniverse universe;
+
+	public ImageVolumeViewer(
+			final Map< String, SourceAndConverter< ? > > sourceAndConverters,
+			UniverseManager universeManager )
+	{
+		this.sourceAndConverters = sourceAndConverters;
+		this.universeManager = universeManager;
+
+		this.meshSmoothingIterations = 5;
+		this.maxNumVoxels = 100 * 100 * 100;
+		this.sacToContent = new ConcurrentHashMap<>();
+		this.contentToSac = new ConcurrentHashMap<>();
+		this.meshCreator = new MeshCreator<>( meshSmoothingIterations, maxNumVoxels );
+	}
+
+	public void setMaxNumVoxels( long maxNumVoxels )
+	{
+		this.maxNumVoxels = maxNumVoxels;
+	}
+
+	public void updateView()
+	{
+		if ( universe == null ) return;
+
+		for ( SourceAndConverter< ? > sac : sourceAndConverters.values() )
+		{
+			if ( sacToContent.containsKey( sac ) )
+			{
+				final Content content = sacToContent.get( sac );
+				universe.removeContent( content.getName() );
+				sacToContent.remove( sac );
+				if ( content.isVisible() )
+					addSourceToUniverse( sac );
+			}
+		}
+	}
+
+	public synchronized void showImages( boolean show )
+	{
+		this.showImages = show;
+
+		if ( showImages && universe == null )
+		{
+			initUniverse();
+		}
+
+		for ( SourceAndConverter< ? > sac : sourceAndConverters.values() )
+		{
+			if ( sacToContent.containsKey( sac ) )
+			{
+				sacToContent.get( sac ).setVisible( show );
+			}
+			else
+			{
+				if ( show )
+				{
+					addSourceToUniverse( sac );
+				}
+			}
+		}
+	}
+
+	private void addSourceToUniverse( SourceAndConverter< ? > sac )
+	{
+		final int[] contrastLimits = getContrastLimits( sac );
+		final ARGBType color = ( ( ColorConverter ) sac.getConverter() ).getColor();
+		final Content content = addSourceToUniverse( universe, sac.getSpimSource(), voxelSpacing, maxNumVoxels, ContentConstants.SURFACE, color, transparency, contrastLimits[ 0 ], contrastLimits[ 1 ] );
+		sacToContent.put( sac, content );
+	}
+
+	public static < R extends RealType< R > > Content addSourceToUniverse(
+			Image3DUniverse universe,
+			Source< ? > source,
+			double[] voxelSpacing,
+			long maxNumVoxels,
+			int displayType,
+			ARGBType argbType,
+			float transparency,
+			int min,
+			int max )
+	{
+		Integer level;
+		if ( voxelSpacing != null )
+			level = BdvPlaygroundUtils.getLevel( source, voxelSpacing );
+		else
+			level = BdvPlaygroundUtils.getLevel( source, maxNumVoxels );
+
+		logVoxelSpacing( source, getVoxelSpacings( source ).get( level ) );
+
+		if ( level == null )
+		{
+			Logger.warn( "Image is too large to be displayed in 3D." );
+			return null;
+		}
+
+		if ( universe == null )
+		{
+			Logger.warn( "No Universe exists => Cannot show volume." );
+			return null;
+		}
+
+		if ( universe.getWindow() == null )
+		{
+			Logger.warn( "No Universe window exists => Cannot show volume." );
+			return null;
+		}
+
+		final ImagePlus wrap = createUnsignedByteImagePlus( source, min, max, level );
+		final Content content = universe.addContent( wrap, displayType );
+		content.setTransparency( transparency );
+		content.setLocked( true );
+		content.setColor( new Color3f( ColorUtils.getColor( argbType ) ) );
+		universe.setAutoAdjustView( false );
+		return content;
+	}
+
+	public static void logVoxelSpacing( Source< ? > source, double[] voxelSpacings )
+	{
+		IJ.log( "3D View: Fetching source " + source.getName() + " at " + Arrays.stream( voxelSpacings ).mapToObj( x -> "" + x ).collect( Collectors.joining( ", " ) ) + " micrometer..." );
+	}
+
+	public static < R extends RealType< R > > Content addSourceToUniverse(
+			Image3DUniverse universe,
+			Source< ? > source,
+			double voxelSpacing,
+			int displayType,
+			ARGBType argbType,
+			float transparency,
+			int min,
+			int max )
+	{
+		final Integer level = getLevel( source, voxelSpacing );
+		System.out.println( "3D View: Fetching source " + source.getName() + " at resolution " + voxelSpacing + " micrometer..." );
+		final ImagePlus wrap = createUnsignedByteImagePlus( source, min, max, level );
+		final Content content = universe.addContent( wrap, displayType );
+		content.setTransparency( transparency );
+		content.setLocked( true );
+		content.setColor( new Color3f( ColorUtils.getColor( argbType ) ) );
+		universe.setAutoAdjustView( false );
+		return content;
+	}
+
+	private static < R extends RealType< R > & NativeType< R > > ImagePlus createUnsignedByteImagePlus( Source< ? > source, int min, int max, Integer level )
+	{
+		RandomAccessibleInterval< R > rai = ( RandomAccessibleInterval )  source.getSource( 0, level );
+
+		rai = CopyUtils.copyVolumeRaiMultiThreaded( rai, Prefs.getThreads() - 1  ); // TODO: make multi-threading configurable.
+
+		rai = Views.permute( Views.addDimension( rai, 0, 0 ), 2, 3 );
+
+		final ImagePlus wrap = ImageJFunctions.wrapUnsignedByte(
+				rai,
+				new RealUnsignedByteConverter< R >( min, max ),
+				source.getName() );
+
+		final double[] voxelSpacing = getVoxelSpacings( source ).get( level );
+		wrap.getCalibration().pixelWidth = voxelSpacing[ 0 ];
+		wrap.getCalibration().pixelHeight = voxelSpacing[ 1 ];
+		wrap.getCalibration().pixelDepth = voxelSpacing[ 2 ];
+
+		return wrap;
+	}
+
+
+	private int[] getContrastLimits( SourceAndConverter< ? > sac )
+	{
+		final Object type = Util.getTypeFromInterval( sac.getSpimSource().getSource( 0, 0 ) );
+		final int[] contrastLimits = new int[ 2 ];
+		contrastLimits[ 0 ] = 0;
+		if ( type instanceof UnsignedByteType )
+			contrastLimits[ 1 ] = 255;
+		else if ( type instanceof UnsignedShortType )
+			contrastLimits[ 1 ] = 65535;
+		else
+			throw new RuntimeException( "Volume view of image of type " + type + " is currently not supported.");
+		return contrastLimits;
+	}
+
+	private void initUniverse()
+	{
+		universe = universeManager.get();
+		window = universe.getWindow();
+		window.addWindowListener(
+			new WindowAdapter()
+			{
+				public void windowClosing( WindowEvent ev )
+				{
+					window = null;
+					universe = null;
+					sacToContent.clear();
+					contentToSac.clear();
+					showImages = false;
+					universeManager.setUniverse( null );
+					for ( VisibilityListener listener : listeners )
+					{
+						listener.visibility( false );
+					}
+				}
+			} );
+	}
+
+	private boolean addUniverseListener()
+	{
+		universe.addUniverseListener( new UniverseListener()
+		{
+
+			@Override
+			public void transformationStarted( View view )
+			{
+
+			}
+
+			@Override
+			public void transformationUpdated( View view )
+			{
+				// TODO: synchronize with BDV view could be nice...
+			}
+
+			@Override
+			public void transformationFinished( View view )
+			{
+
+			}
+
+			@Override
+			public void contentAdded( Content c )
+			{
+
+			}
+
+			@Override
+			public void contentRemoved( Content c )
+			{
+
+			}
+
+			@Override
+			public void contentChanged( Content c )
+			{
+
+			}
+
+			@Override
+			public void contentSelected( Content c )
+			{
+				if ( c == null ) return;
+
+				if ( ! contentToSac.containsKey( c ) )
+					return;
+
+				// Should we do anything?
+			}
+
+			@Override
+			public void canvasResized()
+			{
+
+			}
+
+			@Override
+			public void universeClosed()
+			{
+				for ( VisibilityListener listener : listeners )
+				{
+					listener.visibility( false );
+				}
+				window = null;
+				universe = null;
+			}
+		} );
+
+		return true;
+	}
+
+	public void setVoxelSpacing( double[] voxelSpacing )
+	{
+		this.voxelSpacing = voxelSpacing;
+	}
+
+	public double[] getVoxelSpacing()
+	{
+		return voxelSpacing;
+	}
+
+	public void close()
+	{
+		showImages( false );
+	}
+
+	public Collection< VisibilityListener > getListeners()
+	{
+		return listeners;
+	}
+
+
+}
diff --git a/src/main/java/org/embl/mobie/viewer/volume/SegmentsVolumeViewer.java b/src/main/java/org/embl/mobie/viewer/volume/SegmentsVolumeViewer.java
index 0495040d3..819271ff4 100644
--- a/src/main/java/org/embl/mobie/viewer/volume/SegmentsVolumeViewer.java
+++ b/src/main/java/org/embl/mobie/viewer/volume/SegmentsVolumeViewer.java
@@ -60,11 +60,8 @@
 
 public class SegmentsVolumeViewer< S extends ImageSegment > implements ColoringListener, SelectionListener< S >
 {
-
 	static { net.imagej.patcher.LegacyInjector.preinit(); }
 
-	public static String VOLUME_VIEW = "Volume view";
-
 	private final SelectionModel< S > selectionModel;
 	private final ColoringModel< S > coloringModel;
 	private final Collection< SourceAndConverter< ? > > sourceAndConverters;
@@ -79,7 +76,7 @@ public class SegmentsVolumeViewer< S extends ImageSegment > implements ColoringL
 	private double segmentFocusZoomLevel;
 	private double segmentFocusDxyMin;
 	private double segmentFocusDzMin;
-	private long maxNumSegmentVoxels;
+	private long maxNumVoxels;
 	private String objectsName;
 	private boolean showSegments = false;
 	private double[] voxelSpacing; // desired voxel spacings; null = auto
@@ -106,12 +103,12 @@ public SegmentsVolumeViewer(
 		this.segmentFocusZoomLevel = 0.8;
 		this.segmentFocusDxyMin = 20.0;
 		this.segmentFocusDzMin = 20.0;
-		this.maxNumSegmentVoxels = 100 * 100 * 100;
+		this.maxNumVoxels = 100 * 100 * 100;
 		this.objectsName = "";
 		this.segmentToContent = new ConcurrentHashMap<>();
 		this.contentToSegment = new ConcurrentHashMap<>();
 
-		this.meshCreator = new MeshCreator<>( meshSmoothingIterations, maxNumSegmentVoxels );
+		this.meshCreator = new MeshCreator<>( meshSmoothingIterations, maxNumVoxels );
 	}
 
 	public void setObjectsName( String objectsName )
@@ -152,9 +149,9 @@ public void setSegmentFocusDzMin( double segmentFocusDzMin )
 		this.segmentFocusDzMin = segmentFocusDzMin;
 	}
 
-	public void setMaxNumSegmentVoxels( long maxNumSegmentVoxels )
+	public void setMaxNumVoxels( long maxNumVoxels )
 	{
-		this.maxNumSegmentVoxels = maxNumSegmentVoxels;
+		this.maxNumVoxels = maxNumVoxels;
 	}
 
 	private void updateSegmentColors()


Test patch:
diff --git a/src/test/java/commands/OpenOMEZARRCommand.java b/src/test/java/commands/OpenOMEZARRCommand.java
index e82f3efb1..c924dc7ab 100644
--- a/src/test/java/commands/OpenOMEZARRCommand.java
+++ b/src/test/java/commands/OpenOMEZARRCommand.java
@@ -1,8 +1,8 @@
 package commands;
 
 import mpicbg.spim.data.SpimData;
-import org.embl.mobie.io.ome.zarr.OMEZarrViewer;
 import org.embl.mobie.io.ome.zarr.openers.OMEZarrOpener;
+import org.embl.mobie.viewer.view.OMEZarrViewer;
 
 import java.io.IOException;
 
diff --git a/src/test/java/commands/OpenOMEZARRFromS3Command.java b/src/test/java/commands/OpenOMEZARRFromS3Command.java
index cb2526dc6..657cd7034 100644
--- a/src/test/java/commands/OpenOMEZARRFromS3Command.java
+++ b/src/test/java/commands/OpenOMEZARRFromS3Command.java
@@ -1,8 +1,8 @@
 package commands;
 
 import mpicbg.spim.data.SpimData;
-import org.embl.mobie.io.ome.zarr.OMEZarrViewer;
 import org.embl.mobie.io.ome.zarr.openers.OMEZarrS3Opener;
+import org.embl.mobie.viewer.view.OMEZarrViewer;
 
 import java.io.IOException;
 
diff --git a/src/test/java/demo/DemoOpenOMEZARRFromS3WithCredentialsCommand.java b/src/test/java/demo/DemoOpenOMEZARRFromS3WithCredentialsCommand.java
new file mode 100644
index 000000000..b7d8c338b
--- /dev/null
+++ b/src/test/java/demo/DemoOpenOMEZARRFromS3WithCredentialsCommand.java
@@ -0,0 +1,15 @@
+package demo;
+
+import org.embl.mobie.viewer.command.OpenOMEZARRFromS3WithCredentialsCommand;
+
+public class DemoOpenOMEZARRFromS3WithCredentialsCommand
+{
+	public static void main( String[] args )
+	{
+		final OpenOMEZARRFromS3WithCredentialsCommand command = new OpenOMEZARRFromS3WithCredentialsCommand();
+		command.s3URL = "https://s3.embl.de/comulis/elena/images/bdv-ome-zarr/SXAA03648.ome.zarr";
+		command.s3AccessKey = "UYP3FNN3V5F0P86DR2O3";
+		command.s3SecretKey = "FIXME";
+		command.run();
+	}
+}
diff --git a/src/test/java/develop/DevelopNormalisedViewerTransforms.java b/src/test/java/develop/DevelopNormalisedViewerTransforms.java
index 292953700..554b15812 100644
--- a/src/test/java/develop/DevelopNormalisedViewerTransforms.java
+++ b/src/test/java/develop/DevelopNormalisedViewerTransforms.java
@@ -1,8 +1,8 @@
 package develop;
 
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.realtransform.Scale3D;
diff --git a/src/test/java/develop/DevelopOmeZarrWriting.java b/src/test/java/develop/DevelopOmeZarrWriting.java
index 63c4b1896..341ffca95 100644
--- a/src/test/java/develop/DevelopOmeZarrWriting.java
+++ b/src/test/java/develop/DevelopOmeZarrWriting.java
@@ -4,7 +4,6 @@
 import ij.IJ;
 import ij.ImagePlus;
 import org.embl.mobie.io.n5.util.DownsampleBlock;
-import org.embl.mobie.io.ome.zarr.writers.imgplus.WriteImgPlusToN5BdvOmeZarr;
 import org.embl.mobie.io.ome.zarr.writers.imgplus.WriteImgPlusToN5OmeZarr;
 import org.janelia.saalfeldlab.n5.GzipCompression;
 
@@ -17,7 +16,7 @@ public static void main( String[] args ) {
         new WriteImgPlusToN5OmeZarr().export(imp, "C:\\Users\\meechan\\Documents\\temp\\test_zarr_writing\\omezarr\\zyx.ome.zarr",
                 DownsampleBlock.DownsamplingMethod.Average, new GzipCompression());
 
-        new WriteImgPlusToN5BdvOmeZarr().export(imp, "C:\\Users\\meechan\\Documents\\temp\\test_zarr_writing\\omezarr\\bdvzyx.xml",
-                DownsampleBlock.DownsamplingMethod.Average, new GzipCompression());
+//        new WriteImgPlusToN5BdvOmeZarr().export(imp, "C:\\Users\\meechan\\Documents\\temp\\test_zarr_writing\\omezarr\\bdvzyx.xml",
+//                DownsampleBlock.DownsamplingMethod.Average, new GzipCompression());
     }
 }
diff --git a/src/test/java/develop/DevelopSourceNameBitEncoding.java b/src/test/java/develop/DevelopSourceNameBitEncoding.java
new file mode 100644
index 000000000..51c44e4e9
--- /dev/null
+++ b/src/test/java/develop/DevelopSourceNameBitEncoding.java
@@ -0,0 +1,42 @@
+package develop;
+
+import net.imglib2.type.numeric.integer.UnsignedIntType;
+import org.embl.mobie.viewer.SourceNameEncoder;
+
+public class DevelopSourceNameBitEncoding
+{
+	public static void main( String[] args )
+	{
+		final UnsignedIntType unsignedIntType = new UnsignedIntType();
+		unsignedIntType.set( 1024 );
+
+		final long label = unsignedIntType.get();
+		System.out.println(Long.toBinaryString((long)label));
+
+		final long image = 10;
+		final long l1 = image << 16;
+		System.out.println(l1);
+		System.out.println(Long.toBinaryString(l1));
+		final long both = label + ( image << 16 );
+		System.out.println(Long.toBinaryString(both));
+		// image
+		final long decodedImage = both >> 16;
+		System.out.println(Long.toBinaryString( decodedImage ));
+		System.out.println("image id: " + decodedImage);
+		// label
+		final long decodedLabel = both & 0x0000FFFF;
+		System.out.println(Long.toBinaryString( decodedLabel ));
+		System.out.println("label id: " + decodedLabel);
+
+		System.out.printf( "----------------------\n" );
+
+		SourceNameEncoder.addName( "hello" );
+		SourceNameEncoder.addName( "world" );
+		final UnsignedIntType labelIndex = new UnsignedIntType( 133 );
+		SourceNameEncoder.encodeName( labelIndex, "hello" );
+		System.out.println(labelIndex.get());
+		System.out.println("name: " + SourceNameEncoder.getName( labelIndex ));
+		System.out.println("value: " + SourceNameEncoder.getValue( labelIndex ));
+
+	}
+}
diff --git a/src/test/java/develop/ExploreAWSS3API.java b/src/test/java/develop/ExploreAWSS3API.java
new file mode 100644
index 000000000..dba959e6a
--- /dev/null
+++ b/src/test/java/develop/ExploreAWSS3API.java
@@ -0,0 +1,89 @@
+package develop;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.client.builder.AwsClientBuilder;
+import com.amazonaws.regions.Regions;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3ClientBuilder;
+import com.amazonaws.services.s3.model.Bucket;
+import com.amazonaws.services.s3.model.GetObjectRequest;
+import com.amazonaws.services.s3.model.ListObjectsV2Request;
+import com.amazonaws.services.s3.model.ListObjectsV2Result;
+import com.amazonaws.services.s3.model.ObjectListing;
+import com.amazonaws.services.s3.model.S3Object;
+import com.amazonaws.services.s3.model.S3ObjectSummary;
+import ij.gui.GenericDialog;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.List;
+
+public class ExploreAWSS3API
+{
+	public static void main( String[] args ) throws IOException
+	{
+		// Get credentials
+		final GenericDialog dialog = new GenericDialog( "" );
+		dialog.addStringField( "accessKey", "" );
+		dialog.addStringField( "secretKey", "" );
+		dialog.showDialog();
+		if (!dialog.wasCanceled());
+		final String accessKey = dialog.getNextString();
+		final String secretKey = dialog.getNextString();
+
+
+		final String serviceEndpoint = "https://s3.embl.de";
+		final String signingRegion = "us-west-2";
+
+		final AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration( serviceEndpoint, signingRegion );
+
+		final BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
+		final AWSStaticCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
+		final AmazonS3 s3 = AmazonS3ClientBuilder
+				.standard()
+				.withCredentials(credentialsProvider)
+				.withEndpointConfiguration(endpointConfiguration)
+				.withPathStyleAccessEnabled(true)
+				.build();
+
+		// List buckets
+		List<Bucket> buckets = s3.listBuckets();
+		System.out.println("Your buckets are:");
+		for (Bucket bucket : buckets) {
+			System.out.println("* " + bucket.getName());
+		}
+
+		// List objects in bucket
+		ListObjectsV2Request request = new ListObjectsV2Request()
+				.withBucketName("comulis")
+				.withPrefix("")
+				.withDelimiter("/");
+		final ListObjectsV2Result result = s3.listObjectsV2( request );
+		final List< S3ObjectSummary > objectSummaries = result.getObjectSummaries();
+
+		for (S3ObjectSummary objectSummary : objectSummaries) {
+			System.out.printf(" - %s (size: %d)\n", objectSummary.getKey(), objectSummary.getSize());
+		}
+
+		// Download object
+		final S3Object s3Object = s3.getObject( new GetObjectRequest( "comulis", "test.txt" ) );
+		System.out.println("Content-Type: " + s3Object.getObjectMetadata().getContentType());
+		System.out.println("Content: ");
+		displayTextInputStream(s3Object.getObjectContent());
+	}
+
+	private static void displayTextInputStream( InputStream input) throws IOException
+	{
+		// Read the text input stream one line at a time and display each line.
+		BufferedReader reader = new BufferedReader(new InputStreamReader(input));
+		String line = null;
+		while ((line = reader.readLine()) != null) {
+			System.out.println(line);
+		}
+		System.out.println();
+	}
+}
diff --git a/src/test/java/develop/LAF.java b/src/test/java/develop/LAF.java
index 0e0960eb5..5e7ecba98 100644
--- a/src/test/java/develop/LAF.java
+++ b/src/test/java/develop/LAF.java
@@ -1,7 +1,5 @@
 package develop;
 
-import org.embl.mobie.viewer.ui.UserInterfaceHelper;
-
 import javax.swing.*;
 
 public class LAF
diff --git a/src/test/java/org/embl/mobie/viewer/projectcreator/DatasetsCreatorTest.java b/src/test/java/org/embl/mobie/viewer/projectcreator/DatasetsCreatorTest.java
index a5d9ce27b..03013ced3 100644
--- a/src/test/java/org/embl/mobie/viewer/projectcreator/DatasetsCreatorTest.java
+++ b/src/test/java/org/embl/mobie/viewer/projectcreator/DatasetsCreatorTest.java
@@ -1,8 +1,13 @@
 package org.embl.mobie.viewer.projectcreator;
 
+import de.embl.cba.tables.FileAndUrlUtils;
+import org.embl.mobie.viewer.Dataset;
 import org.embl.mobie.viewer.Project;
+import org.embl.mobie.viewer.serialize.DatasetJsonParser;
 import org.embl.mobie.viewer.serialize.ProjectJsonParser;
 import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 
 import java.io.File;
 import java.io.IOException;
@@ -15,16 +20,16 @@ class DatasetsCreatorTest {
     private ProjectCreator projectCreator;
     private DatasetsCreator datasetsCreator;
 
-    @org.junit.jupiter.api.BeforeEach
+    @BeforeEach
     void setUp( @TempDir Path tempDir ) throws IOException {
         projectCreator = new ProjectCreator( tempDir.toFile() );
         datasetsCreator = projectCreator.getDatasetsCreator();
     }
 
-    @org.junit.jupiter.api.Test
+    @Test
     void addDataset() throws IOException {
         String datasetName = "test";
-        datasetsCreator.addDataset(datasetName);
+        datasetsCreator.addDataset(datasetName, false);
 
         Project project = new ProjectJsonParser().parseProject( projectCreator.getProjectJson().getAbsolutePath() );
         assertEquals(project.getDatasets().size(), 1);
@@ -32,11 +37,11 @@ void addDataset() throws IOException {
         assertTrue(new File(projectCreator.getDataLocation(), datasetName).exists());
     }
 
-    @org.junit.jupiter.api.Test
+    @Test
     void renameDataset() throws IOException {
         String oldDatasetName = "test";
         String newDatasetName = "newName";
-        datasetsCreator.addDataset(oldDatasetName);
+        datasetsCreator.addDataset(oldDatasetName, false);
         datasetsCreator.renameDataset(oldDatasetName, newDatasetName);
 
         Project project = new ProjectJsonParser().parseProject( projectCreator.getProjectJson().getAbsolutePath() );
@@ -46,12 +51,12 @@ void renameDataset() throws IOException {
         assertFalse(new File(projectCreator.getDataLocation(), oldDatasetName).exists());
     }
 
-    @org.junit.jupiter.api.Test
+    @Test
     void makeDefaultDataset() throws IOException {
         String dataset1Name = "dataset1";
         String dataset2Name = "dataset2";
-        datasetsCreator.addDataset(dataset1Name);
-        datasetsCreator.addDataset(dataset2Name);
+        datasetsCreator.addDataset(dataset1Name, false);
+        datasetsCreator.addDataset(dataset2Name, false);
 
         Project project;
         project = new ProjectJsonParser().parseProject( projectCreator.getProjectJson().getAbsolutePath() );
@@ -62,4 +67,20 @@ void makeDefaultDataset() throws IOException {
         project = new ProjectJsonParser().parseProject( projectCreator.getProjectJson().getAbsolutePath() );
         assertEquals( project.getDefaultDataset(), dataset2Name );
     }
+
+    @Test
+    void makeDataset2D() throws IOException {
+        String datasetName = "test";
+        String datasetJsonPath = FileAndUrlUtils.combinePath( projectCreator.getDataLocation().getAbsolutePath(),
+                datasetName, "dataset.json" );
+
+        Dataset dataset;
+        datasetsCreator.addDataset(datasetName, false);
+        dataset = new DatasetJsonParser().parseDataset( datasetJsonPath );
+        assertFalse( dataset.is2D );
+
+        datasetsCreator.makeDataset2D(datasetName, true);
+        dataset = new DatasetJsonParser().parseDataset( datasetJsonPath );
+        assertTrue( dataset.is2D );
+    }
 }
\ No newline at end of file
diff --git a/src/test/java/org/embl/mobie/viewer/projectcreator/ImagesCreatorTest.java b/src/test/java/org/embl/mobie/viewer/projectcreator/ImagesCreatorTest.java
index f85d2991b..15483bf64 100644
--- a/src/test/java/org/embl/mobie/viewer/projectcreator/ImagesCreatorTest.java
+++ b/src/test/java/org/embl/mobie/viewer/projectcreator/ImagesCreatorTest.java
@@ -1,12 +1,11 @@
 package org.embl.mobie.viewer.projectcreator;
 
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.io.n5.util.DownsampleBlock;
 import org.embl.mobie.io.n5.writers.WriteImgPlusToN5;
-import org.embl.mobie.io.ome.zarr.writers.imgplus.WriteImgPlusToN5BdvOmeZarr;
 import org.embl.mobie.io.ome.zarr.writers.imgplus.WriteImgPlusToN5OmeZarr;
 import org.embl.mobie.viewer.Dataset;
 import org.embl.mobie.viewer.serialize.DatasetJsonParser;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import org.embl.mobie.viewer.source.SegmentationSource;
 import org.embl.mobie.viewer.table.TableDataFormat;
 
@@ -39,7 +38,6 @@ class ImagesCreatorTest {
     private String imageName;
     private String datasetName;
     private AffineTransform3D sourceTransform;
-    private boolean useDefaultSettings;
     private String uiSelectionGroup;
     private String datasetJsonPath;
     private File tempDir;
@@ -54,15 +52,14 @@ void setUp( @TempDir Path tempDir ) throws IOException {
         datasetName = "test";
         uiSelectionGroup = "testGroup";
         sourceTransform = new AffineTransform3D();
-        useDefaultSettings = true;
 
-        projectCreator.getDatasetsCreator().addDataset(datasetName);
+        projectCreator.getDatasetsCreator().addDataset(datasetName, false);
 
         datasetJsonPath = FileAndUrlUtils.combinePath( projectCreator.getDataLocation().getAbsolutePath(),
                 datasetName, "dataset.json" );
     }
 
-    void assertionsForImageAdded(ImageDataFormat imageDataFormat, boolean onlyXmls ) throws IOException {
+    void assertionsForImageAdded( ImageDataFormat imageDataFormat, boolean onlyXmls ) throws IOException {
         assertTrue( new File(datasetJsonPath).exists() );
 
         List<String> filePaths = new ArrayList<>();
@@ -76,13 +73,6 @@ void assertionsForImageAdded(ImageDataFormat imageDataFormat, boolean onlyXmls )
                         datasetName, "images", ProjectCreatorHelper.imageFormatToFolderName(imageDataFormat), imageName + ".n5");
                 break;
 
-            case BdvOmeZarr:
-                xmlLocation = FileAndUrlUtils.combinePath(projectCreator.getDataLocation().getAbsolutePath(),
-                        datasetName, "images", ProjectCreatorHelper.imageFormatToFolderName(imageDataFormat), imageName + ".xml");
-                imageLocation = FileAndUrlUtils.combinePath(projectCreator.getDataLocation().getAbsolutePath(),
-                        datasetName, "images", ProjectCreatorHelper.imageFormatToFolderName(imageDataFormat), imageName + ".ome.zarr");
-                break;
-
             case OmeZarr:
                 imageLocation = FileAndUrlUtils.combinePath(projectCreator.getDataLocation().getAbsolutePath(),
                         datasetName, "images", ProjectCreatorHelper.imageFormatToFolderName(imageDataFormat), imageName + ".ome.zarr");
@@ -107,7 +97,7 @@ void assertionsForImageAdded(ImageDataFormat imageDataFormat, boolean onlyXmls )
         assertTrue( dataset.sources.get(imageName).get().imageData.containsKey(imageDataFormat) );
     }
 
-    void assertionsForTableAdded( ImageDataFormat imageDataFormat ) throws IOException {
+    void assertionsForTableAdded( ) throws IOException {
         String tablePath = FileAndUrlUtils.combinePath( projectCreator.getDataLocation().getAbsolutePath(), datasetName, "tables", imageName, "default.tsv" );
         assertTrue( new File(tablePath).exists() );
 
@@ -116,9 +106,9 @@ void assertionsForTableAdded( ImageDataFormat imageDataFormat ) throws IOExcepti
         assertTrue( segmentationSource.tableData.containsKey(TableDataFormat.TabDelimitedFile) );
     }
 
-    String writeImageAndGetPath( ImageDataFormat imageDataFormat ) {
+    String writeImageAndGetPath( ImageDataFormat imageDataFormat, boolean is2D ) {
         // save example image for testing adding bdv format images
-        ImagePlus imp = makeImage( imageName );
+        ImagePlus imp = makeImage( imageName, is2D );
         DownsampleBlock.DownsamplingMethod downsamplingMethod = DownsampleBlock.DownsamplingMethod.Average;
         Compression compression = new GzipCompression();
         String filePath;
@@ -131,12 +121,6 @@ String writeImageAndGetPath( ImageDataFormat imageDataFormat ) {
                         compression, new String[]{imageName} );
                 break;
 
-            case BdvOmeZarr:
-                filePath = new File(tempDir, imageName + ".xml").getAbsolutePath();
-                new WriteImgPlusToN5BdvOmeZarr().export(imp, filePath, sourceTransform,
-                        downsamplingMethod, compression, new String[]{imageName} );
-                break;
-
             case OmeZarr:
                 filePath = new File(tempDir, imageName + ".ome.zarr").getAbsolutePath();
                 new WriteImgPlusToN5OmeZarr().export(imp, filePath, sourceTransform,
@@ -151,129 +135,134 @@ String writeImageAndGetPath( ImageDataFormat imageDataFormat ) {
         return filePath;
     }
 
-    void testAddingImageInCertainFormat( ImageDataFormat imageDataFormat ) throws IOException {
-
+    void addImageInCertainFormat( ImageDataFormat imageDataFormat, boolean is2D ) throws SpimDataException, IOException {
         // make an image with random values, same size as the imagej sample head image
-        ImagePlus imp = makeImage( imageName );
+        ImagePlus imp = makeImage( imageName, is2D );
 
         imagesCreator.addImage( imp, imageName, datasetName,
                 imageDataFormat, ProjectCreator.ImageType.image,
-                sourceTransform, useDefaultSettings, uiSelectionGroup );
+                sourceTransform, uiSelectionGroup, false );
+    }
 
+    void testAddingImageInCertainFormat( ImageDataFormat imageDataFormat, boolean is2D ) throws IOException, SpimDataException {
+        addImageInCertainFormat( imageDataFormat, is2D );
         assertionsForImageAdded( imageDataFormat, false );
     }
 
-    void testAddingSegmentationInCertainFormat( ImageDataFormat imageDataFormat ) throws IOException {
+    void testAddingSegmentationInCertainFormat( ImageDataFormat imageDataFormat ) throws IOException, SpimDataException {
         ImagePlus seg = makeSegmentation( imageName );
 
         imagesCreator.addImage( seg, imageName, datasetName,
                 imageDataFormat, ProjectCreator.ImageType.segmentation,
-                sourceTransform, useDefaultSettings, uiSelectionGroup );
+                sourceTransform, uiSelectionGroup, false );
 
         assertionsForImageAdded( imageDataFormat, false );
-        assertionsForTableAdded( imageDataFormat );
+        assertionsForTableAdded();
     }
 
-    void testLinkingImagesInCertainFormat( ImageDataFormat imageDataFormat ) throws IOException, SpimDataException {
+    void testLinkingImagesInCertainFormat( ImageDataFormat imageDataFormat, boolean is2D ) throws IOException, SpimDataException {
 
         // save example image
-        String filePath = writeImageAndGetPath( imageDataFormat );
+        String filePath = writeImageAndGetPath( imageDataFormat, is2D );
 
-        imagesCreator.addBdvFormatImage( new File(filePath), datasetName, ProjectCreator.ImageType.image,
-                ProjectCreator.AddMethod.link, uiSelectionGroup, imageDataFormat );
+        imagesCreator.addBdvFormatImage( new File(filePath), imageName, datasetName, ProjectCreator.ImageType.image,
+                ProjectCreator.AddMethod.link, uiSelectionGroup, imageDataFormat, false );
 
         assertionsForImageAdded( imageDataFormat, true );
     }
 
-    void testCopyingImagesInCertainFormat( ImageDataFormat imageDataFormat ) throws IOException, SpimDataException {
-
+    void copyImageInCertainFormat( ImageDataFormat imageDataFormat, boolean is2D ) throws SpimDataException, IOException {
         // save example image
-        String filePath = writeImageAndGetPath( imageDataFormat );
+        String filePath = writeImageAndGetPath( imageDataFormat, is2D );
 
-        imagesCreator.addBdvFormatImage( new File(filePath), datasetName, ProjectCreator.ImageType.image,
-                ProjectCreator.AddMethod.copy, uiSelectionGroup, imageDataFormat );
+        imagesCreator.addBdvFormatImage( new File(filePath), imageName, datasetName, ProjectCreator.ImageType.image,
+                ProjectCreator.AddMethod.copy, uiSelectionGroup, imageDataFormat, false );
+    }
 
+    void testCopyingImagesInCertainFormat( ImageDataFormat imageDataFormat, boolean is2D ) throws IOException, SpimDataException {
+        copyImageInCertainFormat( imageDataFormat, is2D );
         assertionsForImageAdded( imageDataFormat, false );
     }
 
-    void testMovingImagesInCertainFormat( ImageDataFormat imageDataFormat ) throws IOException, SpimDataException {
+    void testMovingImagesInCertainFormat( ImageDataFormat imageDataFormat, boolean is2D ) throws IOException, SpimDataException {
 
         // save example image
-        String filePath = writeImageAndGetPath( imageDataFormat );
+        String filePath = writeImageAndGetPath( imageDataFormat, is2D );
 
-        imagesCreator.addBdvFormatImage( new File(filePath), datasetName, ProjectCreator.ImageType.image,
-                ProjectCreator.AddMethod.move, uiSelectionGroup, imageDataFormat );
+        imagesCreator.addBdvFormatImage( new File(filePath), imageName, datasetName, ProjectCreator.ImageType.image,
+                ProjectCreator.AddMethod.move, uiSelectionGroup, imageDataFormat, false );
 
         assertionsForImageAdded( imageDataFormat, false );
     }
 
     @Test
-    void addImageBdvN5() throws IOException {
-        testAddingImageInCertainFormat( ImageDataFormat.BdvN5 );
+    void addImageBdvN5() throws IOException, SpimDataException {
+        testAddingImageInCertainFormat( ImageDataFormat.BdvN5, false );
     }
 
     @Test
-    void addSegmentationBdvN5() throws IOException {
+    void addSegmentationBdvN5() throws IOException, SpimDataException {
         testAddingSegmentationInCertainFormat( ImageDataFormat.BdvN5 );
     }
 
     @Test
-    void addImageBdvOmeZarr() throws IOException {
-        testAddingImageInCertainFormat( ImageDataFormat.BdvOmeZarr );
-    }
-
-    @Test
-    void addSegmentationBdvOmeZarr() throws IOException {
-        testAddingSegmentationInCertainFormat( ImageDataFormat.BdvOmeZarr );
+    void addImageOmeZarr() throws IOException, SpimDataException {
+        testAddingImageInCertainFormat( ImageDataFormat.OmeZarr, false );
     }
 
     @Test
-    void addImageOmeZarr() throws IOException {
-        testAddingImageInCertainFormat( ImageDataFormat.OmeZarr );
+    void addSegmentationOmeZarr() throws IOException, SpimDataException {
+        testAddingSegmentationInCertainFormat( ImageDataFormat.OmeZarr );
     }
 
     @Test
-    void addSegmentationOmeZarr() throws IOException {
-        testAddingSegmentationInCertainFormat( ImageDataFormat.OmeZarr );
+    void linkToImageBdvN5() throws IOException, SpimDataException {
+        testLinkingImagesInCertainFormat( ImageDataFormat.BdvN5, false );
     }
 
     @Test
-    void linkToImageBdvN5() throws IOException, SpimDataException {
-        testLinkingImagesInCertainFormat( ImageDataFormat.BdvN5 );
+    void copyImageBdvN5() throws IOException, SpimDataException {
+        testCopyingImagesInCertainFormat( ImageDataFormat.BdvN5, false );
     }
 
     @Test
-    void linkToImageBdvOmeZarr() throws IOException, SpimDataException {
-        testLinkingImagesInCertainFormat( ImageDataFormat.BdvOmeZarr );
+    void copyImageOmeZarr() throws IOException, SpimDataException {
+        testCopyingImagesInCertainFormat( ImageDataFormat.OmeZarr, false );
     }
 
     @Test
-    void copyImageBdvN5() throws IOException, SpimDataException {
-        testCopyingImagesInCertainFormat( ImageDataFormat.BdvN5 );
+    void moveImageBdvN5() throws IOException, SpimDataException {
+        testMovingImagesInCertainFormat( ImageDataFormat.BdvN5, false );
     }
 
     @Test
-    void copyImageBdvOmeZarr() throws IOException, SpimDataException {
-        testCopyingImagesInCertainFormat( ImageDataFormat.BdvOmeZarr );
+    void moveImageOmeZarr() throws IOException, SpimDataException {
+        testMovingImagesInCertainFormat( ImageDataFormat.OmeZarr, false );
     }
 
     @Test
-    void copyImageOmeZarr() throws IOException, SpimDataException {
-        testCopyingImagesInCertainFormat( ImageDataFormat.OmeZarr );
+    void add2DImageTo3DDataset() throws SpimDataException, IOException {
+        testAddingImageInCertainFormat( ImageDataFormat.BdvN5, true );
     }
 
     @Test
-    void moveImageBdvN5() throws IOException, SpimDataException {
-        testMovingImagesInCertainFormat( ImageDataFormat.BdvN5 );
+    void copy2DImageTo3DDataset() throws SpimDataException, IOException {
+        testCopyingImagesInCertainFormat( ImageDataFormat.BdvN5, true );
     }
 
     @Test
-    void moveImageBdvOmeZarr() throws IOException, SpimDataException {
-        testMovingImagesInCertainFormat( ImageDataFormat.BdvOmeZarr );
+    void add3DImageTo2DDataset() {
+        projectCreator.getDatasetsCreator().makeDataset2D(datasetName, true);
+        assertThrows( UnsupportedOperationException.class, () -> {
+            addImageInCertainFormat( ImageDataFormat.BdvN5, false);
+        } );
     }
 
     @Test
-    void moveImageOmeZarr() throws IOException, SpimDataException {
-        testMovingImagesInCertainFormat( ImageDataFormat.OmeZarr );
+    void copy3DImageTo2DDataset() {
+        projectCreator.getDatasetsCreator().makeDataset2D(datasetName, true);
+        assertThrows( UnsupportedOperationException.class, () -> {
+            copyImageInCertainFormat( ImageDataFormat.BdvN5, false);
+        } );
     }
 }
diff --git a/src/test/java/org/embl/mobie/viewer/projectcreator/ProjectCreatorTestHelper.java b/src/test/java/org/embl/mobie/viewer/projectcreator/ProjectCreatorTestHelper.java
index d510b479d..dd83e64f0 100644
--- a/src/test/java/org/embl/mobie/viewer/projectcreator/ProjectCreatorTestHelper.java
+++ b/src/test/java/org/embl/mobie/viewer/projectcreator/ProjectCreatorTestHelper.java
@@ -8,9 +8,13 @@ public class ProjectCreatorTestHelper {
 
     static { net.imagej.patcher.LegacyInjector.preinit(); }
 
-    public static ImagePlus makeImage( String imageName ) {
+    public static ImagePlus makeImage( String imageName, boolean is2D ) {
         // make an image with random values, same size as the imagej sample head image
-        return IJ.createImage(imageName, "8-bit noise", 186, 226, 27);
+        if ( !is2D ) {
+            return IJ.createImage(imageName, "8-bit noise", 186, 226, 27);
+        } else {
+            return IJ.createImage(imageName, "8-bit noise", 186, 226, 0);
+        }
     }
 
     public static ImagePlus makeSegmentation( String imageName ) {
diff --git a/src/test/java/org/embl/mobie/viewer/projectcreator/RemoteMetadataCreatorTest.java b/src/test/java/org/embl/mobie/viewer/projectcreator/RemoteMetadataCreatorTest.java
index 056456479..4e8a68851 100644
--- a/src/test/java/org/embl/mobie/viewer/projectcreator/RemoteMetadataCreatorTest.java
+++ b/src/test/java/org/embl/mobie/viewer/projectcreator/RemoteMetadataCreatorTest.java
@@ -1,8 +1,9 @@
 package org.embl.mobie.viewer.projectcreator;
 
+import mpicbg.spim.data.SpimDataException;
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.Dataset;
 import org.embl.mobie.viewer.serialize.DatasetJsonParser;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import de.embl.cba.tables.FileAndUrlUtils;
 import net.imglib2.realtransform.AffineTransform3D;
 import org.junit.jupiter.api.Test;
@@ -37,25 +38,23 @@ void setUp( @TempDir Path tempDir ) throws IOException {
         signingRegion = "us-west-2";
         serviceEndpoint = "https://s3.test.de/test/";
         bucketName = "test-bucket";
-        projectCreator.getDatasetsCreator().addDataset(datasetName);
+        projectCreator.getDatasetsCreator().addDataset(datasetName, false);
         datasetJsonPath = FileAndUrlUtils.combinePath( projectCreator.getDataLocation().getAbsolutePath(),
                 datasetName, "dataset.json" );
     }
 
-    void testAddingRemoteMetadataForCertainFormat( ImageDataFormat imageDataFormat ) throws IOException {
+    void testAddingRemoteMetadataForCertainFormat( ImageDataFormat imageDataFormat ) throws IOException, SpimDataException {
 
         // add image of right type
-        projectCreator.getImagesCreator().addImage( ProjectCreatorTestHelper.makeImage(imageName), imageName, datasetName, imageDataFormat,
-                ProjectCreator.ImageType.image, new AffineTransform3D(), true, uiSelectionGroup);
+        projectCreator.getImagesCreator().addImage( ProjectCreatorTestHelper.makeImage(imageName, false), imageName,
+                datasetName, imageDataFormat, ProjectCreator.ImageType.image, new AffineTransform3D(),
+                uiSelectionGroup, false );
 
         ImageDataFormat remoteFormat = null;
         switch( imageDataFormat ) {
             case BdvN5:
                 remoteFormat = ImageDataFormat.BdvN5S3;
                 break;
-            case BdvOmeZarr:
-                remoteFormat = ImageDataFormat.BdvOmeZarrS3;
-                break;
             case OmeZarr:
                 remoteFormat = ImageDataFormat.OmeZarrS3;
                 break;
@@ -75,17 +74,12 @@ void testAddingRemoteMetadataForCertainFormat( ImageDataFormat imageDataFormat )
     }
 
     @Test
-    void createRemoteMetadataBdvN5() throws IOException {
+    void createRemoteMetadataBdvN5() throws IOException, SpimDataException {
         testAddingRemoteMetadataForCertainFormat( ImageDataFormat.BdvN5 );
     }
 
     @Test
-    void createRemoteMetadataBdvOmeZarr() throws IOException {
-        testAddingRemoteMetadataForCertainFormat( ImageDataFormat.BdvOmeZarr );
-    }
-
-    @Test
-    void createRemoteMetadataOmeZarr() throws IOException {
+    void createRemoteMetadataOmeZarr() throws IOException, SpimDataException {
         testAddingRemoteMetadataForCertainFormat( ImageDataFormat.OmeZarr);
     }
 }
\ No newline at end of file
diff --git a/src/test/java/projects/OmeZarrS3V4Opener.java b/src/test/java/projects/OmeZarrS3V4Opener.java
new file mode 100644
index 000000000..0373ba43e
--- /dev/null
+++ b/src/test/java/projects/OmeZarrS3V4Opener.java
@@ -0,0 +1,20 @@
+package projects;
+
+import net.imagej.ImageJ;
+import org.embl.mobie.io.ImageDataFormat;
+import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.MoBIESettings;
+
+import java.io.IOException;
+
+public class OmeZarrS3V4Opener {
+    public static void main(String[] args) throws IOException {
+        showYX();
+    }
+
+    public static void showYX() throws IOException {
+        final ImageJ imageJ = new ImageJ();
+        imageJ.ui().showUI();
+        final MoBIE moBIE = new MoBIE("https://s3.embl.de/i2k-2020/ngff-example-data/v0.4/yx.ome.zarr", MoBIESettings.settings().imageDataFormat( ImageDataFormat.OmeZarrS3));
+    }
+}
diff --git a/src/test/java/projects/OmeZarrV4FSOpener.java b/src/test/java/projects/OmeZarrV4FSOpener.java
new file mode 100644
index 000000000..48786f5f8
--- /dev/null
+++ b/src/test/java/projects/OmeZarrV4FSOpener.java
@@ -0,0 +1,20 @@
+package projects;
+
+import net.imagej.ImageJ;
+import org.embl.mobie.io.ImageDataFormat;
+import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.MoBIESettings;
+
+import java.io.IOException;
+
+public class OmeZarrV4FSOpener {
+    public static void main(String[] args) throws IOException {
+        showYX();
+    }
+
+    public static void showYX() throws IOException {
+        final ImageJ imageJ = new ImageJ();
+        imageJ.ui().showUI();
+        new MoBIE("/home/katerina/Documents/embl/mnt/kreshuk2/kreshuk/pape/Work/mobie/ngff/ome-ngff-prototypes/single_image/v0.4/yx.ome.zarr", MoBIESettings.settings().imageDataFormat( ImageDataFormat.OmeZarr));
+    }
+}
diff --git a/src/test/java/projects/OpenLocalAutophagosomesEM.java b/src/test/java/projects/OpenLocalAutophagosomesEM.java
index b370ec836..7980243d8 100644
--- a/src/test/java/projects/OpenLocalAutophagosomesEM.java
+++ b/src/test/java/projects/OpenLocalAutophagosomesEM.java
@@ -1,8 +1,8 @@
 package projects;
 
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
diff --git a/src/test/java/projects/OpenLocalBdvOmeZarr.java b/src/test/java/projects/OpenLocalBdvOmeZarr.java
index 414e009c3..4cbdb3838 100644
--- a/src/test/java/projects/OpenLocalBdvOmeZarr.java
+++ b/src/test/java/projects/OpenLocalBdvOmeZarr.java
@@ -1,8 +1,8 @@
 package projects;
 
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
diff --git a/src/test/java/projects/OpenLocalConstantinNoTables.java b/src/test/java/projects/OpenLocalConstantinNoTables.java
new file mode 100644
index 000000000..9ff6c67a1
--- /dev/null
+++ b/src/test/java/projects/OpenLocalConstantinNoTables.java
@@ -0,0 +1,18 @@
+package projects;
+
+import net.imagej.ImageJ;
+import org.embl.mobie.io.ImageDataFormat;
+import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.MoBIESettings;
+
+import java.io.IOException;
+
+public class OpenLocalConstantinNoTables
+{
+	public static void main( String[] args ) throws IOException
+	{
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+		final MoBIE moBIE = new MoBIE("/Volumes/emcf/pape/jil", MoBIESettings.settings().imageDataFormat( ImageDataFormat.BdvN5 ));
+	}
+}
diff --git a/src/test/java/projects/OpenLocalCovidEMCF.java b/src/test/java/projects/OpenLocalCovidEMCF.java
index 22f376110..4a64744eb 100644
--- a/src/test/java/projects/OpenLocalCovidEMCF.java
+++ b/src/test/java/projects/OpenLocalCovidEMCF.java
@@ -1,8 +1,8 @@
 package projects;
 
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
diff --git a/src/test/java/projects/OpenLocalCovidPlate.java b/src/test/java/projects/OpenLocalCovidPlate.java
index b890ed90d..d0666122b 100644
--- a/src/test/java/projects/OpenLocalCovidPlate.java
+++ b/src/test/java/projects/OpenLocalCovidPlate.java
@@ -1,8 +1,8 @@
 package projects;
 
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
diff --git a/src/test/java/projects/OpenLocalCovidTomos.java b/src/test/java/projects/OpenLocalCovidTomos.java
index 6b8f17afa..de883bfad 100644
--- a/src/test/java/projects/OpenLocalCovidTomos.java
+++ b/src/test/java/projects/OpenLocalCovidTomos.java
@@ -1,8 +1,8 @@
 package projects;
 
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
diff --git a/src/test/java/projects/OpenLocalGiuliaMartinCLEM.java b/src/test/java/projects/OpenLocalGiuliaMartinCLEM.java
index 033cd7bd1..17cad8974 100644
--- a/src/test/java/projects/OpenLocalGiuliaMartinCLEM.java
+++ b/src/test/java/projects/OpenLocalGiuliaMartinCLEM.java
@@ -1,9 +1,9 @@
 package projects;
 
 import net.imagej.ImageJ;
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 
 import java.io.IOException;
 
diff --git a/src/test/java/projects/OpenLocalJulianNoTables.java b/src/test/java/projects/OpenLocalJulianNoTables.java
index d0ba6330b..19dcfa228 100644
--- a/src/test/java/projects/OpenLocalJulianNoTables.java
+++ b/src/test/java/projects/OpenLocalJulianNoTables.java
@@ -1,8 +1,8 @@
 package projects;
 
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
@@ -13,6 +13,6 @@ public static void main( String[] args ) throws IOException
 	{
 		final ImageJ imageJ = new ImageJ();
 		imageJ.ui().showUI();
-		final MoBIE moBIE2 = new MoBIE("/Volumes/emcf/hennies/for_constantin/mobie_no_table_test/", MoBIESettings.settings().imageDataFormat( ImageDataFormat.BdvN5 ));
+		final MoBIE moBIE = new MoBIE("/Volumes/emcf/hennies/for_constantin/mobie_no_table_test/", MoBIESettings.settings().imageDataFormat( ImageDataFormat.BdvN5 ));
 	}
 }
diff --git a/src/test/java/projects/OpenLocalOmeZarr.java b/src/test/java/projects/OpenLocalOmeZarr.java
index 24877ae56..a54fd2377 100644
--- a/src/test/java/projects/OpenLocalOmeZarr.java
+++ b/src/test/java/projects/OpenLocalOmeZarr.java
@@ -1,8 +1,8 @@
 package projects;
 
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
diff --git a/src/test/java/projects/OpenLocalPlankton.java b/src/test/java/projects/OpenLocalPlankton.java
index e4c285c16..4bed1bbd1 100644
--- a/src/test/java/projects/OpenLocalPlankton.java
+++ b/src/test/java/projects/OpenLocalPlankton.java
@@ -2,10 +2,10 @@
 
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
+import org.embl.mobie.io.ImageDataFormat;
 
 public class OpenLocalPlankton
 {
diff --git a/src/test/java/projects/OpenLocalPlatynereis.java b/src/test/java/projects/OpenLocalPlatynereis.java
index a31ee90e2..a3d4ff405 100644
--- a/src/test/java/projects/OpenLocalPlatynereis.java
+++ b/src/test/java/projects/OpenLocalPlatynereis.java
@@ -1,8 +1,8 @@
 package projects;
 
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
diff --git a/src/test/java/projects/OpenLocalTest.java b/src/test/java/projects/OpenLocalTest.java
index bf2636086..a4e9e3c1f 100644
--- a/src/test/java/projects/OpenLocalTest.java
+++ b/src/test/java/projects/OpenLocalTest.java
@@ -1,8 +1,8 @@
 package projects;
 
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
@@ -13,7 +13,6 @@ public static void main( String[] args ) throws IOException
 	{
 		final ImageJ imageJ = new ImageJ();
 		imageJ.ui().showUI();
-		new MoBIE("/g/kreshuk/pape/Work/mobie/covid-if-project/data", MoBIESettings.settings().imageDataFormat( ImageDataFormat.OmeZarr ).view( "default_with_tables" ) );
-//		new MoBIE("/g/emcf/pape/covid-if-project", MoBIESettings.settings().imageDataFormat( ImageDataFormat.BdvN5 ));
+		new MoBIE("/Users/tischer/Desktop/mobie/MartinFloat", MoBIESettings.settings().imageDataFormat( ImageDataFormat.OmeZarr ));
 	}
 }
diff --git a/src/test/java/projects/OpenLocalTestData.java b/src/test/java/projects/OpenLocalTestData.java
index 38952047f..a391d2df0 100644
--- a/src/test/java/projects/OpenLocalTestData.java
+++ b/src/test/java/projects/OpenLocalTestData.java
@@ -2,10 +2,10 @@
 
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
+import org.embl.mobie.io.ImageDataFormat;
 
 public class OpenLocalTestData
 {
diff --git a/src/test/java/projects/OpenLocalTobias.java b/src/test/java/projects/OpenLocalTobias.java
index 53e706f8f..92844a925 100644
--- a/src/test/java/projects/OpenLocalTobias.java
+++ b/src/test/java/projects/OpenLocalTobias.java
@@ -2,10 +2,10 @@
 
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
+import org.embl.mobie.io.ImageDataFormat;
 
 public class OpenLocalTobias
 {
diff --git a/src/test/java/projects/OpenRemoteArabidopsis.java b/src/test/java/projects/OpenRemoteArabidopsis.java
index f88fdf97c..8e1a0c182 100644
--- a/src/test/java/projects/OpenRemoteArabidopsis.java
+++ b/src/test/java/projects/OpenRemoteArabidopsis.java
@@ -2,10 +2,10 @@
 
 import org.embl.mobie.viewer.MoBIESettings;
 import org.embl.mobie.viewer.MoBIE;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
+import org.embl.mobie.io.ImageDataFormat;
 
 public class OpenRemoteArabidopsis
 {
diff --git a/src/test/java/projects/OpenRemoteAutophagosomesCLEM.java b/src/test/java/projects/OpenRemoteAutophagosomesCLEM.java
index 853325f0e..186e0c9e7 100644
--- a/src/test/java/projects/OpenRemoteAutophagosomesCLEM.java
+++ b/src/test/java/projects/OpenRemoteAutophagosomesCLEM.java
@@ -2,10 +2,10 @@
 
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
+import org.embl.mobie.io.ImageDataFormat;
 
 public class OpenRemoteAutophagosomesCLEM
 {
diff --git a/src/test/java/projects/OpenRemoteBdvOmeZarr.java b/src/test/java/projects/OpenRemoteBdvOmeZarr.java
index 2e40afaaa..0dba05746 100644
--- a/src/test/java/projects/OpenRemoteBdvOmeZarr.java
+++ b/src/test/java/projects/OpenRemoteBdvOmeZarr.java
@@ -1,8 +1,8 @@
 package projects;
 
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
@@ -11,6 +11,6 @@ public class OpenRemoteBdvOmeZarr {
     public static void main(String[] args) throws IOException {
         final ImageJ imageJ = new ImageJ();
         imageJ.ui().showUI();
-        new MoBIE("https://s3.embl.de/i2k-2020/project-bdv-ome-zarr", MoBIESettings.settings().imageDataFormat(ImageDataFormat.BdvOmeZarr));
+        new MoBIE("https://s3.embl.de/i2k-2020/project-bdv-ome-zarr", MoBIESettings.settings().imageDataFormat( ImageDataFormat.BdvOmeZarr));
     }
 }
\ No newline at end of file
diff --git a/src/test/java/projects/OpenRemoteCLEMExample.java b/src/test/java/projects/OpenRemoteCLEMExample.java
index 02fa2c2ff..fb86c5bf4 100644
--- a/src/test/java/projects/OpenRemoteCLEMExample.java
+++ b/src/test/java/projects/OpenRemoteCLEMExample.java
@@ -2,10 +2,10 @@
 
 import org.embl.mobie.viewer.MoBIESettings;
 import org.embl.mobie.viewer.MoBIE;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
+import org.embl.mobie.io.ImageDataFormat;
 
 public class OpenRemoteCLEMExample
 {
@@ -14,6 +14,6 @@ public static void main( String[] args ) throws IOException
 		final ImageJ imageJ = new ImageJ();
 		imageJ.ui().showUI();
 
-		new MoBIE("https://github.com/mobie/clem-example-project/", MoBIESettings.settings().gitProjectBranch( "update-views" ).imageDataFormat( ImageDataFormat.BdvN5S3 ).view( "tomos-merged" ) );
+		new MoBIE("https://github.com/mobie/clem-example-project/", MoBIESettings.settings().imageDataFormat( ImageDataFormat.BdvN5S3 ) );
 	}
 }
diff --git a/src/test/java/projects/OpenRemoteCOMULIS.java b/src/test/java/projects/OpenRemoteCOMULIS.java
new file mode 100644
index 000000000..4ba8cc709
--- /dev/null
+++ b/src/test/java/projects/OpenRemoteCOMULIS.java
@@ -0,0 +1,18 @@
+package projects;
+
+import net.imagej.ImageJ;
+import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.MoBIESettings;
+
+import java.io.IOException;
+import org.embl.mobie.io.ImageDataFormat;
+
+public class OpenRemoteCOMULIS
+{
+	public static void main( String[] args ) throws IOException
+	{
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+		new MoBIE("https://s3.embl.de/comulis", MoBIESettings.settings().imageDataFormat( ImageDataFormat.BdvOmeZarrS3 ).s3AccessAndSecretKey( new String[]{"UYP3FNN3V5F0P86DR2O3","3EL7Czzg0vVwx2L4v27GQiX0Ct1GkMHS+tbcJR3D"} ) );
+	}
+}
diff --git a/src/test/java/projects/OpenRemoteCentriolesBranch.java b/src/test/java/projects/OpenRemoteCentriolesBranch.java
index 393211e5a..eede9800b 100644
--- a/src/test/java/projects/OpenRemoteCentriolesBranch.java
+++ b/src/test/java/projects/OpenRemoteCentriolesBranch.java
@@ -3,9 +3,9 @@
 import net.imagej.ImageJ;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 
 import java.io.IOException;
+import org.embl.mobie.io.ImageDataFormat;
 
 public class OpenRemoteCentriolesBranch
 {
diff --git a/src/test/java/projects/OpenRemoteCovidScreen.java b/src/test/java/projects/OpenRemoteCovidScreen.java
new file mode 100644
index 000000000..2160c3d35
--- /dev/null
+++ b/src/test/java/projects/OpenRemoteCovidScreen.java
@@ -0,0 +1,24 @@
+package projects;
+
+import net.imagej.ImageJ;
+import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.MoBIESettings;
+
+import java.io.IOException;
+import org.embl.mobie.io.ImageDataFormat;
+
+public class OpenRemoteCovidScreen
+{
+	public static void main( String[] args )
+	{
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+
+		try {
+			new MoBIE("https://github.com/mobie/covid-if-project",
+					MoBIESettings.settings().gitProjectBranch( "main" ).imageDataFormat( ImageDataFormat.OmeZarrS3 ).view( "single_image" ) );
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+	}
+}
diff --git a/src/test/java/projects/OpenRemoteCovidTomogramsBranch.java b/src/test/java/projects/OpenRemoteCovidTomogramsBranch.java
index 054bd41ad..446a5f11b 100644
--- a/src/test/java/projects/OpenRemoteCovidTomogramsBranch.java
+++ b/src/test/java/projects/OpenRemoteCovidTomogramsBranch.java
@@ -2,10 +2,10 @@
 
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
+import org.embl.mobie.io.ImageDataFormat;
 
 public class OpenRemoteCovidTomogramsBranch
 {
diff --git a/src/test/java/projects/OpenRemoteOmeZarr.java b/src/test/java/projects/OpenRemoteOmeZarr.java
index 806669270..99f477803 100644
--- a/src/test/java/projects/OpenRemoteOmeZarr.java
+++ b/src/test/java/projects/OpenRemoteOmeZarr.java
@@ -2,10 +2,10 @@
 
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
+import org.embl.mobie.io.ImageDataFormat;
 
 public class OpenRemoteOmeZarr
 {
diff --git a/src/test/java/projects/OpenRemoteOpenOrganelle.java b/src/test/java/projects/OpenRemoteOpenOrganelle.java
index f7de48f65..56c82b7c1 100644
--- a/src/test/java/projects/OpenRemoteOpenOrganelle.java
+++ b/src/test/java/projects/OpenRemoteOpenOrganelle.java
@@ -2,10 +2,10 @@
 
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
+import org.embl.mobie.io.ImageDataFormat;
 
 public class OpenRemoteOpenOrganelle
 {
diff --git a/src/test/java/projects/OpenRemotePlankton.java b/src/test/java/projects/OpenRemotePlankton.java
new file mode 100644
index 000000000..fd5b2ac65
--- /dev/null
+++ b/src/test/java/projects/OpenRemotePlankton.java
@@ -0,0 +1,21 @@
+package projects;
+
+import net.imagej.ImageJ;
+import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.MoBIESettings;
+
+import java.io.IOException;
+
+public class OpenRemotePlankton
+{
+	public static void main( String[] args )
+	{
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+		try {
+			new MoBIE("https://s3.embl.de/plankton-fibsem", MoBIESettings.settings());
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+	}
+}
diff --git a/src/test/java/projects/OpenRemotePlatynereis.java b/src/test/java/projects/OpenRemotePlatynereis.java
index 7b1840d49..d6a2f8c49 100644
--- a/src/test/java/projects/OpenRemotePlatynereis.java
+++ b/src/test/java/projects/OpenRemotePlatynereis.java
@@ -2,10 +2,10 @@
 
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
+import org.embl.mobie.io.ImageDataFormat;
 
 public class OpenRemotePlatynereis
 {
@@ -16,7 +16,7 @@ public static void main( String[] args )
 
 		try {
 			new MoBIE("https://github.com/platybrowser/platybrowser",
-					MoBIESettings.settings().gitProjectBranch("spec-v2").imageDataFormat( ImageDataFormat.BdvN5S3 ) );
+					MoBIESettings.settings().gitProjectBranch("main").imageDataFormat( ImageDataFormat.BdvN5S3 ) );
 		} catch (IOException e) {
 			e.printStackTrace();
 		}
diff --git a/src/test/java/projects/OpenRemotePlatynereisBranch.java b/src/test/java/projects/OpenRemotePlatynereisBranch.java
index a998b4703..85e3b4c3d 100644
--- a/src/test/java/projects/OpenRemotePlatynereisBranch.java
+++ b/src/test/java/projects/OpenRemotePlatynereisBranch.java
@@ -12,6 +12,6 @@ public static void main( String[] args ) throws IOException
 	{
 		final ImageJ imageJ = new ImageJ();
 		imageJ.ui().showUI();
-		new MoBIE( "https://github.com/mobie/platybrowser-datasets", MoBIESettings.settings().gitProjectBranch( "spec-v2" ) );
+		new MoBIE( "https://github.com/mobie/platybrowser-datasets", MoBIESettings.settings().gitProjectBranch( "normal-vie" ) );
 	}
 }
diff --git a/src/test/java/projects/OpenRemoteTomograms.java b/src/test/java/projects/OpenRemoteTomograms.java
index 437f7ba84..00693c9db 100644
--- a/src/test/java/projects/OpenRemoteTomograms.java
+++ b/src/test/java/projects/OpenRemoteTomograms.java
@@ -2,10 +2,10 @@
 
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
+import org.embl.mobie.io.ImageDataFormat;
 
 public class OpenRemoteTomograms
 {
diff --git a/src/test/java/projects/OpenRemoteZebrafish.java b/src/test/java/projects/OpenRemoteZebrafish.java
index ff35e2b2f..762705b35 100644
--- a/src/test/java/projects/OpenRemoteZebrafish.java
+++ b/src/test/java/projects/OpenRemoteZebrafish.java
@@ -2,10 +2,10 @@
 
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 import net.imagej.ImageJ;
 
 import java.io.IOException;
+import org.embl.mobie.io.ImageDataFormat;
 
 public class OpenRemoteZebrafish
 {
diff --git a/src/test/java/run/RunImageJ.java b/src/test/java/run/RunImageJ.java
index 37eead2cf..15e855f04 100644
--- a/src/test/java/run/RunImageJ.java
+++ b/src/test/java/run/RunImageJ.java
@@ -1,12 +1,14 @@
 package run;
 
 import net.imagej.ImageJ;
+import org.scijava.command.CommandService;
 
 public class RunImageJ
 {
 	public static void main( String[] args )
 	{
-		final ImageJ imageJ = new ImageJ();
+		final ImageJ imageJ = new net.imagej.ImageJ();
 		imageJ.ui().showUI();
+		final CommandService command = imageJ.command();
 	}
 }
diff --git a/src/test/java/tests/TestCovidIF.java b/src/test/java/tests/TestCovidIF.java
index 6f0ef6f27..06d5ca841 100644
--- a/src/test/java/tests/TestCovidIF.java
+++ b/src/test/java/tests/TestCovidIF.java
@@ -1,15 +1,6 @@
 package tests;
 
-import net.imagej.ImageJ;
-import org.embl.mobie.viewer.MoBIE;
-import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
-import org.embl.mobie.viewer.view.View;
-import org.embl.mobie.viewer.view.additionalviews.AdditionalViewsLoader;
-import org.junit.Test;
-
 import java.io.IOException;
-import java.util.Map;
 
 public class TestCovidIF
 {
diff --git a/src/test/java/tests/TestOpenAdvancedProject.java b/src/test/java/tests/TestOpenAdvancedProject.java
index 9d4ffd176..5edd695fa 100644
--- a/src/test/java/tests/TestOpenAdvancedProject.java
+++ b/src/test/java/tests/TestOpenAdvancedProject.java
@@ -1,9 +1,9 @@
 package tests;
 
 import net.imagej.ImageJ;
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
-import org.embl.mobie.viewer.source.ImageDataFormat;
 
 import java.io.IOException;
 
diff --git a/src/test/java/tests/TestRemotePlanktonS3.java b/src/test/java/tests/TestRemotePlanktonS3.java
new file mode 100644
index 000000000..01bf845b6
--- /dev/null
+++ b/src/test/java/tests/TestRemotePlanktonS3.java
@@ -0,0 +1,36 @@
+package tests;
+
+import net.imagej.ImageJ;
+import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.MoBIESettings;
+import org.embl.mobie.viewer.view.View;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class TestRemotePlanktonS3
+{
+	public static void main( String[] args ) throws IOException
+	{
+		new TestRemotePlanktonS3().run();
+	}
+
+	@Test
+	public void run() throws IOException
+	{
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+
+		// This is special as it does not go via github but
+		// all information is on S3
+		final MoBIE moBIE = new MoBIE("https://s3.embl.de/plankton-fibsem", MoBIESettings.settings());
+
+		// Check all views
+		final Map< String, View > views = moBIE.getViews();
+		for ( View view : views.values() )
+		{
+			moBIE.getViewManager().show( view );
+		}
+	}
+}
diff --git a/src/test/java/tests/TestRemoteZebrafish.java b/src/test/java/tests/TestRemoteZebrafish.java
index d9bb2d69c..50424363e 100644
--- a/src/test/java/tests/TestRemoteZebrafish.java
+++ b/src/test/java/tests/TestRemoteZebrafish.java
@@ -1,17 +1,14 @@
 package tests;
 
 import net.imagej.ImageJ;
-import net.imagej.patcher.LegacyInjector;
+import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
 import org.embl.mobie.viewer.display.SegmentationSourceDisplay;
-import org.embl.mobie.viewer.source.ImageDataFormat;
-import org.embl.mobie.viewer.view.View;
 import org.embl.mobie.viewer.view.additionalviews.AdditionalViewsLoader;
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
-import java.util.Map;
 
 public class TestRemoteZebrafish
 {