use crate::geometry::Point;
use crate::imgproc::rng::Prng;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Line {
pub a: f32,
pub b: f32,
pub c: f32,
}
impl Line {
pub fn from_points(p1: Point, p2: Point) -> Option<Line> {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let len = (dx * dx + dy * dy).sqrt();
if len < f32::EPSILON {
return None;
}
let a = dy / len;
let b = -dx / len;
let c = -(a * p1.x + b * p1.y);
Some(Line { a, b, c })
}
pub fn distance(&self, p: Point) -> f32 {
self.a * p.x + self.b * p.y + self.c
}
}
pub fn fit_line_least_squares(points: &[Point]) -> Option<Line> {
if points.len() < 2 {
return None;
}
let n = points.len() as f64;
let mut mx = 0.0f64;
let mut my = 0.0f64;
for p in points {
mx += f64::from(p.x);
my += f64::from(p.y);
}
mx /= n;
my /= n;
let mut sxx = 0.0f64;
let mut syy = 0.0f64;
let mut sxy = 0.0f64;
for p in points {
let dx = f64::from(p.x) - mx;
let dy = f64::from(p.y) - my;
sxx += dx * dx;
syy += dy * dy;
sxy += dx * dy;
}
if sxx + syy < 1e-12 {
return None; }
let diff = sxx - syy;
let disc = (diff * diff + 4.0 * sxy * sxy).sqrt();
let lambda_min = 0.5 * (sxx + syy - disc);
let (mut a, mut b) = if (sxx - lambda_min).abs() > (syy - lambda_min).abs() {
(-sxy, sxx - lambda_min)
} else {
(syy - lambda_min, -sxy)
};
let len = (a * a + b * b).sqrt();
if len < 1e-12 {
a = 1.0;
b = 0.0;
} else {
a /= len;
b /= len;
}
let c = -(a * mx + b * my);
Some(Line {
a: a as f32,
b: b as f32,
c: c as f32,
})
}
pub fn ransac_line(
points: &[Point],
iterations: usize,
threshold: f32,
rng: &mut Prng,
) -> Option<(Line, Vec<usize>)> {
if points.len() < 2 {
return None;
}
let thr = threshold.abs();
let mut best_line: Option<Line> = None;
let mut best_inliers: Vec<usize> = Vec::new();
for _ in 0..iterations.max(1) {
let i = rng.below(points.len());
let mut j = rng.below(points.len());
if j == i {
j = (j + 1) % points.len();
}
let Some(candidate) = Line::from_points(points[i], points[j]) else {
continue;
};
let inliers: Vec<usize> = points
.iter()
.enumerate()
.filter(|&(_, &p)| candidate.distance(p).abs() <= thr)
.map(|(idx, _)| idx)
.collect();
if inliers.len() > best_inliers.len() {
best_inliers = inliers;
best_line = Some(candidate);
}
}
let line = best_line?;
let inlier_pts: Vec<Point> = best_inliers.iter().map(|&idx| points[idx]).collect();
let refined = fit_line_least_squares(&inlier_pts).unwrap_or(line);
Some((refined, best_inliers))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_through_two_points() {
let l = Line::from_points(Point::new(0.0, 0.0), Point::new(1.0, 1.0)).unwrap();
assert!(l.distance(Point::new(2.0, 2.0)).abs() < 1e-5);
assert!((l.distance(Point::new(1.0, 0.0)).abs() - (0.5f32).sqrt()).abs() < 1e-5);
}
#[test]
fn coincident_points_have_no_line() {
assert!(Line::from_points(Point::new(3.0, 3.0), Point::new(3.0, 3.0)).is_none());
}
#[test]
fn least_squares_recovers_horizontal() {
let pts: Vec<Point> = (0..10).map(|i| Point::new(i as f32, 5.0)).collect();
let l = fit_line_least_squares(&pts).unwrap();
for p in &pts {
assert!(l.distance(*p).abs() < 1e-4);
}
assert!(l.a.abs() < 1e-4);
assert!((l.b.abs() - 1.0).abs() < 1e-4);
}
#[test]
fn least_squares_recovers_vertical() {
let pts: Vec<Point> = (0..10).map(|i| Point::new(7.0, i as f32)).collect();
let l = fit_line_least_squares(&pts).unwrap();
for p in &pts {
assert!(l.distance(*p).abs() < 1e-4);
}
assert!(l.b.abs() < 1e-4);
assert!((l.a.abs() - 1.0).abs() < 1e-4);
}
#[test]
fn least_squares_recovers_diagonal_with_noise() {
let pts: Vec<Point> = (0..20)
.map(|i| {
let x = i as f32;
let noise = if i % 2 == 0 { 0.05 } else { -0.05 };
Point::new(x, 2.0 * x + 1.0 + noise)
})
.collect();
let l = fit_line_least_squares(&pts).unwrap();
for p in &pts {
assert!(l.distance(*p).abs() < 0.1);
}
let slope = -l.a / l.b;
assert!((slope - 2.0).abs() < 0.05, "slope {slope}");
}
#[test]
fn ransac_ignores_outliers() {
let mut pts: Vec<Point> = (0..30).map(|i| Point::new(i as f32, 3.0)).collect();
for i in 0..10 {
pts.push(Point::new(i as f32 * 2.0, 40.0 + i as f32));
}
let mut rng = Prng::new(12345);
let (line, inliers) = ransac_line(&pts, 200, 0.5, &mut rng).unwrap();
assert_eq!(inliers.len(), 30);
for &idx in &inliers {
assert!(line.distance(pts[idx]).abs() < 0.5);
}
}
#[test]
fn ransac_is_reproducible() {
let pts: Vec<Point> = (0..40)
.map(|i| Point::new(i as f32, 0.5 * i as f32))
.collect();
let mut a = Prng::new(777);
let mut b = Prng::new(777);
let ra = ransac_line(&pts, 50, 0.25, &mut a).unwrap();
let rb = ransac_line(&pts, 50, 0.25, &mut b).unwrap();
assert_eq!(ra.1, rb.1);
assert_eq!(ra.0, rb.0);
}
}