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

Issue:
Navigation does not work for segmentation table in mergedGrid
Navigating to segments via selecting rows in the tables or segments in the image does not work for segmentations in a merged grid. There are two issues:
- when selecting a segment the viewer navigates to a wrong location (outside the image space), see second screenshot below
- the selection applies to all cells with the same id, not just the cell with this id in the image at hand. we somehow need to take the image-id into account here.

The example is from https://github.com/mobie/covid-if-project using the "cell segmentation".

![aaa-nav1](https://user-images.githubusercontent.com/4263537/142634667-fc17d7c1-afdd-421b-9422-7dbab874fd11.png)
![aaa-nav2](https://user-images.githubusercontent.com/4263537/142634674-61211599-13b6-4001-9bbb-c889f50a448a.png)
![aaa-nav3](https://user-images.githubusercontent.com/4263537/142634690-a08b59db-6462-4bf7-b7d0-ca5a8f78e7d5.png)




Discussion / hints:
Thanks @constantinpape,
I found quite a couple of bugs! 
I filed a PR.
@constantinpape 

The navigation to the sites (images) works now (not merged yet), but as @K-Meech pointed out, it does not go to the correct cell within the image. 

In which units are the anchor coordinates? Could it be that there is something off?

Another idea: maybe the x and y coordinates are switched or flipped or mirrored?

> In which units are the anchor coordinates? Could it be that there is something off?
> 

The units are in relative image coordinates. I.e. for each segmentation the coordinates go from 0 to 1024 (here we use 'pixels' with scale factor 1 as physical coordinate space, but in general this would be in physical coordinates).
So you have to offset these coordinates with the relative position of the image in the grid.

> Another idea: maybe the x and y coordinates are switched or flipped or mirrored?

I don't think so, as I use the same code as everywhere else for computing the table here and this was never an issue before.

OK, there is some deeper issue...

The coloring of the label sources is solely done via the label index in the image. 

Now, for a `MergedGridSource` this does not work, because several objects in the same source can have the same label index...

I am in fact not quite sure how to fix this right now...

@tischi I have added `encodeSource: True` to  the well grids for all the segmentations in `covid-if` now.
Thanks! Seems to work 🥳 

However, are you sure about the x and y axis not being mixed up in the cell's anchor points?

In below example I am specifically selecting/highlighting cells with low values in their x-coordinate and they all show up at the top of the image (where the low y-coordinates should be):

<img width="827" alt="image" src="https://user-images.githubusercontent.com/2157566/142934267-8546f064-4edb-478e-a644-fc311cb06ec7.png">



I can double check the x and y issue once we have the new working version on MoBIE or MoBIE-beta.
Ok! The new version is now on MoBIE-beta.
Indeed something with the anchors is wrong, but it's not a simple switch of x and y. I will investigate further and let you know once it's fixed.
The anchors should be correct in all tables now. I still have some issues with the navigation though: when trying to select a segment in an image I get the following error:
```
Exception in thread "Thread-401" java.lang.ClassCastException: de.embl.cba.tables.imagesegment.DefaultImageSegment cannot be cast to de.embl.cba.tables.tablerow.TableRowImageSegment
	at org.embl.mobie.viewer.segment.BdvSegmentSelector.toggleSelectionAtMousePosition(BdvSegmentSelector.java:90)
	at org.embl.mobie.viewer.segment.BdvSegmentSelector.run(BdvSegmentSelector.java:144)
	at org.embl.mobie.viewer.bdv.view.SliceViewer.lambda$null$5(SliceViewer.java:122)
	at java.lang.Thread.run(Thread.java:748)
```
(On latest MoBIE-beta)

Could you please check this issue, @tischi @K-Meech?
I will have a look...
Do you know the repo where Julian had the example data where there are segmentations **without tables**? I think I'll need this to clean up the code.
No sorry, I am not sure. @jhennies could you point us to the MoBIE project where you had a table without segmentations?
@constantinpape 

I cannot reproduce this using the current develop branch. When I click on the cells in both the image and the table it works for me (i.e. no error). But they still move to the wrong location.

@K-Meech Can you please try to reproduce? In develop and in MoBIE-beta?

Same for me. I can't reproduce it on MoBIE-beta.
@tischi The test project is in /g/emcf/hennies/for_constantin/mobie_no_table_test
@constantinpape can you check the version number of mobie in your MoBIE-beta? Maybe it did not download the latest?

Ok, I tried again and now the error is gone works. Not sure what was going on there...

Regarding the coordinates in the tables:
I am sure that they are correct now because I am validating them for all segmentations (https://github.com/mobie/covid-if-project/blob/main/check_anchors.py#L52).

Maybe we are interpreting y and x different here ... but it would be easiest if you could just check this by switching the columns when loading @tischi, then I don't have to upload new tables for debugging.  
I am on a retreat now. I would look into it on Monday.
@K-Meech if you feel like testing this, go ahead!
@constantinpape I had another look and I think I need some help.

Is there any logic as to how the label_ids are given? I my code often the labeling starts in some corner of the image, but here I cannot see any correlation between label_id and anchor points. Is this normal?

<img width="955" alt="image" src="https://user-images.githubusercontent.com/2157566/143618116-28972eae-7ecf-4428-9572-5dad9fee9554.png">
 
-----

Could you please for the image `cell_segmentation_WellE06_0000` just give me one cell (with a screenshot) where you know the label_id and which has a distinct shape such that I can find it back? Because I think at the moment the whole assignment from table row to image segment is wrong as color by column does not give the expected coloring when coloring by n_pixels. Bigger cells show be more yellow using viridis, which clearly is not the case:

<img width="630" alt="image" src="https://user-images.githubusercontent.com/2157566/143618440-779a276e-1f2a-4d96-aaa5-1ebf968ace12.png">

Couple of other ideas for the debugging:
- a view with just a single site (no grid transformation)
- the current default view but with the normal grid transformer (without merging)
> Is there any logic as to how the label_ids are given? I my code often the labeling starts in some corner of the image, but here I cannot see any correlation between label_id and anchor points. Is this normal?

The segmentation is based on a seeded watershed from nuclei segmented with stardist. So this boils down to how stardist assigns ids to nuclei; no idea if this follows any systematic and in general you cannot rely on any pattern of id assignments because segmentations are invariant under relabeling.

> Could you please for the image `cell_segmentation_WellE06_0000` just give me one cell (with a screenshot) where you know the label_id and which has a distinct shape such that I can find it back?

That's label-id 132:
![label132](https://user-images.githubusercontent.com/4263537/143624896-3fcfe85c-3836-4c05-9983-70147bf2321f.png)

Here's the python script to get it, you can change the label id with the `-l` argument:
https://github.com/mobie/covid-if-project/blob/main/view_segment.py

> * a view with just a single site (no grid transformation)
> 

`bookmarks->single_image`

> * the current default view but with the normal grid transformer (without merging)

`plate->default_transform_grid_with_tables_with_segmentations`
Thanks! That helps! In the single image view:

<img width="801" alt="image" src="https://user-images.githubusercontent.com/2157566/143636523-6913099e-2b30-4cb6-be72-01915859a577.png">

So, label index seems to be OK, but the coordinates in the table are different from the the coordinates of the cell in BDV (see upper right corner of BDV). `(0,0)` is the upper left corner of the image in BDV. I guess you could also check yourself. Any ideas?

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 f8c88599e..c09fe0495 100644
--- a/src/main/java/org/embl/mobie/viewer/MoBIE.java
+++ b/src/main/java/org/embl/mobie/viewer/MoBIE.java
@@ -1,6 +1,5 @@
 package org.embl.mobie.viewer;
 
-import bdv.util.volatiles.SharedQueue;
 import bdv.img.n5.N5ImageLoader;
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
@@ -39,16 +38,13 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 
 public class MoBIE
 {
 	static { net.imagej.patcher.LegacyInjector.preinit(); }
 
-	public static final int N_THREADS = Runtime.getRuntime().availableProcessors() - 1;
-	public static final SharedQueue sharedQueue = new SharedQueue( N_THREADS );
 	public static final String PROTOTYPE_DISPLAY_VALUE = "01234567890123456789";
-	public static final ExecutorService executorService = Executors.newFixedThreadPool( N_THREADS );
 
 	private final String projectName;
 	private MoBIESettings settings;
@@ -181,17 +177,16 @@ private void setProjectImageAndTableRootLocations( )
 
 		Map< String, SourceAndConverter< ? > > sourceAndConverters = new ConcurrentHashMap< >();
 
-		final ExecutorService executorService = Executors.newFixedThreadPool( N_THREADS );
+		final ArrayList< Future< ? > > futures = ThreadUtils.getFutures();
 		for ( String sourceName : sources )
 		{
-			executorService.execute( () -> {
-				sourceAndConverters.put( sourceName, openSourceAndConverter( sourceName ) );
-			} );
+			futures.add(
+					ThreadUtils.ioExecutorService.submit( () -> { sourceAndConverters.put( sourceName, openSourceAndConverter( sourceName ) ); }
+				) );
 		}
+		ThreadUtils.waitUntilFinished( futures );
 
-		MoBIEUtils.waitUntilFinishedAndShutDown( executorService );
-
-		System.out.println( "Fetched " + sourceAndConverters.size() + " image source(s) in " + (System.currentTimeMillis() - start) + " ms, using " + N_THREADS + " thread(s).");
+		System.out.println( "Fetched " + sourceAndConverters.size() + " image source(s) in " + (System.currentTimeMillis() - start) + " ms, using " + ThreadUtils.N_IO_THREADS + " thread(s).");
 
 		return sourceAndConverters;
 	}
@@ -291,7 +286,7 @@ public synchronized ImageSource getSource( String sourceName )
 		final String imagePath = getImagePath( imageSource );
         IJ.log( "Opening image:\n" + imagePath );
         final ImageDataFormat imageDataFormat = settings.values.getImageDataFormat();
-        SpimData spimData = new SpimDataOpener().openSpimData( imagePath, imageDataFormat, sharedQueue );
+        SpimData spimData = new SpimDataOpener().openSpimData( imagePath, imageDataFormat, ThreadUtils.sharedQueue );
         sourceNameToImgLoader.put( sourceName, spimData.getSequenceDescription().getImgLoader() );
 
         final SourceAndConverterFromSpimDataCreator creator = new SourceAndConverterFromSpimDataCreator( spimData );
@@ -381,20 +376,20 @@ private List< Map< String, List< String > > > loadAdditionalTables( List<String>
 		final List< Map< String, List< String > > > additionalTables = new CopyOnWriteArrayList<>();
 
 		final long start = System.currentTimeMillis();
-		final ExecutorService executorService = Executors.newFixedThreadPool( N_THREADS );
-
+		final ExecutorService executorService = ThreadUtils.ioExecutorService;
+		final ArrayList< Future< ? > > futures = ThreadUtils.getFutures();
 		for ( String sourceName : sources )
 		{
-			executorService.execute( () -> {
-				Map< String, List< String > > columns =
-						loadAdditionalTable( sourceName, getTablePath( ( SegmentationSource ) getSource( sourceName ), table ) );
+			futures.add(
+				executorService.submit( () -> {
+					Map< String, List< String > > columns = loadAdditionalTable( sourceName, getTablePath( ( SegmentationSource ) getSource( sourceName ), table ) );
 				additionalTables.add( columns );
-			} );
+				} )
+			);
 		}
+		ThreadUtils.waitUntilFinished( futures );
 
-		MoBIEUtils.waitUntilFinishedAndShutDown( executorService );
-
-		System.out.println( "Fetched " + sources.size() + " table(s) in " + (System.currentTimeMillis() - start) + " ms, using " + N_THREADS + " thread(s).");
+		System.out.println( "Fetched " + sources.size() + " table(s) in " + (System.currentTimeMillis() - start) + " ms, using " + ThreadUtils.N_IO_THREADS + " thread(s).");
 
 		return additionalTables;
 	}
@@ -416,21 +411,23 @@ private ArrayList< List< TableRowImageSegment > > loadPrimarySegmentsTables( Seg
 			sourceNameToRootSources.put( sourceName, rootSources );
 		}
 
-		// TODO: make parallel
+
 		final ArrayList< List< TableRowImageSegment > > primaryTables = new ArrayList<>();
+		final ArrayList< Future< ? > > futures = ThreadUtils.getFutures();
 		for ( String displayedSourceName : segmentationDisplaySources )
 		{
 			final Set< Source > rootSources = sourceNameToRootSources.get( displayedSourceName );
 			for ( Source rootSource : rootSources )
 			{
-				addPrimaryTable( table, primaryTables, rootSource.getName() );
+				futures.add( ThreadUtils.ioExecutorService.submit( () -> 				loadAndAddPrimaryTable( table, primaryTables, rootSource.getName() ) ) );
 			}
 		}
+		ThreadUtils.waitUntilFinished( futures );
 
 		return primaryTables;
 	}
 
-	private void addPrimaryTable( String tableName, ArrayList< List< TableRowImageSegment > > primaryTables, String tableSourceName )
+	private void loadAndAddPrimaryTable( String tableName, ArrayList< List< TableRowImageSegment > > primaryTables, String tableSourceName )
 	{
 		final List< TableRowImageSegment > primaryTable = loadImageSegmentsTable( tableSourceName, tableName );
 		primaryTables.add( primaryTable );
diff --git a/src/main/java/org/embl/mobie/viewer/MoBIEUtils.java b/src/main/java/org/embl/mobie/viewer/MoBIEUtils.java
index e98272fba..d04994ef0 100644
--- a/src/main/java/org/embl/mobie/viewer/MoBIEUtils.java
+++ b/src/main/java/org/embl/mobie/viewer/MoBIEUtils.java
@@ -1,56 +1,42 @@
 package org.embl.mobie.viewer;
 
-import bdv.SpimSource;
-import bdv.tools.transformation.TransformedSource;
 import bdv.util.BdvHandle;
-import bdv.util.ResampledSource;
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
 import de.embl.cba.bdv.utils.BdvUtils;
 import de.embl.cba.tables.FileAndUrlUtils;
 import de.embl.cba.tables.TableColumns;
-import de.embl.cba.tables.imagesegment.ImageSegment;
 import de.embl.cba.tables.imagesegment.SegmentProperty;
 import de.embl.cba.tables.imagesegment.SegmentUtils;
 import de.embl.cba.tables.tablerow.TableRowImageSegment;
 import ij.IJ;
 import ij.gui.GenericDialog;
-import net.imglib2.*;
+import net.imglib2.FinalRealInterval;
+import net.imglib2.RealPoint;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.realtransform.Scale3D;
-import net.imglib2.util.Intervals;
-import org.embl.mobie.viewer.transform.MergedGridSource;
 
 import javax.swing.*;
 import javax.swing.filechooser.FileNameExtensionFilter;
-import java.io.*;
+import java.io.File;
 import java.net.URI;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 import static de.embl.cba.bdv.utils.BdvUtils.getBdvWindowCenter;
-import static de.embl.cba.tables.Utils.*;
-import static de.embl.cba.tables.Utils.getVoxelSpacings;
-import static org.embl.mobie.viewer.ui.SwingHelper.selectionDialog;
 import static de.embl.cba.tables.imagesegment.SegmentUtils.BB_MAX_Z;
 import static de.embl.cba.tables.imagesegment.SegmentUtils.BB_MIN_Z;
+import static org.embl.mobie.viewer.ui.SwingHelper.selectionDialog;
 
 public abstract class MoBIEUtils
 {
 	static { net.imagej.patcher.LegacyInjector.preinit(); }
 
-	public static void waitUntilFinishedAndShutDown( ExecutorService executorService )
-	{
-		executorService.shutdown();
-		try {
-			executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
-		} catch (InterruptedException e) {
-		}
-	}
-
 	public static int[] asInts( long[] longs) {
 		int[] ints = new int[longs.length];
 
diff --git a/src/main/java/org/embl/mobie/viewer/SourceNameEncoder.java b/src/main/java/org/embl/mobie/viewer/SourceNameEncoder.java
index 6094fd69a..606e3b9f3 100644
--- a/src/main/java/org/embl/mobie/viewer/SourceNameEncoder.java
+++ b/src/main/java/org/embl/mobie/viewer/SourceNameEncoder.java
@@ -42,27 +42,36 @@ public static String getName( final UnsignedIntType unsignedIntType )
 
 	public static String getName( final VolatileUnsignedIntType unsignedIntType )
 	{
-		final long imageIndex = unsignedIntType.get().get() >> valueBits;
+		return getName( unsignedIntType.get().get() );
+	}
+
+	public static String getName( final long l )
+	{
+		final long imageIndex = l >> valueBits;
 		final String name = longToName.get( imageIndex );
 		return name;
 	}
 
 	public static long getValue( final UnsignedIntType unsignedIntType )
 	{
-		final long value = unsignedIntType.get() & 0x0000FFFF;
-		return value;
+		return getValue( unsignedIntType.get() );
 	}
 
 	public static long getValue( final VolatileUnsignedIntType unsignedIntType )
 	{
-		final long value = unsignedIntType.get().get() & 0x0000FFFF;
+		return getValue( unsignedIntType.get().get() );
+	}
+
+	public static long getValue( final long l )
+	{
+		final long value = l & 0x0000FFFF;
 		return value;
 	}
 
 	public static void encodeName( final UnsignedIntType value, final String name )
 	{
-		final long encoded = value.get() + ( nameToLong.get( name ) << valueBits );
+		final long l = value.get();
+		final long encoded = l + ( nameToLong.get( name ) << valueBits );
 		value.set( encoded );
 	}
-
 }
diff --git a/src/main/java/org/embl/mobie/viewer/ThreadUtils.java b/src/main/java/org/embl/mobie/viewer/ThreadUtils.java
new file mode 100644
index 000000000..7cba8a84a
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/ThreadUtils.java
@@ -0,0 +1,45 @@
+package org.embl.mobie.viewer;
+
+import bdv.util.volatiles.SharedQueue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+public class ThreadUtils
+{
+	public static final int N_FETCHER_THREADS = Runtime.getRuntime().availableProcessors() - 1;
+	public static final SharedQueue sharedQueue = new SharedQueue( N_FETCHER_THREADS );
+
+	public static final int N_IO_THREADS = 8;
+	public static final ExecutorService ioExecutorService = Executors.newFixedThreadPool( N_IO_THREADS );
+
+	public static final int N_THREADS = Runtime.getRuntime().availableProcessors() - 1;;
+	public static final ExecutorService executorService = Executors.newFixedThreadPool( N_THREADS );
+
+	public static void waitUntilFinished( List< Future< ? > > futures )
+	{
+		for ( Future< ? > future : futures )
+		{
+			try
+			{
+				future.get();
+			} catch ( InterruptedException e )
+			{
+				e.printStackTrace();
+			} catch ( ExecutionException e )
+			{
+				e.printStackTrace();
+			}
+		}
+	}
+
+	public static ArrayList< Future< ? > > getFutures()
+	{
+		return new ArrayList<>();
+	}
+}
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/view/SegmentationSliceView.java b/src/main/java/org/embl/mobie/viewer/bdv/view/SegmentationSliceView.java
index abc0f1f46..3762f748e 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
@@ -101,6 +101,7 @@ 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 );
diff --git a/src/main/java/org/embl/mobie/viewer/color/LabelConverter.java b/src/main/java/org/embl/mobie/viewer/color/LabelConverter.java
index 801ef45de..32d0f780f 100644
--- a/src/main/java/org/embl/mobie/viewer/color/LabelConverter.java
+++ b/src/main/java/org/embl/mobie/viewer/color/LabelConverter.java
@@ -48,13 +48,12 @@ public class LabelConverter< S extends ImageSegment > implements Converter< Real
 	private int timePointIndex = 0;
 	private double opacity = 1.0;
 
-	// No imageId given => decode from pixel value
 	public LabelConverter(
 			SegmentAdapter< S > segmentAdapter,
 			MoBIEColoringModel< S > coloringModel )
 	{
 		this.segmentAdapter = segmentAdapter;
-		this.imageId = null;
+		this.imageId = null; // No imageId given => decode from pixel value
 		this.coloringModel = coloringModel;
 	}
 
@@ -80,48 +79,43 @@ public void convert( RealType label, ARGBType color )
 			}
 		}
 
-		if ( label.getRealDouble() == 0 )
+		if ( imageId == null )
 		{
-			color.set( 0 );
-			return;
-		}
+			final long labelId = SourceNameEncoder.getValue( ( VolatileUnsignedIntType ) label );
 
-		S imageSegment = getImageSegment( label );
+			if ( labelId == 0 )
+			{
+				color.set( 0 );
+				return;
+			}
 
-		if ( imageSegment == null )
-		{
-			color.set( 0 );
-			return;
+			final String imageId = SourceNameEncoder.getName( ( VolatileUnsignedIntType ) label );
+			S segment = segmentAdapter.getSegment( labelId, timePointIndex, imageId );
+			setColorBySegment( color, segment );
 		}
 		else
 		{
-			coloringModel.convert( imageSegment, color );
-			final int alpha = ARGBType.alpha( color.get() );
-			color.mul( alpha / 255.0 );
-		}
-
-		color.mul( opacity );
-	}
+			final double labelId = label.getRealDouble();
 
-	// TODO: figure out how to make this work for more types
-	private S getImageSegment( RealType label )
-	{
-		if ( imageId == null )
-		{
-			final long value = SourceNameEncoder.getValue( ( VolatileUnsignedIntType ) label );
-			if ( value == 0 )
+			if ( labelId == 0 )
 			{
-				return null; // background
+				color.set( 0 );
+				return;
 			}
-			final String imageId = SourceNameEncoder.getName( ( VolatileUnsignedIntType ) label );
-			return segmentAdapter.getSegmentCreateIfNotExist( value, timePointIndex, imageId );
-		}
-		else
-		{
-			return segmentAdapter.getSegmentCreateIfNotExist( label.getRealDouble(), timePointIndex, imageId );
+
+			final S segment = segmentAdapter.getSegment( labelId, timePointIndex, imageId );
+			setColorBySegment( color, segment );
 		}
 	}
 
+	private void setColorBySegment( ARGBType color, S imageSegment )
+	{
+		coloringModel.convert( imageSegment, color );
+		final int alpha = ARGBType.alpha( color.get() );
+		color.mul( alpha / 255.0 );
+		color.mul( opacity );
+	}
+
 	@Override
 	public void timePointChanged( int timePointIndex )
 	{
diff --git a/src/main/java/org/embl/mobie/viewer/project/PublishedProjectsCreator.java b/src/main/java/org/embl/mobie/viewer/project/PublishedProjectsCreator.java
index e6b892f9f..95413aed8 100644
--- a/src/main/java/org/embl/mobie/viewer/project/PublishedProjectsCreator.java
+++ b/src/main/java/org/embl/mobie/viewer/project/PublishedProjectsCreator.java
@@ -14,23 +14,25 @@ public PublishedProjectsCreator()
 		platybrowserProject.name = "Vergara et al. (2020) \"PlatyBrowser\", bioRxiv";
 		platybrowserProject.location = "https://github.com/mobie/platybrowser-datasets";
 		platybrowserProject.pulicationURL = "https://www.biorxiv.org/content/10.1101/2020.02.26.961037v1";
-
 		publishedProjects.put( platybrowserProject.name, platybrowserProject );
 
 		final PublishedProject covidTomoProject = new PublishedProject();
-		covidTomoProject.name = "Cortese et al. (2020) covid tomograms, Cell Host & Microbe";
+		covidTomoProject.name = "Cortese et al. (2020) Covid tomograms, Cell Host & Microbe";
 		covidTomoProject.location = "https://github.com/mobie/covid-tomo-datasets";
 		covidTomoProject.pulicationURL = "https://www.sciencedirect.com/science/article/pii/S193131282030620X";
-
 		publishedProjects.put( covidTomoProject.name, covidTomoProject );
 
 		final PublishedProject covidFIBProject = new PublishedProject();
-		covidFIBProject.name = "Cortese et al. (2020) covid FIB-SEM, Cell Host & Microbe";
+		covidFIBProject.name = "Cortese et al. (2020) Covid FIB-SEM, Cell Host & Microbe";
 		covidFIBProject.location = "https://github.com/mobie/covid-em-datasets";
 		covidFIBProject.pulicationURL = "https://www.sciencedirect.com/science/article/pii/S193131282030620X";
-
 		publishedProjects.put( covidFIBProject.name, covidFIBProject );
 
+		final PublishedProject spongeProject = new PublishedProject();
+		spongeProject.name = "Musser et al. (2021) Sponge FIB-SEM, Science";
+		spongeProject.location = "https://github.com/mobie/sponge-fibsem-project";
+		spongeProject.pulicationURL = "https://www.science.org/doi/abs/10.1126/science.abj2949";
+		publishedProjects.put( spongeProject.name, spongeProject );
 	}
 
 	public HashMap< String, PublishedProject > getPublishedProjects()
diff --git a/src/main/java/org/embl/mobie/viewer/segment/BdvSegmentSelector.java b/src/main/java/org/embl/mobie/viewer/segment/BdvSegmentSelector.java
index f17487519..e1b9ce222 100644
--- a/src/main/java/org/embl/mobie/viewer/segment/BdvSegmentSelector.java
+++ b/src/main/java/org/embl/mobie/viewer/segment/BdvSegmentSelector.java
@@ -5,8 +5,10 @@
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
 import ij.IJ;
+import net.imglib2.type.volatiles.VolatileUnsignedIntType;
 import org.embl.mobie.io.n5.source.LabelSource;
 import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.SourceNameEncoder;
 import org.embl.mobie.viewer.bdv.BdvMousePositionProvider;
 import org.embl.mobie.viewer.display.SegmentationSourceDisplay;
 import de.embl.cba.tables.tablerow.TableRowImageSegment;
@@ -25,8 +27,10 @@ public class BdvSegmentSelector implements Runnable
 {
 	private BdvHandle bdvHandle;
 	private boolean is2D;
-	private Supplier< Collection<SegmentationSourceDisplay> > segmentationDisplaySupplier;
+	private Supplier< Collection< SegmentationSourceDisplay > > segmentationDisplaySupplier;
 
+	// A segmentationDisplaySupplier is used such that the segmentation images can
+	// change during runtime.
 	public BdvSegmentSelector( BdvHandle bdvHandle, boolean is2D, Supplier< Collection< SegmentationSourceDisplay > > segmentationDisplaySupplier )
 	{
 		this.bdvHandle = bdvHandle;
@@ -36,7 +40,7 @@ public BdvSegmentSelector( BdvHandle bdvHandle, boolean is2D, Supplier< Collecti
 
 	public synchronized void clearSelection()
 	{
-		final Collection< SegmentationSourceDisplay > segmentationDisplays = segmentationDisplaySupplier.get();
+		final Collection< SegmentationSourceDisplay > segmentationDisplays = getCurrent();
 
 		for ( SegmentationSourceDisplay segmentationDisplay : segmentationDisplays )
 		{
@@ -44,6 +48,11 @@ public synchronized void clearSelection()
 		}
 	}
 
+	private Collection< SegmentationSourceDisplay > getCurrent()
+	{
+		return segmentationDisplaySupplier.get();
+	}
+
 	private synchronized void toggleSelectionAtMousePosition()
 	{
 		final BdvMousePositionProvider positionProvider = new BdvMousePositionProvider( bdvHandle );
@@ -54,52 +63,69 @@ private synchronized void toggleSelectionAtMousePosition()
 
 		for ( SegmentationSourceDisplay segmentationDisplay : segmentationDisplays )
 		{
-			final Collection< SourceAndConverter< ? > > displayedSourceAndConverters = segmentationDisplay.sourceNameToSourceAndConverter.values();
-			for ( SourceAndConverter< ? > displayedSourceAndConverter : displayedSourceAndConverters )
+			final Collection< SourceAndConverter< ? > > segmentationSourceAndConverters = segmentationDisplay.sourceNameToSourceAndConverter.values();
+
+			for ( SourceAndConverter< ? > sourceAndConverter : segmentationSourceAndConverters )
 			{
-				final boolean sourceVisible = bdvHandle.getViewerPanel().state().isSourceVisible( displayedSourceAndConverter );
-				if ( ! sourceVisible )
+				if ( ! bdvHandle.getViewerPanel().state().isSourceVisible( sourceAndConverter ) )
 				{
 					continue;
 				}
 
-				// The source might be a MergedGridSource and thereby represent several sources that
-				// need to be inspected for whether they contain selectable image segments.
-				// TODO: Probably more efficient and consistent to simplify this,
-				//   using the image name encoding instead.
-				final Collection< SourceAndConverter< ? > > sourceAndConverters = getContainedSourceAndConverters( displayedSourceAndConverter );
-
-				for ( SourceAndConverter< ? > sourceAndConverter : sourceAndConverters )
+				if ( SourceAndConverterHelper.isPositionWithinSourceInterval( sourceAndConverter, position, timePoint, is2D ) )
 				{
-					if ( SourceAndConverterHelper.isPositionWithinSourceInterval( sourceAndConverter, position, timePoint, is2D ) )
-					{
-						final Source< ? > source = sourceAndConverter.getSpimSource();
+					final Source< ? > source = sourceAndConverter.getSpimSource();
+
+					final double pixelValue = getPixelValue( timePoint, position, source );
+					final String sourceName = getSourceName( source, pixelValue );
+					double labelIndex = getLabelIndex( source, pixelValue );
 
-						final double labelIndex = getPixelValue( timePoint, position, source );
-						if ( labelIndex == 0 ) continue; // background
+					if ( labelIndex == 0 ) continue; // image background
 
-						final String sourceName = source.getName();
+					final boolean containsSegment = segmentationDisplay.segmentAdapter.containsSegment( labelIndex, timePoint, sourceName );
 
-						final boolean containsSegment = segmentationDisplay.segmentAdapter.containsSegment( labelIndex, timePoint, sourceName );
+					if ( ! containsSegment )
+					{
+						// This happens when there is a segmentation without
+						// a segment table
+						continue;
+					}
 
-						if ( ! containsSegment )
-						{
-							continue;
-						}
+					final TableRowImageSegment segment = segmentationDisplay.segmentAdapter.getSegment( labelIndex, timePoint, sourceName );
 
-						final TableRowImageSegment segment = segmentationDisplay.segmentAdapter.getSegment( labelIndex, timePoint, sourceName );
+					segmentationDisplay.selectionModel.toggle( segment );
 
-						segmentationDisplay.selectionModel.toggle( segment );
-						if ( segmentationDisplay.selectionModel.isSelected( segment ) )
-						{
-							segmentationDisplay.selectionModel.focus( segment );
-						}
+					if ( segmentationDisplay.selectionModel.isSelected( segment ) )
+					{
+						segmentationDisplay.selectionModel.focus( segment );
 					}
 				}
 			}
 		}
 	}
 
+	private static double getLabelIndex( Source< ? > source, double pixelValue )
+	{
+		if ( MergedGridSource.instanceOf( source ) )
+		{
+			return SourceNameEncoder.getValue( Double.valueOf( pixelValue ).longValue() );
+		}
+		else
+		{
+			return pixelValue;
+		}
+	}
+
+	private static String getSourceName( Source< ? > source, double labelIndex )
+	{
+		if ( MergedGridSource.instanceOf( source ) )
+		{
+			return SourceNameEncoder.getName( Double.valueOf( labelIndex ).longValue() );
+		}
+		final String sourceName = source.getName();
+		return sourceName;
+	}
+
 	private Collection< SourceAndConverter< ? > > getContainedSourceAndConverters( SourceAndConverter< ? > sourceAndConverter )
 	{
 		final Collection< SourceAndConverter< ? > > containedSourceAndConverters = new HashSet<>();
@@ -134,8 +160,7 @@ private static double getPixelValue( int timePoint, RealPoint position, Source<
 		final RandomAccess< RealType > randomAccess = ( RandomAccess< RealType > ) source.getSource( timePoint, 0 ).randomAccess();
 		final long[] positionInSource = SourceAndConverterHelper.getVoxelPositionInSource( source, position, timePoint, 0 );
 		randomAccess.setPosition( positionInSource );
-		final double labelIndex = randomAccess.get().getRealDouble();
-		return labelIndex;
+		return randomAccess.get().getRealDouble();
 	}
 
 	@Override
diff --git a/src/main/java/org/embl/mobie/viewer/transform/MergedGridSource.java b/src/main/java/org/embl/mobie/viewer/transform/MergedGridSource.java
index 30e6180a8..34181fe51 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/MergedGridSource.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/MergedGridSource.java
@@ -6,6 +6,7 @@
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
 import net.imglib2.type.numeric.integer.UnsignedIntType;
+import org.embl.mobie.io.n5.source.LabelSource;
 import org.embl.mobie.viewer.MoBIEUtils;
 import mpicbg.spim.data.sequence.VoxelDimensions;
 import net.imglib2.Cursor;
@@ -67,7 +68,17 @@ public MergedGridSource( List< Source< T > > gridSources, List< int[] > position
 
 	public static boolean instanceOf( SourceAndConverter< ? > sourceAndConverter )
 	{
-		return (( TransformedSource ) sourceAndConverter.getSpimSource() ).getWrappedSource() instanceof MergedGridSource || sourceAndConverter.getSpimSource() instanceof MergedGridSource;
+		final Source< ? > source = sourceAndConverter.getSpimSource();
+		return instanceOf( source );
+	}
+
+	public static boolean instanceOf( Source< ? > source )
+	{
+		if ( source instanceof LabelSource )  source = ( ( LabelSource ) source ).getWrappedSource();
+
+		final Source wrappedSource = ( ( TransformedSource ) source ).getWrappedSource();
+
+		return wrappedSource instanceof MergedGridSource || source instanceof MergedGridSource;
 	}
 
 	public List< Source< T > > getGridSources()
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 1f27b7ca1..f67b6bef5 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/MergedGridSourceTransformer.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/MergedGridSourceTransformer.java
@@ -5,12 +5,11 @@
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
 import de.embl.cba.tables.Logger;
-import ij.IJ;
-import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIEUtils;
 import net.imglib2.FinalRealInterval;
 import net.imglib2.converter.Converter;
 import net.imglib2.type.numeric.ARGBType;
+import org.embl.mobie.viewer.ThreadUtils;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -19,6 +18,7 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 import java.util.stream.Collectors;
 
 public class MergedGridSourceTransformer extends AbstractSourceTransformer
@@ -59,34 +59,27 @@ public void transform( Map< String, SourceAndConverter< ? > > sourceNameToSource
 
 	private void transformContainedSources( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, List< SourceAndConverter< ? > > gridSources )
 	{
-		final long start = System.currentTimeMillis();
-
 		final ArrayList< SourceAndConverter< ? > > referenceSources = new ArrayList<>();
 		referenceSources.add( gridSources.get( 0 ) );
 
 		final double[] gridCellRealDimensions = mergedGridSource.getCellRealDimensions();
 
-		// due to margin...
+		// account for grid margin
 		translationRealOffset = computeTranslationOffset( gridSources, gridCellRealDimensions );
 
-		final int nThreads = MoBIE.N_THREADS;
-		final ExecutorService executorService = Executors.newFixedThreadPool( nThreads );
-
 		final int numSources = gridSources.size();
+		final ArrayList< Future< ? > > futures = ThreadUtils.getFutures();
 		for ( int positionIndex = 0; positionIndex < numSources; positionIndex++ )
 		{
 			final int finalPositionIndex = positionIndex;
 
 			final ArrayList< String > sourceNamesAtGridPosition = getSourcesAtGridPosition( gridSources, finalPositionIndex );
 
-			executorService.execute( () -> {
+			futures.add( ThreadUtils.executorService.submit( () -> {
 				recursivelyTransformSources( sourceNameToSourceAndConverter, gridCellRealDimensions, finalPositionIndex, sourceNamesAtGridPosition );
-			} );
-
+			} ) );
 		}
-		MoBIEUtils.waitUntilFinishedAndShutDown( executorService );
-
-		IJ.log( "Transformed " + transformedSourceAndConverters.size() + " source(s) in " + (System.currentTimeMillis() - start) + " ms, using " + nThreads + " thread(s)." );
+		ThreadUtils.waitUntilFinished( futures );
 	}
 
 	private double[] computeTranslationOffset( List< SourceAndConverter< ? > > gridSources, double[] gridCellRealDimensions )
@@ -107,7 +100,10 @@ private double[] computeTranslationOffset( List< SourceAndConverter< ? > > gridS
 	private void recursivelyTransformSources( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, double[] gridCellRealDimensions, int finalPositionIndex, ArrayList< String > transformedSourceNames )
 	{
 		// transform the sources
-		TransformedGridSourceTransformer.translate( sourceNameToSourceAndConverter, transformedSourceNames, null, centerAtOrigin, gridCellRealDimensions[ 0 ] * positions.get( finalPositionIndex )[ 0 ] + translationRealOffset[ 0 ], gridCellRealDimensions[ 1 ] * positions.get( finalPositionIndex )[ 1 ] + translationRealOffset[ 1 ]);
+		final double translationX = gridCellRealDimensions[ 0 ] * positions.get( finalPositionIndex )[ 0 ] + translationRealOffset[ 0 ];
+		final double translationY = gridCellRealDimensions[ 1 ] * positions.get( finalPositionIndex )[ 1 ] + translationRealOffset[ 1 ];
+
+		TransformedGridSourceTransformer.translate( sourceNameToSourceAndConverter, transformedSourceNames, null, centerAtOrigin, translationX, translationY );
 		addTransformedSources( sourceNameToSourceAndConverter, transformedSourceNames );
 
 		// if there are any, also transform contained sources
@@ -160,7 +156,7 @@ public List< String > getSources()
 	{
 		mergedGridSource = new MergedGridSource( gridSources, positions, mergedGridSourceName, TransformedGridSourceTransformer.RELATIVE_CELL_MARGIN, encodeSource );
 
-		final VolatileSource< ?, ? > volatileMergedGridSource = new VolatileSource<>( mergedGridSource, MoBIE.sharedQueue );
+		final VolatileSource< ?, ? > volatileMergedGridSource = new VolatileSource<>( mergedGridSource, ThreadUtils.sharedQueue );
 
 		final SourceAndConverter< ? > volatileSourceAndConverter = new SourceAndConverter( volatileMergedGridSource, volatileConverter );
 
diff --git a/src/main/java/org/embl/mobie/viewer/transform/TransformedGridSourceTransformer.java b/src/main/java/org/embl/mobie/viewer/transform/TransformedGridSourceTransformer.java
index e32873ecd..1eaf7bb38 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/TransformedGridSourceTransformer.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/TransformedGridSourceTransformer.java
@@ -2,8 +2,7 @@
 
 import bdv.viewer.SourceAndConverter;
 import de.embl.cba.tables.Logger;
-import org.embl.mobie.viewer.MoBIE;
-import org.embl.mobie.viewer.MoBIEUtils;
+import org.embl.mobie.viewer.ThreadUtils;
 import org.embl.mobie.viewer.playground.SourceAffineTransformer;
 import net.imglib2.realtransform.AffineTransform3D;
 
@@ -12,6 +11,7 @@
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 
 public class TransformedGridSourceTransformer extends AbstractSourceTransformer
 {
@@ -47,27 +47,20 @@ public List< String > getSources()
 
 	private void transform( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, double[] cellRealDimensions )
 	{
-		final long start = System.currentTimeMillis();
-
-		final int nThreads = MoBIE.N_THREADS;
-		final ExecutorService executorService = Executors.newFixedThreadPool( nThreads );
-
 		final int numGridPositions = sources.size();
 
+		final ArrayList< Future< ? > > futures = ThreadUtils.getFutures();
 		for ( int gridIndex = 0; gridIndex < numGridPositions; gridIndex++ )
 		{
 			int finalGridIndex = gridIndex;
-			executorService.execute( () -> {
+			futures.add( ThreadUtils.executorService.submit( () -> {
 				if ( sourceNamesAfterTransform != null )
 					translate( sourceNameToSourceAndConverter, sources.get( finalGridIndex ), sourceNamesAfterTransform.get( finalGridIndex ), centerAtOrigin, cellRealDimensions[ 0 ] * positions.get( finalGridIndex )[ 0 ], cellRealDimensions[ 1 ] * positions.get( finalGridIndex )[ 1 ] );
 				else
 					translate( sourceNameToSourceAndConverter, sources.get( finalGridIndex ), null, centerAtOrigin, cellRealDimensions[ 0 ] * positions.get( finalGridIndex )[ 0 ], cellRealDimensions[ 1 ] * positions.get( finalGridIndex )[ 1 ] );
-			} );
+			} ) );
 		}
-
-		MoBIEUtils.waitUntilFinishedAndShutDown( executorService );
-
-		System.out.println( "Transformed " + sourceNameToSourceAndConverter.size() + " image source(s) in " + (System.currentTimeMillis() - start) + " ms, using " + nThreads + " thread(s)." );
+		ThreadUtils.waitUntilFinished( futures );
 	}
 
 	public static void translate( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, List< String > sourceNames, List< String > sourceNamesAfterTransform, boolean centerAtOrigin, double translationX, double translationY )


Test patch:
diff --git a/src/test/java/projects/OpenRemoteCovidScreen.java b/src/test/java/projects/OpenRemoteCovidScreen.java
index 93f52460d..5ad585e6b 100644
--- a/src/test/java/projects/OpenRemoteCovidScreen.java
+++ b/src/test/java/projects/OpenRemoteCovidScreen.java
@@ -16,7 +16,7 @@ public static void main( String[] args )
 
 		try {
 			new MoBIE("https://github.com/mobie/covid-if-project",
-					MoBIESettings.settings().gitProjectBranch( "main" ).imageDataFormat( ImageDataFormat.OmeZarrS3 ) );
+					MoBIESettings.settings().gitProjectBranch( "main" ).imageDataFormat( ImageDataFormat.OmeZarrS3 ).view( "single_image" ) );
 		} catch (IOException e) {
 			e.printStackTrace();
 		}
diff --git a/src/test/java/projects/OpenRemotePlankton.java b/src/test/java/projects/OpenRemotePlankton.java
new file mode 100644
index 000000000..fd5b2ac65
--- /dev/null
+++ b/src/test/java/projects/OpenRemotePlankton.java
@@ -0,0 +1,21 @@
+package projects;
+
+import net.imagej.ImageJ;
+import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.MoBIESettings;
+
+import java.io.IOException;
+
+public class OpenRemotePlankton
+{
+	public static void main( String[] args )
+	{
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+		try {
+			new MoBIE("https://s3.embl.de/plankton-fibsem", MoBIESettings.settings());
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+	}
+}
diff --git a/src/test/java/tests/TestRemotePlanktonS3.java b/src/test/java/tests/TestRemotePlanktonS3.java
new file mode 100644
index 000000000..a3111de1c
--- /dev/null
+++ b/src/test/java/tests/TestRemotePlanktonS3.java
@@ -0,0 +1,39 @@
+package tests;
+
+import net.imagej.ImageJ;
+import org.embl.mobie.viewer.MoBIE;
+import org.embl.mobie.viewer.MoBIESettings;
+import org.embl.mobie.viewer.display.SegmentationSourceDisplay;
+import org.embl.mobie.viewer.source.ImageDataFormat;
+import org.embl.mobie.viewer.view.View;
+import org.embl.mobie.viewer.view.additionalviews.AdditionalViewsLoader;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class TestRemotePlanktonS3
+{
+	public static void main( String[] args ) throws IOException
+	{
+		new TestRemotePlanktonS3().run();
+	}
+
+	@Test
+	public void run() throws IOException
+	{
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+
+		// This is special as it does not go via github but
+		// all information is on S3
+		final MoBIE moBIE = new MoBIE("https://s3.embl.de/plankton-fibsem", MoBIESettings.settings());
+
+		// Check all views
+		final Map< String, View > views = moBIE.getViews();
+		for ( View view : views.values() )
+		{
+			moBIE.getViewManager().show( view );
+		}
+	}
+}
diff --git a/src/test/java/tests/TestRemoteZebrafish.java b/src/test/java/tests/TestRemoteZebrafish.java
index d9bb2d69c..69fc46aa5 100644
--- a/src/test/java/tests/TestRemoteZebrafish.java
+++ b/src/test/java/tests/TestRemoteZebrafish.java
@@ -1,17 +1,14 @@
 package tests;
 
 import net.imagej.ImageJ;
-import net.imagej.patcher.LegacyInjector;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIESettings;
 import org.embl.mobie.viewer.display.SegmentationSourceDisplay;
 import org.embl.mobie.viewer.source.ImageDataFormat;
-import org.embl.mobie.viewer.view.View;
 import org.embl.mobie.viewer.view.additionalviews.AdditionalViewsLoader;
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
-import java.util.Map;
 
 public class TestRemoteZebrafish
 {