use crate::core::coords::PixelCoord;
use crate::core::geometry::Ellipse;
use crate::core::image_view::ImageView;
use crate::core::scalar::Scalar;
#[derive(Debug, Clone)]
pub struct RadialProfile {
pub values: Vec<Scalar>,
pub radii: Vec<Scalar>,
pub center: PixelCoord,
}
pub fn compute_radial_profile(
image: &ImageView<'_, f32>,
center: PixelCoord,
max_radius: Scalar,
num_radial_steps: usize,
num_angles: usize,
) -> RadialProfile {
let mut values = Vec::with_capacity(num_radial_steps);
let mut radii = Vec::with_capacity(num_radial_steps);
for ri in 0..num_radial_steps {
let r = max_radius * (ri as Scalar + 0.5) / num_radial_steps as Scalar;
let mut sum = 0.0f32;
let mut count = 0u32;
for ai in 0..num_angles {
let theta = 2.0 * std::f32::consts::PI * ai as Scalar / num_angles as Scalar;
let sx = center.x + r * theta.cos();
let sy = center.y + r * theta.sin();
if let Some(val) = image.sample(sx, sy) {
sum += val;
count += 1;
}
}
let avg = if count > 0 {
sum / count as Scalar
} else {
0.0
};
values.push(avg);
radii.push(r);
}
RadialProfile {
values,
radii,
center,
}
}
pub fn compute_normal_profile(
image: &ImageView<'_, f32>,
ellipse: &Ellipse,
parametric_angle: Scalar,
profile_half_length: Scalar,
num_steps: usize,
) -> Vec<Scalar> {
let cos_a = ellipse.angle.cos();
let sin_a = ellipse.angle.sin();
let ex = ellipse.semi_major * parametric_angle.cos();
let ey = ellipse.semi_minor * parametric_angle.sin();
let nx_local = ellipse.semi_minor * parametric_angle.cos();
let ny_local = ellipse.semi_major * parametric_angle.sin();
let n_mag = (nx_local * nx_local + ny_local * ny_local).sqrt();
if n_mag < 1e-8 {
return vec![0.0; num_steps];
}
let nx_local = nx_local / n_mag;
let ny_local = ny_local / n_mag;
let base_x = ellipse.center.x + ex * cos_a - ey * sin_a;
let base_y = ellipse.center.y + ex * sin_a + ey * cos_a;
let dir_x = nx_local * cos_a - ny_local * sin_a;
let dir_y = nx_local * sin_a + ny_local * cos_a;
let mut profile = Vec::with_capacity(num_steps);
for si in 0..num_steps {
let t = -profile_half_length
+ 2.0 * profile_half_length * si as Scalar / (num_steps - 1).max(1) as Scalar;
let sx = base_x + t * dir_x;
let sy = base_y + t * dir_y;
let val = image.sample(sx, sy).unwrap_or(0.0);
profile.push(val);
}
profile
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::geometry::Ellipse;
use crate::core::image_view::OwnedImage;
#[test]
fn radial_profile_of_ring() {
let size = 80;
let cx = 40.0f32;
let cy = 40.0f32;
let r_inner = 14.0f32;
let r_outer = 18.0f32;
let mut data = vec![0.0f32; size * size];
for y in 0..size {
for x in 0..size {
let dx = x as f32 - cx;
let dy = y as f32 - cy;
let r = (dx * dx + dy * dy).sqrt();
if r >= r_inner && r <= r_outer {
data[y * size + x] = 1.0;
}
}
}
let img = OwnedImage::from_vec(data, size, size).unwrap();
let profile = compute_radial_profile(&img.view(), PixelCoord::new(cx, cy), 30.0, 30, 64);
assert_eq!(profile.values.len(), 30);
assert_eq!(profile.radii.len(), 30);
let (peak_idx, _) = profile
.values
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.unwrap();
let peak_r = profile.radii[peak_idx];
let mid_r = (r_inner + r_outer) / 2.0;
assert!(
(peak_r - mid_r).abs() < 3.0,
"peak at r={peak_r}, expected near {mid_r}"
);
}
#[test]
fn radial_profile_of_uniform() {
let size = 40;
let data = vec![0.5f32; size * size];
let img = OwnedImage::from_vec(data, size, size).unwrap();
let profile =
compute_radial_profile(&img.view(), PixelCoord::new(20.0, 20.0), 10.0, 10, 32);
for &v in &profile.values {
assert!((v - 0.5).abs() < 0.01, "expected ~0.5, got {v}");
}
}
#[test]
fn normal_profile_length() {
let size = 40;
let data = vec![1.0f32; size * size];
let img = OwnedImage::from_vec(data, size, size).unwrap();
let ellipse = Ellipse::new(PixelCoord::new(20.0, 20.0), 10.0, 10.0, 0.0);
let profile = compute_normal_profile(&img.view(), &ellipse, 0.0, 5.0, 11);
assert_eq!(profile.len(), 11);
}
}