Repository: image-rs/image-tiff
Pull request #257 (2024-12-28 03:46:47)

Add FP16 support
- Adds support for floating point-16 bit images
- Use the half crate since half precision floats are nightly only 
- added several tests of different features, including prediction

micahcc: @fintelia I removed the previous image which I had pulled from the internet and switched to a pure white image generated with GIMP. I transfer all ownership of the plane white image to the image-rs project :) 
micahcc: @fintelia how do I merge this?

Patch:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index a686be2..109e5fb 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -9,14 +9,14 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        rust: ["1.61.0", stable, beta, nightly]
+        rust: ["1.70.0", stable, beta, nightly]
     steps:
     - uses: actions/checkout@v2
 
     - uses: dtolnay/rust-toolchain@nightly
-      if: ${{ matrix.rust == '1.61.0' }}
+      if: ${{ matrix.rust == '1.70.0' }}
     - name: Generate Cargo.lock with minimal-version dependencies
-      if: ${{ matrix.rust == '1.61.0' }}
+      if: ${{ matrix.rust == '1.70.0' }}
       run: cargo -Zminimal-versions generate-lockfile
 
     - uses: dtolnay/rust-toolchain@v1
@@ -29,7 +29,7 @@ jobs:
     - name: build
       run: cargo build -v
     - name: test
-      if: ${{ matrix.rust != '1.61.0' }}
+      if: ${{ matrix.rust != '1.70.0' }}
       run: cargo test -v && cargo doc -v
 
   rustfmt:
diff --git a/Cargo.toml b/Cargo.toml
index a71fd27..51c203c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,6 +17,7 @@ categories = ["multimedia::images", "multimedia::encoding"]
 exclude = ["tests/images/*", "tests/fuzz_images/*"]
 
 [dependencies]
+half = { version = "2.4.1" }
 weezl = "0.1.0"
 jpeg = { package = "jpeg-decoder", version = "0.3.0", default-features = false }
 flate2 = "1.0.20"
diff --git a/src/bytecast.rs b/src/bytecast.rs
index 6e9d762..88d562c 100644
--- a/src/bytecast.rs
+++ b/src/bytecast.rs
@@ -12,6 +12,8 @@
 //! TODO: Would like to use std-lib here.
 use std::{mem, slice};
 
+use half::f16;
+
 macro_rules! integral_slice_as_bytes{($int:ty, $const:ident $(,$mut:ident)*) => {
     pub(crate) fn $const(slice: &[$int]) -> &[u8] {
         assert!(mem::align_of::<$int>() <= mem::size_of::<$int>());
@@ -31,4 +33,5 @@ integral_slice_as_bytes!(i32, i32_as_ne_bytes, i32_as_ne_mut_bytes);
 integral_slice_as_bytes!(u64, u64_as_ne_bytes, u64_as_ne_mut_bytes);
 integral_slice_as_bytes!(i64, i64_as_ne_bytes, i64_as_ne_mut_bytes);
 integral_slice_as_bytes!(f32, f32_as_ne_bytes, f32_as_ne_mut_bytes);
+integral_slice_as_bytes!(f16, f16_as_ne_bytes, f16_as_ne_mut_bytes);
 integral_slice_as_bytes!(f64, f64_as_ne_bytes, f64_as_ne_mut_bytes);
diff --git a/src/decoder/image.rs b/src/decoder/image.rs
index 532a8fb..6133fa1 100644
--- a/src/decoder/image.rs
+++ b/src/decoder/image.rs
@@ -1,7 +1,7 @@
 use super::ifd::{Directory, Value};
 use super::stream::{ByteOrder, DeflateReader, LZWReader, PackBitsReader};
 use super::tag_reader::TagReader;
-use super::{predict_f32, predict_f64, Limits};
+use super::{predict_f16, predict_f32, predict_f64, Limits};
 use super::{stream::SmartReader, ChunkType};
 use crate::tags::{
     CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag,
@@ -378,11 +378,9 @@ impl Image {
             CompressionMethod::None => Box::new(reader),
             CompressionMethod::LZW => {
                 Box::new(LZWReader::new(reader, usize::try_from(compressed_length)?))
-            },
+            }
             #[cfg(feature = "zstd")]
-            CompressionMethod::ZSTD => {
-                Box::new(zstd::Decoder::new(reader)?)
-            },
+            CompressionMethod::ZSTD => Box::new(zstd::Decoder::new(reader)?),
             CompressionMethod::PackBits => Box::new(PackBitsReader::new(reader, compressed_length)),
             CompressionMethod::Deflate | CompressionMethod::OldDeflate => {
                 Box::new(DeflateReader::new(reader))
@@ -594,7 +592,10 @@ impl Image {
 
         // Validate that the predictor is supported for the sample type.
         match (self.predictor, self.sample_format) {
-            (Predictor::Horizontal, SampleFormat::Int | SampleFormat::Uint) => {}
+            (
+                Predictor::Horizontal,
+                SampleFormat::Int | SampleFormat::Uint | SampleFormat::IEEEFP,
+            ) => {}
             (Predictor::Horizontal, _) => {
                 return Err(TiffError::UnsupportedError(
                     TiffUnsupportedError::HorizontalPredictor(color_type),
@@ -674,6 +675,7 @@ impl Image {
 
                 let row = &mut row[..data_row_bytes];
                 match color_type.bit_depth() {
+                    16 => predict_f16(&mut encoded, row, samples),
                     32 => predict_f32(&mut encoded, row, samples),
                     64 => predict_f64(&mut encoded, row, samples),
                     _ => unreachable!(),
diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs
index 2b8b0dd..9cb92a4 100644
--- a/src/decoder/mod.rs
+++ b/src/decoder/mod.rs
@@ -8,6 +8,7 @@ use crate::tags::{
 use crate::{
     bytecast, ColorType, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
 };
+use half::f16;
 
 use self::ifd::Directory;
 use self::image::Image;
@@ -29,6 +30,8 @@ pub enum DecodingResult {
     U32(Vec<u32>),
     /// A vector of 64 bit unsigned ints
     U64(Vec<u64>),
+    /// A vector of 16 bit IEEE floats (held in u16)
+    F16(Vec<f16>),
     /// A vector of 32 bit IEEE floats
     F32(Vec<f32>),
     /// A vector of 64 bit IEEE floats
@@ -92,6 +95,14 @@ impl DecodingResult {
         }
     }
 
+    fn new_f16(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
+        if size > limits.decoding_buffer_size / std::mem::size_of::<u16>() {
+            Err(TiffError::LimitsExceeded)
+        } else {
+            Ok(DecodingResult::F16(vec![f16::ZERO; size]))
+        }
+    }
+
     fn new_i8(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
         if size > limits.decoding_buffer_size / std::mem::size_of::<i8>() {
             Err(TiffError::LimitsExceeded)
@@ -130,6 +141,7 @@ impl DecodingResult {
             DecodingResult::U16(ref mut buf) => DecodingBuffer::U16(&mut buf[start..]),
             DecodingResult::U32(ref mut buf) => DecodingBuffer::U32(&mut buf[start..]),
             DecodingResult::U64(ref mut buf) => DecodingBuffer::U64(&mut buf[start..]),
+            DecodingResult::F16(ref mut buf) => DecodingBuffer::F16(&mut buf[start..]),
             DecodingResult::F32(ref mut buf) => DecodingBuffer::F32(&mut buf[start..]),
             DecodingResult::F64(ref mut buf) => DecodingBuffer::F64(&mut buf[start..]),
             DecodingResult::I8(ref mut buf) => DecodingBuffer::I8(&mut buf[start..]),
@@ -150,6 +162,8 @@ pub enum DecodingBuffer<'a> {
     U32(&'a mut [u32]),
     /// A slice of 64 bit unsigned ints
     U64(&'a mut [u64]),
+    /// A slice of 16 bit IEEE floats
+    F16(&'a mut [f16]),
     /// A slice of 32 bit IEEE floats
     F32(&'a mut [f32]),
     /// A slice of 64 bit IEEE floats
@@ -175,6 +189,7 @@ impl<'a> DecodingBuffer<'a> {
             DecodingBuffer::I32(buf) => bytecast::i32_as_ne_mut_bytes(buf),
             DecodingBuffer::U64(buf) => bytecast::u64_as_ne_mut_bytes(buf),
             DecodingBuffer::I64(buf) => bytecast::i64_as_ne_mut_bytes(buf),
+            DecodingBuffer::F16(buf) => bytecast::f16_as_ne_mut_bytes(buf),
             DecodingBuffer::F32(buf) => bytecast::f32_as_ne_mut_bytes(buf),
             DecodingBuffer::F64(buf) => bytecast::f64_as_ne_mut_bytes(buf),
         }
@@ -303,6 +318,19 @@ fn predict_f32(input: &mut [u8], output: &mut [u8], samples: usize) {
     }
 }
 
+fn predict_f16(input: &mut [u8], output: &mut [u8], samples: usize) {
+    for i in samples..input.len() {
+        input[i] = input[i].wrapping_add(input[i - samples]);
+    }
+
+    for (i, chunk) in output.chunks_mut(2).enumerate() {
+        chunk.copy_from_slice(&u16::to_ne_bytes(u16::from_be_bytes([
+            input[i],
+            input[input.len() / 2 + i],
+        ])));
+    }
+}
+
 fn predict_f64(input: &mut [u8], output: &mut [u8], samples: usize) {
     for i in samples..input.len() {
         input[i] = input[i].wrapping_add(input[i - samples]);
@@ -340,6 +368,7 @@ fn fix_endianness_and_predict(
         Predictor::FloatingPoint => {
             let mut buffer_copy = buf.to_vec();
             match bit_depth {
+                16 => predict_f16(&mut buffer_copy, buf, samples),
                 32 => predict_f32(&mut buffer_copy, buf, samples),
                 64 => predict_f64(&mut buffer_copy, buf, samples),
                 _ => unreachable!("Caller should have validated arguments. Please file a bug."),
@@ -1004,6 +1033,7 @@ impl<R: Read + Seek> Decoder<R> {
                 )),
             },
             SampleFormat::IEEEFP => match max_sample_bits {
+                16 => DecodingResult::new_f16(buffer_size, &self.limits),
                 32 => DecodingResult::new_f32(buffer_size, &self.limits),
                 64 => DecodingResult::new_f64(buffer_size, &self.limits),
                 n => Err(TiffError::UnsupportedError(


Test patch:
diff --git a/tests/decode_fp16_images.rs b/tests/decode_fp16_images.rs
new file mode 100644
index 0000000..ba6976c
--- /dev/null
+++ b/tests/decode_fp16_images.rs
@@ -0,0 +1,195 @@
+extern crate tiff;
+
+use tiff::decoder::{Decoder, DecodingResult};
+use tiff::ColorType;
+
+use std::fs::File;
+use std::path::PathBuf;
+
+const TEST_IMAGE_DIR: &str = "./tests/images/";
+
+/// Test a basic all white image
+#[test]
+fn test_white_ieee_fp16() {
+    let filenames = ["white-fp16.tiff"];
+
+    for filename in filenames.iter() {
+        let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
+        let img_file = File::open(path).expect("Cannot find test image!");
+        let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
+        assert_eq!(
+            decoder.dimensions().expect("Cannot get dimensions"),
+            (256, 256)
+        );
+        assert_eq!(
+            decoder.colortype().expect("Cannot get colortype"),
+            ColorType::Gray(16)
+        );
+        if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
+            for p in img {
+                assert!(p == half::f16::from_f32_const(1.0));
+            }
+        } else {
+            panic!("Wrong data type");
+        }
+    }
+}
+
+/// Test a single black pixel, to make sure scaling is ok
+#[test]
+fn test_one_black_pixel_ieee_fp16() {
+    let filenames = ["single-black-fp16.tiff"];
+
+    for filename in filenames.iter() {
+        let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
+        let img_file = File::open(path).expect("Cannot find test image!");
+        let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
+        assert_eq!(
+            decoder.dimensions().expect("Cannot get dimensions"),
+            (256, 256)
+        );
+        assert_eq!(
+            decoder.colortype().expect("Cannot get colortype"),
+            ColorType::Gray(16)
+        );
+        if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
+            for (i, p) in img.iter().enumerate() {
+                if i == 0 {
+                    assert!(p < &half::f16::from_f32_const(0.001));
+                } else {
+                    assert!(p == &half::f16::from_f32_const(1.0));
+                }
+            }
+        } else {
+            panic!("Wrong data type");
+        }
+    }
+}
+
+/// Test white with horizontal differencing predictor
+#[test]
+fn test_pattern_horizontal_differencing_ieee_fp16() {
+    let filenames = ["white-fp16-pred2.tiff"];
+
+    for filename in filenames.iter() {
+        let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
+        let img_file = File::open(path).expect("Cannot find test image!");
+        let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
+        assert_eq!(
+            decoder.dimensions().expect("Cannot get dimensions"),
+            (256, 256)
+        );
+        assert_eq!(
+            decoder.colortype().expect("Cannot get colortype"),
+            ColorType::Gray(16)
+        );
+        if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
+            // 0, 2, 5, 8, 12, 16, 255 are black
+            let black = [0, 2, 5, 8, 12, 16, 255];
+            for (i, p) in img.iter().enumerate() {
+                if black.contains(&i) {
+                    assert!(p < &half::f16::from_f32_const(0.001));
+                } else {
+                    assert!(p == &half::f16::from_f32_const(1.0));
+                }
+            }
+        } else {
+            panic!("Wrong data type");
+        }
+    }
+}
+
+/// Test white with floating point predictor
+#[test]
+fn test_pattern_predictor_ieee_fp16() {
+    let filenames = ["white-fp16-pred3.tiff"];
+
+    for filename in filenames.iter() {
+        let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
+        let img_file = File::open(path).expect("Cannot find test image!");
+        let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
+        assert_eq!(
+            decoder.dimensions().expect("Cannot get dimensions"),
+            (256, 256)
+        );
+        assert_eq!(
+            decoder.colortype().expect("Cannot get colortype"),
+            ColorType::Gray(16)
+        );
+        if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
+            // 0, 2, 5, 8, 12, 16, 255 are black
+            let black = [0, 2, 5, 8, 12, 16, 255];
+            for (i, p) in img.iter().enumerate() {
+                if black.contains(&i) {
+                    assert!(p < &half::f16::from_f32_const(0.001));
+                } else {
+                    assert!(p == &half::f16::from_f32_const(1.0));
+                }
+            }
+        } else {
+            panic!("Wrong data type");
+        }
+    }
+}
+
+/// Test several random images
+/// we'rell compare against a pnm file, that scales from 0 (for 0.0) to 65767 (for 1.0)
+#[test]
+fn test_predictor_ieee_fp16() {
+    // first parse pnm, skip the first 4 \n
+    let pnm_path = PathBuf::from(TEST_IMAGE_DIR).join("random-fp16.pgm");
+    let pnm_bytes = std::fs::read(pnm_path).expect("Failed to read expected PNM file");
+
+    // PGM looks like this:
+    // ---
+    // P5
+    // #Created with GIMP
+    // 16 16
+    // 65535
+    // ... <big-endian bytes>
+    // ---
+    // get index of 4th \n
+    let byte_start = pnm_bytes
+        .iter()
+        .enumerate()
+        .filter(|(_, &v)| v == b'\n')
+        .map(|(i, _)| i)
+        .nth(3)
+        .expect("Must be 4 \\n's");
+
+    let pnm_values: Vec<f32> = pnm_bytes[(byte_start + 1)..]
+        .chunks(2)
+        .map(|slice| {
+            let bts = [slice[0], slice[1]];
+            (u16::from_be_bytes(bts) as f32) / (u16::MAX as f32)
+        })
+        .collect();
+    assert!(pnm_values.len() == 256);
+
+    let filenames = [
+        "random-fp16-pred2.tiff",
+        "random-fp16-pred3.tiff",
+        "random-fp16.tiff",
+    ];
+
+    for filename in filenames.iter() {
+        let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
+        let img_file = File::open(path).expect("Cannot find test image!");
+        let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
+        assert_eq!(
+            decoder.dimensions().expect("Cannot get dimensions"),
+            (16, 16)
+        );
+        assert_eq!(
+            decoder.colortype().expect("Cannot get colortype"),
+            ColorType::Gray(16)
+        );
+        if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
+            for (exp, found) in std::iter::zip(pnm_values.iter(), img.iter()) {
+                assert!((exp - found.to_f32()).abs() < 0.0001);
+            }
+        } else {
+            panic!("Wrong data type");
+        }
+    }
+}