Axes in impy

Basics

Understanding the concept of axes is important to use multi-dimensional arrays.

Each dimension of an array has its own meaning. A 3-D array can be composed of three spatial axes or two spatial axes and a time axis.

You can create a ImgArray with any axes you want using axes argument.

import impy as ip
img = ip.zeros((10, 128, 128), axes=["z", "y", "x"])

Any iterable objects can be used. In general, each axis label is represented by a single character. In this case, it’s simpler to use a str because a str itself is an iterable of str.

img = ip.zeros((10, 128, 128), axes="zyx")
If you give axes of wrong length, or axes containing the same charactor, array

creation will fail.

img = ip.zeros((10, 128, 128), axes="yx")  # Error!!
img = ip.zeros((10, 128, 128), axes="tzyx")  # Error!!

Each axis is a Axis object. They are available by indexing Axes object.

img.axes  # Axes['z', 'y', 'x']
img.axes[0]  # Axis['z']
img.axes["y"]  # Axis['y']
img.axes["a"]  # Error!

Undefined Axis

Some functions and operations, such as ravel() and reshape(), creates arrays with unknown axes. In this case, UndefAxis objects are used for these axes and are represented by "#".

img.axes  # Axes['z', 'y', 'x']
np.expand_dims(img, axis=0).axes  # Axes['#', 'z', 'y', 'x']
img[np.newaxis].axes  # Axes['#', 'z', 'y', 'x']
img[img>0].axes  # Axes['#']
img[[1, 2, 3], [2, 3, 4]].axes  # Axes['#', 'x']
img.ravel().axes  # Axes["#"]

Axis Metadata

Each axis could be tagged with some metadata. The major ones are physical scale, physical scale unit and labels.

img.axes[0].scale  # scale of the first axis
img.axes[0].unit  # scale unit of the first axis
img.axes["c"].labels  # e.g. ("Red", "Green")
img.axes[0].scale = 0.21
img.axes[0].unit = "µm"

Physical scale

Physical scale is the length of value between a[i] and a[i+1]. In image analysis, this value is usually represented as “µm/pixel” or “nm/pixel” for spatial axes and “sec” for time axis.

img.axes[0].scale  # scale of the first axis
img.axes["x"].scale  # scale of x-axis
img.axes[0].scale = 0.21  # update the scale of the first axis

You can refer to the scale unit with unit property.

img.axes[0].unit  # scale unit of the first axis
img.axes[0].unit = "µm"  # update the scale unit

Since these values are tagged to Axis objects, they will be inherited after slicing, filtering or any other operations.

img[0].axes["x"].scale == img.axes["x"].scale  # True
img.gaussian_filter(sigma=1.0).axes["x"].scale == img.axes["x"].scale  # True
(img + 1).axes["x"].scale == img.axes["x"].scale  # True
np.mean(img, axis=0).axes["x"].scale == img.axes["x"].scale  # True

It is not always the case if you called certain functions that will change scales.

img[::2].axes[0].scale == img.axes[0].scale * 2  # True
img[::-3].axes[0].scale == img.axes[0].scale * 3  # True
img.binning(3) == img.axes[0].scale * 3

Axis Labels

Sometimes an axis is tagged with “labels” that explains what each slice means. Axis object retains labels information and can be referred to as a tuple.

assert img.shape["t"] == 4  # say the length of t-axis is 4
img.axes["t"].labels = ["0 sec", "10 sec", "30 sec", "1 min"]
img.axes["t"].labels == ("0 sec", "10 sec", "30 sec", "1 min")

Because the length of labels must match corresponding shape of an array, it is safer to use set_axis_label method. It checks the new labels.

img.set_axis_label(t=["0 sec", "10 sec", "30 sec", "1 min"])
img.set_axis_label(t=["wrong", "input"])  # Error!

When array is sliced, labels are also correctly inherited

img.set_axis_label(t=["0 sec", "10 sec", "30 sec", "1 min"])
img["t=:2"].axes["t"].labels == ("0 sec", "10 sec")  # True
img["t=1,3"].axes["t"].labels == ("10 sec", "1 min")  # True

Practical Usage of Axes

Slicing and Formatting

Axes object is very useful in slicing multi-dimensional arrays.

Axis-targeted slicing

As shown in tutorial, the easiest way to slice an array is to use axis axis-targeted slicing.

img["t=1"]
img["t=3:5"]

This slicing method, however, ignores Python type-checking a little bit since you’ll not notice any wrong slicing grammar in the string until you run the code.

impy also support a Slicer object for safer axis-targeted slicing.

ip.slicer.t[2].x[4:6]
Slicer of
    t ==> 2
    x ==> 4:6

A Slicer object can be used for indexing an axis-implemented array.

img[ip.slicer.t[1]]  # equivalent to img["t=1"]
img[ip.slicer.t[3:5]]  # equivalent to img["t=3:5"]
img[ip.slicer.t[2, 4, 6]]  # equivalent to img["t=2,4,6"]
img[ip.slicer.t[2].x[4]]  # equivalent to img["t=1;x=4"]

Slice Formatting

Sometimes you would slice many times at the same axes.

img[ip.slicer.z[0].t[2]].gaussian_filter(1.0)
img[ip.slicer.z[1].t[1]].gaussian_filter(1.5)
img[ip.slicer.z[2].t[0]].gaussian_filter(1.0)

In this case, you can format slices using get_formatter method.

fmt = ip.slicer.get_formatter("zt")
fmt
SliceFormatter of
    z ==> Undefined
    t ==> Undefined
fmt[0, 2]
Slicer of
    z ==> 0
    t ==> 2

Thus, you’ll code will be

img[fmt[0, 2]].gaussian_filter(1.0)
img[fmt[1, 1]].gaussian_filter(1.5)
img[fmt[2, 0]].gaussian_filter(1.0)

Broadcasting

By using axes information, arrays can be broadcasted in a more flexible but strict way.

  • Examples

    img0 = ip.random.random((12, 10, 14), axes="zyx")
    img1 = ip.random.random((12, 14), axes="zx")
    
    np.asarray(img0) + np.asarray(img1)  # ValueError
    img0 + img1  # OK!
    
    img = ip.random.random((12, 12, 12), axes="tyx")
    img0 = np.mean(img, axis="y")  # axes: 't', 'x'
    img1 = np.mean(img, axis="x")  # axes: 't', 'y'
    np.asarray(img0) + np.asarray(img1)  # No error, but they should not be added!
    img0 + img1  # Error!
    

impy also has a broadcast_arrays function for broadcasting arrays as flexible as possible.

  • Examples

    x = ip.arange(10, axes="x")
    y = ip.arange(8, axes="y")
    out = ip.broadcast_arrays(y, x)
    out[0].shape  # AxesShape(y=8, x=10)
    out[1].shape  # AxesShape(y=8, x=10)
    
    x = ip.random.random((5, 6, 7), axes="tzx")
    y = ip.random.random((4, 5, 7), axes="ntx")
    out = ip.broadcast_arrays(y, x)
    out[0].shape  # AxesShape(n=4, t=5, z=6, x=7)
    out[1].shape  # AxesShape(n=4, t=5, z=6, x=7)