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 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}