之前折腾过一段时间配准发现自己写的一点都不准,最近需要进行图像的拼接,偶然的机会查到了opencv原来有拼接的库,发现opencv处理配准之外还做了许多的操作,就这个机会查找了相关的资料,同时也研究了以下他的源代码,做一个简单的总结。
因为OpenCV已经将算法进行了高度的封装,所以用起来跟OpenGL类似,遵循了一条管线进行处理。
上图是OpenCV官方网站中提供的流程图。
从这个图中我们也能看出这是一个很复杂的过程,源代码中提到了七个部分:
之前我做的只考虑了特征和变形,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这个函数获得单应矩阵,而是通过相机的方式进行投影变换。
未完,待续。