Skip to main content

optical_flow_lk/
float_image.rs

1use std::fmt;
2
3/// A borrowed, validated, row-major f32 image, e.g. a thermal frame in °C.
4///
5/// The f32 detector and tracker work in the image's own unit: no range
6/// normalization and no 8-bit conversion takes place. Validation guarantees
7/// that every value is finite and at most [`MAX_ABS_VALUE`](Self::MAX_ABS_VALUE)
8/// in magnitude, so no intermediate can overflow and no NaN can reach the
9/// score comparisons. Replace invalid pixels (e.g. dead sensor pixels) before
10/// creating the view.
11#[derive(Clone, Copy, Debug)]
12pub struct FloatImageView<'a> {
13    pixels: &'a [f32],
14    width: u32,
15    height: u32,
16}
17
18impl<'a> FloatImageView<'a> {
19    /// Largest accepted pixel magnitude, 2^24. Within it, Scharr gradients,
20    /// structure-tensor sums and their determinant stay finite.
21    pub const MAX_ABS_VALUE: f32 = 16_777_216.0;
22
23    /// Validates `pixels` as a `width` x `height` row-major image.
24    ///
25    /// Costs one pass over the pixels.
26    pub fn new(pixels: &'a [f32], width: u32, height: u32) -> Result<Self, FloatImageError> {
27        let expected = width as u64 * height as u64;
28        if pixels.len() as u64 != expected {
29            return Err(FloatImageError::SizeMismatch {
30                expected,
31                actual: pixels.len(),
32            });
33        }
34        if let Some(index) = first_invalid(pixels) {
35            return Err(FloatImageError::InvalidValue {
36                index,
37                value: pixels[index],
38            });
39        }
40        Ok(Self {
41            pixels,
42            width,
43            height,
44        })
45    }
46
47    /// Width in pixels.
48    pub fn width(&self) -> u32 {
49        self.width
50    }
51
52    /// Height in pixels.
53    pub fn height(&self) -> u32 {
54        self.height
55    }
56
57    /// `(width, height)` in pixels.
58    pub fn dimensions(&self) -> (u32, u32) {
59        (self.width, self.height)
60    }
61
62    /// The validated pixels, row by row.
63    pub fn pixels(&self) -> &'a [f32] {
64        self.pixels
65    }
66}
67
68/// Why [`FloatImageView::new`] rejected an image.
69#[derive(Clone, Copy, Debug, PartialEq)]
70pub enum FloatImageError {
71    /// The pixel slice does not hold exactly `width * height` values.
72    SizeMismatch {
73        /// `width * height`.
74        expected: u64,
75        /// Length of the pixel slice.
76        actual: usize,
77    },
78    /// A pixel is NaN, infinite, or larger in magnitude than
79    /// [`FloatImageView::MAX_ABS_VALUE`]. Reports the first such pixel.
80    InvalidValue {
81        /// Row-major index of the pixel.
82        index: usize,
83        /// Its value.
84        value: f32,
85    },
86}
87
88impl fmt::Display for FloatImageError {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        match self {
91            Self::SizeMismatch { expected, actual } => {
92                write!(f, "image needs {expected} pixels, got {actual}")
93            }
94            Self::InvalidValue { index, value } => write!(
95                f,
96                "pixel {index} is {value}; values must be finite and at most {} in magnitude",
97                FloatImageView::MAX_ABS_VALUE
98            ),
99        }
100    }
101}
102
103impl std::error::Error for FloatImageError {}
104
105/// Index of the first pixel that is NaN, infinite or out of range.
106fn first_invalid(pixels: &[f32]) -> Option<usize> {
107    // NaN fails the comparison, so it is invalid as well.
108    let valid = |value: f32| value.abs() <= FloatImageView::MAX_ABS_VALUE;
109    const CHUNK: usize = 256;
110    for (chunk_index, chunk) in pixels.chunks(CHUNK).enumerate() {
111        // A branch-free reduction per chunk keeps the common case vectorized.
112        let all_valid = chunk.iter().fold(true, |all, &value| all & valid(value));
113        if !all_valid {
114            let offset = chunk.iter().position(|&value| !valid(value));
115            return offset.map(|offset| chunk_index * CHUNK + offset);
116        }
117    }
118    None
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn accepts_finite_values_within_range_and_empty_images() {
127        let max = FloatImageView::MAX_ABS_VALUE;
128        let pixels = [0.0, -0.0, 1e-30, -max, max, 25.5];
129        let view = FloatImageView::new(&pixels, 3, 2).unwrap();
130        assert_eq!(view.dimensions(), (3, 2));
131        assert_eq!(view.pixels(), &pixels);
132        assert!(FloatImageView::new(&[], 0, 7).is_ok());
133    }
134
135    #[test]
136    fn rejects_size_mismatch_without_overflow() {
137        assert_eq!(
138            FloatImageView::new(&[0.0; 5], 2, 3).unwrap_err(),
139            FloatImageError::SizeMismatch {
140                expected: 6,
141                actual: 5
142            }
143        );
144        let error = FloatImageView::new(&[0.0; 4], u32::MAX, u32::MAX).unwrap_err();
145        assert!(matches!(error, FloatImageError::SizeMismatch { .. }));
146    }
147
148    #[test]
149    fn reports_the_first_invalid_value() {
150        let max = FloatImageView::MAX_ABS_VALUE;
151        for (bad, index) in [
152            (f32::NAN, 0),
153            (f32::INFINITY, 300),
154            (f32::NEG_INFINITY, 511),
155            (max * 2.0, 999),
156            (-max * 2.0, 257),
157        ] {
158            let mut pixels = vec![1.0f32; 1000];
159            pixels[index] = bad;
160            // A later invalid value must not hide the first one.
161            if index + 2 < pixels.len() {
162                pixels[index + 2] = f32::NAN;
163            }
164            match FloatImageView::new(&pixels, 100, 10).unwrap_err() {
165                FloatImageError::InvalidValue { index: got, value } => {
166                    assert_eq!(got, index);
167                    assert_eq!(value.to_bits(), bad.to_bits());
168                }
169                other => panic!("unexpected {other:?}"),
170            }
171        }
172    }
173}