The ImgLib2 “blocks” API is about performing computations on blocks of (image) data more efficiently than going pixel-by-pixel using RandomAccess, Type, etc.
A fundamental limitation of this framework is that it only works with NativeType, and (so far) only with those that map 1:1 to primitives. For example, UnsignedByteType works, but ComplexDoubleType does not.
Idea
The idea is simple:
Efficiently extract a region from a RandomAccessible<T> into a flat primitive array.
Implement small, efficient functions that compute the output flat primitive array from the input flat primitive array. (Importantly, these functions don’t do anything “clever”: out-of-bounds extension is handled outside; if the input data requires padding, assume the input array is padded. No per-element type checks, etc. If the function should work on float[] and int[] inputs, we have two versions.)
Wrap and assemble the computed flat primitive arrays back into RandomAccessibleInterval.
We also provide some infrastructure for chaining intermediate ‘block’ operations.
Outline
There are two levels of API:
The low-level PrimitiveBlocks and BlockProcessor deal with primitive java arrays.
The high-level BlockSupplier and UnaryBlockOperator wrap this in a layer of syntactic sugar and type-safety.
We look into the low-level API first. You’ll never have to use it unless you want to implement a new “blocks” algorithm, but it is useful to illustrate whats happening.
PrimitiveBlocks
Load and show 2D image that we will to work with…
Code
importij.IJ;importnet.imglib2.*;importnet.imglib2.util.*;importnet.imglib2.position.*;importnet.imglib2.img.array.*;importnet.imglib2.type.numeric.real.*;importnet.imglib2.type.numeric.integer.*;importnet.imglib2.converter.RealTypeConverters;importnet.imglib2.img.display.imagej.ImageJFunctions;// var fn = "/Users/pietzsch/workspace/data/DrosophilaWing.tif";// var fn = "https://mirror.imagej.net/ij/images/boats.gif";var fn ="boats.gif";RandomAccessibleInterval<UnsignedByteType> img = ImageJFunctions.wrap(IJ.openImage(fn));display(img);
89b8d9d7-72b6-4998-a81a-0e71ffe5c055
Code
var crop = Intervals.createMinSize(350,220,64,64);var cropped = img.view().interval(crop).zeroMin();display(cropped);
ee567bcf-71fa-4f9a-9df4-ea7c80a932e1
For any RandomAccessible<T> we can get a “block copier” using PrimitiveBlocks.of(...). As mentioned above, this only works with NativeType that map 1:1 to primitives.
(Note that we copy starting from (0, 0) now, as the shift to (350, 220) happens in the View transform.)
When PrimitiveBlocks.of(...) “understands” a View construction that ultimately end in CellImg, ArrayImg, etc., it and will create an optimized copier: Instead of using RandomAccess that checks for every pixel whether it enters a new Cell, whether it is out-of-bounds, etc., all these checks are precomputed and then relevant data from each Cell is copied in one go.
Details, details…
The speedup can be dramatic, in particular if the underlying source data is in a CellImg. Here is for example results of CopyBenchmarkViewPrimitiveBlocks
If a source RandomAccessible cannot be understood, PrimitiveBlocks.of(...) will return a fall-back implementation (based on LoopBuilder). With the optional OnFallback argument of PrimitiveBlocks.of(...) it can be configured whether fall-back should be * silently accepted (ACCEPT), * a warning should be printed (WARN) – the default, * or an IllegalArgumentException thrown (FAIL). The warning/exception message explains why the source RandomAccessible requires fall-back.
The RandomAccessible net.imglib2.position.FunctionRandomAccessible@6e4e6a73 is only be supported through the fall-back implementation of PrimitiveBlocks.
Cannot analyze view net.imglib2.position.FunctionRandomAccessible@6e4e6a73 of class FunctionRandomAccessible
Once we can extract primitive arrays from RandomAccessibleInterval, the actual computation happens in BlockProcessor.
This happens in two steps: 1. Backward: Inverse-transform the desired target interval to determine which source data we need to provide to the BlockProcessor. 2. Forward: Push the source data through the BlockProcessor to compute the target data (both primitive arrays).
To try that, we’ll use a BlockProcessor for 2x downsampling. Normally we don’t use BlockProcessor directly, so the constructor of that one is not public. We need to extract it from a high-level operator (explained later):
1. Backward: Inverse-transform the desired target interval to determine which source data we need to provide to the BlockProcessor.
Now we can ask the processor which source interval we will need to produce a given target interval. (In this case, the source interval is 2x larger and appropriately shifted for downsampling).
2. Forward: Push the source data through the BlockProcessor to compute the target data (both primitive arrays).
We first need to provide some source data for processing. We get that via PrimitiveBlocks.copy(...), where we specify the source pos/size as the source interval obtained from processor. (We can also ask processor to allocate a primitive array for holding the input data.)
Finally, we show the result wrapped as an ArrayImg, as well as the source area it was obtained from.
Code
var downsampled = ArrayImgs.unsignedBytes(targetData,64,64);display(downsampled);display(img.view().interval(processor.getSourceInterval()));
5858c8c3-5e67-431f-9c81-a9c59d335974
As you can imagine, it is straightforward to chain BlockProcessors, sending the target interval backwards through the chain, and the source data forwards via approriate temporary intermediate arrays. In fact, our BlockProcessor is already such a concatenation (convert byte[] to float[], downsample float[] to float[], convert float[] to byte[]).
Code
System.out.println(processor.getClass());
class net.imglib2.algorithm.blocks.ConcatenatedBlockProcessor
BlockSupplier and UnaryBlockOperator
With the details out of the way, let’s look at how this is wrapped into a more ImgLib2-like API.
The BlockSupplier interface is more or less equivalent to PrimitiveBlocks. The above code works fine if you replace PrimitiveBlocks with BlockSupplier.
BlockSupplier provides additional default methods for chaining operations, and support for wrapping into a CachedCellImg, etc. This is not available in imglib2 core, where PrimitiveBlocks lives, therefore the duplication… (BlockSupplier lives in imglib2-algorithm.)
The UnaryBlockOperator<S,T> interface wraps BlockProcessor<I,O>. The generic parameters <S,T> are the ImgLib2 Types corresponding to the primitive array types <I,O>.
Here is the downsampling operator we already used above:
This allows for type-safe (at compile-time) and “dimensionality-safe” (at runtime) concatenation.
Concatenating two UnaryBlockOperators yields a new UnaryBlockOperator. Concatenating a BlockSupplier and a UnaryBlockOperators yields a new BlockSupplier.
Note, that creating the operator we had to specify source type (new UnsignedByteType() and number of source dimensions (2). These must match those of blocks, so we could also directly get them from the thing we are concatenating to.
To avoid this duplication, BlockSupplier.andThen(...) also accepts operator factory functions.
BlockSupplier.tile(...) splits the computation of a requested target interval into computations of sub-intervals of a specified (maximum) size. The results are stored into temporary buffers and then copied into the target primitive array.
Applications for this are: * Computing large outputs (e. g. to write to N5 or wrap as ArrayImg) with operators that have better performance with smaller block sizes. * Avoiding excessively large blocks (e. g. when chaining multiple downsampling operators).
Extend the image, make a BlockSupplier, convert to FloatType, apply Gaussian smoothing, and cache in a CellImg. For showing it in BigDataViewer, we wrap it VolatileViews.wrapAsVolatile(...) for asynchronous (lazy) loading, so that we can see it working.