use scirs2_core::ndarray::{Array2, Array3};
use std::f64::consts::PI;
use crate::error::{NdimageError, NdimageResult};
use crate::registration::{AffineTransform2D, RigidTransform2D};
#[derive(Debug, Clone)]
pub struct DisplacementField {
pub field: Vec<f64>,
pub dims: Vec<usize>,
pub n_components: usize,
}
impl DisplacementField {
pub fn zeros_2d(rows: usize, cols: usize) -> Self {
Self {
field: vec![0.0; rows * cols * 2],
dims: vec![rows, cols],
n_components: 2,
}
}
pub fn zeros_3d(nz: usize, ny: usize, nx: usize) -> Self {
Self {
field: vec![0.0; nz * ny * nx * 3],
dims: vec![nz, ny, nx],
n_components: 3,
}
}
pub fn num_voxels(&self) -> usize {
self.dims.iter().product()
}
pub fn get_2d(&self, r: usize, c: usize) -> NdimageResult<[f64; 2]> {
if self.dims.len() != 2 || self.n_components != 2 {
return Err(NdimageError::InvalidInput(
"DisplacementField::get_2d: field is not 2D".to_string(),
));
}
let cols = self.dims[1];
let base = (r * cols + c) * 2;
if base + 1 >= self.field.len() {
return Err(NdimageError::InvalidInput(
"DisplacementField::get_2d: index out of bounds".to_string(),
));
}
Ok([self.field[base], self.field[base + 1]])
}
pub fn set_2d(&mut self, r: usize, c: usize, dy: f64, dx: f64) -> NdimageResult<()> {
if self.dims.len() != 2 || self.n_components != 2 {
return Err(NdimageError::InvalidInput(
"DisplacementField::set_2d: field is not 2D".to_string(),
));
}
let cols = self.dims[1];
let base = (r * cols + c) * 2;
if base + 1 >= self.field.len() {
return Err(NdimageError::InvalidInput(
"DisplacementField::set_2d: index out of bounds".to_string(),
));
}
self.field[base] = dy;
self.field[base + 1] = dx;
Ok(())
}
pub fn get_3d(&self, iz: usize, iy: usize, ix: usize) -> NdimageResult<[f64; 3]> {
if self.dims.len() != 3 || self.n_components != 3 {
return Err(NdimageError::InvalidInput(
"DisplacementField::get_3d: field is not 3D".to_string(),
));
}
let ny = self.dims[1];
let nx = self.dims[2];
let base = (iz * ny * nx + iy * nx + ix) * 3;
if base + 2 >= self.field.len() {
return Err(NdimageError::InvalidInput(
"DisplacementField::get_3d: index out of bounds".to_string(),
));
}
Ok([
self.field[base],
self.field[base + 1],
self.field[base + 2],
])
}
pub fn compose_2d(&self, other: &DisplacementField) -> NdimageResult<DisplacementField> {
if self.dims.len() != 2
|| other.dims.len() != 2
|| self.dims != other.dims
|| self.n_components != 2
|| other.n_components != 2
{
return Err(NdimageError::DimensionError(
"DisplacementField::compose_2d: incompatible fields".to_string(),
));
}
let rows = self.dims[0];
let cols = self.dims[1];
let mut result = DisplacementField::zeros_2d(rows, cols);
for r in 0..rows {
for c in 0..cols {
let [dy0, dx0] = self.get_2d(r, c)?;
let wr = r as f64 + dy0;
let wc = c as f64 + dx0;
let [dy1, dx1] = bilinear_sample_2d_field(other, wr, wc);
result.set_2d(r, c, dy0 + dy1, dx0 + dx1)?;
}
}
Ok(result)
}
pub fn gaussian_smooth_2d(&mut self, sigma: f64) -> NdimageResult<()> {
if self.dims.len() != 2 || self.n_components != 2 {
return Err(NdimageError::InvalidInput(
"DisplacementField::gaussian_smooth_2d: only 2D fields supported".to_string(),
));
}
let rows = self.dims[0];
let cols = self.dims[1];
for comp in 0..2 {
let mut component: Vec<f64> = (0..rows * cols)
.map(|i| self.field[i * 2 + comp])
.collect();
gaussian_smooth_1d_separable(&mut component, rows, cols, sigma);
for i in 0..rows * cols {
self.field[i * 2 + comp] = component[i];
}
}
Ok(())
}
pub fn rms_magnitude(&self) -> f64 {
let n = self.num_voxels();
if n == 0 {
return 0.0;
}
let sum_sq: f64 = (0..n)
.map(|i| {
let base = i * self.n_components;
(0..self.n_components)
.map(|c| self.field[base + c].powi(2))
.sum::<f64>()
})
.sum();
(sum_sq / n as f64).sqrt()
}
}
pub struct JacobianDeterminant;
impl JacobianDeterminant {
pub fn compute_2d(field: &DisplacementField) -> NdimageResult<Array2<f64>> {
if field.dims.len() != 2 || field.n_components != 2 {
return Err(NdimageError::InvalidInput(
"JacobianDeterminant::compute_2d: field must be 2D".to_string(),
));
}
let rows = field.dims[0];
let cols = field.dims[1];
if rows < 3 || cols < 3 {
return Err(NdimageError::InvalidInput(
"JacobianDeterminant::compute_2d: domain must be at least 3×3".to_string(),
));
}
let mut det = Array2::<f64>::zeros((rows, cols));
for r in 0..rows {
for c in 0..cols {
let rn = r.saturating_sub(1);
let rp = (r + 1).min(rows - 1);
let cn = c.saturating_sub(1);
let cp = (c + 1).min(cols - 1);
let hr = (rp - rn) as f64;
let hc = (cp - cn) as f64;
let [dy_rn, dx_rn] = field.get_2d(rn, c)?;
let [dy_rp, dx_rp] = field.get_2d(rp, c)?;
let [dy_rcn, dx_rcn] = field.get_2d(r, cn)?;
let [dy_rcp, dx_rcp] = field.get_2d(r, cp)?;
let dphi_r_dr = 1.0 + (dy_rp - dy_rn) / hr;
let dphi_r_dc = (dy_rcp - dy_rcn) / hc;
let dphi_c_dr = (dx_rp - dx_rn) / hr;
let dphi_c_dc = 1.0 + (dx_rcp - dx_rcn) / hc;
det[[r, c]] = dphi_r_dr * dphi_c_dc - dphi_r_dc * dphi_c_dr;
}
}
Ok(det)
}
pub fn folding_fraction_2d(field: &DisplacementField) -> NdimageResult<f64> {
let det = Self::compute_2d(field)?;
let total = det.len();
if total == 0 {
return Ok(0.0);
}
let folded = det.iter().filter(|&&v| v <= 0.0).count();
Ok(folded as f64 / total as f64)
}
}
#[derive(Debug, Clone)]
pub struct CompositeTransform {
pub rigid: Option<RigidTransform2D>,
pub affine: Option<AffineTransform2D>,
pub deformable: Option<DisplacementField>,
}
impl CompositeTransform {
pub fn identity() -> Self {
Self {
rigid: None,
affine: None,
deformable: None,
}
}
pub fn apply_to_point(&self, r: f64, c: f64) -> NdimageResult<(f64, f64)> {
let (lr, lc) = if let Some(ref aff) = self.affine {
apply_affine_point(aff, r, c)
} else if let Some(ref rig) = self.rigid {
apply_rigid_point(rig, r, c)
} else {
(r, c)
};
if let Some(ref field) = self.deformable {
if field.dims.len() == 2 && field.n_components == 2 {
let [dy, dx] = bilinear_sample_2d_field(field, lr, lc);
Ok((lr + dy, lc + dx))
} else {
Ok((lr, lc))
}
} else {
Ok((lr, lc))
}
}
pub fn compose(&self, other: &CompositeTransform) -> NdimageResult<CompositeTransform> {
let deformable = match (&self.deformable, &other.deformable) {
(Some(a), Some(b)) => Some(a.compose_2d(b)?),
(Some(a), None) => Some(a.clone()),
(None, Some(b)) => Some(b.clone()),
(None, None) => None,
};
Ok(CompositeTransform {
rigid: self.rigid.clone().or_else(|| other.rigid.clone()),
affine: self.affine.clone().or_else(|| other.affine.clone()),
deformable,
})
}
}
#[derive(Debug, Clone)]
pub struct DemonsConfig {
pub max_iterations: usize,
pub convergence_threshold: f64,
pub fluid_sigma: f64,
pub diffeo_sigma: f64,
pub step_size: f64,
}
impl Default for DemonsConfig {
fn default() -> Self {
Self {
max_iterations: 100,
convergence_threshold: 1e-3,
fluid_sigma: 1.5,
diffeo_sigma: 1.0,
step_size: 1.0,
}
}
}
#[derive(Debug, Clone)]
pub struct DemonsResult {
pub field: DisplacementField,
pub iterations: usize,
pub rms_history: Vec<f64>,
pub converged: bool,
}
pub struct DemonsDiffeo {
config: DemonsConfig,
}
impl DemonsDiffeo {
pub fn new() -> Self {
Self { config: DemonsConfig::default() }
}
pub fn with_config(config: DemonsConfig) -> Self {
Self { config }
}
pub fn register(
&self,
fixed: &Array2<f64>,
moving: &Array2<f64>,
) -> NdimageResult<DemonsResult> {
let fshape = fixed.shape();
let mshape = moving.shape();
if fshape != mshape {
return Err(NdimageError::DimensionError(format!(
"DemonsDiffeo: fixed shape {:?} != moving shape {:?}",
fshape, mshape
)));
}
let rows = fshape[0];
let cols = fshape[1];
if rows < 3 || cols < 3 {
return Err(NdimageError::InvalidInput(
"DemonsDiffeo: images must be at least 3×3".to_string(),
));
}
let mut disp = DisplacementField::zeros_2d(rows, cols);
let mut rms_history = Vec::with_capacity(self.config.max_iterations);
let mut converged = false;
for iter in 0..self.config.max_iterations {
let warped = warp_image_2d(moving, &disp);
let mut update = DisplacementField::zeros_2d(rows, cols);
for r in 0..rows {
for c in 0..cols {
let f_val = fixed[[r, c]];
let m_val = warped[r * cols + c];
let diff = f_val - m_val;
let f_gn = if r > 0 { fixed[[r - 1, c]] } else { fixed[[r, c]] };
let f_gp = if r + 1 < rows { fixed[[r + 1, c]] } else { fixed[[r, c]] };
let f_gcn = if c > 0 { fixed[[r, c - 1]] } else { fixed[[r, c]] };
let f_gcp = if c + 1 < cols { fixed[[r, c + 1]] } else { fixed[[r, c]] };
let gx = (f_gp - f_gn) * 0.5;
let gy = (f_gcp - f_gcn) * 0.5;
let denom = gx * gx + gy * gy + diff * diff + 1e-10;
let uy = self.config.step_size * diff * gx / denom;
let ux = self.config.step_size * diff * gy / denom;
update.set_2d(r, c, uy, ux)?;
}
}
update.gaussian_smooth_2d(self.config.fluid_sigma)?;
let rms = update.rms_magnitude();
rms_history.push(rms);
disp = disp.compose_2d(&update)?;
disp.gaussian_smooth_2d(self.config.diffeo_sigma)?;
if rms < self.config.convergence_threshold {
converged = true;
let final_iter = iter + 1;
return Ok(DemonsResult {
field: disp,
iterations: final_iter,
rms_history,
converged,
});
}
}
Ok(DemonsResult {
field: disp,
iterations: self.config.max_iterations,
rms_history,
converged,
})
}
}
#[derive(Debug, Clone)]
pub struct FluidConfig {
pub max_iterations: usize,
pub viscosity: f64,
pub step_size: f64,
pub convergence_threshold: f64,
}
impl Default for FluidConfig {
fn default() -> Self {
Self {
max_iterations: 100,
viscosity: 2.0,
step_size: 0.5,
convergence_threshold: 1e-4,
}
}
}
#[derive(Debug, Clone)]
pub struct FluidResult {
pub field: DisplacementField,
pub energy_history: Vec<f64>,
pub iterations: usize,
pub converged: bool,
}
pub struct FluidRegistration {
config: FluidConfig,
}
impl FluidRegistration {
pub fn new() -> Self {
Self { config: FluidConfig::default() }
}
pub fn with_config(config: FluidConfig) -> Self {
Self { config }
}
pub fn register(
&self,
fixed: &Array2<f64>,
moving: &Array2<f64>,
) -> NdimageResult<FluidResult> {
let fshape = fixed.shape();
if fshape != moving.shape() {
return Err(NdimageError::DimensionError(format!(
"FluidRegistration: shape mismatch {:?} vs {:?}",
fshape,
moving.shape()
)));
}
let rows = fshape[0];
let cols = fshape[1];
if rows < 3 || cols < 3 {
return Err(NdimageError::InvalidInput(
"FluidRegistration: images must be at least 3×3".to_string(),
));
}
let mut vel = DisplacementField::zeros_2d(rows, cols);
let mut disp = DisplacementField::zeros_2d(rows, cols);
let mut energy_history = Vec::with_capacity(self.config.max_iterations);
let mut prev_energy = f64::INFINITY;
let mut converged = false;
for iter in 0..self.config.max_iterations {
let warped = warp_image_2d(moving, &disp);
let mut ssd = 0.0_f64;
for r in 0..rows {
for c in 0..cols {
let diff = fixed[[r, c]] - warped[r * cols + c];
ssd += diff * diff;
let w_rn = if r > 0 { warped[(r - 1) * cols + c] } else { warped[r * cols + c] };
let w_rp = if r + 1 < rows { warped[(r + 1) * cols + c] } else { warped[r * cols + c] };
let w_cn = if c > 0 { warped[r * cols + c - 1] } else { warped[r * cols + c] };
let w_cp = if c + 1 < cols { warped[r * cols + c + 1] } else { warped[r * cols + c] };
let gy = (w_rp - w_rn) * 0.5;
let gx = (w_cp - w_cn) * 0.5;
let fy = 2.0 * diff * gy;
let fx = 2.0 * diff * gx;
vel.set_2d(r, c, fy, fx)?;
}
}
energy_history.push(ssd);
vel.gaussian_smooth_2d(self.config.viscosity)?;
for i in 0..rows * cols {
disp.field[i * 2] += self.config.step_size * vel.field[i * 2];
disp.field[i * 2 + 1] += self.config.step_size * vel.field[i * 2 + 1];
}
let rel_change = (prev_energy - ssd).abs() / (prev_energy.abs() + 1e-12);
if rel_change < self.config.convergence_threshold {
converged = true;
return Ok(FluidResult {
field: disp,
energy_history,
iterations: iter + 1,
converged,
});
}
prev_energy = ssd;
}
Ok(FluidResult {
field: disp,
energy_history,
iterations: self.config.max_iterations,
converged,
})
}
}
#[derive(Debug, Clone)]
pub struct FfdConfig {
pub grid_size: [usize; 2],
pub max_iterations: usize,
pub step_size: f64,
pub regularisation: f64,
pub convergence_threshold: f64,
}
impl Default for FfdConfig {
fn default() -> Self {
Self {
grid_size: [8, 8],
max_iterations: 100,
step_size: 0.1,
regularisation: 0.01,
convergence_threshold: 1e-4,
}
}
}
#[derive(Debug, Clone)]
pub struct FfdResult {
pub field: DisplacementField,
pub ctrl_dy: Array2<f64>,
pub ctrl_dx: Array2<f64>,
pub energy_history: Vec<f64>,
pub iterations: usize,
pub converged: bool,
}
pub struct FreeFormDeformation {
config: FfdConfig,
}
impl FreeFormDeformation {
pub fn new() -> Self {
Self { config: FfdConfig::default() }
}
pub fn with_config(config: FfdConfig) -> Self {
let mut cfg = config;
cfg.grid_size[0] = cfg.grid_size[0].max(4);
cfg.grid_size[1] = cfg.grid_size[1].max(4);
Self { config: cfg }
}
pub fn register(
&self,
fixed: &Array2<f64>,
moving: &Array2<f64>,
) -> NdimageResult<FfdResult> {
let fshape = fixed.shape();
if fshape != moving.shape() {
return Err(NdimageError::DimensionError(format!(
"FreeFormDeformation: shape mismatch {:?} vs {:?}",
fshape,
moving.shape()
)));
}
let rows = fshape[0];
let cols = fshape[1];
if rows < 4 || cols < 4 {
return Err(NdimageError::InvalidInput(
"FreeFormDeformation: images must be at least 4×4".to_string(),
));
}
let [gr, gc] = self.config.grid_size;
let mut ctrl_dy = Array2::<f64>::zeros((gr, gc));
let mut ctrl_dx = Array2::<f64>::zeros((gr, gc));
let mut energy_history = Vec::with_capacity(self.config.max_iterations);
let mut prev_energy = f64::INFINITY;
let mut converged = false;
for iter in 0..self.config.max_iterations {
let disp = self.ctrl_to_dense(&ctrl_dy, &ctrl_dx, rows, cols);
let warped = warp_image_2d(moving, &disp);
let mut grad_dy = Array2::<f64>::zeros((gr, gc));
let mut grad_dx = Array2::<f64>::zeros((gr, gc));
let mut ssd = 0.0_f64;
for r in 0..rows {
for c in 0..cols {
let diff = fixed[[r, c]] - warped[r * cols + c];
ssd += diff * diff;
let w_rn = if r > 0 { warped[(r - 1) * cols + c] } else { warped[r * cols + c] };
let w_rp = if r + 1 < rows { warped[(r + 1) * cols + c] } else { warped[r * cols + c] };
let w_cn = if c > 0 { warped[r * cols + c - 1] } else { warped[r * cols + c] };
let w_cp = if c + 1 < cols { warped[r * cols + c + 1] } else { warped[r * cols + c] };
let gy = (w_rp - w_rn) * 0.5;
let gx = (w_cp - w_cn) * 0.5;
let t_r = r as f64 / rows as f64 * (gr - 1) as f64;
let t_c = c as f64 / cols as f64 * (gc - 1) as f64;
let pr = (t_r.floor() as isize).clamp(0, gr as isize - 1) as usize;
let pc = (t_c.floor() as isize).clamp(0, gc as isize - 1) as usize;
let fr = t_r - pr as f64;
let fc = t_c - pc as f64;
for dr in 0..2_usize {
for dc in 0..2_usize {
let nrr = (pr + dr).min(gr - 1);
let ncc = (pc + dc).min(gc - 1);
let wr = if dr == 0 { 1.0 - fr } else { fr };
let wc = if dc == 0 { 1.0 - fc } else { fc };
let w = wr * wc;
grad_dy[[nrr, ncc]] -= 2.0 * diff * gy * w;
grad_dx[[nrr, ncc]] -= 2.0 * diff * gx * w;
}
}
}
}
let bend = self.bending_energy(&ctrl_dy, &ctrl_dx, gr, gc);
let total_energy = ssd + self.config.regularisation * bend;
energy_history.push(total_energy);
for r in 1..gr - 1 {
for c in 1..gc - 1 {
let lap_dy = ctrl_dy[[r - 1, c]] - 2.0 * ctrl_dy[[r, c]] + ctrl_dy[[r + 1, c]]
+ ctrl_dy[[r, c - 1]] - 2.0 * ctrl_dy[[r, c]] + ctrl_dy[[r, c + 1]];
let lap_dx = ctrl_dx[[r - 1, c]] - 2.0 * ctrl_dx[[r, c]] + ctrl_dx[[r + 1, c]]
+ ctrl_dx[[r, c - 1]] - 2.0 * ctrl_dx[[r, c]] + ctrl_dx[[r, c + 1]];
grad_dy[[r, c]] -= self.config.regularisation * lap_dy;
grad_dx[[r, c]] -= self.config.regularisation * lap_dx;
}
}
for r in 0..gr {
for c in 0..gc {
ctrl_dy[[r, c]] -= self.config.step_size * grad_dy[[r, c]];
ctrl_dx[[r, c]] -= self.config.step_size * grad_dx[[r, c]];
}
}
let rel_change = (prev_energy - total_energy).abs() / (prev_energy.abs() + 1e-12);
if rel_change < self.config.convergence_threshold {
converged = true;
let final_disp = self.ctrl_to_dense(&ctrl_dy, &ctrl_dx, rows, cols);
return Ok(FfdResult {
field: final_disp,
ctrl_dy,
ctrl_dx,
energy_history,
iterations: iter + 1,
converged,
});
}
prev_energy = total_energy;
}
let final_disp = self.ctrl_to_dense(&ctrl_dy, &ctrl_dx, rows, cols);
Ok(FfdResult {
field: final_disp,
ctrl_dy,
ctrl_dx,
energy_history,
iterations: self.config.max_iterations,
converged,
})
}
fn ctrl_to_dense(
&self,
ctrl_dy: &Array2<f64>,
ctrl_dx: &Array2<f64>,
rows: usize,
cols: usize,
) -> DisplacementField {
let [gr, gc] = self.config.grid_size;
let mut disp = DisplacementField::zeros_2d(rows, cols);
for r in 0..rows {
for c in 0..cols {
let t_r = r as f64 / rows as f64 * (gr - 1) as f64;
let t_c = c as f64 / cols as f64 * (gc - 1) as f64;
let pr = (t_r.floor() as isize).clamp(0, gr as isize - 1) as usize;
let pc = (t_c.floor() as isize).clamp(0, gc as isize - 1) as usize;
let fr = t_r - pr as f64;
let fc = t_c - pc as f64;
let mut dy = 0.0;
let mut dx = 0.0;
for dr in 0..2_usize {
for dc in 0..2_usize {
let nrr = (pr + dr).min(gr - 1);
let ncc = (pc + dc).min(gc - 1);
let wr = if dr == 0 { 1.0 - fr } else { fr };
let wc = if dc == 0 { 1.0 - fc } else { fc };
dy += wr * wc * ctrl_dy[[nrr, ncc]];
dx += wr * wc * ctrl_dx[[nrr, ncc]];
}
}
let base = (r * cols + c) * 2;
disp.field[base] = dy;
disp.field[base + 1] = dx;
}
}
disp
}
fn bending_energy(&self, dy: &Array2<f64>, dx: &Array2<f64>, gr: usize, gc: usize) -> f64 {
let mut energy = 0.0;
for r in 1..gr.saturating_sub(1) {
for c in 1..gc.saturating_sub(1) {
let d2y_rr = dy[[r - 1, c]] - 2.0 * dy[[r, c]] + dy[[r + 1, c]];
let d2y_cc = dy[[r, c - 1]] - 2.0 * dy[[r, c]] + dy[[r, c + 1]];
let d2x_rr = dx[[r - 1, c]] - 2.0 * dx[[r, c]] + dx[[r + 1, c]];
let d2x_cc = dx[[r, c - 1]] - 2.0 * dx[[r, c]] + dx[[r, c + 1]];
energy += d2y_rr * d2y_rr + d2y_cc * d2y_cc + d2x_rr * d2x_rr + d2x_cc * d2x_cc;
}
}
energy
}
}
fn warp_image_2d(image: &Array2<f64>, field: &DisplacementField) -> Vec<f64> {
let rows = field.dims[0];
let cols = field.dims[1];
let mut out = vec![0.0_f64; rows * cols];
for r in 0..rows {
for c in 0..cols {
let base = (r * cols + c) * 2;
let dy = field.field[base];
let dx = field.field[base + 1];
let src_r = r as f64 - dy;
let src_c = c as f64 - dx;
out[r * cols + c] = bilinear_interpolate(image, src_r, src_c);
}
}
out
}
fn bilinear_interpolate(image: &Array2<f64>, r: f64, c: f64) -> f64 {
let rows = image.nrows();
let cols = image.ncols();
let r0 = r.floor() as isize;
let c0 = c.floor() as isize;
let fr = r - r.floor();
let fc = c - c.floor();
let clamp_r = |v: isize| v.clamp(0, rows as isize - 1) as usize;
let clamp_c = |v: isize| v.clamp(0, cols as isize - 1) as usize;
let r0u = clamp_r(r0);
let r1u = clamp_r(r0 + 1);
let c0u = clamp_c(c0);
let c1u = clamp_c(c0 + 1);
let v00 = image[[r0u, c0u]];
let v01 = image[[r0u, c1u]];
let v10 = image[[r1u, c0u]];
let v11 = image[[r1u, c1u]];
v00 * (1.0 - fr) * (1.0 - fc)
+ v01 * (1.0 - fr) * fc
+ v10 * fr * (1.0 - fc)
+ v11 * fr * fc
}
fn bilinear_sample_2d_field(field: &DisplacementField, r: f64, c: f64) -> [f64; 2] {
let rows = field.dims[0];
let cols = field.dims[1];
let r0 = r.floor() as isize;
let c0 = c.floor() as isize;
let fr = r - r.floor();
let fc = c - c.floor();
let clamp_r = |v: isize| v.clamp(0, rows as isize - 1) as usize;
let clamp_c = |v: isize| v.clamp(0, cols as isize - 1) as usize;
let corners = [
(clamp_r(r0), clamp_c(c0)),
(clamp_r(r0), clamp_c(c0 + 1)),
(clamp_r(r0 + 1), clamp_c(c0)),
(clamp_r(r0 + 1), clamp_c(c0 + 1)),
];
let weights = [
(1.0 - fr) * (1.0 - fc),
(1.0 - fr) * fc,
fr * (1.0 - fc),
fr * fc,
];
let mut dy = 0.0_f64;
let mut dx = 0.0_f64;
for (idx, &(cr, cc)) in corners.iter().enumerate() {
let base = (cr * cols + cc) * 2;
dy += weights[idx] * field.field[base];
dx += weights[idx] * field.field[base + 1];
}
[dy, dx]
}
fn apply_affine_point(aff: &AffineTransform2D, r: f64, c: f64) -> (f64, f64) {
let m = &aff.matrix;
let nr = m[[0, 0]] * r + m[[0, 1]] * c + m[[0, 2]];
let nc = m[[1, 0]] * r + m[[1, 1]] * c + m[[1, 2]];
(nr, nc)
}
fn apply_rigid_point(rig: &RigidTransform2D, r: f64, c: f64) -> (f64, f64) {
let cos_a = rig.angle.cos();
let sin_a = rig.angle.sin();
let nr = cos_a * r - sin_a * c + rig.ty;
let nc = sin_a * r + cos_a * c + rig.tx;
(nr, nc)
}
fn gaussian_smooth_1d_separable(buf: &mut Vec<f64>, rows: usize, cols: usize, sigma: f64) {
let radius = (3.0 * sigma).ceil() as usize;
let kernel: Vec<f64> = {
let two_sig2 = 2.0 * sigma * sigma;
let k: Vec<f64> = (0..=radius)
.flat_map(|i| {
if i == 0 {
vec![(-(0_f64.powi(2)) / two_sig2).exp()]
} else {
let v = (-(i as f64).powi(2) / two_sig2).exp();
vec![v, v]
}
})
.collect();
let mut full = Vec::with_capacity(2 * radius + 1);
for i in (1..=radius).rev() {
full.push((-(i as f64).powi(2) / two_sig2).exp());
}
full.push(0.0_f64.exp()); for i in 1..=radius {
full.push((-(i as f64).powi(2) / two_sig2).exp());
}
let sum: f64 = full.iter().sum();
let _ = k; full.iter().map(|v| v / sum).collect()
};
let klen = kernel.len();
let krad = klen / 2;
let mut tmp = buf.clone();
for r in 0..rows {
for c in 0..cols {
let mut acc = 0.0;
for (ki, &kv) in kernel.iter().enumerate() {
let sc = c as isize + ki as isize - krad as isize;
let sc_clamped = sc.clamp(0, cols as isize - 1) as usize;
acc += kv * buf[r * cols + sc_clamped];
}
tmp[r * cols + c] = acc;
}
}
for r in 0..rows {
for c in 0..cols {
let mut acc = 0.0;
for (ki, &kv) in kernel.iter().enumerate() {
let sr = r as isize + ki as isize - krad as isize;
let sr_clamped = sr.clamp(0, rows as isize - 1) as usize;
acc += kv * tmp[sr_clamped * cols + c];
}
buf[r * cols + c] = acc;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
fn make_test_image(rows: usize, cols: usize, offset: f64) -> Array2<f64> {
let mut img = Array2::<f64>::zeros((rows, cols));
for r in 0..rows {
for c in 0..cols {
img[[r, c]] = ((r as f64 + offset).sin() + (c as f64).cos()) * 50.0 + 128.0;
}
}
img
}
#[test]
fn test_displacement_field_create_and_access() {
let mut df = DisplacementField::zeros_2d(10, 10);
df.set_2d(3, 4, 1.5, -2.0).expect("set_2d should succeed for valid coordinates");
let [dy, dx] = df.get_2d(3, 4).expect("get_2d should succeed for valid coordinates");
assert!((dy - 1.5).abs() < 1e-10);
assert!((dx + 2.0).abs() < 1e-10);
}
#[test]
fn test_displacement_field_compose_identity() {
let a = DisplacementField::zeros_2d(8, 8);
let b = DisplacementField::zeros_2d(8, 8);
let composed = a.compose_2d(&b).expect("compose_2d should succeed with identical-size fields");
assert!(composed.rms_magnitude() < 1e-10);
}
#[test]
fn test_jacobian_determinant_identity() {
let field = DisplacementField::zeros_2d(10, 10);
let det = JacobianDeterminant::compute_2d(&field).expect("compute_2d should succeed on identity field");
for v in det.iter() {
assert!((v - 1.0).abs() < 1e-8, "Expected det≈1, got {}", v);
}
}
#[test]
fn test_jacobian_folding_fraction_zero_for_identity() {
let field = DisplacementField::zeros_2d(10, 10);
let frac = JacobianDeterminant::folding_fraction_2d(&field).expect("folding_fraction_2d should succeed on identity field");
assert!(frac < 1e-10);
}
#[test]
fn test_demons_diffeo_smoke() {
let fixed = make_test_image(16, 16, 0.0);
let moving = make_test_image(16, 16, 0.3);
let reg = DemonsDiffeo::new();
let result = reg.register(&fixed, &moving).expect("DemonsDiffeo register should succeed on valid images");
assert!(result.iterations > 0);
let _ = result.field;
}
#[test]
fn test_fluid_registration_smoke() {
let fixed = make_test_image(16, 16, 0.0);
let moving = make_test_image(16, 16, 0.3);
let reg = FluidRegistration::new();
let result = reg.register(&fixed, &moving).expect("FluidRegistration register should succeed on valid images");
assert!(result.iterations > 0);
}
#[test]
fn test_ffd_registration_smoke() {
let fixed = make_test_image(16, 16, 0.0);
let moving = make_test_image(16, 16, 0.3);
let reg = FreeFormDeformation::new();
let result = reg.register(&fixed, &moving).expect("FreeFormDeformation register should succeed on valid images");
assert!(result.iterations > 0);
}
#[test]
fn test_composite_transform_identity() {
let t = CompositeTransform::identity();
let (nr, nc) = t.apply_to_point(5.0, 7.0).expect("apply_to_point should succeed for identity transform");
assert!((nr - 5.0).abs() < 1e-10);
assert!((nc - 7.0).abs() < 1e-10);
}
#[test]
fn test_gaussian_smooth_does_not_panic() {
let mut df = DisplacementField::zeros_2d(8, 8);
df.set_2d(4, 4, 10.0, -5.0).expect("set_2d should succeed for valid coordinates");
df.gaussian_smooth_2d(1.0).expect("gaussian_smooth_2d should succeed with sigma=1");
let [dy, _dx] = df.get_2d(4, 4).expect("get_2d should succeed for valid coordinates");
assert!(dy < 10.0);
}
}