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>,
}
#[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,
},
}
pub fn reproject_exact<T: Float>(
image_in: ArrayView2<T>,
pixel_corners: ArrayView3<f64>,
) -> 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 });
}
let image_in_f64: Array2<f64> = image_in.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));
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,
image_in_f64.view(),
pixel_corners,
);
});
Ok(ReprojectExactOutput {
image: image_out,
footprint,
weight,
})
}
#[cfg(test)]
fn reproject_exact_sequential<T: Float>(
image_in: ArrayView2<T>,
pixel_corners: ArrayView3<f64>,
) -> 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 });
}
let image_in_f64: Array2<f64> = image_in.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));
for (row_index, ((row_image, row_footprint), row_weight)) in image_out
.outer_iter_mut()
.zip(footprint.outer_iter_mut())
.zip(weight.outer_iter_mut())
.enumerate()
{
process_output_row(
row_index,
row_image,
row_footprint,
row_weight,
image_in_f64.view(),
pixel_corners,
);
}
Ok(ReprojectExactOutput {
image: image_out,
footprint,
weight,
})
}
pub(crate) fn process_output_row(
row_index: usize,
mut row_image: ArrayViewMut1<f64>,
mut row_footprint: ArrayViewMut1<f64>,
mut row_weight: ArrayViewMut1<f64>,
image_in_f64: ArrayView2<f64>,
pixel_corners: ArrayView3<f64>,
) {
let height_in = image_in_f64.shape()[0];
let width_in = image_in_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;
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_in_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 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;
}
}
}
#[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()).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()).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()).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()).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()).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()).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()).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()).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()).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()).unwrap();
let sequential = reproject_exact_sequential(image.view(), corners.view()).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()).unwrap_err();
assert_eq!(
err,
ReprojectError::PixelCornersShape {
rows: 1,
cols: 3,
last: 2
}
);
}
}