#678, #679: TIFF read support for YCbCr Planar with or without subsampling

This commit is contained in:
Harald Kuhr 2022-05-12 23:01:12 +02:00
parent 8c85c4ca96
commit 44eebff62f
15 changed files with 357 additions and 26 deletions

View File

@ -173,10 +173,14 @@ public final class TIFFEntry extends AbstractEntry {
return "TileByteCounts";
case TIFF.TAG_COPYRIGHT:
return "Copyright";
case TIFF.TAG_YCBCR_COEFFICIENTS:
return "YCbCrCoefficients";
case TIFF.TAG_YCBCR_SUB_SAMPLING:
return "YCbCrSubSampling";
case TIFF.TAG_YCBCR_POSITIONING:
return "YCbCrPositioning";
case TIFF.TAG_REFERENCE_BLACK_WHITE:
return "ReferenceBlackWhite";
case TIFF.TAG_COLOR_MAP:
return "ColorMap";
case TIFF.TAG_INK_SET:

View File

@ -1092,19 +1092,14 @@ public final class TIFFImageReader extends ImageReaderBase {
: createStreamAdapter(imageInput);
adapter = createFillOrderStream(fillOrder, adapter);
adapter = createDecompressorStream(compression, stripTileWidth, numBands, adapter);
adapter = createUnpredictorStream(predictor, stripTileWidth, numBands, bitsPerSample, adapter, imageInput.getByteOrder());
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR && rowRaster.getTransferType() == DataBuffer.TYPE_BYTE) {
adapter = new YCbCrUpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile);
}
else if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR && rowRaster.getTransferType() == DataBuffer.TYPE_USHORT) {
adapter = new YCbCr16UpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile, imageInput.getByteOrder());
}
else if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
// Handled in getRawImageType
throw new AssertionError();
}
// For subsampled planar, the compressed data will not be full width
int compressedStripTileWidth = planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR && b > 0 && yCbCrSubsampling != null
? ((stripTileWidth + yCbCrSubsampling[0] - 1) / yCbCrSubsampling[0])
: stripTileWidth;
adapter = createDecompressorStream(compression, compressedStripTileWidth, numBands, adapter);
adapter = createUnpredictorStream(predictor, compressedStripTileWidth, numBands, bitsPerSample, adapter, imageInput.getByteOrder());
adapter = createYCbCrUpsamplerStream(interpretation, planarConfiguration, b, rowRaster.getTransferType(), yCbCrSubsampling, yCbCrPos, colsInTile, adapter, imageInput.getByteOrder());
if (needsBitPadding) {
// We'll pad "odd" bitsPerSample streams to the smallest data type (byte/short/int) larger than the input
@ -1129,6 +1124,11 @@ public final class TIFFImageReader extends ImageReaderBase {
readStripTileData(clippedRow, srcRegion, xSub, ySub, b, numBands, interpretation, destRaster, col, srcRow, colsInTile, rowsInTile, input);
}
// Need to do color normalization after reading all bands for planar
if (planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR) {
normalizeColorPlanar(interpretation, destRaster);
}
col += colsInTile;
if (abortRequested()) {
@ -1582,8 +1582,8 @@ public final class TIFFImageReader extends ImageReaderBase {
case TIFFCustom.COMPRESSION_SGILOG:
case TIFFCustom.COMPRESSION_SGILOG24:
case TIFFCustom.COMPRESSION_JPEG2000: // Doable with JPEG2000 plugin?
throw new IIOException("Unsupported TIFF Compression value: " + compression);
default:
throw new IIOException("Unknown TIFF Compression value: " + compression);
}
@ -1595,6 +1595,28 @@ public final class TIFFImageReader extends ImageReaderBase {
return destination;
}
private InputStream createYCbCrUpsamplerStream(int photometricInterpretation, int planarConfiguration, int plane, int transferType,
int[] yCbCrSubsampling, int yCbCrPos, int colsInTile, InputStream stream, ByteOrder byteOrder) {
if (photometricInterpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
if (planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR && transferType == DataBuffer.TYPE_BYTE) {
// For planar YCbCr, only the chroma planes are subsampled
return plane > 0 && (yCbCrSubsampling[0] != 1 || yCbCrSubsampling[1] != 1)
? new YCbCrPlanarUpsamplerStream(stream, yCbCrSubsampling, yCbCrPos, colsInTile) : stream;
}
else if (transferType == DataBuffer.TYPE_BYTE) {
return new YCbCrUpsamplerStream(stream, yCbCrSubsampling, yCbCrPos, colsInTile);
}
else if (transferType == DataBuffer.TYPE_USHORT) {
return new YCbCr16UpsamplerStream(stream, yCbCrSubsampling, yCbCrPos, colsInTile, byteOrder);
}
// Handled in getRawImageType
throw new AssertionError();
}
return stream;
}
private boolean containsZero(long[] byteCounts) {
for (long byteCount : byteCounts) {
if (byteCount <= 0) {
@ -1919,12 +1941,8 @@ public final class TIFFImageReader extends ImageReaderBase {
}
}
// if (banded) {
// // TODO: Normalize colors for tile (need to know tile region and sample model)
// // Unfortunately, this will disable acceleration...
// }
break;
case DataBuffer.TYPE_USHORT:
case DataBuffer.TYPE_SHORT:
/*for (int band = 0; band < bands; band++)*/ {
@ -1962,6 +1980,7 @@ public final class TIFFImageReader extends ImageReaderBase {
}
break;
case DataBuffer.TYPE_INT:
/*for (int band = 0; band < bands; band++)*/ {
int[] rowDataInt = ((DataBufferInt) dataBuffer).getData(band);
@ -2101,6 +2120,102 @@ public final class TIFFImageReader extends ImageReaderBase {
}
}
private void normalizeColorPlanar(int photometricInterpretation, WritableRaster raster) throws IIOException {
// TODO: Other transfer types?
if (raster.getTransferType() != DataBuffer.TYPE_BYTE) {
return;
}
byte[] pixel = null;
switch (photometricInterpretation) {
case TIFFExtension.PHOTOMETRIC_YCBCR:
// Default: CCIR Recommendation 601-1: 299/1000, 587/1000 and 114/1000
double[] coefficients = getValueAsDoubleArray(TIFF.TAG_YCBCR_COEFFICIENTS, "YCbCrCoefficients", false, 3);
// "Default" [0, 255, 128, 255, 128, 255] for YCbCr (real default is [0, 255, 0, 255, 0, 255] for RGB)
double[] referenceBW = getValueAsDoubleArray(TIFF.TAG_REFERENCE_BLACK_WHITE, "ReferenceBlackWhite", false, 6);
if ((coefficients == null || Arrays.equals(coefficients, CCIR_601_1_COEFFICIENTS))
&& (referenceBW == null || Arrays.equals(referenceBW, REFERENCE_BLACK_WHITE_YCC_DEFAULT))) {
// Fast, default conversion
for (int y = 0; y < raster.getHeight(); y++) {
for (int x = 0; x < raster.getWidth(); x++) {
pixel = (byte[]) raster.getDataElements(x, y, pixel);
YCbCrConverter.convertJPEGYCbCr2RGB(pixel, pixel, 0);
raster.setDataElements(x, y, pixel);
}
}
}
else {
// If one of the values are null, we'll need the other here...
if (coefficients == null) {
coefficients = CCIR_601_1_COEFFICIENTS;
}
if (referenceBW != null && Arrays.equals(referenceBW, REFERENCE_BLACK_WHITE_YCC_DEFAULT)) {
referenceBW = null;
}
for (int y = 0; y < raster.getHeight(); y++) {
for (int x = 0; x < raster.getWidth(); x++) {
pixel = (byte[]) raster.getDataElements(x, y, pixel);
YCbCrConverter.convertYCbCr2RGB(pixel, pixel, coefficients, referenceBW, 0);
raster.setDataElements(x, y, pixel);
}
}
}
break;
case TIFFExtension.PHOTOMETRIC_CIELAB:
case TIFFExtension.PHOTOMETRIC_ICCLAB:
case TIFFExtension.PHOTOMETRIC_ITULAB:
// TODO: White point may be encoded in separate tag
CIELabColorConverter converter = new CIELabColorConverter(
photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB
? Illuminant.D65
: Illuminant.D50
);
float[] temp = new float[3];
for (int y = 0; y < raster.getHeight(); y++) {
for (int x = 0; x < raster.getWidth(); x++) {
pixel = (byte[]) raster.getDataElements(x, y, pixel);
float LStar = (pixel[0] & 0xff) * 100f / 255.0f;
float aStar;
float bStar;
if (photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB) {
// -128...127
aStar = pixel[1];
bStar = pixel[2];
}
else {
// Assumes same data for ICC and ITU (unsigned)
// 0...255
aStar = (pixel[1] & 0xff) - 128;
bStar = (pixel[2] & 0xff) - 128;
}
converter.toRGB(LStar, aStar, bStar, temp);
pixel[0] = (byte) temp[0];
pixel[1] = (byte) temp[1];
pixel[2] = (byte) temp[2];
raster.setDataElements(x, y, pixel);
}
}
break;
}
}
private void normalizeColor(int photometricInterpretation, byte[] data) throws IOException {
switch (photometricInterpretation) {
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
@ -2705,8 +2820,14 @@ public final class TIFFImageReader extends ImageReaderBase {
try {
long start = System.currentTimeMillis();
int width = reader.getWidth(imageNo);
int height = reader.getHeight(imageNo);
// int width = reader.getWidth(imageNo);
// int height = reader.getHeight(imageNo);
if (param.canSetSourceRenderSize()) {
int thumbSize = 512;
float aspectRatio = reader.getAspectRatio(imageNo);
param.setSourceRenderSize(aspectRatio > 1f ? new Dimension(thumbSize, (int) Math.ceil(thumbSize / aspectRatio))
: new Dimension((int) Math.ceil(thumbSize * aspectRatio), thumbSize));
}
// param.setSourceRegion(new Rectangle(width / 4, height / 4, width / 2, height / 2));
// param.setSourceRegion(new Rectangle(100, 300, 400, 400));
// param.setSourceRegion(new Rectangle(95, 105, 100, 100));

View File

@ -0,0 +1,198 @@
/*
* Copyright (c) 2022, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.lang.Validate;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Input stream that provides on-the-fly upsampling of TIFF subsampled YCbCr samples.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: YCbCrUpsamplerStream.java,v 1.0 31.01.13 09:25 haraldk Exp$
*/
final class YCbCrPlanarUpsamplerStream extends FilterInputStream {
private final int horizChromaSub;
private final int vertChromaSub;
private final int yCbCrPos;
private final int columns;
private final int units;
private final byte[] decodedRows;
int decodedLength;
int decodedPos;
private final byte[] buffer;
int bufferLength;
int bufferPos;
public YCbCrPlanarUpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns) {
super(Validate.notNull(stream, "stream"));
Validate.notNull(chromaSub, "chromaSub");
Validate.isTrue(chromaSub.length == 2, "chromaSub.length != 2");
this.horizChromaSub = chromaSub[0];
this.vertChromaSub = chromaSub[1];
this.yCbCrPos = yCbCrPos;
this.columns = columns;
units = (columns + horizChromaSub - 1) / horizChromaSub; // If columns % horizChromasSub != 0...
// ...each coded row will be padded to fill unit
decodedRows = new byte[columns * vertChromaSub];
buffer = new byte[units];
}
private void fetch() throws IOException {
if (bufferPos >= bufferLength) {
int pos = 0;
int read;
// This *SHOULD* read an entire row of units into the buffer, otherwise decodeRows will throw EOFException
while (pos < buffer.length && (read = in.read(buffer, pos, buffer.length - pos)) > 0) {
pos += read;
}
bufferLength = pos;
bufferPos = 0;
}
if (bufferLength > 0) {
decodeRows();
}
else {
decodedLength = -1;
}
}
private void decodeRows() throws EOFException {
decodedLength = decodedRows.length;
for (int u = 0; u < units; u++) {
if (u >= bufferLength) {
throw new EOFException("Unexpected end of stream");
}
// Decode one unit
byte c = buffer[u];
for (int y = 0; y < vertChromaSub; y++) {
for (int x = 0; x < horizChromaSub; x++) {
// Skip padding at end of row
int column = horizChromaSub * u + x;
if (column >= columns) {
break;
}
int pixelOff = column + columns * y;
decodedRows[pixelOff] = c;
}
}
}
bufferPos = bufferLength;
decodedPos = 0;
}
@Override
public int read() throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
return decodedRows[decodedPos++] & 0xff;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
int read = Math.min(decodedLength - decodedPos, len);
System.arraycopy(decodedRows, decodedPos, b, off, read);
decodedPos += read;
return read;
}
@Override
public long skip(long n) throws IOException {
if (decodedLength < 0) {
return -1;
}
if (decodedPos >= decodedLength) {
fetch();
if (decodedLength < 0) {
return -1;
}
}
int skipped = (int) Math.min(decodedLength - decodedPos, n);
decodedPos += skipped;
return skipped;
}
@Override
public boolean markSupported() {
return false;
}
@Override
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
}

View File

@ -213,8 +213,4 @@ final class YCbCrUpsamplerStream extends FilterInputStream {
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
private static byte clamp(int val) {
return (byte) Math.max(0, Math.min(255, val));
}
}

View File

@ -173,7 +173,19 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
new TestData(getClassLoaderResource("/tiff/jpeg-lossless-24bit-rgb.tif"), new Dimension(512, 512)), // Lossless JPEG RGB, 8 bit/sample
// Custom PIXTIFF ZIP (Compression: 50013)
new TestData(getClassLoaderResource("/tiff/pixtiff/40-8bit-gray-zip.tif"), new Dimension(801, 1313)), // ZIP Gray, 8 bit/sample
new TestData(getClassLoaderResource("/tiff/part.tif"), new Dimension(50, 50)) // Gray/BlackIsZero, uncompressed, striped signed int (SampleFormat 2)
new TestData(getClassLoaderResource("/tiff/part.tif"), new Dimension(50, 50)), // Gray/BlackIsZero, uncompressed, striped signed int (SampleFormat 2)
// Planar YCbCr full chroma
new TestData(getClassLoaderResource("/tiff/planar-yuv444-jpeg-uncompressed.tif"), new Dimension(256, 64)), // YCbCr, JPEG coefficients, uncompressed, striped
new TestData(getClassLoaderResource("/tiff/planar-yuv444-jpeg-lzw.tif"), new Dimension(256, 64)), // YCbCr, JPEG coefficients, LZW compressed, striped
// Planar YCbCr subsampled
new TestData(getClassLoaderResource("/tiff/planar-yuv422-bt601-uncompressed.tif"), new Dimension(256, 64)), // YCbCr, Rec.601 coefficients, uncompressed, striped
new TestData(getClassLoaderResource("/tiff/planar-yuv422-bt601-lzw.tif"), new Dimension(256, 64)), // YCbCr, Rec.601 coefficients,LZW compressed, striped
new TestData(getClassLoaderResource("/tiff/planar-yuv422-jpeg-uncompressed.tif"), new Dimension(256, 64)), // YCbCr, JPEG coefficients, uncompressed, striped
new TestData(getClassLoaderResource("/tiff/planar-yuv422-jpeg-lzw.tif"), new Dimension(256, 64)), // YCbCr, JPEG coefficients,LZW compressed, striped
new TestData(getClassLoaderResource("/tiff/planar-yuv420-jpeg-uncompressed.tif"), new Dimension(256, 64)), // YCbCr, JPEG coefficients, uncompressed, striped
new TestData(getClassLoaderResource("/tiff/planar-yuv420-jpeg-lzw.tif"), new Dimension(256, 64)), // YCbCr, JPEG coefficients,LZW compressed, striped
new TestData(getClassLoaderResource("/tiff/planar-yuv410-jpeg-uncompressed.tif"), new Dimension(256, 64)), // YCbCr, JPEG coefficients, uncompressed, striped
new TestData(getClassLoaderResource("/tiff/planar-yuv410-jpeg-lzw.tif"), new Dimension(256, 64)) // YCbCr, JPEG coefficients,LZW compressed, striped
);
}