ImageAxes.jl

While images can often be represented as plain Arrays, sometimes additional information about the "meaning" of each axis of the array is needed. For example, in a 3-dimensional MRI scan, the voxels may not have the same spacing along the z-axis that they do along the x- and y-axes, and this fact should be accounted for during the display and/or analysis of such images. Likewise, a movie has two spatial axes and one temporal axis; this fact may be relevant for how one performs image processing.

The ImageAxes package (which is incorporated into Images) combines features from AxisArrays and SimpleTraits to provide a convenient representation and programming paradigm for dealing with such images.

Installation

If you want to directly use ImageAxes, add it via the package manager.

Usage

Names and locations

The simplest thing you can do is to provide names to your image axes:

using ImageAxes
img = AxisArray(reshape(1:192, (8,8,3)), :x, :y, :z)
3-dimensional AxisArray{Int64,3,...} with axes:
    :x, Base.OneTo(8)
    :y, Base.OneTo(8)
    :z, Base.OneTo(3)
And data, a 8×8×3 reshape(::UnitRange{Int64}, 8, 8, 3) with eltype Int64:
[:, :, 1] =
 1   9  17  25  33  41  49  57
 2  10  18  26  34  42  50  58
 3  11  19  27  35  43  51  59
 4  12  20  28  36  44  52  60
 5  13  21  29  37  45  53  61
 6  14  22  30  38  46  54  62
 7  15  23  31  39  47  55  63
 8  16  24  32  40  48  56  64

[:, :, 2] =
 65  73  81  89   97  105  113  121
 66  74  82  90   98  106  114  122
 67  75  83  91   99  107  115  123
 68  76  84  92  100  108  116  124
 69  77  85  93  101  109  117  125
 70  78  86  94  102  110  118  126
 71  79  87  95  103  111  119  127
 72  80  88  96  104  112  120  128

[:, :, 3] =
 129  137  145  153  161  169  177  185
 130  138  146  154  162  170  178  186
 131  139  147  155  163  171  179  187
 132  140  148  156  164  172  180  188
 133  141  149  157  165  173  181  189
 134  142  150  158  166  174  182  190
 135  143  151  159  167  175  183  191
 136  144  152  160  168  176  184  192

As described in more detail in the AxisArrays documentation, you can now take slices like this:

slz = img[Axis{:z}(2)]
2-dimensional AxisArray{Int64,2,...} with axes:
    :x, Base.OneTo(8)
    :y, Base.OneTo(8)
And data, a 8×8 Matrix{Int64}:
 65  73  81  89   97  105  113  121
 66  74  82  90   98  106  114  122
 67  75  83  91   99  107  115  123
 68  76  84  92  100  108  116  124
 69  77  85  93  101  109  117  125
 70  78  86  94  102  110  118  126
 71  79  87  95  103  111  119  127
 72  80  88  96  104  112  120  128
slx = img[Axis{:x}(2)]
2-dimensional AxisArray{Int64,2,...} with axes:
    :y, Base.OneTo(8)
    :z, Base.OneTo(3)
And data, a 8×3 Matrix{Int64}:
  2   66  130
 10   74  138
 18   82  146
 26   90  154
 34   98  162
 42  106  170
 50  114  178
 58  122  186
sly = img[Axis{:y}(2)]
2-dimensional AxisArray{Int64,2,...} with axes:
    :x, Base.OneTo(8)
    :z, Base.OneTo(3)
And data, a 8×3 Matrix{Int64}:
  9  73  137
 10  74  138
 11  75  139
 12  76  140
 13  77  141
 14  78  142
 15  79  143
 16  80  144

You can also give units to the axes:

using ImageAxes, Unitful
const mm = u"mm"
img = AxisArray(reshape(1:192, (8,8,3)),
                Axis{:x}(1mm:1mm:8mm),
                Axis{:y}(1mm:1mm:8mm),
                Axis{:z}(2mm:3mm:8mm))
3-dimensional AxisArray{Int64,3,...} with axes:
    :x, (1:8) mm
    :y, (1:8) mm
    :z, (2:3:8) mm
And data, a 8×8×3 reshape(::UnitRange{Int64}, 8, 8, 3) with eltype Int64:
[:, :, 1] =
 1   9  17  25  33  41  49  57
 2  10  18  26  34  42  50  58
 3  11  19  27  35  43  51  59
 4  12  20  28  36  44  52  60
 5  13  21  29  37  45  53  61
 6  14  22  30  38  46  54  62
 7  15  23  31  39  47  55  63
 8  16  24  32  40  48  56  64

[:, :, 2] =
 65  73  81  89   97  105  113  121
 66  74  82  90   98  106  114  122
 67  75  83  91   99  107  115  123
 68  76  84  92  100  108  116  124
 69  77  85  93  101  109  117  125
 70  78  86  94  102  110  118  126
 71  79  87  95  103  111  119  127
 72  80  88  96  104  112  120  128

[:, :, 3] =
 129  137  145  153  161  169  177  185
 130  138  146  154  162  170  178  186
 131  139  147  155  163  171  179  187
 132  140  148  156  164  172  180  188
 133  141  149  157  165  173  181  189
 134  142  150  158  166  174  182  190
 135  143  151  159  167  175  183  191
 136  144  152  160  168  176  184  192

which specifies that x and y have spacing of 1mm and z has a spacing of 3mm, as well as the location of the center of each voxel.

Temporal axes

Any array possessing an axis Axis{:time} will be recognized as having a temporal dimension. Given an array A,

using ImageAxes, Unitful
const s = u"s"
img = AxisArray(reshape(1:9*300, (3,3,300)),
                Axis{:x}(1:3),
                Axis{:y}(1:3),
                Axis{:time}(1s/30:1s/30:10s))
3-dimensional AxisArray{Int64,3,...} with axes:
    :x, 1:3
    :y, 1:3
    :time, (0.03333333333333333:0.03333333333333333:10.0) s
And data, a 3×3×300 reshape(::UnitRange{Int64}, 3, 3, 300) with eltype Int64:
[:, :, 1] =
 1  4  7
 2  5  8
 3  6  9

[:, :, 2] =
 10  13  16
 11  14  17
 12  15  18

[:, :, 3] =
 19  22  25
 20  23  26
 21  24  27

;;; … 

[:, :, 298] =
 2674  2677  2680
 2675  2678  2681
 2676  2679  2682

[:, :, 299] =
 2683  2686  2689
 2684  2687  2690
 2685  2688  2691

[:, :, 300] =
 2692  2695  2698
 2693  2696  2699
 2694  2697  2700

you can retrieve its temporal axis with

ax = timeaxis(img)
Axis{:time, StepRangeLen{Unitful.Quantity{Float64, 𝐓, Unitful.FreeUnits{(s,), 𝐓, nothing}}, Base.TwicePrecision{Unitful.Quantity{Float64, 𝐓, Unitful.FreeUnits{(s,), 𝐓, nothing}}}, Base.TwicePrecision{Unitful.Quantity{Float64, 𝐓, Unitful.FreeUnits{(s,), 𝐓, nothing}}}, Int64}}((0.03333333333333333:0.03333333333333333:10.0) s)

and index it like

img[ax(4)]  # returns the 4th "timeslice"
2-dimensional AxisArray{Int64,2,...} with axes:
    :x, 1:3
    :y, 1:3
And data, a 3×3 Matrix{Int64}:
 28  31  34
 29  32  35
 30  33  36

You can also specialize methods like this:

using ImageAxes, SimpleTraits
@traitfn nimages(img::AA) where {AA<:AxisArray;  HasTimeAxis{AA}} = length(timeaxis(img))
@traitfn nimages(img::AA) where {AA<:AxisArray; !HasTimeAxis{AA}} = 1
nimages (generic function with 3 methods)

where the pre-defined HasTimeAxis trait will restrict that method to arrays that have a timeaxis. A more complex example is

using ImageAxes, SimpleTraits, Statistics

@traitfn meanintensity(img::AA) where {AA<:AxisArray; !HasTimeAxis{AA}} = mean(img)
@traitfn function meanintensity(img::AA) where {AA<:AxisArray; HasTimeAxis{AA}}
    ax = timeaxis(img)
    n = length(ax)
    intensity = zeros(eltype(img), n)
    for ti in 1:n
        sl = view(img, ax(ti))
        intensity[ti] = mean(sl)
    end
    intensity
end

and, when appropriate, it will return the mean intensity at each timeslice.

Custom temporal axes

Using SimpleTraits's @traitimpl, you can add Axis{:t} or Axis{:scantime} or any other name to the list of axes that have a temporal dimension:

using ImageAxes, SimpleTraits
@traitimpl TimeAxis{Axis{:t}}

Note this declaration affects all arrays throughout your entire session. Moreover, it should be made before calling any functions on array-types that possess such axes; a convenient place to do this is right after you say using ImageAxes in your top-level script.