[OpenJDK 2D-Dev] MaskFill incorrect gamma correction (sRGB != linear RGB)
Laurent Bourgès
bourges.laurent at gmail.com
Wed Aug 13 21:00:06 UTC 2014
Dear java2d-members,
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 correctly by the java2d pipeline
(AlphaComposite.srcOver) ie sRGB <=> linear RGB (blending)
Could someone confirm that gamma correction (sRGB <=> Linear RGB) is used
for normal color blending ?
2- Alpha mask filling (maskFill / maskBlit operators) is used for
antialiasing and several implementations are implemented in C (software,
opengl or xrender variants) but it seems 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 !
Maybe text rendering should be improved too ?
3- Colors are blended / mixed in RGB color space => yellow + blue = gray
issue !
I could try implementing RGB<=>CIE-Lch color mixing ... but it requires
a lot of computations (sRGB <=> Linear RGB <=> CIE-XYZ <=> CIE-LAB <=>
CIE-Lch); so I should take care to use proper approximations or cache
results (LRU color cache).
To illustrate the alpha mask filling problem (2), I hacked the
sun.java2d.GeneralCompositePipe to perform my own 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 ...) later !
To illustrate changes, look at the LineTests outputs: "ropiness" effect has
disappeared (as expeted) and the antialiased lines looks better !!
Gamma corrected mask fill:
http://apps.jmmc.fr/~bourgesl/share/MaskFill/LinesTest-gamma-corrected-maskFill.png
Original mask fill:
http://apps.jmmc.fr/~bourgesl/share/MaskFill/LinesTest-original-maskFill.png
These images were produced using the marlin-renderer (pisces fork) with
Open JDK 8.
PS: enable/disable the useCustomComposite flag in the paint() method to
enable / disable the fix !
Of course, the modified GeneralCompositePipe & BlendComposite class must be
packaged into a patch.jar and the jvm must be started with
-Xbootclasspath/p:<path>/patch.jar
Looking forward your comments,
Laurent Bourgès
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.openjdk.java.net/pipermail/2d-dev/attachments/20140813/ff6c3e1f/attachment.html>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: GeneralCompositePipe.java
Type: text/x-java
Size: 6654 bytes
Desc: not available
URL: <http://mail.openjdk.java.net/pipermail/2d-dev/attachments/20140813/ff6c3e1f/GeneralCompositePipe.java>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: BlendComposite.java
Type: text/x-java
Size: 9624 bytes
Desc: not available
URL: <http://mail.openjdk.java.net/pipermail/2d-dev/attachments/20140813/ff6c3e1f/BlendComposite.java>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: LineTests.java
Type: text/x-java
Size: 7422 bytes
Desc: not available
URL: <http://mail.openjdk.java.net/pipermail/2d-dev/attachments/20140813/ff6c3e1f/LineTests.java>
More information about the 2d-dev
mailing list