OpenCV总结3——图像拼接Stitching

之前折腾过一段时间配准发现自己写的一点都不准,最近需要进行图像的拼接,偶然的机会查到了opencv原来有拼接的库,发现opencv处理配准之外还做了许多的操作,就这个机会查找了相关的资料,同时也研究了以下他的源代码,做一个简单的总结。

Stitching

因为OpenCV已经将算法进行了高度的封装,所以用起来跟OpenGL类似,遵循了一条管线进行处理。
OpenCV总结3——图像拼接Stitching_第1张图片
上图是OpenCV官方网站中提供的流程图。

从这个图中我们也能看出这是一个很复杂的过程,源代码中提到了七个部分:

  • Features Finding and Images Matching:功能查找和图像匹配(感觉与配准的内容比较接近,都是计算特征点和匹配的过程)
  • Rotation Estimation:旋转估计
  • Autocalibration:自动校准
  • Images Warping:图像变形
  • Seam Estimation:接缝估计
  • Exposure Compensation:曝光补偿
  • Image Blenders :图像混合器

之前我做的只考虑了特征和变形,OpenCV这里的要素非常多,所以它的效果也很好。

看英文比较费事,自己做了个翻译:
OpenCV总结3——图像拼接Stitching_第2张图片
写个最简单的调用:

	vector<Mat> ss;
	Ptr<Stitcher> stitcher = Stitcher::create();
	ss.push_back(source_image);
	ss.push_back(target_image);
	Mat pano;
	Stitcher::Status status = stitcher->stitch(ss, pano);
	if (status != Stitcher::OK)
	{
		cout << "Can't stitch images, error code = " << int(status) << endl;
		return -1;
	}

通过上述的调用就可以得到一个效果不错的拼接全景图像。(这里调用的时候采用的是指针,surf和sift也是同样,因为OpenCV在2后有比较大的改版,所以调用全部改成了指针调用的形式),我在调用这个接口的时候图像只能是用三通道的,单通道会报错。

因为我这里使用的是OpenCV4,所以没有使用2的写法,有一个博客关于OpenCV2的拼接部分源码解析的特别好,很详细。https://blog.csdn.net/zhaocj/article/details/78798687

这里根据自己的理解对OpenCV4的源代码进行简单的解析:

创建接口

Ptr<Stitcher> Stitcher::create(Mode mode)
{
    Ptr<Stitcher> stitcher = makePtr<Stitcher>();//获取指针

    stitcher->setRegistrationResol(0.6);//配准分辨率,最大是1,如果想要获得较多的特征点要将这个值调大,因为它是缩放时乘以的系数之一
    stitcher->setSeamEstimationResol(0.1);//接缝处的分辨率
    stitcher->setCompositingResol(ORIG_RESOL);//合成分辨率
    stitcher->setPanoConfidenceThresh(1);//匹配置信度阈值
    stitcher->setSeamFinder(makePtr<detail::GraphCutSeamFinder>(detail::GraphCutSeamFinderBase::COST_COLOR));//基于最小图割的接缝查找
    stitcher->setBlender(makePtr<detail::MultiBandBlender>(false));//采用多段混合算法进行混合,细节(高频)采用较小的平滑渐变,宏观(低频)采用较大的平滑渐变
    stitcher->setFeaturesFinder(ORB::create());//设置特征查找的方法
    stitcher->setInterpolationFlags(INTER_LINEAR);//设置插值的方法

    stitcher->work_scale_ = 1;//计算是的尺度
    stitcher->seam_scale_ = 1;//接缝尺度
    stitcher->seam_work_aspect_ = 1;//接缝与计算的尺度比例
    stitcher->warped_image_scale_ = 1;//图像变换的尺度

    switch (mode)
    {
    case PANORAMA: // PANORAMA is the default
        // mostly already setup
        stitcher->setEstimator(makePtr<detail::HomographyBasedEstimator>());//单应矩阵估计
        stitcher->setWaveCorrection(true);//波校正
        stitcher->setWaveCorrectKind(detail::WAVE_CORRECT_HORIZ);//波校正的方法
        stitcher->setFeaturesMatcher(makePtr<detail::BestOf2NearestMatcher>(false));//特征点匹配方法knn
        stitcher->setBundleAdjuster(makePtr<detail::BundleAdjusterRay>());//相机光线修正,估计焦距
        stitcher->setWarper(makePtr<SphericalWarper>());//变换模式:球
        stitcher->setExposureCompensator(makePtr<detail::BlocksGainCompensator>());//曝光补偿
    break;

    case SCANS:
        stitcher->setEstimator(makePtr<detail::AffineBasedEstimator>());//仿射变换
        stitcher->setWaveCorrection(false);//不采用波修正
        stitcher->setFeaturesMatcher(makePtr<detail::AffineBestOf2NearestMatcher>(false, false));//knn
        stitcher->setBundleAdjuster(makePtr<detail::BundleAdjusterAffinePartial>());//估计仿射变换的参数
        stitcher->setWarper(makePtr<AffineWarper>());//仿射变换
        stitcher->setExposureCompensator(makePtr<detail::NoExposureCompensator>());//不进行曝光补偿
    break;

    default:
        CV_Error(Error::StsBadArg, "Invalid stitching mode. Must be one of Stitcher::Mode");
    break;
    }

    return stitcher;
}

原本在之前的版本中会有createDefault这个函数,但是在新版中没有了,只有一个调用的接口,所以以前的调用方式不能再使用。

以上代码中,接缝查找器具有不同的四种算法:NoSeamFinder(无需接缝查找)、PairwiseSeamFinder(逐点查找)、DpSeamFinder(动态规划查找)、GraphCutSeamFinder(最小图割查找),其中逐点查找只实现了voronoi图法(泰森多边形),接缝的具体内容查看接缝部分的解析。

混合采用多段混合,将图像按照频率展开金字塔,先按照不同频率平滑加权,然后把各个频率的分量加权,得到最后的结果,具体查看混合部分的解析。

特征查找的方法也有许多中:BRAISK(一种二进制的特征描述算子,适合较大模糊图像的配准)、ORB(快速特征点提取的算法,是FAST和BRIEF的结合,在此基础上进行优化改进,比surf快十倍,是sift的百倍)、MSER(最大极值稳定区域,利用分水岭算法得到形状稳定的区域)、FAST(如果像素与邻域内的大量像素点的差距较大,则认为是特征点)、AGAST(自适应通用加速分割检测,比surf和sift都快)、shi_tomas(为避免harris出现聚簇现象特出的一种算法)、blob(斑点特征,blob是一块联通区域,通过对前后背景的二值化,进行联通区域的提取与标记)、KAZE、AKAZE(非线性尺度空间检测,保证图像边缘尺度变化中信息损失较少,保留细节信息),具体查看配准相关信息。

图像的变换方式分为两种:单应变换和仿射变换,单应变换会将一张图中的特征点映射到另一张图的特征点上面,仿射变换是整张图像的变换,并且仿射变换会保留线的并行性。

波校正分为水平和垂直方向。

拼接接口

Stitcher::Status Stitcher::stitch(InputArrayOfArrays images, OutputArray pano)
{
	//默认是需要遮罩的,如果没有遮罩,则传入空的图像
    return stitch(images, noArray(), pano);
}


Stitcher::Status Stitcher::stitch(InputArrayOfArrays images, InputArrayOfArrays masks, OutputArray pano)
{
    CV_INSTRUMENT_REGION();

    Status status = estimateTransform(images, masks);//估计旋转矩阵也就是计算特征进行匹配以及旋转等相关内容,也就是流程图中的上半部分
    if (status != OK)
        return status;
    return composePanorama(pano);
}

上面的代码是我们调用的接口代码,将整个过程分为了两个大的部分,一部分进行配准等操作,另一部分进行融合操作。

配准

Stitcher::Status Stitcher::estimateTransform(InputArrayOfArrays images, InputArrayOfArrays masks)
{
    CV_INSTRUMENT_REGION();

    images.getUMatVector(imgs_);//这里将图像转化为GPU中可以使用的图像,也就是说使图像支持并行处理,遵循opencl标准
    masks.getUMatVector(masks_);

    Status status;

    if ((status = matchImages()) != OK)//匹配图像
        return status;

    if ((status = estimateCameraParams()) != OK)//估计相机参数
        return status;

    return OK;
}

这里的估计变换部分又分成了两部分,一部分用于匹配图像,另一部分用于相机参数的估计。

匹配图像

以下是图像匹配部分的代码:

Stitcher::Status Stitcher::matchImages()
{
    if ((int)imgs_.size() < 2)//因为配准需要两幅图以上,所以在调用接口的时候也可以看到传入的是图像的数组
    {
        LOGLN("Need more images");
        return ERR_NEED_MORE_IMGS;
    }

    work_scale_ = 1;//匹配尺度
    seam_work_aspect_ = 1;//匹配尺度和接缝尺度的比值
    seam_scale_ = 1;//接缝尺度
    bool is_work_scale_set = false;//匹配尺度是否被设置
    bool is_seam_scale_set = false;//接缝尺度是否被设置
    features_.resize(imgs_.size());//features是保存所有图像特征的一个数组,所以数量上是一一对应的,所以此处对特征数组的数量进行设定
    seam_est_imgs_.resize(imgs_.size());//接缝估计数组,因为在整个处理过程中还有接缝误差的估计,所以这里有接缝相关的内容
    full_img_sizes_.resize(imgs_.size());//保存每个图像的尺寸

    LOGLN("Finding features...");
#if ENABLE_LOG
    int64 t = getTickCount();//返回系统启动经过的时间
#endif

    std::vector<UMat> feature_find_imgs(imgs_.size());//保存进行过尺度处理的图像
    std::vector<UMat> feature_find_masks(masks_.size());//保存图像的遮罩

    for (size_t i = 0; i < imgs_.size(); ++i)
    {
        full_img_sizes_[i] = imgs_[i].size();//保存图像的大小
        if (registr_resol_ < 0)//判断配准分辨率,默认值是-1
        {//分辨率-1应该表示尺度不变
            feature_find_imgs[i] = imgs_[i];//保存图像
            work_scale_ = 1;//修改配准尺度
            is_work_scale_set = true;
        }
        else
        {	
            if (!is_work_scale_set)//没有设置配准尺度,需要设置
            {
                work_scale_ = std::min(1.0, std::sqrt(registr_resol_ * 1e6 / full_img_sizes_[i].area()));//为配准分辨率乘以10的6次方,除以图像的面积,计算一个新的配准尺度
                is_work_scale_set = true;
            }
            resize(imgs_[i], feature_find_imgs[i], Size(), work_scale_, work_scale_, INTER_LINEAR_EXACT);//缩放至配准尺度,精确双线性插值
        }
        if (!is_seam_scale_set)//没有接缝尺度,设置接缝尺度
        {
            seam_scale_ = std::min(1.0, std::sqrt(seam_est_resol_ * 1e6 / full_img_sizes_[i].area()));//同样计算接缝尺度
            seam_work_aspect_ = seam_scale_ / work_scale_;//改变比例大小
            is_seam_scale_set = true;
        }

        if (!masks_.empty())//如果所有的区域都寻找特征,没有遮罩
        {
            resize(masks_[i], feature_find_masks[i], Size(), work_scale_, work_scale_, INTER_NEAREST);//将遮罩缩放至配准图像大小,最邻近插值
        }
        features_[i].img_idx = (int)i;//标识当前特征属于哪张图像
        LOGLN("Features in image #" << i+1 << ": " << features_[i].keypoints.size());

        resize(imgs_[i], seam_est_imgs_[i], Size(), seam_scale_, seam_scale_, INTER_LINEAR_EXACT);//设置接缝图像,缩放至接缝尺度
    }

    // find features possibly in parallel
    detail::computeImageFeatures(features_finder_, feature_find_imgs, features_, feature_find_masks);//查找图像的特征信息,这是一个并行的过程

    // Do it to save memory
    feature_find_imgs.clear();//清空图像节省内存
    feature_find_masks.clear();

    LOGLN("Finding features, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec");

    LOG("Pairwise matching");
#if ENABLE_LOG
    t = getTickCount();
#endif
    (*features_matcher_)(features_, pairwise_matches_, matching_mask_);//对特征进行进行成对的匹配,其中mask代表的是哪些图像是需要进行匹配的,不是抠图是用的mask,其中会用到一些并行的加速方法
    features_matcher_->collectGarbage();//回收内存
    LOGLN("Pairwise matching, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec");

    // Leave only images we are sure are from the same panorama
    indices_ = detail::leaveBiggestComponent(features_, pairwise_matches_, (float)conf_thresh_);//通过置信度判断,小于置信度的图像认为不在同一幅全景图中,被舍弃
    std::vector<UMat> seam_est_imgs_subset;
    std::vector<UMat> imgs_subset;
    std::vector<Size> full_img_sizes_subset;
    for (size_t i = 0; i < indices_.size(); ++i)//提取出可以用于拼接的子集图像
    {
        imgs_subset.push_back(imgs_[indices_[i]]);
        seam_est_imgs_subset.push_back(seam_est_imgs_[indices_[i]]);
        full_img_sizes_subset.push_back(full_img_sizes_[indices_[i]]);
    }
    seam_est_imgs_ = seam_est_imgs_subset;//将当前的图像集修改为可以拼接的子集
    imgs_ = imgs_subset;
    full_img_sizes_ = full_img_sizes_subset;

    if ((int)imgs_.size() < 2)//子集只有两张以上才可以拼接
    {
        LOGLN("Need more images");
        return ERR_NEED_MORE_IMGS;
    }

    return OK;
}

特征结构体:ImageFeatures
上面代码中用来保存特征的数组的类型是下面的这个结构体。

struct CV_EXPORTS ImageFeatures
{
    int img_idx;//图像标号
    Size img_size;//图像大小
    std::vector<KeyPoint> keypoints;//特征点
    UMat descriptors;//特征描述
};

图像特征并行查找:computeImageFeatures
有两个重载的函数,一个是用于单张图像的,另一个是用于多张图像的并行处理,主要使用的就是配准过程中用到的函数

void computeImageFeatures(
    const Ptr<Feature2D> &featuresFinder,
    InputArrayOfArrays  images,
    std::vector<ImageFeatures> &features,
    InputArrayOfArrays masks)
{//对于多张图像进行一个加速处理
    // compute all features
    std::vector<std::vector<KeyPoint>> keypoints;//特征点
    std::vector<UMat> descriptors;//描述
    // TODO replace with 1 call to new over load of detectAndCompute
    featuresFinder->detect(images, keypoints, masks);//查找所有图像特征点
    featuresFinder->compute(images, keypoints, descriptors);//计算所有图像的特征描述

    // store to ImageFeatures
    size_t count = images.total();
    features.resize(count);//保存计算后的特征信息ImageFeatures
    CV_Assert(count == keypoints.size() && count == descriptors.size());
    for (size_t i = 0; i < count; ++i)
    {
        features[i].img_size = images.size(int(i));
        features[i].keypoints = std::move(keypoints[i]);
        features[i].descriptors = std::move(descriptors[i]);
    }
}

void computeImageFeatures(
    const Ptr<Feature2D> &featuresFinder,
    InputArray image,
    ImageFeatures &features,
    InputArray mask)
{//单张图像的处理
    features.img_size = image.size();
    featuresFinder->detectAndCompute(image, mask, features.keypoints, features.descriptors);
}

去除置信度低的图像

std::vector<int> leaveBiggestComponent(std::vector<ImageFeatures> &features,  std::vector<MatchesInfo> &pairwise_matches,
                                      float conf_threshold)
{
    const int num_images = static_cast<int>(features.size());//获得图像的数量

    DisjointSets comps(num_images);//不相交集,一种用于求最小生成树等的通用数据结构
    for (int i = 0; i < num_images; ++i)
    {
        for (int j = 0; j < num_images; ++j)
        {
            if (pairwise_matches[i*num_images + j].confidence < conf_threshold)//判断置信度
                continue;
            int comp1 = comps.findSetByElem(i);//查找元素
            int comp2 = comps.findSetByElem(j);
            if (comp1 != comp2)
                comps.mergeSets(comp1, comp2);//将rank小的集合合并到大的集合
        }
    }

    int max_comp = static_cast<int>(std::max_element(comps.size.begin(), comps.size.end()) - comps.size.begin());//查找元素中的最大值的标号

    std::vector<int> indices;
    std::vector<int> indices_removed;
    for (int i = 0; i < num_images; ++i)
        if (comps.findSetByElem(i) == max_comp)// 是否与置信度最大值有关
            indices.push_back(i);//保存标号
        else
            indices_removed.push_back(i);

    std::vector<ImageFeatures> features_subset;//特征子集
    std::vector<MatchesInfo> pairwise_matches_subset;//匹配子集
    for (size_t i = 0; i < indices.size(); ++i)
    {
        features_subset.push_back(features[indices[i]]);//将置信度高的特征提取出来
        for (size_t j = 0; j < indices.size(); ++j)
        {
            pairwise_matches_subset.push_back(pairwise_matches[indices[i]*num_images + indices[j]]);//抽取对应的匹配子集
            pairwise_matches_subset.back().src_img_idx = static_cast<int>(i);
            pairwise_matches_subset.back().dst_img_idx = static_cast<int>(j);
        }
    }

    if (static_cast<int>(features_subset.size()) == num_images)
        return indices;//返回需要匹配的图像标号

    LOG("Removed some images, because can't match them or there are too similar images: (");
    LOG(indices_removed[0] + 1);
    for (size_t i = 1; i < indices_removed.size(); ++i)
        LOG(", " << indices_removed[i]+1);
    LOGLN(").");
    LOGLN("Try to decrease the match confidence threshold and/or check if you're stitching duplicates.");
	//改变特征集合匹配集
    features = features_subset;
    pairwise_matches = pairwise_matches_subset;

    return indices;
}

DisjointSets

class CV_EXPORTS DisjointSets
{
public:
    DisjointSets(int elem_count = 0) { createOneElemSets(elem_count); }

    void createOneElemSets(int elem_count);
    int findSetByElem(int elem);
    int mergeSets(int set1, int set2);//低rank的合并在高rank中

    std::vector<int> parent;
    std::vector<int> size;

private:
    std::vector<int> rank_;
};

不相交集,用于计算最小树,联通集等,具体的之后再研究,这就是一个比较通用的数据结构。

以上是图像配准部分的代码。

相机参数估计

Stitcher::Status Stitcher::estimateCameraParams()//相机参数估计
{	//全局框架下估计单应性
    // estimate homography in global frame
    if (!(*estimator_)(features_, pairwise_matches_, cameras_))
        return ERR_HOMOGRAPHY_EST_FAIL;

    for (size_t i = 0; i < cameras_.size(); ++i)
    {
        Mat R;
        cameras_[i].R.convertTo(R, CV_32F);//旋转矩阵转化为浮点型
        cameras_[i].R = R;//保存旋转矩阵
        //LOGLN("Initial intrinsic parameters #" << indices_[i] + 1 << ":\n " << cameras_[i].K());
    }
	//因为噪声的存在使得图像的投影出现误差,通过进行最小化误差来进行修正
    bundle_adjuster_->setConfThresh(conf_thresh_);//光束平差法设置置信度
    if (!(*bundle_adjuster_)(features_, pairwise_matches_, cameras_))//利用光束平差法精确相机参数
        return ERR_CAMERA_PARAMS_ADJUST_FAIL;
	//找到焦距,并且使它作为图像最后的尺度
    // Find median focal length and use it as final image scale
    std::vector<double> focals;//保存焦距
    for (size_t i = 0; i < cameras_.size(); ++i)
    {//遍历相机参数得到焦距
        //LOGLN("Camera #" << indices_[i] + 1 << ":\n" << cameras_[i].K());
        focals.push_back(cameras_[i].focal);
    }

    std::sort(focals.begin(), focals.end());//焦距排序
    if (focals.size() % 2 == 1)//将焦距中间值作为图像的尺度
        warped_image_scale_ = static_cast<float>(focals[focals.size() / 2]);
    else
        warped_image_scale_ = static_cast<float>(focals[focals.size() / 2 - 1] + focals[focals.size() / 2]) * 0.5f;

    if (do_wave_correct_)//波形修正
    {
        std::vector<Mat> rmats;
        for (size_t i = 0; i < cameras_.size(); ++i)
            rmats.push_back(cameras_[i].R.clone());//得到相机的旋转矩阵
        detail::waveCorrect(rmats, wave_correct_kind_);//对相机旋转矩阵进行校正从而优化相机的参数
        for (size_t i = 0; i < cameras_.size(); ++i)
            cameras_[i].R = rmats[i];//将优化后的旋转矩阵重新保存
    }

    return OK;
}

以上是对相机参数的估计以及优化,波校正和不相交集之后再说。
相机参数:CameraParams

struct CV_EXPORTS CameraParams
{
    CameraParams();
    CameraParams(const CameraParams& other);
    CameraParams& operator =(const CameraParams& other);
    Mat K() const;

    double focal; // 焦距
    double aspect; // 纵横比
    double ppx; // 主要点x
    double ppy; // 主要点y
    Mat R; // 旋转
    Mat t; // 平移
};

以上的所有代码完成了对于图像特征的估计以及相机的估计,得到了有关变换的参数。

全景融合

Stitcher::Status Stitcher::composePanorama(InputArrayOfArrays images, OutputArray pano)
{
    CV_INSTRUMENT_REGION();

    LOGLN("Warping images (auxiliary)... ");

    std::vector<UMat> imgs;
    images.getUMatVector(imgs);//获得可以并行计算的图像
    if (!imgs.empty())
    {
        CV_Assert(imgs.size() == imgs_.size());

        UMat img;
        seam_est_imgs_.resize(imgs.size());//接缝数量

        for (size_t i = 0; i < imgs.size(); ++i)
        {
            imgs_[i] = imgs[i];
            resize(imgs[i], img, Size(), seam_scale_, seam_scale_, INTER_LINEAR_EXACT);//将图像缩放至接缝尺度
            seam_est_imgs_[i] = img.clone();//保存缩放后的图像
        }

        std::vector<UMat> seam_est_imgs_subset;//用于保存需要融合的图像子集
        std::vector<UMat> imgs_subset;

        for (size_t i = 0; i < indices_.size(); ++i)
        {
            imgs_subset.push_back(imgs_[indices_[i]]);
            seam_est_imgs_subset.push_back(seam_est_imgs_[indices_[i]]);
        }

        seam_est_imgs_ = seam_est_imgs_subset;
        imgs_ = imgs_subset;//重新赋值
    }

    UMat pano_;//保存全景图

#if ENABLE_LOG
    int64 t = getTickCount();
#endif

    std::vector<Point> corners(imgs_.size());//保存图像的左上角的位置
    std::vector<UMat> masks_warped(imgs_.size());//遮罩变换
    std::vector<UMat> images_warped(imgs_.size());//图像变换
    std::vector<Size> sizes(imgs_.size());//尺寸
    std::vector<UMat> masks(imgs_.size());//遮罩

    // Prepare image masks
    for (size_t i = 0; i < imgs_.size(); ++i)//准备遮罩图像
    {
        masks[i].create(seam_est_imgs_[i].size(), CV_8U);
        masks[i].setTo(Scalar::all(255));
    }

    // Warp images and their masks
    Ptr<detail::RotationWarper> w = warper_->create(float(warped_image_scale_ * seam_work_aspect_));//设置映射的尺度
    for (size_t i = 0; i < imgs_.size(); ++i)
    {
        Mat_<float> K;//相机内参数
        cameras_[i].K().convertTo(K, CV_32F);//转化为浮点类型
        K(0,0) *= (float)seam_work_aspect_;//缩放至接缝尺度
        K(0,2) *= (float)seam_work_aspect_;
        K(1,1) *= (float)seam_work_aspect_;
        K(1,2) *= (float)seam_work_aspect_;
		//对图像进行投影变换,得到变换后的图像以及左上角点的坐标
        corners[i] = w->warp(seam_est_imgs_[i], K, cameras_[i].R, interp_flags_, BORDER_REFLECT, images_warped[i]);//边缘使用反向重复
        sizes[i] = images_warped[i].size();//获得变换后的图像大小

        w->warp(masks[i], K, cameras_[i].R, INTER_NEAREST, BORDER_CONSTANT, masks_warped[i]);//变换掩码
    }


    LOGLN("Warping images, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec");
	//在找到接缝之前进行曝光补偿
    // Compensate exposure before finding seams
    exposure_comp_->feed(corners, images_warped, masks_warped);//计算曝光补偿的系数
    for (size_t i = 0; i < imgs_.size(); ++i)
        exposure_comp_->apply(int(i), corners[i], images_warped[i], masks_warped[i]);//曝光补偿

    // Find seams
    std::vector<UMat> images_warped_f(imgs_.size());
    for (size_t i = 0; i < imgs_.size(); ++i)
        images_warped[i].convertTo(images_warped_f[i], CV_32F);
    seam_finder_->find(images_warped_f, corners, masks_warped);//查找缝隙,并判断缝隙处的像素值

    // Release unused memory
    seam_est_imgs_.clear();
    images_warped.clear();
    images_warped_f.clear();
    masks.clear();

    LOGLN("Compositing...");
#if ENABLE_LOG
    t = getTickCount();
#endif

    UMat img_warped, img_warped_s;
    UMat dilated_mask, seam_mask, mask, mask_warped;

    //double compose_seam_aspect = 1;
    double compose_work_aspect = 1;//合成全景图的工作尺度
    bool is_blender_prepared = false;//融合

    double compose_scale = 1;//合成尺度
    bool is_compose_scale_set = false;

    std::vector<detail::CameraParams> cameras_scaled(cameras_);//加载相机

    UMat full_img, img;
    for (size_t img_idx = 0; img_idx < imgs_.size(); ++img_idx)
    {
        LOGLN("Compositing image #" << indices_[img_idx] + 1);
#if ENABLE_LOG
        int64 compositing_t = getTickCount();
#endif

        // Read image and resize it if necessary
        full_img = imgs_[img_idx];//遍历图像
        if (!is_compose_scale_set)//是否设置了合成尺度
        {
            if (compose_resol_ > 0)//合成分辨率
                compose_scale = std::min(1.0, std::sqrt(compose_resol_ * 1e6 / full_img.size().area()));//设置合成分辨率
            is_compose_scale_set = true;

            // Compute relative scales
            //compose_seam_aspect = compose_scale / seam_scale_;
            compose_work_aspect = compose_scale / work_scale_;//计算相对尺度

            // Update warped image scale
            float warp_scale = static_cast<float>(warped_image_scale_ * compose_work_aspect);/更新变换尺度
            w = warper_->create(warp_scale);//设置新的映射尺度

            // Update corners and sizes
            for (size_t i = 0; i < imgs_.size(); ++i)
            {
                // Update intrinsics
                cameras_scaled[i].ppx *= compose_work_aspect;//根据尺度更新参数
                cameras_scaled[i].ppy *= compose_work_aspect;
                cameras_scaled[i].focal *= compose_work_aspect;

                // Update corner and size
                Size sz = full_img_sizes_[i];//更新大小
                if (std::abs(compose_scale - 1) > 1e-1)//图像的尺度1+-0.1
                {
                    sz.width = cvRound(full_img_sizes_[i].width * compose_scale);
                    sz.height = cvRound(full_img_sizes_[i].height * compose_scale);
                }

                Mat K;
                cameras_scaled[i].K().convertTo(K, CV_32F);
                Rect roi = w->warpRoi(sz, K, cameras_scaled[i].R);//计算大小为sz的局部区域变换后的结果
                corners[i] = roi.tl();//保存新的边缘位置
                sizes[i] = roi.size();
            }
        }
        if (std::abs(compose_scale - 1) > 1e-1)//合成尺度在一定的范围内
        {
#if ENABLE_LOG
            int64 resize_t = getTickCount();
#endif
            resize(full_img, img, Size(), compose_scale, compose_scale, INTER_LINEAR_EXACT);//图像缩放至合成尺寸
            LOGLN("  resize time: " << ((getTickCount() - resize_t) / getTickFrequency()) << " sec");
        }
        else
            img = full_img;
        full_img.release();
        Size img_size = img.size();

        LOGLN(" after resize time: " << ((getTickCount() - compositing_t) / getTickFrequency()) << " sec");

        Mat K;
        cameras_scaled[img_idx].K().convertTo(K, CV_32F);//将相机内参数矩阵转化为浮点数

#if ENABLE_LOG
        int64 pt = getTickCount();
#endif
        // Warp the current image
        w->warp(img, K, cameras_[img_idx].R, interp_flags_, BORDER_REFLECT, img_warped);//重新计算变换
        LOGLN(" warp the current image: " << ((getTickCount() - pt) / getTickFrequency()) << " sec");
#if ENABLE_LOG
        pt = getTickCount();
#endif

        // Warp the current image mask
        mask.create(img_size, CV_8U);
        mask.setTo(Scalar::all(255));
        w->warp(mask, K, cameras_[img_idx].R, INTER_NEAREST, BORDER_CONSTANT, mask_warped);//重新计算遮罩的变化
        LOGLN(" warp the current image mask: " << ((getTickCount() - pt) / getTickFrequency()) << " sec");
#if ENABLE_LOG
        pt = getTickCount();
#endif

        // Compensate exposure
        exposure_comp_->apply((int)img_idx, corners[img_idx], img_warped, mask_warped);//进行曝光补偿
        LOGLN(" compensate exposure: " << ((getTickCount() - pt) / getTickFrequency()) << " sec");
#if ENABLE_LOG
        pt = getTickCount();
#endif

        img_warped.convertTo(img_warped_s, CV_16S);
        img_warped.release();
        img.release();
        mask.release();

        // Make sure seam mask has proper size
        dilate(masks_warped[img_idx], dilated_mask, Mat());//确保接缝遮罩有适当的尺寸
        resize(dilated_mask, seam_mask, mask_warped.size(), 0, 0, INTER_LINEAR_EXACT);//膨胀后的图像要缩放回原有的大小

        bitwise_and(seam_mask, mask_warped, mask_warped);//元遮罩与接缝遮罩取交集,过滤出接缝的所在地

        LOGLN(" other: " << ((getTickCount() - pt) / getTickFrequency()) << " sec");
#if ENABLE_LOG
        pt = getTickCount();
#endif

        if (!is_blender_prepared)//是否准备好了混合
        {
            blender_->prepare(corners, sizes);//设置混合区域
            is_blender_prepared = true;
        }

        LOGLN(" other2: " << ((getTickCount() - pt) / getTickFrequency()) << " sec");

        LOGLN(" feed...");
#if ENABLE_LOG
        int64 feed_t = getTickCount();
#endif
        // Blend the current image
        blender_->feed(img_warped_s, mask_warped, corners[img_idx]);//混合当前图像
        LOGLN(" feed time: " << ((getTickCount() - feed_t) / getTickFrequency()) << " sec");
        LOGLN("Compositing ## time: " << ((getTickCount() - compositing_t) / getTickFrequency()) << " sec");
    }

#if ENABLE_LOG
        int64 blend_t = getTickCount();
#endif
    UMat result;
    blender_->blend(result, result_mask_);//得到最终图像
    LOGLN("blend time: " << ((getTickCount() - blend_t) / getTickFrequency()) << " sec");

    LOGLN("Compositing, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec");

    // Preliminary result is in CV_16SC3 format, but all values are in [0,255] range,
    // so convert it to avoid user confusing
    result.convertTo(pano, CV_8U);

    return OK;
}

从上面的代码中可以看到,配准和融合是的尺度是不同的,并且对相机的参数进行了两次的优化,并没有使用findHomography这个函数获得单应矩阵,而是通过相机的方式进行投影变换。

未完,待续。

你可能感兴趣的:(OpenCV,opencv)