链接:https://zhuanlan.zhihu.com/p/62238520

RANSAC简介

RANSAC(Random Sample Consensus,随机采样一致)算法是从一组含有“外点”(outliers)的数据中正确估计数学模型参数的迭代算法。“外点”一般指的的数据中的噪声,比如说匹配中的误匹配和估计曲线中的离群点。所以,RANSAC也是一种“外点”检测算法。RANSAC算法是一种不确定算法,它只能在一种概率下产生结果,并且这个概率会随着迭代次数的增加而加大(之后会解释为什么这个算法是这样的)。RANSAC算最早是由Fischler和Bolles在SRI上提出用来解决LDP(Location Determination Proble)问题的。

对于RANSAC算法来说一个基本的假设就是数据是由“内点”和“外点”组成的。“内点”就是组成模型参数的数据,“外点”就是不适合模型的数据。同时RANSAC假设:在给定一组含有少部分“内点”的数据,存在一个程序可以估计出符合“内点”的模型。

算法基本思想和流程

RANSAC是通过反复选择数据集去估计出模型,一直迭代到估计出认为比较好的模型。
具体的实现步骤可以分为以下几步:

  1. 选择出可以估计出模型的最小数据集;(对于直线拟合来说就是两个点,对于计算Homography矩阵就是4个点)
  2. 使用这个数据集来计算出数据模型;
  3. 将所有数据带入这个模型,计算出“内点”的数目;(累加在一定误差范围内的适合当前迭代推出模型的数据)
  4. 比较当前模型和之前推出的最好的模型的“内点“的数量,记录最大“内点”数的模型参数和“内点”数;
  5. 重复1-4步,直到迭代结束或者当前模型已经足够好了(“内点数目大于一定数量”)。

迭代次数推导

这里有一点就是迭代的次数我们应该选择多大呢?这个值是否可以事先知道应该设为多少呢?还是只能凭经验决定呢? 这个值其实是可以估算出来的。下面我们就来推算一下。

假设“内点”在数据中的占比为\(t\)

\[t = \frac{n_{inliiers}}{n_{inliiers} + n_{outliiers}}
\]

那么我们每次计算模型使用\(N\)个点的情况下,选取的点至少有一个外点的情况就是

\[1-t^{N}
\]

也就是说,在迭代 \(k\)次的情况下,\((1-t_n)^k\)就是\(k\)次迭代计算模型都至少采样到一个“外点”去计算模型的概率。那么能采样到正确的 \(N\)个点去计算出正确模型的概率就是

\[P = 1-(1-t_n)^k
\]

通过上式,可以求得

\[k=\frac{log⁡(1−P)}{log⁡(1−t^n)}
\]

“内点”的概率 \(t\)通常是一个先验值。然后\(P\)是我们希望RANSAC得到正确模型的概率。如果事先不知道\(t\)的值,可以使用自适应迭代次数的方法。也就是一开始设定一个无穷大的迭代次数,然后每次更新模型参数估计的时候,用当前的“内点”比值当成\(t\)来估算出迭代次数。

用Python实现直线/平面拟合

import numpy as np
import random
import matplotlib
import matplotlib.pyplot as plt
from mpl_toolkits import mplot3d

# 将数据增加一个维度,最后一位是1
def augment(xys):
    axy = np.ones((len(xys), len(xys[0]) + 1))
    axy[:, :len(xys[0])] = xys
    return axy

# 计算方程组的解
def estimate(xys):
    axy = augment(xys)
    return np.linalg.svd(axy)[-1][-1, :]

# 判断是否是inlier点, 方程:ax + by + c < threshold
def is_inlier(coeffs, xy, threshold = 0.01):
    return np.abs(coeffs.dot(augment([xy]).T)) < threshold

def run_ransac(data, sample_size, goal_inliers, max_iterations, stop_at_goal=True, random_seed=None):
    """
    :param data: 待拟合数据
    :param sample_size:
            2:两个点确定一条线
            3:三个点确定一个平面。
    :param goal_inliers: inliers点的个数
    :param max_iterations: 最大迭代次数
    :param stop_at_goal: 是否在满足goal_inliers条件时候结束迭代
    :param random_seed: 随机初始化种子
    :return:
            1:返回拟合参数(a, b, c ...)
            2: 拟合数据量
    """
    best_ic = 0
    best_model = None
    random.seed(random_seed)
    # random.sample cannot deal with "data" being a numpy array
    data = list(data)
    for i in range(max_iterations):
        s = random.sample(data, int(sample_size))
        m = estimate(s)
        ic = 0
        for j in range(len(data)):
            if is_inlier(m, data[j]):
                ic += 1
        if ic > best_ic:
            best_ic = ic
            best_model = m
            if ic > goal_inliers and stop_at_goal:
                break
    print('took iterations:', i+1, 'best model:', best_model, 'explains:', best_ic)
    return best_model, best_ic

def line_fit():
    n = 100
    max_iterations = 100
    goal_inliers = n * 0.3
    # test data
    xys = np.random.random((n, 2)) * 10
    xys[:50, 1:] = xys[:50, :1]
    plt.scatter(xys.T[0], xys.T[1])

    # RANSAC
    m, b = run_ransac(xys, 2, goal_inliers, max_iterations)
    a, b, c = m

    plt.plot([0, 10], [-c / b, -(c + 10 * a) / b], color=(0, 1, 0))
    plt.show()

def plane_fit():
    fig = plt.figure()
    ax = mplot3d.Axes3D(fig)

    def plot_plane(a, b, c, d):
        xx, yy = np.mgrid[:10, :10]
        return xx, yy, (-d - a * xx - b * yy) / c

    n = 100
    max_iterations = 100
    goal_inliers = n * 0.3

    # test data
    xyzs = np.random.random((n, 3)) * 10
    xyzs[:50, 2:] = xyzs[:50, :1]

    ax.scatter3D(xyzs.T[0], xyzs.T[1], xyzs.T[2])

    # RANSAC
    m, b = run_ransac(xyzs, 3, goal_inliers, max_iterations)
    a, b, c, d = m
    xx, yy, zz = plot_plane(a, b, c, d)
    ax.plot_surface(xx, yy, zz, color=(0, 1, 0, 0.5))

    plt.show()
if __name__ == '__main__':
    # line_fit()   # 拟合直线
    plane_fit()  # 拟合平面