use crate::error::AlgorithmError;
use oxigdal_core::vector::Point;
const LCG_MULTIPLIER: u64 = 6_364_136_223_846_793_005;
const LCG_INCREMENT: u64 = 1_442_695_040_888_963_407;
const DEGENERATE_EPSILON: f64 = 1e-12;
#[derive(Debug, Clone, PartialEq)]
pub struct RansacOptions {
pub max_iterations: usize,
pub inlier_threshold: f64,
pub min_inlier_count: usize,
pub seed: u64,
pub confidence: f64,
}
impl Default for RansacOptions {
fn default() -> Self {
Self {
max_iterations: 1000,
inlier_threshold: 1e-3,
min_inlier_count: 2,
seed: 0xDEAD_BEEF,
confidence: 0.99,
}
}
}
#[derive(Debug, Clone)]
pub struct RansacResult<M> {
pub model: M,
pub inliers: Vec<usize>,
pub iterations: usize,
pub converged: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RansacLineModel {
pub a: f64,
pub b: f64,
pub c: f64,
}
impl RansacLineModel {
#[must_use]
pub fn from_two_points(p1: Point, p2: Point) -> Option<Self> {
let dx = p2.x() - p1.x();
let dy = p2.y() - p1.y();
let norm = (dx * dx + dy * dy).sqrt();
if !norm.is_finite() || norm <= DEGENERATE_EPSILON {
return None;
}
let a = -dy / norm;
let b = dx / norm;
let c = -(a * p1.x() + b * p1.y());
Some(Self { a, b, c })
}
#[must_use]
pub fn distance_to(&self, p: Point) -> f64 {
(self.a * p.x() + self.b * p.y() + self.c).abs()
}
#[must_use]
pub fn direction(&self) -> (f64, f64) {
(-self.b, self.a)
}
#[must_use]
pub fn point_on_line(&self) -> (f64, f64) {
(-self.a * self.c, -self.b * self.c)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RansacPlaneModel {
pub a: f64,
pub b: f64,
pub c: f64,
pub d: f64,
}
impl RansacPlaneModel {
#[must_use]
pub fn from_three_points(
p1: (f64, f64, f64),
p2: (f64, f64, f64),
p3: (f64, f64, f64),
) -> Option<Self> {
let v1 = (p2.0 - p1.0, p2.1 - p1.1, p2.2 - p1.2);
let v2 = (p3.0 - p1.0, p3.1 - p1.1, p3.2 - p1.2);
let nx = v1.1 * v2.2 - v1.2 * v2.1;
let ny = v1.2 * v2.0 - v1.0 * v2.2;
let nz = v1.0 * v2.1 - v1.1 * v2.0;
let norm = (nx * nx + ny * ny + nz * nz).sqrt();
if !norm.is_finite() || norm <= DEGENERATE_EPSILON {
return None;
}
let a = nx / norm;
let b = ny / norm;
let c = nz / norm;
let d = -(a * p1.0 + b * p1.1 + c * p1.2);
Some(Self { a, b, c, d })
}
#[must_use]
pub fn distance_to(&self, p: (f64, f64, f64)) -> f64 {
(self.a * p.0 + self.b * p.1 + self.c * p.2 + self.d).abs()
}
#[must_use]
pub fn normal(&self) -> (f64, f64, f64) {
(self.a, self.b, self.c)
}
}
#[inline]
fn lcg_next(state: &mut u64) -> u64 {
*state = state
.wrapping_mul(LCG_MULTIPLIER)
.wrapping_add(LCG_INCREMENT);
*state
}
#[inline]
fn lcg_index(n: usize, state: &mut u64) -> usize {
let raw = lcg_next(state);
((raw >> 33) as usize) % n
}
fn sample_two_distinct(n: usize, state: &mut u64) -> (usize, usize) {
let i = lcg_index(n, state);
let mut j = lcg_index(n, state);
while j == i {
j = lcg_index(n, state);
}
(i, j)
}
fn sample_three_distinct(n: usize, state: &mut u64) -> (usize, usize, usize) {
let i = lcg_index(n, state);
let mut j = lcg_index(n, state);
while j == i {
j = lcg_index(n, state);
}
let mut k = lcg_index(n, state);
while k == i || k == j {
k = lcg_index(n, state);
}
(i, j, k)
}
fn adaptive_iteration_count(
inlier_ratio: f64,
sample_size: usize,
confidence: f64,
max: usize,
) -> usize {
if max == 0 {
return 0;
}
if !inlier_ratio.is_finite() || inlier_ratio <= 0.0 {
return max;
}
if inlier_ratio >= 1.0 {
return 1;
}
let conf = if !confidence.is_finite() {
return max;
} else {
confidence.clamp(0.0, 1.0 - f64::EPSILON)
};
let prob_all_inliers = inlier_ratio.powi(sample_size as i32);
let denom = (1.0 - prob_all_inliers).ln();
let numer = (1.0 - conf).ln();
if !denom.is_finite() || !numer.is_finite() || denom >= 0.0 {
return max;
}
let raw = numer / denom;
if !raw.is_finite() {
return max;
}
let needed = raw.ceil();
if needed <= 1.0 {
1
} else if needed >= max as f64 {
max
} else {
needed as usize
}
}
fn least_squares_line(points: &[Point], indices: &[usize]) -> Option<RansacLineModel> {
let n = indices.len();
if n < 2 {
return None;
}
let inv_n = 1.0 / n as f64;
let mut mean_x = 0.0;
let mut mean_y = 0.0;
for &idx in indices {
mean_x += points[idx].x();
mean_y += points[idx].y();
}
mean_x *= inv_n;
mean_y *= inv_n;
let mut sxx = 0.0;
let mut sxy = 0.0;
let mut syy = 0.0;
for &idx in indices {
let dx = points[idx].x() - mean_x;
let dy = points[idx].y() - mean_y;
sxx += dx * dx;
sxy += dx * dy;
syy += dy * dy;
}
if sxx + syy <= DEGENERATE_EPSILON {
return None;
}
let trace_half = 0.5 * (sxx + syy);
let diff_half = 0.5 * (sxx - syy);
let radius = (diff_half * diff_half + sxy * sxy).sqrt();
let lambda_min = trace_half - radius;
let (mut a, mut b) = {
let r0x = sxx - lambda_min;
let r0y = sxy;
let r1x = sxy;
let r1y = syy - lambda_min;
if r0x * r0x + r0y * r0y >= r1x * r1x + r1y * r1y {
(-r0y, r0x)
} else {
(-r1y, r1x)
}
};
let norm = (a * a + b * b).sqrt();
if !norm.is_finite() || norm <= DEGENERATE_EPSILON {
return None;
}
a /= norm;
b /= norm;
let c = -(a * mean_x + b * mean_y);
Some(RansacLineModel { a, b, c })
}
#[inline]
fn sym3_mul(m: &[f64; 6], v: (f64, f64, f64)) -> (f64, f64, f64) {
(
m[0] * v.0 + m[1] * v.1 + m[2] * v.2,
m[1] * v.0 + m[3] * v.1 + m[4] * v.2,
m[2] * v.0 + m[4] * v.1 + m[5] * v.2,
)
}
#[inline]
fn vec3_norm(v: (f64, f64, f64)) -> f64 {
(v.0 * v.0 + v.1 * v.1 + v.2 * v.2).sqrt()
}
#[inline]
fn vec3_normalize(v: (f64, f64, f64)) -> Option<(f64, f64, f64)> {
let n = vec3_norm(v);
if !n.is_finite() || n <= DEGENERATE_EPSILON {
None
} else {
Some((v.0 / n, v.1 / n, v.2 / n))
}
}
#[inline]
fn vec3_cross(a: (f64, f64, f64), b: (f64, f64, f64)) -> (f64, f64, f64) {
(
a.1 * b.2 - a.2 * b.1,
a.2 * b.0 - a.0 * b.2,
a.0 * b.1 - a.1 * b.0,
)
}
#[inline]
fn vec3_dot(a: (f64, f64, f64), b: (f64, f64, f64)) -> f64 {
a.0 * b.0 + a.1 * b.1 + a.2 * b.2
}
fn dominant_eigenvector(m: &[f64; 6], seed: (f64, f64, f64)) -> Option<((f64, f64, f64), f64)> {
const MAX_ITER: usize = 50;
const TOL: f64 = 1e-12;
let mut v = vec3_normalize(seed).unwrap_or((1.0, 0.0, 0.0));
let mut prev_eigenvalue = 0.0;
for _ in 0..MAX_ITER {
let mv = sym3_mul(m, v);
let next = vec3_normalize(mv)?;
let eigenvalue = vec3_dot(next, sym3_mul(m, next));
if (eigenvalue - prev_eigenvalue).abs() <= TOL * eigenvalue.abs().max(1.0) {
return Some((next, eigenvalue));
}
prev_eigenvalue = eigenvalue;
v = next;
}
let eigenvalue = vec3_dot(v, sym3_mul(m, v));
Some((v, eigenvalue))
}
fn least_squares_plane(points: &[(f64, f64, f64)], indices: &[usize]) -> Option<RansacPlaneModel> {
let n = indices.len();
if n < 3 {
return None;
}
let inv_n = 1.0 / n as f64;
let mut mean = (0.0, 0.0, 0.0);
for &idx in indices {
let p = points[idx];
mean.0 += p.0;
mean.1 += p.1;
mean.2 += p.2;
}
mean.0 *= inv_n;
mean.1 *= inv_n;
mean.2 *= inv_n;
let mut cov = [0.0_f64; 6];
for &idx in indices {
let p = points[idx];
let dx = p.0 - mean.0;
let dy = p.1 - mean.1;
let dz = p.2 - mean.2;
cov[0] += dx * dx;
cov[1] += dx * dy;
cov[2] += dx * dz;
cov[3] += dy * dy;
cov[4] += dy * dz;
cov[5] += dz * dz;
}
let total_spread = cov[0] + cov[3] + cov[5];
if total_spread <= DEGENERATE_EPSILON {
return None;
}
let (v1, lambda1) = dominant_eigenvector(&cov, (1.0, 0.0, 0.0))?;
let deflated = [
cov[0] - lambda1 * v1.0 * v1.0,
cov[1] - lambda1 * v1.0 * v1.1,
cov[2] - lambda1 * v1.0 * v1.2,
cov[3] - lambda1 * v1.1 * v1.1,
cov[4] - lambda1 * v1.1 * v1.2,
cov[5] - lambda1 * v1.2 * v1.2,
];
let helper = if v1.0.abs() <= 0.9 {
(1.0, 0.0, 0.0)
} else {
(0.0, 1.0, 0.0)
};
let seed2 = vec3_cross(v1, helper);
let (v2, _lambda2) = dominant_eigenvector(&deflated, seed2)?;
let normal = vec3_normalize(vec3_cross(v1, v2))?;
let d = -(normal.0 * mean.0 + normal.1 * mean.1 + normal.2 * mean.2);
Some(RansacPlaneModel {
a: normal.0,
b: normal.1,
c: normal.2,
d,
})
}
fn all_points_finite_2d(points: &[Point]) -> bool {
points
.iter()
.all(|p| p.x().is_finite() && p.y().is_finite())
}
fn all_points_finite_3d(points: &[(f64, f64, f64)]) -> bool {
points
.iter()
.all(|p| p.0.is_finite() && p.1.is_finite() && p.2.is_finite())
}
pub fn ransac_fit_line(
points: &[Point],
options: &RansacOptions,
) -> Result<RansacResult<RansacLineModel>, AlgorithmError> {
if points.len() < 2 {
return Err(AlgorithmError::InvalidInput(format!(
"ransac_fit_line requires at least 2 points, got {}",
points.len()
)));
}
if !all_points_finite_2d(points) {
return Err(AlgorithmError::InvalidInput(
"ransac_fit_line requires all point coordinates to be finite".to_string(),
));
}
let n = points.len();
let threshold = options.inlier_threshold;
let min_inliers = options.min_inlier_count.max(2);
let mut state = options.seed;
let mut best_model: Option<RansacLineModel> = None;
let mut best_inlier_count = 0usize;
let mut iterations = 0usize;
let mut needed_iterations = options.max_iterations;
while iterations < needed_iterations && iterations < options.max_iterations {
iterations += 1;
let (i, j) = sample_two_distinct(n, &mut state);
let candidate = match RansacLineModel::from_two_points(points[i].clone(), points[j].clone())
{
Some(model) => model,
None => continue,
};
let mut inlier_count = 0usize;
for p in points {
if candidate.distance_to(p.clone()) <= threshold {
inlier_count += 1;
}
}
if inlier_count > best_inlier_count {
best_inlier_count = inlier_count;
best_model = Some(candidate);
let inlier_ratio = inlier_count as f64 / n as f64;
needed_iterations = adaptive_iteration_count(
inlier_ratio,
2,
options.confidence,
options.max_iterations,
);
}
}
let converged = best_inlier_count >= min_inliers;
let sampled_model = best_model.unwrap_or_else(|| {
RansacLineModel::from_two_points(points[0].clone(), points[1].clone()).unwrap_or(
RansacLineModel {
a: 0.0,
b: 1.0,
c: -points[0].y(),
},
)
});
let inliers: Vec<usize> = points
.iter()
.enumerate()
.filter(|(_, p)| sampled_model.distance_to((*p).clone()) <= threshold)
.map(|(idx, _)| idx)
.collect();
let model = match least_squares_line(points, &inliers) {
Some(refined) => {
let refit_inliers = points
.iter()
.filter(|p| refined.distance_to((*p).clone()) <= threshold)
.count();
if refit_inliers >= inliers.len() {
refined
} else {
sampled_model
}
}
None => sampled_model,
};
let final_inliers: Vec<usize> = points
.iter()
.enumerate()
.filter(|(_, p)| model.distance_to((*p).clone()) <= threshold)
.map(|(idx, _)| idx)
.collect();
Ok(RansacResult {
model,
inliers: final_inliers,
iterations,
converged,
})
}
pub fn ransac_fit_plane(
points: &[(f64, f64, f64)],
options: &RansacOptions,
) -> Result<RansacResult<RansacPlaneModel>, AlgorithmError> {
if points.len() < 3 {
return Err(AlgorithmError::InvalidInput(format!(
"ransac_fit_plane requires at least 3 points, got {}",
points.len()
)));
}
if !all_points_finite_3d(points) {
return Err(AlgorithmError::InvalidInput(
"ransac_fit_plane requires all point coordinates to be finite".to_string(),
));
}
let n = points.len();
let threshold = options.inlier_threshold;
let min_inliers = options.min_inlier_count.max(3);
let mut state = options.seed;
let mut best_model: Option<RansacPlaneModel> = None;
let mut best_inlier_count = 0usize;
let mut iterations = 0usize;
let mut needed_iterations = options.max_iterations;
while iterations < needed_iterations && iterations < options.max_iterations {
iterations += 1;
let (i, j, k) = sample_three_distinct(n, &mut state);
let candidate = match RansacPlaneModel::from_three_points(points[i], points[j], points[k]) {
Some(model) => model,
None => continue,
};
let mut inlier_count = 0usize;
for &p in points {
if candidate.distance_to(p) <= threshold {
inlier_count += 1;
}
}
if inlier_count > best_inlier_count {
best_inlier_count = inlier_count;
best_model = Some(candidate);
let inlier_ratio = inlier_count as f64 / n as f64;
needed_iterations = adaptive_iteration_count(
inlier_ratio,
3,
options.confidence,
options.max_iterations,
);
}
}
let converged = best_inlier_count >= min_inliers;
let sampled_model = best_model.unwrap_or_else(|| {
RansacPlaneModel::from_three_points(points[0], points[1], points[2]).unwrap_or(
RansacPlaneModel {
a: 0.0,
b: 0.0,
c: 1.0,
d: -points[0].2,
},
)
});
let inliers: Vec<usize> = points
.iter()
.enumerate()
.filter(|(_, p)| sampled_model.distance_to(**p) <= threshold)
.map(|(idx, _)| idx)
.collect();
let model = match least_squares_plane(points, &inliers) {
Some(refined) => {
let refit_inliers = points
.iter()
.filter(|p| refined.distance_to(**p) <= threshold)
.count();
if refit_inliers >= inliers.len() {
refined
} else {
sampled_model
}
}
None => sampled_model,
};
let final_inliers: Vec<usize> = points
.iter()
.enumerate()
.filter(|(_, p)| model.distance_to(**p) <= threshold)
.map(|(idx, _)| idx)
.collect();
Ok(RansacResult {
model,
inliers: final_inliers,
iterations,
converged,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lcg_is_deterministic_and_advances() {
let mut s1 = 12345;
let mut s2 = 12345;
let a = lcg_next(&mut s1);
let b = lcg_next(&mut s2);
assert_eq!(a, b);
let c = lcg_next(&mut s1);
assert_ne!(a, c);
}
#[test]
fn lcg_index_in_range() {
let mut state = 7;
for _ in 0..1000 {
let idx = lcg_index(10, &mut state);
assert!(idx < 10);
}
}
#[test]
fn sample_two_distinct_yields_distinct() {
let mut state = 99;
for _ in 0..1000 {
let (i, j) = sample_two_distinct(5, &mut state);
assert_ne!(i, j);
assert!(i < 5 && j < 5);
}
}
#[test]
fn sample_three_distinct_yields_distinct() {
let mut state = 99;
for _ in 0..1000 {
let (i, j, k) = sample_three_distinct(6, &mut state);
assert_ne!(i, j);
assert_ne!(i, k);
assert_ne!(j, k);
assert!(i < 6 && j < 6 && k < 6);
}
}
#[test]
fn adaptive_count_guards() {
assert_eq!(adaptive_iteration_count(1.0, 2, 0.99, 1000), 1);
assert_eq!(adaptive_iteration_count(1.5, 2, 0.99, 1000), 1);
assert_eq!(adaptive_iteration_count(0.0, 2, 0.99, 1000), 1000);
assert_eq!(adaptive_iteration_count(-0.5, 2, 0.99, 1000), 1000);
assert_eq!(adaptive_iteration_count(f64::NAN, 2, 0.99, 1000), 1000);
assert_eq!(adaptive_iteration_count(f64::INFINITY, 2, 0.99, 1000), 1000);
assert_eq!(adaptive_iteration_count(0.5, 2, f64::NAN, 1000), 1000);
let mid = adaptive_iteration_count(0.5, 2, 0.99, 1000);
assert!((1..=1000).contains(&mid));
}
#[test]
fn line_from_coincident_points_is_none() {
let p = Point::new(1.0, 1.0);
assert!(RansacLineModel::from_two_points(p.clone(), p).is_none());
}
#[test]
fn plane_from_collinear_points_is_none() {
let p1 = (0.0, 0.0, 0.0);
let p2 = (1.0, 1.0, 1.0);
let p3 = (2.0, 2.0, 2.0);
assert!(RansacPlaneModel::from_three_points(p1, p2, p3).is_none());
}
}