散点拟合圆---RANSAC
一、算法原理
随机样本一致性(Random Sample Consensus RANSAC) 是一种迭代方法,用于从包含异常值的观察数据中估计出数学模型参数,因此也可以理解为一种异常值检测方法。RANSAC的一个基本假设是,数据由内点("inliers")和外点("outliers")组成,其中内点是在一定误差范围内可以通过一些模型参数来解释的数据,外点是不符合模型的数据。RANSAC的另一个假设是,随机选取的样本数据都是内点,存在一个可以估计模型参数的过程,该模型可以最佳地解释或拟合该数据。
二、算法步骤
- step1 从原始数据集中随机选择子集,为假设的内点(子集一般为最小子集,如:直线选取两个点,圆选择三个点)
- step2 依据子集估计模型参数
- step3 遍历数据集中除子集外的所有数据,如果数据点在给定误差以内,则标记为内点,否则标记为外点
- step4 所有内点组成一致集,如果一致集中点的个数满足给定阈值,则用一致集中所有内点重新估计模型参数,然后结束算法
- step5 如果一致集中内点个数少于阈值,则重新选择新的子集,并重复step1-4
- step6 经过次迭代,选择一个内点数量最多的一致集,用一致集中所有内点重新估计模型参数,然后结束算法
三、代码
3.1 伪代码
-
输入:
: 观测数据集
Model
: 待求模型(如:直线,圆)
: 估计模型参数最小数据个数
: 最大迭代次数
:允许的误差阈值
:内点个数阈值 -
输出:
最满足数据的模型参数
iterations
= 0
bestFit
= null
bestInliers
= null
bestErr
= 无穷大
while iterations
< do
MaybeInliers
:= 从数据集中随机选择个样本
maybeModel
:= 通过MaybeInliers中拟合的模型参数
alsoInliers
:= 空集
for 数据集中除MaybeInliers
外的所有点pt
do
if 点pt
和现有模型的误差值小于
将点pt
添加至alsoInliers
end if
end for
if 内点个数大小于 then
// 内点已经足够多, 依据内点重新估计模型参数
betterModel
:= 通过alsoInliers
和MaybeInliers
估计模型参数
thisErr
:= 模型估计后与数据集的误差
if thisErr
< bestErr
then
bestFit
:= betterModel
bestErr
:= thisErr
return bestFit
end if
else
if alsoInliers
的内点数量 大于 bestInliers
的内点数量 then
bestInliers
:= alsoInliers
+ maybeInliers
end if
end if
iterations
增加1
end while
bestFit
:= 通过bestInliers
估计模型参数
return bestFit
3.2 C++ 代码
点击展开
#include <limits>
#include <cmath>
inline void GetCircle(const core::Point2& p1, const core::Point2& p2, const core::Point2& p3, core::Point2& center, double & radius2)
{
double r12 = p1.x * p1.x + p1.y * p1.y;
double r22 = p2.x * p2.x + p2.y * p2.y;
double r32 = p3.x * p3.x + p3.y * p3.y;
double A = p1.x * (p2.y - p3.y) - p1.y * (p2.x - p3.x) + p2.x * p3.y - p3.x * p2.y;
double B = r12 * (p2.y - p3.y) + r22 * (p3.y - p1.y) + r32 * (p1.y - p2.y);
double C = r12 * (p2.y - p3.y) + r22 * (p3.y - p1.y) + r32 * (p1.y - p2.y);
center.x = B / (2 * A);
center.y = C / (2 * A);
radius2 = (center.x - p1.x) * (center.x - p1.x) + (center.y - p1.y) * (center.y - p1.y);
}
inline void GetNRand(const int maxV, const int N, std::set<int>& idxs)
{
if (N > maxV) {
return;
}
while(idxs.size() < N) {
idxs.insert(rand() % maxV);
}
}
double Fitter::FitCircleByRANSAC(const std::vector<core::Point2>& pointArray, core::Point2& center, double& radius, const int iterNum, const double e, const float ratio)
{
const int N = pointArray.size();
const int targetN = N * ratio;
int iter = 0;
std::vector<core::Point2> bestInliers;
while (iter < iterNum) {
std::set<int> seedIds;
GetNRand(N, 3, seedIds); // circle need 3 point
if (seedIds.size() < 3) {
break;
}
std::vector<core::Point2> seedPts;
for (const int idx : seedIds) {
seedPts.push_back(pointArray[idx]);
}
core::Point2 seedCenter;
double seedR2 = 0.0;
GetCircle(seedPts[0], seedPts[1], seedPts[2], seedCenter, seedR2);
std::vector<core::Point2> maybeInliers;
for (const core::Point2 pt : pointArray) {
if (std::abs((pt.x - seedCenter.x) * (pt.x - seedCenter.x) + (pt.y - seedCenter.y) * (pt.y - seedCenter.y) - seedR2) < e) {
maybeInliers.push_back(pt);
}
}
if (maybeInliers.size() > targetN) {
// it show the inliers is enough
return FitCircleByLeastSquare(maybeInliers, center, radius);
}
else {
if (maybeInliers.size() > bestInliers.size()) {
bestInliers.swap(maybeInliers);
for (const core::Point2 pt : seedPts) {
bestInliers.push_back(pt);
}
}
}
++iter;
}
return FitCircleByLeastSquare(bestInliers, center, radius);
}
- test.cpp
#include "opencv2/opencv.hpp"
int main(int argc, char * argv[])
{
std::vector<core::Point2> pts;
pts.push_back(core::Point2(50.0, 60.0));
pts.push_back(core::Point2(60.0, 50.0));
pts.push_back(core::Point2(50.0, 40.0));
pts.push_back(core::Point2(40.0, 50.0));
pts.push_back(core::Point2(39.0, 50.0));
pts.push_back(core::Point2(38.0, 50.0));
pts.push_back(core::Point2(38.5, 49.0));
pts.push_back(core::Point2(30.5, 49.0));
pts.push_back(core::Point2(35.5, 49.0));
core::Point2 center1, center2;
double r1, r2;
double err1 = core::Fitter().FitCircleByLeastSquare(pts, center1, r1);
double err2 = core::Fitter().FitCircleByRANSAC(pts, center2, r2);
cv::Mat img = cv::Mat::zeros(cv::Size(100, 100), CV_8UC3);
img.setTo(255);
for (core::Point2 pt : pts) {
cv::circle(img, cv::Point(pt.x, pt.y), 1, cv::Scalar(0, 0, 0), -1); // draw pt
}
cv::circle(img, cv::Point(center1.x, center1.y), r1, cv::Scalar(0, 255, 0), 1);
cv::circle(img, cv::Point(center2.x, center2.y), r2, cv::Scalar(0, 0, 255), 1);
cv::namedWindow("img", cv::WINDOW_NORMAL);
cv::imshow("img", img);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
在这个例子中RANSAC拟合(红色圆)比最小二乘拟合(绿色圆)的误差相对小一些
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理