0

I'm trying to create an algorithm to overlay an image with transparencies on top of fully opaque image. On the next sample I have a back fully opaque image, and front image which is a blue frame with diffuse edges. The problem I'm having is that my implementation overlays incorrectly the semi-transparent areas producing darkish pixels.

enter image description here

Here is my implementation:

#define OPAQUE 0xFF
#define TRANSPARENT 0
#define ALPHA(argb)  (uint8_t)(argb >> 24)
#define RED(argb)    (uint8_t)(argb >> 16)
#define GREEN(argb)  (uint8_t)(argb >> 8)
#define BLUE(argb)   (uint8_t)(argb)
#define ARGB(a, r, g, b) (a << 24) | ((r & 0xff) << 16) | ((g & 0xff) << 8) | (b & 0xff)
#define BLEND(a, b, alpha) ((a * alpha) + (b * (255 - alpha))) / 255

void ImageUtil::overlay(const uint32_t* front, uint32_t* back, const unsigned int width, const unsigned int height)
{
    const size_t totalPixels = width * height;

    for (unsigned long index = 0; index < totalPixels; index++)
    {
        const uint32_t alpha = ALPHA(*front);

        const uint32_t R = BLEND(RED(*front), RED(*back), alpha);
        const uint32_t G = BLEND(GREEN(*front), GREEN(*back), alpha);
        const uint32_t B = BLEND(BLUE(*front), BLUE(*back), alpha);

        *backPixels++ = ARGB(OPAQUE, R , G, B);
        *frontPixels++;
    }
}

UPDATE:

Test Images files

DOWNLOAD

PerracoLabs
  • 16,449
  • 15
  • 74
  • 127
  • What did you discover when you debugged this? – Oliver Charlesworth Nov 16 '14 at 11:26
  • 2
    how did you load your image? is it possible your image loader pre-multiplied that image? – gman Nov 16 '14 at 11:33
  • Images are loaded correctly. I've tried that by loading and saving them again separately without merging, which produces the same original images. I think the problem might be in the formula mixing the pixels, but I can't spot the problem. – PerracoLabs Nov 16 '14 at 11:41
  • 1
    The standard alphablend algorithm is `((RGB1 * alpha) + (RGB2 * (255 - alpha))) / 255`. – Jonathan Potter Nov 16 '14 at 11:49
  • Yes this is the formula which I'm using in the algorithm, split into steps. But I'm not sure if is the correct formula to be used when overlaying pixels. – PerracoLabs Nov 16 '14 at 11:55
  • Just to limit the code to consider, comment out completely the if (frontAlpha == TRANSPARENT) { *backPixels++; *frontPixels++; continue; } if (frontAlpha == OPAQUE) { *backPixels++ = *frontPixels++; continue; } And confirm that you have exactly the same results ! So we may consider the rest of the code. – George Kourtis Nov 16 '14 at 12:17
  • I've just did the test, and is still producing the incorrect blend. I've also updated the code as these lines have no impact in the problem. – PerracoLabs Nov 16 '14 at 12:24
  • It seems all correct, so test by putting frontAlpha=255 and frontAlpha=0 ( instead of = ALPHA(*frontPixels) ) so you may see what happens and have a clue. – George Kourtis Nov 16 '14 at 12:28
  • 1
    Maybe your front image has premultiplied alpha. If it does and you're multiplying it by alpha again, it would cause exactly the result you're getting (darker result where alpha is not 0 or 255). – interjay Nov 16 '14 at 12:33
  • Just a comment on how to simplify a little bit ( but may be nothing to do with the problem). struct pixel{ uint8_t a,c[3]; } . In that way you may access each part by using a and c[i] . – George Kourtis Nov 16 '14 at 12:36
  • I've added a link at the bottom of the question to a zip with the test images – PerracoLabs Nov 16 '14 at 13:11
  • r = new Color(); r.A = 1 - (1 - fg.A) * (1 - bg.A); if (r.A < 1.0e-6) return r; // Fully transparent -- R,G,B not important r.R = fg.R * fg.A / r.A + bg.R * bg.A * (1 - fg.A) / r.A; r.G = fg.G * fg.A / r.A + bg.G * bg.A * (1 - fg.A) / r.A; r.B = fg.B * fg.A / r.A + bg.B * bg.A * (1 - fg.A) / r.A; – George Kourtis Nov 16 '14 at 15:03
  • Please check http://stackoverflow.com/questions/726549/algorithm-for-additive-color-mixing-for-rgb-values as it may give suggestion on how to mix. – George Kourtis Nov 16 '14 at 15:03

1 Answers1

0

Following the tips from the comments by gman and interjay, I've investigated further and yes the data is being loaded with pre-multiplied alpha. This produces the darkening when blending. The solution has been to un-multiply the front pixels, and finally I've got the expected result.

Unmultiply formula:

((0xFF * color) / alpha)

Final code:

#define OPAQUE 0xFF;
#define TRANSPARENT 0;

#define ALPHA(rgb) (uint8_t)(rgb >> 24)
#define RED(rgb)   (uint8_t)(rgb >> 16)
#define GREEN(rgb) (uint8_t)(rgb >> 8)
#define BLUE(rgb)  (uint8_t)(rgb)

#define UNMULTIPLY(color, alpha) ((0xFF * color) / alpha)
#define BLEND(back, front, alpha) ((front * alpha) + (back * (255 - alpha))) / 255
#define ARGB(a, r, g, b) (a << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF)

void ImageUtil::overlay(const uint32_t* front, uint32_t* back, const unsigned int width, const unsigned int height)
{
    const size_t totalPixels = width * height;

    for (unsigned long index = 0; index < totalPixels; index++)
    {
        const uint32_t frontAlpha = ALPHA(*front);

        if (frontAlpha == TRANSPARENT)
        {
            *back++;
            *front++;
            continue;
        }

        if (frontAlpha == OPAQUE)
        {
            *back++ = *front++;
            continue;
        }

        const uint8_t backR = RED(*back);
        const uint8_t backG = GREEN(*back);
        const uint8_t backB = BLUE(*back);

        const uint8_t frontR = UNMULTIPLY(RED(*front), frontAlpha);
        const uint8_t frontG = UNMULTIPLY(GREEN(*front), frontAlpha);
        const uint8_t frontB = UNMULTIPLY(BLUE(*front), frontAlpha);

        const uint32_t R = BLEND(backR, frontR, frontAlpha);
        const uint32_t G = BLEND(backG, frontG, frontAlpha);
        const uint32_t B = BLEND(backB, frontB, frontAlpha);

        *back++ = ARGB(OPAQUE, R , G, B);
        *front++;
    }
}
PerracoLabs
  • 16,449
  • 15
  • 74
  • 127