Layer Compositing

Source code Author Update time

This demonstration shows how the layer version alpha compositing can be done in 10 lines of code using OffsetArrays and PaddedViews. If you are looking for more well-organized utilities, ColorBlendModes provides the definitions and compositing operations of the blend modes.

using Images
using OffsetArrays # provide `OffsetArray`

1. basic offset and pad operation

Assume that we have two objects with transparent color and we want to place them on a canvas with partially overlapping.

red_patch = fill(RGBA(1., 0., 0., 1), 24, 24)
green_patch = fill(RGBA(0., 1., 0., 1), 32, 32)
mosaicview(red_patch, green_patch; npad=20, nrow=1, fillvalue=colorant"white")

Assume the canvas axes starts from (1, 1), here we keep the red patch unshifted, and shift the green patch 6 pixels downward and 16 pixels rightward, and then pad them to a common axes

green_o = OffsetArray(green_patch, 6, 16)
r, g = paddedviews(Gray(0.2), red_patch, green_o)
mosaicview(r, g; npad=20, nrow=1, fillvalue=colorant"white")

Note that their axes and sizes are changed after shifting and padding:

# Regardless of implementaion details, `Base.OneTo(32)` is mostly equivalent to `1:32`
println("before shifting -- size: ", size(green_patch), " axes: ", axes(green_patch))
println("after shifting  -- size: ", size(green_o), " axes: ", axes(green_o))
println("after padding   -- size: ", size(g), " axes: ", axes(g))
before shifting -- size: (32, 32) axes: (Base.OneTo(32), Base.OneTo(32))
after shifting  -- size: (32, 32) axes: (OffsetArrays.IdOffsetRange(values=7:38, indices=7:38), OffsetArrays.IdOffsetRange(values=17:48, indices=17:48))
after padding   -- size: (38, 48) axes: (Base.IdentityUnitRange(1:38), Base.IdentityUnitRange(1:48))

Axes are preserved after padding, which means you can easily get original image from padded results using

r[axes(red_patch)...]
g[axes(green_o)...]

As described here, there are several compositing methods:

# add operation
out_add = r .+ g

# clear operation
out_clear = copy(r)
out_clear[axes(green_o)...] .= colorant"black"

# multiply operation
out_mul = copy(r)
# channel-wise multiplication
channelview(out_mul)[:, axes(green_o)...] .*= channelview(green_o)

# overlap operation
out_over = copy(r)
out_over[axes(green_o)...] .= green_o

# display the results of these operation
mosaicview(out_add, out_clear, out_mul, out_over;
           npad=20, nrow=1, fillvalue=colorant"white")

2. build the three-primary color panel

Now, let's use the same trick to build something more meaningful. First we create three circles with colors red, green and blue

using ImageDraw
function make_circle(sz, c::T) where T
    # fill with transparent color to avoid black region
    fillvalue = ARGB(c)
    img = fill(ARGB{eltype(T)}(0., 0., 0., 0.), sz...)
    origin = sz .÷ 2
    r = sz .÷ 4
    draw!(img, Ellipse(origin..., r...), fillvalue)
    img
end

# create three circles with color red, green and blue
red_c   = make_circle((256, 256), ARGB(1., 0., 0., 1.))
green_c = make_circle((256, 256), ARGB(0., 1., 0., 1.))
blue_c  = make_circle((256, 256), ARGB(0., 0., 1., 1.))

mosaicview(red_c, green_c, blue_c; nrow=1)

Then, shift these circles to appropriate positions, pad them to common axes, and finally composite using the add operation:

r = size(red_c, 1) ÷ 8
red_o   = OffsetArray(red_c,    r,  r)
green_o = OffsetArray(green_c, -r,  0)
blue_o  = OffsetArray(blue_c,   r, -r)

color_panel = sum(paddedviews(zero(eltype(red_o)), red_o, green_o, blue_o))
color_panel = color_panel[axes(red_c)...] # crop empty region

This page was generated using DemoCards.jl and Literate.jl.