def match_key_points(self, key_points_a, key_points_b, descriptors_a, descriptors_b, ratio, threshold, method="homography"): """ match key points given by SIFT/SURF :param method: homography/affine :param key_points_a: key points of first image :param key_points_b: key points of second image :param descriptors_a: descriptors of first image :param descriptors_b: descriptors of second image :param ratio: :param threshold: :return: matches and homography/affine matrix """ # Find matches raw_matches = self.desc_matcher.knnMatch(descriptors_a, descriptors_b, 2) matches = [] # Save matches for m in raw_matches: if len(m) == 2 and m[0].distance < m[1].distance * ratio: matches.append((m[0].trainIdx, m[0].queryIdx)) logger_instance.log(LogLevel.DEBUG, "matches: " + str(len(matches))) # Estimate geometrical transformation if len(matches) > self.matches_required: points_a = np.float32([key_points_a[i] for (_, i) in matches]) points_b = np.float32([key_points_b[i] for (i, _) in matches]) if method == "homography": (H, status) = cv2.findHomography(points_a, points_b, cv2.RANSAC, threshold) elif method == "affine": (H, status) = cv2.estimateAffine2D( points_a, points_b, cv2.RANSAC, ransacReprojThreshold=threshold) else: raise Exception( "Invalid call, unsupported transformation type: " + method) return matches, H, status return None
def panorama_loop(args, images, panorama_image): """ Main loop of the panorama stitching. Finds the most viable image and stitches it to the panorama. Ends when conditions for stitching aren't met. :param args: program arguments :param images: list of PanoramaImage :param panorama_image: main panorama image """ added = True cnt = 0 logger_instance.log(LogLevel.STATUS, "Starting main panorama loop") while added: added = False # Calculate descriptors and best matches panorama_image.calculate_descriptors(matcher) panorama_image.calculate_matches(images, matcher, args.pano_type.lower()) match_count, index = panorama_image.find_best_match() logger_instance.log( LogLevel.DEBUG, "Index: " + str(index) + " Cnt: " + str(match_count)) if not index == -1: logger_instance.log( LogLevel.INFO, "Stitching with: " + panorama_image.matches[index][1].name) # Use homography or affine transformation matches, h, status = panorama_image.matches[index][0] panorama_image.image, panorama_image.mask = Stitcher.stitch_images( panorama_image.image, panorama_image.mask, panorama_image.matches[index][1].image, panorama_image.matches[index][1].mask, h, args.pano_type.lower(), args.blender.lower()) # Changed image, continue panorama_image.matches[index][1].checked = True added = True if args.debug: save_location = args.out + str(cnt) + ".png" logger_instance.log( LogLevel.DEBUG, "saving intermediate result to: " + save_location) cv2.imwrite(save_location, panorama_image.image) cnt += 1 logger_instance.log( LogLevel.STATUS, "Matched " + str(cnt + 1) + "/" + str(len(images)) + " images")
def print_image_info(images): """ Print info about images used in a panorama :param images: list of PanoramaImage """ # Print images used in panorama logger_instance.log(LogLevel.INFO, "List of images used in panorama:") for img in images: if img.checked: logger_instance.log(LogLevel.NONE, "\t\t" + img.name) # Print not used images in panorama logger_instance.log(LogLevel.INFO, "List of images NOT used in panorama:") for img in images: if not img.checked: logger_instance.log(LogLevel.NONE, "\t\t" + img.name)
def panorama(args, main_image, images): """ Homography panorama. :param args: program arguments :param main_image: main panorama image :param images: list of PanoramaImage """ # Calculate descriptors logger_instance.log(LogLevel.STATUS, "Calculating image descriptors... ") f = 750 for img in images: if args.cyl_wrap: h, w = img.image.shape[:2] K = np.array([[f, 0, w / 2], [0, f, h / 2], [0, 0, 1]]) # Geometrically transform image and add border mask = np.ones(img.image.shape, dtype=np.uint8) * 255 img.image = Stitcher.wrap_image_on_cylinder(img.image, K)[0] img.mask = Stitcher.wrap_image_on_cylinder(mask, K)[0] if main_image.name == img.name: main_image.mask = img.mask img.calculate_descriptors(matcher) logger_instance.log(LogLevel.STATUS, "Calculating image descriptors... Done") # Get main panorama image panorama_image = MainPanoramaImage(main_image.name, main_image.image, main_image.mask) main_image.checked = True # Create wonderful panorama panorama_loop(args, images, panorama_image) """ # Global color reduction logger_instance.log(LogLevel.STATUS, "Color balancing...") panorama_image.image = PanoUtils.balance_global_image(panorama_image.image) logger_instance.log(LogLevel.STATUS, "Color balancing... Done.") """ logger_instance.log(LogLevel.STATUS, "Saving finished panorama image") logger_instance.log(LogLevel.INFO, "Final image location: " + args.dest) cv2.imwrite(args.dest, panorama_image.image) # Save created panorama print_image_info(images)
def main(args): logger_instance.set_debug(args.debug) # Setup key point detectors global matcher if args.kp_detector == 'SIFT': logger_instance.log(LogLevel.STATUS, "Using SIFT key point detector") matcher = Matcher(KeyPointDetector.SIFT, 4) elif args.kp_detector == 'SURF': logger_instance.log(LogLevel.STATUS, "Using SURF key point detector") matcher = Matcher(KeyPointDetector.SURF, 4) logger_instance.log(LogLevel.STATUS, "Using " + args.pano_type + " transform") if args.cyl_wrap: logger_instance.log(LogLevel.STATUS, "Using cylinder projection on images") # Load images in gained folder images = PanoUtils.load_images(args.folder) logger_instance.log(LogLevel.INFO, "Images in folder:") for img in images: # Log file name logger_instance.log(LogLevel.NONE, "\t\t" + img.name) for x in images: # Check if file exists if x.name == os.path.basename(args.img): break else: # Nope x = None if x is None: logger_instance.log( LogLevel.ERROR, "File \"" + os.path.basename(args.img) + "\" not found") return # Set main image main_image = x logger_instance.log(LogLevel.INFO, "Main image:") logger_instance.log(LogLevel.NONE, "\t\t" + main_image.name) # Color balancing of all images #logger_instance.log(LogLevel.STATUS, "Color balancing...") #images = PanoUtils.balance_color(images, main_image.image) #logger_instance.log(LogLevel.STATUS, "Color balancing... Done.") # Call panorama stitcher panorama(args, main_image, images)
return # Set main image main_image = x logger_instance.log(LogLevel.INFO, "Main image:") logger_instance.log(LogLevel.NONE, "\t\t" + main_image.name) # Color balancing of all images #logger_instance.log(LogLevel.STATUS, "Color balancing...") #images = PanoUtils.balance_color(images, main_image.image) #logger_instance.log(LogLevel.STATUS, "Color balancing... Done.") # Call panorama stitcher panorama(args, main_image, images) if __name__ == "__main__": # Save start time of the process start_time = datetime.datetime.now() # Call main function try: main(parser.parse_args()) except Exception as e: logger_instance.log_exc(e) # Done, write end time and close app end_time = datetime.datetime.now() logger_instance.log( LogLevel.INFO, "Total execution time: " + str((end_time - start_time)))