use ndarray::Array2;
pub(crate) fn sincosdg(angle_deg: f64) -> (f64, f64) {
let a = angle_deg.rem_euclid(360.0);
match a {
0.0 => (1.0, 0.0),
90.0 => (0.0, 1.0),
180.0 => (-1.0, 0.0),
270.0 => (0.0, -1.0),
v => {
let rad = v * std::f64::consts::PI / 180.0;
(rad.cos(), rad.sin())
}
}
}
fn output_shape(shape: (usize, usize), angle_deg: f64, reshape: bool) -> (usize, usize) {
let (c, s) = sincosdg(angle_deg);
let (nrows, ncols) = shape;
if !reshape {
return shape;
}
let mut rs = [0.0f64; 4];
let mut cs = [0.0f64; 4];
for (k, (r, col)) in [
(0.0, 0.0),
(0.0, ncols as f64),
(nrows as f64, 0.0),
(nrows as f64, ncols as f64),
]
.into_iter()
.enumerate()
{
rs[k] = c * r + s * col;
cs[k] = -s * r + c * col;
}
let ptp_r =
rs.iter().cloned().fold(f64::MIN, f64::max) - rs.iter().cloned().fold(f64::MAX, f64::min);
let ptp_c =
cs.iter().cloned().fold(f64::MIN, f64::max) - cs.iter().cloned().fold(f64::MAX, f64::min);
((ptp_r + 0.5) as usize, (ptp_c + 0.5) as usize)
}
pub fn rotate(input: &Array2<f64>, angle_deg: f64, reshape: bool, order: u32) -> Array2<f64> {
assert!(order <= 1, "only spline orders 0 and 1 are supported");
let (nrows, ncols) = (input.nrows(), input.ncols());
let (c, s) = sincosdg(angle_deg);
let (out_rows, out_cols) = output_shape((nrows, ncols), angle_deg, reshape);
let in_center = ((nrows as f64 - 1.0) / 2.0, (ncols as f64 - 1.0) / 2.0);
let out_center_r = c * (out_rows as f64 - 1.0) / 2.0 + s * (out_cols as f64 - 1.0) / 2.0;
let out_center_c = -s * (out_rows as f64 - 1.0) / 2.0 + c * (out_cols as f64 - 1.0) / 2.0;
let offset = (in_center.0 - out_center_r, in_center.1 - out_center_c);
let mut out = Array2::<f64>::zeros((out_rows, out_cols));
for or in 0..out_rows {
for oc in 0..out_cols {
let pr = c * or as f64 + s * oc as f64 + offset.0;
let pc = -s * or as f64 + c * oc as f64 + offset.1;
out[(or, oc)] = match order {
0 => sample_nearest(input, pr, pc, 0.0),
_ => sample_bilinear(input, pr, pc, 0.0),
};
}
}
out
}
pub(crate) fn sample_nearest(input: &Array2<f64>, pr: f64, pc: f64, fill: f64) -> f64 {
let max_r = input.nrows() as f64 - 1.0;
let max_c = input.ncols() as f64 - 1.0;
if pr < 0.0 || pc < 0.0 || pr > max_r || pc > max_c {
return fill;
}
let r = (pr + 0.5).floor() as usize; let c = (pc + 0.5).floor() as usize;
input[(r, c)]
}
pub(crate) fn sample_bilinear(input: &Array2<f64>, pr: f64, pc: f64, fill: f64) -> f64 {
let max_r = input.nrows() as f64 - 1.0;
let max_c = input.ncols() as f64 - 1.0;
if pr < 0.0 || pc < 0.0 || pr > max_r || pc > max_c {
return fill;
}
let r0 = pr.floor();
let c0 = pc.floor();
let tr = pr - r0;
let tc = pc - c0;
let (ri, ci) = (r0 as usize, c0 as usize);
let r1 = (ri + 1).min(input.nrows() - 1);
let c1 = (ci + 1).min(input.ncols() - 1);
let top = input[(ri, ci)] * (1.0 - tc) + input[(ri, c1)] * tc;
let bot = input[(r1, ci)] * (1.0 - tc) + input[(r1, c1)] * tc;
top * (1.0 - tr) + bot * tr
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
#[test]
fn identity_at_zero() {
let a = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let out = rotate(&a, 0.0, true, 0);
assert_eq!(out, a);
}
#[test]
fn quarter_turn_matches_scipy() {
let a = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let out = rotate(&a, 90.0, true, 0);
assert_eq!(out.dim(), (3, 2));
assert_eq!(out, array![[3.0, 6.0], [2.0, 5.0], [1.0, 4.0]]);
}
#[test]
fn crop_keeps_shape_and_zeroes_outside() {
let a = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]];
let out = rotate(&a, 45.0, false, 0);
assert_eq!(out.dim(), (4, 2));
assert_eq!(out, array![[0.0, 0.0], [4.0, 0.0], [0.0, 6.0], [0.0, 0.0]]);
}
#[test]
fn zero_fill_mode_untouched() {
let a = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]];
let out = rotate(&a, 45.0, false, 0);
assert_eq!(out[(3, 0)], 0.0);
}
#[test]
fn reshape_grows_the_canvas() {
let a = Array2::<f64>::from_elem((80, 300), 1.0);
let out45 = rotate(&a, 45.0, true, 0);
assert_eq!(out45.dim(), (269, 269));
let out11 = rotate(&a, 11.0, true, 0);
assert_eq!(out11.dim(), (136, 310));
}
#[test]
fn negative_and_large_angles() {
let a = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let out = rotate(&a, -270.0, true, 0); assert_eq!(out, rotate(&a, 90.0, true, 0));
let out2 = rotate(&a, 450.0, true, 0);
assert_eq!(out2, rotate(&a, 90.0, true, 0));
}
}