Skip to content

Commit 3bfb43e

Browse files
authored
Add FP16 support (#257)
1 parent d42e6be commit 3bfb43e

File tree

14 files changed

+239
-6
lines changed

14 files changed

+239
-6
lines changed

.github/workflows/rust.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ jobs:
99
runs-on: ubuntu-latest
1010
strategy:
1111
matrix:
12-
rust: ["1.61.0", stable, beta, nightly]
12+
rust: ["1.70.0", stable, beta, nightly]
1313
steps:
1414
- uses: actions/checkout@v2
1515

1616
- uses: dtolnay/rust-toolchain@nightly
17-
if: ${{ matrix.rust == '1.61.0' }}
17+
if: ${{ matrix.rust == '1.70.0' }}
1818
- name: Generate Cargo.lock with minimal-version dependencies
19-
if: ${{ matrix.rust == '1.61.0' }}
19+
if: ${{ matrix.rust == '1.70.0' }}
2020
run: cargo -Zminimal-versions generate-lockfile
2121

2222
- uses: dtolnay/rust-toolchain@v1
@@ -29,7 +29,7 @@ jobs:
2929
- name: build
3030
run: cargo build -v
3131
- name: test
32-
if: ${{ matrix.rust != '1.61.0' }}
32+
if: ${{ matrix.rust != '1.70.0' }}
3333
run: cargo test -v && cargo doc -v
3434

3535
rustfmt:

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ categories = ["multimedia::images", "multimedia::encoding"]
1717
exclude = ["tests/images/*", "tests/fuzz_images/*"]
1818

1919
[dependencies]
20+
half = { version = "2.4.1" }
2021
weezl = "0.1.0"
2122
jpeg = { package = "jpeg-decoder", version = "0.3.0", default-features = false }
2223
flate2 = "1.0.20"

src/bytecast.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
//! TODO: Would like to use std-lib here.
1313
use std::{mem, slice};
1414

15+
use half::f16;
16+
1517
macro_rules! integral_slice_as_bytes{($int:ty, $const:ident $(,$mut:ident)*) => {
1618
pub(crate) fn $const(slice: &[$int]) -> &[u8] {
1719
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);
3133
integral_slice_as_bytes!(u64, u64_as_ne_bytes, u64_as_ne_mut_bytes);
3234
integral_slice_as_bytes!(i64, i64_as_ne_bytes, i64_as_ne_mut_bytes);
3335
integral_slice_as_bytes!(f32, f32_as_ne_bytes, f32_as_ne_mut_bytes);
36+
integral_slice_as_bytes!(f16, f16_as_ne_bytes, f16_as_ne_mut_bytes);
3437
integral_slice_as_bytes!(f64, f64_as_ne_bytes, f64_as_ne_mut_bytes);

src/decoder/image.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use super::ifd::{Directory, Value};
22
use super::stream::{ByteOrder, DeflateReader, LZWReader, PackBitsReader};
33
use super::tag_reader::TagReader;
4-
use super::{predict_f32, predict_f64, Limits};
4+
use super::{predict_f16, predict_f32, predict_f64, Limits};
55
use super::{stream::SmartReader, ChunkType};
66
use crate::tags::{
77
CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, SampleFormat, Tag,
@@ -592,7 +592,10 @@ impl Image {
592592

593593
// Validate that the predictor is supported for the sample type.
594594
match (self.predictor, self.sample_format) {
595-
(Predictor::Horizontal, SampleFormat::Int | SampleFormat::Uint) => {}
595+
(
596+
Predictor::Horizontal,
597+
SampleFormat::Int | SampleFormat::Uint | SampleFormat::IEEEFP,
598+
) => {}
596599
(Predictor::Horizontal, _) => {
597600
return Err(TiffError::UnsupportedError(
598601
TiffUnsupportedError::HorizontalPredictor(color_type),
@@ -672,6 +675,7 @@ impl Image {
672675

673676
let row = &mut row[..data_row_bytes];
674677
match color_type.bit_depth() {
678+
16 => predict_f16(&mut encoded, row, samples),
675679
32 => predict_f32(&mut encoded, row, samples),
676680
64 => predict_f64(&mut encoded, row, samples),
677681
_ => unreachable!(),

src/decoder/mod.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::tags::{
88
use crate::{
99
bytecast, ColorType, TiffError, TiffFormatError, TiffResult, TiffUnsupportedError, UsageError,
1010
};
11+
use half::f16;
1112

1213
use self::ifd::Directory;
1314
use self::image::Image;
@@ -29,6 +30,8 @@ pub enum DecodingResult {
2930
U32(Vec<u32>),
3031
/// A vector of 64 bit unsigned ints
3132
U64(Vec<u64>),
33+
/// A vector of 16 bit IEEE floats (held in u16)
34+
F16(Vec<f16>),
3235
/// A vector of 32 bit IEEE floats
3336
F32(Vec<f32>),
3437
/// A vector of 64 bit IEEE floats
@@ -92,6 +95,14 @@ impl DecodingResult {
9295
}
9396
}
9497

98+
fn new_f16(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
99+
if size > limits.decoding_buffer_size / std::mem::size_of::<u16>() {
100+
Err(TiffError::LimitsExceeded)
101+
} else {
102+
Ok(DecodingResult::F16(vec![f16::ZERO; size]))
103+
}
104+
}
105+
95106
fn new_i8(size: usize, limits: &Limits) -> TiffResult<DecodingResult> {
96107
if size > limits.decoding_buffer_size / std::mem::size_of::<i8>() {
97108
Err(TiffError::LimitsExceeded)
@@ -130,6 +141,7 @@ impl DecodingResult {
130141
DecodingResult::U16(ref mut buf) => DecodingBuffer::U16(&mut buf[start..]),
131142
DecodingResult::U32(ref mut buf) => DecodingBuffer::U32(&mut buf[start..]),
132143
DecodingResult::U64(ref mut buf) => DecodingBuffer::U64(&mut buf[start..]),
144+
DecodingResult::F16(ref mut buf) => DecodingBuffer::F16(&mut buf[start..]),
133145
DecodingResult::F32(ref mut buf) => DecodingBuffer::F32(&mut buf[start..]),
134146
DecodingResult::F64(ref mut buf) => DecodingBuffer::F64(&mut buf[start..]),
135147
DecodingResult::I8(ref mut buf) => DecodingBuffer::I8(&mut buf[start..]),
@@ -150,6 +162,8 @@ pub enum DecodingBuffer<'a> {
150162
U32(&'a mut [u32]),
151163
/// A slice of 64 bit unsigned ints
152164
U64(&'a mut [u64]),
165+
/// A slice of 16 bit IEEE floats
166+
F16(&'a mut [f16]),
153167
/// A slice of 32 bit IEEE floats
154168
F32(&'a mut [f32]),
155169
/// A slice of 64 bit IEEE floats
@@ -175,6 +189,7 @@ impl<'a> DecodingBuffer<'a> {
175189
DecodingBuffer::I32(buf) => bytecast::i32_as_ne_mut_bytes(buf),
176190
DecodingBuffer::U64(buf) => bytecast::u64_as_ne_mut_bytes(buf),
177191
DecodingBuffer::I64(buf) => bytecast::i64_as_ne_mut_bytes(buf),
192+
DecodingBuffer::F16(buf) => bytecast::f16_as_ne_mut_bytes(buf),
178193
DecodingBuffer::F32(buf) => bytecast::f32_as_ne_mut_bytes(buf),
179194
DecodingBuffer::F64(buf) => bytecast::f64_as_ne_mut_bytes(buf),
180195
}
@@ -303,6 +318,19 @@ fn predict_f32(input: &mut [u8], output: &mut [u8], samples: usize) {
303318
}
304319
}
305320

321+
fn predict_f16(input: &mut [u8], output: &mut [u8], samples: usize) {
322+
for i in samples..input.len() {
323+
input[i] = input[i].wrapping_add(input[i - samples]);
324+
}
325+
326+
for (i, chunk) in output.chunks_mut(2).enumerate() {
327+
chunk.copy_from_slice(&u16::to_ne_bytes(u16::from_be_bytes([
328+
input[i],
329+
input[input.len() / 2 + i],
330+
])));
331+
}
332+
}
333+
306334
fn predict_f64(input: &mut [u8], output: &mut [u8], samples: usize) {
307335
for i in samples..input.len() {
308336
input[i] = input[i].wrapping_add(input[i - samples]);
@@ -340,6 +368,7 @@ fn fix_endianness_and_predict(
340368
Predictor::FloatingPoint => {
341369
let mut buffer_copy = buf.to_vec();
342370
match bit_depth {
371+
16 => predict_f16(&mut buffer_copy, buf, samples),
343372
32 => predict_f32(&mut buffer_copy, buf, samples),
344373
64 => predict_f64(&mut buffer_copy, buf, samples),
345374
_ => unreachable!("Caller should have validated arguments. Please file a bug."),
@@ -1004,6 +1033,7 @@ impl<R: Read + Seek> Decoder<R> {
10041033
)),
10051034
},
10061035
SampleFormat::IEEEFP => match max_sample_bits {
1036+
16 => DecodingResult::new_f16(buffer_size, &self.limits),
10071037
32 => DecodingResult::new_f32(buffer_size, &self.limits),
10081038
64 => DecodingResult::new_f64(buffer_size, &self.limits),
10091039
n => Err(TiffError::UnsupportedError(

tests/decode_fp16_images.rs

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
extern crate tiff;
2+
3+
use tiff::decoder::{Decoder, DecodingResult};
4+
use tiff::ColorType;
5+
6+
use std::fs::File;
7+
use std::path::PathBuf;
8+
9+
const TEST_IMAGE_DIR: &str = "./tests/images/";
10+
11+
/// Test a basic all white image
12+
#[test]
13+
fn test_white_ieee_fp16() {
14+
let filenames = ["white-fp16.tiff"];
15+
16+
for filename in filenames.iter() {
17+
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
18+
let img_file = File::open(path).expect("Cannot find test image!");
19+
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
20+
assert_eq!(
21+
decoder.dimensions().expect("Cannot get dimensions"),
22+
(256, 256)
23+
);
24+
assert_eq!(
25+
decoder.colortype().expect("Cannot get colortype"),
26+
ColorType::Gray(16)
27+
);
28+
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
29+
for p in img {
30+
assert!(p == half::f16::from_f32_const(1.0));
31+
}
32+
} else {
33+
panic!("Wrong data type");
34+
}
35+
}
36+
}
37+
38+
/// Test a single black pixel, to make sure scaling is ok
39+
#[test]
40+
fn test_one_black_pixel_ieee_fp16() {
41+
let filenames = ["single-black-fp16.tiff"];
42+
43+
for filename in filenames.iter() {
44+
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
45+
let img_file = File::open(path).expect("Cannot find test image!");
46+
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
47+
assert_eq!(
48+
decoder.dimensions().expect("Cannot get dimensions"),
49+
(256, 256)
50+
);
51+
assert_eq!(
52+
decoder.colortype().expect("Cannot get colortype"),
53+
ColorType::Gray(16)
54+
);
55+
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
56+
for (i, p) in img.iter().enumerate() {
57+
if i == 0 {
58+
assert!(p < &half::f16::from_f32_const(0.001));
59+
} else {
60+
assert!(p == &half::f16::from_f32_const(1.0));
61+
}
62+
}
63+
} else {
64+
panic!("Wrong data type");
65+
}
66+
}
67+
}
68+
69+
/// Test white with horizontal differencing predictor
70+
#[test]
71+
fn test_pattern_horizontal_differencing_ieee_fp16() {
72+
let filenames = ["white-fp16-pred2.tiff"];
73+
74+
for filename in filenames.iter() {
75+
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
76+
let img_file = File::open(path).expect("Cannot find test image!");
77+
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
78+
assert_eq!(
79+
decoder.dimensions().expect("Cannot get dimensions"),
80+
(256, 256)
81+
);
82+
assert_eq!(
83+
decoder.colortype().expect("Cannot get colortype"),
84+
ColorType::Gray(16)
85+
);
86+
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
87+
// 0, 2, 5, 8, 12, 16, 255 are black
88+
let black = [0, 2, 5, 8, 12, 16, 255];
89+
for (i, p) in img.iter().enumerate() {
90+
if black.contains(&i) {
91+
assert!(p < &half::f16::from_f32_const(0.001));
92+
} else {
93+
assert!(p == &half::f16::from_f32_const(1.0));
94+
}
95+
}
96+
} else {
97+
panic!("Wrong data type");
98+
}
99+
}
100+
}
101+
102+
/// Test white with floating point predictor
103+
#[test]
104+
fn test_pattern_predictor_ieee_fp16() {
105+
let filenames = ["white-fp16-pred3.tiff"];
106+
107+
for filename in filenames.iter() {
108+
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
109+
let img_file = File::open(path).expect("Cannot find test image!");
110+
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
111+
assert_eq!(
112+
decoder.dimensions().expect("Cannot get dimensions"),
113+
(256, 256)
114+
);
115+
assert_eq!(
116+
decoder.colortype().expect("Cannot get colortype"),
117+
ColorType::Gray(16)
118+
);
119+
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
120+
// 0, 2, 5, 8, 12, 16, 255 are black
121+
let black = [0, 2, 5, 8, 12, 16, 255];
122+
for (i, p) in img.iter().enumerate() {
123+
if black.contains(&i) {
124+
assert!(p < &half::f16::from_f32_const(0.001));
125+
} else {
126+
assert!(p == &half::f16::from_f32_const(1.0));
127+
}
128+
}
129+
} else {
130+
panic!("Wrong data type");
131+
}
132+
}
133+
}
134+
135+
/// Test several random images
136+
/// we'rell compare against a pnm file, that scales from 0 (for 0.0) to 65767 (for 1.0)
137+
#[test]
138+
fn test_predictor_ieee_fp16() {
139+
// first parse pnm, skip the first 4 \n
140+
let pnm_path = PathBuf::from(TEST_IMAGE_DIR).join("random-fp16.pgm");
141+
let pnm_bytes = std::fs::read(pnm_path).expect("Failed to read expected PNM file");
142+
143+
// PGM looks like this:
144+
// ---
145+
// P5
146+
// #Created with GIMP
147+
// 16 16
148+
// 65535
149+
// ... <big-endian bytes>
150+
// ---
151+
// get index of 4th \n
152+
let byte_start = pnm_bytes
153+
.iter()
154+
.enumerate()
155+
.filter(|(_, &v)| v == b'\n')
156+
.map(|(i, _)| i)
157+
.nth(3)
158+
.expect("Must be 4 \\n's");
159+
160+
let pnm_values: Vec<f32> = pnm_bytes[(byte_start + 1)..]
161+
.chunks(2)
162+
.map(|slice| {
163+
let bts = [slice[0], slice[1]];
164+
(u16::from_be_bytes(bts) as f32) / (u16::MAX as f32)
165+
})
166+
.collect();
167+
assert!(pnm_values.len() == 256);
168+
169+
let filenames = [
170+
"random-fp16-pred2.tiff",
171+
"random-fp16-pred3.tiff",
172+
"random-fp16.tiff",
173+
];
174+
175+
for filename in filenames.iter() {
176+
let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
177+
let img_file = File::open(path).expect("Cannot find test image!");
178+
let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
179+
assert_eq!(
180+
decoder.dimensions().expect("Cannot get dimensions"),
181+
(16, 16)
182+
);
183+
assert_eq!(
184+
decoder.colortype().expect("Cannot get colortype"),
185+
ColorType::Gray(16)
186+
);
187+
if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
188+
for (exp, found) in std::iter::zip(pnm_values.iter(), img.iter()) {
189+
assert!((exp - found.to_f32()).abs() < 0.0001);
190+
}
191+
} else {
192+
panic!("Wrong data type");
193+
}
194+
}
195+
}
683 Bytes
Binary file not shown.
698 Bytes
Binary file not shown.

tests/images/random-fp16.pgm

546 Bytes
Binary file not shown.

tests/images/random-fp16.tiff

621 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)