use mappers::{ConversionPipe, Projection};
use ndarray::{Array, Axis, concatenate, stack};
use crate::{
GenericXYPair, IJPair, IXJYPair, MinMaxPair, RasterBounds, ResamplingFilter, SourceXYPair,
TargetXYPair, WarperError,
};
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct WarperParameters {
pub scales: GenericXYPair,
pub offsets: IJPair,
}
impl WarperParameters {
pub fn compute<F: ResamplingFilter, SP: Projection, TP: Projection>(
source_bounds: &RasterBounds<SP, SourceXYPair>,
target_bounds: &RasterBounds<TP, TargetXYPair>,
) -> Result<Self, WarperError> {
let wrap_margin = F::X_RADIUS.max(F::Y_RADIUS) as u32;
let tgt_extrema = compute_target_outer_extrema(source_bounds, target_bounds)?;
let clamped_extrema =
compute_clamped_extrema(&tgt_extrema, source_bounds.shape, wrap_margin)?;
let (offsets, scales) = compute_offsets_and_scales(
&tgt_extrema,
&clamped_extrema,
source_bounds,
target_bounds,
IJPair {
i: F::X_RADIUS as u32,
j: F::Y_RADIUS as u32,
},
);
Ok(Self { scales, offsets })
}
}
fn compute_target_outer_extrema<SP: Projection, TP: Projection>(
source_bounds: &RasterBounds<SP, SourceXYPair>,
target_bounds: &RasterBounds<TP, TargetXYPair>,
) -> Result<MinMaxPair<IXJYPair>, WarperError> {
let proj_pipe = &target_bounds.proj.pipe_to(&source_bounds.proj);
let tgt_extr = get_target_extrema_on_source(target_bounds, proj_pipe)?;
let min_x_out = ((tgt_extr.min.x - source_bounds.min.x) / source_bounds.spacing.x) + 0.5;
let max_x_out = ((tgt_extr.max.x - source_bounds.min.x) / source_bounds.spacing.x) + 0.5;
let max_y_out = ((source_bounds.max.y - tgt_extr.min.y) / source_bounds.spacing.y) + 0.5;
let min_y_out = ((source_bounds.max.y - tgt_extr.max.y) / source_bounds.spacing.y) + 0.5;
Ok(MinMaxPair {
min: IXJYPair {
ix: min_x_out,
jy: min_y_out,
},
max: IXJYPair {
ix: max_x_out,
jy: max_y_out,
},
})
}
fn compute_clamped_extrema(
tgt_extr: &MinMaxPair<IXJYPair>,
src_shape: IJPair,
min_margin: u32,
) -> Result<MinMaxPair<IJPair>, WarperError> {
if tgt_extr.min.ix < f64::from(min_margin)
|| tgt_extr.min.jy < f64::from(min_margin)
|| tgt_extr.max.ix > f64::from(src_shape.i - min_margin)
|| tgt_extr.max.jy > f64::from(src_shape.j - min_margin)
{
return Err(WarperError::SourceRasterTooSmall);
}
let n_min_x_out_clamped = tgt_extr.min.ix.floor() as u32;
let n_min_y_out_clamped = tgt_extr.min.jy.floor() as u32;
let n_max_x_out_clamped = tgt_extr.max.ix.ceil() as u32;
let n_max_y_out_clamped = tgt_extr.max.jy.ceil() as u32;
Ok(MinMaxPair {
min: IJPair {
i: n_min_x_out_clamped,
j: n_min_y_out_clamped,
},
max: IJPair {
i: n_max_x_out_clamped,
j: n_max_y_out_clamped,
},
})
}
fn compute_offsets_and_scales<SP: Projection, TP: Projection>(
tgt_extrema: &MinMaxPair<IXJYPair>,
clamped_extrema: &MinMaxPair<IJPair>,
source_bounds: &RasterBounds<SP, SourceXYPair>,
target_bounds: &RasterBounds<TP, TargetXYPair>,
kernel_radius: IJPair,
) -> (IJPair, GenericXYPair) {
let offsets = compute_src_offsets(clamped_extrema.min, source_bounds.shape, kernel_radius);
let src_x_size_raw = f64::from(source_bounds.shape.i - clamped_extrema.min.i)
.min(tgt_extrema.max.ix - tgt_extrema.min.ix)
.max(0.0);
let src_y_size_raw = f64::from(source_bounds.shape.j - clamped_extrema.min.j)
.min(tgt_extrema.max.jy - tgt_extrema.min.jy)
.max(0.0);
let src_x_size = (source_bounds.shape.i as i32 - offsets.i as i32)
.min(clamped_extrema.max.i as i32 - offsets.i as i32 + kernel_radius.i as i32)
.max(0) as u32;
let src_y_size = (source_bounds.shape.j as i32 - offsets.j as i32)
.min(clamped_extrema.max.j as i32 - offsets.j as i32 + kernel_radius.j as i32)
.max(0) as u32;
let src_x_extra_size = f64::from(src_x_size) - src_x_size_raw;
let src_y_extra_size = f64::from(src_y_size) - src_y_size_raw;
let x_scale = f64::from(target_bounds.shape.i) / (f64::from(src_x_size) - src_x_extra_size);
let y_scale = f64::from(target_bounds.shape.j) / (f64::from(src_y_size) - src_y_extra_size);
(
offsets,
GenericXYPair {
x: x_scale,
y: y_scale,
},
)
}
fn compute_src_offsets(clamped_min: IJPair, src_shape: IJPair, kernel_radius: IJPair) -> IJPair {
let n_src_x_off = clamped_min
.i
.saturating_sub(kernel_radius.i)
.min(src_shape.i);
let n_src_y_off = clamped_min
.j
.saturating_sub(kernel_radius.j)
.min(src_shape.j);
IJPair {
i: n_src_x_off,
j: n_src_y_off,
}
}
fn get_target_extrema_on_source<SP: Projection, TP: Projection>(
target_bounds: &RasterBounds<TP, TargetXYPair>,
proj_pipe: &ConversionPipe<TP, SP>,
) -> Result<MinMaxPair<SourceXYPair>, WarperError> {
let x_min = 0.5f64.mul_add(-target_bounds.spacing.x, target_bounds.min.x);
let x_max = 0.5f64.mul_add(target_bounds.spacing.x, target_bounds.max.x);
let y_min = 0.5f64.mul_add(-target_bounds.spacing.y, target_bounds.min.y);
let y_max = 0.5f64.mul_add(target_bounds.spacing.y, target_bounds.max.y);
let u_edge_x = Array::range(x_min, x_max, target_bounds.spacing.x);
let u_edge_y = Array::from_elem(u_edge_x.raw_dim(), y_max);
let r_edge_y = Array::range(y_max, y_min, -target_bounds.spacing.y);
let r_edge_x = Array::from_elem(r_edge_y.raw_dim(), x_max);
let b_edge_x = Array::range(x_max, x_min, -target_bounds.spacing.x);
let b_edge_y = Array::from_elem(b_edge_x.raw_dim(), y_min);
let l_edge_y = Array::range(y_min, y_max, target_bounds.spacing.y);
let l_edge_x = Array::from_elem(l_edge_y.raw_dim(), x_min);
let u_edge_xy = stack(Axis(1), &[u_edge_x.view(), u_edge_y.view()])?;
let r_edge_xy = stack(Axis(1), &[r_edge_x.view(), r_edge_y.view()])?;
let b_edge_xy = stack(Axis(1), &[b_edge_x.view(), b_edge_y.view()])?;
let l_edge_xy = stack(Axis(1), &[l_edge_x.view(), l_edge_y.view()])?;
let edges_xy = concatenate(
Axis(0),
&[
u_edge_xy.view(),
r_edge_xy.view(),
b_edge_xy.view(),
l_edge_xy.view(),
],
)?;
let mut min_src_x = f64::INFINITY;
let mut max_src_x = f64::NEG_INFINITY;
let mut min_src_y = f64::INFINITY;
let mut max_src_y = f64::NEG_INFINITY;
edges_xy
.rows()
.into_iter()
.try_for_each(|xy| -> Result<(), WarperError> {
let (src_x, src_y) = proj_pipe.convert(xy[0], xy[1])?;
min_src_x = min_src_x.min(src_x);
max_src_x = max_src_x.max(src_x);
min_src_y = min_src_y.min(src_y);
max_src_y = max_src_y.max(src_y);
Ok(())
})?;
Ok(MinMaxPair {
min: SourceXYPair {
x: min_src_x,
y: min_src_y,
},
max: SourceXYPair {
x: max_src_x,
y: max_src_y,
},
})
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use float_cmp::assert_approx_eq;
use super::{compute_clamped_extrema, compute_target_outer_extrema};
use crate::{
CubicBSpline, IJPair, ResamplingFilter, SourceXYPair, TargetXYPair, tests::reference_setup,
warp_params::compute_offsets_and_scales,
};
#[test]
fn assert_with_sample_values() -> Result<()> {
let (source_bounds, target_bounds) = reference_setup()?;
let source_bounds = source_bounds.cast_xy_pairs::<SourceXYPair>();
let target_bounds = target_bounds.cast_xy_pairs::<TargetXYPair>();
let extrema = compute_target_outer_extrema(&source_bounds, &target_bounds).unwrap();
assert_approx_eq!(f64, extrema.min.ix, 4.457_122_955_747_991, epsilon = 1e-6);
assert_approx_eq!(f64, extrema.min.jy, 6.936_329_855_097_795_9, epsilon = 1e-6);
assert_approx_eq!(f64, extrema.max.ix, 26.145_584_743_939_651, epsilon = 1e-6);
assert_approx_eq!(f64, extrema.max.jy, 28.722_607_331_122_93, epsilon = 1e-6);
let clamped_extrema = compute_clamped_extrema(&extrema, source_bounds.shape, 1)?;
assert_eq!(clamped_extrema.min.i, 4);
assert_eq!(clamped_extrema.min.j, 6);
assert_eq!(clamped_extrema.max.i, 27);
assert_eq!(clamped_extrema.max.j, 29);
let (offsets, scales) = compute_offsets_and_scales(
&extrema,
&clamped_extrema,
&source_bounds,
&target_bounds,
IJPair {
i: CubicBSpline::X_RADIUS as u32,
j: CubicBSpline::Y_RADIUS as u32,
},
);
assert_eq!(offsets.i, 2);
assert_eq!(offsets.j, 4);
assert_approx_eq!(f64, scales.x, 1.982_621_009_269_152_7, epsilon = 1e-6);
assert_approx_eq!(f64, scales.y, 2.570_425_354_291_278_3, epsilon = 1e-6);
Ok(())
}
}