.FlatFieldCorrectedRandomAccess copy = new FlatFieldCorrectedRandomAccess();
+ copy.setPosition(sourceRA);
return copy;
}
-
}
public static , Q extends RealType< Q >> double getMeanCorrected(RandomAccessibleInterval< P > brightImg, RandomAccessibleInterval< Q > darkImg)
@@ -148,7 +304,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 +328,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 +339,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..a2f1c2a31 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
@@ -22,86 +22,145 @@
*/
package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield;
-import java.util.Arrays;
-
-import bdv.util.ConstantRandomAccessible;
-import bdv.viewer.overlay.SourceInfoOverlayRenderer;
-import net.imglib2.FinalInterval;
import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.algorithm.blocks.BlockAlgoUtils;
+import net.imglib2.algorithm.blocks.BlockSupplier;
+import net.imglib2.cache.img.CachedCellImg;
+import net.imglib2.converter.RealTypeConverters;
+import net.imglib2.type.NativeType;
import net.imglib2.type.numeric.RealType;
import net.imglib2.type.numeric.real.FloatType;
import net.imglib2.view.Views;
+/**
+ * Factory methods for creating flatfield-corrected images.
+ *
+ * All methods use efficient block-based processing internally via
+ * {@link FlatfieldCorrectionBlockSupplier}, backed by a {@link CachedCellImg}.
+ */
public class FlatFieldCorrectedRandomAccessibleIntervals
{
- public static < R extends RealType< R >, S extends RealType< S >, T extends RealType< T >> RandomAccessibleInterval< R > create(
- RandomAccessibleInterval< R > sourceImg,
- RandomAccessibleInterval< S > brightImg,
- RandomAccessibleInterval< T > darkImg )
+ /** Default cell size for block-based flatfield correction */
+ private static final int[] DEFAULT_CELL_SIZE = new int[] {64, 64, 64};
+
+ /**
+ * Create a flatfield-corrected image with the same type as the source.
+ * Uses efficient block-based processing internally.
+ *
+ * @param sourceImg source image
+ * @param brightImg bright (flatfield) image, can be null
+ * @param darkImg dark image, can be null
+ * @return corrected image with same type as source
+ */
+ public static & NativeType, S extends RealType, T extends RealType>
+ RandomAccessibleInterval create(
+ final RandomAccessibleInterval sourceImg,
+ final RandomAccessibleInterval brightImg,
+ final RandomAccessibleInterval darkImg)
{
- R type = Views.iterable( sourceImg ).firstElement().createVariable();
- return create( sourceImg, brightImg, darkImg, type );
+ final R type = sourceImg.getType().createVariable();
+ return create(sourceImg, brightImg, darkImg, type);
}
- public static , R extends RealType< R >, S extends RealType< S >, T extends RealType< T >> RandomAccessibleInterval< O > create(
- RandomAccessibleInterval< R > sourceImg,
- RandomAccessibleInterval< S > brightImg,
- RandomAccessibleInterval< T > darkImg,
- O outputType)
+
+ /**
+ * Create a flatfield-corrected image with a specified output type.
+ * Uses efficient block-based processing internally.
+ *
+ * @param sourceImg source image
+ * @param brightImg bright (flatfield) image, can be null
+ * @param darkImg dark image, can be null
+ * @param outputType the desired output type
+ * @return corrected image with specified output type
+ */
+ @SuppressWarnings("unchecked")
+ public static , R extends RealType & NativeType, S extends RealType, T extends RealType>
+ RandomAccessibleInterval create(
+ final RandomAccessibleInterval sourceImg,
+ final RandomAccessibleInterval brightImg,
+ final RandomAccessibleInterval darkImg,
+ final O outputType)
{
+ // Create block-based corrected image (always FloatType internally)
+ final RandomAccessibleInterval correctedFloat = createBlockBased(
+ sourceImg, brightImg, darkImg, DEFAULT_CELL_SIZE);
- // get intervals for bright and/or dark imgs: interval of source img, but only for dimensionality of bright/dark
- final long[] minsBright;
- final long[] maxsBright;
- FinalInterval intervalBright = null;
- if (brightImg != null)
- {
- minsBright = new long[brightImg.numDimensions()];
- maxsBright = new long[brightImg.numDimensions()];
- Arrays.fill( maxsBright, 1 );
- for (int d = 0; d < brightImg.numDimensions(); ++d)
- {
- minsBright[d] = sourceImg.min( d );
- maxsBright[d] = sourceImg.max( d );
- }
- intervalBright = new FinalInterval( minsBright, maxsBright );
- }
- final long[] minsDark;
- final long[] maxsDark;
- FinalInterval intervalDark = null;
- if (darkImg != null)
- {
- minsDark = new long[darkImg.numDimensions()];
- maxsDark = new long[darkImg.numDimensions()];
- Arrays.fill( maxsDark, 1 );
- for (int d = 0; d < darkImg.numDimensions(); ++d)
- {
- minsDark[d] = sourceImg.min( d );
- maxsDark[d] = sourceImg.max( d );
- }
- intervalDark = new FinalInterval( minsDark, maxsDark );
- }
+ // If output type is FloatType, return directly
+ if (outputType instanceof FloatType)
+ return (RandomAccessibleInterval) correctedFloat;
+
+ // Otherwise, convert to the requested output type
+ return RealTypeConverters.convert(correctedFloat, outputType);
+ }
+
+ /**
+ * Create a flatfield-corrected FloatType image using efficient block-based processing.
+ * The result is backed by a CachedCellImg that computes correction on-demand.
+ *
+ * @param sourceImg source image (can be any RealType)
+ * @param brightImg bright (flatfield) image, can be null
+ * @param darkImg dark image, can be null
+ * @return FloatType RandomAccessibleInterval with flatfield correction applied
+ */
+ public static & NativeType, S extends RealType, T extends RealType>
+ RandomAccessibleInterval createBlockBased(
+ final RandomAccessibleInterval sourceImg,
+ final RandomAccessibleInterval brightImg,
+ final RandomAccessibleInterval darkImg)
+ {
+ return createBlockBased(sourceImg, brightImg, darkImg, DEFAULT_CELL_SIZE);
+ }
+ /**
+ * Create a flatfield-corrected FloatType image using efficient block-based processing.
+ * The result is backed by a CachedCellImg that computes correction on-demand.
+ *
+ * @param sourceImg source image (can be any RealType)
+ * @param brightImg bright (flatfield) image, can be null
+ * @param darkImg dark image, can be null
+ * @param cellSize cell size for the cached image
+ * @return FloatType RandomAccessibleInterval with flatfield correction applied
+ */
+ public static & NativeType, S extends RealType, T extends RealType>
+ RandomAccessibleInterval createBlockBased(
+ final RandomAccessibleInterval sourceImg,
+ final RandomAccessibleInterval brightImg,
+ final RandomAccessibleInterval darkImg,
+ final int[] cellSize)
+ {
+ // Handle null bright/dark - if both null, just convert source to float
if (brightImg == null && darkImg == null)
{
- // 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() );
- 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() );
- 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() );
- return new FlatFieldCorrectedRandomAccessibleInterval<>(outputType, sourceImg, Views.interval( Views.extendBorder( brightImg ), intervalBright ), Views.interval( constantDark, sourceImg ) );
+ final BlockSupplier blocks = BlockSupplier
+ .of(Views.extendBorder(sourceImg))
+ .andThen(net.imglib2.algorithm.blocks.convert.Convert.convert(new FloatType()));
+ return wrapWithOffset(blocks, sourceImg, cellSize);
}
-
- return new FlatFieldCorrectedRandomAccessibleInterval<>(outputType, sourceImg, Views.interval( Views.extendBorder( brightImg ), intervalBright ), Views.interval( Views.extendBorder( darkImg ), intervalDark ) );
+
+ // Create the block supplier with flatfield correction
+ final BlockSupplier blocks = FlatfieldCorrectionBlockSupplier.of(
+ sourceImg, brightImg, darkImg);
+
+ return wrapWithOffset(blocks, sourceImg, cellSize);
+ }
+
+ /**
+ * Wrap a BlockSupplier in a CachedCellImg and translate to match source interval.
+ */
+ private static > RandomAccessibleInterval wrapWithOffset(
+ final BlockSupplier blocks,
+ final RandomAccessibleInterval sourceImg,
+ final int[] cellSize)
+ {
+ // Create CachedCellImg (zero-min)
+ final CachedCellImg cellImg = BlockAlgoUtils.cellImg(
+ blocks.threadSafe(),
+ sourceImg.dimensionsAsLongArray(),
+ cellSize);
+
+ // Translate to match source interval if not zero-min
+ if (Views.isZeroMin(sourceImg))
+ return cellImg;
+ else
+ return Views.translate(cellImg, sourceImg.minAsLongArray());
}
}
diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldCorrectionBlockSupplier.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldCorrectionBlockSupplier.java
new file mode 100644
index 000000000..abb68b330
--- /dev/null
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldCorrectionBlockSupplier.java
@@ -0,0 +1,288 @@
+/*-
+ * #%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 net.imglib2.type.PrimitiveType.FLOAT;
+import static net.imglib2.util.Util.safeInt;
+
+import bdv.util.ConstantRandomAccessible;
+import net.imglib2.FinalInterval;
+import net.imglib2.Interval;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.algorithm.blocks.BlockSupplier;
+import net.imglib2.algorithm.blocks.convert.Convert;
+import net.imglib2.blocks.BlockInterval;
+import net.imglib2.blocks.TempArray;
+import net.imglib2.type.NativeType;
+import net.imglib2.type.numeric.RealType;
+import net.imglib2.type.numeric.real.FloatType;
+import net.imglib2.util.Cast;
+import net.imglib2.util.Intervals;
+import net.imglib2.view.Views;
+
+/**
+ * Block-based flatfield correction for efficient fusion processing.
+ *
+ * Applies the correction formula:
+ * corrected = (source - dark) * meanBrightCorrected / (bright - dark)
+ *
+ * The bright and dark images are 2D (XY only), while the source may be 3D.
+ * For 3D blocks, the same 2D bright/dark data is reused for all Z slices,
+ * making this much more efficient than per-pixel RandomAccess.
+ */
+public class FlatfieldCorrectionBlockSupplier implements BlockSupplier {
+ private final BlockSupplier source;
+ private final BlockSupplier bright;
+ private final BlockSupplier dark;
+ private final double meanBrightCorrected;
+ private final int numDimensions;
+
+ // Temp arrays for block data
+ private final TempArray srcTemp;
+ private final TempArray brightTemp;
+ private final TempArray darkTemp;
+
+ /**
+ * Create a flatfield correction BlockSupplier.
+ *
+ * @param source source image BlockSupplier (can be 2D or 3D)
+ * @param bright bright (flatfield) image as BlockSupplier (2D)
+ * @param dark dark image as BlockSupplier (2D)
+ * @param meanBrightCorrected pre-computed mean of (bright - dark)
+ */
+ public FlatfieldCorrectionBlockSupplier(
+ final BlockSupplier source,
+ final BlockSupplier bright,
+ final BlockSupplier dark,
+ final double meanBrightCorrected)
+ {
+ this.source = source;
+ this.bright = bright;
+ this.dark = dark;
+ this.meanBrightCorrected = meanBrightCorrected;
+ this.numDimensions = source.numDimensions();
+
+ this.srcTemp = TempArray.forPrimitiveType(FLOAT);
+ this.brightTemp = TempArray.forPrimitiveType(FLOAT);
+ this.darkTemp = TempArray.forPrimitiveType(FLOAT);
+ }
+
+ private FlatfieldCorrectionBlockSupplier(final FlatfieldCorrectionBlockSupplier s) {
+ this.source = s.source.independentCopy();
+ this.bright = s.bright.independentCopy();
+ this.dark = s.dark.independentCopy();
+ this.meanBrightCorrected = s.meanBrightCorrected;
+ this.numDimensions = s.numDimensions;
+
+ this.srcTemp = TempArray.forPrimitiveType(FLOAT);
+ this.brightTemp = TempArray.forPrimitiveType(FLOAT);
+ this.darkTemp = TempArray.forPrimitiveType(FLOAT);
+ }
+
+ /**
+ * Factory method to create a flatfield correction BlockSupplier from an existing FloatType source.
+ */
+ public static BlockSupplier of(
+ final BlockSupplier source,
+ final RandomAccessibleInterval bright,
+ final RandomAccessibleInterval dark,
+ final double meanBrightCorrected)
+ {
+ return new FlatfieldCorrectionBlockSupplier(
+ source,
+ BlockSupplier.of(bright),
+ BlockSupplier.of(dark),
+ meanBrightCorrected);
+ }
+
+ /**
+ * Factory method to create a flatfield correction BlockSupplier from RAIs.
+ * Handles interval adjustment for 2D bright/dark with 3D source, similar to
+ * {@link FlatFieldCorrectedRandomAccessibleIntervals#create}.
+ *
+ * @param sourceImg source image (can be any RealType, will be converted to FloatType)
+ * @param brightImg bright (flatfield) image, can be null
+ * @param darkImg dark image, can be null
+ * @return BlockSupplier that applies flatfield correction
+ */
+ public static & NativeType, S extends RealType, R extends RealType>
+ BlockSupplier of(
+ final RandomAccessibleInterval sourceImg,
+ final RandomAccessibleInterval brightImg,
+ final RandomAccessibleInterval darkImg)
+ {
+ // Create source BlockSupplier and convert to float
+ final BlockSupplier source = BlockSupplier
+ .of(Views.extendBorder(sourceImg))
+ .andThen(Convert.convert(new FloatType()));
+
+ // Handle null bright/dark images
+ final RandomAccessibleInterval bright;
+ final RandomAccessibleInterval dark;
+
+ if (brightImg == null && darkImg == null) {
+ // No correction needed, return source as-is
+ return source;
+ } else if (brightImg == null) {
+ // Assume bright == constant 1.0
+ final ConstantRandomAccessible constantBright =
+ new ConstantRandomAccessible<>(new FloatType(1.0f), darkImg.numDimensions());
+ bright = Views.interval(constantBright, createInterval(sourceImg, darkImg.numDimensions()));
+ dark = Views.interval(Views.extendBorder(convertToFloat(darkImg)),
+ createInterval(sourceImg, darkImg.numDimensions()));
+ } else if (darkImg == null) {
+ // Assume dark == constant 0.0
+ final ConstantRandomAccessible constantDark =
+ new ConstantRandomAccessible<>(new FloatType(0.0f), brightImg.numDimensions());
+ bright = Views.interval(Views.extendBorder(convertToFloat(brightImg)),
+ createInterval(sourceImg, brightImg.numDimensions()));
+ dark = Views.interval(constantDark, createInterval(sourceImg, brightImg.numDimensions()));
+ } else {
+ bright = Views.interval(Views.extendBorder(convertToFloat(brightImg)),
+ createInterval(sourceImg, brightImg.numDimensions()));
+ dark = Views.interval(Views.extendBorder(convertToFloat(darkImg)),
+ createInterval(sourceImg, darkImg.numDimensions()));
+ }
+
+ final double meanBrightCorrected = FlatFieldCorrectedRandomAccessibleInterval.getMeanCorrected(bright, dark);
+
+ return new FlatfieldCorrectionBlockSupplier(
+ source,
+ BlockSupplier.of(bright),
+ BlockSupplier.of(dark),
+ meanBrightCorrected);
+ }
+
+ /**
+ * Create an interval matching the source's first N dimensions.
+ */
+ private static FinalInterval createInterval(final Interval source, final int numDimensions) {
+ final long[] mins = new long[numDimensions];
+ final long[] maxs = new long[numDimensions];
+ for (int d = 0; d < numDimensions; d++) {
+ mins[d] = source.min(d);
+ maxs[d] = source.max(d);
+ }
+ return new FinalInterval(mins, maxs);
+ }
+
+ /**
+ * Convert a RealType RAI to FloatType.
+ */
+ @SuppressWarnings("unchecked")
+ private static > RandomAccessibleInterval convertToFloat(
+ final RandomAccessibleInterval img)
+ {
+ if (img.getType() instanceof FloatType) {
+ return (RandomAccessibleInterval) img;
+ }
+
+ return net.imglib2.converter.RealTypeConverters.convert(img, new FloatType());
+ }
+
+ @Override
+ public void copy(final Interval interval, final Object dest) {
+ final BlockInterval blockInterval = BlockInterval.asBlockInterval(interval);
+ final int[] size = blockInterval.size();
+
+ final int len = safeInt(Intervals.numElements(size));
+ final float[] srcArray = srcTemp.get(len);
+
+ // Copy source block (full 3D or 2D)
+ source.copy(interval, srcArray);
+
+ // Determine XY size for bright/dark
+ final int xyLen;
+ final int zSize;
+ if (numDimensions >= 3) {
+ xyLen = size[0] * size[1];
+ zSize = size[2];
+ } else {
+ xyLen = len;
+ zSize = 1;
+ }
+
+ final float[] brightArray = brightTemp.get(xyLen);
+ final float[] darkArray = darkTemp.get(xyLen);
+
+ // Create 2D interval for bright/dark (XY only)
+ final Interval interval2D;
+ if (numDimensions >= 3) {
+ interval2D = new FinalInterval(
+ new long[]{interval.min(0), interval.min(1)},
+ new long[]{interval.max(0), interval.max(1)});
+ } else {
+ interval2D = interval;
+ }
+
+ // Copy bright/dark blocks (2D)
+ bright.copy(interval2D, brightArray);
+ dark.copy(interval2D, darkArray);
+
+ // Apply correction
+ final float[] fdest = Cast.unchecked(dest);
+ final float meanBC = (float) meanBrightCorrected;
+
+ for (int z = 0; z < zSize; z++) {
+ final int zOffset = z * xyLen;
+ for (int i = 0; i < xyLen; i++) {
+ final float darkVal = darkArray[i];
+ final float corrBright = brightArray[i] - darkVal;
+ final float srcVal = srcArray[zOffset + i];
+ final float corrImg = srcVal - darkVal;
+
+ if (corrBright == 0) {
+ fdest[zOffset + i] = 0;
+ } else {
+ fdest[zOffset + i] = corrImg * meanBC / corrBright;
+ }
+ }
+ }
+ }
+
+ @Override
+ public BlockSupplier independentCopy() {
+ return new FlatfieldCorrectionBlockSupplier(this);
+ }
+
+ @Override
+ public BlockSupplier threadSafe() {
+ return new FlatfieldCorrectionBlockSupplier(
+ source.threadSafe(),
+ bright.threadSafe(),
+ dark.threadSafe(),
+ meanBrightCorrected);
+ }
+
+ @Override
+ public int numDimensions() {
+ return numDimensions;
+ }
+
+ private static final FloatType type = new FloatType();
+
+ @Override
+ public FloatType getType() {
+ return type;
+ }
+}
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..14e0f0464 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
@@ -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
* .
@@ -22,18 +22,28 @@
*/
package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield;
-import java.io.File;
-
import mpicbg.spim.data.sequence.ImgLoader;
import mpicbg.spim.data.sequence.ViewId;
public interface FlatfieldCorrectionWrappedImgLoader extends ImgLoader
{
- public IL getWrappedImgLoder();
- public void setActive(boolean active);
- 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);
+ IL getWrappedImgLoder();
+ void setActive(boolean active);
+ boolean isActive();
+ void setCached(boolean cached);
+ boolean isCached();
+
+ /**
+ * Set the bright (flatfield) image for a view.
+ * @param vId view id
+ * @param info flatfield image info containing URI, format, and optional dataset path
+ */
+ void setBrightImage(ViewId vId, FlatfieldImageInfo info);
+
+ /**
+ * Set the dark (darkfield) image for a view.
+ * @param vId view id
+ * @param info flatfield image info containing URI, format, and optional dataset path
+ */
+ void setDarkImage(ViewId vId, FlatfieldImageInfo info);
}
diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldImageInfo.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldImageInfo.java
new file mode 100644
index 000000000..3f241d5be
--- /dev/null
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldImageInfo.java
@@ -0,0 +1,122 @@
+/*-
+ * #%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.net.URI;
+import java.util.Objects;
+
+import org.janelia.saalfeldlab.n5.universe.StorageFormat;
+
+/**
+ * Data class holding flatfield image metadata: URI, format, and dataset path.
+ */
+public class FlatfieldImageInfo {
+
+ private final URI uri;
+ private final StorageFormat format;
+ private final String dataset;
+
+ /**
+ * Create a FlatfieldImageInfo with all parameters.
+ *
+ * @param uri the URI to the flatfield image
+ * @param format the storage format (TIF uses null, others use StorageFormat enum)
+ * @param dataset the dataset path within the container (null or empty for root)
+ */
+ public FlatfieldImageInfo(final URI uri, final StorageFormat format, final String dataset) {
+ this.uri = uri;
+ this.format = format;
+ this.dataset = dataset;
+ }
+
+ /**
+ * Create a FlatfieldImageInfo with URI, using TIF format and root dataset.
+ */
+ public FlatfieldImageInfo(final URI uri) {
+ this(uri, null, null);
+ }
+
+ /**
+ * Create a FlatfieldImageInfo with URI and format, using root dataset.
+ */
+ public FlatfieldImageInfo(final URI uri, final StorageFormat format) {
+ this(uri, format, null);
+ }
+
+ public URI getUri() {
+ return uri;
+ }
+
+ /**
+ * Get the storage format.
+ * @return the format, or null for TIF format
+ */
+ public StorageFormat getFormat() {
+ return format;
+ }
+
+ /**
+ * Get the dataset path within the container.
+ * @return the dataset path, or null/empty for root
+ */
+ public String getDataset() {
+ return dataset;
+ }
+
+ /**
+ * Get the effective dataset path (empty string if null).
+ */
+ public String getEffectiveDataset() {
+ return dataset == null ? "" : dataset;
+ }
+
+ /**
+ * Check if this is a TIF format (format is null).
+ */
+ public boolean isTif() {
+ return format == null;
+ }
+
+ @Override
+ public String toString() {
+ return "FlatfieldImageInfo{uri=" + uri + ", format=" + format + ", dataset=" + dataset + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ FlatfieldImageInfo that = (FlatfieldImageInfo) o;
+ if (!Objects.equals(uri, that.uri)) return false;
+ if (format != that.format) return false;
+ return Objects.equals(dataset, that.dataset);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = uri != null ? uri.hashCode() : 0;
+ result = 31 * result + (format != null ? format.hashCode() : 0);
+ result = 31 * result + (dataset != null ? dataset.hashCode() : 0);
+ return result;
+ }
+}
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..6704a545a
--- /dev/null
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/FlatfieldImageLoader.java
@@ -0,0 +1,210 @@
+/*-
+ * #%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:
+ * - Storage of bright/dark image info per view (URI, format, dataset path)
+ * - Lazy loading and caching of images
+ * - Support for TIF, N5, Zarr (v2/v3), and HDF5 formats
+ * - Support for cloud storage (S3, GCS) for N5/Zarr formats
+ */
+public class FlatfieldImageLoader {
+
+ protected final Map> raiMap;
+ protected final Map> infoMap;
+
+ private static final Pair NULL_PAIR = new ValuePair<>(null, null);
+
+ public FlatfieldImageLoader() {
+ raiMap = new HashMap<>();
+ infoMap = new HashMap<>();
+ }
+
+ public void setBrightImage(ViewId vId, FlatfieldImageInfo info) {
+ final Pair oldPair = infoMap.getOrDefault(vId, NULL_PAIR);
+ infoMap.put(vId, new ValuePair<>(info, oldPair.getB()));
+ }
+
+ public void setDarkImage(ViewId vId, FlatfieldImageInfo info) {
+ final Pair oldPair = infoMap.getOrDefault(vId, NULL_PAIR);
+ infoMap.put(vId, new ValuePair<>(oldPair.getA(), info));
+ }
+
+ 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 infoSelector function to select info from pair
+ * @return image, or null if not set
+ */
+ private RandomAccessibleInterval getImg(ViewId vId, Function, FlatfieldImageInfo> infoSelector) {
+ if (!infoMap.containsKey(vId))
+ return null;
+
+ final FlatfieldImageInfo info = infoSelector.apply(infoMap.get(vId));
+ if (info == null)
+ return null;
+
+ return loadImageIfNecessary(info);
+ }
+
+ /**
+ * Load an image using the specified format. Supports:
+ * - TIF files (local only, via ImageJ)
+ * - N5 containers (local + cloud)
+ * - Zarr v2/v3 containers (local + cloud)
+ * - HDF5 files (local only)
+ *
+ * @param info the flatfield image info containing URI, format, and dataset path
+ * @return the loaded image as FloatType
+ */
+ public RandomAccessibleInterval loadImageIfNecessary(FlatfieldImageInfo info) {
+ if (!raiMap.containsKey(info)) {
+ RandomAccessibleInterval img;
+
+ if (info.isTif()) {
+ // TIF format via ImageJ
+ final File file = new File(info.getUri());
+ final ImagePlus imp = IJ.openImage(file.getAbsolutePath());
+ if (imp == null)
+ throw new RuntimeException("Failed to load TIF image from: " + info.getUri());
+ img = ImageJFunctions.convertFloat(imp).copy();
+ } else {
+ // N5, Zarr, or HDF5 via N5 API
+ final StorageFormat format = info.getFormat();
+ final URI uri = info.getUri();
+
+ // Validate HDF5 is not used with cloud storage
+ if (format == StorageFormat.HDF5) {
+ final String scheme = uri.getScheme();
+ if ("s3".equals(scheme) || "gs".equals(scheme)) {
+ throw new RuntimeException("HDF5 format does not support cloud storage (s3/gs). URI: " + uri);
+ }
+ }
+
+ final N5Reader reader = URITools.instantiateN5Reader(format, uri);
+ final String dataset = info.getEffectiveDataset();
+ final RandomAccessibleInterval> raw = N5Utils.open(reader, dataset);
+ img = RealTypeConverters.convert(Cast.unchecked(raw), new FloatType());
+ }
+
+ raiMap.put(info, img);
+ }
+ return raiMap.get(info);
+ }
+
+ /**
+ * Get the info map for bright/dark images per view.
+ * @return map from ViewId to (brightInfo, darkInfo) pair
+ */
+ public Map> getInfoMap() {
+ return infoMap;
+ }
+
+ // ==================== Format Parsing Utilities ====================
+
+ /**
+ * Parse a format string to StorageFormat.
+ * @param formatStr the format string (tif, n5, zarr, zarr2, hdf5)
+ * @return StorageFormat, or null for TIF format
+ * @throws IllegalArgumentException if format string is unknown
+ */
+ public static StorageFormat parseFormat(String formatStr) {
+ if (formatStr == null || formatStr.isEmpty())
+ throw new IllegalArgumentException("Format attribute is required");
+
+ switch (formatStr.toLowerCase()) {
+ case "tif":
+ case "tiff":
+ return null; // null indicates TIF format
+ case "n5":
+ return StorageFormat.N5;
+ case "zarr":
+ case "zarr3":
+ return StorageFormat.ZARR;
+ case "zarr2":
+ return StorageFormat.ZARR2;
+ case "hdf5":
+ case "h5":
+ return StorageFormat.HDF5;
+ default:
+ throw new IllegalArgumentException("Unknown flatfield format: " + formatStr
+ + ". Supported formats: tif, n5, zarr, zarr2, hdf5");
+ }
+ }
+
+ /**
+ * Convert StorageFormat to format string for XML serialization.
+ * @param format the StorageFormat (null for TIF)
+ * @return format string
+ */
+ public static String formatToString(StorageFormat format) {
+ if (format == null)
+ return "tif";
+
+ switch (format) {
+ case N5:
+ return "n5";
+ case ZARR:
+ return "zarr";
+ case ZARR2:
+ return "zarr2";
+ case HDF5:
+ return "hdf5";
+ default:
+ throw new IllegalArgumentException("Unsupported format for flatfield: " + format);
+ }
+ }
+}
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..8e43f2cdd 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,57 @@
package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield;
import java.io.File;
-import java.util.HashMap;
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, FlatfieldImageInfo info) {
+ imageLoader.setBrightImage(vId, info);
}
@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, FlatfieldImageInfo info) {
+ imageLoader.setDarkImage(vId, info);
}
-
- 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 FlatfieldImageInfo(new File("/Users/David/Desktop/ell2.tif").toURI()));
+ RandomAccessibleInterval brightImg = testImgLoader.getBrightImg(new ViewId(0, 0));
+
+ ImageJFunctions.show(brightImg);
}
- public Map< ViewId, Pair< File, File > > getFileMap()
+ /**
+ * Get the info map for bright/dark images per view.
+ * @return map from ViewId to (brightInfo, darkInfo) pair
+ */
+ public Map> getInfoMap()
{
- return fileMap;
+ return imageLoader.getInfoMap();
}
-
-
}
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..55c435434 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,13 @@
package net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield;
import java.io.File;
-import java.util.ArrayList;
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 +37,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 +59,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 +66,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 +89,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, FlatfieldImageInfo> infoSelector,
+ 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<>(infoSelector.apply(getInfoMap().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 +169,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 +192,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 +206,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 +223,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 +259,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 +273,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 +290,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 +312,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 +327,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 +395,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)
@@ -488,7 +455,7 @@ public static void main(String[] args)
MultiResolutionImgLoader il = (MultiResolutionImgLoader) data.getSequenceDescription().getImgLoader();
MultiResolutionFlatfieldCorrectionWrappedImgLoader ffcil = new MultiResolutionFlatfieldCorrectionWrappedImgLoader(
il );
- ffcil.setDarkImage( new ViewId( 0, 0 ), new File( "/Users/david/desktop/ff.tif" ) );
+ ffcil.setDarkImage( new ViewId( 0, 0 ), new FlatfieldImageInfo( new File( "/Users/david/desktop/ff.tif" ).toURI(), null ) );
data.getSequenceDescription().setImgLoader( ffcil );
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..4fe5b8662
--- /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, new FlatfieldImageInfo(darkfield.toURI(), null));
+ System.out.println(" Setup " + setupId + ": darkfield = " + darkfield.getName());
+ }
+
+ if (flatfield.exists()) {
+ correctedLoader.setBrightImage(viewId, new FlatfieldImageInfo(flatfield.toURI(), null));
+ 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..4910a12be
--- /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_viewer_s3.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..6d9709213
--- /dev/null
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/ViewerFlatfieldCorrectionWrappedImgLoader.java
@@ -0,0 +1,431 @@
+/*-
+ * #%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.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 FlatfieldImageInfo info) {
+ imageLoader.setBrightImage(vId, info);
+ }
+
+ public void setDarkImage(final ViewId vId, final FlatfieldImageInfo info) {
+ imageLoader.setDarkImage(vId, info);
+ }
+
+ /**
+ * Get the info map for bright/dark images per view.
+ * @return map from ViewId to (brightInfo, darkInfo) pair
+ */
+ public Map> getInfoMap() {
+ return imageLoader.getInfoMap();
+ }
+
+ // ========== 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, FlatfieldImageInfo> infoSelector,
+ 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<>(infoSelector.apply(imageLoader.getInfoMap().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..6d7b2edd4 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
* .
@@ -26,15 +26,19 @@
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 static net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.XmlIoViewerFlatfieldCorrectionWrappedImgLoader.FORMAT_ATTR;
+import static net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.XmlIoViewerFlatfieldCorrectionWrappedImgLoader.DATASET_ATTR;
+import static net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.XmlIoViewerFlatfieldCorrectionWrappedImgLoader.parseFlatfieldImageInfo;
+import static net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.XmlIoViewerFlatfieldCorrectionWrappedImgLoader.createFlatfieldImageElement;
import java.io.File;
+import java.net.URI;
import java.util.Map;
import org.jdom2.DataConversionException;
import org.jdom2.Element;
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;
@@ -44,12 +48,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 +63,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 +90,102 @@ 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));
+ ViewId viewId = new ViewId(tp, vs);
+
+ FlatfieldImageInfo brightInfo = parseFlatfieldImageInfo(flatfield, BRIGHTIMG_TAG, basePathURI);
+ FlatfieldImageInfo darkInfo = parseFlatfieldImageInfo(flatfield, DARKIMG_TAG, basePathURI);
+
+ if (brightInfo != null)
+ res.setBrightImage(viewId, brightInfo);
+ if (darkInfo != null)
+ res.setDarkImage(viewId, darkInfo);
}
- 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> infoMap =
+ ((LazyLoadingFlatFieldCorrectionMap extends ImgLoader>) imgLoader).getInfoMap();
- 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 : infoMap.keySet())
{
- final Pair< File, File > files = fileMap.get( vid );
- if ( files == null || ( files.getA() == null && files.getB() == null ) )
+ final Pair infos = infoMap.get(vid);
+ if (infos == null || (infos.getA() == null && infos.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 (infos.getA() != null)
+ elFlatfield.addContent(createFlatfieldImageElement(BRIGHTIMG_TAG, infos.getA(), basePathURI));
+ if (infos.getB() != null)
+ elFlatfield.addContent(createFlatfieldImageElement(DARKIMG_TAG, infos.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..fb6e56b4b
--- /dev/null
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/flatfield/XmlIoViewerFlatfieldCorrectionWrappedImgLoader.java
@@ -0,0 +1,219 @@
+/*-
+ * #%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.janelia.saalfeldlab.n5.universe.StorageFormat;
+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";
+ public final static String FORMAT_ATTR = "format";
+ public final static String DATASET_ATTR = "dataset";
+
+ @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));
+ ViewId viewId = new ViewId(tp, vs);
+
+ FlatfieldImageInfo brightInfo = parseFlatfieldImageInfo(flatfield, BRIGHTIMG_TAG, basePathURI);
+ FlatfieldImageInfo darkInfo = parseFlatfieldImageInfo(flatfield, DARKIMG_TAG, basePathURI);
+
+ if (brightInfo != null)
+ res.setBrightImage(viewId, brightInfo);
+ if (darkInfo != null)
+ res.setDarkImage(viewId, darkInfo);
+ }
+
+ 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> infoMap = imgLoader.getInfoMap();
+
+ 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 : infoMap.keySet()) {
+ final Pair infos = infoMap.get(vid);
+ if (infos == null || (infos.getA() == null && infos.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 (infos.getA() != null)
+ elFlatfield.addContent(createFlatfieldImageElement(BRIGHTIMG_TAG, infos.getA(), basePathURI));
+ if (infos.getB() != null)
+ elFlatfield.addContent(createFlatfieldImageElement(DARKIMG_TAG, infos.getB(), basePathURI));
+
+ elFlatfields.addContent(elFlatfield);
+ }
+
+ wholeElem.addContent(wrappedIL);
+ wholeElem.addContent(elFlatfields);
+ return wholeElem;
+ }
+
+ /**
+ * Parse a flatfield image element (BrightImg or DarkImg) into a FlatfieldImageInfo.
+ *
+ * @param parent parent element containing the image element
+ * @param tag the tag name (BRIGHTIMG_TAG or DARKIMG_TAG)
+ * @param basePathURI base path for resolving relative URIs
+ * @return FlatfieldImageInfo, or null if element doesn't exist
+ */
+ protected static FlatfieldImageInfo parseFlatfieldImageInfo(Element parent, String tag, URI basePathURI) {
+ Element imgElement = parent.getChild(tag);
+ if (imgElement == null)
+ return null;
+
+ URI uri = XmlHelpers.loadPathURI(parent, tag, basePathURI);
+ if (uri == null)
+ return null;
+
+ String formatStr = imgElement.getAttributeValue(FORMAT_ATTR);
+ String dataset = imgElement.getAttributeValue(DATASET_ATTR);
+
+ StorageFormat format = FlatfieldImageLoader.parseFormat(formatStr);
+ return new FlatfieldImageInfo(uri, format, dataset);
+ }
+
+ /**
+ * Create an XML element for a flatfield image with format and dataset attributes.
+ *
+ * @param tag the tag name (BRIGHTIMG_TAG or DARKIMG_TAG)
+ * @param info the flatfield image info
+ * @param basePathURI base path for creating relative URIs
+ * @return the XML element
+ */
+ protected static Element createFlatfieldImageElement(String tag, FlatfieldImageInfo info, URI basePathURI) {
+ Element el = XmlHelpers.pathElementURI(tag, info.getUri(), basePathURI);
+ el.setAttribute(FORMAT_ATTR, FlatfieldImageLoader.formatToString(info.getFormat()));
+ if (info.getDataset() != null && !info.getDataset().isEmpty())
+ el.setAttribute(DATASET_ATTR, info.getDataset());
+ return el;
+ }
+}
diff --git a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/splitting/SplitMultiResolutionSetupImgLoader.java b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/splitting/SplitMultiResolutionSetupImgLoader.java
index 0d925d273..7a7724e71 100644
--- a/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/splitting/SplitMultiResolutionSetupImgLoader.java
+++ b/src/main/java/net/preibisch/mvrecon/fiji/spimdata/imgloaders/splitting/SplitMultiResolutionSetupImgLoader.java
@@ -170,7 +170,11 @@ public int numMipmapLevels()
public RandomAccessibleInterval< FloatType > getFloatImage( int timepointId,
int level, boolean normalize, ImgLoaderHint... hints )
{
- throw new RuntimeException( "not supported." );
+ final RandomAccessibleInterval< FloatType > full = underlyingSetupImgLoader.getFloatImage( timepointId, level, normalize, hints );
+
+ updateScaledIntervals( this.scaledIntervals, level, n, full );
+
+ return Views.zeroMin( Views.interval( full, scaledIntervals[ level ] ) );
}
@Override
diff --git a/src/test/java/net/preibisch/mvrecon/tests/BenchmarkFlatfieldFusion.java b/src/test/java/net/preibisch/mvrecon/tests/BenchmarkFlatfieldFusion.java
new file mode 100644
index 000000000..1627007fe
--- /dev/null
+++ b/src/test/java/net/preibisch/mvrecon/tests/BenchmarkFlatfieldFusion.java
@@ -0,0 +1,291 @@
+/*-
+ * #%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.tests;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.function.Consumer;
+
+import mpicbg.spim.data.SpimDataException;
+import mpicbg.spim.data.sequence.ViewId;
+import net.imglib2.FinalInterval;
+import net.imglib2.Interval;
+import net.imglib2.algorithm.blocks.BlockSupplier;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.numeric.integer.UnsignedShortType;
+import net.preibisch.mvrecon.fiji.plugin.fusion.FusionGUI.FusionType;
+import net.preibisch.mvrecon.fiji.spimdata.SpimData2;
+import net.preibisch.mvrecon.fiji.spimdata.XmlIoSpimData2;
+import net.preibisch.mvrecon.fiji.spimdata.boundingbox.BoundingBox;
+import net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.FlatfieldCorrectionWrappedImgLoader;
+import net.preibisch.mvrecon.fiji.spimdata.imgloaders.flatfield.ViewerFlatfieldCorrectionWrappedImgLoader;
+import net.preibisch.mvrecon.process.boundingbox.BoundingBoxMaximal;
+import net.preibisch.mvrecon.process.fusion.FusionTools;
+import net.preibisch.mvrecon.process.fusion.blk.BlkAffineFusion;
+import net.preibisch.mvrecon.process.fusion.transformed.TransformVirtual;
+import util.BlockSupplierUtils;
+
+/**
+ * Benchmark to measure the performance overhead of lazy flatfield correction during multi-view fusion.
+ *
+ * This benchmark:
+ * 1. Loads a dataset with flatfield correction configured (data/dataset_corrected_viewer.xml)
+ * 2. Toggles flatfield correction on/off using setActive()
+ * 3. Runs fusion with and without correction
+ * 4. Measures and reports timing comparison
+ *
+ * Run with: mvn compile exec:java -Dexec.mainClass="net.preibisch.mvrecon.tests.BenchmarkFlatfieldFusion"
+ * Or run from IDE as a Java application.
+ */
+public class BenchmarkFlatfieldFusion {
+ // Benchmark configuration
+ private static final double DOWNSAMPLING = 4.0;
+ private static final double ANISOTROPY_FACTOR = 3.0; // matches dataset's 1x1x3 voxel size
+ private static final int WARMUP_ITERATIONS = 10;
+ private static final int BENCHMARK_ITERATIONS = 50;
+ private static final FusionType FUSION_TYPE = FusionType.AVG_BLEND;
+ private static final int[] BLOCK_SIZE = new int[] {64, 64, 64};
+
+ public static void main(String[] args) {
+ // Path to dataset with flatfield correction
+ final File xmlFile = new File("data/dataset_corrected_viewer.xml");
+
+ if (!xmlFile.exists()) {
+ System.err.println("Dataset not found: " + xmlFile.getAbsolutePath());
+ System.err.println("Please ensure data/dataset_corrected_viewer.xml exists.");
+ System.exit(1);
+ }
+
+ try {
+ runBenchmark(xmlFile);
+ } catch (Exception e) {
+ System.err.println("Benchmark failed: " + e.getMessage());
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void runBenchmark(final File xmlFile) throws SpimDataException {
+ System.out.println("============================================================");
+ System.out.println("Flatfield Correction Benchmark During Fusion");
+ System.out.println("============================================================");
+ System.out.println();
+
+ // Load the dataset
+ System.out.println("Loading dataset: " + xmlFile.getAbsolutePath());
+ final SpimData2 spimData = new XmlIoSpimData2().load(xmlFile.toURI());
+
+ // Get the flatfield-wrapped image loader
+ // Support both FlatfieldCorrectionWrappedImgLoader (interface) and ViewerFlatfieldCorrectionWrappedImgLoader (class)
+ final FlatfieldCorrectionWrappedImgLoader> ffLoader;
+ final ViewerFlatfieldCorrectionWrappedImgLoader viewerFfLoader;
+
+ if (spimData.getSequenceDescription().getImgLoader() instanceof FlatfieldCorrectionWrappedImgLoader) {
+ ffLoader = (FlatfieldCorrectionWrappedImgLoader>) spimData.getSequenceDescription().getImgLoader();
+ viewerFfLoader = null;
+ } else if (spimData.getSequenceDescription().getImgLoader() instanceof ViewerFlatfieldCorrectionWrappedImgLoader) {
+ ffLoader = null;
+ viewerFfLoader = (ViewerFlatfieldCorrectionWrappedImgLoader) spimData.getSequenceDescription().getImgLoader();
+ } else {
+ System.err.println("Dataset does not have a flatfield correction image loader.");
+ System.err.println("ImgLoader type: " + spimData.getSequenceDescription().getImgLoader().getClass().getName());
+ return;
+ }
+
+ // Create a lambda to toggle flatfield correction for either loader type
+ final Consumer setActive = (active) -> {
+ if (ffLoader != null)
+ ffLoader.setActive(active);
+ else
+ viewerFfLoader.setActive(active);
+ };
+
+ // Get all views
+ final List viewIds = new ArrayList<>();
+ viewIds.addAll(spimData.getSequenceDescription().getViewDescriptions().values());
+ SpimData2.filterMissingViews(spimData, viewIds);
+
+ System.out.println();
+ System.out.println("Dataset Information:");
+ System.out.println(" Views: " + viewIds.size());
+ System.out.println(" Downsampling: " + DOWNSAMPLING + ", Anisotropy: " + ANISOTROPY_FACTOR);
+ System.out.println(" Fusion type: " + FUSION_TYPE);
+ System.out.println(" Block size: " + BLOCK_SIZE[0] + "x" + BLOCK_SIZE[1] + "x" + BLOCK_SIZE[2]);
+ System.out.println(" Warmup iterations: " + WARMUP_ITERATIONS + ", Benchmark iterations: " + BENCHMARK_ITERATIONS);
+
+ // Compute bounding box
+ final BoundingBox bb = new BoundingBoxMaximal(viewIds, spimData).estimate("benchmark");
+ System.out.println(" Bounding box: [" + bb.getMin()[0] + ", " + bb.getMin()[1] + ", " + bb.getMin()[2] +
+ "] to [" + bb.getMax()[0] + ", " + bb.getMax()[1] + ", " + bb.getMax()[2] + "]");
+
+ // Apply anisotropy and downsampling to bounding box
+ Interval boundingBoxFusion = FusionTools.createAnisotropicBoundingBox(bb, ANISOTROPY_FACTOR).getA();
+ boundingBoxFusion = FusionTools.createDownsampledBoundingBox(boundingBoxFusion, DOWNSAMPLING).getA();
+
+ final long[] dims = boundingBoxFusion.dimensionsAsLongArray();
+ System.out.println(" Fusion dimensions: " + dims[0] + " x " + dims[1] + " x " + dims[2]);
+ System.out.println();
+
+ // Adjust view registrations for anisotropy and downsampling
+ final HashMap registrations =
+ TransformVirtual.adjustAllTransforms(
+ viewIds,
+ spimData.getViewRegistrations().getViewRegistrations(),
+ ANISOTROPY_FACTOR,
+ DOWNSAMPLING);
+
+ // Warmup runs
+ System.out.println("--- Warmup Phase ---");
+
+ for (int i = 0; i < WARMUP_ITERATIONS; i++) {
+ System.out.print(" Warmup WITHOUT FF (" + (i + 1) + "/" + WARMUP_ITERATIONS + ")... ");
+ setActive.accept(false);
+ runFusion(spimData, viewIds, registrations, boundingBoxFusion);
+ System.out.println("done");
+ }
+
+ for (int i = 0; i < WARMUP_ITERATIONS; i++) {
+ System.out.print(" Warmup WITH FF (" + (i + 1) + "/" + WARMUP_ITERATIONS + ")... ");
+ setActive.accept(true);
+ runFusion(spimData, viewIds, registrations, boundingBoxFusion);
+ System.out.println("done");
+ }
+
+ System.out.println();
+
+ // Benchmark WITHOUT flatfield correction
+ System.out.println("--- Benchmark Phase ---");
+ System.out.println("Running WITHOUT flatfield correction...");
+ setActive.accept(false);
+ final long[] timesWithout = new long[BENCHMARK_ITERATIONS];
+
+ for (int i = 0; i < BENCHMARK_ITERATIONS; i++) {
+ System.out.print(" Iteration " + (i + 1) + "/" + BENCHMARK_ITERATIONS + ": ");
+ final long start = System.currentTimeMillis();
+ runFusion(spimData, viewIds, registrations, boundingBoxFusion);
+ timesWithout[i] = System.currentTimeMillis() - start;
+ System.out.println(timesWithout[i] + " ms");
+ }
+
+ System.out.println();
+
+ // Benchmark WITH flatfield correction
+ System.out.println("Running WITH flatfield correction...");
+ setActive.accept(true);
+ final long[] timesWith = new long[BENCHMARK_ITERATIONS];
+
+ for (int i = 0; i < BENCHMARK_ITERATIONS; i++) {
+ System.out.print(" Iteration " + (i + 1) + "/" + BENCHMARK_ITERATIONS + ": ");
+ final long start = System.currentTimeMillis();
+ runFusion(spimData, viewIds, registrations, boundingBoxFusion);
+ timesWith[i] = System.currentTimeMillis() - start;
+ System.out.println(timesWith[i] + " ms");
+ }
+
+ System.out.println();
+
+ // Calculate and report statistics
+ final Stats statsWithout = calculateStats(timesWithout);
+ final Stats statsWith = calculateStats(timesWith);
+
+ System.out.println("--- Benchmark Results ---");
+ System.out.printf("Without FF: avg %d ms (min %d, max %d, stddev %.1f)%n",
+ statsWithout.avg, statsWithout.min, statsWithout.max, statsWithout.stddev);
+ System.out.printf("With FF: avg %d ms (min %d, max %d, stddev %.1f)%n",
+ statsWith.avg, statsWith.min, statsWith.max, statsWith.stddev);
+
+ final long overhead = statsWith.avg - statsWithout.avg;
+ final double overheadPercent = 100.0 * overhead / statsWithout.avg;
+
+ System.out.println();
+ System.out.printf("Overhead: %+d ms (%+.1f%%)%n", overhead, overheadPercent);
+ System.out.println("============================================================");
+ }
+
+ /**
+ * Run fusion and force full computation by copying to ArrayImg.
+ */
+ private static void runFusion(
+ final SpimData2 spimData,
+ final List viewIds,
+ final HashMap registrations,
+ final Interval boundingBoxFusion) {
+ // Create fusion BlockSupplier
+ final BlockSupplier blocks = BlkAffineFusion.init(
+ (i, o) -> o.set(Math.round(i.get())),
+ spimData.getSequenceDescription().getImgLoader(),
+ viewIds,
+ registrations,
+ spimData.getSequenceDescription().getViewDescriptions(),
+ FUSION_TYPE,
+ ANISOTROPY_FACTOR,
+ 1, // interpolation: linear
+ null, // no intensity adjustments
+ boundingBoxFusion,
+ new UnsignedShortType(),
+ BLOCK_SIZE);
+
+ // Force full computation by copying to ArrayImg (not lazy CellImg)
+ // This ensures all flatfield calculations are performed
+ BlockSupplierUtils.arrayImg(blocks, new FinalInterval(boundingBoxFusion.dimensionsAsLongArray()));
+ }
+
+ private static Stats calculateStats(final long[] times) {
+ long sum = 0;
+ long min = Long.MAX_VALUE;
+ long max = Long.MIN_VALUE;
+
+ for (final long t : times) {
+ sum += t;
+ min = Math.min(min, t);
+ max = Math.max(max, t);
+ }
+
+ final long avg = sum / times.length;
+
+ double variance = 0;
+ for (final long t : times) {
+ variance += (t - avg) * (t - avg);
+ }
+ variance /= times.length;
+ final double stddev = Math.sqrt(variance);
+
+ return new Stats(avg, min, max, stddev);
+ }
+
+ private static class Stats {
+ final long avg;
+ final long min;
+ final long max;
+ final double stddev;
+
+ Stats(long avg, long min, long max, double stddev) {
+ this.avg = avg;
+ this.min = min;
+ this.max = max;
+ this.stddev = stddev;
+ }
+ }
+}