Interactively render the Juliaset as a lambda function in BigDataViewer.
imglib2
lambda
fractal
juliaset
bigdataviewer
Author
Stephan Saalfeld
Published
May 2, 2022
Import dependencies. BigDataViewer Vistools is the convenience API that we use to display the results of our experiments and it includes all ImgLib2 related dependencies that we need. We also import the ImgLib2 bridge to ImageJ, because we want to use ImageJ’s API to display ImgLib2 data in this notebook.
// register renderer for ImgLib2 data and arraysgetKernelInstance().getRenderer().createRegistration(RandomAccessibleInterval.class).preferring(MIMEType.IMAGE_PNG).supporting(MIMEType.IMAGE_JPEG, MIMEType.IMAGE_GIF).register((rai, context)->Image.renderImage( ImageJFunctions.wrap(rai, rai.toString()).getBufferedImage(), context));getKernelInstance().getRenderer().createRegistration(RandomAccessible.class).preferring(MIMEType.IMAGE_PNG).supporting(MIMEType.IMAGE_JPEG, MIMEType.IMAGE_GIF).register((ra, context)->Image.renderImage( ImageJFunctions.wrap( Views.interval( ra,newFinalInterval(Arrays.copyOf(newlong[]{512,512}, ra.numDimensions()))), ra.toString()).getBufferedImage(), context));getKernelInstance().getRenderer().createRegistration(RealRandomAccessible.class).preferring(MIMEType.IMAGE_PNG).supporting(MIMEType.IMAGE_JPEG, MIMEType.IMAGE_GIF).register((rra, context)->Image.renderImage( ImageJFunctions.wrap( Views.interval( Views.raster(rra),newFinalInterval(Arrays.copyOf(newlong[]{512,512}, rra.numDimensions()))), rra.toString()).getBufferedImage(), context));
We define the Juliaset as a function in 2D real space using a BiConsumer lambda. The BiConsumer receives two parameters, the first one (x) is the 2D coordinate, the second one (y) is the target of the function whose value will be set in place, here we use an IntType. We also have to provide a Supplier for instances of the target such that multiple threads can each create their own.
Code
var juliaset =new FunctionRealRandomAccessible<>(2,(x, fx)->{int i =0;double v =0, c = x.getDoublePosition(0), d = x.getDoublePosition(1);for(; i <255&& v <4096;++i){finaldouble e = c * c - d * d; d =2* c * d; c = e +0.3; d +=0.6; v =Math.sqrt(c * c + d * d);++i;} fx.set(i);}, UnsignedByteType::new);
Now we show this function with BigDataViewer. Use your mouse and keyboard to zoom in until you reach the precision limit of double.
var transform =newScaleAndTranslation(newdouble[]{255,255},newdouble[]{255,255});var transformed = RealViews.affineReal(juliaset, transform);transformed;
Cool? I think so! Now let’s embed one of the two parameters of the Juliaset as a third dimension. The code is almost the same except that we introduce a a variable a.
Code
var juliaset3 =new FunctionRealRandomAccessible<>(3,(x, fx)->{int i =0;double v =0, c = x.getDoublePosition(0), d = x.getDoublePosition(1), a = x.getDoublePosition(2);for(; i <255&& v <4096;++i){finaldouble e = c * c - d * d; d =2* c * d; c = e + a; d +=0.6; v =Math.sqrt(c * c + d * d);++i;} fx.set(i);}, IntType::new);
And now we show this as a 3D volume in BigDataViewer. You can scroll through the ‘z’-dimension or arbitrarily slice through the 3D volume.
This was using a stateless function, i.e. each RealRandomAccess uses the same instance. If you have stateful functions, you want to use a function provider. Spot the difference:
Code
finalvar rnd =newRandom();var juliaset3Stripes =new FunctionRealRandomAccessible<>(3,()->{finalint offset = rnd.nextInt(100);return(x, fx)->{int i =0;double v =0, c = x.getDoublePosition(0), d = x.getDoublePosition(1), a = x.getDoublePosition(2);for(; i <255&& v <4096;++i){finaldouble e = c * c - d * d; d =2* c * d; c = e + a; d +=0.6; v =Math.sqrt(c * c + d * d);++i;} fx.set(i + offset);};}, IntType::new);
We see here, how BigDataViewer uses multiple threads to render each frame, and each thread creates its own independent RealRandomAccess to ‘access the pixels’ (the generator function generates them on the fly). To visualize this, we add a random number to the pixel values that the generator produces. This state is unique to the generator of each RealRandomAccess and that allows us to see the regions in the output image that are generated by each thread.