From 7f856abcb222600c7c3fac9ec11d50c1f4f5683b Mon Sep 17 00:00:00 2001 From: wuaiguaingsue Date: Tue, 25 Mar 2025 10:29:13 +0800 Subject: [PATCH 1/2] Add Chinese comments to all functions and key code --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/OpenStitching/stitching?shareId=XXXX-XXXX-XXXX-XXXX). --- stitching/blender.py | 28 ++++ stitching/camera_adjuster.py | 17 +++ stitching/camera_estimator.py | 11 ++ stitching/camera_wave_corrector.py | 9 ++ stitching/cropper.py | 85 +++++++++++ stitching/exposure_error_compensator.py | 17 ++- stitching/feature_detector.py | 30 ++++ stitching/feature_matcher.py | 64 +++++++++ stitching/images.py | 121 +++++++++++++++- stitching/megapix_scaler.py | 31 ++++ stitching/seam_finder.py | 120 ++++++++++++++-- stitching/stitcher.py | 182 ++++++++++++++++++++++++ stitching/stitching_error.py | 6 + stitching/subsetter.py | 41 ++++++ stitching/timelapser.py | 29 ++++ stitching/verbose.py | 46 ++++-- stitching/warper.py | 60 +++++++- 17 files changed, 868 insertions(+), 29 deletions(-) diff --git a/stitching/blender.py b/stitching/blender.py index 8876f26..01ae70d 100644 --- a/stitching/blender.py +++ b/stitching/blender.py @@ -16,11 +16,21 @@ class Blender: def __init__( self, blender_type=DEFAULT_BLENDER, blend_strength=DEFAULT_BLEND_STRENGTH ): + """ + 初始化Blender类 + :param blender_type: 混合器类型 + :param blend_strength: 混合强度 + """ self.blender_type = blender_type self.blend_strength = blend_strength self.blender = None def prepare(self, corners, sizes): + """ + 准备混合器 + :param corners: 图像的角点 + :param sizes: 图像的尺寸 + """ dst_sz = cv.detail.resultRoi(corners=corners, sizes=sizes) blend_width = np.sqrt(dst_sz[2] * dst_sz[3]) * self.blend_strength / 100 @@ -38,9 +48,19 @@ def prepare(self, corners, sizes): self.blender.prepare(dst_sz) def feed(self, img, mask, corner): + """ + 向混合器中添加图像 + :param img: 图像 + :param mask: 掩码 + :param corner: 角点 + """ self.blender.feed(cv.UMat(img.astype(np.int16)), mask, corner) def blend(self): + """ + 混合图像 + :return: 混合后的图像和掩码 + """ result = None result_mask = None result, result_mask = self.blender.blend(result, result_mask) @@ -49,6 +69,14 @@ def blend(self): @classmethod def create_panorama(cls, imgs, masks, corners, sizes): + """ + 创建全景图 + :param imgs: 图像列表 + :param masks: 掩码列表 + :param corners: 角点列表 + :param sizes: 尺寸列表 + :return: 全景图和掩码 + """ blender = cls("no") blender.prepare(corners, sizes) for img, mask, corner in zip(imgs, masks, corners): diff --git a/stitching/camera_adjuster.py b/stitching/camera_adjuster.py index 067ff86..6e456f3 100644 --- a/stitching/camera_adjuster.py +++ b/stitching/camera_adjuster.py @@ -24,11 +24,21 @@ def __init__( refinement_mask=DEFAULT_REFINEMENT_MASK, confidence_threshold=1.0, ): + """ + 初始化CameraAdjuster类 + :param adjuster: 调整器类型 + :param refinement_mask: 精细化掩码 + :param confidence_threshold: 置信度阈值 + """ self.adjuster = CameraAdjuster.CAMERA_ADJUSTER_CHOICES[adjuster]() self.set_refinement_mask(refinement_mask) self.adjuster.setConfThresh(confidence_threshold) def set_refinement_mask(self, refinement_mask): + """ + 设置精细化掩码 + :param refinement_mask: 精细化掩码 + """ mask_matrix = np.zeros((3, 3), np.uint8) if refinement_mask[0] == "x": mask_matrix[0, 0] = 1 @@ -43,6 +53,13 @@ def set_refinement_mask(self, refinement_mask): self.adjuster.setRefinementMask(mask_matrix) def adjust(self, features, pairwise_matches, estimated_cameras): + """ + 调整相机参数 + :param features: 特征点 + :param pairwise_matches: 成对匹配 + :param estimated_cameras: 估计的相机参数 + :return: 调整后的相机参数 + """ b, cameras = self.adjuster.apply(features, pairwise_matches, estimated_cameras) if not b: raise StitchingError("Camera parameters adjusting failed.") diff --git a/stitching/camera_estimator.py b/stitching/camera_estimator.py index e493b30..ae9997c 100644 --- a/stitching/camera_estimator.py +++ b/stitching/camera_estimator.py @@ -16,9 +16,20 @@ class CameraEstimator: DEFAULT_CAMERA_ESTIMATOR = list(CAMERA_ESTIMATOR_CHOICES.keys())[0] def __init__(self, estimator=DEFAULT_CAMERA_ESTIMATOR, **kwargs): + """ + 初始化CameraEstimator类 + :param estimator: 估计器类型 + :param kwargs: 其他参数 + """ self.estimator = CameraEstimator.CAMERA_ESTIMATOR_CHOICES[estimator](**kwargs) def estimate(self, features, pairwise_matches): + """ + 估计相机参数 + :param features: 特征点 + :param pairwise_matches: 成对匹配 + :return: 估计的相机参数 + """ b, cameras = self.estimator.apply(features, pairwise_matches, None) if not b: raise StitchingError("Homography estimation failed.") diff --git a/stitching/camera_wave_corrector.py b/stitching/camera_wave_corrector.py index d57c2fc..21bcb97 100644 --- a/stitching/camera_wave_corrector.py +++ b/stitching/camera_wave_corrector.py @@ -16,9 +16,18 @@ class WaveCorrector: DEFAULT_WAVE_CORRECTION = list(WAVE_CORRECT_CHOICES.keys())[0] def __init__(self, wave_correct_kind=DEFAULT_WAVE_CORRECTION): + """ + 初始化WaveCorrector类 + :param wave_correct_kind: 波形校正类型 + """ self.wave_correct_kind = WaveCorrector.WAVE_CORRECT_CHOICES[wave_correct_kind] def correct(self, cameras): + """ + 校正相机波形 + :param cameras: 相机参数列表 + :return: 校正后的相机参数列表 + """ if self.wave_correct_kind is not None: rmats = [np.copy(cam.R) for cam in cameras] rmats = cv.detail.waveCorrect(rmats, self.wave_correct_kind) diff --git a/stitching/cropper.py b/stitching/cropper.py index 4668f9a..d75c57e 100644 --- a/stitching/cropper.py +++ b/stitching/cropper.py @@ -46,11 +46,22 @@ class Cropper: DEFAULT_CROP = True def __init__(self, crop=DEFAULT_CROP): + """ + 初始化Cropper类 + :param crop: 是否进行裁剪 + """ self.do_crop = crop self.overlapping_rectangles = [] self.cropping_rectangles = [] def prepare(self, imgs, masks, corners, sizes): + """ + 准备裁剪器 + :param imgs: 图像列表 + :param masks: 掩码列表 + :param corners: 角点列表 + :param sizes: 尺寸列表 + """ if self.do_crop: mask = self.estimate_panorama_mask(imgs, masks, corners, sizes) lir = self.estimate_largest_interior_rectangle(mask) @@ -62,10 +73,23 @@ def prepare(self, imgs, masks, corners, sizes): ) def crop_images(self, imgs, aspect=1): + """ + 裁剪图像 + :param imgs: 图像列表 + :param aspect: 缩放比例 + :return: 裁剪后的图像生成器 + """ for idx, img in enumerate(imgs): yield self.crop_img(img, idx, aspect) def crop_img(self, img, idx, aspect=1): + """ + 裁剪单张图像 + :param img: 图像 + :param idx: 图像索引 + :param aspect: 缩放比例 + :return: 裁剪后的图像 + """ if self.do_crop: intersection_rect = self.intersection_rectangles[idx] scaled_intersection_rect = intersection_rect.times(aspect) @@ -74,6 +98,13 @@ def crop_img(self, img, idx, aspect=1): return img def crop_rois(self, corners, sizes, aspect=1): + """ + 裁剪感兴趣区域(ROI) + :param corners: 角点列表 + :param sizes: 尺寸列表 + :param aspect: 缩放比例 + :return: 裁剪后的角点和尺寸 + """ if self.do_crop: scaled_overlaps = [r.times(aspect) for r in self.overlapping_rectangles] cropped_corners = [r.corner for r in scaled_overlaps] @@ -84,10 +115,23 @@ def crop_rois(self, corners, sizes, aspect=1): @staticmethod def estimate_panorama_mask(imgs, masks, corners, sizes): + """ + 估计全景图掩码 + :param imgs: 图像列表 + :param masks: 掩码列表 + :param corners: 角点列表 + :param sizes: 尺寸列表 + :return: 全景图掩码 + """ _, mask = Blender.create_panorama(imgs, masks, corners, sizes) return mask def estimate_largest_interior_rectangle(self, mask): + """ + 估计最大内部矩形 + :param mask: 掩码 + :return: 最大内部矩形 + """ # largestinteriorrectangle is only imported if cropping # is explicitly desired (needs some time to compile at the first run!) import largestinteriorrectangle @@ -105,12 +149,23 @@ def estimate_largest_interior_rectangle(self, mask): @staticmethod def get_zero_center_corners(corners): + """ + 获取以零为中心的角点 + :param corners: 角点列表 + :return: 以零为中心的角点列表 + """ min_corner_x = min([corner[0] for corner in corners]) min_corner_y = min([corner[1] for corner in corners]) return [(x - min_corner_x, y - min_corner_y) for x, y in corners] @staticmethod def get_rectangles(corners, sizes): + """ + 获取矩形列表 + :param corners: 角点列表 + :param sizes: 尺寸列表 + :return: 矩形列表 + """ rectangles = [] for corner, size in zip(corners, sizes): rectangle = Rectangle(*corner, *size) @@ -119,10 +174,22 @@ def get_rectangles(corners, sizes): @staticmethod def get_overlaps(rectangles, lir): + """ + 获取重叠矩形列表 + :param rectangles: 矩形列表 + :param lir: 最大内部矩形 + :return: 重叠矩形列表 + """ return [Cropper.get_overlap(r, lir) for r in rectangles] @staticmethod def get_overlap(rectangle1, rectangle2): + """ + 获取两个矩形的重叠部分 + :param rectangle1: 矩形1 + :param rectangle2: 矩形2 + :return: 重叠部分矩形 + """ x1 = max(rectangle1.x, rectangle2.x) y1 = max(rectangle1.y, rectangle2.y) x2 = min(rectangle1.x2, rectangle2.x2) @@ -133,6 +200,12 @@ def get_overlap(rectangle1, rectangle2): @staticmethod def get_intersections(rectangles, overlapping_rectangles): + """ + 获取矩形与重叠矩形的交集 + :param rectangles: 矩形列表 + :param overlapping_rectangles: 重叠矩形列表 + :return: 交集矩形列表 + """ return [ Cropper.get_intersection(r, overlap_r) for r, overlap_r in zip(rectangles, overlapping_rectangles) @@ -140,6 +213,12 @@ def get_intersections(rectangles, overlapping_rectangles): @staticmethod def get_intersection(rectangle, overlapping_rectangle): + """ + 获取矩形与重叠矩形的交集 + :param rectangle: 矩形 + :param overlapping_rectangle: 重叠矩形 + :return: 交集矩形 + """ x = abs(overlapping_rectangle.x - rectangle.x) y = abs(overlapping_rectangle.y - rectangle.y) width = overlapping_rectangle.width @@ -148,4 +227,10 @@ def get_intersection(rectangle, overlapping_rectangle): @staticmethod def crop_rectangle(img, rectangle): + """ + 裁剪矩形区域 + :param img: 图像 + :param rectangle: 矩形区域 + :return: 裁剪后的图像 + """ return img[rectangle.y : rectangle.y2, rectangle.x : rectangle.x2] diff --git a/stitching/exposure_error_compensator.py b/stitching/exposure_error_compensator.py index 7698042..4189113 100644 --- a/stitching/exposure_error_compensator.py +++ b/stitching/exposure_error_compensator.py @@ -25,6 +25,12 @@ def __init__( nr_feeds=DEFAULT_NR_FEEDS, block_size=DEFAULT_BLOCK_SIZE, ): + """ + 初始化ExposureErrorCompensator类 + :param compensator: 曝光补偿器类型 + :param nr_feeds: 曝光补偿器的数量 + :param block_size: 块大小 + """ if compensator == "channel": self.compensator = cv.detail_ChannelsCompensator(nr_feeds) elif compensator == "channel_blocks": @@ -37,9 +43,16 @@ def __init__( ) def feed(self, *args): - """https://docs.opencv.org/4.x/d2/d37/classcv_1_1detail_1_1ExposureCompensator.html#ae6b0cc69a7bc53818ddea53eddb6bdba""" # noqa + """ + 向曝光补偿器提供数据 + :param args: 数据参数 + """ self.compensator.feed(*args) def apply(self, *args): - """https://docs.opencv.org/4.x/d2/d37/classcv_1_1detail_1_1ExposureCompensator.html#a473eaf1e585804c08d77c91e004f93aa""" # noqa + """ + 应用曝光补偿 + :param args: 数据参数 + :return: 曝光补偿后的结果 + """ return self.compensator.apply(*args) diff --git a/stitching/feature_detector.py b/stitching/feature_detector.py index 9cf7112..bb60559 100644 --- a/stitching/feature_detector.py +++ b/stitching/feature_detector.py @@ -19,15 +19,38 @@ class FeatureDetector: DEFAULT_DETECTOR = list(DETECTOR_CHOICES.keys())[0] def __init__(self, detector=DEFAULT_DETECTOR, **kwargs): + """ + 初始化FeatureDetector类 + :param detector: 特征检测器类型 + :param kwargs: 其他参数 + """ self.detector = FeatureDetector.DETECTOR_CHOICES[detector](**kwargs) def detect_features(self, img, *args, **kwargs): + """ + 检测图像中的特征 + :param img: 图像 + :param args: 其他参数 + :param kwargs: 其他参数 + :return: 检测到的特征 + """ return cv.detail.computeImageFeatures2(self.detector, img, *args, **kwargs) def detect(self, imgs): + """ + 检测图像列表中的特征 + :param imgs: 图像列表 + :return: 检测到的特征列表 + """ return [self.detect_features(img) for img in imgs] def detect_with_masks(self, imgs, masks): + """ + 使用掩码检测图像列表中的特征 + :param imgs: 图像列表 + :param masks: 掩码列表 + :return: 检测到的特征列表 + """ features = [] for idx, (img, mask) in enumerate(zip(imgs, masks)): assert len(img.shape) == 3 and len(mask.shape) == 2 @@ -43,6 +66,13 @@ def detect_with_masks(self, imgs, masks): @staticmethod def draw_keypoints(img, features, **kwargs): + """ + 在图像上绘制特征点 + :param img: 图像 + :param features: 特征点 + :param kwargs: 其他参数 + :return: 绘制了特征点的图像 + """ kwargs.setdefault("color", (0, 255, 0)) keypoints = features.getKeypoints() return cv.drawKeypoints(img, keypoints, None, **kwargs) diff --git a/stitching/feature_matcher.py b/stitching/feature_matcher.py index b7e39f8..35598a6 100644 --- a/stitching/feature_matcher.py +++ b/stitching/feature_matcher.py @@ -14,6 +14,12 @@ class FeatureMatcher: def __init__( self, matcher_type=DEFAULT_MATCHER, range_width=DEFAULT_RANGE_WIDTH, **kwargs ): + """ + 初始化FeatureMatcher类 + :param matcher_type: 匹配器类型 + :param range_width: 范围宽度 + :param kwargs: 其他参数 + """ if matcher_type == "affine": self.matcher = cv.detail_AffineBestOf2NearestMatcher(**kwargs) elif range_width == -1: @@ -22,6 +28,13 @@ def __init__( self.matcher = cv.detail_BestOf2NearestRangeMatcher(range_width, **kwargs) def match_features(self, features, *args, **kwargs): + """ + 匹配特征点 + :param features: 特征点 + :param args: 其他参数 + :param kwargs: 其他参数 + :return: 成对匹配 + """ pairwise_matches = self.matcher.apply2(features, *args, **kwargs) self.matcher.collectGarbage() return pairwise_matches @@ -30,6 +43,16 @@ def match_features(self, features, *args, **kwargs): def draw_matches_matrix( imgs, features, matches, conf_thresh=1, inliers=False, **kwargs ): + """ + 绘制匹配矩阵 + :param imgs: 图像列表 + :param features: 特征点列表 + :param matches: 成对匹配 + :param conf_thresh: 置信度阈值 + :param inliers: 是否只绘制内点 + :param kwargs: 其他参数 + :return: 匹配矩阵图像生成器 + """ matches_matrix = FeatureMatcher.get_matches_matrix(matches) for idx1, idx2 in FeatureMatcher.get_all_img_combinations(len(imgs)): match = matches_matrix[idx1, idx2] @@ -43,6 +66,16 @@ def draw_matches_matrix( @staticmethod def draw_matches(img1, features1, img2, features2, match1to2, **kwargs): + """ + 绘制匹配结果 + :param img1: 图像1 + :param features1: 图像1的特征点 + :param img2: 图像2 + :param features2: 图像2的特征点 + :param match1to2: 图像1到图像2的匹配结果 + :param kwargs: 其他参数 + :return: 绘制了匹配结果的图像 + """ kwargs.setdefault("flags", cv.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS) keypoints1 = features1.getKeypoints() @@ -55,10 +88,20 @@ def draw_matches(img1, features1, img2, features2, match1to2, **kwargs): @staticmethod def get_matches_matrix(pairwise_matches): + """ + 获取匹配矩阵 + :param pairwise_matches: 成对匹配 + :return: 匹配矩阵 + """ return FeatureMatcher.array_in_square_matrix(pairwise_matches) @staticmethod def get_confidence_matrix(pairwise_matches): + """ + 获取置信度矩阵 + :param pairwise_matches: 成对匹配 + :return: 置信度矩阵 + """ matches_matrix = FeatureMatcher.get_matches_matrix(pairwise_matches) match_confs = [[m.confidence for m in row] for row in matches_matrix] match_conf_matrix = np.array(match_confs) @@ -66,6 +109,11 @@ def get_confidence_matrix(pairwise_matches): @staticmethod def array_in_square_matrix(array): + """ + 将数组转换为方阵 + :param array: 数组 + :return: 方阵 + """ matrix_dimension = int(math.sqrt(len(array))) rows = [] for i in range(0, len(array), matrix_dimension): @@ -73,18 +121,34 @@ def array_in_square_matrix(array): return np.array(rows) def get_all_img_combinations(number_imgs): + """ + 获取所有图像组合 + :param number_imgs: 图像数量 + :return: 图像组合生成器 + """ ii, jj = np.triu_indices(number_imgs, k=1) for i, j in zip(ii, jj): yield i, j @staticmethod def get_match_conf(match_conf, feature_detector_type): + """ + 获取匹配置信度 + :param match_conf: 匹配置信度 + :param feature_detector_type: 特征检测器类型 + :return: 匹配置信度 + """ if match_conf is None: match_conf = FeatureMatcher.get_default_match_conf(feature_detector_type) return match_conf @staticmethod def get_default_match_conf(feature_detector_type): + """ + 获取默认匹配置信度 + :param feature_detector_type: 特征检测器类型 + :return: 默认匹配置信度 + """ if feature_detector_type == "orb": return 0.3 return 0.65 diff --git a/stitching/images.py b/stitching/images.py index 2be6f4e..10113a3 100644 --- a/stitching/images.py +++ b/stitching/images.py @@ -23,6 +23,14 @@ def of( low_megapix=Resolution.LOW.value, final_megapix=Resolution.FINAL.value, ): + """ + 创建Images对象 + :param images: 图像列表 + :param medium_megapix: 中等分辨率 + :param low_megapix: 低分辨率 + :param final_megapix: 最终分辨率 + :return: Images对象 + """ if not isinstance(images, list): raise StitchingError("images must be a list of images or filenames") if len(images) == 0: @@ -40,6 +48,13 @@ def of( @abstractmethod def __init__(self, images, medium_megapix, low_megapix, final_megapix): + """ + 初始化Images类 + :param images: 图像列表 + :param medium_megapix: 中等分辨率 + :param low_megapix: 低分辨率 + :param final_megapix: 最终分辨率 + """ if medium_megapix < low_megapix: raise StitchingError( "Medium resolution megapix need to be " @@ -58,20 +73,38 @@ def __init__(self, images, medium_megapix, low_megapix, final_megapix): @property def sizes(self): + """ + 获取图像尺寸列表 + :return: 图像尺寸列表 + """ assert self._sizes_set return self._sizes @property def names(self): + """ + 获取图像名称列表 + :return: 图像名称列表 + """ assert self._names_set return self._names @abstractmethod def subset(self, indices): + """ + 获取子集 + :param indices: 索引列表 + """ self._sizes = [self._sizes[i] for i in indices] self._names = [self._names[i] for i in indices] def resize(self, resolution, imgs=None): + """ + 调整图像大小 + :param resolution: 分辨率 + :param imgs: 图像列表 + :return: 调整大小后的图像生成器 + """ img_iterable = self.__iter__() if imgs is None else imgs for idx, img in enumerate(img_iterable): yield Images.resize_img_by_scaler( @@ -83,16 +116,31 @@ def __iter__(self): pass def _set_scales(self, size): + """ + 设置缩放比例 + :param size: 图像尺寸 + """ if not self._scales_set: for scaler in self._scalers.values(): scaler.set_scale_by_img_size(size) self._scales_set = True def _get_scaler(self, resolution): + """ + 获取缩放器 + :param resolution: 分辨率 + :return: 缩放器 + """ Images.check_resolution(resolution) return self._scalers[resolution.name] def get_ratio(self, from_resolution, to_resolution): + """ + 获取分辨率比例 + :param from_resolution: 源分辨率 + :param to_resolution: 目标分辨率 + :return: 分辨率比例 + """ assert self._scales_set Images.check_resolution(from_resolution) Images.check_resolution(to_resolution) @@ -102,6 +150,11 @@ def get_ratio(self, from_resolution, to_resolution): ) def get_scaled_img_sizes(self, resolution): + """ + 获取缩放后的图像尺寸列表 + :param resolution: 分辨率 + :return: 缩放后的图像尺寸列表 + """ assert self._scales_set and self._sizes_set Images.check_resolution(resolution) return [ @@ -110,6 +163,11 @@ def get_scaled_img_sizes(self, resolution): @staticmethod def read_image(img_name): + """ + 读取图像 + :param img_name: 图像文件名 + :return: 图像 + """ img = cv.imread(img_name) if img is None: raise StitchingError("Cannot read image " + img_name) @@ -117,30 +175,61 @@ def read_image(img_name): @staticmethod def get_image_size(img): - """(width, height)""" + """ + 获取图像尺寸 + :param img: 图像 + :return: 图像尺寸(宽度,高度) + """ return (img.shape[1], img.shape[0]) @staticmethod def resize_img_by_scaler(scaler, size, img): + """ + 使用缩放器调整图像大小 + :param scaler: 缩放器 + :param size: 图像尺寸 + :param img: 图像 + :return: 调整大小后的图像 + """ desired_size = scaler.get_scaled_img_size(size) return cv.resize(img, desired_size, interpolation=cv.INTER_LINEAR_EXACT) @staticmethod def check_resolution(resolution): + """ + 检查分辨率 + :param resolution: 分辨率 + """ assert isinstance(resolution, Enum) and resolution in Images.Resolution @staticmethod def resolve_wildcards(img_names): + """ + 解析通配符 + :param img_names: 图像文件名列表 + :return: 解析后的图像文件名列表 + """ if len(img_names) == 1: img_names = [i for i in glob(img_names[0]) if not os.path.isdir(i)] return img_names @staticmethod def check_list_element_types(list_, type_): + """ + 检查列表元素类型 + :param list_: 列表 + :param type_: 类型 + :return: 是否全部为指定类型 + """ return all([isinstance(element, type_) for element in list_]) @staticmethod def to_binary(img): + """ + 转换为二值图像 + :param img: 图像 + :return: 二值图像 + """ if len(img.shape) == 3: img = cv.cvtColor(img, cv.COLOR_BGR2GRAY) _, binary = cv.threshold(img, 0.5, 255.0, cv.THRESH_BINARY) @@ -149,6 +238,13 @@ def to_binary(img): class _NumpyImages(Images): def __init__(self, images, medium_megapix, low_megapix, final_megapix): + """ + 初始化_NumpyImages类 + :param images: 图像列表 + :param medium_megapix: 中等分辨率 + :param low_megapix: 低分辨率 + :param final_megapix: 最终分辨率 + """ super().__init__(images, medium_megapix, low_megapix, final_megapix) if len(images) < 2: raise StitchingError("2 or more Images needed") @@ -160,16 +256,31 @@ def __init__(self, images, medium_megapix, low_megapix, final_megapix): self._set_scales(self._sizes[0]) def subset(self, indices): + """ + 获取子集 + :param indices: 索引列表 + """ super().subset(indices) self._images = [self._images[i] for i in indices] def __iter__(self): + """ + 迭代图像 + :return: 图像生成器 + """ for img in self._images: yield img class _FilenameImages(Images): def __init__(self, images, medium_megapix, low_megapix, final_megapix): + """ + 初始化_FilenameImages类 + :param images: 图像文件名列表 + :param medium_megapix: 中等分辨率 + :param low_megapix: 低分辨率 + :param final_megapix: 最终分辨率 + """ super().__init__(images, medium_megapix, low_megapix, final_megapix) self._names = Images.resolve_wildcards(images) self._names_set = True @@ -178,9 +289,17 @@ def __init__(self, images, medium_megapix, low_megapix, final_megapix): self._sizes = [] def subset(self, indices): + """ + 获取子集 + :param indices: 索引列表 + """ super().subset(indices) def __iter__(self): + """ + 迭代图像 + :return: 图像生成器 + """ for idx, name in enumerate(self.names): img = Images.read_image(name) size = Images.get_image_size(img) diff --git a/stitching/megapix_scaler.py b/stitching/megapix_scaler.py index dd8214e..c465e5b 100644 --- a/stitching/megapix_scaler.py +++ b/stitching/megapix_scaler.py @@ -3,23 +3,45 @@ class MegapixScaler: def __init__(self, megapix): + """ + 初始化MegapixScaler类 + :param megapix: 目标百万像素数 + """ self.megapix = megapix self.is_scale_set = False self.scale = None def set_scale_by_img_size(self, img_size): + """ + 根据图像尺寸设置缩放比例 + :param img_size: 图像尺寸 + """ self.set_scale(self.get_scale_by_resolution(img_size[0] * img_size[1])) def set_scale(self, scale): + """ + 设置缩放比例 + :param scale: 缩放比例 + """ self.scale = scale self.is_scale_set = True def get_scale_by_resolution(self, resolution): + """ + 根据分辨率获取缩放比例 + :param resolution: 分辨率 + :return: 缩放比例 + """ if self.megapix > 0: return np.sqrt(self.megapix * 1e6 / resolution) return 1.0 def get_scaled_img_size(self, img_size): + """ + 获取缩放后的图像尺寸 + :param img_size: 原始图像尺寸 + :return: 缩放后的图像尺寸 + """ width = int(round(img_size[0] * self.scale)) height = int(round(img_size[1] * self.scale)) return (width, height) @@ -28,8 +50,17 @@ def get_scaled_img_size(self, img_size): class MegapixDownscaler(MegapixScaler): @staticmethod def force_downscale(scale): + """ + 强制缩小比例 + :param scale: 缩放比例 + :return: 缩小后的比例 + """ return min(1.0, scale) def set_scale(self, scale): + """ + 设置缩小后的比例 + :param scale: 缩放比例 + """ scale = self.force_downscale(scale) super().set_scale(scale) diff --git a/stitching/seam_finder.py b/stitching/seam_finder.py index 637e472..2609b7d 100644 --- a/stitching/seam_finder.py +++ b/stitching/seam_finder.py @@ -28,14 +28,31 @@ class SeamFinder: DEFAULT_SEAM_FINDER = list(SEAM_FINDER_CHOICES.keys())[0] def __init__(self, finder=DEFAULT_SEAM_FINDER): + """ + 初始化SeamFinder类 + :param finder: 接缝查找器类型 + """ self.finder = SeamFinder.SEAM_FINDER_CHOICES[finder] def find(self, imgs, corners, masks): + """ + 查找接缝 + :param imgs: 图像列表 + :param corners: 角点列表 + :param masks: 掩码列表 + :return: 接缝掩码列表 + """ imgs_float = [img.astype(np.float32) for img in imgs] return self.finder.find(imgs_float, corners, masks) @staticmethod def resize(seam_mask, mask): + """ + 调整接缝掩码大小 + :param seam_mask: 接缝掩码 + :param mask: 掩码 + :return: 调整大小后的接缝掩码 + """ dilated_mask = cv.dilate(seam_mask, None) resized_seam_mask = cv.resize( dilated_mask, (mask.shape[1], mask.shape[0]), 0, 0, cv.INTER_LINEAR_EXACT @@ -44,6 +61,13 @@ def resize(seam_mask, mask): @staticmethod def draw_seam_mask(img, seam_mask, color=(0, 0, 0)): + """ + 在图像上绘制接缝掩码 + :param img: 图像 + :param seam_mask: 接缝掩码 + :param color: 颜色 + :return: 绘制了接缝掩码的图像 + """ seam_mask = cv.UMat.get(seam_mask) overlaid_img = np.copy(img) overlaid_img[seam_mask == 0] = color @@ -51,10 +75,25 @@ def draw_seam_mask(img, seam_mask, color=(0, 0, 0)): @staticmethod def draw_seam_polygons(panorama, blended_seam_masks, alpha=0.5): + """ + 在全景图上绘制接缝多边形 + :param panorama: 全景图 + :param blended_seam_masks: 混合接缝掩码 + :param alpha: 透明度 + :return: 绘制了接缝多边形的全景图 + """ return add_weighted_image(panorama, blended_seam_masks, alpha) @staticmethod def draw_seam_lines(panorama, blended_seam_masks, linesize=1, color=(0, 0, 255)): + """ + 在全景图上绘制接缝线 + :param panorama: 全景图 + :param blended_seam_masks: 混合接缝掩码 + :param linesize: 线条宽度 + :param color: 颜色 + :return: 绘制了接缝线的全景图 + """ seam_lines = SeamFinder.extract_seam_lines(blended_seam_masks, linesize) panorama_with_seam_lines = panorama.copy() panorama_with_seam_lines[seam_lines == 255] = color @@ -62,6 +101,12 @@ def draw_seam_lines(panorama, blended_seam_masks, linesize=1, color=(0, 0, 255)) @staticmethod def extract_seam_lines(blended_seam_masks, linesize=1): + """ + 提取接缝线 + :param blended_seam_masks: 混合接缝掩码 + :param linesize: 线条宽度 + :return: 接缝线 + """ seam_lines = cv.Canny(np.uint8(blended_seam_masks), 100, 200) seam_indices = (seam_lines == 255).nonzero() seam_lines = remove_invalid_line_pixels( @@ -77,17 +122,25 @@ def blend_seam_masks( corners, sizes, colors=( - (255, 000, 000), # Red - (000, 000, 255), # Blue - (000, 255, 000), # Green - (000, 255, 255), # Yellow - (255, 000, 255), # Purple - (128, 128, 255), # Pink - (128, 128, 128), # Gray - (000, 000, 128), # Dark Blue - (000, 128, 255), # Light Blue + (255, 000, 000), # 红色 + (000, 000, 255), # 蓝色 + (000, 255, 000), # 绿色 + (000, 255, 255), # 黄色 + (255, 000, 255), # 紫色 + (128, 128, 255), # 粉色 + (128, 128, 128), # 灰色 + (000, 000, 128), # 深蓝色 + (000, 128, 255), # 浅蓝色 ), ): + """ + 混合接缝掩码 + :param seam_masks: 接缝掩码列表 + :param corners: 角点列表 + :param sizes: 尺寸列表 + :param colors: 颜色列表 + :return: 混合后的接缝掩码 + """ imgs = colored_img_generator(sizes, colors) blended_seam_masks, _ = Blender.create_panorama( imgs, seam_masks, corners, sizes @@ -96,9 +149,15 @@ def blend_seam_masks( def colored_img_generator(sizes, colors): + """ + 生成彩色图像 + :param sizes: 尺寸列表 + :param colors: 颜色列表 + :return: 彩色图像生成器 + """ if len(sizes) + 1 > len(colors): warnings.warn( - "Without additional colors, there will be seam masks with identical colors", # noqa: E501 + "没有额外的颜色,将有接缝掩码具有相同的颜色", # noqa: E501 StitchingWarning, ) @@ -107,6 +166,12 @@ def colored_img_generator(sizes, colors): def create_img_by_size(size, color=(0, 0, 0)): + """ + 根据尺寸创建图像 + :param size: 尺寸 + :param color: 颜色 + :return: 创建的图像 + """ width, height = size img = np.zeros((height, width, 3), np.uint8) img[:] = color @@ -114,10 +179,24 @@ def create_img_by_size(size, color=(0, 0, 0)): def add_weighted_image(img1, img2, alpha): + """ + 添加加权图像 + :param img1: 图像1 + :param img2: 图像2 + :param alpha: 透明度 + :return: 加权后的图像 + """ return cv.addWeighted(img1, alpha, img2, (1.0 - alpha), 0.0) def remove_invalid_line_pixels(indices, lines, mask): + """ + 移除无效的线条像素 + :param indices: 索引 + :param lines: 线条 + :param mask: 掩码 + :return: 移除无效线条像素后的线条 + """ for x, y in zip(*indices): if check_if_pixel_or_neighbor_is_black(mask, x, y): lines[x, y] = 0 @@ -125,6 +204,13 @@ def remove_invalid_line_pixels(indices, lines, mask): def check_if_pixel_or_neighbor_is_black(img, x, y): + """ + 检查像素或邻居是否为黑色 + :param img: 图像 + :param x: x坐标 + :param y: y坐标 + :return: 是否为黑色 + """ check = [ is_pixel_black(img, x, y), is_pixel_black(img, x + 1, y), @@ -136,10 +222,24 @@ def check_if_pixel_or_neighbor_is_black(img, x, y): def is_pixel_black(img, x, y): + """ + 检查像素是否为黑色 + :param img: 图像 + :param x: x坐标 + :param y: y坐标 + :return: 是否为黑色 + """ return np.all(get_pixel_value(img, x, y) == 0) def get_pixel_value(img, x, y): + """ + 获取像素值 + :param img: 图像 + :param x: x坐标 + :param y: y坐标 + :return: 像素值 + """ try: return img[x, y] except IndexError: diff --git a/stitching/stitcher.py b/stitching/stitcher.py index fd45513..94a5201 100644 --- a/stitching/stitcher.py +++ b/stitching/stitcher.py @@ -48,9 +48,17 @@ class Stitcher: } def __init__(self, **kwargs): + """ + 初始化Stitcher类 + :param kwargs: 其他参数 + """ self.initialize_stitcher(**kwargs) def initialize_stitcher(self, **kwargs): + """ + 初始化拼接器 + :param kwargs: 其他参数 + """ self.settings = self.DEFAULT_SETTINGS.copy() self.validate_kwargs(kwargs) self.kwargs = kwargs @@ -89,9 +97,22 @@ def initialize_stitcher(self, **kwargs): self.timelapser = Timelapser(args.timelapse, args.timelapse_prefix) def stitch_verbose(self, images, feature_masks=[], verbose_dir=None): + """ + 详细拼接图像 + :param images: 图像列表 + :param feature_masks: 特征掩码列表 + :param verbose_dir: 详细结果���存目录 + :return: 拼接结果 + """ return verbose_stitching(self, images, feature_masks, verbose_dir) def stitch(self, images, feature_masks=[]): + """ + 拼接图像 + :param images: 图像列表 + :param feature_masks: 特征掩码列表 + :return: 拼接结果 + """ self.images = Images.of( images, self.medium_megapix, self.low_megapix, self.final_megapix ) @@ -128,9 +149,19 @@ def stitch(self, images, feature_masks=[]): return self.create_final_panorama() def resize_medium_resolution(self): + """ + 调整图像到中等分辨率 + :return: 调整后的图像列表 + """ return list(self.images.resize(Images.Resolution.MEDIUM)) def find_features(self, imgs, feature_masks=[]): + """ + 查找图像特征 + :param imgs: 图像列表 + :param feature_masks: 特征掩码列表 + :return: 特征列表 + """ if len(feature_masks) == 0: return self.detector.detect(imgs) else: @@ -142,9 +173,21 @@ def find_features(self, imgs, feature_masks=[]): return self.detector.detect_with_masks(imgs, feature_masks) def match_features(self, features): + """ + 匹配图像特征 + :param features: 特征列表 + :return: 匹配结果 + """ return self.matcher.match_features(features) def subset(self, imgs, features, matches): + """ + 获取图像子集 + :param imgs: 图像列表 + :param features: 特征列表 + :param matches: 匹配结果 + :return: 子集图像、特征和匹配结果 + """ indices = self.subsetter.subset(self.images.names, features, matches) imgs = Subsetter.subset_list(imgs, indices) features = Subsetter.subset_list(features, indices) @@ -153,21 +196,54 @@ def subset(self, imgs, features, matches): return imgs, features, matches def estimate_camera_parameters(self, features, matches): + """ + 估计相机参数 + :param features: 特征列表 + :param matches: 匹配结果 + :return: 相机参数 + """ return self.camera_estimator.estimate(features, matches) def refine_camera_parameters(self, features, matches, cameras): + """ + 调整相机参数 + :param features: 特征列表 + :param matches: 匹配结果 + :param cameras: 相机参数 + :return: 调整后的相机参数 + """ return self.camera_adjuster.adjust(features, matches, cameras) def perform_wave_correction(self, cameras): + """ + 执行波形校正 + :param cameras: 相机参数 + :return: 校正后的相机参数 + """ return self.wave_corrector.correct(cameras) def estimate_scale(self, cameras): + """ + 估计图像缩放比例 + :param cameras: 相机参数 + """ self.warper.set_scale(cameras) def resize_low_resolution(self, imgs=None): + """ + 调整图像到低分辨率 + :param imgs: 图像列表 + :return: 调整后的图像列表 + """ return list(self.images.resize(Images.Resolution.LOW, imgs)) def warp_low_resolution(self, imgs, cameras): + """ + 扭曲低分辨率图像 + :param imgs: 图像列表 + :param cameras: 相机参数 + :return: 扭曲后的图像、掩码、角点和尺寸 + """ sizes = self.images.get_scaled_img_sizes(Images.Resolution.LOW) camera_aspect = self.images.get_ratio( Images.Resolution.MEDIUM, Images.Resolution.LOW @@ -176,6 +252,12 @@ def warp_low_resolution(self, imgs, cameras): return list(imgs), list(masks), corners, sizes def warp_final_resolution(self, imgs, cameras): + """ + 扭曲最终分辨率图像 + :param imgs: 图像列表 + :param cameras: 相机参数 + :return: 扭曲后的图像、掩码、角点和尺寸 + """ sizes = self.images.get_scaled_img_sizes(Images.Resolution.FINAL) camera_aspect = self.images.get_ratio( Images.Resolution.MEDIUM, Images.Resolution.FINAL @@ -183,52 +265,129 @@ def warp_final_resolution(self, imgs, cameras): return self.warp(imgs, cameras, sizes, camera_aspect) def warp(self, imgs, cameras, sizes, aspect=1): + """ + 扭曲图像 + :param imgs: 图像列表 + :param cameras: 相机参数 + :param sizes: 图像尺寸 + :param aspect: 缩放比例 + :return: 扭曲后的图像、掩码、角点和尺寸 + """ imgs = self.warper.warp_images(imgs, cameras, aspect) masks = self.warper.create_and_warp_masks(sizes, cameras, aspect) corners, sizes = self.warper.warp_rois(sizes, cameras, aspect) return imgs, masks, corners, sizes def prepare_cropper(self, imgs, masks, corners, sizes): + """ + 准备裁剪器 + :param imgs: 图像列表 + :param masks: 掩码列表 + :param corners: 角点列表 + :param sizes: 尺寸列表 + """ self.cropper.prepare(imgs, masks, corners, sizes) def crop_low_resolution(self, imgs, masks, corners, sizes): + """ + 裁剪低分辨率图像 + :param imgs: 图像列表 + :param masks: 掩码列表 + :param corners: 角点列表 + :param sizes: 尺寸列表 + :return: 裁剪后的图像、掩码、角点和尺寸 + """ imgs, masks, corners, sizes = self.crop(imgs, masks, corners, sizes) return list(imgs), list(masks), corners, sizes def crop_final_resolution(self, imgs, masks, corners, sizes): + """ + 裁剪最终分辨率图像 + :param imgs: 图像列表 + :param masks: 掩码列表 + :param corners: 角点列表 + :param sizes: 尺寸列表 + :return: 裁剪后的图像、掩码、角点和尺寸 + """ lir_aspect = self.images.get_ratio( Images.Resolution.LOW, Images.Resolution.FINAL ) return self.crop(imgs, masks, corners, sizes, lir_aspect) def crop(self, imgs, masks, corners, sizes, aspect=1): + """ + 裁剪图像 + :param imgs: 图像列表 + :param masks: 掩码列表 + :param corners: 角点列表 + :param sizes: 尺寸列表 + :param aspect: 缩放比例 + :return: 裁剪后的图像、掩码、角点和尺寸 + """ masks = self.cropper.crop_images(masks, aspect) imgs = self.cropper.crop_images(imgs, aspect) corners, sizes = self.cropper.crop_rois(corners, sizes, aspect) return imgs, masks, corners, sizes def estimate_exposure_errors(self, corners, imgs, masks): + """ + 估计曝光误差 + :param corners: 角点列表 + :param imgs: 图像列表 + :param masks: 掩码列表 + """ self.compensator.feed(corners, imgs, masks) def find_seam_masks(self, imgs, corners, masks): + """ + 查找接缝掩码 + :param imgs: 图像列表 + :param corners: 角点列表 + :param masks: 掩码列表 + :return: 接缝掩码列表 + """ return self.seam_finder.find(imgs, corners, masks) def resize_final_resolution(self): + """ + 调整图像到最终分辨率 + :return: 调整后的图像列表 + """ return self.images.resize(Images.Resolution.FINAL) def compensate_exposure_errors(self, corners, imgs): + """ + 补偿曝光误差 + :param corners: 角点列表 + :param imgs: 图像列表 + :return: 补偿后的图像生成器 + """ for idx, (corner, img) in enumerate(zip(corners, imgs)): yield self.compensator.apply(idx, corner, img, self.get_mask(idx)) def resize_seam_masks(self, seam_masks): + """ + 调整接缝掩码大小 + :param seam_masks: 接缝掩码列表 + :return: 调整大小后的接缝掩码生成器 + """ for idx, seam_mask in enumerate(seam_masks): yield SeamFinder.resize(seam_mask, self.get_mask(idx)) def set_masks(self, mask_generator): + """ + 设置掩码生成器 + :param mask_generator: 掩码生成器 + """ self.masks = mask_generator self.mask_index = -1 def get_mask(self, idx): + """ + 获取掩码 + :param idx: 掩码索引 + :return: 掩码 + """ if idx == self.mask_index + 1: self.mask_index += 1 self.mask = next(self.masks) @@ -239,12 +398,23 @@ def get_mask(self, idx): raise StitchingError("Invalid Mask Index!") def initialize_composition(self, corners, sizes): + """ + 初始化图像合成 + :param corners: 角点列表 + :param sizes: 尺寸列表 + """ if self.timelapser.do_timelapse: self.timelapser.initialize(corners, sizes) else: self.blender.prepare(corners, sizes) def blend_images(self, imgs, masks, corners): + """ + 混合图像 + :param imgs: 图像列表 + :param masks: 掩码列表 + :param corners: 角点列表 + """ for idx, (img, mask, corner) in enumerate(zip(imgs, masks, corners)): if self.timelapser.do_timelapse: self.timelapser.process_and_save_frame( @@ -254,11 +424,19 @@ def blend_images(self, imgs, masks, corners): self.blender.feed(img, mask, corner) def create_final_panorama(self): + """ + 创建最终全景图 + :return: 全景图 + """ if not self.timelapser.do_timelapse: panorama, _ = self.blender.blend() return panorama def validate_kwargs(self, kwargs): + """ + 验证参数 + :param kwargs: 参数字典 + """ for arg in kwargs: if arg not in self.DEFAULT_SETTINGS: raise StitchingError("Invalid Argument: " + arg) @@ -278,6 +456,10 @@ class AffineStitcher(Stitcher): DEFAULT_SETTINGS.update(AFFINE_DEFAULTS) def initialize_stitcher(self, **kwargs): + """ + 初始化仿射拼接器 + :param kwargs: 其他参数 + """ for key, value in kwargs.items(): if key in self.AFFINE_DEFAULTS and value != self.AFFINE_DEFAULTS[key]: warnings.warn( diff --git a/stitching/stitching_error.py b/stitching/stitching_error.py index 9fbc32b..ae1a13e 100644 --- a/stitching/stitching_error.py +++ b/stitching/stitching_error.py @@ -1,6 +1,12 @@ class StitchingError(Exception): + """ + 拼接错误异常类 + """ pass class StitchingWarning(UserWarning): + """ + 拼接警告类 + """ pass diff --git a/stitching/subsetter.py b/stitching/subsetter.py index d56ae31..ef70e00 100644 --- a/stitching/subsetter.py +++ b/stitching/subsetter.py @@ -21,10 +21,22 @@ def __init__( confidence_threshold=DEFAULT_CONFIDENCE_THRESHOLD, matches_graph_dot_file=DEFAULT_MATCHES_GRAPH_DOT_FILE, ): + """ + 初始化Subsetter类 + :param confidence_threshold: 置信度阈值 + :param matches_graph_dot_file: 匹配图DOT文件 + """ self.confidence_threshold = confidence_threshold self.save_file = matches_graph_dot_file def subset(self, img_names, features, matches): + """ + 获取图像子集 + :param img_names: 图像名称列表 + :param features: 特征列表 + :param matches: 匹配结果 + :return: 子集索引列表 + """ self.save_matches_graph_dot_file(img_names, matches) indices = self.get_indices_to_keep(features, matches) @@ -37,11 +49,22 @@ def subset(self, img_names, features, matches): return indices def save_matches_graph_dot_file(self, img_names, pairwise_matches): + """ + 保存匹配图DOT文件 + :param img_names: 图像名称列表 + :param pairwise_matches: 成对匹配 + """ if self.save_file: with open(self.save_file, "w") as filehandler: filehandler.write(self.get_matches_graph(img_names, pairwise_matches)) def get_matches_graph(self, img_names, pairwise_matches): + """ + 获取匹配图 + :param img_names: 图像名称列表 + :param pairwise_matches: 成对匹配 + :return: 匹配图字符串 + """ return cv.detail.matchesGraphAsString( img_names, pairwise_matches, @@ -53,6 +76,12 @@ def get_matches_graph(self, img_names, pairwise_matches): ) def get_indices_to_keep(self, features, pairwise_matches): + """ + 获取要保留的索引 + :param features: 特征列表 + :param pairwise_matches: 成对匹配 + :return: 要保留的索引列表 + """ indices = cv.detail.leaveBiggestComponent( features, pairwise_matches, self.confidence_threshold ) @@ -69,10 +98,22 @@ def get_indices_to_keep(self, features, pairwise_matches): @staticmethod def subset_list(list_to_subset, indices): + """ + 获取子集列表 + :param list_to_subset: 要获取子集的列表 + :param indices: 索引列表 + :return: 子集列表 + """ return [list_to_subset[i] for i in indices] @staticmethod def subset_matches(pairwise_matches, indices): + """ + 获取子集匹配结果 + :param pairwise_matches: 成对匹配 + :param indices: 索引列表 + :return: 子集匹配结果 + """ matches_matrix = FeatureMatcher.get_matches_matrix(pairwise_matches) matches_matrix_subset = matches_matrix[np.ix_(indices, indices)] matches_subset_list = list(chain.from_iterable(matches_matrix_subset.tolist())) diff --git a/stitching/timelapser.py b/stitching/timelapser.py index 7e0d134..27322ea 100644 --- a/stitching/timelapser.py +++ b/stitching/timelapser.py @@ -18,6 +18,11 @@ class Timelapser: def __init__( self, timelapse=DEFAULT_TIMELAPSE, timelapse_prefix=DEFAULT_TIMELAPSE_PREFIX ): + """ + 初始化Timelapser类 + :param timelapse: 延时类型 + :param timelapse_prefix: 延时前缀 + """ self.do_timelapse = True self.timelapse_type = None self.timelapser = None @@ -34,23 +39,47 @@ def __init__( self.timelapser = cv.detail.Timelapser_createDefault(self.timelapse_type) def initialize(self, *args): + """ + 初始化延时器 + :param args: 参数 + """ self.timelapser.initialize(*args) def process_and_save_frame(self, img_name, img, corner): + """ + 处理并保存帧 + :param img_name: 图像名称 + :param img: 图像 + :param corner: 角点 + """ self.process_frame(img, corner) cv.imwrite(self.get_fixed_filename(img_name), self.get_frame()) def process_frame(self, img, corner): + """ + 处理帧 + :param img: 图像 + :param corner: 角点 + """ mask = np.ones((img.shape[0], img.shape[1]), np.uint8) img = img.astype(np.int16) self.timelapser.process(img, mask, corner) def get_frame(self): + """ + 获取帧 + :return: 帧 + """ frame = self.timelapser.getDst() frame = np.float32(cv.UMat.get(frame)) frame = cv.convertScaleAbs(frame) return frame def get_fixed_filename(self, img_name): + """ + 获取固定文件名 + :param img_name: 图像名称 + :return: 固定文件名 + """ dirname, filename = os.path.split(img_name) return os.path.join(dirname, self.timelapse_prefix + filename) diff --git a/stitching/verbose.py b/stitching/verbose.py index d5de93e..7fb4a80 100644 --- a/stitching/verbose.py +++ b/stitching/verbose.py @@ -8,6 +8,14 @@ def verbose_stitching(stitcher, images, feature_masks=[], verbose_dir=None): + """ + 详细拼接图像 + :param stitcher: 拼接器对象 + :param images: 图像列表 + :param feature_masks: 特征掩码列表 + :param verbose_dir: 详细结果保存目录 + :return: 拼接结果 + """ _dir = "." if verbose_dir is None else verbose_dir with open(verbose_output(_dir, "00_stitcher.txt"), "w") as file: @@ -17,21 +25,21 @@ def verbose_stitching(stitcher, images, feature_masks=[], verbose_dir=None): images, stitcher.medium_megapix, stitcher.low_megapix, stitcher.final_megapix ) - # Resize Images + # 调整图像到中等分辨率 imgs = list(images.resize(Images.Resolution.MEDIUM)) - # Find Features + # 查找图像特征 finder = stitcher.detector features = stitcher.find_features(imgs, feature_masks) for idx, img_features in enumerate(features): img_with_features = finder.draw_keypoints(imgs[idx], img_features) write_verbose_result(_dir, f"01_features_img{idx + 1}.jpg", img_with_features) - # Match Features + # 匹配图像特征 matcher = stitcher.matcher matches = matcher.match_features(features) - # Subset + # 获取图像子集 subsetter = stitcher.subsetter all_relevant_matches = list( @@ -49,7 +57,7 @@ def verbose_stitching(stitcher, images, feature_masks=[], verbose_dir=None): _dir, f"02_matches_img{idx1 + 1}_to_img{idx2 + 1}.jpg", img ) - # Subset + # 获取图像子集 subsetter = stitcher.subsetter subsetter.save_file = verbose_output(_dir, "03_matches_graph.txt") subsetter.save_matches_graph_dot_file(images.names, matches) @@ -61,7 +69,7 @@ def verbose_stitching(stitcher, images, feature_masks=[], verbose_dir=None): matches = subsetter.subset_matches(matches, indices) images.subset(indices) - # Camera Estimation, Adjustion and Correction + # 估计、调整和校正相机参数 camera_estimator = stitcher.camera_estimator camera_adjuster = stitcher.camera_adjuster wave_corrector = stitcher.wave_corrector @@ -70,9 +78,9 @@ def verbose_stitching(stitcher, images, feature_masks=[], verbose_dir=None): cameras = camera_adjuster.adjust(features, matches, cameras) cameras = wave_corrector.correct(cameras) - # Warp Images + # 扭曲图像 low_imgs = list(images.resize(Images.Resolution.LOW, imgs)) - imgs = None # free memory + imgs = None # 释放内存 warper = stitcher.warper warper.set_scale(cameras) @@ -97,7 +105,7 @@ def verbose_stitching(stitcher, images, feature_masks=[], verbose_dir=None): for idx, warped_img in enumerate(final_imgs): write_verbose_result(_dir, f"04_warped_img{idx + 1}.jpg", warped_img) - # Excursion: Timelapser + # 延时处理 timelapser = Timelapser("as_is") timelapser.initialize(final_corners, final_sizes) @@ -106,7 +114,7 @@ def verbose_stitching(stitcher, images, feature_masks=[], verbose_dir=None): frame = timelapser.get_frame() write_verbose_result(_dir, f"05_timelapse_img{idx + 1}.jpg", frame) - # Crop + # 裁剪图像 cropper = stitcher.cropper if cropper.do_crop: @@ -142,7 +150,7 @@ def verbose_stitching(stitcher, images, feature_masks=[], verbose_dir=None): frame = timelapser.get_frame() write_verbose_result(_dir, f"07_timelapse_cropped_img{idx + 1}.jpg", frame) - # Seam Masks + # 查找接缝掩码 seam_finder = stitcher.seam_finder seam_masks = seam_finder.find(low_imgs, low_corners, low_masks) @@ -158,7 +166,7 @@ def verbose_stitching(stitcher, images, feature_masks=[], verbose_dir=None): for idx, seam_mask in enumerate(seam_masks_plots): write_verbose_result(_dir, f"08_seam_mask{idx + 1}.jpg", seam_mask) - # Exposure Error Compensation + # 曝光误差补偿 compensator = stitcher.compensator compensator.feed(low_corners, low_imgs, low_masks) @@ -173,7 +181,7 @@ def verbose_stitching(stitcher, images, feature_masks=[], verbose_dir=None): for idx, compensated_img in enumerate(compensated_imgs): write_verbose_result(_dir, f"08_compensated{idx + 1}.jpg", compensated_img) - # Blending + # 混合图像 blender = stitcher.blender blender.prepare(final_corners, final_sizes) for img, mask, corner in zip(compensated_imgs, seam_masks, final_corners): @@ -197,8 +205,20 @@ def verbose_stitching(stitcher, images, feature_masks=[], verbose_dir=None): def write_verbose_result(dir_name, img_name, img): + """ + 保存详细结果 + :param dir_name: 目录名称 + :param img_name: 图像名称 + :param img: 图像 + """ cv.imwrite(verbose_output(dir_name, img_name), img) def verbose_output(dir_name, file): + """ + 获取详细结果输出路径 + :param dir_name: 目录名称 + :param file: 文件名称 + :return: 详细结果输出路径 + """ return os.path.join(dir_name, file) diff --git a/stitching/warper.py b/stitching/warper.py index 58a19dc..4694e0c 100644 --- a/stitching/warper.py +++ b/stitching/warper.py @@ -29,18 +29,40 @@ class Warper: DEFAULT_WARP_TYPE = "spherical" def __init__(self, warper_type=DEFAULT_WARP_TYPE): + """ + 初始化Warper类 + :param warper_type: 扭曲器类型 + """ self.warper_type = warper_type self.scale = None def set_scale(self, cameras): + """ + 设置缩放比例 + :param cameras: 相机参数列表 + """ focals = [cam.focal for cam in cameras] self.scale = median(focals) def warp_images(self, imgs, cameras, aspect=1): + """ + 扭曲图像列表 + :param imgs: 图像列表 + :param cameras: 相机参数列表 + :param aspect: 缩放比例 + :return: 扭曲后的图像生成器 + """ for img, camera in zip(imgs, cameras): yield self.warp_image(img, camera, aspect) def warp_image(self, img, camera, aspect=1): + """ + 扭曲单张图像 + :param img: 图像 + :param camera: 相机参数 + :param aspect: 缩放比例 + :return: 扭曲后的图像 + """ warper = cv.PyRotationWarper(self.warper_type, self.scale * aspect) _, warped_image = warper.warp( img, @@ -52,10 +74,24 @@ def warp_image(self, img, camera, aspect=1): return warped_image def create_and_warp_masks(self, sizes, cameras, aspect=1): + """ + 创建并扭曲掩码列表 + :param sizes: 图像尺寸列表 + :param cameras: 相机参数列表 + :param aspect: 缩放比例 + :return: 扭曲后的掩码生成器 + """ for size, camera in zip(sizes, cameras): yield self.create_and_warp_mask(size, camera, aspect) def create_and_warp_mask(self, size, camera, aspect=1): + """ + 创建并扭曲单个掩码 + :param size: 图像尺寸 + :param camera: 相机参数 + :param aspect: 缩放比例 + :return: 扭曲后的掩码 + """ warper = cv.PyRotationWarper(self.warper_type, self.scale * aspect) mask = 255 * np.ones((size[1], size[0]), np.uint8) _, warped_mask = warper.warp( @@ -68,6 +104,13 @@ def create_and_warp_mask(self, size, camera, aspect=1): return warped_mask def warp_rois(self, sizes, cameras, aspect=1): + """ + 扭曲感兴趣区域(ROI) + :param sizes: 图像尺寸列表 + :param cameras: 相机参数列表 + :param aspect: 缩放比例 + :return: 扭曲后的角点和尺寸 + """ roi_corners = [] roi_sizes = [] for size, camera in zip(sizes, cameras): @@ -77,16 +120,27 @@ def warp_rois(self, sizes, cameras, aspect=1): return roi_corners, roi_sizes def warp_roi(self, size, camera, aspect=1): + """ + 扭曲单个感兴趣区域(ROI) + :param size: 图像尺寸 + :param camera: 相机参数 + :param aspect: 缩放比例 + :return: 扭曲后的感兴趣区域(ROI) + """ warper = cv.PyRotationWarper(self.warper_type, self.scale * aspect) K = Warper.get_K(camera, aspect) return warper.warpRoi(size, K, camera.R) @staticmethod def get_K(camera, aspect=1): + """ + 获取相机内参矩阵 + :param camera: 相机参数 + :param aspect: 缩放比例 + :return: 相机内参矩阵 + """ K = camera.K().astype(np.float32) - """ Modification of intrinsic parameters needed if cameras were - obtained on different scale than the scale of the Images which should - be warped """ + """ 修改内参矩阵,如果相机参数是在与图像不同的缩放比例下获得的 """ K[0, 0] *= aspect K[0, 2] *= aspect K[1, 1] *= aspect From e54e9b7d8607ab18d77e6c6daf72bd6efd16ea72 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Mar 2025 02:29:21 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- stitching/stitching_error.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stitching/stitching_error.py b/stitching/stitching_error.py index ae1a13e..de306f7 100644 --- a/stitching/stitching_error.py +++ b/stitching/stitching_error.py @@ -2,6 +2,7 @@ class StitchingError(Exception): """ 拼接错误异常类 """ + pass @@ -9,4 +10,5 @@ class StitchingWarning(UserWarning): """ 拼接警告类 """ + pass