1use serde::{Deserialize, Serialize};
2
3use num_complex::Complex64;
4
5use crate::{Array2, Result, error::Error};
6
7#[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 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 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}