> ## Documentation Index
> Fetch the complete documentation index at: https://aegean.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Random Sample Consensus (RANSAC)

> Fitting models from random minimal samples, with no starting estimate.

The robust methods of the [previous section](https://aegean.ai/aiml-common/lectures/robust-estimation/least-squares-outliers.md) all improve a fit they already have. They reweight the data around a current estimate, so they fail when that estimate is already pulled toward the outliers. Random sample consensus (RANSAC), introduced by Fischler and Bolles in 1981, needs no starting estimate. It fits the model many times, each time to a randomly chosen subset just large enough to determine it, and keeps the fit that the most data points agree with.

This section derives how many random subsets are enough, how to set the threshold that decides which points agree, and how to score and refine the winning fit. It applies RANSAC to a line and explains why it does not work for the 82-parameter grid model of the previous section.

The notation follows the previous section: data $(x^{(i)}, y^{(i)})$ for $i = 1, \ldots, m$, parameters $\boldsymbol\theta$, residuals $e_i = y^{(i)} - \boldsymbol\theta^\top\boldsymbol\phi(x^{(i)})$ and inlier noise variance $\sigma^2$. Three symbols are new. The inlier fraction is $\pi$, the mixing coefficient of the inlier component in the [EM](https://aegean.ai/aiml-common/lectures/mixture-of-gaussians/em-algorithm.md) inlier and outlier mixture. The size of a minimal sample, the number of data points that determine the model, is $s$. The number of random trials is $L$.

```python theme={null}
import numpy as np
from scipy.stats import chi2, norm
```

## Sample, fit, score

RANSAC repeats three steps $L$ times:

1. Draw a minimal sample of $s$ data points at random.
2. Fit the model to those points exactly. For a line, $s = 2$ points determine $\boldsymbol\theta$.
3. Score the fit on all the data. The basic score is the size of the *consensus set*, the number of points whose residual satisfies $|e_i| < \tau$ for a threshold $\tau$.

It keeps the fit with the largest consensus set and finally refits the model to that set by least squares. A sample that contains even one outlier gives a wrong line, which few points agree with. A sample of two inliers gives a line close to the true one, which all the inliers agree with. RANSAC only needs to draw one such sample.

The data below have the line $y = 1 + 0.5x$ with noise $\sigma = 0.3$, as before, but the 24 outliers among 60 points are not scattered. They lie along a second line at the right end of the range, where they have high leverage. Outliers that agree with each other are the hard case of the previous section.

```python theme={null}
def make_structured_data(n_in=36, n_out=24, sigma=0.3, seed=7):
    # inliers on y = 1 + 0.5 x; outliers on a second line over 6 <= x <= 10
    rng = np.random.default_rng(seed)
    x = np.r_[rng.uniform(0, 10, n_in), rng.uniform(6, 10, n_out)]
    y = np.r_[1 + 0.5 * x[:n_in], 10 - 0.6 * (x[n_in:] - 6)] + sigma * rng.standard_normal(n_in + n_out)
    return x, y, np.r_[np.zeros(n_in, bool), np.ones(n_out, bool)]


def design(x):
    return np.column_stack([np.ones_like(x), x])


def least_squares(Phi, y):
    return np.linalg.lstsq(Phi, y, rcond=None)[0]


sigma = 0.3
x_s, y_s, out_s = make_structured_data()
Phi_s = design(x_s)
```

The number of trials and the threshold are derived in the next two parts. The function below takes them as inputs. It also accepts a degeneracy test: two points with the same $x$ do not determine a line of the form $\theta_0 + \theta_1 x$, and the solver must reject such a sample instead of fitting it.

```python theme={null}
def ransac(m, s, fit_minimal, residuals, tau, L, rng, score="inliers", L_adaptive=None):
    # fit_minimal(idx) returns theta or None for a degenerate sample; residuals(theta) returns e for all points.
    # score: "inliers" (count), "msac" (truncated squared error) or a callable cost(e) to minimize.
    # L_adaptive(n_inliers) optionally returns an updated number of trials after each new best.
    best_theta, best_cost, best_inliers, trial, history = None, np.inf, None, 0, []
    while trial < L:
        trial += 1
        theta = fit_minimal(rng.choice(m, s, replace=False))
        if theta is None:
            continue
        e = residuals(theta)
        inliers = np.abs(e) < tau
        if score == "inliers":
            cost = -inliers.sum()
        elif score == "msac":
            cost = np.minimum(e**2, tau**2).sum()
        else:
            cost = score(e)
        if cost < best_cost:
            best_theta, best_cost, best_inliers = theta, cost, inliers
            if L_adaptive is not None:
                L = min(L, L_adaptive(inliers.sum()))
            history.append((trial, inliers.sum(), L))
    return best_theta, best_inliers, trial, history


def line_through(idx, x=None, y=None):
    # exact line through two points; None if the two x values coincide
    x1, x2 = x[idx]
    if abs(x2 - x1) < 1e-9:
        return None
    slope = (y[idx[1]] - y[idx[0]]) / (x2 - x1)
    return np.array([y[idx[0]] - slope * x1, slope])
```

```python theme={null}
rng = np.random.default_rng(0)
tau = sigma * np.sqrt(chi2.ppf(0.95, df=1))
theta_r, inl_r, trials_r, _ = ransac(
    len(x_s), 2, lambda idx: line_through(idx, x_s, y_s), lambda th: y_s - Phi_s @ th, tau, L=50, rng=rng
)
theta_refit = least_squares(Phi_s[inl_r], y_s[inl_r])
print(f"threshold tau = {tau:.3f}")
print(f"best minimal-sample line: theta = [{theta_r[0]:.2f}, {theta_r[1]:.3f}], consensus set {inl_r.sum()} points "
      f"({(inl_r & ~out_s).sum()} true inliers, {(inl_r & out_s).sum()} outliers)")
print(f"refit on the consensus set: theta = [{theta_refit[0]:.2f}, {theta_refit[1]:.3f}]")
th_ls = least_squares(Phi_s, y_s)
print(f"least squares on all points: theta = {np.round(th_ls, 3)}")
```

```output theme={null}
threshold tau = 0.588
best minimal-sample line: theta = [0.73, 0.529], consensus set 35 points (35 true inliers, 0 outliers)
refit on the consensus set: theta = [0.91, 0.513]
least squares on all points: theta = [0.697 0.795]
```

![Scatter of 60 points: inliers along a rising line and a cluster of outliers along a falling line at the right. The RANSAC line, with a shaded band of width tau on each side, follows the inliers; the dashed least squares line is tilted toward the outlier cluster.](https://mintcdn.com/aegeanaiinc/uADPagtO6esItILu/aiml-common/lectures/robust-estimation/ransac/images/cell_6_output_1.svg?fit=max\&auto=format\&n=uADPagtO6esItILu\&q=85\&s=e1b8fbfdec5150fd4e101419f1925f27)

*RANSAC on a line with 24 structured outliers among 60 points. The shaded band is the inlier threshold $\pm\tau$ around the refit line.*

For comparison, the code runs the methods of the previous section on the same data, each started from the least squares fit.

```python theme={null}
def weighted_least_squares(Phi, y, w):
    sw = np.sqrt(w)
    return np.linalg.lstsq(Phi * sw[:, None], y * sw, rcond=None)[0]


def irls(Phi, y, weight, theta, n_iter=100):
    for _ in range(n_iter):
        e = y - Phi @ theta
        theta = weighted_least_squares(Phi, y, weight(e / (1.4826 * np.median(np.abs(e - np.median(e))))))
    return theta


def em_inlier_outlier(Phi, y, theta, n_iter=200, pi=0.5):
    u = 1.0 / (y.max() - y.min())
    sigma2 = np.var(y - Phi @ theta)
    for _ in range(n_iter):
        inlier = pi * norm.pdf(y, Phi @ theta, np.sqrt(sigma2))
        gamma = inlier / (inlier + (1 - pi) * u)
        theta = weighted_least_squares(Phi, y, gamma)
        sigma2 = max(np.sum(gamma * (y - Phi @ theta) ** 2) / gamma.sum(), 1e-12)
        pi = gamma.mean()
    return theta, pi, gamma


huber = lambda a: np.minimum(1.0, 1.345 / np.maximum(np.abs(a), 1e-12))
cauchy = lambda a: 1.0 / (1.0 + (a / 2.385) ** 2)
theta_em_s, pi_em_s, _ = em_inlier_outlier(Phi_s, y_s, th_ls)
print(f"Huber IRLS:  theta = {np.round(irls(Phi_s, y_s, huber, th_ls), 3)}")
print(f"Cauchy IRLS: theta = {np.round(irls(Phi_s, y_s, cauchy, th_ls), 3)}")
print(f"EM mixture:  theta = {np.round(theta_em_s, 3)}, pi = {pi_em_s:.2f} (true inlier fraction {np.mean(~out_s):.2f})")
```

```output theme={null}
Huber IRLS:  theta = [0.578 0.788]
Cauchy IRLS: theta = [0.533 0.792]
EM mixture:  theta = [0.965 0.506], pi = 0.53 (true inlier fraction 0.60)
```

Least squares is tilted toward the outlier cluster, to a slope of 0.795, and the Huber and Cauchy fits stay near it. The outliers agree with each other, so once the fit leans toward them their residuals are moderate and their weights stay high. On this data EM does better: its slope is 0.506, although it puts the inlier fraction at 0.53 instead of 0.60. RANSAC needed no start, only one trial out of its 50 in which both points were inliers.

## How many trials

When all $s$ of its points are inliers, a minimal sample consists only of inliers. If the points are drawn independently and the inlier fraction is $\pi$, this happens with probability $\pi^s$. The probability that a trial fails to draw an all-inlier sample is $1 - \pi^s$, and the probability that all $L$ independent trials fail is $(1 - \pi^s)^L$. Requiring at least one all-inlier sample with probability $P$ means $(1 - \pi^s)^L \le 1 - P$, so

$$
L = \left\lceil \frac{\ln(1 - P)}{\ln(1 - \pi^s)} \right\rceil.
$$

$P$ is a confidence you choose, often 0.99 or 0.999, and every quoted number of trials depends on it. Drawing without replacement makes the true probability of an all-inlier sample slightly smaller than $\pi^s$; for $m$ much larger than $s$ the difference is negligible.

```python theme={null}
def trials_needed(pi, s, P):
    # smallest L with (1 - pi^s)^L <= 1 - P
    return int(np.ceil(np.log(1 - P) / np.log1p(-(pi**s))))


pis = [0.95, 0.9, 0.8, 0.7, 0.5, 0.3]
sizes = {2: "line", 3: "plane", 4: "homography", 5: "essential matrix (five-point)", 7: "fundamental matrix (seven-point)", 8: "fundamental matrix (eight-point)"}
for P in (0.99, 0.999):
    print(f"P = {P}")
    print(f"  {'s':>2}  {'model':32s}" + "".join(f"pi={p:<6}" for p in pis))
    for s, name in sizes.items():
        print(f"  {s:>2}  {name:32s}" + "".join(f"{trials_needed(p, s, P):<9d}" for p in pis))
```

```output theme={null}
P = 0.99
   s  model                           pi=0.95  pi=0.9   pi=0.8   pi=0.7   pi=0.5   pi=0.3   
   2  line                            2        3        5        7        17       49       
   3  plane                           3        4        7        11       35       169      
   4  homography                      3        5        9        17       72       567      
   5  essential matrix (five-point)   4        6        12       26       146      1893     
   7  fundamental matrix (seven-point)4        8        20       54       588      21055    
   8  fundamental matrix (eight-point)5        9        26       78       1177     70188    
P = 0.999
   s  model                           pi=0.95  pi=0.9   pi=0.8   pi=0.7   pi=0.5   pi=0.3   
   2  line                            3        5        7        11       25       74       
   3  plane                           4        6        10       17       52       253      
   4  homography                      5        7        14       26       108      850      
   5  essential matrix (five-point)   5        8        18       38       218      2840     
   7  fundamental matrix (seven-point)6        11       30       81       881      31583    
   8  fundamental matrix (eight-point)7        13       38       117      1765     105282
```

The number of trials grows quickly with $s$ when $\pi$ is low, because $\pi^s$ shrinks geometrically. At $P = 0.999$ and half the data wrong, the five-point solver for the essential matrix needs 218 trials and the eight-point solver 1,765. A model that can be determined from fewer points needs far fewer trials, which is the reason to prefer minimal solvers such as the five-point algorithm.

The MIT vision book (Foundations of Computer Vision, n.d.) gives the same formula (Torralba, Isola and Freeman, Section 41.3.2, Eq. 41.8) and works an example: a line, $s = 2$, with 2 outliers among 11 points, and confidence 0.95. It substitutes 0.18, the outlier fraction, for the fraction that is raised to the power $s$, and reports 91 trials. The formula needs the inlier fraction, $9/11 \approx 0.82$.

```python theme={null}
print(f"with 0.18, the outlier fraction, in place of pi: L = {trials_needed(0.18, 2, 0.95)}")
print(f"with 0.82, the inlier fraction:                 L = {trials_needed(0.82, 2, 0.95)}")
```

```output theme={null}
with 0.18, the outlier fraction, in place of pi: L = 91
with 0.82, the inlier fraction:                 L = 3
```

With the inlier fraction, the formula gives three trials. With 82% inliers, two random points are both inliers with probability $0.82^2 \approx 0.67$, and three independent trials all fail with probability about $0.33^3 \approx 0.035$, which is below $1 - 0.95$.

In practice $\pi$ is unknown. RANSAC can start with a pessimistic value and update $L$ whenever it finds a larger consensus set, using that set's size divided by $m$ as the estimate of $\pi$. The run below starts with $L = 1000$ on the structured line data and records each update.

```python theme={null}
rng = np.random.default_rng(1)
_, inl_a, trials_a, history = ransac(
    len(x_s), 2, lambda idx: line_through(idx, x_s, y_s), lambda th: y_s - Phi_s @ th, tau, L=1000, rng=rng,
    L_adaptive=lambda n: trials_needed(n / len(x_s), 2, 0.99),
)
for trial, n_inl, L_now in history:
    print(f"trial {trial:3d}: new best consensus set of {n_inl:2d} points, estimated pi = {n_inl / len(x_s):.2f}, L becomes {L_now}")
print(f"stopped after {trials_a} trials")
```

```output theme={null}
trial   1: new best consensus set of 33 points, estimated pi = 0.55, L becomes 13
stopped after 13 trials
```

## The threshold as a chi-squared gate

The threshold $\tau$ should accept a known fraction of the inliers. Under the inlier model the residual is Gaussian, $e_i \sim \mathcal N(0, \sigma^2)$, so $e_i^2/\sigma^2$ follows a $\chi^2$ distribution with one degree of freedom. When the residual is a vector of dimension $d$ with independent components of variance $\sigma^2$, such as the 2D reprojection error of a homography, $\lVert \mathbf e_i \rVert^2/\sigma^2$ follows $\chi^2_d$. To accept an inlier with probability $1 - \alpha$, set

$$
\tau = \sigma\sqrt{\chi^2_d(1 - \alpha)},
$$

where $\chi^2_d(1 - \alpha)$ is the $1 - \alpha$ quantile. The threshold then follows from the same noise model as the least squares fit. A larger $\tau$ accepts more inliers and also lets more outliers fall inside the band.

```python theme={null}
for d in (1, 2, 3):
    print(f"d = {d}: tau = {np.sqrt(chi2.ppf(0.95, d)):.3f} sigma at 95%,  {np.sqrt(chi2.ppf(0.99, d)):.3f} sigma at 99%")
e_true = (y_s - Phi_s @ np.array([1.0, 0.5]))[~out_s]
print(f"fraction of the true inliers within tau = 1.96 sigma of the true line: {np.mean(np.abs(e_true) < tau):.3f}")
```

```output theme={null}
d = 1: tau = 1.960 sigma at 95%,  2.576 sigma at 99%
d = 2: tau = 2.448 sigma at 95%,  3.035 sigma at 99%
d = 3: tau = 2.795 sigma at 95%,  3.368 sigma at 99%
fraction of the true inliers within tau = 1.96 sigma of the true line: 0.972
```

## Scoring: MSAC and MLESAC

Counting inliers treats every point inside the band alike and every point outside alike. Two refinements by Torr and Zisserman (2000) score hypotheses more finely. MSAC charges each point its squared residual, capped at $\tau^2$:

$$
C_{\text{MSAC}}(\boldsymbol\theta) = \sum_{i=1}^{m} \min(e_i^2, \tau^2).
$$

Among hypotheses with the same consensus set it prefers the one that fits the inliers better. MLESAC goes further and scores each hypothesis by the likelihood of the inlier and outlier mixture of the EM section,

$$
-\sum_{i=1}^{m} \ln\Big\{\pi\, \mathcal N(e_i \mid 0, \sigma^2) + (1 - \pi)\,\mathcal U\Big\},
$$

with $\sigma$ known and $\pi$ estimated for each hypothesis by a few EM steps on $\pi$ alone: compute the responsibilities $\gamma(z_i)$, set $\pi$ to their mean, repeat. Counting inliers is the hard version of this: the test $|e_i| < \tau$ replaces the responsibility $\gamma(z_i)$ by 0 or 1, as the k-means indicator $r_{ik}$ replaces the responsibilities $\gamma(z_{ik})$ of a Gaussian mixture.

The experiment below compares the three scores on 300 data sets with the same line, 60 points and half of them uniform outliers. Each run uses 30 trials, and the code records the slope error of the best hypothesis before any refit.

```python theme={null}
def mlesac_cost(e, sigma, u, n_em=5):
    # negative log likelihood of the Gaussian plus uniform mixture, pi estimated by a few EM steps
    pi = 0.5
    for _ in range(n_em):
        g_in = pi * norm.pdf(e, 0, sigma)
        pi = np.mean(g_in / (g_in + (1 - pi) * u))
    return -np.sum(np.log(pi * norm.pdf(e, 0, sigma) + (1 - pi) * u))


def make_uniform_outlier_data(rng, m=60, n_out=30, sigma=0.3):
    x = rng.uniform(0, 10, m)
    y = 1.0 + 0.5 * x + sigma * rng.standard_normal(m)
    y[:n_out] = rng.uniform(-4, 12, n_out)
    return x, y


errs = {"inlier count": [], "MSAC": [], "MLESAC": []}
for rep in range(300):
    data_rng = np.random.default_rng(1000 + rep)
    x, y = make_uniform_outlier_data(data_rng)
    Phi = design(x)
    u = 1 / (y.max() - y.min())
    for name, score in [("inlier count", "inliers"), ("MSAC", "msac"), ("MLESAC", lambda e: mlesac_cost(e, sigma, u))]:
        th, *_ = ransac(len(x), 2, lambda idx: line_through(idx, x, y), lambda th: y - Phi @ th, tau, L=30,
                        rng=np.random.default_rng(rep), score=score)
        errs[name].append(abs(th[1] - 0.5))
for name, e in errs.items():
    e = np.array(e)
    print(f"{name:13s} median slope error {np.median(e):.4f}, 90th percentile {np.percentile(e, 90):.4f}")
```

```output theme={null}
inlier count  median slope error 0.0327, 90th percentile 0.0756
MSAC          median slope error 0.0296, 90th percentile 0.0720
MLESAC        median slope error 0.0310, 90th percentile 0.0704
```

MSAC and MLESAC reduce the median slope error from 0.033 to 0.030 and 0.031. The gain is small here because the line has only two parameters and the refit that follows removes most of the difference. The scores matter more when the threshold is loose, so that many hypotheses tie on the count.

## Refinement

The winning hypothesis was fitted to only $s$ points, so its accuracy is that of $s$ noisy measurements. The final step refits the model to the whole consensus set by least squares, which uses all the inliers. Because the refit line can change which points fall inside the band, the refit and the inlier test can be repeated until the consensus set stops changing. Starting EM on the inlier and outlier mixture from the RANSAC fit goes one step further, and replaces the hard consensus set by responsibilities. It gives EM the uncontaminated start that the grid example of the previous section lacked.

```python theme={null}
theta_rs = theta_r
for it in range(10):
    inl = np.abs(y_s - Phi_s @ theta_rs) < tau
    theta_new = least_squares(Phi_s[inl], y_s[inl])
    if np.allclose(theta_new, theta_rs):
        break
    theta_rs = theta_new
theta_em_r, pi_em_r, gamma_r = em_inlier_outlier(Phi_s, y_s, theta_rs, pi=inl.mean())
print(f"minimal sample:            theta = {np.round(theta_r, 3)}")
print(f"iterated refit ({it} steps):  theta = {np.round(theta_rs, 3)}, {inl.sum()} inliers")
print(f"EM from the RANSAC fit:    theta = {np.round(theta_em_r, 3)}, pi = {pi_em_r:.2f}")
print(f"least squares, true inliers only: theta = {np.round(least_squares(Phi_s[~out_s], y_s[~out_s]), 3)}")
```

```output theme={null}
minimal sample:            theta = [0.733 0.529]
iterated refit (2 steps):  theta = [0.949 0.508], 36 inliers
EM from the RANSAC fit:    theta = [0.965 0.506], pi = 0.53
least squares, true inliers only: theta = [0.949 0.508]
```

Most modern variants change one of the steps above. PROSAC draws samples from the best-matching data first instead of uniformly. LO-RANSAC runs a local optimization, such as the refit above, inside the loop whenever a new best hypothesis appears. MAGSAC++ removes the single threshold by marginalizing the score over a range of noise levels.

## Where RANSAC stops working

The grid model of the previous section has 82 columns. A minimal sample would be $s = 82$ hours, and the probability that 82 random hours are all inliers is $\pi^{82}$. The code compares the number of trials with that of a line, at $P = 0.99$.

```python theme={null}
for pi in (0.95, 0.9, 0.8):
    print(f"pi = {pi}: line (s = 2) L = {trials_needed(pi, 2, 0.99)},  grid model (s = 82) L = {trials_needed(pi, 82, 0.99):,}"
          f",  s = 79: L = {trials_needed(pi, 79, 0.99):,}")
```

```output theme={null}
pi = 0.95: line (s = 2) L = 2,  grid model (s = 82) L = 307,  s = 79: L = 263
pi = 0.9: line (s = 2) L = 3,  grid model (s = 82) L = 26,021,  s = 79: L = 18,969
pi = 0.8: line (s = 2) L = 5,  grid model (s = 82) L = 407,255,304,  s = 79: L = 208,514,715
```

At 95% inliers 82 hours already need 307 trials, at 90% about 26,000, and at 80% about $4 \times 10^8$. The storm of the previous section made about 10% of the hours outliers.

A second problem is that a minimal sample must determine the model, and almost no sample of 82 hours does. Each hour of the day has its own indicator and two interaction columns, so the sample needs at least three hours at every hour of the day. Three of the 82 columns are also linear combinations of the others (the 24 hour indicators and the 7 day indicators both sum to one, and the hour-by-temperature columns sum to the temperature columns), so the design has rank 79 and no sample can determine all 82 parameters. The code builds the same 82 columns for every hour from 1 December to 19 February, with temperatures drawn at random, and counts how often a random sample of 82 hours reaches the rank of the full design.

```python theme={null}
def grid_design_columns(hour, dow, temp_f):
    T = (temp_f - 60.0) / 20.0
    H = np.eye(24)[hour]
    D = np.eye(7)[dow]
    return np.column_stack([T, T**2, T**3, H, D, H * T[:, None], H * (T**2)[:, None]])


hours = np.arange(80 * 24)  # 1 December 2020 (a Tuesday) to 19 February 2021
rng = np.random.default_rng(0)
Phi_cal = grid_design_columns(hours % 24, (1 + hours // 24) % 7, rng.uniform(25, 75, len(hours)))
full_rank = np.linalg.matrix_rank(Phi_cal)
n_draws = 20000
full = every_hour_3 = 0
for _ in range(n_draws):
    idx = rng.choice(len(hours), 82, replace=False)
    full += np.linalg.matrix_rank(Phi_cal[idx]) == full_rank
    every_hour_3 += np.bincount(hours[idx] % 24, minlength=24).min() >= 3
print(f"rank of the full design: {full_rank} of {Phi_cal.shape[1]} columns")
print(f"random 82-hour samples with at least 3 hours at every hour of the day: {every_hour_3} of {n_draws}")
print(f"random 82-hour samples that reach rank {full_rank}: {full} of {n_draws}")
```

```output theme={null}
rank of the full design: 79 of 82 columns
random 82-hour samples with at least 3 hours at every hour of the day: 0 of 20000
random 82-hour samples that reach rank 79: 0 of 20000
```

None of the 20,000 samples had three hours at every hour of the day, and none reached the rank of the full design. RANSAC needs a minimal sample that is both all inliers and well posed, and for this model neither is available. Minimal samples work for models with few parameters, such as lines, planes, homographies, essential matrices and poses. For a regression with many parameters, the reweighting methods of the previous section are the practical choice, started from a fit you have reason to trust.

The [next section](https://aegean.ai/aiml-common/lectures/robust-estimation/ransac-applications.md) applies RANSAC to a plane in a point cloud, to the homography of an earlier section, and to the registration of point clouds together with ICP.

**Key references**: (Hartley & Zisserman, 2004; Foundations of Computer Vision, n.d.; Bishop, n.d.)

## References

* Hartley, R., Zisserman, A. (2004). *Multiple View Geometry in Computer Vision*.
* (n.d.). *Foundations of Computer Vision*.
* Bishop (n.d.). *Pattern Recognition and Machine Learning*.

***

Found an error on this page? [Report an issue](https://github.com/aegean-ai/eaia-feedback/issues/new?template=page-issue.yml\&page=/aiml-common/lectures/robust-estimation/ransac).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.