Cris’ Image Analysis Blog

theory, methods, algorithms, applications

DIPimage issues that I can't solve

Edit December 2018

Over the past two years I’ve been working on DIPimage 3, an updated version of DIPimage that is based on a rewritten DIPlib. Just about all of the issues discussed in this blog post have been resolved there, with the exception of the end operator, which will now do the wrong thing if used in curly braces (tensor indexing), and the limitation to the order of spatial and tensor indexing. We incurred a few backwards compatibility breaks, but it was necessary for the long-term health of the toolbox.

There are a few things in DIPimage that I’m annoyed with but can’t solve. Some of these are caused by the limitations of the MATLAB interpreter, and some are caused by my own poor design choices. DIPimage evolved over time to do things that I hadn’t at first even thought about, such as processing color images. Tensor and color images were “tagged on”, if you will, on top of the dip_image object, introducing some oddities and inconsistencies. To fix these up we would need to change existing behavior, which we don’t want to do because it would break too much existing code. This blog entry is about the few weird things with the dip_image object and how to work around them.

The dip_image object was born to contain only a single grey-value object. Pretty soon it was extended to contain multiple, unrelated images (mirroring DIPlib’s dip_ImageArray structure). It seemed logical to me to distinguish a single image and an array of images, even though to MATLAB they are the same object, so I overloaded the isa and class methods to make this distinction artificially. When there is more than one image in the dip_image object, it reports itself as an object of class dip_image_array. This caused a few problems when we introduced tensor (and color) images. As they say, hindsight is always 20-20. Now I wish I had implemented image arrays as a separate class, or even simply as a cell array with dip_image objects.

Indexing into a dip_image or a dip_image_array

Indexing into a dip_image_array is done using braces: A{1}; indexing into a dip_image is done using brackets: A(20). Because of a limitation of the MATLAB parser, it is only possible to combine these two indices by putting the braces before the brackets: A{1}(20). This is not so bad if you consider the images in the array to be unrelated: first you select image number 1, then you select pixel 20 in that image. But when we introduced the concept of tensor images, this limitation became frustrating. In a tensor image each pixel is a matrix. This is represented as a dip_image_array where each image has the same size, and contains the data for one of the tensor elements. For example, an RGB image is a tensor image with a 3-element vector at each pixel. The first image in the dip_image_array is the red channel, the second is green, the third is blue. In such an image, A(20) is a tensor (in this example an RGB triplet). It would sometimes make more sense to index a tensor element by A(20){1} rather than A{1}(20). But it generates an error when you type it in, and there’s nothing I can do about that.

Methods overloaded for dip_image and dip_image_array objects

Even worse is the way that the functions size, length, ndims, numel and end work. When the object is of type dip_image, they work on the image, and when the object is of type dip_image_array they work on the array, not the images inside. This made a little bit of sense initially, since it was assumed that the images inside the array were unrelated. But with a color image A:

size(A)
B = colorspace(A,'grey');
size(B)
ans =
     [3,1]

ans =
     [256,256]

In the same way, size(A{1}) would return [256,256] and size(A{1:2}) would return [2,1]. Needless to say, this has lead to some hard-to-find bugs.

It is possible to get around this issue by using only the functions imsize and imarsize for the image size and the image array size, respectively. length(A) should be written as max(imsize(A)), and ndims(A) as length(imsize(A)). numel always returns 1. This was implemented to circumvent a bug in MATLAB 6 that broke the indexing (I don’t know if this was fixed in later versions). numel(A) should be written as prod(imsize(A)).

Finally, the end operator is the worst of the bunch, because I keep using it without thinking about it, and it always takes me a while to find the cause of the bug. Also, the workaround is rather ugly. Like size, end uses the image size if there is only one image in the object, but uses the array size if there is more than one image. Thus, A(0:2:end,:) works perfectly well when A is a grey-value image, but doesn’t do what you’d expect if A is a tensor image. The workaround again is using imsize: A(0:2:imsize(A,1)-1,:). Ugly, no? The reason for this issue is that MATLAB does not report to the overloaded end method whether it is invoked in brackets or braces. The end method existed before cell arrays and braces indexing were introduced in MATLAB. It seems that MATLAB itself also has problems caused by evolution of the software and the desire to keep backwards compatibility! Because the end method does not know the context in which it is called, it has to guess what it is that the user wants, just like size and length have to do. The decision to have all these functions make the same assumptions is sound, but if we would have thought about tensor and color images before implementing these, they might have always looked only at the image dimensions and never at the array dimensions.

Concatenating dip_image and dip_image_array objects

The last issue I have is with concatenation of images. Again, this was implemented before we even thought of the possibility of using color images, and image arrays usually contained unrelated images. When concatenating an image array, it is treated like one wishes to concatenate the images in the array. Therefore, [A,B], with A an image array, is equivalent to [A{1},A{2},...,A{end},B]. This is very frustrating when you have two color images that you wish to concatenate together. The workaround is, as usual with color images, to use iterate.

iterate repeats one operation on every image in an image array. This is often the only simple way of applying a filter to an image. Bit by bit we are improving functions so that they work on tensor images. For example, gaussf(A) now works as expected on a color image. But many functions do not work yet, and many will never work (for example, mathematical morphology on tensor images can not be defined properly, because there is no universally correct way to decide which of a set of tensors is the largest). For these functions it is possible to use iterate to apply them to each of the components of a color image: iterate('dilation',A,25,'rectangular'). Note that the result contains colors not in the input image: this operation is not a dilation!

iterate is clever enough that, when it is called with multiple image arrays as input, it applies the function with corresponding images from each of the arrays. Thus, to concatenate two color images, one can do iterate('cat',1,A,B).

A better design

If we had thought about color images from the start, we would have created a dip_image object that can hold a grey-value, tensor or color image, but never unrelated images. An image array would be either a cell array with images or a completely separate object. In that way it would be possible to have an array with one image, which now is impossible. The size and related functions would know whether the input is an image (whatever the type) or an image array. Concatenation would be able to do different things on an image array and on a tensor image. However, the end operator would still be incomplete, only usable within brackets but not braces, and the limitation to the order of brackets and braces would still stand. We would also have to introduce methods to return the size, length and number of dimensions of the tensor.

But it is too late for this type of change, I feel. I’m hoping this post will help people understand the design choices made, and help them get around the limitations and oddities of the toolbox as it stands now.