Skip to content
Merged
9 changes: 5 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,11 @@ group first, then the array's own `dimension_names`) – and leaves three `prote
reader-specific steps: `readMultiscale`, `tryReadLevelFromParent` and `tryReadArrayNodeOnly` (the two `try*` hooks
return `null` for "not applicable, try the next"; the last one throws `NotAMultiscaleImageException` when there is no
array at all, so a missing location is never reported as unknown axes). `read` and `readSingleArray` are `final`, so
the order is fixed for every backend. Both `PyramidContents` and the per-level `CachedCellImg`s, transforms, calibration
and optional OMERO metadata it holds are immutable. Each backend also exposes a static `readPyramid(URI)` convenience
entry point for outside API users — not named `read`, because Java forbids a static method hiding an inherited instance
method.
the order is fixed for every backend. `PyramidContents` is immutable except for pixel data (issue #114): accessors hand
out copies or unmodifiable views, but the lazily loaded `CachedCellImg`s are shared, since copying one would read the
whole dataset — this matters because `OmeZarr` caches one instance for every opener, third-party ones included. Each
backend also exposes a static `readPyramid(URI)` convenience entry point for outside API users — not named `read`,
because Java forbids a static method hiding an inherited instance method.

**A too-old reader library is reported, not thrown at the console.** A Fiji-Stable installation whose N5 stack predates
this plugin fails inside the backend with a `NoClassDefFoundError` (e.g. on `OmeNgffMetadataParser`), which is useless
Expand Down
8 changes: 5 additions & 3 deletions doc/DEVELOPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,14 @@ public interface PyramidBackend
}
```

`PyramidContents<T>` is what you get back, and it is immutable — public final fields, no setters:
`PyramidContents<T>` is what you get back, and it is immutable — accessors only, no setters. The one exception is
pixel data: every `asImg(…)` call returns the same lazily loaded image, so pixel writes show up for every user and
live in memory only — they may be lost when the cache frees memory.

| | |
|------------------------|----------------------------------------------------------------------------------------------------------------------------------------|
| **Pixel data** | `cachedCellImgs[]` (one per resolution level), `type`, `name` |
| **Visualisation data** | `transforms[]`, `axesPerLevel[][]`, `omero`, `hasPlaceholderCalibration` |
| **Pixel data** | `asImg(int)` (one per resolution level) · `type()` · `name()` |
| **Visualisation data** | `transform(int)` · `axes(int)` · `omero()` · `hasPlaceholderCalibration()` |
| **Levels** | `asImg()` (= level 0) · `asLargestImg()` · `asSmallestImg()` · `asImg(int)` · `numResolutionLevels()` · `smallestResolutionLevel()` |
| **Level selection** | `suggestResolutionLevel(Integer preferredMaxWidth)` |
| **Shape by axis name** | `hasAxis("z")` · `axisIndex("c")` · `sizeAlongAxis("t")` · `numDimensions()` · `numChannels()` · `numTimepoints()` · `channelLabels()` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ static < T extends NativeType< T > & RealType< T > > String describeLevel( final
sb.append( ", c=" ).append( contents.sizeAlongAxis( AxisCalibration.C, level ) );
if ( contents.hasAxis( AxisCalibration.T ) )
sb.append( ", t=" ).append( contents.sizeAlongAxis( AxisCalibration.T, level ) );
return sb.append( ", " ).append( dataTypeName( contents.type ) )
return sb.append( ", " ).append( dataTypeName( contents.type() ) )
.append( ", slice " ).append( ImageSizes.formatPixels( contents.numXYSlicePixels( level ) ) )
.append( " ~ " ).append( ImageSizes.formatBytes( contents.uncompressedXYSliceBytes( level ) ) )
.append( ", total " ).append( ImageSizes.formatBytes( contents.uncompressedBytes( level ) ) )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,35 +64,35 @@ public static void main( final String[] args ) throws Exception

private static void printInfo( final PyramidContents< ? > contents )
{
final Omero omero = contents.omero;
final Omero omero = contents.omero();
if ( omero == null )
{
System.out.println( "OMERO metadata : not available" );
}
else
{
System.out.println( "--- OMERO metadata ---" );
System.out.println( " id : " + omero.id );
System.out.println( " name : " + omero.name );
System.out.println( " id : " + omero.id() );
System.out.println( " name : " + omero.name() );

if ( omero.rdefs != null )
if ( omero.rdefs() != null )
{
System.out.println( " rdefs.defaultT : " + omero.rdefs.defaultT );
System.out.println( " rdefs.defaultZ : " + omero.rdefs.defaultZ );
System.out.println( " rdefs.model : " + omero.rdefs.model );
System.out.println( " rdefs.defaultT : " + omero.rdefs().defaultT() );
System.out.println( " rdefs.defaultZ : " + omero.rdefs().defaultZ() );
System.out.println( " rdefs.model : " + omero.rdefs().model() );
}

if ( omero.channels != null )
if ( omero.channels() != null )
{
System.out.println( " channels (" + omero.channels.size() + "):" );
for ( int i = 0; i < omero.channels.size(); i++ )
System.out.println( " channels (" + omero.channels().size() + "):" );
for ( int i = 0; i < omero.channels().size(); i++ )
{
final Omero.Channel ch = omero.channels.get( i );
final Omero.Channel ch = omero.channels().get( i );
System.out.printf( " [%d] label=%-20s active=%-5b color=%s%n",
i, ch.label, ch.active, ch.color );
if ( ch.window != null )
i, ch.label(), ch.active(), ch.color() );
if ( ch.window() != null )
System.out.printf( " window: start=%.1f end=%.1f min=%.1f max=%.1f%n",
ch.window.start, ch.window.end, ch.window.min, ch.window.max );
ch.window().start(), ch.window().end(), ch.window().min(), ch.window().max() );
}
}
}
Expand Down
13 changes: 7 additions & 6 deletions ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public List< SourceAndConverter< T > > asSources()

public String getName()
{
return contents.name;
return contents.name();
}

@SuppressWarnings( "unchecked" )
Expand All @@ -103,15 +103,16 @@ List< SourceAndConverter< T > > initSourceAndConverters( final PyramidContents<
{
final int nLevels = contents.numResolutionLevels();
final int numChannels = contents.numChannels();
final T type = contents.type;
final AffineTransform3D[] mipmapTransforms = contents.transforms;
final T type = contents.type();
final AffineTransform3D[] mipmapTransforms = new AffineTransform3D[ nLevels ];
Arrays.setAll( mipmapTransforms, contents::transform );
final VoxelDimensions voxelDimensions = voxelDimensions( contents );

final RandomAccessibleInterval< V >[] volatileImgs = createVolatileImgs( contents );
final V volatileType = volatileImgs[ 0 ].getType();

final RandomAccessibleInterval< T >[][] levelToChannels = new RandomAccessibleInterval[ nLevels ][];
Arrays.setAll( levelToChannels, level -> splitInputStackIntoSourceStacks( contents, contents.cachedCellImgs[ level ] ) );
Arrays.setAll( levelToChannels, level -> splitInputStackIntoSourceStacks( contents, contents.asImg( level ) ) );

final RandomAccessibleInterval< V >[][] levelToVolatileChannels = new RandomAccessibleInterval[ nLevels ][];
Arrays.setAll( levelToVolatileChannels, level -> splitInputStackIntoSourceStacks( contents, volatileImgs[ level ] ) );
Expand Down Expand Up @@ -149,7 +150,7 @@ private static VoxelDimensions voxelDimensions( final PyramidContents< ? > conte
double yScale = 1.0;
double zScale = 1.0;
String unit = "";
for ( final AxisCalibration axis : contents.axesPerLevel[ 0 ] )
for ( final AxisCalibration axis : contents.axes( 0 ) )
{
switch ( axis.name )
{
Expand Down Expand Up @@ -180,7 +181,7 @@ RandomAccessibleInterval< V >[] createVolatileImgs( final PyramidContents< T > c
final SharedQueue sharedQueue = new SharedQueue( Math.max( 1, Runtime.getRuntime().availableProcessors() / 2 ) );
final RandomAccessibleInterval< V >[] volatileImgs = new RandomAccessibleInterval[ contents.numResolutionLevels() ];
for ( int level = 0; level < contents.numResolutionLevels(); level++ )
volatileImgs[ level ] = VolatileViews.wrapAsVolatile( contents.cachedCellImgs[ level ], sharedQueue );
volatileImgs[ level ] = VolatileViews.wrapAsVolatile( contents.asImg( level ), sharedQueue );
return volatileImgs;
}

Expand Down
13 changes: 7 additions & 6 deletions ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalDataset.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.scijava.Context;
Expand Down Expand Up @@ -79,7 +80,7 @@ public < T extends NativeType< T > & RealType< T > > PyramidalDataset( final Con
this.resolutionLevel = resolutionLevel;
setRGBMerged( false );
if ( contents.numResolutionLevels() > 1 )
setName( multiResolutionName( contents.name, resolutionLevel, contents.numResolutionLevels() ) );
setName( multiResolutionName( contents.name(), resolutionLevel, contents.numResolutionLevels() ) );
}

public int resolutionLevel()
Expand Down Expand Up @@ -136,12 +137,12 @@ private static < T extends NativeType< T > & RealType< T > > ImgPlus< T > create
" (numResolutionLevels = " + contents.numResolutionLevels() + ")" );
}

final AxisCalibration[] selectedAxes = contents.axesPerLevel[ resolutionLevel ];
final ImgPlus< T > imgPlus = new ImgPlus<>( contents.cachedCellImgs[ resolutionLevel ], contents.name );
for ( int i = 0; i < selectedAxes.length; i++ )
final List< AxisCalibration > selectedAxes = contents.axes( resolutionLevel );
final ImgPlus< T > imgPlus = new ImgPlus<>( contents.asImg( resolutionLevel ), contents.name() );
for ( int i = 0; i < selectedAxes.size(); i++ )
{
final AxisType axisType = AXIS_TYPE_MAP.getOrDefault( selectedAxes[ i ].name, Axes.unknown() );
imgPlus.setAxis( new DefaultLinearAxis( axisType, selectedAxes[ i ].unit, selectedAxes[ i ].scale ), i );
final AxisType axisType = AXIS_TYPE_MAP.getOrDefault( selectedAxes.get( i ).name, Axes.unknown() );
imgPlus.setAxis( new DefaultLinearAxis( axisType, selectedAxes.get( i ).unit, selectedAxes.get( i ).scale ), i );
}
return imgPlus;
}
Expand Down
6 changes: 3 additions & 3 deletions ome-zarr-fiji/src/main/java/ome/zarr/fiji/read/OmeZarr.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
* BigDataViewer displays all levels and streams them lazily.
* <p>
* Independently of the width, every display path refuses to show an image whose
* calibration is a placeholder ({@link PyramidContents#hasPlaceholderCalibration})
* calibration is a placeholder ({@link PyramidContents#hasPlaceholderCalibration()})
* unless the user confirms.
*/
public class OmeZarr
Expand Down Expand Up @@ -267,7 +267,7 @@ public PyramidalDataset showInImageJ()

/**
* Whether the image must not be shown: {@code true} only when its calibration is
* a placeholder (see {@link PyramidContents#hasPlaceholderCalibration}) and the
* a placeholder (see {@link PyramidContents#hasPlaceholderCalibration()}) and the
* user declined to open it anyway.
* <p>
* Every display path asks this, because the invented scale and unit belong to
Expand All @@ -276,7 +276,7 @@ public PyramidalDataset showInImageJ()
*/
private boolean uncalibratedAndDeclined( final PyramidContents< ? > contents )
{
if ( !contents.hasPlaceholderCalibration )
if ( !contents.hasPlaceholderCalibration() )
return false;
final boolean openAnyway = openAnywayConfirmation.test( uncalibratedMessage() );
logger.info( "{} has no scale or unit of its own. Opening it anyway: {}.", inputUri, openAnyway );
Expand Down
41 changes: 21 additions & 20 deletions ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/BdvUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ public static BdvHandle show( final PyramidalBdv< ? > pyramidalBdv, final BdvHan
shownIn.getViewerPanel().putClientProperty( BDV_HANDLE_KEY, shownIn );

if ( bdvHandle == null )
setTimepoint( pyramidContents.omero, shownIn.getViewerPanel().state() );
final List< Omero.Channel > omeroChannels = omeroChannels( pyramidContents.omero, pyramidalBdv.asSources().size() );
setTimepoint( pyramidContents.omero(), shownIn.getViewerPanel().state() );
final List< Omero.Channel > omeroChannels = omeroChannels( pyramidContents.omero(), pyramidalBdv.asSources().size() );
setChannelProperties( omeroChannels, pyramidalBdv.asSources(), shownIn.getConverterSetups(), shownIn.getViewerPanel().state() );

Container topLevelContainer = shownIn.getViewerPanel().getRootPane().getParent();
Expand Down Expand Up @@ -213,15 +213,16 @@ private static boolean isCurrentSource( final PyramidalBdv< ? > pyramidalBdv, fi
}

/**
* Colors each of {@code sources} the way the dataset's OMERO metadata asks
* for, gives it that channel's display range, and shows or hides it.
* Colors each of {@code sources} the way encoded in the OMERO channel metadata.
* Sets each source's display range and bounds to what the metadata asks for.
* Shows or hides each source according to the metadata.
* <p>
* @param omeroChannels the channels to apply. Nothing is applied if it is empty
* @param omeroChannels the list of OMERO channels to apply, one per source and in source order
* @param sources the viewer's sources, one per channel and in channel order
* @param converterSetups the viewer's converter setups, to look {@code sources} up in
* @param converterSetups the viewer's converter setups to look {@code sources} up in
* @param state the viewer state that decides which sources are visible
*/
private static void setChannelProperties( final List< Omero.Channel > omeroChannels,
public static void setChannelProperties( final List< Omero.Channel > omeroChannels,
final List< ? extends SourceAndConverter< ? > > sources,
final ConverterSetups converterSetups, final ViewerState state )
{
Expand All @@ -233,8 +234,8 @@ private static void setChannelProperties( final List< Omero.Channel > omeroChann
final Omero.Channel omeroChannel = omeroChannels.get( channelNumber );
final ConverterSetup converterSetup = converterSetups.getConverterSetup( source );
converterSetup.setColor( channelColor( omeroChannel ) );
if ( omeroChannel != null && omeroChannel.window != null )
setDisplayRangeAndBounds( converterSetup, converterSetups.getBounds(), omeroChannel.window );
if ( omeroChannel != null && omeroChannel.window() != null )
setDisplayRangeAndBounds( converterSetup, converterSetups.getBounds(), omeroChannel.window() );
state.setSourceActive( source, isChannelActive( omeroChannel ) );
}
}
Expand All @@ -254,9 +255,9 @@ private static void setChannelProperties( final List< Omero.Channel > omeroChann
private static void setDisplayRangeAndBounds( final ConverterSetup converterSetup,
final ConverterSetupBounds converterSetupBounds, final Omero.Channel.Window window )
{
converterSetup.setDisplayRange( window.start, window.end );
if ( window.max > window.min )
converterSetupBounds.setBounds( converterSetup, new Bounds( window.min, window.max ) );
converterSetup.setDisplayRange( window.start(), window.end() );
if ( window.max() > window.min() )
converterSetupBounds.setBounds( converterSetup, new Bounds( window.min(), window.max() ) );
}

/**
Expand All @@ -272,16 +273,16 @@ private static void setDisplayRangeAndBounds( final ConverterSetup converterSetu
*/
static List< Omero.Channel > omeroChannels( final Omero omero, final int numChannels )
{
if ( omero == null || omero.channels == null || omero.channels.isEmpty() )
if ( omero == null || omero.channels() == null || omero.channels().isEmpty() )
return Collections.emptyList();
if ( omero.channels.size() != numChannels )
if ( omero.channels().size() != numChannels )
{
logger.warn(
"The number of channels in the Omero metadata ({}) does not match the number of sources in the dataset ({}). Channel properties will not be applied.",
omero.channels.size(), numChannels );
omero.channels().size(), numChannels );
return Collections.emptyList();
}
return omero.channels;
return omero.channels();
}

/**
Expand All @@ -294,9 +295,9 @@ static List< Omero.Channel > omeroChannels( final Omero omero, final int numChan
*/
private static ARGBType channelColor( final Omero.Channel omeroChannel )
{
final Color color = omeroChannel == null || omeroChannel.color == null
final Color color = omeroChannel == null || omeroChannel.color() == null
? Color.white
: Color.decode( "#" + omeroChannel.color );
: Color.decode( "#" + omeroChannel.color() );
final int opaque = 255;
return new ARGBType( ARGBType.rgba( color.getRed(), color.getGreen(), color.getBlue(), opaque ) );
}
Expand All @@ -311,7 +312,7 @@ private static ARGBType channelColor( final Omero.Channel omeroChannel )
*/
private static boolean isChannelActive( final Omero.Channel omeroChannel )
{
return omeroChannel == null || omeroChannel.active;
return omeroChannel == null || omeroChannel.active();
}

/**
Expand All @@ -323,7 +324,7 @@ private static boolean isChannelActive( final Omero.Channel omeroChannel )
*/
private static int defaultTimepoint( final Omero omero )
{
return omero == null || omero.rdefs == null ? 0 : omero.rdefs.defaultT;
return omero == null || omero.rdefs() == null ? 0 : omero.rdefs().defaultT();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ private OmeZarrImagesChooser()
public static List< PyramidContents< ? > > choose( final List< PyramidContents< ? > > candidates, final PyramidContents< ? > preselected )
{
final List< JCheckBox > checkboxes = candidates.stream()
.map( contents -> new JCheckBox( contents.name, contents == preselected ) )
.map( contents -> new JCheckBox( contents.name(), contents == preselected ) )
.collect( Collectors.toList() );
final JOptionPane pane = new JOptionPane( checkboxes.toArray(), JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION );
final JDialog dialog = pane.createDialog( "OME-Zarr Images to open" );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import org.scijava.Context;

import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

import bdv.tools.brightness.ConverterSetup;
Expand All @@ -56,6 +57,7 @@
import ome.zarr.ZarrTestUtils;
import ome.zarr.fiji.util.BdvUtils;
import ome.zarr.imglib2.PyramidContents;
import ome.zarr.imglib2.metadata.Omero;

/**
* Shared parameterized tests for the BigDataViewer Fiji wrapper
Expand Down Expand Up @@ -265,13 +267,21 @@ default void testOmeroDisplaySettings( final String resource ) throws URISyntaxE
try (Context context = new Context())
{
PyramidContents< ? > contents = read( resource, context );
// NB: neither example turns a channel off, so switch one off to cover both answers.
contents.omero.channels.get( 1 ).active = false;
PyramidalBdv< ? > pyramidalBdv = new PyramidalBdv<>( context, contents );
BdvHandle bdvHandle = BdvUtils.show( pyramidalBdv, null );
ViewerState state = bdvHandle.getViewerPanel().state();
assertEquals( 1, state.getCurrentTimepoint(), "the timepoint the rendering defaults name" );
List< ? extends SourceAndConverter< ? > > sources = pyramidalBdv.asSources();
assertNotNull( contents );
Omero omero = contents.omero();
assertNotNull( omero );
assertNotNull( omero.channels() );
// NB: neither example turns a channel off, so switch one off to cover both answers.
List< Omero.Channel > channels = new ArrayList<>( omero.channels() );
Omero.Channel on = channels.get( 1 );
channels.set( 1, new Omero.Channel( false, on.coefficient(), on.color(), on.family(), on.inverted(),
on.label(), on.window() ) );
BdvUtils.setChannelProperties( channels, sources, bdvHandle.getConverterSetups(), state );
assertTrue( state.isSourceActive( sources.get( 0 ) ) );
assertFalse( state.isSourceActive( sources.get( 1 ) ) );
assertTrue( state.isSourceActive( sources.get( 2 ) ) );
Expand Down
Loading
Loading