Skip to content

Commit

Permalink
Merge pull request #1064 from mobie/omeZarrHcsS3
Browse files Browse the repository at this point in the history
Read OME-Zarr HCS from S3
  • Loading branch information
tischi authored Nov 23, 2023
2 parents 6bc8164 + 573e537 commit 711da38
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 71 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>org.scijava</groupId>
<artifactId>pom-scijava</artifactId>
<version>37.0.0</version>
<version>36.0.0</version>
</parent>

<groupId>org.embl.mobie</groupId>
Expand Down
32 changes: 6 additions & 26 deletions src/main/java/org/embl/mobie/MoBIE.java
Original file line number Diff line number Diff line change
Expand Up @@ -390,31 +390,6 @@ private void setProjectImageAndTableRootLocations( )
}
}

private String getLog( AtomicInteger dataSetIndex, int numTotal, AtomicInteger dataSetLoggingInterval, AtomicLong lastLogMillis )
{
final int currentDatasetIndex = dataSetIndex.incrementAndGet();

if ( currentDatasetIndex % dataSetLoggingInterval.get() == 0 )
{
// Update logging frequency
// such that a message appears
// approximately every 5000 ms
final long currentTimeMillis = System.currentTimeMillis();
if ( currentTimeMillis - lastLogMillis.get() < 4000 )
dataSetLoggingInterval.set( Math.max( 1, dataSetLoggingInterval.get() * 2 ) );
else if ( currentTimeMillis - lastLogMillis.get() > 6000 )
dataSetLoggingInterval.set( Math.max( 1, dataSetLoggingInterval.get() / 2 ) );
lastLogMillis.set( currentTimeMillis );

// Return log message
return "Initialising (" + currentDatasetIndex + "/" + numTotal + "): ";
}
else
{
return null;
}
}

private void openAndViewDataset( String datasetName, String viewName ) throws IOException
{
IJ.log("Opening dataset: " + datasetName );
Expand Down Expand Up @@ -665,7 +640,7 @@ public void initDataSources( Collection< DataSource > dataSources )
futures.add(
ThreadHelper.ioExecutorService.submit( () ->
{
String log = getLog( sourceIndex, numImages, sourceLoggingModulo, lastLogMillis );
String log = MoBIEHelper.getLog( sourceIndex, numImages, sourceLoggingModulo, lastLogMillis );
initDataSource( dataSource, log );
}
) );
Expand Down Expand Up @@ -761,6 +736,11 @@ private void initDataSource( DataSource dataSource, String log )
return new SpimDataImage( ImageDataFormat.OmeZarr, site.absolutePath, site.channel, name, ThreadHelper.sharedQueue, false );
}

if ( site.getImageDataFormat().equals( ImageDataFormat.OmeZarrS3 ) )
{
return new SpimDataImage( ImageDataFormat.OmeZarrS3, site.absolutePath, site.channel, name, ThreadHelper.sharedQueue, false );
}

return new SpimDataImage( site, name, ThreadHelper.sharedQueue );
}

Expand Down
31 changes: 29 additions & 2 deletions src/main/java/org/embl/mobie/lib/MoBIEHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -262,11 +264,11 @@ public static Metadata getMetadataFromSource( ImageDataSource imageDataSource )
return metadata;
}

public static ImagePlus openOMEZarrAsImagePlus( String path, int setupID )
public static ImagePlus openAsImagePlus( String path, int setupID, ImageDataFormat imageDataFormat )
{
try
{
AbstractSpimData< ? > spimData = new SpimDataOpener().open( path, ImageDataFormat.OmeZarr );
AbstractSpimData< ? > spimData = new SpimDataOpener().open( path, imageDataFormat );
final SpimSource< ? > spimSource = new SpimSource( spimData, setupID, "" );
final ImagePlus imagePlus = new SourceToImagePlusConverter<>( spimSource ).getImagePlus( 0 );
return imagePlus;
Expand Down Expand Up @@ -301,6 +303,31 @@ public static String createPath( String rootLocation, String githubBranch, Strin
return path;
}

public static String getLog( AtomicInteger dataSetIndex, int numTotal, AtomicInteger dataSetLoggingInterval, AtomicLong lastLogMillis )
{
final int currentDatasetIndex = dataSetIndex.incrementAndGet();

if ( currentDatasetIndex % dataSetLoggingInterval.get() == 0 )
{
// Update logging frequency
// such that a message appears
// approximately every 5000 ms
final long currentTimeMillis = System.currentTimeMillis();
if ( currentTimeMillis - lastLogMillis.get() < 4000 )
dataSetLoggingInterval.set( Math.max( 1, dataSetLoggingInterval.get() * 2 ) );
else if ( currentTimeMillis - lastLogMillis.get() > 6000 )
dataSetLoggingInterval.set( Math.max( 1, dataSetLoggingInterval.get() / 2 ) );
lastLogMillis.set( currentTimeMillis );

// Return log message
return "Initialising (" + currentDatasetIndex + "/" + numTotal + "): ";
}
else
{
return null;
}
}

public enum FileLocation
{
Project,
Expand Down
88 changes: 46 additions & 42 deletions src/main/java/org/embl/mobie/lib/hcs/Plate.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@
import org.embl.mobie.io.ImageDataFormat;
import org.embl.mobie.io.SpimDataOpener;
import org.embl.mobie.io.toml.TPosition;
import org.embl.mobie.io.util.IOHelper;
import org.embl.mobie.lib.MoBIEHelper;
import org.embl.mobie.lib.color.ColorHelper;

import ch.epfl.biop.bdv.img.bioformats.entity.SeriesIndex;
import org.embl.mobie.lib.hcs.omezarr.OMEZarrHCSHelper;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -74,6 +75,7 @@ public class Plate
private ImageDataFormat imageDataFormat;
private OperettaMetadata operettaMetadata;
private AbstractSpimData< ? > spimDataPlate;
private boolean siteIDsAreOneBased = true;


public Plate( String hcsDirectory ) throws IOException
Expand All @@ -83,48 +85,23 @@ public Plate( String hcsDirectory ) throws IOException
// FIXME: fetch operetta paths from XML?!
// FIXME: fetch OME-Zarr paths entry point JSON?!
List< String > imageSitePaths;

if ( hcsDirectory.endsWith( ".zarr" ) )
{
hcsPattern = HCSPattern.OMEZarr;
imageDataFormat = ImageDataFormat.OmeZarr;

final int minDepth = 3;
final int maxDepth = 3;
final Path rootPath = Paths.get(hcsDirectory);
final int rootPathDepth = rootPath.getNameCount();
imageSitePaths = Files.walk( rootPath, maxDepth )
.filter( e -> e.toFile().isDirectory() )
.filter( e -> e.getNameCount() - rootPathDepth >= minDepth )
.map( e -> e.toString() )
.collect( Collectors.toList() );
}
else
{
imageSitePaths = Files.walk( Paths.get( hcsDirectory ), 3 )
.map( p -> p.toString() )
.collect( Collectors.toList() );
hcsPattern = determineHCSPattern( hcsDirectory, imageSitePaths );
imageSitePaths = imageSitePaths.stream()
.filter( path -> hcsPattern.setMatcher( path ) ) // skip files like .DS_Store a.s.o.
.collect( Collectors.toList() );
}

if ( hcsPattern == HCSPattern.Operetta )
{
//final File xml = new File( hcsDirectory, "Index.idx.xml" );
final File xml = new File( hcsDirectory, "Index.xml" );
operettaMetadata = new OperettaMetadata( xml );
imageSitePaths = imageSitePaths.stream()
.filter( path -> operettaMetadata.contains( path ) ) // skip files like .DS_Store a.s.o.
.collect( Collectors.toList() );
if ( IOHelper.getType( hcsDirectory ).equals( IOHelper.ResourceType.S3 ) )
imageDataFormat = ImageDataFormat.OmeZarrS3;
else
imageDataFormat = ImageDataFormat.OmeZarr;

}
else if ( hcsPattern == HCSPattern.OMEZarr )
{
imageSitePaths = OMEZarrHCSHelper.sitePathsFromMetadata( hcsDirectory );

// determine the number of channels
try
{
final String firstImagePath = imageSitePaths.get( 0 );
AbstractSpimData< ? > spimData = new SpimDataOpener().open( firstImagePath, ImageDataFormat.OmeZarr );
AbstractSpimData< ? > spimData = new SpimDataOpener().open( firstImagePath, imageDataFormat );
final int numChannels = spimData.getSequenceDescription().getViewSetupsOrdered().size();
final List< String > channels = IntStream.range( 0, numChannels )
.mapToObj( i -> ( ( Integer ) i ).toString() )
Expand All @@ -136,6 +113,27 @@ else if ( hcsPattern == HCSPattern.OMEZarr )
throw new RuntimeException( e );
}
}
else
{
imageSitePaths = Files.walk( Paths.get( hcsDirectory ), 3 )
.map( p -> p.toString() )
.collect( Collectors.toList() );
hcsPattern = determineHCSPattern( hcsDirectory, imageSitePaths );
imageSitePaths = imageSitePaths.stream()
.filter( path -> hcsPattern.setMatcher( path ) ) // skip files like .DS_Store a.s.o.
.collect( Collectors.toList() );

if ( hcsPattern == HCSPattern.Operetta )
{
// only keep paths that are also in the XML
//final File xml = new File( hcsDirectory, "Index.idx.xml" );
final File xml = new File( hcsDirectory, "Index.xml" );
operettaMetadata = new OperettaMetadata( xml );
imageSitePaths = imageSitePaths.stream()
.filter( path -> operettaMetadata.contains( path ) ) // skip files like .DS_Store a.s.o.
.collect( Collectors.toList() );
}
}

buildPlateMap( imageSitePaths );
}
Expand All @@ -145,7 +143,7 @@ private void buildPlateMap( List< String > paths )
channelWellSites = new HashMap<>();
tPositions = new HashSet<>();

IJ.log("Parsing " + paths.size() + " HCS image paths...");
IJ.log("Parsing " + paths.size() + " sites...");

for ( String path : paths )
{
Expand Down Expand Up @@ -257,6 +255,8 @@ private void buildPlateMap( List< String > paths )
site.setDimensions( siteDimensions );
site.setVoxelDimensions( voxelDimensions );
channelWellSites.get( channel ).get( well ).add( site );
if ( Integer.parseInt( site.getId() ) == 0 )
siteIDsAreOneBased = false; // zero based
final int numSites = channelWellSites.get( channel ).get( well ).size();
if ( numSites > sitesPerWell )
sitesPerWell = numSites; // needed to compute the site position within a well
Expand All @@ -278,23 +278,24 @@ private void buildPlateMap( List< String > paths )
}

IJ.log( "Initialised HCS plate: " + getName() );
IJ.log( "Images: " + paths.size() );
IJ.log( "Channels: " + channelWellSites.keySet().size() );
IJ.log( "Wells: " + wellsPerPlate );
IJ.log( "Sites per well: " + sitesPerWell );
IJ.log( "Sites: " + paths.size() );
IJ.log( "Channels: " + channelWellSites.keySet().size() );
}

private ImagePlus openImagePlus( String path, String channelName )
{
if ( imageDataFormat.equals( ImageDataFormat.Tiff ) )
if ( this.imageDataFormat.equals( ImageDataFormat.Tiff ) )
{
final File file = new File( path );
return ( new Opener() ).openTiff( file.getParent(), file.getName() );
}
else if ( imageDataFormat.equals( ImageDataFormat.OmeZarr ) )
else if ( this.imageDataFormat.equals( ImageDataFormat.OmeZarr )
|| this.imageDataFormat.equals( ImageDataFormat.OmeZarrS3 ) )
{
final int setupID = Integer.parseInt( channelName );
return MoBIEHelper.openOMEZarrAsImagePlus( path, setupID );
return MoBIEHelper.openAsImagePlus( path, setupID, imageDataFormat );
}
else
{
Expand Down Expand Up @@ -416,7 +417,10 @@ public int[] getDefaultGridPosition( Site site )
if ( sitesPerWell == 1 )
return new int[]{ 0, 0 };

int siteIndex = Integer.parseInt( site.getId() ) - 1;
// TODO: not obvious that the ID can be parsed to an Integer here
int siteIndex = Integer.parseInt( site.getId() );
if ( siteIDsAreOneBased )
siteIndex -= 1;
int numSiteColumns = (int) Math.sqrt( sitesPerWell );

int[] gridPosition = new int[ 2 ];
Expand Down
96 changes: 96 additions & 0 deletions src/main/java/org/embl/mobie/lib/hcs/omezarr/OMEZarrHCSHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.embl.mobie.lib.hcs.omezarr;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import ij.IJ;
import org.embl.mobie.io.util.IOHelper;
import org.embl.mobie.lib.MoBIEHelper;
import org.embl.mobie.lib.ThreadHelper;
import org.embl.mobie.lib.serialize.JsonHelper;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

public class OMEZarrHCSHelper
{
public static final String ZATTRS = "/.zattrs";

public static List< String > sitePathsFromFolderStructure( String hcsDirectory ) throws IOException
{
List< String > imageSitePaths;
final int minDepth = 3;
final int maxDepth = 3;
final Path rootPath = Paths.get( hcsDirectory );
final int rootPathDepth = rootPath.getNameCount();
imageSitePaths = Files.walk( rootPath, maxDepth )
.filter( e -> e.toFile().isDirectory() )
.filter( e -> e.getNameCount() - rootPathDepth >= minDepth )
.map( e -> e.toString() )
.collect( Collectors.toList() );
return imageSitePaths;
}

public static List< String > sitePathsFromMetadata( String hcsDirectory ) throws IOException
{
List< String > imageSitePaths = new CopyOnWriteArrayList<>();
Gson gson = JsonHelper.buildGson(false);

String plateUri = hcsDirectory;
final String plateJson = IOHelper.read( plateUri + ZATTRS );
//System.out.println( plateJson );
HCSMetadata hcsMetadata = gson.fromJson(plateJson, new TypeToken< HCSMetadata >() {}.getType());

int numWells = hcsMetadata.plate.wells.size();
// lots of code for nice logging...
AtomicInteger wellIndex = new AtomicInteger(0);
AtomicInteger sourceLoggingModulo = new AtomicInteger(1);
AtomicLong lastLogMillis = new AtomicLong( System.currentTimeMillis() );
final long startTime = System.currentTimeMillis();
IJ.log( "Parsing " + numWells + " wells..." );

ArrayList< Future< ? > > futures = ThreadHelper.getFutures();
for ( Well well : hcsMetadata.plate.wells )
{
futures.add(
ThreadHelper.ioExecutorService.submit( () ->
{
String log = MoBIEHelper.getLog( wellIndex, numWells, sourceLoggingModulo, lastLogMillis );
if ( log != null )
IJ.log( log + well.path );

String wellUri = IOHelper.combinePath( plateUri, well.path);
final String wellJson;
try
{
wellJson = IOHelper.read( wellUri + ZATTRS );
} catch ( IOException e )
{
throw new RuntimeException( e );
}
WellMetadata wellMetadata = gson.fromJson( wellJson, new TypeToken< WellMetadata >() {}.getType() );

for ( Image image : wellMetadata.well.images )
{
String imageUri = IOHelper.combinePath( wellUri, image.path );
imageSitePaths.add( imageUri );
}
}
) );
}

ThreadHelper.waitUntilFinished( futures );

IJ.log( "Parsed " + numWells + " wells in " + (System.currentTimeMillis() - startTime) + " ms, using up to " + ThreadHelper.getNumIoThreads() + " thread(s).");

return imageSitePaths;
}
}
Loading

0 comments on commit 711da38

Please sign in to comment.