%mavenRepo scijava.public https://maven.scijava.org/content/groups/public
%maven net.imglib2:imglib2:6.0.0
%maven jitk:jitk-tps:3.0.3
%maven net.imagej:ij:1.53t
%maven net.imglib2:imglib2-ij:2.0.0-beta-46
%maven net.imglib2:imglib2-realtransform:3.1.2
How to display ImgLib2 data in a notebook?
Render ImgLib2 data into notebook objects
In this notebook, we will explore how to store, process and visualize data with ImgLib2 in a notebook.
First let’s add the necessary dependencies. We will use ImageJ to load example images and to generate RenderedImage
outputs that we can use to render in the notebook. Then, we will import ImgLib2 and the modules to share data between ImgLib2 and ImageJ and the imglib2-realtransform
module that includes various transformations.
Let’s open one of ImageJ’s example images and show it in the notebook. This uses Spencer Park’s image renderer:
import ij.*;
var imp = IJ.openImage("https://mirror.imagej.net/ij/images/clown.jpg");
.getBufferedImage(); imp
If we want to work with this image in ImgLib2, we need to provide it as an ImgLib2 interface:
import net.imglib2.*;
import net.imglib2.img.imageplus.*;
var imp = IJ.openImage("https://mirror.imagej.net/ij/images/clown.jpg");
// for later use without the compiler losing its mind, we must provide type information
// for the ImagePlus wrapper, so let's not use var here
<?> rai = ImagePlusImgs.from(imp);
RandomAccessibleInterval; rai
IntImagePlus [320x200]
There is no default renderer for ImgLib2 interfaces available to the notebook kernel, so we see a default String
representation of the result (when rendering this cell the first time). So let’s register some simple renderers that use ImgLib2’s ImageJ bridge and Spencer Park’s image renderer to render ImgLib2 data into the notebook. We add a version that renders the first 2D slice of a RandomAccessibleInterval
and a second version that renders a default interval 512x512+0+0
of the 2D slice at position 0 in all other dimensions of an infinite RandomAccessible
.
import io.github.spencerpark.jupyter.kernel.display.common.*;
import io.github.spencerpark.jupyter.kernel.display.mime.*;
import net.imglib2.img.display.imagej.*;
import net.imglib2.view.*;
getKernelInstance().getRenderer().createRegistration(RandomAccessibleInterval.class)
.preferring(MIMEType.IMAGE_PNG)
.supporting(MIMEType.IMAGE_JPEG, MIMEType.IMAGE_GIF)
.register((rai, context) -> Image.renderImage(
.wrap(rai, rai.toString()).getBufferedImage(),
ImageJFunctions));
context
getKernelInstance().getRenderer().createRegistration(RandomAccessible.class)
.preferring(MIMEType.IMAGE_PNG)
.supporting(MIMEType.IMAGE_JPEG, MIMEType.IMAGE_GIF)
.register((ra, context) -> Image.renderImage(
.wrap(
ImageJFunctions.interval(
Views,
ranew FinalInterval(
Arrays.copyOf(
new long[]{512, 512},
.numDimensions()))),
ra.toString()).getBufferedImage(),
ra)); context
Now let’s try the same again:
var imp = IJ.openImage("https://mirror.imagej.net/ij/images/clown.jpg");
// for later use without the compiler losing its mind, we must provide type information
// for the ImagePlus wrapper, so let's not use var here
<?> rai = ImagePlusImgs.from(imp);
RandomAccessibleInterval; rai
Ok, great! Let’s try the ‘infinite’ version:
var ra = Views.extendBorder(rai);
; ra
Wonderful! We can of course still render a String
representation or alternative encodings with the injected display
methods of the kernel:
display(rai, "text/plain");
display(ra, "text/plain");
display(rai, "image/jpeg");
display(ra, "image/gif");
IntImagePlus [320x200]
net.imglib2.view.ExtendedRandomAccessibleInterval@7b93c33
net.imglib2.view.ExtendedRandomAccessibleInterval@7b93c33
83cb29e1-36d5-4c3d-957c-4f0cc021af97
You may have noticed that the output of this cell ends with an obscure identifier. We see this, because we did not catch the output of the display
method which provides an identifier for the output object that it generates. This identifier can be used to update the contents of this object. We can use this to render simple animations, e.g. to slice through a 3D volume. Let’s try this with a 3D volume from the ImageJ example images:
var imp = IJ.openImage("https://mirror.imagej.net/ij/images/flybrain.zip");
<?> rai = ImagePlusImgs.from(imp);
RandomAccessibleIntervalvar refSlice = display(Views.hyperSlice(rai, 2, rai.dimension(2) / 2), "image/jpeg");
var refLabel = display("slice " + rai.dimension(2) / 2);
for (int z = 0; z < rai.dimension(2); ++z) {
var slice = Views.hyperSlice(rai, 2, z);
updateDisplay(refSlice, slice, "image/jpeg");
updateDisplay(refLabel, "slice " + z);
Thread.sleep(100);
}
// for static notebook export
updateDisplay(refSlice, Views.hyperSlice(rai, 2, rai.dimension(2) / 2), "image/jpeg");
slice 56
Of course, you can only see the animation if you actually run the notebook cell. In a future iteration, we are planning to implement an animated GIF generator for offline animations, but not this time. Let’s see what else we can do with these renderers.
First, let’s apply some transformations to images. Already in the above border extension example as well as in the slicing animation, we have used ImgLib2’s default behavior to apply transformations lazily, i.e. only when a ‘pixel’ is actually queried (e.g. to render it into a RenderedImage
raster), the transformations are applied. Transformations can be applied to both coordinates and values. Lets apply some transformations to values:
import net.imglib2.converter.*;
import net.imglib2.type.numeric.*;
var imp = IJ.openImage("https://mirror.imagej.net/ij/images/clown.jpg");
<ARGBType> rai = ImagePlusImgs.from(imp);
RandomAccessibleIntervaldisplay(Converters.argbChannel(rai, 1));
display("red");
display(Converters.argbChannel(rai, 2));
display("green");
display(Converters.argbChannel(rai, 3));
display("blue");
display(
.<ARGBType, ARGBType>convert2(
Converters,
rai(in, out) -> {
final int argb = in.get();
final double grey = 0.3 * ARGBType.red(argb) + 0.6 * ARGBType.green(argb) + 0.1 * ARGBType.blue(argb);
.set(ARGBType.rgba(255 - grey, grey, grey, 255));
out},
::new));
ARGBTypedisplay("grey to red-cyan ramp");
red
green
blue
grey to red-cyan ramp
b677ba7f-39df-4995-9861-d1c75f28d437
And now some integer coordinate transformations:
display(Views.invertAxis(rai, 0));
display("flip axis 0");
display(Views.permute(rai, 0, 1));
display("permute axes");
display(Views.extendMirrorSingle(rai));
display("mirror extension without repeated border pixels");
display(Views.subsample(Views.shear(Views.extendPeriodic(rai), 0, 1), 3, 1));
display("extend periodically, shear axis 1 into axis 0, subsample by (3, 1)");
flip axis 0
permute axes
mirror extension without repeated border pixels
extend periodically, shear axis 1 into axis 0, subsample by (3, 1)
2e864958-c2a5-4cf2-91cf-c74e2788bbfc
While most trivial integer transformations such as flipping axes work on intervals, you probably noticed that we had to extend the image to infinity in order to shear it, so ImgLib2 can provide values for coordinates outside of the source interval. For real coordinate transformations we will also need to interpolate values at non-integer coordinates. Finally, in order to render the result, we have to read it from a raster. Let’s do this:
import net.imglib2.interpolation.randomaccess.*;
import net.imglib2.realtransform.*;
var imp = IJ.openImage("https://mirror.imagej.net/ij/images/clown.jpg");
<ARGBType> rai = ImagePlusImgs.from(imp);
RandomAccessibleIntervalvar ra = Views.extendValue(rai, new ARGBType(0xff00ff00)); // < green background
var interpolated = Views.interpolate(ra, new ClampingNLinearInterpolatorFactory<>()); // n-linear interpolation
/**
* This would be
* var interpolated = Views.interpolate(ra, new NLinearInterpolatorFactory<>());
* if you have no concern about value overflows
*/
var affine = new AffineTransform2D();
var transformed = Views.interval(RealViews.affine(interpolated, affine), rai); // shortcut for affines
var refImage = display(transformed, "image/jpeg");
var refLabel = display("", "text/html");
final int steps = 20;
for (int i = 0; i < steps; ++i) {
.translate(-rai.dimension(0) / 2, -rai.dimension(1) / 2);
affine.rotate(Math.PI / 6.0 / steps);
affine.scale(1.0 + 0.7 / steps);
affine.translate(rai.dimension(0) / 2, rai.dimension(1) / 2);
affine
updateDisplay(refImage, Views.interval(transformed, rai), "image/jpeg");
updateDisplay(
,
refLabelString.format("""
<p>affine transformation matrix:</p>
<table>
<tr><td>%.2f</td><td>%.2f</td><td>%.2f</td></tr>
<tr><td>%.2f</td><td>%.2f</td><td>%.2f</td></tr>
</table>""",
.get(0, 0), affine.get(0, 1), affine.get(0, 2),
affine.get(1, 0), affine.get(1, 1), affine.get(1, 2)), "text/html");
affineThread.sleep(100);
}
affine transformation matrix:
1.72 | -0.99 | -16.22 |
0.99 | 1.72 | -231.50 |
Affine transformation are probably the most well known and simple real coordinate transformations, but there are many more. Let’s try a ThinplateSplineTransform
and format text output with markdown:
var refImage = display(rai, "image/jpeg");
var refLabel = display("", "text/markdown");
int steps = 20;
double stretch = 40;
for (int i = 0; i < steps; ++i) {
final double offset = stretch * i / steps;
final double[][] p = {
{0, rai.dimension(0), 0, rai.dimension(0), rai.dimension(0) * 0.25, rai.dimension(0) * 0.75, rai.dimension(0) * 0.25, rai.dimension(0) * 0.75},
{0, 0, rai.dimension(1), rai.dimension(1), rai.dimension(1) * 0.25, rai.dimension(1) * 0.25, rai.dimension(1) * 0.75, rai.dimension(1) * 0.75}
};
final double[][] q = {
{0, rai.dimension(0), 0, rai.dimension(0),
.dimension(0) * 0.25 + offset , rai.dimension(0) * 0.75 - offset, rai.dimension(0) * 0.25 + offset, rai.dimension(0) * 0.75 - offset},
rai{0, 0, rai.dimension(1), rai.dimension(1),
.dimension(1) * 0.25 + offset, rai.dimension(1) * 0.25 + offset, rai.dimension(1) * 0.75 - offset, rai.dimension(1) * 0.75 - offset}
rai};
final var transform = new ThinplateSplineTransform(p, q);
final var warped = new RealTransformRandomAccessible<>(interpolated, transform);
String text = """
thinplate spline transformation controls points:
| | p<sub>x</sub> | p<sub>y</sub> | q<sub>x</sub> | q<sub>y</sub> |
| --- | ---: | ---: | ---: | ---: |
""";
for (int j = 0; j < p[0].length; ++j)
+= String.format("""
text | %d | %.2f | %.2f | %.2f | %.2f |
""",
, p[0][j], p[1][j], q[0][j], q[1][j]);
j
updateDisplay(refImage, Views.interval(warped, rai), "image/jpeg");
updateDisplay(refLabel, text, "text/markdown");
Thread.sleep(100);
}
thinplate spline transformation controls points:
px | py | qx | qy | |
---|---|---|---|---|
0 | 0.00 | 0.00 | 0.00 | 0.00 |
1 | 320.00 | 0.00 | 320.00 | 0.00 |
2 | 0.00 | 200.00 | 0.00 | 200.00 |
3 | 320.00 | 200.00 | 320.00 | 200.00 |
4 | 80.00 | 50.00 | 116.00 | 86.00 |
5 | 240.00 | 50.00 | 204.00 | 86.00 |
6 | 80.00 | 150.00 | 116.00 | 114.00 |
7 | 240.00 | 150.00 | 204.00 | 114.00 |