mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-06 04:55:30 -04:00
#417: Half precision support (clean-up)
This commit is contained in:
parent
01a4e55185
commit
9adf0f4da3
@ -0,0 +1,160 @@
|
|||||||
|
package com.twelvemonkeys.imageio.metadata.tiff;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IEEE 754 half-precision floating point data type.
|
||||||
|
*
|
||||||
|
* @see <a href="https://stackoverflow.com/a/6162687/259991">Stack Overflow answer by x4u</a>
|
||||||
|
* @see <a href="https://en.wikipedia.org/wiki/Half-precision_floating-point_format>Wikipedia</a>
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
|
* @author last modified by $Author: haraldk$
|
||||||
|
* @version $Id: Half.java,v 1.0 10/04/2021 haraldk Exp$
|
||||||
|
*/
|
||||||
|
public final class Half extends Number implements Comparable<Half> {
|
||||||
|
// Short, Int, Long
|
||||||
|
// Half, Float, Double :-)
|
||||||
|
|
||||||
|
public static final int SIZE = 16;
|
||||||
|
|
||||||
|
private final short shortBits;
|
||||||
|
private final transient float floatValue;
|
||||||
|
|
||||||
|
public Half(short shortBits) {
|
||||||
|
this.shortBits = shortBits;
|
||||||
|
this.floatValue = shortBitsToFloat(shortBits);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int intValue() {
|
||||||
|
return (int) floatValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long longValue() {
|
||||||
|
return (long) floatValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float floatValue() {
|
||||||
|
return floatValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double doubleValue() {
|
||||||
|
return floatValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int hashCode() {
|
||||||
|
return shortBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
return (other instanceof Half)
|
||||||
|
&& ((Half) other).shortBits == shortBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(final Half other) {
|
||||||
|
return Float.compare(floatValue, other.floatValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return Float.toString(floatValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Half valueOf(String value) throws NumberFormatException {
|
||||||
|
return new Half(parseHalf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static short parseHalf(String value) throws NumberFormatException {
|
||||||
|
return floatToShortBits(Float.parseFloat(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an IEEE 754 half-precision data type to single-precision.
|
||||||
|
*
|
||||||
|
* @param shortBits a 16 bit half precision value
|
||||||
|
* @return an IEE 754 single precision float
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public static float shortBitsToFloat(final short shortBits) {
|
||||||
|
int mantissa = shortBits & 0x03ff; // 10 bits mantissa
|
||||||
|
int exponent = shortBits & 0x7c00; // 5 bits exponent
|
||||||
|
|
||||||
|
if (exponent == 0x7c00) { // NaN/Inf
|
||||||
|
exponent = 0x3fc00; // -> NaN/Inf
|
||||||
|
}
|
||||||
|
else if (exponent != 0) { // Normalized value
|
||||||
|
exponent += 0x1c000; // exp - 15 + 127
|
||||||
|
|
||||||
|
// Smooth transition
|
||||||
|
if (mantissa == 0 && exponent > 0x1c400) {
|
||||||
|
return Float.intBitsToFloat((shortBits & 0x8000) << 16 | exponent << 13 | 0x3ff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (mantissa != 0) { // && exp == 0 -> subnormal
|
||||||
|
exponent = 0x1c400; // Make it normal
|
||||||
|
|
||||||
|
do {
|
||||||
|
mantissa <<= 1; // mantissa * 2
|
||||||
|
exponent -= 0x400; // Decrease exp by 1
|
||||||
|
} while ((mantissa & 0x400) == 0); // while not normal
|
||||||
|
|
||||||
|
mantissa &= 0x3ff; // Discard subnormal bit
|
||||||
|
} // else +/-0 -> +/-0
|
||||||
|
|
||||||
|
// Combine all parts, sign << (31 - 15), value << (23 - 10)
|
||||||
|
return Float.intBitsToFloat((shortBits & 0x8000) << 16 | (exponent | mantissa) << 13);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a float value to IEEE 754 half-precision bits.
|
||||||
|
*
|
||||||
|
* @param floatValue a float value
|
||||||
|
* @return the IEE 754 single precision 16 bits value
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public static short floatToShortBits(final float floatValue) {
|
||||||
|
// TODO: Is this okay? Need test
|
||||||
|
return (short) floatTo16Bits(floatValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int floatTo16Bits(final float floatValue) {
|
||||||
|
int fbits = Float.floatToIntBits(floatValue);
|
||||||
|
int sign = fbits >>> 16 & 0x8000; // sign only
|
||||||
|
int val = (fbits & 0x7fffffff) + 0x1000; // rounded value
|
||||||
|
|
||||||
|
if (val >= 0x47800000) { // might be or become NaN/Inf, avoid Inf due to rounding
|
||||||
|
if ((fbits & 0x7fffffff) >= 0x47800000) { // is or must become NaN/Inf
|
||||||
|
if (val < 0x7f800000) { // was value but too large
|
||||||
|
return sign | 0x7c00; // make it +/-Inf
|
||||||
|
}
|
||||||
|
|
||||||
|
return sign | 0x7c00 | // remains +/-Inf or NaN
|
||||||
|
(fbits & 0x007fffff) >>> 13;// keep NaN (and Inf) bits
|
||||||
|
}
|
||||||
|
|
||||||
|
return sign | 0x7bff; // unrounded not quite Inf
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val >= 0x38800000) { // remains normalized value
|
||||||
|
return sign | val - 0x38000000 >>> 13; // exp - 127 + 15
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val < 0x33000000) { // too small for subnormal
|
||||||
|
return sign; // becomes +/-0
|
||||||
|
}
|
||||||
|
|
||||||
|
val = (fbits & 0x7fffffff) >>> 23; // tmp exp for subnormal calc
|
||||||
|
|
||||||
|
return sign | ((fbits & 0x7fffff | 0x800000)// add subnormal bit
|
||||||
|
+ (0x800000 >>> val - 102) // round depending on cut off
|
||||||
|
>>> 126 - val); // div by 2^(1-(exp-127+15)) and >> 13 | exp=0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restores the floatValue on de-serialization
|
||||||
|
private Object readResolve() {
|
||||||
|
return new Half(shortBits);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,181 @@
|
|||||||
|
package com.twelvemonkeys.imageio.metadata.tiff;
|
||||||
|
|
||||||
|
import com.twelvemonkeys.io.FastByteArrayOutputStream;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HalfTest.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
|
* @author last modified by $Author: haraldk$
|
||||||
|
* @version $Id: HalfTest.java,v 1.0 10/04/2021 haraldk Exp$
|
||||||
|
*/
|
||||||
|
public class HalfTest {
|
||||||
|
Random random = new Random(8374698541237L);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSize() {
|
||||||
|
assertEquals(16, Half.SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRoundTrip() {
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
short half = (short) random.nextInt(Short.MAX_VALUE & 0x3FFF);
|
||||||
|
float floatValue = Half.shortBitsToFloat(half);
|
||||||
|
assertEquals(half, Half.floatToShortBits(floatValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRoundTripBack() {
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
float floatValue = random.nextFloat();
|
||||||
|
short half = Half.floatToShortBits(floatValue);
|
||||||
|
assertEquals(floatValue, Half.shortBitsToFloat(half), 0.0003); // Might lose some precision 32 -> 16 bit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHashCode() {
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
short halfBits = (short) random.nextInt(Short.MAX_VALUE);
|
||||||
|
Half half = new Half(halfBits);
|
||||||
|
assertEquals(halfBits, half.hashCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEquals() {
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
short halfBits = (short) random.nextInt(Short.MAX_VALUE);
|
||||||
|
Half half = new Half(halfBits);
|
||||||
|
assertEquals(new Half(halfBits), half);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCompareEquals() {
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
short halfBits = (short) random.nextInt(Short.MAX_VALUE);
|
||||||
|
Half half = new Half(halfBits);
|
||||||
|
assertEquals(0, new Half(halfBits).compareTo(half));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCompareLess() {
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
short halfBits = (short) random.nextInt(Short.MAX_VALUE & 0x3FFF);
|
||||||
|
Half half = new Half(halfBits );
|
||||||
|
assertEquals(-1, new Half((short) (halfBits - 2)).compareTo(half));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCompareGreater() {
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
short halfBits = (short) random.nextInt(Short.MAX_VALUE & 0x3FFF);
|
||||||
|
Half half = new Half(halfBits);
|
||||||
|
assertEquals(1, new Half((short) (halfBits + 2)).compareTo(half));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testToString() {
|
||||||
|
assertEquals("0.0", new Half((short) 0).toString());
|
||||||
|
// TODO: More... But we just delegate to Float.toString, so no worries... :-)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = NullPointerException.class)
|
||||||
|
public void testParseHAlfNull() {
|
||||||
|
Half.parseHalf(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = NumberFormatException.class)
|
||||||
|
public void testParseHalfBad() {
|
||||||
|
Half.parseHalf("foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseHalf() {
|
||||||
|
short half = Half.parseHalf("9876.5432");
|
||||||
|
assertEquals(Half.floatToShortBits(9876.5432f), half);
|
||||||
|
// TODO: More... But we just delegate to Float.valueOf, so no worries... :-)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = NullPointerException.class)
|
||||||
|
public void testValueOfNull() {
|
||||||
|
Half.valueOf(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = NumberFormatException.class)
|
||||||
|
public void testValueOfBad() {
|
||||||
|
Half.valueOf("foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValueOf() {
|
||||||
|
Half half = Half.valueOf("12.3456");
|
||||||
|
assertEquals(new Half(Half.floatToShortBits(12.3456f)), half);
|
||||||
|
// TODO: More... But we just delegate to Float.valueOf, so no worries... :-)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIntValue() {
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
int intValue = i << 1;
|
||||||
|
Half half = new Half(Half.floatToShortBits((float) intValue));
|
||||||
|
assertEquals(intValue, half.intValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLongValue() {
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
long longValue = i << 2;
|
||||||
|
Half half = new Half(Half.floatToShortBits((float) longValue));
|
||||||
|
assertEquals(longValue, half.longValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFloatValue() {
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
float floatValue = random.nextFloat();
|
||||||
|
Half half = new Half(Half.floatToShortBits(floatValue));
|
||||||
|
assertEquals(floatValue, half.floatValue(), 0.0003);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDoubleValue() {
|
||||||
|
for (int i = 0; i < 1024; i++) {
|
||||||
|
double doubleValue = random.nextDouble();
|
||||||
|
Half half = new Half(Half.floatToShortBits((float) doubleValue));
|
||||||
|
assertEquals(doubleValue, half.doubleValue(), 0.0003);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerializationRoundTrip() throws IOException, ClassNotFoundException {
|
||||||
|
Half original = new Half((short) 0x3D75);
|
||||||
|
FastByteArrayOutputStream bytes = new FastByteArrayOutputStream(64);
|
||||||
|
new ObjectOutputStream(bytes).writeObject(original);
|
||||||
|
|
||||||
|
Object restored = new ObjectInputStream(bytes.createInputStream()).readObject();
|
||||||
|
assertTrue(restored instanceof Half);
|
||||||
|
assertEquals(original, restored); // Only tests bits, not transient float value
|
||||||
|
|
||||||
|
assertEquals(original.floatValue(), ((Half) restored).floatValue(), 0);
|
||||||
|
}
|
||||||
|
}
|
@ -42,6 +42,7 @@ import com.twelvemonkeys.imageio.metadata.iptc.IPTCReader;
|
|||||||
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
|
||||||
import com.twelvemonkeys.imageio.metadata.psd.PSD;
|
import com.twelvemonkeys.imageio.metadata.psd.PSD;
|
||||||
import com.twelvemonkeys.imageio.metadata.psd.PSDReader;
|
import com.twelvemonkeys.imageio.metadata.psd.PSDReader;
|
||||||
|
import com.twelvemonkeys.imageio.metadata.tiff.Half;
|
||||||
import com.twelvemonkeys.imageio.metadata.tiff.Rational;
|
import com.twelvemonkeys.imageio.metadata.tiff.Rational;
|
||||||
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
|
import com.twelvemonkeys.imageio.metadata.tiff.TIFF;
|
||||||
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
|
import com.twelvemonkeys.imageio.metadata.tiff.TIFFReader;
|
||||||
@ -2019,49 +2020,10 @@ public final class TIFFImageReader extends ImageReaderBase {
|
|||||||
|
|
||||||
private void toFloat(final float[] rowDataFloat, final short[] rowDataShort) {
|
private void toFloat(final float[] rowDataFloat, final short[] rowDataShort) {
|
||||||
for (int i = 0; i < rowDataFloat.length; i++) {
|
for (int i = 0; i < rowDataFloat.length; i++) {
|
||||||
rowDataFloat[i] = toFloat(rowDataShort[i]);
|
rowDataFloat[i] = Half.shortBitsToFloat(rowDataShort[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an IEEE 754 half-precision data type to single-precision.
|
|
||||||
*
|
|
||||||
* @param shortValue a 16 bit half precision value
|
|
||||||
* @return an IEE 754 single precision float
|
|
||||||
*
|
|
||||||
* @see <a href="https://stackoverflow.com/a/6162687/259991">Stack Overflow answer by x4u</a>
|
|
||||||
* @see <a href="https://en.wikipedia.org/wiki/Half-precision_floating-point_format>Wikipedia</a>
|
|
||||||
*/
|
|
||||||
private float toFloat(final short shortValue) {
|
|
||||||
int mantissa = shortValue & 0x03ff; // 10 bits mantissa
|
|
||||||
int exponent = shortValue & 0x7c00; // 5 bits exponent
|
|
||||||
|
|
||||||
if (exponent == 0x7c00) { // NaN/Inf
|
|
||||||
exponent = 0x3fc00; // -> NaN/Inf
|
|
||||||
}
|
|
||||||
else if (exponent != 0) { // Normalized value
|
|
||||||
exponent += 0x1c000; // exp - 15 + 127
|
|
||||||
|
|
||||||
// Smooth transition
|
|
||||||
if (mantissa == 0 && exponent > 0x1c400) {
|
|
||||||
return Float.intBitsToFloat((shortValue & 0x8000) << 16 | exponent << 13 | 0x3ff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (mantissa != 0) { // && exp == 0 -> subnormal
|
|
||||||
exponent = 0x1c400; // Make it normal
|
|
||||||
|
|
||||||
do {
|
|
||||||
mantissa <<= 1; // mantissa * 2
|
|
||||||
exponent -= 0x400; // Decrease exp by 1
|
|
||||||
} while ((mantissa & 0x400) == 0); // while not normal
|
|
||||||
|
|
||||||
mantissa &= 0x3ff; // Discard subnormal bit
|
|
||||||
} // else +/-0 -> +/-0
|
|
||||||
|
|
||||||
// Combine all parts, sign << (31 - 15), value << (23 - 10)
|
|
||||||
return Float.intBitsToFloat((shortValue & 0x8000) << 16 | (exponent | mantissa) << 13);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clamp(final float[] rowDataFloat) {
|
private void clamp(final float[] rowDataFloat) {
|
||||||
for (int i = 0; i < rowDataFloat.length; i++) {
|
for (int i = 0; i < rowDataFloat.length; i++) {
|
||||||
if (rowDataFloat[i] > 1f) {
|
if (rowDataFloat[i] > 1f) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user