app
AI Flow: Reverse Engineering
Public Made by Adomby adom
Photos of a PCB to a tagged KiCad copy: parts placed, copper and nets with their evidence, a schematic, a measured confidence score, bench tests and a video of the run.
main
Caleb
Add the Rust source, flow definition, assets and install scripts
cf9809b
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
//! Geometry: robust line fits, the photo-to-board homography, convex hulls, minimum-area rectangles and
//! polygon simplification.
pub type P = (f64, f64);
/// Fit `u = a * t + b` to (t, u) pairs, dropping outliers (residual over 3 x the median absolute
/// residual, floor `min_tol`) and refitting. Used for board edges, where a connector can hang over.
pub fn robust_line(pts: &[(f64, f64)], min_tol: f64) -> Option<(f64, f64)> {
let fit = |p: &[(f64, f64)]| -> Option<(f64, f64)> {
let n = p.len() as f64; if n < 2.0 { return None; }
let (st, su) = p.iter().fold((0.0, 0.0), |a, q| (a.0 + q.0, a.1 + q.1));
let (mt, mu) = (st / n, su / n);
let (mut sxx, mut sxy) = (0.0, 0.0);
for q in p { sxx += (q.0 - mt) * (q.0 - mt); sxy += (q.0 - mt) * (q.1 - mu); }
if sxx < 1e-12 { return None; }
let a = sxy / sxx; Some((a, mu - a * mt))
};
// consensus first (RANSAC over deterministic pairs): the edge with the most points within tolerance,
// so a long overhanging connector cannot drag the fit; then refine on those inliers
let mut cur: Vec<(f64, f64)> = pts.to_vec();
if pts.len() >= 10 {
let mut seed = 0x9e3779b97f4a7c15u64;
let mut rnd = |n: usize| { seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17; (seed % n as u64) as usize };
let mut best: (usize, f64, f64) = (0, 0.0, 0.0);
for _ in 0..400 {
let (i, j) = (rnd(pts.len()), rnd(pts.len()));
let (p, q) = (pts[i], pts[j]);
if (q.0 - p.0).abs() < 1e-9 || (q.0 - p.0).abs() < (pts.len() as f64).sqrt() { continue; }
let a = (q.1 - p.1) / (q.0 - p.0); let b = p.1 - a * p.0;
let n = pts.iter().filter(|r| (r.1 - (a * r.0 + b)).abs() <= min_tol).count();
if n > best.0 { best = (n, a, b); }
}
if best.0 >= 2 { cur = pts.iter().filter(|r| (r.1 - (best.1 * r.0 + best.2)).abs() <= min_tol * 1.5).copied().collect(); }
}
let mut line = fit(&cur)?;
for _ in 0..6 {
let mut res: Vec<f64> = cur.iter().map(|q| (q.1 - (line.0 * q.0 + line.1)).abs()).collect();
let mut s = res.clone(); s.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mad = s[s.len() / 2];
let tol = (3.0 * mad).max(min_tol);
let keep: Vec<(f64, f64)> = cur.iter().zip(res.drain(..)).filter(|(_, r)| *r <= tol).map(|(q, _)| *q).collect();
if keep.len() == cur.len() || keep.len() < 2 { break; }
cur = keep; line = fit(&cur)?;
}
Some(line)
}
/// 3x3 homography (row-major, h[8] = 1) mapping src[i] -> dst[i] for four point pairs.
pub fn homography(src: &[P; 4], dst: &[P; 4]) -> Option<[f64; 9]> {
let mut a = [[0f64; 9]; 8];
for i in 0..4 {
let ((x, y), (u, v)) = (src[i], dst[i]);
a[2 * i] = [x, y, 1.0, 0.0, 0.0, 0.0, -u * x, -u * y, u];
a[2 * i + 1] = [0.0, 0.0, 0.0, x, y, 1.0, -v * x, -v * y, v];
}
// Gaussian elimination with partial pivoting on the 8x8 system
for c in 0..8 {
let p = (c..8).max_by(|i, j| a[*i][c].abs().partial_cmp(&a[*j][c].abs()).unwrap())?;
if a[p][c].abs() < 1e-12 { return None; }
a.swap(c, p);
for r in 0..8 {
if r == c { continue; }
let f = a[r][c] / a[c][c];
for k in c..9 { a[r][k] -= f * a[c][k]; }
}
}
let mut h = [0f64; 9];
for i in 0..8 { h[i] = a[i][8] / a[i][i]; }
h[8] = 1.0;
Some(h)
}
pub fn apply(h: &[f64; 9], p: P) -> P {
let w = h[6] * p.0 + h[7] * p.1 + h[8];
((h[0] * p.0 + h[1] * p.1 + h[2]) / w, (h[3] * p.0 + h[4] * p.1 + h[5]) / w)
}
pub fn invert(h: &[f64; 9]) -> Option<[f64; 9]> {
let m = h;
let det = m[0] * (m[4] * m[8] - m[5] * m[7]) - m[1] * (m[3] * m[8] - m[5] * m[6]) + m[2] * (m[3] * m[7] - m[4] * m[6]);
if det.abs() < 1e-15 { return None; }
let inv = [
(m[4] * m[8] - m[5] * m[7]) / det, (m[2] * m[7] - m[1] * m[8]) / det, (m[1] * m[5] - m[2] * m[4]) / det,
(m[5] * m[6] - m[3] * m[8]) / det, (m[0] * m[8] - m[2] * m[6]) / det, (m[2] * m[3] - m[0] * m[5]) / det,
(m[3] * m[7] - m[4] * m[6]) / det, (m[1] * m[6] - m[0] * m[7]) / det, (m[0] * m[4] - m[1] * m[3]) / det,
];
Some(inv.map(|v| v / inv[8]))
}
/// Intersection of x = a1*y + b1 (a near-vertical edge) and y = a2*x + b2 (a near-horizontal edge).
pub fn cross_vh(v: (f64, f64), hz: (f64, f64)) -> P {
// x = a1*(a2*x + b2) + b1 => x (1 - a1 a2) = a1 b2 + b1
let x = (v.0 * hz.1 + v.1) / (1.0 - v.0 * hz.0);
(x, hz.0 * x + hz.1)
}
pub fn dist(a: P, b: P) -> f64 { ((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt() }
/// Convex hull (monotone chain), counter-clockwise in y-down coordinates' mirror; order is consistent.
pub fn hull(pts: &[P]) -> Vec<P> {
let mut p: Vec<P> = pts.to_vec();
p.sort_by(|a, b| a.partial_cmp(b).unwrap());
p.dedup();
if p.len() < 3 { return p; }
let cross = |o: P, a: P, b: P| (a.0 - o.0) * (b.1 - o.1) - (a.1 - o.1) * (b.0 - o.0);
let mut lo: Vec<P> = vec![];
for q in &p { while lo.len() >= 2 && cross(lo[lo.len() - 2], lo[lo.len() - 1], *q) <= 0.0 { lo.pop(); } lo.push(*q); }
let mut up: Vec<P> = vec![];
for q in p.iter().rev() { while up.len() >= 2 && cross(up[up.len() - 2], up[up.len() - 1], *q) <= 0.0 { up.pop(); } up.push(*q); }
lo.pop(); up.pop(); lo.extend(up); lo
}
/// Minimum-area enclosing rectangle: (centre, long side, short side, angle of the long side in degrees, 0..180).
pub fn min_rect(pts: &[P]) -> (P, f64, f64, f64) {
let h = hull(pts);
if h.len() < 3 { let c = h.first().copied().unwrap_or((0.0, 0.0)); return (c, 0.0, 0.0, 0.0); }
let mut best = (f64::MAX, (0.0, 0.0), 0.0, 0.0, 0.0);
for i in 0..h.len() {
let (a, b) = (h[i], h[(i + 1) % h.len()]);
let ang = (b.1 - a.1).atan2(b.0 - a.0);
let (c, s) = (ang.cos(), ang.sin());
let (mut x0, mut x1, mut y0, mut y1) = (f64::MAX, f64::MIN, f64::MAX, f64::MIN);
for q in &h { let u = q.0 * c + q.1 * s; let v = -q.0 * s + q.1 * c; x0 = x0.min(u); x1 = x1.max(u); y0 = y0.min(v); y1 = y1.max(v); }
let area = (x1 - x0) * (y1 - y0);
if area < best.0 {
let (cu, cv) = ((x0 + x1) / 2.0, (y0 + y1) / 2.0);
let centre = (cu * c - cv * s, cu * s + cv * c);
let (w, hh) = (x1 - x0, y1 - y0);
let (long, short, a) = if w >= hh { (w, hh, ang) } else { (hh, w, ang + std::f64::consts::FRAC_PI_2) };
best = (area, centre, long, short, a.to_degrees().rem_euclid(180.0));
}
}
(best.1, best.2, best.3, best.4)
}
/// Douglas-Peucker simplification of a closed ring.
pub fn simplify(ring: &[P], tol: f64) -> Vec<P> {
if ring.len() < 4 { return ring.to_vec(); }
fn rec(p: &[P], tol: f64, out: &mut Vec<P>) {
let (a, b) = (p[0], p[p.len() - 1]);
let (mut idx, mut dmax) = (0, 0.0);
let l = dist(a, b).max(1e-9);
for (i, q) in p.iter().enumerate().take(p.len() - 1).skip(1) {
let d = ((b.0 - a.0) * (a.1 - q.1) - (a.0 - q.0) * (b.1 - a.1)).abs() / l;
if d > dmax { dmax = d; idx = i; }
}
if dmax > tol { rec(&p[..=idx], tol, out); out.pop(); rec(&p[idx..], tol, out); } else { out.push(a); out.push(b); }
}
// split the ring at its farthest point from the start so both halves are open chains
let far = (1..ring.len()).max_by(|i, j| dist(ring[0], ring[*i]).partial_cmp(&dist(ring[0], ring[*j])).unwrap()).unwrap();
let mut a = vec![]; rec(&ring[..=far], tol, &mut a);
let mut second: Vec<P> = ring[far..].to_vec(); second.push(ring[0]);
let mut b = vec![]; rec(&second, tol, &mut b);
a.pop(); b.pop(); a.extend(b); a
}
pub fn polygon_area(p: &[P]) -> f64 {
let mut s = 0.0;
for i in 0..p.len() { let (a, b) = (p[i], p[(i + 1) % p.len()]); s += a.0 * b.1 - b.0 * a.1; }
s.abs() / 2.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn homography_round_trip() {
let src = [(10.0, 12.0), (410.0, 5.0), (420.0, 330.0), (3.0, 320.0)];
let dst = [(0.0, 0.0), (400.0, 0.0), (400.0, 320.0), (0.0, 320.0)];
let h = homography(&src, &dst).unwrap();
for i in 0..4 { let q = apply(&h, src[i]); assert!(dist(q, dst[i]) < 1e-6, "{q:?}"); }
let hi = invert(&h).unwrap();
assert!(dist(apply(&hi, (200.0, 160.0)), apply(&hi, apply(&h, apply(&hi, (200.0, 160.0))))) < 1e-6);
}
#[test]
fn rect_and_line() {
// a 30 x 10 rectangle turned 30 degrees
let (c, s) = (30f64.to_radians().cos(), 30f64.to_radians().sin());
let mut pts = vec![];
for i in 0..=30 { for j in 0..=10 { let (x, y) = (i as f64 - 15.0, j as f64 - 5.0); pts.push((50.0 + x * c - y * s, 40.0 + x * s + y * c)); } }
let (ctr, l, w, a) = min_rect(&pts);
assert!((l - 30.0).abs() < 0.01 && (w - 10.0).abs() < 0.01 && (a - 30.0).abs() < 0.01 && dist(ctr, (50.0, 40.0)) < 0.01);
let mut line: Vec<(f64, f64)> = (0..50).map(|t| (t as f64, 2.0 * t as f64 + 1.0)).collect();
line.push((25.0, 500.0)); // an overhanging connector
let (a, b) = robust_line(&line, 0.5).unwrap();
assert!((a - 2.0).abs() < 1e-6 && (b - 1.0).abs() < 1e-6);
let sq = vec![(0.0, 0.0), (5.0, 0.01), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
assert_eq!(simplify(&sq, 0.1).len(), 4);
assert_eq!(polygon_area(&simplify(&sq, 0.1)), 100.0);
}
}