Re: Pisces vs marlin - private

157 views
Skip to first unread message

Laurent Bourgès

unread,
Aug 13, 2014, 8:21:04 AM8/13/14
to Clemens Eisserer, Andrea Aime, marlin-...@googlegroups.com, 2d-...@openjdk.java.net
Dear all,

I am looking in depth into java2d pipelines (composite, mask fill) to understand if the alpha blending is performed correctly (gamma correction, color mixing).


FYI, I read a lot of articles related to color blending ans its problems related to gamma correction & color spaces.

I think that java compositing is gamma corrected (sRGB) but suffers bad color mixing (luminance and saturation issue).

Look at this article for correct color mixing:
http://www.stuartdenman.com/improved-color-blending/
Correct gamma correction:
http://www.chez-jim.net/agg/gamma-correct-rendering-part-3


I figured out that:
1- gamma correction seems performed by the java2d pipeline (AlphaComposite.srcOver) ie sRGB => linear RGB (blending)

2- alpha mask filling (maskFill / maskBlit operators) are implemented in C (software, opengl or xrender variants) but it is incorrect:

For alpha = 50% (byte=128) => a black line over a white background gives middle gray (byte=128 and not byte=192): it uses linear on sRGB values instead of linear RGB values => gamma correction should be fixed !

3- colors are mixed in RGB color space => yellow + blue = gray issue !
    I could try implementing RGB<=>CIE-Lch color mixing ...


To illustrate the alpha mask filling problem, I hacked the sun.java2d.GeneralCompositePipe to perform a custom mask Fill in java using a custom Composite (BlendComposite).

In my test, the image is a default RGBA buffered image (sRGB) so I can easily handle R,G,B & alpha values.

GeneralCompositePipe changes:
        if (sg.composite instanceof BlendComposite) {
            // define mask alpha into dstOut:

1/ Copy the alpha coverage mask (from antialiasing renderer) into dstOut raster
            // INT_RGBA only
            final int[] dstPixels = new int[w];
           
              for (int j = 0; j < h; j++) {
                for (int i = 0; i < w; i++) {
                    dstPixels[i] = atile[ j * tilesize + (i + offset)] << 24;
                }
                dstOut.setDataElements(0, j, w, 1, dstPixels);
              }
   
2/ Delegate alpha color blending to Java code (my BlendComposite impl)       
             compCtxt.compose(srcRaster, dstIn, dstOut);
        }
...
            if (dstRaster instanceof WritableRaster
                    && ((atile == null) || sg.composite instanceof BlendComposite)) {
3/ As mask fill was done (by the BlendComposite), just copy raster pixels into image
                ((WritableRaster) dstRaster).setDataElements(x, y, dstOut);
            }

BlendComposite:

    private final static BlendComposite.GammaLUT gamma_LUT = new BlendComposite.GammaLUT(2.2);

        private final static int MAX_COLORS = 256;
        final int[] dir = new int[MAX_COLORS]; // pow(0..1,     2.2)
        final int[] inv = new int[MAX_COLORS]; // pow(0..1, 1./2.2)

It contains the gamma correction (quick & dirty LUT 8bits) that could be improved to 12 or 16bits ... That code already exists in DirectColorModel (tosRGB8LUT, fromsRGB8LUT8 = algorithm for linear RGB to nonlinear sRGB conversion)


Fixing the Alpha mask blending:
           for (int y = 0; y < height; y++) {
                src.getDataElements(0, y, width, 1, srcPixels);           // shape color as pixels
                dstIn.getDataElements(0, y, width, 1, dstPixels);       // background color as pixels
                dstOut.getDataElements(0, y, width, 1, maskPixels); // get alpha mask values

                for (int x = 0; x < width; x++) {
                    // pixels are stored as INT_ARGB
                    // our arrays are [R, G, B, A]
                    pixel = maskPixels[x];
                    alpha = (pixel >> 24) & 0xFF;

                    if (alpha == 255) {
                        dstPixels[x] = srcPixels[x]; // opacity = 1 => result = shape color
                    } else if (alpha != 0) { // opacity = 0 => result = background color
//                        System.out.println("alpha = " + alpha);
                       
                        // blend
                        pixel = srcPixels[x];
// Convert sRGB to linear RGB (gamma correction):
                        srcPixel[0] = gamma_dir[(pixel >> 16) & 0xFF];
                        srcPixel[1] = gamma_dir[(pixel >> 8) & 0xFF];
                        srcPixel[2] = gamma_dir[(pixel) & 0xFF];
                        srcPixel[3] = (pixel >> 24) & 0xFF;

                        pixel = dstPixels[x];
// Convert sRGB to linear RGB (gamma correction):
                        dstPixel[0] = gamma_dir[(pixel >> 16) & 0xFF];
                        dstPixel[1] = gamma_dir[(pixel >> 8) & 0xFF];
                        dstPixel[2] = gamma_dir[(pixel) & 0xFF];
                        dstPixel[3] = (pixel >> 24) & 0xFF;

// Blend linear RGB & alpha values :
                        blender.blend(srcPixel, dstPixel, alpha, result);

                        // mixes the result with the opacity
// Convert linear RGB to sRGB (inverse gamma correction):
                        dstPixels[x] = (/*result[3] & */0xFF) << 24   // discard alpha (RGBA blending to be fixed asap)
                                | gamma_inv[result[0] & 0xFF] << 16
                                | gamma_inv[result[1] & 0xFF] << 8
                                | gamma_inv[result[2] & 0xFF];
                    }
                }
// Copy pixels into raster:
                dstOut.setDataElements(0, y, width, 1, dstPixels);
            }

My very simple alpha combination uses only the alpha coverage values (not alpha from src & dst pixel) :
                      public void blend(final int[] src, final int[] dst, final int alpha, final int[] result) {
                            final float src_alpha = alpha / 255f;
                            final float comp_src_alpha = 1f - src_alpha;
                            // src & dst are gamma corrected

                            result[0] = Math.max(0, Math.min(255, (int) (src[0] * src_alpha + dst[0] * comp_src_alpha)));
                            result[1] = Math.max(0, Math.min(255, (int) (src[1] * src_alpha + dst[1] * comp_src_alpha)));
                            result[2] = Math.max(0, Math.min(255, (int) (src[2] * src_alpha + dst[2] * comp_src_alpha)));
                            result[3] = 255; /* Math.max(0, Math.min(255, (int) (255f * (src_alpha + comp_src_alpha)))) */
                        }

It could be optimized to use integer maths (not float) later... and perform other color corrections (CIE-lch interpolation ...)


To illustrate changes, look at the LineTests outputs: the line ropiness has disappeared and the antialiased lines looks better !!


--
Laurent Bourgès
GeneralCompositePipe.java
BlendComposite.java
LinesTest-gamma-corrected-maskFill.png
LinesTest-original-maskFill.png
LineTests.java
Reply all
Reply to author
Forward
0 new messages