Skip to main content

fpm_rs/model/
crop.rs

1use serde::{Deserialize, Serialize};
2
3use num_complex::Complex64;
4
5use crate::{Array2, Result, error::Error};
6
7/// Fractional Fourier-grid displacement relative to an integer crop origin.
8#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
9pub struct FourierOffset {
10    pub row: f64,
11    pub column: f64,
12}
13
14impl FourierOffset {
15    pub const fn new(row: f64, column: f64) -> Self {
16        Self { row, column }
17    }
18
19    pub fn is_zero(self) -> bool {
20        self.row.abs() <= 1e-12 && self.column.abs() <= 1e-12
21    }
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
25pub struct FourierCrop {
26    pub start_row: usize,
27    pub start_col: usize,
28    pub height: usize,
29    pub width: usize,
30}
31
32impl FourierCrop {
33    pub fn new(start_row: usize, start_col: usize, height: usize, width: usize) -> Self {
34        Self {
35            start_row,
36            start_col,
37            height,
38            width,
39        }
40    }
41
42    pub fn validate_inside(&self, shape: (usize, usize)) -> Result<()> {
43        let end_row = self.start_row.checked_add(self.height);
44        let end_col = self.start_col.checked_add(self.width);
45        if self.height == 0
46            || self.width == 0
47            || end_row.is_none_or(|end| end > shape.0)
48            || end_col.is_none_or(|end| end > shape.1)
49        {
50            return Err(Error::InvalidModel(format!(
51                "crop {self:?} lies outside reconstruction shape {shape:?}"
52            )));
53        }
54        Ok(())
55    }
56
57    pub fn extract<T: Copy>(&self, source: &Array2<T>, destination: &mut [T]) -> Result<()> {
58        self.validate_inside(source.shape())?;
59        if destination.len() != self.height * self.width {
60            return Err(Error::LengthMismatch {
61                actual: destination.len(),
62                expected: self.height * self.width,
63                shape: (self.height, self.width),
64            });
65        }
66        for row in 0..self.height {
67            let source_start = (self.start_row + row) * source.width() + self.start_col;
68            let destination_start = row * self.width;
69            destination[destination_start..destination_start + self.width]
70                .copy_from_slice(&source.as_slice()[source_start..source_start + self.width]);
71        }
72        Ok(())
73    }
74
75    pub fn validate_subpixel_inside(
76        &self,
77        shape: (usize, usize),
78        offset: FourierOffset,
79    ) -> Result<()> {
80        interpolation_axis(self.start_row, self.height, shape.0, offset.row)?;
81        interpolation_axis(self.start_col, self.width, shape.1, offset.column)?;
82        Ok(())
83    }
84
85    /// Bilinearly samples a potentially fractional crop from `source`.
86    ///
87    /// This is a local Fourier-grid interpolation, not a bandlimited shift
88    /// operator. Its approximation error grows with grid-frequency content and
89    /// fractional displacement; low-bandwidth objects are the intended regime.
90    pub fn extract_subpixel(
91        &self,
92        source: &Array2<Complex64>,
93        destination: &mut [Complex64],
94        offset: FourierOffset,
95    ) -> Result<()> {
96        if destination.len() != self.height * self.width {
97            return Err(Error::LengthMismatch {
98                actual: destination.len(),
99                expected: self.height * self.width,
100                shape: (self.height, self.width),
101            });
102        }
103        if offset.is_zero() {
104            return self.extract(source, destination);
105        }
106        let rows = interpolation_axis(self.start_row, self.height, source.height(), offset.row)?;
107        let columns =
108            interpolation_axis(self.start_col, self.width, source.width(), offset.column)?;
109        for row in 0..self.height {
110            let lower_row = rows.lower_start + row;
111            let upper_row = rows.upper_start + row;
112            for column in 0..self.width {
113                let lower_column = columns.lower_start + column;
114                let upper_column = columns.upper_start + column;
115                destination[row * self.width + column] = source[(lower_row, lower_column)]
116                    * (rows.lower_weight * columns.lower_weight)
117                    + source[(lower_row, upper_column)]
118                        * (rows.lower_weight * columns.upper_weight)
119                    + source[(upper_row, lower_column)]
120                        * (rows.upper_weight * columns.lower_weight)
121                    + source[(upper_row, upper_column)]
122                        * (rows.upper_weight * columns.upper_weight);
123            }
124        }
125        Ok(())
126    }
127
128    /// Adds the exact adjoint of [`Self::extract_subpixel`] to `destination`.
129    pub fn insert_subpixel_adjoint(
130        &self,
131        destination: &mut Array2<Complex64>,
132        update: &[Complex64],
133        scale: f64,
134        offset: FourierOffset,
135    ) -> Result<()> {
136        let destination_shape = destination.shape();
137        self.insert_subpixel_adjoint_slice(
138            destination.as_mut_slice(),
139            destination_shape,
140            update,
141            scale,
142            offset,
143        )
144    }
145
146    pub(crate) fn insert_subpixel_adjoint_slice(
147        &self,
148        destination: &mut [Complex64],
149        destination_shape: (usize, usize),
150        update: &[Complex64],
151        scale: f64,
152        offset: FourierOffset,
153    ) -> Result<()> {
154        if destination.len() != destination_shape.0 * destination_shape.1 {
155            return Err(Error::LengthMismatch {
156                actual: destination.len(),
157                expected: destination_shape.0 * destination_shape.1,
158                shape: destination_shape,
159            });
160        }
161        if update.len() != self.height * self.width {
162            return Err(Error::LengthMismatch {
163                actual: update.len(),
164                expected: self.height * self.width,
165                shape: (self.height, self.width),
166            });
167        }
168        if !scale.is_finite() {
169            return Err(Error::InvalidParameter {
170                name: "scale",
171                reason: "must be finite".into(),
172            });
173        }
174        if offset.is_zero() {
175            self.validate_inside(destination_shape)?;
176            for row in 0..self.height {
177                for column in 0..self.width {
178                    destination
179                        [(self.start_row + row) * destination_shape.1 + self.start_col + column] +=
180                        scale * update[row * self.width + column];
181                }
182            }
183            return Ok(());
184        }
185        let rows =
186            interpolation_axis(self.start_row, self.height, destination_shape.0, offset.row)?;
187        let columns = interpolation_axis(
188            self.start_col,
189            self.width,
190            destination_shape.1,
191            offset.column,
192        )?;
193        for row in 0..self.height {
194            let lower_row = rows.lower_start + row;
195            let upper_row = rows.upper_start + row;
196            for column in 0..self.width {
197                let lower_column = columns.lower_start + column;
198                let upper_column = columns.upper_start + column;
199                let value = scale * update[row * self.width + column];
200                destination[lower_row * destination_shape.1 + lower_column] +=
201                    value * (rows.lower_weight * columns.lower_weight);
202                destination[lower_row * destination_shape.1 + upper_column] +=
203                    value * (rows.lower_weight * columns.upper_weight);
204                destination[upper_row * destination_shape.1 + lower_column] +=
205                    value * (rows.upper_weight * columns.lower_weight);
206                destination[upper_row * destination_shape.1 + upper_column] +=
207                    value * (rows.upper_weight * columns.upper_weight);
208            }
209        }
210        Ok(())
211    }
212}
213
214#[derive(Clone, Copy, Debug)]
215struct AxisInterpolation {
216    lower_start: usize,
217    upper_start: usize,
218    lower_weight: f64,
219    upper_weight: f64,
220}
221
222fn interpolation_axis(
223    start: usize,
224    length: usize,
225    bound: usize,
226    offset: f64,
227) -> Result<AxisInterpolation> {
228    if length == 0 || bound == 0 || !offset.is_finite() {
229        return Err(Error::InvalidModel(
230            "subpixel crop dimensions and offset must be finite and non-zero".into(),
231        ));
232    }
233    let nearest = offset.round();
234    let (integer_offset, fraction) = if (offset - nearest).abs() <= 1e-12 {
235        (nearest, 0.0)
236    } else {
237        let floor = offset.floor();
238        (floor, offset - floor)
239    };
240    if integer_offset < isize::MIN as f64 || integer_offset > isize::MAX as f64 {
241        return Err(Error::InvalidModel(
242            "subpixel crop offset is outside the supported index range".into(),
243        ));
244    }
245    let start = isize::try_from(start).map_err(|_| {
246        Error::InvalidModel("crop origin is outside the supported index range".into())
247    })?;
248    let lower_start = start
249        .checked_add(integer_offset as isize)
250        .filter(|value| *value >= 0)
251        .ok_or_else(|| Error::InvalidModel("subpixel crop starts outside the grid".into()))?;
252    let lower_start = usize::try_from(lower_start)
253        .map_err(|_| Error::InvalidModel("subpixel crop starts outside the grid".into()))?;
254    let lower_end = lower_start
255        .checked_add(length)
256        .ok_or_else(|| Error::InvalidModel("subpixel crop dimensions overflow the grid".into()))?;
257    let needs_upper = fraction > 0.0;
258    if lower_end > bound || (needs_upper && lower_end >= bound) {
259        return Err(Error::InvalidModel(
260            "subpixel crop interpolation stencil lies outside the grid".into(),
261        ));
262    }
263    Ok(AxisInterpolation {
264        lower_start,
265        upper_start: lower_start + usize::from(needs_upper),
266        lower_weight: 1.0 - fraction,
267        upper_weight: fraction,
268    })
269}
270
271#[derive(Clone, Debug, Serialize, Deserialize)]
272pub struct CropIndices {
273    pub crops: Vec<FourierCrop>,
274}
275
276impl CropIndices {
277    pub fn new(crops: Vec<FourierCrop>) -> Self {
278        Self { crops }
279    }
280
281    pub fn len(&self) -> usize {
282        self.crops.len()
283    }
284
285    pub fn is_empty(&self) -> bool {
286        self.crops.is_empty()
287    }
288
289    pub fn get(&self, frame: usize) -> Result<FourierCrop> {
290        self.crops
291            .get(frame)
292            .copied()
293            .ok_or(Error::FrameOutOfRange {
294                index: frame,
295                frames: self.crops.len(),
296            })
297    }
298}