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)