#173 Support for PhotometricInterpretation 8/CIELab, 9/ICCLab and 10/ITULab

This commit is contained in:
Harald Kuhr
2015-10-22 21:35:10 +02:00
parent 30a7283b35
commit 8a38b2fde6
13 changed files with 658 additions and 436 deletions

View File

@@ -0,0 +1,152 @@
package com.twelvemonkeys.imageio.color;
import com.twelvemonkeys.lang.Validate;
/**
* Converts between CIE L*a*b* and sRGB color spaces.
*/
// Code adapted from ImageJ's Color_Space_Converter.java (Public Domain):
// http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java
public final class CIELabColorConverter {
// TODO: Add tests
// TODO: Create interface in the color package?
// TODO: Make YCbCr/YCCK -> RGB/CMYK implement same interface?
public enum Illuminant {
D50(new float[] {96.4212f, 100.0f, 82.5188f}),
D65(new float[] {95.0429f, 100.0f, 108.8900f});
private final float[] whitePoint;
Illuminant(final float[] wp) {
whitePoint = Validate.isTrue(wp != null && wp.length == 3, wp, "Bad white point definition: %s");
}
public float[] getWhitePoint() {
return whitePoint;
}
}
private final float[] whitePoint;
public CIELabColorConverter(final Illuminant illuminant) {
whitePoint = Validate.notNull(illuminant, "illuminant").getWhitePoint();
}
private float clamp(float x) {
if (x < 0.0f) {
return 0.0f;
}
else if (x > 255.0f) {
return 255.0f;
}
else {
return x;
}
}
public void toRGB(float l, float a, float b, float[] rgbResult) {
XYZtoRGB(LABtoXYZ(l, a, b, rgbResult), rgbResult);
}
/**
* Convert LAB to XYZ.
* @param L
* @param a
* @param b
* @return XYZ values
*/
private float[] LABtoXYZ(float L, float a, float b, float[] xyzResult) {
// Significant speedup: Removing Math.pow
float y = (L + 16.0f) / 116.0f;
float y3 = y * y * y; // Math.pow(y, 3.0);
float x = (a / 500.0f) + y;
float x3 = x * x * x; // Math.pow(x, 3.0);
float z = y - (b / 200.0f);
float z3 = z * z * z; // Math.pow(z, 3.0);
if (y3 > 0.008856f) {
y = y3;
}
else {
y = (y - (16.0f / 116.0f)) / 7.787f;
}
if (x3 > 0.008856f) {
x = x3;
}
else {
x = (x - (16.0f / 116.0f)) / 7.787f;
}
if (z3 > 0.008856f) {
z = z3;
}
else {
z = (z - (16.0f / 116.0f)) / 7.787f;
}
xyzResult[0] = x * whitePoint[0];
xyzResult[1] = y * whitePoint[1];
xyzResult[2] = z * whitePoint[2];
return xyzResult;
}
/**
* Convert XYZ to RGB
* @param xyz
* @return RGB values
*/
private float[] XYZtoRGB(final float[] xyz, final float[] rgbResult) {
return XYZtoRGB(xyz[0], xyz[1], xyz[2], rgbResult);
}
private float[] XYZtoRGB(final float X, final float Y, final float Z, float[] rgbResult) {
float x = X / 100.0f;
float y = Y / 100.0f;
float z = Z / 100.0f;
float r = x * 3.2406f + y * -1.5372f + z * -0.4986f;
float g = x * -0.9689f + y * 1.8758f + z * 0.0415f;
float b = x * 0.0557f + y * -0.2040f + z * 1.0570f;
// assume sRGB
if (r > 0.0031308f) {
r = ((1.055f * (float) pow(r, 1.0 / 2.4)) - 0.055f);
}
else {
r = (r * 12.92f);
}
if (g > 0.0031308f) {
g = ((1.055f * (float) pow(g, 1.0 / 2.4)) - 0.055f);
}
else {
g = (g * 12.92f);
}
if (b > 0.0031308f) {
b = ((1.055f * (float) pow(b, 1.0 / 2.4)) - 0.055f);
}
else {
b = (b * 12.92f);
}
// convert 0..1 into 0..255
rgbResult[0] = clamp(r * 255);
rgbResult[1] = clamp(g * 255);
rgbResult[2] = clamp(b * 255);
return rgbResult;
}
// TODO: Test, to figure out if accuracy is good enough.
// Visual inspection looks good! The author claims 5-12% error, worst case up to 25%...
// http://martin.ankerl.com/2007/10/04/optimized-pow-approximation-for-java-and-c-c/
static double pow(final double a, final double b) {
long tmp = Double.doubleToLongBits(a);
long tmp2 = (long) (b * (tmp - 4606921280493453312L)) + 4606921280493453312L;
return Double.longBitsToDouble(tmp2);
}
}

View File

@@ -0,0 +1,81 @@
package com.twelvemonkeys.imageio.color;
/**
* Fast YCbCr to RGB conversion.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author Original code by Werner Randelshofer (used by permission).
*/
public final class YCbCrConverter {
/**
* Define tables for YCC->RGB color space conversion.
*/
private final static int SCALEBITS = 16;
private final static int MAXJSAMPLE = 255;
private final static int CENTERJSAMPLE = 128;
private final static int ONE_HALF = 1 << (SCALEBITS - 1);
private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1];
private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1];
/**
* Initializes tables for YCC->RGB color space conversion.
*/
private static void buildYCCtoRGBtable() {
if (ColorSpaces.DEBUG) {
System.err.println("Building YCC conversion table");
}
for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) {
// i is the actual input pixel value, in the range 0..MAXJSAMPLE
// The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE
// Cr=>R value is nearest int to 1.40200 * x
Cr_R_LUT[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
// Cb=>B value is nearest int to 1.77200 * x
Cb_B_LUT[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
// Cr=>G value is scaled-up -0.71414 * x
Cr_G_LUT[i] = -(int) (0.71414 * (1 << SCALEBITS) + 0.5) * x;
// Cb=>G value is scaled-up -0.34414 * x
// We also add in ONE_HALF so that need not do it in inner loop
Cb_G_LUT[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF;
}
}
static {
buildYCCtoRGBtable();
}
public static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) {
double y = (yCbCr[offset] & 0xff);
double cb = (yCbCr[offset + 1] & 0xff) - 128;
double cr = (yCbCr[offset + 2] & 0xff) - 128;
double lumaRed = coefficients[0];
double lumaGreen = coefficients[1];
double lumaBlue = coefficients[2];
int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y);
int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y);
int green = (int) Math.round((y - lumaRed * red - lumaBlue * blue) / lumaGreen);
rgb[offset] = clamp(red);
rgb[offset + 2] = clamp(blue);
rgb[offset + 1] = clamp(green);
}
public static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) {
int y = yCbCr[offset] & 0xff;
int cr = yCbCr[offset + 2] & 0xff;
int cb = yCbCr[offset + 1] & 0xff;
rgb[offset] = clamp(y + Cr_R_LUT[cr]);
rgb[offset + 1] = clamp(y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
rgb[offset + 2] = clamp(y + Cb_B_LUT[cb]);
}
private static byte clamp(int val) {
return (byte) Math.max(0, Math.min(255, val));
}
}

View File

@@ -0,0 +1,68 @@
package com.twelvemonkeys.imageio.color;
import com.twelvemonkeys.imageio.color.CIELabColorConverter.Illuminant;
import org.junit.Test;
import static org.junit.Assert.assertArrayEquals;
/**
* CIELabColorConverterTest.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: CIELabColorConverterTest.java,v 1.0 22/10/15 harald.kuhr Exp$
*/
public class CIELabColorConverterTest {
@Test(expected = IllegalArgumentException.class)
public void testNoIllumninant() {
new CIELabColorConverter(null);
}
@Test
public void testD50() {
CIELabColorConverter converter = new CIELabColorConverter(Illuminant.D50);
float[] rgb = new float[3];
converter.toRGB(100, -128, -128, rgb);
assertArrayEquals(new float[] {0, 255, 255}, rgb, 1);
converter.toRGB(100, 0, 0, rgb);
assertArrayEquals(new float[] {255, 252, 220}, rgb, 5);
converter.toRGB(0, 0, 0, rgb);
assertArrayEquals(new float[] {0, 0, 0}, rgb, 1);
converter.toRGB(100, 0, 127, rgb);
assertArrayEquals(new float[] {255, 249, 0}, rgb, 5);
converter.toRGB(50, -128, 127, rgb);
assertArrayEquals(new float[] {0, 152, 0}, rgb, 2);
converter.toRGB(50, 127, -128, rgb);
assertArrayEquals(new float[] {222, 0, 255}, rgb, 2);
}
@Test
public void testD65() {
CIELabColorConverter converter = new CIELabColorConverter(Illuminant.D65);
float[] rgb = new float[3];
converter.toRGB(100, -128, -128, rgb);
assertArrayEquals(new float[] {0, 255, 255}, rgb, 1);
converter.toRGB(100, 0, 0, rgb);
assertArrayEquals(new float[] {255, 252, 255}, rgb, 5);
converter.toRGB(0, 0, 0, rgb);
assertArrayEquals(new float[] {0, 0, 0}, rgb, 1);
converter.toRGB(100, 0, 127, rgb);
assertArrayEquals(new float[] {255, 250, 0}, rgb, 5);
converter.toRGB(50, -128, 127, rgb);
assertArrayEquals(new float[] {0, 152, 0}, rgb, 3);
converter.toRGB(50, 127, -128, rgb);
assertArrayEquals(new float[] {184, 0, 255}, rgb, 5);
}
}