RANSAC (Random Sample Consensus) 개념 및 실습

RANSAC 이란

RANSAC(Random Sample Consensus)는 데이터를 최소한의 random sample로 모델을 fitting하여, 전체 데이터에서 가장 적절한 모델을 반복하여, 찾는 알고리즘이다.

RANSAC의 필요성

  • outlier가 많은 상황에서도 안정적인 파라미터 추정이 가능하다.
  • 구현이 단순하고 응용이 쉽다.
  • 선형/다항/비선형 등 다양한 모델과 결합 가능하다.

RANSAC 개념

데이터에서 최소표본 크기 $s$개를 무작위로 뽑아 모델 $\theta$를 추정하고, 에러 $e(x,\theta)$가 임곗값 $\tau$ 이하인 점들의 집합 $\mathcal I(\theta)$ 크기를 평가한다. 가장 큰 $\lvert \mathcal I \rvert$를 주는 $\theta^\ast$를 기록하면서 반복하고, 마지막에 $\mathcal I(\theta^\ast)$만으로 재추정(refit)해 정밀도를 높인다.

RANSAC의 파라미터 셋팅법

성공확률과 반복횟수

inlier 비율을 $\eta$, 최소표본 크기를 $s$, 목표 신뢰도 $p$라 하면, 필요한 반복 횟수 $N$는

\[\begin{aligned} N= \frac{\log(1-p)}{\log(1-\eta^s)} \end{aligned}\]

$\eta$를 모르면 보수적으로 0.5부터 가정하고, 반복 중 $\hat\eta = \lvert \mathcal I \rvert/\lvert \mathcal D \rvert$로 $N$을 동적 갱신하면 평균 연산량을 크게 줄일 수 있다.

임곗값

센서/피처 잔차의 표준편차 $\sigma$가 알려졌다면 보통 $\tau \approx k\sigma$ ($k=2\sim3$). 픽셀/미터 등 단위가 섞이는 문제를 피하려면 기하적으로 의미 있는 에러(예: 수선거리, 리프로젝션 에러, Sampson 에러)를 사용한다.

최소표본 크기 s

가능한 “최소”가 유리하다. 선형 s=2s=2, 2차 다항식 s=3s=3, 평면 s=3s=3등. 단, 퇴화(degeneracy)를 피하기 위한 샘플 간격/기하 제약을 두는 것이 좋다.

Early Stop 사용법

  • min_iterations: 너무 이르게 종료하지 않도록 하는 최소 반복횟수
  • max_iterations: $N$ 공식을 기반으로 설정.
  • stop_inlier_ratio: $\lvert \mathcal I \rvert/\lvert \mathcal D \rvert$ 가 이 비율을 넘으면 즉시 종료. 실전에서는 min을 작게, stop 비율을 보수적으로 두고, 좋은 모델이 나오면 즉시 끝내는 것이 빠르고 효과적이다.

RANSAC의 장단점

장점

  • 이상치에 매우 강건.
  • 구현과 이식이 쉬움.

단점/주의

  • 난수성으로 재현성 관리가 필요(시드 고정).
  • $\tau$에 민감. 너무 작으면 inlier 과소평가, 너무 크면 outlier 유입.
  • 특정 구조적 outlier(편향 분포)가 많으면 그 분포를 적합해버릴 수 있음 → 사전 필터링/검정 필요.

RANSAC Python Code (다항식 예제)

import numpy as np
from numpy.typing import ArrayLike
from dataclasses import dataclass
from typing import Tuple, Optional

@dataclass
class RansacResult:
    coeffs: np.ndarray          # np.polyfit 계수 (고차항이 앞)
    inlier_idx: np.ndarray      # inlier 인덱스 (bool mask 또는 정수 배열)
    tau: float                  # 사용한 임곗값
    iterations: int             # 실제 반복 횟수
    inlier_ratio: float         # 최종 inlier 비율

def _poly_fit_safe(x: np.ndarray, y: np.ndarray, deg: int) -> Optional[np.ndarray]:
    try:
        return np.polyfit(x, y, deg)
    except np.linalg.LinAlgError:
        return None

def ransac_polyfit(
    x: ArrayLike,
    y: ArrayLike,
    deg: int,
    tau: float,
    p: float = 0.99,
    min_iterations: int = 0,
    max_iterations: Optional[int] = None,
    stop_inlier_ratio: Optional[float] = None,
    rng: Optional[np.random.Generator] = np.random.default_rng(0),
) -> RansacResult:
    """
    RANSAC으로 1D 다항식을 추정한다. error는 |y - polyval(c, x)|.

    Parameters
    ----------
    deg : 다항 차수 (선형=1, 이차=2, …)
    tau : inlier 임곗값
    p   : 원하는 신뢰도
    min_iterations, max_iterations, stop_inlier_ratio : Early stop 제어
    """
    x = np.asarray(x).ravel()
    y = np.asarray(y).ravel()
    n = x.size
    s = deg + 1                       # 최소표본 크기

    # 초기 inlier 비율 가정으로 N 계산
    if max_iterations is None:
        eta_guess = 0.5
        max_iterations = int(np.ceil(np.log(1 - p) / np.log(1 - eta_guess**s)))
        max_iterations = max(max_iterations, 100)

    best_coeffs = None
    best_inliers = None
    best_count = -1
    N = max_iterations

    for it in range(N):
        # 무작위 최소 샘플
        idx = rng.choice(n, size=s, replace=False)
        coeffs = _poly_fit_safe(x[idx], y[idx], deg)
        if coeffs is None:
            continue

        y_pred = np.polyval(coeffs, x)
        resid = np.abs(y - y_pred)
        inliers = resid <= tau
        count = int(inliers.sum())

        # 최고 기록 갱신
        if count > best_count:
            best_count = count
            best_inliers = inliers
            best_coeffs = coeffs

            # 동적 N 갱신
            eta_hat = np.clip(best_count / n, 1e-6, 0.999)
            N_new = int(np.ceil(np.log(1 - p) / np.log(1 - eta_hat**s)))
            N = min(N, max(N_new, it + 1))

            # early stop
            if it + 1 >= max(min_iterations, 1) and stop_inlier_ratio is not None:
                if eta_hat >= stop_inlier_ratio:
                    break

    # 리피팅(정제): inlier만으로 재적합
    if best_inliers is None or best_count < s:
        raise RuntimeError("유효한 모델을 찾지 못했습니다.")
    coeffs_refined = np.polyfit(x[best_inliers], y[best_inliers], deg)
    inlier_ratio = float(best_inliers.mean())
    return RansacResult(coeffs_refined, np.where(best_inliers)[0], tau, it + 1, inlier_ratio)

사용 예시

# 예제 데이터
rng = np.random.default_rng(42)
X = np.linspace(-10, 10, 200)
Y_true = 2*X + 5
Y = Y_true + rng.normal(0, 1.0, size=X.size)
# outlier 추가
out_idx = rng.choice(X.size, size=40, replace=False)
Y[out_idx] += rng.normal(0, 15.0, size=out_idx.size)

res = ransac_polyfit(
    X, Y, deg=1, tau=3.0, p=0.99,
    stop_inlier_ratio=0.7, rng=rng
)
print(res.coeffs, res.inlier_ratio, res.iterations)
---
[1.99085194 4.9955631 ] 0.82 1

시각화

import matplotlib.pyplot as plt

plt.figure(figsize=(10, 6))
plt.scatter(X, Y, label='Original Data', alpha=0.6)
plt.plot(X, Y_true, 'r--', label='True Line')

# Plot the RANSAC fitted line
X_fit = np.linspace(X.min(), X.max(), 100)
Y_fit = np.polyval(res.coeffs, X_fit)
plt.plot(X_fit, Y_fit, 'g-', label='RANSAC Fitted Line')

# Highlight inliers
plt.scatter(X[res.inlier_idx], Y[res.inlier_idx], color='cyan', label='RANSAC Inliers')

plt.xlabel('X')
plt.ylabel('Y')
plt.title('RANSAC Polynomial Fit')
plt.legend()
plt.grid(True)
plt.show()

Lo-RANSAC 개념

기본 RANSAC은 “전 데이터”에서만 반복 샘플링한다. LO-RANSAC(Locally Optimized RANSAC)은 최고 모델이 갱신될 때마다 그 inlier 집합 안에서 한 번 더 샘플링/재적합(내부 RANSAC)하고, 이어서 inlier 전부를 사용한 로컬 최적화(예: 최소제곱/LM)를 수행해 품질과 수렴속도를 끌어올리는 기법이다. 좋은 결과가 나왔을때, 주변(local)에서 더 파고드는 전략이다.

Lo-RANSAC Python Code (다항식 예제)

from typing import Callable

def lo_ransac_polyfit(
    x: ArrayLike,
    y: ArrayLike,
    deg: int,
    tau: float,
    p: float = 0.99,
    min_iterations: int = 0,
    max_iterations: Optional[int] = None,
    stop_inlier_ratio: Optional[float] = None,
    rng: Optional[np.random.Generator] = np.random.default_rng(0),
) -> RansacResult:
    """
    LO-RANSAC: 최고 모델이 갱신될 때 inlier 집합 내에서 추가 샘플링 후,
    inlier 전체로 로컬 정제를 수행한다.
    """
    # 1) 바깥 RANSAC 한 번 수행 (초기 해)
    outer = ransac_polyfit(
        x, y, deg, tau, p, min_iterations, max_iterations, stop_inlier_ratio, rng
    )

    X = np.asarray(x).ravel()
    Y = np.asarray(y).ravel()
    n = X.size
    s = deg + 1

    best_coeffs = outer.coeffs.copy()
    y_pred = np.polyval(best_coeffs, X)
    inliers = np.abs(Y - y_pred) <= tau
    best_inliers = inliers
    best_count = int(inliers.sum())

    # 2) inlier 집합에서 한 번 더 RANSAC
    if best_count >= s:
        idx_in = np.where(inliers)[0]
        # 내부 반복 횟수: inlier 비율 기반으로 축소
        eta = best_count / n
        if max_iterations is None:
            N_inner = int(np.ceil(np.log(1 - p) / np.log(1 - max(eta,1e-3)**s)))
            N_inner = max(min(N_inner, 200), 50)
        else:
            N_inner = max_iterations // 3

        for _ in range(N_inner):
            if idx_in.size < s:
                break
            sub = rng.choice(idx_in, size=s, replace=False)
            coeffs = _poly_fit_safe(X[sub], Y[sub], deg)
            if coeffs is None:
                continue
            resid = np.abs(Y - np.polyval(coeffs, X))
            in2 = resid <= tau
            cnt2 = int(in2.sum())
            if cnt2 > best_count:
                best_count = cnt2
                best_inliers = in2
                best_coeffs = coeffs

    # 3) 로컬 최적화: inlier 전체로 재적합(+옵션: 가중 최소제곱/Huber)
    if best_inliers.sum() >= s:
        best_coeffs = np.polyfit(X[best_inliers], Y[best_inliers], deg)

    return RansacResult(best_coeffs, np.where(best_inliers)[0], tau, outer.iterations, best_inliers.mean())

언제 LO-RANSAC이 이득인가

  • inlier가 한 덩어리로 잘 모여 있고, 한 번 모델이 크게 갱신된 후 그 주변 탐색이 유의미할 때. Early stop과 결합하면 전체 반복 수가 줄면서 정확도는 올라간다.