#948: TIFF 64 bit FP support

This commit is contained in:
Harald Kuhr 2024-05-28 20:18:54 +02:00
parent 1e574ca429
commit 3e7ad05973
4 changed files with 112 additions and 28 deletions

View File

@ -40,7 +40,7 @@ import javax.imageio.spi.ServiceRegistry;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.*;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.InputStream;
@ -330,4 +330,21 @@ public final class IIOUtil {
System.arraycopy(srcRow, srcPos + x, destRow, destPos + x / samplePeriod, pixelStride);
}
}
public static void subsampleRow(double[] srcRow, int srcPos, int srcWidth,
double[] destRow, int destPos,
int samplesPerPixel, int bitsPerSample, int samplePeriod) {
Validate.isTrue(samplePeriod > 1, "samplePeriod must be > 1"); // Period == 1 could be a no-op...
Validate.isTrue(bitsPerSample > 0 && bitsPerSample <= 64 && (bitsPerSample == 1 || bitsPerSample % 2 == 0),
"bitsPerSample must be > 0 and <= 64 and a power of 2");
Validate.isTrue(samplesPerPixel > 0, "samplesPerPixel must be > 0");
Validate.isTrue(samplesPerPixel * bitsPerSample <= 64 || samplesPerPixel * bitsPerSample % 64 == 0,
"samplesPerPixel * bitsPerSample must be < 64 or a multiple of 64");
int pixelStride = bitsPerSample * samplesPerPixel / 64;
for (int x = 0; x < srcWidth * pixelStride; x += samplePeriod * pixelStride) {
// System.arraycopy should be intrinsic, but consider using direct array access for pixelStride == 1
System.arraycopy(srcRow, srcPos + x, destRow, destPos + x / samplePeriod, pixelStride);
}
}
}

View File

@ -815,8 +815,11 @@ public final class TIFFImageReader extends ImageReaderBase {
if (bitsPerSample == 16 || bitsPerSample == 32) {
return DataBuffer.TYPE_FLOAT;
}
else if (bitsPerSample == 64) {
return DataBuffer.TYPE_DOUBLE;
}
throw new IIOException("Unsupported BitsPerSample for SampleFormat 3/Floating Point (expected 16/32): " + bitsPerSample);
throw new IIOException("Unsupported BitsPerSample for SampleFormat 3/Floating Point (expected 16/32/64): " + bitsPerSample);
default:
throw new IIOException("Unknown TIFF SampleFormat (expected 1, 2, 3 or 4): " + sampleFormat);
}
@ -1153,11 +1156,9 @@ public final class TIFFImageReader extends ImageReaderBase {
}
// Need to do color normalization after reading all bands for planar
if (planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR) {
if (normalize) {
if (normalize && planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR) {
normalizeColorPlanar(interpretation, destRaster);
}
}
col += colsInTile;
@ -1252,20 +1253,18 @@ public final class TIFFImageReader extends ImageReaderBase {
// We'll have to use readAsRaster and later apply color space conversion ourselves
Raster raster = jpegReader.readRaster(0, jpegParam);
// TODO: Refactor + duplicate this for all JPEG-in-TIFF cases
if (normalize) {
switch (raster.getTransferType()) {
case DataBuffer.TYPE_BYTE:
if (normalize) {
normalizeColor(interpretation, samplesInTile, ((DataBufferByte) raster.getDataBuffer()).getData());
}
break;
case DataBuffer.TYPE_USHORT:
if (normalize) {
normalizeColor(interpretation, samplesInTile, ((DataBufferUShort) raster.getDataBuffer()).getData());
}
break;
default:
throw new IllegalStateException("Unsupported transfer type: " + raster.getTransferType());
}
}
destination.getRaster().setDataElements(offset.x, offset.y, raster);
}
@ -2080,6 +2079,36 @@ public final class TIFFImageReader extends ImageReaderBase {
break;
case DataBuffer.TYPE_DOUBLE:
/*for (int band = 0; band < bands; band++)*/ {
double[] rowDataDouble = ((DataBufferDouble) tileRowRaster.getDataBuffer()).getData(band);
for (int row = startRow; row < startRow + rowsInTile; row++) {
if (row >= srcRegion.y + srcRegion.height) {
break; // We're done with this tile
}
input.readFully(rowDataDouble, 0, rowDataDouble.length);
if (row >= srcRegion.y) {
if (normalize) {
normalizeColor(interpretation, numBands, rowDataDouble);
}
// Subsample horizontal
if (xSub != 1) {
subsampleRow(rowDataDouble, srcRegion.x * numBands, colsInTile,
rowDataDouble, srcRegion.x * numBands / xSub, numBands, bitsPerSample, xSub);
}
destChannel.setDataElements(startCol, row - srcRegion.y, srcChannel);
}
// Else skip data
}
}
break;
default:
throw new AssertionError("Unsupported data type: " + tileRowRaster.getTransferType());
}
@ -2091,13 +2120,24 @@ public final class TIFFImageReader extends ImageReaderBase {
}
}
private void clamp(final float[] rowDataFloat) {
for (int i = 0; i < rowDataFloat.length; i++) {
if (rowDataFloat[i] > 1f) {
rowDataFloat[i] = 1f;
private void clamp(final float[] data) {
for (int i = 0; i < data.length; i++) {
if (data[i] > 1f) {
data[i] = 1f;
}
else if (rowDataFloat[i] < 0f) {
rowDataFloat[i] = 0f;
else if (data[i] < 0f) {
data[i] = 0f;
}
}
}
private void clamp(final double[] data) {
for (int i = 0; i < data.length; i++) {
if (data[i] > 1d) {
data[i] = 1d;
}
else if (data[i] < 0d) {
data[i] = 0d;
}
}
}
@ -2395,6 +2435,28 @@ public final class TIFFImageReader extends ImageReaderBase {
}
}
private void normalizeColor(int photometricInterpretation, @SuppressWarnings("unused") int numBands, double[] data) {
// TODO: Allow param to decide tone mapping strategy, like in the HDRImageReader
clamp(data);
switch (photometricInterpretation) {
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
// Inverse values
for (int i = 0; i < data.length; i++) {
data[i] = 1d - data[i];
}
break;
case TIFFExtension.PHOTOMETRIC_CIELAB:
case TIFFExtension.PHOTOMETRIC_ICCLAB:
case TIFFExtension.PHOTOMETRIC_ITULAB:
case TIFFExtension.PHOTOMETRIC_YCBCR:
// Not supported
break;
}
}
private void convertYCbCr2RGB(final short[] yCbCr, final short[] rgb, final double[] coefficients, final double[] referenceBW, final int offset) {
double y;
double cb;

View File

@ -32,6 +32,7 @@ package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.imageio.color.ColorSpaces;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest;
import org.junit.Test;
import javax.imageio.IIOException;
@ -43,7 +44,7 @@ import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.color.*;
import java.awt.image.*;
import java.io.IOException;
import java.nio.ByteOrder;
@ -55,7 +56,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.AdditionalMatchers.and;
import static org.mockito.Mockito.*;
@ -100,6 +104,7 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
new TestData(getClassLoaderResource("/tiff/signed-integral-8bit.tif"), new Dimension(439, 167)), // Gray, 8 bit *signed* integral
new TestData(getClassLoaderResource("/tiff/floatingpoint-16bit.tif"), new Dimension(151, 151)), // RGB, 16 bit floating point
new TestData(getClassLoaderResource("/tiff/floatingpoint-32bit.tif"), new Dimension(300, 100)), // RGB, 32 bit floating point
new TestData(getClassLoaderResource("/tiff/floatingpoint-64bit.tif"), new Dimension(64, 46)), // Gray, 64 bit floating point
new TestData(getClassLoaderResource("/tiff/general-cmm-error.tif"), new Dimension(1181, 860)), // RGB, LZW compression with broken/incompatible ICC profile
new TestData(getClassLoaderResource("/tiff/lzw-rgba-padded-icc.tif"), new Dimension(19, 11)), // RGBA, LZW compression with padded ICC profile
new TestData(getClassLoaderResource("/tiff/lzw-rgba-4444.tif"), new Dimension(64, 64)), // RGBA, LZW compression with UINT 4/4/4/4 + gray 2/2