关于OpenCV图像拼接的方法,如果不熟悉的话,可以先看看我整理的如下四篇博客:
-
OpenCV常用图像拼接方法(一):直接拼接(硬拼)
-
OpenCV常用图像拼接方法(二):基于模板匹配拼接
-
OpenCV常用图像拼接方法(三):基于特征匹配拼接
-
OpenCV常用图像拼接方法(四):基于Stitcher类拼接
本篇博客是Stitcher类的扩展介绍,通过例程stitching_detailed.cpp的使用和参数介绍,帮助大家了解Stitcher类拼接的具体步骤和方法,先看看其内部的流程结构图(如下):
stitching_detailed.cpp目录如下,可以在自己安装的OpenCV目录下找到,笔者这里使用的OpenCV4.4版本
stitching_detailed.cpp具体源码如下:
1 // 05_Image_Stitch_Stitching_Detailed.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 2 // 3 #include "pch.h"
4 #include <iostream>
5 #include <fstream>
6 #include <string>
7 #include "opencv2/opencv_modules.hpp"
8 #include <opencv2/core/utility.hpp>
9 #include "opencv2/imgcodecs.hpp"
10 #include "opencv2/highgui.hpp"
11 #include "opencv2/stitching/detail/autocalib.hpp"
12 #include "opencv2/stitching/detail/blenders.hpp"
13 #include "opencv2/stitching/detail/timelapsers.hpp"
14 #include "opencv2/stitching/detail/camera.hpp"
15 #include "opencv2/stitching/detail/exposure_compensate.hpp"
16 #include "opencv2/stitching/detail/matchers.hpp"
17 #include "opencv2/stitching/detail/motion_estimators.hpp"
18 #include "opencv2/stitching/detail/seam_finders.hpp"
19 #include "opencv2/stitching/detail/warpers.hpp"
20 #include "opencv2/stitching/warpers.hpp"
21
22 #ifdef HAVE_OPENCV_XFEATURES2D 23 #include "opencv2/xfeatures2d.hpp"
24 #include "opencv2/xfeatures2d/nonfree.hpp"
25 #endif
26
27 #define ENABLE_LOG 1
28 #define LOG(msg) std::cout << msg
29 #define LOGLN(msg) std::cout << msg << std::endl
30
31 using namespace std; 32 using namespace cv; 33 using namespace cv::detail; 34
35 static void printUsage(char** argv) 36 { 37 cout <<
38 "Rotation model images stitcher.\n\n"
39 << argv[0] << " img1 img2 [...imgN] [flags]\n\n"
40 "Flags:\n"
41 " --preview\n"
42 " Run stitching in the preview mode. Works faster than usual mode,\n"
43 " but output image will have lower resolution.\n"
44 " --try_cuda (yes|no)\n"
45 " Try to use CUDA. The default value is 'no'. All default values\n"
46 " are for CPU mode.\n"
47 "\nMotion Estimation Flags:\n"
48 " --work_megapix <float>\n"
49 " Resolution for image registration step. The default is 0.6 Mpx.\n"
50 " --features (surf|orb|sift|akaze)\n"
51 " Type of features used for images matching.\n"
52 " The default is surf if available, orb otherwise.\n"
53 " --matcher (homography|affine)\n"
54 " Matcher used for pairwise image matching.\n"
55 " --estimator (homography|affine)\n"
56 " Type of estimator used for transformation estimation.\n"
57 " --match_conf <float>\n"
58 " Confidence for feature matching step. The default is 0.65 for surf and 0.3 for orb.\n"
59 " --conf_thresh <float>\n"
60 " Threshold for two images are from the same panorama confidence.\n"
61 " The default is 1.0.\n"
62 " --ba (no|reproj|ray|affine)\n"
63 " Bundle adjustment cost function. The default is ray.\n"
64 " --ba_refine_mask (mask)\n"
65 " Set refinement mask for bundle adjustment. It looks like 'x_xxx',\n"
66 " where 'x' means refine respective parameter and '_' means don't\n"
67 " refine one, and has the following format:\n"
68 " <fx><skew><ppx><aspect><ppy>. The default mask is 'xxxxx'. If bundle\n"
69 " adjustment doesn't support estimation of selected parameter then\n"
70 " the respective flag is ignored.\n"
71 " --wave_correct (no|horiz|vert)\n"
72 " Perform wave effect correction. The default is 'horiz'.\n"
73 " --save_graph <file_name>\n"
74 " Save matches graph represented in DOT language to <file_name> file.\n"
75 " Labels description: Nm is number of matches, Ni is number of inliers,\n"
76 " C is confidence.\n"
77 "\nCompositing Flags:\n"
78 " --warp (affine|plane|cylindrical|spherical|fisheye|stereographic|compressedPlaneA2B1|compressedPlaneA1.5B1|compressedPlanePortraitA2B1|compressedPlanePortraitA1.5B1|paniniA2B1|paniniA1.5B1|paniniPortraitA2B1|paniniPortraitA1.5B1|mercator|transverseMercator)\n"
79 " Warp surface type. The default is 'spherical'.\n"
80 " --seam_megapix <float>\n"
81 " Resolution for seam estimation step. The default is 0.1 Mpx.\n"
82 " --seam (no|voronoi|gc_color|gc_colorgrad)\n"
83 " Seam estimation method. The default is 'gc_color'.\n"
84 " --compose_megapix <float>\n"
85 " Resolution for compositing step. Use -1 for original resolution.\n"
86 " The default is -1.\n"
87 " --expos_comp (no|gain|gain_blocks|channels|channels_blocks)\n"
88 " Exposure compensation method. The default is 'gain_blocks'.\n"
89 " --expos_comp_nr_feeds <int>\n"
90 " Number of exposure compensation feed. The default is 1.\n"
91 " --expos_comp_nr_filtering <int>\n"
92 " Number of filtering iterations of the exposure compensation gains.\n"
93 " Only used when using a block exposure compensation method.\n"
94 " The default is 2.\n"
95 " --expos_comp_block_size <int>\n"
96 " BLock size in pixels used by the exposure compensator.\n"
97 " Only used when using a block exposure compensation method.\n"
98 " The default is 32.\n"
99 " --blend (no|feather|multiband)\n"
100 " Blending method. The default is 'multiband'.\n"
101 " --blend_strength <float>\n"
102 " Blending strength from [0,100] range. The default is 5.\n"
103 " --output <result_img>\n"
104 " The default is 'result.jpg'.\n"
105 " --timelapse (as_is|crop) \n"
106 " Output warped images separately as frames of a time lapse movie, with 'fixed_' prepended to input file names.\n"
107 " --rangewidth <int>\n"
108 " uses range_width to limit number of images to match with.\n"; 109 } 110
111
112 // Default command line args
113 vector<String> img_names; 114 bool preview = false; 115 bool try_cuda = false; 116 double work_megapix = 0.6; 117 double seam_megapix = 0.1; 118 double compose_megapix = -1; 119 float conf_thresh = 1.f; 120 #ifdef HAVE_OPENCV_XFEATURES2D 121 string features_type = "surf"; 122 float match_conf = 0.65f; 123 #else
124 string features_type = "orb"; 125 float match_conf = 0.3f; 126 #endif
127 string matcher_type = "homography"; 128 string estimator_type = "homography"; 129 string ba_cost_func = "ray"; 130 string ba_refine_mask = "xxxxx"; 131 bool do_wave_correct = true; 132 WaveCorrectKind wave_correct = detail::WAVE_CORRECT_HORIZ; 133 bool save_graph = false; 134 std::string save_graph_to; 135 string warp_type = "spherical"; 136 int expos_comp_type = ExposureCompensator::GAIN_BLOCKS; 137 int expos_comp_nr_feeds = 1; 138 int expos_comp_nr_filtering = 2; 139 int expos_comp_block_size = 32; 140 string seam_find_type = "gc_color"; 141 int blend_type = Blender::MULTI_BAND; 142 int timelapse_type = Timelapser::AS_IS; 143 float blend_strength = 5; 144 string result_name = "result.jpg"; 145 bool timelapse = false; 146 int range_width = -1; 147
148
149 static int parseCmdArgs(int argc, char** argv) 150 { 151 if (argc == 1) 152 { 153 printUsage(argv); 154 return -1; 155 } 156 for (int i = 1; i < argc; ++i) 157 { 158 if (string(argv[i]) == "--help" || string(argv[i]) == "/?") 159 { 160 printUsage(argv); 161 return -1; 162 } 163 else if (string(argv[i]) == "--preview") 164 { 165 preview = true; 166 } 167 else if (string(argv[i]) == "--try_cuda") 168 { 169 if (string(argv[i + 1]) == "no") 170 try_cuda = false; 171 else if (string(argv[i + 1]) == "yes") 172 try_cuda = true; 173 else
174 { 175 cout << "Bad --try_cuda flag value\n"; 176 return -1; 177 } 178 i++; 179 } 180 else if (string(argv[i]) == "--work_megapix") 181 { 182 work_megapix = atof(argv[i + 1]); 183 i++; 184 } 185 else if (string(argv[i]) == "--seam_megapix") 186 { 187 seam_megapix = atof(argv[i + 1]); 188 i++; 189 } 190 else if (string(argv[i]) == "--compose_megapix") 191 { 192 compose_megapix = atof(argv[i + 1]); 193 i++; 194 } 195 else if (string(argv[i]) == "--result") 196 { 197 result_name = argv[i + 1]; 198 i++; 199 } 200 else if (string(argv[i]) == "--features") 201 { 202 features_type = argv[i + 1]; 203 if (string(features_type) == "orb") 204 match_conf = 0.3f; 205 i++; 206 } 207 else if (string(argv[i]) == "--matcher") 208 { 209 if (string(argv[i + 1]) == "homography" || string(argv[i + 1]) == "affine") 210 matcher_type = argv[i + 1]; 211 else
212 { 213 cout << "Bad --matcher flag value\n"; 214 return -1; 215 } 216 i++; 217 } 218 else if (string(argv[i]) == "--estimator") 219 { 220 if (string(argv[i + 1]) == "homography" || string(argv[i + 1]) == "affine") 221 estimator_type = argv[i + 1]; 222 else
223 { 224 cout << "Bad --estimator flag value\n"; 225 return -1; 226 } 227 i++; 228 } 229 else if (string(argv[i]) == "--match_conf") 230 { 231 match_conf = static_cast<float>(atof(argv[i + 1])); 232 i++; 233 } 234 else if (string(argv[i]) == "--conf_thresh") 235 { 236 conf_thresh = static_cast<float>(atof(argv[i + 1])); 237 i++; 238 } 239 else if (string(argv[i]) == "--ba") 240 { 241 ba_cost_func = argv[i + 1]; 242 i++; 243 } 244 else if (string(argv[i]) == "--ba_refine_mask") 245 { 246 ba_refine_mask = argv[i + 1]; 247 if (ba_refine_mask.size() != 5) 248 { 249 cout << "Incorrect refinement mask length.\n"; 250 return -1; 251 } 252 i++; 253 } 254 else if (string(argv[i]) == "--wave_correct") 255 { 256 if (string(argv[i + 1]) == "no") 257 do_wave_correct = false; 258 else if (string(argv[i + 1]) == "horiz") 259 { 260 do_wave_correct = true; 261 wave_correct = detail::WAVE_CORRECT_HORIZ; 262 } 263 else if (string(argv[i + 1]) == "vert") 264 { 265 do_wave_correct = true; 266 wave_correct = detail::WAVE_CORRECT_VERT; 267 } 268 else
269 { 270 cout << "Bad --wave_correct flag value\n"; 271 return -1; 272 } 273 i++; 274 } 275 else if (string(argv[i]) == "--save_graph") 276 { 277 save_graph = true; 278 save_graph_to = argv[i + 1]; 279 i++; 280 } 281 else if (string(argv[i]) == "--warp") 282 { 283 warp_type = string(argv[i + 1]); 284 i++; 285 } 286 else if (string(argv[i]) == "--expos_comp") 287 { 288 if (string(argv[i + 1]) == "no") 289 expos_comp_type = ExposureCompensator::NO; 290 else if (string(argv[i + 1]) == "gain") 291 expos_comp_type = ExposureCompensator::GAIN; 292 else if (string(argv[i + 1]) == "gain_blocks") 293 expos_comp_type = ExposureCompensator::GAIN_BLOCKS; 294 else if (string(argv[i + 1]) == "channels") 295 expos_comp_type = ExposureCompensator::CHANNELS; 296 else if (string(argv[i + 1]) == "channels_blocks") 297 expos_comp_type = ExposureCompensator::CHANNELS_BLOCKS; 298 else
299 { 300 cout << "Bad exposure compensation method\n"; 301 return -1; 302 } 303 i++; 304 } 305 else if (string(argv[i]) == "--expos_comp_nr_feeds") 306 { 307 expos_comp_nr_feeds = atoi(argv[i + 1]); 308 i++; 309 } 310 else if (string(argv[i]) == "--expos_comp_nr_filtering") 311 { 312 expos_comp_nr_filtering = atoi(argv[i + 1]); 313 i++; 314 } 315 else if (string(argv[i]) == "--expos_comp_block_size") 316 { 317 expos_comp_block_size = atoi(argv[i + 1]); 318 i++; 319 } 320 else if (string(argv[i]) == "--seam") 321 { 322 if (string(argv[i + 1]) == "no" ||
323 string(argv[i + 1]) == "voronoi" ||
324 string(argv[i + 1]) == "gc_color" ||
325 string(argv[i + 1]) == "gc_colorgrad" ||
326 string(argv[i + 1]) == "dp_color" ||
327 string(argv[i + 1]) == "dp_colorgrad") 328 seam_find_type = argv[i + 1]; 329 else
330 { 331 cout << "Bad seam finding method\n"; 332 return -1; 333 } 334 i++; 335 } 336 else if (string(argv[i]) == "--blend") 337 { 338 if (string(argv[i + 1]) == "no") 339 blend_type = Blender::NO; 340 else if (string(argv[i + 1]) == "feather") 341 blend_type = Blender::FEATHER; 342 else if (string(argv[i + 1]) == "multiband") 343 blend_type = Blender::MULTI_BAND; 344 else
345 { 346 cout << "Bad blending method\n"; 347 return -1; 348 } 349 i++; 350 } 351 else if (string(argv[i]) == "--timelapse") 352 { 353 timelapse = true; 354
355 if (string(argv[i + 1]) == "as_is") 356 timelapse_type = Timelapser::AS_IS; 357 else if (string(argv[i + 1]) == "crop") 358 timelapse_type = Timelapser::CROP; 359 else
360 { 361 cout << "Bad timelapse method\n"; 362 return -1; 363 } 364 i++; 365 } 366 else if (string(argv[i]) == "--rangewidth") 367 { 368 range_width = atoi(argv[i + 1]); 369 i++; 370 } 371 else if (string(argv[i]) == "--blend_strength") 372 { 373 blend_strength = static_cast<float>(atof(argv[i + 1])); 374 i++; 375 } 376 else if (string(argv[i]) == "--output") 377 { 378 result_name = argv[i + 1]; 379 i++; 380 } 381 else
382 img_names.push_back(argv[i]); 383 } 384 if (preview) 385 { 386 compose_megapix = 0.6; 387 } 388 return 0; 389 } 390
391
392 int main(int argc, char* argv[]) 393 { 394 #if ENABLE_LOG
395 int64 app_start_time = getTickCount(); 396 #endif
397
398 #if 0
399 cv::setBreakOnError(true); 400 #endif
401
402 int retval = parseCmdArgs(argc, argv); 403 if (retval) 404 return retval; 405
406 // Check if have enough images
407 int num_images = static_cast<int>(img_names.size()); 408 if (num_images < 2) 409 { 410 LOGLN("Need more images"); 411 return -1; 412 } 413
414 double work_scale = 1, seam_scale = 1, compose_scale = 1; 415 bool is_work_scale_set = false, is_seam_scale_set = false, is_compose_scale_set = false; 416
417 LOGLN("Finding features..."); 418 #if ENABLE_LOG
419 int64 t = getTickCount(); 420 #endif
421
422 Ptr<Feature2D> finder; 423 if (features_type == "orb") 424 { 425 finder = ORB::create(); 426 } 427 else if (features_type == "akaze") 428 { 429 finder = AKAZE::create(); 430 } 431 #ifdef HAVE_OPENCV_XFEATURES2D 432 else if (features_type == "surf") 433 { 434 finder = xfeatures2d::SURF::create(); 435 } 436 #endif
437 else if (features_type == "sift") 438 { 439 finder = SIFT::create(); 440 } 441 else
442 { 443 cout << "Unknown 2D features type: '" << features_type << "'.\n"; 444 return -1; 445 } 446
447 Mat full_img, img; 448 vector<ImageFeatures> features(num_images); 449 vector<Mat> images(num_images); 450 vector<Size> full_img_sizes(num_images); 451 double seam_work_aspect = 1; 452
453 for (int i = 0; i < num_images; ++i) 454 { 455 full_img = imread(samples::findFile(img_names[i])); 456 full_img_sizes[i] = full_img.size(); 457
458 if (full_img.empty()) 459 { 460 LOGLN("Can't open image " << img_names[i]); 461 return -1; 462 } 463 if (work_megapix < 0) 464 { 465 img = full_img; 466 work_scale = 1; 467 is_work_scale_set = true; 468 } 469 else
470 { 471 if (!is_work_scale_set) 472 { 473 work_scale = min(1.0, sqrt(work_megapix * 1e6 / full_img.size().area())); 474 is_work_scale_set = true; 475 } 476 resize(full_img, img, Size(), work_scale, work_scale, INTER_LINEAR_EXACT); 477 } 478 if (!is_seam_scale_set) 479 { 480 seam_scale = min(1.0, sqrt(seam_megapix * 1e6 / full_img.size().area())); 481 seam_work_aspect = seam_scale / work_scale; 482 is_seam_scale_set = true; 483 } 484
485 computeImageFeatures(finder, img, features[i]); 486 features[i].img_idx = i; 487 LOGLN("Features in image #" << i + 1 << ": " << features[i].keypoints.size()); 488
489 resize(full_img, img, Size(), seam_scale, seam_scale, INTER_LINEAR_EXACT); 490 images[i] = img.clone(); 491 } 492
493 full_img.release(); 494 img.release(); 495
496 LOGLN("Finding features, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec"); 497
498 LOG("Pairwise matching"); 499 #if ENABLE_LOG
500 t = getTickCount(); 501 #endif
502 vector<MatchesInfo> pairwise_matches; 503 Ptr<FeaturesMatcher> matcher; 504 if (matcher_type == "affine") 505 matcher = makePtr<AffineBestOf2NearestMatcher>(false, try_cuda, match_conf); 506 else if (range_width == -1) 507 matcher = makePtr<BestOf2NearestMatcher>(try_cuda, match_conf); 508 else
509 matcher = makePtr<BestOf2NearestRangeMatcher>(range_width, try_cuda, match_conf); 510
511 (*matcher)(features, pairwise_matches); 512 matcher->collectGarbage(); 513
514 LOGLN("Pairwise matching, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec"); 515
516 // Check if we should save matches graph
517 if (save_graph) 518 { 519 LOGLN("Saving matches graph..."); 520 ofstream f(save_graph_to.c_str()); 521 f << matchesGraphAsString(img_names, pairwise_matches, conf_thresh); 522 } 523
524 // Leave only images we are sure are from the same panorama
525 vector<int> indices = leaveBiggestComponent(features, pairwise_matches, conf_thresh); 526 vector<Mat> img_subset; 527 vector<String> img_names_subset; 528 vector<Size> full_img_sizes_subset; 529 for (size_t i = 0; i < indices.size(); ++i) 530 { 531 img_names_subset.push_back(img_names[indices[i]]); 532 img_subset.push_back(images[indices[i]]); 533 full_img_sizes_subset.push_back(full_img_sizes[indices[i]]); 534 } 535
536 images = img_subset; 537 img_names = img_names_subset; 538 full_img_sizes = full_img_sizes_subset; 539
540 // Check if we still have enough images
541 num_images = static_cast<int>(img_names.size()); 542 if (num_images < 2) 543 { 544 LOGLN("Need more images"); 545 return -1; 546 } 547
548 Ptr<Estimator> estimator; 549 if (estimator_type == "affine") 550 estimator = makePtr<AffineBasedEstimator>(); 551 else
552 estimator = makePtr<HomographyBasedEstimator>(); 553
554 vector<CameraParams> cameras; 555 if (!(*estimator)(features, pairwise_matches, cameras)) 556 { 557 cout << "Homography estimation failed.\n"; 558 return -1; 559 } 560
561 for (size_t i = 0; i < cameras.size(); ++i) 562 { 563 Mat R; 564 cameras[i].R.convertTo(R, CV_32F); 565 cameras[i].R = R; 566 LOGLN("Initial camera intrinsics #" << indices[i] + 1 << ":\nK:\n" << cameras[i].K() << "\nR:\n" << cameras[i].R); 567 } 568
569 Ptr<detail::BundleAdjusterBase> adjuster; 570 if (ba_cost_func == "reproj") adjuster = makePtr<detail::BundleAdjusterReproj>(); 571 else if (ba_cost_func == "ray") adjuster = makePtr<detail::BundleAdjusterRay>(); 572 else if (ba_cost_func == "affine") adjuster = makePtr<detail::BundleAdjusterAffinePartial>(); 573 else if (ba_cost_func == "no") adjuster = makePtr<NoBundleAdjuster>(); 574 else
575 { 576 cout << "Unknown bundle adjustment cost function: '" << ba_cost_func << "'.\n"; 577 return -1; 578 } 579 adjuster->setConfThresh(conf_thresh); 580 Mat_<uchar> refine_mask = Mat::zeros(3, 3, CV_8U); 581 if (ba_refine_mask[0] == 'x') refine_mask(0, 0) = 1; 582 if (ba_refine_mask[1] == 'x') refine_mask(0, 1) = 1; 583 if (ba_refine_mask[2] == 'x') refine_mask(0, 2) = 1; 584 if (ba_refine_mask[3] == 'x') refine_mask(1, 1) = 1; 585 if (ba_refine_mask[4] == 'x') refine_mask(1, 2) = 1; 586 adjuster->setRefinementMask(refine_mask); 587 if (!(*adjuster)(features, pairwise_matches, cameras)) 588 { 589 cout << "Camera parameters adjusting failed.\n"; 590 return -1; 591 } 592
593 // Find median focal length
594
595 vector<double> focals; 596 for (size_t i = 0; i < cameras.size(); ++i) 597 { 598 LOGLN("Camera #" << indices[i] + 1 << ":\nK:\n" << cameras[i].K() << "\nR:\n" << cameras[i].R); 599 focals.push_back(cameras[i].focal); 600 } 601
602 sort(focals.begin(), focals.end()); 603 float warped_image_scale; 604 if (focals.size() % 2 == 1) 605 warped_image_scale = static_cast<float>(focals[focals.size() / 2]); 606 else
607 warped_image_scale = static_cast<float>(focals[focals.size() / 2 - 1] + focals[focals.size() / 2]) * 0.5f; 608
609 if (do_wave_correct) 610 { 611 vector<Mat> rmats; 612 for (size_t i = 0; i < cameras.size(); ++i) 613 rmats.push_back(cameras[i].R.clone()); 614 waveCorrect(rmats, wave_correct); 615 for (size_t i = 0; i < cameras.size(); ++i) 616 cameras[i].R = rmats[i]; 617 } 618
619 LOGLN("Warping images (auxiliary)... "); 620 #if ENABLE_LOG
621 t = getTickCount(); 622 #endif
623
624 vector<Point> corners(num_images); 625 vector<UMat> masks_warped(num_images); 626 vector<UMat> images_warped(num_images); 627 vector<Size> sizes(num_images); 628 vector<UMat> masks(num_images); 629
630 // Prepare images masks
631 for (int i = 0; i < num_images; ++i) 632 { 633 masks[i].create(images[i].size(), CV_8U); 634 masks[i].setTo(Scalar::all(255)); 635 } 636
637 // Warp images and their masks
638
639 Ptr<WarperCreator> warper_creator; 640 #ifdef HAVE_OPENCV_CUDAWARPING 641 if (try_cuda && cuda::getCudaEnabledDeviceCount() > 0) 642 { 643 if (warp_type == "plane") 644 warper_creator = makePtr<cv::PlaneWarperGpu>(); 645 else if (warp_type == "cylindrical") 646 warper_creator = makePtr<cv::CylindricalWarperGpu>(); 647 else if (warp_type == "spherical") 648 warper_creator = makePtr<cv::SphericalWarperGpu>(); 649 } 650 else
651 #endif
652 { 653 if (warp_type == "plane") 654 warper_creator = makePtr<cv::PlaneWarper>(); 655 else if (warp_type == "affine") 656 warper_creator = makePtr<cv::AffineWarper>(); 657 else if (warp_type == "cylindrical") 658 warper_creator = makePtr<cv::CylindricalWarper>(); 659 else if (warp_type == "spherical") 660 warper_creator = makePtr<cv::SphericalWarper>(); 661 else if (warp_type == "fisheye") 662 warper_creator = makePtr<cv::FisheyeWarper>(); 663 else if (warp_type == "stereographic") 664 warper_creator = makePtr<cv::StereographicWarper>(); 665 else if (warp_type == "compressedPlaneA2B1") 666 warper_creator = makePtr<cv::CompressedRectilinearWarper>(2.0f, 1.0f); 667 else if (warp_type == "compressedPlaneA1.5B1") 668 warper_creator = makePtr<cv::CompressedRectilinearWarper>(1.5f, 1.0f); 669 else if (warp_type == "compressedPlanePortraitA2B1") 670 warper_creator = makePtr<cv::CompressedRectilinearPortraitWarper>(2.0f, 1.0f); 671 else if (warp_type == "compressedPlanePortraitA1.5B1") 672 warper_creator = makePtr<cv::CompressedRectilinearPortraitWarper>(1.5f, 1.0f); 673 else if (warp_type == "paniniA2B1") 674 warper_creator = makePtr<cv::PaniniWarper>(2.0f, 1.0f); 675 else if (warp_type == "paniniA1.5B1") 676 warper_creator = makePtr<cv::PaniniWarper>(1.5f, 1.0f); 677 else if (warp_type == "paniniPortraitA2B1") 678 warper_creator = makePtr<cv::PaniniPortraitWarper>(2.0f, 1.0f); 679 else if (warp_type == "paniniPortraitA1.5B1") 680 warper_creator = makePtr<cv::PaniniPortraitWarper>(1.5f, 1.0f); 681 else if (warp_type == "mercator") 682 warper_creator = makePtr<cv::MercatorWarper>(); 683 else if (warp_type == "transverseMercator") 684 warper_creator = makePtr<cv::TransverseMercatorWarper>(); 685 } 686
687 if (!warper_creator) 688 { 689 cout << "Can't create the following warper '" << warp_type << "'\n"; 690 return 1; 691 } 692
693 Ptr<RotationWarper> warper = warper_creator->create(static_cast<float>(warped_image_scale * seam_work_aspect)); 694
695 for (int i = 0; i < num_images; ++i) 696 { 697 Mat_<float> K; 698 cameras[i].K().convertTo(K, CV_32F); 699 float swa = (float)seam_work_aspect; 700 K(0, 0) *= swa; K(0, 2) *= swa; 701 K(1, 1) *= swa; K(1, 2) *= swa; 702
703 corners[i] = warper->warp(images[i], K, cameras[i].R, INTER_LINEAR, BORDER_REFLECT, images_warped[i]); 704 sizes[i] = images_warped[i].size(); 705
706 warper->warp(masks[i], K, cameras[i].R, INTER_NEAREST, BORDER_CONSTANT, masks_warped[i]); 707 } 708
709 vector<UMat> images_warped_f(num_images); 710 for (int i = 0; i < num_images; ++i) 711 images_warped[i].convertTo(images_warped_f[i], CV_32F); 712
713 LOGLN("Warping images, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec"); 714
715 LOGLN("Compensating exposure..."); 716 #if ENABLE_LOG
717 t = getTickCount(); 718 #endif
719
720 Ptr<ExposureCompensator> compensator = ExposureCompensator::createDefault(expos_comp_type); 721 if (dynamic_cast<GainCompensator*>(compensator.get())) 722 { 723 GainCompensator* gcompensator = dynamic_cast<GainCompensator*>(compensator.get()); 724 gcompensator->setNrFeeds(expos_comp_nr_feeds); 725 } 726
727 if (dynamic_cast<ChannelsCompensator*>(compensator.get())) 728 { 729 ChannelsCompensator* ccompensator = dynamic_cast<ChannelsCompensator*>(compensator.get()); 730 ccompensator->setNrFeeds(expos_comp_nr_feeds); 731 } 732
733 if (dynamic_cast<BlocksCompensator*>(compensator.get())) 734 { 735 BlocksCompensator* bcompensator = dynamic_cast<BlocksCompensator*>(compensator.get()); 736 bcompensator->setNrFeeds(expos_comp_nr_feeds); 737 bcompensator->setNrGainsFilteringIterations(expos_comp_nr_filtering); 738 bcompensator->setBlockSize(expos_comp_block_size, expos_comp_block_size); 739 } 740
741 compensator->feed(corners, images_warped, masks_warped); 742
743 LOGLN("Compensating exposure, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec"); 744
745 LOGLN("Finding seams..."); 746 #if ENABLE_LOG
747 t = getTickCount(); 748 #endif
749
750 Ptr<SeamFinder> seam_finder; 751 if (seam_find_type == "no") 752 seam_finder = makePtr<detail::NoSeamFinder>(); 753 else if (seam_find_type == "voronoi") 754 seam_finder = makePtr<detail::VoronoiSeamFinder>(); 755 else if (seam_find_type == "gc_color") 756 { 757 #ifdef HAVE_OPENCV_CUDALEGACY 758 if (try_cuda && cuda::getCudaEnabledDeviceCount() > 0) 759 seam_finder = makePtr<detail::GraphCutSeamFinderGpu>(GraphCutSeamFinderBase::COST_COLOR); 760 else
761 #endif
762 seam_finder = makePtr<detail::GraphCutSeamFinder>(GraphCutSeamFinderBase::COST_COLOR); 763 } 764 else if (seam_find_type == "gc_colorgrad") 765 { 766 #ifdef HAVE_OPENCV_CUDALEGACY 767 if (try_cuda && cuda::getCudaEnabledDeviceCount() > 0) 768 seam_finder = makePtr<detail::GraphCutSeamFinderGpu>(GraphCutSeamFinderBase::COST_COLOR_GRAD); 769 else
770 #endif
771 seam_finder = makePtr<detail::GraphCutSeamFinder>(GraphCutSeamFinderBase::COST_COLOR_GRAD); 772 } 773 else if (seam_find_type == "dp_color") 774 seam_finder = makePtr<detail::DpSeamFinder>(DpSeamFinder::COLOR); 775 else if (seam_find_type == "dp_colorgrad") 776 seam_finder = makePtr<detail::DpSeamFinder>(DpSeamFinder::COLOR_GRAD); 777 if (!seam_finder) 778 { 779 cout << "Can't create the following seam finder '" << seam_find_type << "'\n"; 780 return 1; 781 } 782
783 seam_finder->find(images_warped_f, corners, masks_warped); 784
785 LOGLN("Finding seams, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec"); 786
787 // Release unused memory
788 images.clear(); 789 images_warped.clear(); 790 images_warped_f.clear(); 791 masks.clear(); 792
793 LOGLN("Compositing..."); 794 #if ENABLE_LOG
795 t = getTickCount(); 796 #endif
797
798 Mat img_warped, img_warped_s; 799 Mat dilated_mask, seam_mask, mask, mask_warped; 800 Ptr<Blender> blender; 801 Ptr<Timelapser> timelapser; 802 //double compose_seam_aspect = 1;
803 double compose_work_aspect = 1; 804
805 for (int img_idx = 0; img_idx < num_images; ++img_idx) 806 { 807 LOGLN("Compositing image #" << indices[img_idx] + 1); 808
809 // Read image and resize it if necessary
810 full_img = imread(samples::findFile(img_names[img_idx])); 811 if (!is_compose_scale_set) 812 { 813 if (compose_megapix > 0) 814 compose_scale = min(1.0, sqrt(compose_megapix * 1e6 / full_img.size().area())); 815 is_compose_scale_set = true; 816
817 // Compute relative scales 818 //compose_seam_aspect = compose_scale / seam_scale;
819 compose_work_aspect = compose_scale / work_scale; 820
821 // Update warped image scale
822 warped_image_scale *= static_cast<float>(compose_work_aspect); 823 warper = warper_creator->create(warped_image_scale); 824
825 // Update corners and sizes
826 for (int i = 0; i < num_images; ++i) 827 { 828 // Update intrinsics
829 cameras[i].focal *= compose_work_aspect; 830 cameras[i].ppx *= compose_work_aspect; 831 cameras[i].ppy *= compose_work_aspect; 832
833 // Update corner and size
834 Size sz = full_img_sizes[i]; 835 if (std::abs(compose_scale - 1) > 1e-1) 836 { 837 sz.width = cvRound(full_img_sizes[i].width * compose_scale); 838 sz.height = cvRound(full_img_sizes[i].height * compose_scale); 839 } 840
841 Mat K; 842 cameras[i].K().convertTo(K, CV_32F); 843 Rect roi = warper->warpRoi(sz, K, cameras[i].R); 844 corners[i] = roi.tl(); 845 sizes[i] = roi.size(); 846 } 847 } 848 if (abs(compose_scale - 1) > 1e-1) 849 resize(full_img, img, Size(), compose_scale, compose_scale, INTER_LINEAR_EXACT); 850 else
851 img = full_img; 852 full_img.release(); 853 Size img_size = img.size(); 854
855 Mat K; 856 cameras[img_idx].K().convertTo(K, CV_32F); 857
858 // Warp the current image
859 warper->warp(img, K, cameras[img_idx].R, INTER_LINEAR, BORDER_REFLECT, img_warped); 860
861 // Warp the current image mask
862 mask.create(img_size, CV_8U); 863 mask.setTo(Scalar::all(255)); 864 warper->warp(mask, K, cameras[img_idx].R, INTER_NEAREST, BORDER_CONSTANT, mask_warped); 865
866 // Compensate exposure
867 compensator->apply(img_idx, corners[img_idx], img_warped, mask_warped); 868
869 img_warped.convertTo(img_warped_s, CV_16S); 870 img_warped.release(); 871 img.release(); 872 mask.release(); 873
874 dilate(masks_warped[img_idx], dilated_mask, Mat()); 875 resize(dilated_mask, seam_mask, mask_warped.size(), 0, 0, INTER_LINEAR_EXACT); 876 mask_warped = seam_mask & mask_warped; 877
878 if (!blender && !timelapse) 879 { 880 blender = Blender::createDefault(blend_type, try_cuda); 881 Size dst_sz = resultRoi(corners, sizes).size(); 882 float blend_width = sqrt(static_cast<float>(dst_sz.area())) * blend_strength / 100.f; 883 if (blend_width < 1.f) 884 blender = Blender::createDefault(Blender::NO, try_cuda); 885 else if (blend_type == Blender::MULTI_BAND) 886 { 887 MultiBandBlender* mb = dynamic_cast<MultiBandBlender*>(blender.get()); 888 mb->setNumBands(static_cast<int>(ceil(log(blend_width) / log(2.)) - 1.)); 889 LOGLN("Multi-band blender, number of bands: " << mb->numBands()); 890 } 891 else if (blend_type == Blender::FEATHER) 892 { 893 FeatherBlender* fb = dynamic_cast<FeatherBlender*>(blender.get()); 894 fb->setSharpness(1.f / blend_width); 895 LOGLN("Feather blender, sharpness: " << fb->sharpness()); 896 } 897 blender->prepare(corners, sizes); 898 } 899 else if (!timelapser && timelapse) 900 { 901 timelapser = Timelapser::createDefault(timelapse_type); 902 timelapser->initialize(corners, sizes); 903 } 904
905 // Blend the current image
906 if (timelapse) 907 { 908 timelapser->process(img_warped_s, Mat::ones(img_warped_s.size(), CV_8UC1), corners[img_idx]); 909 String fixedFileName; 910 size_t pos_s = String(img_names[img_idx]).find_last_of("/\\"); 911 if (pos_s == String::npos) 912 { 913 fixedFileName = "fixed_" + img_names[img_idx]; 914 } 915 else
916 { 917 fixedFileName = "fixed_" + String(img_names[img_idx]).substr(pos_s + 1, String(img_names[img_idx]).length() - pos_s); 918 } 919 imwrite(fixedFileName, timelapser->getDst()); 920 } 921 else
922 { 923 blender->feed(img_warped_s, mask_warped, corners[img_idx]); 924 } 925 } 926
927 if (!timelapse) 928 { 929 Mat result, result_mask; 930 blender->blend(result, result_mask); 931
932 LOGLN("Compositing, time: " << ((getTickCount() - t) / getTickFrequency()) << " sec"); 933
934 imwrite(result_name, result); 935 } 936
937 LOGLN("Finished, total time: " << ((getTickCount() - app_start_time) / getTickFrequency()) << " sec"); 938 return 0; 939 }
stitching_detail 程序运行流程
- 命令行调用程序,输入源图像以及程序的参数
- 特征点检测,判断是使用 surf 还是 orb,默认是 surf
- 对图像的特征点进行匹配,使用最近邻和次近邻方法,将两个最优的匹配的置信度 保存下来
- 对图像进行排序以及将置信度高的图像保存到同一个集合中,删除置信度比较低的图像间的匹配,得到能正确匹配的图像序列。这样将置信度高于门限的所有匹配合并到一个集合中
- 对所有图像进行相机参数粗略估计,然后求出旋转矩阵
- 使用光束平均法进一步精准的估计出旋转矩阵
- 波形校正,水平或者垂直
- 拼接
- 融合,多频段融合,光照补偿
stitching_detail 程序接口介绍
- img1 img2 img3 输入图像
- --preview 以预览模式运行程序,比正常模式要快,但输出图像分辨率低,拼接的分辨 率 compose_megapix 设置为 0.6
- --try_gpu (yes|no) 是否使用 CUDA加速,默认为 no,使用CPU模式
- /* 运动估计参数 */
- --work_megapix <--work_megapix <float>> 图像匹配时的分辨率大小,默认为 0.6
- --features (surf | orb | sift | akaze) 选择 surf 或者 orb 算法进行特征点匹配,默认为 surf
- --matcher (homography | affine) 用于成对图像匹配的匹配器
- --estimator (homography | affine) 用于转换估计的估计器类型
- --match_conf <float> 特征点匹配步骤的匹配置信度,最近邻匹配距离与次近邻匹配距离的比值,surf 默认为 0.65,orb 默认为 0.3
- --conf_thresh <float> 两幅图来自同一全景图的置信度,默认为 1.0
- --ba (no | reproj | ray | affine) 光束平均法的误差函数选择,默认是 ray 方法
- --ba_refine_mask (mask) 光束平均法设置优化掩码
- --wave_correct (no|horiz|vert) 波形校验水平,垂直或者没有 默认是 horiz(水平)
- --save_graph <file_name> 将匹配的图形以点的形式保存到文件中, Nm 代表匹配的数量,NI代表正确匹配的数量,C 表示置信度
- /*图像融合参数:*/
- --warp (plane|cylindrical|spherical|fisheye|stereographic|compressedPlaneA2B1|compressedPla neA1.5B1|compressedPlanePortraitA2B1|compressedPlanePortraitA1.5B1|paniniA2B1|paniniA1.5B1|paniniPortraitA2B1|paniniPor traitA1.5B1|mercator|transverseMercator) 选择融合的平面,默认是球形
- --seam_megapix <float> 拼接缝像素的大小 默认是 0.1
- --seam (no|voronoi|gc_color|gc_colorgrad) 拼接缝隙估计方法 默认是 gc_color
- --compose_megapix <float> 拼接分辨率,默认为-1
- --expos_comp (no|gain|gain_blocks) 光照补偿方法,默认是 gain_blocks
- --blend (no|feather|multiband) 融合方法,默认是多频段融合
- --blend_strength <float> 融合强度,0-100.默认是 5.
- --output <result_img> 输出图像的文件名,默认是 result,jpg 命令使用实例,以及程序运行时的提示:
上面使用默认参数,详细输出信息如下:
1 E:\Practice\OpenCV\Algorithm_Summary\Image_Stitching\x64\Debug>05_Image_Stitch_Stitching_Detailed.exe ./imgs/boat1.jpg ./imgs/boat2.jpg ./imgs/boat3.jpg ./imgs/boat4.jpg ./imgs/boat5.jpg ./imgs/boat6.jpg 2 Finding features... 3 [ INFO:0] global C:\build\master_winpack-build-win64-vc15\opencv\modules\core\src\ocl.cpp (891) cv::ocl::haveOpenCL Initialize OpenCL runtime... 4 Features in image #1: 500
5 [ INFO:0] global C:\build\master_winpack-build-win64-vc15\opencv\modules\core\src\ocl.cpp (433) cv::ocl::OpenCLBinaryCacheConfigurator::OpenCLBinaryCacheConfigurator Successfully initialized OpenCL cache directory: C:\Users\A4080599\AppData\Local\Temp\opencv\4.4\opencl_cache\ 6 [ INFO:0] global C:\build\master_winpack-build-win64-vc15\opencv\modules\core\src\ocl.cpp (457) cv::ocl::OpenCLBinaryCacheConfigurator::prepareCacheDirectoryForContext Preparing OpenCL cache configuration for context: NVIDIA_Corporation--GeForce_GTX_1070--411_31 7 Features in image #2: 500
8 Features in image #3: 500
9 Features in image #4: 500
10 Features in image #5: 500
11 Features in image #6: 500
12 Finding features, time: 5.46377 sec 13 Pairwise matchingPairwise matching, time: 3.24159 sec 14 Initial camera intrinsics #1: 15 K: 16 [534.6674906996568, 0, 474.5; 17 0, 534.6674906996568, 316; 18 0, 0, 1] 19 R: 20 [0.91843718, -0.09762425, -1.1678253; 21 0.0034433089, 1.0835428, -0.025021957; 22 0.28152198, 0.16100603, 0.91920781] 23 Initial camera intrinsics #2: 24 K: 25 [534.6674906996568, 0, 474.5; 26 0, 534.6674906996568, 316; 27 0, 0, 1] 28 R: 29 [1.001171, -0.085758291, -0.64530683; 30 0.010103324, 1.0520245, -0.030576767; 31 0.15743911, 0.12035993, 1] 32 Initial camera intrinsics #3: 33 K: 34 [534.6674906996568, 0, 474.5; 35 0, 534.6674906996568, 316; 36 0, 0, 1] 37 R: 38 [1, 0, 0; 39 0, 1, 0; 40 0, 0, 1] 41 Initial camera intrinsics #4: 42 K: 43 [534.6674906996568, 0, 474.5; 44 0, 534.6674906996568, 316; 45 0, 0, 1] 46 R: 47 [0.8474561, 0.028589081, 0.75133896; 48 -0.0014587968, 0.92028928, 0.033205934; 49 -0.17483309, 0.018777205, 0.84592116] 50 Initial camera intrinsics #5: 51 K: 52 [534.6674906996568, 0, 474.5; 53 0, 534.6674906996568, 316; 54 0, 0, 1] 55 R: 56 [0.60283858, 0.069275051, 1.2121853; 57 -0.014153662, 0.85474133, 0.014057174; 58 -0.29529575, 0.053770453, 0.61932623] 59 Initial camera intrinsics #6: 60 K: 61 [534.6674906996568, 0, 474.5; 62 0, 534.6674906996568, 316; 63 0, 0, 1] 64 R: 65 [0.41477469, 0.075901195, 1.4396564; 66 -0.015423983, 0.82344943, 0.0061162044; 67 -0.35168326, 0.055747174, 0.42653102] 68 Camera #1: 69 K: 70 [1068.953598931666, 0, 474.5; 71 0, 1068.953598931666, 316; 72 0, 0, 1] 73 R: 74 [0.84266716, -0.010490002, -0.53833258; 75 0.004485324, 0.99991232, -0.01246338; 76 0.53841609, 0.0080878884, 0.84264034] 77 Camera #2: 78 K: 79 [1064.878323247434, 0, 474.5; 80 0, 1064.878323247434, 316; 81 0, 0, 1] 82 R: 83 [0.95117813, -0.015436338, -0.3082563; 84 0.01137107, 0.99982315, -0.014980057; 85 0.308433, 0.010743499, 0.95118535] 86 Camera #3: 87 K: 88 [1065.382193682081, 0, 474.5; 89 0, 1065.382193682081, 316; 90 0, 0, 1] 91 R: 92 [1, -1.6298145e-09, 0; 93 -1.5716068e-09, 1, 0; 94 0, 0, 1] 95 Camera #4: 96 K: 97 [1067.611537959627, 0, 474.5; 98 0, 1067.611537959627, 316; 99 0, 0, 1] 100 R: 101 [0.91316396, -7.9067249e-06, 0.40759254; 102 -0.0075879274, 0.99982637, 0.017019274; 103 -0.4075219, -0.018634165, 0.91300529] 104 Camera #5: 105 K: 106 [1080.708135180496, 0, 474.5; 107 0, 1080.708135180496, 316; 108 0, 0, 1] 109 R: 110 [0.70923853, 0.0025724203, 0.70496398; 111 -0.0098195076, 0.99993235, 0.0062302947; 112 -0.70490021, -0.01134116, 0.70921582] 113 Camera #6: 114 K: 115 [1080.90412660159, 0, 474.5; 116 0, 1080.90412660159, 316; 117 0, 0, 1] 118 R: 119 [0.49985889, 3.5938341e-05, 0.86610687; 120 -0.00682831, 0.99996907, 0.0038993564; 121 -0.86607999, -0.0078631733, 0.49984369] 122 Warping images (auxiliary)... 123 Warping images, time: 0.0791121 sec 124 Compensating exposure... 125 Compensating exposure, time: 0.72288 sec 126 Finding seams... 127 Finding seams, time: 3.09237 sec 128 Compositing... 129 Compositing image #1
130 Multi-band blender, number of bands: 8
131 Compositing image #2
132 Compositing image #3
133 Compositing image #4
134 Compositing image #5
135 Compositing image #6
136 Compositing, time: 13.7766 sec 137 Finished, total time: 29.4535 sec
输入图像boat1.jpg、boat2.jpg、boat3.jpg、boat4.jpg、boat5.jpg、boat6.jpg如下(可以在OpenCV安装目录下找到D:\OpenCV4.4\opencv_extra-master\testdata\stitching)
结果图:
参数warp_type 设置为"plane",效果图如下:
参数warp_type 设置为"fisheye",效果图如下(旋转90°后):
其他的参数可以根据自己需要修改,如果要自己完成还需要详细了解拼接步骤再优化。