mirror of https://github.com/opencv/opencv.git
Open Source Computer Vision Library
https://opencv.org/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
207 lines
8.8 KiB
207 lines
8.8 KiB
from types import SimpleNamespace |
|
|
|
from .image_handler import ImageHandler |
|
from .feature_detector import FeatureDetector |
|
from .feature_matcher import FeatureMatcher |
|
from .subsetter import Subsetter |
|
from .camera_estimator import CameraEstimator |
|
from .camera_adjuster import CameraAdjuster |
|
from .camera_wave_corrector import WaveCorrector |
|
from .warper import Warper |
|
from .panorama_estimation import estimate_final_panorama_dimensions |
|
from .exposure_error_compensator import ExposureErrorCompensator |
|
from .seam_finder import SeamFinder |
|
from .blender import Blender |
|
from .timelapser import Timelapser |
|
from .stitching_error import StitchingError |
|
|
|
|
|
class Stitcher: |
|
DEFAULT_SETTINGS = { |
|
"medium_megapix": ImageHandler.DEFAULT_MEDIUM_MEGAPIX, |
|
"detector": FeatureDetector.DEFAULT_DETECTOR, |
|
"nfeatures": 500, |
|
"matcher_type": FeatureMatcher.DEFAULT_MATCHER, |
|
"range_width": FeatureMatcher.DEFAULT_RANGE_WIDTH, |
|
"try_use_gpu": False, |
|
"match_conf": None, |
|
"confidence_threshold": Subsetter.DEFAULT_CONFIDENCE_THRESHOLD, |
|
"matches_graph_dot_file": Subsetter.DEFAULT_MATCHES_GRAPH_DOT_FILE, |
|
"estimator": CameraEstimator.DEFAULT_CAMERA_ESTIMATOR, |
|
"adjuster": CameraAdjuster.DEFAULT_CAMERA_ADJUSTER, |
|
"refinement_mask": CameraAdjuster.DEFAULT_REFINEMENT_MASK, |
|
"wave_correct_kind": WaveCorrector.DEFAULT_WAVE_CORRECTION, |
|
"warper_type": Warper.DEFAULT_WARP_TYPE, |
|
"low_megapix": ImageHandler.DEFAULT_LOW_MEGAPIX, |
|
"compensator": ExposureErrorCompensator.DEFAULT_COMPENSATOR, |
|
"nr_feeds": ExposureErrorCompensator.DEFAULT_NR_FEEDS, |
|
"block_size": ExposureErrorCompensator.DEFAULT_BLOCK_SIZE, |
|
"finder": SeamFinder.DEFAULT_SEAM_FINDER, |
|
"final_megapix": ImageHandler.DEFAULT_FINAL_MEGAPIX, |
|
"blender_type": Blender.DEFAULT_BLENDER, |
|
"blend_strength": Blender.DEFAULT_BLEND_STRENGTH, |
|
"timelapse": Timelapser.DEFAULT_TIMELAPSE} |
|
|
|
def __init__(self, **kwargs): |
|
self.initialize_stitcher(**kwargs) |
|
|
|
def initialize_stitcher(self, **kwargs): |
|
self.settings = Stitcher.DEFAULT_SETTINGS.copy() |
|
self.validate_kwargs(kwargs) |
|
self.settings.update(kwargs) |
|
|
|
args = SimpleNamespace(**self.settings) |
|
self.img_handler = ImageHandler(args.medium_megapix, |
|
args.low_megapix, |
|
args.final_megapix) |
|
self.detector = \ |
|
FeatureDetector(args.detector, nfeatures=args.nfeatures) |
|
match_conf = \ |
|
FeatureMatcher.get_match_conf(args.match_conf, args.detector) |
|
self.matcher = FeatureMatcher(args.matcher_type, args.range_width, |
|
try_use_gpu=args.try_use_gpu, |
|
match_conf=match_conf) |
|
self.subsetter = \ |
|
Subsetter(args.confidence_threshold, args.matches_graph_dot_file) |
|
self.camera_estimator = CameraEstimator(args.estimator) |
|
self.camera_adjuster = \ |
|
CameraAdjuster(args.adjuster, args.refinement_mask) |
|
self.wave_corrector = WaveCorrector(args.wave_correct_kind) |
|
self.warper = Warper(args.warper_type) |
|
self.compensator = \ |
|
ExposureErrorCompensator(args.compensator, args.nr_feeds, |
|
args.block_size) |
|
self.seam_finder = SeamFinder(args.finder) |
|
self.blender = Blender(args.blender_type, args.blend_strength) |
|
self.timelapser = Timelapser(args.timelapse) |
|
|
|
def stitch(self, img_names): |
|
self.initialize_registration(img_names) |
|
|
|
imgs = self.resize_medium_resolution() |
|
features = self.find_features(imgs) |
|
matches = self.match_features(features) |
|
imgs, features, matches = self.subset(imgs, features, matches) |
|
cameras = self.estimate_camera_parameters(features, matches) |
|
cameras = self.refine_camera_parameters(features, matches, cameras) |
|
cameras = self.perform_wave_correction(cameras) |
|
panorama_scale, panorama_corners, panorama_sizes = \ |
|
self.estimate_final_panorama_dimensions(cameras) |
|
|
|
self.initialize_composition(panorama_corners, panorama_sizes) |
|
|
|
imgs = self.resize_low_resolution(imgs) |
|
imgs = self.warp_low_resolution_images(imgs, cameras, panorama_scale) |
|
self.estimate_exposure_errors(imgs) |
|
seam_masks = self.find_seam_masks(imgs) |
|
|
|
imgs = self.resize_final_resolution() |
|
imgs = self.warp_final_resolution_images(imgs, cameras, panorama_scale) |
|
imgs = self.compensate_exposure_errors(imgs) |
|
seam_masks = self.resize_seam_masks(seam_masks) |
|
self.blend_images(imgs, seam_masks) |
|
|
|
return self.create_final_panorama() |
|
|
|
def initialize_registration(self, img_names): |
|
self.img_handler.set_img_names(img_names) |
|
|
|
def resize_medium_resolution(self): |
|
return list(self.img_handler.resize_to_medium_resolution()) |
|
|
|
def find_features(self, imgs): |
|
return [self.detector.detect_features(img) for img in imgs] |
|
|
|
def match_features(self, features): |
|
return self.matcher.match_features(features) |
|
|
|
def subset(self, imgs, features, matches): |
|
names, sizes, imgs, features, matches = \ |
|
self.subsetter.subset(self.img_handler.img_names, |
|
self.img_handler.img_sizes, |
|
imgs, features, matches) |
|
self.img_handler.img_names, self.img_handler.img_sizes = names, sizes |
|
return imgs, features, matches |
|
|
|
def estimate_camera_parameters(self, features, matches): |
|
return self.camera_estimator.estimate(features, matches) |
|
|
|
def refine_camera_parameters(self, features, matches, cameras): |
|
return self.camera_adjuster.adjust(features, matches, cameras) |
|
|
|
def perform_wave_correction(self, cameras): |
|
return self.wave_corrector.correct(cameras) |
|
|
|
def estimate_final_panorama_dimensions(self, cameras): |
|
return estimate_final_panorama_dimensions(cameras, self.warper, |
|
self.img_handler) |
|
|
|
def initialize_composition(self, corners, sizes): |
|
if self.timelapser.do_timelapse: |
|
self.timelapser.initialize(corners, sizes) |
|
else: |
|
self.blender.prepare(corners, sizes) |
|
|
|
def resize_low_resolution(self, imgs=None): |
|
return list(self.img_handler.resize_to_low_resolution(imgs)) |
|
|
|
def warp_low_resolution_images(self, imgs, cameras, final_scale): |
|
camera_aspect = self.img_handler.get_medium_to_low_ratio() |
|
scale = final_scale * self.img_handler.get_final_to_low_ratio() |
|
return list(self.warp_images(imgs, cameras, scale, camera_aspect)) |
|
|
|
def warp_final_resolution_images(self, imgs, cameras, scale): |
|
camera_aspect = self.img_handler.get_medium_to_final_ratio() |
|
return self.warp_images(imgs, cameras, scale, camera_aspect) |
|
|
|
def warp_images(self, imgs, cameras, scale, aspect=1): |
|
self._masks = [] |
|
self._corners = [] |
|
for img_warped, mask_warped, corner in \ |
|
self.warper.warp_images_and_image_masks( |
|
imgs, cameras, scale, aspect |
|
): |
|
self._masks.append(mask_warped) |
|
self._corners.append(corner) |
|
yield img_warped |
|
|
|
def estimate_exposure_errors(self, imgs): |
|
self.compensator.feed(self._corners, imgs, self._masks) |
|
|
|
def find_seam_masks(self, imgs): |
|
return self.seam_finder.find(imgs, self._corners, self._masks) |
|
|
|
def resize_final_resolution(self): |
|
return self.img_handler.resize_to_final_resolution() |
|
|
|
def compensate_exposure_errors(self, imgs): |
|
for idx, img in enumerate(imgs): |
|
yield self.compensator.apply(idx, self._corners[idx], |
|
img, self._masks[idx]) |
|
|
|
def resize_seam_masks(self, seam_masks): |
|
for idx, seam_mask in enumerate(seam_masks): |
|
yield SeamFinder.resize(seam_mask, self._masks[idx]) |
|
|
|
def blend_images(self, imgs, masks): |
|
for idx, (img, mask) in enumerate(zip(imgs, masks)): |
|
if self.timelapser.do_timelapse: |
|
self.timelapser.process_and_save_frame( |
|
self.img_handler.img_names[idx], img, self._corners[idx] |
|
) |
|
else: |
|
self.blender.feed(img, mask, self._corners[idx]) |
|
|
|
def create_final_panorama(self): |
|
if not self.timelapser.do_timelapse: |
|
return self.blender.blend() |
|
|
|
@staticmethod |
|
def validate_kwargs(kwargs): |
|
for arg in kwargs: |
|
if arg not in Stitcher.DEFAULT_SETTINGS: |
|
raise StitchingError("Invalid Argument: " + arg) |
|
|
|
def collect_garbage(self): |
|
del self.img_handler.img_names, self.img_handler.img_sizes, |
|
del self._corners, self._masks
|
|
|