RANSAC(RANdom SAmple Consensus),即随机采样一致性。该方法最早是由Fischler和Bolles提出的一种鲁棒估计方法,最早用于计算机视觉中位姿估计问题,现在已广泛应用于已知模型的参数估计问题中。对于数据中存在大比例外点(错误数据、野值点)时,RANSAC方法十分有效。下面就以直线估计的例子来说明RANSAC的基本思想。
RANSAC的思想比较简单,主要有以下几步:
- 随机选择两点(确定一条直线所需要的最小点集);由这两个点确定一条线 l ;
- 根据阈值t,确定与直线 l 的几何距离小于t 的数据点集 S(l) ,并称它为直线 l 的一致集;
- 重复若干次随机选择,得到直线l1, l2 ,…, ln 和相应的一致集 S(l1) , S(l2) ,…, S(ln) ;
- 使用几何距离,求最大一致集的最佳拟合直线,作为数据点的最佳匹配直线。
如下图所示,首先在点集中随机选择两个点,求解它们构成的直线参数,再计算点集中剩余点到该直线的距离,距离小于设置的距离阈值的点为内点,统计内点个数;接下来再随机选取两个点,同样统计内点个数,以此类推;其中内点个数最多的点集即为最大一致集,最后将该最大一致集里面的点利用最小二乘拟合出一条直线。
根据上面RANSAC基本原理的介绍,在这算法流程中存在两个重要的参数需要设置,采样次数和距离阈值。
为了保证算法的效率,当数据量比较大时,我们不可能采集所有的样本,将所有的可能情况都计算一遍。如何在抽样当中保证至少有一个好样本(不包含外点)是我们需要考虑的问题。Fischler和Bolles在原文中通过概率统计的方法得出了采样次数与数据中外点比例和得到一个好样本概率之间的数学关系,在这里我直接给出结论,有兴趣推导该关系的博友可以自己阅读原文或者由吴福朝编写的《计算机视觉中的数学方法》。
K=log(1−z)log(1−wn)
其中, K 为需要采样的次数;z为获取一个好样本的概率,一般设为99%; w 为点集中内点的比例,一般可以在初始时设置一个较小值,如0.1,然后迭代更新;n为模型参数估计需要的最小点个数,直线拟合最少需要2个点。
距离阈值一般需要根据经验来设定,在直线拟合实验中,我设置的参数为2.99。当观测误差符合0均值和 sigma 标准差的高斯分布时,则可以计算距离阈值。当内点被接受的概率为95%时,距离阈值 t2=3.84σ2 。
结合OpenCV库(2.0版本以上)实现了RANSAC直线拟合,在主函数中给出了一个例子,仅供大家参考。
#include
#include
#include
using namespace std;
using namespace cv;
//生成[0,1]之间符合均匀分布的数
double uniformRandom(void)
{
return (double)rand() / (double)RAND_MAX;
}
//生成[0,1]之间符合高斯分布的数
double gaussianRandom(void)
{
/* This Gaussian routine is stolen from Numerical Recipes and is their
copyright. */
static int next_gaussian = 0;
static double saved_gaussian_value;
double fac, rsq, v1, v2;
if (next_gaussian == 0) {
do {
v1 = 2 * uniformRandom() - 1;
v2 = 2 * uniformRandom() - 1;
rsq = v1*v1 + v2*v2;
} while (rsq >= 1.0 || rsq == 0.0);
fac = sqrt(-2 * log(rsq) / rsq);
saved_gaussian_value = v1*fac;
next_gaussian = 1;
return v2*fac;
}
else {
next_gaussian = 0;
return saved_gaussian_value;
}
}
//根据点集拟合直线ax+by+c=0,res为残差
void calcLinePara(vector pts, double &a, double &b, double &c, double &res)
{
res = 0;
Vec4f line;
vector ptsF;
for (unsigned int i = 0; i < pts.size(); i++)
ptsF.push_back(pts[i]);
fitLine(ptsF, line, CV_DIST_L2, 0, 1e-2, 1e-2);
a = line[1];
b = -line[0];
c = line[0] * line[3] - line[1] * line[2];
for (unsigned int i = 0; i < pts.size(); i++)
{
double resid_ = fabs(pts[i].x * a + pts[i].y * b + c);
res += resid_;
}
res /= pts.size();
}
//得到直线拟合样本,即在直线采样点集上随机选2个点
bool getSample(vector<int> set, vector<int> &sset)
{
int i[2];
if (set.size() > 2)
{
do
{
for (int n = 0; n < 2; n++)
i[n] = int(uniformRandom() * (set.size() - 1));
} while (!(i[1] != i[0]));
for (int n = 0; n < 2; n++)
{
sset.push_back(i[n]);
}
}
else
{
return false;
}
return true;
}
//直线样本中两随机点位置不能太近
bool verifyComposition(const vector pts)
{
cv::Point2d pt1 = pts[0];
cv::Point2d pt2 = pts[1];
if (abs(pt1.x - pt2.x) < 5 && abs(pt1.y - pt2.y) < 5)
return false;
return true;
}
//RANSAC直线拟合
void fitLineRANSAC(vector ptSet, double &a, double &b, double &c, vector<bool> &inlierFlag)
{
double residual_error = 2.99; //内点阈值
bool stop_loop = false;
int maximum = 0; //最大内点数
//最终内点标识及其残差
inlierFlag = vector<bool>(ptSet.size(), false);
vector<double> resids_(ptSet.size(), 3);
int sample_count = 0;
int N = 500;
double res = 0;
// RANSAC
srand((unsigned int)time(NULL)); //设置随机数种子
vector<int> ptsID;
for (unsigned int i = 0; i < ptSet.size(); i++)
ptsID.push_back(i);
while (N > sample_count && !stop_loop)
{
vector<bool> inlierstemp;
vector<double> residualstemp;
vector<int> ptss;
int inlier_count = 0;
if (!getSample(ptsID, ptss))
{
stop_loop = true;
continue;
}
vector pt_sam;
pt_sam.push_back(ptSet[ptss[0]]);
pt_sam.push_back(ptSet[ptss[1]]);
if (!verifyComposition(pt_sam))
{
++sample_count;
continue;
}
// 计算直线方程
calcLinePara(pt_sam, a, b, c, res);
//内点检验
for (unsigned int i = 0; i < ptSet.size(); i++)
{
Point2d pt = ptSet[i];
double resid_ = fabs(pt.x * a + pt.y * b + c);
residualstemp.push_back(resid_);
inlierstemp.push_back(false);
if (resid_ < residual_error)
{
++inlier_count;
inlierstemp[i] = true;
}
}
// 找到最佳拟合直线
if (inlier_count >= maximum)
{
maximum = inlier_count;
resids_ = residualstemp;
inlierFlag = inlierstemp;
}
// 更新RANSAC迭代次数,以及内点概率
if (inlier_count == 0)
{
N = 500;
}
else
{
double epsilon = 1.0 - double(inlier_count) / (double)ptSet.size(); //野值点比例
double p = 0.99; //所有样本中存在1个好样本的概率
double s = 2.0;
N = int(log(1.0 - p) / log(1.0 - pow((1.0 - epsilon), s)));
}
++sample_count;
}
//利用所有内点重新拟合直线
vector pset;
for (unsigned int i = 0; i < ptSet.size(); i++)
{
if (inlierFlag[i])
pset.push_back(ptSet[i]);
}
calcLinePara(pset, a, b, c, res);
}
void main()
{
//demo
int width = 640;
int height = 320;
//直线参数
double a = 1, b = 2, c = -640;
//随机获取直线上20个点
int ninliers = 0;
vector ptSet;
srand((unsigned int)time(NULL)); //设置随机数种子
while (true)
{
double x = uniformRandom()*(width - 1);
double y = -(a*x + c) / b;
//加0.5高斯噪声
x += gaussianRandom()*0.5;
y += gaussianRandom()*0.5;
if (x >= 640 && y >= 320)
continue;
Point2d pt(x, y);
ptSet.push_back(pt);
ninliers++;
if (ninliers == 20)
break;
}
int nOutliers = 0;
//随机获取10个野值点
while (true)
{
double x = uniformRandom() * (width - 1);
double y = uniformRandom() * (height - 1);
if (fabs(a*x + b*y + c) < 10) //野值点到直线距离不小于10个像素
continue;
Point2d pt(x, y);
ptSet.push_back(pt);
nOutliers++;
if (nOutliers == 10)
break;
}
//绘制点集中所有点
Mat img(321, 641, CV_8UC3, Scalar(255, 255, 255));
for (unsigned int i = 0; i < ptSet.size(); i++)
circle(img, ptSet[i], 3, Scalar(255, 0, 0), 3, 8);
double A, B, C;
vector<bool> inliers;
fitLineRANSAC(ptSet, A, B, C, inliers);
B = B / A;
C = C / A;
A = A / A;
//绘制直线
Point2d ptStart, ptEnd;
ptStart.x = 0;
ptStart.y = -(A*ptStart.x + C) / B;
ptEnd.x = -(B*ptEnd.y + C) / A;
ptEnd.y = 0;
line(img, ptStart, ptEnd, Scalar(0, 255, 255), 2, 8);
cout << "A:" << A << " " << "B:" << B << " " << "C:" << C << " " << endl;
imshow("line fitting", img);
waitKey();
}