PRB: BlendModes and Alpha Images

Jul 22, 2010 at 7:11 AM

I'm trying to use the BlendModes with a non-alpha base and an alpha blend layer and I'm not getting the correct result. I'm testing this using the simple Multiply blend mode. The results I get with Pant.Net and PhotoShop match each other but the Blit method returns a vastly different result (first rectangle is all black in the test image). The test images can be found in this Blending tutorial:

http://graphicssoft.about.com/od/glossary/ig/Blending-Modes/Blending-Mode-Introduction.htm (find psd files here)

http://graphicssoft.about.com/od/glossary/ig/Blending-Modes/Blend-Multiply.htm (expected result here)

In case its helpful heres the code I'm using:

var baseImage = new WriteableBitmap(0, 0).FromResource(@"Images\base.png");
var blendImage = new WriteableBitmap(0, 0).FromResource(@"Images\blend.png");

blendImage.Blit(new Rect(0, 0, baseImage.PixelWidth, baseImage.PixelHeight), baseImage,
                                    new Rect(0, 0, baseImage.PixelWidth, baseImage.PixelHeight),
                                    WriteableBitmapExtensions.BlendMode.Multiply);

Any help is very much appreciated.

 

Coordinator
Jul 22, 2010 at 7:57 AM

Hi,

could be a problem with the Multiply blend mode. Have you tried it with a different blend mode, like Alpha blend or Subtractive? 

You should also try to change the order of blending. Try baseImage.Blit(... blendImage ...);

This is not the problem here, but you should check the parameters of the method, esp. the sourceRect parameter uses the size of the destination image in the snippet above.

 

- Rene

Jul 22, 2010 at 5:01 PM
Thanks for the reply - I've yet to do more testing but changing my Alpha Blend Image to a jpg (therefore no Alpha) produced a better result. So its 'probably' an issue with the premultiplied values. I'm in the process of refactoring the original code as its well in need of some help. I hope to post the revisions soon incase anyone else wants a more extendable version. I might add some of the other blend modes too...
Coordinator
Jul 22, 2010 at 5:35 PM
Edited Jul 22, 2010 at 5:36 PM

That sounds great. Contributions are always welcome. I've actually created a ticket for this a while ago: http://writeablebitmapex.codeplex.com/workitem/13420

I must admit that the Blit code structure is not the best. I always wanted to change these contributed Blit pieces and make it more consistent, but the code works and I postponed it. It also takes a good amount of time to unknot it and I can't do this anytime soon. 

One think is also always important: Speed. This has to run fast and for example calling a delegate for each pixel would slow this down.

I can add you as a developer if you like.

 

- Rene

Jul 23, 2010 at 11:49 PM
Edited Jul 23, 2010 at 11:51 PM

So I've finished the first round of refactorings (might need to clean it up a little still). However, its probably good enough to now. I've removed some 'features' to reduce the complexity and hopefully make it simplier to use. While the API has support for opacity its not implemented as yet, that's next on the list. Also the original code seemed to have some bugs when dealing with Alpha images, which were fixed. Havign said that the code still doesn't handle alpha images correctly for some modes (the original didn't either). If you put your alpha image ontop of a white background then process it will work correctly. Anyway I've included some performance tests and the full code at the end.

Here are the results for three environments (you might be quite interested in the last two, I was!). The results are based on using debug, non-debug results are only for the Windows Mobile 7 Device as I had to manually write them down. These were based on two images both 1600x1200 which is quite big (see original post for source images). I've also included the Xor mode that was posted here too. I was fairly happy with the results and the new API creates a new image to preserve the originals.

The only one I can't work out is the None which is a BulkCopy and its slower than the original code?? I'll have to look at that to see if it can be improved (though both are extremely fast anyway). If anyone finds any bugs or can improve the performance post a comment.

Silverlight Desktop
Blend Mode Alpha - Original 67 vs Refactored 43.7 = 53.3180778032037 % gain
Blend Mode Additive - Original 101.4 vs Refactored 70.2 = 44.4444444444444 % gain
Blend Mode Subtractive - Original 99.8 vs Refactored 62.4 = 59.9358974358974 % gain
Blend Mode Mask - Original 70.2 vs Refactored 35.8 = 96.0893854748604 % gain
Blend Mode Multiply - Original 118.6 vs Refactored 74.9 = 58.3444592790387 % gain
Blend Mode None - Original 6.2 vs Refactored 9.4 = -34.0425531914894 % gain
Blend Mode Xor - Original ?? vs Refactored 25 = ?? % gain

Windows Phone 7 Emulator
Blend Mode Alpha - Original 222.1 vs Refactored 65.1 = 241.167434715822 % gain
Blend Mode Additive - Original 256 vs Refactored 74 = 245.945945945946 % gain
Blend Mode Subtractive - Original 257.8 vs Refactored 57.7 = 346.793760831889 % gain
Blend Mode Mask - Original 216.9 vs Refactored 42.8 = 406.775700934579 % gain
Blend Mode Multiply - Original 283.1 vs Refactored 69.2 = 309.104046242775 % gain
Blend Mode None - Original 6.1 vs Refactored 16.6 = -63.2530120481928 % gain
Blend Mode Xor - Original ?? vs Refactored 31.4 = ?? % gain

Windows Phone 7 Device (a real one! - attached to debugger)
Blend Mode Alpha - Original 773 vs Refactored 394.9 = 95.7457584198531 % gain
Blend Mode Additive - Original 960.2 vs Refactored 416.8 = 130.374280230326 % gain
Blend Mode Subtractive - Original 985.4 vs Refactored 314.7 = 213.123609787099 % gain
Blend Mode Mask - Original 771.6 vs Refactored 279.3 = 176.262083780881 % gain
Blend Mode Multiply - Original 1175.9 vs Refactored 442.6 = 165.680072300045 % gain
Blend Mode None - Original 22.9 vs Refactored 36.5 = -37.2602739726027 % gain
Blend Mode Xor - Original ?? vs Refactored 185.8 = ?? % gain

Windows Phone 7 Device (a real one! - release build)
Blend Mode Alpha - Original 361.3 vs Refactored 241.4= 49.668 % gain
Blend Mode Additive - Original 471.2 vs Refactored 86.097 = 86.097 % gain
Blend Mode Subtractive - Original 447.6 vs Refactored 247.8 = 80.269 % gain
Blend Mode Mask - Original 433.3 vs Refactored 213.7 = 102.760 % gain
Blend Mode Multiply - Original 529.5 vs Refactored 312.8 = 69.277 % gain
Blend Mode None - Original 23 vs Refactored 36.5 = -36.986 % gain
Blend Mode Xor - Original ?? vs Refactored 149.6 = ?? % gain

    public static class BlendExtension
    {
        public enum BlendMode
        {
            /// <summary>
            /// Alpha blendiing uses the alpha channel to combine the source and destination.
            /// </summary>
            Alpha,

            /// <summary>
            /// Additive blending adds the colors of the source and the destination.
            /// </summary>
            Additive, //done

            /// <summary>
            /// Subtractive blending subtracts the source color from the destination.
            /// </summary>
            Subtractive, //done

            /// <summary>
            /// Uses the source color as a mask.
            /// </summary>
            Mask, //done

            /// <summary>
            /// Multiplies the source color with the destination color.
            /// </summary>
            Multiply, //done

            /// <summary>
            /// No blending just copies the pixels from the source.
            /// </summary>
            None, //done

            Xor,
        }

        public static WriteableBitmap Blend(this WriteableBitmap lowerLayer, WriteableBitmap upperLayer, BlendMode blendMode)
        {
            return Blend(lowerLayer, upperLayer, 255, blendMode);
        }

        public static WriteableBitmap Blend(this WriteableBitmap lowerLayer, WriteableBitmap upperLayer, int opacity, BlendMode blendMode)
        {
            if (blendMode == BlendMode.Multiply)
                return BlendMultiply(lowerLayer, upperLayer, opacity);
            else if (blendMode == BlendMode.Subtractive)
                return BlendSubtract(lowerLayer, upperLayer, opacity);
            else if (blendMode == BlendMode.Additive)
                return BlendAdditive(lowerLayer, upperLayer, opacity);
            else if (blendMode == BlendMode.Mask)
                return BlendMask(lowerLayer, upperLayer, opacity);
            else if (blendMode == BlendMode.None)
                return BlendNone(lowerLayer, upperLayer, opacity);
            else if (blendMode == BlendMode.Alpha)
                return BlendAlpha(lowerLayer, upperLayer, opacity);
            else if (blendMode == BlendMode.Xor)
                return BlendXor(lowerLayer, upperLayer, opacity);

            throw new ArgumentException("Blend Mode not recognised.");
        }

        public static WriteableBitmap BlendNone(this WriteableBitmap lowerLayer, WriteableBitmap upperLayer, int opacity)
        {
            return upperLayer.Clone();
        }

        public static WriteableBitmap BlendMultiply(this WriteableBitmap lowerLayer, WriteableBitmap upperLayer, int opacity)
        {
            var newBitmap = new WriteableBitmap(upperLayer.PixelWidth, upperLayer.PixelHeight);
            var lowerLayerPixels = lowerLayer.Pixels;
            var upperLayerPixels = upperLayer.Pixels;
            var blendedPixels = newBitmap.Pixels;

            int sa;
            int sr;
            int sg;
            int sb;

            int da;
            int dr;
            int dg;
            int db;
            int lowerLayerPixel;
            int upperLayerPixel;

            for (int i = 0; i < upperLayerPixels.Length; i++)
            {
                lowerLayerPixel = lowerLayerPixels[i];
                upperLayerPixel = upperLayerPixels[i];

                sa = ((lowerLayerPixel >> 24) & 0xff);

                sr = ((lowerLayerPixel >> 16) & 0xff);
                sg = ((lowerLayerPixel >> 8) & 0xff);
                sb = ((lowerLayerPixel) & 0xff);

                da = ((upperLayerPixel >> 24) & 0xff);
                dr = ((upperLayerPixel >> 16) & 0xff);
                dg = ((upperLayerPixel >> 8) & 0xff);
                db = ((upperLayerPixel) & 0xff);

                // Faster than a division like (s * d) / 255 are 2 shifts and 2 adds
                int ta = (sa * da) + 128;
                int tr = (sr * dr) + 128;
                int tg = (sg * dg) + 128;
                int tb = (sb * db) + 128;

                int ba = ((ta >> 8) + ta) >> 8;
                int br = ((tr >> 8) + tr) >> 8;
                int bg = ((tg >> 8) + tg) >> 8;
                int bb = ((tb >> 8) + tb) >> 8;

                blendedPixels[i] = (ba << 24) | ((ba <= br ? ba : br) << 16) | ((ba <= bg ? ba : bg) << 8) | ((ba <= bb ? ba : bb));
            }

            return newBitmap;
        }

        public static WriteableBitmap BlendAdditive(this WriteableBitmap lowerLayer, WriteableBitmap upperLayer, int opacity)
        {
            var newBitmap = new WriteableBitmap(upperLayer.PixelWidth, upperLayer.PixelHeight);
            var lowerLayerPixels = lowerLayer.Pixels;
            var upperLayerPixels = upperLayer.Pixels;
            var blendedPixels = newBitmap.Pixels;

            int sa;
            int sr;
            int sg;
            int sb;

            int da;
            int dr;
            int dg;
            int db;
            int lowerLayerPixel;
            int upperLayerPixel;

            for (int i = 0; i < upperLayerPixels.Length; i++)
            {
                lowerLayerPixel = lowerLayerPixels[i];
                upperLayerPixel = upperLayerPixels[i];

                sa = ((lowerLayerPixel >> 24) & 0xff);
                if (sa == 0)
                {
                    blendedPixels[i] = lowerLayerPixel;
                    continue;
                }

                sr = ((lowerLayerPixel >> 16) & 0xff);
                sg = ((lowerLayerPixel >> 8) & 0xff);
                sb = ((lowerLayerPixel) & 0xff);

                da = ((upperLayerPixel >> 24) & 0xff);
                dr = ((upperLayerPixel >> 16) & 0xff);
                dg = ((upperLayerPixel >> 8) & 0xff);
                db = ((upperLayerPixel) & 0xff);

                int a = (255 <= sa + da) ? 255 : (sa + da);
                blendedPixels[i] = (a << 24) | (((a <= sr + dr) ? a : (sr + dr)) << 16) | (((a <= sg + dg) ? a : (sg + dg)) << 8) | (((a <= sb + db) ? a : (sb + db)));
            }

            return newBitmap;
        }

        public static WriteableBitmap BlendSubtract(this WriteableBitmap lowerLayer, WriteableBitmap upperLayer, int opacity)
        {
            var newBitmap = new WriteableBitmap(upperLayer.PixelWidth, upperLayer.PixelHeight);
            var lowerLayerPixels = lowerLayer.Pixels;
            var upperLayerPixels = upperLayer.Pixels;
            var blendedPixels = newBitmap.Pixels;

            int sa;
            int sr;
            int sg;
            int sb;

            int da;
            int dr;
            int dg;
            int db;
            int lowerLayerPixel;
            int upperLayerPixel;

            for (int i = 0; i < upperLayerPixels.Length; i++)
            {
                lowerLayerPixel = lowerLayerPixels[i];
                upperLayerPixel = upperLayerPixels[i];

                sr = ((upperLayerPixel >> 16) & 0xff);
                sg = ((upperLayerPixel >> 8) & 0xff);
                sb = ((upperLayerPixel) & 0xff);

                da = ((lowerLayerPixel >> 24) & 0xff);
                dr = ((lowerLayerPixel >> 16) & 0xff);
                dg = ((lowerLayerPixel >> 8) & 0xff);
                db = ((lowerLayerPixel) & 0xff);

                int a = da;
                blendedPixels[i] = (a << 24) | (((sr >= dr) ? 0 : (sr - dr)) << 16) | (((sg >= dg) ? 0 : (sg - dg)) << 8) | (((sb >= db) ? 0 : (sb - db)));
            }

            return newBitmap;
        }

        public static WriteableBitmap BlendMask(this WriteableBitmap lowerLayer, WriteableBitmap upperLayer, int opacity)
        {
            var newBitmap = new WriteableBitmap(upperLayer.PixelWidth, upperLayer.PixelHeight);
            var lowerLayerPixels = lowerLayer.Pixels;
            var upperLayerPixels = upperLayer.Pixels;
            var blendedPixels = newBitmap.Pixels;

            int sa;

            int da;
            int dr;
            int dg;
            int db;
            int lowerLayerPixel;
            int upperLayerPixel;

            for (int i = 0; i < upperLayerPixels.Length; i++)
            {
                lowerLayerPixel = lowerLayerPixels[i];
                upperLayerPixel = upperLayerPixels[i];

                sa = ((upperLayerPixel >> 24) & 0xff);

                da = ((lowerLayerPixel >> 24) & 0xff);
                dr = ((lowerLayerPixel >> 16) & 0xff);
                dg = ((lowerLayerPixel >> 8) & 0xff);
                db = ((lowerLayerPixel) & 0xff);

                blendedPixels[i] = ((((da * sa) * 0x8081) >> 23) << 24) |
                            ((((dr * sa) * 0x8081) >> 23) << 16) |
                            ((((dg * sa) * 0x8081) >> 23) << 8) |
                            ((((db * sa) * 0x8081) >> 23));
            }

            return newBitmap;
        }

        public static WriteableBitmap BlendAlpha(this WriteableBitmap lowerLayer, WriteableBitmap upperLayer, int opacity)
        {
            var newBitmap = new WriteableBitmap(upperLayer.PixelWidth, upperLayer.PixelHeight);
            var lowerLayerPixels = lowerLayer.Pixels;
            var upperLayerPixels = upperLayer.Pixels;
            var blendedPixels = newBitmap.Pixels;

            int sa;
            int sr;
            int sg;
            int sb;

            int da;
            int dr;
            int dg;
            int db;
            int lowerLayerPixel;
            int upperLayerPixel;

            for (int i = 0; i < upperLayerPixels.Length; i++)
            {
                lowerLayerPixel = lowerLayerPixels[i];
                upperLayerPixel = upperLayerPixels[i];

                sa = ((upperLayerPixel >> 24) & 0xff);
                if (sa == 0)
                {
                    blendedPixels[i] = lowerLayerPixel;
                    continue;
                }

                sr = ((upperLayerPixel >> 16) & 0xff);
                sg = ((upperLayerPixel >> 8) & 0xff);
                sb = ((upperLayerPixel) & 0xff);

                da = ((lowerLayerPixel >> 24) & 0xff);
                dr = ((lowerLayerPixel >> 16) & 0xff);
                dg = ((lowerLayerPixel >> 8) & 0xff);
                db = ((lowerLayerPixel) & 0xff);

                blendedPixels[i] = ((sa + (((da * (255 - sa)) * 0x8081) >> 23)) << 24) |
                 ((sr + (((dr * (255 - sa)) * 0x8081) >> 23)) << 16) |
                 ((sg + (((dg * (255 - sa)) * 0x8081) >> 23)) << 8) |
                 ((sb + (((db * (255 - sa)) * 0x8081) >> 23)));
            }

            return newBitmap;
        }

        public static WriteableBitmap BlendXor(this WriteableBitmap lowerLayer, WriteableBitmap upperLayer, int opacity)
        {
            var newBitmap = new WriteableBitmap(upperLayer.PixelWidth, upperLayer.PixelHeight);
            var lowerLayerPixels = lowerLayer.Pixels;
            var upperLayerPixels = upperLayer.Pixels;
            var blendedPixels = newBitmap.Pixels;

            int lowerLayerPixel;
            int upperLayerPixel;

            for (int i = 0; i < upperLayerPixels.Length; i++)
            {
                lowerLayerPixel = lowerLayerPixels[i];
                upperLayerPixel = upperLayerPixels[i];

                unchecked
                {
                    blendedPixels[i] = lowerLayerPixel ^ upperLayerPixel ^ (int)0xFF000000;
                }
            }

            return newBitmap;
        }
    }

 

Coordinator
Jul 24, 2010 at 12:25 PM
Edited Jul 24, 2010 at 12:26 PM

Thanks for your great work.

I see you removed the source and destination rectangles. This is actually needed when you want to copy only portions of one image to a certain region in the destination image. In the above code, both bitmaps need to have the same size and in this case the None method can be removed, since it does nothing. The size functionality is pretty essential for blitting and a direct performance comparison is not very meaningful when its left out. I guess most users need the blit functionality for this reason, so we gotta add it again.

Please don't understand this in the wrong way, it's meant to be constructive feedback. Will you continue to work on this?

Again, thanks for your effort! That's why I love open source. :)

- Rene

Jul 25, 2010 at 8:26 AM
Edited Jul 25, 2010 at 8:31 AM

Rene - thanks for the feedback. Regarding the rectangle omission it was on purpose. I was treating it more like you would in Photoshop in which you typically want to merge two layers together (which for my work is all I need) and didn't want to take the pef hit for a feature I didn't use. However, as you mention it seems to be useful for other applications. I'll try adding it back and see what kind of a hit I get, if its too much then I'll have to see what can be done without making the code too hard to maintain. I'll see how I go...

Oh and with regads to the None option even it appears redundant its still needed to allow consistent use. That is if a user is selecting an option, though I guess you could treat the special case differently to avoid an unnessary hit.

Coordinator
Jul 25, 2010 at 9:36 AM
Great! I appreciate it a lot.
None makes a Lot sense when a blitting with Source and Destination regions is performed, although most people use an AlphaBlend.
Maybe it makes sense to make two methods for the most used Alpha blend mode. The Blend method like you have it above and the Blit method with the source an the destination parameters. For the other blend modes only the Blit method. This would result in redundant code for the alpha mode, but there's a fast path if max performance is needed. Thanks for your help! - Rene