Cris’ Image Analysis Blog

theory, methods, algorithms, applications

DIPlib 3.4.0 released

Yesterday we released DIPlib version 3.4.0. The change log is quite extensive. The improved median filter I discussed in my previous blog post is in this release, as well as a much faster built-in FFT implementation (used when not linking against FFTW), a bunch of new functionality including various iterative deconvolution algorithms, and some bug fixes.

New function forms in Python bindings

DIPib has always had two signatures for each function, one where the output image is passed as an input argument to be modified, one that returns the output image. For example, the dip::Gauss function can be called in these two ways:

dip::Image out;
dip::Gauss(img, out, {3}, {0}, "FIR");

dip::Image out = dip::Gauss(img, {3}, {0}, "FIR");

(I’m purposefully specifying more parameters here than necessary, bear with me.)

When I first created the Python bindings, PyDIP, I only created bindings for the second form, with the output image as return value. It is the more user-friendly form, and I might have been subconsciously imitating the MATLAB syntax of DIPimage. In this latest release, we’re adding bindings for the other form as well.

In Python it is not possible to have multiple signatures for one function. We use Pybind11 to create the Python bindings for DIPlib, which does allow multiple overloaded functions to be called from Python. It has an overload resolution algorithm built-in, where it attempts to convert input arguments to the types needed by the first overload, if it fails it tries the second overload, etc. But Python is not as strongly typed as C++, so it can be difficult to avoid certain type conversions from happening, making the two overloads difficult to distinguish for some functions. To avoid ambiguity, we made the output image argument a keyword-only argument. All arguments that come after must be keyword-only as well, as per Python syntax. So, whenever there is an out=xxx argument specified, we mean the one function form, and whenever that argument is not specified, we mean the other function form. This has the added benefit of making the output image argument clearly recognizable.

For example, where previously you could call dip.Gauss as

out = dip.Gauss(img, 3, 0, "FIR")

or

out = dip.Gauss(img, sigmas=3, method="FIR")

you can now also call it as

out = dip.Image()
dip.Gauss(img, out=out, sigmas=3, method="FIR")

(I mean to say sigmas and method must be given as keyword arguments in this new form).

Note, in that last call, that we didn’t need to allocate space for out, we didn’t need to worry about how large the output image would be, or what data type it would have. The DIPlib function will reallocate the output image to match its needs.

Why bother?

The reason that I wanted to add this alternate, less user-friendly form of each function, is because of the possibilities it opens up. The obvious advantage is that it allows re-using already allocated images, reducing memory requirements, and possibly speed up some computations by working in-place. For example,

dip.Abs(img, out=img)

works faster than

img = dip.Abs(img)

But the more important advantage is being able to determine the data type of the output. For this to work we need the “protect” flag (this works the same as in the DIPlib C++ library of course). The “protect” flag protects an image from being reallocated. That is, if an image is protected, its data segment cannot be freed or changed. This makes the image’s sizes and data type fixed. So one could create, for example, complex-valued image, and use that as the output:

out = img.Similar("SCOMPLEX")
out.Protect()
dip.Gauss(img, out=out, sigmas=3, method="FIR")

The image being protected, the dip.Gauss function must use it as-is for its output, and so will produce, in this case, a complex-valued result even if it would otherwise have produced a real-valued floating-point result. Note that if the protected image were to have the wrong sizes or number of tensor elements, an exception would be raised.

To make this process simpler (i.e. letting the filter function do the allocation), DIPlib has another trick: it is possible to protect a raw image (an image without a data segment). For such an image it is still possible to change its sizes and number of tensor elements, and then allocate its data segment. But all DIPlib functions will see the “protect” flag and not change the data type. This allows the following code to work as expected:

out = dip.Image()
out.SetDataType("SCOMPLEX")
out.Protect()
dip.Gauss(img, out=out, sigmas=3, method="FIR")

In-place processing

The following code might not work in place:

dip.Gauss(img, out=img, sigmas=3, method="FIR")

If img is of an integer type, the output will be adjusted to be a floating-point type, and so will still create a new output data segment. To force a function to work in place, protect the image:

img.Protect()
dip.Gauss(img, out=img, sigmas=3, method="FIR")

Some functions cannot work in place, they will always create an intermediate image to do the computation, maybe by copying the input image so that it can be used as a separate output image. Some of the functions that cannot work in place will raise an exception if the input and output images are the same. There will always be things that could be improved!

Let us know what you think of this new functionality!

Questions or comments on this topic?
Join the discussion on LinkedIn