Opencv2.4.9源码分析——Stitching(九)
2018-01-06 16:15
344 查看
9、Stitcher类
图像拼接方法用到的算法较多,内容较复杂,可能对于一些人来说用起来过于繁琐,因此Opencv把拼接算法封装到了Stitcher类中,这样就不必理会拼接算法中的具体实现过程。
下面我们就简单介绍一下Stitcher类中的一些常用的函数:
用系统缺省值创建图像拼接器stitcher:
Stitcher Stitcher::createDefault(bool try_use_gpu)
//try_use_gpu表示是否应用图像处理器
{
Stitcher stitcher; //实例化Stitcher类
//设置全局变量registr_resol_为0.6,registr_resol_表示图像配准分辨率
stitcher.setRegistrationResol(0.6);
//设置全局变量seam_est_resol_为0.1,seam_est_resol_表示接缝分辨率
stitcher.setSeamEstimationResol(0.1);
//ORIG_RESOL = -1,设置全局变量compose_resol_为ORIG_RESOL,compose_resol_表示合成分辨率
stitcher.setCompositingResol(ORIG_RESOL);
//设置全局变量conf_thresh_为1,conf_thresh_表示匹配置信度,即式25
stitcher.setPanoConfidenceThresh(1);
//设置全局变量do_wave_correct_为true,do_wave_correct_表示是否进行波形校正
stitcher.setWaveCorrection(true);
//设置全局变量wave_correct_kind_为WAVE_CORRECT_HORIZ,wave_correct_kind_表示波形校正的方式,这里是用的水平校正
stitcher.setWaveCorrectKind(detail::WAVE_CORRECT_HORIZ);
//设置全局变量features_matcher_为BestOf2NearestMatcher(),features_matcher_表示特征匹配方法,这里是用2-NN方法
stitcher.setFeaturesMatcher(new detail::BestOf2NearestMatcher(try_use_gpu));
//设置全局变量bundle_adjuster_为BundleAdjusterRay(),bundle_adjuster_表示光束平差法,这里用的是射线发散方法
stitcher.setBundleAdjuster(new detail::BundleAdjusterRay());
#if defined(HAVE_OPENCV_GPU) && !defined(DYNAMIC_CUDA_SUPPORT)
if (try_use_gpu && gpu::getCudaEnabledDeviceCount() > 0)
{
#if defined(HAVE_OPENCV_NONFREE)
stitcher.setFeaturesFinder(new detail::SurfFeaturesFinderGpu());
#else
stitcher.setFeaturesFinder(new detail::OrbFeaturesFinder());
#endif
stitcher.setWarper(new SphericalWarperGpu());
stitcher.setSeamFinder(new detail::GraphCutSeamFinderGpu());
}
else
#endif
{
#ifdef HAVE_OPENCV_NONFREE //表示可以使用NONFREE
//设置全局变量features_finder_为SurfFeaturesFinder(),features_finder_表示特征检测的方法,在这里是用SURF算法
stitcher.setFeaturesFinder(new detail::SurfFeaturesFinder());
#else //表示不可以使用NONFREE
//设置全局变量features_finder_为OrbFeaturesFinder(),在这里用的是ORB算法
stitcher.setFeaturesFinder(new detail::OrbFeaturesFinder());
#endif
//设置全局变量warper_为SphericalWarper(),warper_表示图像投影变换的方法,在这里是球面投影方法
stitcher.setWarper(new SphericalWarper());
//设置全局变量seam_finder_为GraphCutSeamFinderBase::COST_COLOR,seam_finder_表示寻找接缝线的算法,在这里是图割法,而且误差表面函数是直接法
stitcher.setSeamFinder(new detail::GraphCutSeamFinder(detail::GraphCutSeamFinderBase::COST_COLOR));
}
//设置全局变量exposure_comp_为BlocksGainCompensator(),exposure_comp_表示曝光补偿方法,在这里是分块增益补偿方法
stitcher.setExposureCompensator(new detail::BlocksGainCompensator());
//设置全局变量blender_为MultiBandBlender(),blender_表示融合算法,在这里是多频段融合方法
stitcher.setBlender(new detail::MultiBandBlender(try_use_gpu));
return stitcher; //返回Stitcher类
}
estimateTransform函数用于匹配图像,并评估相机的旋转参数:
composePanorama函数用于合并拼接图像:
如果对拼接的执行过程不熟悉,那么也是不清楚estimateTransform函数和composePanorama函数的具体含义和执行顺序的。好在Opencv还定义了stitch函数,它完全回避了拼接的具体执行过程,也就是直接调用该函数即可:
下面我们再给出前面函数用到的两个重要函数:
通过以上分析,我们会发现,Stitcher类实现的拼接与第8节给出的实现方法最大的不同之处在于,Stitcher类在拼接的各个阶段,借助于尺度不断缩小图像的尺寸:特征检测与匹配是在work_scale_(匹配尺度)下进行的,映射变换是在seam_scale_(接缝尺度)下进行的,而图像合成又是在compose_scale(合成尺度)下进行的。这么做的优点显而易见,既提高了效率,又节省了内存开销,但最终得到的全景图像的尺寸缩小了。
下面我们给出利用Stitcher类实现的图像拼接:
该程序输入的9幅图像与第8节中的9幅图像完全相同,最终用时不到4分钟。
我们最后总结一下Stitcher类所使用的各种默认算法:SURF特征检测算法(NONFREE条件下),2-NN特征匹配算法,光束平差法中的射线发散法,进行了波形水平校正,球面投影变换,曝光补偿法中的分块增益补偿法,直接图割法,以及多频段融合算法。
图像拼接方法用到的算法较多,内容较复杂,可能对于一些人来说用起来过于繁琐,因此Opencv把拼接算法封装到了Stitcher类中,这样就不必理会拼接算法中的具体实现过程。
下面我们就简单介绍一下Stitcher类中的一些常用的函数:
用系统缺省值创建图像拼接器stitcher:
Stitcher Stitcher::createDefault(bool try_use_gpu)
//try_use_gpu表示是否应用图像处理器
{
Stitcher stitcher; //实例化Stitcher类
//设置全局变量registr_resol_为0.6,registr_resol_表示图像配准分辨率
stitcher.setRegistrationResol(0.6);
//设置全局变量seam_est_resol_为0.1,seam_est_resol_表示接缝分辨率
stitcher.setSeamEstimationResol(0.1);
//ORIG_RESOL = -1,设置全局变量compose_resol_为ORIG_RESOL,compose_resol_表示合成分辨率
stitcher.setCompositingResol(ORIG_RESOL);
//设置全局变量conf_thresh_为1,conf_thresh_表示匹配置信度,即式25
stitcher.setPanoConfidenceThresh(1);
//设置全局变量do_wave_correct_为true,do_wave_correct_表示是否进行波形校正
stitcher.setWaveCorrection(true);
//设置全局变量wave_correct_kind_为WAVE_CORRECT_HORIZ,wave_correct_kind_表示波形校正的方式,这里是用的水平校正
stitcher.setWaveCorrectKind(detail::WAVE_CORRECT_HORIZ);
//设置全局变量features_matcher_为BestOf2NearestMatcher(),features_matcher_表示特征匹配方法,这里是用2-NN方法
stitcher.setFeaturesMatcher(new detail::BestOf2NearestMatcher(try_use_gpu));
//设置全局变量bundle_adjuster_为BundleAdjusterRay(),bundle_adjuster_表示光束平差法,这里用的是射线发散方法
stitcher.setBundleAdjuster(new detail::BundleAdjusterRay());
#if defined(HAVE_OPENCV_GPU) && !defined(DYNAMIC_CUDA_SUPPORT)
if (try_use_gpu && gpu::getCudaEnabledDeviceCount() > 0)
{
#if defined(HAVE_OPENCV_NONFREE)
stitcher.setFeaturesFinder(new detail::SurfFeaturesFinderGpu());
#else
stitcher.setFeaturesFinder(new detail::OrbFeaturesFinder());
#endif
stitcher.setWarper(new SphericalWarperGpu());
stitcher.setSeamFinder(new detail::GraphCutSeamFinderGpu());
}
else
#endif
{
#ifdef HAVE_OPENCV_NONFREE //表示可以使用NONFREE
//设置全局变量features_finder_为SurfFeaturesFinder(),features_finder_表示特征检测的方法,在这里是用SURF算法
stitcher.setFeaturesFinder(new detail::SurfFeaturesFinder());
#else //表示不可以使用NONFREE
//设置全局变量features_finder_为OrbFeaturesFinder(),在这里用的是ORB算法
stitcher.setFeaturesFinder(new detail::OrbFeaturesFinder());
#endif
//设置全局变量warper_为SphericalWarper(),warper_表示图像投影变换的方法,在这里是球面投影方法
stitcher.setWarper(new SphericalWarper());
//设置全局变量seam_finder_为GraphCutSeamFinderBase::COST_COLOR,seam_finder_表示寻找接缝线的算法,在这里是图割法,而且误差表面函数是直接法
stitcher.setSeamFinder(new detail::GraphCutSeamFinder(detail::GraphCutSeamFinderBase::COST_COLOR));
}
//设置全局变量exposure_comp_为BlocksGainCompensator(),exposure_comp_表示曝光补偿方法,在这里是分块增益补偿方法
stitcher.setExposureCompensator(new detail::BlocksGainCompensator());
//设置全局变量blender_为MultiBandBlender(),blender_表示融合算法,在这里是多频段融合方法
stitcher.setBlender(new detail::MultiBandBlender(try_use_gpu));
return stitcher; //返回Stitcher类
}
estimateTransform函数用于匹配图像,并评估相机的旋转参数:
Stitcher::Status Stitcher::estimateTransform(InputArray images) //images表示待拼接的输入图像 { //调用另一种形式的estimateTransform函数 return estimateTransform(images, vector<vector<Rect> >()); }
Stitcher::Status Stitcher::estimateTransform(InputArray images, const vector<vector<Rect> > &rois) //images表示待拼接的输入图像 //rois表示输入图像中感兴趣的矩形区域,即只对该区域进行拼接 { images.getMatVector(imgs_); //图像赋值 rois_ = rois; //赋值 Status status; //调用matchImages函数,用于匹配图像,该函数在后面给出介绍 if ((status = matchImages()) != OK) return status; //调用estimateCameraParams函数,用于评估相机参数,该函数在后面给出介绍 estimateCameraParams(); return OK; }
composePanorama函数用于合并拼接图像:
Stitcher::Status Stitcher::composePanorama(OutputArray pano) //pano表示最终得到的全景图像 { //调用另一种形式的composePanorama函数 return composePanorama(vector<Mat>(), pano); }
Stitcher::Status Stitcher::composePanorama(InputArray images, OutputArray pano) //images表示经过变形处理后的图像 //pano表示最终得到的全景图像 { LOGLN("Warping images (auxiliary)... "); vector<Mat> imgs; images.getMatVector(imgs); //赋值 //如果imgs不为空,即输入参数images有值,则需要把images的相关值赋予imgs_ if (!imgs.empty()) //如果imgs不为空,即有值 { CV_Assert(imgs.size() == imgs_.size()); //确保图像数量相同 Mat img; //定义seam_est_imgs_的数量,seam_est_imgs_表示在进行接缝线算法后得到的图像集合,由于尺度变换,该图像的尺寸与源图尺寸会不一样 seam_est_imgs_.resize(imgs.size()); for (size_t i = 0; i < imgs.size(); ++i) //遍历各个图像,改变图像尺寸 { imgs_[i] = imgs[i]; //赋值 //根据尺度seam_scale_变换当前图像的尺寸 resize(imgs[i], img, Size(), seam_scale_, seam_scale_); seam_est_imgs_[i] = img.clone(); //图像赋值 } //下面两个变量分别表示图像seam_est_imgs_和图像imgs_的子集,这两个集合的图像只是图像尺寸不同 vector<Mat> seam_est_imgs_subset; vector<Mat> imgs_subset; //indices_表示真正可以拼接在一幅全景图像的待拼接图像的数量,即可能有些图像是不属于拼接图像的,indices_由leaveBiggestComponent得到 for (size_t i = 0; i < indices_.size(); ++i) //遍历图像 { //分别得到图像seam_est_imgs_和图像imgs_的子集 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; //赋值 } Mat &pano_ = pano.getMatRef(); //赋值 #if ENABLE_LOG int64 t = getTickCount(); //用于计时 #endif //表示经过映射变换后,各个图像在全景图像坐标系上的左上角坐标 vector<Point> corners(imgs_.size()); vector<Mat> masks_warped(imgs_.size()); //表示映射变换图的掩码 vector<Mat> images_warped(imgs_.size()); //表示映射变换图 vector<Size> sizes(imgs_.size()); //表示映射变换图的尺寸 vector<Mat> 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)); //掩码矩阵先赋值为255,表示不掩码 } // Warp images and their masks //定义图像映射变换类,这里的warped_image_scale_就是焦距,seam_work_aspect_表示接缝尺度与匹配尺度的比值,warped_image_scale_和seam_work_aspect_的乘积表示映射变换的尺度 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); //得到当前相机的内参数 //依据seam_work_aspect_的大小调整相机的内参数,之所以要调整内参数,是因为得到内参数的图像与要映射变换的图像的尺寸不一样 K(0,0) *= (float)seam_work_aspect_; //表示fu K(0,2) *= (float)seam_work_aspect_; //表示cx K(1,1) *= (float)seam_work_aspect_; //表示fv K(1,2) *= (float)seam_work_aspect_; //表示cy //对图像进行映射变换,得到图像映射图images_warped corners[i] = w->warp(seam_est_imgs_[i], K, cameras_[i].R, INTER_LINEAR, BORDER_REFLECT, images_warped[i]); sizes[i] = images_warped[i].size(); //得到尺寸 //对掩码进行映射变换,得到掩码映射图masks_warped w->warp(masks[i], K, cameras_[i].R, INTER_NEAREST, BORDER_CONSTANT, masks_warped[i]); } vector<Mat> images_warped_f(imgs_.size()); for (size_t i = 0; i < imgs_.size(); ++i) //变换图像的数据类型 images_warped[i].convertTo(images_warped_f[i], CV_32F); LOGLN("Warping images, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec"); // Find seams //生成曝光补偿器 exposure_comp_->feed(corners, images_warped, masks_warped); 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 Mat img_warped, img_warped_s; //表示不同数据类型的映射变换图像 //分别表示不同过程中的掩码图像 Mat 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; //表示合成尺度是否设置好 Mat full_img, img; for (size_t img_idx = 0; img_idx < imgs_.size(); ++img_idx) //遍历所有图像 { LOGLN("Compositing image #" << indices_[img_idx] + 1); // Read image and resize it if necessary full_img = imgs_[img_idx]; //得到当前图像 if (!is_compose_scale_set) //compose_scale还没有被设置 { //compose_resol_被初始化为-1,因此compose_scale并没有被重新赋值 if (compose_resol_ > 0) compose_scale = min(1.0, sqrt(compose_resol_ * 1e6 / full_img.size().area())); //表示compose_scale已被设置,则在下次遍历图像时,无需再调整映射变换图的尺寸和左上角坐标 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 //调整映射变换尺度 warped_image_scale_ *= static_cast<float>(compose_work_aspect); //根据调整后的映射变换尺度,重新得到映射变换器 w = warper_->create((float)warped_image_scale_); // Update corners and sizes //更新映射变换图像的参数:左上角坐标和尺寸 for (size_t i = 0; i < imgs_.size(); ++i) //遍历所有图像 { // Update intrinsics //更新相机的内参数 cameras_[i].focal *= compose_work_aspect; cameras_[i].ppx *= compose_work_aspect; cameras_[i].ppy *= compose_work_aspect; // Update corner and size Size sz = full_img_sizes_[i]; //表示当前图像尺寸 if (std::abs(compose_scale - 1) > 1e-1) //调整图像尺寸 { sz.width = cvRound(full_img_sizes_[i].width * compose_scale); sz.height = cvRound(full_img_sizes_[i].height * compose_scale); } Mat K; cameras_[i].K().convertTo(K, CV_32F); //转换相机参数的数据格式 Rect roi = w->warpRoi(sz, K, cameras_[i].R); //得到映射变换图的矩形 corners[i] = roi.tl(); //映射变换图在全景图像坐标系上的左上角坐标 sizes[i] = roi.size(); //得到映射变换图的尺寸 } } //依据合成尺度compose_scale,调整图像的尺寸 if (std::abs(compose_scale - 1) > 1e-1) resize(full_img, img, Size(), compose_scale, compose_scale); else img = full_img; full_img.release(); //释放一些内存空间 Size img_size = img.size(); Mat K; cameras_[img_idx].K().convertTo(K, CV_32F); //转换相机参数的数据格式 // Warp the current image //对改变了尺寸的当前图像进行映射变换 w->warp(img, K, cameras_[img_idx].R, INTER_LINEAR, BORDER_REFLECT, img_warped); // 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); // Compensate exposure //完成曝光补偿 exposure_comp_->apply((int)img_idx, corners[img_idx], img_warped, mask_warped); img_warped.convertTo(img_warped_s, CV_16S); //改变映射变换图的数据类型 img_warped.release(); //释放一些内存空间 img.release(); mask.release(); // Make sure seam mask has proper size //对因合成而改变图像尺寸之前的映射变换掩码(即对该函数内seam_finder_->find函数得到的掩码)进行膨胀运算,使掩码面积缩小,从而扩展了接缝线附近的区域 dilate(masks_warped[img_idx], dilated_mask, Mat()); //再把掩码图像的尺寸调整为mask_warped(即调整到因合成而改变图像尺寸下的映射变换掩码,由该函数内exposure_comp_->apply函数得到)的大小尺寸, resize(dilated_mask, seam_mask, mask_warped.size()); //“与”操作,得到更准确的掩码图像,即mask_warped仅仅是扩展了接缝线附近的区域 mask_warped = seam_mask & mask_warped; //在该循环体的第一次循环的时候,准备融合器,即定义好全景图像的大小 if (!is_blender_prepared) { blender_->prepare(corners, sizes); //准备融合器 is_blender_prepared = true; //置1,避免重复调用blender_->prepare } // Blend the current image //把当前图像的数据添加进融合器内 blender_->feed(img_warped_s, mask_warped, corners[img_idx]); } Mat result, result_mask; blender_->blend(result, result_mask); //得到全景图像result及它的掩码result_mask 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); //得到全景图像pano_ return OK; }
如果对拼接的执行过程不熟悉,那么也是不清楚estimateTransform函数和composePanorama函数的具体含义和执行顺序的。好在Opencv还定义了stitch函数,它完全回避了拼接的具体执行过程,也就是直接调用该函数即可:
Stitcher::Status Stitcher::stitch(InputArray images, OutputArray pano) //images表示输入的待拼接图像 //pano表示最终得到的全景图像 { Status status = estimateTransform(images); //调用estimateTransform函数 if (status != OK) return status; return composePanorama(pano); //调用composePanorama函数 }
Stitcher::Status Stitcher::stitch(InputArray images, const vector<vector<Rect> > &rois, OutputArray pano) //images表示输入的待拼接图像 //rois表示输入图像中感兴趣的矩形区域,即只对该区域进行拼接 //pano表示最终得到的全景图像 { Status status = estimateTransform(images, rois); //调用estimateTransform函数 if (status != OK) return status; return composePanorama(pano); //调用composePanorama函数 }
下面我们再给出前面函数用到的两个重要函数:
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; //表示work_scale_和seam_scale_的比值 seam_scale_ = 1; //表接缝尺度 bool is_work_scale_set = false; //表示work_scale_是否被设置 bool is_seam_scale_set = false; //表示seam_scale_是否被设置 Mat full_img, img; features_.resize(imgs_.size()); //定义特征点的矢量数量 seam_est_imgs_.resize(imgs_.size()); //定义seam_est_imgs_的矢量数量 full_img_sizes_.resize(imgs_.size()); //full_img_sizes_表示图像尺寸 LOGLN("Finding features..."); #if ENABLE_LOG int64 t = getTickCount(); #endif for (size_t i = 0; i < imgs_.size(); ++i) //遍历所有图像,寻找特征点 { full_img = imgs_[i]; //得到当前图像 full_img_sizes_[i] = full_img.size(); //得到当前图像的尺寸 //registr_resol_被设置为0.6,所以执行的是else内容 if (registr_resol_ < 0) { img = full_img; work_scale_ = 1; is_work_scale_set = true; } else { //没有设置work_scale_,则此时需要设置work_scale_ if (!is_work_scale_set) { //由图像配准分辨率和图像面积设置匹配尺度 work_scale_ = min(1.0, sqrt(registr_resol_ * 1e6 / full_img.size().area())); is_work_scale_set = true; //避免再次循环重复设置work_scale_ } //由work_scale_调整图像尺寸 resize(full_img, img, Size(), work_scale_, work_scale_); } //没有设置seam_scale_,则此时需要设置seam_scale_ if (!is_seam_scale_set) { //由接缝分辨率和图像面积设置接缝尺度 seam_scale_ = min(1.0, sqrt(seam_est_resol_ * 1e6 / full_img.size().area())); //设置seam_work_aspect_ seam_work_aspect_ = seam_scale_ / work_scale_; is_seam_scale_set = true; //避免再次循环重复设置seam_scale_ } if (rois_.empty()) //表示图像的所有区域都是寻找特征点的区域 (*features_finder_)(img, features_[i]); //寻找图像img的特征点 else //否则在指定区域内寻找特征点 { vector<Rect> rois(rois_[i].size()); //定义区域矩形 for (size_t j = 0; j < rois_[i].size(); ++j) //得到该区域在当前图像的位置 { Point tl(cvRound(rois_[i][j].x * work_scale_), cvRound(rois_[i][j].y * work_scale_)); Point br(cvRound(rois_[i][j].br().x * work_scale_), cvRound(rois_[i][j].br().y * work_scale_)); rois[j] = Rect(tl, br); //得到区域矩形 } //在图像img中的rois区域内寻找特征点 (*features_finder_)(img, features_[i], rois); } features_[i].img_idx = (int)i; //赋值索引 LOGLN("Features in image #" << i+1 << ": " << features_[i].keypoints.size()); resize(full_img, img, Size(), seam_scale_, seam_scale_); //改变图像尺寸 seam_est_imgs_[i] = img.clone(); //赋值 } // Do it to save memory features_finder_->collectGarbage(); //收集内存碎片 full_img.release(); //释放内存 img.release(); //释放内存 LOGLN("Finding features, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec"); LOG("Pairwise matching"); #if ENABLE_LOG t = getTickCount(); #endif (*features_matcher_)(features_, pairwise_matches_, matching_mask_); //特征点匹配 features_matcher_->collectGarbage(); //收集内存碎片 LOGLN("Pairwise matching, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec"); // Leave only images we are sure are from the same panorama //调用leaveBiggestComponent函数,得到全景图像集合(即输入图像中有可能有些图像并不能构成全景图像),indices_为全景图像集合的图像索引 indices_ = detail::leaveBiggestComponent(features_, pairwise_matches_, (float)conf_thresh_); vector<Mat> seam_est_imgs_subset; //表示seam_est_imgs_的子集 vector<Mat> imgs_subset; //表示imgs_的子集 vector<Size> full_img_sizes_subset; //表示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]]); 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) //图像数量必须要大于2 { LOGLN("Need more images"); return ERR_NEED_MORE_IMGS; } return OK; }
void Stitcher::estimateCameraParams() //相机参数评估 { detail::HomographyBasedEstimator estimator; //表示相机参数评估器 estimator(features_, pairwise_matches_, cameras_); //得到相机参数cameras_ 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_); //设置匹配置信度 //利用光束平差法精确化相机参数cameras_ (*bundle_adjuster_)(features_, pairwise_matches_, cameras_); // Find median focal length and use it as final image scale 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()); //焦距排序 //把焦距的中间值作为图像映射变换尺度warped_image_scale_ 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_) //进行波形校正 { vector<Mat> rmats; for (size_t i = 0; i < cameras_.size(); ++i) rmats.push_back(cameras_[i].R); //得到相机旋转参数 detail::waveCorrect(rmats, wave_correct_kind_); //波形校正 for (size_t i = 0; i < cameras_.size(); ++i) cameras_[i].R = rmats[i]; //校正后的旋转参数 } }
通过以上分析,我们会发现,Stitcher类实现的拼接与第8节给出的实现方法最大的不同之处在于,Stitcher类在拼接的各个阶段,借助于尺度不断缩小图像的尺寸:特征检测与匹配是在work_scale_(匹配尺度)下进行的,映射变换是在seam_scale_(接缝尺度)下进行的,而图像合成又是在compose_scale(合成尺度)下进行的。这么做的优点显而易见,既提高了效率,又节省了内存开销,但最终得到的全景图像的尺寸缩小了。
下面我们给出利用Stitcher类实现的图像拼接:
#include <iostream> #include <fstream> #include <string> #include "opencv2/opencv_modules.hpp" #include "opencv2/highgui/highgui.hpp" #include "opencv2/stitching/stitcher.hpp" using namespace std; using namespace cv; using namespace cv::detail; int main( ) { vector<Mat> imgs; //输入9幅图像 Mat img; img = imread("4.jpg"); imgs.push_back(img); img = imread("5.jpg"); imgs.push_back(img); img = imread("6.jpg"); imgs.push_back(img); img = imread("7.jpg"); imgs.push_back(img); img = imread("8.jpg"); imgs.push_back(img); img = imread("9.jpg"); imgs.push_back(img); img = imread("10.jpg"); imgs.push_back(img); img = imread("11.jpg"); imgs.push_back(img); img = imread("12.jpg"); imgs.push_back(img); int num_images = 9; Mat pano; //全景图像 Stitcher stitcher = Stitcher::createDefault(false); //定义全景图像拼接器 Stitcher::Status status = stitcher.stitch(imgs, pano); //图像拼接 if (status != Stitcher::OK) { cout << "Can't stitch images, error code = " << int(status) << endl; return -1; } imwrite("pano.jpg", pano); //存储图像 return 0; }
该程序输入的9幅图像与第8节中的9幅图像完全相同,最终用时不到4分钟。
我们最后总结一下Stitcher类所使用的各种默认算法:SURF特征检测算法(NONFREE条件下),2-NN特征匹配算法,光束平差法中的射线发散法,进行了波形水平校正,球面投影变换,曝光补偿法中的分块增益补偿法,直接图割法,以及多频段融合算法。
相关文章推荐
- Opencv2.4.9源码分析——Stitching(一)
- Opencv2.4.9源码分析——Stitching(三)
- Opencv2.4.9源码分析——Stitching(二)
- Opencv2.4.9源码分析——Stitching(三)
- Opencv2.4.9源码分析——Stitching(五)
- Opencv2.4.9源码分析——Stitching(七)
- Opencv2.4.9源码分析——Stitching(六)
- Opencv2.4.9源码分析——Stitching(八)
- Opencv2.4.9源码分析——Stitching(八)
- Opencv2.4.9源码分析——Stitching(四)
- Opencv2.4.9源码分析——HoughCircles
- opencv2.4.9中stitching_detailed源码环境搭建
- Opencv2.4.9源码分析——HoughCircles
- Opencv2.4.9源码分析——Expectation Maximization
- Opencv2.4.9源码分析(相机参数评估)
- Opencv2.4.9源码分析——Normal Bayes Classifier
- Opencv2.4.9源码分析——Decision Trees
- Opencv2.4.9源码分析——Cascade Classification(二)
- Opencv2.4.9源码分析——Boosting
- Opencv2.4.9源码分析——SURF