Magic Image: Alpha Channel
Mix.install([
{:evision, "~> 0.2"},
{:kino, "~> 0.11"},
{:req, "~> 0.5"},
{:nx, "~> 0.4"}
])
:ok
Get Images for Front and Back Layers (optional)
Upload your favorite images for this interesting experiment!
Note that the image of the frontlayer should be preferrablly bright, while the image of the backlayer should be relatively dimmed.
But if you don’t have any images at hand, this livebook will load some example images, and you can come back later!
front_layer_input = Kino.Input.image("Frontlayer")
back_layer_input = Kino.Input.image("Backlayer")
Kino.Layout.grid([front_layer_input, back_layer_input], columns: 2)
Load Images
load_layer = fn kino_input, url, save_as ->
case Kino.Input.read(kino_input) do
%{file_ref: file_ref, height: height, width: width} ->
file_ref
|> Kino.Input.file_path()
|> File.read!()
|> Evision.Mat.from_binary({:u, 8}, height, width, 3)
|> Evision.cvtColor(Evision.Constant.cv_COLOR_BGR2GRAY())
nil ->
unless File.exists?(save_as) do
Req.get!(url, http_errors: :raise, output: save_as, cache: false)
end
Evision.imread(save_as, flags: Evision.Constant.cv_IMREAD_GRAYSCALE())
end
end
front =
load_layer.(
front_layer_input,
"https://raw.githubusercontent.com/cocoa-xu/evision/master/test/testdata/front.jpg",
"front.jpg"
)
back =
load_layer.(
back_layer_input,
"https://raw.githubusercontent.com/cocoa-xu/evision/master/test/testdata/back.jpg",
"back.jpg"
)
Kino.Layout.grid([front, back], columns: 2)
%Evision.Mat{
channels: 1,
dims: 2,
type: {:u, 8},
raw_type: 0,
shape: {667, 1000},
ref: #Reference<0.3899563693.4086431769.126173>
}
%Evision.Mat{
channels: 1,
dims: 2,
type: {:u, 8},
raw_type: 0,
shape: {563, 1000},
ref: #Reference<0.3899563693.4086431769.126174>
}
Resize Layers to Same Size
resize_layers = fn front_layer, back_layer ->
{{f_cols, f_rows}, {b_cols, b_rows}} = {front_layer.shape, back_layer.shape}
{rows, cols} = {round(max(f_rows, b_rows)), round(max(f_cols, b_cols))}
size = {cols, rows}
front_canvas = Evision.Mat.full(size, 255.0, :f32)
back_canvas = Evision.Mat.full(size, 0.0, :f32)
do_resize = fn layer, canvas ->
{layer_cols, layer_rows} = layer.shape
{overlay_rows_start, overlay_cols_start} =
if layer_cols == cols do
{0, round((cols - layer_cols) / 2)}
else
{round((rows - layer_rows) / 2), 0}
end
Evision.Mat.update_roi(
canvas,
[
{overlay_cols_start, overlay_cols_start + layer_cols},
{overlay_rows_start, overlay_rows_start + layer_rows}
],
Evision.Mat.as_type(layer, :f32)
)
end
front = do_resize.(front_layer, front_canvas)
back = do_resize.(back_layer, back_canvas)
{front, back}
end
{f, b} = resize_layers.(front, back)
{%Evision.Mat{
channels: 1,
dims: 2,
type: {:f, 32},
raw_type: 5,
shape: {667, 1000},
ref: #Reference<0.3899563693.4086431768.119728>
},
%Evision.Mat{
channels: 1,
dims: 2,
type: {:f, 32},
raw_type: 5,
shape: {667, 1000},
ref: #Reference<0.3899563693.4086431768.119730>
}}
Adjust Intensity
To achieve best result, the image of the frontlayer should be preferrablly bright, while the image of the backlayer should be generally dimmed.
We can do this by increasing (and decreasing) their pixel intensity accordingly.
front_shift = Kino.Input.range("Frontlayer Intensity Shift", min: -255, max: 255, default: 40)
back_shift = Kino.Input.range("Backlayer Intensity Shift", min: -255, max: 255, default: -10)
Kino.Layout.grid([front_shift, back_shift], columns: 2)
shift_color = fn layer, shift ->
Nx.add(Evision.Mat.to_nx(layer, Nx.BinaryBackend), shift)
|> Nx.clip(0, 255)
end
f_ready = shift_color.(f, Kino.Input.read(front_shift))
b_ready = shift_color.(b, Kino.Input.read(back_shift))
#Nx.Tensor<
f32[667][1000]
[
[130.0, 133.0, 137.0, 140.0, 143.0, 147.0, 151.0, 153.0, 159.0, 161.0, 165.0, 167.0, 169.0, 171.0, 173.0, 175.0, 177.0, 179.0, 180.0, 182.0, 183.0, 184.0, 186.0, 187.0, 186.0, 187.0, 188.0, 190.0, 191.0, 191.0, 191.0, 190.0, 189.0, 192.0, 196.0, 198.0, 200.0, 199.0, 195.0, 190.0, 180.0, 161.0, 140.0, 118.0, 93.0, 81.0, 81.0, 79.0, 78.0, 80.0, ...],
...
]
>
Compute Values for Alpha and Grey Channels
Based on the formular given on the Wiki page of alpha blending
$$ \left{\begin{aligned} outA &= src_A + dst_A(1-src_A)\ out{RGB} &= \frac{(src{RGB}src_A + dst{RGB}dstA(1-src_A))}{out_A}\ out_A &= 0 \implies out{RGB} = 0 \end{aligned}\right. $$
If the destination background is opaque, i.e., $dst_A = 1$, we have
$$ \left{\begin{aligned} outA &= 1\ out{RGB} &= src{RGB}src_A + dst{RGB}(1-src_A) \end{aligned}\right. $$
Therefore, for the merged image $I$, given background $B$, we have
$$ \left{\begin{aligned} outA &= 1\ out{G} &= I{G}I_A + B{G}(1-I_A) \end{aligned}\right. $$
where $I_G$ and $I_A$ stands for the grey and alpha channel (of the merged image $I$) respectively.
If the background color is white, i.e., $B_G=255$, then
$$ \left{\begin{aligned} outA &= 1\ out{G} &= I{G}I_A + 255(1-I_A)\ &= \textit{front}{G} \end{aligned}\right. $$
Similarly, when the background color is black ($B_G=0$),
$$ \left{\begin{aligned} outA &= 1\ out{G} &= I{G}I_A + 0(1-I_A)\ &= I{G}IA\ &= \textit{back}{G} \end{aligned}\right. $$
Now we have two unknown variables, $I{G}$ and $I{A}$, and the simultaneous equations
$$ \left{\begin{aligned} \textit{front}{G} &= I{G}I{A} + 255(1-I_A)\ \textit{back}{G} &= I{G}I{A} \end{aligned}\right. $$
therefore, we have
$$ \left{\begin{aligned} I{A} &= 1 - \frac{(front{G} - back{G})}{255}\ I{G} &= \frac{backG}{I{A}} \end{aligned}\right. $$
compute_alpha = fn front, back ->
Nx.add(back, 255)
|> Nx.subtract(front)
|> Nx.clip(0, 255)
|> Nx.add(1.0e-12)
|> Nx.clip(0, 255)
end
alpha = compute_alpha.(f_ready, b_ready)
#Nx.Tensor<
f32[667][1000]
[
[182.0, 186.0, 191.0, 194.0, 196.0, 200.0, 205.0, 208.0, 214.0, 214.0, 216.0, 218.0, 220.0, 223.0, 225.0, 229.0, 230.0, 232.0, 232.0, 234.0, 234.0, 235.0, 237.0, 238.0, 236.0, 237.0, 240.0, 243.0, 241.0, 241.0, 242.0, 240.0, 239.0, 242.0, 246.0, 248.0, 250.0, 249.0, 245.0, 240.0, 230.0, 208.0, 186.0, 165.0, 140.0, 129.0, 129.0, 124.0, 127.0, 128.0, ...],
...
]
>
compute_grey = fn back, alpha ->
Nx.multiply(back, 255)
|> Nx.divide(alpha)
end
grey = compute_grey.(b_ready, alpha)
#Nx.Tensor<
f32[667][1000]
[
[182.14285278320312, 182.33871459960938, 182.90576171875, 184.02061462402344, 186.04591369628906, 187.4250030517578, 187.8292694091797, 187.57211303710938, 189.46261596679688, 191.84579467773438, 194.7916717529297, 195.3440399169922, 195.88636779785156, 195.53811645507812, 196.06666564941406, 194.86898803710938, 196.2391357421875, 196.74569702148438, 197.84483337402344, 198.3333282470703, 199.42308044433594, 199.65957641601562, 200.1265869140625, 200.35714721679688, 200.97457885742188, 201.20252990722656, 199.75, 199.38272094726562, 202.09542846679688, 202.09542846679688, 201.2603302001953, 201.875, 201.65272521972656, 202.31405639648438, 203.1707305908203, 203.58871459960938, 204.0, 203.79518127441406, 202.9591827392578, 201.875, 199.56521606445312, 197.3798065185547, 191.93548583984375, 182.36363220214844, 169.39285278320312, 160.11627197265625, 160.11627197265625, 162.4596710205078, 156.61416625976562, 159.375, ...],
...
]
>
Merge into An Image with Alpha Channel
merged = Evision.merge([grey, grey, grey, alpha])
{cols, rows, _} = merged.shape
{667, 1000, 4}
Visualise
Drag the generated image and hover it over the black background on the right hand-side, and it will “change” from one image (front layer) to the other one (back layer).
[
["Generated Image", merged],
[
"Black Background",
Evision.Mat.last_dim_as_channel(Evision.Mat.full({cols, rows, 3}, 0, :u8))
]
]
|> Enum.map(fn [label, img] ->
Kino.Layout.grid([img, Kino.Markdown.new("**#{label}**")], boxed: true)
end)
|> Kino.Layout.grid(columns: 2)
%Evision.Mat{
channels: 4,
dims: 2,
type: {:f, 32},
raw_type: 29,
shape: {667, 1000, 4},
ref: #Reference<0.3899563693.4086431768.119735>
}
Save the Merged Image
Evision.imwrite("merged.png", merged)
true