From 976e5d621092b7f155c526b1eb0c08c4517df182 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 26 Aug 2021 16:47:51 +0200 Subject: [PATCH] #619: Fix WebP Y'CbCr->RGB conversion (now uses rec 601) --- .../imageio/color/YCbCrConverter.java | 118 +++++++++++++----- .../imageio/plugins/jpeg/EXIFThumbnail.java | 2 +- .../imageio/plugins/jpeg/JPEGImageReader.java | 8 +- .../imageio/plugins/tiff/TIFFImageReader.java | 3 +- .../imageio/plugins/webp/WebPImageReader.java | 13 +- .../plugins/webp/lossless/VP8LDecoder.java | 7 +- .../imageio/plugins/webp/vp8/VP8Frame.java | 13 +- .../plugins/webp/WebPImageReaderTest.java | 19 ++- .../src/test/resources/webp/blue_tile.webp | Bin 0 -> 190 bytes 9 files changed, 120 insertions(+), 63 deletions(-) create mode 100644 imageio/imageio-webp/src/test/resources/webp/blue_tile.webp diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/YCbCrConverter.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/YCbCrConverter.java index aedb1231..adf9b4e0 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/YCbCrConverter.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/YCbCrConverter.java @@ -45,36 +45,82 @@ public final class YCbCrConverter { 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]; + 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]; + 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"); + /** + * Initializes tables for YCC->RGB color space conversion. + */ + private static void buildYCCtoRGBtable() { + if (ColorSpaces.DEBUG) { + System.err.println("Building JPEG 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 + // 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; + } } - 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(); } } - 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) { @@ -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) { - int y = yCbCr[offset] & 0xff; - int cr = yCbCr[offset + 2] & 0xff; + public static void convertJPEGYCbCr2RGB(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(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)); } } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnail.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnail.java index 0cdece31..3485f8e2 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnail.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnail.java @@ -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: diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 43934b99..19540366 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -123,7 +123,7 @@ public final class JPEGImageReader extends ImageReaderBase { private int currentStreamIndex = 0; private final List 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); } diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java index 986dc906..0e89452c 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java @@ -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 { diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java index 91d3279a..e984d8db 100644 --- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java +++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReader.java @@ -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: diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/VP8LDecoder.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/VP8LDecoder.java index d25c3ee6..be067cf9 100644 --- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/VP8LDecoder.java +++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/lossless/VP8LDecoder.java @@ -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) { diff --git a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/vp8/VP8Frame.java b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/vp8/VP8Frame.java index 7ab7a3c5..2ba4cace 100644 --- a/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/vp8/VP8Frame.java +++ b/imageio/imageio-webp/src/main/java/com/twelvemonkeys/imageio/plugins/webp/vp8/VP8Frame.java @@ -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); } } } diff --git a/imageio/imageio-webp/src/test/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReaderTest.java b/imageio/imageio-webp/src/test/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReaderTest.java index 57751bdb..d4277562 100644 --- a/imageio/imageio-webp/src/test/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReaderTest.java +++ b/imageio/imageio-webp/src/test/java/com/twelvemonkeys/imageio/plugins/webp/WebPImageReaderTest.java @@ -75,13 +75,28 @@ public class WebPImageReaderTest extends ImageReaderAbstractTest RGB conversion", 0xFF72AED5, image.getRGB(80, 80), 1); } finally { reader.dispose(); diff --git a/imageio/imageio-webp/src/test/resources/webp/blue_tile.webp b/imageio/imageio-webp/src/test/resources/webp/blue_tile.webp new file mode 100644 index 0000000000000000000000000000000000000000..23568be624f37aba7f8f6af88dffbcf03bffc186 GIT binary patch literal 190 zcmWIYbaUIrz`zjh>J$(bV4<)I$lf5pFqct_fsujHF4t_PXA@Hc2g8kN)82_^3%)(T zz-Zf0{dV$%@3++z-*5ACzMnIvecw$>{@P7Na=%la*nT^Cf~wLQ4FCQ+bN~M*v+~Q^ RH}(H-Y@T*tBhZZy006{0TAKg> literal 0 HcmV?d00001