use ndarray::{Array2, ArrayView2, ArrayView3, ArrayViewMut1, Axis};
use rayon::prelude::*;
use thiserror::Error;
use crate::float::Float;
use crate::image::polygon::{Point, clip_quad_against_unit_cell, signed_area};
#[derive(Debug, Clone, PartialEq)]
pub struct ReprojectExactOutput {
pub image: Array2<f64>,
pub footprint: Array2<f64>,
pub weight: Array2<f64>,
pub error: Option<Array2<f64>>,
}
#[derive(Debug, Error, PartialEq)]
pub enum ReprojectError {
#[error(
"pixel_corners must have shape (H_out + 1, W_out + 1, 2) with last dim == 2 and both grid dims >= 2; got ({rows}, {cols}, {last})"
)]
PixelCornersShape {
rows: usize,
cols: usize,
last: usize,
},
#[error(
"error must have the same shape as image ({image_rows}, {image_cols}); got ({error_rows}, {error_cols})"
)]
ErrorShapeMismatch {
image_rows: usize,
image_cols: usize,
error_rows: usize,
error_cols: usize,
},
}
pub fn reproject_exact<T: Float>(
image: ArrayView2<T>,
pixel_corners: ArrayView3<f64>,
error: Option<ArrayView2<T>>,
) -> Result<ReprojectExactOutput, ReprojectError> {
let corner_shape = pixel_corners.shape();
let rows = corner_shape[0];
let cols = corner_shape[1];
let last = corner_shape[2];
if last != 2 || rows < 2 || cols < 2 {
return Err(ReprojectError::PixelCornersShape { rows, cols, last });
}
if let Some(error_view) = error.as_ref() {
let image_shape = image.shape();
let error_shape = error_view.shape();
if image_shape != error_shape {
return Err(ReprojectError::ErrorShapeMismatch {
image_rows: image_shape[0],
image_cols: image_shape[1],
error_rows: error_shape[0],
error_cols: error_shape[1],
});
}
}
let image_f64: Array2<f64> = image.mapv(|value| value.to_f64().unwrap_or(f64::NAN));
let error_f64: Option<Array2<f64>> =
error.map(|view| view.mapv(|value| value.to_f64().unwrap_or(f64::NAN)));
let height_out = rows - 1;
let width_out = cols - 1;
let mut image_out = Array2::<f64>::from_elem((height_out, width_out), f64::NAN);
let mut footprint = Array2::<f64>::zeros((height_out, width_out));
let mut weight = Array2::<f64>::zeros((height_out, width_out));
let error_out = match error_f64.as_ref() {
Some(error_f64_view) => {
let mut error_out = Array2::<f64>::from_elem((height_out, width_out), f64::NAN);
image_out
.axis_iter_mut(Axis(0))
.into_par_iter()
.zip(footprint.axis_iter_mut(Axis(0)).into_par_iter())
.zip(weight.axis_iter_mut(Axis(0)).into_par_iter())
.zip(error_out.axis_iter_mut(Axis(0)).into_par_iter())
.enumerate()
.for_each(
|(row_index, (((row_image, row_footprint), row_weight), row_error))| {
process_output_row(
row_index,
row_image,
row_footprint,
row_weight,
Some(row_error),
image_f64.view(),
Some(error_f64_view.view()),
pixel_corners,
);
},
);
Some(error_out)
}
None => {
image_out
.axis_iter_mut(Axis(0))
.into_par_iter()
.zip(footprint.axis_iter_mut(Axis(0)).into_par_iter())
.zip(weight.axis_iter_mut(Axis(0)).into_par_iter())
.enumerate()
.for_each(|(row_index, ((row_image, row_footprint), row_weight))| {
process_output_row(
row_index,
row_image,
row_footprint,
row_weight,
None,
image_f64.view(),
None,
pixel_corners,
);
});
None
}
};
Ok(ReprojectExactOutput {
image: image_out,
footprint,
weight,
error: error_out,
})
}
#[cfg(test)]
fn reproject_exact_sequential<T: Float>(
image: ArrayView2<T>,
pixel_corners: ArrayView3<f64>,
error: Option<ArrayView2<T>>,
) -> Result<ReprojectExactOutput, ReprojectError> {
let corner_shape = pixel_corners.shape();
let rows = corner_shape[0];
let cols = corner_shape[1];
let last = corner_shape[2];
if last != 2 || rows < 2 || cols < 2 {
return Err(ReprojectError::PixelCornersShape { rows, cols, last });
}
if let Some(error_view) = error.as_ref() {
let image_shape = image.shape();
let error_shape = error_view.shape();
if image_shape != error_shape {
return Err(ReprojectError::ErrorShapeMismatch {
image_rows: image_shape[0],
image_cols: image_shape[1],
error_rows: error_shape[0],
error_cols: error_shape[1],
});
}
}
let image_f64: Array2<f64> = image.mapv(|value| value.to_f64().unwrap_or(f64::NAN));
let error_f64: Option<Array2<f64>> =
error.map(|view| view.mapv(|value| value.to_f64().unwrap_or(f64::NAN)));
let height_out = rows - 1;
let width_out = cols - 1;
let mut image_out = Array2::<f64>::from_elem((height_out, width_out), f64::NAN);
let mut footprint = Array2::<f64>::zeros((height_out, width_out));
let mut weight = Array2::<f64>::zeros((height_out, width_out));
let mut error_out = error_f64
.as_ref()
.map(|_| Array2::<f64>::from_elem((height_out, width_out), f64::NAN));
let error_f64_view = error_f64.as_ref().map(|array| array.view());
for row_index in 0..height_out {
let row_error = error_out
.as_mut()
.map(|array| array.index_axis_mut(Axis(0), row_index));
process_output_row(
row_index,
image_out.index_axis_mut(Axis(0), row_index),
footprint.index_axis_mut(Axis(0), row_index),
weight.index_axis_mut(Axis(0), row_index),
row_error,
image_f64.view(),
error_f64_view,
pixel_corners,
);
}
Ok(ReprojectExactOutput {
image: image_out,
footprint,
weight,
error: error_out,
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn process_output_row(
row_index: usize,
mut row_image: ArrayViewMut1<f64>,
mut row_footprint: ArrayViewMut1<f64>,
mut row_weight: ArrayViewMut1<f64>,
mut row_error: Option<ArrayViewMut1<f64>>,
image_f64: ArrayView2<f64>,
error_f64: Option<ArrayView2<f64>>,
pixel_corners: ArrayView3<f64>,
) {
let height_in = image_f64.shape()[0];
let width_in = image_f64.shape()[1];
let width_out = row_image.len();
for column_index in 0..width_out {
let corner_top_left = corner_at(pixel_corners, row_index, column_index);
let corner_top_right = corner_at(pixel_corners, row_index, column_index + 1);
let corner_bottom_right = corner_at(pixel_corners, row_index + 1, column_index + 1);
let corner_bottom_left = corner_at(pixel_corners, row_index + 1, column_index);
let any_nan_corner = corner_top_left[0].is_nan()
|| corner_top_left[1].is_nan()
|| corner_top_right[0].is_nan()
|| corner_top_right[1].is_nan()
|| corner_bottom_right[0].is_nan()
|| corner_bottom_right[1].is_nan()
|| corner_bottom_left[0].is_nan()
|| corner_bottom_left[1].is_nan();
if any_nan_corner {
row_image[column_index] = f64::NAN;
row_footprint[column_index] = 0.0;
row_weight[column_index] = 0.0;
continue;
}
let quad: [Point; 4] = [
[corner_top_left[0] + 0.5, corner_top_left[1] + 0.5],
[corner_top_right[0] + 0.5, corner_top_right[1] + 0.5],
[corner_bottom_right[0] + 0.5, corner_bottom_right[1] + 0.5],
[corner_bottom_left[0] + 0.5, corner_bottom_left[1] + 0.5],
];
let output_pixel_area = signed_area(&quad).abs();
let mut x_min_f = quad[0][0];
let mut x_max_f = quad[0][0];
let mut y_min_f = quad[0][1];
let mut y_max_f = quad[0][1];
for vertex in quad.iter().skip(1) {
if vertex[0] < x_min_f {
x_min_f = vertex[0];
}
if vertex[0] > x_max_f {
x_max_f = vertex[0];
}
if vertex[1] < y_min_f {
y_min_f = vertex[1];
}
if vertex[1] > y_max_f {
y_max_f = vertex[1];
}
}
if x_max_f <= 0.0
|| y_max_f <= 0.0
|| x_min_f >= width_in as f64
|| y_min_f >= height_in as f64
{
row_image[column_index] = f64::NAN;
row_footprint[column_index] = 0.0;
row_weight[column_index] = 0.0;
continue;
}
let column_lo = x_min_f.floor().max(0.0) as i64;
let column_hi = (x_max_f.ceil() as i64).min(width_in as i64);
let row_lo = y_min_f.floor().max(0.0) as i64;
let row_hi = (y_max_f.ceil() as i64).min(height_in as i64);
let mut numerator = 0.0_f64;
let mut denominator_valid = 0.0_f64;
let mut denominator_geom = 0.0_f64;
let mut variance_numerator = 0.0_f64;
let mut error_poisoned = false;
for cell_row in row_lo..row_hi {
for cell_column in column_lo..column_hi {
let clipped =
clip_quad_against_unit_cell(&quad, (cell_column as i32, cell_row as i32));
if clipped.is_empty() {
continue;
}
let overlap_area = signed_area(&clipped).abs();
if overlap_area == 0.0 {
continue;
}
denominator_geom += overlap_area;
let pixel_value = image_f64[(cell_row as usize, cell_column as usize)];
if pixel_value.is_nan() {
continue;
}
numerator += overlap_area * pixel_value;
denominator_valid += overlap_area;
if let Some(error) = error_f64.as_ref() {
let sigma = error[(cell_row as usize, cell_column as usize)];
if sigma.is_finite() && sigma >= 0.0 {
variance_numerator += overlap_area * overlap_area * sigma * sigma;
} else {
error_poisoned = true;
}
}
}
}
if denominator_valid > 0.0 {
row_image[column_index] = numerator / denominator_valid;
} else {
row_image[column_index] = f64::NAN;
}
if output_pixel_area > 0.0 {
row_footprint[column_index] = (denominator_geom / output_pixel_area).clamp(0.0, 1.0);
row_weight[column_index] = (denominator_valid / output_pixel_area).clamp(0.0, 1.0);
} else {
row_footprint[column_index] = 0.0;
row_weight[column_index] = 0.0;
}
if let Some(row_error) = row_error.as_mut() {
row_error[column_index] = if denominator_valid > 0.0 && !error_poisoned {
variance_numerator.sqrt() / denominator_valid
} else {
f64::NAN
};
}
}
}
#[inline]
fn corner_at(pixel_corners: ArrayView3<f64>, row: usize, column: usize) -> [f64; 2] {
[
pixel_corners[(row, column, 0)],
pixel_corners[(row, column, 1)],
]
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::{Array2, Array3};
const TOL: f64 = 1e-12;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol * a.abs().max(b.abs()).max(1.0)
}
fn identity_corners(height_out: usize, width_out: usize) -> Array3<f64> {
let mut corners = Array3::<f64>::zeros((height_out + 1, width_out + 1, 2));
for i_node in 0..=height_out {
for j_node in 0..=width_out {
corners[(i_node, j_node, 0)] = j_node as f64 - 0.5;
corners[(i_node, j_node, 1)] = i_node as f64 - 0.5;
}
}
corners
}
#[test]
fn identity_reprojection_preserves_image_footprint_and_weight_f64() {
let image: Array2<f64> =
ndarray::array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0],];
let corners = identity_corners(3, 3);
let output = reproject_exact(image.view(), corners.view(), None).unwrap();
for i in 0..3 {
for j in 0..3 {
assert!(approx_eq(output.image[(i, j)], image[(i, j)], TOL));
assert!(approx_eq(output.footprint[(i, j)], 1.0, TOL));
assert!(approx_eq(output.weight[(i, j)], 1.0, TOL));
}
}
}
#[test]
fn identity_reprojection_preserves_image_footprint_and_weight_f32() {
let image: Array2<f32> =
ndarray::array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0],];
let corners = identity_corners(3, 3);
let output = reproject_exact(image.view(), corners.view(), None).unwrap();
for i in 0..3 {
for j in 0..3 {
assert!(approx_eq(output.image[(i, j)], image[(i, j)] as f64, TOL));
assert!(approx_eq(output.footprint[(i, j)], 1.0, TOL));
assert!(approx_eq(output.weight[(i, j)], 1.0, TOL));
}
}
}
#[test]
fn constant_image_yields_constant_output_where_weight_positive() {
let constant_value = 4.2_f64;
let image: Array2<f64> = Array2::from_elem((5, 5), constant_value);
let mut corners = identity_corners(5, 5);
for i_node in 0..corners.shape()[0] {
for j_node in 0..corners.shape()[1] {
corners[(i_node, j_node, 0)] += 0.3;
corners[(i_node, j_node, 1)] -= 0.2;
}
}
let output = reproject_exact(image.view(), corners.view(), None).unwrap();
for i in 0..5 {
for j in 0..5 {
assert!(
approx_eq(output.footprint[(i, j)], output.weight[(i, j)], TOL),
"i={i} j={j} footprint {} != weight {}",
output.footprint[(i, j)],
output.weight[(i, j)]
);
if output.weight[(i, j)] > 0.0 {
assert!(
approx_eq(output.image[(i, j)], constant_value, TOL),
"i={i} j={j} got {} weight {}",
output.image[(i, j)],
output.weight[(i, j)]
);
}
}
}
}
#[test]
fn half_pixel_shift_linear_average_of_two_columns() {
let image: Array2<f64> = ndarray::array![[1.0, 2.0, 3.0, 4.0]];
let mut corners = identity_corners(1, 4);
for i_node in 0..corners.shape()[0] {
for j_node in 0..corners.shape()[1] {
corners[(i_node, j_node, 0)] += 0.5;
}
}
let output = reproject_exact(image.view(), corners.view(), None).unwrap();
assert!(approx_eq(output.image[(0, 0)], 1.5, TOL));
assert!(approx_eq(output.image[(0, 1)], 2.5, TOL));
assert!(approx_eq(output.image[(0, 2)], 3.5, TOL));
assert!(approx_eq(output.image[(0, 3)], 4.0, TOL));
assert!(approx_eq(output.footprint[(0, 0)], 1.0, TOL));
assert!(approx_eq(output.footprint[(0, 3)], 0.5, TOL));
assert!(approx_eq(output.weight[(0, 0)], 1.0, TOL));
assert!(approx_eq(output.weight[(0, 3)], 0.5, TOL));
}
#[test]
fn rotation_by_90_degrees_preserves_surface_brightness() {
let constant_value = 7.0_f64;
let image: Array2<f64> = Array2::from_elem((3, 3), constant_value);
let mut corners = Array3::<f64>::zeros((4, 4, 2));
for i_node in 0..4 {
for j_node in 0..4 {
let x_orig = j_node as f64 - 0.5;
let y_orig = i_node as f64 - 0.5;
let x_centered = x_orig - 1.0;
let y_centered = y_orig - 1.0;
let x_rotated = y_centered + 1.0;
let y_rotated = -x_centered + 1.0;
corners[(i_node, j_node, 0)] = x_rotated;
corners[(i_node, j_node, 1)] = y_rotated;
}
}
let output = reproject_exact(image.view(), corners.view(), None).unwrap();
for i in 0..3 {
for j in 0..3 {
assert!(approx_eq(output.image[(i, j)], constant_value, TOL));
assert!(approx_eq(output.footprint[(i, j)], 1.0, TOL));
assert!(approx_eq(output.weight[(i, j)], 1.0, TOL));
}
}
}
#[test]
fn output_pixels_outside_footprint_are_nan_zero() {
let image: Array2<f64> = ndarray::array![[1.0, 2.0], [3.0, 4.0]];
let mut corners = Array3::<f64>::zeros((3, 3, 2));
for i_node in 0..3 {
for j_node in 0..3 {
corners[(i_node, j_node, 0)] = 100.0 + j_node as f64;
corners[(i_node, j_node, 1)] = 100.0 + i_node as f64;
}
}
let output = reproject_exact(image.view(), corners.view(), None).unwrap();
for i in 0..2 {
for j in 0..2 {
assert!(output.image[(i, j)].is_nan());
assert_eq!(output.footprint[(i, j)], 0.0);
assert_eq!(output.weight[(i, j)], 0.0);
}
}
}
#[test]
fn nan_input_separates_footprint_from_weight() {
let image: Array2<f64> = ndarray::array![[1.0, f64::NAN, 3.0]];
let mut corners = identity_corners(1, 3);
for i_node in 0..corners.shape()[0] {
for j_node in 0..corners.shape()[1] {
corners[(i_node, j_node, 0)] += 0.5;
}
}
let output = reproject_exact(image.view(), corners.view(), None).unwrap();
assert!(approx_eq(output.image[(0, 0)], 1.0, TOL));
assert!(approx_eq(output.footprint[(0, 0)], 1.0, TOL));
assert!(approx_eq(output.weight[(0, 0)], 0.5, TOL));
assert!(approx_eq(output.image[(0, 1)], 3.0, TOL));
assert!(approx_eq(output.footprint[(0, 1)], 1.0, TOL));
assert!(approx_eq(output.weight[(0, 1)], 0.5, TOL));
assert!(approx_eq(output.image[(0, 2)], 3.0, TOL));
assert!(approx_eq(output.footprint[(0, 2)], 0.5, TOL));
assert!(approx_eq(output.weight[(0, 2)], 0.5, TOL));
}
#[test]
fn nan_in_corners_marks_output_nan_zero() {
let image: Array2<f64> = ndarray::array![[1.0, 2.0], [3.0, 4.0]];
let mut corners = identity_corners(2, 2);
corners[(1, 1, 0)] = f64::NAN;
let output = reproject_exact(image.view(), corners.view(), None).unwrap();
for i in 0..2 {
for j in 0..2 {
assert!(output.image[(i, j)].is_nan());
assert_eq!(output.footprint[(i, j)], 0.0);
assert_eq!(output.weight[(i, j)], 0.0);
}
}
}
#[test]
fn shape_error_last_dim_not_two() {
let image: Array2<f64> = Array2::zeros((2, 2));
let corners = Array3::<f64>::zeros((3, 3, 3));
let err = reproject_exact(image.view(), corners.view(), None).unwrap_err();
assert_eq!(
err,
ReprojectError::PixelCornersShape {
rows: 3,
cols: 3,
last: 3
}
);
}
#[test]
fn parallel_matches_sequential_bitwise_on_moderate_input() {
let height = 32usize;
let width = 40usize;
let mut image: Array2<f64> = Array2::zeros((height, width));
for i in 0..height {
for j in 0..width {
image[(i, j)] = ((i * 13 + j * 7) % 97) as f64 / 11.0 + (i as f64).sin();
}
}
let mut corners = Array3::<f64>::zeros((height + 1, width + 1, 2));
for i_node in 0..=height {
for j_node in 0..=width {
corners[(i_node, j_node, 0)] = j_node as f64 - 0.5 + 0.37;
corners[(i_node, j_node, 1)] = i_node as f64 - 0.5 - 0.21;
}
}
let parallel = reproject_exact(image.view(), corners.view(), None).unwrap();
let sequential = reproject_exact_sequential(image.view(), corners.view(), None).unwrap();
assert_eq!(parallel.image.shape(), sequential.image.shape());
for i in 0..height {
for j in 0..width {
let p_image = parallel.image[(i, j)];
let s_image = sequential.image[(i, j)];
if p_image.is_nan() {
assert!(s_image.is_nan());
} else {
assert_eq!(p_image.to_bits(), s_image.to_bits());
}
assert_eq!(
parallel.footprint[(i, j)].to_bits(),
sequential.footprint[(i, j)].to_bits()
);
assert_eq!(
parallel.weight[(i, j)].to_bits(),
sequential.weight[(i, j)].to_bits()
);
}
}
}
#[test]
fn shape_error_front_dims_too_small() {
let image: Array2<f64> = Array2::zeros((2, 2));
let corners = Array3::<f64>::zeros((1, 3, 2));
let err = reproject_exact(image.view(), corners.view(), None).unwrap_err();
assert_eq!(
err,
ReprojectError::PixelCornersShape {
rows: 1,
cols: 3,
last: 2
}
);
}
#[test]
fn error_is_none_when_not_requested() {
let image: Array2<f64> = ndarray::array![[1.0, 2.0], [3.0, 4.0]];
let corners = identity_corners(2, 2);
let output = reproject_exact(image.view(), corners.view(), None).unwrap();
assert!(output.error.is_none());
}
#[test]
fn identity_error_passes_through() {
let image: Array2<f64> = ndarray::array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
let error: Array2<f64> = ndarray::array![[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]];
let corners = identity_corners(3, 3);
let output = reproject_exact(image.view(), corners.view(), Some(error.view())).unwrap();
let output_error = output.error.expect("error requested");
for i in 0..3 {
for j in 0..3 {
assert!(approx_eq(output_error[(i, j)], error[(i, j)], TOL));
}
}
}
#[test]
fn half_pixel_shift_averages_error_in_quadrature() {
let image: Array2<f64> = ndarray::array![[1.0, 2.0, 3.0, 4.0]];
let sigma = 0.3_f64;
let error: Array2<f64> = Array2::from_elem((1, 4), sigma);
let mut corners = identity_corners(1, 4);
for i_node in 0..corners.shape()[0] {
for j_node in 0..corners.shape()[1] {
corners[(i_node, j_node, 0)] += 0.5;
}
}
let output = reproject_exact(image.view(), corners.view(), Some(error.view())).unwrap();
let output_error = output.error.expect("error requested");
let expected = sigma / 2.0_f64.sqrt();
assert!(approx_eq(output_error[(0, 0)], expected, TOL));
assert!(approx_eq(output_error[(0, 1)], expected, TOL));
assert!(approx_eq(output_error[(0, 2)], expected, TOL));
assert!(approx_eq(output_error[(0, 3)], sigma, TOL));
}
#[test]
fn nonfinite_sigma_poisons_error_but_not_image() {
let image: Array2<f64> = ndarray::array![[1.0, 2.0, 3.0, 4.0]];
let mut error: Array2<f64> = Array2::from_elem((1, 4), 0.3_f64);
error[(0, 1)] = f64::NAN;
let mut corners = identity_corners(1, 4);
for i_node in 0..corners.shape()[0] {
for j_node in 0..corners.shape()[1] {
corners[(i_node, j_node, 0)] += 0.5;
}
}
let without_error = reproject_exact(image.view(), corners.view(), None).unwrap();
let with_error = reproject_exact(image.view(), corners.view(), Some(error.view())).unwrap();
for j in 0..4 {
assert_eq!(
without_error.image[(0, j)].to_bits(),
with_error.image[(0, j)].to_bits()
);
}
let output_error = with_error.error.expect("error requested");
assert!(output_error[(0, 0)].is_nan());
assert!(output_error[(0, 1)].is_nan());
assert!(output_error[(0, 2)].is_finite());
assert!(output_error[(0, 3)].is_finite());
}
#[test]
fn negative_sigma_poisons_error() {
let image: Array2<f64> = ndarray::array![[1.0, 2.0]];
let error: Array2<f64> = ndarray::array![[-1.0, 0.2]];
let corners = identity_corners(1, 2);
let output = reproject_exact(image.view(), corners.view(), Some(error.view())).unwrap();
let output_error = output.error.expect("error requested");
assert!(output_error[(0, 0)].is_nan());
assert!(approx_eq(output_error[(0, 1)], 0.2, TOL));
}
#[test]
fn value_nan_input_never_consults_its_sigma() {
let image: Array2<f64> = ndarray::array![[1.0, f64::NAN, 3.0]];
let error: Array2<f64> = ndarray::array![[2.0, f64::NAN, 4.0]];
let mut corners = identity_corners(1, 3);
for i_node in 0..corners.shape()[0] {
for j_node in 0..corners.shape()[1] {
corners[(i_node, j_node, 0)] += 0.5;
}
}
let output = reproject_exact(image.view(), corners.view(), Some(error.view())).unwrap();
let output_error = output.error.expect("error requested");
assert!(approx_eq(output_error[(0, 0)], 2.0, TOL));
assert!(approx_eq(output_error[(0, 1)], 4.0, TOL));
}
#[test]
fn nan_corner_and_out_of_bounds_give_nan_error() {
let image: Array2<f64> = ndarray::array![[1.0, 2.0], [3.0, 4.0]];
let error: Array2<f64> = Array2::from_elem((2, 2), 0.5_f64);
let mut corners = identity_corners(2, 2);
corners[(1, 1, 0)] = f64::NAN;
let output = reproject_exact(image.view(), corners.view(), Some(error.view())).unwrap();
let output_error = output.error.expect("error requested");
for i in 0..2 {
for j in 0..2 {
assert!(output_error[(i, j)].is_nan());
}
}
let mut far_corners = Array3::<f64>::zeros((3, 3, 2));
for i_node in 0..3 {
for j_node in 0..3 {
far_corners[(i_node, j_node, 0)] = 100.0 + j_node as f64;
far_corners[(i_node, j_node, 1)] = 100.0 + i_node as f64;
}
}
let far_error: Array2<f64> = Array2::from_elem((2, 2), 0.5_f64);
let far =
reproject_exact(image.view(), far_corners.view(), Some(far_error.view())).unwrap();
let far_output_error = far.error.expect("error requested");
for i in 0..2 {
for j in 0..2 {
assert!(far_output_error[(i, j)].is_nan());
}
}
}
#[test]
fn error_shape_mismatch_is_rejected() {
let image: Array2<f64> = Array2::zeros((3, 4));
let error: Array2<f64> = Array2::zeros((3, 5));
let corners = identity_corners(3, 4);
let err = reproject_exact(image.view(), corners.view(), Some(error.view())).unwrap_err();
assert_eq!(
err,
ReprojectError::ErrorShapeMismatch {
image_rows: 3,
image_cols: 4,
error_rows: 3,
error_cols: 5,
}
);
}
#[test]
fn error_f32_and_f64_inputs_agree() {
let image_f64: Array2<f64> = ndarray::array![[1.0, 2.0], [3.0, 4.0]];
let error_f64: Array2<f64> = ndarray::array![[0.1, 0.2], [0.3, 0.4]];
let image_f32: Array2<f32> = image_f64.mapv(|value| value as f32);
let error_f32: Array2<f32> = error_f64.mapv(|value| value as f32);
let corners = identity_corners(2, 2);
let output_f64 =
reproject_exact(image_f64.view(), corners.view(), Some(error_f64.view())).unwrap();
let output_f32 =
reproject_exact(image_f32.view(), corners.view(), Some(error_f32.view())).unwrap();
let error_out_f64 = output_f64.error.expect("error requested");
let error_out_f32 = output_f32.error.expect("error requested");
for i in 0..2 {
for j in 0..2 {
assert!(approx_eq(
error_out_f64[(i, j)],
error_out_f32[(i, j)],
1e-6
));
}
}
}
#[test]
fn parallel_matches_sequential_bitwise_with_error() {
let height = 32usize;
let width = 40usize;
let mut image: Array2<f64> = Array2::zeros((height, width));
let mut error: Array2<f64> = Array2::zeros((height, width));
for i in 0..height {
for j in 0..width {
image[(i, j)] = ((i * 13 + j * 7) % 97) as f64 / 11.0 + (i as f64).sin();
error[(i, j)] = ((i * 5 + j * 3) % 13) as f64 / 7.0 + 0.05;
}
}
let mut corners = Array3::<f64>::zeros((height + 1, width + 1, 2));
for i_node in 0..=height {
for j_node in 0..=width {
corners[(i_node, j_node, 0)] = j_node as f64 - 0.5 + 0.37;
corners[(i_node, j_node, 1)] = i_node as f64 - 0.5 - 0.21;
}
}
let parallel = reproject_exact(image.view(), corners.view(), Some(error.view())).unwrap();
let sequential =
reproject_exact_sequential(image.view(), corners.view(), Some(error.view())).unwrap();
let parallel_error = parallel.error.expect("error requested");
let sequential_error = sequential.error.expect("error requested");
for i in 0..height {
for j in 0..width {
let p = parallel_error[(i, j)];
let s = sequential_error[(i, j)];
if p.is_nan() {
assert!(s.is_nan());
} else {
assert_eq!(p.to_bits(), s.to_bits());
}
}
}
}
}