Search code examples
mathimage-processingcolorshex

Calculate hex color with transparency based on other colors


I have the following image:

image example

I want to find the hex color and transparency of the red circle based on the below information :

  • purple background circle is #afb0cc
  • human figure is #ffffff

By using eye-dropper I was able to figure out some additional information:

  • Red Circle over purple background circle is #d75866
  • Red Circle over human figure is #ff7f7f

Solution

  • Since your picture has part of the red circle overlapping nothing else, and actually being transparent in that area, we can cheat and just pick there.

    It's the RGBA tuple (255, 0, 0, 128).


    How to calculate that, if we didn't have it? System of equations.

    Let's say we have two layers, or pixels, or colors, and their composite:

    • A on top, with alpha channel alpha
    • B under it, background, fully opaque (no alpha channel)
    • C being the composite of A on top of B, also fully opaque

    I'll make the alpha channel be floating point and range from 0.0 to 1.0. Makes the arithmetic nicer.

    Now, compositing happens like this:

    C = (A * alpha) + (B * (1 - alpha))
    # or equivalently
    C = B + (A - B) * alpha
    
    # rearranged for alpha
    alpha = (C - B) / (A - B)
    
    # rearranged for A
    A = B + (C - B) / alpha
    

    You have a few values for C and B, and you want A and alpha. Each color is three values, but let's simplify and consider that "one" value (a vector). That means the equation contains two unknowns (A and alpha), so you need two equations to solve for them.

    Your "measurements" are those:

    # red circle over white:
    C1 = (255, 127, 127)
    B1 = (255, 255, 255)
    
    # red circle over black:
    C2 = (128,   0,   0)
    B2 = (  0,   0,   0)
    
    # red circle over blue/purple, which we don't need
    C3 = (215,  88, 102)
    B3 = (175, 176, 204)
    

    Now plug those in to get two equations.

    C1 = B1 + (A - B1) * alpha
    C2 = B2 + (A - B2) * alpha
    

    Some algebra (moving things around) (I checked it):

    alpha = (B1 - B2 - C1 + C2) / (B1 - B2)
    A = (B1*C2 - B2*C1) / (B1 - B2 - C1 + C2)
    

    And that gets you this value for your red transparent circle:

    # alpha
    array([0.50196, 0.50196, 0.50196])
    array([128., 128., 128.]) # / 255
    
    # A
    array([255.,   0.,   0.])
    

    Which is exactly what we expected.


    If you used other values for B and C, say from the blue/purple area (B3, C3), you would get close, but not exactly the right answer. The values for C are integers, which were necessarily rounded from the exact composition equation's result.

    The exact values for C3, given B and the fortunately known A and alpha, would be:

    C3 = array([215.15686,  87.6549 , 101.6    ])
    

    Yes, my alpha isn't a single value but a vector. That's not right, but it's good enough for those simple equations. You could set up more complex ones that carry each color component (RGB) individually, so that all color planes use the same alpha.

    If you wanted to use more samples of C and B, this would become a least squares problem.

    Variables are the vector A (A_r, A_g, A_b) and the scalar alpha. Let's isolate those terms, and rearrange to fit the A x = b form of a system of linear equations:

    A = B + (C - B) / alpha  # see above
    A - (C - B) / alpha = B 
    A + (B - C) / alpha = B 
    1*A   +   (B-C) * 1/alpha   =   B
    

    Unfortunately, we don't get alpha here but 1/alpha. That should not surprise because in the case of alpha = 0 or approaching zero, the composite C would contain that little of A in it. We'd be really squeezing the data.

    The system becomes:

    # x_ = [[A_r], [A_g], [A_b], [ 1/alpha ] ] # column vector
    
    A_ = [[ 1,     0,     0,   (B1_r - C1_r) ], # first set of B and C
          [ 0,     1,     0,   (B1_g - C1_g) ],
          [ 0,     0,     1,   (B1_b - C1_b) ],
          [ 1,     0,     0,   (B2_r - C2_r) ], # second set of B and C
          [ 0,     1,     0,   (B2_g - C2_g) ],
          [ 0,     0,     1,   (B2_b - C2_b) ],
          ... # more B and C?
    ]
    b_ = [[B1_r], [B1_g], [B1_b], # first B
          [B2_r], [B2_g], [B2_b], # second B
          ... # more B?
    ]
    

    This is an example having just two (pixel) samples. If you have more, just dump them all into the A_ and b_. Yes, every pixel of every image. That's fine. It shouldn't even make the calculation cost explode.

    Specific example: you have B being completely black and completely white. Then take all your C pixel samples, pair them with a B = (0,0,0), add, take all C samples again, pair with B = (255,255,255), add those too.

    Plug that into the solver, invert the 1/alpha to get alpha, done.

    (x_, residuals, rank, sgval) = np.linalg.lstsq(A_, b_)
    
    x_ = x_.flatten()
    print("x_:", x_)
    print("residuals:", residuals)
    print("rank:", rank, "(good)" if (rank == 4) else "(bad)")
    
    A = x_[0:3]
    alpha = 1 / x_[3]
    
    print("Solution:")
    print("A:", A)
    print("α:", alpha, "or", alpha * 255)
    
    x_: [254.95158   0.15749   0.18931   1.99318]
    residuals: [0.77748]
    rank: 4 (good)
    Solution:
    A: [254.95158   0.15749   0.18931]
    α: 0.5017100863307523 or 127.93607201434185