#619: Fix WebP Y'CbCr->RGB conversion (now uses rec 601)

This commit is contained in:
Harald Kuhr 2021-08-26 16:47:51 +02:00
parent 6daca00fcd
commit 976e5d6210
9 changed files with 120 additions and 63 deletions

View File

@ -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) {
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));
}
}

View File

@ -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:

View File

@ -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);
}

View File

@ -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 {

View File

@ -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:

View File

@ -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) {

View File

@ -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);
}
}
}

View File

@ -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();

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B