diff --git a/pom.xml b/pom.xml index 3b8cbbf05..12b129426 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 40.0.0 + 41.0.0 @@ -143,8 +143,7 @@ bsd_2 BigDataViewer developers. - 7.1.5 - 0.18.1 + 8.0.0 sign,deploy-to-scijava diff --git a/src/main/java/bdv/AbstractSpimSource.java b/src/main/java/bdv/AbstractSpimSource.java index 02d2a8d64..1d766c263 100644 --- a/src/main/java/bdv/AbstractSpimSource.java +++ b/src/main/java/bdv/AbstractSpimSource.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -34,6 +34,7 @@ import java.util.Set; import bdv.viewer.Interpolation; +import bdv.viewer.MaskUtils; import bdv.viewer.Source; import mpicbg.spim.data.generic.AbstractSpimData; import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription; @@ -52,7 +53,11 @@ import net.imglib2.interpolation.randomaccess.ClampingNLinearInterpolatorFactory; import net.imglib2.interpolation.randomaccess.NearestNeighborInterpolatorFactory; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.mask.Masked; +import net.imglib2.type.numeric.ARGBType; import net.imglib2.type.numeric.NumericType; +import net.imglib2.type.volatiles.VolatileARGBType; +import net.imglib2.util.Cast; import net.imglib2.view.Views; public abstract class AbstractSpimSource< T extends NumericType< T > > implements Source< T > @@ -129,19 +134,14 @@ public boolean equals( final Object obj ) private final int numMipmapLevels; - private final static int numInterpolationMethods = 2; - - private final static int iNearestNeighborMethod = 0; - - private final static int iNLinearMethod = 1; - - private final InterpolatorFactory< T, RandomAccessible< T > >[] interpolatorFactories; - private final UncheckedCache< ImgKey, RandomAccessibleInterval< T > > cachedSources; private final UncheckedCache< ImgKey, RealRandomAccessible< T > > cachedInterpolatedSources; - @SuppressWarnings( "unchecked" ) + private final UncheckedCache< ImgKey, RandomAccessibleInterval< ? extends Masked< T > > > cachedMaskedSources; + + private final UncheckedCache< ImgKey, RealRandomAccessible< ? extends Masked< T > > > cachedInterpolatedMaskedSources; + public AbstractSpimSource( final AbstractSpimData< ? > spimData, final int setupId, final String name ) { this.setupId = setupId; @@ -155,28 +155,40 @@ public AbstractSpimSource( final AbstractSpimData< ? > spimData, final int setup voxelDimensions = seq.getViewSetups().get( setupId ).getVoxelSize(); numMipmapLevels = ( ( ViewerImgLoader ) seq.getImgLoader() ).getSetupImgLoader( setupId ).numMipmapLevels(); - interpolatorFactories = new InterpolatorFactory[ numInterpolationMethods ]; - interpolatorFactories[ iNearestNeighborMethod ] = new NearestNeighborInterpolatorFactory<>(); - interpolatorFactories[ iNLinearMethod ] = new ClampingNLinearInterpolatorFactory<>(); - - final CacheLoader< ImgKey, RandomAccessibleInterval< T > > loader = key -> { - return getImage( timePointsOrdered.get( key.timepoint ).getId(), key.level ); - }; + final CacheLoader< ImgKey, RandomAccessibleInterval< T > > loader = key -> + getImage( timePointsOrdered.get( key.timepoint ).getId(), key.level ); cachedSources = Caches.unchecked( Caches.withLoader( new BoundedSoftRefLoaderCache<>( 3 * numMipmapLevels ), loader ) ); - final CacheLoader< ImgKey, RealRandomAccessible< T > > interpolLoader = key -> { - final T zero = getType().createVariable(); - zero.setZero(); - final int i = key.method == Interpolation.NLINEAR ? iNLinearMethod : iNearestNeighborMethod; - final InterpolatorFactory< T, RandomAccessible< T > > factory = interpolatorFactories[ i ]; - return Views.interpolate( Views.extendValue( getSource( key.timepoint, key.level ), zero ), factory ); - }; + final CacheLoader< ImgKey, RealRandomAccessible< T > > interpolLoader = key -> + Views.interpolate( Views.extendZero( getSource( key.timepoint, key.level, key.threadGroup ) ), interpolator( key.method ) ); cachedInterpolatedSources = Caches.unchecked( Caches.withLoader( - new BoundedSoftRefLoaderCache<>( 3 * numMipmapLevels * numInterpolationMethods ), + new BoundedSoftRefLoaderCache<>( 3 * numMipmapLevels * Interpolation.values().length ), interpolLoader ) ); + final CacheLoader< ImgKey, RandomAccessibleInterval< ? extends Masked< T > > > maskedLoader = key -> + Masked.withConstant( getSource( key.timepoint, key.level, key.threadGroup ), 1 ); + cachedMaskedSources = Caches.unchecked( Caches.withLoader( + new BoundedSoftRefLoaderCache<>( 3 * numMipmapLevels ), + maskedLoader ) ); + + final CacheLoader< ImgKey, RealRandomAccessible< ? extends Masked< T > > > maskedInterpolLoader = key -> { + if ( getType() instanceof ARGBType || getType() instanceof VolatileARGBType ) + { + // TODO: How to correctly handle ARGBType sources? We could make a + // Masked wrapper that uses the ARGBType's A as mask? + return Masked.withConstant( getInterpolatedSource( key.timepoint, key.level, key.method, key.threadGroup ), 1 ); + } + else + { + return MaskUtils.extendAndInterpolateMasked( Cast.unchecked( getMaskedSource( key.timepoint, key.level, key.threadGroup ) ), key.method ); + } + }; + cachedInterpolatedMaskedSources = Caches.unchecked( Caches.withLoader( + new BoundedSoftRefLoaderCache<>( 3 * numMipmapLevels * Interpolation.values().length ), + maskedInterpolLoader ) ); + currentSourceTransforms = new AffineTransform3D[ numMipmapLevels ]; for ( int level = 0; level < numMipmapLevels; level++ ) currentSourceTransforms[ level ] = new AffineTransform3D(); @@ -184,6 +196,13 @@ public AbstractSpimSource( final AbstractSpimData< ? > spimData, final int setup currentTimePointIndex = -1; } + private static < T extends NumericType< T > > InterpolatorFactory< T, RandomAccessible< T > > interpolator( final Interpolation method ) + { + return method == Interpolation.NEARESTNEIGHBOR + ? new NearestNeighborInterpolatorFactory<>() + : new ClampingNLinearInterpolatorFactory<>(); + } + void loadTimepoint( final int timepointIndex ) { currentTimePointIndex = timepointIndex; @@ -291,4 +310,34 @@ public void reload() { currentTimePointIndex = -1; } + + @Override + public RandomAccessibleInterval< ? extends Masked< T > > getMaskedSource( final int t, final int level ) + { + return getMaskedSource( t, level, Thread.currentThread().getThreadGroup() ); + } + + public synchronized RandomAccessibleInterval< ? extends Masked< T > > getMaskedSource( final int t, final int level, final ThreadGroup threadGroup ) + { + if ( t != currentTimePointIndex ) + loadTimepoint( t ); + return currentTimePointIsPresent + ? cachedMaskedSources.get( new ImgKey( t, level, null, threadGroup ) ) + : null; + } + + @Override + public RealRandomAccessible< ? extends Masked< T > > getInterpolatedMaskedSource( int t, int level, final Interpolation method ) + { + return getInterpolatedMaskedSource( t, level, method, Thread.currentThread().getThreadGroup() ); + } + + public synchronized RealRandomAccessible< ? extends Masked< T > > getInterpolatedMaskedSource( final int t, final int level, final Interpolation method, final ThreadGroup threadGroup ) + { + if ( t != currentTimePointIndex ) + loadTimepoint( t ); + return currentTimePointIsPresent + ? cachedInterpolatedMaskedSources.get( new ImgKey( t, level, method, threadGroup ) ) + : null; + } } diff --git a/src/main/java/bdv/tools/InitializeViewerState.java b/src/main/java/bdv/tools/InitializeViewerState.java index e30d624a2..df48af74b 100644 --- a/src/main/java/bdv/tools/InitializeViewerState.java +++ b/src/main/java/bdv/tools/InitializeViewerState.java @@ -36,6 +36,7 @@ import net.imglib2.histogram.Histogram1d; import net.imglib2.histogram.Real1dBinMapper; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.integer.UnsignedByteType; import net.imglib2.type.numeric.integer.UnsignedShortType; import net.imglib2.util.LinAlgHelpers; @@ -198,27 +199,7 @@ public static Bounds estimateSourceRange( final Source< ? > source, final int ti { @SuppressWarnings( "unchecked" ) final RandomAccessibleInterval< UnsignedShortType > img = ( RandomAccessibleInterval< UnsignedShortType > ) source.getSource( timepoint, source.getNumMipmapLevels() - 1 ); - final long z = ( img.min( 2 ) + img.max( 2 ) + 1 ) / 2; - - final int numBins = 6535; - final Histogram1d< ? > histogram = new Histogram1d<>( Views.hyperSlice( img, 2, z ), new Real1dBinMapper<>( 0, 65535, numBins, false ) ); - final DiscreteFrequencyDistribution dfd = histogram.dfd(); - final long[] bin = new long[] { 0 }; - double cumulative = 0; - int i = 0; - for ( ; i < numBins && cumulative < cumulativeMinCutoff; ++i ) - { - bin[ 0 ] = i; - cumulative += dfd.relativeFrequency( bin ); - } - final int min = i * 65535 / numBins; - for ( ; i < numBins && cumulative < cumulativeMaxCutoff; ++i ) - { - bin[ 0 ] = i; - cumulative += dfd.relativeFrequency( bin ); - } - final int max = i * 65535 / numBins; - return new Bounds( min, max ); + return estimateBounds( img, cumulativeMinCutoff, cumulativeMaxCutoff); } else if ( type instanceof UnsignedByteType ) return new Bounds( 0, 255 ); @@ -226,6 +207,39 @@ else if ( type instanceof UnsignedByteType ) return new Bounds( 0, 65535 ); } + /** + * @param img + * 3D image, histogram of the center plane is used for estimating instensity range. + * @param cumulativeMinCutoff + * fraction of pixels that are allowed to be saturated at the lower end of the range. + * @param cumulativeMaxCutoff + * fraction of pixels that are allowed to be saturated at the upper end of the range. + */ + private static < T extends RealType< T > > Bounds estimateBounds( final RandomAccessibleInterval< T > img, final double cumulativeMinCutoff, final double cumulativeMaxCutoff ) + { + final long z = ( img.min( 2 ) + img.max( 2 ) + 1 ) / 2; + + final int numBins = 6535; + final Histogram1d< ? > histogram = new Histogram1d<>( Views.hyperSlice( img, 2, z ), new Real1dBinMapper<>( 0, 65535, numBins, false ) ); + final DiscreteFrequencyDistribution dfd = histogram.dfd(); + final long[] bin = new long[] { 0 }; + double cumulative = 0; + int i = 0; + for ( ; i < numBins && cumulative < cumulativeMinCutoff; ++i ) + { + bin[ 0 ] = i; + cumulative += dfd.relativeFrequency( bin ); + } + final int min = i * 65535 / numBins; + for ( ; i < numBins && cumulative < cumulativeMaxCutoff; ++i ) + { + bin[ 0 ] = i; + cumulative += dfd.relativeFrequency( bin ); + } + final int max = i * 65535 / numBins; + return new Bounds( min, max ); + } + @Deprecated public static AffineTransform3D initTransform( final int viewerWidth, final int viewerHeight, final boolean zoomedIn, final bdv.viewer.state.ViewerState state ) { diff --git a/src/main/java/bdv/tools/RecordMaxProjectionDialog.java b/src/main/java/bdv/tools/RecordMaxProjectionDialog.java index 5aadc050a..1ad809d1e 100644 --- a/src/main/java/bdv/tools/RecordMaxProjectionDialog.java +++ b/src/main/java/bdv/tools/RecordMaxProjectionDialog.java @@ -367,7 +367,7 @@ public int getHeight() final MyTarget target = new MyTarget(); final MultiResolutionRenderer renderer = new MultiResolutionRenderer( target, () -> {}, new double[] { 1 }, 0, 1, null, false, - viewer.getOptionValues().getAccumulateProjectorFactory(), new CacheControl.Dummy() ); + new CacheControl.Dummy() ); progressWriter.setProgress( 0 ); for ( int timepoint = minTimepointIndex; timepoint <= maxTimepointIndex; ++timepoint ) { diff --git a/src/main/java/bdv/tools/RecordMovieDialog.java b/src/main/java/bdv/tools/RecordMovieDialog.java index 0df77e58a..1bb06fa9a 100644 --- a/src/main/java/bdv/tools/RecordMovieDialog.java +++ b/src/main/java/bdv/tools/RecordMovieDialog.java @@ -301,7 +301,7 @@ public int getHeight() final MyTarget target = new MyTarget(); final MultiResolutionRenderer renderer = new MultiResolutionRenderer( target, () -> {}, new double[] { 1 }, 0, 1, null, false, - viewer.getOptionValues().getAccumulateProjectorFactory(), new CacheControl.Dummy() ); + new CacheControl.Dummy() ); progressWriter.setProgress( 0 ); for ( int timepoint = minTimepointIndex; timepoint <= maxTimepointIndex; ++timepoint ) { diff --git a/src/main/java/bdv/tools/boundingbox/TransformedBoxPlaceHolderSource.java b/src/main/java/bdv/tools/boundingbox/TransformedBoxPlaceHolderSource.java index 5fe3129bd..1f11e7e36 100644 --- a/src/main/java/bdv/tools/boundingbox/TransformedBoxPlaceHolderSource.java +++ b/src/main/java/bdv/tools/boundingbox/TransformedBoxPlaceHolderSource.java @@ -37,8 +37,8 @@ import net.imglib2.RealLocalizable; import net.imglib2.RealRandomAccess; import net.imglib2.RealRandomAccessible; -import net.imglib2.Sampler; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.mask.Masked; import net.imglib2.type.numeric.integer.UnsignedShortType; import net.imglib2.util.Intervals; import net.imglib2.view.Views; @@ -106,6 +106,18 @@ public RealRandomAccessible< Void > getInterpolatedSource( final int t, final in return rra; } + @Override + public RandomAccessibleInterval< ? extends Masked< Void > > getMaskedSource( final int t, final int level ) + { + return Views.interval( Views.raster( Masked.withConstant( rra, 0 ) ), Intervals.smallestContainingInterval( bbSource.getInterval() ) ); + } + + @Override + public RealRandomAccessible< ? extends Masked< Void > > getInterpolatedMaskedSource( final int t, final int level, final Interpolation method ) + { + return Masked.withConstant( rra, 0 ); + } + @Override public void getSourceTransform( final int t, final int level, final AffineTransform3D transform ) { diff --git a/src/main/java/bdv/tools/transformation/TransformedSource.java b/src/main/java/bdv/tools/transformation/TransformedSource.java index b69a3657b..138d68e18 100644 --- a/src/main/java/bdv/tools/transformation/TransformedSource.java +++ b/src/main/java/bdv/tools/transformation/TransformedSource.java @@ -36,6 +36,7 @@ import net.imglib2.RandomAccessibleInterval; import net.imglib2.RealRandomAccessible; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.mask.Masked; /** * A {@link Source} that wraps another {@link Source} and allows to decorate it @@ -295,4 +296,16 @@ public Source< T > getWrappedSource() { return source; } + + @Override + public RandomAccessibleInterval< ? extends Masked< T > > getMaskedSource( final int t, final int level ) + { + return source.getMaskedSource( t, level ); + } + + @Override + public RealRandomAccessible< ? extends Masked< T > > getInterpolatedMaskedSource( final int t, final int level, final Interpolation method ) + { + return source.getInterpolatedMaskedSource( t, level, method ); + } } diff --git a/src/main/java/bdv/ui/viewermodepanel/DisplaySettingsPanel.java b/src/main/java/bdv/ui/viewermodepanel/DisplaySettingsPanel.java index 2197e7ba5..882f9a36c 100644 --- a/src/main/java/bdv/ui/viewermodepanel/DisplaySettingsPanel.java +++ b/src/main/java/bdv/ui/viewermodepanel/DisplaySettingsPanel.java @@ -30,15 +30,18 @@ import static bdv.viewer.Interpolation.NEARESTNEIGHBOR; import static bdv.viewer.Interpolation.NLINEAR; +import static bdv.viewer.ViewerStateChange.ACCUMULATE_PROJECTOR_CHANGED; import static bdv.viewer.ViewerStateChange.DISPLAY_MODE_CHANGED; import static bdv.viewer.ViewerStateChange.INTERPOLATION_CHANGED; +import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.UIManager; import bdv.ui.UIUtils; +import bdv.viewer.BlendModeSwitcher; import bdv.viewer.DisplayMode; import bdv.viewer.Interpolation; import bdv.viewer.ViewerState; @@ -59,12 +62,21 @@ public class DisplaySettingsPanel extends JPanel private static final String SOURCE_MODE_TOOL_TIP = "Source/Group"; private static final String NEAREST_INTERPOLATION_TOOL_TIP = "Nearest/Linear"; private static final String LINEAR_INTERPOLATION_TOOL_TIP = "Nearest/Linear"; + private static final String SUM_BLENDING_TOOL_TIP = "Sum/Average"; + private static final String AVG_BLENDING_TOOL_TIP = "Sum/Average"; + private static final String CUSTOM_BLENDING_TOOL_TIP = "Sum/Average/Custom"; private final LabeledToggleButton fusion; private final LabeledToggleButton grouping; private final LabeledToggleButton interpolation; + private final LabeledOptionButton blending; public DisplaySettingsPanel( final ViewerState state ) + { + this( state, true ); // TODO: change to false by default, so that it doesn't show in BVV + } + + public DisplaySettingsPanel( final ViewerState state, final boolean showBlendMode ) { super( new MigLayout( "ins 2 0 0 0, fillx, filly", "[][][]", "top" ) ); @@ -128,6 +140,35 @@ else if ( e == INTERPOLATION_CHANGED ) this.add( fusion ); this.add( grouping ); this.add( interpolation ); + + if ( showBlendMode ) + { + final BlendModeSwitcher blendModeSwitcher = new BlendModeSwitcher( state ); + blending = new LabeledOptionButton( + new Icon[] { + new ImageIcon( this.getClass().getResource( "blend_sum" + isDark + isLarge + ".png" ) ), + new ImageIcon( this.getClass().getResource( "blend_avg" + isDark + isLarge + ".png" ) ), + new ImageIcon( this.getClass().getResource( "blend_custom" + isDark + isLarge + ".png" ) ) }, + new String[] { + "Sum", + "Avg", + "Custom" + }, + new String[] { + SUM_BLENDING_TOOL_TIP, + AVG_BLENDING_TOOL_TIP, + CUSTOM_BLENDING_TOOL_TIP } ); + blending.setOption( blendModeSwitcher.getCurrentMode().ordinal() ); + blending.addActionListener( e -> blendModeSwitcher.switchToNextMode() ); + blendModeSwitcher.changeListeners().add( mode -> { + blending.setOption( mode.ordinal() ); + } ); + this.add( blending ); + } + else + { + blending = null; + } } @Override @@ -152,6 +193,14 @@ public void updateUI() new ImageIcon( this.getClass().getResource( "nearest" + isDark + isLarge + ".png" ) ), new ImageIcon( this.getClass().getResource( "linear" + isDark + isLarge + ".png" ) ) ); + if ( blending != null ) + { + blending.setIcons( new Icon[] { + new ImageIcon( this.getClass().getResource( "blend_sum" + isDark + isLarge + ".png" ) ), + new ImageIcon( this.getClass().getResource( "blend_avg" + isDark + isLarge + ".png" ) ), + new ImageIcon( this.getClass().getResource( "blend_custom" + isDark + isLarge + ".png" ) ) + } ); + } } } } diff --git a/src/main/java/bdv/ui/viewermodepanel/LabeledOptionButton.java b/src/main/java/bdv/ui/viewermodepanel/LabeledOptionButton.java new file mode 100644 index 000000000..8bf595c4e --- /dev/null +++ b/src/main/java/bdv/ui/viewermodepanel/LabeledOptionButton.java @@ -0,0 +1,73 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2025 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.viewermodepanel; + +import javax.swing.Icon; +import javax.swing.JLabel; + +import bdv.ui.UIUtils; + +public class LabeledOptionButton extends OptionButton +{ + private final JLabel label; + + public LabeledOptionButton( + final Icon[] icons, + final String[] texts, + final String[] tooltipTexts ) + { + super( icons, tooltipTexts ); + if ( texts.length != icons.length ) + throw new IllegalArgumentException(); + + label = new JLabel( texts[ 0 ] ); + setFont( label ); + button.addPropertyChangeListener( e -> { + if ( e.getPropertyName().equals( "option" ) ) + label.setText( texts[ getOption() ] ); + } ); + + this.add( label, "center" ); + } + + private void setFont( final JLabel label ) + { + label.setFont( UIUtils.getFont( "monospaced.bold.mini.font" ) ); +// label.setFont( new Font( Font.MONOSPACED, Font.BOLD, ( int )Math.round(9 * UIUtils.getUIScaleFactor( this ) ) ) ); + } + + @Override + public void updateUI() + { + super.updateUI(); + + if ( label != null ) + setFont( label ); + } +} diff --git a/src/main/java/bdv/ui/viewermodepanel/LabeledToggleButton.java b/src/main/java/bdv/ui/viewermodepanel/LabeledToggleButton.java index 88d21e1dc..e3cd059b8 100644 --- a/src/main/java/bdv/ui/viewermodepanel/LabeledToggleButton.java +++ b/src/main/java/bdv/ui/viewermodepanel/LabeledToggleButton.java @@ -55,14 +55,9 @@ public LabeledToggleButton( label = new JLabel( text ); setFont( label ); - this.add( label, "center" ); - } + button.addItemListener( e -> label.setText( isSelected() ? selectedText : text ) ); - @Override - public void setSelected( final boolean selected ) - { - super.setSelected( selected ); - label.setText( selected ? selectedText : text ); + this.add( label, "center" ); } private void setFont( final JLabel label ) diff --git a/src/main/java/bdv/ui/viewermodepanel/OptionButton.java b/src/main/java/bdv/ui/viewermodepanel/OptionButton.java new file mode 100644 index 000000000..63ba9d137 --- /dev/null +++ b/src/main/java/bdv/ui/viewermodepanel/OptionButton.java @@ -0,0 +1,126 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2025 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.viewermodepanel; + +import java.awt.Dimension; +import java.awt.event.ActionListener; + +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; + +/** + * A compact UI button that can be used to cycles through multiple exclusive + * options (icon + tooltip) each time it's clicked. + *

+ * The update of the current option has to be managed by outside logic that + * calls {@link #setOption} in response to {@code ActionEvent}s. + */ +class OptionButton extends JPanel +{ + final JButton button; + + private final Icon[] icons; + + private int currentOption = 0; + + public OptionButton( + final Icon[] icons, + final String[] tooltipTexts ) + { + super( new MigLayout( "ins 0, fillx, filly", "[]", "[]0lp![]" ) ); + + if ( icons == null || tooltipTexts == null ) + throw new NullPointerException(); + + if ( icons.length == 0 || icons.length != tooltipTexts.length ) + throw new IllegalArgumentException(); + + this.icons = icons; + + button = new JButton( icons[ 0 ] ); + button.setToolTipText( tooltipTexts[ 0 ] ); + setLook( button ); + + button.addPropertyChangeListener( e -> { + if ( e.getPropertyName().equals( "option" ) ) + { + button.setIcon( this.icons[ currentOption ] ); + button.setToolTipText( tooltipTexts[ currentOption ] ); + } + } ); + + this.add( button, "growx, center, wrap" ); + } + + public void setIcons( final Icon[] icons ) + { + if ( icons.length != this.icons.length ) + throw new IllegalArgumentException(); + + System.arraycopy( icons, 0, this.icons, 0, icons.length ); + + button.setIcon( icons[ currentOption ] ); + setLook( button ); + } + + public int getOption() + { + return currentOption; + } + + public void setOption( int option ) + { + if ( option < 0 || option >= icons.length ) + throw new IllegalArgumentException( "Invalid option index: " + option ); + final int oldOption = currentOption; + currentOption = option; + button.firePropertyChange( "option", oldOption, currentOption ); + } + + public void addActionListener( final ActionListener l ) + { + button.addActionListener( l ); + } + + public void removeActionListener( final ActionListener l ) + { + button.removeActionListener( l ); + } + + private void setLook( final JButton button ) + { + button.setMaximumSize( new Dimension( button.getIcon().getIconWidth(), button.getIcon().getIconHeight() ) ); + button.setBorderPainted( false ); + button.setFocusPainted( false ); + button.setContentAreaFilled( false ); + } +} diff --git a/src/main/java/bdv/ui/viewermodepanel/ToggleButton.java b/src/main/java/bdv/ui/viewermodepanel/ToggleButton.java index 48cc80a71..ac01aa91a 100644 --- a/src/main/java/bdv/ui/viewermodepanel/ToggleButton.java +++ b/src/main/java/bdv/ui/viewermodepanel/ToggleButton.java @@ -42,7 +42,7 @@ class ToggleButton extends JPanel private final String tooltipText; private final String selectedTooltipText; - private final JToggleButton button; + final JToggleButton button; public ToggleButton( final Icon icon, @@ -58,6 +58,8 @@ public ToggleButton( button.setSelectedIcon( selectedIcon ); setLook( button ); + button.addItemListener( e -> button.setToolTipText( isSelected() ? selectedTooltipText : tooltipText ) ); + this.add( button, "growx, center, wrap" ); } @@ -71,7 +73,6 @@ public void setIcons( final Icon defaultIcon, final Icon selectedIcon ) public void setSelected( final boolean selected ) { button.setSelected( selected ); - button.setToolTipText( selected ? selectedTooltipText : tooltipText ); } public boolean isSelected() diff --git a/src/main/java/bdv/util/AbstractSource.java b/src/main/java/bdv/util/AbstractSource.java index d5496e0b3..304dd106e 100644 --- a/src/main/java/bdv/util/AbstractSource.java +++ b/src/main/java/bdv/util/AbstractSource.java @@ -31,13 +31,16 @@ import java.util.function.Supplier; import bdv.viewer.Interpolation; +import bdv.viewer.MaskUtils; import bdv.viewer.Source; import mpicbg.spim.data.sequence.DefaultVoxelDimensions; -import mpicbg.spim.data.sequence.FinalVoxelDimensions; import mpicbg.spim.data.sequence.VoxelDimensions; +import net.imglib2.RandomAccessibleInterval; import net.imglib2.RealRandomAccessible; import net.imglib2.type.Type; +import net.imglib2.type.mask.Masked; import net.imglib2.type.numeric.NumericType; +import net.imglib2.util.Cast; import net.imglib2.view.Views; public abstract class AbstractSource< T extends NumericType< T > > implements Source< T > @@ -107,6 +110,18 @@ public RealRandomAccessible< T > getInterpolatedSource( final int t, final int l return Views.interpolate( Views.extendZero( getSource( t, level ) ), interpolators.get( method ) ); } + @Override + public RandomAccessibleInterval< ? extends Masked< T > > getMaskedSource( final int t, final int level ) + { + return Masked.withConstant( getSource( t, level ), 1 ); + } + + @Override + public RealRandomAccessible< ? extends Masked< T > > getInterpolatedMaskedSource( final int t, final int level, final Interpolation method ) + { + return MaskUtils.extendAndInterpolateMasked( Cast.unchecked( getMaskedSource( t, level ) ), method ); + } + @Override public String getName() { diff --git a/src/main/java/bdv/util/PlaceHolderSource.java b/src/main/java/bdv/util/PlaceHolderSource.java index 55189f2fd..315688676 100644 --- a/src/main/java/bdv/util/PlaceHolderSource.java +++ b/src/main/java/bdv/util/PlaceHolderSource.java @@ -35,6 +35,7 @@ import bdv.viewer.Interpolation; import bdv.viewer.Source; import mpicbg.spim.data.sequence.VoxelDimensions; +import net.imglib2.type.mask.Masked; /** * A dummy {@link Source} that represents a {@link BdvOverlay}. @@ -101,6 +102,18 @@ public RealRandomAccessible< Void > getInterpolatedSource( final int t, final in return null; } + @Override + public RandomAccessibleInterval< ? extends Masked< Void > > getMaskedSource( final int t, final int level ) + { + return null; + } + + @Override + public RealRandomAccessible< ? extends Masked< Void > > getInterpolatedMaskedSource( final int t, final int level, final Interpolation method ) + { + return null; + } + @Override public void getSourceTransform( final int t, final int level, final AffineTransform3D transform ) { diff --git a/src/main/java/bdv/util/RandomAccessibleIntervalMipmapSource4D.java b/src/main/java/bdv/util/RandomAccessibleIntervalMipmapSource4D.java index 792f1a354..89044ae17 100644 --- a/src/main/java/bdv/util/RandomAccessibleIntervalMipmapSource4D.java +++ b/src/main/java/bdv/util/RandomAccessibleIntervalMipmapSource4D.java @@ -153,14 +153,12 @@ private void loadTimepoint( final int timepointIndex ) currentTimePointIndex = timepointIndex; if ( isPresent( timepointIndex ) ) { - final T zero = getType().createVariable(); - zero.setZero(); for ( int level = 0; level < currentMipmaps.length; ++level ) { currentMipmaps[level] = Views.hyperSlice(mipmapSources[level], 3, timepointIndex ); for ( final Interpolation method : Interpolation.values() ) currentInterpolatedSources[ method.ordinal() ][ level ] = - Views.interpolate( Views.extendValue( currentMipmaps[level], zero ), interpolators.get( method ) ); + Views.interpolate( Views.extendZero( currentMipmaps[level] ), interpolators.get( method ) ); } } else diff --git a/src/main/java/bdv/util/RandomAccessibleIntervalSource.java b/src/main/java/bdv/util/RandomAccessibleIntervalSource.java index 4f5626e81..37d31e8b9 100644 --- a/src/main/java/bdv/util/RandomAccessibleIntervalSource.java +++ b/src/main/java/bdv/util/RandomAccessibleIntervalSource.java @@ -72,10 +72,8 @@ public RandomAccessibleIntervalSource( this.source = img; this.sourceTransform = sourceTransform; interpolatedSources = new RealRandomAccessible[ Interpolation.values().length ]; - final T zero = getType().createVariable(); - zero.setZero(); for ( final Interpolation method : Interpolation.values() ) - interpolatedSources[ method.ordinal() ] = Views.interpolate( Views.extendValue( source, zero ), interpolators.get( method ) ); + interpolatedSources[ method.ordinal() ] = Views.interpolate( Views.extendZero( source ), interpolators.get( method ) ); } @Override diff --git a/src/main/java/bdv/util/RandomAccessibleIntervalSource4D.java b/src/main/java/bdv/util/RandomAccessibleIntervalSource4D.java index a43e033ff..f4244e1b0 100644 --- a/src/main/java/bdv/util/RandomAccessibleIntervalSource4D.java +++ b/src/main/java/bdv/util/RandomAccessibleIntervalSource4D.java @@ -75,11 +75,9 @@ private void loadTimepoint( final int timepointIndex ) currentTimePointIndex = timepointIndex; if ( isPresent( timepointIndex ) ) { - final T zero = getType().createVariable(); - zero.setZero(); currentSource = Views.hyperSlice( source, 3, timepointIndex ); for ( final Interpolation method : Interpolation.values() ) - currentInterpolatedSources[ method.ordinal() ] = Views.interpolate( Views.extendValue( currentSource, zero ), interpolators.get( method ) ); + currentInterpolatedSources[ method.ordinal() ] = Views.interpolate( Views.extendZero( currentSource ), interpolators.get( method ) ); } else { diff --git a/src/main/java/bdv/util/RandomAccessibleSource.java b/src/main/java/bdv/util/RandomAccessibleSource.java index 144625a5d..474a342fa 100644 --- a/src/main/java/bdv/util/RandomAccessibleSource.java +++ b/src/main/java/bdv/util/RandomAccessibleSource.java @@ -34,6 +34,7 @@ import net.imglib2.RandomAccessibleInterval; import net.imglib2.RealRandomAccessible; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.mask.Masked; import net.imglib2.type.numeric.NumericType; import net.imglib2.view.Views; @@ -90,12 +91,24 @@ public RandomAccessibleInterval< T > getSource( final int t, final int level ) return Views.interval( source, interval ); } + @Override + public RandomAccessibleInterval< ? extends Masked< T > > getMaskedSource( final int t, final int level ) + { + return Views.interval( Masked.withConstant( source, 1 ), interval ); + } + @Override public RealRandomAccessible< T > getInterpolatedSource( final int t, final int level, final Interpolation method ) { return interpolatedSources[ method.ordinal() ]; } + @Override + public RealRandomAccessible< ? extends Masked< T > > getInterpolatedMaskedSource( final int t, final int level, final Interpolation method ) + { + return Masked.withConstant( getInterpolatedSource( t, level, method ), 1 ); + } + @Override public synchronized void getSourceTransform( final int t, final int level, final AffineTransform3D transform ) { diff --git a/src/main/java/bdv/util/RandomAccessibleSource4D.java b/src/main/java/bdv/util/RandomAccessibleSource4D.java index ffabc89fd..fa2615ee5 100644 --- a/src/main/java/bdv/util/RandomAccessibleSource4D.java +++ b/src/main/java/bdv/util/RandomAccessibleSource4D.java @@ -36,6 +36,7 @@ import net.imglib2.RandomAccessibleInterval; import net.imglib2.RealRandomAccessible; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.mask.Masked; import net.imglib2.type.numeric.NumericType; import net.imglib2.util.Intervals; import net.imglib2.view.Views; @@ -52,6 +53,8 @@ public class RandomAccessibleSource4D< T extends NumericType< T > > extends Abst private RandomAccessibleInterval< T > currentSource; + private RandomAccessibleInterval< ? extends Masked< T > > currentMaskedSource; + private final RealRandomAccessible< T >[] currentInterpolatedSources; private final AffineTransform3D sourceTransform; @@ -98,10 +101,9 @@ private void loadTimepoint( final int timepointIndex ) currentTimePointIndex = timepointIndex; if ( isPresent( timepointIndex ) ) { - final T zero = getType().createVariable(); - zero.setZero(); final RandomAccessible< T > slice = Views.hyperSlice( source, 3, timepointIndex ); currentSource = Views.interval( slice, timeSliceInterval ); + currentMaskedSource = Views.interval( Masked.withConstant( slice, 1 ), timeSliceInterval ); for ( final Interpolation method : Interpolation.values() ) currentInterpolatedSources[ method.ordinal() ] = Views.interpolate( slice, interpolators.get( method ) ); } @@ -126,6 +128,14 @@ public synchronized RandomAccessibleInterval< T > getSource( final int t, final return currentSource; } + @Override + public synchronized RandomAccessibleInterval< ? extends Masked< T > > getMaskedSource( final int t, final int level ) + { + if ( t != currentTimePointIndex ) + loadTimepoint( t ); + return currentMaskedSource; + } + @Override public synchronized RealRandomAccessible< T > getInterpolatedSource( final int t, final int level, final Interpolation method ) { @@ -134,6 +144,12 @@ public synchronized RealRandomAccessible< T > getInterpolatedSource( final int t return currentInterpolatedSources[ method.ordinal() ]; } + @Override + public RealRandomAccessible< ? extends Masked< T > > getInterpolatedMaskedSource( final int t, final int level, final Interpolation method ) + { + return Masked.withConstant( getInterpolatedSource( t, level, method ), 1 ); + } + @Override public synchronized void getSourceTransform( final int t, final int level, final AffineTransform3D transform ) { diff --git a/src/main/java/bdv/util/RealRandomAccessibleSource.java b/src/main/java/bdv/util/RealRandomAccessibleSource.java index 541b4a787..36c0ac6eb 100644 --- a/src/main/java/bdv/util/RealRandomAccessibleSource.java +++ b/src/main/java/bdv/util/RealRandomAccessibleSource.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -36,6 +36,7 @@ import net.imglib2.RealRandomAccessible; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.type.Type; +import net.imglib2.type.mask.Masked; import net.imglib2.view.Views; /** @@ -104,6 +105,18 @@ public RealRandomAccessible< T > getInterpolatedSource( final int t, final int l return accessible; } + @Override + public RandomAccessibleInterval< ? extends Masked< T > > getMaskedSource( final int t, final int level ) + { + return Views.interval( Views.raster( Masked.withConstant( accessible, 1 ) ), getInterval( t, level ) ); + } + + @Override + public RealRandomAccessible< ? extends Masked< T > > getInterpolatedMaskedSource( final int t, final int level, final Interpolation method ) + { + return Masked.withConstant( accessible, 1 ); + } + @Override public void getSourceTransform( final int t, final int level, final AffineTransform3D transform ) { diff --git a/src/main/java/bdv/viewer/BasicViewerState.java b/src/main/java/bdv/viewer/BasicViewerState.java index e31e8eb18..aa1b2b7ad 100644 --- a/src/main/java/bdv/viewer/BasicViewerState.java +++ b/src/main/java/bdv/viewer/BasicViewerState.java @@ -30,6 +30,8 @@ import bdv.util.Affine3DHelpers; import bdv.util.WrappedList; +import bdv.viewer.render.AccumulateProjectorARGB; +import bdv.viewer.render.AccumulateProjectorFactory; import gnu.trove.map.TObjectIntMap; import gnu.trove.map.hash.TObjectIntHashMap; import java.util.ArrayList; @@ -43,8 +45,11 @@ import java.util.Objects; import java.util.Set; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.ARGBType; + import org.scijava.listeners.Listeners; +import static bdv.viewer.ViewerStateChange.ACCUMULATE_PROJECTOR_CHANGED; import static bdv.viewer.ViewerStateChange.CURRENT_GROUP_CHANGED; import static bdv.viewer.ViewerStateChange.CURRENT_SOURCE_CHANGED; import static bdv.viewer.ViewerStateChange.CURRENT_TIMEPOINT_CHANGED; @@ -102,6 +107,11 @@ public class BasicViewerState implements ViewerState */ private DisplayMode displayMode; + /** + * The current AccumulateProjectorFactory. + */ + private AccumulateProjectorFactory< ARGBType > accumulateProjectorFactory; + // -- sources -- private final List< SourceAndConverter< ? > > sources; @@ -149,6 +159,7 @@ public BasicViewerState() viewerTransform = new AffineTransform3D(); interpolation = Interpolation.NEARESTNEIGHBOR; displayMode = DisplayMode.SINGLE; + accumulateProjectorFactory = AccumulateProjectorARGB.factory; sources = new ArrayList<>(); unmodifiableSources = new UnmodifiableSources(); activeSources = new HashSet<>(); @@ -176,6 +187,7 @@ public BasicViewerState( final ViewerState other ) viewerTransform = other.getViewerTransform(); interpolation = other.getInterpolation(); displayMode = other.getDisplayMode(); + accumulateProjectorFactory = other.getAccumulateProjectorFactory(); sources = new ArrayList<>( other.getSources() ); unmodifiableSources = new UnmodifiableSources(); @@ -215,6 +227,7 @@ public void set( final ViewerState other ) viewerTransform.set( other.getViewerTransform() ); interpolation = other.getInterpolation(); displayMode = other.getDisplayMode(); + accumulateProjectorFactory = other.getAccumulateProjectorFactory(); sources.clear(); sources.addAll( other.getSources() ); @@ -297,6 +310,23 @@ public void setDisplayMode( final DisplayMode mode ) } } + @Override + public AccumulateProjectorFactory< ARGBType > getAccumulateProjectorFactory() + { + return accumulateProjectorFactory; + } + + @Override + public void setAccumulateProjectorFactory( final AccumulateProjectorFactory< ARGBType > factory ) + { + if ( accumulateProjectorFactory != factory ) + { + accumulateProjectorFactory = factory; + notifyListeners( ACCUMULATE_PROJECTOR_CHANGED ); + } + + } + @Override public int getNumTimepoints() { diff --git a/src/main/java/bdv/viewer/BlendMode.java b/src/main/java/bdv/viewer/BlendMode.java new file mode 100644 index 000000000..c4b2cd033 --- /dev/null +++ b/src/main/java/bdv/viewer/BlendMode.java @@ -0,0 +1,69 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2025 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer; + +import bdv.viewer.render.AccumulateProjectorARGB; +import bdv.viewer.render.AccumulateProjectorFactory; +import bdv.viewer.render.AlphaWeightedAccumulateProjectorARGB; +import net.imglib2.type.numeric.ARGBType; + +/** + * Blending modes. + *

+ * The {@code BlendMode} is determined by the {@link AccumulateProjectorFactory} + * used for combining overlapping sources into the final screen image. + */ +public enum BlendMode +{ + SUM( "sum sources" ), + AVG( "average sources" ), + CUSTOM( "custom source accumulator" ); + + private final String name; + + BlendMode( final String name ) + { + this.name = name; + } + + public String getName() + { + return name; + } + + public static BlendMode of( final AccumulateProjectorFactory< ARGBType > factory ) + { + if ( factory instanceof AccumulateProjectorARGB.Factory ) + return BlendMode.SUM; + else if ( factory instanceof AlphaWeightedAccumulateProjectorARGB.Factory ) + return BlendMode.AVG; + else + return BlendMode.CUSTOM; + } +} diff --git a/src/main/java/bdv/viewer/BlendModeSwitcher.java b/src/main/java/bdv/viewer/BlendModeSwitcher.java new file mode 100644 index 000000000..c031221bf --- /dev/null +++ b/src/main/java/bdv/viewer/BlendModeSwitcher.java @@ -0,0 +1,106 @@ +package bdv.viewer; + +import static bdv.viewer.BlendMode.AVG; +import static bdv.viewer.BlendMode.CUSTOM; +import static bdv.viewer.BlendMode.SUM; +import static bdv.viewer.ViewerStateChange.ACCUMULATE_PROJECTOR_CHANGED; + +import org.scijava.listeners.Listeners; + +import bdv.viewer.render.AccumulateProjectorARGB; +import bdv.viewer.render.AccumulateProjectorFactory; +import bdv.viewer.render.AlphaWeightedAccumulateProjectorARGB; +import net.imglib2.type.numeric.ARGBType; + +public class BlendModeSwitcher +{ + private final ViewerState state; + + private AccumulateProjectorFactory< ARGBType > customFactory; + + private BlendMode mode; + + public BlendModeSwitcher( final ViewerState state ) + { + this.state = state; + listeners = new Listeners.List<>(); + + state.changeListeners().add( c -> + { + if ( c == ACCUMULATE_PROJECTOR_CHANGED ) + factoryChanged( state.getAccumulateProjectorFactory() ); + } ); + factoryChanged( state.getAccumulateProjectorFactory() ); + } + + /** + * {@code BlendModeChangeListener}s are notified about blend mode changes. + */ + public interface BlendModeChangeListener + { + void blendModeChanged(BlendMode newMode); + } + + private final Listeners.List< BlendModeChangeListener > listeners; + + public Listeners< BlendModeChangeListener > changeListeners() + { + return listeners; + } + + public BlendMode getCurrentMode() + { + return mode; + } + + public void switchToNextMode() + { + mode = next(); + state.setAccumulateProjectorFactory( factory() ); + listeners.list.forEach( l -> l.blendModeChanged( mode ) ); + } + + private BlendMode next() + { + switch ( mode ) + { + case SUM: + return AVG; + case AVG: + return customFactory != null ? CUSTOM : SUM; + case CUSTOM: + return SUM; + default: + throw new IllegalArgumentException(); + } + } + + private AccumulateProjectorFactory< ARGBType > factory() + { + switch ( mode ) + { + case SUM: + return AccumulateProjectorARGB.factory; + case AVG: + return AlphaWeightedAccumulateProjectorARGB.factory; + case CUSTOM: + return customFactory; + default: + throw new IllegalArgumentException(); + } + } + + private void factoryChanged( final AccumulateProjectorFactory< ARGBType > factory ) + { + final BlendMode oldMode = mode; + mode = BlendMode.of( factory ); + + if ( mode == CUSTOM ) + customFactory = factory; + + if ( mode != oldMode ) + { + listeners.list.forEach( l -> l.blendModeChanged( mode ) ); + } + } +} diff --git a/src/main/java/bdv/viewer/InteractiveDisplayCanvas.java b/src/main/java/bdv/viewer/InteractiveDisplayCanvas.java index 3850e4eda..a53702984 100644 --- a/src/main/java/bdv/viewer/InteractiveDisplayCanvas.java +++ b/src/main/java/bdv/viewer/InteractiveDisplayCanvas.java @@ -28,9 +28,11 @@ */ package bdv.viewer; +import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; +import java.awt.Graphics2D; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.FocusListener; @@ -207,6 +209,8 @@ public void setTransformEventHandler( final TransformEventHandler transformEvent @Override public void paintComponent( final Graphics g ) { + g.setColor( Color.BLACK ); + g.fillRect( 0, 0, getWidth(), getHeight() ); overlayRenderers.list.forEach( r -> r.drawOverlays( g ) ); } diff --git a/src/main/java/bdv/viewer/MaskUtils.java b/src/main/java/bdv/viewer/MaskUtils.java new file mode 100644 index 000000000..bb46e55ad --- /dev/null +++ b/src/main/java/bdv/viewer/MaskUtils.java @@ -0,0 +1,29 @@ +package bdv.viewer; + +import net.imglib2.RandomAccessible; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.RealRandomAccessible; +import net.imglib2.interpolation.InterpolatorFactory; +import net.imglib2.interpolation.randomaccess.NearestNeighborInterpolatorFactory; +import net.imglib2.type.Type; +import net.imglib2.type.mask.Masked; +import net.imglib2.type.mask.interpolation.MaskedClampingNLinearInterpolatorFactory; +import net.imglib2.type.numeric.NumericType; +import net.imglib2.view.Views; + +public class MaskUtils +{ + /** + * Zero-extend (value and mask are set to zero) and interpolate a {@code RandomAccessibleInterval>}. + */ + public static < M extends Masked< ? extends NumericType< ? > > & Type< M > > RealRandomAccessible< M > extendAndInterpolateMasked( final RandomAccessibleInterval< M > source, final Interpolation method ) + { + final InterpolatorFactory< M, RandomAccessible< M > > factory = ( method == Interpolation.NEARESTNEIGHBOR ) + ? new NearestNeighborInterpolatorFactory<>() + : new MaskedClampingNLinearInterpolatorFactory<>(); + final M zero = source.getType().createVariable(); + zero.setMask( 0 ); + zero.value().setZero(); + return Views.interpolate( Views.extendValue( source, zero ), factory ); + } +} diff --git a/src/main/java/bdv/viewer/NavigationActions.java b/src/main/java/bdv/viewer/NavigationActions.java index bd3f6f9b9..a2d344260 100644 --- a/src/main/java/bdv/viewer/NavigationActions.java +++ b/src/main/java/bdv/viewer/NavigationActions.java @@ -48,6 +48,7 @@ public class NavigationActions extends Actions public static final String TOGGLE_INTERPOLATION = "toggle interpolation"; public static final String TOGGLE_FUSED_MODE = "toggle fused mode"; public static final String TOGGLE_GROUPING = "toggle grouping"; + public static final String TOGGLE_BLEND_MODE = "toggle blending mode"; public static final String SET_CURRENT_SOURCE = "set current source %d"; public static final String TOGGLE_SOURCE_VISIBILITY = "toggle source visibility %d"; public static final String ALIGN_XY_PLANE = "align XY plane"; @@ -59,6 +60,7 @@ public class NavigationActions extends Actions public static final String[] TOGGLE_INTERPOLATION_KEYS = new String[] { "I" }; public static final String[] TOGGLE_FUSED_MODE_KEYS = new String[] { "F" }; public static final String[] TOGGLE_GROUPING_KEYS = new String[] { "G" }; + public static final String[] TOGGLE_BLEND_MODE_KEYS = new String[] { "U" }; public static final String SET_CURRENT_SOURCE_KEYS_FORMAT = "%s"; public static final String TOGGLE_SOURCE_VISIBILITY_KEYS_FORMAT = "shift %s"; public static final String[] ALIGN_XY_PLANE_KEYS = new String[] { "shift Z" }; @@ -84,6 +86,7 @@ public void getCommandDescriptions( final CommandDescriptions descriptions ) descriptions.add( TOGGLE_INTERPOLATION, TOGGLE_INTERPOLATION_KEYS, "Switch between nearest-neighbor and n-linear interpolation mode in BigDataViewer." ); descriptions.add( TOGGLE_FUSED_MODE, TOGGLE_FUSED_MODE_KEYS, "TODO" ); descriptions.add( TOGGLE_GROUPING, TOGGLE_GROUPING_KEYS, "TODO" ); + descriptions.add( TOGGLE_BLEND_MODE, TOGGLE_BLEND_MODE_KEYS, "Switch between summing and averaging (and possibly custom blending) of overlapping sources" ); final String[] numkeys = new String[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }; IntStream.range( 0, numkeys.length ).forEach( i -> { @@ -121,6 +124,9 @@ public static void installModeActions( final Actions actions, final ViewerState actions.runnableAction( () -> toggleInterpolation( state ), TOGGLE_INTERPOLATION, TOGGLE_INTERPOLATION_KEYS ); actions.runnableAction( () -> toggleFusedMode( state ), TOGGLE_FUSED_MODE, TOGGLE_FUSED_MODE_KEYS ); actions.runnableAction( () -> toggleGroupingMode( state ), TOGGLE_GROUPING, TOGGLE_GROUPING_KEYS ); + + final BlendModeSwitcher blending = new BlendModeSwitcher( state ); + actions.runnableAction( blending::switchToNextMode, TOGGLE_BLEND_MODE, TOGGLE_BLEND_MODE_KEYS ); } public static void installTimeActions( final Actions actions, final ViewerState state ) diff --git a/src/main/java/bdv/viewer/Source.java b/src/main/java/bdv/viewer/Source.java index 2956fe1ca..f37bbef00 100644 --- a/src/main/java/bdv/viewer/Source.java +++ b/src/main/java/bdv/viewer/Source.java @@ -35,6 +35,7 @@ import net.imglib2.RandomAccessibleInterval; import net.imglib2.RealRandomAccessible; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.mask.Masked; /** * Provides image data for all timepoints of one view setup. @@ -132,4 +133,44 @@ default boolean doBoundingBoxCulling() VoxelDimensions getVoxelDimensions(); int getNumMipmapLevels(); + + /** + * Get a {@code Masked} version of the {@link #getSource 3D stack} at + * timepoint index t. + *

+ * By default, this is just {@link #getSource} augmented with a constant + * mask {@code 1.0}. + * + * @param t + * timepoint index + * @param level + * mipmap level + * @return the masked stack + */ + default RandomAccessibleInterval< ? extends Masked< T > > getMaskedSource( int t, int level ) { + return Masked.withConstant( getSource( t, level ), 1 ); + } + + /** + * Get a {@code Masked} version of the {@link #getInterpolatedSource 3D + * stack} at timepoint index t, extended to infinity and interpolated. + *

+ * By default, this is just {@link #getInterpolatedSource} augmented with a + * constant mask {@code 1.0}. + *

+ * However, most sources should override this to extend {@link + * #getMaskedSource(int, int)} to infinity (with a {@code 0} mask) and + * interpolate. + * + * @param t + * timepoint index + * @param level + * mipmap level + * @param method + * interpolation method to use + * @return the extended and interpolated {@link RandomAccessible stack}. + */ + default RealRandomAccessible< ? extends Masked< T > > getInterpolatedMaskedSource( int t, int level, final Interpolation method ) { + return Masked.withConstant( getInterpolatedSource( t, level, method ), 1 ); + } } diff --git a/src/main/java/bdv/viewer/SynchronizedViewerState.java b/src/main/java/bdv/viewer/SynchronizedViewerState.java index 655fbae13..6dbe28949 100644 --- a/src/main/java/bdv/viewer/SynchronizedViewerState.java +++ b/src/main/java/bdv/viewer/SynchronizedViewerState.java @@ -34,7 +34,9 @@ import java.util.List; import java.util.Set; +import bdv.viewer.render.AccumulateProjectorFactory; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.ARGBType; import org.scijava.listeners.Listeners; @@ -126,6 +128,18 @@ public synchronized void setDisplayMode( final DisplayMode mode ) state.setDisplayMode( mode ); } + @Override + public synchronized AccumulateProjectorFactory< ARGBType > getAccumulateProjectorFactory() + { + return state.getAccumulateProjectorFactory(); + } + + @Override + public synchronized void setAccumulateProjectorFactory( final AccumulateProjectorFactory< ARGBType > factory ) + { + state.setAccumulateProjectorFactory( factory ); + } + @Override public synchronized int getNumTimepoints() { diff --git a/src/main/java/bdv/viewer/UnmodifiableViewerState.java b/src/main/java/bdv/viewer/UnmodifiableViewerState.java index c31706780..c8249a967 100644 --- a/src/main/java/bdv/viewer/UnmodifiableViewerState.java +++ b/src/main/java/bdv/viewer/UnmodifiableViewerState.java @@ -32,7 +32,11 @@ import java.util.Comparator; import java.util.List; import java.util.Set; + +import bdv.viewer.render.AccumulateProjectorFactory; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.ARGBType; + import org.scijava.listeners.Listeners; /** @@ -86,6 +90,18 @@ public void setDisplayMode( final DisplayMode mode ) throw new UnsupportedOperationException(); } + @Override + public AccumulateProjectorFactory< ARGBType > getAccumulateProjectorFactory() + { + return state.getAccumulateProjectorFactory(); + } + + @Override + public void setAccumulateProjectorFactory( final AccumulateProjectorFactory< ARGBType > factory ) + { + throw new UnsupportedOperationException(); + } + @Override public int getNumTimepoints() { diff --git a/src/main/java/bdv/viewer/ViewerFrame.java b/src/main/java/bdv/viewer/ViewerFrame.java index 3e32337bd..3839a6e32 100644 --- a/src/main/java/bdv/viewer/ViewerFrame.java +++ b/src/main/java/bdv/viewer/ViewerFrame.java @@ -109,7 +109,7 @@ public ViewerFrame( final ViewerOptions optional ) { // super( "BigDataViewer", GuiUtil.getSuitableGraphicsConfiguration( GuiUtil.ARGB_COLOR_MODEL ) ); - super( "BigDataViewer", AWTUtils.getSuitableGraphicsConfiguration( AWTUtils.RGB_COLOR_MODEL ) ); + super( "BigDataViewer", AWTUtils.getSuitableGraphicsConfiguration( AWTUtils.ARGB_COLOR_MODEL ) ); this.keymapManager = keymapManager; this.appearanceManager = appearanceManager; viewer = new ViewerPanel( sources, numTimepoints, cacheControl, optional ); diff --git a/src/main/java/bdv/viewer/ViewerPanel.java b/src/main/java/bdv/viewer/ViewerPanel.java index ac33e00d3..f28c70f6e 100644 --- a/src/main/java/bdv/viewer/ViewerPanel.java +++ b/src/main/java/bdv/viewer/ViewerPanel.java @@ -229,6 +229,7 @@ public ViewerPanel( final List< SourceAndConverter< ? > > sources, final int num options = optional.values; state = setupState( sources, numTimepoints, options.getNumSourceGroups() ); + state.setAccumulateProjectorFactory( options.getAccumulateProjectorFactory() ); deprecatedState = new bdv.viewer.state.ViewerState( state ); multiBoxOverlayRenderer = new MultiBoxOverlayRenderer(); @@ -255,7 +256,6 @@ public ViewerPanel( final List< SourceAndConverter< ? > > sources, final int num options.getNumRenderingThreads(), renderingExecutorService, options.isUseVolatileIfAvailable(), - options.getAccumulateProjectorFactory(), cacheControl ); display.addHandler( mouseCoordinates ); @@ -588,6 +588,10 @@ public void viewerStateChanged( final ViewerStateChange change ) interpolationModeListeners.list.forEach( l -> l.interpolationModeChanged( interpolation ) ); requestRepaint(); break; + case ACCUMULATE_PROJECTOR_CHANGED: + showMessage( BlendMode.of( state.getAccumulateProjectorFactory() ).getName() ); + requestRepaint(); + break; case NUM_TIMEPOINTS_CHANGED: { final int numTimepoints = state.getNumTimepoints(); diff --git a/src/main/java/bdv/viewer/ViewerState.java b/src/main/java/bdv/viewer/ViewerState.java index d0d1bd62e..19b9bb3bf 100644 --- a/src/main/java/bdv/viewer/ViewerState.java +++ b/src/main/java/bdv/viewer/ViewerState.java @@ -32,7 +32,11 @@ import java.util.Comparator; import java.util.List; import java.util.Set; + +import bdv.viewer.render.AccumulateProjectorFactory; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.ARGBType; + import org.scijava.listeners.Listeners; /** @@ -97,6 +101,27 @@ public interface ViewerState */ void setDisplayMode( DisplayMode mode ); + /** + * Get the current {@code AccumulateProjectorFactory}. + *

+ * This factory provides AccumulateProjectors for blending rendered + * overlapping sources into the final screen image. + * + * @return current AccumulateProjector factory + */ + AccumulateProjectorFactory< ARGBType > getAccumulateProjectorFactory(); + + /** + * Set the {@code AccumulateProjectorFactory}. + *

+ * This factory provides AccumulateProjectors for blending rendered + * overlapping sources into the final screen image. This can be used for + * switching between "sum" and "average" blending mode for example. + * + * @param factory AccumulateProjector factory to set + */ + void setAccumulateProjectorFactory( AccumulateProjectorFactory< ARGBType > factory ); + /** * Get the number of timepoints. * diff --git a/src/main/java/bdv/viewer/ViewerStateChange.java b/src/main/java/bdv/viewer/ViewerStateChange.java index 384764aa7..46548f670 100644 --- a/src/main/java/bdv/viewer/ViewerStateChange.java +++ b/src/main/java/bdv/viewer/ViewerStateChange.java @@ -47,6 +47,7 @@ public enum ViewerStateChange VISIBILITY_CHANGED, DISPLAY_MODE_CHANGED, INTERPOLATION_CHANGED, + ACCUMULATE_PROJECTOR_CHANGED, NUM_TIMEPOINTS_CHANGED, CURRENT_TIMEPOINT_CHANGED, VIEWER_TRANSFORM_CHANGED; diff --git a/src/main/java/bdv/viewer/render/AbstractVolatileHierarchyProjector.java b/src/main/java/bdv/viewer/render/AbstractVolatileHierarchyProjector.java new file mode 100644 index 000000000..7a2267a80 --- /dev/null +++ b/src/main/java/bdv/viewer/render/AbstractVolatileHierarchyProjector.java @@ -0,0 +1,285 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2025 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer.render; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import net.imglib2.FinalInterval; +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessible; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.Volatile; +import net.imglib2.cache.iotiming.CacheIoTiming; +import net.imglib2.cache.iotiming.IoStatistics; +import net.imglib2.converter.Converter; +import net.imglib2.type.operators.SetZero; +import net.imglib2.util.Intervals; +import net.imglib2.util.StopWatch; +import net.imglib2.view.Views; + +/** + * {@link VolatileProjector} for a hierarchy of {@link Volatile} inputs. After each + * {@link #map()} call, the projector has a {@link #isValid() state} that + * signalizes whether all projected pixels were perfect. + * + * @author Stephan Saalfeld + * @author Tobias Pietzsch + */ +abstract class AbstractVolatileHierarchyProjector< A, B extends SetZero > implements VolatileProjector +{ + /** + * A converter from the source pixel type to the target pixel type. + */ + final Converter< ? super A, B > converter; + + /** + * The target interval. Pixels of the target interval should be set by + * {@link #map} + */ + private final RandomAccessibleInterval< B > target; + + /** + * List of source resolutions starting with the optimal resolution at index + * 0. During each {@link #map(boolean)}, for every pixel, resolution levels + * are successively queried until a valid pixel is found. + */ + private final List< RandomAccessible< A > > sources; + + /** + * Records, for every target pixel, the best (smallest index) source + * resolution level that has provided a valid value. Only better (lower + * index) resolutions are re-tried in successive {@link #map(boolean)} + * calls. + */ + final byte[] mask; + + /** + * {@code true} iff all target pixels were rendered with valid data from the + * optimal resolution level (level {@code 0}). + */ + private volatile boolean valid = false; + + /** + * How many levels (starting from level {@code 0}) have to be re-rendered in + * the next rendering pass, i.e., {@code map()} call. + */ + private int numInvalidLevels; + + /** + * Source interval which will be used for rendering. This is the 2D target + * interval expanded to source dimensionality (usually 3D) with + * {@code min=max=0} in the additional dimensions. + */ + private final FinalInterval sourceInterval; + + /** + * Time needed for rendering the last frame, in nano-seconds. + * This does not include time spent in blocking IO. + */ + private long lastFrameRenderNanoTime; + + /** + * Time spent in blocking IO rendering the last frame, in nano-seconds. + */ + private long lastFrameIoNanoTime; // TODO move to derived implementation for local sources only + + /** + * temporary variable to store the number of invalid pixels in the current + * rendering pass. + */ + private int numInvalidPixels; + + /** + * Flag to indicate that someone is trying to {@link #cancel()} rendering. + */ + private volatile boolean canceled = false; + + AbstractVolatileHierarchyProjector( + final List< ? extends RandomAccessible< A > > sources, + final Converter< ? super A, B > converter, + final RandomAccessibleInterval< B > target, + final byte[] maskArray) + { + this.converter = converter; + this.target = target; + this.sources = new ArrayList<>( sources ); + numInvalidLevels = sources.size(); + mask = maskArray; + + final int n = Math.max( 2, sources.get( 0 ).numDimensions() ); + final long[] min = new long[ n ]; + final long[] max = new long[ n ]; + min[ 0 ] = target.min( 0 ); + max[ 0 ] = target.max( 0 ); + min[ 1 ] = target.min( 1 ); + max[ 1 ] = target.max( 1 ); + sourceInterval = new FinalInterval( min, max ); + + lastFrameRenderNanoTime = -1; + clearMask(); + } + + @Override + public void cancel() + { + canceled = true; + } + + @Override + public long getLastFrameRenderNanoTime() + { + return lastFrameRenderNanoTime; + } + + public long getLastFrameIoNanoTime() + { + return lastFrameIoNanoTime; + } + + @Override + public boolean isValid() + { + return valid; + } + + /** + * Set all pixels in target to 100% transparent zero, and mask to all + * Integer.MAX_VALUE. + */ + public void clearMask() + { + final int size = ( int ) Intervals.numElements( target ); + Arrays.fill( mask, 0, size, Byte.MAX_VALUE ); + numInvalidLevels = sources.size(); + } + + /** + * Clear target pixels that were never written. + */ + private void clearUntouchedTargetPixels() + { + int i = 0; + for ( final B t : Views.flatIterable( target ) ) + if ( mask[ i++ ] == Byte.MAX_VALUE ) + t.setZero(); + } + + @Override + public boolean map( final boolean clearUntouchedTargetPixels ) + { + if ( canceled ) + return false; + + valid = false; + + final StopWatch stopWatch = StopWatch.createAndStart(); + final IoStatistics iostat = CacheIoTiming.getIoStatistics(); + final long startTimeIo = iostat.getIoNanoTime(); + + /* + * After the for loop, resolutionLevel is the highest (coarsest) + * resolution for which all pixels could be filled from valid data. This + * means that in the next pass, i.e., map() call, levels up to + * resolutionLevel have to be re-rendered. + */ + int resolutionLevel; + for ( resolutionLevel = 0; resolutionLevel < numInvalidLevels; ++resolutionLevel ) + { + numInvalidPixels = 0; + map( ( byte ) resolutionLevel); + if ( canceled ) + return false; + if ( numInvalidPixels == 0 ) + // if this pass was all valid + numInvalidLevels = resolutionLevel; + } + + if ( clearUntouchedTargetPixels && numInvalidPixels != 0 && !canceled ) + clearUntouchedTargetPixels(); + + lastFrameIoNanoTime = iostat.getIoNanoTime() - startTimeIo; + final long lastFrameTime = stopWatch.nanoTime(); + + // TODO (FORKJOIN): This is inaccurate now, should only use the io time used by current thread + // --> requires additional API in IoStatistics (imglib2-cache) + lastFrameRenderNanoTime = lastFrameTime - lastFrameIoNanoTime; + + valid = numInvalidLevels == 0; + + return !canceled; + } + + /** + * Copy lines from {@code y = startHeight} up to {@code endHeight} + * (exclusive) from source {@code resolutionIndex} to target. Check after + * each line whether rendering was {@link #cancel() canceled}. + *

+ * Only valid source pixels with a current mask value + * {@code mask>resolutionIndex} are copied to target, and their mask value + * is set to {@code mask=resolutionIndex}. Invalid source pixels are + * ignored. Pixels with {@code mask<=resolutionIndex} are ignored, because + * they have already been written to target during a previous pass. + *

+ * + * @param resolutionIndex + * index of source resolution level + */ + private void map( final byte resolutionIndex ) + { + if ( canceled ) + return; + + final RandomAccess< B > targetRandomAccess = target.randomAccess( target ); + final RandomAccess< A > sourceRandomAccess = sources.get( resolutionIndex ).randomAccess( sourceInterval ); + final int width = ( int ) target.dimension( 0 ); + final int height = ( int ) target.dimension( 1 ); + final long[] smin = Intervals.minAsLongArray( sourceInterval ); + int myNumInvalidPixels = 0; + + for ( int y = 0; y < height; ++y ) + { + // TODO (FORKJOIN) With tiles being granular enough, probably + // projectors shouldn't check after each line for cancellation + // (maybe not at all). + if ( canceled ) + return; + + sourceRandomAccess.setPosition( smin ); + targetRandomAccess.setPosition( smin ); + myNumInvalidPixels += processLine( sourceRandomAccess, targetRandomAccess, resolutionIndex, width, y ); + ++smin[ 1 ]; + } + + numInvalidPixels += myNumInvalidPixels; + } + + abstract int processLine(RandomAccess< A > sourceRandomAccess, RandomAccess< B > targetRandomAccess, byte resolutionIndex, int width, int y ); +} diff --git a/src/main/java/bdv/viewer/render/AccumulateProjectorARGB.java b/src/main/java/bdv/viewer/render/AccumulateProjectorARGB.java index 017fe17c4..2ba924a49 100644 --- a/src/main/java/bdv/viewer/render/AccumulateProjectorARGB.java +++ b/src/main/java/bdv/viewer/render/AccumulateProjectorARGB.java @@ -270,14 +270,10 @@ protected void accumulate( final Cursor< ? extends ARGBType >[] accesses, final gSum += g; bSum += b; } - if ( aSum > 255 ) - aSum = 255; - if ( rSum > 255 ) - rSum = 255; - if ( gSum > 255 ) - gSum = 255; - if ( bSum > 255 ) - bSum = 255; + aSum = Math.min( 255, aSum ); + rSum = Math.min( 255, rSum ); + gSum = Math.min( 255, gSum ); + bSum = Math.min( 255, bSum ); target.set( ARGBType.rgba( rSum, gSum, bSum, aSum ) ); } } diff --git a/src/main/java/bdv/viewer/render/AccumulateProjectorFactory.java b/src/main/java/bdv/viewer/render/AccumulateProjectorFactory.java index 3d6488e7b..6d32825f5 100644 --- a/src/main/java/bdv/viewer/render/AccumulateProjectorFactory.java +++ b/src/main/java/bdv/viewer/render/AccumulateProjectorFactory.java @@ -74,6 +74,20 @@ default VolatileProjector createProjector( return createAccumulateProjector( sp, spimSources, si, targetScreenImage, numThreads, executorService ); } + /** + * Returns {@code true} if projectors created by this factory make use of + * alpha masks. + *

+ * (In that case, individual sources should be rendered using + * {@link Source#getInterpolatedMaskedSource} instead of + * {@link Source#getInterpolatedSource}.) + * + * @return true, if projectors created by this factory make use of alpha masks + */ + default boolean requiresMaskedSources() { + return false; + } + /** * @deprecated Use {@link #createProjector(List, List, List, RandomAccessibleInterval, int, ExecutorService)} instead. * The new variant of the method is named "createProjector" instead of diff --git a/src/main/java/bdv/viewer/render/AlphaWeightedAccumulateProjectorARGB.java b/src/main/java/bdv/viewer/render/AlphaWeightedAccumulateProjectorARGB.java new file mode 100644 index 000000000..6ae10b139 --- /dev/null +++ b/src/main/java/bdv/viewer/render/AlphaWeightedAccumulateProjectorARGB.java @@ -0,0 +1,308 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2025 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer.render; + +import bdv.viewer.SourceAndConverter; +import bdv.viewer.render.ProjectorUtils.ArrayData; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ForkJoinTask; +import java.util.stream.Collectors; +import net.imglib2.Cursor; +import net.imglib2.Interval; +import net.imglib2.RandomAccessible; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.util.Intervals; +import net.imglib2.util.StopWatch; + +public class AlphaWeightedAccumulateProjectorARGB +{ + public static class Factory implements AccumulateProjectorFactory< ARGBType > + { + @Override + public VolatileProjector createProjector( + final List< VolatileProjector > sourceProjectors, + final List< SourceAndConverter< ? > > sources, + final List< ? extends RandomAccessible< ? extends ARGBType > > sourceScreenImages, + final RandomAccessibleInterval< ARGBType > targetScreenImage, + final int numThreads, + final ExecutorService executorService ) + { + final ProjectorData projectorData = getProjectorData( sourceScreenImages, targetScreenImage ); + if ( projectorData == null ) + return new AccumulateProjectorARGBGeneric( sourceProjectors, sourceScreenImages, targetScreenImage ); + else + return new AccumulateProjectorARGBArrayData( sourceProjectors, projectorData ); + } + + @Override + public boolean requiresMaskedSources() { + return true; + } + } + + public static AccumulateProjectorFactory< ARGBType > factory = new Factory(); + + private static ProjectorData getProjectorData( + final List< ? extends RandomAccessible< ? extends ARGBType > > sources, + final RandomAccessibleInterval< ARGBType > target ) + { + final ArrayData targetData = ProjectorUtils.getARGBArrayData( target ); + if ( targetData == null ) + return null; + + final int numSources = sources.size(); + final List< int[] > sourceData = new ArrayList<>( numSources ); + for ( int i = 0; i < numSources; ++i ) + { + final RandomAccessible< ? extends ARGBType > source = sources.get( i ); + if ( ! ( source instanceof RandomAccessibleInterval ) ) + return null; + if ( ! Intervals.equals( target, ( Interval ) source ) ) + return null; + final int[] data = ProjectorUtils.getARGBArrayImgData( source ); + if ( data == null ) + return null; + sourceData.add( data ); + } + + return new ProjectorData( targetData, sourceData ); + } + + private static class ProjectorData + { + private final ArrayData targetData; + private final List< int[] > sourceData; + + ProjectorData( final ArrayData targetData, final List< int[] > sourceData ) + { + this.targetData = targetData; + this.sourceData = sourceData; + } + + ArrayData targetData() + { + return targetData; + } + + List< int[] > sourceData() + { + return sourceData; + } + } + + private static class AccumulateProjectorARGBArrayData implements VolatileProjector + { + /** + * Projectors that render the source images to accumulate. + * For every rendering pass, ({@link VolatileProjector#map(boolean)}) is run on each source projector that is not yet {@link VolatileProjector#isValid() valid}. + */ + private List< VolatileProjector > sourceProjectors; + + /** + * The source images to accumulate + */ + private final List< int[] > sources; + + /** + * The target interval. + */ + private final ArrayData target; + + /** + * Time needed for rendering the last frame, in nano-seconds. + */ + private long lastFrameRenderNanoTime; + + private volatile boolean canceled = false; + + private volatile boolean valid = false; + + public AccumulateProjectorARGBArrayData( + final List< VolatileProjector > sourceProjectors, + final ProjectorData projectorData ) + { + this.sourceProjectors = sourceProjectors; + this.target = projectorData.targetData(); + this.sources = projectorData.sourceData(); + } + + @Override + public boolean map( final boolean clearUntouchedTargetPixels ) + { + if ( canceled ) + return false; + + if ( isValid() ) + return true; + + final StopWatch stopWatch = StopWatch.createAndStart(); + + if ( target.size() < Tiling.MIN_ACCUMULATE_FORK_SIZE ) + { + sourceProjectors.forEach( p -> p.map( clearUntouchedTargetPixels ) ); + } + else + { + ForkJoinTask.invokeAll( + sourceProjectors.stream() + .map( p -> ForkJoinTask.adapt( () -> p.map( clearUntouchedTargetPixels ) ) ) + .collect( Collectors.toList() ) ); + } + if ( canceled ) + return false; + mapAccumulate(); + sourceProjectors = sourceProjectors.stream() + .filter( p -> !p.isValid() ) + .collect( Collectors.toList() ); + lastFrameRenderNanoTime = stopWatch.nanoTime(); + valid = sourceProjectors.isEmpty(); + return !canceled; + } + + /** + * Accumulate pixels of all sources to target. Before starting, check + * whether rendering was {@link #cancel() canceled}. + */ + private void mapAccumulate() + { + if ( canceled ) + return; + + final int numSources = sources.size(); + final int[] acc = new int[ target.width() << 2 ]; + for ( int y = 0; y < target.height(); ++y ) + { + final int oTarget = ( y + target.oy() ) * target.stride() + target.ox(); + final int oSource = y * target.width(); + Arrays.fill( acc, 0 ); + for ( int s = 0; s < numSources; ++s ) + { + final int[] source = sources.get( s ); + for ( int x = 0; x < target.width(); ++x ) + { + final int value = source[ oSource + x ]; + final int alpha = ARGBType.alpha( value ); + final int red = ARGBType.red( value ); + final int green = ARGBType.green( value ); + final int blue = ARGBType.blue( value ); + acc[ ( x << 2 ) ] += alpha; + acc[ ( x << 2 ) + 1 ] += red * alpha; + acc[ ( x << 2 ) + 2 ] += green * alpha; + acc[ ( x << 2 ) + 3 ] += blue * alpha; + } + } + for ( int x = 0; x < target.width(); ++x ) + { + int aSum = acc[ ( x << 2 ) ]; + final int rSum; + final int gSum; + final int bSum; + if ( aSum == 0 ) { + rSum = 0; + gSum = 0; + bSum = 0; + } else { + rSum = Math.min( 255, acc[ ( x << 2 ) + 1 ] / aSum ); + gSum = Math.min( 255, acc[ ( x << 2 ) + 2 ] / aSum ); + bSum = Math.min( 255, acc[ ( x << 2 ) + 3 ] / aSum ); + aSum = Math.min( 255, aSum ); + } + target.data()[ oTarget + x ] = ARGBType.rgba( rSum, gSum, bSum, aSum ); + } + } + } + + @Override + public void cancel() + { + canceled = true; + for ( final VolatileProjector p : sourceProjectors ) + p.cancel(); + } + + @Override + public long getLastFrameRenderNanoTime() + { + return lastFrameRenderNanoTime; + } + + @Override + public boolean isValid() + { + return valid; + } + } + + private static class AccumulateProjectorARGBGeneric extends AccumulateProjector< ARGBType, ARGBType > + { + public AccumulateProjectorARGBGeneric( + final List< VolatileProjector > sourceProjectors, + final List< ? extends RandomAccessible< ? extends ARGBType > > sources, + final RandomAccessibleInterval< ARGBType > target ) + { + super( sourceProjectors, sources, target ); + } + + @Override + protected void accumulate( final Cursor< ? extends ARGBType >[] accesses, final ARGBType target ) + { + int aSum = 0, rSum = 0, gSum = 0, bSum = 0; + for ( final Cursor< ? extends ARGBType > access : accesses ) + { + final int value = access.get().get(); + final int alpha = ARGBType.alpha( value ); + final int red = ARGBType.red( value ); + final int green = ARGBType.green( value ); + final int blue = ARGBType.blue( value ); + aSum += alpha; + rSum += red * alpha; + gSum += green * alpha; + bSum += blue * alpha; + } + if ( aSum == 0 ) + { + rSum = 0; + gSum = 0; + bSum = 0; + } + else + { + aSum = Math.min( 255, aSum ); + rSum = Math.min( 255, rSum / aSum ); + gSum = Math.min( 255, gSum / aSum ); + bSum = Math.min( 255, bSum / aSum ); + } + target.set( ARGBType.rgba( rSum, gSum, bSum, aSum ) ); + } + } +} diff --git a/src/main/java/bdv/viewer/render/MaskedVolatileHierarchyProjector.java b/src/main/java/bdv/viewer/render/MaskedVolatileHierarchyProjector.java new file mode 100644 index 000000000..a71488af7 --- /dev/null +++ b/src/main/java/bdv/viewer/render/MaskedVolatileHierarchyProjector.java @@ -0,0 +1,92 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2025 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer.render; + +import java.util.List; + +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessible; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.Volatile; +import net.imglib2.converter.Converter; +import net.imglib2.type.mask.Masked; +import net.imglib2.type.operators.SetZero; + +/** + * {@link VolatileProjector} for a hierarchy of {@link Masked} {@link Volatile} inputs. + * After each {@link #map()} call, the projector has a {@link #isValid() state} + * that signalizes whether all projected pixels were perfect. + * + * @author Stephan Saalfeld + * @author Tobias Pietzsch + */ +class MaskedVolatileHierarchyProjector< A extends Masked< ? extends Volatile< ? > >, B extends SetZero > extends AbstractVolatileHierarchyProjector< A, B > +{ + public MaskedVolatileHierarchyProjector( + final List< ? extends RandomAccessible< A > > sources, + final Converter< ? super A, B > converter, + final RandomAccessibleInterval< B > target ) + { + this( sources, converter, target, new byte[ ( int ) ( target.dimension( 0 ) * target.dimension( 1 ) ) ] ); + } + + public MaskedVolatileHierarchyProjector( + final List< ? extends RandomAccessible< A > > sources, + final Converter< ? super A, B > converter, + final RandomAccessibleInterval< B > target, + final byte[] maskArray ) + { + super( sources, converter, target, maskArray ); + } + + @Override + int processLine(final RandomAccess< A > sourceRandomAccess, final RandomAccess< B > targetRandomAccess, final byte resolutionIndex, final int width, final int y ) + { + int numInvalidPixels = 0; + final int mi = y * width; + for ( int x = 0; x < width; ++x ) + { + if ( mask[ mi + x ] > resolutionIndex ) + { + final A a = sourceRandomAccess.get(); + final boolean v = a.value().isValid(); + if ( v ) + { + converter.convert( a, targetRandomAccess.get() ); + mask[ mi + x ] = resolutionIndex; + } + else + ++numInvalidPixels; + } + sourceRandomAccess.fwd( 0 ); + targetRandomAccess.fwd( 0 ); + } + return numInvalidPixels; + } +} diff --git a/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java b/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java index e3b8cc745..472737885 100644 --- a/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java +++ b/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java @@ -279,8 +279,6 @@ public class MultiResolutionRenderer * @param useVolatileIfAvailable * whether volatile versions of sources should be used if * available. - * @param accumulateProjectorFactory - * can be used to customize how sources are combined. * @param cacheControl * the cache controls IO budgeting and fetcher queue. */ @@ -292,7 +290,6 @@ public MultiResolutionRenderer( final int numRenderingThreads, final ExecutorService renderingExecutorService, final boolean useVolatileIfAvailable, - final AccumulateProjectorFactory< ARGBType > accumulateProjectorFactory, final CacheControl cacheControl ) { this.display = display; @@ -324,8 +321,7 @@ public MultiResolutionRenderer( projectorFactory = new ProjectorFactory( numRenderingThreads, renderingForkJoinPool, - useVolatileIfAvailable, - accumulateProjectorFactory ); + useVolatileIfAvailable ); } /** diff --git a/src/main/java/bdv/viewer/render/ProjectorFactory.java b/src/main/java/bdv/viewer/render/ProjectorFactory.java index 06cf75b01..320402d05 100644 --- a/src/main/java/bdv/viewer/render/ProjectorFactory.java +++ b/src/main/java/bdv/viewer/render/ProjectorFactory.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -40,8 +40,10 @@ import net.imglib2.Volatile; import net.imglib2.cache.volatiles.CacheHints; import net.imglib2.cache.volatiles.LoadingStrategy; +import net.imglib2.display.MaskedToARGBConverter; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.realtransform.RealViews; +import net.imglib2.type.mask.Masked; import net.imglib2.type.numeric.ARGBType; import bdv.img.cache.VolatileCachedCellImg; @@ -50,6 +52,7 @@ import bdv.viewer.Source; import bdv.viewer.SourceAndConverter; import bdv.viewer.ViewerState; +import net.imglib2.util.Cast; import net.imglib2.view.Views; /** @@ -75,13 +78,6 @@ class ProjectorFactory */ private final boolean useVolatileIfAvailable; - /** - * Constructs projector that combines rendere ARGB images for individual - * sources to the final screen image. This can be used to customize how - * sources are combined. - */ - private final AccumulateProjectorFactory< ARGBType > accumulateProjectorFactory; - /** * Whether repainting should be triggered after the previously * {@link #createProjector constructed} projector returns an incomplete @@ -115,26 +111,22 @@ class ProjectorFactory /** * @param numRenderingThreads - * How many threads to use for rendering. + * How many threads to use for rendering. * @param renderingExecutorService - * if non-null, this is used for rendering. Note, that it is still - * important to supply the numRenderingThreads parameter, because that - * is used to determine into how many sub-tasks rendering is split. + * if non-null, this is used for rendering. Note, that it is still + * important to supply the numRenderingThreads parameter, because that + * is used to determine into how many sub-tasks rendering is split. * @param useVolatileIfAvailable - * whether volatile versions of sources should be used if available. - * @param accumulateProjectorFactory - * can be used to customize how sources are combined. + * whether volatile versions of sources should be used if available. */ public ProjectorFactory( final int numRenderingThreads, final ExecutorService renderingExecutorService, - final boolean useVolatileIfAvailable, - final AccumulateProjectorFactory< ARGBType > accumulateProjectorFactory ) + final boolean useVolatileIfAvailable ) { this.numRenderingThreads = numRenderingThreads; this.renderingExecutorService = renderingExecutorService; this.useVolatileIfAvailable = useVolatileIfAvailable; - this.accumulateProjectorFactory = accumulateProjectorFactory; } /** @@ -144,6 +136,20 @@ public ProjectorFactory( * timepoint of the {@code ViewerState}, and the specified * {@code screenTransform} from global coordinates to coordinates in the * {@code screenImage}. + * + * @param viewerState + * the ViewerState to render + * @param visibleSourcesOnScreen + * @param accumulateProjectorFactory + * constructs a projector that combines the rendered ARGB images for individual sources into the final screen image. + * @param screenImage + * the ARGB screen image to render to + * @param screenTransform + * transforms global to screenImage coordinates + * @param renderStorage + * temporary storage + * + * @return a new projector */ public VolatileProjector createProjector( final ViewerState viewerState, @@ -161,13 +167,16 @@ public VolatileProjector createProjector( final int width = ( int ) screenImage.dimension( 0 ); final int height = ( int ) screenImage.dimension( 1 ); + final AccumulateProjectorFactory< ARGBType > accumulateProjectorFactory = viewerState.getAccumulateProjectorFactory(); + final boolean useAlphaMaskedSources = accumulateProjectorFactory.requiresMaskedSources(); + VolatileProjector projector; if ( visibleSourcesOnScreen.isEmpty() ) projector = new EmptyProjector<>( screenImage ); else if ( visibleSourcesOnScreen.size() == 1 ) { final byte[] maskArray = renderStorage.getMaskArray( 0 ); - projector = createSingleSourceProjector( viewerState, visibleSourcesOnScreen.get( 0 ), screenImage, screenTransform, maskArray ); + projector = createSingleSourceProjector( viewerState, visibleSourcesOnScreen.get( 0 ), useAlphaMaskedSources, screenImage, screenTransform, maskArray ); } else { @@ -183,7 +192,7 @@ else if ( visibleSourcesOnScreen.size() == 1 ) ++j; final AffineTransform3D renderTransform = screenTransform.copy(); renderTransform.translate( -offsetX, -offsetY, 0 ); - final VolatileProjector p = createSingleSourceProjector( viewerState, source, renderImage, renderTransform, maskArray ); + final VolatileProjector p = createSingleSourceProjector( viewerState, source, useAlphaMaskedSources,renderImage, renderTransform, maskArray ); sourceProjectors.add( p ); sourceImages.add( renderImage ); } @@ -192,9 +201,25 @@ else if ( visibleSourcesOnScreen.size() == 1 ) return projector; } + /** + * NB. Unfortunately, "mask" refers both to the target mask and the source + * alpha mask. + *

+ * The target mask records, for every target pixel, the best resolution + * level (so far) that has provided a valid value. + *

+ * The {@code useAlphaMaskedSources} determines whether {@link + * Source#getMaskedSource masked} versions of sources should be used for + * rendering (to provide accurate alpha values for the final ARGBType + * accumulation). + * + * @param useAlphaMaskedSources whether to use the {@link Source#getMaskedSource masked} version of the source for rendering + * @param maskArray the target mask + */ private < T > VolatileProjector createSingleSourceProjector( final ViewerState viewerState, final SourceAndConverter< T > source, + final boolean useAlphaMaskedSources, final RandomAccessibleInterval< ARGBType > screenImage, final AffineTransform3D screenTransform, final byte[] maskArray ) @@ -202,29 +227,39 @@ private < T > VolatileProjector createSingleSourceProjector( if ( useVolatileIfAvailable ) { if ( source.asVolatile() != null ) - return createSingleSourceVolatileProjector( viewerState, source.asVolatile(), screenImage, screenTransform, maskArray ); + return createSingleSourceVolatileProjector( viewerState, source.asVolatile(), useAlphaMaskedSources, screenImage, screenTransform, maskArray ); else if ( source.getSpimSource().getType() instanceof Volatile ) { @SuppressWarnings( "unchecked" ) final SourceAndConverter< ? extends Volatile< ? > > vsource = ( SourceAndConverter< ? extends Volatile< ? > > ) source; - return createSingleSourceVolatileProjector( viewerState, vsource, screenImage, screenTransform, maskArray ); + return createSingleSourceVolatileProjector( viewerState, vsource, useAlphaMaskedSources, screenImage, screenTransform, maskArray ); } } final int bestLevel = getBestMipMapLevel( viewerState, source, screenTransform ); - return new SimpleVolatileProjector<>( - getTransformedSource( viewerState, source.getSpimSource(), screenTransform, bestLevel, null ), - source.getConverter(), screenImage ); + + if ( useAlphaMaskedSources ) + { + return new SimpleVolatileProjector<>( + getTransformedMaskedSource( viewerState, source.getSpimSource(), screenTransform, bestLevel, null ), + MaskedToARGBConverter.wrap( source.getConverter() ), screenImage ); + } + else + { + return new SimpleVolatileProjector<>( + getTransformedSource( viewerState, source.getSpimSource(), screenTransform, bestLevel, null ), + source.getConverter(), screenImage ); + } } - private < T extends Volatile< ? > > VolatileProjector createSingleSourceVolatileProjector( + private < T extends Volatile< ? >, M extends Masked< T > > VolatileProjector createSingleSourceVolatileProjector( final ViewerState viewerState, final SourceAndConverter< T > source, + final boolean useAlphaMaskedSources, final RandomAccessibleInterval< ARGBType > screenImage, final AffineTransform3D screenTransform, final byte[] maskArray ) { - final ArrayList< RandomAccessible< T > > renderList = new ArrayList<>(); final Source< T > spimSource = source.getSpimSource(); final int t = viewerState.getCurrentTimepoint(); @@ -244,15 +279,25 @@ else if ( source.getSpimSource().getType() instanceof Volatile ) prefetch( viewerState, spimSource, screenTransform, l.getMipmapLevel(), cacheHints, screenImage ); } } - levels.sort( MipmapOrdering.renderOrderComparator ); - for ( final MipmapOrdering.Level l : levels ) - renderList.add( getTransformedSource( viewerState, spimSource, screenTransform, l.getMipmapLevel(), l.getRenderCacheHints() ) ); if ( hints.renewHintsAfterPaintingOnce() ) newFrameRequest = true; - return new VolatileHierarchyProjector<>( renderList, source.getConverter(), screenImage, maskArray ); + if ( useAlphaMaskedSources ) + { + final ArrayList< RandomAccessible< M > > renderList = new ArrayList<>(); + for ( final MipmapOrdering.Level l : levels ) + renderList.add( Cast.unchecked(getTransformedMaskedSource( viewerState, spimSource, screenTransform, l.getMipmapLevel(), l.getRenderCacheHints() ) ) ); + return new MaskedVolatileHierarchyProjector<>( renderList, MaskedToARGBConverter.wrap( source.getConverter() ), screenImage, maskArray ); + } + else + { + final ArrayList< RandomAccessible< T > > renderList = new ArrayList<>(); + for ( final MipmapOrdering.Level l : levels ) + renderList.add( getTransformedSource( viewerState, spimSource, screenTransform, l.getMipmapLevel(), l.getRenderCacheHints() ) ); + return new VolatileHierarchyProjector<>( renderList, source.getConverter(), screenImage, maskArray ); + } } /** @@ -280,21 +325,48 @@ private static < T > RandomAccessible< T > getTransformedSource( final CacheHints cacheHints ) { final int timepoint = viewerState.getCurrentTimepoint(); + applyCacheHints( source, timepoint, mipmapIndex, cacheHints ); + final RealRandomAccessible< T > ipimg = source.getInterpolatedSource( timepoint, mipmapIndex, viewerState.getInterpolation() ); + return transformToScreen( source, screenTransform, timepoint, mipmapIndex, ipimg ); + } - final RandomAccessibleInterval< T > img = source.getSource( timepoint, mipmapIndex ); - if ( img instanceof VolatileCachedCellImg ) - ( ( VolatileCachedCellImg< ?, ? > ) img ).setCacheHints( cacheHints ); - - final Interpolation interpolation = viewerState.getInterpolation(); - final RealRandomAccessible< T > ipimg = source.getInterpolatedSource( timepoint, mipmapIndex, interpolation ); + private static < T > RandomAccessible< ? extends Masked< T > > getTransformedMaskedSource( + final ViewerState viewerState, + final Source< T > source, + final AffineTransform3D screenTransform, + final int mipmapIndex, + final CacheHints cacheHints ) + { + final int timepoint = viewerState.getCurrentTimepoint(); + applyCacheHints( source, timepoint, mipmapIndex, cacheHints ); + final RealRandomAccessible< ? extends Masked< T > > ipimg = source.getInterpolatedMaskedSource( timepoint, mipmapIndex, viewerState.getInterpolation() ); + return transformToScreen( source, screenTransform, timepoint, mipmapIndex, ipimg ); + } + private static < T > RandomAccessible< T > transformToScreen( + final Source< ? > source, + final AffineTransform3D screenTransform, + final int timepoint, + final int mipmapIndex, + final RealRandomAccessible< T > ipimg ) + { final AffineTransform3D sourceToScreen = new AffineTransform3D(); source.getSourceTransform( timepoint, mipmapIndex, sourceToScreen ); sourceToScreen.preConcatenate( screenTransform ); - return RealViews.affine( ipimg, sourceToScreen ); } + private static void applyCacheHints( + final Source< ? > source, + final int timepoint, + final int mipmapIndex, + final CacheHints cacheHints ) + { + final RandomAccessibleInterval< ? > img = source.getSource( timepoint, mipmapIndex ); + if ( img instanceof VolatileCachedCellImg ) + ( ( VolatileCachedCellImg< ?, ? > ) img ).setCacheHints( cacheHints ); + } + private static < T > void prefetch( final ViewerState viewerState, final Source< T > source, diff --git a/src/main/java/bdv/viewer/render/TiledProjector.java b/src/main/java/bdv/viewer/render/TiledProjector.java index 652026c36..edbe1cb33 100644 --- a/src/main/java/bdv/viewer/render/TiledProjector.java +++ b/src/main/java/bdv/viewer/render/TiledProjector.java @@ -28,14 +28,10 @@ */ package bdv.viewer.render; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.Callable; import java.util.concurrent.ForkJoinTask; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Predicate; import java.util.stream.Collectors; -import net.imglib2.parallel.Parallelization; + import net.imglib2.util.StopWatch; /** diff --git a/src/main/java/bdv/viewer/render/VolatileHierarchyProjector.java b/src/main/java/bdv/viewer/render/VolatileHierarchyProjector.java index 5e939efca..8b0106160 100644 --- a/src/main/java/bdv/viewer/render/VolatileHierarchyProjector.java +++ b/src/main/java/bdv/viewer/render/VolatileHierarchyProjector.java @@ -28,105 +28,29 @@ */ package bdv.viewer.render; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import net.imglib2.FinalInterval; + import net.imglib2.RandomAccess; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; import net.imglib2.Volatile; -import net.imglib2.cache.iotiming.CacheIoTiming; -import net.imglib2.cache.iotiming.IoStatistics; import net.imglib2.converter.Converter; import net.imglib2.type.operators.SetZero; -import net.imglib2.util.Intervals; -import net.imglib2.util.StopWatch; -import net.imglib2.view.Views; /** - * {@link VolatileProjector} for a hierarchy of {@link Volatile} inputs. After each - * {@link #map()} call, the projector has a {@link #isValid() state} that - * signalizes whether all projected pixels were perfect. + * {@link VolatileProjector} for a hierarchy of {@link Volatile} inputs. + * After each {@link #map()} call, the projector has a {@link #isValid() state} + * that signalizes whether all projected pixels were perfect. * * @author Stephan Saalfeld * @author Tobias Pietzsch */ -public class VolatileHierarchyProjector< A extends Volatile< ? >, B extends SetZero > implements VolatileProjector +public class VolatileHierarchyProjector< A extends Volatile< ? >, B extends SetZero > extends AbstractVolatileHierarchyProjector< A, B > { - /** - * A converter from the source pixel type to the target pixel type. - */ - private final Converter< ? super A, B > converter; - - /** - * The target interval. Pixels of the target interval should be set by - * {@link #map} - */ - private final RandomAccessibleInterval< B > target; - - /** - * List of source resolutions starting with the optimal resolution at index - * 0. During each {@link #map(boolean)}, for every pixel, resolution levels - * are successively queried until a valid pixel is found. - */ - private final List< RandomAccessible< A > > sources; - - /** - * Records, for every target pixel, the best (smallest index) source - * resolution level that has provided a valid value. Only better (lower - * index) resolutions are re-tried in successive {@link #map(boolean)} - * calls. - */ - private final byte[] mask; - - /** - * {@code true} iff all target pixels were rendered with valid data from the - * optimal resolution level (level {@code 0}). - */ - private volatile boolean valid = false; - - /** - * How many levels (starting from level {@code 0}) have to be re-rendered in - * the next rendering pass, i.e., {@code map()} call. - */ - private int numInvalidLevels; - - /** - * Source interval which will be used for rendering. This is the 2D target - * interval expanded to source dimensionality (usually 3D) with - * {@code min=max=0} in the additional dimensions. - */ - private final FinalInterval sourceInterval; - - /** - * Time needed for rendering the last frame, in nano-seconds. - * This does not include time spent in blocking IO. - */ - private long lastFrameRenderNanoTime; - - /** - * Time spent in blocking IO rendering the last frame, in nano-seconds. - */ - private long lastFrameIoNanoTime; // TODO move to derived implementation for local sources only - - /** - * temporary variable to store the number of invalid pixels in the current - * rendering pass. - */ - private int numInvalidPixels; - - /** - * Flag to indicate that someone is trying to {@link #cancel()} rendering. - */ - private volatile boolean canceled = false; - public VolatileHierarchyProjector( final List< ? extends RandomAccessible< A > > sources, final Converter< ? super A, B > converter, - final RandomAccessibleInterval< B > target) + final RandomAccessibleInterval< B > target ) { this( sources, converter, target, new byte[ ( int ) ( target.dimension( 0 ) * target.dimension( 1 ) ) ] ); } @@ -135,175 +59,33 @@ public VolatileHierarchyProjector( final List< ? extends RandomAccessible< A > > sources, final Converter< ? super A, B > converter, final RandomAccessibleInterval< B > target, - final byte[] maskArray) - { - this.converter = converter; - this.target = target; - this.sources = new ArrayList<>( sources ); - numInvalidLevels = sources.size(); - mask = maskArray; - - final int n = Math.max( 2, sources.get( 0 ).numDimensions() ); - final long[] min = new long[ n ]; - final long[] max = new long[ n ]; - min[ 0 ] = target.min( 0 ); - max[ 0 ] = target.max( 0 ); - min[ 1 ] = target.min( 1 ); - max[ 1 ] = target.max( 1 ); - sourceInterval = new FinalInterval( min, max ); - - lastFrameRenderNanoTime = -1; - clearMask(); - } - - @Override - public void cancel() - { - canceled = true; - } - - @Override - public long getLastFrameRenderNanoTime() - { - return lastFrameRenderNanoTime; - } - - public long getLastFrameIoNanoTime() - { - return lastFrameIoNanoTime; - } - - @Override - public boolean isValid() + final byte[] maskArray ) { - return valid; - } - - /** - * Set all pixels in target to 100% transparent zero, and mask to all - * Integer.MAX_VALUE. - */ - public void clearMask() - { - final int size = ( int ) Intervals.numElements( target ); - Arrays.fill( mask, 0, size, Byte.MAX_VALUE ); - numInvalidLevels = sources.size(); - } - - /** - * Clear target pixels that were never written. - */ - private void clearUntouchedTargetPixels() - { - int i = 0; - for ( final B t : Views.flatIterable( target ) ) - if ( mask[ i++ ] == Byte.MAX_VALUE ) - t.setZero(); + super( sources, converter, target, maskArray ); } @Override - public boolean map( final boolean clearUntouchedTargetPixels ) - { - if ( canceled ) - return false; - - valid = false; - - final StopWatch stopWatch = StopWatch.createAndStart(); - final IoStatistics iostat = CacheIoTiming.getIoStatistics(); - final long startTimeIo = iostat.getIoNanoTime(); - - /* - * After the for loop, resolutionLevel is the highest (coarsest) - * resolution for which all pixels could be filled from valid data. This - * means that in the next pass, i.e., map() call, levels up to - * resolutionLevel have to be re-rendered. - */ - int resolutionLevel; - for ( resolutionLevel = 0; resolutionLevel < numInvalidLevels; ++resolutionLevel ) - { - numInvalidPixels = 0; - map( ( byte ) resolutionLevel); - if ( canceled ) - return false; - if ( numInvalidPixels == 0 ) - // if this pass was all valid - numInvalidLevels = resolutionLevel; - } - - if ( clearUntouchedTargetPixels && numInvalidPixels != 0 && !canceled ) - clearUntouchedTargetPixels(); - - lastFrameIoNanoTime = iostat.getIoNanoTime() - startTimeIo; - final long lastFrameTime = stopWatch.nanoTime(); - - // TODO (FORKJOIN): This is inaccurate now, should only use the io time used by current thread - // --> requires additional API in IoStatistics (imglib2-cache) - lastFrameRenderNanoTime = lastFrameTime - lastFrameIoNanoTime; - - valid = numInvalidLevels == 0; - - return !canceled; - } - - /** - * Copy lines from {@code y = startHeight} up to {@code endHeight} - * (exclusive) from source {@code resolutionIndex} to target. Check after - * each line whether rendering was {@link #cancel() canceled}. - *

- * Only valid source pixels with a current mask value - * {@code mask>resolutionIndex} are copied to target, and their mask value - * is set to {@code mask=resolutionIndex}. Invalid source pixels are - * ignored. Pixels with {@code mask<=resolutionIndex} are ignored, because - * they have already been written to target during a previous pass. - *

- * - * @param resolutionIndex - * index of source resolution level - */ - private void map( final byte resolutionIndex ) + int processLine(final RandomAccess< A > sourceRandomAccess, final RandomAccess< B > targetRandomAccess, final byte resolutionIndex, final int width, final int y ) { - if ( canceled ) - return; - - final RandomAccess< B > targetRandomAccess = target.randomAccess( target ); - final RandomAccess< A > sourceRandomAccess = sources.get( resolutionIndex ).randomAccess( sourceInterval ); - final int width = ( int ) target.dimension( 0 ); - final int height = ( int ) target.dimension( 1 ); - final long[] smin = Intervals.minAsLongArray( sourceInterval ); - int myNumInvalidPixels = 0; - - for ( int y = 0; y < height; ++y ) + int numInvalidPixels = 0; + final int mi = y * width; + for ( int x = 0; x < width; ++x ) { - // TODO (FORKJOIN) With tiles being granular enough, probably - // projectors shouldn't check after each line for cancellation - // (maybe not at all). - if ( canceled ) - return; - - sourceRandomAccess.setPosition( smin ); - targetRandomAccess.setPosition( smin ); - final int mi = y * width; - for ( int x = 0; x < width; ++x ) + if ( mask[ mi + x ] > resolutionIndex ) { - if ( mask[ mi + x ] > resolutionIndex ) + final A a = sourceRandomAccess.get(); + final boolean v = a.isValid(); + if ( v ) { - final A a = sourceRandomAccess.get(); - final boolean v = a.isValid(); - if ( v ) - { - converter.convert( a, targetRandomAccess.get() ); - mask[ mi + x ] = resolutionIndex; - } - else - ++myNumInvalidPixels; + converter.convert( a, targetRandomAccess.get() ); + mask[ mi + x ] = resolutionIndex; } - sourceRandomAccess.fwd( 0 ); - targetRandomAccess.fwd( 0 ); + else + ++numInvalidPixels; } - ++smin[ 1 ]; + sourceRandomAccess.fwd( 0 ); + targetRandomAccess.fwd( 0 ); } - - numInvalidPixels += myNumInvalidPixels; + return numInvalidPixels; } } diff --git a/src/main/java/bdv/viewer/render/awt/BufferedImageRenderResult.java b/src/main/java/bdv/viewer/render/awt/BufferedImageRenderResult.java index 9be5962d8..1a685a735 100644 --- a/src/main/java/bdv/viewer/render/awt/BufferedImageRenderResult.java +++ b/src/main/java/bdv/viewer/render/awt/BufferedImageRenderResult.java @@ -67,7 +67,7 @@ public void init( final int width, final int height ) data = new int[ width * height ]; screenImage = new ARGBScreenImage( width, height, data ); - bufferedImage = AWTUtils.getBufferedImage( screenImage );; + bufferedImage = AWTUtils.getBufferedImage( screenImage, false );; } @Override diff --git a/src/main/resources/bdv/ui/keymap/default.yaml b/src/main/resources/bdv/ui/keymap/default.yaml index 475ddedba..54e409876 100644 --- a/src/main/resources/bdv/ui/keymap/default.yaml +++ b/src/main/resources/bdv/ui/keymap/default.yaml @@ -71,6 +71,10 @@ action: toggle grouping contexts: [bdv] triggers: [G] +- !mapping + action: toggle blending mode + contexts: [bdv] + triggers: [U] - !mapping action: set current source 0 contexts: [bdv] diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_avg.png b/src/main/resources/bdv/ui/viewermodepanel/blend_avg.png new file mode 100644 index 000000000..759f03f26 Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/blend_avg.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_avg.svg b/src/main/resources/bdv/ui/viewermodepanel/blend_avg.svg new file mode 100644 index 000000000..a9501a82e --- /dev/null +++ b/src/main/resources/bdv/ui/viewermodepanel/blend_avg.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_avg_200.png b/src/main/resources/bdv/ui/viewermodepanel/blend_avg_200.png new file mode 100644 index 000000000..96d58a9e9 Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/blend_avg_200.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_avg_dark.png b/src/main/resources/bdv/ui/viewermodepanel/blend_avg_dark.png new file mode 100644 index 000000000..a1efe60b8 Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/blend_avg_dark.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_avg_dark.svg b/src/main/resources/bdv/ui/viewermodepanel/blend_avg_dark.svg new file mode 100644 index 000000000..ba4f8cbdc --- /dev/null +++ b/src/main/resources/bdv/ui/viewermodepanel/blend_avg_dark.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_avg_dark_200.png b/src/main/resources/bdv/ui/viewermodepanel/blend_avg_dark_200.png new file mode 100644 index 000000000..f276257e5 Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/blend_avg_dark_200.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_custom.png b/src/main/resources/bdv/ui/viewermodepanel/blend_custom.png new file mode 100644 index 000000000..439006bfa Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/blend_custom.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_custom.svg b/src/main/resources/bdv/ui/viewermodepanel/blend_custom.svg new file mode 100644 index 000000000..82e8b1568 --- /dev/null +++ b/src/main/resources/bdv/ui/viewermodepanel/blend_custom.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + ? + + + diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_custom_200.png b/src/main/resources/bdv/ui/viewermodepanel/blend_custom_200.png new file mode 100644 index 000000000..3e6b0c643 Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/blend_custom_200.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_custom_dark.png b/src/main/resources/bdv/ui/viewermodepanel/blend_custom_dark.png new file mode 100644 index 000000000..65cd2ebde Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/blend_custom_dark.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_custom_dark.svg b/src/main/resources/bdv/ui/viewermodepanel/blend_custom_dark.svg new file mode 100644 index 000000000..cfa6e53fe --- /dev/null +++ b/src/main/resources/bdv/ui/viewermodepanel/blend_custom_dark.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + ? + + + diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_custom_dark_200.png b/src/main/resources/bdv/ui/viewermodepanel/blend_custom_dark_200.png new file mode 100644 index 000000000..2af7745e7 Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/blend_custom_dark_200.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_sum.png b/src/main/resources/bdv/ui/viewermodepanel/blend_sum.png new file mode 100644 index 000000000..7a3d21ccf Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/blend_sum.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_sum.svg b/src/main/resources/bdv/ui/viewermodepanel/blend_sum.svg new file mode 100644 index 000000000..2e0dd97c8 --- /dev/null +++ b/src/main/resources/bdv/ui/viewermodepanel/blend_sum.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_sum_200.png b/src/main/resources/bdv/ui/viewermodepanel/blend_sum_200.png new file mode 100644 index 000000000..fa6446648 Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/blend_sum_200.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_sum_dark.png b/src/main/resources/bdv/ui/viewermodepanel/blend_sum_dark.png new file mode 100644 index 000000000..d5b7ede06 Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/blend_sum_dark.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_sum_dark.svg b/src/main/resources/bdv/ui/viewermodepanel/blend_sum_dark.svg new file mode 100644 index 000000000..b86938703 --- /dev/null +++ b/src/main/resources/bdv/ui/viewermodepanel/blend_sum_dark.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/bdv/ui/viewermodepanel/blend_sum_dark_200.png b/src/main/resources/bdv/ui/viewermodepanel/blend_sum_dark_200.png new file mode 100644 index 000000000..575eb06ce Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/blend_sum_dark_200.png differ diff --git a/src/test/java/bdv/ui/CreateViewerState.java b/src/test/java/bdv/ui/CreateViewerState.java index bac63ea00..4ea54fde7 100644 --- a/src/test/java/bdv/ui/CreateViewerState.java +++ b/src/test/java/bdv/ui/CreateViewerState.java @@ -39,6 +39,7 @@ import net.imglib2.RandomAccessibleInterval; import net.imglib2.RealRandomAccessible; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.mask.Masked; import net.imglib2.type.numeric.ARGBType; import net.imglib2.type.numeric.integer.UnsignedShortType; @@ -136,6 +137,18 @@ public RealRandomAccessible< UnsignedShortType > getInterpolatedSource( final in return null; } + @Override + public RandomAccessibleInterval< ? extends Masked< UnsignedShortType > > getMaskedSource( final int t, final int level ) + { + return null; + } + + @Override + public RealRandomAccessible< ? extends Masked< UnsignedShortType > > getInterpolatedMaskedSource( final int t, final int level, final Interpolation method ) + { + return null; + } + @Override public void getSourceTransform( final int t, final int level, final AffineTransform3D transform ) { diff --git a/src/test/java/bdv/util/benchmark/RenderingSetup.java b/src/test/java/bdv/util/benchmark/RenderingSetup.java index 01028df2a..02f624ee9 100644 --- a/src/test/java/bdv/util/benchmark/RenderingSetup.java +++ b/src/test/java/bdv/util/benchmark/RenderingSetup.java @@ -149,11 +149,10 @@ public static class Renderer public Renderer(final int[] targetSize, final int numRenderingThreads) { target = new BenchmarkRenderTarget( targetSize[ 0 ], targetSize[ 1 ] ); - final AccumulateProjectorFactory< ARGBType > accumulateProjectorFactory = AccumulateProjectorARGB.factory; renderer = new MultiResolutionRenderer( target, () -> {}, new double[] { 1 }, 0, numRenderingThreads, null, false, - accumulateProjectorFactory, new CacheControl.Dummy() ); + new CacheControl.Dummy() ); } public void render( final ViewerState state ) diff --git a/src/test/java/bdv/viewer/BasicViewerStateTest.java b/src/test/java/bdv/viewer/BasicViewerStateTest.java index df1c6a0f2..b23c6f8f0 100644 --- a/src/test/java/bdv/viewer/BasicViewerStateTest.java +++ b/src/test/java/bdv/viewer/BasicViewerStateTest.java @@ -40,6 +40,8 @@ import net.imglib2.RandomAccessibleInterval; import net.imglib2.RealRandomAccessible; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.mask.Masked; + import org.junit.Assert; import org.junit.Test; @@ -397,6 +399,18 @@ public RealRandomAccessible< Void > getInterpolatedSource( final int t, final in return null; } + @Override + public RandomAccessibleInterval< ? extends Masked< Void > > getMaskedSource( final int t, final int level ) + { + return null; + } + + @Override + public RealRandomAccessible< ? extends Masked< Void > > getInterpolatedMaskedSource( final int t, final int level, final Interpolation method ) + { + return null; + } + @Override public void getSourceTransform( final int t, final int level, final AffineTransform3D transform ) {