use nalgebra::Matrix3;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq)]
pub enum ConicError {
TooFewPoints {
needed: usize,
got: usize,
},
InsufficientInliers {
needed: usize,
found: usize,
},
}
impl std::fmt::Display for ConicError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooFewPoints { needed, got } => {
write!(f, "too few points: need {}, got {}", needed, got)
}
Self::InsufficientInliers { needed, found } => {
write!(f, "insufficient inliers: need {}, found {}", needed, found)
}
}
}
}
impl std::error::Error for ConicError {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ConicCoeffs(pub [f64; 6]);
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Ellipse {
pub cx: f64,
pub cy: f64,
pub a: f64,
pub b: f64,
pub angle: f64,
}
impl Ellipse {
pub fn mean_axis(&self) -> f64 {
(self.a + self.b) * 0.5
}
pub fn center(&self) -> [f64; 2] {
[self.cx, self.cy]
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Conic2D {
pub mat: Matrix3<f64>,
}
impl Conic2D {
pub fn from_quadratic_coeffs(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Self {
Self {
mat: Matrix3::new(
a,
b * 0.5,
d * 0.5,
b * 0.5,
c,
e * 0.5,
d * 0.5,
e * 0.5,
f,
),
}
}
pub fn from_coeffs(c: &ConicCoeffs) -> Self {
let [a, b, cc, d, e, f] = c.0;
Self::from_quadratic_coeffs(a, b, cc, d, e, f)
}
pub fn from_ellipse(e: &Ellipse) -> Self {
Self::from_coeffs(&ellipse_to_conic(e))
}
pub fn normalize_frobenius(&self) -> Option<Self> {
let n = self.mat.norm();
if !n.is_finite() || n <= 1e-15 {
return None;
}
Some(Self { mat: self.mat / n })
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RansacConfig {
pub max_iters: usize,
pub inlier_threshold: f64,
pub min_inliers: usize,
pub seed: u64,
}
impl Default for RansacConfig {
fn default() -> Self {
Self {
max_iters: 500,
inlier_threshold: 1.0, min_inliers: 10,
seed: 42,
}
}
}
#[derive(Debug, Clone)]
pub struct RansacResult {
pub ellipse: Ellipse,
pub num_inliers: usize,
}
impl ConicCoeffs {
pub fn normalized(&self) -> Option<Self> {
let [a, b, c, d, e, f] = self.0;
let trace = a + c;
if trace.abs() < 1e-15 {
return None;
}
let s = 1.0 / trace;
Some(Self([a * s, b * s, c * s, d * s, e * s, f * s]))
}
pub fn algebraic_distance(&self, x: f64, y: f64) -> f64 {
let [a, b, c, d, e, f] = self.0;
a * x * x + b * x * y + c * y * y + d * x + e * y + f
}
pub fn is_ellipse(&self) -> bool {
let [a, b, c, ..] = self.0;
b * b - 4.0 * a * c < 0.0
}
pub fn to_ellipse(self) -> Option<Ellipse> {
conic_to_ellipse(&self)
}
}
impl Ellipse {
pub fn is_valid(&self) -> bool {
self.a > 0.0
&& self.b > 0.0
&& self.a.is_finite()
&& self.b.is_finite()
&& self.cx.is_finite()
&& self.cy.is_finite()
&& self.angle.is_finite()
}
pub fn aspect_ratio(&self) -> f64 {
if self.a >= self.b {
self.a / self.b
} else {
self.b / self.a
}
}
pub fn to_conic(self) -> ConicCoeffs {
ellipse_to_conic(&self)
}
pub fn sample_points(&self, n: usize) -> Vec<[f64; 2]> {
let cos_a = self.angle.cos();
let sin_a = self.angle.sin();
(0..n)
.map(|i| {
let t = 2.0 * std::f64::consts::PI * (i as f64) / (n as f64);
let px = self.a * t.cos();
let py = self.b * t.sin();
let x = self.cx + cos_a * px - sin_a * py;
let y = self.cy + sin_a * px + cos_a * py;
[x, y]
})
.collect()
}
pub fn sampson_distance(&self, x: f64, y: f64) -> f64 {
let c = self.to_conic();
let [ca, cb, cc, cd, ce, _cf] = c.0;
let alg = c.algebraic_distance(x, y);
let gx = 2.0 * ca * x + cb * y + cd;
let gy = cb * x + 2.0 * cc * y + ce;
let grad_mag_sq = gx * gx + gy * gy;
if grad_mag_sq < 1e-30 {
return alg.abs();
}
alg.abs() / grad_mag_sq.sqrt()
}
}
pub fn conic_to_ellipse(c: &ConicCoeffs) -> Option<Ellipse> {
let [a, b, c_coeff, d, e, f] = c.0;
let disc = b * b - 4.0 * a * c_coeff;
if disc >= 0.0 {
return None;
}
let m = Matrix3::new(
a,
b / 2.0,
d / 2.0,
b / 2.0,
c_coeff,
e / 2.0,
d / 2.0,
e / 2.0,
f,
);
let det_m = m.determinant();
if det_m.abs() < 1e-15 {
return None;
}
let denom = 4.0 * a * c_coeff - b * b; let cx = (b * e - 2.0 * c_coeff * d) / denom;
let cy = (b * d - 2.0 * a * e) / denom;
let angle = if (a - c_coeff).abs() < 1e-15 {
if b > 0.0 {
std::f64::consts::FRAC_PI_4
} else if b < 0.0 {
-std::f64::consts::FRAC_PI_4
} else {
0.0
}
} else {
0.5 * (b).atan2(a - c_coeff)
};
let sum = a + c_coeff;
let diff = ((a - c_coeff).powi(2) + b * b).sqrt();
let lambda1 = (sum + diff) / 2.0;
let lambda2 = (sum - diff) / 2.0;
let f_prime = a * cx * cx + b * cx * cy + c_coeff * cy * cy + d * cx + e * cy + f;
if f_prime.abs() < 1e-15 {
return None;
}
let a_sq = -f_prime / lambda1;
let b_sq = -f_prime / lambda2;
if a_sq <= 0.0 || b_sq <= 0.0 {
return None;
}
let semi_a = a_sq.sqrt();
let semi_b = b_sq.sqrt();
let (semi_a, semi_b, angle) = if semi_a >= semi_b {
(semi_a, semi_b, angle)
} else {
(semi_b, semi_a, angle + std::f64::consts::FRAC_PI_2)
};
let angle = normalize_angle(angle);
Some(Ellipse {
cx,
cy,
a: semi_a,
b: semi_b,
angle,
})
}
pub fn ellipse_to_conic(e: &Ellipse) -> ConicCoeffs {
let cos_a = e.angle.cos();
let sin_a = e.angle.sin();
let a2 = e.a * e.a;
let b2 = e.b * e.b;
let ca = cos_a * cos_a / a2 + sin_a * sin_a / b2;
let cb = 2.0 * cos_a * sin_a * (1.0 / a2 - 1.0 / b2);
let cc = sin_a * sin_a / a2 + cos_a * cos_a / b2;
let cd = -2.0 * ca * e.cx - cb * e.cy;
let ce = -cb * e.cx - 2.0 * cc * e.cy;
let cf = ca * e.cx * e.cx + cb * e.cx * e.cy + cc * e.cy * e.cy - 1.0;
ConicCoeffs([ca, cb, cc, cd, ce, cf])
}
fn normalize_angle(mut angle: f64) -> f64 {
let pi = std::f64::consts::PI;
while angle > pi / 2.0 {
angle -= pi;
}
while angle <= -pi / 2.0 {
angle += pi;
}
angle
}