Cris’ Image Analysis Blog

theory, methods, algorithms, applications

DIPlib 3.3.0 released

Today saw another DIPlib release, version 3.3.0. As usual, there is some new functionality, some improvements to existing functionality, and some bug fixes, see the change log for details. An increasing number of people is using the Python bindings, and I’ve been using them extensively at work now as well. And so a lot of the changes in this release are usability improvements to these bindings.

For example, it is now possible to choose (in Python) which dimension order to use. By default the order is the same as in the C++ library (and the order that makes intuitively most sense to an old-timer like me): x, y, z, … NumPy arrays are typically interpreted as the last dimension being horizontal (x), so a 3D image would be indexed as (z, y, x).

>>> img = dip.Image((30, 20, 10))
>>> img.Fill(0)
>>> img[1, 2, 3] = 100
>>> arr = np.asarray(img)
>>> arr[1, 2, 3]
0.0
>>> arr[3, 2, 1]
100.0

To me this is awkward, but people that first learned image processing with Python are used to that order. So we introduced dip.ReverseDimensions(), which reverses the order of the dimensions of images in PyDIP (in the Python binds to DIPlib, not DIPlib itself). This function is most useful when combining DIPlib with other imaging libraries such as skimage, which uses NumPy arrays to store images, and matches the array’s dimension order when specifying, for example, filter sizes (that is, filters are specified as z, y, x also).

>>> dip.ReverseDimensions()
>>> img = dip.Image((30, 20, 10))
>>> img.Fill(0)
>>> img[1, 2, 3] = 100
>>> arr = np.asarray(img)
>>> arr[1, 2, 3]
100.0
>>> arr[3, 2, 1]
0.0

It is important to call dip.ReverseDimensions() only at the beginning of a program, running it half-way could cause a lot of confusion. Also, you cannot use this within a function: it always affects the whole program, and cannot be reversed.

Because I like to find fault in OpenCV, let me point out here that OpenCV also uses NumPy arrays to store images, but the OpenCV functions take coordinates and sizes in the x, y order, meaning that it is internally inconsistent!

The biggest change this release is the introduction of the @ operator for matrix multiplication of tensor images, and the change of the * operator, which now always does element-wise multiplication.

When I created the Python bindings to DIPlib, I implemented the * operator for dip.Image objects to do the same thing as the * operator does for dip::Image objects in C++: matrix multiplication of tensor images. C++ does not have a separate operator for element-wise multiplication and matrix multiplication, as Python does. So it makes sense, in C++, to pick matrix multiplication as the meaning of the * operator. But in Python this choice was awkward. One needs to go through hoops to do an element-wise multiplication (granted, it is not needed all that often), and the meaning of the operators do not match what is expected by the user.

And so we decided to introduce this change, even though it might break existing code. Because PyDIP is still rather rough, it would be very limiting to never introduce breaking changes at this point.

Do note that both operators do exactly the same thing if one of the operands is a scalar image or a scalar constant; this change affects only code that uses matrix multiplications.

Here is an example of using the element-wise multiplication: min-max scaling of each channel of the image individually:

img -= dip.Minimum(img)
img *= 255 / dip.Maximum(img)

Here is an example of using the matrix multiplication: converting the RGB image to gray scale (obviously one would normally use dip.ColorSpaceManager for this purpose):

img = dip.Transpose(img) @ [0.3, 0.6, 0.1]

Yes, the syntax here is still a bit awkward, a list of values is always interpreted as a column vector, even if it looks more like a row vector. And we cannot use img @= [...] because the image needs to be transposed first.

For more complex examples using tensor image arithmetics, see the “Why tensors?” page in the documentation, or a blog post I wrote last year about “The Hessian and the Structure Tensor”

If you have suggestions for improving the syntax in PyDIP (or any other aspect!) please let us know by creating an issue on GitHub, or through the LinkedIn or Twitter threads linked below. Thank you!

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