Views

View types defined in ImageCore

It is quite possible that the default representation of images will satisfy most or all of your needs. However, to enhance flexibility in working with image data, it is possible to leverage several different kinds of "views." Generically, a view is an interpretation of array data, one that may change the apparent meaning of the array but which shares the same underlying storage: change an element of the view, and you also change the original array. Views can facilitate processing images of immense size without making copies, and writing algorithms in the most convenient format often without having to worry about the potential cost of converting from one format to another.

To illustrate views, it's helpful to begin with a very simple image:

julia> using Colors

julia> img = [RGB(1,0,0) RGB(0,1,0);
              RGB(0,0,1) RGB(0,0,0)]
2×2 Matrix{RGB{FixedPointNumbers.N0f8}}:
 RGB(1.0, 0.0, 0.0)  RGB(0.0, 1.0, 0.0)
 RGB(0.0, 0.0, 1.0)  RGB(0.0, 0.0, 0.0)

which displays as

rgbk

Most commonly, it's convenient that all dimensions of this array correspond to pixel indices: you don't need to worry about some dimensions of the array corresponding to "color channels" and other the spatial location, and you're guaranteed to get the entire pixel contents when you access that location.

That said, occasionally there are reasons to want to treat RGB as a 3-component vector. That's motivation for introducing our first view:

julia> v = channelview(img)
3×2×2 reinterpret(reshape, N0f8, ::Matrix{RGB{N0f8}}) with eltype N0f8:
[:, :, 1] =
 1.0  0.0
 0.0  0.0
 0.0  1.0

[:, :, 2] =
 0.0  0.0
 1.0  0.0
 0.0  0.0

channelview does exactly what the name suggests: provide a view of the array using separate channels for the color components.

To access the underlying representation of the N0f8 numbers, there's another view called rawview:

julia> r = rawview(v)
3×2×2 rawview(reinterpret(reshape, N0f8, ::Matrix{RGB{N0f8}})) with eltype UInt8:
[:, :, 1] =
 0xff  0x00
 0x00  0x00
 0x00  0xff

[:, :, 2] =
 0x00  0x00
 0xff  0x00
 0x00  0x00

Let's make a change in one of the entries:

julia> r[3,1,1] = 128
128

If we display img, now we get this:

mgbk

You can see that the first pixel has taken on a magenta hue, which is a mixture of red and blue. Why does this happen? Let's look at the array values themselves:

julia> r
3×2×2 rawview(reinterpret(reshape, N0f8, ::Matrix{RGB{N0f8}})) with eltype UInt8:
[:, :, 1] =
 0xff  0x00
 0x00  0x00
 0x80  0xff

[:, :, 2] =
 0x00  0x00
 0xff  0x00
 0x00  0x00

julia> v
3×2×2 reinterpret(reshape, N0f8, ::Matrix{RGB{N0f8}}) with eltype N0f8:
[:, :, 1] =
 1.0    0.0
 0.0    0.0
 0.502  1.0

[:, :, 2] =
 0.0  0.0
 1.0  0.0
 0.0  0.0

julia> img
2×2 Matrix{RGB{N0f8}}:
 RGB(1.0, 0.0, 0.502)  RGB(0.0, 1.0, 0.0)
 RGB(0.0, 0.0, 1.0)    RGB(0.0, 0.0, 0.0)

The hexadecimal representation of 128 is 0x80; this is approximately halfway to 255, and as a consequence the N0f8 representation is very near 0.5. You can see the same change is reflected in r, v, and img: there is only one underlying array, img, and the two views simply reference it.

Maybe you're used to having the color channel be the last dimension, rather than the first. We can achieve that using PermutedDimsArray:

julia> p = PermutedDimsArray(v, (2,3,1))
2×2×3 PermutedDimsArray(reinterpret(reshape, N0f8, ::Matrix{RGB{N0f8}}), (2, 3, 1)) with eltype N0f8:
[:, :, 1] =
 1.0  0.0
 0.0  0.0

[:, :, 2] =
 0.0  1.0
 0.0  0.0

[:, :, 3] =
 0.502  0.0
 1.0    0.0

julia> p[1,2,:] .= 0.25
3-element view(PermutedDimsArray(reinterpret(reshape, N0f8, ::Matrix{RGB{N0f8}}), (2, 3, 1)), 1, 2, :) with eltype N0f8:
 0.251N0f8
 0.251N0f8
 0.251N0f8

julia> p
2×2×3 PermutedDimsArray(reinterpret(reshape, N0f8, ::Matrix{RGB{N0f8}}), (2, 3, 1)) with eltype N0f8:
[:, :, 1] =
 1.0  0.251
 0.0  0.0

[:, :, 2] =
 0.0  0.251
 0.0  0.0

[:, :, 3] =
 0.502  0.251
 1.0    0.0

julia> v
3×2×2 reinterpret(reshape, N0f8, ::Matrix{RGB{N0f8}}) with eltype N0f8:
[:, :, 1] =
 1.0    0.0
 0.0    0.0
 0.502  1.0

[:, :, 2] =
 0.251  0.0
 0.251  0.0
 0.251  0.0

julia> img
2×2 Matrix{RGB{N0f8}}:
 RGB(1.0, 0.0, 0.502)  RGB(0.251, 0.251, 0.251)
 RGB(0.0, 0.0, 1.0)    RGB(0.0, 0.0, 0.0)

Once again, p is a view, and as a consequence changing it leads to changes in all the coupled arrays and views.

Finally, you can combine multiple arrays into a "virtual" multichannel array. We'll use the lighthouse image:

using ImageCore, TestImages, Colors
img = testimage("lighthouse")
# Split out into separate channels
cv = channelview(img)
# Recombine the channels, filling in 0 for the middle (green) channel
rb = colorview(RGB, cv[1,:,:], zeroarray, cv[3,:,:])
Example block output

zeroarray is a constant which serves as a placeholder to create a (virtual) all-zeros array of size that matches the other arguments.

rb looks like this:

redblue

In this case, we could have done the same thing somewhat more simply with cv[2,:,:] .= 0 and then visualize img. However, more generally you can apply this to independent arrays which may not allow you to set values to 0. In IJulia,

linspace1

The error comes from the fact that img1d does not store values separately from the LinSpace objects used to create it, and LinSpace (which uses a compact representation of a range, storing just the endpoints and the number of values) does not allow you to set specific values. However, if you need to set individual values, you can make a copy:

linspace2

The fact that no storage is allocated by colorview is very convenient in certain situations, particularly when processing large images.

colorview's ability to combine multiple grayscale images is based on another view, StackedView, which you can also use directly.