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

Issue:
Table Annotation: annotate sources & general improvements
I tried the table annotation for source tables now. In principle it works, but there are a few things that would be nice to improve and that could also benefit the annotation functionality in general.

## UI

The UI says `segment` everywhere. This is a bit weird when annotating sources / images, see the screenshot below.

![image](https://user-images.githubusercontent.com/4263537/161999006-07991c02-5d06-4e33-9a58-8820884424ee.png)

I think we have two options here:
- use a more generic name like `object`
- make the UI reactive, i.e. use `segment` if it's called from a segmentation table, or `source` when it's called from a source annotation table

I would prefer option 2, if that's feasible.

## Color scheme and image coloring

When changing the color of the classes it is not updated in the table or in the image overlay. (See screenshot above).

## Navigation

Navigation for source annotation does not work properly, since "Select segment with label id" does not work. So to get to some image one needs to click through all previous ones.

In general, I think it would be very helpful if one could also use the table for navigating in the annotation mode, i.e. when selecting a row in the table it should be also the one that's being annotated. (And in general it would be nice to sync the table row selection and annotation). This currently does not work, see this video:

https://user-images.githubusercontent.com/4263537/162001222-c4c67e74-b968-4fd2-8fef-a870696b0ef5.mp4
Bring log window to front while initialisation

MergedGrid does not work with centerAtOrigin=true
The MergedGrid does not work with `centerAtOrigin=true`.

The reason is that `centerAtOrigin=true` shifts the individual sources such that (a) their middle is a (0,0). Initially we had it such that (b) their upper left corner would be at (0,0).

The (a) behavior currently is inconsistent with how the `MergedGridSource` is built.

Fixing this could be a bigger operation and I don't know when I will get to do it.

@constantinpape I could either hard code for now that  `MergedGridSource` always has `centerAtOrigin=false` or you change it in the JSONs for the covid-if. 

Not sure right now what is better....

If the sources are initially without any transformation, which is what we usually encourage, setting `centerAtOrigin=false` in the JSON will lead to the desired behavior, this only is an issue if one wants to put sources in a grid that have spatial offsets. Thus, since we know that the initial images in the covid-if have no spatial offset, maybe putting `centerAtOrigin=false` in the JSON would actually make sense?! 

And I could, for now, throw an error if center at origin is true for the merged grid?


Discussion / hints:

Fix patch:
diff --git a/src/main/java/org/embl/mobie/viewer/MoBIE.java b/src/main/java/org/embl/mobie/viewer/MoBIE.java
index abf962786..65c50bf0d 100644
--- a/src/main/java/org/embl/mobie/viewer/MoBIE.java
+++ b/src/main/java/org/embl/mobie/viewer/MoBIE.java
@@ -11,8 +11,8 @@
 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.display.AnnotatedSourceDisplay;
+import org.embl.mobie.viewer.annotate.AnnotatedMaskCreator;
 import org.embl.mobie.viewer.annotate.AnnotatedMaskTableRow;
 import org.embl.mobie.viewer.plugins.platybrowser.GeneSearchCommand;
 import org.embl.mobie.viewer.serialize.DatasetJsonParser;
@@ -39,9 +39,12 @@
 import java.io.IOException;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 
 public class MoBIE
 {
@@ -62,6 +65,7 @@ public class MoBIE
 	private HashMap< String, ImgLoader > sourceNameToImgLoader;
 	private Map< String, SourceAndConverter< ? > > sourceNameToTransformedSourceAndConverter;
 	private ArrayList< String > projectCommands = new ArrayList<>();;
+	public static int minLogTimeMillis = 100;
 
 	public MoBIE( String projectRoot ) throws IOException
 	{
@@ -70,7 +74,10 @@ public MoBIE( String projectRoot ) throws IOException
 
 	public MoBIE( String projectLocation, MoBIESettings settings ) throws IOException
 	{
-		IJ.log("MoBIE");
+		IJ.log("# MoBIE\n" );
+		IJ.log("Opening project: " + projectLocation + "\n" );
+		WindowArrangementHelper.setLogWindowPositionAndSize();
+
 		this.settings = settings.projectLocation( projectLocation );
 		setS3Credentials( settings );
 		setProjectImageAndTableRootLocations( );
@@ -144,7 +151,7 @@ else if ( imageDataFormats.contains( ImageDataFormat.BdvN5 ) )
 		return settings;
 	}
 
-	public static void mergeAnnotatedIntervalTable( List< AnnotatedMaskTableRow > intervalTableRows, Map< String, List< String > > columns )
+	public static void mergeAnnotatedMaskTable( List< AnnotatedMaskTableRow > intervalTableRows, Map< String, List< String > > columns )
 	{
 		final HashMap< String, List< String > > referenceColumns = new HashMap<>();
 		final ArrayList< String > gridIdColumn = TableColumns.getColumn( intervalTableRows, TableColumnNames.ANNOTATION_ID );
@@ -215,24 +222,54 @@ private void setProjectImageAndTableRootLocations( )
 	 */
 	public Map< String, SourceAndConverter< ? > > openSourceAndConverters( Collection< String > sources )
 	{
-		final long start = System.currentTimeMillis();
+		final long startTime = System.currentTimeMillis();
 
 		Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverters = new ConcurrentHashMap< >();
 
 		final ArrayList< Future< ? > > futures = ThreadUtils.getFutures();
+		AtomicInteger sourceIndex = new AtomicInteger(0);
+		final int numImages = sources.size();
+		AtomicInteger sourceLoggingModulo = new AtomicInteger(1);
+		AtomicLong lastLogMillis = new AtomicLong( System.currentTimeMillis() );
+
 		for ( String sourceName : sources )
 		{
 			futures.add(
-					ThreadUtils.ioExecutorService.submit( () -> { sourceNameToSourceAndConverters.put( sourceName, openSourceAndConverter( sourceName ) ); }
+					ThreadUtils.ioExecutorService.submit( () -> {
+						String log = getLog( sourceIndex, numImages, sourceLoggingModulo, lastLogMillis );
+						sourceNameToSourceAndConverters.put( sourceName, openSourceAndConverter( sourceName, log ) );
+					}
 				) );
 		}
 		ThreadUtils.waitUntilFinished( futures );
 
-		IJ.log( "Fetched " + sourceNameToSourceAndConverters.size() + " image(s)  in " + (System.currentTimeMillis() - start) + " ms, using " + ThreadUtils.N_IO_THREADS + " thread(s).");
+		IJ.log( "Opened " + sourceNameToSourceAndConverters.size() + " image(s) in " + (System.currentTimeMillis() - startTime) + " ms, using " + ThreadUtils.getnIoThreads() + " thread(s).\n");
 
 		return sourceNameToSourceAndConverters;
 	}
 
+	private String getLog( AtomicInteger index, int numTotal, AtomicInteger modulo, AtomicLong lastLogMillis )
+	{
+		if ( ( index.incrementAndGet() - 1 ) % modulo.get() == 0  )
+		{
+			final long currentTimeMillis = System.currentTimeMillis();
+			if ( currentTimeMillis - lastLogMillis.get() < 4000 )
+			{
+				modulo.set( modulo.get() * 2 );
+			}
+			else if ( currentTimeMillis - lastLogMillis.get() > 6000 )
+			{
+				modulo.set( modulo.get() / 2 );
+			}
+			lastLogMillis.set( currentTimeMillis );
+			return "Opening (" + index.get() + "/" + numTotal + "): ";
+		}
+		else
+		{
+			return null;
+		}
+	}
+
 	private void openDataset( String datasetName ) throws IOException
 	{
 		sourceNameToImgLoader = new HashMap<>();
@@ -243,11 +280,21 @@ private void openDataset( String datasetName ) throws IOException
 		viewManager = new ViewManager( this, userInterface, dataset.is2D, dataset.timepoints );
 		final View view = dataset.views.get( settings.values.getView() );
 		view.setName( settings.values.getView() );
+
+		System.out.println("# Views");
+		for ( String viewName : dataset.views.keySet() )
+		{
+			System.out.println( viewName );
+		}
+
+		IJ.log( "Opening view: " + view.getName() + "\n" );
+		final long startTime = System.currentTimeMillis();
 		viewManager.show( view );
+		IJ.log("Opened view: " + view.getName() + ", in " + (System.currentTimeMillis() - startTime) + " ms.\n" );
 
 		// arrange windows
-		WindowArrangementHelper.setLogWindowPositionAndSize( userInterface.getWindow() );
-		WindowArrangementHelper.rightAlignWindow( userInterface.getWindow(), viewManager.getSliceViewer().getWindow(), false, true );
+		//WindowArrangementHelper.setLogWindowPositionAndSize( userInterface.getWindow() );
+		//WindowArrangementHelper.rightAlignWindow( userInterface.getWindow(), viewManager.getSliceViewer().getWindow(), false, true );
 	}
 
 	private void setDatasetName( String datasetName )
@@ -320,11 +367,14 @@ public synchronized ImageSource getSource( String sourceName )
 		return dataset.sources.get( sourceName ).get();
 	}
 
-	public SourceAndConverter< ? > openSourceAndConverter( String sourceName )
+	public SourceAndConverter< ? > openSourceAndConverter( String sourceName, String log )
 	{
 		final ImageSource imageSource = getSource( sourceName );
 		final String imagePath = getImagePath( imageSource );
-		IJ.log( "Opening image:\n" + imagePath );
+
+		if( log != null )
+			IJ.log( log + imagePath );
+
 		final ImageDataFormat imageDataFormat = settings.values.getImageDataFormat();
 
 		try
@@ -334,6 +384,9 @@ public synchronized ImageSource getSource( String sourceName )
 
 			final SourceAndConverterFromSpimDataCreator creator = new SourceAndConverterFromSpimDataCreator( spimData );
 			SourceAndConverter< ? > sourceAndConverter = creator.getSetupIdToSourceAndConverter().values().iterator().next();
+			// Touch the source once to initiate the cache,
+			// as this speeds up future accesses significantly
+			sourceAndConverter.getSpimSource().getSource( 0,0 );
 			return sourceAndConverter;
 		}
 		catch ( Exception e )
@@ -422,20 +475,19 @@ private String createPath( String root, String[] files )
 		return location;
 	}
 
-	private List< TableRowImageSegment > loadImageSegmentsTable( String sourceName, String tableName )
+	private List< TableRowImageSegment > loadImageSegmentsTable( String sourceName, String tableName, String log )
 	{
 		final SegmentationSource tableSource = ( SegmentationSource ) getSource( sourceName );
-
 		final String defaultTablePath = getTablePath( tableSource, tableName );
-
+		if ( log != null )
+			IJ.log( log + defaultTablePath );
 		final List< TableRowImageSegment > segments = MoBIEHelper.createAnnotatedImageSegmentsFromTableFile( defaultTablePath, sourceName );
-
 		return segments;
 	}
 
 	private Map< String, List< String > > loadAdditionalTable( String imageID, String tablePath )
 	{
-		Logger.log( "Opening table:\n" + tablePath );
+		Logger.log( "Opening additional table:\n" + tablePath );
 		Map< String, List< String > > columns = TableColumns.stringColumnsFromTableFile( tablePath );
 		TableColumns.addLabelImageIdColumn( columns, TableColumnNames.LABEL_IMAGE_ID, imageID );
 		return columns;
@@ -459,7 +511,10 @@ private List< Map< String, List< String > > > loadAdditionalTables( List<String>
 		}
 		ThreadUtils.waitUntilFinished( futures );
 
-		System.out.println( "Fetched " + sources.size() + " table(s) in " + (System.currentTimeMillis() - start) + " ms, using " + ThreadUtils.N_IO_THREADS + " thread(s).");
+		final long durationMillis = System.currentTimeMillis() - start;
+
+		if ( durationMillis > minLogTimeMillis )
+			IJ.log( "Read " + sources.size() + " table(s) in " + durationMillis + " ms, using " + ThreadUtils.getnIoThreads() + " thread(s).\n");
 
 		return additionalTables;
 	}
@@ -469,7 +524,7 @@ private List< Map< String, List< String > > > loadAdditionalTables( List<String>
 		return sourceNameToTransformedSourceAndConverter;
 	}
 
-	private ArrayList< List< TableRowImageSegment > > loadPrimarySegmentsTables( SegmentationSourceDisplay segmentationDisplay, String table )
+	private Collection< List< TableRowImageSegment > > loadPrimarySegmentsTables( SegmentationSourceDisplay segmentationDisplay, String tableName )
 	{
 		final List< String > segmentationDisplaySources = segmentationDisplay.getSources();
 		final ConcurrentHashMap< String, Set< Source< ? > > > sourceNameToRootSources = new ConcurrentHashMap();
@@ -481,26 +536,40 @@ private ArrayList< List< TableRowImageSegment > > loadPrimarySegmentsTables( Seg
 			sourceNameToRootSources.put( sourceName, rootSources );
 		}
 
-
-		final ArrayList< List< TableRowImageSegment > > primaryTables = new ArrayList<>();
+		final Queue< List< TableRowImageSegment > > primaryTables = new ConcurrentLinkedQueue<>();
+		final int numTables = getNumTables( segmentationDisplaySources, sourceNameToRootSources );
+		final long startTimeMillis = System.currentTimeMillis();
+		final AtomicLong lastLogMillis = new AtomicLong(startTimeMillis);
+		final AtomicInteger tableLoggingModulo = new AtomicInteger(1);
+		final AtomicInteger tableIndex = new AtomicInteger();
 		final ArrayList< Future< ? > > futures = ThreadUtils.getFutures();
 		for ( String displayedSourceName : segmentationDisplaySources )
 		{
 			final Set< Source< ? > > rootSources = sourceNameToRootSources.get( displayedSourceName );
 			for ( Source rootSource : rootSources )
 			{
-				futures.add( ThreadUtils.ioExecutorService.submit( () -> 				loadAndAddPrimaryTable( table, primaryTables, rootSource.getName() ) ) );
+				futures.add( ThreadUtils.ioExecutorService.submit( () ->
+				{
+					final String log = getLog( tableIndex, numTables, tableLoggingModulo, lastLogMillis );
+					final List< TableRowImageSegment > primaryTable = loadImageSegmentsTable( rootSource.getName(), tableName, log );
+					primaryTables.add( primaryTable );
+				} ) );
 			}
 		}
 		ThreadUtils.waitUntilFinished( futures );
-
+		IJ.log( "Fetched " + numTables + " tables(s) in " + (System.currentTimeMillis() - startTimeMillis) + " ms, using " + ThreadUtils.getnIoThreads() + " thread(s).\n");
 		return primaryTables;
 	}
 
-	private void loadAndAddPrimaryTable( String tableName, ArrayList< List< TableRowImageSegment > > primaryTables, String tableSourceName )
+	private int getNumTables( List< String > segmentationDisplaySources, ConcurrentHashMap< String, Set< Source< ? > > > sourceNameToRootSources )
 	{
-		final List< TableRowImageSegment > primaryTable = loadImageSegmentsTable( tableSourceName, tableName );
-		primaryTables.add( primaryTable );
+		int numTables = 0;
+		for ( String displayedSourceName : segmentationDisplaySources )
+		{
+			final Set< Source< ? > > rootSources = sourceNameToRootSources.get( displayedSourceName );
+			numTables += rootSources.size();
+		}
+		return numTables;
 	}
 
 	private Map< String, List< String > > createColumnsForMerging( List< TableRowImageSegment > segments, Map< String, List< String > > newColumns )
@@ -569,7 +638,7 @@ public void appendSegmentsTables( SegmentationSourceDisplay segmentationDisplay,
 	public void loadPrimarySegmentsTables( SegmentationSourceDisplay segmentationDisplay )
 	{
 		segmentationDisplay.tableRows = new ArrayList<>();
-		final ArrayList< List< TableRowImageSegment > > primaryTables = loadPrimarySegmentsTables( segmentationDisplay, segmentationDisplay.getTables().get( 0 ) );
+		final Collection< List< TableRowImageSegment > > primaryTables = loadPrimarySegmentsTables( segmentationDisplay, segmentationDisplay.getTables().get( 0 ) );
 
 		for ( List< TableRowImageSegment > primaryTable : primaryTables )
 		{
@@ -577,7 +646,7 @@ public void loadPrimarySegmentsTables( SegmentationSourceDisplay segmentationDis
 		}
 	}
 
-	public List< AnnotatedMaskTableRow > loadAnnotatedIntervalTables( AnnotatedIntervalDisplay annotationDisplay )
+	public List< AnnotatedMaskTableRow > loadAnnotatedMaskTables( AnnotatedSourceDisplay annotationDisplay )
 	{
 		// open
 		final List< Map< String, List< String > > > tables = new ArrayList<>();
@@ -585,21 +654,24 @@ public List< AnnotatedMaskTableRow > loadAnnotatedIntervalTables( AnnotatedInter
 		{
 			String tablePath = getTablePath( annotationDisplay.getTableDataFolder( TableDataFormat.TabDelimitedFile ), table );
 			tablePath = MoBIEHelper.resolveTablePath( tablePath );
-			Logger.log( "Opening table:\n" + tablePath );
+			final long startTime = System.currentTimeMillis();
 			tables.add( TableColumns.stringColumnsFromTableFile( tablePath ) );
+			final long durationMillis = System.currentTimeMillis() - startTime;
+			if ( durationMillis > minLogTimeMillis )
+				Logger.log( "Read in "+ durationMillis +" ms: " + tablePath );
 		}
 
-		// create primary AnnotatedIntervalTableRow table
+		// create primary AnnotatedMaskTableRow 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.getTransformedSourceAndConverter( sourceName )  );
-		final List< AnnotatedMaskTableRow > intervalTableRows = annotatedIntervalCreator.getAnnotatedIntervalTableRows();
+		// TODO: The AnnotatedMaskCreator does not need the sources, but just the source's real intervals
+		final AnnotatedMaskCreator annotatedMaskCreator = new AnnotatedMaskCreator( referenceTable, annotationDisplay.getAnnotationIdToSources(), (String sourceName ) -> getTransformedSourceAndConverter( sourceName )  );
+		final List< AnnotatedMaskTableRow > intervalTableRows = annotatedMaskCreator.getAnnotatedMaskTableRows();
 
 		final List< Map< String, List< String > > > additionalTables = tables.subList( 1, tables.size() );
 
 		for ( int i = 0; i < additionalTables.size(); i++ )
 		{
-			MoBIE.mergeAnnotatedIntervalTable( intervalTableRows, additionalTables.get( i ) );
+			MoBIE.mergeAnnotatedMaskTable( intervalTableRows, additionalTables.get( i ) );
 		}
 
 		return intervalTableRows;
diff --git a/src/main/java/org/embl/mobie/viewer/MoBIEHelper.java b/src/main/java/org/embl/mobie/viewer/MoBIEHelper.java
index b5c81536e..4fd0ceb7e 100644
--- a/src/main/java/org/embl/mobie/viewer/MoBIEHelper.java
+++ b/src/main/java/org/embl/mobie/viewer/MoBIEHelper.java
@@ -18,6 +18,7 @@
 import loci.plugins.in.ImporterOptions;
 import net.imglib2.FinalRealInterval;
 import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.RealInterval;
 import net.imglib2.RealPoint;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.roi.RealMaskRealInterval;
@@ -25,7 +26,8 @@
 import org.embl.mobie.io.util.FileAndUrlUtils;
 import org.embl.mobie.viewer.source.LabelSource;
 import org.embl.mobie.viewer.transform.MergedGridSource;
-import org.embl.mobie.viewer.transform.TransformHelper;
+import org.embl.mobie.viewer.transform.RealMaskSource;
+import org.embl.mobie.viewer.transform.TransformHelpers;
 
 import javax.swing.*;
 import javax.swing.filechooser.FileNameExtensionFilter;
@@ -144,9 +146,20 @@ public static RealMaskRealInterval unionRealMask( List< ? extends Source< ? > >
 			final RealMaskRealInterval mask = getMask( source );
 
 			if ( union == null )
+			{
 				union = mask;
+			}
 			else
-				union = union.or( mask );
+			{
+				if ( Arrays.equals( mask.minAsDoubleArray(), union.minAsDoubleArray() ) && Arrays.equals( mask.maxAsDoubleArray(), union.maxAsDoubleArray() ))
+				{
+					continue;
+				}
+				else
+				{
+					union = union.or( mask );
+				}
+			}
 		}
 
 		return union;
@@ -332,8 +345,6 @@ public static List< TableRowImageSegment > createAnnotatedImageSegmentsFromTable
 			String tablePath,
 			String imageId )
 	{
-		IJ.log( "Opening table:\n" + tablePath );
-
 		tablePath = resolveTablePath( tablePath );
 
 		Map< String, List< String > > columns = TableColumns.stringColumnsFromTableFile( tablePath );
@@ -407,7 +418,7 @@ public static Map< SegmentProperty, List< String > > createSegmentPropertyToColu
 
 	public static String createNormalisedViewerTransformString( BdvHandle bdv, double[] position )
 	{
-		final AffineTransform3D view = TransformHelper.createNormalisedViewerTransform( bdv.getViewerPanel(), position );
+		final AffineTransform3D view = TransformHelpers.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;
@@ -429,14 +440,6 @@ public static AffineTransform3D asAffineTransform3D( double[] doubles )
 		return view;
 	}
 
-	public static FinalRealInterval estimateBounds( Source< ? > source )
-	{
-		final AffineTransform3D affineTransform3D = new AffineTransform3D();
-		source.getSourceTransform( 0, 0, affineTransform3D );
-		final FinalRealInterval bounds = affineTransform3D.estimateBounds( source.getSource( 0, 0 ) );
-		return bounds;
-	}
-
 	public static RealMaskRealInterval getMask( Source< ? > source )
 	{
 		final AffineTransform3D affineTransform3D = new AffineTransform3D();
diff --git a/src/main/java/org/embl/mobie/viewer/ThreadUtils.java b/src/main/java/org/embl/mobie/viewer/ThreadUtils.java
index 7cba8a84a..07ccf6efb 100644
--- a/src/main/java/org/embl/mobie/viewer/ThreadUtils.java
+++ b/src/main/java/org/embl/mobie/viewer/ThreadUtils.java
@@ -15,11 +15,22 @@ 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 );
+	private static int N_IO_THREADS = 16;
+	public static 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 int N_THREADS = Runtime.getRuntime().availableProcessors() - 1;;
+	public static ExecutorService executorService = Executors.newFixedThreadPool( N_THREADS );
+
+	public static void setnIoThreads( int nIoThreads )
+	{
+		N_IO_THREADS = nIoThreads;
+		ioExecutorService = Executors.newFixedThreadPool( N_IO_THREADS );
+	}
+
+	public static int getnIoThreads()
+	{
+		return N_IO_THREADS;
+	}
 
 	public static void waitUntilFinished( List< Future< ? > > futures )
 	{
diff --git a/src/main/java/org/embl/mobie/viewer/annotate/AnnotatedIntervalAdapter.java b/src/main/java/org/embl/mobie/viewer/annotate/AnnotatedIntervalAdapter.java
deleted file mode 100644
index c3f806227..000000000
--- a/src/main/java/org/embl/mobie/viewer/annotate/AnnotatedIntervalAdapter.java
+++ /dev/null
@@ -1,97 +0,0 @@
-package org.embl.mobie.viewer.annotate;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Objects;
-
-public class AnnotatedIntervalAdapter< T extends AnnotatedMask >
-{
-	class TimepointAndAnnotationId
-	{
-		private final String annotationId;
-		private final int timepoint;
-
-		public TimepointAndAnnotationId( int timepoint, String annotationId )
-		{
-			this.timepoint = timepoint;
-			this.annotationId = annotationId;
-		}
-
-		public TimepointAndAnnotationId( AnnotatedMask annotatedMask )
-		{
-			this.timepoint = annotatedMask.getTimepoint();
-			this.annotationId = annotatedMask.getName();
-		}
-
-		@Override
-		public boolean equals( Object o )
-		{
-			if ( this == o ) return true;
-			if ( o == null || getClass() != o.getClass() ) return false;
-			TimepointAndAnnotationId that = ( TimepointAndAnnotationId ) o;
-			return Integer.compare( this.timepoint, that.getTimepoint() ) == 0 &&
-					Objects.equals( this.annotationId, that.getAnnotationId() );
-		}
-
-		@Override
-		public int hashCode()
-		{
-			return Objects.hash( annotationId, timepoint );
-		}
-
-		public String getAnnotationId()
-		{
-			return annotationId;
-		}
-
-		public int getTimepoint()
-		{
-			return timepoint;
-		}
-	}
-
-
-	private HashMap< TimepointAndAnnotationId, T > timepointAndAnnotationToObject;
-
-	/**
-	 * For lazy initialization
-	 */
-	public AnnotatedIntervalAdapter()
-	{
-		timepointAndAnnotationToObject = new HashMap<>();
-	}
-
-	public AnnotatedIntervalAdapter( List< T > objects )
-	{
-		timepointAndAnnotationToObject = new HashMap<>();
-
-		for ( T object : objects )
-			timepointAndAnnotationToObject.put( new TimepointAndAnnotationId( object ), object );
-	}
-
-	public T getAnnotatedInterval( int timepoint, String annotationId )
-	{
-		final TimepointAndAnnotationId timepointAndAnnotationId = new TimepointAndAnnotationId( timepoint, annotationId  );
-
-		return getAnnotatedInterval( timepointAndAnnotationId );
-	}
-
-	private T getAnnotatedInterval( TimepointAndAnnotationId timepointAndAnnotationId )
-	{
-		return timepointAndAnnotationToObject.get( timepointAndAnnotationId );
-	}
-
-	// deserialize
-	public List< T > getAnnotatedIntervals( List< String > strings )
-	{
-		final ArrayList< T > annotatedIntervals = new ArrayList<>();
-		for ( String string : strings )
-		{
-			final String[] split = string.split( ";" );
-			final TimepointAndAnnotationId timepointAndAnnotationId = new TimepointAndAnnotationId( Integer.parseInt( split[0] ), split[1] );
-			annotatedIntervals.add( getAnnotatedInterval( timepointAndAnnotationId ) );
-		}
-		return annotatedIntervals;
-	}
-}
diff --git a/src/main/java/org/embl/mobie/viewer/annotate/AnnotatedMask.java b/src/main/java/org/embl/mobie/viewer/annotate/AnnotatedMask.java
index 6dea9fd70..0e4a68bfc 100644
--- a/src/main/java/org/embl/mobie/viewer/annotate/AnnotatedMask.java
+++ b/src/main/java/org/embl/mobie/viewer/annotate/AnnotatedMask.java
@@ -1,12 +1,11 @@
 package org.embl.mobie.viewer.annotate;
 
-import net.imglib2.RealInterval;
-import net.imglib2.roi.RealMask;
+import net.imglib2.RealLocalizable;
 import net.imglib2.roi.RealMaskRealInterval;
 
 public interface AnnotatedMask
 {
-	RealMaskRealInterval getMask();
-	Integer getTimepoint();
-	String getName();
+	RealMaskRealInterval mask();
+	Integer timePoint();
+	String name();
 }
diff --git a/src/main/java/org/embl/mobie/viewer/annotate/AnnotatedMaskAdapter.java b/src/main/java/org/embl/mobie/viewer/annotate/AnnotatedMaskAdapter.java
new file mode 100644
index 000000000..f5ed62ff1
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/annotate/AnnotatedMaskAdapter.java
@@ -0,0 +1,154 @@
+package org.embl.mobie.viewer.annotate;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
+
+public class AnnotatedMaskAdapter
+{
+	class TimepointAndAnnotationId
+	{
+		private final String annotationId;
+		private final int timepoint;
+
+		public TimepointAndAnnotationId( int timepoint, String annotationId )
+		{
+			this.timepoint = timepoint;
+			this.annotationId = annotationId;
+		}
+
+		public TimepointAndAnnotationId( AnnotatedMask annotatedMask )
+		{
+			this.timepoint = annotatedMask.timePoint();
+			this.annotationId = annotatedMask.name();
+		}
+
+		@Override
+		public boolean equals( Object o )
+		{
+			if ( this == o ) return true;
+			if ( o == null || getClass() != o.getClass() ) return false;
+			TimepointAndAnnotationId that = ( TimepointAndAnnotationId ) o;
+			return Integer.compare( this.timepoint, that.getTimepoint() ) == 0 &&
+					Objects.equals( this.annotationId, that.getAnnotationId() );
+		}
+
+		@Override
+		public int hashCode()
+		{
+			return Objects.hash( annotationId, timepoint );
+		}
+
+		public String getAnnotationId()
+		{
+			return annotationId;
+		}
+
+		public int getTimepoint()
+		{
+			return timepoint;
+		}
+	}
+
+	class TimePointAndLabel
+	{
+		private final double label;
+		private final int timepoint;
+
+		public TimePointAndLabel( int timepoint, double label )
+		{
+			this.timepoint = timepoint;
+			this.label = label;
+		}
+
+		@Override
+		public boolean equals( Object o )
+		{
+			if ( this == o ) return true;
+			if ( o == null || getClass() != o.getClass() ) return false;
+			TimePointAndLabel that = ( TimePointAndLabel ) o;
+			return this.timepoint == that.getTimepoint() &&
+					Objects.equals( this.label, that.getLabel() );
+		}
+
+		@Override
+		public int hashCode()
+		{
+			return Objects.hash( timepoint, label );
+		}
+
+		public double getLabel()
+		{
+			return label;
+		}
+
+		public int getTimepoint()
+		{
+			return timepoint;
+		}
+	}
+
+
+	private HashMap< TimepointAndAnnotationId, AnnotatedMaskTableRow > timepointAndAnnotationToObject;
+	private HashMap< TimePointAndLabel, AnnotatedMaskTableRow > timePointAndLabelToObject;
+
+	/**
+	 * For lazy initialization
+	 */
+	public AnnotatedMaskAdapter()
+	{
+		timepointAndAnnotationToObject = new HashMap<>();
+	}
+
+	public AnnotatedMaskAdapter( List< AnnotatedMaskTableRow > tableRows )
+	{
+		timepointAndAnnotationToObject = new HashMap<>();
+		timePointAndLabelToObject = new HashMap<>();
+
+		final int numTableRows = tableRows.size();
+		for ( int rowIndex = 0; rowIndex < numTableRows; rowIndex++ )
+		{
+			final AnnotatedMaskTableRow annotatedMaskTableRow = tableRows.get( rowIndex );
+			timepointAndAnnotationToObject.put( new TimepointAndAnnotationId( annotatedMaskTableRow ), annotatedMaskTableRow );
+			timePointAndLabelToObject.put( new TimePointAndLabel( annotatedMaskTableRow.timePoint(), rowIndex ), annotatedMaskTableRow );
+		}
+	}
+
+	public AnnotatedMaskTableRow getAnnotatedMask( int timepoint, String annotationId )
+	{
+		final TimepointAndAnnotationId timepointAndAnnotationId = new TimepointAndAnnotationId( timepoint, annotationId  );
+
+		return getAnnotatedMask( timepointAndAnnotationId );
+	}
+
+	public AnnotatedMaskTableRow getAnnotatedMask( int timepoint, double label )
+	{
+		final TimePointAndLabel timepointAndAnnotationId = new TimePointAndLabel( timepoint, label  );
+
+		return getAnnotatedMask( timepointAndAnnotationId );
+	}
+
+	private AnnotatedMaskTableRow getAnnotatedMask( TimepointAndAnnotationId timepointAndAnnotationId )
+	{
+		return timepointAndAnnotationToObject.get( timepointAndAnnotationId );
+	}
+
+	private AnnotatedMaskTableRow getAnnotatedMask( TimePointAndLabel timePointAndLabel )
+	{
+		return timePointAndLabelToObject.get( timePointAndLabel );
+	}
+
+	// deserialize
+	public List< AnnotatedMaskTableRow > getAnnotatedMasks( List< String > strings )
+	{
+		final ArrayList< AnnotatedMaskTableRow > annotatedMasks = new ArrayList<>();
+		for ( String string : strings )
+		{
+			final String[] split = string.split( ";" );
+			final TimepointAndAnnotationId timepointAndAnnotationId = new TimepointAndAnnotationId( Integer.parseInt( split[0] ), split[1] );
+			annotatedMasks.add( getAnnotatedMask( timepointAndAnnotationId ) );
+		}
+		return annotatedMasks;
+	}
+}
diff --git a/src/main/java/org/embl/mobie/viewer/annotate/AnnotatedIntervalCreator.java b/src/main/java/org/embl/mobie/viewer/annotate/AnnotatedMaskCreator.java
similarity index 54%
rename from src/main/java/org/embl/mobie/viewer/annotate/AnnotatedIntervalCreator.java
rename to src/main/java/org/embl/mobie/viewer/annotate/AnnotatedMaskCreator.java
index 3b8fffdf5..05168c138 100644
--- a/src/main/java/org/embl/mobie/viewer/annotate/AnnotatedIntervalCreator.java
+++ b/src/main/java/org/embl/mobie/viewer/annotate/AnnotatedMaskCreator.java
@@ -2,56 +2,67 @@
 
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
+import ij.IJ;
 import net.imglib2.roi.RealMaskRealInterval;
 import org.embl.mobie.viewer.MoBIEHelper;
 import org.embl.mobie.viewer.TableColumnNames;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
-public class AnnotatedIntervalCreator
+public class AnnotatedMaskCreator
 {
 	private final Map< String, List< String > > columns;
 	private final Map< String, List< String > > annotationIdToSources;
 	private final Function< String, SourceAndConverter< ? > > sourceAndConverterSupplier;
-	private List< AnnotatedMaskTableRow > annotatedIntervalTableRows;
+	private List< AnnotatedMaskTableRow > annotatedMaskTableRows;
 
-	public AnnotatedIntervalCreator( Map< String, List< String > > columns, Map< String, List< String > > annotationIdToSources, Function< String, SourceAndConverter< ? > > sourceAndConverterSupplier)
+	public AnnotatedMaskCreator( Map< String, List< String > > columns, Map< String, List< String > > annotationIdToSources, Function< String, SourceAndConverter< ? > > sourceAndConverterSupplier)
 	{
 		this.columns = columns;
 		this.annotationIdToSources = annotationIdToSources;
 		this.sourceAndConverterSupplier = sourceAndConverterSupplier;
-		createAnnotatedIntervals();
+		createAnnotatedMasks();
 	}
 
-	private void createAnnotatedIntervals()
+	private void createAnnotatedMasks()
 	{
-		annotatedIntervalTableRows = new ArrayList<>();
+		final long currentTimeMillis = System.currentTimeMillis();
+
+		annotatedMaskTableRows = new ArrayList<>();
 		final Set< String > annotationIds = annotationIdToSources.keySet();
 		final List< String > annotationIdColumn = columns.get( TableColumnNames.ANNOTATION_ID );
 
 		for ( String annotationId : annotationIds )
 		{
 			final List< ? extends Source< ? > > sources = annotationIdToSources.get( annotationId ).stream().map( name -> sourceAndConverterSupplier.apply( name ).getSpimSource() ).collect( Collectors.toList() );
+			// TODO: if all the sources cover the same interval, could we simplify the below call?
 			final RealMaskRealInterval mask = MoBIEHelper.unionRealMask( sources );
-			final int rowIndex = annotationIdColumn.indexOf( annotationId );
+			System.out.println( annotationId );
+			System.out.println( sources.size() );
+			System.out.println( Arrays.toString( mask.minAsDoubleArray() ));
 
-			annotatedIntervalTableRows.add(
+			annotatedMaskTableRows.add(
 					new DefaultAnnotatedMaskTableRow(
 							annotationId,
 							mask,
 							columns,
-							rowIndex )
+							annotationIdColumn.indexOf( annotationId ) )
 			);
 		}
+
+		final long durationMillis = System.currentTimeMillis() - currentTimeMillis;
+		if ( durationMillis > 100 )
+			IJ.log("Created " + annotationIds.size() + " annotated intervals in " + durationMillis + " ms.");
 	}
 
-	public List< AnnotatedMaskTableRow > getAnnotatedIntervalTableRows()
+	public List< AnnotatedMaskTableRow > getAnnotatedMaskTableRows()
 	{
-		return annotatedIntervalTableRows;
+		return annotatedMaskTableRows;
 	}
 }
diff --git a/src/main/java/org/embl/mobie/viewer/annotate/Annotator.java b/src/main/java/org/embl/mobie/viewer/annotate/Annotator.java
index 0d4fb7217..1859c242c 100644
--- a/src/main/java/org/embl/mobie/viewer/annotate/Annotator.java
+++ b/src/main/java/org/embl/mobie/viewer/annotate/Annotator.java
@@ -32,7 +32,8 @@
 import de.embl.cba.tables.SwingUtils;
 import de.embl.cba.tables.color.CategoryTableRowColumnColoringModel;
 import de.embl.cba.tables.color.ColorUtils;
-import de.embl.cba.tables.select.SelectionModel;
+import org.embl.mobie.viewer.select.SelectionListener;
+import org.embl.mobie.viewer.select.SelectionModel;
 import de.embl.cba.tables.tablerow.TableRow;
 import de.embl.cba.tables.tablerow.TableRowImageSegment;
 import ij.IJ;
@@ -47,7 +48,7 @@
 import java.util.List;
 import java.util.Set;
 
-public class Annotator< T extends TableRow > extends JFrame
+public class Annotator< T extends TableRow > extends JFrame implements SelectionListener< T >
 {
 	static { net.imagej.patcher.LegacyInjector.preinit(); }
 
@@ -60,13 +61,14 @@ public class Annotator< T extends TableRow > extends JFrame
 	private final RowSorter< ? extends TableModel > rowSorter;
 	private final JPanel panel;
 	private boolean skipNone;
-	private boolean isSingleRowBrowsingMode = false; // TODO: think about how to get out of this mode!
+	private boolean isKeepSelectedMode = false;
 	private JTextField goToRowIndexTextField;
 	private HashMap< String, T > annotationToTableRow;
 	private JPanel annotationButtonsContainer;
 	private JScrollPane annotationButtonsScrollPane;
 	private T currentlySelectedRow;
 	private Set< String > annotationNames;
+	private String objectName = "entity";
 
 	public Annotator( String columnName, List< T > tableRows, SelectionModel< T > selectionModel, CategoryTableRowColumnColoringModel< T > coloringModel, RowSorter< ? extends TableModel > rowSorter )
 	{
@@ -79,8 +81,22 @@ public Annotator( String columnName, List< T > tableRows, SelectionModel< T > se
 		this.rowSorter = rowSorter;
 		this.currentlySelectedRow = tableRows.get( rowSorter.convertRowIndexToModel( 0 ) );
 		this.coloringModel.fixedColorMode( true );
+		setNames( tableRows );
+		selectionModel.listeners().add( this );
+
 		this.panel = new JPanel();
+	}
 
+	private void setNames( List< T > tableRows )
+	{
+		if ( tableRows.get( 0 ) instanceof TableRowImageSegment )
+		{
+			objectName = "segment";
+		}
+		else if ( tableRows.get( 0 ) instanceof AnnotatedMaskTableRow )
+		{
+			objectName = "region";
+		}
 	}
 
 	public void showDialog()
@@ -100,12 +116,14 @@ private void createDialog()
 		panel.add( new JSeparator( SwingConstants.HORIZONTAL ) );
 		addAnnotationButtons();
 		panel.add( new JSeparator( SwingConstants.HORIZONTAL ) );
-		addTableRowBrowserSelectPanel();
-		addTableRowBrowserSelectPreviousAndNextPanel();
+		addSelectPreviousAndNextPanel();
+		addSelectIDPanel();
 		addSkipNonePanel();
-		// this has to be done at the end, to make the packing work correctly
-		// otherwise, continuing an annotation with many categories will be
-		// packed to a size too large for the screen
+		addKeepSelectedPanel();
+		// The annotation button panel has to be added at the end
+		// to make the packing work correctly.
+		// Otherwise, continuing an annotation with many categories will be
+		// packed to a size too large for the screen.
 		addAnnotationButtonPanels();
 	}
 
@@ -143,7 +161,7 @@ private void addAnnotationButtons()
 		panel.add( annotationButtonsPanel );
 
 		final JPanel panel = SwingUtils.horizontalLayoutPanel();
-		panel.add( new JLabel( "Annotate selected segment(s) as:" ) );
+		panel.add( new JLabel( "Annotate selected " + objectName + "(s) as:" ) );
 		panel.add( new JLabel( "      " ) );
 		annotationButtonsPanel.add( panel );
 
@@ -190,9 +208,7 @@ private void addAnnotationButtonPanel( String annotationName, T tableRow )
 				row.setCell( annotationColumnName, annotationName );
 			}
 
-			if ( selected.size() > 1 ) isSingleRowBrowsingMode = false;
-
-			if( isSingleRowBrowsingMode )
+			if( isKeepSelectedMode )
 			{
 				selectionModel.clearSelection(); // Hack to notify all listeners that the coloring might have changed.
 				// select again such that the user could still change its mind
@@ -219,17 +235,19 @@ private void addAnnotationButtonPanel( String annotationName, T tableRow )
 		MoBIELookAndFeelToggler.resetMoBIELaf();
 	}
 
-	private void addTableRowBrowserSelectPreviousAndNextPanel( )
+	private void addSelectPreviousAndNextPanel( )
 	{
 		final JPanel panel = SwingUtils.horizontalLayoutPanel();
+		final JButton clear = createClearSelectionButton();
 		final JButton previous = createSelectPreviousButton();
 		final JButton next = createSelectNextButton();
+		panel.add( clear );
 		panel.add( previous );
 		panel.add( next );
 		this.panel.add( panel );
 	}
 
-	private void addTableRowBrowserSelectPanel( )
+	private void addSelectIDPanel( )
 	{
 		final JPanel panel = SwingUtils.horizontalLayoutPanel();
 		final JButton button = createSelectButton();
@@ -248,8 +266,6 @@ private JButton createSelectNextButton()
 
 		next.addActionListener( e ->
 		{
-			isSingleRowBrowsingMode = true;
-
 			// rowIndex in sorted "units"
 			int rowIndex = rowSorter.convertRowIndexToView( tableRows.indexOf( currentlySelectedRow ) );
 			if ( rowIndex < tableRows.size() - 1 )
@@ -290,6 +306,21 @@ private JButton createSelectNextButton()
 		return next;
 	}
 
+	private JButton createClearSelectionButton()
+	{
+		final JButton clear = new JButton( "Clear selection" );
+		//previous.setFont( new Font("monospaced", Font.PLAIN, 12) );
+		clear.setAlignmentX( Component.CENTER_ALIGNMENT );
+
+		clear.addActionListener( e ->
+		{
+			selectionModel.clearSelection();
+		} );
+
+		return clear;
+	}
+
+
 	private JButton createSelectPreviousButton()
 	{
 		final JButton previous = new JButton( "Select previous" );
@@ -298,8 +329,6 @@ private JButton createSelectPreviousButton()
 
 		previous.addActionListener( e ->
 		{
-			isSingleRowBrowsingMode = true;
-
 			// row index in sorted "units"
 			int rowIndex = rowSorter.convertRowIndexToView( tableRows.indexOf( currentlySelectedRow ) );
 			if ( rowIndex > 0 )
@@ -344,12 +373,11 @@ private JButton createSelectPreviousButton()
 
 	private JButton createSelectButton()
 	{
-		final JButton button = new JButton( "Select segment with label id" );
+		final JButton button = new JButton( "Select " + objectName + " ID" );
 		button.setAlignmentX( Component.CENTER_ALIGNMENT );
 
 		button.addActionListener( e ->
 		{
-			isSingleRowBrowsingMode = true;
 			T selectedRow = getSelectedRow();
 			if ( selectedRow != null ) selectRow( selectedRow );
 		} );
@@ -358,7 +386,6 @@ private JButton createSelectButton()
 
 	private T getSelectedRow()
 	{
-		// TODO: in principle a flaw in logic as it assumes that all tableRows are of same type...
 		if ( tableRows.get( 0 ) instanceof TableRowImageSegment )
 		{
 			final double selectedLabelId = Double.parseDouble( goToRowIndexTextField.getText() );
@@ -370,7 +397,20 @@ private T getSelectedRow()
 					return tableRow;
 				}
 			}
-			throw new UnsupportedOperationException( "Could not find segment with label " + selectedLabelId );
+			throw new UnsupportedOperationException( "Could not find " + objectName + " with ID " + selectedLabelId );
+		}
+		else if ( tableRows.get( 0 ) instanceof AnnotatedMaskTableRow )
+		{
+			final String annotationID = goToRowIndexTextField.getText();
+			for ( T tableRow : tableRows )
+			{
+				final String name = ( ( AnnotatedMaskTableRow ) tableRow ).name();
+				if ( name.equals( annotationID ) )
+				{
+					return tableRow;
+				}
+			}
+			throw new UnsupportedOperationException( "Could not find " + objectName + " with ID " + annotationID );
 		}
 		else
 		{
@@ -387,22 +427,10 @@ private boolean isNoneOrNan( T row )
 
 	private void selectRow( T row )
 	{
-		//currentlySelectedRowIndex = sortedRowIndex;
 		currentlySelectedRow = row;
-
-
-//		if ( isNoneOrNan( row ) )
-//		{
-//			selectionColoringModel.setSelectionColoringMode( SelectionColoringModel.SelectionColoringMode.SelectionColor );
-//		}
-//		else
-//		{
-//			selectionColoringModel.setSelectionColoringMode( SelectionColoringModel.SelectionColoringMode.OnlyShowSelected );
-//		}
-
 		selectionModel.clearSelection();
 		selectionModel.setSelected( row, true );
-		selectionModel.focus( row );
+		selectionModel.focus( row, this );
 	}
 
 	private void addSkipNonePanel( )
@@ -421,6 +449,22 @@ private void addSkipNonePanel( )
 		this.panel.add( panel );
 	}
 
+	private void addKeepSelectedPanel( )
+	{
+		final JPanel panel = SwingUtils.horizontalLayoutPanel();
+
+		final JCheckBox checkBox = new JCheckBox( "Keep "+ objectName +"(s) selected after assignment" );
+		checkBox.setSelected( false );
+		isKeepSelectedMode = checkBox.isSelected();
+
+		checkBox.addActionListener( e -> {
+			isKeepSelectedMode = checkBox.isSelected();
+		}  );
+
+		panel.add( checkBox );
+		this.panel.add( panel );
+	}
+
 	private void setButtonColor( JButton button, T tableRow )
 	{
 		if ( tableRow != null )
@@ -462,4 +506,16 @@ private void refreshDialog()
 			this.pack();
 		}
 	}
+
+	@Override
+	public void selectionChanged()
+	{
+
+	}
+
+	@Override
+	public void focusEvent( T selection, Object origin )
+	{
+		currentlySelectedRow = selection;
+	}
 }
diff --git a/src/main/java/org/embl/mobie/viewer/annotate/DefaultAnnotatedMaskTableRow.java b/src/main/java/org/embl/mobie/viewer/annotate/DefaultAnnotatedMaskTableRow.java
index 060c69fad..caabb77ec 100644
--- a/src/main/java/org/embl/mobie/viewer/annotate/DefaultAnnotatedMaskTableRow.java
+++ b/src/main/java/org/embl/mobie/viewer/annotate/DefaultAnnotatedMaskTableRow.java
@@ -1,7 +1,6 @@
 package org.embl.mobie.viewer.annotate;
 
 import de.embl.cba.tables.tablerow.AbstractTableRow;
-import net.imglib2.roi.RealMask;
 import net.imglib2.roi.RealMaskRealInterval;
 
 import java.util.ArrayList;
@@ -15,15 +14,15 @@ public class DefaultAnnotatedMaskTableRow extends AbstractTableRow implements An
 {
 	protected final RealMaskRealInterval mask;
 	protected final Map< String, String > cells;
-	protected final String siteName;
+	protected final String name;
 
 	public DefaultAnnotatedMaskTableRow(
-			String siteName,
+			String name,
 			RealMaskRealInterval mask,
 			Map< String, List< String > > columns,
 			int rowIndex )
 	{
-		this.siteName = siteName;
+		this.name = name;
 		this.mask = mask;
 
 		// set cells
@@ -37,13 +36,13 @@ public DefaultAnnotatedMaskTableRow(
 	}
 
 	@Override
-	public RealMaskRealInterval getMask()
+	public RealMaskRealInterval mask()
 	{
 		return mask;
 	}
 
 	@Override
-	public Integer getTimepoint()
+	public Integer timePoint()
 	{
 		if ( cells.containsKey( "timepoint" ) )
 			return Integer.parseInt( cells.get( "timepoint" ) );
@@ -52,9 +51,9 @@ public Integer getTimepoint()
 	}
 
 	@Override
-	public String getName()
+	public String name()
 	{
-		return siteName;
+		return name;
 	}
 
 	@Override
diff --git a/src/main/java/org/embl/mobie/viewer/annotate/TableRowsIntervalImage.java b/src/main/java/org/embl/mobie/viewer/annotate/TableRowsIntervalImage.java
index 583a924da..dd3c2bd63 100644
--- a/src/main/java/org/embl/mobie/viewer/annotate/TableRowsIntervalImage.java
+++ b/src/main/java/org/embl/mobie/viewer/annotate/TableRowsIntervalImage.java
@@ -3,11 +3,15 @@
 import bdv.util.BdvOverlay;
 import bdv.util.RealRandomAccessibleIntervalSource;
 import bdv.viewer.SourceAndConverter;
+import ij.IJ;
+import net.imglib2.Interval;
+import net.imglib2.RealInterval;
 import net.imglib2.roi.RealMaskRealInterval;
+import net.imglib2.roi.geom.GeomMasks;
+import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.color.ListItemsARGBConverter;
 import de.embl.cba.tables.color.ColorUtils;
 import de.embl.cba.tables.color.ColoringModel;
-import net.imglib2.RealInterval;
 import net.imglib2.RealLocalizable;
 import net.imglib2.position.FunctionRealRandomAccessible;
 import net.imglib2.type.numeric.ARGBType;
@@ -16,7 +20,7 @@
 import org.embl.mobie.viewer.source.LabelSource;
 
 import java.awt.*;
-import java.util.HashMap;
+import java.util.Arrays;
 import java.util.List;
 import java.util.function.BiConsumer;
 
@@ -25,11 +29,10 @@ public class TableRowsIntervalImage< T extends AnnotatedMaskTableRow >
 	private final List< T > tableRows;
 	private final ColoringModel< T > coloringModel;
 	private double[] contrastLimits;
-	private HashMap< String, T > nameToTableRow;
-	private HashMap< String, Integer > nameToTableRowIndex;
 	private String name;
 	private SourceAndConverter< IntType > sourceAndConverter;
-	private RealMaskRealInterval union;
+	private RealMaskRealInterval unionMask;
+	private RealInterval unionInterval;
 	private int size;
 
 	public TableRowsIntervalImage(
@@ -37,46 +40,69 @@ public TableRowsIntervalImage(
 			ColoringModel< T > coloringModel,
 			String name )
 	{
+		final long currentTimeMillis = System.currentTimeMillis();
+
 		this.tableRows = tableRows;
 		this.coloringModel = coloringModel;
 		this.name = name;
+		this.size = tableRows.size();
 
-		init( tableRows );
+		setUnionMask( tableRows );
 		createImage();
+
+		final long duration = System.currentTimeMillis() - currentTimeMillis;
+		if ( duration > MoBIE.minLogTimeMillis )
+			IJ.log("Created annotation image "+name+" in " + duration + " ms." );
 	}
 
-	public void init( List< T > tableRows )
+	public void setUnionMask( List< T > tableRows )
 	{
 		size = tableRows.size();
-		nameToTableRow = new HashMap<>();
-		nameToTableRowIndex = new HashMap();
 
-		int rowIndex = 0;
 		for ( T tableRow : tableRows )
 		{
-			nameToTableRow.put( tableRow.getName(), tableRow );
-			nameToTableRowIndex.put( tableRow.getName(), rowIndex++ );
-			if ( union == null )
-				union = tableRow.getMask();
+			final RealMaskRealInterval mask = tableRow.mask();
+
+			if ( unionInterval == null )
+			{
+				//unionMask = mask;
+				unionInterval = mask;
+			}
 			else
-				union = union.or( tableRow.getMask() );
+			{
+				if ( Arrays.equals( mask.minAsDoubleArray(), unionInterval.minAsDoubleArray() ) && Arrays.equals( mask.maxAsDoubleArray(), unionInterval.maxAsDoubleArray() ))
+				{
+					continue;
+				}
+				else
+				{
+					// TODO: Below hangs
+					//unionMask = unionMask.or( mask );
+					unionInterval = Intervals.union( unionInterval, mask );
+				}
+			}
+			//System.out.println( tableRow.getName() );
+			//System.out.println( Arrays.toString( mask.minAsDoubleArray() ) );
+			//System.out.println( Arrays.toString( unionMask.minAsDoubleArray() ) );
 		}
 
+		// TODO: this is a work around
+		unionMask = GeomMasks.closedBox( unionInterval.minAsDoubleArray(), unionInterval.maxAsDoubleArray() );
 	}
 
 	private void createImage( )
 	{
-		BiConsumer< RealLocalizable, IntType > biConsumer = ( l, t ) ->
+		BiConsumer< RealLocalizable, IntType > biConsumer = ( location, value ) ->
 		{
-			t.setInteger( ListItemsARGBConverter.OUT_OF_BOUNDS_ROW_INDEX );
+			value.setInteger( ListItemsARGBConverter.OUT_OF_BOUNDS_ROW_INDEX );
 
 			for ( int i = 0; i < size; i++ )
 			{
-				final RealMaskRealInterval mask = tableRows.get( i ).getMask();
+				final RealMaskRealInterval mask = tableRows.get( i ).mask();
 
-				if ( mask.test( l ) )
+				if ( mask.test( location ) )
 				{
-					t.setInteger( i );
+					value.setInteger( i );
 					return;
 				}
 			}
@@ -84,12 +110,14 @@ private void createImage( )
 
 		final FunctionRealRandomAccessible< IntType > randomAccessible = new FunctionRealRandomAccessible( 3, biConsumer, IntType::new );
 
-		final IntType intType = randomAccessible.realRandomAccess().get();
-		final RealRandomAccessibleIntervalSource< IntType > source = new RealRandomAccessibleIntervalSource( randomAccessible, Intervals.smallestContainingInterval( union ), intType, name );
+		//final IntType intType = randomAccessible.realRandomAccess().get();
+		final double realMin = unionMask.realMin( 0 );
+		final Interval interval = Intervals.smallestContainingInterval( unionMask );
+		final RealRandomAccessibleIntervalSource< IntType > source = new RealRandomAccessibleIntervalSource( randomAccessible, interval, new IntType(), name );
 
 		final ListItemsARGBConverter< T > argbConverter = new ListItemsARGBConverter<>( tableRows, coloringModel );
 
-		sourceAndConverter = new SourceAndConverter( new LabelSource<>( source, ListItemsARGBConverter.OUT_OF_BOUNDS_ROW_INDEX, union ), argbConverter );
+		sourceAndConverter = new SourceAndConverter( new LabelSource<>( source, ListItemsARGBConverter.OUT_OF_BOUNDS_ROW_INDEX, unionMask ), argbConverter );
 
 		contrastLimits = new double[]{ 0, 255 };
 	}
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 ad516fdf6..391780983 100644
--- a/src/main/java/org/embl/mobie/viewer/bdv/ViewerTransformLogger.java
+++ b/src/main/java/org/embl/mobie/viewer/bdv/ViewerTransformLogger.java
@@ -11,7 +11,7 @@
 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.embl.mobie.viewer.transform.TransformHelpers;
 import org.scijava.plugin.Parameter;
 import org.scijava.plugin.Plugin;
 import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
@@ -41,7 +41,7 @@ public void run()
 			final AffineViewerTransform affineViewerTransform = new AffineViewerTransform( affineTransform3D.getRowPackedCopy(), timepoint );
 
 			// normalized affine
-			final AffineTransform3D normalisedViewerTransform = TransformHelper.createNormalisedViewerTransform( bdv.getViewerPanel() );
+			final AffineTransform3D normalisedViewerTransform = TransformHelpers.createNormalisedViewerTransform( bdv.getViewerPanel() );
 			final NormalizedAffineViewerTransform normalizedAffineViewerTransform = new NormalizedAffineViewerTransform( normalisedViewerTransform.getRowPackedCopy(), timepoint );
 
 			// normal vector
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
deleted file mode 100644
index 8a22417b1..000000000
--- a/src/main/java/org/embl/mobie/viewer/bdv/view/AnnotatedIntervalSliceView.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package org.embl.mobie.viewer.bdv.view;
-
-import bdv.util.BdvHandle;
-import bdv.viewer.SourceAndConverter;
-import org.embl.mobie.viewer.MoBIE;
-import org.embl.mobie.viewer.annotate.AnnotatedMask;
-import org.embl.mobie.viewer.annotate.AnnotatedMaskTableRow;
-import org.embl.mobie.viewer.annotate.TableRowsIntervalImage;
-import org.embl.mobie.viewer.color.ListItemsARGBConverter;
-import org.embl.mobie.viewer.color.OpacityAdjuster;
-import org.embl.mobie.viewer.display.AnnotatedIntervalDisplay;
-import org.embl.mobie.viewer.transform.PositionViewerTransform;
-import org.embl.mobie.viewer.transform.MoBIEViewerTransformChanger;
-import de.embl.cba.tables.color.ColoringListener;
-import de.embl.cba.tables.select.SelectionListener;
-import net.imglib2.type.numeric.integer.IntType;
-import sc.fiji.bdvpg.scijava.services.SourceAndConverterBdvDisplayService;
-import sc.fiji.bdvpg.services.SourceAndConverterServices;
-
-import javax.swing.*;
-import java.awt.*;
-import java.util.HashMap;
-
-
-// TODO: code duplication with SegmentationSourceDisplay => derive from a parent class
-public class AnnotatedIntervalSliceView< S extends AnnotatedMask > implements ColoringListener, SelectionListener< S >
-{
-	private final SourceAndConverterBdvDisplayService displayService;
-	private final MoBIE moBIE;
-	private final AnnotatedIntervalDisplay display;
-	private BdvHandle bdvHandle;
-
-	public AnnotatedIntervalSliceView( MoBIE moBIE, AnnotatedIntervalDisplay display, BdvHandle bdvHandle  )
-	{
-		this.moBIE = moBIE;
-		this.display = display;
-		this.bdvHandle = bdvHandle;
-		this.displayService = SourceAndConverterServices.getBdvDisplayService();
-		show();
-	}
-
-	private void show( )
-	{
-		display.selectionModel.listeners().add( this );
-		display.coloringModel.listeners().add( this );
-
-		// TODO: Make a SourceAnnotationSliceView with the listeners for the focussing.
-		final TableRowsIntervalImage< AnnotatedMaskTableRow > intervalImage = new TableRowsIntervalImage<>( display.tableRows, display.coloringModel, display.getName() );
-		SourceAndConverter< IntType > sourceAndConverter = intervalImage.getSourceAndConverter();
-		display.sourceNameToSourceAndConverter = new HashMap<>();
-		display.sourceNameToSourceAndConverter.put( intervalImage.getName(), sourceAndConverter );
-
-		// set opacity
-		OpacityAdjuster.adjustOpacity( sourceAndConverter, display.getOpacity() );
-
-		// show
-		final boolean visible = display.isVisible();
-		displayService.show( bdvHandle, visible, sourceAndConverter );
-
-		// add listener
-		ListItemsARGBConverter converter = ( ListItemsARGBConverter ) sourceAndConverter.getConverter();
-		bdvHandle.getViewerPanel().addTimePointListener(  converter );
-	}
-
-	public void close( boolean closeImgLoader )
-	{
-		for ( SourceAndConverter< ? > sourceAndConverter : display.sourceNameToSourceAndConverter.values() )
-		{
-			moBIE.closeSourceAndConverter( sourceAndConverter, closeImgLoader );
-		}
-		display.sourceNameToSourceAndConverter.clear();
-	};
-
-	@Override
-	public synchronized void coloringChanged()
-	{
-		bdvHandle.getViewerPanel().requestRepaint();
-	}
-
-	@Override
-	public synchronized void selectionChanged()
-	{
-		bdvHandle.getViewerPanel().requestRepaint();
-	}
-
-	@Override
-	public synchronized void focusEvent( S selection )
-	{
-		if ( selection.getTimepoint() != getBdvHandle().getViewerPanel().state().getCurrentTimepoint() )
-		{
-			getBdvHandle().getViewerPanel().state().setCurrentTimepoint( selection.getTimepoint() );
-		}
-
-		final double[] max = selection.getMask().maxAsDoubleArray();
-		final double[] min = selection.getMask().minAsDoubleArray();
-		final double[] center = new double[ min.length ];
-		for ( int d = 0; d < 3; d++ )
-		{
-			center[ d ] = ( max[ d ] + min[ d ] ) / 2;
-		}
-
-		MoBIEViewerTransformChanger.changeViewerTransform( bdvHandle, new PositionViewerTransform( center, bdvHandle.getViewerPanel().state().getCurrentTimepoint() ) );
-
-	}
-
-	public BdvHandle getBdvHandle()
-	{
-		return bdvHandle;
-	}
-
-	public Window getWindow()
-	{
-		return SwingUtilities.getWindowAncestor( bdvHandle.getViewerPanel() );
-	}
-}
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/view/AnnotatedMaskSliceView.java b/src/main/java/org/embl/mobie/viewer/bdv/view/AnnotatedMaskSliceView.java
new file mode 100644
index 000000000..5a0f0f770
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/bdv/view/AnnotatedMaskSliceView.java
@@ -0,0 +1,62 @@
+package org.embl.mobie.viewer.bdv.view;
+
+import bdv.util.BdvHandle;
+import bdv.viewer.SourceAndConverter;
+import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.annotate.AnnotatedMaskTableRow;
+import org.embl.mobie.viewer.annotate.TableRowsIntervalImage;
+import org.embl.mobie.viewer.color.ListItemsARGBConverter;
+import org.embl.mobie.viewer.display.AnnotatedSourceDisplay;
+import org.embl.mobie.viewer.segment.SliceViewRegionSelector;
+import org.embl.mobie.viewer.transform.PositionViewerTransform;
+import org.embl.mobie.viewer.transform.MoBIEViewerTransformChanger;
+import net.imglib2.type.numeric.integer.IntType;
+
+
+public class AnnotatedMaskSliceView extends AnnotatedRegionSliceView< AnnotatedMaskTableRow >
+{
+	public AnnotatedMaskSliceView( MoBIE moBIE, AnnotatedSourceDisplay display, BdvHandle bdvHandle  )
+	{
+		super( moBIE, display, bdvHandle );
+
+		final SourceAndConverter< IntType > sourceAndConverter = createSourceAndConverter();
+
+		show( sourceAndConverter );
+	}
+
+	private SourceAndConverter< IntType > createSourceAndConverter()
+	{
+		final TableRowsIntervalImage< AnnotatedMaskTableRow > maskImage = new TableRowsIntervalImage<>( display.tableRows, display.coloringModel, display.getName() );
+
+		final SourceAndConverter< IntType > sourceAndConverter = maskImage.getSourceAndConverter();
+		return sourceAndConverter;
+	}
+
+	@Override
+	public synchronized void focusEvent( AnnotatedMaskTableRow selection, Object origin  )
+	{
+		if ( origin instanceof SliceViewRegionSelector )
+			return;
+
+		if ( selection.timePoint() != getBdvHandle().getViewerPanel().state().getCurrentTimepoint() )
+		{
+			getBdvHandle().getViewerPanel().state().setCurrentTimepoint( selection.timePoint() );
+		}
+
+		final double[] center = getPosition( selection );
+
+		MoBIEViewerTransformChanger.changeViewerTransform( bdvHandle, new PositionViewerTransform( center, bdvHandle.getViewerPanel().state().getCurrentTimepoint() ) );
+	}
+
+	private double[] getPosition( AnnotatedMaskTableRow selection )
+	{
+		final double[] max = selection.mask().maxAsDoubleArray();
+		final double[] min = selection.mask().minAsDoubleArray();
+		final double[] center = new double[ min.length ];
+		for ( int d = 0; d < 3; d++ )
+		{
+			center[ d ] = ( max[ d ] + min[ d ] ) / 2;
+		}
+		return center;
+	}
+}
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/view/AnnotatedRegionSliceView.java b/src/main/java/org/embl/mobie/viewer/bdv/view/AnnotatedRegionSliceView.java
new file mode 100644
index 000000000..275829039
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/bdv/view/AnnotatedRegionSliceView.java
@@ -0,0 +1,100 @@
+package org.embl.mobie.viewer.bdv.view;
+
+import bdv.util.BdvHandle;
+import bdv.viewer.SourceAndConverter;
+import bdv.viewer.TimePointListener;
+import de.embl.cba.tables.color.ColoringListener;
+import de.embl.cba.tables.tablerow.TableRow;
+import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.color.OpacityAdjuster;
+import org.embl.mobie.viewer.display.AnnotatedRegionDisplay;
+import org.embl.mobie.viewer.select.SelectionListener;
+import org.embl.mobie.viewer.source.LabelSource;
+import sc.fiji.bdvpg.scijava.services.SourceAndConverterBdvDisplayService;
+import sc.fiji.bdvpg.services.SourceAndConverterServices;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.HashMap;
+
+
+public abstract class AnnotatedRegionSliceView< T extends TableRow > implements ColoringListener, SelectionListener< T >
+{
+	protected final SourceAndConverterBdvDisplayService displayService;
+	protected final MoBIE moBIE;
+	protected final AnnotatedRegionDisplay< T > display;
+	protected BdvHandle bdvHandle;
+
+	public AnnotatedRegionSliceView( MoBIE moBIE, AnnotatedRegionDisplay< T > display, BdvHandle bdvHandle  )
+	{
+		this.moBIE = moBIE;
+		this.display = display;
+		this.bdvHandle = bdvHandle;
+		this.displayService = SourceAndConverterServices.getBdvDisplayService();
+		display.sourceNameToSourceAndConverter = new HashMap<>();
+		display.selectionModel.listeners().add( this );
+		display.coloringModel.listeners().add( this );
+	}
+
+	protected void show( SourceAndConverter< ? > sourceAndConverter )
+	{
+		adjustLabelRendering( sourceAndConverter );
+
+		OpacityAdjuster.adjustOpacity( sourceAndConverter, display.getOpacity() );
+
+		// show
+		final boolean visible = display.isVisible();
+		displayService.show( bdvHandle, visible, sourceAndConverter );
+
+		bdvHandle.getViewerPanel().addTimePointListener( ( TimePointListener ) sourceAndConverter.getConverter() );
+
+		// register
+		display.sourceNameToSourceAndConverter.put( sourceAndConverter.getSpimSource().getName(), sourceAndConverter );
+	}
+
+	private void adjustLabelRendering( SourceAndConverter< ? > sourceAndConverter )
+	{
+		final boolean showAsBoundaries = display.isShowAsBoundaries();
+		final float boundaryThickness = display.getBoundaryThickness();
+		( (LabelSource) sourceAndConverter.getSpimSource() ).showAsBoundary( showAsBoundaries, boundaryThickness );
+		if ( sourceAndConverter.asVolatile() != null )
+			( (LabelSource) sourceAndConverter.asVolatile().getSpimSource() ).showAsBoundary( showAsBoundaries, boundaryThickness );
+	}
+
+	public void close( boolean closeImgLoader )
+	{
+		for ( SourceAndConverter< ? > sourceAndConverter : display.sourceNameToSourceAndConverter.values() )
+		{
+			moBIE.closeSourceAndConverter( sourceAndConverter, closeImgLoader );
+		}
+		display.sourceNameToSourceAndConverter.clear();
+	};
+
+	@Override
+	public synchronized void coloringChanged()
+	{
+		bdvHandle.getViewerPanel().requestRepaint();
+	}
+
+	@Override
+	public synchronized void selectionChanged()
+	{
+		bdvHandle.getViewerPanel().requestRepaint();
+	}
+
+	@Override
+	public synchronized void focusEvent( T selection, Object origin  )
+	{
+		// must be defined in child classes
+	}
+
+	public BdvHandle getBdvHandle()
+	{
+		return bdvHandle;
+	}
+
+	public Window getWindow()
+	{
+		return SwingUtilities.getWindowAncestor( bdvHandle.getViewerPanel() );
+	}
+}
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 69c8bab1f..535d61cf7 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
@@ -4,6 +4,7 @@
 import bdv.util.BdvHandle;
 import bdv.viewer.SourceAndConverter;
 import de.embl.cba.bdv.utils.lut.GlasbeyARGBLut;
+import net.imglib2.realtransform.AffineTransform3D;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.bdv.render.BlendingMode;
 import org.embl.mobie.viewer.color.OpacityAdjuster;
@@ -69,6 +70,9 @@ private void show( )
 			// show
 			SourceAndConverterServices.getBdvDisplayService().show( bdvHandle, display.isVisible(), sourceAndConverter );
 
+			final AffineTransform3D transform3D = new AffineTransform3D();
+			sourceAndConverter.getSpimSource().getSourceTransform( 0,0, transform3D );
+
 			// adapt contrast limits
 			final ConverterSetup converterSetup = SourceAndConverterServices.getSourceAndConverterService().getConverterSetup( sourceAndConverter );
 			converterSetup.setDisplayRange( display.getContrastLimits()[ 0 ], display.getContrastLimits()[ 1 ] );
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 25a6c9ca4..1e027733d 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
@@ -2,98 +2,49 @@
 
 import bdv.util.BdvHandle;
 import bdv.viewer.SourceAndConverter;
-import bdv.viewer.TimePointListener;
+import de.embl.cba.tables.tablerow.TableRowImageSegment;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.bdv.render.BlendingMode;
-import org.embl.mobie.viewer.color.OpacityAdjuster;
 import org.embl.mobie.viewer.color.LabelConverter;
 import org.embl.mobie.viewer.display.SegmentationSourceDisplay;
-import de.embl.cba.tables.color.ColoringListener;
-import de.embl.cba.tables.imagesegment.ImageSegment;
-import de.embl.cba.tables.select.SelectionListener;
+import org.embl.mobie.viewer.segment.SliceViewRegionSelector;
 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;
 import sc.fiji.bdvpg.services.SourceAndConverterServices;
 
-import javax.swing.*;
-import java.awt.*;
-import java.util.HashMap;
-import java.util.Map;
-
-public class SegmentationSliceView< S extends ImageSegment > implements ColoringListener, SelectionListener< S >
+public class SegmentationSliceView extends AnnotatedRegionSliceView< TableRowImageSegment >
 {
-	private final SourceAndConverterBdvDisplayService displayService;
-	private final MoBIE moBIE;
-	private final SegmentationSourceDisplay display;
-	private BdvHandle bdvHandle;
-
 	public SegmentationSliceView( MoBIE moBIE, SegmentationSourceDisplay display, BdvHandle bdvHandle )
 	{
-		this.moBIE = moBIE;
-		this.display = display;
-		this.bdvHandle = bdvHandle;
-
-		displayService = SourceAndConverterServices.getBdvDisplayService();
-		show();
-	}
-
-	private void show( )
-	{
-		display.selectionModel.listeners().add( this );
-		display.coloringModel.listeners().add( this );
-
-		Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter = new HashMap<>();
-		for ( String name : display.getSources() ) {
-			sourceNameToSourceAndConverter.put( name, moBIE.getTransformedSourceAndConverter( name ) );
-		}
-
-		// convert to labelSource
-		sourceNameToSourceAndConverter = asLabelSources( sourceNameToSourceAndConverter );
+		super( moBIE, display, bdvHandle );
 
-		display.sourceNameToSourceAndConverter = new HashMap<>();
-		for ( String name : sourceNameToSourceAndConverter.keySet() )
+		for ( String name : display.getSources() )
 		{
-			SourceAndConverter< ? > sourceAndConverter = sourceNameToSourceAndConverter.get( name );
-			// set opacity
-			OpacityAdjuster.adjustOpacity( sourceAndConverter, display.getOpacity() );
+			final SourceAndConverter< ? > sourceAndConverter = moBIE.getTransformedSourceAndConverter( name );
 
-			// show
-			displayService.show( bdvHandle, display.isVisible(), sourceAndConverter );
+			SourceAndConverter< ? > labelSourceAndConverter = asLabelSourceAndConverter( display, sourceAndConverter );
 
-			// set blending mode
-			if ( display.getBlendingMode() != null )
-				SourceAndConverterServices.getSourceAndConverterService().setMetadata( sourceAndConverter, BlendingMode.BLENDING_MODE, display.getBlendingMode() );
-
-			bdvHandle.getViewerPanel().addTimePointListener( ( TimePointListener ) sourceAndConverter.getConverter() );
+			show( labelSourceAndConverter );
 
-			display.sourceNameToSourceAndConverter.put( name, sourceAndConverter );
+			if ( display.getBlendingMode() != null )
+				SourceAndConverterServices.getSourceAndConverterService().setMetadata( labelSourceAndConverter, BlendingMode.BLENDING_MODE, display.getBlendingMode() );
 		}
 	}
 
-	private Map< String, SourceAndConverter< ? > > asLabelSources( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter )
+	private SourceAndConverter< ? > asLabelSourceAndConverter( SegmentationSourceDisplay display, SourceAndConverter< ? > sourceAndConverter )
 	{
-		Map< String, SourceAndConverter< ? > > sourceNameToLabelSourceAndConverter = new HashMap<>();
-
-		for ( String name: sourceNameToSourceAndConverter.keySet() )
-		{
-			SourceAndConverter< ? > sourceAndConverter = sourceNameToSourceAndConverter.get( name );
+		LabelConverter labelConverter = getLabelConverter( display, sourceAndConverter );
 
-			LabelConverter< S > labelConverter = getLabelConverter( sourceAndConverter );
+		SourceAndConverter< ? > labelSourceAndConverter = asLabelSourceAndConverter( sourceAndConverter, labelConverter );
 
-			SourceAndConverter< ? > sourceAndLabelConverter = asSourceAndLabelConverter( sourceAndConverter, labelConverter );
-
-			sourceNameToLabelSourceAndConverter.put( name, sourceAndLabelConverter );
-		}
-
-		return sourceNameToLabelSourceAndConverter;
+		return labelSourceAndConverter;
 	}
 
-	private LabelConverter< S > getLabelConverter( SourceAndConverter< ? > sourceAndConverter )
+	private LabelConverter getLabelConverter( SegmentationSourceDisplay display, SourceAndConverter< ? > sourceAndConverter )
 	{
 		if ( MergedGridSource.instanceOf( sourceAndConverter ) )
 		{
@@ -101,7 +52,6 @@ private LabelConverter< S > getLabelConverter( SourceAndConverter< ? > sourceAnd
 			// 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 );
@@ -115,7 +65,7 @@ private LabelConverter< S > getLabelConverter( SourceAndConverter< ? > sourceAnd
 		}
 	}
 
-	private SourceAndConverter asSourceAndLabelConverter( SourceAndConverter< ? > sourceAndConverter, LabelConverter labelConverter )
+	private SourceAndConverter asLabelSourceAndConverter( SourceAndConverter< ? > sourceAndConverter, LabelConverter labelConverter )
 	{
 		LabelSource volatileLabelSource = new LabelSource( sourceAndConverter.asVolatile().getSpimSource() );
 		SourceAndConverter volatileSourceAndConverter = new SourceAndConverter( volatileLabelSource, labelConverter );
@@ -123,30 +73,12 @@ private SourceAndConverter asSourceAndLabelConverter( SourceAndConverter< ? > so
 		return new SourceAndConverter( labelSource, labelConverter, volatileSourceAndConverter );
 	}
 
-	public void close( boolean closeImgLoader )
-	{
-		for ( SourceAndConverter< ? > sourceAndConverter : display.sourceNameToSourceAndConverter.values() )
-		{
-			moBIE.closeSourceAndConverter( sourceAndConverter, closeImgLoader );
-		}
-		display.sourceNameToSourceAndConverter.clear();
-	};
-
 	@Override
-	public synchronized void coloringChanged()
+	public synchronized void focusEvent( TableRowImageSegment selection, Object origin  )
 	{
-		bdvHandle.getViewerPanel().requestRepaint();
-	}
-
-	@Override
-	public synchronized void selectionChanged()
-	{
-		bdvHandle.getViewerPanel().requestRepaint();
-	}
+		if ( origin instanceof SliceViewRegionSelector )
+			return;
 
-	@Override
-	public synchronized void focusEvent( S selection )
-	{
 		if ( selection.timePoint() != getBdvHandle().getViewerPanel().state().getCurrentTimepoint() )
 		{
 			getBdvHandle().getViewerPanel().state().setCurrentTimepoint( selection.timePoint() );
@@ -180,15 +112,4 @@ private void adaptPosition( double[] position, String sourceName )
 		// adapt
 		sourceTransform.apply( position, position );
 	}
-
-	public BdvHandle getBdvHandle()
-	{
-		return bdvHandle;
-	}
-
-	public Window getWindow()
-	{
-		return SwingUtilities.getWindowAncestor( bdvHandle.getViewerPanel() );
-	}
-
 }
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 c416ed860..76d85d78e 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
@@ -16,7 +16,7 @@
 import org.embl.mobie.viewer.command.ShowRasterImagesCommand;
 import org.embl.mobie.viewer.command.SourceAndConverterBlendingModeChangerCommand;
 import org.embl.mobie.viewer.command.ScreenShotMakerCommand;
-import org.embl.mobie.viewer.segment.BdvSegmentSelector;
+import org.embl.mobie.viewer.segment.SliceViewRegionSelector;
 import org.embl.mobie.viewer.command.RandomColorSeedChangerCommand;
 import org.embl.mobie.viewer.view.ViewManager;
 import org.scijava.ui.behaviour.ClickBehaviour;
@@ -33,6 +33,8 @@
 import java.util.*;
 import java.util.function.Supplier;
 
+import static org.embl.mobie.viewer.ui.WindowArrangementHelper.setBdvWindowPositionAndSize;
+
 public class SliceViewer implements Supplier< BdvHandle >
 {
 	public static final String UNDO_SEGMENT_SELECTIONS = "Undo Segment Selections [ Ctrl Shift N ]";
@@ -61,6 +63,7 @@ public SliceViewer( boolean is2D, ViewManager viewManager, int timepoints, Array
 		sacDisplayService = SourceAndConverterServices.getBdvDisplayService();
 
 		bdvHandle = createBdv( timepoints, is2D, FRAME_TITLE );
+		setBdvWindowPositionAndSize( bdvHandle );
 		sacDisplayService.registerBdvHandle( bdvHandle );
 
 		installContextMenuAndKeyboardShortCuts();
@@ -79,19 +82,13 @@ public BdvHandle get()
 
 	private void installContextMenuAndKeyboardShortCuts( )
 	{
-		final BdvSegmentSelector segmentBdvSelector = new BdvSegmentSelector( bdvHandle, is2D, () -> viewManager.getSegmentationDisplays() );
+		final SliceViewRegionSelector sliceViewRegionSelector = new SliceViewRegionSelector( bdvHandle, is2D, () -> viewManager.getAnnotatedRegionDisplays() );
 
 		sacService.registerAction( UNDO_SEGMENT_SELECTIONS, sourceAndConverters -> {
 			// TODO: Maybe only do this for the sacs at the mouse position
-			segmentBdvSelector.clearSelection();
+			sliceViewRegionSelector.clearSelection();
 		} );
 
-		// 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
 			viewManager.getAdditionalViewsLoader().loadAdditionalViewsDialog();
@@ -137,15 +134,12 @@ private void installContextMenuAndKeyboardShortCuts( )
 
 		behaviours.behaviour(
 				( ClickBehaviour ) ( x, y ) ->
-						new Thread( () -> segmentBdvSelector.run() ).start(),
+						new Thread( () -> sliceViewRegionSelector.run() ).start(),
 				"Toggle selection", "ctrl button1" ) ;
 
 		behaviours.behaviour(
 				( ClickBehaviour ) ( x, y ) ->
-						new Thread( () ->
-						{
-							segmentBdvSelector.clearSelection();
-						}).start(),
+						new Thread( () -> sliceViewRegionSelector.clearSelection() ).start(),
 				"Clear selection", "ctrl shift N" ) ;
 
 		behaviours.behaviour(
diff --git a/src/main/java/org/embl/mobie/viewer/color/MoBIEColoringModel.java b/src/main/java/org/embl/mobie/viewer/color/MoBIEColoringModel.java
index c853546d1..3322bcbdc 100644
--- a/src/main/java/org/embl/mobie/viewer/color/MoBIEColoringModel.java
+++ b/src/main/java/org/embl/mobie/viewer/color/MoBIEColoringModel.java
@@ -29,7 +29,7 @@
 package org.embl.mobie.viewer.color;
 
 import de.embl.cba.tables.color.*;
-import de.embl.cba.tables.select.SelectionModel;
+import org.embl.mobie.viewer.select.SelectionModel;
 import net.imglib2.type.numeric.ARGBType;
 
 public class MoBIEColoringModel< T > extends AbstractColoringModel< T >
diff --git a/src/main/java/org/embl/mobie/viewer/command/BigWarpRegistrationCommand.java b/src/main/java/org/embl/mobie/viewer/command/BigWarpRegistrationCommand.java
index bb996ce25..cc9d4292e 100644
--- a/src/main/java/org/embl/mobie/viewer/command/BigWarpRegistrationCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/BigWarpRegistrationCommand.java
@@ -12,7 +12,7 @@
 import ij.gui.NonBlockingGenericDialog;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.realtransform.InvertibleRealTransform;
-import org.embl.mobie.viewer.transform.TransformHelper;
+import org.embl.mobie.viewer.transform.TransformHelpers;
 import org.scijava.plugin.Parameter;
 import org.scijava.plugin.Plugin;
 import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
@@ -70,7 +70,7 @@ public void run()
 
 		bigWarp = bigWarpLauncher.getBigWarp();
 
-		final AffineTransform3D normalisedViewerTransform = TransformHelper.createNormalisedViewerTransform( bdvHandle.getViewerPanel() );
+		final AffineTransform3D normalisedViewerTransform = TransformHelpers.createNormalisedViewerTransform( bdvHandle.getViewerPanel() );
 		applyViewerTransform( normalisedViewerTransform, bigWarp.getViewerFrameQ().getViewerPanel() );
 		applyViewerTransform( normalisedViewerTransform, bigWarp.getViewerFrameP().getViewerPanel() );
 		bigWarp.setTransformType( TransformTypeSelectDialog.AFFINE );
@@ -81,7 +81,7 @@ public void run()
 
 	private void applyViewerTransform( AffineTransform3D normalisedViewerTransform, BigWarpViewerPanel viewerPanel )
 	{
-		viewerPanel.state().setViewerTransform( TransformHelper.createUnnormalizedViewerTransform( normalisedViewerTransform, viewerPanel ) );
+		viewerPanel.state().setViewerTransform( TransformHelpers.createUnnormalizedViewerTransform( normalisedViewerTransform, viewerPanel ) );
 	}
 
 	private void showDialog()
diff --git a/src/main/java/org/embl/mobie/viewer/command/LabelRenderingConfiguratorCommand.java b/src/main/java/org/embl/mobie/viewer/command/LabelRenderingConfiguratorCommand.java
index 08fd14c24..76b354371 100644
--- a/src/main/java/org/embl/mobie/viewer/command/LabelRenderingConfiguratorCommand.java
+++ b/src/main/java/org/embl/mobie/viewer/command/LabelRenderingConfiguratorCommand.java
@@ -22,8 +22,8 @@ public class LabelRenderingConfiguratorCommand implements BdvPlaygroundActionCom
 	@Parameter ( label = "Show as boundaries")
 	boolean showAsBoundary;
 
-	@Parameter ( label = "Boundary thickness" )
-	double boundaryThickness = 1.0;
+	@Parameter ( label = "Boundary thickness", style="format:#.00" )
+	float boundaryThickness = 1.0F;
 
 	@Override
 	public void run()
diff --git a/src/main/java/org/embl/mobie/viewer/display/AnnotatedRegionDisplay.java b/src/main/java/org/embl/mobie/viewer/display/AnnotatedRegionDisplay.java
index 12d69be4d..4900a50c4 100644
--- a/src/main/java/org/embl/mobie/viewer/display/AnnotatedRegionDisplay.java
+++ b/src/main/java/org/embl/mobie/viewer/display/AnnotatedRegionDisplay.java
@@ -1,5 +1,7 @@
 package org.embl.mobie.viewer.display;
 
+import bdv.tools.transformation.TransformedSource;
+import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
 import de.embl.cba.tables.color.ColoringModel;
 import de.embl.cba.tables.color.ColumnColoringModel;
@@ -8,9 +10,10 @@
 import org.embl.mobie.viewer.color.LabelConverter;
 import org.embl.mobie.viewer.color.MoBIEColoringModel;
 import org.embl.mobie.viewer.plot.ScatterPlotViewer;
+import org.embl.mobie.viewer.source.LabelSource;
 import org.embl.mobie.viewer.table.TableViewer;
 import de.embl.cba.tables.color.ColoringLuts;
-import de.embl.cba.tables.select.SelectionModel;
+import org.embl.mobie.viewer.select.SelectionModel;
 import de.embl.cba.tables.tablerow.TableRow;
 
 import java.util.ArrayList;
@@ -26,6 +29,8 @@ public abstract class AnnotatedRegionDisplay< T extends TableRow > extends Abstr
 	protected String[] scatterPlotAxes = new String[]{ TableColumnNames.ANCHOR_X, TableColumnNames.ANCHOR_Y };
 	protected List< String > tables; // tables to display
 	protected boolean showTable = true;
+	protected boolean showAsBoundaries = false;
+	protected float boundaryThickness = 1.0F;
 
 	// Runtime
 	public transient SelectionModel< T > selectionModel;
@@ -69,14 +74,26 @@ public boolean showTable()
 		return showTable;
 	}
 
-	protected void fetchCurrentSettings ( AnnotatedRegionDisplay<T> annotatedRegionDisplay ) {
+	public boolean isShowAsBoundaries()
+	{
+		return showAsBoundaries;
+	}
+
+	public float getBoundaryThickness()
+	{
+		return boundaryThickness;
+	}
+
+	protected void fetchCurrentSettings( AnnotatedRegionDisplay<T> annotatedRegionDisplay )
+	{
 		this.name = annotatedRegionDisplay.name;
 
 		final SourceAndConverter< ? > sourceAndConverter = annotatedRegionDisplay.sourceNameToSourceAndConverter.values().iterator().next();
 
 		if( sourceAndConverter.getConverter() instanceof LabelConverter)
 		{
-			this.opacity = ( ( LabelConverter ) sourceAndConverter.getConverter() ).getOpacity();
+			final LabelConverter labelConverter = ( LabelConverter ) sourceAndConverter.getConverter();
+			this.opacity = labelConverter.getOpacity();
 		}
 
 		this.lut = annotatedRegionDisplay.coloringModel.getARGBLutName();
@@ -108,6 +125,16 @@ protected void fetchCurrentSettings ( AnnotatedRegionDisplay<T> annotatedRegionD
 		}
 
 		this.showTable = annotatedRegionDisplay.tableViewer.getWindow().isVisible();
-	}
 
+		if ( sourceAndConverter.getSpimSource() instanceof TransformedSource )
+		{
+			final Source< ? > source = ( ( TransformedSource< ? > ) sourceAndConverter.getSpimSource() ).getWrappedSource();
+			if ( source instanceof LabelSource )
+			{
+				final LabelSource labelSource = ( LabelSource ) source;
+				this.showAsBoundaries = labelSource.isShowAsBoundaries();
+				this.boundaryThickness = labelSource.getBoundaryWidth();
+			}
+		}
+	}
 }
diff --git a/src/main/java/org/embl/mobie/viewer/display/AnnotatedIntervalDisplay.java b/src/main/java/org/embl/mobie/viewer/display/AnnotatedSourceDisplay.java
similarity index 55%
rename from src/main/java/org/embl/mobie/viewer/display/AnnotatedIntervalDisplay.java
rename to src/main/java/org/embl/mobie/viewer/display/AnnotatedSourceDisplay.java
index 696af922f..0c414ff50 100644
--- a/src/main/java/org/embl/mobie/viewer/display/AnnotatedIntervalDisplay.java
+++ b/src/main/java/org/embl/mobie/viewer/display/AnnotatedSourceDisplay.java
@@ -1,14 +1,17 @@
 package org.embl.mobie.viewer.display;
 
-import org.embl.mobie.viewer.annotate.AnnotatedIntervalAdapter;
+import org.embl.mobie.viewer.annotate.AnnotatedMaskAdapter;
 import org.embl.mobie.viewer.annotate.AnnotatedMaskTableRow;
-import org.embl.mobie.viewer.bdv.view.AnnotatedIntervalSliceView;
+import org.embl.mobie.viewer.bdv.view.AnnotatedMaskSliceView;
 import org.embl.mobie.viewer.source.StorageLocation;
 import org.embl.mobie.viewer.table.TableDataFormat;
 
 import java.util.*;
 
-public class AnnotatedIntervalDisplay extends AnnotatedRegionDisplay< AnnotatedMaskTableRow >
+// Even though within the rest of the code we call things now AnnotatedMask*
+// the name of this class for now remains AnnotatedSourceDisplay
+// in order to stay consistent with the nomenclature in dataset.json
+public class AnnotatedSourceDisplay extends AnnotatedRegionDisplay< AnnotatedMaskTableRow >
 {
 	// Serialization
 	protected Map< String, List< String > > sources;
@@ -16,8 +19,8 @@ public class AnnotatedIntervalDisplay extends AnnotatedRegionDisplay< AnnotatedM
 	protected Map< TableDataFormat, StorageLocation > tableData;
 
 	// Runtime
-	public transient AnnotatedIntervalAdapter< AnnotatedMaskTableRow > annotatedIntervalAdapter;
-	public transient AnnotatedIntervalSliceView< AnnotatedMaskTableRow > sliceView;
+	public transient AnnotatedMaskAdapter annotatedMaskAdapter;
+	public transient AnnotatedMaskSliceView sliceView;
 
 	// Getters for the serialised fields
 	public List< String > getSelectedAnnotationIds()
@@ -44,9 +47,11 @@ public List< String > getSources()
 		return allSources;
 	}
 
-	public AnnotatedIntervalDisplay() {}
+	// Needed for Gson
+	public AnnotatedSourceDisplay() {}
 
-	public AnnotatedIntervalDisplay( String name, double opacity, Map< String, List< String > > sources, String lut, String colorByColumn, Double[] valueLimits, List< String > selectedSegmentIds, boolean showScatterPlot, String[] scatterPlotAxes, List< String > tables )
+	// TODO: Looks like we do not need it? Maybe for the interactive grid view?
+	public AnnotatedSourceDisplay( String name, double opacity, Map< String, List< String > > sources, String lut, String colorByColumn, Double[] valueLimits, List< String > selectedSegmentIds, boolean showScatterPlot, String[] scatterPlotAxes, List< String > tables )
 	{
 		this.name = name;
 		this.opacity = opacity;
@@ -63,25 +68,25 @@ public AnnotatedIntervalDisplay( String name, double opacity, Map< String, List<
 	/**
 	 * Create a serializable copy
 	 *
-	 * @param annotatedIntervalDisplay
+	 * @param annotatedSourceDisplay
 	 */
-	public AnnotatedIntervalDisplay( AnnotatedIntervalDisplay annotatedIntervalDisplay )
+	public AnnotatedSourceDisplay( AnnotatedSourceDisplay annotatedSourceDisplay )
 	{
-		fetchCurrentSettings( annotatedIntervalDisplay );
+		fetchCurrentSettings( annotatedSourceDisplay );
 
 		this.sources = new HashMap<>();
-		this.sources.putAll( annotatedIntervalDisplay.sources );
+		this.sources.putAll( annotatedSourceDisplay.sources );
 
-		Set< AnnotatedMaskTableRow > currentSelectedRows = annotatedIntervalDisplay.selectionModel.getSelected();
+		Set< AnnotatedMaskTableRow > currentSelectedRows = annotatedSourceDisplay.selectionModel.getSelected();
 		if ( currentSelectedRows != null && currentSelectedRows.size() > 0 ) {
 			ArrayList<String> selectedIds = new ArrayList<>();
 			for ( AnnotatedMaskTableRow row : currentSelectedRows ) {
-				selectedIds.add( row.getTimepoint() + ";" + row.getName() );
+				selectedIds.add( row.timePoint() + ";" + row.name() );
 			}
 			this.selectedAnnotationIds = selectedIds;
 		}
 
 		this.tableData = new HashMap<>();
-		this.tableData.putAll( annotatedIntervalDisplay.tableData );
+		this.tableData.putAll( annotatedSourceDisplay.tableData );
 	}
 }
diff --git a/src/main/java/org/embl/mobie/viewer/display/GridOverlaySourceDisplay.txt b/src/main/java/org/embl/mobie/viewer/display/GridOverlaySourceDisplay.txt
deleted file mode 100644
index 4455c070b..000000000
--- a/src/main/java/org/embl/mobie/viewer/display/GridOverlaySourceDisplay.txt
+++ /dev/null
@@ -1,153 +0,0 @@
-package de.embl.cba.mobie.display;
-
-import bdv.util.BdvHandle;
-import bdv.viewer.SourceAndConverter;
-import de.embl.cba.bdv.utils.Logger;
-import Utils;
-import MoBIE;
-import AnnotatedOME-Zarrator;
-import AnnotatedIntervalTableRow;
-import DefaultAnnotatedIntervalTableRow;
-import TableRowsIntervalImage;
-import PositionViewerTransform;
-import de.embl.cba.mobie.transform.ViewerTransformChanger;
-import MoBIEColoringModel;
-import GridSourceTransformer;
-import TableViewer;
-import de.embl.cba.tables.TableColumns;
-import de.embl.cba.tables.color.ColoringListener;
-import de.embl.cba.tables.color.ColoringLuts;
-import de.embl.cba.tables.select.DefaultSelectionModel;
-import de.embl.cba.tables.select.SelectionListener;
-import net.imglib2.type.numeric.integer.IntType;
-import sc.fiji.bdvpg.services.SourceAndConverterServices;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class GridOverlaySourceDisplay extends SourceDisplay implements ColoringListener, SelectionListener< DefaultAnnotatedIntervalTableRow >
-{
-	private final MoBIEColoringModel< DefaultAnnotatedIntervalTableRow > coloringModel;
-	private final DefaultSelectionModel< DefaultAnnotatedIntervalTableRow > selectionModel;
-	private final TableViewer< DefaultAnnotatedIntervalTableRow > tableViewer;
-
-	private final BdvHandle bdvHandle;
-
-	public GridOverlaySourceDisplay( MoBIE moBIE, BdvHandle bdvHandle, String tableDataFolder, GridSourceTransformer sourceTransformer )
-	{
-		this.bdvHandle = bdvHandle;
-		this.name = sourceTransformer.getName();
-
-		// open tables
-		final List< DefaultAnnotatedIntervalTableRow > tableRows = openGridTables( moBIE, tableDataFolder, sourceTransformer, sourceTransformer.tables );
-
-		coloringModel = new MoBIEColoringModel< >( ColoringLuts.GLASBEY );
-		selectionModel = new DefaultSelectionModel< >();
-		coloringModel.setSelectionModel( selectionModel );
-
-		HashMap<String, String> nameToTableDir = new HashMap<>();
-		nameToTableDir.put( sourceTransformer.getName(), tableDataFolder );
-		tableViewer = new TableViewer<>( moBIE, tableRows, selectionModel, coloringModel, name, nameToTableDir, true ).show();
-
-		coloringModel.listeners().add( tableViewer );
-		selectionModel.listeners().add( tableViewer );
-
-		showGridImage( bdvHandle, name, tableRows );
-
-		coloringModel.listeners().add( this );
-		selectionModel.listeners().add( this );
-	}
-
-	// TODO: maybe replace the GridSourceTransform by a functional?
-	/GridOverlaySourceDisplay.txt
-	private List< AnnotatedIntervalTableRow > openGridTables( MoBIE moBIE, String tableDataFolder, GridSourceTransformer sourceTransformer, List< String > relativeTablePaths )
-/GridOverlaySourceDisplay.java
-	{
-		// open
-		final List< Map< String, List< String > > > tables = new ArrayList<>();
-		for ( String table : relativeTablePaths )
-		{
-			String tablePath = moBIE.getTablePath( tableDataFolder, table );
-			tablePath = Utils.resolveTablePath( tablePath );
-			Logger.log( "Opening table:\n" + tablePath );
-			tables.add( TableColumns.stringColumnsFromTableFile( tablePath ) );
-		}
-
-		// create primary AnnotatedIntervalTableRow table
-		final Map< String, List< String > > referenceTable = tables.get( 0 );
-		final AnnotatedIntervalCreator annotatedIntervalCreator = new AnnotatedIntervalCreator( referenceTable, sourceTransformer );
-		final List< AnnotatedIntervalTableRow > intervalTableRows = annotatedIntervalCreator.getTableRows();
-
-		final List< Map< String, List< String > > > additionalTables = tables.subList( 1, tables.size() );
-
-		for ( int i = 0; i < additionalTables.size(); i++ )
-		{
-			MoBIE.mergeSourceAnnotationTable( intervalTableRows, additionalTables.get( i ) );
-		}
-
-		return intervalTableRows;
-	}
-
-	private void showGridImage( BdvHandle bdvHandle, String name, List< DefaultAnnotatedIntervalTableRow > tableRows )
-	{
-		final TableRowsIntervalImage< DefaultAnnotatedIntervalTableRow > intervalImage = new TableRowsIntervalImage<>( tableRows, coloringModel, name );
-		SourceAndConverter< IntType > sourceAndConverter = intervalImage.getSourceAndConverter();
-		SourceAndConverterServices.getBdvDisplayService().show( bdvHandle, sourceAndConverter );
-		sourceAndConverters = new ArrayList<>();
-		sourceAndConverters.add( sourceAndConverter );
-	}
-
-	public TableViewer< DefaultAnnotatedIntervalTableRow > getTableViewer()
-	{
-		return tableViewer;
-	}
-
-	public BdvHandle getBdvHandle()
-	{
-		return bdvHandle;
-	}
-
-	@Override
-	public void coloringChanged()
-	{
-		bdvHandle.getViewerPanel().requestRepaint();
-	}
-
-	@Override
-	public void selectionChanged()
-	{
-		bdvHandle.getViewerPanel().requestRepaint();
-	}
-
-	@Override
-	public void focusEvent( DefaultAnnotatedIntervalTableRow selection )
-	{
-		final double[] max = selection.interval.maxAsDoubleArray();
-		final double[] min = selection.interval.minAsDoubleArray();
-		final double[] center = new double[ min.length ];
-		for ( int d = 0; d < 3; d++ )
-		{
-			center[ d ] = ( max[ d ] + min[ d ] ) / 2;
-		}
-
-		ViewerTransformChanger.changeViewerTransform( bdvHandle, new PositionViewerTransform( center, bdvHandle.getViewerPanel().state().getCurrentTimepoint() ) );
-
-	}
-
-	public String getName()
-	{
-		return name;
-	}
-
-	public void close()
-	{
-		for ( SourceAndConverter< ? > sourceAndConverter : sourceAndConverters )
-		{
-			SourceAndConverterServices.getBdvDisplayService().removeFromAllBdvs( sourceAndConverter );
-		}
-
-		tableViewer.close();
-	}
-}
diff --git a/src/main/java/org/embl/mobie/viewer/display/SegmentationSourceDisplay.java b/src/main/java/org/embl/mobie/viewer/display/SegmentationSourceDisplay.java
index 0b9d89699..9e27c7d2d 100644
--- a/src/main/java/org/embl/mobie/viewer/display/SegmentationSourceDisplay.java
+++ b/src/main/java/org/embl/mobie/viewer/display/SegmentationSourceDisplay.java
@@ -26,7 +26,7 @@ public class SegmentationSourceDisplay extends AnnotatedRegionDisplay< TableRowI
 	// Runtime
 	public transient SegmentAdapter< TableRowImageSegment > segmentAdapter;
 	public transient SegmentsVolumeViewer< TableRowImageSegment > segmentsVolumeViewer;
-	public transient SegmentationSliceView< TableRowImageSegment > sliceView;
+	public transient SegmentationSliceView sliceView;
 
 	public List< String > getSources()
 	{
@@ -50,6 +50,7 @@ public boolean showSelectedSegmentsIn3d()
 		return showSelectedSegmentsIn3d;
 	}
 
+	// Needed for Gson
 	public SegmentationSourceDisplay(){}
 
 	public SegmentationSourceDisplay( String name, double opacity, List< String > sources,
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 4c1eba03f..9be1ff407 100644
--- a/src/main/java/org/embl/mobie/viewer/plot/ScatterPlotViewer.java
+++ b/src/main/java/org/embl/mobie/viewer/plot/ScatterPlotViewer.java
@@ -39,8 +39,8 @@
 import de.embl.cba.tables.plot.RealPointARGBTypeBiConsumerSupplier;
 import de.embl.cba.tables.plot.ScatterPlotDialog;
 import de.embl.cba.tables.plot.TableRowKDTreeSupplier;
-import de.embl.cba.tables.select.SelectionListener;
-import de.embl.cba.tables.select.SelectionModel;
+import org.embl.mobie.viewer.select.SelectionListener;
+import org.embl.mobie.viewer.select.SelectionModel;
 import de.embl.cba.tables.tablerow.TableRow;
 import ij.IJ;
 import ij.gui.GenericDialog;
@@ -299,7 +299,7 @@ private synchronized void focusAndSelectClosestPoints( )
 				if ( selectionModel.isSelected( selection ) )
 				{
 					recentFocus = selection;
-					selectionModel.focus( selection );
+					selectionModel.focus( selection, this );
 				}
 			}
 		}
@@ -321,7 +321,7 @@ private synchronized void focusClosestPoint()
 		if ( selection != null )
 		{
 			recentFocus = selection;
-			selectionModel.focus( selection );
+			selectionModel.focus( selection, this );
 		}
 		else
 		{
@@ -414,7 +414,7 @@ public void selectionChanged()
 	}
 
 	@Override
-	public void focusEvent( T selection )
+	public void focusEvent( T selection, Object origin  )
 	{
 		if ( bdvHandle == null ) return;
 
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 55c22d8b2..0dc33bcda 100644
--- a/src/main/java/org/embl/mobie/viewer/projectcreator/DatasetJsonCreator.java
+++ b/src/main/java/org/embl/mobie/viewer/projectcreator/DatasetJsonCreator.java
@@ -193,29 +193,22 @@ private View createImageView( String imageName, String uiSelectionGroup, boolean
         return view;
     }
 
-    private View createSegmentationView( String imageName, String uiSelectionGroup, boolean isExclusive,
-                                         AffineTransform3D sourceTransform ) {
+    private View createSegmentationView( String imageName, String uiSelectionGroup, boolean isExclusive, AffineTransform3D sourceTransform ) {
         ArrayList< SourceDisplay > sourceDisplays = new ArrayList<>();
         ArrayList<String> sources = new ArrayList<>();
         sources.add( imageName );
 
         ArrayList<String> tables = new ArrayList<>();
         tables.add( "default.tsv" );
-        SegmentationSourceDisplay segmentationSourceDisplay = new SegmentationSourceDisplay(
-                imageName, 0.5, sources, ColoringLuts.GLASBEY,
-                null, null, null, false,
-                false, new String[]{ TableColumnNames.ANCHOR_X, TableColumnNames.ANCHOR_Y },
-                tables, null, null );
+        SegmentationSourceDisplay segmentationSourceDisplay = new SegmentationSourceDisplay( imageName, 0.5, sources, ColoringLuts.GLASBEY, null,null, null, false, false, new String[]{ TableColumnNames.ANCHOR_X, TableColumnNames.ANCHOR_Y }, tables, null, null );
         sourceDisplays.add( segmentationSourceDisplay );
 
-        View view;
         if ( sourceTransform.isIdentity() ) {
-            view = new View( uiSelectionGroup, sourceDisplays, null, null, isExclusive );
+            return new View( uiSelectionGroup, sourceDisplays, null, null, isExclusive );
         } else {
             List<SourceTransformer> sourceTransformerList = createSourceTransformerList( sourceTransform, sources );
-            view = new View( uiSelectionGroup, sourceDisplays, sourceTransformerList, null, isExclusive );
+            return new View( uiSelectionGroup, sourceDisplays, sourceTransformerList, null, isExclusive );
         }
-        return view;
     }
 
     public void writeDatasetJson ( String datasetName, Dataset dataset ) {
diff --git a/src/main/java/org/embl/mobie/viewer/segment/BdvSegmentSelector.java b/src/main/java/org/embl/mobie/viewer/segment/SliceViewRegionSelector.java
similarity index 56%
rename from src/main/java/org/embl/mobie/viewer/segment/BdvSegmentSelector.java
rename to src/main/java/org/embl/mobie/viewer/segment/SliceViewRegionSelector.java
index 5cd3c85c0..99106c820 100644
--- a/src/main/java/org/embl/mobie/viewer/segment/BdvSegmentSelector.java
+++ b/src/main/java/org/embl/mobie/viewer/segment/SliceViewRegionSelector.java
@@ -4,13 +4,16 @@
 import bdv.util.BdvHandle;
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
-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 de.embl.cba.tables.tablerow.TableRow;
 import net.imglib2.RandomAccess;
 import net.imglib2.RealPoint;
 import net.imglib2.type.numeric.RealType;
+import org.embl.mobie.viewer.SourceNameEncoder;
+import org.embl.mobie.viewer.annotate.AnnotatedMaskAdapter;
+import org.embl.mobie.viewer.bdv.BdvGlobalMousePositionProvider;
+import org.embl.mobie.viewer.display.AnnotatedSourceDisplay;
+import org.embl.mobie.viewer.display.AnnotatedRegionDisplay;
+import org.embl.mobie.viewer.display.SegmentationSourceDisplay;
 import org.embl.mobie.viewer.source.LabelSource;
 import org.embl.mobie.viewer.transform.MergedGridSource;
 import sc.fiji.bdvpg.sourceandconverter.SourceAndConverterHelper;
@@ -19,34 +22,32 @@
 import java.util.HashSet;
 import java.util.function.Supplier;
 
-public class BdvSegmentSelector implements Runnable
+public class SliceViewRegionSelector implements Runnable
 {
 	private BdvHandle bdvHandle;
 	private boolean is2D;
-	private Supplier< Collection< SegmentationSourceDisplay > > segmentationDisplaySupplier;
+	private Supplier< Collection< AnnotatedRegionDisplay > > annotatedRegionDisplaySupplier;
 
-	// A segmentationDisplaySupplier is used such that the segmentation images can
-	// change during runtime.
-	public BdvSegmentSelector( BdvHandle bdvHandle, boolean is2D, Supplier< Collection< SegmentationSourceDisplay > > segmentationDisplaySupplier )
+	public SliceViewRegionSelector( BdvHandle bdvHandle, boolean is2D, Supplier< Collection< AnnotatedRegionDisplay > > annotatedRegionDisplaySupplier )
 	{
 		this.bdvHandle = bdvHandle;
 		this.is2D = is2D;
-		this.segmentationDisplaySupplier = segmentationDisplaySupplier;
+		this.annotatedRegionDisplaySupplier = annotatedRegionDisplaySupplier;
 	}
 
 	public synchronized void clearSelection()
 	{
-		final Collection< SegmentationSourceDisplay > segmentationDisplays = getCurrent();
+		final Collection< AnnotatedRegionDisplay > regionDisplays = getCurrent();
 
-		for ( SegmentationSourceDisplay segmentationDisplay : segmentationDisplays )
+		for ( AnnotatedRegionDisplay regionDisplay : regionDisplays )
 		{
-			segmentationDisplay.selectionModel.clearSelection();
+			regionDisplay.selectionModel.clearSelection();
 		}
 	}
 
-	private Collection< SegmentationSourceDisplay > getCurrent()
+	private Collection< AnnotatedRegionDisplay > getCurrent()
 	{
-		return segmentationDisplaySupplier.get();
+		return annotatedRegionDisplaySupplier.get();
 	}
 
 	private synchronized void toggleSelectionAtMousePosition()
@@ -55,13 +56,13 @@ private synchronized void toggleSelectionAtMousePosition()
 		final int timePoint = positionProvider.getTimePoint();
 		final RealPoint position = positionProvider.getPositionAsRealPoint();
 
-		final Collection< SegmentationSourceDisplay > segmentationDisplays = segmentationDisplaySupplier.get();
+		final Collection< AnnotatedRegionDisplay > regionDisplays = annotatedRegionDisplaySupplier.get();
 
-		for ( SegmentationSourceDisplay segmentationDisplay : segmentationDisplays )
+		for ( AnnotatedRegionDisplay regionDisplay : regionDisplays )
 		{
-			final Collection< SourceAndConverter< ? > > segmentationSourceAndConverters = segmentationDisplay.sourceNameToSourceAndConverter.values();
+			final Collection< SourceAndConverter< ? > > sourceAndConverters = regionDisplay.sourceNameToSourceAndConverter.values();
 
-			for ( SourceAndConverter< ? > sourceAndConverter : segmentationSourceAndConverters )
+			for ( SourceAndConverter< ? > sourceAndConverter : sourceAndConverters )
 			{
 				if ( ! bdvHandle.getViewerPanel().state().isSourceVisible( sourceAndConverter ) )
 				{
@@ -76,30 +77,52 @@ private synchronized void toggleSelectionAtMousePosition()
 					final String sourceName = getSourceName( source, pixelValue );
 					double labelIndex = getLabelIndex( source, pixelValue );
 
-					if ( labelIndex == 0 ) continue; // image background
+					TableRow tableRow = getTableRow( timePoint, regionDisplay, sourceName, labelIndex );
 
-					final boolean containsSegment = segmentationDisplay.segmentAdapter.containsSegment( labelIndex, timePoint, sourceName );
-
-					if ( ! containsSegment )
+					if ( tableRow != null )
 					{
-						// This happens when there is a segmentation without
-						// a segment table
-						continue;
-					}
+						regionDisplay.selectionModel.toggle( tableRow );
 
-					final TableRowImageSegment segment = segmentationDisplay.segmentAdapter.getSegment( labelIndex, timePoint, sourceName );
-
-					segmentationDisplay.selectionModel.toggle( segment );
-
-					if ( segmentationDisplay.selectionModel.isSelected( segment ) )
-					{
-						segmentationDisplay.selectionModel.focus( segment );
+						if ( regionDisplay.selectionModel.isSelected( tableRow ) )
+						{
+							//regionDisplay.
+							regionDisplay.selectionModel.focus( tableRow, this );
+						}
 					}
 				}
 			}
 		}
 	}
 
+	private TableRow getTableRow( int timePoint, AnnotatedRegionDisplay regionDisplay, String sourceName, double labelIndex )
+	{
+		if ( regionDisplay instanceof SegmentationSourceDisplay )
+		{
+			final boolean containsSegment = (( SegmentationSourceDisplay ) regionDisplay ).segmentAdapter.containsSegment( labelIndex, timePoint, sourceName );
+
+			if ( ! containsSegment )
+			{
+				// This happens when there is a segmentation without
+				// a segment table, or when the background label has been selected
+				return null;
+			}
+			else
+			{
+				return ( ( SegmentationSourceDisplay ) regionDisplay ).segmentAdapter.getSegment( labelIndex, timePoint, sourceName );
+			}
+		}
+		else if ( regionDisplay instanceof AnnotatedSourceDisplay )
+		{
+			final AnnotatedSourceDisplay annotatedSourceDisplay = ( AnnotatedSourceDisplay ) regionDisplay;
+			final AnnotatedMaskAdapter adapter = annotatedSourceDisplay.annotatedMaskAdapter;
+			return adapter.getAnnotatedMask( timePoint, labelIndex );
+		}
+		else
+		{
+			throw new UnsupportedOperationException( "Region display of type " + regionDisplay.getClass().getName() + " is not supported for selection.");
+		}
+	}
+
 	private static double getLabelIndex( Source< ? > source, double pixelValue )
 	{
 		if ( MergedGridSource.instanceOf( source ) )
@@ -118,10 +141,14 @@ private static String getSourceName( Source< ? > source, double labelIndex )
 		{
 			return SourceNameEncoder.getName( Double.valueOf( labelIndex ).longValue() );
 		}
-		final String sourceName = source.getName();
-		return sourceName;
+		else
+		{
+			final String sourceName = source.getName();
+			return sourceName;
+		}
 	}
 
+	@Deprecated
 	private Collection< SourceAndConverter< ? > > getContainedSourceAndConverters( SourceAndConverter< ? > sourceAndConverter )
 	{
 		final Collection< SourceAndConverter< ? > > containedSourceAndConverters = new HashSet<>();
diff --git a/src/main/java/org/embl/mobie/viewer/select/Listeners.java b/src/main/java/org/embl/mobie/viewer/select/Listeners.java
new file mode 100644
index 000000000..87ec37629
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/select/Listeners.java
@@ -0,0 +1,155 @@
+/*-
+ * #%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.select;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.function.Consumer;
+
+/**
+ * A set of listeners of type {@code T}.
+ *
+ * @param <T>
+ *            listener type
+ */
+public interface Listeners< T >
+{
+	/**
+	 * Add a listener to this set.
+	 *
+	 * @param listener
+	 *            the listener to add.
+	 * @return {@code true} if the listener was added. {@code false} if it was
+	 *         already present.
+	 */
+	boolean add( final T listener );
+
+	/**
+	 * Removes a listener from this set.
+	 *
+	 * @param listener
+	 *            the listener to remove.
+	 * @return {@code true} if the listener was successfully removed.
+	 *         {@code false} if the listener was not present.
+	 */
+	boolean remove( final T listener );
+
+	default boolean addAll( final Collection< ? extends T > listeners )
+	{
+		if ( listeners.isEmpty() )
+			return false;
+		return listeners.stream().map( this::add ).reduce( Boolean::logicalOr ).get();
+	}
+
+	default boolean removeAll( final Collection< ? extends T > listeners )
+	{
+		if ( listeners.isEmpty() )
+			return false;
+		return listeners.stream().map( this::remove ).reduce( Boolean::logicalOr ).get();
+	}
+
+	/**
+	 * Implements {@link Listeners} using an {@link ArrayList}.
+	 */
+	class List< T > implements Listeners< T >
+	{
+		private final Consumer< T > onAdd;
+
+		public List( final Consumer< T > onAdd )
+		{
+			this.onAdd = onAdd;
+		}
+
+		public List()
+		{
+			this( o -> {} );
+		}
+
+		public final ArrayList< T > list = new ArrayList<>();
+
+		@Override
+		public boolean add( final T listener )
+		{
+			if ( !list.contains( listener ) )
+			{
+				list.add( listener );
+				onAdd.accept( listener );
+				return true;
+			}
+			return false;
+		}
+
+		@Override
+		public boolean remove( final T listener )
+		{
+			return list.remove( listener );
+		}
+
+		public ArrayList< T > listCopy()
+		{
+			return new ArrayList<>( list );
+		}
+	}
+
+	/**
+	 * Extends {@link List}, making {@code add} and {@code remove}
+	 * methods synchronized.
+	 */
+	class SynchronizedList< T > extends List< T >
+	{
+		public SynchronizedList( final Consumer< T > onAdd )
+		{
+			super( onAdd );
+		}
+
+		public SynchronizedList()
+		{
+			super();
+		}
+
+		@Override
+		public synchronized boolean add( final T listener )
+		{
+			return super.add( listener );
+		}
+
+		@Override
+		public synchronized boolean remove( final T listener )
+		{
+			return super.remove( listener );
+		}
+
+		@Override
+		public synchronized ArrayList< T > listCopy()
+		{
+			return super.listCopy();
+		}
+	}
+}
+
diff --git a/src/main/java/org/embl/mobie/viewer/select/MoBIESelectionModel.java b/src/main/java/org/embl/mobie/viewer/select/MoBIESelectionModel.java
new file mode 100644
index 000000000..c067437e2
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/select/MoBIESelectionModel.java
@@ -0,0 +1,175 @@
+/*-
+ * #%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.select;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+public class MoBIESelectionModel< T > implements SelectionModel< T >
+{
+	private final Listeners.SynchronizedList< SelectionListener > listeners;
+	private final Set< T > selected;
+	private T focusObject;
+
+	public MoBIESelectionModel()
+	{
+		listeners = new Listeners.SynchronizedList<>(  );
+		selected = new HashSet();
+	}
+
+	@Override
+	public synchronized boolean isSelected( T object )
+	{
+		return selected.contains( object );
+	}
+
+	@Override
+	public synchronized void setSelected( T object, boolean select )
+	{
+		setSelected( object, select, true );
+	}
+
+	private synchronized void setSelected( T object, boolean select, boolean notify )
+	{
+		if ( select )
+			add( object, notify );
+		else
+			remove( object, notify );
+	}
+
+	private synchronized void remove( T object, boolean notify )
+	{
+		if ( selected.contains( object ) )
+		{
+			selected.remove( object );
+			if ( notify )
+				notifySelectionListeners();
+			notifySelectionListeners();
+		}
+	}
+
+	private synchronized void add( T object, boolean notify )
+	{
+		if ( ! selected.contains( object ) )
+		{
+			selected.add( object );
+			if ( notify )
+				notifySelectionListeners();
+		}
+	}
+
+	private void notifySelectionListeners()
+	{
+		for ( SelectionListener listener : listeners.list )
+			new Thread( () -> listener.selectionChanged() ).start();
+	}
+
+	@Override
+	public synchronized void toggle( T object )
+	{
+		if ( selected.contains( object ) )
+			remove( object, true );
+		else
+			add( object, true );
+	}
+
+	@Override
+	public synchronized void focus( T object, Object origin )
+	{
+		focusObject = object;
+
+		for ( SelectionListener listener : listeners.list )
+			new Thread( () -> listener.focusEvent( object, origin ) ).start();
+	}
+
+	@Override
+	public boolean isFocused( T object )
+	{
+		if ( focusObject != null && focusObject.equals( object ) )
+			return true;
+		else
+			return false;
+	}
+
+	@Override
+	public synchronized boolean setSelected( Collection< T > objects, boolean select )
+	{
+		for( T object : objects )
+			setSelected( object, select, false );
+
+		notifySelectionListeners();
+
+		return true;
+	}
+
+	@Override
+	public synchronized boolean clearSelection()
+	{
+		if ( selected.size() == 0 )
+			return false;
+		else
+		{
+			selected.clear();
+			notifySelectionListeners();
+			return true;
+		}
+	}
+
+	@Override
+	public synchronized Set< T > getSelected()
+	{
+		return new HashSet< T >( selected );
+	}
+
+	@Override
+	public boolean isEmpty()
+	{
+		return selected.isEmpty();
+	}
+
+	@Override
+	public Listeners< SelectionListener > listeners()
+	{
+		return listeners;
+	}
+
+	@Override
+	public void resumeListeners()
+	{
+
+	}
+
+	@Override
+	public void pauseListeners()
+	{
+
+	}
+
+}
diff --git a/src/main/java/org/embl/mobie/viewer/select/SelectionListener.java b/src/main/java/org/embl/mobie/viewer/select/SelectionListener.java
new file mode 100644
index 000000000..4e0dba5f9
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/select/SelectionListener.java
@@ -0,0 +1,50 @@
+/*-
+ * #%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.select;
+
+import org.embl.mobie.viewer.select.SelectionModel;
+
+/**
+ * Interface for listeners of a {@link SelectionModel}.
+ *
+ */
+public interface SelectionListener< T >
+{
+	/**
+	 * Notifies when the select has changed.
+	 */
+	void selectionChanged();
+
+	/**
+	 * Notifies when a focus event happened.
+	 * Focus events do not necessarily enter the select at all..
+	 */
+	void focusEvent( T selection, Object origin );
+
+}
diff --git a/src/main/java/org/embl/mobie/viewer/select/SelectionModel.java b/src/main/java/org/embl/mobie/viewer/select/SelectionModel.java
new file mode 100644
index 000000000..c1302e99b
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/select/SelectionModel.java
@@ -0,0 +1,127 @@
+/*-
+ * #%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.select;
+
+import org.embl.mobie.viewer.select.SelectionListener;
+
+import java.util.Collection;
+import java.util.Set;
+
+public interface SelectionModel< T >
+{
+	/**
+	 * Get the selected state of a object.
+	 *
+	 * @param object
+	 *            a object.
+	 * @return {@code true} if specified object is selected.
+	 */
+	public boolean isSelected( final T object );
+
+	/**
+	 * Sets the selected state of a object.
+	 *
+	 * @param object
+	 *            a object.
+	 * @param select
+	 *            selected state to set for specified object.
+	 */
+	public void setSelected( final T object, final boolean select );
+	
+	/**
+	 * Toggles the selected state of a object.
+	 *
+	 * @param object
+	 *            a object.
+	 */
+	public void toggle( final T object );
+
+
+	/**
+	 * Focus on an object without changing its selection state.
+	 *
+	 * @param object
+	 *            the object to be focussed.
+	 *  @param origin
+	 *            the origin of the focus, knowing this enables to avoid "self-focussing"
+	 */
+	public void focus( final T object, final Object origin );
+
+
+	/**
+	 * Get the focus state of an object
+	 *
+	 * @param object
+	 *            a object.
+	 */
+	public boolean isFocused( final T object );
+
+	/**
+	 * Sets the selected state of a collection of imagesegment.
+	 *
+	 * @param objects
+	 *            the object collection.
+	 * @param select
+	 *            selected state to set for specified object collection.
+	 * @return {@code true} if the select was changed by this call.
+	 */
+	public boolean setSelected( final Collection< T > objects, final boolean select );
+
+
+
+	/**
+	 * Clears this select.
+	 *
+	 * @return {@code true} if this select was not empty prior to
+	 *         calling this method.
+	 */
+	public boolean clearSelection();
+
+	/**
+	 * Get the selected imagesegment.
+	 **
+	 * @return a <b>new</b> {@link Set} containing all the selected imagesegment.
+	 */
+	public Set< T > getSelected();
+
+	public boolean isEmpty();
+
+	/**
+	 * Get the list of select listeners. Add a {@link SelectionListener} to
+	 * this list, for being notified when the object/edge select changes.
+	 *
+	 * @return the list of listeners
+	 */
+	public Listeners< org.embl.mobie.viewer.select.SelectionListener > listeners();
+
+	public void resumeListeners();
+
+	public void pauseListeners();
+}
+
diff --git a/src/main/java/org/embl/mobie/viewer/serialize/SourceDisplayListAdapter.java b/src/main/java/org/embl/mobie/viewer/serialize/SourceDisplayListAdapter.java
index 1844e0bcb..0339fd9dc 100644
--- a/src/main/java/org/embl/mobie/viewer/serialize/SourceDisplayListAdapter.java
+++ b/src/main/java/org/embl/mobie/viewer/serialize/SourceDisplayListAdapter.java
@@ -2,7 +2,7 @@
 
 import com.google.gson.*;
 import com.google.gson.reflect.TypeToken;
-import org.embl.mobie.viewer.display.AnnotatedIntervalDisplay;
+import org.embl.mobie.viewer.display.AnnotatedSourceDisplay;
 import org.embl.mobie.viewer.display.ImageSourceDisplay;
 import org.embl.mobie.viewer.display.SegmentationSourceDisplay;
 import org.embl.mobie.viewer.display.SourceDisplay;
@@ -21,8 +21,8 @@ public class SourceDisplayListAdapter implements JsonSerializer< List< SourceDis
 		classToName.put(ImageSourceDisplay.class.getName(), "imageDisplay");
 		nameToClass.put("segmentationDisplay", SegmentationSourceDisplay.class);
 		classToName.put(SegmentationSourceDisplay.class.getName(), "segmentationDisplay");
-		nameToClass.put("sourceAnnotationDisplay", AnnotatedIntervalDisplay.class);
-		classToName.put(AnnotatedIntervalDisplay.class.getName(), "sourceAnnotationDisplay");
+		nameToClass.put("sourceAnnotationDisplay", AnnotatedSourceDisplay.class);
+		classToName.put( AnnotatedSourceDisplay.class.getName(), "sourceAnnotationDisplay");
 	}
 
 	@Override
@@ -50,8 +50,8 @@ public JsonElement serialize( List< SourceDisplay > sourceDisplays, Type type, J
 				ja.add( context.serialize( nameToSourceDisplay, new TypeToken< Map< String, ImageSourceDisplay > >() {}.getType() ) );
 			} else if ( sourceDisplay instanceof  SegmentationSourceDisplay ) {
 				ja.add( context.serialize( nameToSourceDisplay , new TypeToken< Map< String, SegmentationSourceDisplay > >() {}.getType() ) );
-			} else if ( sourceDisplay instanceof  AnnotatedIntervalDisplay ) {
-				ja.add( context.serialize( nameToSourceDisplay, new TypeToken< Map< String, AnnotatedIntervalDisplay > >() {}.getType() ) );
+			} else if ( sourceDisplay instanceof AnnotatedSourceDisplay ) {
+				ja.add( context.serialize( nameToSourceDisplay, new TypeToken< Map< String, AnnotatedSourceDisplay > >() {}.getType() ) );
 			} else
 			{
 				throw new UnsupportedOperationException( "Could not serialise SourceDisplay of type: " + sourceDisplay.getClass().toString() );
diff --git a/src/main/java/org/embl/mobie/viewer/source/LabelSource.java b/src/main/java/org/embl/mobie/viewer/source/LabelSource.java
index b6b77f4b2..01e159baa 100644
--- a/src/main/java/org/embl/mobie/viewer/source/LabelSource.java
+++ b/src/main/java/org/embl/mobie/viewer/source/LabelSource.java
@@ -22,7 +22,7 @@
 public class LabelSource<T extends NumericType<T> & RealType<T>> implements Source<T> {
     protected final Source<T> source;
     private boolean showAsBoundaries;
-    private double boundaryWidth;
+    private float boundaryWidth;
     private float background;
     private final RealMaskRealInterval bounds;
     private ArrayList< Integer > boundaryDimensions;
@@ -44,7 +44,7 @@ public LabelSource( final Source<T> source, float background, RealMaskRealInterv
         this.bounds = bounds;
     }
 
-    public void showAsBoundary( boolean showAsBoundaries, double boundaryWidth ) {
+    public void showAsBoundary( boolean showAsBoundaries, float boundaryWidth ) {
         this.showAsBoundaries = showAsBoundaries;
         this.boundaryWidth = boundaryWidth;
         this.boundaryDimensions = boundaryDimensions();
@@ -104,6 +104,9 @@ private ArrayList< Integer > boundaryDimensions()
         final ArrayList< Integer > dimensions = new ArrayList<>();
         if ( bounds != null )
         {
+            // check whether the source is effectively 2D,
+            // i.e. much thinner along one dimension than the
+            // requested boundary width
             for ( int d = 0; d < 3; d++ )
             {
                 final double sourceBound = Math.abs( bounds.realMax( d ) - bounds.realMin( d ) );
@@ -111,12 +114,20 @@ private ArrayList< Integer > boundaryDimensions()
                     dimensions.add( d );
             }
         }
+        else if ( source.getSource( 0,0 ).dimension( 2 ) == 1 )
+        {
+            // 2D source
+            dimensions.add( 0 );
+            dimensions.add( 1 );
+        }
         else
         {
+            // 3D source
             dimensions.add( 0 );
             dimensions.add( 1 );
             dimensions.add( 2 );
         }
+
         return dimensions;
     }
 
@@ -239,4 +250,14 @@ public int getNumMipmapLevels() {
     public Source<T> getWrappedSource() {
         return source;
     }
+
+    public boolean isShowAsBoundaries()
+    {
+        return showAsBoundaries;
+    }
+
+    public float getBoundaryWidth()
+    {
+        return boundaryWidth;
+    }
 }
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 67f7f7b57..758190308 100644
--- a/src/main/java/org/embl/mobie/viewer/table/TableViewer.java
+++ b/src/main/java/org/embl/mobie/viewer/table/TableViewer.java
@@ -38,8 +38,8 @@
 import de.embl.cba.tables.color.*;
 import de.embl.cba.tables.plot.ScatterPlotDialog;
 
-import de.embl.cba.tables.select.SelectionListener;
-import de.embl.cba.tables.select.SelectionModel;
+import org.embl.mobie.viewer.select.SelectionListener;
+import org.embl.mobie.viewer.select.SelectionModel;
 import de.embl.cba.tables.tablerow.JTableFromTableRowsModelCreator;
 import de.embl.cba.tables.tablerow.TableRow;
 import de.embl.cba.tables.tablerow.TableRowImageSegment;
@@ -422,7 +422,8 @@ public void addAdditionalTable(String tablePath) {
 		additionalTables.add(tableName);
 	}
 
-	private Map< String, List< String > > openTable( String tablePath ) {
+	private Map< String, List< String > > openTable( String tablePath )
+	{
 		String resolvedPath = MoBIEHelper.resolveTablePath( tablePath );
 		Logger.info( "Opening table:\n" + resolvedPath );
 		return TableColumns.stringColumnsFromTableFile( resolvedPath );
@@ -447,7 +448,7 @@ private void loadColumnsFromProject()
 				for ( String tableDir: sourceNameToTableDir.values() )
 				{
 					final Map< String, List< String > > table = openTable( FileAndUrlUtils.combinePath( tableDir, tableName ) );
-					MoBIE.mergeAnnotatedIntervalTable( (List< AnnotatedMaskTableRow >) tableRows, table );
+					MoBIE.mergeAnnotatedMaskTable( (List< AnnotatedMaskTableRow >) tableRows, table );
 				}
 			}
 			addAdditionalTable( tableName );
@@ -468,9 +469,8 @@ private void loadColumnsFromFileSystem()
 				}
 				else
 				{
-
 					Map< String, List< String > > table = openTable( path );
-					MoBIE.mergeAnnotatedIntervalTable( ( List< AnnotatedMaskTableRow > ) tableRows, table );
+					MoBIE.mergeAnnotatedMaskTable( ( List< AnnotatedMaskTableRow > ) tableRows, table );
 				}
 				enableRowSorting( true );
 			}).start();
@@ -891,13 +891,13 @@ public void mouseClicked( MouseEvent e )
 
 				if ( tableRowSelectionMode.equals( TableRowSelectionMode.FocusOnly ) )
 				{
-					selectionModel.focus( object );
+					selectionModel.focus( object, this );
 				}
 				else if ( tableRowSelectionMode.equals( TableRowSelectionMode.ToggleSelectionAndFocusIfSelected ) )
 				{
 					selectionModel.toggle( object );
 					if ( selectionModel.isSelected( object ) )
-						selectionModel.focus( object );
+						selectionModel.focus( object, this );
 				}
 			})
 		);
@@ -1017,7 +1017,7 @@ public synchronized void selectionChanged()
 	}
 
 	@Override
-	public synchronized void focusEvent( T selection )
+	public synchronized void focusEvent( T selection, Object origin )
 	{
 		SwingUtilities.invokeLater( () -> moveToSelectedTableRow( selection ) );
 	}
diff --git a/src/main/java/org/embl/mobie/viewer/transform/CropSourceTransformer.java b/src/main/java/org/embl/mobie/viewer/transform/CropSourceTransformer.java
index 8d2b6dfc6..292ecf90e 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/CropSourceTransformer.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/CropSourceTransformer.java
@@ -40,7 +40,7 @@ public void transform( Map< String, SourceAndConverter< ? > > sourceNameToSource
 
 				if ( centerAtOrigin )
 				{
-					croppedSourceAndConverter = TransformHelper.centerAtOrigin( croppedSourceAndConverter );
+					croppedSourceAndConverter = TransformHelpers.centerAtOrigin( croppedSourceAndConverter );
 				}
 
 				// store result
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 f3bb94f1f..2a70ea82f 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/MergedGridSource.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/MergedGridSource.java
@@ -1,14 +1,13 @@
 package org.embl.mobie.viewer.transform;
 
 import bdv.tools.transformation.TransformedSource;
+import bdv.util.Affine3DHelpers;
 import bdv.util.DefaultInterpolators;
 import bdv.viewer.Interpolation;
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
 import mpicbg.spim.data.sequence.VoxelDimensions;
 import net.imglib2.Cursor;
-import net.imglib2.FinalInterval;
-import net.imglib2.FinalRealInterval;
 import net.imglib2.RandomAccess;
 import net.imglib2.RandomAccessibleInterval;
 import net.imglib2.RealRandomAccessible;
@@ -18,10 +17,11 @@
 import net.imglib2.cache.img.ReadOnlyCachedCellImgOptions;
 import net.imglib2.cache.img.SingleCellArrayImg;
 import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.roi.RealMaskRealInterval;
+import net.imglib2.roi.geom.GeomMasks;
 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.MoBIEHelper;
 import org.embl.mobie.viewer.SourceNameEncoder;
@@ -35,7 +35,7 @@
 import java.util.NoSuchElementException;
 import java.util.Set;
 
-public class MergedGridSource< T extends NativeType< T > & NumericType< T > > implements Source< T >
+public class MergedGridSource< T extends NativeType< T > & NumericType< T > > implements Source< T >, RealMaskSource
 {
 	private final T type;
 	private final Source< T > referenceSource;
@@ -51,6 +51,7 @@ public class MergedGridSource< T extends NativeType< T > & NumericType< T > > im
 	private int[][] cellDimensions;
 	private double[] cellRealDimensions;
 	private Set< SourceAndConverter > containedSourceAndConverters;
+	private RealMaskRealInterval mask;
 
 	public MergedGridSource( List< Source< T > > gridSources, List< int[] > positions, String mergedGridSourceName, double relativeCellMargin, boolean encodeSource )
 	{
@@ -61,9 +62,9 @@ public MergedGridSource( List< Source< T > > gridSources, List< int[] > position
 		this.interpolators = new DefaultInterpolators<>();
 		this.referenceSource = gridSources.get( 0 );
 		this.mergedGridSourceName = mergedGridSourceName;
-		this.type = Util.getTypeFromInterval( referenceSource.getSource( 0, 0 ) );
+		this.type = referenceSource.getType();
 
-		mergedRandomAccessibleIntervals = createMergedRandomAccessibleIntervals();
+		mergedRandomAccessibleIntervals = createMergedRAIs();
 	}
 
 	public static boolean instanceOf( SourceAndConverter< ? > sourceAndConverter )
@@ -74,11 +75,19 @@ public static boolean instanceOf( SourceAndConverter< ? > sourceAndConverter )
 
 	public static boolean instanceOf( Source< ? > source )
 	{
-		if ( source instanceof LabelSource )  source = ( ( LabelSource ) source ).getWrappedSource();
+		if ( source instanceof LabelSource )
+			source = ( ( LabelSource ) source ).getWrappedSource();
 
-		final Source wrappedSource = ( ( TransformedSource ) source ).getWrappedSource();
+		if ( source instanceof MergedGridSource )
+			return true;
 
-		return wrappedSource instanceof MergedGridSource || source instanceof MergedGridSource;
+		if ( source instanceof TransformedSource )
+			source = ( ( TransformedSource ) source ).getWrappedSource();
+
+		if ( source instanceof MergedGridSource )
+			return true;
+
+		return false;
 	}
 
 	public List< Source< T > > getGridSources()
@@ -86,15 +95,18 @@ public List< Source< T > > getGridSources()
 		return gridSources;
 	}
 
-	private List< RandomAccessibleInterval< T > > createMergedRandomAccessibleIntervals()
+	private List< RandomAccessibleInterval< T > > createMergedRAIs()
 	{
-		final List< RandomAccessibleInterval< T >> mergedRandomAccessibleIntervals = new ArrayList<>();
 		final int numMipmapLevels = referenceSource.getNumMipmapLevels();
-		cellDimensions = computeCellDimensions( numMipmapLevels );
-		final AffineTransform3D affineTransform3D = new AffineTransform3D();
-		final FinalInterval interval = createInterval();
-		final FinalRealInterval bounds = affineTransform3D.estimateBounds( interval );
-		cellRealDimensions = bounds.maxAsDoubleArray();
+		setCellDimensions( numMipmapLevels );
+		setCellRealDimensions( cellDimensions[ 0 ] );
+		setMask( positions, cellDimensions[ 0 ] );
+		return createMergedRAIs( numMipmapLevels );
+	}
+
+	private List< RandomAccessibleInterval< T > > createMergedRAIs( int numMipmapLevels )
+	{
+		final List< RandomAccessibleInterval< T >> mergedRandomAccessibleIntervals = new ArrayList<>();
 
 		for ( int level = 0; level < numMipmapLevels; level++ )
 		{
@@ -123,14 +135,20 @@ private List< RandomAccessibleInterval< T > > createMergedRandomAccessibleInterv
 		return mergedRandomAccessibleIntervals;
 	}
 
-	private FinalInterval createInterval()
+	private void setCellRealDimensions( int[] cellDimension )
+	{
+		final AffineTransform3D referenceTransform = new AffineTransform3D();
+		referenceSource.getSourceTransform( 0, 0, referenceTransform );
+		cellRealDimensions = new double[ 3 ];
+		for ( int d = 0; d < 2; d++ )
+		{
+			cellRealDimensions[ d ] = cellDimension[ d ] * Affine3DHelpers.extractScale( referenceTransform, d );
+		}
+	}
+
+	public RealMaskRealInterval getRealMask()
 	{
-		final long[] max = MoBIEHelper.asLongs( cellDimensions[ 0 ] );
-		for ( int d = 0; d < max.length; d++ )
-			max[ d ] -= 1;
-		final long[] min = new long[ 3 ];
-		final FinalInterval interval = new FinalInterval( min, max );
-		return interval;
+		return mask;
 	}
 
 	public double[] getCellRealDimensions()
@@ -138,7 +156,7 @@ public double[] getCellRealDimensions()
 		return cellRealDimensions;
 	}
 
-	private int[][] computeCellDimensions( int numMipmapLevels )
+	private void setCellDimensions( int numMipmapLevels )
 	{
 		final int numDimensions = referenceSource.getVoxelDimensions().numDimensions();
 
@@ -158,19 +176,19 @@ private int[][] computeCellDimensions( int numMipmapLevels )
 						);
 		}
 
-		double[][] downsamplingFactors = new double[ numMipmapLevels ][ numDimensions ];
+		double[][] downSamplingFactors = new double[ numMipmapLevels ][ numDimensions ];
 		for ( int level = 1; level < numMipmapLevels; level++ )
 			for ( int d = 0; d < numDimensions; d++ )
-				downsamplingFactors[ level ][ d ] = voxelSizes[ level ][ d ] / voxelSizes[ level - 1 ][ d ];
+				downSamplingFactors[ level ][ d ] = voxelSizes[ level ][ d ] / voxelSizes[ level - 1 ][ d ];
 
 		final double[] downsamplingFactorProducts = new double[ numDimensions ];
 		Arrays.fill( downsamplingFactorProducts, 1.0D );
 
 		for ( int level = 1; level < numMipmapLevels; level++ )
 			for ( int d = 0; d < numDimensions; d++ )
-				downsamplingFactorProducts[ d ] *= downsamplingFactors[ level ][ d ];
+				downsamplingFactorProducts[ d ] *= downSamplingFactors[ level ][ d ];
 
-		int[][] cellDimensions = new int[ numMipmapLevels ][ numDimensions ];
+		cellDimensions = new int[ numMipmapLevels ][ numDimensions ];
 
 		// Adapt the cell dimensions such that they are divisible
 		// by all relative changes of the resolutions between the different levels.
@@ -189,10 +207,8 @@ private int[][] computeCellDimensions( int numMipmapLevels )
 		for ( int level = 1; level < numMipmapLevels; level++ )
 			for ( int d = 0; d < numDimensions; d++ )
 			{
-				cellDimensions[ level ][ d ] = (int) ( cellDimensions[ level - 1 ][ d ] / downsamplingFactors[ level ][ d ] );
+				cellDimensions[ level ][ d ] = (int) ( cellDimensions[ level - 1 ][ d ] / downSamplingFactors[ level ][ d ] );
 			}
-
-		return cellDimensions;
 	}
 
 	private Map< String, Source< T > > createCellKeyToSource( int[] cellDimensions )
@@ -246,6 +262,34 @@ private static long[] getDimensions( List< int[] > positions, int[] cellDimensio
 		return dimensions;
 	}
 
+	private void setMask( List< int[] > positions, int[] cellDimensions )
+	{
+		final long[] minPos = new long[ 3 ];
+		final long[] maxPos = new long[ 3 ];
+		for ( int d = 0; d < 2; d++ )
+		{
+			final int finalD = d;
+			minPos[ d ] = positions.stream().mapToInt( pos -> pos[ finalD ] ).min().orElseThrow( NoSuchElementException::new );
+			maxPos[ d ] = positions.stream().mapToInt( pos -> pos[ finalD ] ).max().orElseThrow( NoSuchElementException::new );
+		}
+
+		final double[] min = new double[ 3 ];
+		final double[] max = new double[ 3 ];
+
+		final AffineTransform3D referenceTransform = new AffineTransform3D();
+		referenceSource.getSourceTransform( 0, 0, referenceTransform );
+
+		for ( int d = 0; d < 2; d++ )
+		{
+			final double scale = Affine3DHelpers.extractScale( referenceTransform, d );
+			min[ d ] = minPos[ d ] * cellDimensions[ d ] * scale;
+			max[ d ] = ( maxPos[ d ] + 1 ) * cellDimensions[ d ] * scale;
+		}
+
+		mask = GeomMasks.closedBox( min, max );
+	}
+
+
 	private static String getCellKey( long[] cellMins )
 	{
 		String key = "_";
@@ -340,6 +384,7 @@ public void setContainedSourceAndConverters( Set< SourceAndConverter > contained
 		this.containedSourceAndConverters = containedSourceAndConverters;
 	}
 
+	@Deprecated
 	public Set< SourceAndConverter > getContainedSourceAndConverters()
 	{
 		return containedSourceAndConverters;
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 a2b41c012..3e7393f85 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/MergedGridSourceTransformer.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/MergedGridSourceTransformer.java
@@ -5,10 +5,11 @@
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
 import de.embl.cba.tables.Logger;
-import org.embl.mobie.viewer.MoBIEHelper;
-import net.imglib2.FinalRealInterval;
+import ij.IJ;
+import net.imglib2.RealInterval;
 import net.imglib2.converter.Converter;
 import net.imglib2.type.numeric.ARGBType;
+import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.ThreadUtils;
 
 import java.util.ArrayList;
@@ -25,7 +26,7 @@ public class MergedGridSourceTransformer extends AbstractSourceTransformer
 	protected List< String > sources;
 	protected String mergedGridSourceName;
 	protected List< int[] > positions;
-	protected boolean centerAtOrigin = false;
+	protected boolean centerAtOrigin = false; // TODO: should actually be true, but: https://github.com/mobie/mobie-viewer-fiji/issues/685#issuecomment-1108179599
 	protected boolean encodeSource = false;
 
 	// Runtime
@@ -36,7 +37,14 @@ public class MergedGridSourceTransformer extends AbstractSourceTransformer
 	@Override
 	public void transform( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter )
 	{
-		Logger.info("Merging " + sources.size() + " sources into " + mergedGridSourceName );
+		if ( centerAtOrigin == true )
+		{
+			// https://github.com/mobie/mobie-viewer-fiji/issues/685
+			IJ.log( "[WARNING]: centerAtOrigin = true, is currently not properly working for the merged grid; will thus try with centerAtOrigin = false. See here: https://github.com/mobie/mobie-viewer-fiji/issues/685" );
+			centerAtOrigin = false;
+		}
+
+		final long startTime = System.currentTimeMillis();
 
 		final List< SourceAndConverter< ? > > gridSources = getGridSources( sourceNameToSourceAndConverter );
 
@@ -53,6 +61,10 @@ public void transform( Map< String, SourceAndConverter< ? > > sourceNameToSource
 		transformedSourceAndConverters = ConcurrentHashMap.newKeySet();
 		transformContainedSources( sourceNameToSourceAndConverter, gridSources );
 		mergedGridSource.setContainedSourceAndConverters( transformedSourceAndConverters );
+
+		final long duration = System.currentTimeMillis() - startTime;
+		if ( duration > MoBIE.minLogTimeMillis )
+			IJ.log("Merged " + sources.size() + " sources into " + mergedGridSourceName + " in " + duration + "ms (centerAtOrigin="+centerAtOrigin+").");
 	}
 
 	private void transformContainedSources( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, List< SourceAndConverter< ? > > gridSources )
@@ -82,7 +94,7 @@ private void transformContainedSources( Map< String, SourceAndConverter< ? > > s
 
 	private double[] computeTranslationOffset( List< SourceAndConverter< ? > > gridSources, double[] gridCellRealDimensions )
 	{
-		final FinalRealInterval dataRealBounds = MoBIEHelper.estimateBounds( gridSources.get( 0 ).getSpimSource() );
+		final RealInterval dataRealBounds = TransformHelpers.estimateBounds( gridSources.get( 0 ).getSpimSource(), 0 );
 
 		final double[] dataRealDimensions = new double[ 3 ];
 		for ( int d = 0; d < 3; d++ )
diff --git a/src/main/java/org/embl/mobie/viewer/transform/MoBIEViewerTransformAdjuster.java b/src/main/java/org/embl/mobie/viewer/transform/MoBIEViewerTransformAdjuster.java
new file mode 100644
index 000000000..04c11bd43
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/transform/MoBIEViewerTransformAdjuster.java
@@ -0,0 +1,90 @@
+package org.embl.mobie.viewer.transform;
+
+import bdv.util.Affine3DHelpers;
+import bdv.util.BdvHandle;
+import bdv.viewer.Source;
+import bdv.viewer.SynchronizedViewerState;
+import net.imglib2.Interval;
+import net.imglib2.RealInterval;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.util.LinAlgHelpers;
+
+import java.util.List;
+
+public class MoBIEViewerTransformAdjuster {
+	private final BdvHandle bdvHandle;
+	private final List< Source< ? > > sources;
+
+	public MoBIEViewerTransformAdjuster( BdvHandle bdvHandle, List< Source< ? > > sources) {
+		this.bdvHandle = bdvHandle;
+		this.sources = sources;
+	}
+
+	public AffineTransform3D getSingleSourceTransform()
+	{
+		SynchronizedViewerState state = bdvHandle.getViewerPanel().state();
+		int viewerWidth = bdvHandle.getBdvHandle().getViewerPanel().getWidth();
+		int viewerHeight = bdvHandle.getBdvHandle().getViewerPanel().getHeight();
+		double cX = (double)viewerWidth / 2.0D;
+		double cY = (double)viewerHeight / 2.0D;
+		int timepoint = state.getCurrentTimepoint();
+		Source< ? > source = sources.get( 0 );
+		if (! source.isPresent(timepoint)) {
+			return new AffineTransform3D();
+		} else {
+			AffineTransform3D sourceTransform = new AffineTransform3D();
+			source.getSourceTransform(timepoint, 0, sourceTransform);
+			Interval sourceInterval = source.getSource(timepoint, 0);
+			double sX0 = (double)sourceInterval.min(0);
+			double sX1 = (double)sourceInterval.max(0);
+			double sY0 = (double)sourceInterval.min(1);
+			double sY1 = (double)sourceInterval.max(1);
+			double sZ0 = (double)sourceInterval.min(2);
+			double sZ1 = (double)sourceInterval.max(2);
+			double sX = (sX0 + sX1) / 2.0D;
+			double sY = (sY0 + sY1) / 2.0D;
+			double sZ = (double)Math.round((sZ0 + sZ1) / 2.0D);
+			double[][] m = new double[3][4];
+			double[] qSource = new double[4];
+			double[] qViewer = new double[4];
+
+			Affine3DHelpers.extractApproximateRotationAffine(sourceTransform, qSource, 2);
+			LinAlgHelpers.quaternionInvert(qSource, qViewer);
+			LinAlgHelpers.quaternionToR(qViewer, m);
+
+			double[] centerSource = new double[]{sX, sY, sZ};
+			double[] centerGlobal = new double[3];
+			double[] translation = new double[3];
+			sourceTransform.apply(centerSource, centerGlobal);
+			double[] pSource = new double[]{sX1 + 0.5D, sY1 + 0.5D, sZ};
+			double[] pGlobal = new double[3];
+			double[] pScreen = new double[3];
+			sourceTransform.apply(pSource, pGlobal);
+
+
+			LinAlgHelpers.quaternionApply(qViewer, centerGlobal, translation);
+			LinAlgHelpers.scale(translation, -1.0D, translation);
+			LinAlgHelpers.setCol(3, translation, m);
+			AffineTransform3D viewerTransform = new AffineTransform3D();
+			viewerTransform.set(m);
+			viewerTransform.apply(pGlobal, pScreen);
+
+			double scaleX = cX / pScreen[0];
+			double scaleY = cY / pScreen[1];
+			double scale = Math.min(scaleX, scaleY);
+			viewerTransform.scale(scale);
+			viewerTransform.set(viewerTransform.get(0, 3) + cX - 0.5D, 0, 3);
+			viewerTransform.set(viewerTransform.get(1, 3) + cY - 0.5D, 1, 3);
+			return viewerTransform;
+		}
+	}
+
+	public AffineTransform3D getMultiSourceTransform() {
+		SynchronizedViewerState state = bdvHandle.getViewerPanel().state();
+		final RealInterval bounds = TransformHelpers.estimateBounds( sources, state.getCurrentTimepoint() );
+		final AffineTransform3D transform = TransformHelpers.getIntervalViewerTransform( bdvHandle, bounds );
+		return transform;
+	}
+
+}
+
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 a5fa13552..4b7b9dd54 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/MoBIEViewerTransformChanger.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/MoBIEViewerTransformChanger.java
@@ -44,7 +44,7 @@ else if ( viewerTransform instanceof AffineViewerTransform )
 		}
 		else if ( viewerTransform instanceof NormalizedAffineViewerTransform )
 		{
-			final AffineTransform3D transform = TransformHelper.createUnnormalizedViewerTransform( MoBIEHelper.asAffineTransform3D( viewerTransform.getParameters() ), bdv.getBdvHandle().getViewerPanel() );
+			final AffineTransform3D transform = TransformHelpers.createUnnormalizedViewerTransform( MoBIEHelper.asAffineTransform3D( viewerTransform.getParameters() ), bdv.getBdvHandle().getViewerPanel() );
 			changeViewerTransform( bdv, transform, animationDurationMillis );
 			adaptTimepoint( bdv, viewerTransform );
 		}
diff --git a/src/main/java/org/embl/mobie/viewer/transform/RealMaskSource.java b/src/main/java/org/embl/mobie/viewer/transform/RealMaskSource.java
new file mode 100644
index 000000000..cf7725ba2
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/transform/RealMaskSource.java
@@ -0,0 +1,8 @@
+package org.embl.mobie.viewer.transform;
+
+import net.imglib2.roi.RealMaskRealInterval;
+
+public interface RealMaskSource
+{
+	RealMaskRealInterval getRealMask();
+}
diff --git a/src/main/java/org/embl/mobie/viewer/transform/TransformHelper.java b/src/main/java/org/embl/mobie/viewer/transform/TransformHelpers.java
similarity index 61%
rename from src/main/java/org/embl/mobie/viewer/transform/TransformHelper.java
rename to src/main/java/org/embl/mobie/viewer/transform/TransformHelpers.java
index 32d1590eb..de368a375 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/TransformHelper.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/TransformHelpers.java
@@ -1,11 +1,12 @@
 package org.embl.mobie.viewer.transform;
 
+import bdv.tools.transformation.TransformedSource;
+import bdv.util.BdvHandle;
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
 import bdv.viewer.ViewerPanel;
 import net.imglib2.realtransform.Scale3D;
-import net.imglib2.roi.RealMask;
-import org.embl.mobie.viewer.MoBIEHelper;
+import net.imglib2.roi.RealMaskRealInterval;
 import org.embl.mobie.viewer.playground.BdvPlaygroundUtils;
 import org.embl.mobie.viewer.playground.SourceAffineTransformer;
 import net.imglib2.FinalRealInterval;
@@ -18,15 +19,15 @@
 import java.util.Map;
 import java.util.stream.Collectors;
 
-public class TransformHelper
+public class TransformHelpers
 {
-	public static RealInterval unionRealInterval( List< ? extends Source< ? > > sources )
+	public static RealInterval estimateBounds( List< ? extends Source< ? > > sources, int t )
 	{
 		RealInterval union = null;
 
 		for ( Source< ? > source : sources )
 		{
-			final FinalRealInterval bounds = MoBIEHelper.estimateBounds( source );
+			final RealInterval bounds = estimateBounds( source, t );
 
 			if ( union == null )
 				union = bounds;
@@ -52,7 +53,7 @@ public static RealInterval unionRealInterval( List< ? extends Source< ? > > sour
 
 	public static double[] getCenter( SourceAndConverter< ? > sourceAndConverter )
 	{
-		final FinalRealInterval bounds = MoBIEHelper.estimateBounds( sourceAndConverter.getSpimSource() );
+		final RealInterval bounds = estimateBounds( sourceAndConverter.getSpimSource(), 0 );
 		final double[] center = bounds.minAsDoubleArray();
 		final double[] max = bounds.maxAsDoubleArray();
 		for ( int d = 0; d < max.length; d++ )
@@ -75,9 +76,9 @@ public static AffineTransform3D createTranslationTransform3D( double translation
 		return translationTransform;
 	}
 
-	public static double[] computeSourceUnionRealDimensions( List< SourceAndConverter< ? > > sources, double relativeMargin )
+	public static double[] computeSourceUnionRealDimensions( List< SourceAndConverter< ? > > sources, double relativeMargin, int t )
 	{
-		RealInterval bounds = unionRealInterval( sources.stream().map( sac -> sac.getSpimSource() ).collect( Collectors.toList() ));
+		RealInterval bounds = estimateBounds( sources.stream().map( sac -> sac.getSpimSource() ).collect( Collectors.toList() ), t );
 		final double[] realDimensions = new double[ 2 ];
 		for ( int d = 0; d < 2; d++ )
 			realDimensions[ d ] = ( 1.0 + 2.0 * relativeMargin ) * ( bounds.realMax( d ) - bounds.realMin( d ) );
@@ -90,7 +91,7 @@ public static double[] getMaximalSourceUnionRealDimensions( Map< String, SourceA
 		for ( List< String > sourceNames : sourceNamesList )
 		{
 			final List< SourceAndConverter< ? > > sourceAndConverters = sourceNames.stream().map( name -> sourceNameToSourceAndConverter.get( name ) ).collect( Collectors.toList() );
-			final double[] realDimensions = computeSourceUnionRealDimensions( sourceAndConverters, TransformedGridSourceTransformer.RELATIVE_CELL_MARGIN );
+			final double[] realDimensions = computeSourceUnionRealDimensions( sourceAndConverters, TransformedGridSourceTransformer.RELATIVE_CELL_MARGIN, 0 );
 			for ( int d = 0; d < 2; d++ )
 				maximalDimensions[ d ] = realDimensions[ d ] > maximalDimensions[ d ] ? realDimensions[ d ] : maximalDimensions[ d ];
 		}
@@ -136,4 +137,75 @@ public static AffineTransform3D createUnnormalizedViewerTransform( AffineTransfo
 
 		return transform;
 	}
+
+	public static AffineTransform3D getIntervalViewerTransform( BdvHandle bdv, RealInterval interval  )
+	{
+		final AffineTransform3D affineTransform3D = new AffineTransform3D();
+
+		double[] centerPosition = new double[ 3 ];
+		for( int d = 0; d < 3; ++d )
+		{
+			final double center = ( interval.realMin( d ) + interval.realMax( d ) ) / 2.0;
+			centerPosition[ d ] = - center;
+		}
+		affineTransform3D.translate( centerPosition );
+
+		int[] bdvWindowDimensions = getWindowDimensions( bdv );
+
+		double scale = Double.MAX_VALUE;
+		for ( int d = 0; d < 2; d++ )
+		{
+			final double size = interval.realMax( d ) - interval.realMin( d );
+			scale = Math.min( scale, 1.0 * bdvWindowDimensions[ d ] / size );
+		}
+		affineTransform3D.scale( scale );
+
+		double[] shiftToBdvWindowCenter = new double[ 3 ];
+		for( int d = 0; d < 2; ++d )
+		{
+			shiftToBdvWindowCenter[ d ] += bdvWindowDimensions[ d ] / 2.0;
+		}
+		affineTransform3D.translate( shiftToBdvWindowCenter );
+
+		return affineTransform3D;
+	}
+
+	private static int[] getWindowDimensions( BdvHandle bdv )
+	{
+		int[] bdvWindowDimensions = new int[ 2 ];
+		bdvWindowDimensions[ 0 ] = bdv.getBdvHandle().getViewerPanel().getWidth();
+		bdvWindowDimensions[ 1 ] = bdv.getBdvHandle().getViewerPanel().getHeight();
+		return bdvWindowDimensions;
+	}
+
+	public static RealInterval estimateBounds( Source< ? > source, int t )
+	{
+		if ( source instanceof RealMaskSource )
+		{
+			final RealMaskRealInterval realMask = ( ( RealMaskSource ) source ).getRealMask();
+			return realMask;
+		}
+
+		if ( source instanceof TransformedSource )
+		{
+			final Source< ? > wrappedSource = ( ( TransformedSource< ? > ) source ).getWrappedSource();
+			if ( wrappedSource instanceof RealMaskSource )
+			{
+				final RealMaskRealInterval realMask = ( ( RealMaskSource ) wrappedSource ).getRealMask();
+				if ( realMask != null )
+				{
+					final AffineTransform3D fixedTransform = new AffineTransform3D();
+					( ( TransformedSource< ? > ) source ).getFixedTransform( fixedTransform );
+					final FinalRealInterval realInterval = fixedTransform.estimateBounds( realMask );
+					return realInterval;
+				}
+			}
+		}
+
+		// else:
+		final AffineTransform3D affineTransform3D = new AffineTransform3D();
+		source.getSourceTransform( t, 0, affineTransform3D );
+		final FinalRealInterval bounds = affineTransform3D.estimateBounds( source.getSource( t, 0 ) );
+		return bounds;
+	}
 }
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 1eaf7bb38..adaa5a5fd 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/TransformedGridSourceTransformer.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/TransformedGridSourceTransformer.java
@@ -2,6 +2,7 @@
 
 import bdv.viewer.SourceAndConverter;
 import de.embl.cba.tables.Logger;
+import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.ThreadUtils;
 import org.embl.mobie.viewer.playground.SourceAffineTransformer;
 import net.imglib2.realtransform.AffineTransform3D;
@@ -9,8 +10,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 
 public class TransformedGridSourceTransformer extends AbstractSourceTransformer
@@ -27,13 +26,17 @@ public class TransformedGridSourceTransformer extends AbstractSourceTransformer
 	@Override
 	public void transform( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter )
 	{
-		Logger.info("Transforming " + sources.size() + " sources into a grid...");
+		final long startTime = System.currentTimeMillis();
 		if ( positions == null )
 			autoSetPositions();
 
-		final double[] cellRealDimensions = TransformHelper.getMaximalSourceUnionRealDimensions( sourceNameToSourceAndConverter, sources );
+		// TODO: https://github.com/mobie/mobie-viewer-fiji/issues/674
+		final double[] cellRealDimensions = TransformHelpers.getMaximalSourceUnionRealDimensions( sourceNameToSourceAndConverter, sources );
 
 		transform( sourceNameToSourceAndConverter, cellRealDimensions );
+		final long duration = System.currentTimeMillis() - startTime;
+		if ( duration > MoBIE.minLogTimeMillis )
+			Logger.info("Transformed " + sources.size() + " group(s) with "+ sources.get( 0 ).size() +" source(s) each into a grid in " + duration + "ms (centerAtOrigin="+centerAtOrigin+").");
 	}
 
 	@Override
@@ -72,7 +75,7 @@ public static void translate( Map< String, SourceAndConverter< ? > > sourceNameT
 			if ( sourceAndConverter == null )
 			  continue;
 
-			AffineTransform3D translationTransform = TransformHelper.createTranslationTransform3D( translationX, translationY, sourceAndConverter, centerAtOrigin );
+			AffineTransform3D translationTransform = TransformHelpers.createTranslationTransform3D( translationX, translationY, sourceAndConverter, centerAtOrigin );
 
 			final SourceAffineTransformer transformer = createSourceAffineTransformer( sourceName, sourceNames, sourceNamesAfterTransform, translationTransform );
 
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 0afbf96d1..ea10d4cdc 100644
--- a/src/main/java/org/embl/mobie/viewer/ui/UserInterface.java
+++ b/src/main/java/org/embl/mobie/viewer/ui/UserInterface.java
@@ -1,7 +1,7 @@
 package org.embl.mobie.viewer.ui;
 
 import org.embl.mobie.viewer.MoBIE;
-import org.embl.mobie.viewer.display.AnnotatedIntervalDisplay;
+import org.embl.mobie.viewer.display.AnnotatedSourceDisplay;
 import org.embl.mobie.viewer.view.View;
 import org.embl.mobie.viewer.display.ImageSourceDisplay;
 import org.embl.mobie.viewer.display.SegmentationSourceDisplay;
@@ -108,9 +108,9 @@ else if ( sourceDisplay instanceof SegmentationSourceDisplay)
 		{
 			return userInterfaceHelpers.createSegmentationDisplaySettingsPanel( ( SegmentationSourceDisplay ) sourceDisplay );
 		}
-		else if ( sourceDisplay instanceof AnnotatedIntervalDisplay)
+		else if ( sourceDisplay instanceof AnnotatedSourceDisplay )
 		{
-			return userInterfaceHelpers.createAnnotatedIntervalDisplaySettingsPanel( ( AnnotatedIntervalDisplay ) sourceDisplay );
+			return userInterfaceHelpers.createAnnotatedMaskDisplaySettingsPanel( ( AnnotatedSourceDisplay ) sourceDisplay );
 		}
 		else
 		{
diff --git a/src/main/java/org/embl/mobie/viewer/ui/UserInterfaceHelpers.java b/src/main/java/org/embl/mobie/viewer/ui/UserInterfaceHelpers.java
index 7be7c8eda..c4a613bec 100644
--- a/src/main/java/org/embl/mobie/viewer/ui/UserInterfaceHelpers.java
+++ b/src/main/java/org/embl/mobie/viewer/ui/UserInterfaceHelpers.java
@@ -4,14 +4,16 @@
 import bdv.tools.brightness.SliderPanelDouble;
 import bdv.util.BdvHandle;
 import bdv.util.BoundedValueDouble;
+import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
 import com.google.gson.Gson;
 import de.embl.cba.bdv.utils.BrightnessUpdateListener;
 import org.embl.mobie.viewer.*;
 import org.embl.mobie.viewer.display.AbstractSourceDisplay;
-import org.embl.mobie.viewer.display.AnnotatedIntervalDisplay;
+import org.embl.mobie.viewer.display.AnnotatedSourceDisplay;
 import org.embl.mobie.viewer.plot.ScatterPlotViewer;
 import org.embl.mobie.viewer.serialize.JsonHelper;
+import org.embl.mobie.viewer.transform.MoBIEViewerTransformAdjuster;
 import org.embl.mobie.viewer.transform.ViewerTransform;
 import org.embl.mobie.viewer.transform.MoBIEViewerTransformChanger;
 import org.embl.mobie.viewer.color.OpacityAdjuster;
@@ -26,7 +28,6 @@
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.type.numeric.ARGBType;
 import org.embl.mobie.viewer.MoBIEHelper;
-import sc.fiji.bdvpg.bdv.navigate.ViewerTransformAdjuster;
 import sc.fiji.bdvpg.bdv.navigate.ViewerTransformChanger;
 import sc.fiji.bdvpg.services.SourceAndConverterServices;
 import sc.fiji.bdvpg.sourceandconverter.display.ColorChanger;
@@ -40,6 +41,7 @@
 import java.net.URL;
 import java.util.*;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import static org.embl.mobie.viewer.ui.SwingHelpers.TEXT_FIELD_HEIGHT;
 
@@ -197,14 +199,14 @@ public static void showOpacityDialog(
 		frame.setVisible( true );
 	}
 
-	public JPanel createAnnotatedIntervalDisplaySettingsPanel( AnnotatedIntervalDisplay display )
+	public JPanel createAnnotatedMaskDisplaySettingsPanel( AnnotatedSourceDisplay display )
 	{
 		JPanel panel = createDisplayPanel( display.getName() );
 		List< SourceAndConverter< ? > > sourceAndConverters = new ArrayList<>( display.sourceNameToSourceAndConverter.values() );
 
 		// Buttons
 		panel.add( createSpace() );
-		panel.add( createButtonPlaceholder() ); // focus
+		panel.add( createFocusButton( display, display.sliceViewer.getBdvHandle(), sourceAndConverters.stream().map( sac -> sac.getSpimSource() ).collect( Collectors.toList() ) ) );
 		panel.add( createOpacityButton( sourceAndConverters, display.getName(), display.sliceViewer.getBdvHandle() ) );
 		panel.add( createButtonPlaceholder() ); // color
 		panel.add( createButtonPlaceholder() ); // brightness
@@ -284,7 +286,7 @@ public JPanel createImageDisplaySettingsPanel( ImageSourceDisplay display )
 
 		// Buttons
 		panel.add( createSpace() );
-		panel.add( createFocusButton( display, sourceAndConverters, display.sliceViewer.getBdvHandle() ) );
+		panel.add( createFocusButton( display, display.sliceViewer.getBdvHandle(), sourceAndConverters.stream().map( sac -> sac.getSpimSource() ).collect( Collectors.toList() ) ) );
 		panel.add( createOpacityButton( sourceAndConverters, display.getName(), display.sliceViewer.getBdvHandle() ) );
 		panel.add( createColorButton( panel, sourceAndConverters, display.sliceViewer.getBdvHandle() ) );
 		panel.add( createImageDisplayBrightnessButton( display ) );
@@ -330,7 +332,7 @@ public JPanel createSegmentationDisplaySettingsPanel( SegmentationSourceDisplay
 				new ArrayList<>( display.sourceNameToSourceAndConverter.values() );
 
 		panel.add( createSpace() );
-		panel.add( createFocusButton( display, sourceAndConverters, display.sliceViewer.getBdvHandle() ) );
+		panel.add( createFocusButton( display, display.sliceViewer.getBdvHandle(), sourceAndConverters.stream().map( sac -> sac.getSpimSource() ).collect( Collectors.toList() ) ) );
 		panel.add( createOpacityButton( sourceAndConverters, display.getName(), display.sliceViewer.getBdvHandle() ) );
 		panel.add( createButtonPlaceholder() );
 		panel.add( createButtonPlaceholder() );
@@ -743,16 +745,17 @@ public void visibility( boolean isVisible )
 		return checkBox;
 	}
 
-	public static JButton createFocusButton(AbstractSourceDisplay sourceDisplay, List< SourceAndConverter< ? > > sourceAndConverters, BdvHandle bdvHandle )
+	public static JButton createFocusButton( AbstractSourceDisplay sourceDisplay, BdvHandle bdvHandle, List< Source< ? > > sources )
 	{
 		JButton button = new JButton( "F" );
 		button.setPreferredSize( PREFERRED_BUTTON_SIZE );
 
 		button.addActionListener( e ->
 		{
-			// TODO: make this work for multiple sources!
-			final AffineTransform3D transform = new ViewerTransformAdjuster(  sourceDisplay.sliceViewer.getBdvHandle(), sourceAndConverters.get( 0 ) ).getTransform();
-			new ViewerTransformChanger( bdvHandle, transform, false, 1000 ).run();
+//			final AffineTransform3D transform = new ViewerTransformAdjuster(  sourceDisplay.sliceViewer.getBdvHandle(), sourceAndConverters.get( 0 ) ).getTransform();
+
+			final AffineTransform3D transform = new MoBIEViewerTransformAdjuster( sourceDisplay.sliceViewer.getBdvHandle(), sources ).getMultiSourceTransform();
+			new ViewerTransformChanger( bdvHandle, transform, false, 500 ).run();
 		} );
 
 		return button;
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 091e7b7b2..a4f2d1117 100644
--- a/src/main/java/org/embl/mobie/viewer/ui/WindowArrangementHelper.java
+++ b/src/main/java/org/embl/mobie/viewer/ui/WindowArrangementHelper.java
@@ -19,27 +19,18 @@ public static int getDefaultWindowWidth()
 		return (int) ( Toolkit.getDefaultToolkit().getScreenSize().width / 3.1 );
 	}
 
-	@Deprecated
-	public static void setImageJLogWindowPositionAndSize( JFrame parentComponent )
+	public static void setBdvWindowPositionAndSize( BdvHandle bdvHandle )
 	{
-		final Frame log = WindowManager.getFrame( "Log" );
-		if (log != null) {
-			Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
-			final int logWindowHeight = screenSize.height - ( parentComponent.getLocationOnScreen().y + parentComponent.getHeight() + 20 );
-			log.setSize( parentComponent.getWidth(), logWindowHeight  );
-			log.setLocation( parentComponent.getLocationOnScreen().x, parentComponent.getLocationOnScreen().y + parentComponent.getHeight() );
-		}
-	}
+		Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
+		final double relativeHeight = 2.0/3;
+		final int height = (int) ( relativeHeight * screenSize.height - 2 * UserInterfaceHelpers.SPACING );
+		final int width = screenSize.width / 2 - 2 * UserInterfaceHelpers.SPACING;
 
-	public static void setBdvWindowPositionAndSize( BdvHandle bdvHandle, JFrame frame )
-	{
 		BdvUtils.getViewerFrame( bdvHandle ).setLocation(
-				frame.getLocationOnScreen().x + frame.getWidth(),
-				frame.getLocationOnScreen().y );
-
-		BdvUtils.getViewerFrame( bdvHandle ).setSize( frame.getHeight(), frame.getHeight() );
+				screenSize.width / 2 + UserInterfaceHelpers.SPACING,
+				UserInterfaceHelpers.SPACING );
 
-		bdvHandle.getViewerPanel().setInterpolation( Interpolation.NLINEAR );
+		BdvUtils.getViewerFrame( bdvHandle ).setSize( width, height );
 	}
 
 	public static void setLogWindowPositionAndSize( Window reference )
@@ -53,6 +44,19 @@ public static void setLogWindowPositionAndSize( Window reference )
 		}
 	}
 
+	public static void setLogWindowPositionAndSize()
+	{
+		final Frame log = WindowManager.getFrame( "Log" );
+		if (log != null) {
+			Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
+			final double relativeHeight = 1.0/3;
+			final int height = (int) ( relativeHeight * screenSize.height ) - 2 * UserInterfaceHelpers.SPACING;
+			final int width = screenSize.width - 2 * UserInterfaceHelpers.SPACING;
+			log.setSize( width, height  );
+			log.setLocation( UserInterfaceHelpers.SPACING, (int) ( ( 1.0 - relativeHeight ) * screenSize.height ) + UserInterfaceHelpers.SPACING );
+		}
+	}
+
 	public static void rightAlignWindow( Window reference, Window window, boolean adjustWidth, boolean adjustHeight )
 	{
 		window.setLocation(
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 8af3bc4ec..e90fece53 100644
--- a/src/main/java/org/embl/mobie/viewer/view/ViewManager.java
+++ b/src/main/java/org/embl/mobie/viewer/view/ViewManager.java
@@ -4,19 +4,17 @@
 import bdv.util.BdvHandle;
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
-import de.embl.cba.tables.Logger;
 import de.embl.cba.tables.color.ColoringModel;
 import de.embl.cba.tables.color.ColumnColoringModelCreator;
-import de.embl.cba.tables.select.DefaultSelectionModel;
 import de.embl.cba.tables.tablerow.TableRowImageSegment;
 import ij.IJ;
 import net.imglib2.realtransform.AffineTransform3D;
 import org.apache.commons.lang.ArrayUtils;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.SourceNameEncoder;
-import org.embl.mobie.viewer.annotate.AnnotatedIntervalAdapter;
+import org.embl.mobie.viewer.annotate.AnnotatedMaskAdapter;
 import org.embl.mobie.viewer.annotate.AnnotatedMaskTableRow;
-import org.embl.mobie.viewer.bdv.view.AnnotatedIntervalSliceView;
+import org.embl.mobie.viewer.bdv.view.AnnotatedMaskSliceView;
 import org.embl.mobie.viewer.bdv.view.ImageSliceView;
 import org.embl.mobie.viewer.bdv.view.SegmentationSliceView;
 import org.embl.mobie.viewer.bdv.view.SliceViewer;
@@ -25,6 +23,7 @@
 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.select.MoBIESelectionModel;
 import org.embl.mobie.viewer.source.LabelSource;
 import org.embl.mobie.viewer.source.SegmentationSource;
 import org.embl.mobie.viewer.table.TableDataFormat;
@@ -33,7 +32,7 @@
 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.transform.TransformHelpers;
 import org.embl.mobie.viewer.ui.MoBIELookAndFeelToggler;
 import org.embl.mobie.viewer.ui.UserInterface;
 import org.embl.mobie.viewer.ui.WindowArrangementHelper;
@@ -143,9 +142,10 @@ public void initTableViewer( SegmentationSourceDisplay display  )
 			{
 				sourceNameToTableDir.put( source, moBIE.getTablesDirectoryPath( ( SegmentationSource ) moBIE.getSource( source ) )
 				);
-			} catch ( Exception e )
+			}
+			catch ( Exception e )
 			{
-				Logger.info("[WARNING] Could not store table directory for " + source );
+				System.out.println("[WARNING] Could not store table directory for " + source );
 				sourceNameToTableDir.put( source, null );
 			}
 		}
@@ -223,11 +223,11 @@ public View getCurrentView( String uiSelectionGroup, boolean isExclusive, boolea
 				if ( hasColumnsOutsideProject( segmentationSourceDisplay ) ) { return null; }
 				currentDisplay = new SegmentationSourceDisplay( segmentationSourceDisplay );
 				addManualTransforms( viewSourceTransforms, segmentationSourceDisplay.sourceNameToSourceAndConverter );
-			} else if ( sourceDisplay instanceof AnnotatedIntervalDisplay )
+			} else if ( sourceDisplay instanceof AnnotatedSourceDisplay )
 			{
-				AnnotatedIntervalDisplay annotatedIntervalDisplay = ( AnnotatedIntervalDisplay ) sourceDisplay;
-				if ( hasColumnsOutsideProject( annotatedIntervalDisplay ) ) { return null; }
-				currentDisplay = new AnnotatedIntervalDisplay( annotatedIntervalDisplay );
+				AnnotatedSourceDisplay annotatedSourceDisplay = ( AnnotatedSourceDisplay ) sourceDisplay;
+				if ( hasColumnsOutsideProject( annotatedSourceDisplay ) ) { return null; }
+				currentDisplay = new AnnotatedSourceDisplay( annotatedSourceDisplay );
 			}
 
 			if ( currentDisplay != null )
@@ -238,7 +238,7 @@ public View getCurrentView( String uiSelectionGroup, boolean isExclusive, boolea
 
 		if ( includeViewerTransform )
 		{
-			AffineTransform3D normalisedViewTransform = TransformHelper.createNormalisedViewerTransform( bdvHandle.getViewerPanel() );
+			AffineTransform3D normalisedViewTransform = TransformHelpers.createNormalisedViewerTransform( bdvHandle.getViewerPanel() );
 
 			final NormalizedAffineViewerTransform transform = new NormalizedAffineViewerTransform( normalisedViewTransform.getRowPackedCopy(), bdvHandle.getViewerPanel().state().getCurrentTimepoint() );
 			return new View(uiSelectionGroup, viewSourceDisplays, viewSourceTransforms, transform, isExclusive);
@@ -259,7 +259,7 @@ public synchronized void show( View view )
 			removeAllSourceDisplays();
 		}
 
-		openAndTransformViewSources( view );
+		openAndTransformSources( view );
 
 		// show the displays
 		MoBIELookAndFeelToggler.setMoBIELaf();
@@ -272,7 +272,7 @@ public synchronized void show( View view )
 		adjustViewerTransform( view );
 	}
 
-	public void openAndTransformViewSources( View view )
+	public void openAndTransformSources( View view )
 	{
 		// fetch the names of all sources that are either shown or to be transformed
 		final Set< String > sources = fetchSources( view );
@@ -351,9 +351,9 @@ else if ( sourceDisplay instanceof SegmentationSourceDisplay )
 		{
 			showSegmentationDisplay( ( SegmentationSourceDisplay ) sourceDisplay );
 		}
-		else if ( sourceDisplay instanceof AnnotatedIntervalDisplay )
+		else if ( sourceDisplay instanceof AnnotatedSourceDisplay )
 		{
-			showAnnotatedIntervalDisplay( ( AnnotatedIntervalDisplay ) sourceDisplay );
+			showAnnotatedMaskDisplay( ( AnnotatedSourceDisplay ) sourceDisplay );
 		}
 
 		userInterface.addSourceDisplay( sourceDisplay );
@@ -393,21 +393,21 @@ private void initImageVolumeViewer( ImageSourceDisplay imageDisplay )
 		}
 	}
 
-	private void showAnnotatedIntervalDisplay( AnnotatedIntervalDisplay annotationDisplay )
+	private void showAnnotatedMaskDisplay( AnnotatedSourceDisplay annotationDisplay )
 	{
 		annotationDisplay.sliceViewer = sliceViewer;
-		annotationDisplay.tableRows = moBIE.loadAnnotatedIntervalTables( annotationDisplay );
-		annotationDisplay.annotatedIntervalAdapter = new AnnotatedIntervalAdapter<>( annotationDisplay.tableRows );
+		annotationDisplay.tableRows = moBIE.loadAnnotatedMaskTables( annotationDisplay );
+		annotationDisplay.annotatedMaskAdapter = new AnnotatedMaskAdapter( annotationDisplay.tableRows );
 
 		configureMoBIEColoringModel( annotationDisplay );
-		annotationDisplay.selectionModel = new DefaultSelectionModel<>();
-		annotationDisplay.coloringModel.setSelectionModel(  annotationDisplay.selectionModel );
+		annotationDisplay.selectionModel = new MoBIESelectionModel<>();
+		annotationDisplay.coloringModel.setSelectionModel( annotationDisplay.selectionModel );
 
 		// set selected segments
 		if ( annotationDisplay.getSelectedAnnotationIds() != null )
 		{
-			final List< AnnotatedMaskTableRow > annotatedIntervals = annotationDisplay.annotatedIntervalAdapter.getAnnotatedIntervals( annotationDisplay.getSelectedAnnotationIds() );
-			annotationDisplay.selectionModel.setSelected( annotatedIntervals, true );
+			final List< AnnotatedMaskTableRow > annotatedMasks = annotationDisplay.annotatedMaskAdapter.getAnnotatedMasks( annotationDisplay.getSelectedAnnotationIds() );
+			annotationDisplay.selectionModel.setSelected( annotatedMasks, true );
 		}
 
 		showInSliceViewer( annotationDisplay );
@@ -420,11 +420,13 @@ private void showAnnotatedIntervalDisplay( AnnotatedIntervalDisplay annotationDi
 		} );
 	}
 
-	private void initTableViewer( AnnotatedIntervalDisplay annotationDisplay )
+	private void initTableViewer( AnnotatedSourceDisplay display )
 	{
 		HashMap<String, String> nameToTableDir = new HashMap<>();
-		nameToTableDir.put( annotationDisplay.getName(), annotationDisplay.getTableDataFolder( TableDataFormat.TabDelimitedFile ) );
-		annotationDisplay.tableViewer = new TableViewer<>( moBIE, annotationDisplay.tableRows, annotationDisplay.selectionModel, annotationDisplay.coloringModel, annotationDisplay.getName(), nameToTableDir, true ).show();
+		nameToTableDir.put( display.getName(), display.getTableDataFolder( TableDataFormat.TabDelimitedFile ) );
+		display.tableViewer = new TableViewer<>( moBIE, display.tableRows, display.selectionModel, display.coloringModel, display.getName(), nameToTableDir, true ).show();
+		display.selectionModel.listeners().add( display.tableViewer );
+		display.coloringModel.listeners().add( display.tableViewer );
 	}
 
 	private void showSegmentationDisplay( SegmentationSourceDisplay segmentationDisplay )
@@ -442,7 +444,7 @@ private void showSegmentationDisplay( SegmentationSourceDisplay segmentationDisp
 		}
 
 		configureMoBIEColoringModel( segmentationDisplay );
-		segmentationDisplay.selectionModel = new DefaultSelectionModel<>();
+		segmentationDisplay.selectionModel = new MoBIESelectionModel<>();
 		segmentationDisplay.coloringModel.setSelectionModel(  segmentationDisplay.selectionModel );
 
 		// set selected segments
@@ -496,12 +498,12 @@ private void loadTablesAndCreateImageSegments( SegmentationSourceDisplay segment
 
 	private void showInSliceViewer( SegmentationSourceDisplay segmentationDisplay )
 	{
-		segmentationDisplay.sliceView = new SegmentationSliceView<>( moBIE, segmentationDisplay, bdvHandle );
+		segmentationDisplay.sliceView = new SegmentationSliceView( moBIE, segmentationDisplay, bdvHandle );
 	}
 
-	private void showInSliceViewer( AnnotatedIntervalDisplay annotatedIntervalDisplay )
+	private void showInSliceViewer( AnnotatedSourceDisplay annotatedSourceDisplay )
 	{
-		annotatedIntervalDisplay.sliceView = new AnnotatedIntervalSliceView( moBIE, annotatedIntervalDisplay, bdvHandle );
+		annotatedSourceDisplay.sliceView = new AnnotatedMaskSliceView( moBIE, annotatedSourceDisplay, bdvHandle );
 	}
 
 	private void initSegmentationVolumeViewer( SegmentationSourceDisplay segmentationDisplay )
@@ -523,7 +525,7 @@ private void initSegmentationVolumeViewer( SegmentationSourceDisplay segmentatio
 
 	public synchronized void removeSourceDisplay( SourceDisplay sourceDisplay, boolean closeImgLoader )
 	{
-		if ( sourceDisplay instanceof SegmentationSourceDisplay )
+		if ( sourceDisplay instanceof AnnotatedRegionDisplay )
 		{
 			final SegmentationSourceDisplay segmentationDisplay = ( SegmentationSourceDisplay ) sourceDisplay;
 			segmentationDisplay.sliceView.close( closeImgLoader );
@@ -539,13 +541,13 @@ else if ( sourceDisplay instanceof ImageSourceDisplay )
 			final ImageSourceDisplay imageDisplay = ( ImageSourceDisplay ) sourceDisplay;
 			imageDisplay.imageSliceView.close( false );
 		}
-		else if ( sourceDisplay instanceof AnnotatedIntervalDisplay )
+		else if ( sourceDisplay instanceof AnnotatedSourceDisplay )
 		{
 			// TODO: Code duplication (sourceDisplay instanceof SegmentationSourceDisplay)
-			final AnnotatedIntervalDisplay annotatedIntervalDisplay = ( AnnotatedIntervalDisplay ) sourceDisplay;
-			annotatedIntervalDisplay.sliceView.close( false );
-			annotatedIntervalDisplay.tableViewer.close();
-			annotatedIntervalDisplay.scatterPlotViewer.close();
+			final AnnotatedSourceDisplay annotatedSourceDisplay = ( AnnotatedSourceDisplay ) sourceDisplay;
+			annotatedSourceDisplay.sliceView.close( false );
+			annotatedSourceDisplay.tableViewer.close();
+			annotatedSourceDisplay.scatterPlotViewer.close();
 		}
 
 		userInterface.removeDisplaySettingsPanel( sourceDisplay );
@@ -568,11 +570,11 @@ else if ( sourceDisplay instanceof AnnotatedIntervalDisplay )
 		}
 	}
 
-	public Collection< SegmentationSourceDisplay > getSegmentationDisplays()
+	public Collection< AnnotatedRegionDisplay > getAnnotatedRegionDisplays()
 	{
-		final List< SegmentationSourceDisplay > segmentationDisplays = getCurrentSourceDisplays().stream().filter( s -> s instanceof SegmentationSourceDisplay ).map( s -> ( SegmentationSourceDisplay ) s ).collect( Collectors.toList() );
+		final List< AnnotatedRegionDisplay > displays = getCurrentSourceDisplays().stream().filter( s -> s instanceof AnnotatedRegionDisplay ).map( s -> ( AnnotatedRegionDisplay ) s ).collect( Collectors.toList() );
 
-		return segmentationDisplays;
+		return displays;
 	}
 
 	public void close()
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 819271ff4..627dbb98e 100644
--- a/src/main/java/org/embl/mobie/viewer/volume/SegmentsVolumeViewer.java
+++ b/src/main/java/org/embl/mobie/viewer/volume/SegmentsVolumeViewer.java
@@ -38,8 +38,8 @@
 import de.embl.cba.tables.color.ColoringModel;
 import de.embl.cba.tables.ij3d.AnimatedViewAdjuster;
 import de.embl.cba.tables.imagesegment.ImageSegment;
-import de.embl.cba.tables.select.SelectionListener;
-import de.embl.cba.tables.select.SelectionModel;
+import org.embl.mobie.viewer.select.SelectionListener;
+import org.embl.mobie.viewer.select.SelectionModel;
 import ij3d.Content;
 import ij3d.Image3DUniverse;
 import ij3d.UniverseListener;
@@ -395,7 +395,7 @@ public void contentSelected( Content c )
 				else
 				{
 					recentFocus = segment; // avoids "self-focusing"
-					selectionModel.focus( segment );
+					selectionModel.focus( segment, this );
 				}
 			}
 
@@ -457,7 +457,7 @@ public synchronized void selectionChanged()
 	}
 
 	@Override
-	public synchronized void focusEvent( S selection )
+	public synchronized void focusEvent( S selection, Object origin  )
 	{
 		if ( ! showSegments ) return;
 


Test patch:
diff --git a/src/test/java/debug/DebugMultiThreadedIOCovidIF.java b/src/test/java/debug/DebugMultiThreadedIOCovidIF.java
new file mode 100644
index 000000000..5c82299e6
--- /dev/null
+++ b/src/test/java/debug/DebugMultiThreadedIOCovidIF.java
@@ -0,0 +1,31 @@
+package debug;
+
+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.ThreadUtils;
+
+import java.io.IOException;
+
+public class DebugMultiThreadedIOCovidIF
+{
+	public static void main( String[] args )
+	{
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+
+		try {
+			run();
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+	}
+
+	private static void run() throws IOException
+	{
+		ThreadUtils.setnIoThreads( 16 );
+		new MoBIE( "https://github.com/mobie/covid-if-project", MoBIESettings.settings().gitProjectBranch( "main" ).imageDataFormat( ImageDataFormat.OmeZarrS3 ).view( "boundary-segment-test" ) ).close();
+		//"default_transform_grid_with_tables_with_segmentations_without_segmentation_tables"
+	}
+}
diff --git a/src/test/java/develop/DevelopRealMaskOfSources.java b/src/test/java/develop/DevelopRealMaskRealIntervals.java
similarity index 72%
rename from src/test/java/develop/DevelopRealMaskOfSources.java
rename to src/test/java/develop/DevelopRealMaskRealIntervals.java
index 746701bb5..ab92cec20 100644
--- a/src/test/java/develop/DevelopRealMaskOfSources.java
+++ b/src/test/java/develop/DevelopRealMaskRealIntervals.java
@@ -2,12 +2,13 @@
 
 import net.imglib2.RealPoint;
 import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.roi.Mask;
 import net.imglib2.roi.RealMaskRealInterval;
 import net.imglib2.roi.geom.GeomMasks;
 
 import java.util.Arrays;
 
-public class DevelopRealMaskOfSources
+public class DevelopRealMaskRealIntervals
 {
 	public static void main( String[] args )
 	{
@@ -20,11 +21,21 @@ public static void main( String[] args )
 		final RealMaskRealInterval maskB = GeomMasks.closedBox( new double[]{ 0.5, 0.5, 0.5 }, new double[]{ 2, 2, 2 } ).transform( transformB );
 
 		final RealMaskRealInterval maskAB = maskA.or( maskB );
+		final double realMin = maskAB.realMin( 0 );
 
 		final RealPoint point = new RealPoint( 1.5, 1.5, 1.5 );
 		System.out.println( maskAB.getClass() );
 		System.out.println( Arrays.toString( maskAB.minAsDoubleArray() ) );
 		System.out.println( Arrays.toString( maskAB.maxAsDoubleArray()) );
 		System.out.println( maskAB.test( point ) );
+
+		final RealMaskRealInterval mask = GeomMasks.closedBox( new double[]{ 0, 0, 0 }, new double[]{ 1, 1, 1 } );
+		RealMaskRealInterval test = mask;
+		for ( int i = 0; i < 20; i++ )
+		{
+			test = test.or( mask );
+			System.out.println( i );
+			System.out.println( test.realMin( 0 ));
+		}
 	}
 }
diff --git a/src/test/java/develop/ExploreJsonLoadingFromGitlab2.java b/src/test/java/develop/ExploreJsonLoadingFromGitlab2.java
index e02c18654..ab6e34449 100644
--- a/src/test/java/develop/ExploreJsonLoadingFromGitlab2.java
+++ b/src/test/java/develop/ExploreJsonLoadingFromGitlab2.java
@@ -20,6 +20,5 @@ public static void main( String[] args ) throws IOException
 
 		GsonBuilder builder = new GsonBuilder();
 		Object o = builder.create().fromJson(reader, Object.class);
-		int a = 1;
 	}
 }
diff --git a/src/test/java/projects/OpenRemoteCovidIF.java b/src/test/java/projects/OpenRemoteCovidIF.java
index 3be16557c..729a19eb6 100644
--- a/src/test/java/projects/OpenRemoteCovidIF.java
+++ b/src/test/java/projects/OpenRemoteCovidIF.java
@@ -5,7 +5,10 @@
 import org.embl.mobie.viewer.MoBIESettings;
 
 import java.io.IOException;
+import java.util.Map;
+
 import org.embl.mobie.io.ImageDataFormat;
+import org.embl.mobie.viewer.view.View;
 
 public class OpenRemoteCovidIF
 {
@@ -15,7 +18,7 @@ public static void main( String[] args )
 		imageJ.ui().showUI();
 
 		try {
-			new MoBIE("https://github.com/mobie/covid-if-project", MoBIESettings.settings().gitProjectBranch( "main" ).imageDataFormat( ImageDataFormat.OmeZarrS3 ) );
+			final MoBIE moBIE = new MoBIE( "https://github.com/mobie/covid-if-project", MoBIESettings.settings().gitProjectBranch( "main" ).imageDataFormat( ImageDataFormat.OmeZarrS3 ).view( "merge-grid-no-tables" ) );
 		} catch (IOException e) {
 			e.printStackTrace();
 		}
diff --git a/src/test/java/tests/TestRemoteZebrafish.java b/src/test/java/tests/TestRemoteZebrafish.java
index 50424363e..8fd115a9f 100644
--- a/src/test/java/tests/TestRemoteZebrafish.java
+++ b/src/test/java/tests/TestRemoteZebrafish.java
@@ -4,6 +4,7 @@
 import org.embl.mobie.io.ImageDataFormat;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
+import org.embl.mobie.viewer.display.AnnotatedRegionDisplay;
 import org.embl.mobie.viewer.display.SegmentationSourceDisplay;
 import org.embl.mobie.viewer.view.additionalviews.AdditionalViewsLoader;
 import org.junit.jupiter.api.Test;
@@ -29,14 +30,14 @@ public void testSmallGridView() throws IOException
 		moBIE.getViewManager().show( "small-grid-view" );
 
 		// select some image segments
-		final SegmentationSourceDisplay display = moBIE.getViewManager().getSegmentationDisplays().iterator().next();
+		final AnnotatedRegionDisplay display = moBIE.getViewManager().getAnnotatedRegionDisplays().iterator().next();
 		display.selectionModel.setSelected( display.tableRows.get( 0 ), true );
-		display.selectionModel.focus( display.tableRows.get( 0 ) );
+		display.selectionModel.focus( display.tableRows.get( 0 ), this );
 		display.selectionModel.setSelected( display.tableRows.get( 1 ), true );
-		display.selectionModel.focus( display.tableRows.get( 1 ) );
+		display.selectionModel.focus( display.tableRows.get( 1 ), this );
 
 		// show in 3D
-		display.segmentsVolumeViewer.showSegments( true );
+		((SegmentationSourceDisplay) display).segmentsVolumeViewer.showSegments( true );
 	}
 
 	@Test
@@ -52,8 +53,8 @@ public void testTransformedGridView() throws IOException
 		moBIE.getViewManager().show( "test-transformed-grid" );
 
 		// select some image segments
-		final SegmentationSourceDisplay display = moBIE.getViewManager().getSegmentationDisplays().iterator().next();
+		final AnnotatedRegionDisplay display = moBIE.getViewManager().getAnnotatedRegionDisplays().iterator().next();
 		display.selectionModel.setSelected( display.tableRows.get( 0 ), true );
-		display.selectionModel.focus( display.tableRows.get( 0 ) );
+		display.selectionModel.focus( display.tableRows.get( 0 ), this );
 	}
 }