Skip to content

Instantly share code, notes, and snippets.

@pthom
Last active June 30, 2023 10:16
Show Gist options
  • 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()
@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