Skip to main content

eventcv_core/
image.rs

1use std::{error::Error, fmt};
2
3use crate::representation::{EventFrame, EventFrameData};
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
6pub enum PoolingMethod {
7    #[default]
8    Average,
9    Sum,
10}
11
12impl EventFrame {
13    /// Returns a resized frame, pooling shrinking axes and interpolating growing axes.
14    pub fn resize(
15        &self,
16        width: usize,
17        height: usize,
18        pooling: PoolingMethod,
19    ) -> Result<Self, ResizeError> {
20        if width == 0 || height == 0 {
21            return Err(ResizeError::InvalidDimensions);
22        }
23        width
24            .checked_mul(height)
25            .and_then(|plane_len| plane_len.checked_mul(self.channels))
26            .ok_or(ResizeError::SizeOverflow)?;
27        if width == self.width && height == self.height && pooling == PoolingMethod::Average {
28            return Ok(self.clone());
29        }
30
31        let data = match (&self.data, pooling) {
32            (EventFrameData::U8(data), PoolingMethod::Average) => EventFrameData::U8(
33                resize_average(data, self.width, self.height, width, height, self.channels),
34            ),
35            (EventFrameData::U16(data), PoolingMethod::Average) => EventFrameData::U16(
36                resize_average(data, self.width, self.height, width, height, self.channels),
37            ),
38            (EventFrameData::U64(data), PoolingMethod::Average) => EventFrameData::U64(
39                resize_average(data, self.width, self.height, width, height, self.channels),
40            ),
41            (EventFrameData::F32(data), PoolingMethod::Average) => EventFrameData::F32(
42                resize_average(data, self.width, self.height, width, height, self.channels),
43            ),
44            (EventFrameData::U8(data), PoolingMethod::Sum) => EventFrameData::U64(resize_sum(
45                data,
46                self.width,
47                self.height,
48                width,
49                height,
50                self.channels,
51            )?),
52            (EventFrameData::U16(data), PoolingMethod::Sum) => EventFrameData::U64(resize_sum(
53                data,
54                self.width,
55                self.height,
56                width,
57                height,
58                self.channels,
59            )?),
60            (EventFrameData::U64(data), PoolingMethod::Sum) => EventFrameData::U64(resize_sum(
61                data,
62                self.width,
63                self.height,
64                width,
65                height,
66                self.channels,
67            )?),
68            (EventFrameData::F32(data), PoolingMethod::Sum) => EventFrameData::F32(
69                resize_float_sum(data, self.width, self.height, width, height, self.channels),
70            ),
71        };
72
73        Ok(Self {
74            data,
75            channels: self.channels,
76            width,
77            height,
78            kind: self.kind,
79            channel_names: self.channel_names.clone(),
80        })
81    }
82}
83
84trait ResizeValue: Copy {
85    fn to_f64(self) -> f64;
86    fn from_f64(value: f64) -> Self;
87}
88
89trait IntegerResizeValue: ResizeValue {
90    fn to_u64(self) -> u64;
91}
92
93macro_rules! impl_resize_value {
94    ($type:ty) => {
95        impl ResizeValue for $type {
96            fn to_f64(self) -> f64 {
97                self as f64
98            }
99
100            fn from_f64(value: f64) -> Self {
101                value.round() as Self
102            }
103        }
104
105        impl IntegerResizeValue for $type {
106            fn to_u64(self) -> u64 {
107                self as u64
108            }
109        }
110    };
111}
112
113impl_resize_value!(u8);
114impl_resize_value!(u16);
115impl_resize_value!(u64);
116
117impl ResizeValue for f32 {
118    fn to_f64(self) -> f64 {
119        f64::from(self)
120    }
121
122    fn from_f64(value: f64) -> Self {
123        value as f32
124    }
125}
126
127fn resize_average<T: ResizeValue>(
128    data: &[T],
129    source_width: usize,
130    source_height: usize,
131    width: usize,
132    height: usize,
133    channels: usize,
134) -> Vec<T> {
135    let x_samples = average_axis_samples(source_width, width);
136    let y_samples = average_axis_samples(source_height, height);
137    resize_weighted(
138        data,
139        source_width,
140        source_height,
141        width,
142        height,
143        channels,
144        &x_samples,
145        &y_samples,
146    )
147}
148
149fn resize_sum<T: IntegerResizeValue>(
150    data: &[T],
151    source_width: usize,
152    source_height: usize,
153    width: usize,
154    height: usize,
155    channels: usize,
156) -> Result<Vec<u64>, ResizeError> {
157    let summed_width = width.min(source_width);
158    let summed_height = height.min(source_height);
159    let summed = sum_bins(
160        data,
161        source_width,
162        source_height,
163        summed_width,
164        summed_height,
165        channels,
166    )?;
167
168    if summed_width == width && summed_height == height {
169        return Ok(summed);
170    }
171
172    Ok(resize_average(
173        &summed,
174        summed_width,
175        summed_height,
176        width,
177        height,
178        channels,
179    ))
180}
181
182fn resize_float_sum(
183    data: &[f32],
184    source_width: usize,
185    source_height: usize,
186    width: usize,
187    height: usize,
188    channels: usize,
189) -> Vec<f32> {
190    let summed_width = width.min(source_width);
191    let summed_height = height.min(source_height);
192    let summed = sum_float_bins(
193        data,
194        source_width,
195        source_height,
196        summed_width,
197        summed_height,
198        channels,
199    );
200
201    if summed_width == width && summed_height == height {
202        return summed;
203    }
204
205    resize_average(
206        &summed,
207        summed_width,
208        summed_height,
209        width,
210        height,
211        channels,
212    )
213}
214
215fn sum_bins<T: IntegerResizeValue>(
216    data: &[T],
217    source_width: usize,
218    source_height: usize,
219    width: usize,
220    height: usize,
221    channels: usize,
222) -> Result<Vec<u64>, ResizeError> {
223    let x_ranges = axis_ranges(source_width, width);
224    let y_ranges = axis_ranges(source_height, height);
225    let mut resized = Vec::with_capacity(channels * width * height);
226    let source_plane_len = source_width * source_height;
227
228    for channel in 0..channels {
229        let channel_offset = channel * source_plane_len;
230        for &(y_start, y_end) in &y_ranges {
231            for &(x_start, x_end) in &x_ranges {
232                let mut sum = 0_u64;
233                for source_y in y_start..y_end {
234                    for source_x in x_start..x_end {
235                        sum = sum
236                            .checked_add(
237                                data[channel_offset + source_y * source_width + source_x].to_u64(),
238                            )
239                            .ok_or(ResizeError::SumOverflow)?;
240                    }
241                }
242                resized.push(sum);
243            }
244        }
245    }
246
247    Ok(resized)
248}
249
250fn sum_float_bins(
251    data: &[f32],
252    source_width: usize,
253    source_height: usize,
254    width: usize,
255    height: usize,
256    channels: usize,
257) -> Vec<f32> {
258    let x_ranges = axis_ranges(source_width, width);
259    let y_ranges = axis_ranges(source_height, height);
260    let mut resized = Vec::with_capacity(channels * width * height);
261    let source_plane_len = source_width * source_height;
262
263    for channel in 0..channels {
264        let channel_offset = channel * source_plane_len;
265        for &(y_start, y_end) in &y_ranges {
266            for &(x_start, x_end) in &x_ranges {
267                let mut sum = 0.0;
268                for source_y in y_start..y_end {
269                    for source_x in x_start..x_end {
270                        sum += data[channel_offset + source_y * source_width + source_x];
271                    }
272                }
273                resized.push(sum);
274            }
275        }
276    }
277
278    resized
279}
280
281#[allow(clippy::too_many_arguments)]
282fn resize_weighted<T: ResizeValue>(
283    data: &[T],
284    source_width: usize,
285    source_height: usize,
286    width: usize,
287    height: usize,
288    channels: usize,
289    x_samples: &[Vec<(usize, f64)>],
290    y_samples: &[Vec<(usize, f64)>],
291) -> Vec<T> {
292    let mut resized = Vec::with_capacity(channels * width * height);
293    let source_plane_len = source_width * source_height;
294
295    for channel in 0..channels {
296        let channel_offset = channel * source_plane_len;
297        for y_sample in y_samples {
298            for x_sample in x_samples {
299                let mut value = 0.0;
300                for &(source_y, y_weight) in y_sample {
301                    for &(source_x, x_weight) in x_sample {
302                        value += data[channel_offset + source_y * source_width + source_x].to_f64()
303                            * x_weight
304                            * y_weight;
305                    }
306                }
307                resized.push(T::from_f64(value));
308            }
309        }
310    }
311
312    resized
313}
314
315fn average_axis_samples(source_length: usize, length: usize) -> Vec<Vec<(usize, f64)>> {
316    if length < source_length {
317        return axis_ranges(source_length, length)
318            .into_iter()
319            .map(|(start, end)| {
320                let weight = 1.0 / (end - start) as f64;
321                (start..end).map(|index| (index, weight)).collect()
322            })
323            .collect();
324    }
325
326    if length == source_length {
327        return (0..length).map(|index| vec![(index, 1.0)]).collect();
328    }
329
330    (0..length)
331        .map(|index| {
332            let position = ((index as f64 + 0.5) * source_length as f64 / length as f64 - 0.5)
333                .clamp(0.0, (source_length - 1) as f64);
334            let lower = position.floor() as usize;
335            let upper = (lower + 1).min(source_length - 1);
336            if lower == upper {
337                vec![(lower, 1.0)]
338            } else {
339                let upper_weight = position - lower as f64;
340                vec![(lower, 1.0 - upper_weight), (upper, upper_weight)]
341            }
342        })
343        .collect()
344}
345
346fn axis_ranges(source_length: usize, length: usize) -> Vec<(usize, usize)> {
347    (0..length)
348        .map(|index| {
349            (
350                proportional_boundary(index, source_length, length),
351                proportional_boundary(index + 1, source_length, length),
352            )
353        })
354        .collect()
355}
356
357fn proportional_boundary(index: usize, source_length: usize, length: usize) -> usize {
358    let numerator = index as u128 * source_length as u128;
359    numerator.div_ceil(length as u128) as usize
360}
361
362#[derive(Debug, PartialEq, Eq)]
363pub enum ResizeError {
364    InvalidDimensions,
365    SizeOverflow,
366    SumOverflow,
367}
368
369impl fmt::Display for ResizeError {
370    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
371        match self {
372            Self::InvalidDimensions => formatter.write_str("resize dimensions must be positive"),
373            Self::SizeOverflow => formatter.write_str("resize dimensions are too large"),
374            Self::SumOverflow => formatter.write_str("pooled value exceeds uint64 capacity"),
375        }
376    }
377}
378
379impl Error for ResizeError {}
380
381#[cfg(test)]
382mod tests {
383    use super::{PoolingMethod, ResizeError};
384    use crate::representation::{EventFrame, EventFrameData, RepresentationKind};
385
386    #[test]
387    fn average_pools_proportional_bins_and_preserves_metadata() {
388        let frame = EventFrame {
389            data: EventFrameData::U8(vec![1, 2, 3, 4, 5, 5, 4, 3, 2, 1]),
390            channels: 2,
391            width: 5,
392            height: 1,
393            kind: RepresentationKind::Polarity,
394            channel_names: vec!["positive".to_owned(), "negative".to_owned()],
395        };
396
397        let resized = frame.resize(2, 1, PoolingMethod::Average).unwrap();
398
399        assert_eq!(resized.shape(), (2, 1, 2));
400        assert_eq!(resized.channel_names(), ["positive", "negative"]);
401        assert_eq!(resized.kind(), RepresentationKind::Polarity);
402        assert_eq!(resized.data(), &EventFrameData::U8(vec![2, 5, 4, 2]));
403        assert_eq!(frame.shape(), (2, 1, 5));
404    }
405
406    #[test]
407    fn sum_pooling_widens_and_preserves_channel_totals() {
408        let frame = EventFrame {
409            data: EventFrameData::U16(vec![1, 2, 3, 4, 5, 5, 4, 3, 2, 1]),
410            channels: 2,
411            width: 5,
412            height: 1,
413            kind: RepresentationKind::Polarity,
414            channel_names: vec!["positive".to_owned(), "negative".to_owned()],
415        };
416
417        let resized = frame.resize(2, 1, PoolingMethod::Sum).unwrap();
418
419        assert_eq!(resized.data(), &EventFrameData::U64(vec![6, 9, 12, 3]));
420        let EventFrameData::U64(values) = resized.data() else {
421            panic!("sum pooling must return uint64 data");
422        };
423        assert_eq!(values.iter().sum::<u64>(), 30);
424    }
425
426    #[test]
427    fn bilinear_enlargement_is_center_aligned_and_preserves_dtype() {
428        let frame = EventFrame {
429            data: EventFrameData::U8(vec![0, 10, 10, 0]),
430            channels: 2,
431            width: 2,
432            height: 1,
433            kind: RepresentationKind::Polarity,
434            channel_names: vec!["positive".to_owned(), "negative".to_owned()],
435        };
436
437        let resized = frame.resize(4, 1, PoolingMethod::Average).unwrap();
438
439        assert_eq!(
440            resized.data(),
441            &EventFrameData::U8(vec![0, 3, 8, 10, 10, 8, 3, 0])
442        );
443    }
444
445    #[test]
446    fn supports_mixed_pooling_and_interpolation() {
447        let frame = EventFrame {
448            data: EventFrameData::U16(
449                [vec![7_u16; 8], vec![9_u16; 8]]
450                    .into_iter()
451                    .flatten()
452                    .collect(),
453            ),
454            channels: 2,
455            width: 2,
456            height: 4,
457            kind: RepresentationKind::Polarity,
458            channel_names: vec!["positive".to_owned(), "negative".to_owned()],
459        };
460
461        let resized = frame.resize(4, 2, PoolingMethod::Average).unwrap();
462        let summed = frame.resize(4, 2, PoolingMethod::Sum).unwrap();
463
464        assert_eq!(resized.shape(), (2, 2, 4));
465        assert_eq!(
466            resized.data(),
467            &EventFrameData::U16(
468                [vec![7_u16; 8], vec![9_u16; 8]]
469                    .into_iter()
470                    .flatten()
471                    .collect()
472            )
473        );
474        assert_eq!(
475            summed.data(),
476            &EventFrameData::U64(
477                [vec![14_u64; 8], vec![18_u64; 8]]
478                    .into_iter()
479                    .flatten()
480                    .collect()
481            )
482        );
483    }
484
485    #[test]
486    fn identity_resize_preserves_values() {
487        let frame = EventFrame {
488            data: EventFrameData::U16(vec![1, 2, 3, 4]),
489            channels: 2,
490            width: 2,
491            height: 1,
492            kind: RepresentationKind::Polarity,
493            channel_names: vec!["positive".to_owned(), "negative".to_owned()],
494        };
495
496        let resized = frame.resize(2, 1, PoolingMethod::Average).unwrap();
497
498        assert_eq!(resized.data(), frame.data());
499    }
500
501    #[test]
502    fn resizes_float_frames_without_integer_conversion() {
503        let frame = EventFrame {
504            data: EventFrameData::F32(vec![0.25, 0.75, -1.0, 2.0]),
505            channels: 1,
506            width: 2,
507            height: 2,
508            kind: RepresentationKind::Voxel,
509            channel_names: vec!["bin_0".to_owned()],
510        };
511
512        let average = frame.resize(1, 1, PoolingMethod::Average).unwrap();
513        let sum = frame.resize(1, 1, PoolingMethod::Sum).unwrap();
514
515        assert_eq!(average.data(), &EventFrameData::F32(vec![0.5]));
516        assert_eq!(sum.data(), &EventFrameData::F32(vec![2.0]));
517    }
518
519    #[test]
520    fn rejects_invalid_resize_dimensions_and_sum_overflow() {
521        let frame = EventFrame {
522            data: EventFrameData::U64(vec![u64::MAX, 1, 0, 0]),
523            channels: 2,
524            width: 2,
525            height: 1,
526            kind: RepresentationKind::Polarity,
527            channel_names: vec!["positive".to_owned(), "negative".to_owned()],
528        };
529
530        assert_eq!(
531            frame.resize(0, 1, PoolingMethod::Average).unwrap_err(),
532            ResizeError::InvalidDimensions
533        );
534        assert_eq!(
535            frame
536                .resize(usize::MAX, 2, PoolingMethod::Average)
537                .unwrap_err(),
538            ResizeError::SizeOverflow
539        );
540        assert_eq!(
541            frame.resize(1, 1, PoolingMethod::Sum).unwrap_err(),
542            ResizeError::SumOverflow
543        );
544    }
545}