RANSAC的代码和原理

一. RANSAC代码原理

https://blog.csdn.net/robinhjwy/article/details/79174914

二. RANSAC的Opencv代码

这里只摘抄和RANSAC有关的部分,摘抄自:https://blog.csdn.net/Darlingqiang/article/details/79775542

  1. OpenCV中此功能通过调用findHomography函数调用,下面是个例程:

[cpp] view plain copy

 #include <iostream>  
    #include "opencv2/opencv.hpp"  
    #include "opencv2/core/core.hpp"  
    #include "opencv2/features2d/features2d.hpp"  
    #include "opencv2/highgui/highgui.hpp"  
    using namespace cv;  
    using namespace std;  
    int main(int argc, char** argv)  
    {  
        Mat obj=imread("F:\\Picture\\obj.jpg");   //载入目标图像  
        Mat scene=imread("F:\\Picture\\scene.jpg"); //载入场景图像  
        if (obj.empty() || scene.empty() )  
        {  
            cout<<"Can't open the picture!\n";  
            return 0;  
        }  
        vector<KeyPoint> obj_keypoints,scene_keypoints;  
        Mat obj_descriptors,scene_descriptors;  
        ORB detector;     //采用ORB算法提取特征点  
        detector.detect(obj,obj_keypoints);  
        detector.detect(scene,scene_keypoints);  
        detector.compute(obj,obj_keypoints,obj_descriptors);  
        detector.compute(scene,scene_keypoints,scene_descriptors);  
        BFMatcher matcher(NORM_HAMMING,true); //汉明距离做为相似度度量  
        vector<DMatch> matches;  
        matcher.match(obj_descriptors, scene_descriptors, matches);  
        Mat match_img;  
        drawMatches(obj,obj_keypoints,scene,scene_keypoints,matches,match_img);  
        imshow("滤除误匹配前",match_img);  
      
        //保存匹配对序号  
        vector<int> queryIdxs( matches.size() ), trainIdxs( matches.size() );  
        for( size_t i = 0; i < matches.size(); i++ )  
        {  
            queryIdxs[i] = matches[i].queryIdx;  
            trainIdxs[i] = matches[i].trainIdx;  
        }     
      
        Mat H12;   //变换矩阵  
      
        vector<Point2f> points1; KeyPoint::convert(obj_keypoints, points1, queryIdxs);  
        vector<Point2f> points2; KeyPoint::convert(scene_keypoints, points2, trainIdxs);  
        int ransacReprojThreshold = 5;  //拒绝阈值  
      
      
        H12 = findHomography( Mat(points1), Mat(points2), CV_RANSAC, ransacReprojThreshold );  
        vector<char> matchesMask( matches.size(), 0 );    
        Mat points1t;  
        perspectiveTransform(Mat(points1), points1t, H12);  
        for( size_t i1 = 0; i1 < points1.size(); i1++ )  //保存‘内点’  
        {  
            if( norm(points2[i1] - points1t.at<Point2f>((int)i1,0)) <= ransacReprojThreshold ) //给内点做标记  
            {  
                matchesMask[i1] = 1;  
            }     
        }  
        Mat match_img2;   //滤除‘外点’后  
        drawMatches(obj,obj_keypoints,scene,scene_keypoints,matches,match_img2,Scalar(0,0,255),Scalar::all(-1),matchesMask);  
      
        //画出目标位置  
        std::vector<Point2f> obj_corners(4);  
        obj_corners[0] = cvPoint(0,0); obj_corners[1] = cvPoint( obj.cols, 0 );  
        obj_corners[2] = cvPoint( obj.cols, obj.rows ); obj_corners[3] = cvPoint( 0, obj.rows );  
        std::vector<Point2f> scene_corners(4);  
        perspectiveTransform( obj_corners, scene_corners, H12);  
        line( match_img2, scene_corners[0] + Point2f(static_cast<float>(obj.cols), 0),   
            scene_corners[1] + Point2f(static_cast<float>(obj.cols), 0),Scalar(0,0,255),2);  
        line( match_img2, scene_corners[1] + Point2f(static_cast<float>(obj.cols), 0),   
            scene_corners[2] + Point2f(static_cast<float>(obj.cols), 0),Scalar(0,0,255),2);  
        line( match_img2, scene_corners[2] + Point2f(static_cast<float>(obj.cols), 0),   
            scene_corners[3] + Point2f(static_cast<float>(obj.cols), 0),Scalar(0,0,255),2);  
        line( match_img2, scene_corners[3] + Point2f(static_cast<float>(obj.cols), 0),  
            scene_corners[0] + Point2f(static_cast<float>(obj.cols), 0),Scalar(0,0,255),2);  
      
        imshow("滤除误匹配后",match_img2);  
        waitKey(0);  
          
        return 0;  
    }  
  1. RANSAC源码解析

(1)findHomography内部调用cvFindHomography函数

srcPoints:目标图像点集

dstPoints:场景图像点集

method: 最小中值法、RANSAC方法、最小二乘法

ransacReprojTheshold:投影误差阈值

mask:掩码

[cpp] view plain copy

cvFindHomography( const CvMat* objectPoints, const CvMat* imagePoints,  
                      CvMat* __H, int method, double ransacReprojThreshold,  
                      CvMat* mask )  
    {  
        const double confidence = 0.995;  //置信度  
        const int maxIters = 2000;    //迭代最大次数  
        const double defaultRANSACReprojThreshold = 3; //默认拒绝阈值  
        bool result = false;  
        Ptr<CvMat> m, M, tempMask;  
      
        double H[9];  
        CvMat matH = cvMat( 3, 3, CV_64FC1, H );  //变换矩阵  
        int count;  
      
        CV_Assert( CV_IS_MAT(imagePoints) && CV_IS_MAT(objectPoints) );  
      
        count = MAX(imagePoints->cols, imagePoints->rows);  
        CV_Assert( count >= 4 );           //至少有4个样本  
        if( ransacReprojThreshold <= 0 )  
            ransacReprojThreshold = defaultRANSACReprojThreshold;  
      
        m = cvCreateMat( 1, count, CV_64FC2 );  //转换为齐次坐标  
        cvConvertPointsHomogeneous( imagePoints, m );  
      
        M = cvCreateMat( 1, count, CV_64FC2 );//转换为齐次坐标  
        cvConvertPointsHomogeneous( objectPoints, M );  
      
        if( mask )  
        {  
            CV_Assert( CV_IS_MASK_ARR(mask) && CV_IS_MAT_CONT(mask->type) &&  
                (mask->rows == 1 || mask->cols == 1) &&  
                mask->rows*mask->cols == count );  
        }  
        if( mask || count > 4 )  
            tempMask = cvCreateMat( 1, count, CV_8U );  
        if( !tempMask.empty() )  
            cvSet( tempMask, cvScalarAll(1.) );  
      
        CvHomographyEstimator estimator(4);  
        if( count == 4 )  
            method = 0;  
        if( method == CV_LMEDS )  //最小中值法  
            result = estimator.runLMeDS( M, m, &matH, tempMask, confidence, maxIters );  
        else if( method == CV_RANSAC )    //采用RANSAC算法  
            result = estimator.runRANSAC( M, m, &matH, tempMask, ransacReprojThreshold, confidence, maxIters);//(2)解析  
        else  
            result = estimator.runKernel( M, m, &matH ) > 0; //最小二乘法  
      
        if( result && count > 4 )  
        {  
            icvCompressPoints( (CvPoint2D64f*)M->data.ptr, tempMask->data.ptr, 1, count );  //保存内点集  
            count = icvCompressPoints( (CvPoint2D64f*)m->data.ptr, tempMask->data.ptr, 1, count );  
            M->cols = m->cols = count;  
            if( method == CV_RANSAC )  //  
                estimator.runKernel( M, m, &matH );  //内点集上采用最小二乘法重新估算变换矩阵  
            estimator.refine( M, m, &matH, 10 );//   
        }  
      
        if( result )  
            cvConvert( &matH, __H );  //保存变换矩阵  
      
        if( mask && tempMask )  
        {  
            if( CV_ARE_SIZES_EQ(mask, tempMask) )  
               cvCopy( tempMask, mask );  
            else  
               cvTranspose( tempMask, mask );  
        }  
      
        return (int)result;  
    }  

(2) runRANSAC
maxIters:最大迭代次数

m1,m2 :数据样本

model:变换矩阵

reprojThreshold:投影误差阈值

confidence:置信度 0.995

[cpp] view plain copy

bool CvModelEstimator2::runRANSAC( const CvMat* m1, const CvMat* m2, CvMat* model,  
                                        CvMat* mask0, double reprojThreshold,  
                                        double confidence, int maxIters )  
    {  
        bool result = false;  
        cv::Ptr<CvMat> mask = cvCloneMat(mask0);  
        cv::Ptr<CvMat> models, err, tmask;  
        cv::Ptr<CvMat> ms1, ms2;  
      
        int iter, niters = maxIters;  
        int count = m1->rows*m1->cols, maxGoodCount = 0;  
        CV_Assert( CV_ARE_SIZES_EQ(m1, m2) && CV_ARE_SIZES_EQ(m1, mask) );  
      
        if( count < modelPoints )  
            return false;  
      
        models = cvCreateMat( modelSize.height*maxBasicSolutions, modelSize.width, CV_64FC1 );  
        err = cvCreateMat( 1, count, CV_32FC1 );//保存每组点对应的投影误差  
        tmask = cvCreateMat( 1, count, CV_8UC1 );  
      
        if( count > modelPoints )  //多于4个点  
        {  
            ms1 = cvCreateMat( 1, modelPoints, m1->type );  
            ms2 = cvCreateMat( 1, modelPoints, m2->type );  
        }  
        else  //等于4个点  
        {  
            niters = 1; //迭代一次  
            ms1 = cvCloneMat(m1);  //保存每次随机找到的样本点  
            ms2 = cvCloneMat(m2);  
        }  
      
        for( iter = 0; iter < niters; iter++ )  //不断迭代  
        {  
            int i, goodCount, nmodels;  
            if( count > modelPoints )  
            {  
               //在(3)解析getSubset  
                bool found = getSubset( m1, m2, ms1, ms2, 300 ); //随机选择4组点,且三点之间不共线,(3)解析  
                if( !found )  
                {  
                    if( iter == 0 )  
                        return false;  
                    break;  
                }  
            }  
      
            nmodels = runKernel( ms1, ms2, models );  //估算近似变换矩阵,返回1  
            if( nmodels <= 0 )  
                continue;  
            for( i = 0; i < nmodels; i++ )//执行一次   
            {  
                CvMat model_i;  
                cvGetRows( models, &model_i, i*modelSize.height, (i+1)*modelSize.height );//获取3×3矩阵元素  
                goodCount = findInliers( m1, m2, &model_i, err, tmask, reprojThreshold );  //找出内点,(4)解析  
      
                if( goodCount > MAX(maxGoodCount, modelPoints-1) )  //当前内点集元素个数大于最优内点集元素个数  
                {  
                    std::swap(tmask, mask);  
                    cvCopy( &model_i, model );  //更新最优模型  
                    maxGoodCount = goodCount;  
                    //confidence 为置信度默认0.995,modelPoints为最少所需样本数=4,niters迭代次数  
                    niters = cvRANSACUpdateNumIters( confidence,  //更新迭代次数,(5)解析  
                        (double)(count - goodCount)/count, modelPoints, niters );  
                }  
            }  
        }  
      
        if( maxGoodCount > 0 )  
        {  
            if( mask != mask0 )  
                cvCopy( mask, mask0 );  
            result = true;  
        }  
      
        return result;  
    }  

step one niters最初的值为2000,这就是初始时的RANSAC算法的循环次数,getSubset()函数是从一组对应的序列中随机的选出4组(因为要想计算出一个3X3的矩阵,至少需要4组对应的坐标),m1和m2是我们输入序列,ms1和ms2是随机选出的对应的4组匹配。

随机的选出4组匹配后,就应该根据这4个匹配计算相应的矩阵,所以函数runKernel()就是根据4组匹配计算矩阵,参数里的models就是得到的矩阵。

stept wo 这个矩阵只是一个假设,为了验证这个假设,需要用其他的点去计算,看看其他的点是内点还是外点。

findInliers()函数就是用来计算内点的。利用前面得到的矩阵,把所有的序列带入,计算得出哪些是内点,哪些是外点,函数的返回值为goodCount,就是此次计算的内点的个数。

step three 函数中还有一个值为maxGoodCount,每次循环的内点个数的最大值保存在这个值中,一个估计的矩阵如果有越多的内点,那么这个矩阵就越有可能是正确的。

step four 所以计算内点个数以后,紧接着判断一下goodCount和maxGoodCount的大小关系,如果goodCount>maxGoodCount,则把goodCount赋值给maxGoodCount。赋值之后的一行代码非常关键,我们单独拿出来说一下,代码如下:

  niters = cvRANSACUpdateNumIters( confidence,  
                       (double)(count - goodCount)/count, modelPoints, niters );  

niters本来是迭代的次数,也就是循环的次数。但是通过这行代码我们发现,每次循环后,都会对niters这个值进行更新,也就是每次循环后都会改变循环的总次数。

cvRANSACUpdateNumIters()函数利用confidence(置信度)count(总匹配个数)goodCount(当前内点个数)niters(当前的总迭代次数)这几个参数,来动态的改变总迭代次数的大小。该函数的中心思想就是当内点占的比例较多时,那么很有可能已经找到了正确的估计,所以就适当的减少迭代次数来节省时间。这个迭代次数的减少是以指数形式减少的,所以节省的时间开销也是非常的可观。因此最初设计的2000的迭代次数,可能最终的迭代次数只有几十。同样的,如果你自己一开始把迭代次数设置成10000或者更大,进过几次迭代后,niters又会变得非常小了。所以初始时的niters设置的再大,其实对最终的运行时间也没什么影响。

所以,们现在应该清楚为什么输入数据增加,而算法运行时间不会增加了。openCV的RANSAC算法首先把迭代的次数设置为2000,然后再迭代的过程中,动态的改变总迭代次数,无论输入数据有多少,总的迭代次数不会增加,并且通过4个匹配计算出估计的矩阵这个时间是不变的,通过估计矩阵来计算内点,这方面的增加的时间开销基本上可以忽略。所以导致的最终结果就是,无论输入点有多少,运算时间基本不会有太大变化。

以上就是RANSAC算法的核心代码,其中用到的一些函数,下面一一给出。

(3)getSubset

ms1,ms2:保存随机找到的4个样本

maxAttempts:最大迭代次数,300

[cpp] view plain copy

bool CvModelEstimator2::getSubset( const CvMat* m1, const CvMat* m2,  
                                       CvMat* ms1, CvMat* ms2, int maxAttempts )  
    {  
        cv::AutoBuffer<int> _idx(modelPoints); //modelPoints 所需要最少的样本点个数  
        int* idx = _idx;  
        int i = 0, j, k, idx_i, iters = 0;  
        int type = CV_MAT_TYPE(m1->type), elemSize = CV_ELEM_SIZE(type);  
        const int *m1ptr = m1->data.i, *m2ptr = m2->data.i;  
        int *ms1ptr = ms1->data.i, *ms2ptr = ms2->data.i;  
        int count = m1->cols*m1->rows;  
      
        assert( CV_IS_MAT_CONT(m1->type & m2->type) && (elemSize % sizeof(int) == 0) );  
        elemSize /= sizeof(int); //每个数据占用字节数  
      
        for(; iters < maxAttempts; iters++)  
        {  
            for( i = 0; i < modelPoints && iters < maxAttempts; )  
            {  
                idx[i] = idx_i = cvRandInt(&rng) % count;  // 随机选取1组点  
                for( j = 0; j < i; j++ )  // 检测是否重复选择  
                    if( idx_i == idx[j] )  
                        break;  
                if( j < i )  
                    continue;  //重新选择  
                for( k = 0; k < elemSize; k++ )  //拷贝点数据  
                {  
                    ms1ptr[i*elemSize + k] = m1ptr[idx_i*elemSize + k];  
                    ms2ptr[i*elemSize + k] = m2ptr[idx_i*elemSize + k];  
                }  
                if( checkPartialSubsets && (!checkSubset( ms1, i+1 ) || !checkSubset( ms2, i+1 )))//检测点之间是否共线  
                {  
                    iters++;  //若共线,重新选择一组  
                    continue;  
                }  
                i++;  
            }  
            if( !checkPartialSubsets && i == modelPoints &&  
                (!checkSubset( ms1, i ) || !checkSubset( ms2, i )))  
                continue;  
            break;  
        }  
      
        return i == modelPoints && iters < maxAttempts;  //返回找到的样本点个数  
    }  

(4) findInliers & computerReprojError

[cpp] view plain copy

 int CvModelEstimator2::findInliers( const CvMat* m1, const CvMat* m2,  
                                        const CvMat* model, CvMat* _err,  
                                        CvMat* _mask, double threshold )  
    {  
        int i, count = _err->rows*_err->cols, goodCount = 0;  
        const float* err = _err->data.fl;  
        uchar* mask = _mask->data.ptr;  
      
        computeReprojError( m1, m2, model, _err );  //计算每组点的投影误差  
        threshold *= threshold;  
        for( i = 0; i < count; i++ )  
            goodCount += mask[i] = err[i] <= threshold;//误差在限定范围内,加入‘内点集’  
        return goodCount;  
    }  
    //--------------------------------------  
    void CvHomographyEstimator::computeReprojError( const CvMat* m1, const CvMat* m2,const CvMat* model, CvMat* _err )  
    {  
        int i, count = m1->rows*m1->cols;  
        const CvPoint2D64f* M = (const CvPoint2D64f*)m1->data.ptr;  
        const CvPoint2D64f* m = (const CvPoint2D64f*)m2->data.ptr;  
        const double* H = model->data.db;  
        float* err = _err->data.fl;  
      
        for( i = 0; i < count; i++ )        //保存每组点的投影误差,对应上述变换公式  
        {  
            double ww = 1./(H[6]*M[i].x + H[7]*M[i].y + 1.);      
            double dx = (H[0]*M[i].x + H[1]*M[i].y + H[2])*ww - m[i].x;  
            double dy = (H[3]*M[i].x + H[4]*M[i].y + H[5])*ww - m[i].y;  
            err[i] = (float)(dx*dx + dy*dy);  
        }  
    }  

(5)cvRANSACUpdateNumIters

对应上述k的计算公式

p:置信度

ep:外点比例

[cpp] view plain copy

 cvRANSACUpdateNumIters( double p, double ep,  
                            int model_points, int max_iters )  
    {  
        if( model_points <= 0 )  
            CV_Error( CV_StsOutOfRange, "the number of model points should be positive" );  
      
        p = MAX(p, 0.);  
        p = MIN(p, 1.);  
        ep = MAX(ep, 0.);  
        ep = MIN(ep, 1.);  
      
        // avoid inf's & nan's  
        double num = MAX(1. - p, DBL_MIN);  //num=1-p,做分子  
        double denom = 1. - pow(1. - ep,model_points);//做分母  
        if( denom < DBL_MIN )  
            return 0;  
      
        num = log(num);  
        denom = log(denom);  
      
        return denom >= 0 || -num >= max_iters*(-denom) ?  
            max_iters : cvRound(num/denom);  
    }  

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

在处理立体图像对的时候经常会用到对极几何的知识,计算基础矩阵也是很常见的事。OpenCV实现了基本矩阵的算法。对于老版本的C代码,计算基本矩阵的RANSAC方法中有一个迭代次数不收敛的bug,可能导致每次计算的采样次数都达到最大限制的次数,其根源在于计算采样可信度的公式有误,新版本的代码还没有仔细看过,不敢确定是否已经修正了这个bug。但是这个bug并不会对最后的结果造成多大影响,只是会影响计算的效率——原本几次采样即可结束迭代,在这个bug的影响下可能要采样数百次。

新版本的计算基本矩阵的函数的使用也有一些问题,下面来看看cv::findFundamentalMat函数:
//! the algorithm for finding fundamental matrix
enum
{
    FM_7POINT = CV_FM_7POINT, //!< 7-point algorithm
    FM_8POINT = CV_FM_8POINT, //!< 8-point algorithm
    FM_LMEDS = CV_FM_LMEDS,  //!< least-median algorithm
    FM_RANSAC = CV_FM_RANSAC  //!< RANSAC algorithm
};

//! finds fundamental matrix from a set of corresponding 2D points
CV_EXPORTS Mat findFundamentalMat( const Mat& points1, const Mat& points2,
                                     CV_OUT vector<uchar>& mask, int method=FM_RANSAC,
                                     double param1=3., double param2=0.99 );

//! finds fundamental matrix from a set of corresponding 2D points
CV_EXPORTS_W Mat findFundamentalMat( const Mat& points1, const Mat& points2,
                                     int method=FM_RANSAC,
                                     double param1=3., double param2=0.99 );

上面是OpenCV计算基本矩阵的C++接口,其内部实现仍然是调用的C代码函数,仅仅是在上层进行了一次封装。网上有些文档中说const Mat& points1和points2这两个参数可以直接由vector类型作为传入参数,其实这是错误的。直接用vector传递,在编译时可能不会报错,但是运行时会直接崩溃。因为cv::Mat的构造函数并不会把Point2f转化为两个浮点数存于Mat的两个元素中,而是仍然以Point2f存于Mat的一个元素中,于是findFundamentalMat一读取这个Mat就会出错。

因此我们最好老老实实地去构建Mat points1和Mat points2。该矩阵的维度可以是2xN,也可以是Nx2的,其中N是特征点的数目。另一个要注意的地方就是该矩阵的类型不能是CV_64F,也就是说findFundamentalMat内部解析points1和points2时是按照float类型去解析的,设为CV_64F将导致读取数据失败,程序崩溃。最好设为CV_32F。下面是从匹配好的特征点计算F的代码示例:

// vector<KeyPoint> m_LeftKey;
// vector<KeyPoint> m_RightKey;

// vector<DMatch> m_Matches;

// 以上三个变量已经被计算出来,分别是提取的关键点及其匹配,下面直接计算F

// 分配空间

int ptCount = (int)m_Matches.size();
Mat p1(ptCount, 2, CV_32F);
Mat p2(ptCount, 2, CV_32F);

// 把Keypoint转换为Mat

Point2f pt;
for (int i=0; i<ptCount; i++)
{
     pt = m_LeftKey[m_Matches[i].queryIdx].pt;
     p1.at<float>(i, 0) = pt.x;
     p1.at<float>(i, 1) = pt.y;
  
     pt = m_RightKey[m_Matches[i].trainIdx].pt;
     p2.at<float>(i, 0) = pt.x;
     p2.at<float>(i, 1) = pt.y;
}

// 用RANSAC方法计算F

// Mat m_Fundamental;

// 上面这个变量是基本矩阵

// vector<uchar> m_RANSACStatus;

// 上面这个变量已经定义过,用于存储RANSAC后每个点的状态

m_Fundamental = findFundamentalMat(p1, p2, m_RANSACStatus, FM_RANSAC);

// 计算野点个数

int OutlinerCount = 0;
for (int i=0; i<ptCount; i++)
{
     if (m_RANSACStatus[i] == 0) // 状态为0表示野点
     {
          OutlinerCount++;
     }
}

// 计算内点

// vector<Point2f> m_LeftInlier;
// vector<Point2f> m_RightInlier;
// vector<DMatch> m_InlierMatches;

// 上面三个变量用于保存内点和匹配关系

int InlinerCount = ptCount - OutlinerCount;
m_InlierMatches.resize(InlinerCount);
m_LeftInlier.resize(InlinerCount);
m_RightInlier.resize(InlinerCount);
InlinerCount = 0;
for (int i=0; i<ptCount; i++)
{
     if (m_RANSACStatus[i] != 0)
     {
          m_LeftInlier[InlinerCount].x = p1.at<float>(i, 0);
          m_LeftInlier[InlinerCount].y = p1.at<float>(i, 1);
          m_RightInlier[InlinerCount].x = p2.at<float>(i, 0);
          m_RightInlier[InlinerCount].y = p2.at<float>(i, 1);
          m_InlierMatches[InlinerCount].queryIdx = InlinerCount;
          m_InlierMatches[InlinerCount].trainIdx = InlinerCount;
          InlinerCount++;
     }
}

// 把内点转换为drawMatches可以使用的格式

vector<KeyPoint> key1(InlinerCount);
vector<KeyPoint> key2(InlinerCount);
KeyPoint::convert(m_LeftInlier, key1);
KeyPoint::convert(m_RightInlier, key2);

// 显示计算F过后的内点匹配

// Mat m_matLeftImage;
// Mat m_matRightImage;

// 以上两个变量保存的是左右两幅图像

Mat OutImage;
drawMatches(m_matLeftImage, key1, m_matRightImage, key2, m_InlierMatches, OutImage);
cvNamedWindow( "Match features", 1);
cvShowImage("Match features", &(IplImage(OutImage)));
cvWaitKey( 0 );
cvDestroyWindow( "Match features" );
————————————————

版权声明:本文为CSDN博主「sunshine_guoqiang」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Darlingqiang/article/details/79775542

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值