Views

ImageCore provides 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 allow one to process images of immense size without making copies, and write 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 Array{ColorTypes.RGB{FixedPointNumbers.UFixed{UInt8,8}},2}:
 RGB{U8}(1.0,0.0,0.0)  RGB{U8}(0.0,1.0,0.0)
 RGB{U8}(0.0,0.0,1.0)  RGB{U8}(0.0,0.0,0.0)

RGB is described in the Colors package, and the image is just a plain 2×2 array containing red, green, blue, and black pixels. In Julia's color package, "1" means "saturated" (e.g., "full red"), and "0" means "black". In a moment you'll see that's true no matter how the information is represented internally.

As with all of Julia's arrays, you can access individual elements:

julia> img[1,2]
RGB{U8}(0.0,1.0,0.0)

One of the nice things about this representation of the image is that all of the indices in img[i,j,...] correspond to locations in the image: 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, occassionally 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 ImageCore.ChannelView{FixedPointNumbers.UFixed{UInt8,8},3,Array{ColorTypes.RGB{FixedPointNumbers.UFixed{UInt8,8}},2}}:
[:, :, 1] =
 UFixed8(1.0)  UFixed8(0.0)
 UFixed8(0.0)  UFixed8(0.0)
 UFixed8(0.0)  UFixed8(1.0)

[:, :, 2] =
 UFixed8(0.0)  UFixed8(0.0)
 UFixed8(1.0)  UFixed8(0.0)
 UFixed8(0.0)  UFixed8(0.0)

v is a 3×2×2 array of numbers (UFixed8 is defined in FixedPointNumbers and can be abbreviated as U8), where the three elements of the first dimension correspond to the red, green, and blue color channels, respectively. channelview does exactly what the name suggests: provide a view of the array using separate channels for the color components.

If you're not familiar with UFixed8, then you may find another view type, rawview, illuminating:

julia> r = rawview(v)
3×2×2 MappedArrays.MappedArray{UInt8,3,ImageCore.ChannelView{FixedPointNumbers.UFixed{UInt8,8},3,Array{ColorTypes.RGB{FixedPointNumbers.UFixed{UInt8,8}},2}},ImageCore.##11#13,ImageCore.##12#14{FixedPointNumbers.UFixed{UInt8,8}}}:
[:, :, 1] =
 0xff  0x00
 0x00  0x00
 0x00  0xff

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

This is an array of UInt8 numbers, with 0 printed as 0x00 and 255 printed as 0xff. Despite the apparent "floating point" representation of the image above, we see that it's actually represented using 8-bit unsigned integers. The UFixed8 type presents such an integer as a fixed-point number ranging from 0 to 1. As a consequence, there is no discrepancy in "meaning" between the encoding of images represented as floating point or 8-bit or 16-bit integers: 0 always means "black" and 1 always means "white" or "saturated."

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

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

julia> r
3×2×2 MappedArrays.MappedArray{UInt8,3,ImageCore.ChannelView{FixedPointNumbers.UFixed{UInt8,8},3,Array{ColorTypes.RGB{FixedPointNumbers.UFixed{UInt8,8}},2}},ImageCore.##11#13,ImageCore.##12#14{FixedPointNumbers.UFixed{UInt8,8}}}:
[:, :, 1] =
 0xff  0x00
 0x00  0x00
 0x80  0xff

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

julia> v
3×2×2 ImageCore.ChannelView{FixedPointNumbers.UFixed{UInt8,8},3,Array{ColorTypes.RGB{FixedPointNumbers.UFixed{UInt8,8}},2}}:
[:, :, 1] =
 UFixed8(1.0)    UFixed8(0.0)
 UFixed8(0.0)    UFixed8(0.0)
 UFixed8(0.502)  UFixed8(1.0)

[:, :, 2] =
 UFixed8(0.0)  UFixed8(0.0)
 UFixed8(1.0)  UFixed8(0.0)
 UFixed8(0.0)  UFixed8(0.0)

julia> img
2×2 Array{ColorTypes.RGB{FixedPointNumbers.UFixed{UInt8,8}},2}:
 RGB{U8}(1.0,0.0,0.502)  RGB{U8}(0.0,1.0,0.0)
 RGB{U8}(0.0,0.0,1.0)    RGB{U8}(0.0,0.0,0.0)

The hexidecimal representation of 128 is 0x80; this is approximately halfway to 255, and as a consequence the UFixed8 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 permuteddimsview:

julia> p = permuteddimsview(v, (2,3,1))
2×2×3 Base.PermutedDimsArrays.PermutedDimsArray{FixedPointNumbers.UFixed{UInt8,8},3,(2,3,1),(3,1,2),ImageCore.ChannelView{FixedPointNumbers.UFixed{UInt8,8},3,Array{ColorTypes.RGB{FixedPointNumbers.UFixed{UInt8,8}},2}}}:
[:, :, 1] =
 UFixed8(1.0)  UFixed8(0.0)
 UFixed8(0.0)  UFixed8(0.0)

[:, :, 2] =
 UFixed8(0.0)  UFixed8(1.0)
 UFixed8(0.0)  UFixed8(0.0)

[:, :, 3] =
 UFixed8(0.502)  UFixed8(0.0)
 UFixed8(1.0)    UFixed8(0.0)

julia> p[1,2,:] = 0.25
0.25

julia> p
2×2×3 Base.PermutedDimsArrays.PermutedDimsArray{FixedPointNumbers.UFixed{UInt8,8},3,(2,3,1),(3,1,2),ImageCore.ChannelView{FixedPointNumbers.UFixed{UInt8,8},3,Array{ColorTypes.RGB{FixedPointNumbers.UFixed{UInt8,8}},2}}}:
[:, :, 1] =
 UFixed8(1.0)  UFixed8(0.251)
 UFixed8(0.0)  UFixed8(0.0)

[:, :, 2] =
 UFixed8(0.0)  UFixed8(0.251)
 UFixed8(0.0)  UFixed8(0.0)

[:, :, 3] =
 UFixed8(0.502)  UFixed8(0.251)
 UFixed8(1.0)    UFixed8(0.0)

julia> v
3×2×2 ImageCore.ChannelView{FixedPointNumbers.UFixed{UInt8,8},3,Array{ColorTypes.RGB{FixedPointNumbers.UFixed{UInt8,8}},2}}:
[:, :, 1] =
 UFixed8(1.0)    UFixed8(0.0)
 UFixed8(0.0)    UFixed8(0.0)
 UFixed8(0.502)  UFixed8(1.0)

[:, :, 2] =
 UFixed8(0.251)  UFixed8(0.0)
 UFixed8(0.251)  UFixed8(0.0)
 UFixed8(0.251)  UFixed8(0.0)

julia> img
2×2 Array{ColorTypes.RGB{FixedPointNumbers.UFixed{UInt8,8}},2}:
 RGB{U8}(1.0,0.0,0.502)  RGB{U8}(0.251,0.251,0.251)
 RGB{U8}(0.0,0.0,1.0)    RGB{U8}(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. In conjunction with colorview, this can be used to combine two or three grayscale images into single color image. 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
s = StackedView(cv[1,:,:], zeroarray, cv[3,:,:])

julia> size(s)
(3,512,768)

sc = colorview(RGB, s)

Within the context of StackedView, zeroarray stands for an all-zeros array of size that matches the other arguments of StackedView.

sc 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 StackedView lets you link two images that might have come from different sources, "stacking" them along the first dimension (which is readily reinterpreted as a color channel).