use ndarray::Array2;
use serde_json::{json, Value};
use super::linalg::{allclose, det, eye, inv};
use crate::json::format_g;
use crate::{Error, Result};
pub const ORTHONORMAL_TOL: f64 = 1e-4;
pub fn build_affine(spacing: &[f64], origin: &[f64], direction: &Array2<f64>) -> Result<Array2<f64>> {
let s = spacing.len();
if origin.len() != s || direction.dim() != (s, s) {
return Err(Error::coded(
"E109",
format!(
"spacing ({s},), origin ({},) and direction {:?} must agree on the number of spatial axes",
origin.len(),
direction.dim()
),
));
}
let mut affine = eye(s + 1);
for r in 0..s {
for c in 0..s {
affine[[r, c]] = direction[[r, c]] * spacing[c];
}
affine[[r, s]] = origin[r];
}
Ok(affine)
}
pub fn spatial_axes(affine: &Array2<f64>) -> Result<usize> {
let (rows, cols) = affine.dim();
if rows != cols || rows < 2 {
return Err(Error::Value(format!(
"an index-to-world affine is (S+1, S+1) with at least one spatial axis; got shape ({rows}, {cols})"
)));
}
Ok(rows - 1)
}
fn check_points(s: usize, flat: &[f64]) -> Result<()> {
if flat.len() % s != 0 {
return Err(Error::Value(format!(
"points must have {s} coordinates each, one per spatial axis of the affine; got {} values",
flat.len()
)));
}
Ok(())
}
pub fn decompose_affine(affine: &Array2<f64>) -> Result<(Vec<f64>, Vec<f64>, Array2<f64>)> {
let s = spatial_axes(affine)?;
let mut spacing = vec![0.0; s];
for (c, sp) in spacing.iter_mut().enumerate() {
*sp = (0..s).map(|r| affine[[r, c]].powi(2)).sum::<f64>().sqrt();
}
if spacing.iter().any(|v| *v <= 0.0) {
return Err(Error::coded("E104", "affine has a degenerate spatial axis"));
}
let mut direction = Array2::zeros((s, s));
for r in 0..s {
for c in 0..s {
direction[[r, c]] = affine[[r, c]] / spacing[c];
}
}
let origin = (0..s).map(|r| affine[[r, s]]).collect();
Ok((spacing, origin, direction))
}
fn residual(direction: &Array2<f64>) -> Array2<f64> {
let n = direction.nrows();
direction.t().dot(direction) - eye(n)
}
pub fn is_orthonormal(direction: &Array2<f64>, tol: f64) -> bool {
if direction.nrows() != direction.ncols() {
return false;
}
let n = direction.nrows();
let gram = direction.t().dot(direction);
allclose(gram.as_standard_layout().as_slice().unwrap(), eye(n).as_slice().unwrap(), tol, 0.0)
}
pub fn check_orthonormal(direction: &Array2<f64>, tol: f64, what: &str) -> Result<()> {
if !is_orthonormal(direction, tol) {
let r = if direction.nrows() == direction.ncols() {
residual(direction).iter().fold(0.0f64, |m, v| m.max(v.abs()))
} else {
f64::NAN
};
return Err(Error::coded(
"E102",
format!("{what} is not orthonormal to {} (max residual {})", format_g(tol, 6), format_g(r, 3)),
));
}
Ok(())
}
pub fn is_proper_rotation(matrix: &Array2<f64>, tol: f64) -> bool {
if !is_orthonormal(matrix, tol) {
return false;
}
(det(matrix) - 1.0).abs() <= tol.max(1e-6)
}
pub fn index_to_world(affine: &Array2<f64>, indices: &[f64]) -> Result<Vec<f64>> {
let s = spatial_axes(affine)?;
check_points(s, indices)?;
let mut out = Vec::with_capacity(indices.len());
for p in indices.chunks(s) {
for r in 0..s {
let mut v = 0.0;
for c in 0..s {
v += affine[[r, c]] * p[c];
}
out.push(v + affine[[r, s]]);
}
}
Ok(out)
}
pub fn world_to_index(affine: &Array2<f64>, points: &[f64]) -> Result<Vec<f64>> {
let s = spatial_axes(affine)?;
check_points(s, points)?;
let linear = affine.slice(ndarray::s![..s, ..s]).to_owned();
let inverse = inv(&linear)?;
let mut out = Vec::with_capacity(points.len());
for p in points.chunks(s) {
for r in 0..s {
let mut v = 0.0;
for c in 0..s {
v += inverse[[r, c]] * (p[c] - affine[[c, s]]);
}
out.push(v);
}
}
Ok(out)
}
pub type SliceBounds = (i64, i64);
pub fn box_to_slices(bbox: &[f64], shape: Option<&[usize]>) -> Result<Vec<SliceBounds>> {
if bbox.len() % 2 != 0 {
return Err(Error::invalid(format!("box must have shape (S, 2), got ({},)", bbox.len())));
}
let s = bbox.len() / 2;
if let Some(extent) = shape {
if extent.len() != s {
return Err(Error::Value(format!(
"a box with {s} axes is clipped to a shape of {s} extents; got {}",
crate::json::repr_int_tuple(extent)
)));
}
}
if (0..s).any(|k| !(bbox[2 * k].is_finite() && bbox[2 * k + 1].is_finite() && bbox[2 * k] <= bbox[2 * k + 1])) {
return Err(Error::coded("E406", "box has lo > hi, or a bound that is not finite, on at least one axis"));
}
let mut out = Vec::with_capacity(s);
for k in 0..s {
let mut start = (bbox[2 * k] + 0.5).floor() as i64;
let mut stop = (bbox[2 * k + 1] + 0.5).floor() as i64;
if let Some(extent) = shape {
let n = extent[k] as i64;
start = start.clamp(0, n);
stop = stop.clamp(start, n);
}
out.push((start, stop));
}
Ok(out)
}
pub fn slices_to_box(slices: &[(i64, i64)]) -> Vec<f32> {
let mut out = Vec::with_capacity(slices.len() * 2);
for (start, stop) in slices {
out.push(*start as f32 - 0.5);
out.push(*stop as f32 - 0.5);
}
out
}
pub const MAX_CORNER_AXES: usize = 16;
pub fn box_corners(bbox: &[f64]) -> Result<Vec<Vec<f64>>> {
if bbox.len() % 2 != 0 {
return Err(Error::invalid(format!("box must have shape (S, 2), got ({},)", bbox.len())));
}
let s = bbox.len() / 2;
if s > MAX_CORNER_AXES {
return Err(Error::Value(format!(
"a box with {s} axes has 2**{s} corners; at most {MAX_CORNER_AXES} axes are enumerated"
)));
}
let mut out = Vec::with_capacity(1 << s);
for n in 0..(1usize << s) {
let mut corner = Vec::with_capacity(s);
for k in 0..s {
let bit = (n >> (s - 1 - k)) & 1;
corner.push(bbox[2 * k + bit]);
}
out.push(corner);
}
Ok(out)
}
pub fn apply_affine_to_box(affine: &Array2<f64>, bbox: &[f64]) -> Result<Vec<f64>> {
let s = spatial_axes(affine)?;
if bbox.len() != 2 * s {
return Err(Error::Value(format!(
"a box under an affine with {s} spatial axes has shape ({s}, 2); got {} values",
bbox.len()
)));
}
let corners: Vec<f64> = box_corners(bbox)?.into_iter().flatten().collect();
let world = index_to_world(affine, &corners)?;
let mut out = Vec::with_capacity(2 * s);
for k in 0..s {
let values = world.iter().skip(k).step_by(s);
let lo = values.clone().fold(f64::INFINITY, |a, b| a.min(*b));
let hi = values.fold(f64::NEG_INFINITY, |a, b| a.max(*b));
out.push(lo);
out.push(hi);
}
Ok(out)
}
pub fn voxel_volume(spacing: &[f64]) -> f64 {
spacing.iter().product()
}
pub fn affine_summary(affine: &Array2<f64>) -> Result<Value> {
let (spacing, origin, direction) = decompose_affine(affine)?;
Ok(json!({
"spacing": spacing,
"origin": origin,
"direction": direction.rows().into_iter().map(|r| r.to_vec()).collect::<Vec<_>>(),
"determinant": det(&direction),
"orthonormal": is_orthonormal(&direction, ORTHONORMAL_TOL),
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn affine_roundtrip() {
let dir = eye(3);
let a = build_affine(&[1.5, 0.8, 0.8], &[-12.0, -9.6, -9.6], &dir).unwrap();
let w = index_to_world(&a, &[1.0, 2.0, 3.0]).unwrap();
assert_eq!(w, vec![-10.5, -8.0, -7.199999999999999]);
let back = world_to_index(&a, &w).unwrap();
assert!(allclose(&back, &[1.0, 2.0, 3.0], 1e-12, 0.0));
}
#[test]
fn boxes_round_half_up() {
assert_eq!(box_to_slices(&[11.5, 39.5], None).unwrap(), vec![(12, 40)]);
assert_eq!(box_to_slices(&[1.0, 2.0], None).unwrap(), vec![(1, 2)]);
assert_eq!(box_to_slices(&[2.0, 3.0], None).unwrap(), vec![(2, 3)]);
assert_eq!(slices_to_box(&[(12, 40)]), vec![11.5, 39.5]);
assert_eq!(box_to_slices(&[2.0, 1.0], None).unwrap_err().code(), Some("E406"));
}
#[test]
fn s8_1_a_bound_that_is_not_finite_is_e406() {
let (nan, inf) = (f64::NAN, f64::INFINITY);
for (lo, hi) in [(nan, 4.0), (0.0, nan), (nan, nan), (-inf, 4.0), (0.0, inf), (-inf, inf), (inf, inf)] {
let bbox = [0.0, 1.0, lo, hi];
for shape in [None, Some(&[4, 10][..])] {
assert_eq!(box_to_slices(&bbox, shape).unwrap_err().code(), Some("E406"), "{bbox:?} {shape:?}");
}
}
assert_eq!(box_to_slices(&[0.0, 1.0, -0.5, 9.5], Some(&[4, 10])).unwrap(), vec![(0, 1), (0, 10)]);
}
#[test]
fn corners_in_odometer_order() {
let c = box_corners(&[0.0, 1.0, 10.0, 11.0]).unwrap();
assert_eq!(c, vec![vec![0.0, 10.0], vec![0.0, 11.0], vec![1.0, 10.0], vec![1.0, 11.0]]);
}
#[test]
fn s3_3_misshapen_arguments_are_errors_not_panics() {
let tall = Array2::from_shape_fn((4, 3), |(r, c)| if r == c { 2.0 } else { 0.0 });
let empty = Array2::<f64>::zeros((0, 0));
let one = Array2::<f64>::ones((1, 1));
for bad in [&tall, &empty, &one] {
assert!(matches!(spatial_axes(bad), Err(Error::Value(_))), "{:?}", bad.dim());
assert!(decompose_affine(bad).is_err());
assert!(index_to_world(bad, &[1.0, 2.0, 3.0]).is_err());
assert!(world_to_index(bad, &[1.0, 2.0, 3.0]).is_err());
assert!(apply_affine_to_box(bad, &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0]).is_err());
}
let a = build_affine(&[1.0, 1.0, 1.0], &[0.0, 0.0, 0.0], &eye(3)).unwrap();
assert!(matches!(index_to_world(&a, &[1.0, 2.0]), Err(Error::Value(_))));
assert!(matches!(world_to_index(&a, &[1.0, 2.0, 3.0, 4.0]), Err(Error::Value(_))));
assert!(apply_affine_to_box(&a, &[0.0, 1.0, 0.0, 1.0]).is_err());
assert!(matches!(box_to_slices(&[0.0, 1.0, 0.0, 1.0, 0.0, 1.0], Some(&[4, 4])), Err(Error::Value(_))));
assert!(matches!(box_to_slices(&[0.0, 1.0, 0.0, 1.0], Some(&[4, 4, 4])), Err(Error::Value(_))));
assert!(box_corners(&[0.0; 3]).is_err());
assert!(matches!(box_corners(&[0.0; 2 * 64]), Err(Error::Value(_))));
assert_eq!(index_to_world(&a, &[1.0, 2.0, 3.0]).unwrap(), vec![1.0, 2.0, 3.0]);
assert_eq!(box_to_slices(&[0.0, 1.0, 0.0, 1.0], Some(&[4, 4])).unwrap(), vec![(0, 1), (0, 1)]);
assert_eq!(
apply_affine_to_box(&a, &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0]).unwrap(),
vec![0.0, 1.0, 0.0, 1.0, 0.0, 1.0]
);
assert_eq!(decompose_affine(&a).unwrap().0, vec![1.0, 1.0, 1.0]);
}
}