全景拼接学习笔记

作者: 开飞机的乔巴 | 来源:发表于2019-08-15 20:30 被阅读15次

    本博客内容来源于网络以及其他书籍,结合自己学习的心得进行重编辑,因为看了很多文章不便一一标注引用,如图片文字等侵权,请告知删除。

    传统2D计算机视觉学习笔记目录------->传送门
    传统3D计算机视觉学习笔记目录------->传送门

    之前几篇文章都介绍的关于视觉中的基础的算法概念,今天这篇文章我们来学习一下一个更有意思,能够实际应用的算法-- 全景拼接。可能文章中有一些方法,我们会从其他文章详解,所以没有进行详细描述。主要还是理解全景拼接的流程,对一些基础的概念有了解。所以我么开始。

    全景拼接简介

    全景拼接就是将多张图片进行缝合,生成一张视野更大的全景图。全景并不只是指环绕一周的图片,而是我们通过图像拼接技术创建更宽更高的场景。

    现在像360度的全景相机已经很普遍了,价格也有很大的下降,比如insta360等。这一类的全景相机基本上都是由两个鱼眼相机拼接而成,也有一些是通过多个广角相机拼接而成的。大家每个人都应该用过手机上的全景拍摄,通过移动相机,完成拍摄一张视角更宽广的图片,这里也用到全景拼接技术。图像拼接已经有很多的落地场景,但是仍旧有一些问题还有很多学者在优化。

    全景拼接一般流程

    下面只是对基础的全景拼接整理的流程,还有很多算法有很多流程来优化提高最后的结果。

    1. 对图片畸变进行矫正。
    2. 对图片提取特征点,对特征进行匹配,得到输入图像之间的映射关系T。
    3. 图像拼接,根据映射关系T进行图像的Warp变换,对齐图像。
    4. 图像融合,利用颜色调整来消除图像间的色差等方式来消除拼缝,多张图片融合为一张
    5. 定义全景映射模型,常用的包括:球面、柱面、平面,其中球面映射应用最为广泛。

    上面的流程,肯定有很多概念不知道什么意思,下面将对其进行解释。

    全景拼接流程中关键技术点详解

    • 图像矫正

    一般的全景相机会使用两颗或多颗广角或者鱼眼相机来节省成本。广角或者鱼眼相机的畸变都比较大,所以需要通过提前标定好的参数,对图片进行去畸变矫正。如果是正常的畸变较小的小孔相机则基本不需要。

    • 图片匹配

    所谓图片匹配就是找到相邻图片中相对应的点。主要有两种方式:

    1. 与特征无关的匹配方式,一般都用于没有复杂变换的图像拼接情况下,常见的为灰度相关性匹配,这种方法计算简单,仅仅通过灰度模板匹配。
    2. 根于特征进行匹配,常见特征有特征曲线,特征轮廓,特征点,特征点使用较多。分别在图像中找到相应的特征描述点,比如sift,surf,orb等,然后根据描述子,通过描述子的相似性来匹配两种图像中的点,找到对应匹配的点对。针对特征匹配具体讲解会在其他文章中讲解。
    • warp变换

    在上一步我们找到了许多对应的点对,通过这些点对来估算"单应矩阵",然后通过单应矩阵将待匹配的图片转换到原始图片的平面。

    我们来慢慢解释上面的疑问,首先现在我们来看看什么是单应矩阵?
    单应性矩阵为一3x3矩阵,描述了射影几何中平面到平面的映射关系,其自由度为8,由九个元素组成,通常令最后一个元素为1或者使其F范数为1,该矩阵可将无穷远点投射于有限处,即空间中平行线在图像上相交于有限处。

    上图中描述将image2平面中的点投影到image1中的点的变换矩阵即单应矩阵。上图两个投影中心在一点,但单应性矩阵可以表达出image2平面投影到image1平面的旋转和平移。

    现在我们知道单应矩阵是干什么用的,所以也可以将待匹配的图投影到原始图片的平面上了。现在需要知道的就是怎么来求单应矩阵?
    为了提高单应矩阵的正确性,要提高匹配准确率,一般可以配合上RANSAC算法的方式进行估计,ransac主要帮助筛选出匹配准确性更高的点对。

    假设两图像上的像点 p1(x1,y1) p2(x2,y2) 是一对匹配的点对,其单应矩阵为H,则有: 即:

    所以我们至少需要四对点对就可以算出单应矩阵。

    • 图像融合

    在拼接完成后,如果两幅图像因拍摄环境的不同,则会产生明显的的过渡区域(可以观察下面的实验结果),此时需要利用图像融合,将重叠区域的像素按两幅图像权重相加,使其能形成缓慢的过度。
    常用的方式有:基于金字塔的图像融合,根据距重叠区边缘的距离来设定对应的权重,或者是直接将两幅图的重叠区按某个比例权重相加。这是一种比较基础简单的方式,还有更多优秀的方式暂不介绍。

    • 投影映射

    对于360度的图片,可以说是一个全包围的图片,我们要把这个图片展开就需要投影映射,就像我们的世界地图一样,可以把平面的地图转换到到地球仪上进行观看。
    映射模型可以看作是用于图像映射的载体,相当于二维图像映射到三维空间的一种变换。

    选择合适的映射模型非常重要,需要与你的图像采集场景以及应用方式相匹配,一般对于水平拼接,采用柱面映射描述性最佳,而对于360°全景,球面映射或者立方体(多面体)映射的效果更好。 柱面映射模型 球体映射模型 立方体映射模型

    全景拼接中的难点

    • 当相邻照片重叠部分过少时,难以匹配
    • 当图片中有多处相似部分,难以消除错误匹配
    • 图片之间存在视差以及匹配误差,拼缝处有时难以达到光滑过度且不变形。
    • 不同图片的亮度对比度等之间的差异,使图像融合不自然
    • 图片在拼接处有动态的物体,无法剔除

    OpenCV 全景拼接效果展示[代码]

    代码较多,直接从主函数步骤比较简单来分析。

    #include <iostream>
    #include <opencv2/opencv.hpp>
    #include <vector>
    #include <boost/filesystem.hpp>
    #include <opencv2/xfeatures2d.hpp>
    std::vector<cv::Mat> process_input_images(std::vector<cv::Mat> input){
        std::vector<cv::Mat> gray_images;
        for(int i=0 ;i < input.size() ;i++){
            cv::Mat gray_image;
            cvtColor(input[i], gray_image, CV_RGB2GRAY);
            gray_images.push_back(gray_image);
        }
        return gray_images;
    }
    
    std::vector<std::string> get_all_image_file(std::string image_folder_path){
        boost::filesystem::path dirpath = image_folder_path;
        boost::filesystem::directory_iterator end;
        std::vector<std::string> files;
        for (boost::filesystem::directory_iterator iter(dirpath); iter != end; iter++)
        {
            boost::filesystem::path p = *iter;
            files.push_back(dirpath.string()+ "/"+ p.leaf().string());
        }
        std::sort(files.begin(),files.end());
        return files;
    }
    
    // 从文件夹中读取图片
    std::vector<cv::Mat> read_input_images(std::string image_folder_path)
    {
        std::vector<cv::Mat> images;
        std::vector<std::string> image_files_path = get_all_image_file(image_folder_path);
        for(int i=0; i<  image_files_path.size() ;i++){
            cv::Mat image;
            image = cv::imread(image_files_path[i]);
            images.push_back(image);
        }
        return images;
    }
    // 提取特征点
    std::vector< std::vector<cv::KeyPoint> > extracte_features(std::vector<cv::Mat> images){
        std::vector< std::vector<cv::KeyPoint> > keypoints;
        for(int i=0 ;i < images.size() ;i ++){
            std::vector<cv::KeyPoint> keypoint;
            cv::Ptr<cv::Feature2D> f2d = cv::xfeatures2d::SIFT::create();
            f2d->detect(images[i],keypoint);
            cv::Mat image_with_kp;
            cv::drawKeypoints(images[i], keypoint, image_with_kp, cv::Scalar::all(-1));
            cv::imwrite("image_with_kp"+std::to_string(i)+".png",image_with_kp);
            keypoints.push_back(keypoint);
        }
        return keypoints;
    }
    
    // 计算描述子
    std::vector<cv::Mat> extracte_descriptors(std::vector<cv::Mat> images,std::vector< std::vector<cv::KeyPoint> >keypoints){
        std::vector< cv::Mat > descriptors;
        for(int i=0 ;i < images.size() ;i ++){
            cv::Mat descriptor;
            cv::Ptr<cv::Feature2D> f2d = cv::xfeatures2d::SIFT::create();
            f2d->compute(images[i],keypoints[i],descriptor);
            descriptors.push_back(descriptor);
        }
        return descriptors;
    }
    
    // 匹配计算出两张图片的变换矩阵
    cv::Mat find_transfer(std::vector<cv::Mat> images,std::vector< std::vector<cv::KeyPoint> >keypoints,std::vector< cv::Mat > descriptors){
        cv::FlannBasedMatcher matcher;
        std::vector<cv::DMatch> matches, goodmatches;
        matcher.match(descriptors[0], descriptors[1], matches);
        cv::Mat  firstmatches;
        cv::drawMatches(images[0], keypoints[0], images[1], keypoints[1],
                matches, firstmatches, cv::Scalar::all(-1), cv::Scalar::all(-1),
                std::vector<char>(), cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
        cv::imwrite("firstmatches.png",firstmatches);
        double max_dist = 0; double min_dist = 1000;
        for (int i = 0; i < descriptors[0].rows; i++) {
            if (matches[i].distance > max_dist) {
                max_dist = matches[i].distance;
            }
            if (matches[i].distance < min_dist) {
                min_dist = matches[i].distance;
            }
        }
        for (int i = 0; i < descriptors[0].rows; i++) {
            if (matches[i].distance < 1.5 * min_dist) {
                goodmatches.push_back(matches[i]);
            }
        }
        cv::Mat img_matches;
        cv::drawMatches(images[0], keypoints[0], images[1], keypoints[1],
                goodmatches, img_matches, cv::Scalar::all(-1), cv::Scalar::all(-1),
                std::vector<char>(), cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
        cv::imwrite("img_matches.png",img_matches);
        std::vector<cv::Point2f> keypoints1, keypoints2;
        for (int i = 0; i < goodmatches.size(); i++) {
            keypoints1.push_back(keypoints[0][goodmatches[i].queryIdx].pt);
            keypoints2.push_back(keypoints[1][goodmatches[i].trainIdx].pt);
        }
        cv::Mat trans = findHomography(keypoints2, keypoints1, CV_RANSAC);
        return trans;
    }
    // 拼接出融合优化
    void OptimizeSeam(cv::Mat& img1, cv::Mat& trans, cv::Mat& dst,std::vector<cv::Point2f> corners)
    {
        int start = MIN(corners[0].x, corners[1].x);
        double processWidth = img1.cols - start;
        int rows = dst.rows;
        int cols = img1.cols; 
        double alpha = 1;
        for (int i = 0; i < rows; i++)
        {
            uchar* p = img1.ptr<uchar>(i);  
            uchar* t = trans.ptr<uchar>(i);
            uchar* d = dst.ptr<uchar>(i);
            for (int j = start; j < cols; j++)
            {
                if (t[j * 3] == 0 && t[j * 3 + 1] == 0 && t[j * 3 + 2] == 0)
                {
                    alpha = 1;
                }
                else
                {
                    alpha = (processWidth - (j - start)) / processWidth;
                }
                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);
            }
        }
    
    }
    // 拼接融合图片
    cv::Mat warp_image(std::vector<cv::Mat> images,cv::Mat trans,std::vector<cv::Point2f> corners){
        cv::Mat image_right_perspective ;
        cv::warpPerspective(images[1], image_right_perspective, trans, cv::Size(MAX(corners[2].x, corners[3].x), images[0].rows));
        cv::imwrite("image_right_perspective.png",image_right_perspective);
        int dst_width = image_right_perspective.cols;
        int dst_height = images[0].rows;
        cv::Mat dst(dst_height, dst_width, CV_8UC3);
        dst.setTo(0);
    
        image_right_perspective.copyTo(dst(cv::Rect(0, 0, image_right_perspective.cols, image_right_perspective.rows)));
        images[0].copyTo(dst(cv::Rect(0, 0, images[0].cols, images[0].rows)));
        cv::imwrite("before_opt.png",dst);
        OptimizeSeam(images[0],image_right_perspective,dst,corners);
        cv::imwrite("reult.png",dst);
        return dst;
    }
    // 就算变换后图片的四个角
    std::vector<cv::Point2f> cal_corner(std::vector<cv::Mat> images,cv::Mat trans){
        double v2[] = { 0, 0, 1 };
        double v1[3];
        std::vector<cv::Point2f> corners;
        cv::Point2f left_top,left_bottom,right_top,right_bottom;
        cv::Mat V2 = cv::Mat(3, 1, CV_64FC1, v2);  
        cv::Mat V1 = cv::Mat(3, 1, CV_64FC1, v1); 
    
        V1 = trans * V2;
        left_top.x = v1[0] / v1[2];
        left_top.y = v1[1] / v1[2];
    
        v2[0] = 0;
        v2[1] = images[1].rows;
        v2[2] = 1;
        V2 = cv::Mat(3, 1, CV_64FC1, v2);  
        V1 = cv::Mat(3, 1, CV_64FC1, v1);  
        V1 = trans * V2;
        left_bottom.x = v1[0] / v1[2];
        left_bottom.y = v1[1] / v1[2];
    
        v2[0] = images[1].cols;
        v2[1] = 0;
        v2[2] = 1;
        V2 = cv::Mat(3, 1, CV_64FC1, v2);  
        V1 = cv::Mat(3, 1, CV_64FC1, v1);  
        V1 = trans * V2;
        right_top.x = v1[0] / v1[2];
        right_top.y = v1[1] / v1[2];
    
        v2[0] = images[1].cols;
        v2[1] = images[1].rows;
        v2[2] = 1;
        V2 = cv::Mat(3, 1, CV_64FC1, v2);  
        V1 = cv::Mat(3, 1, CV_64FC1, v1);  
        V1 = trans * V2;
        right_bottom.x = v1[0] / v1[2];
        right_bottom.y = v1[1] / v1[2];
        corners.push_back(left_top);
        corners.push_back(left_bottom);
        corners.push_back(right_top);
        corners.push_back(right_bottom);
        return  corners;
    }
    
    int main(int argc, char *argv[])
    {
        std::vector<cv::Mat> input_images = read_input_images(argv[1]);
        // 两张图片拼接按步演示
        {
                std::vector<cv::Mat> two_input_images;
                two_input_images.push_back(input_images[0]);
                two_input_images.push_back(input_images[1]);
                std::vector<cv::Mat> gray_images = process_input_images(two_input_images);
                std::vector< std::vector<cv::KeyPoint> > keypoints = extracte_features(gray_images);
                std::vector<cv::Mat> descriptors = extracte_descriptors(gray_images,keypoints);
                cv::Mat trans = find_transfer(two_input_images,keypoints,descriptors);
                std::vector<cv::Point2f> corners =cal_corner(two_input_images,trans);
                cv::Mat after_warp = warp_image(two_input_images,trans,corners);
        }
        // opencv自带函数对多张图片进行拼接
        {
            cv::Stitcher stitch_all = cv::Stitcher::createDefault(true);
            cv::Mat pano;
            cv::Stitcher::Status status = stitch_all.stitch(input_images, pano);
            cv::imwrite("pano_all.png",pano);
        }
    }
    

    实验数据就是我在屋子外面拍的几张照片(都上传的时候都做了resize)

    图片1 图片2 图片3
    图片4 图片5 --

    两张拼接处理选择的第一张与第二张图片

    图片1提取关键点 图片2提取关键点

    配准效果(貌似看不清)

    初次匹配 过滤后
    变换后的右图 拼接后的图片 优化融合后的图片 opencv自带拼接拼图

    总结

    对于简单的版本的拼接算法,基本上还是免不了之前说的困难点,比如拼接处有重影,参数不好,容易匹配失误等。


    重要的事情说三遍:

    如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )

    如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )

    如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )

    传统2D计算机视觉学习笔记目录------->传送门
    传统3D计算机视觉学习笔记目录------->传送门

    任何人或团体、机构全部转载或者部分转载、摘录,请保留本博客链接或标注来源。博客地址:开飞机的乔巴

    作者简介:开飞机的乔巴(WeChat:zhangzheng-thu),现主要从事机器人抓取视觉系统以及三维重建等3D视觉相关方面,另外对slam以及深度学习技术也颇感兴趣,欢迎加我微信或留言交流相关工作。

    相关文章

      网友评论

        本文标题:全景拼接学习笔记

        本文链接:https://www.haomeiwen.com/subject/pnyajctx.html