diff --git a/src/main/java/bvv/core/blocks/ByteUtils.java b/src/main/java/bvv/core/blocks/ByteUtils.java index 52b5829..123c159 100644 --- a/src/main/java/bvv/core/blocks/ByteUtils.java +++ b/src/main/java/bvv/core/blocks/ByteUtils.java @@ -94,7 +94,11 @@ public static void setBytes( final byte src, final long dst, final long csx ) UNSAFE.setMemory( dst, csx, src ); } - public interface Address + public static void copyBytes( final byte[] src, final long dst, final long sox, final long csx) { + UNSAFE.copyMemory( src, BYTE_ARRAY_OFFSET + sox, null, dst, csx ); + } + + public interface Address { long getAddress(); } diff --git a/src/main/java/bvv/core/blocks/CopySubArrayImp.java b/src/main/java/bvv/core/blocks/CopySubArrayImp.java index 0951481..00ab5b1 100644 --- a/src/main/java/bvv/core/blocks/CopySubArrayImp.java +++ b/src/main/java/bvv/core/blocks/CopySubArrayImp.java @@ -50,6 +50,23 @@ public void copysubarray3d( final short[] src, final int sox, final int soy, fin } } + public static class ByteToAddress implements CopySubArray< byte[], ByteUtils.Address > + { + @Override + public void clearsubarray3d( final ByteUtils.Address dst, final int dox, final int doy, final int doz, final int dsx, final int dsy, final int csx, final int csy, final int csz ) + { + final ArrayFill fill = ( o, l ) -> ByteUtils.setBytes( ( byte ) 0, dst.getAddress() + o, l ); + fillsubarray3dn( fill, dox, doy, doz, dsx, dsy, csx, csy, csz ); + } + + @Override + public void copysubarray3d( final byte[] src, final int sox, final int soy, final int soz, final int ssx, final int ssy, final ByteUtils.Address dst, final int dox, final int doy, final int doz, final int dsx, final int dsy, final int csx, final int csy, final int csz ) + { + final ArrayCopy copy = ( so, o, l ) -> ByteUtils.copyBytes( src, dst.getAddress() + o, so, l ); + copysubarray3dn( copy, sox, soy, soz, ssx, ssy, dox, doy, doz, dsx, dsy, csx, csy, csz ); + } + } + static void copysubarray3dn( ArrayCopy copysubarray1dn, final int sox, diff --git a/src/main/java/bvv/core/blocks/TileAccess.java b/src/main/java/bvv/core/blocks/TileAccess.java index a73a8cf..030f1b3 100644 --- a/src/main/java/bvv/core/blocks/TileAccess.java +++ b/src/main/java/bvv/core/blocks/TileAccess.java @@ -42,6 +42,7 @@ import net.imglib2.type.PrimitiveType; import net.imglib2.util.Fraction; +import static net.imglib2.type.PrimitiveType.BYTE; import static net.imglib2.type.PrimitiveType.SHORT; /** @@ -124,6 +125,16 @@ public boolean loadTile( final int[] gridPos, final UploadBuffer buffer ) new CopySubArrayImp.ShortToAddress(), cacheSpec ); + } else if ( cacheSpec.format() == Texture.InternalFormat.R8 && cellimg ) + { + final boolean volatil = type instanceof Volatile; + return new TileAccess<>( + volatil + ? new GridDataAccessImp.VolatileCells<>( ( AbstractCellImg ) img ) + : new GridDataAccessImp.Cells<>( ( AbstractCellImg ) img ), + new CopySubArrayImp.ByteToAddress(), + cacheSpec + ); } } @@ -137,7 +148,7 @@ public static boolean isSupportedType( final Object type ) { final PrimitiveType primitive = ( ( NativeType ) type ).getNativeTypeFactory().getPrimitiveType(); final Fraction epp = ( ( NativeType ) type ).getEntitiesPerPixel(); - if ( primitive == SHORT && epp.getNumerator() == epp.getDenominator() ) + if (( primitive == SHORT && epp.getNumerator() == epp.getDenominator() )|| (primitive == BYTE && epp.getNumerator() == epp.getDenominator())) return true; } diff --git a/src/main/java/bvv/core/render/VolumeRenderer.java b/src/main/java/bvv/core/render/VolumeRenderer.java index bd0aafb..f4c4495 100644 --- a/src/main/java/bvv/core/render/VolumeRenderer.java +++ b/src/main/java/bvv/core/render/VolumeRenderer.java @@ -28,6 +28,7 @@ */ package bvv.core.render; +import static bvv.core.backend.Texture.InternalFormat.R8; import static com.jogamp.opengl.GL.GL_ALWAYS; import static com.jogamp.opengl.GL.GL_BLEND; import static com.jogamp.opengl.GL.GL_DEPTH_TEST; @@ -62,6 +63,8 @@ import net.imglib2.type.numeric.ARGBType; import net.imglib2.type.numeric.integer.UnsignedByteType; import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.type.volatiles.VolatileUnsignedByteType; +import net.imglib2.type.volatiles.VolatileUnsignedShortType; import org.joml.Matrix4f; import bdv.tools.brightness.ConverterSetup; @@ -177,7 +180,7 @@ public VolumeRenderer( // set up gpu cache // TODO This could be packaged into one class and potentially shared between renderers? - cacheSpec = new CacheSpec( R16, cacheBlockSize ); + cacheSpec = new CacheSpec( R8, cacheBlockSize ); //new CacheSpec( R8, cacheBlockSize ); final int[] cacheGridDimensions = TextureCache.findSuitableGridSize( cacheSpec, maxCacheSizeInMB ); textureCache = new TextureCache( cacheGridDimensions, cacheSpec ); pboChain = new PboChain( 5, 100, textureCache ); @@ -276,7 +279,13 @@ else if ( type == LOAD ) if ( !TileAccess.isSupportedType( stack.getType() ) ) throw new IllegalArgumentException(); multiResStacks.add( ( MultiResolutionStack3D< ? > ) stack ); - volumeSignatures.add( new VolumeSignature( MULTIRESOLUTION, USHORT ) ); + final Object pixelType = stack.getType(); + if (( pixelType instanceof UnsignedShortType ) || (pixelType instanceof VolatileUnsignedShortType)) + volumeSignatures.add( new VolumeSignature( MULTIRESOLUTION, USHORT ) ); + else if (( pixelType instanceof UnsignedByteType ) || (pixelType instanceof VolatileUnsignedByteType)) + volumeSignatures.add( new VolumeSignature( MULTIRESOLUTION, UBYTE ) ); + else + throw new IllegalArgumentException("Multiresolution stack with pixel type "+pixelType.getClass().getName()+" unsupported in BigVolumeViewer."); } else if ( stack instanceof SimpleStack3D ) { diff --git a/src/test/java/bvv/vistools/examples/Example01.java b/src/test/java/bvv/vistools/examples/Example01.java index ffaa6ac..3d2563b 100644 --- a/src/test/java/bvv/vistools/examples/Example01.java +++ b/src/test/java/bvv/vistools/examples/Example01.java @@ -43,7 +43,7 @@ public class Example01 */ public static void main( final String[] args ) { - final ImagePlus imp = IJ.openImage( "https://imagej.nih.gov/ij/images/t1-head.zip" ); + final ImagePlus imp = IJ.openImage( "https://imagej.net/ij/images/t1-head.zip" ); final Img< UnsignedShortType > img = ImageJFunctions.wrapShort( imp ); final BvvSource source = BvvFunctions.show( img, "t1-head" ); diff --git a/src/test/java/bvv/vistools/examples/Example02.java b/src/test/java/bvv/vistools/examples/Example02.java index 07f6499..a5865c3 100644 --- a/src/test/java/bvv/vistools/examples/Example02.java +++ b/src/test/java/bvv/vistools/examples/Example02.java @@ -43,7 +43,7 @@ public class Example02 */ public static void main( final String[] args ) { - final ImagePlus imp = IJ.openImage( "https://imagej.nih.gov/ij/images/flybrain.zip" ); + final ImagePlus imp = IJ.openImage( "https://imagej.net/ij/images/flybrain.zip" ); final Img< ARGBType > img = ImageJFunctions.wrapRGBA( imp ); // additional Bvv.options() to specify calibration diff --git a/src/test/java/bvv/vistools/examples/Example03.java b/src/test/java/bvv/vistools/examples/Example03.java index 2d317cd..b70eaa0 100644 --- a/src/test/java/bvv/vistools/examples/Example03.java +++ b/src/test/java/bvv/vistools/examples/Example03.java @@ -46,7 +46,7 @@ public class Example03 */ public static void main( final String[] args ) { - final ImagePlus imp = IJ.openImage( "https://imagej.nih.gov/ij/images/Spindly-GFP.zip" ); + final ImagePlus imp = IJ.openImage( "https://imagej.net/ij/images/Spindly-GFP.zip" ); final Img< UnsignedShortType > img = ImageJFunctions.wrapShort( imp ); final double pw = imp.getCalibration().pixelWidth; diff --git a/src/test/java/bvv/vistools/examples/Example04.java b/src/test/java/bvv/vistools/examples/Example04.java index 21ff346..4c63c87 100644 --- a/src/test/java/bvv/vistools/examples/Example04.java +++ b/src/test/java/bvv/vistools/examples/Example04.java @@ -47,7 +47,7 @@ public class Example04 */ public static void main( final String[] args ) { - final ImagePlus imp = IJ.openImage( "https://imagej.nih.gov/ij/images/t1-head.zip" ); + final ImagePlus imp = IJ.openImage( "https://imagej.net/ij/images/t1-head.zip" ); final Img< UnsignedShortType > img = ImageJFunctions.wrapShort( imp ); final BvvSource source1 = BvvFunctions.show( img, "t1-head" ); diff --git a/src/test/java/bvv/vistools/examples/Example06.java b/src/test/java/bvv/vistools/examples/Example06.java index c989bd0..8b44b19 100644 --- a/src/test/java/bvv/vistools/examples/Example06.java +++ b/src/test/java/bvv/vistools/examples/Example06.java @@ -57,7 +57,7 @@ public class Example06 */ public static void main( final String[] args ) { - final ImagePlus imp = IJ.openImage( "https://imagej.nih.gov/ij/images/flybrain.zip" ); + final ImagePlus imp = IJ.openImage( "https://imagej.net/ij/images/flybrain.zip" ); final RandomAccessibleInterval< ARGBType > flybrain = ImageJFunctions.wrapRGBA( imp ); AffineTransform3D transform = new AffineTransform3D(); diff --git a/src/test/java/bvv/vistools/examples/Example07.java b/src/test/java/bvv/vistools/examples/Example07.java index 96c4c4b..59ca650 100644 --- a/src/test/java/bvv/vistools/examples/Example07.java +++ b/src/test/java/bvv/vistools/examples/Example07.java @@ -48,7 +48,7 @@ public class Example07 */ public static void main( final String[] args ) { - final ImagePlus imp = IJ.openImage( "https://imagej.nih.gov/ij/images/t1-head.zip" ); + final ImagePlus imp = IJ.openImage( "https://imagej.net/ij/images/t1-head.zip" ); final Img< UnsignedShortType > img = ImageJFunctions.wrapShort( imp ); final BvvSource source = BvvFunctions.show( img, "t1-head", diff --git a/src/test/java/bvv/vistools/examples/Example08.java b/src/test/java/bvv/vistools/examples/Example08.java new file mode 100644 index 0000000..d276f63 --- /dev/null +++ b/src/test/java/bvv/vistools/examples/Example08.java @@ -0,0 +1,350 @@ +package bvv.vistools.examples; + +import bdv.cache.SharedQueue; +import bdv.util.volatiles.VolatileViews; +import bvv.vistools.BvvFunctions; +import bvv.vistools.BvvOptions; +import bvv.vistools.BvvStackSource; +import net.imglib2.Cursor; +import net.imglib2.FinalInterval; +import net.imglib2.IterableInterval; +import net.imglib2.IterableRealInterval; +import net.imglib2.KDTree; +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessible; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.RealInterval; +import net.imglib2.RealPoint; +import net.imglib2.RealPointSampleList; +import net.imglib2.RealRandomAccessible; +import net.imglib2.Volatile; +import net.imglib2.algorithm.util.Grids; +import net.imglib2.cache.img.CachedCellImg; +import net.imglib2.cache.img.DiskCachedCellImgFactory; +import net.imglib2.cache.img.DiskCachedCellImgOptions; +import net.imglib2.cache.img.RandomAccessibleCacheLoader; +import net.imglib2.cache.img.ReadOnlyCachedCellImgFactory; +import net.imglib2.img.Img; +import net.imglib2.img.array.ArrayImgFactory; +import net.imglib2.img.basictypeaccess.AccessFlags; +import net.imglib2.img.basictypeaccess.array.ArrayDataAccess; +import net.imglib2.img.cell.CellGrid; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.interpolation.neighborsearch.NearestNeighborSearchInterpolatorFactory; +import net.imglib2.neighborsearch.NearestNeighborSearch; +import net.imglib2.neighborsearch.NearestNeighborSearchOnKDTree; +import net.imglib2.type.NativeType; +import net.imglib2.type.Type; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.type.numeric.RealType; +import net.imglib2.type.numeric.integer.UnsignedByteType; +import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.util.Intervals; +import net.imglib2.util.Util; +import net.imglib2.view.Views; + +import java.util.Random; + +/** + * Show how 8-bit images can be displayed in BigVolumeViewer + * A precomputed label image made of 3D voronoi labels is displayed and an on-the-fly computed + * image consisting of the border between the different labels is displayed. + * + * In order to show the on the fly computation, there's a sleep time of 1 second for each computed block (64 64 64) + * + * @author Nicolas Chiaruttini, EPFL, 2025 + */ +public class Example08 { + + public static void main(String... args) { + + long[] imageVoxelSize = new long[] { 512, 512, 512 }; + int numberOfPoints = 800; + + RandomAccessibleInterval labelImage = get8BitsLabelImage(imageVoxelSize, numberOfPoints); + //RandomAccessibleInterval labelImage = get16BitsLabelImage(imageVoxelSize, numberOfPoints); + //RandomAccessibleInterval labelImage = getFloatLabelImage(imageVoxelSize, numberOfPoints); + + final SharedQueue queue = new SharedQueue( Runtime.getRuntime().availableProcessors()-1 ); + Img nonVRAI = get3DBorderLabelImage(labelImage); + RandomAccessibleInterval> rai = VolatileViews.wrapAsVolatile(nonVRAI, queue); + + //ImageJFunctions.show(nonVRAI, "Borders"); // SLOW -> triggers a lot of computation. Remove Thread.sleep in this file if you want to do it. + + final BvvStackSource< ? > bvvSourceBorders = BvvFunctions.show( rai, "Borders"); + bvvSourceBorders.setDisplayRange( 0, 2 ); + bvvSourceBorders.setColor(new ARGBType(0x00FF00FF)); + + ImageJFunctions.show(labelImage, "Labels"); + final BvvStackSource< ? > bvvSourceLabels = BvvFunctions.show( labelImage, "Labels", BvvOptions.options().addTo(bvvSourceBorders)); + bvvSourceLabels.setDisplayRange( 0, 350 ); + bvvSourceLabels.setColor(new ARGBType(0xFF00FF00)); + + } + + public static RandomAccessibleInterval getFloatLabelImage(long[] imageVoxelSize, int numberOfPoints) { + FinalInterval interval = new FinalInterval( imageVoxelSize ); + IterableRealInterval< FloatType > iterableFloat = createFloatRandomPoints( interval, numberOfPoints ); + // Get test label image + return getTestLabelImage(iterableFloat, imageVoxelSize); + } + + public static RandomAccessibleInterval get8BitsLabelImage(long[] imageVoxelSize, int numberOfPoints) { + FinalInterval interval = new FinalInterval( imageVoxelSize ); + IterableRealInterval< UnsignedByteType > iterableFloat = createByteRandomPoints( interval, numberOfPoints ); + // Get test label image + return getTestLabelImage(iterableFloat, imageVoxelSize); + } + + public static RandomAccessibleInterval get16BitsLabelImage(long[] imageVoxelSize, int numberOfPoints) { + FinalInterval interval = new FinalInterval( imageVoxelSize ); + IterableRealInterval< UnsignedShortType > iterableFloat = createShortRandomPoints( interval, numberOfPoints ); + // Get test label image + return getTestLabelImage(iterableFloat, imageVoxelSize); + } + + public static & Comparable > Img get3DBorderLabelImage(RandomAccessibleInterval lblImg) { + // Make edge display on demand + final int[] cellDimensions = new int[] { 64, 64, 64 }; + + // Cached Image Factory Options + final DiskCachedCellImgOptions factoryOptions = DiskCachedCellImgOptions.options() + .cellDimensions( cellDimensions ) + .cacheType( DiskCachedCellImgOptions.CacheType.BOUNDED ) + .maxCacheSize( 100 ); + + // Expand label image by one pixel to avoid out of bounds exception + final RandomAccessibleInterval lblImgWithBorder = Views.expandBorder(lblImg, 1,1,1); + + // Creates cached image factory of Type Byte + final DiskCachedCellImgFactory< UnsignedByteType > factory = new DiskCachedCellImgFactory<>( new UnsignedByteType(), factoryOptions ); + + // Creates shifted views by one pixel in each dimension + RandomAccessibleInterval lblImgXShift = Views.translate(lblImgWithBorder, 1,0,0); + RandomAccessibleInterval lblImgYShift = Views.translate(lblImgWithBorder, 0,1,0); + RandomAccessibleInterval lblImgZShift = Views.translate(lblImgWithBorder, 0,0,1); + + // Creates border image, with cell Consumer method, which creates the image + final Img borderLabel = factory.create( lblImg, cell -> { + + Thread.sleep(1000); + + // Cursor on the source image + final Cursor inNS = Views.flatIterable( Views.interval( lblImg, cell ) ).cursor(); + + // Cursor on shifted source image + final Cursor inXS = Views.flatIterable( Views.interval( lblImgXShift, cell ) ).cursor(); + final Cursor inYS = Views.flatIterable( Views.interval( lblImgYShift, cell ) ).cursor(); + final Cursor inZS = Views.flatIterable( Views.interval( lblImgZShift, cell ) ).cursor(); + + // Cursor on output image + final Cursor out = Views.flatIterable( cell ).cursor(); + + // Loops through voxels + while ( out.hasNext() ) { + T v = inNS.next(); + if (v.compareTo(inXS.next())!=0) { + out.next().set( (byte) 1 ); + inYS.next(); + inZS.next(); + } else { + if (v.compareTo(inYS.next())!=0) { + out.next().set( (byte) 2 ); + inZS.next(); + } else { + if (v.compareTo(inZS.next())!=0) { + out.next().set( (byte) 3 ); + } else { + out.next(); + } + } + } + } + }, DiskCachedCellImgOptions.options().initializeCellsAsDirty( true ) ); + + return borderLabel; + } + + + //------------------------------------- METHODS TO CREATE TEST LABEL IMAGE + + public static &NativeType> RandomAccessibleInterval< T > getTestLabelImage(IterableRealInterval< T > realInterval, final long[] imgTestSize) { + + // the interval in which to create random points + + // create an IterableRealInterval + // using nearest neighbor search we will be able to return a value an any position in space + NearestNeighborSearch< T > search = + new NearestNeighborSearchOnKDTree<>( + new KDTree<>( realInterval ) ); + + // make it into RealRandomAccessible using nearest neighbor search + RealRandomAccessible< T > realRandomAccessible = + Views.interpolate( search, new NearestNeighborSearchInterpolatorFactory<>() ); + + // convert it into a RandomAccessible which can be displayed + RandomAccessible< T > randomAccessible = Views.raster( realRandomAccessible ); + + // set the initial interval as area to view + RandomAccessibleInterval< T > labelImage = Views.interval( randomAccessible, new FinalInterval(imgTestSize) ); + + final RandomAccessibleInterval< T > labelImageCopy = new ArrayImgFactory( Util.getTypeFromInterval( labelImage ) ).create( labelImage ); + + // Image copied to avoid computing it on the fly + // https://github.com/imglib/imglib2-algorithm/blob/47cd6ed5c97cca4b316c92d4d3260086a335544d/src/main/java/net/imglib2/algorithm/util/Grids.java#L221 used for parallel copy + Grids.collectAllContainedIntervals(imgTestSize, new int[] {128,128,32}).parallelStream().forEach( blockinterval-> + copy(labelImage, Views.interval(labelImageCopy, blockinterval)) + ); + + // Returning it + return labelImageCopy; + } + + + /** + * Copy from a source that is just RandomAccessible to an IterableInterval. Latter one defines + * size and location of the copy operation. It will query the same pixel locations of the + * IterableInterval in the RandomAccessible. It is up to the developer to ensure that these + * coordinates match. + * + * Note that both, input and output could be Views, Img or anything that implements + * those interfaces. + * + * @param source - a RandomAccess as source that can be infinite + * @param target - an IterableInterval as target + */ + public static < T extends Type< T >> void copy(final RandomAccessible< T > source, + final IterableInterval< T > target ) + { + // create a cursor that automatically localizes itself on every move + Cursor< T > targetCursor = target.localizingCursor(); + RandomAccess< T > sourceRandomAccess = source.randomAccess(); + + // iterate over the input cursor + while ( targetCursor.hasNext()) + { + // move input cursor forward + targetCursor.fwd(); + + // set the output cursor to the position of the input cursor + sourceRandomAccess.setPosition( targetCursor ); + + // set the value of this pixel of the output image, every Type supports T.set( T type ) + targetCursor.get().set( sourceRandomAccess.get() ); + } + + } + + + /** + * Create a number of n-dimensional random points in a certain interval + * having a random intensity 0...1 + * + * @param interval - the interval in which points are created + * @param numPoints - the amount of points + * + * @return a RealPointSampleList (which is an IterableRealInterval) + */ + public static RealPointSampleList createFloatRandomPoints( + RealInterval interval, int numPoints ) + { + // the number of dimensions + int numDimensions = interval.numDimensions(); + + // a random number generator + Random rnd = new Random( 2001);//System.currentTimeMillis() ); + + // a list of Samples with coordinates + RealPointSampleList< FloatType > elements = + new RealPointSampleList<>( numDimensions ); + + for ( int i = 0; i < numPoints; ++i ) + { + RealPoint point = new RealPoint( numDimensions ); + + for ( int d = 0; d < numDimensions; ++d ) + point.setPosition( rnd.nextDouble() * + ( interval.realMax( d ) - interval.realMin( d ) ) + interval.realMin( d ), d ); + + // add a new element with a random intensity in the range 0...1 + elements.add( point, new FloatType( rnd.nextFloat() ) ); + } + + return elements; + } + + public static IterableRealInterval createShortRandomPoints(RealInterval interval, int numPoints) { + // the number of dimensions + int numDimensions = interval.numDimensions(); + + // a random number generator + Random rnd = new Random( 2001);//System.currentTimeMillis() ); + + // a list of Samples with coordinates + RealPointSampleList< UnsignedShortType > elements = + new RealPointSampleList<>( numDimensions ); + + for ( int i = 0; i < numPoints; ++i ) + { + RealPoint point = new RealPoint( numDimensions ); + + for ( int d = 0; d < numDimensions; ++d ) + point.setPosition( rnd.nextDouble() * + ( interval.realMax( d ) - interval.realMin( d ) ) + interval.realMin( d ), d ); + + // add a new element with a random intensity in the range 0...1 + elements.add( point, new UnsignedShortType( rnd.nextInt(65535) ) ); + } + + return elements; + } + + /** + * Create a number of n-dimensional random points in a certain interval + * having a random intensity 0...1 + * + * @param interval - the interval in which points are created + * @param numPoints - the amount of points + * + * @return a RealPointSampleList (which is an IterableRealInterval) + */ + public static RealPointSampleList createByteRandomPoints( + RealInterval interval, int numPoints ) + { + // the number of dimensions + int numDimensions = interval.numDimensions(); + + // a random number generator + Random rnd = new Random( 2001);//System.currentTimeMillis() ); + + // a list of Samples with coordinates + RealPointSampleList< UnsignedByteType > elements = + new RealPointSampleList<>( numDimensions ); + + for ( int i = 0; i < numPoints; ++i ) + { + RealPoint point = new RealPoint( numDimensions ); + + for ( int d = 0; d < numDimensions; ++d ) + point.setPosition( rnd.nextDouble() * + ( interval.realMax( d ) - interval.realMin( d ) ) + interval.realMin( d ), d ); + + // add a new element with a random intensity in the range 0...1 + elements.add( point, new UnsignedByteType( rnd.nextInt(255) ) ); + } + + return elements; + } + + public static , A extends ArrayDataAccess, CA extends ArrayDataAccess> CachedCellImg wrapAsCachedCellImg( + final RandomAccessibleInterval source, + final int[] blockSize) { + + final long[] dimensions = Intervals.dimensionsAsLongArray(source); + final CellGrid grid = new CellGrid(dimensions, blockSize); + + final RandomAccessibleCacheLoader loader = RandomAccessibleCacheLoader.get(grid, Views.zeroMin(source), AccessFlags.setOf()); + return new ReadOnlyCachedCellImgFactory().createWithCacheLoader(dimensions, source.randomAccess().get(), loader); + } +} diff --git a/src/test/java/bvv/vistools/examples/Example09.java b/src/test/java/bvv/vistools/examples/Example09.java new file mode 100644 index 0000000..3806ce0 --- /dev/null +++ b/src/test/java/bvv/vistools/examples/Example09.java @@ -0,0 +1,378 @@ +package bvv.vistools.examples; + +import bdv.cache.SharedQueue; +import bdv.util.volatiles.VolatileViews; +import bvv.vistools.BvvFunctions; +import bvv.vistools.BvvOptions; +import bvv.vistools.BvvStackSource; +import net.imglib2.Cursor; +import net.imglib2.FinalInterval; +import net.imglib2.IterableInterval; +import net.imglib2.IterableRealInterval; +import net.imglib2.KDTree; +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessible; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.RealInterval; +import net.imglib2.RealPoint; +import net.imglib2.RealPointSampleList; +import net.imglib2.RealRandomAccessible; +import net.imglib2.Volatile; +import net.imglib2.algorithm.util.Grids; +import net.imglib2.cache.img.CachedCellImg; +import net.imglib2.cache.img.DiskCachedCellImgFactory; +import net.imglib2.cache.img.DiskCachedCellImgOptions; +import net.imglib2.cache.img.RandomAccessibleCacheLoader; +import net.imglib2.cache.img.ReadOnlyCachedCellImgFactory; +import net.imglib2.img.Img; +import net.imglib2.img.array.ArrayImgFactory; +import net.imglib2.img.basictypeaccess.AccessFlags; +import net.imglib2.img.basictypeaccess.array.ArrayDataAccess; +import net.imglib2.img.cell.CellGrid; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.interpolation.neighborsearch.NearestNeighborSearchInterpolatorFactory; +import net.imglib2.neighborsearch.NearestNeighborSearch; +import net.imglib2.neighborsearch.NearestNeighborSearchOnKDTree; +import net.imglib2.type.NativeType; +import net.imglib2.type.Type; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.type.numeric.RealType; +import net.imglib2.type.numeric.integer.UnsignedByteType; +import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.util.Intervals; +import net.imglib2.util.Util; +import net.imglib2.view.Views; + +import java.util.Random; + +/** + * Show both a 8-bit and a 16-bit cached image + * A precomputed label image made of 3D voronoi labels is displayed and an on-the-fly computed + * image consisting of the border between the different labels is displayed. + * + * In order to show the on the fly computation, there's a sleep time of 1 second for each computed block (64 64 64) + * + * @author Nicolas Chiaruttini, EPFL, 2025 + */ +public class Example09 { + + public static void main(String... args) { + + long[] imageVoxelSize = new long[] { 512, 512, 512 }; + int numberOfPoints = 800; + + RandomAccessibleInterval labelImage_1 = get8BitsLabelImage(imageVoxelSize, numberOfPoints); + + RandomAccessibleInterval labelImage_2 = get8BitsLabelImage(imageVoxelSize, numberOfPoints/2); + + final SharedQueue queue_1 = new SharedQueue( Runtime.getRuntime().availableProcessors()-1 ); + Img nonVRAI8bits_1 = get3DBorderLabelImage8Bits(labelImage_1); + RandomAccessibleInterval> rai8bits_1 = VolatileViews.wrapAsVolatile(nonVRAI8bits_1, queue_1); + + final SharedQueue queue_2 = new SharedQueue( Runtime.getRuntime().availableProcessors()-1 ); + Img nonVRAI8bits_2 = get3DBorderLabelImage8Bits(labelImage_2); + RandomAccessibleInterval> rai8bits_2 = VolatileViews.wrapAsVolatile(nonVRAI8bits_2, queue_2); + + Img nonVRAI16bits = get3DBorderLabelImage16Bits(labelImage_1); + RandomAccessibleInterval> rai16bits = VolatileViews.wrapAsVolatile(nonVRAI16bits, queue_1); + + final BvvStackSource< ? > bvvSourceBorders8bits_1 = BvvFunctions.show( rai8bits_1, "Borders 8 bits 1"); + bvvSourceBorders8bits_1.setDisplayRange( 0, 2 ); + bvvSourceBorders8bits_1.setColor(new ARGBType(0x00FF00FF)); + + + final BvvStackSource< ? > bvvSourceBorders8bits_2 = BvvFunctions.show( rai8bits_2, "Borders 8 bits 2", BvvOptions.options().addTo(bvvSourceBorders8bits_1)); + bvvSourceBorders8bits_2.setDisplayRange( 0, 2 ); + bvvSourceBorders8bits_2.setColor(new ARGBType(0x000000FF)); + + /*final BvvStackSource< ? > bvvSourceBorders16bits = BvvFunctions.show( rai16bits, "Borders 16 bits", BvvOptions.options().addTo(bvvSourceBorders8bits)); + bvvSourceBorders16bits.setDisplayRange( 0, 2 ); + bvvSourceBorders16bits.setColor(new ARGBType(0x0000FFFF));*/ + + } + + public static RandomAccessibleInterval getFloatLabelImage(long[] imageVoxelSize, int numberOfPoints) { + FinalInterval interval = new FinalInterval( imageVoxelSize ); + IterableRealInterval< FloatType > iterableFloat = createFloatRandomPoints( interval, numberOfPoints ); + // Get test label image + return getTestLabelImage(iterableFloat, imageVoxelSize); + } + + public static RandomAccessibleInterval get8BitsLabelImage(long[] imageVoxelSize, int numberOfPoints) { + FinalInterval interval = new FinalInterval( imageVoxelSize ); + IterableRealInterval< UnsignedByteType > iterableFloat = createByteRandomPoints( interval, numberOfPoints ); + // Get test label image + return getTestLabelImage(iterableFloat, imageVoxelSize); + } + + public static & Comparable > Img get3DBorderLabelImage8Bits(RandomAccessibleInterval lblImg) { + // Make edge display on demand + final int[] cellDimensions = new int[] { 64, 64, 64 }; + + // Cached Image Factory Options + final DiskCachedCellImgOptions factoryOptions = DiskCachedCellImgOptions.options() + .cellDimensions( cellDimensions ) + .cacheType( DiskCachedCellImgOptions.CacheType.BOUNDED ) + .maxCacheSize( 100 ); + + // Expand label image by one pixel to avoid out of bounds exception + final RandomAccessibleInterval lblImgWithBorder = Views.expandBorder(lblImg, 1,1,1); + + // Creates cached image factory of Type Byte + final DiskCachedCellImgFactory< UnsignedByteType > factory = new DiskCachedCellImgFactory<>( new UnsignedByteType(), factoryOptions ); + + // Creates shifted views by one pixel in each dimension + RandomAccessibleInterval lblImgXShift = Views.translate(lblImgWithBorder, 1,0,0); + RandomAccessibleInterval lblImgYShift = Views.translate(lblImgWithBorder, 0,1,0); + RandomAccessibleInterval lblImgZShift = Views.translate(lblImgWithBorder, 0,0,1); + + // Creates border image, with cell Consumer method, which creates the image + final Img borderLabel = factory.create( lblImg, cell -> { + + Thread.sleep(1000); + + // Cursor on the source image + final Cursor inNS = Views.flatIterable( Views.interval( lblImg, cell ) ).cursor(); + + // Cursor on shifted source image + final Cursor inXS = Views.flatIterable( Views.interval( lblImgXShift, cell ) ).cursor(); + final Cursor inYS = Views.flatIterable( Views.interval( lblImgYShift, cell ) ).cursor(); + final Cursor inZS = Views.flatIterable( Views.interval( lblImgZShift, cell ) ).cursor(); + + // Cursor on output image + final Cursor out = Views.flatIterable( cell ).cursor(); + + // Loops through voxels + while ( out.hasNext() ) { + T v = inNS.next(); + if (v.compareTo(inXS.next())!=0) { + out.next().set( (byte) 1 ); + inYS.next(); + inZS.next(); + } else { + if (v.compareTo(inYS.next())!=0) { + out.next().set( (byte) 2 ); + inZS.next(); + } else { + if (v.compareTo(inZS.next())!=0) { + out.next().set( (byte) 3 ); + } else { + out.next(); + } + } + } + } + }, DiskCachedCellImgOptions.options().initializeCellsAsDirty( true ) ); + + return borderLabel; + } + + public static & Comparable > Img get3DBorderLabelImage16Bits(RandomAccessibleInterval lblImg) { + // Make edge display on demand + final int[] cellDimensions = new int[] { 64, 64, 64 }; + + // Cached Image Factory Options + final DiskCachedCellImgOptions factoryOptions = DiskCachedCellImgOptions.options() + .cellDimensions( cellDimensions ) + .cacheType( DiskCachedCellImgOptions.CacheType.BOUNDED ) + .maxCacheSize( 100 ); + + // Expand label image by one pixel to avoid out of bounds exception + final RandomAccessibleInterval lblImgWithBorder = Views.expandBorder(lblImg, 1,1,1); + + // Creates cached image factory of Type Byte + final DiskCachedCellImgFactory< UnsignedShortType > factory = new DiskCachedCellImgFactory<>( new UnsignedShortType(), factoryOptions ); + + // Creates shifted views by one pixel in each dimension + RandomAccessibleInterval lblImgXShift = Views.translate(lblImgWithBorder, 1,0,0); + RandomAccessibleInterval lblImgYShift = Views.translate(lblImgWithBorder, 0,1,0); + RandomAccessibleInterval lblImgZShift = Views.translate(lblImgWithBorder, 0,0,1); + + // Creates border image, with cell Consumer method, which creates the image + final Img borderLabel = factory.create( lblImg, cell -> { + + Thread.sleep(1000); + + // Cursor on the source image + final Cursor inNS = Views.flatIterable( Views.interval( lblImg, cell ) ).cursor(); + + // Cursor on shifted source image + final Cursor inXS = Views.flatIterable( Views.interval( lblImgXShift, cell ) ).cursor(); + final Cursor inYS = Views.flatIterable( Views.interval( lblImgYShift, cell ) ).cursor(); + final Cursor inZS = Views.flatIterable( Views.interval( lblImgZShift, cell ) ).cursor(); + + // Cursor on output image + final Cursor out = Views.flatIterable( cell ).cursor(); + + // Loops through voxels + while ( out.hasNext() ) { + T v = inNS.next(); + if (v.compareTo(inXS.next())!=0) { + out.next().set( (byte) 255 ); + inYS.next(); + inZS.next(); + } else { + if (v.compareTo(inYS.next())!=0) { + out.next().set( (byte) 512 ); + inZS.next(); + } else { + if (v.compareTo(inZS.next())!=0) { + out.next().set( (byte) 1024 ); + } else { + out.next(); + } + } + } + } + }, DiskCachedCellImgOptions.options().initializeCellsAsDirty( true ) ); + + return borderLabel; + } + + + //------------------------------------- METHODS TO CREATE TEST LABEL IMAGE + + public static &NativeType> RandomAccessibleInterval< T > getTestLabelImage(IterableRealInterval< T > realInterval, final long[] imgTestSize) { + + // the interval in which to create random points + + // create an IterableRealInterval + // using nearest neighbor search we will be able to return a value an any position in space + NearestNeighborSearch< T > search = + new NearestNeighborSearchOnKDTree<>( + new KDTree<>( realInterval ) ); + + // make it into RealRandomAccessible using nearest neighbor search + RealRandomAccessible< T > realRandomAccessible = + Views.interpolate( search, new NearestNeighborSearchInterpolatorFactory<>() ); + + // convert it into a RandomAccessible which can be displayed + RandomAccessible< T > randomAccessible = Views.raster( realRandomAccessible ); + + // set the initial interval as area to view + RandomAccessibleInterval< T > labelImage = Views.interval( randomAccessible, new FinalInterval(imgTestSize) ); + + final RandomAccessibleInterval< T > labelImageCopy = new ArrayImgFactory( Util.getTypeFromInterval( labelImage ) ).create( labelImage ); + + // Image copied to avoid computing it on the fly + // https://github.com/imglib/imglib2-algorithm/blob/47cd6ed5c97cca4b316c92d4d3260086a335544d/src/main/java/net/imglib2/algorithm/util/Grids.java#L221 used for parallel copy + Grids.collectAllContainedIntervals(imgTestSize, new int[] {128,128,32}).parallelStream().forEach( blockinterval-> + copy(labelImage, Views.interval(labelImageCopy, blockinterval)) + ); + + // Returning it + return labelImageCopy; + } + + + /** + * Copy from a source that is just RandomAccessible to an IterableInterval. Latter one defines + * size and location of the copy operation. It will query the same pixel locations of the + * IterableInterval in the RandomAccessible. It is up to the developer to ensure that these + * coordinates match. + * + * Note that both, input and output could be Views, Img or anything that implements + * those interfaces. + * + * @param source - a RandomAccess as source that can be infinite + * @param target - an IterableInterval as target + */ + public static < T extends Type< T >> void copy(final RandomAccessible< T > source, + final IterableInterval< T > target ) + { + // create a cursor that automatically localizes itself on every move + Cursor< T > targetCursor = target.localizingCursor(); + RandomAccess< T > sourceRandomAccess = source.randomAccess(); + + // iterate over the input cursor + while ( targetCursor.hasNext()) + { + // move input cursor forward + targetCursor.fwd(); + + // set the output cursor to the position of the input cursor + sourceRandomAccess.setPosition( targetCursor ); + + // set the value of this pixel of the output image, every Type supports T.set( T type ) + targetCursor.get().set( sourceRandomAccess.get() ); + } + + } + + + /** + * Create a number of n-dimensional random points in a certain interval + * having a random intensity 0...1 + * + * @param interval - the interval in which points are created + * @param numPoints - the amount of points + * + * @return a RealPointSampleList (which is an IterableRealInterval) + */ + public static RealPointSampleList createFloatRandomPoints( + RealInterval interval, int numPoints ) + { + // the number of dimensions + int numDimensions = interval.numDimensions(); + + // a random number generator + Random rnd = new Random( 2001);//System.currentTimeMillis() ); + + // a list of Samples with coordinates + RealPointSampleList< FloatType > elements = + new RealPointSampleList<>( numDimensions ); + + for ( int i = 0; i < numPoints; ++i ) + { + RealPoint point = new RealPoint( numDimensions ); + + for ( int d = 0; d < numDimensions; ++d ) + point.setPosition( rnd.nextDouble() * + ( interval.realMax( d ) - interval.realMin( d ) ) + interval.realMin( d ), d ); + + // add a new element with a random intensity in the range 0...1 + elements.add( point, new FloatType( rnd.nextFloat() ) ); + } + + return elements; + } + + /** + * Create a number of n-dimensional random points in a certain interval + * having a random intensity 0...1 + * + * @param interval - the interval in which points are created + * @param numPoints - the amount of points + * + * @return a RealPointSampleList (which is an IterableRealInterval) + */ + public static RealPointSampleList createByteRandomPoints( + RealInterval interval, int numPoints ) + { + // the number of dimensions + int numDimensions = interval.numDimensions(); + + // a random number generator + Random rnd = new Random( 2001);//System.currentTimeMillis() ); + + // a list of Samples with coordinates + RealPointSampleList< UnsignedByteType > elements = + new RealPointSampleList<>( numDimensions ); + + for ( int i = 0; i < numPoints; ++i ) + { + RealPoint point = new RealPoint( numDimensions ); + + for ( int d = 0; d < numDimensions; ++d ) + point.setPosition( rnd.nextDouble() * + ( interval.realMax( d ) - interval.realMin( d ) ) + interval.realMin( d ), d ); + + // add a new element with a random intensity in the range 0...1 + elements.add( point, new UnsignedByteType( rnd.nextInt(255) ) ); + } + + return elements; + } + +} diff --git a/src/test/java/bvv/vistools/examples/ExampleBoxes.java b/src/test/java/bvv/vistools/examples/ExampleBoxes.java index fac9f60..a4f3e48 100644 --- a/src/test/java/bvv/vistools/examples/ExampleBoxes.java +++ b/src/test/java/bvv/vistools/examples/ExampleBoxes.java @@ -58,7 +58,7 @@ public class ExampleBoxes { public static void main( final String[] args ) { - final ImagePlus imp = IJ.openImage( "https://imagej.nih.gov/ij/images/t1-head.zip" ); + final ImagePlus imp = IJ.openImage( "https://imagej.net/ij/images/t1-head.zip" ); final Img< UnsignedShortType > img = ImageJFunctions.wrapShort( imp ); final BvvSource source = BvvFunctions.show( img, "t1-head",