-
Notifications
You must be signed in to change notification settings - Fork 1
/
image_stitching.py
176 lines (141 loc) · 6.2 KB
/
image_stitching.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import numpy as np
import cv2
class ImageStitching:
"""containts the utilities required to stitch images"""
def __init__(self, query_photo, train_photo):
super().__init__()
width_query_photo = query_photo.shape[1]
width_train_photo = train_photo.shape[1]
lowest_width = min(width_query_photo, width_train_photo)
smoothing_window_percent = 0.10 # consider increasing or decreasing[0.00, 1.00]
self.smoothing_window_size = max(100, min(smoothing_window_percent * lowest_width, 1000))
def give_gray(self, image):
"""receives an image array and returns grayscaled image
Args:
image (numpy array): array of images
Returns:
image (numpy array): same as image input
photo_gray (numpy array): grayscaled images
"""
photo_gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
return image, photo_gray
@staticmethod
def _sift_detector(image):
"""Applies SIFT algorithm to the given image
Args:
image (numpy array): input image
Returns:
keypoints, features
"""
descriptor = cv2.SIFT_create()
keypoints, features = descriptor.detectAndCompute(image, None)
return keypoints, features
def create_and_match_keypoints(self, features_train_image, features_query_image):
"""Creates and Matches keypoints from the SIFT features using Brute Force matching
by checking the L2 norm of the feature vector
Args:
features_train_image: SIFT features of train image
features_query_image: SIFT features of query image
Returns:
matches (List): matches in features of train and query image
"""
bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=True)
best_matches = bf.match(features_train_image, features_query_image)
raw_matches = sorted(best_matches, key=lambda x: x.distance)
return raw_matches
def compute_homography(
self, keypoints_train_image, keypoints_query_image, matches, reprojThresh
):
"""Computes the Homography to map images to a single plane,
uses RANSAC algorithm to find the best matches iteratively.
Args:
keypoints_train_image: keypoints found using SIFT in train image
keypoints_query_image: keypoints found using SIFT in query image
matches: matches found using Brute Force
reprojThresh: threshold for error
Returns:
M (Tuple): (matches, Homography matrix, status)
"""
keypoints_train_image = np.float32(
[keypoint.pt for keypoint in keypoints_train_image]
)
keypoints_query_image = np.float32(
[keypoint.pt for keypoint in keypoints_query_image]
)
if len(matches) >= 4:
points_train = np.float32(
[keypoints_train_image[m.queryIdx] for m in matches]
)
points_query = np.float32(
[keypoints_query_image[m.trainIdx] for m in matches]
)
H, status = cv2.findHomography(
points_train, points_query, cv2.RANSAC, reprojThresh
)
return (matches, H, status)
else:
print(f"Minimum match count not satisfied cannot get homopgrahy")
return None
def create_mask(self, query_image, train_image, version):
"""Creates the mask using query and train images for blending the images,
using a gaussian smoothing window/kernel
Args:
query_image (numpy array)
train_image (numpy array)
version (str) == 'left_image' or 'right_image'
Returns:
masks
"""
height_query_photo = query_image.shape[0]
width_query_photo = query_image.shape[1]
width_train_photo = train_image.shape[1]
height_panorama = height_query_photo
width_panorama = width_query_photo + width_train_photo
offset = int(self.smoothing_window_size / 2)
barrier = query_image.shape[1] - int(self.smoothing_window_size / 2)
mask = np.zeros((height_panorama, width_panorama))
if version == "left_image":
mask[:, barrier - offset : barrier + offset] = np.tile(
np.linspace(1, 0, 2 * offset).T, (height_panorama, 1)
)
mask[:, : barrier - offset] = 1
else:
mask[:, barrier - offset : barrier + offset] = np.tile(
np.linspace(0, 1, 2 * offset).T, (height_panorama, 1)
)
mask[:, barrier + offset :] = 1
return cv2.merge([mask, mask, mask])
def blending_smoothing(self, query_image, train_image, homography_matrix):
"""blends both query and train image via the homography matrix,
and ensures proper blending and smoothing using masks created in create_masks()
to give a seamless panorama.
Args:
query_image (numpy array)
train_image (numpy array)
homography_matrix (numpy array): Homography to map images to a single plane
Returns:
panoramic image (numpy array)
"""
height_img1 = query_image.shape[0]
width_img1 = query_image.shape[1]
width_img2 = train_image.shape[1]
height_panorama = height_img1
width_panorama = width_img1 + width_img2
panorama1 = np.zeros((height_panorama, width_panorama, 3))
mask1 = self.create_mask(query_image, train_image, version="left_image")
panorama1[0 : query_image.shape[0], 0 : query_image.shape[1], :] = query_image
panorama1 *= mask1
mask2 = self.create_mask(query_image, train_image, version="right_image")
panorama2 = (
cv2.warpPerspective(
train_image, homography_matrix, (width_panorama, height_panorama)
)
* mask2
)
result = panorama1 + panorama2
# remove extra blackspace
rows, cols = np.where(result[:, :, 0] != 0)
min_row, max_row = min(rows), max(rows) + 1
min_col, max_col = min(cols), max(cols) + 1
final_result = result[min_row:max_row, min_col:max_col, :]
return final_result