图像拼接——基于homography的特征匹配算法
2023-12-30 13:15:15
目录
1. 任务要求
- 输入:同一个场景的两张待拼接图像(有部分场景重合)。
- 任务:从输入的两张图像中提取特征点和描述子,可以使用现有的图像处理库来执行此任务。自己实现特征匹配算法,将来自两张图像的特征点进行匹配。最后根据匹配的特征点估计单应性变换,从而通过映射拼接成一张全景图。
- 输出:
- 显示两张图像上提取的特征点的位置;
- 显示特征点匹配对应的结果;
- 显示经过几何变换后的图像叠加的结果;
- 显示最终拼接的结果。
2. 数据集
- 其中两组图像“cat”和“bridge”拍摄于杭州。
- 其他图像分别来自测试数据链接和其他来源。
3. 基于homography的特征匹配算法
基于homography的特征匹配算法在图像拼接中起着关键作用,它能够定位和匹配两张待拼接图像中的特征点,从而实现图像的对齐和融合。该算法主要包括以下实现步骤:
- 特征点提取和描述:使用ORB和SIFT等特征检测器对待拼接图像进行特征点提取。这些特征点具有在不同尺度和旋转下的不变性。对每个特征点计算其对应的特征描述子,用于后续的特征匹配。
- 特征匹配:对两幅待拼接图像中的特征点进行匹配。我们使用基于最近邻的匹配,其中对于每个特征点,找到其在另一幅图像中的最佳匹配点。通过计算特征描述子之间的距离或相似度,确定最佳匹配点。
- 计算homography矩阵:使用筛选后的特征点匹配对应的坐标,计算homography矩阵。homography矩阵可以将一个图像上的点映射到另一个图像上,从而实现图像的对齐。
- 图像校准和拼接:使用计算得到的homography矩阵对多张图像进行透视变换,使其对齐。将校准后的图像进行融合,生成拼接结果图像。
import math
import cv2 as cv
import numpy as np
class FeatureMatcher:
def __init__(
self, matcher_type="homography", range_width=-1, **kwargs
):
if matcher_type == "homography":
if range_width == -1:
self.matcher = cv.detail_BestOf2NearestMatcher(**kwargs)
else:
self.matcher = cv.detail_BestOf2NearestRangeMatcher(range_width, **kwargs)
else:
raise ValueError("Unknown matcher type")
def match_features(self, features, *args, **kwargs):
pairwise_matches = self.matcher.apply2(features, *args, **kwargs)
self.matcher.collectGarbage()
return pairwise_matches
@staticmethod
def draw_matches_matrix(
imgs, features, matches, conf_thresh=1, inliers=False, **kwargs
):
matches_matrix = FeatureMatcher.get_matches_matrix(matches)
for idx1, idx2 in FeatureMatcher.get_all_img_combinations(len(imgs)):
match = matches_matrix[idx1, idx2]
if match.confidence < conf_thresh:
continue
if inliers:
kwargs["matchesMask"] = match.getInliers()
yield idx1, idx2, FeatureMatcher.draw_matches(
imgs[idx1], features[idx1], imgs[idx2], features[idx2], match, **kwargs
)
@staticmethod
def get_confidence_matrix(pairwise_matches):
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)
return match_conf_matrix
4. 拼接流程展示
4.1 图片实例
为了演示图像拼接的整体实现流程,这里我们选择一组我本人拍摄的玉泉校内的两只猫和周边环境图——“cat”,两幅有重叠画面的原图如下图所示。其中下面那只白猫几乎是静止不动的,上面的带橘色斑点的白猫在两幅图中的位置有相对移动。
from stitching.images import Images
# 1. load
images, low_imgs, medium_imgs, final_imgs = load_images(img_path)
images_to_match = medium_imgs
# 2. plot original images
plot_images(images_to_match, (20, 20), save=f'{save_path}/1-original.png')
# 3. print image size
print(f'Original image size: {images_to_match[0].shape}')
################ Load images ####################
def load_images(img_path):
images = Images.of(img_path)
medium_imgs = list(images.resize(Images.Resolution.MEDIUM))
low_imgs = list(images.resize(Images.Resolution.LOW))
final_imgs = list(images.resize(Images.Resolution.FINAL))
return images, low_imgs, medium_imgs, final_imgs
################ Plot function####################
def plot_image(img, figsize_in_inches=(10, 10), save=None):
"""N_image = 1"""
def plot_images(imgs, figsize_in_inches=(10, 10), save=None):
"""N_images > 1"""
4.2 特征点位图
根据特征检测器提取的特征点,生成特征点位置图。这里我们以ORB特征检测器为例,下图中的绿色小圈展示了待拼接图像中检测到的特征点的分布情况。
from stitching.feature_detector import FeatureDetector
# 4. Feature detection: ORB, SIFT
finder = FeatureDetector(detector=detector)
features = [finder.detect_features(img) for img in images_to_match]
key_points_img = []
for i in range(len(images_to_match)):
key_points_img.append(finder.draw_keypoints(images_to_match[i], features[i]))
plot_images(key_points_img, (20, 20), save=f'{save_path}/2-key_points.png')
4.3 特征点匹配结果
通过homography特征匹配算法(具体代码见第3节),将两张待拼接图像中匹配的特征点进行连接,生成特征点匹配结果图。下图中的绿色线段展示了特征点之间的对应关系。
from Feature_matcher import *
# 5. Feature matching: homography
matcher = FeatureMatcher()
matches = matcher.match_features(features)
print(matcher.get_confidence_matrix(matches))
# 6. plot matching
all_relevant_matches = matcher.draw_matches_matrix(images_to_match, features, matches, conf_thresh=1,
inliers=True, matchColor=(0, 255, 0))
for idx1, idx2, img in all_relevant_matches:
print(f"Matches Image {idx1 + 1} to Image {idx2 + 1}")
plot_image(img, (20, 10), save=f'{save_path}/3-matching.png')
4.4 相机校准结果
根据homography矩阵,对两张图像进行透视变换,使其对齐,生成校准结果图。下图的子图a为校准过程得到的mask图,子图b展示了经过校准后的待拼接图像,最终拼接图的大小与待拼接图像的大小一致。
from stitching.camera_estimator import CameraEstimator
from stitching.camera_adjuster import CameraAdjuster
from stitching.camera_wave_corrector import WaveCorrector
from stitching.warper import Warper
from stitching.timelapser import Timelapser
# 7. Camera Estimation, Adjustion and Correction
cameras = camera_correction(features, matches)
# 8. Warp images
(warped_low_imgs, warped_low_masks, low_corners, low_sizes,
warped_final_imgs, warped_final_masks, final_corners, final_sizes, frame) \
= warp_image(images, cameras, low_imgs, final_imgs)
plot_images(warped_low_imgs, (10, 10), save=f'{save_path}/4-warped_low_imgs.png')
plot_images(warped_low_masks, (10, 10), save=f'{save_path}/4-warped_low_masks.png')
plot_images(frame, (20, 10), save=f'{save_path}/4-warped_final_imgs.png')
################ Camera Estimation ##################
def camera_correction(features, matches):
camera_estimator = CameraEstimator()
camera_adjuster = CameraAdjuster()
wave_corrector = WaveCorrector()
cameras = camera_estimator.estimate(features, matches)
cameras = camera_adjuster.adjust(features, matches, cameras)
cameras = wave_corrector.correct(cameras)
return cameras
################ Warp images ####################
def warp_image(images, cameras, low_imgs, final_imgs):
warper = Warper()
warper.set_scale(cameras)
low_sizes = images.get_scaled_img_sizes(Images.Resolution.LOW)
camera_aspect = images.get_ratio(Images.Resolution.MEDIUM,
Images.Resolution.LOW) # since cameras were obtained on medium imgs
warped_low_imgs = list(warper.warp_images(low_imgs, cameras, camera_aspect))
warped_low_masks = list(warper.create_and_warp_masks(low_sizes, cameras, camera_aspect))
low_corners, low_sizes = warper.warp_rois(low_sizes, cameras, camera_aspect)
final_sizes = images.get_scaled_img_sizes(Images.Resolution.FINAL)
camera_aspect = images.get_ratio(Images.Resolution.MEDIUM, Images.Resolution.FINAL)
warped_final_imgs = list(warper.warp_images(final_imgs, cameras, camera_aspect))
warped_final_masks = list(warper.create_and_warp_masks(final_sizes, cameras, camera_aspect))
final_corners, final_sizes = warper.warp_rois(final_sizes, cameras, camera_aspect)
# Timelapser
timelapser = Timelapser('as_is')
timelapser.initialize(final_corners, final_sizes)
frame = []
for img, corner in zip(warped_final_imgs, final_corners):
timelapser.process_frame(img, corner)
frame.append(timelapser.get_frame())
return (warped_low_imgs, warped_low_masks, low_corners, low_sizes,
warped_final_imgs, warped_final_masks, final_corners, final_sizes, frame)
4.5 拼接结果
将经过校准的两张图像进行融合,生成拼接结果图。根据用户的选择,可以提供剪裁相机校准结果的选项(stitching(crop = True),默认为False)。图1分别展示了未剪裁和剪裁后的校准图(5a&c)和拼接图时的接缝(5b&d)。最后拼接图结果见图2,上面三幅图不包括剪裁步骤,下面三幅存在剪裁步骤。可以看到,在拼接之前剪裁至规则的四边形对拼接时的seam line的选取有较大的影响,有一定概率导致最终的拼接图像不符合预期。
from stitching.cropper import Cropper
from stitching.seam_finder import SeamFinder
# 9. Crop images
if crop:
(cropped_low_imgs, cropped_low_masks, cropped_final_imgs,
cropped_final_masks, final_corners, final_sizes, frame) = (
crop_image(images, warped_low_imgs, warped_low_masks, low_corners, low_sizes,
warped_final_imgs, warped_final_masks, final_corners, final_sizes))
plot_images(frame, (20, 10), save=f'{save_path}/5-cropped_final_imgs.png')
else:
cropped_low_imgs = warped_low_imgs
cropped_low_masks = warped_low_masks
cropped_final_imgs = warped_final_imgs
cropped_final_masks = warped_final_masks
# 10. Seam Masks
seam_finder, seam_masks_plots, compensated_imgs, seam_masks = (
seam(cropped_low_imgs, low_corners, cropped_low_masks,
cropped_final_masks, cropped_final_imgs, final_corners))
plot_images(seam_masks_plots, (15, 10), save=f'{save_path}/6-seam_masks.png')
# 11. Matching result
blender = Blender()
blender.prepare(final_corners, final_sizes)
for img, mask, corner in zip(compensated_imgs, seam_masks, final_corners):
blender.feed(img, mask, corner)
panorama, _ = blender.blend()
blended_seam_masks = seam_finder.blend_seam_masks(seam_masks, final_corners, final_sizes)
plot_image(panorama, (20, 20), save=f'{save_path}/7-matched_result.png')
plot_image(seam_finder.draw_seam_lines(panorama, blended_seam_masks, linesize=3), (15, 10),
save=f'{save_path}/8-seam_lines.png')
plot_image(seam_finder.draw_seam_polygons(panorama, blended_seam_masks), (15, 10),
save=f'{save_path}/9-seam_polygons.png')
# 12. Done
print('Done!')
################ Crop images ####################
def crop_image(images, warped_low_imgs, warped_low_masks, low_corners, low_sizes,
warped_final_imgs, warped_final_masks, final_corners, final_sizes):
cropper = Cropper()
mask = cropper.estimate_panorama_mask(warped_low_imgs, warped_low_masks, low_corners, low_sizes)
lir = cropper.estimate_largest_interior_rectangle(mask)
low_corners = cropper.get_zero_center_corners(low_corners)
rectangles = cropper.get_rectangles(low_corners, low_sizes)
overlap = cropper.get_overlap(rectangles[1], lir)
intersection = cropper.get_intersection(rectangles[1], overlap)
cropper.prepare(warped_low_imgs, warped_low_masks, low_corners, low_sizes)
cropped_low_masks = list(cropper.crop_images(warped_low_masks))
cropped_low_imgs = list(cropper.crop_images(warped_low_imgs))
low_corners, low_sizes = cropper.crop_rois(low_corners, low_sizes)
lir_aspect = images.get_ratio(Images.Resolution.LOW, Images.Resolution.FINAL) # since lir was obtained on low imgs
cropped_final_masks = list(cropper.crop_images(warped_final_masks, lir_aspect))
cropped_final_imgs = list(cropper.crop_images(warped_final_imgs, lir_aspect))
final_corners, final_sizes = cropper.crop_rois(final_corners, final_sizes, lir_aspect)
# Redo the timelapse with cropped Images:
timelapser = Timelapser('as_is')
timelapser.initialize(final_corners, final_sizes)
frame = []
for img, corner in zip(cropped_final_imgs, final_corners):
timelapser.process_frame(img, corner)
frame.append(timelapser.get_frame())
return (cropped_low_imgs, cropped_low_masks, cropped_final_imgs,
cropped_final_masks, final_corners, final_sizes, frame)
5. 部分图像拼接结果展示
文章来源:https://blog.csdn.net/qq_43811536/article/details/135302979
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!