为什么RANSAC被称为CV界的“多边形战士”?

RANSAC(RAndom SAmple Consensus,随机采样一致)是一种随机参数估计算法,常常应用于二维图像的拟合、分割等等,由于是估计数学模型参数的迭代算法,因此也被用于三维平面、球的估计,这次笔者将主要介绍其算法原理及在点云拟合、分割及粗配准的代码应用分析,希望能够对读者们的工程研究有所帮助。

1. 算法详解

RANSAC算法由Fischler和Bolles于1981年提出,是一种从数据集合中迭代稳健估计模型参数的方法。该算法的基本思想是不断地从数据集合中随机抽取样本集,寻求支持更多局内点的模型参数,然后利用模型余集检验获得的模型参数,通过一定次数的迭代来达到最大的一致性概率,并将该采样样本集作为理解的样本集,且参数解的正确性由样本余集检验支撑。其中数据集合中包含正确数据(即内点inliers)和异常数据(外点outliers)。同时RANSAC假设:在给定一组含有少部分“内点”的数据,存在一个程序可以估计出符合“内点”的模型。

RANSAC的算法比较简单,主要有以下几步:

1)给定一个数据集SSS,从中选择建立模型所需的最小样本数(例如直线最少由两个点确定,所以最小样本数是2,而平面可以根据不共线三点确定,所以最小样本数为3),记选择数据集为SSS

2)使用选择的数据集SSS计算得到一个数学模型MMM

3)用计算的模型MMM去测试数据集中剩余的点,如果测试的数据点在误差允许的范围内,则将该数据点判为内点(inlier),否则判为外点(outlier),记所有内点组成的数据集为S∗S^*S,表示为S1S_1S1的一致性集合;

4)比较当前模型和之前推出的最好的模型的“内点”的数量,记录最大“内点”数量时模型参数和“内点”数量;

5)重复1-4步,直到迭代结束或者当前模型已经足够好了(“内点数目大于设定的阈值”);

这里的迭代次数可以通过理论推算获得。在一定的置信概率下,其最小样本数SSS与至少取得一个良性采样子集的概率PPP满足:

P=1−(1−tS)mP=1-(1-t^{S})^mP=1(1tS)m

其中,t=内点数内点数+外点数t=\frac{内点数}{内点数+外点数}t=内点数+外点数内点数表示为数据集合中内点概率,NNN是计算模型参数需要的最小数据量。

因此,在基本子集的在迭代次数内,至少有一次采样使得采样子集内的NNN个点均为内点,从而保证在迭代次数中,至少存在一次采样能够取得目标函数的最大值,因此终止循环条件应满足:

m≥ln(1−P)ln(1−1N)m\ge \frac{ln(1-P)}{ln(1-1^N)}mln(11N)ln(1P)

2. 算法应用

2.1 拟合

因此,利用上述思想最开始可以进行拟合数据,这里给出C++的点云直线拟合代码及解析:

#include <iostream>
#include <pcl/io/pcd_io.h>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>
#include <pcl/filters/extract_indices.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/visualization/cloud_viewer.h>

using namespace std;
int main() {
    //----------------读取点云数据---------------------
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::PCDReader reader;
    reader.read<pcl::PointXYZ>("L.pcd", *cloud);
    //---------------创建拟合模型---------------------
    pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);
    //inliers用来存储直线上点的索引
    pcl::PointIndices::Ptr inliers(new pcl::PointIndices);  
    //创建一个分割器对象
    pcl::SACSegmentation<pcl::PointXYZ> seg;
    seg.setModelType(pcl::SACMODEL_LINE);   //设置目标几何形状
    seg.setMethodType(pcl::SAC_RANSAC);     //拟合方法:随机采样法
    seg.setDistanceThreshold(0.05);       //设置误差容忍范围(即阈值)
    seg.setMaxIterations(500);            //最大迭代次数
    seg.setInputCloud(cloud);               //输入点云
    seg.segment(*inliers, *coefficients);   //拟合点云
    //拟合直线的模型系数为:coefficients   
    //--------------提取拟合的直线------------------
    pcl::PointCloud<pcl::PointXYZ>::Ptr line(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::ExtractIndices<pcl::PointXYZ> extract;  //创建点云提取对象
    extract.setInputCloud(cloud);    //设置输入点云
    extract.setIndices(inliers);     //设置分割后的内点为需要提取的点集
    extract.setNegative(false);      //false提取内点, true提取外点
    extract.filter(*line);        
    //-----------------点云可视化---------------
    pcl::visualization::PCLVisualizer viewer;
    viewer.addPointCloud(cloud, "cloud");  // 加载比对点云
    pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> line_color(line, 255, 0, 0);
    viewer.addPointCloud(line, line_color, "line");
    viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "line");
    //可视化拟合出来的直线
    viewer.addLine(*coefficients, "line");
    viewer.spin();
    return 0;
}

2.2 分割

分割也是利用拟合数据的思想,代码类似,这里给出主要部分的C++点云直线分割代码及解析:

//--------------------RANSAC分割直线--------
void fitMultipleLines(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud, std::vector<pcl::ModelCoefficients>& lineCoff)
{
	pcl::PointIndices::Ptr inliers(new pcl::PointIndices());
	pcl::SACSegmentation<pcl::PointXYZ> seg;   // 创建拟合对象
    // 设置对估计模型参数进行优化处理
	seg.setOptimizeCoefficients(true);                    
   // 设置拟合模型为直线模型
	seg.setModelType(pcl::SACMODEL_LINE);                     
    // 设置拟合方法为RANSAC
	seg.setMethodType(pcl::SAC_RANSAC);                       
   // 设置最大迭代次数
	seg.setMaxIterations(1000);                               
	seg.setDistanceThreshold(0.1);   //判断是否为模型内点的距离阈值

	int i = 0, nr_points = cloud->points.size();
	int k = 0;
	while (k < 5 && cloud->points.size() > 0.1 * nr_points)// 从0循环到5执行6次,并且每次cloud的点数必须要大于原始总点数的0.1倍
	{
		pcl::ModelCoefficients coefficients;
		seg.setInputCloud(cloud);       // 输入点云			
		seg.segment(*inliers, coefficients);   //内点的索引
		pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_line(new pcl::PointCloud<pcl::PointXYZ>);
		pcl::PointCloud<pcl::PointXYZ>::Ptr outside(new pcl::PointCloud<pcl::PointXYZ>);
		if (inliers->indices.size() > 20) // 小于20则该直线不合格
		{
          // 将参数保存进vector中
			lineCoff.push_back(coefficients);          
          // 创建点云提取对象
			pcl::ExtractIndices<pcl::PointXYZ> extract;   
			extract.setInputCloud(cloud);
			extract.setIndices(inliers);
          // 设置为false,表示提取内点
			extract.setNegative(false);                   
			extract.filter(*cloud_line);
          // true提取外点(该直线之外的点)
			extract.setNegative(true);                    
			extract.filter(*outside);      
         // 将cloud_f中的点云赋值给cloud
          cloud.swap(outside);                         
		}
		else
		{
			PCL_ERROR("Could not estimate a line model for the given dataset.\n");
			break;
		}

		std::stringstream ss;
		ss << "line_" << i + 1 << ".pcd"; 
		pcl::PCDWriter writer;
		writer.write<pcl::PointXYZ>(ss.str(), *cloud_line, false);

		i++;
		k++;
	}
}

	cout << "一共拟合出" << LinesCoefficients.size() << "条直线“;

2.3 粗配准

同理,在点云粗配准中也是通过对应点集中随机选取3个对应点对,并求解刚体变换矩阵。然后计算对应点中剩余点对,在上一步所求的刚体变换矩阵的作用下点对的距离误差,若其中一点对的距离误差小于设定的阅值误差,则该点为样本内点,否则为样本外点,并统计前者数目。最后,达到指定选代次数。这里给出C++的点云粗配准代码及解析:

//--------------------RANSAC点云配准--------
    pcl::SampleConsensusPrerejective<PointT, PointT, pcl::FPFHSignature33> r_sac;
    r_sac.setInputSource(source);            // 源点云
    r_sac.setInputTarget(target);            // 目标点云
    r_sac.setSourceFeatures(source_fpfh);    // 源点云FPFH特征
    r_sac.setTargetFeatures(target_fpfh);   // 目标点云FPFH特征
    // 选择随机特征对应的邻居的数量,数值越大,特征匹配的随机性越大。
    r_sac.setCorrespondenceRandomness(5);    
    // 所需的(输入的)inlier分数
    r_sac.setInlierFraction(0.5f);     
    // 每次迭代中使用的采样点数量    
    r_sac.setNumberOfSamples(3);             
    r_sac.setMaxCorrespondenceDistance(1.0f);// 内点,阈值 
    r_sac.setMaximumIterations(100);      // RANSAC 最大迭代次数
    pointcloud::Ptr align(new pointcloud);
    r_sac.align(*align);

    pcl::transformPointCloud(*source,*align,
         r_sac.getFinalTransformation());
    cout << "变换矩阵:\n" << r_sac.getFinalTransformation() << endl;
}

3. 算法分析

RANSAC的优点是能鲁棒的估计模型参数。例如,它能从包含大量局外点的数据集中估计出高精度的参数。

RANSAC的缺点是计算参数的迭代次数没有上限;如果设置迭代次数的上限,得到的结果可能不是最优的结果,甚至可能得到错误的结果。且要求设置跟问题相关的阈值。

笔者|William

审核|Los

移步公众号【深蓝AI】,第一时间获取自动驾驶、人工智能与机器人行业最新最前沿论文和科技动态。

### 如何将 `CV_RANSAC` 替换为 `cv::RANSAC` 在较新的 OpenCV 版本中,许多宏定义被替换为了命名空间下的枚举值。例如,在早期版本中使用的 `CV_RANSAC` 宏已经被弃用,并由现代 C++ 风格的 `cv::RANSAC` 枚举替代[^1]。 以下是具体的修改方式以及注意事项: #### 修改前后的对比 在旧版代码中,可能有如下形式的调用: ```cpp std::vector<cv::Point2f> points1, points2; cv::Mat fundamentalMatrix = cv::findFundamentalMat(points1, points2, CV_FM_RANSAC); ``` 而在新版 OpenCV 中,应改为以下写法: ```cpp std::vector<cv::Point2f> points1, points2; cv::Mat fundamentalMatrix = cv::findFundamentalMat( points1, points2, cv::FM_RANSAC, 3.0, 0.99, 0.0); ``` 这里需要注意的是,除了将 `CV_FM_RANSAC` 改为 `cv::FM_RANSAC` 外,还需要提供额外的参数以满足新接口的要求。具体来说,`findFundamentalMat` 函数需要指定 RANSAC 的阈值(如上例中的 `3.0`)、置信度(如上例中的 `0.99`),以及其他可选参数[^2]。 对于其他支持 RANSAC 的函数,比如 `cv::findHomography` 或者 `cv::solvePnPRansac`,类似的更改也适用。例如: #### 使用 `cv::findHomography` 示例 旧版代码: ```cpp std::vector<cv::Point2f> srcPoints, dstPoints; cv::Mat homography = cv::findHomography(srcPoints, dstPoints, CV_RANSAC, 3.0); ``` 新版代码: ```cpp std::vector<cv::Point2f> srcPoints, dstPoints; cv::Mat homography = cv::findHomography( srcPoints, dstPoints, cv::RANSAC, 3.0, cv::noArray(), 2000, 0.99); ``` 在这里,`cv::RANSAC` 是作为第三个参数传递给函数的,而后续的参数则用于控制最大迭代次数和期望的成功概率[^3]。 #### 注意事项 - **兼容性问题**:如果正在使用非常老的 OpenCV 版本,则可能无法直接使用 `cv::RANSAC`,此时仍需依赖 `CV_RANSAC`。 - **默认参数调整**:某些情况下,默认参数可能会发生变化,因此建议查阅官方文档确认最新 API 行为。 - **性能优化**:适当调节 RANSAC 参数可以显著影响计算效率与精度,开发者应当根据实际需求设置合理的阈值和置信水平。 ### 总结 通过上述说明可以看出,从 `CV_RANSAC` 到 `cv::RANSAC` 的迁移不仅涉及简单的名称替换,还需注意新增加的功能选项及其合理配置。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值