• ransac算法思想


    Ransac: Random Sample Consensus, 随机抽样一致性。RANSAC算法在1981年由Fischler和Bolles首次提出。

    Ransac是一种通过使用观测到的数据点来估计数学模型参数的迭代方法。其中数据点包括内点(inlier),外点(outlier)。outlier对模型的估计没有价值,因此该方法也可以叫做outlier检测方法。其中inliners指样本中正确的的样本点,常称为内点,内群点,正确样本,outliners指样本中的噪声点(如测量时误差很大的点,数据值不准确的点等),常称为外点,离群点,噪声点。

    所以通俗点将,就是我们有一组观测数据,但是数据点中有很多噪声点,如果我们直接用模型来拟合数据,模型肯定会受噪声点影响,而且噪声点比例越大,拟合出的模型也会越不准确。Ransac就是用来解决样本中的外点问题(噪声),最多可处理50%的外点情况。

    1. Ransac的假设和思想

    再深入点讨论,有一组观测数据,要建立一个模型来拟合数据,我们第一步要做的,肯定是用一个标准对数据进行筛选,去除噪声点,让数据尽量干净和准确。但是如果我们没有一个合适的筛选标准,该怎么办呢?我们可以假设:观测数据中除了外点(噪声),肯定存在内点(准确点),而且内点的个数够我们用来拟合一个模型(比如拟合一条直线,至少要两个数据点)。有了这个假设,我们可以从观测数据中随机挑选出n个数据点,用这n个点来拟合模型,然后对这个模型进行评价。如果通过迭代,反复重复这个过程,只要迭代次数过大,我们随机挑选出来的n个样本点是有可能全部是内点的,再配合上模型评价,就能找到最优的拟合模型。这就是ransac的主要假设和思想,官方叙述如下:

    • RANSAC的基本假设是 “内群”数据可以通过几组模型参数来叙述其数据分布,而“离群”数据则 是不适合模型化的数据。 数据会受噪声影响, 噪声指的是离群:例如从极端的噪声, 错误解释有关数据的测量, 或不正确的假设。RANSAC假定,给定一组(通常很小的)内群,存在一个 程序,这个程序可以估算最佳解释或最适用于这一数据模型的参数。

    宏观点来看,Ransac是一种思想,一种利用迭代来增加概率的方法,即:

    • RANSAC是一种思想,一个求解已知模型的参数的框架。它不限定某一特定的问题,可以是计算机视觉的问题,同样也可以是统计数学,甚至可以是经济学领域的模型参数估计问题。
    • RANSAC是一种迭代的方法,用来在一组包含离群的被观测数据中估算出数学模型的参数。 RANSAC 是一个非确定性算法,在某种意义上说,它会产生一个在一定概率下合理的结果,其允许使用更多次的迭代来使其概率增加

    2. Ransac基本流程和参数设置

    2.1 Ransac步骤

    RANSAC算法的输入:

    • 1.一组观测数据(往往含有较大的噪声或无效点
    • 2.一个用于解释观测数据的参数化模型
    • 3.一些可信的参数

    迭代过程:

    1. 在数据中随机选择n个点设定为内群
    2. 计算适合内群的模型,如线性直线模型y=ax+b
    3. 把其它刚才没选到的点带入刚才建立的模型中,计算是否为内群点
    4. 记下内群数量
    5. 重复以上步骤, 迭代k次
    6. 比较哪次计算中内群数量最多,内群最多的那次所建的模型就是我们所要求的解

    注意:不同问题对应的数学模型不同,因此在计算模型参数时方法必定不同,RANSAC的作用不在于 计算模型参数。(这导致ransac的缺点在于要求数学模型已知)

    2.2 Ransac参数设置

    在上面的步骤中,有两个参数比较重要,需要提前确定,即:

    • 一开始的时候我们要随机选择多少点(n)
    • 以及要重复迭代多少次(k)

    假设每个点是真正内群的概率为 w:

    [w = 内群的数目/(内群数目+外群数目) ]

    通常我们不知道 w 是多少, (w^n)是所选择的n个点都是内群的机率, (1-w^n)是所选择的n个点至少有一个不是内群的机率, ((1 − w^n)^k)是表示重复 k 次都没有全部的n个点都是内群的机率, 假设算法跑 k 次以后成功的机率是p,那么

    [1 − p = (1 − w^n)^k\ p = 1 − (1 − w^n)^k ]

    我们可以通过P反算得到抽取次数K:(K=log(1-P)/log(1-w^n)。)

    所以如果希望成功机率高:

    • 当n不变时,k越大,则p越大;
    • 当w不变时,n越大,所需的k就越大。
    • 通常w未知,所以n选小一点比较好。

    实际设置经验

    可以先设置好P和n,然后K设置为一个很大的数,然后在迭代的过程中,每次迭代完都可以计算一次w,然后对K进行动态修改。 opencv的findHomography函数中ransac就是采用的这一策略:

    H = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5)  # 采用Ransac方法计算单应性矩阵
    

    参考:https://blog.csdn.net/laobai1015/article/details/51683076/

    3. Ransac应用

    在计算机视觉方面,Ransac最主要的应用主要包括直线拟合,平面拟合,单应性矩阵拟合等。这里简单介绍下直线拟合和透视矩阵拟合。

    3.1 Ransac直线拟合

    假设我们知道两个变量X与Y之间呈线性关系,Y=aX+b,我们想确定参数a与b的具体值。通过实验, 可以得到一组X与Y的测试值。虽然理论上两个未知数的方程只需要两组值即可确认,但由于系统误差的原因,任意取两点算出的a与b的值都不尽相同。通常情况下,我们可以采用最小二乘法拟合出直线方程(最小二乘法:通过计算最小均方差关于参数a、b的偏导数为零时的值)。但是最小二乘法只适合于误差较小的情况,如果测量数据中外点较多,误差很大,就需要采用Ransac算法。

    在下图中分别是Ransac和最小二乘法拟合的直线,可以看出两者的差别。直接采用最小二乘法拟合直线,直线会受离群点影响,偏离中间的内群数据点,而ransac只是挑选部分数据点进行直线拟合,所以不会受离群点影响。

    下面是一段示例代码,比较了ransac和最小二乘法拟合直线的区别:

    #include<iostream>
    #include<opencv2/opencv.hpp>
    #include<vector>
    #include<set>
    using namespace std;
    
    bool ransacFitLine(vector<cv::Point2d>& points, cv::Vec4f& bestParam, vector<cv::Point2d>& inlinerPoints, int initInlinerNums, int iter, double thresh, int EndInlinerNums){
    /*
        @param points:  所有的样本点.
        @param bestParam:  拟合最好的直线方程
        @param inlinerPoints:  拟合最好时的内点集合
        @param initInlinerNums: 用来拟合直线的初始化内点个数
        @param iter: 最大迭代次数
        @param thresh: 阈值,用来判断某个点是否在拟合的直线上
        @param EndInlinerNums: 阈值,内点个数超过阈值时停止迭代
     */
    
        set<int> randomIndex;
        vector<cv::Point2d> randomPoints;
        vector<cv::Point2d> tempInliners;
        int n = points.size();
        double minErr=DBL_MAX;
        bool flag = false;
    
        while(iter>=0){
    
            randomIndex.clear();
            randomPoints.clear();
            tempInliners.clear();
    
            // 随机选取initInlinerNums个点,拟合直线,其他点作为测试点
            for(int i=0; i<initInlinerNums; i++){
                int index = rand()%n;
                randomIndex.insert(index);
                randomPoints.push_back(points[index]);
            }
    
            // 采用随机点拟合直线
            cv::Vec4f lineParam;
            cv::fitLine(randomPoints, lineParam, cv::DIST_L2, 0, 0.01, 0.01);
            double linek = lineParam[1]/lineParam[0];
            
            // 计算测试点是否也在拟合直线上, 在直线上的点当作inliner
            for(int i=0; i<n; i++){
                if(randomIndex.find(i)==randomIndex.end()){
                    double err = linek*(points[i].x - lineParam[2]) + lineParam[3] - points[i].y;
                    err = err*err; // 差值的平方作为误差
                    if (err < thresh){
                        tempInliners.push_back(points[i]);
                    }
                }
            }
            
            // 当前inliner内点个数超过阈值时
            if(tempInliners.size()>EndInlinerNums){
                //采用所有的内点,拟合直线并计算平均误差
                tempInliners.insert(tempInliners.end(), randomPoints.begin(), randomPoints.end());
                cv::fitLine(randomPoints, lineParam, cv::DIST_L2, 0, 0.01, 0.01);
                double linek = lineParam[1]/lineParam[0];
                double sumErr = 0;
                for(int i=0; i<tempInliners.size(); i++){
                    double err = linek*(points[i].x - lineParam[2]) + lineParam[3] - points[i].y;
                    sumErr += err*err; // 差值的平方作为误差
                }
                sumErr = sumErr/tempInliners.size();  // 误差平方和的平均值
                if(minErr>sumErr){  // 记录最小的平均误差,
                    minErr = sumErr;
                    bestParam = lineParam;
                    inlinerPoints.assign(tempInliners.begin(), tempInliners.end()); 
                    flag = true;
                }
            }
            iter--;
        }
        if(!flag) cout << "did't meet fit acceptance criteria" << endl;
    
        return flag;
    }
    
    int main(int argc, char* argv[]){
        cv::RNG rng(200);//seed为200
    
        vector<cv::Point2d> points;
    
        // 生成500个随机样本点
        int nSamples = 500;   // 生成样本数据个数
        double k = 1 + rng.gaussian(1);  // 0-2之间的随机斜率
        cout << k << endl;
        for(int i=0; i<nSamples; i++){
            double x = rng.uniform(40, 600);
            double y = k*x + rng.uniform(-30, 30);  // 加入随机噪声
            x += rng.uniform(-30, 30);             // 加入随机噪声
            points.push_back(cv::Point2d(x, y));
        }
    
        // 加入outlier
        for(int j=0; j< 100; j++){
            double x = rng.uniform(200, 600);
            double y = rng.uniform(10, 300);
            points.push_back(cv::Point2d(x, y));
        }
    
    
        cv::Mat img = cv::Mat(720, 720, CV_8UC3, cv::Scalar(255,255,255));
    
        //绘制point
        for(int i=0; i<points.size(); i++){
            cv::circle(img, points[i], 3, cv::Scalar(0, 0, 0), 2);
        }  
        
         // 最小二乘法
        if(1){
            cv::Vec4f lineParam;
            fitLine(points, lineParam, cv::DIST_L2, 0.0, 0.01, 0.01);
            //获取点斜式的点和斜率, y=k(x-x0) + y0
            cv::Point point0;
            point0.x = lineParam[2];
            point0.y = lineParam[3];
            double linek = lineParam[1] / lineParam[0];
            
    
            //绘制最小二乘法拟合的直线
            cv::Point2d point1, point2;
            point1.x=10;
            point1.y = linek*(point1.x-point0.x) + point0.y;
            point2.x = 600;
            point2.y = linek*(point2.x-point0.x) + point0.y;
            cv::line(img, point1, point2, cv::Scalar(0, 255, 0), 4);
        }    
    
        // ransac拟合直线
        if(1){   // 为了避免命名冲突,所以放在if局部范围内
            cv::Vec4f lineParam;
            vector<cv::Point2d> inlinerPoints;
            bool ret = ransacFitLine(points, lineParam, inlinerPoints, 50, 1000, 7e3, 300);
            if(ret){
                cout << ret << endl;
                cout << lineParam << endl;
                cout << inlinerPoints.size() << endl;
                
                //获取点斜式的点和斜率, y=k(x-x0) + y0
                cv::Point point0;
                point0.x = lineParam[2];
                point0.y = lineParam[3];
                double linek = lineParam[1] / lineParam[0];
                
    
                //绘制拟合的直线
                cv::Point2d point1, point2;
                point1.x=10;
                point1.y = linek*(point1.x-point0.x) + point0.y;
                point2.x = 600;
                point2.y = linek*(point2.x-point0.x) + point0.y;
                cv::line(img, point1, point2, cv::Scalar(0, 0, 255), 4);
            }else{
                cout << "Fitting failed, try to change parameters for ransac!!!" << endl;
            }
            
        }
    
        cv::imshow("img", img);
        cv::waitKey(0);
        cv::destroyAllWindows();
        return 0;
    }
    

    代码结果如下:

    3.2 Ransac单应性矩阵拟合

    关于单应矩阵理解:https://zhuanlan.zhihu.com/p/49435367

    在进行图像匹配,全景拼接等时,常会用到单应性矩阵。例如在做全景拼接时,对于同一个场景,相机在不同角度拍摄了两张照片,一般先寻找两幅图片的匹配特征点,然后通过匹配特征点的对应关系计算出一个矩阵,这个矩阵就是单应性矩阵,利用这个矩阵就能将两张图片组合在一起。所以单应矩阵描述的是两组坐标点的关系,即:

    [egin{bmatrix} x_0 \ y_0 \ 1 end{bmatrix} =Hegin{bmatrix} X'_0 \ Y'_0 \ 1 end{bmatrix} \ ]

    上面H即为单应性矩阵,((x_0, y_0))为第一幅图片上的坐标点,((X'_0, Y'_0))为第二幅图片上的坐标点,两个点是一组匹配点。H一般为3x3的矩阵,形式如下:

    [H = egin{bmatrix} a_{11} & a_{12} & a_{13} \ a_{21} & a_{22} & a_{23} \ a_{31} & a_{32} & a_{33} \ end{bmatrix} ]

    这个矩阵有9个未知数(自由度为8),为了求解矩阵我们一般令(a_{33}=1), 则一组匹配点可以列如下两个方程:

    若有四组匹配点,就能列出8个方程,便能计算出单应矩阵H, 如下图所示:

    opencv的getPerspectiveTransform函数就是通过四组匹配点来计算单应性矩阵(投影矩阵/透视矩阵),如下:

     H = cv2.getPerspectiveTransform(src, dst)   # 返回3*3的矩阵
     参数:    
          src:原图像中的四组坐标,如 np.float32([[56,65],[368,52],[28,387],[389,390]])
          dst: 转换后的对应四组坐标,如np.float32([[0,0],[300,0],[0,300],[300,300]])
    

    如果我们只有四组匹配点,而且能够保证匹配点都是正确的,的确可以采用上述方法求解单应矩阵,但是如果我们有100组匹配点,而且有些匹配点可能是错误的,该怎么求解一个拟合最好的矩阵呢?

    仔细观察上面八个方程的形式,发现是一个矩阵求解的问题:

    [AH=B ]

    其中,A是8x8的矩阵,H是8x1的矩阵,B也是8x1的矩阵。若有100组匹配点,则A是200x8的矩阵,B是200x1的矩阵。

    联想到上面最小二乘法拟合直线的问题:(y=wx),那么在这里x就是矩阵A的每一行(8x1的向量),(w)就是矩阵H,(y)

    就是矩阵B的每一行(1x1), 如下图所示:

    所以100组匹配点就是对应200条数据,采用最小二乘法进行拟合(不算是直线了,算平面拟合了)。

    有了求解方法,另一个问题就是:100组匹配点中可能存在错误的匹配,很明显错误的匹配就是所谓的outlier了。很自然的能想到:采用ransac代替上面的最小二乘法即可。

    上述就是采用Ransac拟合单应性矩阵的思想,在opencv中findHomography函数实现了上述过程,如下所示:

    #findHomography(srcPoints,dstPoints,method=0,ransacReprojThreshold=3,mask=noArray(),maxIters=2000,confidence=0.995)	
    
    H = cv2.findHomography(src_pts, dst_pts, method=cv2.RANSAC, ransacReprojThreshold=5)  # 生成变换矩阵
    参数:
    	src_pts:原图像中的坐标(大于等于四组)
    	dst_pts: 转换后的对应坐标(大于等于四组)
    	method:求解单应矩阵的方法,取值如下:
    			0:最小二乘法
    			RANSAC:RANSAC拟合方法
    			LMEDS: Least-Median robust method
    			RHO:PROSAC-based robust method
       ransacReprojThreshold:ransac中判断是否是内点的阈值
    

    下面看一个全景拼接的示例,全景拼接的常规流程 如下:

    • 1、针对某个场景拍摄多张/序列图像

    • 2、通过匹配特征(sift匹配)计算下一张图像与上一张图像之间的匹配特征点

    • 3、对匹配特征点进行筛选,剔除掉部分匹配点。采用ransanc拟合匹配点之间的单应性矩阵

    • 3、利用单应性矩阵进行图像映射,将下一张图像叠加到上一张图像的坐标系中

    • 4、变换后的融合/合成

    下面是一个代码示例:

    # coding: utf-8
    import numpy as np
    import cv2
    
    left_img = cv2.imdecode(np.fromfile(r"D:笔记cppimage_processing图像拼接left.jpg", dtype=np.uint8), 1)
    left_img = cv2.resize(left_img, (600, 400))
    right_img = cv2.imdecode(np.fromfile(r"D:笔记cppimage_processing图像拼接
    ight.jpg", dtype=np.uint8), 1)
    right_img = cv2.resize(right_img, (600, 400))
    left_gray = cv2.cvtColor(left_img, cv2.COLOR_BGR2GRAY)
    right_gray = cv2.cvtColor(right_img, cv2.COLOR_BGR2GRAY)
    
    hessian = 300
    surf = cv2.xfeatures2d.SIFT_create(hessian) # 将Hessian Threshold设置为400,阈值越大能检测的特征就越少
    kp1, des1 = surf.detectAndCompute(left_gray, None)  # 查找关键点和描述符
    kp2, des2 = surf.detectAndCompute(right_gray, None)
    
    # kp1s = np.float32([kp.pt for kp in kp1])
    # kp2s = np.float32([kp.pt for kp in kp2])
    
    # 显示特征点
    img_with_drawKeyPoint_left = cv2.drawKeypoints(left_gray, kp1, np.array([]), flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
    cv2.imshow("img_with_drawKeyPoint_left", img_with_drawKeyPoint_left)
    
    img_with_drawKeyPoint_right = cv2.drawKeypoints(right_gray, kp2, np.array([]), flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
    cv2.imshow("img_with_drawKeyPoint_right", img_with_drawKeyPoint_right)
    
    
    
    '''
    BFMatcher简称暴力匹配,意思就是尝试所有可能匹配,实现最佳匹配。
    
    FlannBasedMatcher简称最近邻近似匹配。
    是一种近似匹配方法,并不追求完美!,因此速度更快。
    可以调整FlannBasedMatcher参数改变匹配精度或改变算法速度。
    参考:https://blog.csdn.net/claroja/article/details/83411108
    '''
    FLANN_INDEX_KDTREE = 0  # 建立FLANN匹配器的参数
    
    indexParams = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)  # 配置索引,密度树的数量为5
    searchParams = dict(checks=50)  # 指定递归次数
    # FlannBasedMatcher:是目前最快的特征匹配算法(最近邻搜索)
    flann = cv2.FlannBasedMatcher(indexParams, searchParams)  # 建立匹配器
    
    # 参考https://docs.opencv.org/master/db/d39/classcv_1_1DescriptorMatcher.html#a378f35c9b1a5dfa4022839a45cdf0e89
    '''
    int queryIdx –>是测试图像的特征点描述符(descriptor)的下标,同时也是描述符对应特征点(keypoint)的下标。
    
    int trainIdx –> 是样本图像的特征点描述符的下标,同样也是相应的特征点的下标。
    
    int imgIdx –>当样本是多张图像的话有用。
    
    float distance –>代表这一对匹配的特征点描述符(本质是向量)的欧氏距离,数值越小也就说明两个特征点越相像。
    '''
    
    matches = flann.knnMatch(des1, des2, k=2)  # 得出匹配的关键点
    
    
    good = []
    # 提取优秀的特征点
    for m, n in matches:
        if m.distance < 0.7 * n.distance:  # 如果第一个邻近距离比第二个邻近距离的0.7倍小,则保留
            good.append(m)
    
    src_pts = np.array([kp1[m.queryIdx].pt for m in good])  # 查询图像的特征描述子索引
    dst_pts = np.array([kp2[m.trainIdx].pt for m in good])  # 训练(模板)图像的特征描述子索引
    
    
    # findHomography参考https://docs.opencv.org/master/d9/d0c/group__calib3d.html#ga4abc2ece9fab9398f2e560d53c8c9780
    # 单应矩阵:https://www.cnblogs.com/wangguchangqing/p/8287585.html
    H = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5)  # 生成变换矩阵
    
    h1, w1 = left_gray.shape[:2]
    h2, w2 = right_gray.shape[:2]
    shift = np.array([[1.0, 0, w1], [0, 1.0, 0], [0, 0, 1.0]])
    # 点积
    M = np.dot(shift, H[0])  # 获取左边图像到右边图像的投影映射关系, 向右移动w1?
    
    dst = cv2.warpPerspective(left_img, M, (w1+w2, max(h1, h2)))  # 透视变换,新图像可容纳完整的两幅图
    cv2.imshow('left_img', dst)  # 显示,第一幅图已在标准位置
    dst[0:h2, w1:w1+w2] = right_img  # 将第二幅图放在右侧
    # cv2.imwrite('tiled.jpg',dst_corners)
    cv2.imshow('total_img', dst)
    cv2.imshow('leftgray', left_img)
    cv2.imshow('rightgray', right_img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    

    效果如下:

    参考:

    https://blog.csdn.net/hzwwpgmwy/article/details/83578694

    https://zhuanlan.zhihu.com/p/82793501

    4 Ransac优缺点

    优点:

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

    缺点:

    1. 它计算参数的迭代次数没有上限;如果设置迭代次数的上限,得到的结果可能不是最优的结果,甚至可能得到错误的结果。
    2. RANSAC只有一定的概率得到可信的模型,概率与迭代次数成正比。
    3. 它要求设置跟问题相关的阀值。
    4. RANSAC只能从特定的数据集中估计出一个模型,如果存在两个(或多个)模型,RANSAC不能找到别的模型。
    5. 要求数学模型已知

    参考:https://zhuanlan.zhihu.com/p/36301702/
    https://blog.csdn.net/leonardohaig/article/details/104570965

  • 相关阅读:
    一个诡异的COOKIE问题
    PHP与JAVA构造函数的区别
    PHP获取上个月最后一天的一个容易忽略的问题
    jquery屏幕滚动计算事件总结
    Javascript 代理模式模拟一个文件同步功能
    Javascript实现HashTable类
    Javacript实现字典结构
    Javascript正则对象方法与字符串正则方法总结
    一个app,多个入口图标,activity-alias实现多程序入口并显示指定view完成
    javascript「篱式」条件判断
  • 原文地址:https://www.cnblogs.com/silence-cho/p/15143415.html
Copyright © 2020-2023  润新知