();
+ if (Math.log(numPx) / Math.log(2) < 31) {
+ imgFactory = new ArrayImgFactory<>(new FloatType());
+ } else {
+ imgFactory = new CellImgFactory<>(new FloatType());
+ }
- Img< FloatType > loadedImg = imgFactory.create( rai, new FloatType() );
- FileMapImgLoaderLOCI2.copy(Views.extendZero( rai ), loadedImg);
+ Img< FloatType > loadedImg = imgFactory.create(rai);
+ RealTypeConverters.copyFromTo(Views.extendZero(rai), loadedImg);
rai = loadedImg;
}
@@ -238,8 +248,8 @@ else if ( cacheResult )
Arrays.fill( cellSize, 1 );
for ( int d = 0; d < rai.numDimensions() - 1; d++ )
cellSize[d] = (int) rai.dimension( d );
- rai = FusionTools.cacheRandomAccessibleInterval( rai, Long.MAX_VALUE,
- Views.iterable( rai ).firstElement().createVariable(), cellSize );
+ rai = FusionTools.cacheRandomAccessibleInterval(
+ rai, Long.MAX_VALUE, rai.firstElement().createVariable(), cellSize);
}
}
diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleInterval.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleInterval.java
index 5e70dc356..161801803 100644
--- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleInterval.java
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleInterval.java
@@ -28,12 +28,10 @@
import net.imglib2.Point;
import net.imglib2.RandomAccess;
import net.imglib2.RandomAccessibleInterval;
-import net.imglib2.Sampler;
import net.imglib2.type.numeric.RealType;
import net.imglib2.util.Pair;
import net.imglib2.util.RealSum;
import net.imglib2.util.ValuePair;
-import net.imglib2.view.Views;
/*
*
@@ -57,6 +55,9 @@ public FlatFieldCorrectedRandomAccessibleInterval(O outputType, RandomAccessible
this.brightImg = brightImg;
this.darkImg = darkImg;
+ if (brightImg.numDimensions() > sourceImg.numDimensions() || darkImg.numDimensions() > sourceImg.numDimensions())
+ throw new IllegalArgumentException("Bright-/darkfield images have more dimensions than source image!");
+
meanBrightCorrected = getMeanCorrected( brightImg, darkImg );
type = outputType;
}
@@ -107,20 +108,15 @@ public FlatFieldCorrectedRandomAccess()
@Override
public O get()
{
- // NB: the flat field images seem to be 3D with 1 z slice
- // if they were truly 2D, we would use position.length - 1
- final long[] positionBright = new long[ nDimBright ];
- final long[] positionDark = new long[ nDimDark ];
- // only copy position of n-1 dimensions
- System.arraycopy( position, 0, positionBright, 0, nDimBright );
- System.arraycopy( position, 0, positionDark, 0, nDimDark );
-
- sourceRA.setPosition( position );
- brightRA.setPosition( positionBright );
- darkRA.setPosition( positionDark );
+ // Use the fact that bright and dark must be of dimensionality <= source,
+ // and that coordinates outside the dimensionality are ignored
+ sourceRA.setPosition(position);
+ brightRA.setPosition(position);
+ darkRA.setPosition(position);
- final double corrBright = brightRA.get().getRealDouble() - darkRA.get().getRealDouble();
- final double corrImg = sourceRA.get().getRealDouble() - darkRA.get().getRealDouble();
+ final double darkValue = darkRA.get().getRealDouble();
+ final double corrBright = brightRA.get().getRealDouble() - darkValue;
+ final double corrImg = sourceRA.get().getRealDouble() - darkValue;
if (corrBright == 0)
value.setReal( 0.0 );
@@ -148,7 +144,7 @@ public static , Q extends RealType< Q >> double getMeanC
final RealSum sum = new RealSum();
long count = 0;
- final Cursor< P > brightCursor = Views.iterable( brightImg ).cursor();
+ final Cursor< P > brightCursor = brightImg.cursor();
final RandomAccess< Q > darkRA = darkImg.randomAccess();
while (brightCursor.hasNext())
@@ -172,7 +168,7 @@ public static
> Pair getMinMax(RandomAcc
double min = Double.MAX_VALUE;
double max = - Double.MAX_VALUE;
- for (final P pixel : Views.iterable( img ))
+ for (final P pixel : img)
{
double value = pixel.getRealDouble();
@@ -183,7 +179,7 @@ public static > Pair getMinMax(RandomAcc
min = value;
}
- return new ValuePair< Double, Double >( min, max );
+ return new ValuePair<>( min, max );
}
}
diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleIntervals.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleIntervals.java
index 4cb23038b..535ac9ffa 100644
--- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleIntervals.java
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatFieldCorrectedRandomAccessibleIntervals.java
@@ -25,7 +25,6 @@
import java.util.Arrays;
import bdv.util.ConstantRandomAccessible;
-import bdv.viewer.overlay.SourceInfoOverlayRenderer;
import net.imglib2.FinalInterval;
import net.imglib2.RandomAccessibleInterval;
import net.imglib2.type.numeric.RealType;
@@ -39,7 +38,7 @@ public static < R extends RealType< R >, S extends RealType< S >, T extends Real
RandomAccessibleInterval< S > brightImg,
RandomAccessibleInterval< T > darkImg )
{
- R type = Views.iterable( sourceImg ).firstElement().createVariable();
+ R type = sourceImg.firstElement().createVariable();
return create( sourceImg, brightImg, darkImg, type );
}
public static , R extends RealType< R >, S extends RealType< S >, T extends RealType< T >> RandomAccessibleInterval< O > create(
@@ -85,20 +84,20 @@ public static , R extends RealType< R >, S extends RealT
{
// assume bright and dark images constant -> should return original
// TODO: 'optimize' by really returning sourceImg?
- final ConstantRandomAccessible< FloatType > constantBright = new ConstantRandomAccessible( new FloatType(1.0f), sourceImg.numDimensions() );
- final ConstantRandomAccessible< FloatType > constantDark = new ConstantRandomAccessible( new FloatType(0.0f), sourceImg.numDimensions() );
+ final ConstantRandomAccessible< FloatType > constantBright = new ConstantRandomAccessible<>( new FloatType(1.0f), sourceImg.numDimensions() );
+ final ConstantRandomAccessible< FloatType > constantDark = new ConstantRandomAccessible<>( new FloatType(0.0f), sourceImg.numDimensions() );
return new FlatFieldCorrectedRandomAccessibleInterval<>(outputType, sourceImg, Views.interval( constantBright, sourceImg ), Views.interval( constantDark, sourceImg ) );
}
else if (brightImg == null)
{
// assume bright image == constant
- final ConstantRandomAccessible< FloatType > constantBright = new ConstantRandomAccessible( new FloatType(1.0f), sourceImg.numDimensions() );
+ final ConstantRandomAccessible< FloatType > constantBright = new ConstantRandomAccessible<>( new FloatType(1.0f), sourceImg.numDimensions() );
return new FlatFieldCorrectedRandomAccessibleInterval<>(outputType, sourceImg, Views.interval( constantBright, sourceImg ), Views.interval( Views.extendBorder( darkImg ), intervalDark ) );
}
else if (darkImg == null)
{
// assume dark image == constant == 0;
- final ConstantRandomAccessible< FloatType > constantDark = new ConstantRandomAccessible( new FloatType(0.0f), sourceImg.numDimensions() );
+ final ConstantRandomAccessible< FloatType > constantDark = new ConstantRandomAccessible<>( new FloatType(0.0f), sourceImg.numDimensions() );
return new FlatFieldCorrectedRandomAccessibleInterval<>(outputType, sourceImg, Views.interval( Views.extendBorder( brightImg ), intervalBright ), Views.interval( constantDark, sourceImg ) );
}
diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldCorrectionWrappedImgLoader.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldCorrectionWrappedImgLoader.java
index 55666c3df..be80c7f53 100644
--- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldCorrectionWrappedImgLoader.java
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldCorrectionWrappedImgLoader.java
@@ -23,6 +23,7 @@
package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield;
import java.io.File;
+import java.net.URI;
import mpicbg.spim.data.sequence.ImgLoader;
import mpicbg.spim.data.sequence.ViewId;
@@ -34,6 +35,36 @@ public interface FlatfieldCorrectionWrappedImgLoader exten
public boolean isActive();
public void setCached(boolean cached);
public boolean isCached();
- public void setBrightImage(ViewId vId, File imgFile);
- public void setDarkImage(ViewId vId, File imgFile);
+
+ /**
+ * Set the bright (flatfield) image for a view.
+ * @param vId view id
+ * @param imgUri URI to the bright image (supports file://, s3://, gs://, local paths)
+ */
+ public void setBrightImage(ViewId vId, URI imgUri);
+
+ /**
+ * Set the dark (darkfield) image for a view.
+ * @param vId view id
+ * @param imgUri URI to the dark image (supports file://, s3://, gs://, local paths)
+ */
+ public void setDarkImage(ViewId vId, URI imgUri);
+
+ /**
+ * Set the bright (flatfield) image for a view from a local file.
+ * @param vId view id
+ * @param imgFile local file path to the bright image
+ */
+ default void setBrightImage(ViewId vId, File imgFile) {
+ setBrightImage(vId, imgFile == null ? null : imgFile.toURI());
+ }
+
+ /**
+ * Set the dark (darkfield) image for a view from a local file.
+ * @param vId view id
+ * @param imgFile local file path to the dark image
+ */
+ default void setDarkImage(ViewId vId, File imgFile) {
+ setDarkImage(vId, imgFile == null ? null : imgFile.toURI());
+ }
}
diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldImageLoader.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldImageLoader.java
new file mode 100644
index 000000000..32cd3d3e2
--- /dev/null
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldImageLoader.java
@@ -0,0 +1,181 @@
+/*-
+ * #%L
+ * Software for the reconstruction of multi-view microscopic acquisitions
+ * like Selective Plane Illumination Microscopy (SPIM) Data.
+ * %%
+ * Copyright (C) 2012 - 2025 Multiview Reconstruction developers.
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield;
+
+import java.io.File;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+import org.janelia.saalfeldlab.n5.N5Reader;
+import org.janelia.saalfeldlab.n5.imglib2.N5Utils;
+import org.janelia.saalfeldlab.n5.universe.StorageFormat;
+
+import ij.IJ;
+import ij.ImagePlus;
+import mpicbg.spim.data.sequence.ViewId;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.converter.RealTypeConverters;
+import net.imglib2.img.display.imagej.ImageJFunctions;
+import net.imglib2.type.numeric.real.FloatType;
+import net.imglib2.util.Cast;
+import net.imglib2.util.Pair;
+import net.imglib2.util.ValuePair;
+import util.URITools;
+
+/**
+ * Helper class for loading flatfield correction images from various sources.
+ *
+ * This class handles:
+ * - URI-based storage of bright/dark image paths per view
+ * - Lazy loading and caching of images
+ * - Auto-detection of format (TIFF vs N5/Zarr)
+ * - Support for cloud storage (S3, GCS)
+ */
+public class FlatfieldImageLoader {
+
+ protected final Map> raiMap;
+ protected final Map> uriMap;
+
+ private static final Pair NULL_PAIR = new ValuePair<>(null, null);
+
+ public FlatfieldImageLoader() {
+ raiMap = new HashMap<>();
+ uriMap = new HashMap<>();
+ }
+
+ public void setBrightImage(ViewId vId, URI imgUri) {
+ final Pair oldPair = uriMap.getOrDefault(vId, NULL_PAIR);
+ uriMap.put(vId, new ValuePair<>(imgUri, oldPair.getB()));
+ }
+
+ public void setDarkImage(ViewId vId, URI imgUri) {
+ final Pair oldPair = uriMap.getOrDefault(vId, NULL_PAIR);
+ uriMap.put(vId, new ValuePair<>(oldPair.getA(), imgUri));
+ }
+
+ public void setBrightImage(ViewId vId, File imgFile) {
+ setBrightImage(vId, imgFile == null ? null : imgFile.toURI());
+ }
+
+ public void setDarkImage(ViewId vId, File imgFile) {
+ setDarkImage(vId, imgFile == null ? null : imgFile.toURI());
+ }
+
+ public RandomAccessibleInterval getBrightImg(ViewId vId) {
+ return getImg(vId, Pair::getA);
+ }
+
+ public RandomAccessibleInterval getDarkImg(ViewId vId) {
+ return getImg(vId, Pair::getB);
+ }
+
+ /**
+ * Get image for view id; the brightfield is stored in the A element of the pair, the darkfield in B
+ * @param vId view id
+ * @param uriSelector function to select URI from pair
+ * @return image, or null if not set
+ */
+ private RandomAccessibleInterval getImg(ViewId vId, Function, URI> uriSelector) {
+ if (!uriMap.containsKey(vId))
+ return null;
+
+ final URI uriToLoad = uriSelector.apply(uriMap.get(vId));
+ if (uriToLoad == null)
+ return null;
+
+ return loadImageIfNecessary(uriToLoad);
+ }
+
+ /**
+ * Load an image from a URI. Supports:
+ * - Local TIFF files (via ImageJ)
+ * - Local/cloud Zarr v3 containers (via N5 API)
+ * - Local/cloud N5 containers (via N5 API)
+ *
+ * @param uri URI to the image
+ * @return the loaded image as FloatType
+ */
+ public RandomAccessibleInterval loadImageIfNecessary(URI uri) {
+ if (!raiMap.containsKey(uri)) {
+ RandomAccessibleInterval img;
+
+ if (isChunkedFormat(uri)) {
+ // Use N5/Zarr API for chunked formats
+ final StorageFormat format = detectStorageFormat(uri);
+ final N5Reader reader = URITools.instantiateN5Reader(format, uri);
+ final RandomAccessibleInterval> raw = N5Utils.open(reader, "");
+ img = RealTypeConverters.convert(Cast.unchecked(raw), new FloatType());
+ } else {
+ // Legacy TIFF path via ImageJ
+ final File file = new File(uri);
+ final ImagePlus imp = IJ.openImage(file.getAbsolutePath());
+ if (imp == null)
+ throw new RuntimeException("Failed to load image from: " + uri);
+ img = ImageJFunctions.convertFloat(imp).copy();
+ }
+
+ raiMap.put(uri, img);
+ }
+ return raiMap.get(uri);
+ }
+
+ /**
+ * Determine if the URI points to a chunked format (N5/Zarr) vs TIFF.
+ */
+ public static boolean isChunkedFormat(URI uri) {
+ final String scheme = uri.getScheme();
+ // Cloud URIs are always chunked format
+ if ("s3".equals(scheme) || "gs".equals(scheme))
+ return true;
+
+ // Check path for .zarr or .n5 extension
+ final String path = uri.getPath();
+ if (path == null)
+ return false;
+
+ final String lowerPath = path.toLowerCase();
+ return lowerPath.endsWith(".zarr") || lowerPath.endsWith(".n5")
+ || lowerPath.contains(".zarr/") || lowerPath.contains(".n5/");
+ }
+
+ /**
+ * Detect the storage format from the URI.
+ */
+ public static StorageFormat detectStorageFormat(URI uri) {
+ final String path = uri.getPath();
+ if (path != null && path.toLowerCase().contains(".n5"))
+ return StorageFormat.N5;
+ // Default to Zarr v3 for cloud and .zarr paths
+ return StorageFormat.ZARR;
+ }
+
+ /**
+ * Get the URI map for bright/dark images per view.
+ * @return map from ViewId to (brightUri, darkUri) pair
+ */
+ public Map> getUriMap() {
+ return uriMap;
+ }
+}
diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/LazyLoadingFlatFieldCorrectionMap.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/LazyLoadingFlatFieldCorrectionMap.java
index 1c98182e6..eda2b2a59 100644
--- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/LazyLoadingFlatFieldCorrectionMap.java
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/LazyLoadingFlatFieldCorrectionMap.java
@@ -9,12 +9,12 @@
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 2 of the
* License, or (at your option) any later version.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
- *
+ *
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* .
@@ -23,104 +23,58 @@
package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield;
import java.io.File;
-import java.util.HashMap;
+import java.net.URI;
import java.util.Map;
-import ij.IJ;
-import ij.ImagePlus;
import mpicbg.spim.data.sequence.ImgLoader;
import mpicbg.spim.data.sequence.ViewId;
import net.imglib2.RandomAccessibleInterval;
import net.imglib2.img.display.imagej.ImageJFunctions;
import net.imglib2.type.numeric.real.FloatType;
import net.imglib2.util.Pair;
-import net.imglib2.util.ValuePair;
-public abstract class LazyLoadingFlatFieldCorrectionMap implements FlatfieldCorrectionWrappedImgLoader< IL >
+public abstract class LazyLoadingFlatFieldCorrectionMap implements FlatfieldCorrectionWrappedImgLoader
{
-
- protected final Map< File, RandomAccessibleInterval< FloatType > > raiMap;
- protected final Map> fileMap;
-
+ protected final FlatfieldImageLoader imageLoader;
+
public LazyLoadingFlatFieldCorrectionMap()
{
- raiMap = new HashMap<>();
- fileMap = new HashMap<>();
+ imageLoader = new FlatfieldImageLoader();
}
-
- @Override
- public void setBrightImage(ViewId vId, File imgFile)
- {
- if (!fileMap.containsKey( vId ))
- fileMap.put( vId, new ValuePair< File, File >( null, null ) );
- final Pair< File, File > oldPair = fileMap.get( vId );
- fileMap.put( vId, new ValuePair< File, File >( imgFile, oldPair.getB() ) );
+ @Override
+ public void setBrightImage(ViewId vId, URI imgUri) {
+ imageLoader.setBrightImage(vId, imgUri);
}
@Override
- public void setDarkImage(ViewId vId, File imgFile)
- {
- if (!fileMap.containsKey( vId ))
- fileMap.put( vId, new ValuePair< File, File >( null, null ) );
-
- final Pair< File, File > oldPair = fileMap.get( vId );
- fileMap.put( vId, new ValuePair< File, File >( oldPair.getA(), imgFile ) );
+ public void setDarkImage(ViewId vId, URI imgUri) {
+ imageLoader.setDarkImage(vId, imgUri);
}
-
- protected RandomAccessibleInterval< FloatType > getBrightImg(ViewId vId)
- {
- if (!fileMap.containsKey( vId ))
- return null;
-
- final File fileToLoad = fileMap.get( vId ).getA();
-
- if (fileToLoad == null)
- return null;
- loadFileIfNecessary( fileToLoad );
- return raiMap.get( fileToLoad );
+ protected RandomAccessibleInterval getBrightImg(ViewId vId) {
+ return imageLoader.getBrightImg(vId);
}
- protected RandomAccessibleInterval< FloatType > getDarkImg(ViewId vId)
- {
- if (!fileMap.containsKey( vId ))
- return null;
-
- final File fileToLoad = fileMap.get( vId ).getB();
-
- if (fileToLoad == null)
- return null;
-
- loadFileIfNecessary( fileToLoad );
- return raiMap.get( fileToLoad );
+ protected RandomAccessibleInterval getDarkImg(ViewId vId) {
+ return imageLoader.getDarkImg(vId);
}
-
- protected void loadFileIfNecessary(File file)
- {
- if (raiMap.containsKey( file ))
- return;
-
- final ImagePlus imp = IJ.openImage( file.getAbsolutePath() );
- final RandomAccessibleInterval< FloatType > img = ImageJFunctions.convertFloat( imp ).copy();
-
- raiMap.put( file, img );
- }
-
+
public static void main(String[] args)
{
- DefaultFlatfieldCorrectionWrappedImgLoader testImgLoader = new DefaultFlatfieldCorrectionWrappedImgLoader( null );
- testImgLoader.setBrightImage( new ViewId(0,0), new File("/Users/David/Desktop/ell2.tif" ));
- RandomAccessibleInterval< FloatType > brightImg = testImgLoader.getBrightImg( new ViewId( 0, 0 ) );
-
- ImageJFunctions.show( brightImg );
-
+ DefaultFlatfieldCorrectionWrappedImgLoader testImgLoader = new DefaultFlatfieldCorrectionWrappedImgLoader(null);
+ testImgLoader.setBrightImage(new ViewId(0, 0), new File("/Users/David/Desktop/ell2.tif"));
+ RandomAccessibleInterval brightImg = testImgLoader.getBrightImg(new ViewId(0, 0));
+
+ ImageJFunctions.show(brightImg);
}
- public Map< ViewId, Pair< File, File > > getFileMap()
+ /**
+ * Get the URI map for bright/dark images per view.
+ * @return map from ViewId to (brightUri, darkUri) pair
+ */
+ public Map> getUriMap()
{
- return fileMap;
+ return imageLoader.getUriMap();
}
-
-
}
diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/MultiResolutionFlatfieldCorrectionWrappedImgLoader.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/MultiResolutionFlatfieldCorrectionWrappedImgLoader.java
index 1edc09e1d..f6a84a326 100644
--- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/MultiResolutionFlatfieldCorrectionWrappedImgLoader.java
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/MultiResolutionFlatfieldCorrectionWrappedImgLoader.java
@@ -23,13 +23,14 @@
package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield;
import java.io.File;
-import java.util.ArrayList;
+import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
-import bdv.export.WriteSequenceToHdf5;
import ij.ImageJ;
import mpicbg.spim.data.generic.sequence.ImgLoaderHint;
import mpicbg.spim.data.generic.sequence.ImgLoaderHints;
@@ -37,16 +38,17 @@
import mpicbg.spim.data.sequence.MultiResolutionSetupImgLoader;
import mpicbg.spim.data.sequence.ViewId;
import mpicbg.spim.data.sequence.VoxelDimensions;
-import net.imglib2.Cursor;
import net.imglib2.Dimensions;
-import net.imglib2.FinalDimensions;
-import net.imglib2.RandomAccess;
-import net.imglib2.RandomAccessible;
import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.algorithm.blocks.BlockAlgoUtils;
+import net.imglib2.algorithm.blocks.BlockSupplier;
+import net.imglib2.algorithm.blocks.downsample.Downsample;
import net.imglib2.converter.RealTypeConverters;
import net.imglib2.img.Img;
import net.imglib2.img.ImgFactory;
import net.imglib2.img.array.ArrayImgFactory;
+import net.imglib2.img.cell.AbstractCellImg;
+import net.imglib2.img.cell.CellGrid;
import net.imglib2.img.cell.CellImgFactory;
import net.imglib2.img.display.imagej.ImageJFunctions;
import net.imglib2.realtransform.AffineTransform3D;
@@ -58,7 +60,6 @@
import net.imglib2.view.Views;
import net.preibisch.mvrecon.fiji.plugin.queryXML.LoadParseQueryXML;
import net.preibisch.mvrecon.fiji.spimdata.SpimData2;
-import net.preibisch.mvrecon.fiji.spimdata.imgloaders.filemap2.FileMapImgLoaderLOCI2;
import net.preibisch.mvrecon.process.fusion.FusionTools;
@@ -66,12 +67,12 @@ public class MultiResolutionFlatfieldCorrectionWrappedImgLoader
extends LazyLoadingFlatFieldCorrectionMap< MultiResolutionImgLoader > implements MultiResolutionImgLoader
{
- private MultiResolutionImgLoader wrappedImgLoader;
+ private final MultiResolutionImgLoader wrappedImgLoader;
private boolean active;
private boolean cacheResult;
/* downsampled bright/dark images */
- private final Map< Pair< File, List< Integer > >, RandomAccessibleInterval< FloatType > > dsRaiMap;
+ private final Map>, RandomAccessibleInterval> dsRaiMap;
public MultiResolutionFlatfieldCorrectionWrappedImgLoader(MultiResolutionImgLoader wrappedImgLoader)
{
@@ -89,55 +90,39 @@ public MultiResolutionFlatfieldCorrectionWrappedImgLoader(MultiResolutionImgLoad
dsRaiMap = new HashMap<>();
}
- protected RandomAccessibleInterval< FloatType > getOrCreateBrightImgDownsampled(ViewId vId,
- int[] downsamplingFactors)
- {
- ArrayList< Integer > dsFactorList = new ArrayList< Integer >();
- for ( int i : downsamplingFactors )
- dsFactorList.add( i );
-
- final ValuePair< File, List< Integer > > key = new ValuePair<>( fileMap.get( vId ).getA(), dsFactorList );
-
- if ( !dsRaiMap.containsKey( key ) )
- {
- final RandomAccessibleInterval< FloatType > brightImg = getBrightImg( vId );
-
- if ( brightImg == null )
- return null;
-
- // NB: we add a singleton z-dimension here for downsampleHDF5 to
- // work
- final RandomAccessibleInterval< FloatType > downsampled = downsampleHDF5(
- Views.addDimension( brightImg, 0, 0 ), downsamplingFactors );
- dsRaiMap.put( key, downsampled );
- }
-
- return dsRaiMap.get( key );
+ protected RandomAccessibleInterval< FloatType > getOrCreateBrightImgDownsampled(ViewId vId, int[] downsamplingFactors) {
+ return getOrCreateDownsampledImg(vId, downsamplingFactors, Pair::getA, this::getBrightImg);
}
- protected RandomAccessibleInterval< FloatType > getOrCreateDarkImgDownsampled(ViewId vId, int[] downsamplingFactors)
- {
- ArrayList< Integer > dsFactorList = new ArrayList< Integer >();
- for ( int i : downsamplingFactors )
- dsFactorList.add( i );
-
- final ValuePair< File, List< Integer > > key = new ValuePair<>( fileMap.get( vId ).getB(), dsFactorList );
-
- if ( !dsRaiMap.containsKey( key ) )
- {
- final RandomAccessibleInterval< FloatType > darkImg = getDarkImg( vId );
+ protected RandomAccessibleInterval< FloatType > getOrCreateDarkImgDownsampled(ViewId vId, int[] downsamplingFactors) {
+ return getOrCreateDownsampledImg(vId, downsamplingFactors, Pair::getB, this::getDarkImg);
+ }
- if ( darkImg == null )
+ /**
+ * Generic method to get a downsampled image or do downsampling on the fly. The bright image
+ * is stored in the A element of the pair, the dark image in B.
+ */
+ private RandomAccessibleInterval getOrCreateDownsampledImg(
+ ViewId vId,
+ int[] downsamplingFactors,
+ Function, URI> uriSelector,
+ Function> imgGetter
+ ) {
+ // Convert to a list here to have a proper hash code for the map key
+ List dsFactorList = Arrays.stream(downsamplingFactors).boxed().collect(Collectors.toList());
+ final ValuePair> key = new ValuePair<>(uriSelector.apply(getUriMap().get(vId)), dsFactorList);
+
+ if (!dsRaiMap.containsKey(key)) {
+ final RandomAccessibleInterval img = imgGetter.apply(vId);
+
+ if (img == null)
return null;
- // NB: we add a singleton z-dimension here for downsampleHDF5 to
- // work
- final RandomAccessibleInterval< FloatType > downsampled = downsampleHDF5(
- Views.addDimension( darkImg, 0, 0 ), downsamplingFactors );
- dsRaiMap.put( key, downsampled );
+ final RandomAccessibleInterval downsampled = downsampleHDF5(img, downsamplingFactors);
+ dsRaiMap.put(key, downsampled);
}
- return dsRaiMap.get( key );
+ return dsRaiMap.get(key);
}
@Override
@@ -185,8 +170,11 @@ public RandomAccessibleInterval< T > getImage(int timepointId, int level, ImgLoa
final MultiResolutionSetupImgLoader< ? > wrpSetupIL = wrappedImgLoader.getSetupImgLoader( setupId );
- if(!active)
- return (RandomAccessibleInterval< T >) wrpSetupIL.getImage( timepointId, level, hints );
+ if(!active) {
+ @SuppressWarnings("unchecked")
+ RandomAccessibleInterval image = (RandomAccessibleInterval) wrpSetupIL.getImage(timepointId, level, hints);
+ return image;
+ }
final int n = wrpSetupIL.getImageSize( timepointId ).numDimensions();
@@ -205,9 +193,12 @@ public RandomAccessibleInterval< T > getImage(int timepointId, int level, ImgLoa
getOrCreateDarkImgDownsampled( new ViewId( timepointId, setupId ), dsFactors ) );
boolean loadCompletelyRequested = false;
- for (ImgLoaderHint hint : hints)
- if (hint == ImgLoaderHints.LOAD_COMPLETELY)
+ for (ImgLoaderHint hint : hints) {
+ if (hint == ImgLoaderHints.LOAD_COMPLETELY) {
loadCompletelyRequested = true;
+ break;
+ }
+ }
if (loadCompletelyRequested)
{
@@ -216,13 +207,14 @@ public RandomAccessibleInterval< T > getImage(int timepointId, int level, ImgLoa
numPx *= rai.dimension( d );
final ImgFactory< T > imgFactory;
- if (Math.log(numPx) / Math.log( 2 ) < 31)
- imgFactory = new ArrayImgFactory();
- else
- imgFactory = new CellImgFactory();
+ if (Math.log(numPx) / Math.log(2) < 31) {
+ imgFactory = new ArrayImgFactory<>(getImageType());
+ } else {
+ imgFactory = new CellImgFactory<>(getImageType());
+ }
- Img< T > loadedImg = imgFactory.create( rai, getImageType() );
- RealTypeConverters.copyFromTo( Views.extendZero( rai ), loadedImg );
+ Img loadedImg = imgFactory.create(rai);
+ RealTypeConverters.copyFromTo(Views.extendZero(rai), loadedImg);
rai = loadedImg;
}
@@ -232,8 +224,8 @@ else if ( cacheResult )
Arrays.fill( cellSize, 1 );
for ( int d = 0; d < rai.numDimensions() - 1; d++ )
cellSize[d] = (int) rai.dimension( d );
- rai = FusionTools.cacheRandomAccessibleInterval( rai, Long.MAX_VALUE,
- Views.iterable( rai ).firstElement().createVariable(), cellSize );
+ rai = FusionTools.cacheRandomAccessibleInterval(
+ rai, Long.MAX_VALUE, rai.firstElement().createVariable(), cellSize);
}
return rai;
}
@@ -268,9 +260,12 @@ public RandomAccessibleInterval< FloatType > getFloatImage(int timepointId, int
RandomAccessibleInterval< FloatType > raiNormalized = new VirtuallyNormalizedRandomAccessibleInterval<>(
rai );
boolean loadCompletelyRequested = false;
- for (ImgLoaderHint hint : hints)
- if (hint == ImgLoaderHints.LOAD_COMPLETELY)
+ for (ImgLoaderHint hint : hints) {
+ if (hint == ImgLoaderHints.LOAD_COMPLETELY) {
loadCompletelyRequested = true;
+ break;
+ }
+ }
if (loadCompletelyRequested)
{
@@ -279,13 +274,14 @@ public RandomAccessibleInterval< FloatType > getFloatImage(int timepointId, int
numPx *= raiNormalized.dimension( d );
final ImgFactory< FloatType > imgFactory;
- if (Math.log(numPx) / Math.log( 2 ) < 31)
- imgFactory = new ArrayImgFactory();
- else
- imgFactory = new CellImgFactory();
+ if (Math.log(numPx) / Math.log(2) < 31) {
+ imgFactory = new ArrayImgFactory<>(new FloatType());
+ } else {
+ imgFactory = new CellImgFactory<>(new FloatType());
+ }
- Img< FloatType > loadedImg = imgFactory.create( raiNormalized, new FloatType() );
- FileMapImgLoaderLOCI2.copy(Views.extendZero( raiNormalized ), loadedImg);
+ Img loadedImg = imgFactory.create(raiNormalized);
+ RealTypeConverters.copyFromTo(Views.extendZero(raiNormalized), loadedImg);
raiNormalized = loadedImg;
}
@@ -295,17 +291,19 @@ else if ( cacheResult )
Arrays.fill( cellSize, 1 );
for ( int d = 0; d < raiNormalized.numDimensions() - 1; d++ )
cellSize[d] = (int) raiNormalized.dimension( d );
- rai = FusionTools.cacheRandomAccessibleInterval( raiNormalized, Long.MAX_VALUE,
- Views.iterable( rai ).firstElement().createVariable(), cellSize );
+ rai = FusionTools.cacheRandomAccessibleInterval(
+ raiNormalized, Long.MAX_VALUE, rai.firstElement().createVariable(), cellSize);
}
rai = raiNormalized;
}
- else
- {
+ else {
boolean loadCompletelyRequested = false;
- for (ImgLoaderHint hint : hints)
- if (hint == ImgLoaderHints.LOAD_COMPLETELY)
+ for (ImgLoaderHint hint : hints) {
+ if (hint == ImgLoaderHints.LOAD_COMPLETELY) {
loadCompletelyRequested = true;
+ break;
+ }
+ }
if (loadCompletelyRequested)
{
@@ -315,12 +313,12 @@ else if ( cacheResult )
final ImgFactory< FloatType > imgFactory;
if (Math.log(numPx) / Math.log( 2 ) < 31)
- imgFactory = new ArrayImgFactory();
+ imgFactory = new ArrayImgFactory<>(new FloatType());
else
- imgFactory = new CellImgFactory();
+ imgFactory = new CellImgFactory<>(new FloatType());
- Img< FloatType > loadedImg = imgFactory.create( rai, new FloatType() );
- FileMapImgLoaderLOCI2.copy(Views.extendZero( rai ), loadedImg);
+ Img loadedImg = imgFactory.create(rai);
+ RealTypeConverters.copyFromTo(Views.extendZero(rai), loadedImg);
rai = loadedImg;
}
@@ -330,8 +328,8 @@ else if ( cacheResult )
Arrays.fill( cellSize, 1 );
for ( int d = 0; d < rai.numDimensions() - 1; d++ )
cellSize[d] = (int) rai.dimension( d );
- rai = FusionTools.cacheRandomAccessibleInterval( rai, Long.MAX_VALUE,
- Views.iterable( rai ).firstElement().createVariable(), cellSize );
+ rai = FusionTools.cacheRandomAccessibleInterval(
+ rai, Long.MAX_VALUE, rai.firstElement().createVariable(), cellSize);
}
}
@@ -398,84 +396,54 @@ public Dimensions getImageSize(int timepointId, int level)
}
/**
- * downsampling code form {@link WriteSequenceToHdf5}, distilled into one
- * method
- *
- * @param input
- * image to downsample
- * @param dsFactor
- * factors to downsample by
- * @param
- * the image type
- * @return downsampled image
+ * Downsample an image using the imglib2-algorithm blocks API.
+ *
+ * If the input is a cell/chunked image, the output cell size is computed
+ * to align with input chunk boundaries (input chunk size / downsampling factor).
+ *
+ * @param input image to downsample
+ * @param dsFactor factors to downsample by (may have more dimensions than input)
+ * @param the image type
+ * @return downsampled image, or input unchanged if no downsampling needed
*/
- public static & NativeType< T >> RandomAccessibleInterval< T > downsampleHDF5(
- RandomAccessibleInterval< T > input, final int[] dsFactor)
- {
- final long[] blockMin = new long[input.numDimensions()];
-
- final long[] outDim = new long[input.numDimensions()];
- for ( int d = 0; d < input.numDimensions(); d++ )
- outDim[d] = Math.max( input.dimension( d ) / dsFactor[d], 1 );
-
- final Img< T > downsampled = new ArrayImgFactory< T >().create( new FinalDimensions( outDim ),
- Views.iterable( input ).firstElement().createVariable() );
- final RandomAccess< T > randomAccess = Views.extendBorder( input ).randomAccess();
-
- final Cursor< T > out = downsampled.cursor();
-
- double scale = 1;
- for ( int f : dsFactor )
- scale *= f;
- scale = 1.0 / scale;
-
- final int numBlockPixels = (int) ( outDim[0] * outDim[1] * outDim[2] );
- final double[] accumulator = new double[numBlockPixels];
-
- randomAccess.setPosition( blockMin );
-
- final int ox = (int) outDim[0];
- final int oy = (int) outDim[1];
- final int oz = (int) outDim[2];
-
- final int sx = ox * dsFactor[0];
- final int sy = oy * dsFactor[1];
- final int sz = oz * dsFactor[2];
+ public static & NativeType> RandomAccessibleInterval downsampleHDF5(
+ RandomAccessibleInterval input,
+ final int[] dsFactor
+ ) {
+ final int n = input.numDimensions();
+
+ // Build effective factors matching input dimensions, check if downsampling needed
+ boolean needsDownsampling = false;
+ final int[] effectiveFactors = new int[n];
+ for (int d = 0; d < n; d++) {
+ effectiveFactors[d] = (d < dsFactor.length) ? dsFactor[d] : 1;
+ if (effectiveFactors[d] > 1)
+ needsDownsampling = true;
+ }
- int i = 0;
- for ( int z = 0, bz = 0; z < sz; ++z )
- {
- for ( int y = 0, by = 0; y < sy; ++y )
- {
- for ( int x = 0, bx = 0; x < sx; ++x )
- {
- accumulator[i] += randomAccess.get().getRealDouble();
- randomAccess.fwd( 0 );
- if ( ++bx == dsFactor[0] )
- {
- bx = 0;
- ++i;
- }
- }
- randomAccess.move( -sx, 0 );
- randomAccess.fwd( 1 );
- if ( ++by == dsFactor[1] )
- by = 0;
- else
- i -= ox;
- }
- randomAccess.move( -sy, 1 );
- randomAccess.fwd( 2 );
- if ( ++bz == dsFactor[2] )
- bz = 0;
- else
- i -= ox * oy;
+ // Return input unchanged if all factors are 1
+ if (!needsDownsampling)
+ return input;
+
+ final long[] outDim = new long[n];
+ for (int d = 0; d < n; d++)
+ outDim[d] = Math.max(input.dimension(d) / effectiveFactors[d], 1);
+
+ // Determine output cell size - use input chunk size if available
+ final int[] cellSize = new int[n];
+ if (input instanceof AbstractCellImg) {
+ @SuppressWarnings("rawtypes")
+ final CellGrid grid = ((AbstractCellImg) input).getCellGrid();
+ grid.cellDimensions(cellSize);
+ } else {
+ // Default fallback for non-chunked images
+ Arrays.fill(cellSize, 128);
}
- for ( int j = 0; j < numBlockPixels; ++j )
- out.next().setReal( accumulator[j] * scale );
+ final BlockSupplier blocks = BlockSupplier.of(input)
+ .andThen(Downsample.downsample(effectiveFactors));
- return downsampled;
+ return BlockAlgoUtils.cellImg(blocks, outDim, cellSize);
}
public static void main(String[] args)
diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestDecoratorChain.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestDecoratorChain.java
new file mode 100644
index 000000000..178f28e1f
--- /dev/null
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestDecoratorChain.java
@@ -0,0 +1,217 @@
+/*-
+ * #%L
+ * Software for the reconstruction of multi-view microscopic acquisitions
+ * like Selective Plane Illumination Microscopy (SPIM) Data.
+ * %%
+ * Copyright (C) 2012 - 2025 Multiview Reconstruction developers.
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield;
+
+import java.io.File;
+import java.util.HashMap;
+
+import ij.ImageJ;
+import mpicbg.spim.data.SpimDataException;
+import mpicbg.spim.data.sequence.MultiResolutionImgLoader;
+import mpicbg.spim.data.sequence.SequenceDescription;
+import mpicbg.spim.data.sequence.ViewId;
+import mpicbg.spim.data.sequence.ViewSetup;
+import net.imglib2.FinalInterval;
+import net.imglib2.Interval;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.img.display.imagej.ImageJFunctions;
+import net.imglib2.type.numeric.real.FloatType;
+import net.preibisch.mvrecon.fiji.spimdata.SpimData2;
+import net.preibisch.mvrecon.fiji.spimdata.XmlIoSpimData2;
+import net.preibisch.mvrecon.fiji.spimdata.imgloaders.splitting.SplitMultiResolutionImgLoader;
+
+/**
+ * Test class demonstrating the full decorator chain:
+ *
+ * BaseLoader (N5/OME-Zarr)
+ * → MultiResolutionFlatfieldCorrectionWrappedImgLoader (applies correction)
+ * → SplitMultiResolutionImgLoader (splits into regions)
+ *
+ * This shows how to compose multiple wrapper/decorator layers while maintaining
+ * the MultiResolutionImgLoader interface throughout the chain.
+ */
+public class TestDecoratorChain {
+ // Mapping from ViewSetup ID to Tile ID (from dataset.xml)
+ private static final int[][] SETUP_TO_TILE = {
+ {0, 187},
+ {1, 188},
+ {2, 189},
+ {3, 200},
+ {4, 201},
+ {5, 202},
+ {6, 213},
+ {7, 214},
+ {8, 215}
+ };
+
+ public static void main(String[] args) throws SpimDataException {
+ // ========== CONFIGURATION ==========
+ final String basePath = "/Users/innerbergerm/Projects/janelia/multiview-reconstruction/";
+ final String xmlPath = basePath + "data/dataset.xml";
+ final String correctionPath = basePath + "dark_and_flatfields/";
+
+ // Which setup to demonstrate (0-8)
+ final int setupToShow = 0;
+ final int timepoint = 0;
+
+ // ========== STEP 1: Load dataset and get base loader ==========
+ System.out.println("=== STEP 1: Loading dataset ===");
+ System.out.println("XML path: " + xmlPath);
+
+ final SpimData2 data = new XmlIoSpimData2().load(xmlPath);
+ final SequenceDescription seqDesc = data.getSequenceDescription();
+
+ // The base loader - this could be N5ImageLoader, AllenOMEZarrLoader, etc.
+ final MultiResolutionImgLoader baseLoader =
+ (MultiResolutionImgLoader) seqDesc.getImgLoader();
+
+ System.out.println("Base loader type: " + baseLoader.getClass().getSimpleName());
+
+ // ========== STEP 2: Wrap with flatfield correction ==========
+ System.out.println("\n=== STEP 2: Wrapping with flatfield correction ===");
+
+ final MultiResolutionFlatfieldCorrectionWrappedImgLoader correctedLoader =
+ new MultiResolutionFlatfieldCorrectionWrappedImgLoader(baseLoader, true);
+
+ // Configure correction images for each view setup
+ for (int[] mapping : SETUP_TO_TILE) {
+ final int setupId = mapping[0];
+ final int tileId = mapping[1];
+
+ // Find darkfield file
+ final File darkfield = new File(correctionPath + "setup" + tileId + "-AVG_darkfield-fromdata.tif");
+
+ // Find flatfield file (try both naming conventions)
+ File flatfield = new File(correctionPath + "setup" + tileId + "-flatfield.tif");
+ if (!flatfield.exists())
+ flatfield = new File(correctionPath + "setup" + tileId + "-flatfield (fixed by mirroring).tif");
+
+ final ViewId viewId = new ViewId(timepoint, setupId);
+
+ if (darkfield.exists()) {
+ correctedLoader.setDarkImage(viewId, darkfield);
+ System.out.println(" Setup " + setupId + ": darkfield = " + darkfield.getName());
+ }
+
+ if (flatfield.exists()) {
+ correctedLoader.setBrightImage(viewId, flatfield);
+ System.out.println(" Setup " + setupId + ": flatfield = " + flatfield.getName());
+ }
+ }
+
+ // ========== STEP 3: Wrap with splitting ==========
+ System.out.println("\n=== STEP 3: Wrapping with splitting ===");
+
+ // Get original image dimensions for the setup we're demonstrating
+ final ViewSetup vs = seqDesc.getViewSetups().get(setupToShow);
+ final long[] dims = new long[3];
+ vs.getSize().dimensions(dims);
+ System.out.println("Original image size: " + dims[0] + " x " + dims[1] + " x " + dims[2]);
+
+ // Create a simple 2x1 split in X dimension
+ // Split the 512-wide image into two 256-wide regions
+ final long splitX = dims[0] / 2;
+
+ // Define the mappings for split regions
+ // New setup IDs 100, 101 will map to original setup 0, with different X intervals
+ final HashMap new2oldSetupId = new HashMap<>();
+ final HashMap newSetupId2Interval = new HashMap<>();
+
+ // Split region 0: left half [0, splitX) x [0, dimY) x [0, dimZ)
+ new2oldSetupId.put(100, setupToShow);
+ newSetupId2Interval.put(100, new FinalInterval(
+ new long[] {0, 0, 0},
+ new long[] {splitX - 1, dims[1] - 1, dims[2] - 1}
+ ));
+
+ // Split region 1: right half [splitX, dimX) x [0, dimY) x [0, dimZ)
+ new2oldSetupId.put(101, setupToShow);
+ newSetupId2Interval.put(101, new FinalInterval(
+ new long[] {splitX, 0, 0},
+ new long[] {dims[0] - 1, dims[1] - 1, dims[2] - 1}
+ ));
+
+ System.out.println("Created 2 split regions:");
+ System.out.println(" Setup 100: X=[0, " + (splitX-1) + "] (left half)");
+ System.out.println(" Setup 101: X=[" + splitX + ", " + (dims[0]-1) + "] (right half)");
+
+ // Create the split loader wrapping the CORRECTED loader
+ // This is the key: correction is applied BEFORE splitting
+ final SplitMultiResolutionImgLoader splitLoader = new SplitMultiResolutionImgLoader(
+ correctedLoader, // <-- corrected loader, not base loader!
+ new2oldSetupId,
+ newSetupId2Interval,
+ seqDesc
+ );
+
+ // ========== STEP 4: Display comparison images ==========
+ System.out.println("\n=== STEP 4: Displaying images ===");
+ new ImageJ();
+
+ final int tileId = SETUP_TO_TILE[setupToShow][1];
+
+ // 4a. Show UNCORRECTED original (full image, level 0)
+ System.out.println("Loading uncorrected image...");
+ final RandomAccessibleInterval uncorrected =
+ baseLoader.getSetupImgLoader(setupToShow).getFloatImage(timepoint, 0, false);
+ ImageJFunctions.show(uncorrected, "1. Uncorrected - Setup " + setupToShow + " (tile " + tileId + ")");
+
+ // 4b. Show CORRECTED (full image, level 0)
+ System.out.println("Loading corrected image...");
+ final RandomAccessibleInterval corrected =
+ correctedLoader.getSetupImgLoader(setupToShow).getFloatImage(timepoint, 0, false);
+ ImageJFunctions.show(corrected, "2. Corrected - Setup " + setupToShow + " (tile " + tileId + ")");
+
+ // 4c. Show CORRECTED + SPLIT (left half, level 0)
+ System.out.println("Loading corrected + split (left half)...");
+ final RandomAccessibleInterval splitLeft =
+ splitLoader.getSetupImgLoader(100).getFloatImage(timepoint, 0, false);
+ ImageJFunctions.show(splitLeft, "3. Corrected+Split LEFT - Setup 100");
+
+ // 4d. Show CORRECTED + SPLIT (right half, level 0)
+ System.out.println("Loading corrected + split (right half)...");
+ final RandomAccessibleInterval splitRight =
+ splitLoader.getSetupImgLoader(101).getFloatImage(timepoint, 0, false);
+ ImageJFunctions.show(splitRight, "4. Corrected+Split RIGHT - Setup 101");
+
+ // ========== Summary ==========
+ System.out.println("\n=== DECORATOR CHAIN SUMMARY ===");
+ System.out.println("Layer 1 (innermost): " + baseLoader.getClass().getSimpleName());
+ System.out.println("Layer 2 (middle): " + correctedLoader.getClass().getSimpleName());
+ System.out.println("Layer 3 (outermost): " + splitLoader.getClass().getSimpleName());
+ System.out.println();
+ System.out.println("Data flow:");
+ System.out.println(" Request for split region 100 or 101");
+ System.out.println(" → SplitMultiResolutionImgLoader maps to setup " + setupToShow + " with interval");
+ System.out.println(" → MultiResolutionFlatfieldCorrectionWrappedImgLoader applies correction");
+ System.out.println(" → Base loader fetches raw pixels from N5");
+ System.out.println(" → Corrected pixels flow back up through the chain");
+ System.out.println(" → Split interval is extracted and returned");
+ System.out.println();
+ System.out.println("Compare the images to verify:");
+ System.out.println(" - Image 1 vs 2: See flatfield correction effect");
+ System.out.println(" - Image 2 vs 3+4: Verify split regions match the corrected full image");
+ System.out.println();
+ System.out.println("Tip: Use Image > Adjust > Brightness/Contrast (Ctrl+Shift+C)");
+ }
+}
diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestFlatfieldCorrection.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestFlatfieldCorrection.java
new file mode 100644
index 000000000..cb996338b
--- /dev/null
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestFlatfieldCorrection.java
@@ -0,0 +1,116 @@
+/*-
+ * #%L
+ * Software for the reconstruction of multi-view microscopic acquisitions
+ * like Selective Plane Illumination Microscopy (SPIM) Data.
+ * %%
+ * Copyright (C) 2012 - 2025 Multiview Reconstruction developers.
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield;
+
+import ij.ImageJ;
+import mpicbg.spim.data.SpimDataException;
+import mpicbg.spim.data.sequence.ImgLoader;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.img.display.imagej.ImageJFunctions;
+import net.imglib2.type.numeric.real.FloatType;
+import net.preibisch.mvrecon.fiji.spimdata.SpimData2;
+import net.preibisch.mvrecon.fiji.spimdata.XmlIoSpimData2;
+
+/**
+ * Test class for XML-based on-the-fly flatfield/darkfield correction.
+ *
+ * Loads dataset_corrected.xml which has flatfield correction configured
+ * directly in the ImageLoader section. No manual configuration needed!
+ *
+ * The XML wraps the N5 loader with MultiResolutionFlatfieldCorrectionWrappedImgLoader
+ * and specifies bright/dark images for each view setup.
+ */
+public class TestFlatfieldCorrection {
+
+ public static void main(String[] args) throws SpimDataException {
+ // Paths
+ final String basePath = "/Users/innerbergerm/Projects/janelia/multiview-reconstruction/";
+ final String correctedXmlPath = basePath + "data/dataset_corrected.xml";
+ final String uncorrectedXmlPath = basePath + "data/dataset.xml";
+
+ // Which setup to display (0-8)
+ final int setupToShow = 0;
+ final int timepoint = 0;
+
+ // ========== Load CORRECTED dataset (from XML with flatfield config) ==========
+ System.out.println("=== Loading CORRECTED dataset ===");
+ System.out.println("XML path: " + correctedXmlPath);
+
+ final SpimData2 correctedData = new XmlIoSpimData2().load(correctedXmlPath);
+ final ImgLoader correctedImgLoader = correctedData.getSequenceDescription().getImgLoader();
+
+ System.out.println("ImgLoader type: " + correctedImgLoader.getClass().getSimpleName());
+
+ // Verify it's a flatfield-corrected loader
+ if (correctedImgLoader instanceof FlatfieldCorrectionWrappedImgLoader) {
+ final FlatfieldCorrectionWrappedImgLoader> ffcLoader =
+ (FlatfieldCorrectionWrappedImgLoader>) correctedImgLoader;
+ System.out.println(" Correction active: " + ffcLoader.isActive());
+ System.out.println(" Caching enabled: " + ffcLoader.isCached());
+ System.out.println(" Wrapped loader: " + ffcLoader.getWrappedImgLoder().getClass().getSimpleName());
+ }
+
+ // ========== Load UNCORRECTED dataset (original XML) ==========
+ System.out.println("\n=== Loading UNCORRECTED dataset ===");
+ System.out.println("XML path: " + uncorrectedXmlPath);
+
+ final SpimData2 uncorrectedData = new XmlIoSpimData2().load(uncorrectedXmlPath);
+ final ImgLoader uncorrectedImgLoader = uncorrectedData.getSequenceDescription().getImgLoader();
+
+ System.out.println("ImgLoader type: " + uncorrectedImgLoader.getClass().getSimpleName());
+
+ // ========== Display images for comparison ==========
+ new ImageJ();
+
+ // Get tile ID from ViewSetup metadata (no hardcoded mapping needed!)
+ final int tileId = correctedData.getSequenceDescription()
+ .getViewSetups().get(setupToShow).getTile().getId();
+
+ System.out.println("\n=== Displaying setup " + setupToShow + " (tile " + tileId + ") ===");
+ System.out.println(" Dimensions: " + correctedData.getSequenceDescription()
+ .getViewSetups().get(setupToShow).getSize());
+
+ // Load and display UNCORRECTED image
+ System.out.println("Loading uncorrected image...");
+ final RandomAccessibleInterval uncorrected =
+ uncorrectedImgLoader.getSetupImgLoader(setupToShow).getFloatImage(timepoint, false);
+ ImageJFunctions.show(uncorrected, "1. Uncorrected - Setup " + setupToShow + " (tile " + tileId + ")");
+
+ // Load and display CORRECTED image
+ System.out.println("Loading corrected image...");
+ final RandomAccessibleInterval corrected =
+ correctedImgLoader.getSetupImgLoader(setupToShow).getFloatImage(timepoint, false);
+ ImageJFunctions.show(corrected, "2. Corrected - Setup " + setupToShow + " (tile " + tileId + ")");
+
+ // ========== Summary ==========
+ System.out.println("\n=== SUMMARY ===");
+ System.out.println("Flatfield correction is now configured in the XML!");
+ System.out.println("No manual setBrightImage()/setDarkImage() calls needed.");
+ System.out.println();
+ System.out.println("Compare the two images to verify correction:");
+ System.out.println(" - Image 1: Raw data from N5");
+ System.out.println(" - Image 2: Corrected with flatfield/darkfield");
+ System.out.println();
+ System.out.println("Tip: Use Image > Adjust > Brightness/Contrast (Ctrl+Shift+C)");
+ }
+}
diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestViewerFlatfieldCorrection.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestViewerFlatfieldCorrection.java
new file mode 100644
index 000000000..661ace0da
--- /dev/null
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/TestViewerFlatfieldCorrection.java
@@ -0,0 +1,231 @@
+/*-
+ * #%L
+ * Software for the reconstruction of multi-view microscopic acquisitions
+ * like Selective Plane Illumination Microscopy (SPIM) Data.
+ * %%
+ * Copyright (C) 2012 - 2025 Multiview Reconstruction developers.
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield;
+
+import java.util.HashMap;
+
+import bdv.ViewerImgLoader;
+import bdv.ViewerSetupImgLoader;
+import ij.ImageJ;
+import mpicbg.spim.data.SpimDataException;
+import mpicbg.spim.data.sequence.MultiResolutionSetupImgLoader;
+import mpicbg.spim.data.sequence.SequenceDescription;
+import mpicbg.spim.data.sequence.ViewSetup;
+import net.imglib2.FinalInterval;
+import net.imglib2.Interval;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.img.display.imagej.ImageJFunctions;
+import net.imglib2.type.numeric.real.FloatType;
+import net.preibisch.mvrecon.fiji.spimdata.SpimData2;
+import net.preibisch.mvrecon.fiji.spimdata.XmlIoSpimData2;
+import net.preibisch.mvrecon.fiji.spimdata.imgloaders.splitting.SplitViewerImgLoader;
+
+/**
+ * Test class for XML-based ViewerFlatfieldCorrectionWrappedImgLoader.
+ *
+ * Demonstrates the full ViewerImgLoader-compatible decorator chain:
+ * N5ImageLoader (ViewerImgLoader)
+ * -> ViewerFlatfieldCorrectionWrappedImgLoader (ViewerImgLoader)
+ * -> SplitViewerImgLoader (ViewerImgLoader)
+ *
+ * Loads dataset_corrected_viewer.xml which has flatfield correction configured
+ * directly in the ImageLoader section. No manual configuration needed!
+ *
+ * This maintains full BDV compatibility throughout the chain, including:
+ * - Cache control delegation
+ * - Volatile image support
+ * - Multi-resolution mipmap levels
+ */
+public class TestViewerFlatfieldCorrection {
+ public static void main(String[] args) throws SpimDataException {
+ // Paths
+ final String basePath = "/Users/innerbergerm/Projects/janelia/multiview-reconstruction/";
+ final String correctedXmlPath = basePath + "data/dataset_corrected_zarr.xml";
+ final String uncorrectedXmlPath = basePath + "data/dataset.xml";
+
+ // Which setup to demonstrate (0-8)
+ final int setupToShow = 0;
+ final int timepoint = 0;
+
+ // ========== STEP 1: Load CORRECTED dataset (from XML with flatfield config) ==========
+ System.out.println("=== STEP 1: Loading CORRECTED dataset ===");
+ System.out.println("XML path: " + correctedXmlPath);
+
+ final SpimData2 correctedData = new XmlIoSpimData2().load(correctedXmlPath);
+ final SequenceDescription correctedSeqDesc = correctedData.getSequenceDescription();
+
+ // Verify it's a ViewerFlatfieldCorrectionWrappedImgLoader
+ if (!(correctedSeqDesc.getImgLoader() instanceof ViewerFlatfieldCorrectionWrappedImgLoader)) {
+ System.err.println("ERROR: Expected ViewerFlatfieldCorrectionWrappedImgLoader!");
+ System.err.println("Loader type: " + correctedSeqDesc.getImgLoader().getClass().getName());
+ return;
+ }
+
+ final ViewerFlatfieldCorrectionWrappedImgLoader correctedLoader =
+ (ViewerFlatfieldCorrectionWrappedImgLoader) correctedSeqDesc.getImgLoader();
+
+ System.out.println("Corrected loader type: " + correctedLoader.getClass().getSimpleName());
+ System.out.println(" Correction active: " + correctedLoader.isActive());
+ System.out.println(" Caching enabled: " + correctedLoader.isCached());
+ System.out.println(" Wrapped loader: " + correctedLoader.getWrappedImgLoader().getClass().getSimpleName());
+ System.out.println(" Implements ViewerImgLoader: " + (correctedLoader instanceof ViewerImgLoader ? "YES" : "NO"));
+
+ // ========== STEP 2: Load UNCORRECTED dataset (original XML) ==========
+ System.out.println("\n=== STEP 2: Loading UNCORRECTED dataset ===");
+ System.out.println("XML path: " + uncorrectedXmlPath);
+
+ final SpimData2 uncorrectedData = new XmlIoSpimData2().load(uncorrectedXmlPath);
+ final SequenceDescription uncorrectedSeqDesc = uncorrectedData.getSequenceDescription();
+
+ // Verify the base loader is a ViewerImgLoader
+ if (!(uncorrectedSeqDesc.getImgLoader() instanceof ViewerImgLoader)) {
+ System.err.println("ERROR: Base loader is not a ViewerImgLoader!");
+ System.err.println("Loader type: " + uncorrectedSeqDesc.getImgLoader().getClass().getName());
+ return;
+ }
+
+ final ViewerImgLoader uncorrectedLoader = (ViewerImgLoader) uncorrectedSeqDesc.getImgLoader();
+ System.out.println("Uncorrected loader type: " + uncorrectedLoader.getClass().getSimpleName());
+
+ // ========== STEP 3: Create SplitViewerImgLoader wrapping the corrected loader ==========
+ System.out.println("\n=== STEP 3: Creating SplitViewerImgLoader ===");
+
+ // Get original image dimensions for the setup we're demonstrating
+ final ViewSetup vs = correctedSeqDesc.getViewSetups().get(setupToShow);
+ final long[] dims = new long[3];
+ vs.getSize().dimensions(dims);
+ System.out.println("Original image size: " + dims[0] + " x " + dims[1] + " x " + dims[2]);
+
+ // Create a simple 2x1 split in X dimension
+ final long splitX = dims[0] / 2;
+
+ // Define the mappings for split regions
+ final HashMap new2oldSetupId = new HashMap<>();
+ final HashMap newSetupId2Interval = new HashMap<>();
+
+ // Split region 0: left half
+ new2oldSetupId.put(100, setupToShow);
+ newSetupId2Interval.put(100, new FinalInterval(
+ new long[] {0, 0, 0},
+ new long[] {splitX - 1, dims[1] - 1, dims[2] - 1}
+ ));
+
+ // Split region 1: right half
+ new2oldSetupId.put(101, setupToShow);
+ newSetupId2Interval.put(101, new FinalInterval(
+ new long[] {splitX, 0, 0},
+ new long[] {dims[0] - 1, dims[1] - 1, dims[2] - 1}
+ ));
+
+ System.out.println("Created 2 split regions:");
+ System.out.println(" Setup 100: X=[0, " + (splitX-1) + "] (left half)");
+ System.out.println(" Setup 101: X=[" + splitX + ", " + (dims[0]-1) + "] (right half)");
+
+ // Create the split loader wrapping the CORRECTED ViewerImgLoader
+ final SplitViewerImgLoader splitLoader = new SplitViewerImgLoader(
+ correctedLoader, // <-- ViewerImgLoader compatible!
+ new2oldSetupId,
+ newSetupId2Interval,
+ correctedSeqDesc
+ );
+
+ System.out.println("Split loader implements ViewerImgLoader: " +
+ (splitLoader instanceof ViewerImgLoader ? "YES" : "NO"));
+
+ // ========== STEP 4: Test ViewerImgLoader-specific features ==========
+ System.out.println("\n=== STEP 4: Testing ViewerImgLoader features ===");
+
+ // Test cache control delegation
+ System.out.println("Cache control available: " + (splitLoader.getCacheControl() != null));
+
+ // Test mipmap levels
+ final ViewerSetupImgLoader, ?> setupImgLoader = splitLoader.getSetupImgLoader(100);
+ System.out.println("Number of mipmap levels: " + setupImgLoader.numMipmapLevels());
+
+ final double[][] resolutions = setupImgLoader.getMipmapResolutions();
+ System.out.println("Mipmap resolutions:");
+ for (int level = 0; level < resolutions.length; level++) {
+ System.out.println(" Level " + level + ": " +
+ resolutions[level][0] + " x " + resolutions[level][1] + " x " + resolutions[level][2]);
+ }
+
+ // ========== STEP 5: Display comparison images ==========
+ System.out.println("\n=== STEP 5: Displaying images ===");
+ new ImageJ();
+
+ // Get tile ID from ViewSetup metadata
+ final int tileId = correctedData.getSequenceDescription()
+ .getViewSetups().get(setupToShow).getTile().getId();
+
+ // 5a. Show UNCORRECTED original at level 0
+ System.out.println("Loading uncorrected image (level 0)...");
+ @SuppressWarnings("unchecked")
+ final MultiResolutionSetupImgLoader uncorrectedSetupLoader =
+ (MultiResolutionSetupImgLoader) uncorrectedLoader.getSetupImgLoader(setupToShow);
+ final RandomAccessibleInterval uncorrected =
+ uncorrectedSetupLoader.getFloatImage(timepoint, 0, false);
+ ImageJFunctions.show(uncorrected, "1. Uncorrected - Setup " + setupToShow + " (tile " + tileId + ")");
+
+ // 5b. Show CORRECTED at level 0
+ System.out.println("Loading corrected image (level 0)...");
+ final RandomAccessibleInterval corrected =
+ correctedLoader.getSetupImgLoader(setupToShow).getFloatImage(timepoint, 0, false);
+ ImageJFunctions.show(corrected, "2. Corrected - Setup " + setupToShow + " (tile " + tileId + ")");
+
+ // 5c. Show CORRECTED + SPLIT (left half) at level 0
+ System.out.println("Loading corrected + split (left half, level 0)...");
+ final RandomAccessibleInterval splitLeft =
+ splitLoader.getSetupImgLoader(100).getFloatImage(timepoint, 0, false);
+ ImageJFunctions.show(splitLeft, "3. Corrected+Split LEFT - Setup 100");
+
+ // 5d. Show at different mipmap level if available
+ if (setupImgLoader.numMipmapLevels() > 1) {
+ System.out.println("Loading corrected + split (left half, level 1)...");
+ final RandomAccessibleInterval splitLeftLevel1 =
+ splitLoader.getSetupImgLoader(100).getFloatImage(timepoint, 1, false);
+ ImageJFunctions.show(splitLeftLevel1, "4. Corrected+Split LEFT (Level 1) - Setup 100");
+ }
+
+ // ========== Summary ==========
+ System.out.println("\n=== VIEWERIMGLOADER CHAIN SUMMARY ===");
+ System.out.println("Layer 1 (innermost): " + correctedLoader.getWrappedImgLoader().getClass().getSimpleName() + " [ViewerImgLoader]");
+ System.out.println("Layer 2 (middle): " + correctedLoader.getClass().getSimpleName() + " [ViewerImgLoader]");
+ System.out.println("Layer 3 (outermost): " + splitLoader.getClass().getSimpleName() + " [ViewerImgLoader]");
+ System.out.println();
+ System.out.println("Flatfield correction is now configured in the XML!");
+ System.out.println("No manual setBrightImage()/setDarkImage() calls needed.");
+ System.out.println();
+ System.out.println("All layers maintain ViewerImgLoader compatibility:");
+ System.out.println(" - Cache control: delegated through chain");
+ System.out.println(" - Volatile images: supported at all levels");
+ System.out.println(" - Multi-resolution: " + setupImgLoader.numMipmapLevels() + " mipmap levels available");
+ System.out.println();
+ System.out.println("Compare the images to verify:");
+ System.out.println(" - Image 1 vs 2: See flatfield correction effect");
+ System.out.println(" - Image 2 vs 3: Verify split region matches corrected full image");
+ if (setupImgLoader.numMipmapLevels() > 1)
+ System.out.println(" - Image 3 vs 4: Compare different mipmap levels");
+ System.out.println();
+ System.out.println("Tip: Use Image > Adjust > Brightness/Contrast (Ctrl+Shift+C)");
+ }
+}
diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/ViewerFlatfieldCorrectionWrappedImgLoader.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/ViewerFlatfieldCorrectionWrappedImgLoader.java
new file mode 100644
index 000000000..7c8a58321
--- /dev/null
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/ViewerFlatfieldCorrectionWrappedImgLoader.java
@@ -0,0 +1,441 @@
+/*-
+ * #%L
+ * Software for the reconstruction of multi-view microscopic acquisitions
+ * like Selective Plane Illumination Microscopy (SPIM) Data.
+ * %%
+ * Copyright (C) 2012 - 2025 Multiview Reconstruction developers.
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield;
+
+import java.io.File;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import bdv.ViewerImgLoader;
+import bdv.ViewerSetupImgLoader;
+import bdv.cache.CacheControl;
+import mpicbg.spim.data.generic.sequence.ImgLoaderHint;
+import mpicbg.spim.data.generic.sequence.ImgLoaderHints;
+import mpicbg.spim.data.sequence.MultiResolutionImgLoader;
+import mpicbg.spim.data.sequence.MultiResolutionSetupImgLoader;
+import mpicbg.spim.data.sequence.ViewId;
+import mpicbg.spim.data.sequence.VoxelDimensions;
+import net.imglib2.Dimensions;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.Volatile;
+import net.imglib2.converter.RealTypeConverters;
+import net.imglib2.img.Img;
+import net.imglib2.img.ImgFactory;
+import net.imglib2.img.array.ArrayImgFactory;
+import net.imglib2.img.cell.CellImgFactory;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.NativeType;
+import net.imglib2.type.numeric.RealType;
+import net.imglib2.type.numeric.real.FloatType;
+import net.imglib2.util.Pair;
+import net.imglib2.util.ValuePair;
+import net.imglib2.view.Views;
+import net.preibisch.mvrecon.process.fusion.FusionTools;
+
+/**
+ * Flatfield correction wrapper for ViewerImgLoader.
+ *
+ * This class wraps a ViewerImgLoader and applies flatfield (bright/dark image) correction
+ * on-the-fly. It implements both ViewerImgLoader and MultiResolutionImgLoader interfaces,
+ * making it compatible with BigDataViewer's caching and async loading infrastructure.
+ *
+ * The correction formula is:
+ * corrected = (source - dark) * mean(bright - dark) / (bright - dark)
+ *
+ * Usage in decorator chain:
+ * N5ImageLoader (ViewerImgLoader)
+ * -> ViewerFlatfieldCorrectionWrappedImgLoader (ViewerImgLoader)
+ * -> SplitViewerImgLoader (ViewerImgLoader)
+ */
+public class ViewerFlatfieldCorrectionWrappedImgLoader
+ implements ViewerImgLoader, MultiResolutionImgLoader {
+
+ private final ViewerImgLoader wrappedImgLoader;
+ private boolean active;
+ private boolean cacheResult;
+
+ /** Helper for loading flatfield images */
+ private final FlatfieldImageLoader imageLoader;
+
+ /** Downsampled bright/dark images for each mipmap level */
+ private final Map>, RandomAccessibleInterval> dsRaiMap;
+
+ public ViewerFlatfieldCorrectionWrappedImgLoader(final ViewerImgLoader wrappedImgLoader) {
+ this(wrappedImgLoader, true);
+ }
+
+ public ViewerFlatfieldCorrectionWrappedImgLoader(final ViewerImgLoader wrappedImgLoader, final boolean cacheResult) {
+ this.wrappedImgLoader = wrappedImgLoader;
+ this.active = true;
+ this.cacheResult = cacheResult;
+ this.imageLoader = new FlatfieldImageLoader();
+ this.dsRaiMap = new HashMap<>();
+ }
+
+ // ========== Configuration methods ==========
+
+ public ViewerImgLoader getWrappedImgLoader() {
+ return wrappedImgLoader;
+ }
+
+ public void setActive(final boolean active) {
+ this.active = active;
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public boolean isCached() {
+ return cacheResult;
+ }
+
+ public void setCached(final boolean cached) {
+ this.cacheResult = cached;
+ }
+
+ public void setBrightImage(final ViewId vId, final URI imgUri) {
+ imageLoader.setBrightImage(vId, imgUri);
+ }
+
+ public void setDarkImage(final ViewId vId, final URI imgUri) {
+ imageLoader.setDarkImage(vId, imgUri);
+ }
+
+ public void setBrightImage(final ViewId vId, final File imgFile) {
+ imageLoader.setBrightImage(vId, imgFile);
+ }
+
+ public void setDarkImage(final ViewId vId, final File imgFile) {
+ imageLoader.setDarkImage(vId, imgFile);
+ }
+
+ /**
+ * Get the URI map for bright/dark images per view.
+ * @return map from ViewId to (brightUri, darkUri) pair
+ */
+ public Map> getUriMap() {
+ return imageLoader.getUriMap();
+ }
+
+ // ========== ViewerImgLoader interface ==========
+
+ @Override
+ public ViewerFlatfieldCorrectionWrappedSetupImgLoader, ?> getSetupImgLoader(final int setupId) {
+ return new ViewerFlatfieldCorrectionWrappedSetupImgLoader<>(setupId);
+ }
+
+ @Override
+ public CacheControl getCacheControl() {
+ return wrappedImgLoader.getCacheControl();
+ }
+
+ @Override
+ public void setNumFetcherThreads(final int n) {
+ wrappedImgLoader.setNumFetcherThreads(n);
+ }
+
+ // ========== Image loading helpers ==========
+
+ protected RandomAccessibleInterval getBrightImg(final ViewId vId) {
+ return imageLoader.getBrightImg(vId);
+ }
+
+ protected RandomAccessibleInterval getDarkImg(final ViewId vId) {
+ return imageLoader.getDarkImg(vId);
+ }
+
+ protected RandomAccessibleInterval getOrCreateBrightImgDownsampled(
+ final ViewId vId,
+ final int[] downsamplingFactors
+ ) {
+ return getOrCreateDownsampledImg(vId, downsamplingFactors, Pair::getA, this::getBrightImg);
+ }
+
+ protected RandomAccessibleInterval getOrCreateDarkImgDownsampled(
+ final ViewId vId,
+ final int[] downsamplingFactors
+ ) {
+ return getOrCreateDownsampledImg(vId, downsamplingFactors, Pair::getB, this::getDarkImg);
+ }
+
+ /**
+ * Generic method to get a downsampled image or do downsampling on the fly. The bright image
+ * is stored in the A element of the pair, the dark image in B.
+ */
+ private RandomAccessibleInterval getOrCreateDownsampledImg(
+ ViewId vId,
+ int[] downsamplingFactors,
+ Function, URI> uriSelector,
+ Function> imgGetter
+ ) {
+ // Convert to a list here to have a proper hash code for the map key
+ List dsFactorList = Arrays.stream(downsamplingFactors).boxed().collect(Collectors.toList());
+ final ValuePair> key = new ValuePair<>(uriSelector.apply(imageLoader.getUriMap().get(vId)), dsFactorList);
+
+ if (!dsRaiMap.containsKey(key)) {
+ final RandomAccessibleInterval img = imgGetter.apply(vId);
+
+ if (img == null)
+ return null;
+
+ final RandomAccessibleInterval downsampled =
+ MultiResolutionFlatfieldCorrectionWrappedImgLoader.downsampleHDF5(img, downsamplingFactors);
+ dsRaiMap.put(key, downsampled);
+ }
+
+ return dsRaiMap.get(key);
+ }
+
+ // ========== Inner class: ViewerSetupImgLoader implementation ==========
+
+ public class ViewerFlatfieldCorrectionWrappedSetupImgLoader & NativeType, V extends Volatile & RealType & NativeType>
+ implements ViewerSetupImgLoader, MultiResolutionSetupImgLoader {
+ private final int setupId;
+
+ ViewerFlatfieldCorrectionWrappedSetupImgLoader(final int setupId) {
+ this.setupId = setupId;
+ }
+
+ @SuppressWarnings("unchecked")
+ private ViewerSetupImgLoader getUnderlyingViewerSetupImgLoader() {
+ return (ViewerSetupImgLoader) wrappedImgLoader.getSetupImgLoader(setupId);
+ }
+
+ @SuppressWarnings("unchecked")
+ private MultiResolutionSetupImgLoader getUnderlyingMultiResSetupImgLoader() {
+ // The wrapped ViewerImgLoader should also be a MultiResolutionImgLoader
+ return (MultiResolutionSetupImgLoader) ((MultiResolutionImgLoader) wrappedImgLoader).getSetupImgLoader(setupId);
+ }
+
+ // ========== Regular image access ==========
+
+ @Override
+ public RandomAccessibleInterval getImage(final int timepointId, final ImgLoaderHint... hints) {
+ return getImage(timepointId, 0, hints);
+ }
+
+ @Override
+ public RandomAccessibleInterval getImage(final int timepointId, final int level, final ImgLoaderHint... hints) {
+ final ViewerSetupImgLoader viewerSetupIL = getUnderlyingViewerSetupImgLoader();
+ final MultiResolutionSetupImgLoader multiResSetupIL = getUnderlyingMultiResSetupImgLoader();
+
+ if (!active)
+ return viewerSetupIL.getImage(timepointId, level, hints);
+
+ final int n = multiResSetupIL.getImageSize(timepointId).numDimensions();
+
+ // Calculate downsampling factors for this mipmap level
+ final int[] dsFactors = new int[n];
+ final double[] dsD = viewerSetupIL.getMipmapResolutions()[level];
+ for (int d = 0; d < n; d++)
+ dsFactors[d] = (int) dsD[d];
+ // Don't downsample z for 2D correction images
+ dsFactors[n - 1] = 1;
+
+ RandomAccessibleInterval rai = FlatFieldCorrectedRandomAccessibleIntervals.create(
+ viewerSetupIL.getImage(timepointId, level, hints),
+ getOrCreateBrightImgDownsampled(new ViewId(timepointId, setupId), dsFactors),
+ getOrCreateDarkImgDownsampled(new ViewId(timepointId, setupId), dsFactors));
+
+ // Handle LOAD_COMPLETELY hint
+ boolean loadCompletelyRequested = false;
+ for (final ImgLoaderHint hint : hints)
+ if (hint == ImgLoaderHints.LOAD_COMPLETELY) {
+ loadCompletelyRequested = true;
+ break;
+ }
+
+ if (loadCompletelyRequested) {
+ long numPx = 1;
+ for (int d = 0; d < rai.numDimensions(); d++)
+ numPx *= rai.dimension(d);
+
+ final ImgFactory imgFactory;
+ if (Math.log(numPx) / Math.log(2) < 31) {
+ imgFactory = new ArrayImgFactory<>(getImageType());
+ } else {
+ imgFactory = new CellImgFactory<>(getImageType());
+ }
+
+ final Img loadedImg = imgFactory.create(rai);
+ RealTypeConverters.copyFromTo(Views.extendZero(rai), loadedImg);
+
+ rai = loadedImg;
+ } else if (cacheResult) {
+ final int[] cellSize = new int[rai.numDimensions()];
+ Arrays.fill(cellSize, 1);
+ for (int d = 0; d < rai.numDimensions() - 1; d++)
+ cellSize[d] = (int) rai.dimension(d);
+ rai = FusionTools.cacheRandomAccessibleInterval(
+ rai, Long.MAX_VALUE, rai.firstElement().createVariable(), cellSize);
+ }
+
+ return rai;
+ }
+
+ // ========== Volatile image access ==========
+
+ @Override
+ public RandomAccessibleInterval getVolatileImage(final int timepointId, final int level, final ImgLoaderHint... hints) {
+ final ViewerSetupImgLoader viewerSetupIL = getUnderlyingViewerSetupImgLoader();
+ final MultiResolutionSetupImgLoader multiResSetupIL = getUnderlyingMultiResSetupImgLoader();
+
+ if (!active)
+ return viewerSetupIL.getVolatileImage(timepointId, level, hints);
+
+ final int n = multiResSetupIL.getImageSize(timepointId).numDimensions();
+
+ // Calculate downsampling factors for this mipmap level
+ final int[] dsFactors = new int[n];
+ final double[] dsD = viewerSetupIL.getMipmapResolutions()[level];
+ for (int d = 0; d < n; d++)
+ dsFactors[d] = (int) dsD[d];
+ dsFactors[n - 1] = 1;
+
+ // Apply correction to volatile image
+ // Note: The volatile validity flag propagation may not be perfect,
+ // but BDV will re-request invalid pixels automatically
+ return FlatFieldCorrectedRandomAccessibleIntervals.create(
+ viewerSetupIL.getVolatileImage(timepointId, level, hints),
+ getOrCreateBrightImgDownsampled(new ViewId(timepointId, setupId), dsFactors),
+ getOrCreateDarkImgDownsampled(new ViewId(timepointId, setupId), dsFactors),
+ getVolatileImageType());
+ }
+
+ // ========== Float image access ==========
+
+ @Override
+ public RandomAccessibleInterval getFloatImage(final int timepointId, final boolean normalize, final ImgLoaderHint... hints) {
+ return getFloatImage(timepointId, 0, normalize, hints);
+ }
+
+ @Override
+ public RandomAccessibleInterval getFloatImage(final int timepointId, final int level, final boolean normalize, final ImgLoaderHint... hints) {
+ final ViewerSetupImgLoader viewerSetupIL = getUnderlyingViewerSetupImgLoader();
+ final MultiResolutionSetupImgLoader multiResSetupIL = getUnderlyingMultiResSetupImgLoader();
+
+ if (!active)
+ return multiResSetupIL.getFloatImage(timepointId, level, normalize, hints);
+
+ final int n = multiResSetupIL.getImageSize(timepointId).numDimensions();
+
+ final int[] dsFactors = new int[n];
+ final double[] dsD = viewerSetupIL.getMipmapResolutions()[level];
+ for (int d = 0; d < n; d++)
+ dsFactors[d] = (int) dsD[d];
+ dsFactors[n - 1] = 1;
+
+ RandomAccessibleInterval rai = FlatFieldCorrectedRandomAccessibleIntervals.create(
+ viewerSetupIL.getImage(timepointId, level, hints),
+ getOrCreateBrightImgDownsampled(new ViewId(timepointId, setupId), dsFactors),
+ getOrCreateDarkImgDownsampled(new ViewId(timepointId, setupId), dsFactors),
+ new FloatType());
+
+ if (normalize) {
+ rai = new VirtuallyNormalizedRandomAccessibleInterval<>(rai);
+ }
+
+ // Handle caching/loading
+ boolean loadCompletelyRequested = false;
+ for (final ImgLoaderHint hint : hints)
+ if (hint == ImgLoaderHints.LOAD_COMPLETELY) {
+ loadCompletelyRequested = true;
+ break;
+ }
+
+ if (loadCompletelyRequested) {
+ long numPx = 1;
+ for (int d = 0; d < rai.numDimensions(); d++)
+ numPx *= rai.dimension(d);
+
+ final ImgFactory imgFactory;
+ if (Math.log(numPx) / Math.log(2) < 31)
+ imgFactory = new ArrayImgFactory<>(new FloatType());
+ else
+ imgFactory = new CellImgFactory<>(new FloatType());
+
+ final Img loadedImg = imgFactory.create(rai);
+ RealTypeConverters.copyFromTo(Views.extendZero(rai), loadedImg);
+
+ rai = loadedImg;
+ } else if (cacheResult) {
+ final int[] cellSize = new int[rai.numDimensions()];
+ Arrays.fill(cellSize, 1);
+ for (int d = 0; d < rai.numDimensions() - 1; d++)
+ cellSize[d] = (int) rai.dimension(d);
+ rai = FusionTools.cacheRandomAccessibleInterval(rai, Long.MAX_VALUE,
+ new FloatType(), cellSize);
+ }
+
+ return rai;
+ }
+
+ // ========== Metadata delegation ==========
+
+ @Override
+ public T getImageType() {
+ return getUnderlyingViewerSetupImgLoader().getImageType();
+ }
+
+ @Override
+ public V getVolatileImageType() {
+ return getUnderlyingViewerSetupImgLoader().getVolatileImageType();
+ }
+
+ @Override
+ public double[][] getMipmapResolutions() {
+ return getUnderlyingViewerSetupImgLoader().getMipmapResolutions();
+ }
+
+ @Override
+ public AffineTransform3D[] getMipmapTransforms() {
+ return getUnderlyingViewerSetupImgLoader().getMipmapTransforms();
+ }
+
+ @Override
+ public int numMipmapLevels() {
+ return getUnderlyingViewerSetupImgLoader().numMipmapLevels();
+ }
+
+ @Override
+ public Dimensions getImageSize(final int timepointId) {
+ return getUnderlyingMultiResSetupImgLoader().getImageSize(timepointId);
+ }
+
+ @Override
+ public Dimensions getImageSize(final int timepointId, final int level) {
+ return getUnderlyingMultiResSetupImgLoader().getImageSize(timepointId, level);
+ }
+
+ @Override
+ public VoxelDimensions getVoxelSize(final int timepointId) {
+ return getUnderlyingMultiResSetupImgLoader().getVoxelSize(timepointId);
+ }
+ }
+}
diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/XmlIoFlatfieldCorrectedWrappedImgLoader.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/XmlIoFlatfieldCorrectedWrappedImgLoader.java
index 70e59c01f..f3d3c0e49 100644
--- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/XmlIoFlatfieldCorrectedWrappedImgLoader.java
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/XmlIoFlatfieldCorrectedWrappedImgLoader.java
@@ -9,12 +9,12 @@
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 2 of the
* License, or (at your option) any later version.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
- *
+ *
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* .
@@ -28,6 +28,7 @@
import static mpicbg.spim.data.XmlKeys.VIEWSETUP_TAG;
import java.io.File;
+import java.net.URI;
import java.util.Map;
import org.jdom2.DataConversionException;
@@ -44,12 +45,11 @@
import mpicbg.spim.data.sequence.MultiResolutionImgLoader;
import mpicbg.spim.data.sequence.ViewId;
import net.imglib2.util.Pair;
-import net.preibisch.legacy.io.IOFunctions;
@ImgLoaderIo(format = "spimreconstruction.wrapped.flatfield.default", type = DefaultFlatfieldCorrectionWrappedImgLoader.class)
public class XmlIoFlatfieldCorrectedWrappedImgLoader
- implements XmlIoBasicImgLoader< FlatfieldCorrectionWrappedImgLoader< ? extends ImgLoader > >
+ implements XmlIoBasicImgLoader>
{
public final static String WRAPPED_IMGLOADER_TAG = "WrappedImgLoader";
public final static String FLATFIELDS_TAG = "FlatFields";
@@ -60,17 +60,24 @@ public class XmlIoFlatfieldCorrectedWrappedImgLoader
public final static String CACHED_TAG = "Cached";
@Override
- public FlatfieldCorrectionWrappedImgLoader< ? extends ImgLoader > fromXml(Element elem, File basePath,
- AbstractSequenceDescription< ?, ?, ? > sequenceDescription)
+ public FlatfieldCorrectionWrappedImgLoader extends ImgLoader> fromXml(Element elem, File basePath,
+ AbstractSequenceDescription, ?, ?> sequenceDescription)
{
- Element wrappedImgLoaderEl = elem.getChild( WRAPPED_IMGLOADER_TAG ).getChild( IMGLOADER_TAG );
- XmlIoBasicImgLoader< ? > xmlIoWrapped = null;
+ return fromXml(elem, basePath == null ? null : basePath.toURI(), sequenceDescription);
+ }
+
+ @Override
+ public FlatfieldCorrectionWrappedImgLoader extends ImgLoader> fromXml(Element elem, URI basePathURI,
+ AbstractSequenceDescription, ?, ?> sequenceDescription)
+ {
+ Element wrappedImgLoaderEl = elem.getChild(WRAPPED_IMGLOADER_TAG).getChild(IMGLOADER_TAG);
+ XmlIoBasicImgLoader> xmlIoWrapped = null;
try
{
xmlIoWrapped = ImgLoaders
- .createXmlIoForFormat( wrappedImgLoaderEl.getAttributeValue( IMGLOADER_FORMAT_ATTRIBUTE_NAME ) );
+ .createXmlIoForFormat(wrappedImgLoaderEl.getAttributeValue(IMGLOADER_FORMAT_ATTRIBUTE_NAME));
}
- catch ( SpimDataInstantiationException e )
+ catch (SpimDataInstantiationException e)
{
e.printStackTrace();
return null;
@@ -80,92 +87,96 @@ public class XmlIoFlatfieldCorrectedWrappedImgLoader
boolean active = false;
try
{
- cached = elem.getAttribute( CACHED_TAG ).getBooleanValue();
- active = elem.getAttribute( ACTIVE_TAG ).getBooleanValue();
+ cached = elem.getAttribute(CACHED_TAG).getBooleanValue();
+ active = elem.getAttribute(ACTIVE_TAG).getBooleanValue();
}
- catch ( DataConversionException e )
+ catch (DataConversionException e)
{
e.printStackTrace();
}
- BasicImgLoader wrappedImgLoader = xmlIoWrapped.fromXml( wrappedImgLoaderEl, basePath, sequenceDescription );
+ BasicImgLoader wrappedImgLoader = xmlIoWrapped.fromXml(wrappedImgLoaderEl, basePathURI, sequenceDescription);
- FlatfieldCorrectionWrappedImgLoader< ? extends ImgLoader > res = null;
+ FlatfieldCorrectionWrappedImgLoader extends ImgLoader> res = null;
- if ( MultiResolutionImgLoader.class.isInstance( wrappedImgLoader ) )
- res = new MultiResolutionFlatfieldCorrectionWrappedImgLoader( (MultiResolutionImgLoader) wrappedImgLoader,
- cached );
- else if ( ImgLoader.class.isInstance( wrappedImgLoader ) )
- res = new DefaultFlatfieldCorrectionWrappedImgLoader( (ImgLoader) wrappedImgLoader, cached );
+ if (MultiResolutionImgLoader.class.isInstance(wrappedImgLoader))
+ res = new MultiResolutionFlatfieldCorrectionWrappedImgLoader((MultiResolutionImgLoader) wrappedImgLoader,
+ cached);
+ else if (ImgLoader.class.isInstance(wrappedImgLoader))
+ res = new DefaultFlatfieldCorrectionWrappedImgLoader((ImgLoader) wrappedImgLoader, cached);
else
return null;
- Element flatfields = elem.getChild( FLATFIELDS_TAG );
- for ( Element flatfield : flatfields.getChildren() )
+ Element flatfields = elem.getChild(FLATFIELDS_TAG);
+ for (Element flatfield : flatfields.getChildren())
{
- int tp = Integer.parseInt( flatfield.getAttributeValue( TIMEPOINTS_TIMEPOINT_TAG ) );
- int vs = Integer.parseInt( flatfield.getAttributeValue( VIEWSETUP_TAG ) );
- File brightImg = XmlHelpers.loadPath( flatfield, BRIGHTIMG_TAG, basePath );
- File darkImg = XmlHelpers.loadPath( flatfield, DARKIMG_TAG, basePath );
- res.setBrightImage( new ViewId( tp, vs ), brightImg );
- res.setDarkImage( new ViewId( tp, vs ), darkImg );
+ int tp = Integer.parseInt(flatfield.getAttributeValue(TIMEPOINTS_TIMEPOINT_TAG));
+ int vs = Integer.parseInt(flatfield.getAttributeValue(VIEWSETUP_TAG));
+ URI brightImg = XmlHelpers.loadPathURI(flatfield, BRIGHTIMG_TAG, basePathURI);
+ URI darkImg = XmlHelpers.loadPathURI(flatfield, DARKIMG_TAG, basePathURI);
+ res.setBrightImage(new ViewId(tp, vs), brightImg);
+ res.setDarkImage(new ViewId(tp, vs), darkImg);
}
- res.setActive( active );
+ res.setActive(active);
return res;
}
@Override
- public Element toXml(FlatfieldCorrectionWrappedImgLoader< ? extends ImgLoader > imgLoader, File basePath)
+ public Element toXml(FlatfieldCorrectionWrappedImgLoader extends ImgLoader> imgLoader, File basePath)
{
+ return toXml(imgLoader, basePath == null ? null : basePath.toURI());
+ }
- final Map< ViewId, Pair< File, File > > fileMap = ( (LazyLoadingFlatFieldCorrectionMap< ? extends ImgLoader >) imgLoader ).fileMap;
+ @Override
+ public Element toXml(FlatfieldCorrectionWrappedImgLoader extends ImgLoader> imgLoader, URI basePathURI)
+ {
+ final Map> uriMap = ((LazyLoadingFlatFieldCorrectionMap extends ImgLoader>) imgLoader).getUriMap();
- final Element wholeElem = new Element( IMGLOADER_TAG );
- wholeElem.setAttribute( IMGLOADER_FORMAT_ATTRIBUTE_NAME,
- this.getClass().getAnnotation( ImgLoaderIo.class ).format() );
- final Element wrappedIL = new Element( WRAPPED_IMGLOADER_TAG );
+ final Element wholeElem = new Element(IMGLOADER_TAG);
+ wholeElem.setAttribute(IMGLOADER_FORMAT_ATTRIBUTE_NAME,
+ this.getClass().getAnnotation(ImgLoaderIo.class).format());
+ final Element wrappedIL = new Element(WRAPPED_IMGLOADER_TAG);
- wholeElem.setAttribute( ACTIVE_TAG, Boolean.toString( imgLoader.isActive() ) );
- wholeElem.setAttribute( CACHED_TAG, Boolean.toString( imgLoader.isCached() ) );
+ wholeElem.setAttribute(ACTIVE_TAG, Boolean.toString(imgLoader.isActive()));
+ wholeElem.setAttribute(CACHED_TAG, Boolean.toString(imgLoader.isCached()));
try
{
- XmlIoBasicImgLoader< ImgLoader > loaderIO = (XmlIoBasicImgLoader< ImgLoader >) ImgLoaders
- .createXmlIoForImgLoaderClass( imgLoader.getWrappedImgLoder().getClass() );
- Element wrappedInner = loaderIO.toXml( (ImgLoader) imgLoader.getWrappedImgLoder(), basePath );
- wrappedIL.addContent( wrappedInner );
-
+ @SuppressWarnings("unchecked")
+ XmlIoBasicImgLoader loaderIO = (XmlIoBasicImgLoader) ImgLoaders
+ .createXmlIoForImgLoaderClass(imgLoader.getWrappedImgLoder().getClass());
+ Element wrappedInner = loaderIO.toXml((ImgLoader) imgLoader.getWrappedImgLoder(), basePathURI);
+ wrappedIL.addContent(wrappedInner);
}
- catch ( SpimDataInstantiationException e )
+ catch (SpimDataInstantiationException e)
{
e.printStackTrace();
return null;
}
- final Element elFlatfields = new Element( FLATFIELDS_TAG );
+ final Element elFlatfields = new Element(FLATFIELDS_TAG);
- for ( ViewId vid : fileMap.keySet() )
+ for (ViewId vid : uriMap.keySet())
{
- final Pair< File, File > files = fileMap.get( vid );
- if ( files == null || ( files.getA() == null && files.getB() == null ) )
+ final Pair uris = uriMap.get(vid);
+ if (uris == null || (uris.getA() == null && uris.getB() == null))
continue;
- final Element elFlatfield = new Element( FLATFIELD_TAG );
- elFlatfield.setAttribute( TIMEPOINTS_TIMEPOINT_TAG, Integer.toString( vid.getTimePointId() ) );
- elFlatfield.setAttribute( VIEWSETUP_TAG, Integer.toString( vid.getViewSetupId() ) );
+ final Element elFlatfield = new Element(FLATFIELD_TAG);
+ elFlatfield.setAttribute(TIMEPOINTS_TIMEPOINT_TAG, Integer.toString(vid.getTimePointId()));
+ elFlatfield.setAttribute(VIEWSETUP_TAG, Integer.toString(vid.getViewSetupId()));
- if ( files.getA() != null )
- elFlatfield.addContent( XmlHelpers.pathElement( BRIGHTIMG_TAG, files.getA(), basePath ) );
- if ( files.getB() != null )
- elFlatfield.addContent( XmlHelpers.pathElement( DARKIMG_TAG, files.getB(), basePath ) );
+ if (uris.getA() != null)
+ elFlatfield.addContent(XmlHelpers.pathElementURI(BRIGHTIMG_TAG, uris.getA(), basePathURI));
+ if (uris.getB() != null)
+ elFlatfield.addContent(XmlHelpers.pathElementURI(DARKIMG_TAG, uris.getB(), basePathURI));
- elFlatfields.addContent( elFlatfield );
+ elFlatfields.addContent(elFlatfield);
}
- wholeElem.addContent( wrappedIL );
- wholeElem.addContent( elFlatfields );
+ wholeElem.addContent(wrappedIL);
+ wholeElem.addContent(elFlatfields);
return wholeElem;
}
-
}
diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/XmlIoViewerFlatfieldCorrectionWrappedImgLoader.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/XmlIoViewerFlatfieldCorrectionWrappedImgLoader.java
new file mode 100644
index 000000000..230d75551
--- /dev/null
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/XmlIoViewerFlatfieldCorrectionWrappedImgLoader.java
@@ -0,0 +1,171 @@
+/*-
+ * #%L
+ * Software for the reconstruction of multi-view microscopic acquisitions
+ * like Selective Plane Illumination Microscopy (SPIM) Data.
+ * %%
+ * Copyright (C) 2012 - 2025 Multiview Reconstruction developers.
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield;
+
+import static mpicbg.spim.data.XmlKeys.IMGLOADER_FORMAT_ATTRIBUTE_NAME;
+import static mpicbg.spim.data.XmlKeys.IMGLOADER_TAG;
+import static mpicbg.spim.data.XmlKeys.TIMEPOINTS_TIMEPOINT_TAG;
+import static mpicbg.spim.data.XmlKeys.VIEWSETUP_TAG;
+
+import java.io.File;
+import java.net.URI;
+import java.util.Map;
+
+import org.jdom2.DataConversionException;
+import org.jdom2.Element;
+
+import bdv.ViewerImgLoader;
+import mpicbg.spim.data.SpimDataInstantiationException;
+import mpicbg.spim.data.XmlHelpers;
+import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription;
+import mpicbg.spim.data.generic.sequence.BasicImgLoader;
+import mpicbg.spim.data.generic.sequence.ImgLoaderIo;
+import mpicbg.spim.data.generic.sequence.ImgLoaders;
+import mpicbg.spim.data.generic.sequence.XmlIoBasicImgLoader;
+import mpicbg.spim.data.sequence.ViewId;
+import net.imglib2.util.Pair;
+
+/**
+ * XML I/O handler for ViewerFlatfieldCorrectionWrappedImgLoader.
+ *
+ * Registers format "spimreconstruction.wrapped.flatfield.viewer" for
+ * ViewerImgLoader-based flatfield correction wrappers.
+ */
+@ImgLoaderIo(format = "spimreconstruction.wrapped.flatfield.viewer", type = ViewerFlatfieldCorrectionWrappedImgLoader.class)
+public class XmlIoViewerFlatfieldCorrectionWrappedImgLoader
+ implements XmlIoBasicImgLoader {
+ public final static String WRAPPED_IMGLOADER_TAG = "WrappedImgLoader";
+ public final static String FLATFIELDS_TAG = "FlatFields";
+ public final static String FLATFIELD_TAG = "FlatField";
+ public final static String BRIGHTIMG_TAG = "BrightImg";
+ public final static String DARKIMG_TAG = "DarkImg";
+ public final static String ACTIVE_TAG = "Active";
+ public final static String CACHED_TAG = "Cached";
+
+ @Override
+ public ViewerFlatfieldCorrectionWrappedImgLoader fromXml(Element elem, File basePath,
+ AbstractSequenceDescription, ?, ?> sequenceDescription) {
+ return fromXml(elem, basePath == null ? null : basePath.toURI(), sequenceDescription);
+ }
+
+ @Override
+ public ViewerFlatfieldCorrectionWrappedImgLoader fromXml(Element elem, URI basePathURI,
+ AbstractSequenceDescription, ?, ?> sequenceDescription) {
+ Element wrappedImgLoaderEl = elem.getChild(WRAPPED_IMGLOADER_TAG).getChild(IMGLOADER_TAG);
+ XmlIoBasicImgLoader> xmlIoWrapped;
+ try {
+ xmlIoWrapped = ImgLoaders
+ .createXmlIoForFormat(wrappedImgLoaderEl.getAttributeValue(IMGLOADER_FORMAT_ATTRIBUTE_NAME));
+ } catch (SpimDataInstantiationException e) {
+ e.printStackTrace();
+ return null;
+ }
+
+ boolean cached = false;
+ boolean active = false;
+ try {
+ cached = elem.getAttribute(CACHED_TAG).getBooleanValue();
+ active = elem.getAttribute(ACTIVE_TAG).getBooleanValue();
+ } catch (DataConversionException e) {
+ e.printStackTrace();
+ }
+
+ BasicImgLoader wrappedImgLoader = xmlIoWrapped.fromXml(wrappedImgLoaderEl, basePathURI, sequenceDescription);
+
+ // Verify wrapped loader is a ViewerImgLoader
+ if (!(wrappedImgLoader instanceof ViewerImgLoader)) {
+ System.err.println("ViewerFlatfieldCorrectionWrappedImgLoader requires a ViewerImgLoader, but got: "
+ + wrappedImgLoader.getClass().getName());
+ return null;
+ }
+
+ ViewerFlatfieldCorrectionWrappedImgLoader res =
+ new ViewerFlatfieldCorrectionWrappedImgLoader((ViewerImgLoader) wrappedImgLoader, cached);
+
+ Element flatfields = elem.getChild(FLATFIELDS_TAG);
+ for (Element flatfield : flatfields.getChildren()) {
+ int tp = Integer.parseInt(flatfield.getAttributeValue(TIMEPOINTS_TIMEPOINT_TAG));
+ int vs = Integer.parseInt(flatfield.getAttributeValue(VIEWSETUP_TAG));
+ URI brightImg = XmlHelpers.loadPathURI(flatfield, BRIGHTIMG_TAG, basePathURI);
+ URI darkImg = XmlHelpers.loadPathURI(flatfield, DARKIMG_TAG, basePathURI);
+ res.setBrightImage(new ViewId(tp, vs), brightImg);
+ res.setDarkImage(new ViewId(tp, vs), darkImg);
+ }
+
+ res.setActive(active);
+ return res;
+ }
+
+ @Override
+ public Element toXml(ViewerFlatfieldCorrectionWrappedImgLoader imgLoader, File basePath) {
+ return toXml(imgLoader, basePath == null ? null : basePath.toURI());
+ }
+
+ @Override
+ public Element toXml(ViewerFlatfieldCorrectionWrappedImgLoader imgLoader, URI basePathURI) {
+ final Map> uriMap = imgLoader.getUriMap();
+
+ final Element wholeElem = new Element(IMGLOADER_TAG);
+ wholeElem.setAttribute(IMGLOADER_FORMAT_ATTRIBUTE_NAME,
+ this.getClass().getAnnotation(ImgLoaderIo.class).format());
+ final Element wrappedIL = new Element(WRAPPED_IMGLOADER_TAG);
+
+ wholeElem.setAttribute(ACTIVE_TAG, Boolean.toString(imgLoader.isActive()));
+ wholeElem.setAttribute(CACHED_TAG, Boolean.toString(imgLoader.isCached()));
+
+ try {
+ @SuppressWarnings({"rawtypes"})
+ XmlIoBasicImgLoader loaderIO = ImgLoaders
+ .createXmlIoForImgLoaderClass(imgLoader.getWrappedImgLoader().getClass());
+ @SuppressWarnings("unchecked")
+ Element wrappedInner = loaderIO.toXml(imgLoader.getWrappedImgLoader(), basePathURI);
+ wrappedIL.addContent(wrappedInner);
+ } catch (SpimDataInstantiationException e) {
+ e.printStackTrace();
+ return null;
+ }
+
+ final Element elFlatfields = new Element(FLATFIELDS_TAG);
+
+ for (ViewId vid : uriMap.keySet()) {
+ final Pair uris = uriMap.get(vid);
+ if (uris == null || (uris.getA() == null && uris.getB() == null))
+ continue;
+
+ final Element elFlatfield = new Element(FLATFIELD_TAG);
+ elFlatfield.setAttribute(TIMEPOINTS_TIMEPOINT_TAG, Integer.toString(vid.getTimePointId()));
+ elFlatfield.setAttribute(VIEWSETUP_TAG, Integer.toString(vid.getViewSetupId()));
+
+ if (uris.getA() != null)
+ elFlatfield.addContent(XmlHelpers.pathElementURI(BRIGHTIMG_TAG, uris.getA(), basePathURI));
+ if (uris.getB() != null)
+ elFlatfield.addContent(XmlHelpers.pathElementURI(DARKIMG_TAG, uris.getB(), basePathURI));
+
+ elFlatfields.addContent(elFlatfield);
+ }
+
+ wholeElem.addContent(wrappedIL);
+ wholeElem.addContent(elFlatfields);
+ return wholeElem;
+ }
+}