#289, #493: Finally implemented subsampling for < 8 bit samples

This commit is contained in:
Harald Kuhr 2019-08-29 19:06:05 +02:00
parent a5def1ba3d
commit 0c2433dc9f
4 changed files with 263 additions and 20 deletions

View File

@ -31,6 +31,7 @@
package com.twelvemonkeys.imageio.util;
import com.twelvemonkeys.image.ImageUtil;
import com.twelvemonkeys.lang.Validate;
import javax.imageio.IIOParam;
import javax.imageio.ImageIO;
@ -148,11 +149,10 @@ public final class IIOUtil {
return null;
}
if (pSourceRegion != null) {
if (pSourceRegion.x != 0 || pSourceRegion.y != 0 || pSourceRegion.width != pImage.getWidth() || pSourceRegion.height != pImage.getHeight()) {
if (pSourceRegion != null
&& (pSourceRegion.x != 0 || pSourceRegion.y != 0 || pSourceRegion.width != pImage.getWidth() || pSourceRegion.height != pImage.getHeight())) {
return pImage.getSubimage(pSourceRegion.x, pSourceRegion.y, pSourceRegion.width, pSourceRegion.height);
}
}
return pImage;
}
@ -192,7 +192,7 @@ public final class IIOUtil {
* The names are all upper-case, and contains no duplicates.
*
* @return a normalized array of {@code String}s.
* @see javax.imageio.ImageIO#getReaderFormatNames()
* @see ImageIO#getReaderFormatNames()
*/
public static String[] getNormalizedReaderFormatNames() {
return normalizeNames(ImageIO.getReaderFormatNames());
@ -203,7 +203,7 @@ public final class IIOUtil {
* The names are all upper-case, and contains no duplicates.
*
* @return a normalized array of {@code String}s.
* @see javax.imageio.ImageIO#getWriterFormatNames()
* @see ImageIO#getWriterFormatNames()
*/
public static String[] getNormalizedWriterFormatNames() {
return normalizeNames(ImageIO.getWriterFormatNames());
@ -216,6 +216,79 @@ public final class IIOUtil {
normalizedNames.add(name.toUpperCase());
}
return normalizedNames.toArray(new String[normalizedNames.size()]);
return normalizedNames.toArray(new String[0]);
}
// TODO: RasterUtils? Subsampler?
public static void subsampleRow(byte[] srcRow, int srcPos, int srcWidth,
byte[] 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 <= 8 && (bitsPerSample == 1 || bitsPerSample % 2 == 0),
"bitsPerSample must be > 0 and <= 8 and a power of 2");
Validate.isTrue(samplesPerPixel > 0, "samplesPerPixel must be > 0");
Validate.isTrue(samplesPerPixel * bitsPerSample <= 8 || samplesPerPixel * bitsPerSample % 8 == 0,
"samplesPerPixel * bitsPerSample must be < 8 or a multiple of 8 ");
if (bitsPerSample * samplesPerPixel % 8 == 0) {
int pixelStride = bitsPerSample * samplesPerPixel / 8;
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);
}
}
else {
// Start bit fiddling...
int pixelStride = bitsPerSample * samplesPerPixel;
int mask = (1 << pixelStride) - 1;
for (int x = 0; x < srcWidth; x += samplePeriod) {
int dstOff = (destPos + x / samplePeriod) * pixelStride / 8;
int srcOff = (srcPos + x) * pixelStride / 8;
int srcBitPos = 8 - pixelStride - (x * pixelStride) % 8;
int srcMask = mask << srcBitPos;
int dstBitPos = 8 - pixelStride - (x * pixelStride / samplePeriod) % 8;
int dstMask = ~(mask << dstBitPos);
int val = ((srcRow[srcOff] & srcMask) >> srcBitPos);
destRow[dstOff] = (byte) ((destRow[dstOff] & dstMask) | val << dstBitPos);
}
}
}
public static void subsampleRow(short[] srcRow, int srcPos, int srcWidth,
short[] 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 <= 16 && (bitsPerSample == 1 || bitsPerSample % 2 == 0),
"bitsPerSample must be > 0 and <= 16 and a power of 2");
Validate.isTrue(samplesPerPixel > 0, "samplesPerPixel must be > 0");
Validate.isTrue(samplesPerPixel * bitsPerSample <= 16 || samplesPerPixel * bitsPerSample % 16 == 0,
"samplesPerPixel * bitsPerSample must be < 16 or a multiple of 16 ");
int pixelStride = bitsPerSample * samplesPerPixel / 16;
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);
}
}
public static void subsampleRow(int[] srcRow, int srcPos, int srcWidth,
int[] 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 <= 32 && (bitsPerSample == 1 || bitsPerSample % 2 == 0),
"bitsPerSample must be > 0 and <= 32 and a power of 2");
Validate.isTrue(samplesPerPixel > 0, "samplesPerPixel must be > 0");
Validate.isTrue(samplesPerPixel * bitsPerSample <= 32 || samplesPerPixel * bitsPerSample % 32 == 0,
"samplesPerPixel * bitsPerSample must be < 32 or a multiple of 32 ");
int pixelStride = bitsPerSample * samplesPerPixel / 32;
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

@ -0,0 +1,145 @@
package com.twelvemonkeys.imageio.util;
import org.junit.Test;
import static org.junit.Assert.assertArrayEquals;
/**
* IIOUtilTest
*/
public class IIOUtilTest {
@Test
public void subsampleRowPeriod2Byte() {
int period = 2;
byte[] input = {-1, 0, (byte) 0xAA, 0, -1};
byte[] output = new byte[divCeil(input.length, period)];
byte[] expected = {-1, (byte) 0xAA, -1};
IIOUtil.subsampleRow(input, 0, input.length, output, 0, 1, 8, period);
assertArrayEquals(expected, output);
}
@Test
public void subsampleRowPeriod2ByteStride3() {
int period = 2;
byte[] input = {-1, -1, -1, 0, 0, 0, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, 0, 0, 0, -1, -1, -1};
byte[] output = new byte[9];
byte[] expected = {-1, -1, -1, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, -1, -1, -1};
IIOUtil.subsampleRow(input, 0, input.length / 3, output, 0, 3, 8, period);
assertArrayEquals(expected, output);
}
@Test
public void subsampleRowPeriod2Byte1() {
int period = 2;
byte[] input = {(byte) 0xaa, (byte) 0xaa, (byte) 0xaa};
byte[] output = new byte[divCeil(input.length, period)];
byte[] expected = {(byte) 0xff, (byte) 0xf0};
IIOUtil.subsampleRow(input, 0, input.length * 8, output, 0, 1, 1, period);
assertArrayEquals(expected, output);
}
@Test
public void subsampleRowPeriod3_1Bit() {
int period = 3;
byte[] input = {(byte) 0x92, (byte) 0x49, (byte) 0x24};
byte[] output = new byte[divCeil(input.length, period)];
byte[] expected = {(byte) 0xff};
IIOUtil.subsampleRow(input, 0, input.length * 8, output, 0, 1, 1, period);
assertArrayEquals(expected, output);
}
@Test
public void subsampleRowPeriod2_2Bit() {
int period = 2;
byte[] input = {(byte) 0xcc, (byte) 0xcc, (byte) 0xcc};
byte[] output = new byte[divCeil(input.length, period)];
byte[] expected = {(byte) 0xff, (byte) 0xf0};
IIOUtil.subsampleRow(input, 0, input.length * 4, output, 0, 1, 2, period);
assertArrayEquals(expected, output);
}
@Test
public void subsampleRowPeriod2_4Bit() {
int period = 2;
byte[] input = {(byte) 0xf0, (byte) 0xf0, (byte) 0xf0};
byte[] output = new byte[divCeil(input.length, period)];
byte[] expected = {(byte) 0xff, (byte) 0xf0};
IIOUtil.subsampleRow(input, 0, input.length * 2, output, 0, 1, 4, period);
assertArrayEquals(expected, output);
}
@Test
public void subsampleRowPeriod2_1Bit2Samples() {
int period = 2;
byte[] input = {(byte) 0xcc, (byte) 0xcc, (byte) 0xcc};
byte[] output = new byte[divCeil(input.length, period)];
byte[] expected = {(byte) 0xff, (byte) 0xf0};
IIOUtil.subsampleRow(input, 0, input.length * 4, output, 0, 2, 1, period);
assertArrayEquals(expected, output);
}
@Test
public void subsampleRowPeriod2_2Bit2Samples() {
int period = 2;
byte[] input = {(byte) 0xf0, (byte) 0xf0, (byte) 0xf0};
byte[] output = new byte[divCeil(input.length, period)];
byte[] expected = {(byte) 0xff, (byte) 0xf0};
IIOUtil.subsampleRow(input, 0, input.length * 2, output, 0, 2, 2, period);
assertArrayEquals(expected, output);
}
@Test
public void subsampleRowPeriod2_4Bit2Samples() {
int period = 2;
byte[] input = {-1, 0, (byte) 0xAA, 0, -1};
byte[] output = new byte[divCeil(input.length, period)];
byte[] expected = {-1, (byte) 0xAA, -1};
IIOUtil.subsampleRow(input, 0, input.length, output, 0, 2, 4, period);
assertArrayEquals(expected, output);
}
@Test
public void subsampleRowPeriod2_1BitOffset1() {
int period = 2;
byte[] input = {(byte) 0xaa, (byte) 0xaa, (byte) 0xaa};
byte[] output = new byte[divCeil(input.length, period)];
byte[] expected = {(byte) 0xff, (byte) 0xf0};
IIOUtil.subsampleRow(input, 1, input.length * 8, output, 0, 1, 1, period);
assertArrayEquals(expected, output);
}
private int divCeil(int numerator, int denominator) {
return (numerator + denominator - 1) / denominator;
}
}

View File

@ -48,6 +48,7 @@ import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
import com.twelvemonkeys.imageio.metadata.xmp.XMPReader;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.io.FastByteArrayOutputStream;
@ -82,8 +83,7 @@ import java.util.*;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import static com.twelvemonkeys.imageio.util.IIOUtil.createStreamAdapter;
import static com.twelvemonkeys.imageio.util.IIOUtil.lookupProviderByName;
import static com.twelvemonkeys.imageio.util.IIOUtil.*;
import static java.util.Arrays.asList;
/**
@ -1841,6 +1841,7 @@ public final class TIFFImageReader extends ImageReaderBase {
DataBuffer dataBuffer = tileRowRaster.getDataBuffer();
int bands = dataBuffer.getNumBanks();
boolean banded = bands > 1;
int bitsPerSample = getBitsPerSample();
switch (tileRowRaster.getTransferType()) {
case DataBuffer.TYPE_BYTE:
@ -1870,9 +1871,8 @@ public final class TIFFImageReader extends ImageReaderBase {
// Subsample horizontal
if (xSub != 1) {
for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + colsInTile) / xSub) * numBands; x += numBands) {
System.arraycopy(rowDataByte, x * xSub, rowDataByte, x, numBands);
}
IIOUtil.subsampleRow(rowDataByte, srcRegion.x * numBands, colsInTile,
rowDataByte, srcRegion.x * numBands / xSub, numBands, bitsPerSample, xSub);
}
destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel);
@ -1913,9 +1913,8 @@ public final class TIFFImageReader extends ImageReaderBase {
// Subsample horizontal
if (xSub != 1) {
for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + colsInTile) / xSub) * numBands; x += numBands) {
System.arraycopy(rowDataShort, x * xSub, rowDataShort, x, numBands);
}
subsampleRow(rowDataShort, srcRegion.x * numBands, colsInTile,
rowDataShort, srcRegion.x * numBands / xSub, numBands, bitsPerSample, xSub);
}
destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel);
@ -1950,9 +1949,8 @@ public final class TIFFImageReader extends ImageReaderBase {
// Subsample horizontal
if (xSub != 1) {
for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + colsInTile) / xSub) * numBands; x += numBands) {
System.arraycopy(rowDataInt, x * xSub, rowDataInt, x, numBands);
}
subsampleRow(rowDataInt, srcRegion.x * numBands, colsInTile,
rowDataInt, srcRegion.x * numBands / xSub, numBands, bitsPerSample, xSub);
}
destChannel.setDataElements(startCol / xSub, (row - srcRegion.y) / ySub, srcChannel);
@ -1998,6 +1996,9 @@ public final class TIFFImageReader extends ImageReaderBase {
}
break;
default:
throw new AssertionError("Unsupported data type: " + tileRowRaster.getTransferType());
}
}
@ -2294,7 +2295,7 @@ public final class TIFFImageReader extends ImageReaderBase {
case TIFFBaseline.COMPRESSION_NONE:
return stream;
case TIFFBaseline.COMPRESSION_PACKBITS:
return new DecoderStream(createFillOrderStream(fillOrder, stream), new PackBitsDecoder(), 1024);
return new DecoderStream(createFillOrderStream(fillOrder, stream), new PackBitsDecoder(), 256);
case TIFFExtension.COMPRESSION_LZW:
// NOTE: Needs large buffer for compatibility with certain encoders
return new DecoderStream(createFillOrderStream(fillOrder, stream), LZWDecoder.create(LZWDecoder.isOldBitReversedStream(stream)), Math.max(width * bands, 4096));
@ -2469,6 +2470,7 @@ public final class TIFFImageReader extends ImageReaderBase {
return null;
}
@Override
public boolean canReadRaster() {
return true;

View File

@ -708,6 +708,29 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
}
}
@Test
public void testReadWithSubsampleParamPixelsBinary() throws IOException {
ImageReader reader = createReader();
TestData data = new TestData(getClassLoaderResource("/tiff/ccitt/group3_2d.tif"), new Dimension(6, 4));
reader.setInput(data.getInputStream());
ImageReadParam param = reader.getDefaultReadParam();
BufferedImage image = null;
BufferedImage subsampled = null;
try {
image = reader.read(0, param);
param.setSourceSubsampling(2, 2, 0, 0);
subsampled = reader.read(0, param);
}
catch (IOException e) {
failBecause("Image could not be read", e);
}
assertSubsampledImageDataEquals("Subsampled image data does not match expected", image, subsampled, param);
}
@Test
public void testReadWithSubsampleParamPixelsJPEG() throws IOException {
// Tiled "new style" JPEG