Skip to content

Instantly share code, notes, and snippets.

@pthom
Last active June 30, 2023 10:16
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pthom/5155d319a7957a38aeb2ac9e54cc0999 to your computer and use it in GitHub Desktop.
Save pthom/5155d319a7957a38aeb2ac9e54cc0999 to your computer and use it in GitHub Desktop.
import numpy as np
import cv2
def overlay_alpha_image_lazy(background_rgb, overlay_rgba, alpha):
# cf https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
# If the destination background is opaque, then
# out_rgb = overlay_rgb * overlay_alpha + background_rgb * (1 - overlay_alpha)
overlay_alpha = overlay_rgba[: , : , 3].astype(np.float) / 255. * alpha
overlay_alpha_3 = np.dstack((overlay_alpha, overlay_alpha, overlay_alpha))
overlay_rgb = overlay_rgba[: , : , : 3].astype(np.float)
background_rgb_f = background_rgb.astype(np.float)
out_rgb = overlay_rgb * overlay_alpha_3 + background_rgb_f * (1. - overlay_alpha_3)
out_rgb = out_rgb.astype(np.uint8)
return out_rgb
def overlay_alpha_image_precise(background_rgb, overlay_rgba, alpha, gamma_factor=2.2):
"""
cf minute physics brilliant clip "Computer color is broken" : https://www.youtube.com/watch?v=LKnqECcg6Gw
the RGB values are gamma-corrected by the sensor (in order to keep accuracy for lower luminancy),
we need to undo this before averaging.
"""
overlay_alpha = overlay_rgba[: , : , 3].astype(np.float) / 255. * alpha
overlay_alpha_3 = np.dstack((overlay_alpha, overlay_alpha, overlay_alpha))
overlay_rgb_squared = np.float_power(overlay_rgba[: , : , : 3].astype(np.float), gamma_factor)
background_rgb_squared = np.float_power( background_rgb.astype(np.float), gamma_factor)
out_rgb_squared = overlay_rgb_squared * overlay_alpha_3 + background_rgb_squared * (1. - overlay_alpha_3)
out_rgb = np.float_power(out_rgb_squared, 1. / gamma_factor)
out_rgb = out_rgb.astype(np.uint8)
return out_rgb
def test_overlay():
img = np.zeros((100, 800, 3), np.uint8)
img[ : , : , : ] = (0, 0, 255)
overlay = np.zeros((100, 800, 4), np.uint8)
def make_gradient(x0, color):
x1 = x0 + 40
for x in range(x0, x1):
for y in range(0, 100):
k = (x - x0) / (x1 - x0)
alpha = int(round(k * 255.))
color_grad = (color[0], color[1], color[2], alpha)
overlay[y, x, :] = color_grad
make_gradient(100, (255, 0, 0))
make_gradient(200, (0, 255, 0))
make_gradient(300, (255, 255, 0))
make_gradient(400, (0, 255, 255))
make_gradient(500, (250, 50, 200))
mix_precise = overlay_alpha_image_precise(img, overlay, alpha=1.)
mix_lazy = overlay_alpha_image_lazy(img, overlay, alpha=1.)
cv2.imshow("mix_precise", mix_precise)
cv2.imshow("mix_lazy", mix_lazy)
#cv2.imshow("img", img)
#cv2.imshow("overlay", overlay)
cv2.waitKey()
test_overlay()
@pthom
Copy link
Author

pthom commented Apr 5, 2018

overlay_alpha_image_precise() gives the following result:
mix_precise

Compared to overlay_alpha_image_lazy()
mix_lazy

@Artoria2e5
Copy link

Artoria2e5 commented Jun 30, 2023

There's an unpleasant edge in both versions (arguably more obvious in the precise one), possibly due to lack of alpha resolution. Might be a good idea to not do the 255 thing to alpha.

@Artoria2e5
Copy link

Artoria2e5 commented Jun 30, 2023

nope, did a full-float version and it still looks harsh. guess 40 steps is just not enough. dithering may do something, but meh that's too much work.

doing gamma in 0-255 space is also not a great idea, but it works out approximately. at least sqrt((255 * x1)^2 * (1-a) + (255 * x2)^2 * a) = 255 * sqrt(x1^2 * (1-a) + x2^2 * a). The same does not quite hold for 2.2 (wolfram alpha), though it remains very close. it always works in cases of a simple power, just not for the more interesting curves like sRGB.

difference is very hard to tell:
image

new code:

import numpy as np
import cv2

def overlay_alpha_image_lazy(background_rgb, overlay_rgba, alpha):
    # cf https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
    # If the destination background is opaque, then
    #   out_rgb = overlay_rgb * overlay_alpha + background_rgb * (1 - overlay_alpha)
    overlay_alpha = overlay_rgba[: , : , 3] * alpha
    overlay_alpha_3 = np.dstack((overlay_alpha, overlay_alpha, overlay_alpha))
    overlay_rgb = overlay_rgba[: , : , : 3]
    background_rgb_f = background_rgb
    out_rgb = overlay_rgb * overlay_alpha_3 + background_rgb_f * (1. - overlay_alpha_3)
    out_rgb *= 255
    out_rgb = out_rgb.astype(np.uint8)
    return out_rgb


def overlay_alpha_image_precise(background_rgb, overlay_rgba, alpha, gamma_factor=2.2):
    """
    cf minute physics brilliant clip "Computer color is broken" : https://www.youtube.com/watch?v=LKnqECcg6Gw
    the RGB values are gamma-corrected by the sensor (in order to keep accuracy for lower luminancy),
    we need to undo this before averaging.
    """
    overlay_alpha = overlay_rgba[: , : , 3] * alpha
    overlay_alpha_3 = np.dstack((overlay_alpha, overlay_alpha, overlay_alpha))

    overlay_rgb_squared = np.float_power(overlay_rgba[: , : , : 3], gamma_factor)
    background_rgb_squared = np.float_power(background_rgb, gamma_factor)
    out_rgb_squared = overlay_rgb_squared * overlay_alpha_3 + background_rgb_squared * (1. - overlay_alpha_3)
    out_rgb = np.float_power(out_rgb_squared, 1. / gamma_factor)
    out_rgb *= 255
    out_rgb = out_rgb.astype(np.uint8)
    return out_rgb


def test_overlay():

    img = np.zeros((100, 800, 3), np.single)
    img[ : , : , : ] = (0, 0, 1)

    overlay = np.zeros((100, 800, 4), np.single)

    def make_gradient(x0, color):
        x1 = x0 + 40
        for x in range(x0, x1):
            for y in range(0, 100):
                alpha = float(x - x0) / float(x1 - x0)
                color_grad = (color[0] / 255., color[1] / 255., color[2] / 255., alpha)
                overlay[y, x, :] = color_grad
    make_gradient(100, (255, 0, 0))
    make_gradient(200, (0, 255, 0))
    make_gradient(300, (255, 255, 0))
    make_gradient(400, (0, 255, 255))
    make_gradient(500, (250, 50, 200))

    mix_precise = overlay_alpha_image_precise(img, overlay, alpha=1.)
    mix_lazy = overlay_alpha_image_lazy(img, overlay, alpha=1.)

    cv2.imshow("mix_precise", mix_precise)
    cv2.imshow("mix_lazy", mix_lazy)
    #cv2.imshow("img", img)
    #cv2.imshow("overlay", overlay)
    cv2.waitKey()

test_overlay()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment