mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-04 20:15:28 -04:00
#619: Fix WebP Y'CbCr->RGB conversion (now uses rec 601)
(cherry picked from commit 976e5d621092b7f155c526b1eb0c08c4517df182)
This commit is contained in:
parent
ca3adb7c45
commit
ed46305d31
@ -45,6 +45,7 @@ public final class YCbCrConverter {
|
||||
private final static int CENTERJSAMPLE = 128;
|
||||
private final static int ONE_HALF = 1 << (SCALEBITS - 1);
|
||||
|
||||
private final static class JPEG {
|
||||
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];
|
||||
@ -55,7 +56,7 @@ public final class YCbCrConverter {
|
||||
*/
|
||||
private static void buildYCCtoRGBtable() {
|
||||
if (ColorSpaces.DEBUG) {
|
||||
System.err.println("Building YCC conversion table");
|
||||
System.err.println("Building JPEG YCbCr conversion table");
|
||||
}
|
||||
|
||||
for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) {
|
||||
@ -76,6 +77,51 @@ public final class YCbCrConverter {
|
||||
static {
|
||||
buildYCCtoRGBtable();
|
||||
}
|
||||
}
|
||||
|
||||
private final static class ITU_R_601 {
|
||||
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];
|
||||
private final static int[] Y_LUT = new int[MAXJSAMPLE + 1];
|
||||
|
||||
/**
|
||||
* Initializes tables for YCC->RGB color space conversion.
|
||||
*/
|
||||
private static void buildYCCtoRGBtable() {
|
||||
if (ColorSpaces.DEBUG) {
|
||||
System.err.println("Building ITU-R REC.601 YCbCr 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
|
||||
|
||||
// Y'CbCr to RGB conversion, using values from BT.601 specification:
|
||||
// R = 1.16438 * (Y'-16) + 1.59603 * (Cr-128)
|
||||
// G = 1.16438 * (Y'-16) - 0.39176 * (Cb-128) - 0.81297 * (Cr-128)
|
||||
// B = 1.16438 * (Y'-16) + 2.01723 * (Cb-128)
|
||||
|
||||
// Cr=>R value is nearest int to 1.59603 * x
|
||||
Cr_R_LUT[i] = ((int) (1.59603 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
|
||||
// Cb=>B value is nearest int to 2.01723 * x
|
||||
Cb_B_LUT[i] = ((int) (2.01723 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
|
||||
// Cr=>G value is scaled-up -0.81297 * x
|
||||
Cr_G_LUT[i] = -(int) (0.81297 * (1 << SCALEBITS) + 0.5) * x;
|
||||
// Cb=>G value is scaled-up -0.39176 * x
|
||||
// We also add in ONE_HALF so that need not do it in inner loop
|
||||
Cb_G_LUT[i] = -(int) ((0.39176) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF;
|
||||
|
||||
// Y`=>RGB
|
||||
Y_LUT[i] = ((int) (1.16438 * (1 << SCALEBITS) + 0.5) * (i - 16) + ONE_HALF) >> SCALEBITS;
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
buildYCCtoRGBtable();
|
||||
}
|
||||
}
|
||||
|
||||
public static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, double[] referenceBW, final int offset) {
|
||||
double y;
|
||||
@ -108,17 +154,27 @@ public final class YCbCrConverter {
|
||||
rgb[offset + 1] = clamp(green);
|
||||
}
|
||||
|
||||
public static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) {
|
||||
public static void convertJPEGYCbCr2RGB(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;
|
||||
int cr = yCbCr[offset + 2] & 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]);
|
||||
rgb[offset ] = clamp(y + JPEG.Cr_R_LUT[cr]);
|
||||
rgb[offset + 1] = clamp(y + (JPEG.Cb_G_LUT[cb] + JPEG.Cr_G_LUT[cr] >> SCALEBITS));
|
||||
rgb[offset + 2] = clamp(y + JPEG.Cb_B_LUT[cb]);
|
||||
}
|
||||
|
||||
private static byte clamp(int val) {
|
||||
public static void convertRec601YCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) {
|
||||
int y = yCbCr[offset ] & 0xff;
|
||||
int cb = yCbCr[offset + 1] & 0xff;
|
||||
int cr = yCbCr[offset + 2] & 0xff;
|
||||
|
||||
rgb[offset ] = clamp(ITU_R_601.Y_LUT[y] + ITU_R_601.Cr_R_LUT[cr]);
|
||||
rgb[offset + 1] = clamp(ITU_R_601.Y_LUT[y] + (ITU_R_601.Cr_G_LUT[cr] + ITU_R_601.Cb_G_LUT[cb] >> SCALEBITS));
|
||||
rgb[offset + 2] = clamp(ITU_R_601.Y_LUT[y] + ITU_R_601.Cb_B_LUT[cb]);
|
||||
}
|
||||
|
||||
private static byte clamp(final int val) {
|
||||
return (byte) Math.max(0, Math.min(255, val));
|
||||
}
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ final class EXIFThumbnail {
|
||||
case 6:
|
||||
// YCbCr
|
||||
for (int i = 0; i < thumbLength; i += 3) {
|
||||
YCbCrConverter.convertYCbCr2RGB(thumbData, thumbData, i);
|
||||
YCbCrConverter.convertJPEGYCbCr2RGB(thumbData, thumbData, i);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
@ -123,7 +123,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
||||
private int currentStreamIndex = 0;
|
||||
private final List<Long> streamOffsets = new ArrayList<>();
|
||||
|
||||
protected JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) {
|
||||
JPEGImageReader(final ImageReaderSpi provider, final ImageReader delegate) {
|
||||
super(provider);
|
||||
|
||||
this.delegate = Validate.notNull(delegate);
|
||||
@ -1169,7 +1169,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
||||
processThumbnailStarted(imageIndex, thumbnailIndex);
|
||||
processThumbnailProgress(0f);
|
||||
|
||||
BufferedImage thumbnail = thumbnails.get(thumbnailIndex).read();;
|
||||
BufferedImage thumbnail = thumbnails.get(thumbnailIndex).read();
|
||||
|
||||
processThumbnailProgress(100f);
|
||||
processThumbnailComplete();
|
||||
@ -1211,7 +1211,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
YCbCrConverter.convertYCbCr2RGB(data, data, (x + y * width) * numComponents);
|
||||
YCbCrConverter.convertJPEGYCbCr2RGB(data, data, (x + y * width) * numComponents);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1225,7 +1225,7 @@ public final class JPEGImageReader extends ImageReaderBase {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int offset = (x + y * width) * 4;
|
||||
// YCC -> CMY
|
||||
YCbCrConverter.convertYCbCr2RGB(data, data, offset);
|
||||
YCbCrConverter.convertJPEGYCbCr2RGB(data, data, offset);
|
||||
// Inverse K
|
||||
data[offset + 3] = (byte) (0xff - data[offset + 3] & 0xff);
|
||||
}
|
||||
|
@ -2136,7 +2136,8 @@ public final class TIFFImageReader extends ImageReaderBase {
|
||||
&& (referenceBW == null || Arrays.equals(referenceBW, REFERENCE_BLACK_WHITE_YCC_DEFAULT))) {
|
||||
// Fast, default conversion
|
||||
for (int i = 0; i < data.length; i += 3) {
|
||||
YCbCrConverter.convertYCbCr2RGB(data, data, i);
|
||||
// TODO: The default is likely neither JPEG or rec 601, as the reference B/W doesn't match...
|
||||
YCbCrConverter.convertJPEGYCbCr2RGB(data, data, i);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@ -86,15 +86,7 @@ final class WebPImageReader extends ImageReaderBase {
|
||||
|
||||
@Override
|
||||
public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) {
|
||||
// TODO: Figure out why this makes the reader order of magnitudes faster (2-3x?)
|
||||
// ...or, how to make VP8 decoder make longer reads/make a better FileImageInputStream...
|
||||
super.setInput(input, seekForwardOnly, ignoreMetadata);
|
||||
// try {
|
||||
// super.setInput(new BufferedImageInputStream((ImageInputStream) input), seekForwardOnly, ignoreMetadata);
|
||||
// }
|
||||
// catch (IOException e) {
|
||||
// throw new IOError(e);
|
||||
// }
|
||||
|
||||
lsbBitReader = new LSBBitReader(imageInput);
|
||||
}
|
||||
@ -344,7 +336,7 @@ final class WebPImageReader extends ImageReaderBase {
|
||||
int reserved = (int) imageInput.readBits(2);
|
||||
if (reserved != 0) {
|
||||
// Spec says SHOULD be 0
|
||||
throw new IIOException(String.format("Unexpected 'ALPH' chunk reserved value, expected 0: %d", reserved));
|
||||
processWarningOccurred(String.format("Unexpected 'ALPH' chunk reserved value, expected 0: %d", reserved));
|
||||
}
|
||||
|
||||
int preProcessing = (int) imageInput.readBits(2);
|
||||
@ -384,6 +376,9 @@ final class WebPImageReader extends ImageReaderBase {
|
||||
|
||||
case WebP.CHUNK_ICCP:
|
||||
// Ignore, we already read this
|
||||
case WebP.CHUNK_EXIF:
|
||||
case WebP.CHUNK_XMP_:
|
||||
// Ignore, we'll read this later
|
||||
break;
|
||||
|
||||
case WebP.CHUNK_ANIM:
|
||||
|
@ -259,12 +259,7 @@ public final class VP8LDecoder {
|
||||
abs(pGreen - GREEN(T)) + abs(pBlue - BLUE(T));
|
||||
|
||||
// Return either left or top, the one closer to the prediction.
|
||||
if (pL < pT) {
|
||||
return L;
|
||||
}
|
||||
else {
|
||||
return T;
|
||||
}
|
||||
return pL < pT ? L : T;
|
||||
}
|
||||
|
||||
private static int average2(final int a, final int b) {
|
||||
|
@ -42,7 +42,7 @@ import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.twelvemonkeys.imageio.color.YCbCrConverter.convertYCbCr2RGB;
|
||||
import static com.twelvemonkeys.imageio.color.YCbCrConverter.convertRec601YCbCr2RGB;
|
||||
|
||||
public final class VP8Frame {
|
||||
private static final int BLOCK_TYPES = 4;
|
||||
@ -54,8 +54,6 @@ public final class VP8Frame {
|
||||
|
||||
private IIOReadProgressListener listener = null;
|
||||
|
||||
// private int bufferCount;
|
||||
// private int buffersToCreate = 1;
|
||||
private final int[][][][] coefProbs;
|
||||
private int filterLevel;
|
||||
|
||||
@ -117,7 +115,6 @@ public final class VP8Frame {
|
||||
|
||||
int c = frame.readUnsignedByte();
|
||||
frameType = getBitAsInt(c, 0);
|
||||
// logger.log("Frame type: " + frameType);
|
||||
|
||||
if (frameType != 0) {
|
||||
return false;
|
||||
@ -478,7 +475,6 @@ public final class VP8Frame {
|
||||
}
|
||||
|
||||
public BufferedImage getDebugImageDiff() {
|
||||
|
||||
BufferedImage bi = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
|
||||
WritableRaster imRas = bi.getWritableTile(0, 0);
|
||||
for (int x = 0; x < getWidth(); x++) {
|
||||
@ -1037,12 +1033,12 @@ public final class VP8Frame {
|
||||
int num_part = 1 << multiTokenPartition;
|
||||
|
||||
if (num_part > 1) {
|
||||
partition += 3 * (num_part - 1);
|
||||
partition += 3L * (num_part - 1);
|
||||
}
|
||||
for (int i = 0; i < num_part; i++) {
|
||||
// Calculate the length of this partition. The last partition size is implicit.
|
||||
if (i < num_part - 1) {
|
||||
partitionSize = readPartitionSize(partitionsStart + (i * 3));
|
||||
partitionSize = readPartitionSize(partitionsStart + (i * 3L));
|
||||
bc.seek();
|
||||
}
|
||||
else {
|
||||
@ -1084,9 +1080,8 @@ public final class VP8Frame {
|
||||
yuv[2] = (byte) macroBlock.getSubBlock(SubBlock.Plane.V, (x / 2) / 4, (y / 2) / 4).getDest()[(x / 2) % 4][(y / 2) % 4];
|
||||
|
||||
// TODO: Consider doing YCbCr -> RGB in reader instead, or pass a flag to allow readRaster reading direct YUV/YCbCr values
|
||||
convertYCbCr2RGB(yuv, rgb, 0);
|
||||
convertRec601YCbCr2RGB(yuv, rgb, 0);
|
||||
byteRGBRaster.setDataElements(dstX, dstY, rgb);
|
||||
// byteRGBRaster.setDataElements(dstX, dstY, yuv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -75,13 +75,28 @@ public class WebPImageReaderTest extends ImageReaderAbstractTest<WebPImageReader
|
||||
try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/webp/photo-iccp-adobergb.webp"))) {
|
||||
reader.setInput(stream);
|
||||
|
||||
// We'll read a small portion of the image into a a destination type that use sRGB
|
||||
// We'll read a small portion of the image into a destination type that use sRGB
|
||||
ImageReadParam param = new ImageReadParam();
|
||||
param.setDestinationType(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR));
|
||||
param.setSourceRegion(new Rectangle(20, 20));
|
||||
|
||||
BufferedImage image = reader.read(0, param);
|
||||
assertRGBEquals("RGB values differ, incorrect ICC profile or conversion?", 0XFFDC9100, image.getRGB(10, 10), 10);
|
||||
assertRGBEquals("RGB values differ, incorrect ICC profile or conversion?", 0xFFEA9600, image.getRGB(10, 10), 8);
|
||||
}
|
||||
finally {
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRec601ColorConversion() throws IOException {
|
||||
WebPImageReader reader = createReader();
|
||||
|
||||
try (ImageInputStream stream = ImageIO.createImageInputStream(getClassLoaderResource("/webp/blue_tile.webp"))) {
|
||||
reader.setInput(stream);
|
||||
|
||||
BufferedImage image = reader.read(0, null);
|
||||
assertRGBEquals("RGB values differ, incorrect Y'CbCr -> RGB conversion", 0xFF72AED5, image.getRGB(80, 80), 1);
|
||||
}
|
||||
finally {
|
||||
reader.dispose();
|
||||
|
BIN
imageio/imageio-webp/src/test/resources/webp/blue_tile.webp
Normal file
BIN
imageio/imageio-webp/src/test/resources/webp/blue_tile.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 190 B |
Loading…
x
Reference in New Issue
Block a user