OpenCV总结3——图像拼接Stitching

2023-11-15

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

Stitching

因为OpenCV已经将算法进行了高度的封装,所以用起来跟OpenGL类似,遵循了一条管线进行处理。
在这里插入图片描述
上图是OpenCV官方网站中提供的流程图。

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

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

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

看英文比较费事,自己做了个翻译:
在这里插入图片描述
写个最简单的调用:

	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这个函数获得单应矩阵,而是通过相机的方式进行投影变换。

未完,待续。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

OpenCV总结3——图像拼接Stitching 的相关文章

  • OpenCV Python - 如何实现RANSAC来检测直线?

    我正在尝试检测包含道路的图像上的线条 使用高斯平滑和 Canny 边缘检测 我在尝试实现 RANSAC 时遇到了困难 我基本上不知道如何去做 我可以获得一个粗略的想法或一个带有实现 RANSAC 的随机图像 foo png 的工作代码吗 N
  • 使用高斯混合模型进行皮肤检测

    我正在根据以下进行皮肤检测算法本文 http www cc gatech edu rehg Papers SkinDetect IJCV lowres pdf 第 21 页有两个模型 高斯皮肤混合模型和非皮肤颜色模型 第一个皮肤检测模型效果
  • Tkinter 嵌套主循环

    我正在写一个视频播放器tkinter python 所以基本上我有一个可以播放视频的 GUI 现在 我想实现一个停止按钮 这意味着我将有一个mainloop 对于 GUI 还有另一个嵌套mainloop 播放 停止视频并返回 GUI 启动窗
  • Aruco 标记与 openCv,获取 3d 角坐标?

    我正在使用 opencv 3 2 检测打印的 Aruco 标记 aruco estimatePoseSingleMarkers corners markerLength camMatrix distCoeffs rvecs tvecs 这将
  • 向 ca cv::Mat 添加文本比 cv::putText() 更好的方法吗?

    我想在 a 上添加一些文字cv Mat but cv putText 对我来说不够灵活 结盟 我需要将可变长度的标签放在运行时已知的几个像素位置 但由于cv putText 将输出的原点始终放在左侧 如果位置离左侧太远 我的文本就会消失在图
  • Ubuntu OpenCV 无法编译

    我正在尝试使用以下命令编译 OpenCV 3 2 1 cmake DCMAKE BUILD TYPE Release DBUILD SHARED LIBS OFF DCMAKE INSTALL PREFIX usr local DOPENC
  • ld:找不到 -llibtbb.dylib 的库

    我尝试从 opencv 2 4 8 apps haarfinder 编译一些文件 但出现以下错误 ld library not found for llibtbb dylib 注意双l在文件名中 我尝试按照这里的教程进行操作 http co
  • 使用 cmake 和 opencv 对符号“gzclose”的未定义引用[关闭]

    Closed 这个问题是无法重现或由拼写错误引起 help closed questions 目前不接受答案 我尝试构建该项目 doppia https bitbucket org rodrigob doppia 但发生链接错误 我想这是一
  • 使用畸变从图像平面计算相机矢量

    我正在尝试使用相机模型来重建可以使用某些相机及其 外部 内部 参数拍摄的图像 这一点我没有任何问题 现在我想添加扭曲 正如它们中所描述的那样OpenCV https docs opencv org 4 x dc dbb tutorial p
  • Python 函数前的星号[重复]

    这个问题在这里已经有答案了 我正在关注这个教程 http www pyimagesearch com 2015 04 20 sorting contours using python and opencv comment 405768 ht
  • 如何使用 OpenCV 找到红色区域? [复制]

    这个问题在这里已经有答案了 我正在尝试编写一个检测红色的程序 然而有时它比平常更暗 所以我不能只使用一个值 检测不同深浅的红色的最佳范围是多少 我目前使用的范围是 128 0 0 255 60 60 但有时它甚至检测不到我放在它前面的红色物
  • 将四边形(四边形)拟合到斑点

    应用不同的过滤和分割技术后 我最终得到如下图像 我可以访问一些轮廓检测函数 这些函数返回该对象边缘上的点列表 或者返回一个拟合的多边形 尽管有很多边 远多于 4 个 我想要一种将四边形适合该形状的方法 因为我知道它是应该是四边形的鞋盒的正面
  • 带有 OpenCV 的增强现实 SDK [关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • CvMat 和 Imread 与 IpImage 和 CvLoadImage

    使用 OpenCv 2 4 我有两个选项来加载图像 1 CvMat and Imread 2 IpImage and CvLoadImage 使用哪一个更好 我尝试将两者混合并最终出现段错误 imread返回一个Mat not CvMat
  • OpenCV:视频结束后如何重新启动?

    我正在播放视频文件 但播放完毕后如何再次播放 Javier 如果您想一遍又一遍地重新启动视频 也称为循环播放 可以通过在帧数达到时使用 if 语句来实现cap get cv2 cv CV CAP PROP FRAME COUNT 然后重置帧
  • Opencv matchTemplate 和 np.where():仅保留唯一值

    继带有马里奥硬币的 opencv 教程 https opencv python tutroals readthedocs io en latest py tutorials py imgproc py template matching p
  • 提高 pytesseract 从图像中正确识别文本的能力

    我正在尝试使用读取验证码pytesseract模块 大多数时候它都能提供准确的文本 但并非总是如此 这是读取图像 操作图像以及从图像中提取文本的代码 import cv2 import numpy as np import pytesser
  • 我是否必须使用我的数据库训练 Viola-Jones 算法才能获得准确的结果?

    我尝试提取面部数据库的面部特征 但我认识到 Viola Jones 算法在两种情况下效果不佳 当我尝试单独检测眼睛时 当我尝试检测嘴巴时 运作不佳 检测图像的不同部分 例如眼睛或嘴巴 或者有时会检测到其中几个 这是不可能的情况 我使用的图像
  • 如何删除树莓派的相机预览

    我在我的 raspberryPi 上安装了 SimpleCv 并安装了用于使用相机板的驱动程序 uv4l 驱动程序 现在我想使用它 当我在 simpleCV shell Camera 0 getImage save foo jpg 上键入时
  • 使用opencv计算深度视差图

    我无法使用 opencv 从视差图计算深度 我知道两个立体图像中的距离是用以下公式计算的z baseline focal disparity p 但我不知道如何使用地图计算视差 我使用的代码如下 为我提供了两个图像的视差图 import n

随机推荐

  • Redis第三讲 Redis 4.0 混合持久化与Redis数据备份策略

    RDB 和 AOF 持久化各有利弊 RDB 可能会导致一定时间内的数据丢失 而 AOF 由于文件较大则会影响 Redis 的启动速度 为了能同时使用 RDB 和 AOF 各种的优点 Redis 4 0 之后新增了混合持久化的方式 加载优先级
  • 【理论实践】指向类模板函数的指针的使用(以std::list为例)

    假设有这个一个场景 我们希望根据条件决定插入元素到list首或尾 条件判断一次 插入操作多次 例如二叉树 至少要处理左和右各一次 普通的代码很简单 每次操作时 都判断一下 简化一下是一个三元表达式 巧妙一点的 可以定义一个变量指定接口函数
  • QT学习笔记:多线程操作

    做了一个demo展示两种形式的多线程操作 第二种常用 new QThread Class Override run new Object Class moveToThread new QThread threadfirst h 第一种形式的
  • Js Jquery 关于endWith() 和startWith() 的使用

    javascript中字符串处理并没有 StartWith 和 EndWith 这俩个方法 这里说的是手动构建这俩个方法 JQuery 也是没有这俩个方法的 而是利用其丰富的选择器来达到此效果 首选javascript下着俩个函数的构建如下
  • RFC7296--Internet密钥交换协议版本2(IKEv2)

    2 8 密钥更新 rekeying IKE ESP和AH安全联盟 SA 使用的共享密钥应该只在有限的时间里保护优先的数据 这限制了整个SA的生存周期 生存周期超时的SA决不能再使用 如果有需要 可以建立新的SA 重建SA以取代过期的SA被称
  • Python 文件操作(IO)

    文章目录 前言 一 打印到屏幕 print 二 读取键盘输入 1 raw input 2 input 三 读写文件 读文件 写文件 前言 和其它编程语言一样 Python 也具有操作文件 I O 的能力 比如打开文件 读取和追加数据 插入和
  • 什么是token?

    什么是token token就是令牌 前后端进行鉴权的一种有效形式 比传统的 session 鉴权更加方便 简单来说 当用户首次登陆时 网站会给你一张 门卡 以后你可以凭借门卡直接进入 而无需再次申请 但一段时间之后门卡实效 你需要再到前台
  • 如何调用本业务模块外的服务——服务调用

    上篇已经引入 Nacos 基础组件 完成了服务注册与发现机制 可以将所有服务统一的管理配置起来 方便服务间调用 本篇将结合需求点 进行服务间调用 完成功能开发 几种服务调用方式 服务间调用常见的两种方式 RPC 与 HTTP RPC 全称
  • 一个简洁的PNG ICO转换工具 支持多分辨率的ICO生成

    一个绝美的PNG ICO转换工具 支持多分辨率的ICO生成 下载地址 http www ppsbbs tech thread 58 htm
  • Kotlin 1.2 新特性

    点击关注异步图书 置顶公众号 每天与你分享IT好书 技术干货 职场知识 在Kotlin 1 1中 团队正式发布了JavaScript目标 允许开发者将Kotlin代码编译为JS并在浏览器中运行 在Kotlin 1 2中 团队增加了在JVM和
  • Intellij Idea怎么撤销,反撤销

    Intellij IDEA中 1 Ctrl z是撤销快捷键 2 反撤销快捷键为 Ctrl Shift Z
  • React 配置路由

    1 在 index 中引入 App 文件 index 是入口文件 并且在 index 中引入样式文件等等 把 App 挂载到 DOM 元素上 2 在 App 组件中
  • 3 亿岗位将被 AI 取代?巴比特深度采访业界后,“失业潮”真相有些出人意料……...

    图片来源 由无界 AI工具生成 人工智能技术的发展正迎来奇点 尤其是今年以来 ChatGPT 和 AIGC 的迅猛势头让无数人猝不及防 真真切切地对各行各业现有的工作岗位产生冲击 近日 蓝色光标全面停止创意设计 方案撰写 文案撰写 短期雇员
  • (简单成功版本)Mysql配置my.ini文件

    目录 一 背景 二 删除原有的mysql服务 三 初始化mysql 四 自行添加my ini文件 五 新建mysql服务 六 启动mysql服务 七 设置数据库密码 7 1 登录mysql数据库 7 2 修改root用户密码 八 配置my
  • Xcode编译报错不提示

    M1 Xcode Version 12 5 1 12E507 编译项目之后提示 Build Failed 但是并不报 小红点 不指示是哪个文件报错 不知道去哪里找报错文件了 Xocode 工具栏上有这个按钮 选择之后点击某次编译 如果有错误
  • DOTA目标检测数据集

    Dota开源目标检测数据集 DOTA v1 5包含16个类别中的40万个带注释的对象实例 这是DOTA v1 0的更新版本 它们都使用相同的航拍图像 但是DOTA v1 5修改并更新了对象的注释 其中许多在DOTA v1 0中丢失的10像素
  • 拷贝构造函数的调用方式以及相关问题【最清晰易懂】

    这几天一直有一个问题在我大脑里挥之不去 之前面试实习的时候也被问过 但是回答的不好 面试官问 你知道的构造函数有哪些 我说 无参构造函数 有参构造函数 拷贝构造函数 移动构造函数 关于一些函数的说明 面试官说 其实拷贝构造函数 移动构造函数
  • java给字符串数组追加字符串_java往字符串数组追加新数据

    public class Test public static void main String args 原字符串数组 String arr 原字符串数据1 原字符串数据2 执行数据添加 arr insert arr 需要追加的字符串数据
  • win11安装mysql5.7带安装包与常见问题如重装,初次登录不上,跳不了密码等

    目录 1下载 2安装 注意1 你的新建只有文件夹而且需要权限 注意2报错The service already existsThe current server installed 3初次登录与密码 注意3密码是只能输入进去的 4设置密码与
  • OpenCV总结3——图像拼接Stitching

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