Search code examples
pythonnumpygimpgimpfu

Adding two images togheter using "addition" blending mode: recreating GIMP "addition" mode in python gives different results


I am trying to add two images together.

enter image description here

enter image description here

When I do so in GIMP, using the 'Addition' layer-mode I get the following desired output. enter image description here

Now, I am trying to execute this in Python, however I can not manage to get it working properly. According to the gimp documentation, as far as I understand the formula for addition is simply the addition of each pixel, set to a max of 255.

Equation for Addition mode: E = min((M+I), 255)

I try to apply this in Python but the output looks different. How can I get the same output in Python as the gimp addition layer blending mode?

My attempt:

# Load in the images
diffuse = cv2.imread('diffuse.png')
reflection = cv2.imread('reflection.png',)
# Convert BGR to RGB
diffuse = cv2.cvtColor(diffuse, cv2.COLOR_BGR2RGB)
reflection = cv2.cvtColor(reflection, cv2.COLOR_BGR2RGB)
# Covnert to uint64 so values can overflow 255
diffuse = diffuse.astype('uint64')
reflection = reflection.astype('uint64')
added = diffuse + reflection # actually add
# clamp values above 255 to be 255
added[added > 255] = 255

# display 
im = Image.fromarray(added.astype('uint8'))
im.save('blended_python.png')

Now, this looks pretty similar but it is actually too bright compared to the GIMP addition. Just open both in a new tab and alt+tab between them and the difference becomes obvious.
enter image description here

How can I get the exact same result as in GIMP? I tried to use Python-fu or gimp batch rendering (as I have thousands of images that I want to blend like this) but I could not get that working, either.


Solution

  • With help from user Quang Hoang, I learned that GIMP by default has an implicit conversion (sRGB to linear sRGB) of the two layers before applying the addition.

    As such, to recreate this behavior in python we need the following functions to convert back and forth from sRGB to linear sRGB. Functions inspired by this question and this answer.

    def srgb_to_linsrgb(srgb):
        # takes np array as input in sRGB color space and convert to linear sRGB
        gamma = pow(((srgb + 0.055) / 1.055), 2.4)
        scale = srgb / 12.92
        return np.where (srgb > 0.04045, gamma, scale)
    
    
    def linsrgb_to_srgb(lin):
        # takes np array as input in linear sRGB color space and convert to sRGB
        gamma =  1.055 * (pow(lin, (1.0 / 2.4))) - 0.055
        scale =  12.92 * lin
        return np.where(lin > 0.0031308,  gamma, scale)
    

    Then we simply add these functions to the existing code from the original question.

    # Load in the images
    diffuse = cv2.imread('diffuse.png')
    reflection = cv2.imread('reflection.png',)
    
    # Convert BGR to RGB
    diffuse = cv2.cvtColor(diffuse, cv2.COLOR_BGR2RGB)
    reflection = cv2.cvtColor(reflection, cv2.COLOR_BGR2RGB)
    
    # Convert to sRGB in linear space
    lin_diffuse = srgb_to_linsrgb(diffuse)
    lin_reflection = srgb_to_linsrgb(reflection)
    
    lin_added = lin_diffuse + lin_reflection # actually add
    added = linsrgb_to_srgb(lin_added)
    added[added > 255] = 255
    
    # # display 
    im = Image.fromarray(added.astype('uint8'))