stitching_detailed.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. """
  2. Stitching sample (advanced)
  3. ===========================
  4. Show how to use Stitcher API from python.
  5. """
  6. # Python 2/3 compatibility
  7. from __future__ import print_function
  8. import argparse
  9. from collections import OrderedDict
  10. import cv2 as cv
  11. import numpy as np
  12. EXPOS_COMP_CHOICES = OrderedDict()
  13. EXPOS_COMP_CHOICES['gain_blocks'] = cv.detail.ExposureCompensator_GAIN_BLOCKS
  14. EXPOS_COMP_CHOICES['gain'] = cv.detail.ExposureCompensator_GAIN
  15. EXPOS_COMP_CHOICES['channel'] = cv.detail.ExposureCompensator_CHANNELS
  16. EXPOS_COMP_CHOICES['channel_blocks'] = cv.detail.ExposureCompensator_CHANNELS_BLOCKS
  17. EXPOS_COMP_CHOICES['no'] = cv.detail.ExposureCompensator_NO
  18. BA_COST_CHOICES = OrderedDict()
  19. BA_COST_CHOICES['ray'] = cv.detail_BundleAdjusterRay
  20. BA_COST_CHOICES['reproj'] = cv.detail_BundleAdjusterReproj
  21. BA_COST_CHOICES['affine'] = cv.detail_BundleAdjusterAffinePartial
  22. BA_COST_CHOICES['no'] = cv.detail_NoBundleAdjuster
  23. FEATURES_FIND_CHOICES = OrderedDict()
  24. try:
  25. cv.xfeatures2d_SURF.create() # check if the function can be called
  26. FEATURES_FIND_CHOICES['surf'] = cv.xfeatures2d_SURF.create
  27. except (AttributeError, cv.error) as e:
  28. print("SURF not available")
  29. # if SURF not available, ORB is default
  30. FEATURES_FIND_CHOICES['orb'] = cv.ORB.create
  31. try:
  32. FEATURES_FIND_CHOICES['sift'] = cv.SIFT_create
  33. except AttributeError:
  34. print("SIFT not available")
  35. try:
  36. FEATURES_FIND_CHOICES['brisk'] = cv.BRISK_create
  37. except AttributeError:
  38. print("BRISK not available")
  39. try:
  40. FEATURES_FIND_CHOICES['akaze'] = cv.AKAZE_create
  41. except AttributeError:
  42. print("AKAZE not available")
  43. SEAM_FIND_CHOICES = OrderedDict()
  44. SEAM_FIND_CHOICES['dp_color'] = cv.detail_DpSeamFinder('COLOR')
  45. SEAM_FIND_CHOICES['dp_colorgrad'] = cv.detail_DpSeamFinder('COLOR_GRAD')
  46. SEAM_FIND_CHOICES['voronoi'] = cv.detail.SeamFinder_createDefault(cv.detail.SeamFinder_VORONOI_SEAM)
  47. SEAM_FIND_CHOICES['no'] = cv.detail.SeamFinder_createDefault(cv.detail.SeamFinder_NO)
  48. ESTIMATOR_CHOICES = OrderedDict()
  49. ESTIMATOR_CHOICES['homography'] = cv.detail_HomographyBasedEstimator
  50. ESTIMATOR_CHOICES['affine'] = cv.detail_AffineBasedEstimator
  51. WARP_CHOICES = (
  52. 'spherical',
  53. 'plane',
  54. 'affine',
  55. 'cylindrical',
  56. 'fisheye',
  57. 'stereographic',
  58. 'compressedPlaneA2B1',
  59. 'compressedPlaneA1.5B1',
  60. 'compressedPlanePortraitA2B1',
  61. 'compressedPlanePortraitA1.5B1',
  62. 'paniniA2B1',
  63. 'paniniA1.5B1',
  64. 'paniniPortraitA2B1',
  65. 'paniniPortraitA1.5B1',
  66. 'mercator',
  67. 'transverseMercator',
  68. )
  69. WAVE_CORRECT_CHOICES = OrderedDict()
  70. WAVE_CORRECT_CHOICES['horiz'] = cv.detail.WAVE_CORRECT_HORIZ
  71. WAVE_CORRECT_CHOICES['no'] = None
  72. WAVE_CORRECT_CHOICES['vert'] = cv.detail.WAVE_CORRECT_VERT
  73. BLEND_CHOICES = ('multiband', 'feather', 'no',)
  74. parser = argparse.ArgumentParser(
  75. prog="stitching_detailed.py", description="Rotation model images stitcher"
  76. )
  77. parser.add_argument(
  78. 'img_names', nargs='+',
  79. help="Files to stitch", type=str
  80. )
  81. parser.add_argument(
  82. '--try_cuda',
  83. action='store',
  84. default=False,
  85. help="Try to use CUDA. The default value is no. All default values are for CPU mode.",
  86. type=bool, dest='try_cuda'
  87. )
  88. parser.add_argument(
  89. '--work_megapix', action='store', default=0.6,
  90. help="Resolution for image registration step. The default is 0.6 Mpx",
  91. type=float, dest='work_megapix'
  92. )
  93. parser.add_argument(
  94. '--features', action='store', default=list(FEATURES_FIND_CHOICES.keys())[0],
  95. help="Type of features used for images matching. The default is '%s'." % list(FEATURES_FIND_CHOICES.keys())[0],
  96. choices=FEATURES_FIND_CHOICES.keys(),
  97. type=str, dest='features'
  98. )
  99. parser.add_argument(
  100. '--matcher', action='store', default='homography',
  101. help="Matcher used for pairwise image matching. The default is 'homography'.",
  102. choices=('homography', 'affine'),
  103. type=str, dest='matcher'
  104. )
  105. parser.add_argument(
  106. '--estimator', action='store', default=list(ESTIMATOR_CHOICES.keys())[0],
  107. help="Type of estimator used for transformation estimation. The default is '%s'." % list(ESTIMATOR_CHOICES.keys())[0],
  108. choices=ESTIMATOR_CHOICES.keys(),
  109. type=str, dest='estimator'
  110. )
  111. parser.add_argument(
  112. '--match_conf', action='store',
  113. help="Confidence for feature matching step. The default is 0.3 for ORB and 0.65 for other feature types.",
  114. type=float, dest='match_conf'
  115. )
  116. parser.add_argument(
  117. '--conf_thresh', action='store', default=1.0,
  118. help="Threshold for two images are from the same panorama confidence.The default is 1.0.",
  119. type=float, dest='conf_thresh'
  120. )
  121. parser.add_argument(
  122. '--ba', action='store', default=list(BA_COST_CHOICES.keys())[0],
  123. help="Bundle adjustment cost function. The default is '%s'." % list(BA_COST_CHOICES.keys())[0],
  124. choices=BA_COST_CHOICES.keys(),
  125. type=str, dest='ba'
  126. )
  127. parser.add_argument(
  128. '--ba_refine_mask', action='store', default='xxxxx',
  129. help="Set refinement mask for bundle adjustment. It looks like 'x_xxx', "
  130. "where 'x' means refine respective parameter and '_' means don't refine, "
  131. "and has the following format:<fx><skew><ppx><aspect><ppy>. "
  132. "The default mask is 'xxxxx'. "
  133. "If bundle adjustment doesn't support estimation of selected parameter then "
  134. "the respective flag is ignored.",
  135. type=str, dest='ba_refine_mask'
  136. )
  137. parser.add_argument(
  138. '--wave_correct', action='store', default=list(WAVE_CORRECT_CHOICES.keys())[0],
  139. help="Perform wave effect correction. The default is '%s'" % list(WAVE_CORRECT_CHOICES.keys())[0],
  140. choices=WAVE_CORRECT_CHOICES.keys(),
  141. type=str, dest='wave_correct'
  142. )
  143. parser.add_argument(
  144. '--save_graph', action='store', default=None,
  145. help="Save matches graph represented in DOT language to <file_name> file.",
  146. type=str, dest='save_graph'
  147. )
  148. parser.add_argument(
  149. '--warp', action='store', default=WARP_CHOICES[0],
  150. help="Warp surface type. The default is '%s'." % WARP_CHOICES[0],
  151. choices=WARP_CHOICES,
  152. type=str, dest='warp'
  153. )
  154. parser.add_argument(
  155. '--seam_megapix', action='store', default=0.1,
  156. help="Resolution for seam estimation step. The default is 0.1 Mpx.",
  157. type=float, dest='seam_megapix'
  158. )
  159. parser.add_argument(
  160. '--seam', action='store', default=list(SEAM_FIND_CHOICES.keys())[0],
  161. help="Seam estimation method. The default is '%s'." % list(SEAM_FIND_CHOICES.keys())[0],
  162. choices=SEAM_FIND_CHOICES.keys(),
  163. type=str, dest='seam'
  164. )
  165. parser.add_argument(
  166. '--compose_megapix', action='store', default=-1,
  167. help="Resolution for compositing step. Use -1 for original resolution. The default is -1",
  168. type=float, dest='compose_megapix'
  169. )
  170. parser.add_argument(
  171. '--expos_comp', action='store', default=list(EXPOS_COMP_CHOICES.keys())[0],
  172. help="Exposure compensation method. The default is '%s'." % list(EXPOS_COMP_CHOICES.keys())[0],
  173. choices=EXPOS_COMP_CHOICES.keys(),
  174. type=str, dest='expos_comp'
  175. )
  176. parser.add_argument(
  177. '--expos_comp_nr_feeds', action='store', default=1,
  178. help="Number of exposure compensation feed.",
  179. type=np.int32, dest='expos_comp_nr_feeds'
  180. )
  181. parser.add_argument(
  182. '--expos_comp_nr_filtering', action='store', default=2,
  183. help="Number of filtering iterations of the exposure compensation gains.",
  184. type=float, dest='expos_comp_nr_filtering'
  185. )
  186. parser.add_argument(
  187. '--expos_comp_block_size', action='store', default=32,
  188. help="BLock size in pixels used by the exposure compensator. The default is 32.",
  189. type=np.int32, dest='expos_comp_block_size'
  190. )
  191. parser.add_argument(
  192. '--blend', action='store', default=BLEND_CHOICES[0],
  193. help="Blending method. The default is '%s'." % BLEND_CHOICES[0],
  194. choices=BLEND_CHOICES,
  195. type=str, dest='blend'
  196. )
  197. parser.add_argument(
  198. '--blend_strength', action='store', default=5,
  199. help="Blending strength from [0,100] range. The default is 5",
  200. type=np.int32, dest='blend_strength'
  201. )
  202. parser.add_argument(
  203. '--output', action='store', default='result.jpg',
  204. help="The default is 'result.jpg'",
  205. type=str, dest='output'
  206. )
  207. parser.add_argument(
  208. '--timelapse', action='store', default=None,
  209. help="Output warped images separately as frames of a time lapse movie, "
  210. "with 'fixed_' prepended to input file names.",
  211. type=str, dest='timelapse'
  212. )
  213. parser.add_argument(
  214. '--rangewidth', action='store', default=-1,
  215. help="uses range_width to limit number of images to match with.",
  216. type=int, dest='rangewidth'
  217. )
  218. __doc__ += '\n' + parser.format_help()
  219. def get_matcher(args):
  220. try_cuda = args.try_cuda
  221. matcher_type = args.matcher
  222. if args.match_conf is None:
  223. if args.features == 'orb':
  224. match_conf = 0.3
  225. else:
  226. match_conf = 0.65
  227. else:
  228. match_conf = args.match_conf
  229. range_width = args.rangewidth
  230. if matcher_type == "affine":
  231. matcher = cv.detail_AffineBestOf2NearestMatcher(False, try_cuda, match_conf)
  232. elif range_width == -1:
  233. matcher = cv.detail_BestOf2NearestMatcher(try_cuda, match_conf)
  234. else:
  235. matcher = cv.detail_BestOf2NearestRangeMatcher(range_width, try_cuda, match_conf)
  236. return matcher
  237. def get_compensator(args):
  238. expos_comp_type = EXPOS_COMP_CHOICES[args.expos_comp]
  239. expos_comp_nr_feeds = args.expos_comp_nr_feeds
  240. expos_comp_block_size = args.expos_comp_block_size
  241. # expos_comp_nr_filtering = args.expos_comp_nr_filtering
  242. if expos_comp_type == cv.detail.ExposureCompensator_CHANNELS:
  243. compensator = cv.detail_ChannelsCompensator(expos_comp_nr_feeds)
  244. # compensator.setNrGainsFilteringIterations(expos_comp_nr_filtering)
  245. elif expos_comp_type == cv.detail.ExposureCompensator_CHANNELS_BLOCKS:
  246. compensator = cv.detail_BlocksChannelsCompensator(
  247. expos_comp_block_size, expos_comp_block_size,
  248. expos_comp_nr_feeds
  249. )
  250. # compensator.setNrGainsFilteringIterations(expos_comp_nr_filtering)
  251. else:
  252. compensator = cv.detail.ExposureCompensator_createDefault(expos_comp_type)
  253. return compensator
  254. def main():
  255. args = parser.parse_args()
  256. img_names = args.img_names
  257. print(img_names)
  258. work_megapix = args.work_megapix
  259. seam_megapix = args.seam_megapix
  260. compose_megapix = args.compose_megapix
  261. conf_thresh = args.conf_thresh
  262. ba_refine_mask = args.ba_refine_mask
  263. wave_correct = WAVE_CORRECT_CHOICES[args.wave_correct]
  264. if args.save_graph is None:
  265. save_graph = False
  266. else:
  267. save_graph = True
  268. warp_type = args.warp
  269. blend_type = args.blend
  270. blend_strength = args.blend_strength
  271. result_name = args.output
  272. if args.timelapse is not None:
  273. timelapse = True
  274. if args.timelapse == "as_is":
  275. timelapse_type = cv.detail.Timelapser_AS_IS
  276. elif args.timelapse == "crop":
  277. timelapse_type = cv.detail.Timelapser_CROP
  278. else:
  279. print("Bad timelapse method")
  280. exit()
  281. else:
  282. timelapse = False
  283. finder = FEATURES_FIND_CHOICES[args.features]()
  284. seam_work_aspect = 1
  285. full_img_sizes = []
  286. features = []
  287. images = []
  288. is_work_scale_set = False
  289. is_seam_scale_set = False
  290. is_compose_scale_set = False
  291. for name in img_names:
  292. full_img = cv.imread(cv.samples.findFile(name))
  293. if full_img is None:
  294. print("Cannot read image ", name)
  295. exit()
  296. full_img_sizes.append((full_img.shape[1], full_img.shape[0]))
  297. if work_megapix < 0:
  298. img = full_img
  299. work_scale = 1
  300. is_work_scale_set = True
  301. else:
  302. if is_work_scale_set is False:
  303. work_scale = min(1.0, np.sqrt(work_megapix * 1e6 / (full_img.shape[0] * full_img.shape[1])))
  304. is_work_scale_set = True
  305. img = cv.resize(src=full_img, dsize=None, fx=work_scale, fy=work_scale, interpolation=cv.INTER_LINEAR_EXACT)
  306. if is_seam_scale_set is False:
  307. seam_scale = min(1.0, np.sqrt(seam_megapix * 1e6 / (full_img.shape[0] * full_img.shape[1])))
  308. seam_work_aspect = seam_scale / work_scale
  309. is_seam_scale_set = True
  310. img_feat = cv.detail.computeImageFeatures2(finder, img)
  311. features.append(img_feat)
  312. img = cv.resize(src=full_img, dsize=None, fx=seam_scale, fy=seam_scale, interpolation=cv.INTER_LINEAR_EXACT)
  313. images.append(img)
  314. matcher = get_matcher(args)
  315. p = matcher.apply2(features)
  316. matcher.collectGarbage()
  317. if save_graph:
  318. with open(args.save_graph, 'w') as fh:
  319. fh.write(cv.detail.matchesGraphAsString(img_names, p, conf_thresh))
  320. indices = cv.detail.leaveBiggestComponent(features, p, conf_thresh)
  321. img_subset = []
  322. img_names_subset = []
  323. full_img_sizes_subset = []
  324. for i in range(len(indices)):
  325. img_names_subset.append(img_names[indices[i, 0]])
  326. img_subset.append(images[indices[i, 0]])
  327. full_img_sizes_subset.append(full_img_sizes[indices[i, 0]])
  328. images = img_subset
  329. img_names = img_names_subset
  330. full_img_sizes = full_img_sizes_subset
  331. num_images = len(img_names)
  332. if num_images < 2:
  333. print("Need more images")
  334. exit()
  335. estimator = ESTIMATOR_CHOICES[args.estimator]()
  336. b, cameras = estimator.apply(features, p, None)
  337. if not b:
  338. print("Homography estimation failed.")
  339. exit()
  340. for cam in cameras:
  341. cam.R = cam.R.astype(np.float32)
  342. adjuster = BA_COST_CHOICES[args.ba]()
  343. adjuster.setConfThresh(1)
  344. refine_mask = np.zeros((3, 3), np.uint8)
  345. if ba_refine_mask[0] == 'x':
  346. refine_mask[0, 0] = 1
  347. if ba_refine_mask[1] == 'x':
  348. refine_mask[0, 1] = 1
  349. if ba_refine_mask[2] == 'x':
  350. refine_mask[0, 2] = 1
  351. if ba_refine_mask[3] == 'x':
  352. refine_mask[1, 1] = 1
  353. if ba_refine_mask[4] == 'x':
  354. refine_mask[1, 2] = 1
  355. adjuster.setRefinementMask(refine_mask)
  356. b, cameras = adjuster.apply(features, p, cameras)
  357. if not b:
  358. print("Camera parameters adjusting failed.")
  359. exit()
  360. focals = []
  361. for cam in cameras:
  362. focals.append(cam.focal)
  363. focals.sort()
  364. if len(focals) % 2 == 1:
  365. warped_image_scale = focals[len(focals) // 2]
  366. else:
  367. warped_image_scale = (focals[len(focals) // 2] + focals[len(focals) // 2 - 1]) / 2
  368. if wave_correct is not None:
  369. rmats = []
  370. for cam in cameras:
  371. rmats.append(np.copy(cam.R))
  372. rmats = cv.detail.waveCorrect(rmats, wave_correct)
  373. for idx, cam in enumerate(cameras):
  374. cam.R = rmats[idx]
  375. corners = []
  376. masks_warped = []
  377. images_warped = []
  378. sizes = []
  379. masks = []
  380. for i in range(0, num_images):
  381. um = cv.UMat(255 * np.ones((images[i].shape[0], images[i].shape[1]), np.uint8))
  382. masks.append(um)
  383. warper = cv.PyRotationWarper(warp_type, warped_image_scale * seam_work_aspect) # warper could be nullptr?
  384. for idx in range(0, num_images):
  385. K = cameras[idx].K().astype(np.float32)
  386. swa = seam_work_aspect
  387. K[0, 0] *= swa
  388. K[0, 2] *= swa
  389. K[1, 1] *= swa
  390. K[1, 2] *= swa
  391. corner, image_wp = warper.warp(images[idx], K, cameras[idx].R, cv.INTER_LINEAR, cv.BORDER_REFLECT)
  392. corners.append(corner)
  393. sizes.append((image_wp.shape[1], image_wp.shape[0]))
  394. images_warped.append(image_wp)
  395. p, mask_wp = warper.warp(masks[idx], K, cameras[idx].R, cv.INTER_NEAREST, cv.BORDER_CONSTANT)
  396. masks_warped.append(mask_wp.get())
  397. images_warped_f = []
  398. for img in images_warped:
  399. imgf = img.astype(np.float32)
  400. images_warped_f.append(imgf)
  401. compensator = get_compensator(args)
  402. compensator.feed(corners=corners, images=images_warped, masks=masks_warped)
  403. seam_finder = SEAM_FIND_CHOICES[args.seam]
  404. masks_warped = seam_finder.find(images_warped_f, corners, masks_warped)
  405. compose_scale = 1
  406. corners = []
  407. sizes = []
  408. blender = None
  409. timelapser = None
  410. # https://github.com/opencv/opencv/blob/4.x/samples/cpp/stitching_detailed.cpp#L725 ?
  411. for idx, name in enumerate(img_names):
  412. full_img = cv.imread(name)
  413. if not is_compose_scale_set:
  414. if compose_megapix > 0:
  415. compose_scale = min(1.0, np.sqrt(compose_megapix * 1e6 / (full_img.shape[0] * full_img.shape[1])))
  416. is_compose_scale_set = True
  417. compose_work_aspect = compose_scale / work_scale
  418. warped_image_scale *= compose_work_aspect
  419. warper = cv.PyRotationWarper(warp_type, warped_image_scale)
  420. for i in range(0, len(img_names)):
  421. cameras[i].focal *= compose_work_aspect
  422. cameras[i].ppx *= compose_work_aspect
  423. cameras[i].ppy *= compose_work_aspect
  424. sz = (int(round(full_img_sizes[i][0] * compose_scale)),
  425. int(round(full_img_sizes[i][1] * compose_scale)))
  426. K = cameras[i].K().astype(np.float32)
  427. roi = warper.warpRoi(sz, K, cameras[i].R)
  428. corners.append(roi[0:2])
  429. sizes.append(roi[2:4])
  430. if abs(compose_scale - 1) > 1e-1:
  431. img = cv.resize(src=full_img, dsize=None, fx=compose_scale, fy=compose_scale,
  432. interpolation=cv.INTER_LINEAR_EXACT)
  433. else:
  434. img = full_img
  435. _img_size = (img.shape[1], img.shape[0])
  436. K = cameras[idx].K().astype(np.float32)
  437. corner, image_warped = warper.warp(img, K, cameras[idx].R, cv.INTER_LINEAR, cv.BORDER_REFLECT)
  438. mask = 255 * np.ones((img.shape[0], img.shape[1]), np.uint8)
  439. p, mask_warped = warper.warp(mask, K, cameras[idx].R, cv.INTER_NEAREST, cv.BORDER_CONSTANT)
  440. compensator.apply(idx, corners[idx], image_warped, mask_warped)
  441. image_warped_s = image_warped.astype(np.int16)
  442. dilated_mask = cv.dilate(masks_warped[idx], None)
  443. seam_mask = cv.resize(dilated_mask, (mask_warped.shape[1], mask_warped.shape[0]), 0, 0, cv.INTER_LINEAR_EXACT)
  444. mask_warped = cv.bitwise_and(seam_mask, mask_warped)
  445. if blender is None and not timelapse:
  446. blender = cv.detail.Blender_createDefault(cv.detail.Blender_NO)
  447. dst_sz = cv.detail.resultRoi(corners=corners, sizes=sizes)
  448. blend_width = np.sqrt(dst_sz[2] * dst_sz[3]) * blend_strength / 100
  449. if blend_width < 1:
  450. blender = cv.detail.Blender_createDefault(cv.detail.Blender_NO)
  451. elif blend_type == "multiband":
  452. blender = cv.detail_MultiBandBlender()
  453. blender.setNumBands((np.log(blend_width) / np.log(2.) - 1.).astype(np.int))
  454. elif blend_type == "feather":
  455. blender = cv.detail_FeatherBlender()
  456. blender.setSharpness(1. / blend_width)
  457. blender.prepare(dst_sz)
  458. elif timelapser is None and timelapse:
  459. timelapser = cv.detail.Timelapser_createDefault(timelapse_type)
  460. timelapser.initialize(corners, sizes)
  461. if timelapse:
  462. ma_tones = np.ones((image_warped_s.shape[0], image_warped_s.shape[1]), np.uint8)
  463. timelapser.process(image_warped_s, ma_tones, corners[idx])
  464. pos_s = img_names[idx].rfind("/")
  465. if pos_s == -1:
  466. fixed_file_name = "fixed_" + img_names[idx]
  467. else:
  468. fixed_file_name = img_names[idx][:pos_s + 1] + "fixed_" + img_names[idx][pos_s + 1:]
  469. cv.imwrite(fixed_file_name, timelapser.getDst())
  470. else:
  471. blender.feed(cv.UMat(image_warped_s), mask_warped, corners[idx])
  472. if not timelapse:
  473. result = None
  474. result_mask = None
  475. result, result_mask = blender.blend(result, result_mask)
  476. cv.imwrite(result_name, result)
  477. zoom_x = 600.0 / result.shape[1]
  478. dst = cv.normalize(src=result, dst=None, alpha=255., norm_type=cv.NORM_MINMAX, dtype=cv.CV_8U)
  479. dst = cv.resize(dst, dsize=None, fx=zoom_x, fy=zoom_x)
  480. cv.imshow(result_name, dst)
  481. cv.waitKey()
  482. print("Done")
  483. if __name__ == '__main__':
  484. print(__doc__)
  485. main()
  486. cv.destroyAllWindows()