[初识Opencv]_利用SURF特征实现图像的拼接

利用Surf实现图像的拼接

目前OpenCV已经有了可以实现图像拼接的类Stitcher可以实现图像拼接,不过我想先自己利用SURF特征实现图像拼接后再去看一下Opencv自带的Stitcher类。
参考资料:
https://www.cnblogs.com/skyfsm/p/7411961.html – SURF特征检测和拼接过程
环境:Win10 + VS2015 + OpenCV3.2

特征点:能够反映出图像特性的一些点(反应特征的规则不定)

分析图像时从图像中选取某些特征点并对图像进行局部分析,而非观察整幅图像,从而可以减少计算量,达到有效的分析目的。

SURF特征:加速稳健特征

改良版的SIFT;
1.尺度不变性:不仅在任何尺度下拍摄的物体都能检测到一致的关键点,而且每个被检测的特征点都对应一个尺度因子。
2.具有较高计算效率。

特征点匹配: 需要提取有优秀的匹配点

如果直接使用SURF的特征点来进行两个图片的特征点匹配,则会有许多糟糕的匹配点出现,会得到不理想的效果(瞎**匹),虽然其中也包括了良好的匹配点,但是如果不加处理会导致相匹配的点太多而得不到好的结果,因此需要对特征点进行选择,找到好的特征点。
SURF与SIFT的特征点选择类似:
SIFT的作者Lowe提出了比较最近邻距离与次近邻距离的SIFT匹配方式:取一幅图像中的一个SIFT关键点,并找出其与另一幅图像中欧式距离最近的前两个关键点,在这两个关键点中,如果最近的距离除以次近的距离得到的比率ratio少于某个阈值T,则接受这一对匹配点。因为对于错误匹配,由于特征空间的高维性,相似的距离可能有大量其他的错误匹配,从而它的ratio值比较高。显然降低这个比例阈值T,SIFT匹配点数目会减少,但更加稳定,反之亦然。
对于特征匹配的方法还有SIFT与ORB,SIFT很精确但是速度不够快,ORB没有旋转不变性和尺度不变性,因此我选择使用SURF特征检测。

代码

我的理解是,对于一个图像的拼接,我们需要两张图片,然后对其中一张图片做变换到另一张图片的坐标系下,然后对这两张图片拼接并优化拼接。因此程序的流程是:
1.特征点提取和匹配
2.图像配准(将一张图片转换到另一张图片的坐标系下)
3.图像拷贝
4.图像融合
完整代码如下:

#include "highgui.hpp"
#include "core.hpp"
#include "features2d.hpp"
#include "xfeatures2d.hpp"
#include "calib3d.hpp"
#include "cv.hpp"
#include 

using namespace std;
using namespace cv;
using namespace cv::xfeatures2d;

/*
    参数 Mat作为参数传递时 使用参数如果为 &img 则如果在调用 f(img)时修改Mat的值的话外面

*/

Mat stichingWithSURF(Mat mat1, Mat mat2);
void calCorners(const Mat& H,const Mat& src);//计算变换后的角点
Mat extractFeatureAndMatch(Mat mat1,Mat mat2); //特征提取和匹配
Mat splicImg(Mat& mat1, Mat& mat2, vector goodMatchPoints, vector keyPoint1, vector keyPoint2);
void optimizeSeam(Mat &mat1,Mat& trans,Mat& dst); //优化拼接

void stichingWithStitcher(Mat mat1, Mat mat2);

typedef struct
{
    //变换后的图片4个点
    Point2f left_top;
    Point2f left_bottom;
    Point2f right_top;
    Point2f right_bottom;
}four_corners_t;
four_corners_t corners;


void main()
{
    Mat img1, img2;
    img1 = imread("img_left.jpg");
    img2 = imread("img_right.jpg");
    resize(img1, img1, Size(img1.cols / 4, img1.rows / 4));
    resize(img2, img2, Size(img2.cols / 4, img2.rows / 4));

    Mat dst = stichingWithSURF(img1, img2);

    imshow("拼好的图像", dst);

    waitKey();
}

Mat stichingWithSURF(Mat mat1,Mat mat2)
{
    //用SURF 是因为 SURF有旋转不变性而且比SIFT更快 
    /*
        1.特征点提取和匹配
        2.图像配准
        3.图像拷贝
        4.图像融合

    */
    return extractFeatureAndMatch(mat1, mat2);

}

//定位图像变换之后的四个角点
void calCorners(const Mat & H, const Mat & src)
{
    //H为 变换矩阵  src为需要变换的图像

    //计算配准图的角点(齐次坐标系描述)
    double v2[] = { 0,0,1 }; //左上角
    double v1[3];  //变换后的坐标值
    //构成 列向量` 这种构成方式将 向量 与 Mat 关联, Mat修改 向量也相应修改
    Mat V2 = Mat(3, 1, CV_64FC1, v2); 
    Mat V1 = Mat(3, 1, CV_64FC1, v1);
    V1 = H * V2; //元素* 
    cout << "0v1:" << v1[0] << endl;
    cout << "V2: " << V2 << endl;
    cout << "V1: " << V1 << endl;

    //左上角(转换为一般的二维坐标系)
    corners.left_top.x = v1[0] / v1[2];
    corners.left_top.y = v1[1] / v1[2];

    //左下角(0,src.rows,1)
    v2[0] = 0;
    v2[1] = src.rows;
    v2[2] = 1;
    V2 = Mat(3, 1, CV_64FC1, v2);
    V1 = Mat(3, 1, CV_64FC1, v1);  //列向量
    V1 = H * V2;
    cout << "1v1:" << v1[0] << endl;
    cout << "V2: " << V2 << endl;
    cout << "V1: " << V1 << endl;
    corners.left_bottom.x = v1[0] / v1[2];
    corners.left_bottom.y = v1[1] / v1[2];

    //右上角(src.cols,0,1)
    v2[0] = src.cols;
    v2[1] = 0;
    v2[2] = 1;
    V2 = Mat(3, 1, CV_64FC1, v2);
    V1 = Mat(3, 1, CV_64FC1, v1);
    V1 = H * V2;
    cout << "2v1:" << v1 << endl;
    cout << "V2: " << V2 << endl;
    cout << "V1: " << V1 << endl;
    corners.right_top.x = v1[0] / v1[2];
    corners.right_top.y = v1[1] / v1[2];

    //右下角(src.cols,src.rows,1)
    v2[0] = src.cols;
    v2[1] = src.rows;
    v2[2] = 1;
    V2 = Mat(3, 1, CV_64FC1, v2);  //列向量
    V1 = Mat(3, 1, CV_64FC1, v1);  //列向量
    V1 = H * V2;
    cout << "3v1:" << v1 << endl;
    cout << "V2: " << V2 << endl;
    cout << "V1: " << V1 << endl;

    corners.right_bottom.x = v1[0] / v1[2];
    corners.right_bottom.y = v1[1] / v1[2];

    cout << endl;
    cout << "left_top:" << corners.left_top << endl;
    cout << "left_bottom:" << corners.left_bottom << endl;
    cout << "right_top:" << corners.right_top << endl;
    cout << "right_bottom:" << corners.right_bottom << endl;
}

Mat extractFeatureAndMatch(Mat mat1, Mat mat2)
{
    Mat matg1, matg2;
    //转化成灰度图
    cvtColor(mat1, matg1,CV_RGB2GRAY);
    cvtColor(mat2, matg2, CV_RGB2GRAY);


    Ptr surfDetector = SURF::create(1000.0f);
    vector keyPoint1, keyPoint2; //特征点
    Mat imgDesc1, imgDesc2; //特征点描述矩阵
    //检测 计算图像的关键点和描述
    surfDetector->detectAndCompute(matg1, noArray(), keyPoint1, imgDesc1);
    surfDetector->detectAndCompute(matg2, noArray(), keyPoint2, imgDesc2);

    cout << "特征点描述矩阵1大小:(列*行) " << imgDesc1.cols << " * " << imgDesc1.rows << endl;

    FlannBasedMatcher matcher;  //匹配点
    vector<vector> matchPoints; //
    vector goodMatchPoints; //良好的匹配点

    /*
        DMatch 特征匹配相关结构
        distance  两个特征向量之间的欧氏距离,越小表明匹配度越高。

    */


    //knn匹配特征点 这里将2作为训练集来训练 对应到后面DMatch的trainIdx
    vector train_disc(1, imgDesc2);
    matcher.add(train_disc);
    matcher.train();
    //用1来匹配该模型(用分类器去分类1),对应到后面DMatch的quiryIdx
    matcher.knnMatch(imgDesc1, matchPoints, 2);//k临近 按顺序排
    cout << "total match points: " << matchPoints.size() << endl;

    /*
    查找集(Query Set)和训练集(Train Set),
    对于每个Query descriptor,DMatch中保存了和其最好匹配的Train descriptor。
    */

    //获取优秀匹配点
    for (int i = 0; i < matchPoints.size(); i++)
    {
        if (matchPoints[i][0].distance < 0.4f*matchPoints[i][1].distance)
        {
            goodMatchPoints.push_back(matchPoints[i][0]);
        }

    }

    Mat firstMatch;
    //这里drawMatches 第一个图片在左边,同时也对应了DMatch的quiryIdx,第二个图片在右边,同时也对应了DMatch的trainIdx
    drawMatches(mat1, keyPoint1, mat2, keyPoint2, goodMatchPoints, firstMatch);

    imshow("匹配", firstMatch);

    vector imagePoints1, imagePoints2;
    for (int i = 0; i < goodMatchPoints.size(); i++)
    {
        imagePoints1.push_back(keyPoint1[goodMatchPoints[i].queryIdx].pt);
        imagePoints2.push_back(keyPoint2[goodMatchPoints[i].trainIdx].pt);
    }

    return splicImg(mat1, mat2, goodMatchPoints, keyPoint1, keyPoint2);

}

//以图像1为准(1在左半边)
Mat splicImg(Mat & mat_left, Mat & mat2, vector goodMatchPoints ,vector keyPoint1, vector keyPoint2)
{
    vector imagePoints1, imagePoints2;
    for (int i = 0; i < goodMatchPoints.size(); i++)
    {
        //这里的queryIdx代表了查询点的目录     trainIdx代表了在匹配时训练分类器所用的点的目录
        imagePoints1.push_back(keyPoint1[goodMatchPoints[i].queryIdx].pt);
        imagePoints2.push_back(keyPoint2[goodMatchPoints[i].trainIdx].pt);
    }

    //获取图像2到图像1的投影映射矩阵 3*3
    Mat homo = findHomography(imagePoints2, imagePoints1, CV_RANSAC);
    cout << "变换矩阵为:\n" << homo << endl << endl; //输出映射矩阵  

    calCorners(homo, mat2); //计算配准图的四个顶点坐标
    Mat imgTransform2;

    //图像配准 warpPerspective 对图像进行透视变换 变换后矩阵的宽高都变化
    warpPerspective(mat2, imgTransform2, homo, Size(MAX(corners.right_top.x, corners.right_bottom.x), mat2.rows));
    imshow("直接经过透视矩阵变换得到的img2", imgTransform2);

    //创建拼接后的图
    int distW = imgTransform2.cols; //长宽
    int distH = mat_left.rows;
    Mat dst(distH, distW, CV_8UC3);
    dst.setTo(0);

    //构成图片  
    //复制img2到dist的右半部分 先复制transform2的图片(因为这个尺寸比较大,后来的图片可以覆盖到他)
    imgTransform2.copyTo(dst(Rect(0, 0, imgTransform2.cols, imgTransform2.rows)));  
    mat_left.copyTo(dst(Rect(0, 0, mat_left.cols, mat_left.rows)));

    imshow("拼接(未优化)", dst);

    optimizeSeam(mat_left, imgTransform2, dst);

    return dst;
}



//优化链接处
void optimizeSeam(Mat &mat_left, Mat& trans, Mat& dst)
{
    int start = MIN(corners.left_bottom.x, corners.left_top.x);//重叠区域的左边界
    float processW = mat_left.cols - start; //重叠区的宽度
    cout << "开始值:" << start << endl;
    cout << "重叠宽度:" << processW << endl;
    int rows = dst.rows;
    int cols = mat_left.cols;
    float alpha = 1.0f; //mat1 中的像素透明度
    //修改dst中的透明度
    for (int i = 0; i < rows; i++)
    {
        //第i行地址
        uchar *p = mat_left.ptr(i);
        uchar *t = trans.ptr(i);
        uchar *d = dst.ptr(i);


        for (int j = start; j < cols; j++)
        {
            //遇到trans中无像素的黑点,则完全拷贝mat_left中的像素
            if (t[j * 3] == 0 && t[j * 3 + 1] == 0 && t[j * 3 + 2] == 0)
            {
                //RGB都为0
                alpha = 1;
            }
            else
            {
                //mat_left中像素的权重与当前处理点距重叠区域左边界的距离成正比
                alpha = (processW - (j - start)) / processW;

            }

            //修改dst中的像素
            d[j * 3] = p[j * 3] * alpha + t[j * 3] * (1 - alpha);
            d[j * 3 + 1] = p[j * 3 + 1] * alpha + t[j * 3 + 1] * (1 - alpha);
            d[j * 3 + 2] = p[j * 3 + 2] * alpha + t[j * 3 + 2] * (1 - alpha);

        }

    }

}

程序要点分析

  1. four_corners_t 这个结构:
    这个结构是用来在后面进行图像拼接之前,实现图像的变换的时候使用的用来存放变换之后的图像的四个角的坐标。后面会有。
  2. SURF特征检测和匹配的使用:
Ptr surfDetector = SURF::create(1000.0f);
vector keyPoint1, keyPoint2; //特征点
Mat imgDesc1, imgDesc2; //特征点描述矩阵
//检测 计算图像的关键点和描述
surfDetector->detectAndCompute(matg1, noArray(), keyPoint1, imgDesc1);
surfDetector->detectAndCompute(matg2, noArray(), keyPoint2, imgDesc2);

定义一个SURFDetector,参数为门限值,调整这个可以调整检测精度,越大越高,不过相应的速度也会慢;detectAndCompute函数实现了检测特征点并计算特征描述矩阵存储到imgDesc1, imgDesc2中。
3.接下来的匹配类DMatch类:这个类存储了图像特征之间的匹配的信息:
CV_PROP_RW int queryIdx; // query descriptor index 查询Index
CV_PROP_RW int trainIdx; // train descriptor index 训练Index
CV_PROP_RW int imgIdx; // train image index label?
CV_PROP_RW float distance; //特征点之间的欧氏距离
对这个类还不是很理解,不过他这三个变量 queryIdx trainIdx distance还是比较重要的。distance不用说,两个特征点之间的距离,trainIdx应该是在训练分类器时输入训练的点的Index;queryIdx是在利用分类器做回归的时候对应的Index (两个不同的图片,一个用来训练,一个用来做测试,训练与测试(分类)的对应的特征点应该是对应的)。
4.训练网络开始分类

FlannBasedMatcher matcher;  //匹配点
vector<vector> matchPoints; // 可以理解成二维矩阵 
vector goodMatchPoints; //良好的匹配点

//knn匹配特征点 这里将2作为训练集来训练 对应到后面DMatch的trainIdx
vector train_disc(1, imgDesc2);
matcher.add(train_disc);
matcher.train();
//用1来匹配该模型(用分类器去分类1),对应到后面DMatch的quiryIdx
matcher.knnMatch(imgDesc1, matchPoints, 2);//k临近 按顺序排

利用KNN实现分类,2代表了2个邻居。这里可以看到,为了让第一张图片出现在后面画图(画match结果)中的左边,让2图片的特征点输入进去做训练集,1图片作为测试集来实现回归(1去匹配2,因此后面2图片DMatch的index对应trainIdx,1图片对应quiryIdx),这样便可以得到两张图片对应的DMatch。

vector <vector> matchPoints

我理解成一个二维矩阵,行为当前正在做回归的点(img1中的),每列代表和当前点做回归的另一张图片(img2)的DMatch值。
5.获取优秀匹配点:

for (int i = 0; i < matchPoints.size(); i++)
{
    if (matchPoints[i][0].distance < 0.4f*matchPoints[i][1].distance)
    {
        goodMatchPoints.push_back(matchPoints[i][0]);
    }
}

根据欧氏距离选择匹配良好的点。

6.尝试着画一下匹配结果:

Mat firstMatch;

drawMatches(mat1, keyPoint1, mat2, keyPoint2, goodMatchPoints, firstMatch);
imshow("匹配", firstMatch);

这里,drawMatches 显示在窗口中时第一个图片(mat1)在左边,同时也对应了DMatch的quiryIdx,第二个图片(mat2)在右边,同时也对应了DMatch的trainIdx,这两个不能倒过来,否则可能会出现数组越界之类的恶心BUG。
[初识Opencv]_利用SURF特征实现图像的拼接_第1张图片

7.提取到特征之后对2图像进行变换,投影到图像1下
首先在得到变换矩阵之前,先得到特征点的Point2f类型的坐标:

vector imagePoints1, imagePoints2;
for (int i = 0; i < goodMatchPoints.size(); i++)
{
    //这里的queryIdx代表了查询点的目录     trainIdx代表了在匹配时训练分类器所用的点的目录
        imagePoints1.push_back(keyPoint1[goodMatchPoints[i].queryIdx].pt);
        imagePoints2.push_back(keyPoint2[goodMatchPoints[i].trainIdx].pt);
}

**一定要注意**queryIdx和trainIdx的对应!!
然后进行透视变换

//获取图像2到图像1的投影映射矩阵 3*3
Mat homo = findHomography(imagePoints2, imagePoints1, CV_RANSAC);
calCorners(homo, mat2); //计算配准图的四个顶点坐标
Mat imgTransform2; //变换过去之后的2图像
//图像配准 warpPerspective 对图像进行透视变换 变换后矩阵的宽高都变化
warpPerspective(mat2, imgTransform2, homo, Size(MAX(corners.right_top.x, corners.right_bottom.x), mat2.rows));

calCorners利用的是齐次坐标系计算坐标面比较方便。warpPerspective是OpenCV自带的透视变换函数。
[初识Opencv]_利用SURF特征实现图像的拼接_第2张图片

8.变换之后进行图像的复制,构成新的图片:

//创建拼接后的图
int distW = imgTransform2.cols; //长宽
int distH = mat_left.rows; //这里对应的mat1
Mat dst(distH, distW, CV_8UC3);
dst.setTo(0);

//构成图片  
//复制img2到dist的右半部分 先复制transform2的图片(因为这个尺寸比较大,后来的图片可以覆盖到他)
imgTransform2.copyTo(dst(Rect(0, 0, imgTransform2.cols, imgTransform2.rows)));  
mat_left.copyTo(dst(Rect(0, 0, mat_left.cols, mat_left.rows)));

copyTo函数在不使用Mask参数时复制的话,将图片黑色部分忽略,仅复制有颜色的部分。也就是黑色会被替换掉。所以要先复制imgTransform2,这个里面会因为变换而产生许多黑色的部分,然后再复制img1(也就是在左边 ,没有变换的图像)过去覆盖掉黑色。反过来的话黑色会把它覆盖掉。
[初识Opencv]_利用SURF特征实现图像的拼接_第3张图片

9.优化拼接
optimizeSeam函数来优化拼接,思想大概是alpha参数根据2图片(变化的图片,右侧的图片)与1重叠的位置来设置值,在重叠部分的值是由两个图片的像素值α加权得到的。注意下面的*3,因为RGB。
[初识Opencv]_利用SURF特征实现图像的拼接_第4张图片

基本上就是这样,利用SURF来实现拼接,可能还有啥没有总结到的地方,后面遇到问题再说吧。。。。

你可能感兴趣的:(OpenCV与计算机视觉)