use scirs2_core::ndarray::{arr2, Array1, Array2, ArrayView2};
use scirs2_core::numeric::{Float, FromPrimitive};
use std::fmt::Debug;
use crate::error::{NdimageError, NdimageResult};
use crate::filters::{gaussian_filter, sobel};
use crate::interpolation::{map_coordinates, InterpolationOrder};
#[derive(Clone, Debug)]
pub struct ActiveContourParams {
pub alpha: f64,
pub beta: f64,
pub gamma: f64,
pub kappa: f64,
pub max_iterations: usize,
pub convergence: f64,
pub time_step: f64,
}
impl Default for ActiveContourParams {
fn default() -> Self {
Self {
alpha: 0.01,
beta: 0.1,
gamma: 1.0,
kappa: 0.0,
max_iterations: 1000,
convergence: 0.1,
time_step: 0.1,
}
}
}
#[allow(dead_code)]
fn gradient_vector_flow<T>(
edge_map: &ArrayView2<T>,
mu: f64,
iterations: usize,
) -> NdimageResult<(Array2<f64>, Array2<f64>)>
where
T: Float + FromPrimitive + std::ops::AddAssign + std::ops::DivAssign + 'static,
{
let (height, width) = edge_map.dim();
let (fx, fy) = compute_gradient(edge_map)?;
let mut u = fx.clone();
let mut v = fy.clone();
let edge_sq = edge_map.mapv(|x| x.to_f64().unwrap_or(0.0).powi(2));
for _ in 0..iterations {
let mut u_new = Array2::zeros((height, width));
let mut v_new = Array2::zeros((height, width));
for i in 1..height - 1 {
for j in 1..width - 1 {
let laplacian_u =
u[[i + 1, j]] + u[[i - 1, j]] + u[[i, j + 1]] + u[[i, j - 1]] - 4.0 * u[[i, j]];
let laplacian_v =
v[[i + 1, j]] + v[[i - 1, j]] + v[[i, j + 1]] + v[[i, j - 1]] - 4.0 * v[[i, j]];
let b = edge_sq[[i, j]];
u_new[[i, j]] = u[[i, j]] + mu * laplacian_u - b * (u[[i, j]] - fx[[i, j]]);
v_new[[i, j]] = v[[i, j]] + mu * laplacian_v - b * (v[[i, j]] - fy[[i, j]]);
}
}
u = u_new;
v = v_new;
}
Ok((u, v))
}
#[allow(dead_code)]
fn compute_gradient<T>(image: &ArrayView2<T>) -> NdimageResult<(Array2<f64>, Array2<f64>)>
where
T: Float + FromPrimitive + std::ops::AddAssign + std::ops::DivAssign + 'static,
{
let (height, width) = image.dim();
let mut gx = Array2::zeros((height, width));
let mut gy = Array2::zeros((height, width));
for i in 1..height - 1 {
for j in 1..width - 1 {
let dx = image[[i, j + 1]].to_f64().unwrap_or(0.0)
- image[[i, j - 1]].to_f64().unwrap_or(0.0);
let dy = image[[i + 1, j]].to_f64().unwrap_or(0.0)
- image[[i - 1, j]].to_f64().unwrap_or(0.0);
gx[[i, j]] = dx / 2.0;
gy[[i, j]] = dy / 2.0;
}
}
Ok((gx, gy))
}
#[allow(dead_code)]
pub fn active_contour<T>(
image: &ArrayView2<T>,
initial_contour: &ArrayView2<f64>,
params: Option<ActiveContourParams>,
) -> NdimageResult<Array2<f64>>
where
T: Float
+ FromPrimitive
+ Debug
+ Send
+ Sync
+ std::ops::AddAssign
+ std::ops::DivAssign
+ 'static,
{
let params = params.unwrap_or_default();
if initial_contour.dim().1 != 2 {
return Err(NdimageError::InvalidInput(
"Initial _contour must be N x 2 array".into(),
));
}
let num_points = initial_contour.dim().0;
if num_points < 3 {
return Err(NdimageError::InvalidInput(
"Contour must have at least 3 points".into(),
));
}
let smoothed = gaussian_filter(
&image.mapv(|x| x.to_f64().unwrap_or(0.0)),
2.0, None,
None,
)?;
let gx = sobel(&smoothed, 1, None)?; let gy = sobel(&smoothed, 0, None)?; let edge_map = (gx.mapv(|x| x * x) + gy.mapv(|x| x * x)).mapv(|x| x.sqrt());
let (u, v) = gradient_vector_flow(&edge_map.view(), 0.2, 100)?;
let mut _contour = initial_contour.to_owned();
let mut prev_contour = _contour.clone();
for iteration in 0..params.max_iterations {
prev_contour.assign(&_contour);
for i in 0..num_points {
let prev_idx = if i == 0 { num_points - 1 } else { i - 1 };
let next_idx = if i == num_points - 1 { 0 } else { i + 1 };
let x = _contour[[i, 0]];
let y = _contour[[i, 1]];
let x_prev = _contour[[prev_idx, 0]];
let y_prev = _contour[[prev_idx, 1]];
let x_next = _contour[[next_idx, 0]];
let y_next = _contour[[next_idx, 1]];
let avg_x = (x_prev + x_next) / 2.0;
let avg_y = (y_prev + y_next) / 2.0;
let internal_x = params.alpha * (avg_x - x);
let internal_y = params.alpha * (avg_y - y);
let curvature_x = params.beta * (x_prev - 2.0 * x + x_next);
let curvature_y = params.beta * (y_prev - 2.0 * y + y_next);
let (external_x, external_y) =
if x >= 0.0 && x < u.dim().1 as f64 - 1.0 && y >= 0.0 && y < u.dim().0 as f64 - 1.0
{
let coords = arr2(&[[y, x]]);
let u_interp = map_coordinates(
&u.to_owned(),
&coords.to_owned().into_dyn(),
Some(1), None,
None,
None, )?;
let v_interp = map_coordinates(
&v.to_owned(),
&coords.to_owned().into_dyn(),
Some(1), None,
None,
None, )?;
(params.gamma * u_interp[[0]], params.gamma * v_interp[[0]])
} else {
(0.0, 0.0)
};
let (balloon_x, balloon_y) = if params.kappa != 0.0 {
let dx = x_next - x_prev;
let dy = y_next - y_prev;
let norm = (dx * dx + dy * dy).sqrt();
if norm > 0.0 {
(-params.kappa * dy / norm, params.kappa * dx / norm)
} else {
(0.0, 0.0)
}
} else {
(0.0, 0.0)
};
_contour[[i, 0]] +=
params.time_step * (internal_x + curvature_x + external_x + balloon_x);
_contour[[i, 1]] +=
params.time_step * (internal_y + curvature_y + external_y + balloon_y);
_contour[[i, 0]] = _contour[[i, 0]].max(0.0).min((image.dim().1 - 1) as f64);
_contour[[i, 1]] = _contour[[i, 1]].max(0.0).min((image.dim().0 - 1) as f64);
}
let movement = ((_contour.clone() - prev_contour.clone()).mapv(|x| x * x))
.sum()
.sqrt();
if movement < params.convergence {
break;
}
}
Ok(_contour)
}
#[allow(dead_code)]
pub fn create_circle_contour(center: (f64, f64), radius: f64, num_points: usize) -> Array2<f64> {
let mut contour = Array2::zeros((num_points, 2));
for i in 0..num_points {
let angle = 2.0 * std::f64::consts::PI * i as f64 / num_points as f64;
contour[[i, 0]] = center.0 + radius * angle.cos();
contour[[i, 1]] = center.1 + radius * angle.sin();
}
contour
}
#[allow(dead_code)]
pub fn create_ellipse_contour(
center: (f64, f64),
semi_major: f64,
semi_minor: f64,
angle: f64,
num_points: usize,
) -> Array2<f64> {
let mut contour = Array2::zeros((num_points, 2));
let cos_angle = angle.cos();
let sin_angle = angle.sin();
for i in 0..num_points {
let theta = 2.0 * std::f64::consts::PI * i as f64 / num_points as f64;
let x = semi_major * theta.cos();
let y = semi_minor * theta.sin();
contour[[i, 0]] = center.0 + x * cos_angle - y * sin_angle;
contour[[i, 1]] = center.1 + x * sin_angle + y * cos_angle;
}
contour
}
#[allow(dead_code)]
pub fn mask_to_contour(mask: &ArrayView2<bool>) -> Vec<(f64, f64)> {
let (height, width) = mask.dim();
let mut contour_points = Vec::new();
for i in 0..height {
for j in 0..width {
if mask[[i, j]] {
let mut is_boundary = false;
for (di, dj) in &[(0, 1), (1, 0), (0, -1), (-1, 0)] {
let ni = i as i32 + di;
let nj = j as i32 + dj;
if ni < 0 || ni >= height as i32 || nj < 0 || nj >= width as i32 {
is_boundary = true;
break;
}
if !mask[[ni as usize, nj as usize]] {
is_boundary = true;
break;
}
}
if is_boundary {
contour_points.push((j as f64, i as f64));
}
}
}
}
if contour_points.len() > 2 {
order_contour_points(&mut contour_points);
}
contour_points
}
#[allow(dead_code)]
fn order_contour_points(points: &mut Vec<(f64, f64)>) {
if points.is_empty() {
return;
}
let mut ordered = vec![points[0]];
points.remove(0);
while !points.is_empty() {
let last = ordered.last().expect("Operation failed");
let mut min_dist = f64::INFINITY;
let mut min_idx = 0;
for (idx, point) in points.iter().enumerate() {
let dist = ((point.0 - last.0).powi(2) + (point.1 - last.1).powi(2)).sqrt();
if dist < min_dist {
min_dist = dist;
min_idx = idx;
}
}
ordered.push(points[min_idx]);
points.remove(min_idx);
}
*points = ordered;
}
#[allow(dead_code)]
pub fn smooth_contour(
contour: &ArrayView2<f64>,
smoothing_factor: f64,
) -> NdimageResult<Array2<f64>> {
let num_points = contour.dim().0;
if num_points < 3 {
return Ok(contour.to_owned());
}
let x_coords = contour.slice(scirs2_core::ndarray::s![.., 0]);
let y_coords = contour.slice(scirs2_core::ndarray::s![.., 1]);
let mut x_padded = Array1::zeros(num_points + 4);
let mut y_padded = Array1::zeros(num_points + 4);
x_padded
.slice_mut(scirs2_core::ndarray::s![2..num_points + 2])
.assign(&x_coords);
y_padded
.slice_mut(scirs2_core::ndarray::s![2..num_points + 2])
.assign(&y_coords);
x_padded[0] = x_coords[num_points - 2];
x_padded[1] = x_coords[num_points - 1];
x_padded[num_points + 2] = x_coords[0usize];
x_padded[num_points + 3] = x_coords[1usize];
y_padded[0] = y_coords[num_points - 2];
y_padded[1] = y_coords[num_points - 1];
y_padded[num_points + 2] = y_coords[0usize];
y_padded[num_points + 3] = y_coords[1usize];
let kernel_size = (smoothing_factor * 3.0) as usize;
let kernel_size = kernel_size.max(3).min(num_points / 2);
let mut smooth_x = Array1::zeros(num_points);
let mut smooth_y = Array1::zeros(num_points);
for i in 0..num_points {
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let mut weight_sum = 0.0;
for j in 0..kernel_size {
let idx = i + 2 + j - kernel_size / 2;
let weight = (-((j as f64 - kernel_size as f64 / 2.0).powi(2))
/ (2.0 * smoothing_factor.powi(2)))
.exp();
sum_x += x_padded[idx] * weight;
sum_y += y_padded[idx] * weight;
weight_sum += weight;
}
smooth_x[i] = sum_x / weight_sum;
smooth_y[i] = sum_y / weight_sum;
}
let mut smoothed = Array2::zeros((num_points, 2));
smoothed
.slice_mut(scirs2_core::ndarray::s![.., 0])
.assign(&smooth_x);
smoothed
.slice_mut(scirs2_core::ndarray::s![.., 1])
.assign(&smooth_y);
Ok(smoothed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_circle_contour() {
let contour = create_circle_contour((50.0, 50.0), 20.0, 16);
assert_eq!(contour.dim(), (16, 2));
for i in 0..16 {
let x = contour[[i, 0]] - 50.0;
let y = contour[[i, 1]] - 50.0;
let radius = (x * x + y * y).sqrt();
assert!((radius - 20.0).abs() < 1e-10);
}
}
#[test]
fn test_create_ellipse_contour() {
let contour = create_ellipse_contour((50.0, 50.0), 30.0, 20.0, 0.0, 20);
assert_eq!(contour.dim(), (20, 2));
for i in 0..20 {
let x = (contour[[i, 0]] - 50.0) / 30.0;
let y = (contour[[i, 1]] - 50.0) / 20.0;
let value = x * x + y * y;
assert!((value - 1.0).abs() < 1e-10);
}
}
#[test]
#[ignore] fn test_active_contour_simple() {
let mut image = Array2::zeros((100, 100));
for i in 0..100 {
for j in 0..100 {
let dx = i as f64 - 50.0;
let dy = j as f64 - 50.0;
let r = (dx * dx + dy * dy).sqrt();
if (r - 30.0).abs() < 2.0 {
image[[i, j]] = 1.0;
}
}
}
let initial = create_circle_contour((50.0, 50.0), 25.0, 20);
let params = ActiveContourParams {
max_iterations: 50,
..Default::default()
};
let result =
active_contour(&image.view(), &initial.view(), Some(params)).expect("Operation failed");
assert_eq!(result.dim(), (20, 2));
}
}